diff --git a/packages/devtools-onboard/README.md b/packages/devtools-onboard/README.md index a3c0eec878..3b105ecb08 100644 --- a/packages/devtools-onboard/README.md +++ b/packages/devtools-onboard/README.md @@ -2,7 +2,7 @@ The entry point of [Nuxt DevTools](https://devtools.nuxt.com). It is a tiny Nuxt module that Nuxt 5 ships instead of depending on `@nuxt/devtools` directly: -- when `@nuxt/devtools` is installed in your project, it loads it; +- when Nuxt DevTools is installed in your project, it loads it: `@nuxt/devtools` v3 or v4, or a [nightly release](https://devtools.nuxt.com/guide/getting-started#nightly-release-channel) aliased as `@nuxt/devtools` or installed as `@nuxt/devtools-nightly` (which wins when both are present); - when it is not, `nuxt dev` shows a floating button (built on [`@devframes/hub-ui-onboard`](https://devfra.me/guide/hub-ui-onboard)) that installs it with your package manager and restarts the dev server, so the real DevTools dock takes its place. Nothing is injected outside `nuxt dev`. diff --git a/packages/devtools-onboard/package.json b/packages/devtools-onboard/package.json index 720403492d..6adf5b9bf8 100644 --- a/packages/devtools-onboard/package.json +++ b/packages/devtools-onboard/package.json @@ -24,7 +24,7 @@ "prepack": "turbo run build --filter=." }, "peerDependencies": { - "@nuxt/devtools": "workspace:^", + "@nuxt/devtools": "*", "@nuxt/kit": "^4.0.0-0 || ^5.0.0-0" }, "peerDependenciesMeta": { diff --git a/packages/devtools-onboard/src/module.ts b/packages/devtools-onboard/src/module.ts index 2a44284b45..9dbea61cec 100644 --- a/packages/devtools-onboard/src/module.ts +++ b/packages/devtools-onboard/src/module.ts @@ -5,9 +5,17 @@ import { defineNuxtModule, installModule, tryResolveModule } from '@nuxt/kit' import { readPackageJSON } from 'pkg-types' const DEVTOOLS = '@nuxt/devtools' +const DEVTOOLS_NIGHTLY = `${DEVTOOLS}-nightly` /** The names Nuxt itself recognizes as DevTools in `modules` (see `loadNuxt` in nuxt core). */ -const DEVTOOLS_MODULES = new Set([DEVTOOLS, `${DEVTOOLS}-nightly`, `${DEVTOOLS}-edge`]) +const DEVTOOLS_MODULES = new Set([DEVTOOLS, DEVTOOLS_NIGHTLY, `${DEVTOOLS}-edge`]) + +/** + * The packages loaded when installed, any major. A nightly under its own name + * is always a deliberate opt-in, while `@nuxt/devtools` may come along with + * another dependency, so the nightly wins. + */ +const DEVTOOLS_PACKAGES = [DEVTOOLS_NIGHTLY, DEVTOOLS] function listsDevtools(modules: NuxtOptions['modules']): boolean { return modules.some((entry) => { @@ -19,7 +27,9 @@ function listsDevtools(modules: NuxtOptions['modules']): boolean { /** * The spec the button installs. Versions are released in lockstep, so a caret * on our own version pins the matching DevTools; a nightly build carries an - * `npm:@nuxt/devtools-nightly@…` alias in its peer range instead. + * `npm:@nuxt/devtools-nightly@…` alias in its peer range instead. Otherwise + * the peer range is `*`: it must not conflict with an installed v3 or nightly, + * and npm only matches prereleases against `*`. */ async function devtoolsSpec(): Promise { const { version, peerDependencies } = await readPackageJSON(fileURLToPath(import.meta.url)) @@ -38,9 +48,11 @@ export default defineNuxtModule({ if (listsDevtools(nuxt.options.modules)) return - const installed = await tryResolveModule(DEVTOOLS, nuxt.options.modulesDir) - if (installed) - return installModule(installed) + for (const name of DEVTOOLS_PACKAGES) { + const installed = await tryResolveModule(name, nuxt.options.modulesDir) + if (installed) + return installModule(installed) + } if (!nuxt.options.dev) return diff --git a/packages/devtools-onboard/test/module.test.ts b/packages/devtools-onboard/test/module.test.ts index 2e55cbb9fe..6af9920549 100644 --- a/packages/devtools-onboard/test/module.test.ts +++ b/packages/devtools-onboard/test/module.test.ts @@ -1,6 +1,6 @@ import type { Nuxt, NuxtConfig } from '@nuxt/schema' import type { IncomingMessage, ServerResponse } from 'node:http' -import { mkdtemp, rm } from 'node:fs/promises' +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { createServer } from 'node:http' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -15,14 +15,24 @@ afterEach(async () => { await Promise.all(cleanups.splice(0).map(fn => fn())) }) +/** A stand-in DevTools package whose module records its name in `appConfig`. */ +async function installStandIn(rootDir: string, name: string) { + const dir = join(rootDir, 'node_modules', name) + await mkdir(dir, { recursive: true }) + await writeFile(join(dir, 'package.json'), JSON.stringify({ name, type: 'module', exports: './module.mjs' })) + await writeFile(join(dir, 'module.mjs'), `export default (_, nuxt) => { nuxt.options.appConfig.loadedDevtools = ${JSON.stringify(name)} }`) +} + /** * Nuxt core pushes the bare `@nuxt/devtools` name into `_modules`; aliasing * it to this module reproduces what Nuxt 5 does with `@nuxt/devtools-onboard`. * `withoutDevtools` drops every `node_modules` Nuxt can resolve from, which is - * the situation of a Nuxt that no longer depends on `@nuxt/devtools` itself. + * the situation of a Nuxt that no longer depends on `@nuxt/devtools` itself; + * `installed` then puts stand-in packages back into the project. */ -async function bootNuxt(overrides: NuxtConfig, { withoutDevtools = false } = {}): Promise { +async function bootNuxt(overrides: NuxtConfig, { withoutDevtools = false, installed = [] }: { withoutDevtools?: boolean, installed?: string[] } = {}): Promise { const rootDir = await mkdtemp(join(tmpdir(), 'devtools-onboard-')) + await Promise.all(installed.map(name => installStandIn(rootDir, name))) const nuxt = await loadNuxt({ cwd: rootDir, ready: false, @@ -80,6 +90,12 @@ describe('@nuxt/devtools-onboard', () => { expect(onboardScripts(nuxt)).toHaveLength(0) }, 60_000) + it('loads @nuxt/devtools-nightly installed under its own name, over @nuxt/devtools', async () => { + const nuxt = await bootNuxt({ dev: true }, { withoutDevtools: true, installed: ['@nuxt/devtools', '@nuxt/devtools-nightly'] }) + expect(nuxt.options.appConfig.loadedDevtools).toBe('@nuxt/devtools-nightly') + expect(onboardScripts(nuxt)).toHaveLength(0) + }, 60_000) + it('offers to install @nuxt/devtools when it is missing', async () => { const nuxt = await bootNuxt({ dev: true }, { withoutDevtools: true }) expect(nuxt.options._requiredModules['@nuxt/devtools']).toBeUndefined()