-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix(nextjs): Split node export condition into import/require
#22793
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import { debug, escapeStringForRegex, loadModule, parseSemver } from '@sentry/co | |
| import * as fs from 'fs'; | ||
| import { createRequire } from 'module'; | ||
| import * as path from 'path'; | ||
| import { fileURLToPath } from 'url'; | ||
| import type { VercelCronsConfig } from '../common/types'; | ||
| import { externalizeOrchestrionRuntimePackages } from './diagnosticsChannelInjection'; | ||
| import { getBuildPluginOptions, normalizePathForGlob } from './getBuildPluginOptions'; | ||
|
|
@@ -27,6 +28,13 @@ import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion | |
| import { getNextjsVersion, getPackageModules } from './util'; | ||
| import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils'; | ||
|
|
||
| // The ESM build has no `__dirname`; derive it from `import.meta.url` in that case, | ||
| // following the `@sentry/bundler-plugins` pattern (Rollup transpiles `import.meta` | ||
| // for the CJS build, and the `typeof __dirname` branch keeps CJS working regardless). | ||
| // eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
| // @ts-ignore Rollup transpiles import.meta for the CJS build | ||
| const _dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| // Next.js runs webpack 3 times, once for the client, the server, and for edge. Because we don't want to print certain | ||
| // warnings 3 times, we keep track of them here. | ||
| let showedMissingGlobalErrorWarningMsg = false; | ||
|
|
@@ -238,7 +246,7 @@ export function constructWebpackConfigFunction({ | |
| test: isPageResource, | ||
| use: [ | ||
| { | ||
| loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), | ||
| loader: path.resolve(_dirname, 'loaders', 'wrappingLoader.js'), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ESM loaders crash on
|
||
| options: { | ||
| ...staticWrappingLoaderOptions, | ||
| wrappingTargetKind: 'page', | ||
|
|
@@ -252,7 +260,7 @@ export function constructWebpackConfigFunction({ | |
| test: isApiRouteResource, | ||
| use: [ | ||
| { | ||
| loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), | ||
| loader: path.resolve(_dirname, 'loaders', 'wrappingLoader.js'), | ||
| options: { | ||
| ...staticWrappingLoaderOptions, | ||
| vercelCronsConfig: vercelCronsConfigForWrapper, | ||
|
|
@@ -269,7 +277,7 @@ export function constructWebpackConfigFunction({ | |
| test: isMiddlewareResource, | ||
| use: [ | ||
| { | ||
| loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), | ||
| loader: path.resolve(_dirname, 'loaders', 'wrappingLoader.js'), | ||
| options: { | ||
| ...staticWrappingLoaderOptions, | ||
| wrappingTargetKind: 'middleware', | ||
|
|
@@ -286,7 +294,7 @@ export function constructWebpackConfigFunction({ | |
| test: isServerComponentResource, | ||
| use: [ | ||
| { | ||
| loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), | ||
| loader: path.resolve(_dirname, 'loaders', 'wrappingLoader.js'), | ||
| options: { | ||
| ...staticWrappingLoaderOptions, | ||
| wrappingTargetKind: 'server-component', | ||
|
|
@@ -300,7 +308,7 @@ export function constructWebpackConfigFunction({ | |
| test: isRouteHandlerResource, | ||
| use: [ | ||
| { | ||
| loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), | ||
| loader: path.resolve(_dirname, 'loaders', 'wrappingLoader.js'), | ||
| options: { | ||
| ...staticWrappingLoaderOptions, | ||
| wrappingTargetKind: 'route-handler', | ||
|
|
@@ -764,7 +772,7 @@ function addValueInjectionLoader({ | |
| test: /(src[\\/])?instrumentation.(js|ts)/, | ||
| use: [ | ||
| { | ||
| loader: path.resolve(__dirname, 'loaders/valueInjectionLoader.js'), | ||
| loader: path.resolve(_dirname, 'loaders/valueInjectionLoader.js'), | ||
| options: { | ||
| values: serverValues, | ||
| }, | ||
|
|
@@ -776,7 +784,7 @@ function addValueInjectionLoader({ | |
| test: /(?:sentry\.client\.config\.(jsx?|tsx?)|(?:src[\\/])?instrumentation-client\.(js|ts))$/, | ||
| use: [ | ||
| { | ||
| loader: path.resolve(__dirname, 'loaders/valueInjectionLoader.js'), | ||
| loader: path.resolve(_dirname, 'loaders/valueInjectionLoader.js'), | ||
| options: { | ||
| values: clientValues, | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import { existsSync, readFileSync } from 'node:fs'; | ||
| import { dirname, resolve } from 'node:path'; | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| /** | ||
| * The `node` export condition must offer an `import`/`require` split like its sibling conditions. When it was a bare | ||
| * string pointing at the CJS server build, every ESM consumer under a plain Node.js loader received the CJS build | ||
| * (`node` matches before the top-level `import` condition), so `cjs-module-lexer` could not see the bindings | ||
| * re-exported from `@sentry/core` / `@sentry/node`: named imports failed to link and namespace imports contained | ||
| * silently-undefined members. | ||
| * | ||
| * Regression test for https://github.com/getsentry/sentry-javascript/issues/22791 | ||
| */ | ||
| describe('package.json exports map', () => { | ||
| const packageRoot = resolve(__dirname, '..'); | ||
| const packageJson = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8')) as { | ||
| exports: Record<string, Record<string, unknown>>; | ||
| }; | ||
|
|
||
| const nodeCondition = packageJson.exports['.']?.['node'] as Record<string, string> | undefined; | ||
|
|
||
| it('has an `import`/`require` split under the `node` condition of the root export', () => { | ||
| expect(nodeCondition).toBeInstanceOf(Object); | ||
| expect(typeof nodeCondition?.import).toBe('string'); | ||
| expect(typeof nodeCondition?.require).toBe('string'); | ||
| }); | ||
|
|
||
| it('points the `node.import` condition at an existing file in the ESM build output', () => { | ||
| expect(nodeCondition?.import).toMatch(/\/esm\//); | ||
| expect(existsSync(resolve(packageRoot, nodeCondition?.import as string))).toBe(true); | ||
| }); | ||
|
|
||
| it('points the `node.require` condition at an existing file in the CJS build output', () => { | ||
| expect(nodeCondition?.require).toMatch(/\/cjs\//); | ||
| expect(existsSync(resolve(packageRoot, nodeCondition?.require as string))).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| /** | ||
| * `next` does not declare an `exports` map, so Node's ESM resolver requires explicit file extensions for deep imports | ||
| * like `next/constants.js`. Extensionless specifiers work in webpack/turbopack but throw `ERR_MODULE_NOT_FOUND` under | ||
| * a plain Node.js loader, which would make the ESM server build (reachable via `node.import`) unloadable. | ||
| * | ||
| * Regression test for https://github.com/getsentry/sentry-javascript/issues/22791 | ||
| */ | ||
| describe('ESM server build is loadable by plain Node.js', () => { | ||
| it('uses explicit file extensions for all `next/*` deep imports in the ESM server module graph', () => { | ||
| const packageRoot = resolve(__dirname, '..'); | ||
| const entry = resolve(packageRoot, 'build/esm/index.server.js'); | ||
| expect(existsSync(entry)).toBe(true); | ||
|
|
||
| const importSpecifierRegex = /(?:from|import)\s*['"]([^'"]+)['"]/g; | ||
| const visited = new Set<string>(); | ||
| const queue = [entry]; | ||
| const extensionlessNextImports: string[] = []; | ||
|
|
||
| while (queue.length > 0) { | ||
| const file = queue.pop() as string; | ||
| if (visited.has(file) || !existsSync(file)) { | ||
| continue; | ||
| } | ||
| visited.add(file); | ||
|
|
||
| const source = readFileSync(file, 'utf8'); | ||
| for (const match of source.matchAll(importSpecifierRegex)) { | ||
| const specifier = match[1] as string; | ||
| if (specifier.startsWith('.')) { | ||
| const resolved = resolve(dirname(file), specifier); | ||
| queue.push(resolved.endsWith('.js') ? resolved : `${resolved}.js`); | ||
| } else if (/^next\/.+/.test(specifier) && !/\.[cm]?js$/.test(specifier)) { | ||
| extensionlessNextImports.push(`${specifier} (in ${file.replace(packageRoot, '')})`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| expect(visited.size).toBeGreaterThan(1); | ||
| expect(extensionlessNextImports).toEqual([]); | ||
| }); | ||
| it('has no unguarded __dirname in the ESM config build graph', () => { | ||
| // `__dirname` does not exist in ESM. The config code derives a module dirname | ||
| // from `import.meta.url` and may only reference `__dirname` behind a | ||
| // `typeof __dirname` guard (the CJS fast path). | ||
| const packageRoot = resolve(__dirname, '..'); | ||
| const entry = resolve(packageRoot, 'build', 'esm', 'config', 'index.js'); | ||
| expect(existsSync(entry)).toBe(true); | ||
|
|
||
| const importSpecifierRegex = /(?:from|import)\s*['"]([^'"]+)['"]/g; | ||
| const visited = new Set<string>(); | ||
| const queue = [entry]; | ||
| const unguardedDirnameUses: string[] = []; | ||
|
|
||
| while (queue.length > 0) { | ||
| const file = queue.pop() as string; | ||
| if (visited.has(file) || !existsSync(file)) { | ||
| continue; | ||
| } | ||
| visited.add(file); | ||
|
|
||
| const source = readFileSync(file, 'utf8'); | ||
| for (const line of source.split('\n')) { | ||
| if (line.includes('__dirname') && !line.includes('typeof __dirname')) { | ||
| unguardedDirnameUses.push(`${line.trim().slice(0, 80)} (in ${file.replace(packageRoot, '')})`); | ||
| } | ||
| } | ||
| for (const match of source.matchAll(importSpecifierRegex)) { | ||
| const specifier = match[1] as string; | ||
| if (specifier.startsWith('.')) { | ||
| const resolved = resolve(dirname(file), specifier); | ||
| queue.push(resolved.endsWith('.js') ? resolved : `${resolved}.js`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| expect(visited.size).toBeGreaterThan(1); | ||
| expect(unguardedDirnameUses).toEqual([]); | ||
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dirname test skips loadersMedium Severity The new unguarded- Triggered by project rule: PR Review Guidelines for Cursor Bot Reviewed by Cursor Bugbot for commit 33840f5. Configure here. |
||
| }); | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The import path for
next/constants.jsinisBuild.tsdoes not match the Rollup external configuration, which still lists'next/constants', causing a build failure.Severity: HIGH
Suggested Fix
Update the
externalarray inpackages/nextjs/rollup.npm.config.mjs. Change the entry from'next/constants'to'next/constants.js'to match the new import path used in the source code. This will ensure Rollup correctly identifies the module as an external dependency during the build process.Prompt for AI Agent
Did we get this right? 👍 / 👎 to inform future reviews.