From cd40ad68692da73bb8dc7599eab7861f430c864f Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:37:21 +0200 Subject: [PATCH 1/2] fix(nuxt): Detect Nitro version via the app's Nuxt dependency chain (#24019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a monorepo where a `nitro` v3 package is resolvable above a Nitro-v2 Nuxt app, the SDK registers the Nitro v3 plugin variants and the app silently loses storage, database, and cache instrumentation. Nitro v2 is published as `nitropack`, v3 as `nitro`. `getNitroMajorVersion()` asked local-pkg for any package named `nitro` with no anchor. Module resolution walks up the directory tree, so an unrelated `nitro` v3 higher up (for example a workspace-root devDependency) wins even though the app's Nuxt imports `nitropack` v2. The fix follows the dependency chain Nuxt itself imports Nitro through: resolve `nuxt` from `rootDir`, hop to `@nuxt/nitro-server` when nuxt declares it (Nuxt >= 3.21), and only resolve `nitro` if that provider declares it. `nitropack` instead means v2: - Nuxt 3.x / 4.0–4.1: `nuxt` → `nitropack@2` - Nuxt ≥4.2 stable: `nuxt` → `@nuxt/nitro-server` → `nitropack@2` - Nuxt 5 nightly: `nuxt` → `@nuxt/nitro-server`(-nightly) → `nitro@3` --- packages/nuxt/src/module.ts | 2 +- packages/nuxt/src/vite/utils.ts | 34 ++++-- packages/nuxt/test/vite/nitroVersion.test.ts | 107 +++++++++++++++++++ 3 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 packages/nuxt/test/vite/nitroVersion.test.ts diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 4cf39d16cdbc..458e0dd5dc5f 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -80,7 +80,7 @@ export default defineNuxtModule({ } const serverConfigFile = await findDefaultSdkInitFile('server', nuxt, moduleOptions); - const isNitroV3 = (await getNitroMajorVersion()) >= 3; + const isNitroV3 = (await getNitroMajorVersion(nuxt.options.rootDir)) >= 3; const nuxtMajor = parseInt((nuxt as unknown as { _version: string })._version?.split('.')[0] ?? '3', 10); const isMinNuxtV4 = nuxtMajor >= 4; diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index 58288302ae0d..18782b774af6 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -6,21 +6,37 @@ import type { SentryNuxtModuleOptions } from '../common/types'; import { resolvePath } from '@nuxt/kit'; /** - * Gets the major version of the installed nitro package. - * Returns 2 as the default if nitro is not found or the version cannot be determined. + * Gets the major version of the Nitro package used by the app's Nuxt installation. + * Returns 2 as the default if the version cannot be determined. + * + * Nitro v2 is published as `nitropack`, v3 as `nitro`. Resolving `nitro` directly is + * unreliable: module resolution walks up the directory tree, so in a monorepo an + * unrelated `nitro` v3 above the app wins even when the app's Nuxt uses `nitropack` v2. + * Instead, follow the dependency chain Nuxt itself imports Nitro through: + * `nuxt` -> (`@nuxt/nitro-server` ->) `nitro` | `nitropack`. */ -export async function getNitroMajorVersion(): Promise { +export async function getNitroMajorVersion(rootDir: string): Promise { try { const { getPackageInfo } = await import('local-pkg'); - const info = await getPackageInfo('nitro'); - if (info?.version) { - const major = parseInt(info.version.split('.')[0] ?? '2', 10); - return isNaN(major) ? 2 : major; + + // The package that declares the Nitro dependency: `nuxt` itself, or `@nuxt/nitro-server` (Nuxt >= 3.21) when nuxt delegates to it + let provider = await getPackageInfo('nuxt', { paths: [rootDir] }); + if (provider?.packageJson.dependencies?.['@nuxt/nitro-server']) { + provider = (await getPackageInfo('@nuxt/nitro-server', { paths: [provider.rootPath] })) ?? provider; } + + if (!provider?.packageJson.dependencies?.nitro) { + return 2; + } + + const info = await getPackageInfo('nitro', { paths: [provider.rootPath] }); + const major = parseInt(info?.version?.split('.')[0] ?? '', 10); + // The provider imports `nitro` (not `nitropack`), so it is at least v3 even if the version is unreadable + return isNaN(major) ? 3 : major; } catch { - // If local-pkg is unavailable or nitro is not found, default to v2 + // If local-pkg is unavailable or resolution fails, default to v2 + return 2; } - return 2; } /** diff --git a/packages/nuxt/test/vite/nitroVersion.test.ts b/packages/nuxt/test/vite/nitroVersion.test.ts new file mode 100644 index 000000000000..a9228231d395 --- /dev/null +++ b/packages/nuxt/test/vite/nitroVersion.test.ts @@ -0,0 +1,107 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { getNitroMajorVersion } from '../../src/vite/utils'; + +// Real filesystem fixtures instead of mocks: the bug this guards against lives in +// module resolution walking up the directory tree, which mocks cannot reproduce. +let monorepoRoot: string; + +function writePackage(dir: string, packageJson: Record): void { + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ main: 'index.js', ...packageJson })); + fs.writeFileSync(path.join(dir, 'index.js'), ''); +} + +function createApp(appName: string, packages: Record>): string { + const appDir = path.join(monorepoRoot, 'apps', appName); + fs.mkdirSync(appDir, { recursive: true }); + for (const [name, packageJson] of Object.entries(packages)) { + writePackage(path.join(appDir, 'node_modules', name), packageJson); + } + return appDir; +} + +beforeAll(() => { + monorepoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-nitro-version-')); + // An unrelated Nitro v3 above the apps, like a monorepo root devDependency + writePackage(path.join(monorepoRoot, 'node_modules', 'nitro'), { name: 'nitro', version: '3.0.0-beta.1' }); +}); + +afterAll(() => { + fs.rmSync(monorepoRoot, { recursive: true, force: true }); +}); + +describe('getNitroMajorVersion', () => { + it('detects v2 when nuxt depends on nitropack directly (Nuxt 3 / <=4.1), ignoring a nitro v3 higher up the tree', async () => { + const appDir = createApp('nuxt-4-old', { + nuxt: { name: 'nuxt', version: '4.1.0', dependencies: { nitropack: '^2.12.0' } }, + nitropack: { name: 'nitropack', version: '2.12.0' }, + }); + + await expect(getNitroMajorVersion(appDir)).resolves.toBe(2); + }); + + it('detects v2 through @nuxt/nitro-server when it depends on nitropack (Nuxt >=3.21 stable)', async () => { + const appDir = createApp('nuxt-4-stable', { + nuxt: { name: 'nuxt', version: '4.5.2', dependencies: { '@nuxt/nitro-server': '4.5.2' } }, + '@nuxt/nitro-server': { name: '@nuxt/nitro-server', version: '4.5.2', dependencies: { nitropack: '^2.13.4' } }, + nitropack: { name: 'nitropack', version: '2.13.4' }, + }); + + await expect(getNitroMajorVersion(appDir)).resolves.toBe(2); + }); + + it('detects v3 through @nuxt/nitro-server when it depends on nitro (Nuxt 5)', async () => { + const appDir = createApp('nuxt-5', { + nuxt: { + name: 'nuxt', + version: '5.0.0', + dependencies: { '@nuxt/nitro-server': 'npm:@nuxt/nitro-server-nightly' }, + }, + '@nuxt/nitro-server': { + name: '@nuxt/nitro-server-nightly', + version: '5.0.0-nightly', + dependencies: { nitro: '^3.0.0-beta' }, + }, + nitro: { name: 'nitro', version: '3.0.0-beta.2' }, + }); + + await expect(getNitroMajorVersion(appDir)).resolves.toBe(3); + }); + + it('detects v3 when nuxt depends on nitro directly, without @nuxt/nitro-server', async () => { + const appDir = createApp('nuxt-direct-nitro', { + nuxt: { name: 'nuxt', version: '5.1.0', dependencies: { nitro: '^3.1.0' } }, + nitro: { name: 'nitro', version: '3.1.0' }, + }); + + await expect(getNitroMajorVersion(appDir)).resolves.toBe(3); + }); + + it('falls back to v3 when the declared nitro package has no readable version (stub package)', async () => { + const appDir = createApp('nuxt-5-stub', { + nuxt: { + name: 'nuxt', + version: '5.0.0', + dependencies: { '@nuxt/nitro-server': 'npm:@nuxt/nitro-server-nightly' }, + }, + '@nuxt/nitro-server': { + name: '@nuxt/nitro-server-nightly', + version: '5.0.0-nightly', + dependencies: { nitro: '^3.0.0-beta' }, + }, + nitro: { name: 'nitro' }, + }); + + await expect(getNitroMajorVersion(appDir)).resolves.toBe(3); + }); + + it('defaults to v2 when nuxt cannot be resolved', async () => { + const appDir = path.join(monorepoRoot, 'apps', 'no-nuxt'); + fs.mkdirSync(appDir, { recursive: true }); + + await expect(getNitroMajorVersion(appDir)).resolves.toBe(2); + }); +}); From 15114b05c353a6cadcdb26c3005104dfe82c0503 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:01:44 +0200 Subject: [PATCH 2/2] switch vitest env for Node 18 --- packages/nuxt/test/vite/nitroVersion.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/nuxt/test/vite/nitroVersion.test.ts b/packages/nuxt/test/vite/nitroVersion.test.ts index a9228231d395..6d9f95dbe7a4 100644 --- a/packages/nuxt/test/vite/nitroVersion.test.ts +++ b/packages/nuxt/test/vite/nitroVersion.test.ts @@ -1,3 +1,7 @@ +/** + * @vitest-environment node + * The code is build-time Node code + */ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path';