Skip to content
Merged
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
86 changes: 85 additions & 1 deletion client/app/app.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,91 @@
<script setup lang="ts">
import { useDevtoolsClient } from '@nuxt/devtools-kit/iframe-client'
import { useDevtoolsClient, onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
import type { HintsClientFunctions } from '../../src/runtime/core/rpc-types'
import { RPC_NAMESPACE } from '../../src/runtime/core/rpc-types'
import type { HydrationMismatchPayload, HydrationMismatchResponse, 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'
import { HYDRATION_ROUTE, LAZY_LOAD_ROUTE, HTML_VALIDATE_ROUTE } from './utils/routes'

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) => {
// Hydration: seed from host payload and fetch from server
if (useHintsFeature('hydration')) {
hydrationMismatches.value = [...client.host.nuxt.payload.__hints.hydration]
$fetch<HydrationMismatchResponse>(new URL(HYDRATION_ROUTE, window.location.origin).href).then((data) => {
hydrationMismatches.value = [
...hydrationMismatches.value,
...data.mismatches.filter(m => !hydrationMismatches.value.some(existing => existing.id === m.id)),
]
})
}
Comment on lines +25 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add defensive check for payload hydration data.

Line 23 spreads client.host.nuxt.payload.__hints.hydration without verifying the property exists. If __hints or hydration is undefined, this will throw. The useHintsFeature('hydration') check doesn't guarantee the payload data was populated.

🛡️ Proposed defensive access
   if (useHintsFeature('hydration')) {
-    hydrationMismatches.value = [...client.host.nuxt.payload.__hints.hydration]
+    hydrationMismatches.value = [...(client.host.nuxt.payload.__hints?.hydration ?? [])]
     $fetch<HydrationMismatchResponse>(new URL(HYDRATION_ROUTE, window.location.origin).href).then((data) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/app/app.vue` around lines 22 - 30, The code spreads
client.host.nuxt.payload.__hints.hydration without guarding for missing
properties; update the block that runs when useHintsFeature('hydration') is true
to defensively read payload hints (e.g., const existing =
client.host?.nuxt?.payload?.__hints?.hydration ?? []) and initialize
hydrationMismatches.value from that empty-safe array, then in the $fetch.then
merge new data into hydrationMismatches.value using the same safe reference (and
fall back to [] when data.mismatches is missing) so no undefined property access
occurs; look for useHintsFeature('hydration'), hydrationMismatches,
client.host.nuxt.payload.__hints.hydration, and HYDRATION_ROUTE to locate the
code to change.


// Lazy load: fetch from server
if (useHintsFeature('lazyLoad')) {
$fetch<ComponentLazyLoadData[]>(new URL(LAZY_LOAD_ROUTE, window.location.origin).href).then((data) => {
lazyLoadHints.value = [
...lazyLoadHints.value,
...(data ?? []).filter(d => !lazyLoadHints.value.some(existing => existing.id === d.id)),
]
})
}

// HTML validate: fetch from server
if (useHintsFeature('htmlValidate')) {
$fetch<HtmlValidateReport[]>(new URL(HTML_VALIDATE_ROUTE, window.location.origin).href).then((data) => {
htmlValidateReports.value = [
...htmlValidateReports.value,
...(data ?? []).filter(d => !htmlValidateReports.value.some(existing => existing.id === d.id)),
]
})
}

// Register client RPC functions for real-time push notifications
client.devtools.extendClientRpc<Record<string, unknown>, HintsClientFunctions>(RPC_NAMESPACE, {
onHydrationMismatch(mismatch: HydrationMismatchPayload) {
if (!hydrationMismatches.value.some(existing => existing.id === mismatch.id)) {
hydrationMismatches.value.push(mismatch)
}
},
onHydrationCleared(ids: string[]) {
hydrationMismatches.value = hydrationMismatches.value.filter(m => !ids.includes(m.id))
},
onLazyLoadReport(data: ComponentLazyLoadData) {
try {
const validated = parse(ComponentLazyLoadDataSchema, data)
if (!lazyLoadHints.value.some(existing => existing.id === validated.id)) {
lazyLoadHints.value.push(validated)
}
}
catch {
console.warn('[hints] Ignoring malformed lazy-load report', data)
}
},
onLazyLoadCleared(id: string) {
lazyLoadHints.value = lazyLoadHints.value.filter(entry => entry.id !== id)
},
onHtmlValidateReport(report: HtmlValidateReport) {
if (!htmlValidateReports.value.some(existing => existing.id === report.id)) {
htmlValidateReports.value = [...htmlValidateReports.value, report]
}
},
onHtmlValidateDeleted(id: string) {
htmlValidateReports.value = htmlValidateReports.value.filter(report => report.id !== id)
},
})
})
</script>

<template>
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`
9 changes: 8 additions & 1 deletion src/devtools.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { addCustomTab } from '@nuxt/devtools-kit'
import { addCustomTab, extendServerRpc, onDevToolsInitialized } from '@nuxt/devtools-kit'
import { existsSync } from 'node:fs'
import type { Nuxt } from '@nuxt/schema'
import { addDevServerHandler, type Resolver } from '@nuxt/kit'
import { proxyRequest, eventHandler } from 'h3'
import type { HintsClientFunctions } from './runtime/core/rpc-types'
import { RPC_NAMESPACE } from './runtime/core/rpc-types'

const DEVTOOLS_UI_ROUTE = '/__nuxt-hints'
const DEVTOOLS_UI_LOCAL_PORT = 3300
Expand Down Expand Up @@ -41,4 +43,9 @@ export function setupDevToolsUI(nuxt: Nuxt, resolver: Resolver) {
src: DEVTOOLS_UI_ROUTE,
},
}, nuxt)

onDevToolsInitialized(() => {
const rpc = extendServerRpc<HintsClientFunctions>(RPC_NAMESPACE, {})
globalThis.__nuxtHintsRpcBroadcast = rpc.broadcast
}, nuxt)
}
10 changes: 2 additions & 8 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { defineNuxtModule, addPlugin, createResolver, addBuildPlugin, addComponent, addServerPlugin, addServerHandler, addTemplate } from '@nuxt/kit'
import { HINTS_SSE_ROUTE } from './runtime/core/server/types'
import { defineNuxtModule, addPlugin, createResolver, addBuildPlugin, addComponent, addServerPlugin, addTemplate } from '@nuxt/kit'
import { setupDevToolsUI } from './devtools'
import { InjectHydrationPlugin } from './plugins/hydration'
import { LazyLoadHintPlugin } from './plugins/lazy-load'
Expand Down Expand Up @@ -55,12 +54,6 @@ export default defineNuxtModule<ModuleOptions>({
priority: 1000,
})

// core handlers
addServerHandler({
route: HINTS_SSE_ROUTE,
handler: resolver.resolve('./runtime/core/server/sse'),
})

// performances
if (isFeatureEnabled(options, 'webVitals')) {
addPlugin(resolver.resolve('./runtime/web-vitals/plugin.client'))
Expand Down Expand Up @@ -112,6 +105,7 @@ export default defineNuxtModule<ModuleOptions>({
if (options.devtools) {
setupDevToolsUI(nuxt, resolver)
addPlugin(resolver.resolve('./runtime/core/plugins/vue-tracer-state.client'))
addServerPlugin(resolver.resolve('./runtime/core/server/rpc-bridge'))
}

nuxt.options.build.transpile.push(moduleName)
Expand Down
18 changes: 18 additions & 0 deletions src/runtime/core/rpc-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { HydrationMismatchPayload } from '../hydration/types'
import type { ComponentLazyLoadData } from '../lazy-load/schema'
import type { HtmlValidateReport } from '../html-validate/types'

export interface HintsClientFunctions {
onHydrationMismatch: (mismatch: HydrationMismatchPayload) => void
onHydrationCleared: (ids: string[]) => void
onLazyLoadReport: (data: ComponentLazyLoadData) => void
onLazyLoadCleared: (id: string) => void
onHtmlValidateReport: (report: HtmlValidateReport) => void
onHtmlValidateDeleted: (id: string) => void
}

export const RPC_NAMESPACE = 'nuxt-hints'

declare global {
var __nuxtHintsRpcBroadcast: HintsClientFunctions | undefined
}
32 changes: 32 additions & 0 deletions src/runtime/core/server/rpc-bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { NitroApp } from 'nitropack/types'
import type { HintsClientFunctions } from '../rpc-types'

function getRpcBroadcast(): HintsClientFunctions | undefined {
return globalThis.__nuxtHintsRpcBroadcast
}

export default function (nitroApp: NitroApp) {
nitroApp.hooks.hook('hints:hydration:mismatch', (mismatch) => {
getRpcBroadcast()?.onHydrationMismatch(mismatch)
})

nitroApp.hooks.hook('hints:hydration:cleared', (payload) => {
getRpcBroadcast()?.onHydrationCleared(payload.id)
})

nitroApp.hooks.hook('hints:lazy-load:report', (data) => {
getRpcBroadcast()?.onLazyLoadReport(data)
})

nitroApp.hooks.hook('hints:lazy-load:cleared', (payload) => {
getRpcBroadcast()?.onLazyLoadCleared(payload.id)
})

nitroApp.hooks.hook('hints:html-validate:report', (report) => {
getRpcBroadcast()?.onHtmlValidateReport(report)
})

nitroApp.hooks.hook('hints:html-validate:deleted', (id) => {
getRpcBroadcast()?.onHtmlValidateDeleted(id)
})
}
23 changes: 0 additions & 23 deletions src/runtime/core/server/sse.ts

This file was deleted.

Loading
Loading