Skip to content

Commit 86b47d2

Browse files
authored
revert: move SSE to devtools RPC (#317)
1 parent 225d2aa commit 86b47d2

File tree

20 files changed

+344
-216
lines changed

20 files changed

+344
-216
lines changed

client/app/app.vue

Lines changed: 1 addition & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,7 @@
11
<script setup lang="ts">
2-
import { useDevtoolsClient, onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
3-
import type { HintsClientFunctions } from '../../src/runtime/core/rpc-types'
4-
import { RPC_NAMESPACE } from '../../src/runtime/core/rpc-types'
5-
import type { HydrationMismatchPayload, HydrationMismatchResponse, LocalHydrationMismatch } from '../../src/runtime/hydration/types'
6-
import type { ComponentLazyLoadData } from '../../src/runtime/lazy-load/schema'
7-
import { ComponentLazyLoadDataSchema } from '../../src/runtime/lazy-load/schema'
8-
import type { HtmlValidateReport } from '../../src/runtime/html-validate/types'
9-
import { parse } from 'valibot'
10-
import { HYDRATION_ROUTE, LAZY_LOAD_ROUTE, HTML_VALIDATE_ROUTE } from './utils/routes'
2+
import { useDevtoolsClient } from '@nuxt/devtools-kit/iframe-client'
113
124
const client = useDevtoolsClient()
13-
14-
const hydrationMismatches = ref<(HydrationMismatchPayload | LocalHydrationMismatch)[]>([])
15-
const lazyLoadHints = ref<ComponentLazyLoadData[]>([])
16-
const htmlValidateReports = ref<HtmlValidateReport[]>([])
17-
18-
const nuxtApp = useNuxtApp()
19-
nuxtApp.provide('hydrationMismatches', hydrationMismatches)
20-
nuxtApp.provide('lazyLoadHints', lazyLoadHints)
21-
nuxtApp.provide('htmlValidateReports', htmlValidateReports)
22-
23-
onDevtoolsClientConnected((client) => {
24-
// Hydration: seed from host payload and fetch from server
25-
if (useHintsFeature('hydration')) {
26-
hydrationMismatches.value = [...client.host.nuxt.payload.__hints.hydration]
27-
$fetch<HydrationMismatchResponse>(new URL(HYDRATION_ROUTE, window.location.origin).href).then((data) => {
28-
hydrationMismatches.value = [
29-
...hydrationMismatches.value,
30-
...data.mismatches.filter(m => !hydrationMismatches.value.some(existing => existing.id === m.id)),
31-
]
32-
})
33-
}
34-
35-
// Lazy load: fetch from server
36-
if (useHintsFeature('lazyLoad')) {
37-
$fetch<ComponentLazyLoadData[]>(new URL(LAZY_LOAD_ROUTE, window.location.origin).href).then((data) => {
38-
lazyLoadHints.value = [
39-
...lazyLoadHints.value,
40-
...(data ?? []).filter(d => !lazyLoadHints.value.some(existing => existing.id === d.id)),
41-
]
42-
})
43-
}
44-
45-
// HTML validate: fetch from server
46-
if (useHintsFeature('htmlValidate')) {
47-
$fetch<HtmlValidateReport[]>(new URL(HTML_VALIDATE_ROUTE, window.location.origin).href).then((data) => {
48-
htmlValidateReports.value = [
49-
...htmlValidateReports.value,
50-
...(data ?? []).filter(d => !htmlValidateReports.value.some(existing => existing.id === d.id)),
51-
]
52-
})
53-
}
54-
55-
// Register client RPC functions for real-time push notifications
56-
client.devtools.extendClientRpc<Record<string, unknown>, HintsClientFunctions>(RPC_NAMESPACE, {
57-
onHydrationMismatch(mismatch: HydrationMismatchPayload) {
58-
if (!hydrationMismatches.value.some(existing => existing.id === mismatch.id)) {
59-
hydrationMismatches.value.push(mismatch)
60-
}
61-
},
62-
onHydrationCleared(ids: string[]) {
63-
hydrationMismatches.value = hydrationMismatches.value.filter(m => !ids.includes(m.id))
64-
},
65-
onLazyLoadReport(data: ComponentLazyLoadData) {
66-
try {
67-
const validated = parse(ComponentLazyLoadDataSchema, data)
68-
if (!lazyLoadHints.value.some(existing => existing.id === validated.id)) {
69-
lazyLoadHints.value.push(validated)
70-
}
71-
}
72-
catch {
73-
console.warn('[hints] Ignoring malformed lazy-load report', data)
74-
}
75-
},
76-
onLazyLoadCleared(id: string) {
77-
lazyLoadHints.value = lazyLoadHints.value.filter(entry => entry.id !== id)
78-
},
79-
onHtmlValidateReport(report: HtmlValidateReport) {
80-
if (!htmlValidateReports.value.some(existing => existing.id === report.id)) {
81-
htmlValidateReports.value = [...htmlValidateReports.value, report]
82-
}
83-
},
84-
onHtmlValidateDeleted(id: string) {
85-
htmlValidateReports.value = htmlValidateReports.value.filter(report => report.id !== id)
86-
},
87-
})
88-
})
895
</script>
906

917
<template>

client/app/plugins/0.sse.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { HINTS_SSE_ROUTE } from '../utils/routes'
2+
3+
export default defineNuxtPlugin(() => {
4+
if (import.meta.test) return
5+
const eventSource = useEventSource(HINTS_SSE_ROUTE, undefined, {
6+
autoReconnect: {
7+
retries: 5,
8+
onFailed() {
9+
console.error(new Error('[@nuxt/hints] Failed to connect to hints SSE after 5 attempts.'))
10+
},
11+
},
12+
})
13+
14+
return {
15+
provide: {
16+
sse: eventSource,
17+
},
18+
}
19+
})
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { HtmlValidateReport } from '../../../src/runtime/html-validate/types'
2+
import { defineNuxtPlugin } from '#imports'
3+
import { HTML_VALIDATE_ROUTE } from '../utils/routes'
4+
5+
export default defineNuxtPlugin(() => {
6+
if (import.meta.test || !useHintsFeature('htmlValidate')) return
7+
const nuxtApp = useNuxtApp()
8+
9+
const { data: htmlValidateReports } = useLazyFetch<HtmlValidateReport[]>(new URL(HTML_VALIDATE_ROUTE, window.location.origin).href, {
10+
default: () => [],
11+
deep: true,
12+
})
13+
14+
function htmlValidateReportHandler(event: MessageEvent) {
15+
try {
16+
const payload: HtmlValidateReport = JSON.parse(event.data)
17+
if (!htmlValidateReports.value.some(existing => existing.id === payload.id)) {
18+
htmlValidateReports.value = [...htmlValidateReports.value, payload]
19+
}
20+
}
21+
catch {
22+
console.warn('[hints] Ignoring malformed hints:html-validate:report event', event.data)
23+
}
24+
}
25+
26+
function htmlValidateDeletedHandler(event: MessageEvent) {
27+
try {
28+
const deletedId = JSON.parse(event.data)
29+
htmlValidateReports.value = htmlValidateReports.value.filter(report => report.id !== deletedId)
30+
}
31+
catch {
32+
console.warn('[hints] Ignoring malformed hints:html-validate:deleted event', event.data)
33+
}
34+
}
35+
36+
useEventListener(nuxtApp.$sse.eventSource, 'hints:html-validate:report', htmlValidateReportHandler)
37+
useEventListener(nuxtApp.$sse.eventSource, 'hints:html-validate:deleted', htmlValidateDeletedHandler)
38+
39+
return {
40+
provide: {
41+
htmlValidateReports,
42+
},
43+
}
44+
})

client/app/plugins/hydration.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { HydrationMismatchPayload, HydrationMismatchResponse, LocalHydrationMismatch } from '../../../src/runtime/hydration/types'
2+
import { defineNuxtPlugin, useHostNuxt, ref } from '#imports'
3+
import { HYDRATION_ROUTE } from '../utils/routes'
4+
5+
export default defineNuxtPlugin(() => {
6+
if (import.meta.test || !useHintsFeature('hydration')) return
7+
const host = useHostNuxt()
8+
const nuxtApp = useNuxtApp()
9+
const hydrationMismatches = ref<(HydrationMismatchPayload | LocalHydrationMismatch)[]>([])
10+
11+
hydrationMismatches.value = [...host.payload.__hints.hydration]
12+
13+
$fetch<HydrationMismatchResponse>(new URL(HYDRATION_ROUTE, window.location.origin).href).then((data: { mismatches: HydrationMismatchPayload[] }) => {
14+
hydrationMismatches.value = [...hydrationMismatches.value, ...data.mismatches.filter(m => !hydrationMismatches.value.some(existing => existing.id === m.id))]
15+
})
16+
17+
const hydrationMismatchHandler = (event: MessageEvent) => {
18+
const mismatch: HydrationMismatchPayload = JSON.parse(event.data)
19+
if (!hydrationMismatches.value.some(existing => existing.id === mismatch.id)) {
20+
hydrationMismatches.value.push(mismatch)
21+
}
22+
}
23+
24+
const hydrationClearedHandler = (event: MessageEvent) => {
25+
const clearedIds: string[] = JSON.parse(event.data)
26+
hydrationMismatches.value = hydrationMismatches.value.filter(m => !clearedIds.includes(m.id))
27+
}
28+
29+
watch(nuxtApp.$sse.eventSource, (newEventSource, oldEventSource) => {
30+
if (newEventSource) {
31+
newEventSource.addEventListener('hints:hydration:mismatch', hydrationMismatchHandler)
32+
newEventSource.addEventListener('hints:hydration:cleared', hydrationClearedHandler)
33+
}
34+
if (oldEventSource) {
35+
oldEventSource.removeEventListener('hints:hydration:mismatch', hydrationMismatchHandler)
36+
oldEventSource.removeEventListener('hints:hydration:cleared', hydrationClearedHandler)
37+
}
38+
}, {
39+
immediate: true,
40+
})
41+
42+
return {
43+
provide: {
44+
hydrationMismatches,
45+
},
46+
}
47+
})

client/app/plugins/lazy-load.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { ComponentLazyLoadData } from '../../../src/runtime/lazy-load/schema'
2+
import { parse } from 'valibot'
3+
import { defineNuxtPlugin } from '#imports'
4+
import { ComponentLazyLoadDataSchema } from '../../../src/runtime/lazy-load/schema'
5+
import { LAZY_LOAD_ROUTE } from '../utils/routes'
6+
7+
export default defineNuxtPlugin(() => {
8+
if (import.meta.test || !useHintsFeature('lazyLoad')) return
9+
const nuxtApp = useNuxtApp()
10+
11+
const { data: lazyLoadHints } = useLazyFetch<ComponentLazyLoadData[]>(new URL(LAZY_LOAD_ROUTE, window.location.origin).href, {
12+
default: () => [],
13+
deep: true,
14+
})
15+
16+
const lazyLoadReportHandler = (event: MessageEvent) => {
17+
try {
18+
const payload = parse(ComponentLazyLoadDataSchema, JSON.parse(event.data))
19+
if (!lazyLoadHints.value.some(existing => existing.id === payload.id)) {
20+
lazyLoadHints.value.push(payload)
21+
}
22+
}
23+
catch {
24+
console.warn('[hints] Ignoring malformed hints:lazy-load:report event', event.data)
25+
return
26+
}
27+
}
28+
29+
const lazyLoadClearedHandler = (event: MessageEvent) => {
30+
try {
31+
const clearedId = JSON.parse(event.data)
32+
lazyLoadHints.value = lazyLoadHints.value.filter(entry => entry.id !== clearedId)
33+
}
34+
catch {
35+
return
36+
}
37+
}
38+
39+
watch(nuxtApp.$sse.eventSource, (newEventSource, oldEventSource) => {
40+
if (newEventSource) {
41+
newEventSource.addEventListener('hints:lazy-load:report', lazyLoadReportHandler)
42+
newEventSource.addEventListener('hints:lazy-load:cleared', lazyLoadClearedHandler)
43+
}
44+
if (oldEventSource) {
45+
oldEventSource.removeEventListener('hints:lazy-load:report', lazyLoadReportHandler)
46+
oldEventSource.removeEventListener('hints:lazy-load:cleared', lazyLoadClearedHandler)
47+
}
48+
}, {
49+
immediate: true,
50+
})
51+
52+
return {
53+
provide: {
54+
lazyLoadHints,
55+
},
56+
}
57+
})

client/app/utils/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
export const HINTS_ROUTE = '/__nuxt_hints'
8+
export const HINTS_SSE_ROUTE = '/__nuxt_hints/sse'
89
export const HYDRATION_ROUTE = `${HINTS_ROUTE}/hydration`
910
export const LAZY_LOAD_ROUTE = `${HINTS_ROUTE}/lazy-load`
1011
export const HTML_VALIDATE_ROUTE = `${HINTS_ROUTE}/html-validate`

src/devtools.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { addCustomTab, extendServerRpc, onDevToolsInitialized } from '@nuxt/devtools-kit'
1+
import { addCustomTab } from '@nuxt/devtools-kit'
22
import { existsSync } from 'node:fs'
33
import type { Nuxt } from '@nuxt/schema'
44
import { addDevServerHandler, type Resolver } from '@nuxt/kit'
55
import { proxyRequest, eventHandler } from 'h3'
6-
import type { HintsClientFunctions } from './runtime/core/rpc-types'
7-
import { RPC_NAMESPACE } from './runtime/core/rpc-types'
86

97
const DEVTOOLS_UI_ROUTE = '/__nuxt-hints'
108
const DEVTOOLS_UI_LOCAL_PORT = 3300
@@ -43,9 +41,4 @@ export function setupDevToolsUI(nuxt: Nuxt, resolver: Resolver) {
4341
src: DEVTOOLS_UI_ROUTE,
4442
},
4543
}, nuxt)
46-
47-
onDevToolsInitialized(() => {
48-
const rpc = extendServerRpc<HintsClientFunctions>(RPC_NAMESPACE, {})
49-
globalThis.__nuxtHintsRpcBroadcast = rpc.broadcast
50-
}, nuxt)
5144
}

