Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 149 additions & 1 deletion client/app/app.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,155 @@
<script setup lang="ts">
import { useDevtoolsClient } from '@nuxt/devtools-kit/iframe-client'
import { useDevtoolsClient, onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
import type { HintsClientFunctions, HintsServerFunctions } from '../../src/runtime/core/rpc-types'
import { RPC_NAMESPACE } from '../../src/runtime/core/rpc-types'
import type { HydrationMismatchPayload, LocalHydrationMismatch } from '../../src/runtime/hydration/types'
import type { ComponentLazyLoadData } from '../../src/runtime/lazy-load/schema'
import { ComponentLazyLoadDataSchema } from '../../src/runtime/lazy-load/schema'
import type { HtmlValidateReport } from '../../src/runtime/html-validate/types'
import { parse } from 'valibot'

const client = useDevtoolsClient()

const hydrationMismatches = ref<(HydrationMismatchPayload | LocalHydrationMismatch)[]>([])
const lazyLoadHints = ref<ComponentLazyLoadData[]>([])
const htmlValidateReports = ref<HtmlValidateReport[]>([])

const nuxtApp = useNuxtApp()
nuxtApp.provide('hydrationMismatches', hydrationMismatches)
nuxtApp.provide('lazyLoadHints', lazyLoadHints)
nuxtApp.provide('htmlValidateReports', htmlValidateReports)

onDevtoolsClientConnected((client) => {
const hydrationTombstones = new Set<string>()
const lazyLoadTombstones = new Set<string>()
const htmlValidateTombstones = new Set<string>()

let hydrationInitialSyncDone = !useHintsFeature('hydration')
let lazyLoadInitialSyncDone = !useHintsFeature('lazyLoad')
let htmlValidateInitialSyncDone = !useHintsFeature('htmlValidate')

const bufferedHydrationMismatches: HydrationMismatchPayload[] = []
const bufferedLazyLoadHints: ComponentLazyLoadData[] = []
const bufferedHtmlValidateReports: HtmlValidateReport[] = []

const applyHydrationMismatch = (mismatch: HydrationMismatchPayload | LocalHydrationMismatch) => {
if (hydrationTombstones.has(mismatch.id)) {
return
}
if (!hydrationMismatches.value.some(existing => existing.id === mismatch.id)) {
hydrationMismatches.value.push(mismatch)
}
}

const applyLazyLoadHint = (entry: ComponentLazyLoadData) => {
if (lazyLoadTombstones.has(entry.id)) {
return
}
if (!lazyLoadHints.value.some(existing => existing.id === entry.id)) {
lazyLoadHints.value.push(entry)
}
}

const applyHtmlValidateReport = (report: HtmlValidateReport) => {
if (htmlValidateTombstones.has(report.id)) {
return
}
if (!htmlValidateReports.value.some(existing => existing.id === report.id)) {
htmlValidateReports.value = [...htmlValidateReports.value, report]
}
}

const rpc = client.devtools.extendClientRpc<HintsServerFunctions, HintsClientFunctions>(RPC_NAMESPACE, {
onHydrationMismatch(mismatch: HydrationMismatchPayload) {
if (!hydrationInitialSyncDone) {
bufferedHydrationMismatches.push(mismatch)
return
}
applyHydrationMismatch(mismatch)
},
onHydrationCleared(ids: string[]) {
ids.forEach(id => hydrationTombstones.add(id))
hydrationMismatches.value = hydrationMismatches.value.filter(m => !ids.includes(m.id))
},
onLazyLoadReport(data: ComponentLazyLoadData) {
try {
const validated = parse(ComponentLazyLoadDataSchema, data)
if (!lazyLoadInitialSyncDone) {
bufferedLazyLoadHints.push(validated)
return
}
applyLazyLoadHint(validated)
}
catch {
console.warn('[hints] Ignoring malformed lazy-load report', data)
}
},
onLazyLoadCleared(id: string) {
lazyLoadTombstones.add(id)
lazyLoadHints.value = lazyLoadHints.value.filter(entry => entry.id !== id)
},
onHtmlValidateReport(report: HtmlValidateReport) {
if (!htmlValidateInitialSyncDone) {
bufferedHtmlValidateReports.push(report)
return
}
applyHtmlValidateReport(report)
},
onHtmlValidateDeleted(id: string) {
htmlValidateTombstones.add(id)
htmlValidateReports.value = htmlValidateReports.value.filter(report => report.id !== id)
},
})

// Hydration: seed from host payload and fetch from server via RPC
if (useHintsFeature('hydration')) {
if (client.host?.nuxt?.payload?.__hints?.hydration) {
hydrationMismatches.value = client.host.nuxt.payload.__hints.hydration
.filter(mismatch => !hydrationTombstones.has(mismatch.id))
}
rpc.getHydrationMismatches().then((data) => {
data.mismatches
.filter(mismatch => !hydrationTombstones.has(mismatch.id))
.forEach(mismatch => applyHydrationMismatch(mismatch))
}).catch((error) => {
console.warn('[hints] Failed to fetch hydration mismatches', error)
}).finally(() => {
hydrationInitialSyncDone = true
bufferedHydrationMismatches.forEach(mismatch => applyHydrationMismatch(mismatch))
bufferedHydrationMismatches.length = 0
})
}

// Lazy load: fetch from server via RPC
if (useHintsFeature('lazyLoad')) {
rpc.getLazyLoadHints().then((data) => {
;(data ?? [])
.filter(entry => !lazyLoadTombstones.has(entry.id))
.forEach(entry => applyLazyLoadHint(entry))
}).catch((error) => {
console.warn('[hints] Failed to fetch lazy-load hints', error)
}).finally(() => {
lazyLoadInitialSyncDone = true
bufferedLazyLoadHints.forEach(entry => applyLazyLoadHint(entry))
bufferedLazyLoadHints.length = 0
})
}

// HTML validate: fetch from server via RPC
if (useHintsFeature('htmlValidate')) {
rpc.getHtmlValidateReports().then((data) => {
;(data ?? [])
.filter(report => !htmlValidateTombstones.has(report.id))
.forEach(report => applyHtmlValidateReport(report))
}).catch((error) => {
console.warn('[hints] Failed to fetch html-validate reports', error)
}).finally(() => {
htmlValidateInitialSyncDone = true
bufferedHtmlValidateReports.forEach(report => applyHtmlValidateReport(report))
bufferedHtmlValidateReports.length = 0
})
}
})
</script>

<template>
Expand Down
7 changes: 7 additions & 0 deletions client/app/composables/features.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FeaturesName } from '../../../src/runtime/core/types'
import { useDevtoolsClient } from '@nuxt/devtools-kit/iframe-client'

