diff --git a/docs/2.guide/3.going-further/4.kit.md b/docs/2.guide/3.going-further/4.kit.md index 0610737513..226762b899 100644 --- a/docs/2.guide/3.going-further/4.kit.md +++ b/docs/2.guide/3.going-further/4.kit.md @@ -15,6 +15,10 @@ Discover all Nuxt Kit utilities. You can install the latest Nuxt Kit by adding it to the `dependencies` section of your `package.json`. However, please consider always explicitly installing the `@nuxt/kit` package even if it is already installed by Nuxt. +::note +`@nuxt/kit` and `@nuxt/schema` are key dependencies for Nuxt. If you are installing it separately, make sure that the versions of `@nuxt/kit` and `@nuxt/schema` are equal to or greater than your `nuxt` version to avoid any unexpected behavior. +:: + ```json [package.json] { "dependencies": { diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 6fffff3ad5..ab4a078a57 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -99,6 +99,7 @@ "pkg-types": "^1.1.1", "radix3": "^1.1.2", "scule": "^1.3.0", + "semver": "^7.6.2", "std-env": "^3.7.0", "strip-literal": "^2.1.0", "ufo": "^1.5.3", diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 390683579d..e607ce4433 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -13,6 +13,7 @@ import fse from 'fs-extra' import { withTrailingSlash, withoutLeadingSlash } from 'ufo' import defu from 'defu' +import { gte } from 'semver' import pagesModule from '../pages/module' import metaModule from '../head/module' import componentsModule from '../components/module' @@ -63,6 +64,11 @@ const nightlies = { '@nuxt/kit': '@nuxt/kit-nightly', } +const keyDependencies = [ + '@nuxt/kit', + '@nuxt/schema', +] + async function initNuxt (nuxt: Nuxt) { // Register user hooks for (const config of nuxt.options._layers.map(layer => layer.config).reverse()) { @@ -616,6 +622,8 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise { const nuxt = createNuxt(options) + await Promise.all(keyDependencies.map(dependency => checkDependencyVersion(dependency, nuxt._version))) + // We register hooks layer-by-layer so any overrides need to be registered separately if (opts.overrides?.hooks) { nuxt.hooks.addHooks(opts.overrides.hooks) @@ -632,4 +640,15 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise { return nuxt } +async function checkDependencyVersion (name: string, nuxtVersion: string): Promise { + const path = await resolvePath(name).catch(() => null) + + if (!path) { return } + const { version } = await readPackageJSON(path) + + if (version && gte(nuxtVersion, version)) { + console.warn(`[nuxt] Expected \`${name}\` to be at least \`${nuxtVersion}\` but got \`${version}\`. This might lead to unexpected behavior. Check your package.json or refresh your lockfile.`) + } +} + const RESTART_RE = /^(?:app|error|app\.config)\.(?:js|ts|mjs|jsx|tsx|vue)$/i diff --git a/packages/nuxt/test/load-nuxt.test.ts b/packages/nuxt/test/load-nuxt.test.ts index d6207329c3..7efadc7e48 100644 --- a/packages/nuxt/test/load-nuxt.test.ts +++ b/packages/nuxt/test/load-nuxt.test.ts @@ -1,11 +1,32 @@ import { fileURLToPath } from 'node:url' -import { describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { normalize } from 'pathe' import { withoutTrailingSlash } from 'ufo' +import { readPackageJSON } from 'pkg-types' +import { inc } from 'semver' import { loadNuxt } from '../src' +import { version } from '../package.json' const repoRoot = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../', import.meta.url)))) +vi.stubGlobal('console', { + ...console, + error: vi.fn(console.error), + warn: vi.fn(console.warn), +}) + +vi.mock('pkg-types', async (og) => { + const originalPkgTypes = (await og()) + return { + ...originalPkgTypes, + readPackageJSON: vi.fn(originalPkgTypes.readPackageJSON), + } +}) + +afterEach(() => { + vi.clearAllMocks() +}) + describe('loadNuxt', () => { it('respects hook overrides', async () => { let hookRan = false @@ -24,3 +45,44 @@ describe('loadNuxt', () => { expect(hookRan).toBe(true) }) }) + +describe('dependency mismatch', () => { + it('expect mismatched dependency to log a warning', async () => { + vi.mocked(readPackageJSON).mockReturnValue(Promise.resolve({ + version: '3.0.0', + })) + + const nuxt = await loadNuxt({ + cwd: repoRoot, + }) + + expect(console.warn).toHaveBeenCalledWith(`[nuxt] Expected \`@nuxt/kit\` to be at least \`${version}\` but got \`3.0.0\`. This might lead to unexpected behavior. Check your package.json or refresh your lockfile.`) + expect(console.warn).toHaveBeenCalledWith(`[nuxt] Expected \`@nuxt/schema\` to be at least \`${version}\` but got \`3.0.0\`. This might lead to unexpected behavior. Check your package.json or refresh your lockfile.`) + + vi.mocked(readPackageJSON).mockRestore() + await nuxt.close() + }) + it.each([ + { + name: 'nuxt version is lower', + depVersion: inc(version, 'minor'), + }, + { + name: 'version matches', + depVersion: version, + }, + ])('expect no warning when $name.', async ({ depVersion }) => { + vi.mocked(readPackageJSON).mockReturnValue(Promise.resolve({ + depVersion, + })) + + const nuxt = await loadNuxt({ + cwd: repoRoot, + }) + + expect(console.warn).not.toHaveBeenCalled() + + await nuxt.close() + vi.mocked(readPackageJSON).mockRestore() + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fa988d46e..2388ae56ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -369,6 +369,9 @@ importers: scule: specifier: ^1.3.0 version: 1.3.0 + semver: + specifier: ^7.6.2 + version: 7.6.2 std-env: specifier: ^3.7.0 version: 3.7.0