diff --git a/package.json b/package.json index 3307cbb10..0c974d8a3 100644 --- a/package.json +++ b/package.json @@ -67,12 +67,13 @@ "dev:prepare": "nuxt prepare && unbuild --stub && pnpm -r dev:prepare" }, "dependencies": { - "@nuxt/kit": "^4.2.1", + "@nuxt/kit": "^3.20.1", "c12": "^3.3.2", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "estree-walker": "^3.0.3", + "exsolve": "^1.0.8", "fake-indexeddb": "^6.2.5", "get-port-please": "^3.2.0", "h3": "^1.15.4", @@ -128,6 +129,7 @@ "peerDependencies": { "@cucumber/cucumber": "^10.3.1 || >=11.0.0", "@jest/globals": "^29.5.0 || >=30.0.0", + "@nuxt/kit": ">=3.20.1", "@playwright/test": "^1.43.1", "@testing-library/vue": "^7.0.0 || ^8.0.1", "@vue/test-utils": "^2.4.2", @@ -143,6 +145,9 @@ "@jest/globals": { "optional": true }, + "@nuxt/kit": { + "optional": true + }, "@playwright/test": { "optional": true }, @@ -170,7 +175,6 @@ }, "resolutions": { "@cucumber/cucumber": "12.3.0", - "@nuxt/kit": "^4.2.1", "@nuxt/schema": "4.2.1", "@nuxt/test-utils": "workspace:*", "@types/node": "24.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3743f7b9..0bf278d4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,6 @@ settings: overrides: '@cucumber/cucumber': 12.3.0 - '@nuxt/kit': ^4.2.1 '@nuxt/schema': 4.2.1 '@nuxt/test-utils': workspace:* '@types/node': 24.10.1 @@ -26,8 +25,8 @@ importers: .: dependencies: '@nuxt/kit': - specifier: ^4.2.1 - version: 4.2.1(magicast@0.5.1) + specifier: ^3.20.1 + version: 3.20.1(magicast@0.5.1) c12: specifier: ^3.3.2 version: 3.3.2(magicast@0.5.1) @@ -43,6 +42,9 @@ importers: estree-walker: specifier: ^3.0.3 version: 3.0.3 + exsolve: + specifier: ^1.0.8 + version: 1.0.8 fake-indexeddb: specifier: ^6.2.5 version: 6.2.5 @@ -1470,6 +1472,10 @@ packages: '@nuxt/icon@2.1.0': resolution: {integrity: sha512-m+XQrgzeK5gQ1HkB7G7u1os6egoD07fiHKijG7NPxqT5yZUGOjKJ7X/Le10l3QWRKyCB+IiU0t+eUqSvh+SULg==} + '@nuxt/kit@3.20.1': + resolution: {integrity: sha512-TIslaylfI5kd3AxX5qts0qyrIQ9Uq3HAA1bgIIJ+c+zpDfK338YS+YrCWxBBzDMECRCbAS58mqAd2MtJfG1ENA==} + engines: {node: '>=18.12.0'} + '@nuxt/kit@4.2.1': resolution: {integrity: sha512-lLt8KLHyl7IClc3RqRpRikz15eCfTRlAWL9leVzPyg5N87FfKE/7EWgWvpiL/z4Tf3dQCIqQb88TmHE0JTIDvA==} engines: {node: '>=18.12.0'} @@ -7527,7 +7533,7 @@ packages: resolution: {integrity: sha512-vfBI/SvD9hJqYNinipVOAj5n8dS8DJXFlCKFR5iLDp2SaQwsfdnfLXgZ+34Kd3YY3YEY9omk8XQg0bwos3Q8ug==} engines: {node: '>=14'} peerDependencies: - '@nuxt/kit': ^4.2.1 + '@nuxt/kit': ^4.0.0 '@vueuse/core': '*' peerDependenciesMeta: '@nuxt/kit': @@ -7548,7 +7554,7 @@ packages: engines: {node: '>=14'} peerDependencies: '@babel/parser': ^7.15.8 - '@nuxt/kit': ^4.2.1 + '@nuxt/kit': ^3.2.2 || ^4.0.0 vue: ^3.5.25 peerDependenciesMeta: '@babel/parser': @@ -9367,7 +9373,7 @@ snapshots: '@nuxt/devtools-kit@2.7.0(magicast@0.5.1)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.40.0)(yaml@2.8.1))': dependencies: - '@nuxt/kit': 4.2.1(magicast@0.5.1) + '@nuxt/kit': 3.20.1(magicast@0.5.1) execa: 8.0.1 vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.40.0)(yaml@2.8.1) transitivePeerDependencies: @@ -9541,6 +9547,32 @@ snapshots: - vite - vue + '@nuxt/kit@3.20.1(magicast@0.5.1)': + dependencies: + c12: 3.3.2(magicast@0.5.1) + consola: 3.4.2 + defu: 6.1.4 + destr: 2.0.5 + errx: 0.1.0 + exsolve: 1.0.8 + ignore: 7.0.5 + jiti: 2.6.1 + klona: 2.0.6 + knitwork: 1.3.0 + mlly: 1.8.0 + ohash: 2.0.11 + pathe: 2.0.3 + pkg-types: 2.3.0 + rc9: 2.1.2 + scule: 1.3.0 + semver: 7.7.3 + tinyglobby: 0.2.15 + ufo: 1.6.1 + unctx: 2.4.1 + untyped: 2.0.0 + transitivePeerDependencies: + - magicast + '@nuxt/kit@4.2.1(magicast@0.5.1)': dependencies: c12: 3.3.2(magicast@0.5.1) @@ -9723,7 +9755,7 @@ snapshots: '@nuxt/telemetry@2.6.6(magicast@0.5.1)': dependencies: - '@nuxt/kit': 4.2.1(magicast@0.5.1) + '@nuxt/kit': 3.20.1(magicast@0.5.1) citty: 0.1.6 consola: 3.4.2 destr: 2.0.5 @@ -9952,7 +9984,7 @@ snapshots: '@nuxtjs/color-mode@3.5.2(magicast@0.5.1)': dependencies: - '@nuxt/kit': 4.2.1(magicast@0.5.1) + '@nuxt/kit': 3.20.1(magicast@0.5.1) pathe: 1.1.2 pkg-types: 1.3.1 semver: 7.7.3 diff --git a/src/config.ts b/src/config.ts index c7458a888..7a410f574 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,9 +9,9 @@ import type { DotenvOptions } from 'c12' import type { UserConfig as ViteUserConfig } from 'vite' import type { DateString } from 'compatx' import { defu } from 'defu' -import { loadNuxt, buildNuxt, createResolver, findPath } from '@nuxt/kit' +import { createResolver, findPath } from '@nuxt/kit' -import { applyEnv } from './utils' +import { applyEnv, loadKit } from './utils' interface GetVitestConfigOptions { nuxt: Nuxt @@ -25,6 +25,7 @@ interface LoadNuxtOptions { // https://github.com/nuxt/framework/issues/6496 async function startNuxtAndGetViteConfig(rootDir = process.cwd(), options: LoadNuxtOptions = {}) { + const { buildNuxt, loadNuxt } = await loadKit(rootDir) const nuxt = await loadNuxt({ cwd: rootDir, dev: false, diff --git a/src/e2e/nuxt.ts b/src/e2e/nuxt.ts index b6fa72d07..c04557874 100644 --- a/src/e2e/nuxt.ts +++ b/src/e2e/nuxt.ts @@ -1,11 +1,8 @@ import { existsSync, promises as fsp } from 'node:fs' import { resolve } from 'node:path' import { defu } from 'defu' -import * as _kit from '@nuxt/kit' import { useTestContext } from './context' - -// @ts-expect-error type cast kit default export -const kit: typeof _kit = _kit.default || _kit +import { loadKit } from '../utils' const isNuxtApp = (dir: string) => { return existsSync(dir) && ( @@ -59,7 +56,8 @@ export async function loadFixture() { // TODO: share Nuxt instance with running Nuxt if possible if (ctx.options.build) { - ctx.nuxt = await kit.loadNuxt({ + const { loadNuxt } = await loadKit(ctx.options.rootDir) + ctx.nuxt = await loadNuxt({ cwd: ctx.options.rootDir, dev: ctx.options.dev, overrides: ctx.options.nuxtConfig, @@ -78,9 +76,11 @@ export async function loadFixture() { export async function buildFixture() { const ctx = useTestContext() + const { buildNuxt, logger } = await loadKit(ctx.options.rootDir) + // Hide build info for test - const prevLevel = kit.logger.level - kit.logger.level = ctx.options.logLevel - await kit.buildNuxt(ctx.nuxt!) - kit.logger.level = prevLevel + const prevLevel = logger.level + logger.level = ctx.options.logLevel + await buildNuxt(ctx.nuxt!) + logger.level = prevLevel } diff --git a/src/experimental.ts b/src/experimental.ts index a435327ec..990b3b2f3 100644 --- a/src/experimental.ts +++ b/src/experimental.ts @@ -1,5 +1,4 @@ import { $fetch as _$fetch, fetch as _fetch } from 'ofetch' -import * as _kit from '@nuxt/kit' import { resolve } from 'pathe' import { stringifyQuery } from 'ufo' import { useTestContext } from './e2e/context' diff --git a/src/module.ts b/src/module.ts index 8a6dfe8e3..653d55c8e 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,7 +1,7 @@ /// import { pathToFileURL } from 'node:url' -import { addVitePlugin, createResolver, defineNuxtModule, logger, resolvePath, importModule } from '@nuxt/kit' +import { createResolver, defineNuxtModule, logger, resolvePath, importModule } from '@nuxt/kit' import type { Vitest, UserConfig as VitestConfig } from 'vitest/node' import type { Reporter } from 'vitest/reporters' import type { RunnerTestFile } from 'vitest' @@ -11,11 +11,12 @@ import { h } from 'vue' import { debounce } from 'perfect-debounce' import { isCI } from 'std-env' import { defu } from 'defu' +import { join, relative } from 'pathe' import { getVitestConfigFromNuxt } from './config' import { setupImportMocking } from './module/mock' import { NuxtRootStubPlugin } from './module/plugins/entry' -import { join, relative } from 'pathe' +import { loadKit } from './utils' export interface NuxtVitestOptions { startOnBoot?: boolean @@ -40,9 +41,11 @@ export default defineNuxtModule({ }, async setup(options, nuxt) { if (nuxt.options.test || nuxt.options.dev) { - setupImportMocking() + await setupImportMocking(nuxt) } + const { addVitePlugin } = await loadKit(nuxt.options.rootDir) + const resolver = createResolver(import.meta.url) addVitePlugin(NuxtRootStubPlugin.vite({ entry: await resolvePath('#app/entry', { alias: nuxt.options.alias }), diff --git a/src/module/mock.ts b/src/module/mock.ts index 8997a621a..462e5280b 100644 --- a/src/module/mock.ts +++ b/src/module/mock.ts @@ -1,16 +1,17 @@ +import type { Nuxt } from '@nuxt/schema' import type { Unimport } from 'unimport' -import { addVitePlugin, resolveIgnorePatterns, useNuxt } from '@nuxt/kit' +import { resolveIgnorePatterns } from '@nuxt/kit' import { createMockPlugin } from './plugins/mock' import type { MockPluginContext } from './plugins/mock' +import { loadKit } from '../utils' /** * This module is a macro that transforms `mockNuxtImport()` to `vi.mock()`, * which make it possible to mock Nuxt imports. */ -export function setupImportMocking() { - const nuxt = useNuxt() - +export async function setupImportMocking(nuxt: Nuxt) { + const { addVitePlugin } = await loadKit(nuxt.options.rootDir) const ctx: MockPluginContext = { components: [], imports: [], diff --git a/src/runtime/global-setup.ts b/src/runtime/global-setup.ts index c3e1ba25e..ac85dd9ed 100644 --- a/src/runtime/global-setup.ts +++ b/src/runtime/global-setup.ts @@ -1,17 +1,14 @@ -import * as _kit from '@nuxt/kit' +import { consola } from 'consola' import { createTest, exposeContextToEnv } from '@nuxt/test-utils/e2e' -// @ts-expect-error type cast kit default export -const kit: typeof _kit = _kit.default || _kit - const options = JSON.parse(process.env.NUXT_TEST_OPTIONS || '{}') const hooks = createTest(options) export const setup = async () => { - kit.logger.info('Building Nuxt app...') + consola.info('Building Nuxt app...') await hooks.beforeAll() exposeContextToEnv() - kit.logger.info('Running tests...') + consola.info('Running tests...') } export const teardown = async () => { diff --git a/src/utils.ts b/src/utils.ts index 053744749..eb668eb4c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,6 +6,8 @@ import destr from 'destr' import { snakeCase } from 'scule' +import { pathToFileURL } from 'node:url' +import { resolveModulePath } from 'exsolve' type EnvOptions = { env?: Record @@ -52,3 +54,38 @@ export function applyEnv(obj: Record, opts: EnvOptions, parentKey = } return obj } + +export async function loadKit(rootDir: string): Promise { + try { + const kitPath = resolveModulePath('@nuxt/kit', { from: tryResolveNuxt(rootDir) || rootDir }) + + let kit: typeof import('@nuxt/kit') = await import(pathToFileURL(kitPath).href) + if (!kit.writeTypes) { + kit = { + ...kit, + writeTypes: () => { + throw new Error('`writeTypes` is not available in this version of `@nuxt/kit`. Please upgrade to v3.7 or newer.') + }, + } + } + return kit + } + catch (e: any) { + if (e.toString().includes('Cannot find module \'@nuxt/kit\'')) { + throw new Error( + 'nuxi requires `@nuxt/kit` to be installed in your project. Try installing `nuxt` v3+ or `@nuxt/bridge` first.', + ) + } + throw e + } +} + +export function tryResolveNuxt(rootDir: string) { + for (const pkg of ['nuxt-nightly', 'nuxt', 'nuxt3', 'nuxt-edge']) { + const path = resolveModulePath(pkg, { from: rootDir, try: true }) + if (path) { + return path + } + } + return null +}