Skip to content

Commit 745fa41

Browse files
authored
fix(onboard): serve embedded.js through Nuxt's Vite dev handler (#1130)
1 parent a8cb5dc commit 745fa41

5 files changed

Lines changed: 46 additions & 25 deletions

File tree

‎packages/devtools-onboard/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"devDependencies": {
4040
"@nuxt/kit": "catalog:prod",
4141
"@nuxt/schema": "catalog:types",
42-
"nuxt": "catalog:buildtools"
42+
"nuxt": "catalog:buildtools",
43+
"vite": "catalog:buildtools"
4344
}
4445
}

‎packages/devtools-onboard/src/module.ts‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ const DEVTOOLS_MODULES = new Set([DEVTOOLS, DEVTOOLS_NIGHTLY, `${DEVTOOLS}-edge`
1818
*/
1919
const DEVTOOLS_PACKAGES = [DEVTOOLS_NIGHTLY, DEVTOOLS]
2020

21+
/**
22+
* Where Vite DevTools mounts the hub, so after the restart the real
23+
* `embedded.js` answers at the URL the button was loaded from.
24+
*/
25+
const BASE = '/__devtools/'
26+
2127
function listsDevtools(modules: NuxtOptions['modules']): boolean {
2228
return modules.some((entry) => {
2329
const id = Array.isArray(entry) ? entry[0] : entry
@@ -59,9 +65,7 @@ export default defineNuxtModule({
5965

6066
const onboarding = createOnboarding({
6167
cwd: nuxt.options.rootDir,
62-
// Where Vite DevTools mounts the hub, so after the restart the real
63-
// `embedded.js` answers at the URL the button was loaded from.
64-
base: '/__devtools/',
68+
base: BASE,
6569
packages: [await devtoolsSpec()],
6670
branding: {
6771
productName: 'Nuxt DevTools',
@@ -85,8 +89,16 @@ export default defineNuxtModule({
8589
})
8690

8791
nuxt.hook('vite:serverCreated', (server, { isClient }) => {
88-
if (isClient)
89-
server.middlewares.use(onboarding.nodeMiddleware)
92+
if (!isClient)
93+
return
94+
// Nuxt only hands a request to Vite when a middleware is mounted on a
95+
// path other than `/`; otherwise it rewrites the URL to `/__skip_vite/…`.
96+
// Connect strips the mount path, so restore the full URL the
97+
// middleware matches against `base`.
98+
server.middlewares.use(BASE.slice(0, -1), (req, res, next) => {
99+
req.url = req.originalUrl || req.url
100+
onboarding.nodeMiddleware(req, res, next)
101+
})
90102
})
91103
if (!onboarding.disabled)
92104
(nuxt.options.app.head.script ??= []).push({ type: 'module', src: onboarding.scriptSrc, tagPosition: 'bodyClose' })

‎packages/devtools-onboard/test/module.test.ts‎

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { Nuxt, NuxtConfig } from '@nuxt/schema'
2-
import type { IncomingMessage, ServerResponse } from 'node:http'
2+
import type { ViteDevServer } from 'vite'
33
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
44
import { createServer } from 'node:http'
55
import { tmpdir } from 'node:os'
66
import { join } from 'node:path'
77
import { fileURLToPath } from 'node:url'
88
import { loadNuxt } from 'nuxt'
9+
import { createServer as createViteServer } from 'vite'
910
import { afterEach, describe, expect, it } from 'vitest'
1011

1112
const onboardModule = fileURLToPath(new URL('../src/module.ts', import.meta.url))
@@ -56,26 +57,24 @@ function onboardScripts(nuxt: Nuxt) {
5657
return (nuxt.options.app.head.script ?? []).filter(script => typeof script === 'object' && script.src === '/__devtools/embedded.js')
5758
}
5859

59-
type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void
60-
61-
/** Serves what the module mounts on the client Vite server; resolves to its origin. */
62-
async function serveViteMiddleware(nuxt: Nuxt): Promise<string> {
63-
const mounted: Middleware[] = []
64-
// Only `middlewares.use` is read; the rest of a real ViteDevServer is irrelevant here.
65-
const viteServer = { middlewares: { use: (fn: Middleware) => mounted.push(fn) } } as never
60+
/** Serves what the module mounts on a real client Vite server; resolves to its origin. */
61+
async function serveViteMiddleware(nuxt: Nuxt): Promise<{ origin: string, viteServer: ViteDevServer }> {
62+
const viteServer = await createViteServer({ configFile: false, logLevel: 'silent', server: { middlewareMode: true, ws: false } })
63+
cleanups.push(() => viteServer.close())
64+
// Mount only what the module adds, not Vite's own transform middlewares.
65+
viteServer.middlewares.stack.length = 0
6666
await nuxt.callHook('vite:serverCreated', viteServer, { isClient: true, isServer: false })
67-
const [middleware] = mounted
68-
if (!middleware)
69-
throw new Error('onboarding middleware was not mounted')
70-
71-
const server = createServer((req, res) => middleware(req, res, () => {
72-
res.statusCode = 404
73-
res.end()
74-
}))
67+
68+
const server = createServer(viteServer.middlewares)
7569
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
7670
cleanups.push(() => new Promise(resolve => server.close(() => resolve())))
7771
const { port } = server.address() as { port: number } // listening on a TCP port, never a pipe
78-
return `http://127.0.0.1:${port}`
72+
return { origin: `http://127.0.0.1:${port}`, viteServer }
73+
}
74+
75+
/** Nuxt's dev handler only passes a URL to Vite when a middleware is mounted on a path other than `/`. */
76+
function nuxtRoutesToVite(viteServer: ViteDevServer, url: string) {
77+
return viteServer.middlewares.stack.some(({ route }) => route.length > 1 && url.startsWith(route))
7978
}
8079

8180
const onboardStatus = (origin: string) => fetch(`${origin}/__devtools/__onboard/status`).then(r => r.json())
@@ -104,7 +103,12 @@ describe('@nuxt/devtools-onboard', () => {
104103
expect(nuxt.options._requiredModules['@nuxt/devtools']).toBeUndefined()
105104
expect(onboardScripts(nuxt)).toHaveLength(1)
106105

107-
const status = await onboardStatus(await serveViteMiddleware(nuxt))
106+
const { origin, viteServer } = await serveViteMiddleware(nuxt)
107+
expect(nuxtRoutesToVite(viteServer, '/__devtools/embedded.js')).toBe(true)
108+
const script = await fetch(`${origin}/__devtools/embedded.js`)
109+
expect(script.headers.get('content-type')).toMatch(/javascript/)
110+
111+
const status = await onboardStatus(origin)
108112
expect(status.state).toBe('idle')
109113
// No lockfile in the fixture, so the package manager falls back to npm.
110114
expect(status.command.slice(0, 3)).toEqual(['npm', 'i', '-D'])
@@ -114,7 +118,7 @@ describe('@nuxt/devtools-onboard', () => {
114118

115119
it('refuses to install when nobody can confirm it in the terminal', async () => {
116120
const nuxt = await bootNuxt({ dev: true }, { withoutDevtools: true })
117-
const origin = await serveViteMiddleware(nuxt)
121+
const { origin } = await serveViteMiddleware(nuxt)
118122

119123
// Vitest workers get a piped stdin, so no terminal can answer the question.
120124
const install = await fetch(`${origin}/__devtools/__onboard/install`, { method: 'POST' })

‎pnpm-lock.yaml‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pnpm-workspace.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ catalogs:
174174
unocss: *unocss
175175
unplugin-vue: ^8.0.0
176176
unplugin-vue-markdown: ^32.1.1
177+
vite: *vite
177178
cli:
178179
'@antfu/eslint-config': ^9.5.1
179180
'@antfu/ni': ^30.6.0

0 commit comments

Comments
 (0)