Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default defineNuxtModule<ModuleOptions>({
}

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;

Expand Down
34 changes: 25 additions & 9 deletions packages/nuxt/src/vite/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,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<number> {
export async function getNitroMajorVersion(rootDir: string): Promise<number> {
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) {
Comment thread
sentry[bot] marked this conversation as resolved.
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;
}

/**
Expand Down
107 changes: 107 additions & 0 deletions packages/nuxt/test/vite/nitroVersion.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>): 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, Record<string, unknown>>): 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);
});
});
Loading