export function useHintsConfig() {
const hostNuxt = useHostNuxt()
Expand All @@ -7,6 +8,10 @@ export function useHintsConfig() {
}

export function useEnabledHintsFeatures(): Record<FeaturesName, boolean> {
const client = useDevtoolsClient().value
if (!client?.host?.nuxt) {
return { hydration: true, lazyLoad: true, webVitals: false, thirdPartyScripts: false, htmlValidate: true }
}
const config = useHintsConfig()
return Object.fromEntries<boolean>(
Object.entries(config.features).map(([feature, flags]) => [
Expand All @@ -17,6 +22,8 @@ export function useEnabledHintsFeatures(): Record<FeaturesName, boolean> {
}

export function useHintsFeature(feature: FeaturesName): boolean {
const client = useDevtoolsClient().value
if (!client?.host?.nuxt) return true
const config = useHintsConfig()
return typeof config.features[feature] === 'object' ? config.features[feature].devtools !== false : Boolean(config.features[feature])
}
4 changes: 2 additions & 2 deletions client/app/composables/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ export function useHostHydration() {
export function useHostNuxt() {
const client = useDevtoolsClient().value

if (!client) {
throw new Error('`useHostNuxt` must be used when the devtools client is connected')
if (!client?.host?.nuxt) {
throw new Error('`useHostNuxt` must be used when the devtools client is connected to a host app')
}

return client.host.nuxt
Expand Down
19 changes: 0 additions & 19 deletions client/app/plugins/0.sse.ts

This file was deleted.

44 changes: 0 additions & 44 deletions client/app/plugins/html-validate.ts

This file was deleted.

47 changes: 0 additions & 47 deletions client/app/plugins/hydration.ts

This file was deleted.

57 changes: 0 additions & 57 deletions client/app/plugins/lazy-load.ts

This file was deleted.

1 change: 0 additions & 1 deletion client/app/utils/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/

export const HINTS_ROUTE = '/__nuxt_hints'
export const HINTS_SSE_ROUTE = '/__nuxt_hints/sse'
export const HYDRATION_ROUTE = `${HINTS_ROUTE}/hydration`
export const LAZY_LOAD_ROUTE = `${HINTS_ROUTE}/lazy-load`
export const HTML_VALIDATE_ROUTE = `${HINTS_ROUTE}/html-validate`
Loading
Loading