src/module.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { defineNuxtModule, addPlugin, createResolver, addBuildPlugin, addComponent, addServerPlugin, addTemplate } from '@nuxt/kit'
1+
import { defineNuxtModule, addPlugin, createResolver, addBuildPlugin, addComponent, addServerPlugin, addServerHandler, addTemplate } from '@nuxt/kit'
2+
import { HINTS_SSE_ROUTE } from './runtime/core/server/types'
23
import { setupDevToolsUI } from './devtools'
34
import { InjectHydrationPlugin } from './plugins/hydration'
45
import { LazyLoadHintPlugin } from './plugins/lazy-load'
@@ -54,6 +55,12 @@ export default defineNuxtModule<ModuleOptions>({
5455
priority: 1000,
5556
})
5657

58+
// core handlers
59+
addServerHandler({
60+
route: HINTS_SSE_ROUTE,
61+
handler: resolver.resolve('./runtime/core/server/sse'),
62+
})
63+
5764
// performances
5865
if (isFeatureEnabled(options, 'webVitals')) {
5966
addPlugin(resolver.resolve('./runtime/web-vitals/plugin.client'))
@@ -105,7 +112,6 @@ export default defineNuxtModule<ModuleOptions>({
105112
if (options.devtools) {
106113
setupDevToolsUI(nuxt, resolver)
107114
addPlugin(resolver.resolve('./runtime/core/plugins/vue-tracer-state.client'))
108-
addServerPlugin(resolver.resolve('./runtime/core/server/rpc-bridge'))
109115
}
110116

111117
nuxt.options.build.transpile.push(moduleName)

src/runtime/core/rpc-types.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/runtime/core/server/rpc-bridge.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)