Skip to content

Commit fc00ed6

Browse files
authored
fix(nuxt): Detect Nitro version via the app's Nuxt dependency chain (#24019)
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`
1 parent ccce5f8 commit fc00ed6

3 files changed

Lines changed: 133 additions & 10 deletions

File tree

packages/nuxt/src/module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export default defineNuxtModule<ModuleOptions>({
9494
}
9595

9696
const serverConfigFile = await findDefaultSdkInitFile('server', nuxt, moduleOptions);
97-
const isNitroV3 = (await getNitroMajorVersion()) >= 3;
97+
const isNitroV3 = (await getNitroMajorVersion(nuxt.options.rootDir)) >= 3;
9898
const nuxtMajor = parseInt((nuxt as unknown as { _version: string })._version?.split('.')[0] ?? '3', 10);
9999
const isMinNuxtV4 = nuxtMajor >= 4;
100100

packages/nuxt/src/vite/utils.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,37 @@ import type { SentryNuxtModuleOptions } from '../common/types';
77
import { resolvePath } from '@nuxt/kit';
88

99
/**
10-
* Gets the major version of the installed nitro package.
11-
* Returns 2 as the default if nitro is not found or the version cannot be determined.
10+
* Gets the major version of the Nitro package used by the app's Nuxt installation.
11+
* Returns 2 as the default if the version cannot be determined.
12+
*
13+
* Nitro v2 is published as `nitropack`, v3 as `nitro`. Resolving `nitro` directly is
14+
* unreliable: module resolution walks up the directory tree, so in a monorepo an
15+
* unrelated `nitro` v3 above the app wins even when the app's Nuxt uses `nitropack` v2.
16+
* Instead, follow the dependency chain Nuxt itself imports Nitro through:
17+
* `nuxt` -> (`@nuxt/nitro-server` ->) `nitro` | `nitropack`.
1218
*/
13-
export async function getNitroMajorVersion(): Promise<number> {
19+
export async function getNitroMajorVersion(rootDir: string): Promise<number> {
1420
try {
1521
const { getPackageInfo } = await import('local-pkg');
16-
const info = await getPackageInfo('nitro');
17-
if (info?.version) {
18-
const major = parseInt(info.version.split('.')[0] ?? '2', 10);
19-
return isNaN(major) ? 2 : major;
22+
23+
// The package that declares the Nitro dependency: `nuxt` itself, or `@nuxt/nitro-server` (Nuxt >= 3.21) when nuxt delegates to it
24+
let provider = await getPackageInfo('nuxt', { paths: [rootDir] });
25+
if (provider?.packageJson.dependencies?.['@nuxt/nitro-server']) {
26+
provider = (await getPackageInfo('@nuxt/nitro-server', { paths: [provider.rootPath] })) ?? provider;
2027
}
28+
29+
if (!provider?.packageJson.dependencies?.nitro) {
30+
return 2;
31+
}
32+
33+
const info = await getPackageInfo('nitro', { paths: [provider.rootPath] });
34+
const major = parseInt(info?.version?.split('.')[0] ?? '', 10);
35+
// The provider imports `nitro` (not `nitropack`), so it is at least v3 even if the version is unreadable
36+
return isNaN(major) ? 3 : major;
2137
} catch {
22-
// If local-pkg is unavailable or nitro is not found, default to v2
38+
// If local-pkg is unavailable or resolution fails, default to v2
39+
return 2;
2340
}
24-
return 2;
2541
}
2642

2743
/**
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import * as fs from 'fs';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
5+
import { getNitroMajorVersion } from '../../src/vite/utils';
6+
7+
// Real filesystem fixtures instead of mocks: the bug this guards against lives in
8+
// module resolution walking up the directory tree, which mocks cannot reproduce.
9+
let monorepoRoot: string;
10+
11+
function writePackage(dir: string, packageJson: Record<string, unknown>): void {
12+
fs.mkdirSync(dir, { recursive: true });
13+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ main: 'index.js', ...packageJson }));
14+
fs.writeFileSync(path.join(dir, 'index.js'), '');
15+
}
16+
17+
function createApp(appName: string, packages: Record<string, Record<string, unknown>>): string {
18+
const appDir = path.join(monorepoRoot, 'apps', appName);
19+
fs.mkdirSync(appDir, { recursive: true });
20+
for (const [name, packageJson] of Object.entries(packages)) {
21+
writePackage(path.join(appDir, 'node_modules', name), packageJson);
22+
}
23+
return appDir;
24+
}
25+
26+
beforeAll(() => {
27+
monorepoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-nitro-version-'));
28+
// An unrelated Nitro v3 above the apps, like a monorepo root devDependency
29+
writePackage(path.join(monorepoRoot, 'node_modules', 'nitro'), { name: 'nitro', version: '3.0.0-beta.1' });
30+
});
31+
32+
afterAll(() => {
33+
fs.rmSync(monorepoRoot, { recursive: true, force: true });
34+
});
35+
36+
describe('getNitroMajorVersion', () => {
37+
it('detects v2 when nuxt depends on nitropack directly (Nuxt 3 / <=4.1), ignoring a nitro v3 higher up the tree', async () => {
38+
const appDir = createApp('nuxt-4-old', {
39+
nuxt: { name: 'nuxt', version: '4.1.0', dependencies: { nitropack: '^2.12.0' } },
40+
nitropack: { name: 'nitropack', version: '2.12.0' },
41+
});
42+
43+
await expect(getNitroMajorVersion(appDir)).resolves.toBe(2);
44+
});
45+
46+
it('detects v2 through @nuxt/nitro-server when it depends on nitropack (Nuxt >=3.21 stable)', async () => {
47+
const appDir = createApp('nuxt-4-stable', {
48+
nuxt: { name: 'nuxt', version: '4.5.2', dependencies: { '@nuxt/nitro-server': '4.5.2' } },
49+
'@nuxt/nitro-server': { name: '@nuxt/nitro-server', version: '4.5.2', dependencies: { nitropack: '^2.13.4' } },
50+
nitropack: { name: 'nitropack', version: '2.13.4' },
51+
});
52+
53+
await expect(getNitroMajorVersion(appDir)).resolves.toBe(2);
54+
});
55+
56+
it('detects v3 through @nuxt/nitro-server when it depends on nitro (Nuxt 5)', async () => {
57+
const appDir = createApp('nuxt-5', {
58+
nuxt: {
59+
name: 'nuxt',
60+
version: '5.0.0',
61+
dependencies: { '@nuxt/nitro-server': 'npm:@nuxt/nitro-server-nightly' },
62+
},
63+
'@nuxt/nitro-server': {
64+
name: '@nuxt/nitro-server-nightly',
65+
version: '5.0.0-nightly',
66+
dependencies: { nitro: '^3.0.0-beta' },
67+
},
68+
nitro: { name: 'nitro', version: '3.0.0-beta.2' },
69+
});
70+
71+
await expect(getNitroMajorVersion(appDir)).resolves.toBe(3);
72+
});
73+
74+
it('detects v3 when nuxt depends on nitro directly, without @nuxt/nitro-server', async () => {
75+
const appDir = createApp('nuxt-direct-nitro', {
76+
nuxt: { name: 'nuxt', version: '5.1.0', dependencies: { nitro: '^3.1.0' } },
77+
nitro: { name: 'nitro', version: '3.1.0' },
78+
});
79+
80+
await expect(getNitroMajorVersion(appDir)).resolves.toBe(3);
81+
});
82+
83+
it('falls back to v3 when the declared nitro package has no readable version (stub package)', async () => {
84+
const appDir = createApp('nuxt-5-stub', {
85+
nuxt: {
86+
name: 'nuxt',
87+
version: '5.0.0',
88+
dependencies: { '@nuxt/nitro-server': 'npm:@nuxt/nitro-server-nightly' },
89+
},
90+
'@nuxt/nitro-server': {
91+
name: '@nuxt/nitro-server-nightly',
92+
version: '5.0.0-nightly',
93+
dependencies: { nitro: '^3.0.0-beta' },
94+
},
95+
nitro: { name: 'nitro' },
96+
});
97+
98+
await expect(getNitroMajorVersion(appDir)).resolves.toBe(3);
99+
});
100+
101+
it('defaults to v2 when nuxt cannot be resolved', async () => {
102+
const appDir = path.join(monorepoRoot, 'apps', 'no-nuxt');
103+
fs.mkdirSync(appDir, { recursive: true });
104+
105+
await expect(getNitroMajorVersion(appDir)).resolves.toBe(2);
106+
});
107+
});

0 commit comments

Comments
 (0)