From b55734afca56c08b5b940ad3b18b175217efa7f1 Mon Sep 17 00:00:00 2001 From: Catalin Irimie Date: Mon, 17 Aug 2026 22:02:31 +0300 Subject: [PATCH 1/5] fix(nuxt): resolve Nitro 2 adapter imports on the Nuxt >=3.7 floor --- .changeset/nuxt-nitro2-adapter-floor.md | 7 +++++++ packages/nuxt/src/runtime/nitro-plugin-v2.ts | 15 ++++++++++++--- packages/nuxt/tests/nitro-plugin.test.mjs | 9 +++++++-- 3 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 .changeset/nuxt-nitro2-adapter-floor.md diff --git a/.changeset/nuxt-nitro2-adapter-floor.md b/.changeset/nuxt-nitro2-adapter-floor.md new file mode 100644 index 0000000000..f67dc580cb --- /dev/null +++ b/.changeset/nuxt-nitro2-adapter-floor.md @@ -0,0 +1,7 @@ +--- +'@posthog/nuxt': patch +--- + +fix(nuxt): make the Nitro 2 adapter work on the declared Nuxt >= 3.7 floor + +The Nitro 2 adapter imported `defineNitroPlugin` and `useRuntimeConfig` from the bare `nitropack/runtime` subpath, which only exists in nitropack >= 2.9.5 — so on Nuxt 3.7–3.11 (which resolve older nitropack) builds emitted unresolved-import warnings and the packed server crashed at startup with `ERR_PACKAGE_PATH_NOT_EXPORTED`. The adapter now gets `useRuntimeConfig` from Nitro's version-agnostic `#imports` virtual module and exports a typed plain plugin function, so the built runtime no longer depends on nitropack's export map. Verified against Nuxt 3.7.0 + nitropack 2.6.2 and Nuxt 4.5; the Nitro 3 (Nuxt 5) adapter is unchanged. diff --git a/packages/nuxt/src/runtime/nitro-plugin-v2.ts b/packages/nuxt/src/runtime/nitro-plugin-v2.ts index 3f57de2bd3..9b002e6dc1 100644 --- a/packages/nuxt/src/runtime/nitro-plugin-v2.ts +++ b/packages/nuxt/src/runtime/nitro-plugin-v2.ts @@ -1,7 +1,14 @@ -import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime' +// Import via the `#imports` virtual module, not bare 'nitropack/runtime': that subpath +// only exists in nitropack >= 2.9.5 (guaranteed from Nuxt 3.12), so value-importing it +// breaks our declared Nuxt >= 3.7 floor — unresolved at build time, then +// ERR_PACKAGE_PATH_NOT_EXPORTED when the packed server starts. `#imports` resolves to +// Nitro's own runtime exports on every Nitro 2 version, and defineNitroPlugin is an +// identity function, so a typed plain export is equivalent. +import { useRuntimeConfig } from '#imports' +import type { NitroAppPlugin } from 'nitropack' import { setupPostHogNitroPlugin } from './nitro-plugin' -export default defineNitroPlugin((nitroApp) => { +const posthogNitroPlugin: NitroAppPlugin = (nitroApp) => { setupPostHogNitroPlugin({ useRuntimeConfig, onError: handler => @@ -10,4 +17,6 @@ export default defineNitroPlugin((nitroApp) => { ), onClose: handler => nitroApp.hooks.hook('close', handler), }) -}) +} + +export default posthogNitroPlugin diff --git a/packages/nuxt/tests/nitro-plugin.test.mjs b/packages/nuxt/tests/nitro-plugin.test.mjs index 9785ccc4dd..e2818f0282 100644 --- a/packages/nuxt/tests/nitro-plugin.test.mjs +++ b/packages/nuxt/tests/nitro-plugin.test.mjs @@ -97,7 +97,9 @@ function loadAdapter(filename, defineName) { const adapterSource = readFileSync(new URL(`../src/runtime/${filename}`, import.meta.url), 'utf8') const executableAdapter = adapterSource .replace(/^import .*$/gm, '') + .replace(': NitroAppPlugin', '') .replace(`export default ${defineName}(`, `return ${defineName}(`) + .replace(/^export default (\w+)$/m, 'return $1') let bindings const plugin = new Function(defineName, 'useRuntimeConfig', 'setupPostHogNitroPlugin', executableAdapter)( value => value, @@ -118,8 +120,11 @@ function loadAdapter(filename, defineName) { } const nitro2 = loadAdapter('nitro-plugin-v2.ts', 'defineNitroPlugin') -assert.match(nitro2.adapterSource, /from 'nitropack\/runtime'/) -assert.doesNotMatch(nitro2.adapterSource, /from '#imports'/) +// Bare 'nitropack/runtime' only exists in nitropack >= 2.9.5, so a value import breaks +// the declared Nuxt >= 3.7 floor at server startup (ERR_PACKAGE_PATH_NOT_EXPORTED); +// the Nitro 2 adapter must use the version-agnostic `#imports` virtual module instead. +assert.doesNotMatch(nitro2.adapterSource, /^import (?!type ).*'nitropack\/runtime'/m) +assert.match(nitro2.adapterSource, /from '#imports'/) let nitro2Request const nitro2Promise = Promise.resolve() nitro2.bindings.onError((_error, request) => { From 1213e723d57a85aeb72b267e66fe766661519642 Mon Sep 17 00:00:00 2001 From: Catalin Irimie Date: Tue, 18 Aug 2026 01:05:35 +0300 Subject: [PATCH 2/5] fix(nuxt): route Nuxt <3.12 to a legacy Nitro 2 adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review flagged that importing useRuntimeConfig from '#imports' breaks apps that set nitro.imports: false. Nitro only registers that virtual module when auto-imports are enabled, and no single import specifier spans both old nitropack (<2.9.5, no bare './runtime' export; deep './runtime/*' files get externalized and crash on '#internal/nitro/virtual/*') and that config. So select at build time instead: Nuxt >=3.12 (nitropack >=2.9.6 guaranteed) keeps the explicit nitropack/runtime adapter, older Nuxt gets a legacy adapter on '#imports' — the mechanism this module shipped with pre-split. --- .changeset/nuxt-nitro2-adapter-floor.md | 4 +-- packages/nuxt/src/module.ts | 12 ++++++++- .../src/runtime/nitro-plugin-v2-legacy.ts | 24 +++++++++++++++++ packages/nuxt/src/runtime/nitro-plugin-v2.ts | 15 +++-------- packages/nuxt/tests/nitro-plugin.test.mjs | 26 +++++++++++++++---- packages/nuxt/tests/sourcemaps-ssr.test.mjs | 6 ++++- 6 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 packages/nuxt/src/runtime/nitro-plugin-v2-legacy.ts diff --git a/.changeset/nuxt-nitro2-adapter-floor.md b/.changeset/nuxt-nitro2-adapter-floor.md index f67dc580cb..a5761d622d 100644 --- a/.changeset/nuxt-nitro2-adapter-floor.md +++ b/.changeset/nuxt-nitro2-adapter-floor.md @@ -2,6 +2,6 @@ '@posthog/nuxt': patch --- -fix(nuxt): make the Nitro 2 adapter work on the declared Nuxt >= 3.7 floor +fix(nuxt): restore the declared Nuxt >= 3.7 floor with a legacy Nitro 2 adapter -The Nitro 2 adapter imported `defineNitroPlugin` and `useRuntimeConfig` from the bare `nitropack/runtime` subpath, which only exists in nitropack >= 2.9.5 — so on Nuxt 3.7–3.11 (which resolve older nitropack) builds emitted unresolved-import warnings and the packed server crashed at startup with `ERR_PACKAGE_PATH_NOT_EXPORTED`. The adapter now gets `useRuntimeConfig` from Nitro's version-agnostic `#imports` virtual module and exports a typed plain plugin function, so the built runtime no longer depends on nitropack's export map. Verified against Nuxt 3.7.0 + nitropack 2.6.2 and Nuxt 4.5; the Nitro 3 (Nuxt 5) adapter is unchanged. +The Nitro 2 adapter imports `defineNitroPlugin` and `useRuntimeConfig` from the bare `nitropack/runtime` subpath, which only exists in nitropack >= 2.9.5 — so on Nuxt 3.7–3.11 (which resolve older nitropack) builds emitted unresolved-import warnings and the packed server crashed at startup with `ERR_PACKAGE_PATH_NOT_EXPORTED`. The module now selects a third adapter at build time: Nuxt >= 3.12 (which guarantees nitropack >= 2.9.6) keeps the existing explicit-import adapter, and older Nuxt gets a legacy adapter using the `#imports` virtual module — the same mechanism this module shipped with before the adapter split, which works on every old Nitro 2 version. Verified against Nuxt 3.7.0 + nitropack 2.6.2 and Nuxt 4.5 (including with `nitro: { imports: false }`); the Nitro 3 (Nuxt 5) adapter is unchanged. diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index adabd5cbb7..88fee021da 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -91,7 +91,17 @@ export default defineNuxtModule({ const normalizedPublicKey = normalizeApiKey(options.publicKey) const normalizedHost = normalizeHost(options.host) addPlugin({ src: resolver.resolve('./runtime/vue-plugin'), mode: 'client' }) - const nitroPlugin = Number.parseInt(getNuxtVersion(nuxt), 10) >= 5 ? 'nitro-plugin-v3' : 'nitro-plugin-v2' + // Nuxt >= 3.12 guarantees nitropack >= 2.9.6, which exports the bare 'nitropack/runtime' + // subpath the v2 adapter needs; older Nuxt gets the legacy adapter (see its header). + const [nuxtMajor = 0, nuxtMinor = 0] = getNuxtVersion(nuxt) + .split('.') + .map(part => Number.parseInt(part, 10)) + const nitroPlugin = + nuxtMajor >= 5 + ? 'nitro-plugin-v3' + : nuxtMajor === 4 || (nuxtMajor === 3 && nuxtMinor >= 12) + ? 'nitro-plugin-v2' + : 'nitro-plugin-v2-legacy' addServerPlugin(resolver.resolve(`./runtime/${nitroPlugin}`)) addImportsDir(resolver.resolve('./runtime/composables')) diff --git a/packages/nuxt/src/runtime/nitro-plugin-v2-legacy.ts b/packages/nuxt/src/runtime/nitro-plugin-v2-legacy.ts new file mode 100644 index 0000000000..ccca6388d3 --- /dev/null +++ b/packages/nuxt/src/runtime/nitro-plugin-v2-legacy.ts @@ -0,0 +1,24 @@ +// Nitro 2 adapter for Nuxt < 3.12, where nitropack can resolve below 2.9.5 and the bare +// 'nitropack/runtime' subpath used by nitro-plugin-v2 does not exist (unresolved at build +// time, ERR_PACKAGE_PATH_NOT_EXPORTED at server startup). Deep subpaths like +// 'nitropack/runtime/config' resolve but old Nitro externalizes them, so they crash at +// runtime on nitro's build-time-only '#internal/nitro/virtual/*' specifiers. The +// `#imports` virtual module is the one mechanism that works on every old Nitro 2 version +// (it is what this module shipped with before the adapter split), and defineNitroPlugin +// is an identity function, so a typed plain export is equivalent. +import { useRuntimeConfig } from '#imports' +import type { NitroAppPlugin } from 'nitropack' +import { setupPostHogNitroPlugin } from './nitro-plugin' + +const posthogNitroPlugin: NitroAppPlugin = (nitroApp) => { + setupPostHogNitroPlugin({ + useRuntimeConfig, + onError: handler => + nitroApp.hooks.hook('error', (error, { event }) => + handler(error, event ? { path: event.path, method: event.method } : undefined), + ), + onClose: handler => nitroApp.hooks.hook('close', handler), + }) +} + +export default posthogNitroPlugin diff --git a/packages/nuxt/src/runtime/nitro-plugin-v2.ts b/packages/nuxt/src/runtime/nitro-plugin-v2.ts index 9b002e6dc1..3f57de2bd3 100644 --- a/packages/nuxt/src/runtime/nitro-plugin-v2.ts +++ b/packages/nuxt/src/runtime/nitro-plugin-v2.ts @@ -1,14 +1,7 @@ -// Import via the `#imports` virtual module, not bare 'nitropack/runtime': that subpath -// only exists in nitropack >= 2.9.5 (guaranteed from Nuxt 3.12), so value-importing it -// breaks our declared Nuxt >= 3.7 floor — unresolved at build time, then -// ERR_PACKAGE_PATH_NOT_EXPORTED when the packed server starts. `#imports` resolves to -// Nitro's own runtime exports on every Nitro 2 version, and defineNitroPlugin is an -// identity function, so a typed plain export is equivalent. -import { useRuntimeConfig } from '#imports' -import type { NitroAppPlugin } from 'nitropack' +import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime' import { setupPostHogNitroPlugin } from './nitro-plugin' -const posthogNitroPlugin: NitroAppPlugin = (nitroApp) => { +export default defineNitroPlugin((nitroApp) => { setupPostHogNitroPlugin({ useRuntimeConfig, onError: handler => @@ -17,6 +10,4 @@ const posthogNitroPlugin: NitroAppPlugin = (nitroApp) => { ), onClose: handler => nitroApp.hooks.hook('close', handler), }) -} - -export default posthogNitroPlugin +}) diff --git a/packages/nuxt/tests/nitro-plugin.test.mjs b/packages/nuxt/tests/nitro-plugin.test.mjs index e2818f0282..a071b4e0b6 100644 --- a/packages/nuxt/tests/nitro-plugin.test.mjs +++ b/packages/nuxt/tests/nitro-plugin.test.mjs @@ -120,11 +120,8 @@ function loadAdapter(filename, defineName) { } const nitro2 = loadAdapter('nitro-plugin-v2.ts', 'defineNitroPlugin') -// Bare 'nitropack/runtime' only exists in nitropack >= 2.9.5, so a value import breaks -// the declared Nuxt >= 3.7 floor at server startup (ERR_PACKAGE_PATH_NOT_EXPORTED); -// the Nitro 2 adapter must use the version-agnostic `#imports` virtual module instead. -assert.doesNotMatch(nitro2.adapterSource, /^import (?!type ).*'nitropack\/runtime'/m) -assert.match(nitro2.adapterSource, /from '#imports'/) +assert.match(nitro2.adapterSource, /from 'nitropack\/runtime'/) +assert.doesNotMatch(nitro2.adapterSource, /from '#imports'/) let nitro2Request const nitro2Promise = Promise.resolve() nitro2.bindings.onError((_error, request) => { @@ -134,6 +131,25 @@ nitro2.bindings.onError((_error, request) => { assert.equal(nitro2.adapterHandlers.error(error, { event: { path: '/v2', method: 'GET' } }), nitro2Promise) assert.deepEqual(nitro2Request, { path: '/v2', method: 'GET' }) +// The legacy adapter serves Nuxt < 3.12, where nitropack can resolve below 2.9.5 and the +// bare 'nitropack/runtime' subpath does not exist (ERR_PACKAGE_PATH_NOT_EXPORTED at server +// startup); it must stay on the `#imports` virtual module, which every Nitro 2 provides. +const nitro2Legacy = loadAdapter('nitro-plugin-v2-legacy.ts', 'defineNitroPlugin') +assert.doesNotMatch(nitro2Legacy.adapterSource, /from 'nitropack\/runtime/) +assert.match(nitro2Legacy.adapterSource, /from '#imports'/) +let legacyRequest +const legacyPromise = Promise.resolve() +nitro2Legacy.bindings.onError((_error, request) => { + legacyRequest = request + return legacyPromise +}) +assert.equal(nitro2Legacy.adapterHandlers.error(error, { event: { path: '/legacy', method: 'GET' } }), legacyPromise) +assert.deepEqual(legacyRequest, { path: '/legacy', method: 'GET' }) + +// module.ts must route old Nuxt to the legacy adapter. +const moduleSource = readFileSync(new URL('../src/module.ts', import.meta.url), 'utf8') +assert.match(moduleSource, /nitro-plugin-v2-legacy/) + const nitro3 = loadAdapter('nitro-plugin-v3.ts', 'definePlugin') assert.match(nitro3.adapterSource, /from 'nitro'/) assert.match(nitro3.adapterSource, /from 'nitro\/runtime-config'/) diff --git a/packages/nuxt/tests/sourcemaps-ssr.test.mjs b/packages/nuxt/tests/sourcemaps-ssr.test.mjs index d6e1202876..d78b354df2 100644 --- a/packages/nuxt/tests/sourcemaps-ssr.test.mjs +++ b/packages/nuxt/tests/sourcemaps-ssr.test.mjs @@ -96,7 +96,11 @@ async function runRegistration({ nuxtVersion, compatibilityVersion }) { } for (const { nuxtVersion, compatibilityVersion, expectedServerPlugin } of [ - { nuxtVersion: '3.7.0', expectedServerPlugin: './runtime/nitro-plugin-v2' }, + // Nuxt < 3.12 can resolve nitropack < 2.9.5, which lacks the bare 'nitropack/runtime' + // subpath the v2 adapter imports — those versions must get the legacy adapter. + { nuxtVersion: '3.7.0', expectedServerPlugin: './runtime/nitro-plugin-v2-legacy' }, + { nuxtVersion: '3.11.2', expectedServerPlugin: './runtime/nitro-plugin-v2-legacy' }, + { nuxtVersion: '3.12.0', expectedServerPlugin: './runtime/nitro-plugin-v2' }, { nuxtVersion: '4.1.2', expectedServerPlugin: './runtime/nitro-plugin-v2' }, { nuxtVersion: '4.1.2', compatibilityVersion: 5, expectedServerPlugin: './runtime/nitro-plugin-v2' }, { nuxtVersion: '5.0.0-0', expectedServerPlugin: './runtime/nitro-plugin-v3' }, From 4eb7568ce37185c41157e9b20a32c10ee912514e Mon Sep 17 00:00:00 2001 From: Catalin Irimie Date: Tue, 18 Aug 2026 01:12:02 +0300 Subject: [PATCH 3/5] fix(nuxt): route Nuxt >=3.11.2 to the explicit Nitro 2 adapter Nuxt 3.11.2 already requires nitropack ^2.9.6, so it can use the nitropack/runtime adapter and keep nitro.imports: false working; only older versions fall back to the legacy #imports adapter. --- .changeset/nuxt-nitro2-adapter-floor.md | 2 +- packages/nuxt/src/module.ts | 12 +++++------- packages/nuxt/tests/sourcemaps-ssr.test.mjs | 5 +++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.changeset/nuxt-nitro2-adapter-floor.md b/.changeset/nuxt-nitro2-adapter-floor.md index a5761d622d..2dcf8ca806 100644 --- a/.changeset/nuxt-nitro2-adapter-floor.md +++ b/.changeset/nuxt-nitro2-adapter-floor.md @@ -4,4 +4,4 @@ fix(nuxt): restore the declared Nuxt >= 3.7 floor with a legacy Nitro 2 adapter -The Nitro 2 adapter imports `defineNitroPlugin` and `useRuntimeConfig` from the bare `nitropack/runtime` subpath, which only exists in nitropack >= 2.9.5 — so on Nuxt 3.7–3.11 (which resolve older nitropack) builds emitted unresolved-import warnings and the packed server crashed at startup with `ERR_PACKAGE_PATH_NOT_EXPORTED`. The module now selects a third adapter at build time: Nuxt >= 3.12 (which guarantees nitropack >= 2.9.6) keeps the existing explicit-import adapter, and older Nuxt gets a legacy adapter using the `#imports` virtual module — the same mechanism this module shipped with before the adapter split, which works on every old Nitro 2 version. Verified against Nuxt 3.7.0 + nitropack 2.6.2 and Nuxt 4.5 (including with `nitro: { imports: false }`); the Nitro 3 (Nuxt 5) adapter is unchanged. +The Nitro 2 adapter imports `defineNitroPlugin` and `useRuntimeConfig` from the bare `nitropack/runtime` subpath, which only exists in nitropack >= 2.9.5 — so on Nuxt 3.7–3.11 (which resolve older nitropack) builds emitted unresolved-import warnings and the packed server crashed at startup with `ERR_PACKAGE_PATH_NOT_EXPORTED`. The module now selects a third adapter at build time: Nuxt >= 3.11.2 (which guarantees nitropack >= 2.9.6) keeps the existing explicit-import adapter, and older Nuxt gets a legacy adapter using the `#imports` virtual module — the same mechanism this module shipped with before the adapter split, which works on every old Nitro 2 version. Verified against Nuxt 3.7.0 + nitropack 2.6.2 and Nuxt 4.5 (including with `nitro: { imports: false }`); the Nitro 3 (Nuxt 5) adapter is unchanged. diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 88fee021da..810480d3dc 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -91,17 +91,15 @@ export default defineNuxtModule({ const normalizedPublicKey = normalizeApiKey(options.publicKey) const normalizedHost = normalizeHost(options.host) addPlugin({ src: resolver.resolve('./runtime/vue-plugin'), mode: 'client' }) - // Nuxt >= 3.12 guarantees nitropack >= 2.9.6, which exports the bare 'nitropack/runtime' + // Nuxt >= 3.11.2 guarantees nitropack >= 2.9.6, which exports the bare 'nitropack/runtime' // subpath the v2 adapter needs; older Nuxt gets the legacy adapter (see its header). - const [nuxtMajor = 0, nuxtMinor = 0] = getNuxtVersion(nuxt) + const [nuxtMajor = 0, nuxtMinor = 0, nuxtPatch = 0] = getNuxtVersion(nuxt) .split('.') .map(part => Number.parseInt(part, 10)) + const nuxtHasNitropackRuntimeExport = + nuxtMajor > 3 || (nuxtMajor === 3 && (nuxtMinor > 11 || (nuxtMinor === 11 && nuxtPatch >= 2))) const nitroPlugin = - nuxtMajor >= 5 - ? 'nitro-plugin-v3' - : nuxtMajor === 4 || (nuxtMajor === 3 && nuxtMinor >= 12) - ? 'nitro-plugin-v2' - : 'nitro-plugin-v2-legacy' + nuxtMajor >= 5 ? 'nitro-plugin-v3' : nuxtHasNitropackRuntimeExport ? 'nitro-plugin-v2' : 'nitro-plugin-v2-legacy' addServerPlugin(resolver.resolve(`./runtime/${nitroPlugin}`)) addImportsDir(resolver.resolve('./runtime/composables')) diff --git a/packages/nuxt/tests/sourcemaps-ssr.test.mjs b/packages/nuxt/tests/sourcemaps-ssr.test.mjs index d78b354df2..20bb91f3a9 100644 --- a/packages/nuxt/tests/sourcemaps-ssr.test.mjs +++ b/packages/nuxt/tests/sourcemaps-ssr.test.mjs @@ -96,10 +96,11 @@ async function runRegistration({ nuxtVersion, compatibilityVersion }) { } for (const { nuxtVersion, compatibilityVersion, expectedServerPlugin } of [ - // Nuxt < 3.12 can resolve nitropack < 2.9.5, which lacks the bare 'nitropack/runtime' + // Nuxt < 3.11.2 can resolve nitropack < 2.9.5, which lacks the bare 'nitropack/runtime' // subpath the v2 adapter imports — those versions must get the legacy adapter. { nuxtVersion: '3.7.0', expectedServerPlugin: './runtime/nitro-plugin-v2-legacy' }, - { nuxtVersion: '3.11.2', expectedServerPlugin: './runtime/nitro-plugin-v2-legacy' }, + { nuxtVersion: '3.11.1', expectedServerPlugin: './runtime/nitro-plugin-v2-legacy' }, + { nuxtVersion: '3.11.2', expectedServerPlugin: './runtime/nitro-plugin-v2' }, { nuxtVersion: '3.12.0', expectedServerPlugin: './runtime/nitro-plugin-v2' }, { nuxtVersion: '4.1.2', expectedServerPlugin: './runtime/nitro-plugin-v2' }, { nuxtVersion: '4.1.2', compatibilityVersion: 5, expectedServerPlugin: './runtime/nitro-plugin-v2' }, From a12d46e6864485d89552d975542e6b218a59eb3a Mon Sep 17 00:00:00 2001 From: Catalin Irimie Date: Tue, 18 Aug 2026 01:19:55 +0300 Subject: [PATCH 4/5] fix(nuxt): select Nitro 2 adapter from the resolved nitropack export map Codex review round 3: a Nuxt-version cutoff misroutes installs whose lockfile floats or pins nitropack across the 2.9.5 boundary. Probe the './runtime' entry in the export map of the nitropack copy Nuxt resolves (via createRequire from appDir) and route on capability instead; fall back to the legacy adapter when no manifest is resolvable. --- .changeset/nuxt-nitro2-adapter-floor.md | 2 +- packages/nuxt/src/module.ts | 32 ++++++++++---- .../src/runtime/nitro-plugin-v2-legacy.ts | 7 +-- packages/nuxt/tests/sourcemaps-ssr.test.mjs | 44 ++++++++++++------- 4 files changed, 57 insertions(+), 28 deletions(-) diff --git a/.changeset/nuxt-nitro2-adapter-floor.md b/.changeset/nuxt-nitro2-adapter-floor.md index 2dcf8ca806..0b3090c88f 100644 --- a/.changeset/nuxt-nitro2-adapter-floor.md +++ b/.changeset/nuxt-nitro2-adapter-floor.md @@ -4,4 +4,4 @@ fix(nuxt): restore the declared Nuxt >= 3.7 floor with a legacy Nitro 2 adapter -The Nitro 2 adapter imports `defineNitroPlugin` and `useRuntimeConfig` from the bare `nitropack/runtime` subpath, which only exists in nitropack >= 2.9.5 — so on Nuxt 3.7–3.11 (which resolve older nitropack) builds emitted unresolved-import warnings and the packed server crashed at startup with `ERR_PACKAGE_PATH_NOT_EXPORTED`. The module now selects a third adapter at build time: Nuxt >= 3.11.2 (which guarantees nitropack >= 2.9.6) keeps the existing explicit-import adapter, and older Nuxt gets a legacy adapter using the `#imports` virtual module — the same mechanism this module shipped with before the adapter split, which works on every old Nitro 2 version. Verified against Nuxt 3.7.0 + nitropack 2.6.2 and Nuxt 4.5 (including with `nitro: { imports: false }`); the Nitro 3 (Nuxt 5) adapter is unchanged. +The Nitro 2 adapter imports `defineNitroPlugin` and `useRuntimeConfig` from the bare `nitropack/runtime` subpath, which only exists in nitropack >= 2.9.5 — so on Nuxt 3.7–3.11 (which can resolve older nitropack) builds emitted unresolved-import warnings and the packed server crashed at startup with `ERR_PACKAGE_PATH_NOT_EXPORTED`. The module now probes the export map of the nitropack copy Nuxt resolves and selects the adapter from it at build time: nitropack with the bare `./runtime` export keeps the existing explicit-import adapter, and older nitropack gets a legacy adapter using the `#imports` virtual module — the same mechanism this module shipped with before the adapter split. Verified against Nuxt 3.7.0 + nitropack 2.6.2 and Nuxt 4.5 (including with `nitro: { imports: false }`); the Nitro 3 (Nuxt 5) adapter is unchanged. diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 810480d3dc..9faed78076 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -4,7 +4,8 @@ import type { PostHogOptions } from 'posthog-node' import type {} from 'nuxt/app' import { resolveBinaryPath, spawnLocal } from '@posthog/plugin-utils' import { fileURLToPath } from 'node:url' -import { dirname } from 'node:path' +import { dirname, join } from 'node:path' +import { createRequire } from 'node:module' const filename = fileURLToPath(import.meta.url) const resolvedDirname = dirname(filename) @@ -23,6 +24,22 @@ function normalizeHost(value?: unknown): string { return normalizedValue || DEFAULT_NUXT_HOST } +// The v2 nitro adapter imports the bare 'nitropack/runtime' subpath, which only exists +// in nitropack >= 2.9.5 (Nuxt < 3.11.2 can resolve older). Probe the export map of the +// nitropack copy Nuxt itself resolves instead of keying off Nuxt versions, so lockfiles +// that float or pin nitropack still get the right adapter. +function nitropackHasBareRuntimeExport(appDir: string): boolean { + try { + const requireFromNuxt = createRequire(join(appDir, 'index.mjs')) + const manifest = requireFromNuxt('nitropack/package.json') + return Boolean(manifest.exports?.['./runtime']) + } catch { + // No resolvable nitropack manifest: fall back to the legacy adapter, which matches + // the behavior this module shipped with before the adapter split. + return false + } +} + type LogLevel = 'debug' | 'info' | 'warn' | 'error' interface SourcemapsConfig { @@ -91,15 +108,12 @@ export default defineNuxtModule({ const normalizedPublicKey = normalizeApiKey(options.publicKey) const normalizedHost = normalizeHost(options.host) addPlugin({ src: resolver.resolve('./runtime/vue-plugin'), mode: 'client' }) - // Nuxt >= 3.11.2 guarantees nitropack >= 2.9.6, which exports the bare 'nitropack/runtime' - // subpath the v2 adapter needs; older Nuxt gets the legacy adapter (see its header). - const [nuxtMajor = 0, nuxtMinor = 0, nuxtPatch = 0] = getNuxtVersion(nuxt) - .split('.') - .map(part => Number.parseInt(part, 10)) - const nuxtHasNitropackRuntimeExport = - nuxtMajor > 3 || (nuxtMajor === 3 && (nuxtMinor > 11 || (nuxtMinor === 11 && nuxtPatch >= 2))) const nitroPlugin = - nuxtMajor >= 5 ? 'nitro-plugin-v3' : nuxtHasNitropackRuntimeExport ? 'nitro-plugin-v2' : 'nitro-plugin-v2-legacy' + Number.parseInt(getNuxtVersion(nuxt), 10) >= 5 + ? 'nitro-plugin-v3' + : nitropackHasBareRuntimeExport(nuxt.options.appDir) + ? 'nitro-plugin-v2' + : 'nitro-plugin-v2-legacy' addServerPlugin(resolver.resolve(`./runtime/${nitroPlugin}`)) addImportsDir(resolver.resolve('./runtime/composables')) diff --git a/packages/nuxt/src/runtime/nitro-plugin-v2-legacy.ts b/packages/nuxt/src/runtime/nitro-plugin-v2-legacy.ts index ccca6388d3..f379a7da9f 100644 --- a/packages/nuxt/src/runtime/nitro-plugin-v2-legacy.ts +++ b/packages/nuxt/src/runtime/nitro-plugin-v2-legacy.ts @@ -1,6 +1,7 @@ -// Nitro 2 adapter for Nuxt < 3.12, where nitropack can resolve below 2.9.5 and the bare -// 'nitropack/runtime' subpath used by nitro-plugin-v2 does not exist (unresolved at build -// time, ERR_PACKAGE_PATH_NOT_EXPORTED at server startup). Deep subpaths like +// Nitro 2 adapter for installs whose nitropack lacks the bare 'nitropack/runtime' subpath +// (nitropack < 2.9.5, possible on Nuxt < 3.11.2), where the import used by nitro-plugin-v2 +// does not resolve (unresolved at build time, ERR_PACKAGE_PATH_NOT_EXPORTED at server +// startup). Deep subpaths like // 'nitropack/runtime/config' resolve but old Nitro externalizes them, so they crash at // runtime on nitro's build-time-only '#internal/nitro/virtual/*' specifiers. The // `#imports` virtual module is the one mechanism that works on every old Nitro 2 version diff --git a/packages/nuxt/tests/sourcemaps-ssr.test.mjs b/packages/nuxt/tests/sourcemaps-ssr.test.mjs index 20bb91f3a9..2d40e7486e 100644 --- a/packages/nuxt/tests/sourcemaps-ssr.test.mjs +++ b/packages/nuxt/tests/sourcemaps-ssr.test.mjs @@ -33,6 +33,7 @@ const executableSource = source .replace(/value\?: unknown/g, 'value') .replace(/\(directory: string, sourcemapsConfig: SourcemapsConfig\)/g, '(directory, sourcemapsConfig)') .replace(/\(args: string\[\]\)/g, '(args)') + .replace(/\(appDir: string\)/g, '(appDir)') .replace(/\): string \{/g, ') {') .replace(/\): boolean \{/g, ') {') .replace(/let (outputDir|publicDir|serverDir): string \| undefined/g, 'let $1') @@ -43,7 +44,7 @@ const executableSource = source // Turn the module's `export default` into a value the wrapper returns. .replace('export default defineNuxtModule(', 'return defineNuxtModule(') -function loadModule({ failPublicUpload = false, nuxtVersion = '4.1.2' } = {}) { +function loadModule({ failPublicUpload = false, nuxtVersion = '4.1.2', nitropackExports = { './runtime': {} } } = {}) { const spawnCalls = [] const pluginCalls = [] const serverPluginCalls = [] @@ -54,6 +55,11 @@ function loadModule({ failPublicUpload = false, nuxtVersion = '4.1.2' } = {}) { addImportsDir: () => {}, createResolver: () => ({ resolve: p => p }), getNuxtVersion: () => nuxtVersion, + createRequire: () => () => { + if (nitropackExports === null) throw new Error('nitropack not resolvable') + return { exports: nitropackExports } + }, + join: (...parts) => parts.join('/'), resolveBinaryPath: () => '/fake/posthog-cli', spawnLocal: async (bin, args) => { spawnCalls.push({ bin, args: [...args] }) @@ -71,13 +77,14 @@ function loadModule({ failPublicUpload = false, nuxtVersion = '4.1.2' } = {}) { return { mod, spawnCalls, pluginCalls, serverPluginCalls } } -async function runRegistration({ nuxtVersion, compatibilityVersion }) { - const { mod, pluginCalls, serverPluginCalls } = loadModule({ nuxtVersion }) +async function runRegistration({ nuxtVersion, compatibilityVersion, nitropackExports }) { + const { mod, pluginCalls, serverPluginCalls } = loadModule({ nuxtVersion, nitropackExports }) const nuxt = { options: { dev: true, future: { compatibilityVersion }, runtimeConfig: { public: {} }, + appDir: '/fake/nuxt/app', }, hook() {}, } @@ -95,19 +102,26 @@ async function runRegistration({ nuxtVersion, compatibilityVersion }) { return { pluginCalls, serverPluginCalls } } -for (const { nuxtVersion, compatibilityVersion, expectedServerPlugin } of [ - // Nuxt < 3.11.2 can resolve nitropack < 2.9.5, which lacks the bare 'nitropack/runtime' - // subpath the v2 adapter imports — those versions must get the legacy adapter. - { nuxtVersion: '3.7.0', expectedServerPlugin: './runtime/nitro-plugin-v2-legacy' }, - { nuxtVersion: '3.11.1', expectedServerPlugin: './runtime/nitro-plugin-v2-legacy' }, - { nuxtVersion: '3.11.2', expectedServerPlugin: './runtime/nitro-plugin-v2' }, - { nuxtVersion: '3.12.0', expectedServerPlugin: './runtime/nitro-plugin-v2' }, - { nuxtVersion: '4.1.2', expectedServerPlugin: './runtime/nitro-plugin-v2' }, - { nuxtVersion: '4.1.2', compatibilityVersion: 5, expectedServerPlugin: './runtime/nitro-plugin-v2' }, - { nuxtVersion: '5.0.0-0', expectedServerPlugin: './runtime/nitro-plugin-v3' }, - { nuxtVersion: '5.0.0-29762631.396a4ae3', expectedServerPlugin: './runtime/nitro-plugin-v3' }, +const NITROPACK_OLD_EXPORTS = { './runtime/*': {} } // nitropack < 2.9.5: no bare './runtime' +const NITROPACK_NEW_EXPORTS = { './runtime': {}, './runtime/*': {} } + +for (const { nuxtVersion, compatibilityVersion, nitropackExports, expectedServerPlugin } of [ + // Adapter choice follows the resolved nitropack's export map, not the Nuxt version: + // nitropack < 2.9.5 lacks the bare 'nitropack/runtime' subpath the v2 adapter imports + // and must get the legacy adapter, while old Nuxt whose lockfile floated to a newer + // nitropack keeps the explicit v2 adapter (it works with `nitro: { imports: false }`). + { nuxtVersion: '3.7.0', nitropackExports: NITROPACK_OLD_EXPORTS, expectedServerPlugin: './runtime/nitro-plugin-v2-legacy' }, + { nuxtVersion: '3.7.0', nitropackExports: NITROPACK_NEW_EXPORTS, expectedServerPlugin: './runtime/nitro-plugin-v2' }, + { nuxtVersion: '3.11.1', nitropackExports: NITROPACK_OLD_EXPORTS, expectedServerPlugin: './runtime/nitro-plugin-v2-legacy' }, + { nuxtVersion: '3.11.1', nitropackExports: NITROPACK_NEW_EXPORTS, expectedServerPlugin: './runtime/nitro-plugin-v2' }, + // Unresolvable nitropack manifest falls back to the legacy adapter. + { nuxtVersion: '3.7.0', nitropackExports: null, expectedServerPlugin: './runtime/nitro-plugin-v2-legacy' }, + { nuxtVersion: '4.1.2', nitropackExports: NITROPACK_NEW_EXPORTS, expectedServerPlugin: './runtime/nitro-plugin-v2' }, + { nuxtVersion: '4.1.2', compatibilityVersion: 5, nitropackExports: NITROPACK_NEW_EXPORTS, expectedServerPlugin: './runtime/nitro-plugin-v2' }, + { nuxtVersion: '5.0.0-0', nitropackExports: null, expectedServerPlugin: './runtime/nitro-plugin-v3' }, + { nuxtVersion: '5.0.0-29762631.396a4ae3', nitropackExports: null, expectedServerPlugin: './runtime/nitro-plugin-v3' }, ]) { - const { pluginCalls, serverPluginCalls } = await runRegistration({ nuxtVersion, compatibilityVersion }) + const { pluginCalls, serverPluginCalls } = await runRegistration({ nuxtVersion, compatibilityVersion, nitropackExports }) assert.deepEqual(pluginCalls, [{ src: './runtime/vue-plugin', mode: 'client' }]) assert.deepEqual(serverPluginCalls, [expectedServerPlugin]) } From 6182fda7d39ea9b71acb5c98f2ff1c4751457d6a Mon Sep 17 00:00:00 2001 From: Catalin Irimie Date: Tue, 18 Aug 2026 01:27:59 +0300 Subject: [PATCH 5/5] fix(nuxt): fall back to Nuxt-version gate when nitropack probe is blind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review round 4: Nuxt >= 4.5 depends on nitropack via @nuxt/nitro-server, so under isolated pnpm installs the manifest is not resolvable from the nuxt package and the probe would misroute modern Nuxt to the legacy adapter (breaking nitro.imports: false there). Treat an unresolvable manifest as inconclusive and gate on Nuxt >= 3.11.2 — the first release whose nitropack range guarantees the bare './runtime' export — instead of assuming legacy. --- packages/nuxt/src/module.ts | 24 ++++++++++++--------- packages/nuxt/tests/sourcemaps-ssr.test.mjs | 8 ++++++- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 9faed78076..f4b2412fb4 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -27,16 +27,17 @@ function normalizeHost(value?: unknown): string { // The v2 nitro adapter imports the bare 'nitropack/runtime' subpath, which only exists // in nitropack >= 2.9.5 (Nuxt < 3.11.2 can resolve older). Probe the export map of the // nitropack copy Nuxt itself resolves instead of keying off Nuxt versions, so lockfiles -// that float or pin nitropack still get the right adapter. -function nitropackHasBareRuntimeExport(appDir: string): boolean { +// that float or pin nitropack still get the right adapter. Returns null when no manifest +// is resolvable — e.g. Nuxt >= 4.5 depends on nitropack via @nuxt/nitro-server, so under +// isolated installs it is not reachable from the nuxt package; the caller falls back to +// a Nuxt-version gate there. +function nitropackHasBareRuntimeExport(appDir: string): boolean | null { try { const requireFromNuxt = createRequire(join(appDir, 'index.mjs')) const manifest = requireFromNuxt('nitropack/package.json') return Boolean(manifest.exports?.['./runtime']) } catch { - // No resolvable nitropack manifest: fall back to the legacy adapter, which matches - // the behavior this module shipped with before the adapter split. - return false + return null } } @@ -108,12 +109,15 @@ export default defineNuxtModule({ const normalizedPublicKey = normalizeApiKey(options.publicKey) const normalizedHost = normalizeHost(options.host) addPlugin({ src: resolver.resolve('./runtime/vue-plugin'), mode: 'client' }) + const [nuxtMajor = 0, nuxtMinor = 0, nuxtPatch = 0] = getNuxtVersion(nuxt) + .split('.') + .map(part => Number.parseInt(part, 10)) + // When the export-map probe is inconclusive, gate on the Nuxt version instead: + // Nuxt >= 3.11.2 is the first release whose nitropack range guarantees >= 2.9.6. + const modernNuxt = nuxtMajor > 3 || (nuxtMajor === 3 && (nuxtMinor > 11 || (nuxtMinor === 11 && nuxtPatch >= 2))) + const hasBareRuntimeExport = nitropackHasBareRuntimeExport(nuxt.options.appDir) ?? modernNuxt const nitroPlugin = - Number.parseInt(getNuxtVersion(nuxt), 10) >= 5 - ? 'nitro-plugin-v3' - : nitropackHasBareRuntimeExport(nuxt.options.appDir) - ? 'nitro-plugin-v2' - : 'nitro-plugin-v2-legacy' + nuxtMajor >= 5 ? 'nitro-plugin-v3' : hasBareRuntimeExport ? 'nitro-plugin-v2' : 'nitro-plugin-v2-legacy' addServerPlugin(resolver.resolve(`./runtime/${nitroPlugin}`)) addImportsDir(resolver.resolve('./runtime/composables')) diff --git a/packages/nuxt/tests/sourcemaps-ssr.test.mjs b/packages/nuxt/tests/sourcemaps-ssr.test.mjs index 2d40e7486e..2e35df2c9c 100644 --- a/packages/nuxt/tests/sourcemaps-ssr.test.mjs +++ b/packages/nuxt/tests/sourcemaps-ssr.test.mjs @@ -36,6 +36,7 @@ const executableSource = source .replace(/\(appDir: string\)/g, '(appDir)') .replace(/\): string \{/g, ') {') .replace(/\): boolean \{/g, ') {') + .replace(/\): boolean \| null \{/g, ') {') .replace(/let (outputDir|publicDir|serverDir): string \| undefined/g, 'let $1') .replace(/const processOptions: string\[\] = /g, 'const processOptions = ') // `import.meta.url` is not available inside `new Function`; the value is @@ -114,8 +115,13 @@ for (const { nuxtVersion, compatibilityVersion, nitropackExports, expectedServer { nuxtVersion: '3.7.0', nitropackExports: NITROPACK_NEW_EXPORTS, expectedServerPlugin: './runtime/nitro-plugin-v2' }, { nuxtVersion: '3.11.1', nitropackExports: NITROPACK_OLD_EXPORTS, expectedServerPlugin: './runtime/nitro-plugin-v2-legacy' }, { nuxtVersion: '3.11.1', nitropackExports: NITROPACK_NEW_EXPORTS, expectedServerPlugin: './runtime/nitro-plugin-v2' }, - // Unresolvable nitropack manifest falls back to the legacy adapter. + // Unresolvable nitropack manifest (e.g. Nuxt >= 4.5 isolated installs, where nitropack + // lives under @nuxt/nitro-server) falls back to the Nuxt-version gate: >= 3.11.2 is the + // first release whose nitropack range guarantees the bare './runtime' export. { nuxtVersion: '3.7.0', nitropackExports: null, expectedServerPlugin: './runtime/nitro-plugin-v2-legacy' }, + { nuxtVersion: '3.11.1', nitropackExports: null, expectedServerPlugin: './runtime/nitro-plugin-v2-legacy' }, + { nuxtVersion: '3.11.2', nitropackExports: null, expectedServerPlugin: './runtime/nitro-plugin-v2' }, + { nuxtVersion: '4.5.1', nitropackExports: null, expectedServerPlugin: './runtime/nitro-plugin-v2' }, { nuxtVersion: '4.1.2', nitropackExports: NITROPACK_NEW_EXPORTS, expectedServerPlugin: './runtime/nitro-plugin-v2' }, { nuxtVersion: '4.1.2', compatibilityVersion: 5, nitropackExports: NITROPACK_NEW_EXPORTS, expectedServerPlugin: './runtime/nitro-plugin-v2' }, { nuxtVersion: '5.0.0-0', nitropackExports: null, expectedServerPlugin: './runtime/nitro-plugin-v3' },