|
| 1 | +import type { Nuxt, NuxtConfig } from '@nuxt/schema' |
| 2 | +import type { IncomingMessage, ServerResponse } from 'node:http' |
| 3 | +import { mkdtemp, rm } from 'node:fs/promises' |
| 4 | +import { createServer } from 'node:http' |
| 5 | +import { tmpdir } from 'node:os' |
| 6 | +import { join } from 'node:path' |
| 7 | +import { fileURLToPath } from 'node:url' |
| 8 | +import { loadNuxt } from 'nuxt' |
| 9 | +import { afterEach, describe, expect, it } from 'vitest' |
| 10 | + |
| 11 | +const onboardModule = fileURLToPath(new URL('../src/module.ts', import.meta.url)) |
| 12 | + |
| 13 | +const cleanups: (() => Promise<void>)[] = [] |
| 14 | +afterEach(async () => { |
| 15 | + await Promise.all(cleanups.splice(0).map(fn => fn())) |
| 16 | +}) |
| 17 | + |
| 18 | +/** |
| 19 | + * Nuxt core pushes the bare `@nuxt/devtools` name into `_modules`; aliasing |
| 20 | + * it to this module reproduces what Nuxt 5 does with `@nuxt/devtools-onboard`. |
| 21 | + * `withoutDevtools` drops every `node_modules` Nuxt can resolve from, which is |
| 22 | + * the situation of a Nuxt that no longer depends on `@nuxt/devtools` itself. |
| 23 | + */ |
| 24 | +async function bootNuxt(overrides: NuxtConfig, { withoutDevtools = false } = {}): Promise<Nuxt> { |
| 25 | + const rootDir = await mkdtemp(join(tmpdir(), 'devtools-onboard-')) |
| 26 | + const nuxt = await loadNuxt({ |
| 27 | + cwd: rootDir, |
| 28 | + ready: false, |
| 29 | + overrides: { |
| 30 | + alias: { '@nuxt/devtools': onboardModule }, |
| 31 | + telemetry: false, |
| 32 | + ...overrides, |
| 33 | + }, |
| 34 | + }) |
| 35 | + cleanups.push(async () => { |
| 36 | + await nuxt.close() |
| 37 | + await rm(rootDir, { recursive: true, force: true }) |
| 38 | + }) |
| 39 | + if (withoutDevtools) |
| 40 | + nuxt.options.modulesDir = [join(rootDir, 'node_modules')] |
| 41 | + await nuxt.ready() |
| 42 | + return nuxt |
| 43 | +} |
| 44 | + |
| 45 | +function onboardScripts(nuxt: Nuxt) { |
| 46 | + return (nuxt.options.app.head.script ?? []).filter(script => typeof script === 'object' && script.src === '/__devtools/embedded.js') |
| 47 | +} |
| 48 | + |
| 49 | +type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void |
| 50 | + |
| 51 | +async function viteMiddlewareFetch(nuxt: Nuxt, path: string): Promise<Response> { |
| 52 | + const mounted: Middleware[] = [] |
| 53 | + // Only `middlewares.use` is read; the rest of a real ViteDevServer is irrelevant here. |
| 54 | + const viteServer = { middlewares: { use: (fn: Middleware) => mounted.push(fn) } } as never |
| 55 | + await nuxt.callHook('vite:serverCreated', viteServer, { isClient: true, isServer: false }) |
| 56 | + const [middleware] = mounted |
| 57 | + if (!middleware) |
| 58 | + throw new Error('onboarding middleware was not mounted') |
| 59 | + |
| 60 | + const server = createServer((req, res) => middleware(req, res, () => { |
| 61 | + res.statusCode = 404 |
| 62 | + res.end() |
| 63 | + })) |
| 64 | + await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve)) |
| 65 | + cleanups.push(() => new Promise(resolve => server.close(() => resolve()))) |
| 66 | + const { port } = server.address() as { port: number } // listening on a TCP port, never a pipe |
| 67 | + return fetch(`http://127.0.0.1:${port}${path}`) |
| 68 | +} |
| 69 | + |
| 70 | +describe('@nuxt/devtools-onboard', () => { |
| 71 | + it('does nothing when devtools are disabled', async () => { |
| 72 | + const nuxt = await bootNuxt({ devtools: false, modules: [onboardModule] }) |
| 73 | + expect(nuxt.options._requiredModules['@nuxt/devtools']).toBeUndefined() |
| 74 | + expect(onboardScripts(nuxt)).toHaveLength(0) |
| 75 | + }, 60_000) |
| 76 | + |
| 77 | + it('loads @nuxt/devtools when it is installed', async () => { |
| 78 | + const nuxt = await bootNuxt({}) |
| 79 | + expect(nuxt.options._requiredModules['@nuxt/devtools']).toBe(true) |
| 80 | + expect(onboardScripts(nuxt)).toHaveLength(0) |
| 81 | + }, 60_000) |
| 82 | + |
| 83 | + it('offers to install @nuxt/devtools when it is missing', async () => { |
| 84 | + const nuxt = await bootNuxt({ dev: true }, { withoutDevtools: true }) |
| 85 | + expect(nuxt.options._requiredModules['@nuxt/devtools']).toBeUndefined() |
| 86 | + expect(onboardScripts(nuxt)).toHaveLength(1) |
| 87 | + |
| 88 | + const status = await viteMiddlewareFetch(nuxt, '/__devtools/__onboard/status').then(r => r.json()) |
| 89 | + expect(status.state).toBe('idle') |
| 90 | + // No lockfile in the fixture, so the package manager falls back to npm. |
| 91 | + expect(status.command).toBe('npm i -D @nuxt/devtools@^4.0.0-beta.3') |
| 92 | + expect(status.branding.productName).toBe('Nuxt DevTools') |
| 93 | + }, 60_000) |
| 94 | + |
| 95 | + it('stays out of the way outside nuxt dev when @nuxt/devtools is missing', async () => { |
| 96 | + const nuxt = await bootNuxt({ dev: false }, { withoutDevtools: true }) |
| 97 | + expect(onboardScripts(nuxt)).toHaveLength(0) |
| 98 | + }, 60_000) |
| 99 | +}) |
0 commit comments