From 2499913ef84a65b013112a8c9f9d62c3e26aa3a8 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 26 Aug 2026 11:07:51 +0200 Subject: [PATCH] ref(nextjs)!: Move `withSentryConfig` to `@sentry/nextjs/config` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Separate build-time config from the SDK runtime by giving `withSentryConfig` and `SentryBuildOptions` their own entry point, matching the other framework SDKs, which expose their build plugins on `./vite`, `./module` and `./middleware` rather than the main entry. `./config` resolves to CommonJS under both conditions, since `next.config.mjs` is loaded by a plain Node ESM loader and the build-time code resolves webpack loader and template paths with `__dirname`, which is not defined in an ES module. This is safe because the module holds no SDK state. Drop the no-op `withSentryConfig` passthroughs from the client and edge builds: they only existed so the name stayed resolvable when Next compiled a module that imported it for those runtimes, which cannot happen now. Add the `.js` extension to the `next/constants` import so the ESM server build is loadable under a plain Node loader, and guard that with a test — Node does no extension resolution for ESM. This is a prerequisite for splitting the `node` export condition into `import`/`require` (#22791), which is left for a follow-up: serving the ESM server build to Next turns two latent problems into build failures, since turbopack can then statically analyse it. Browser-only names that client components import are genuinely absent from the server build, and `cacheComponents` prerendering rejects the `crypto.randomUUID()` inside `captureException`. Both are masked today by CommonJS being opaque. BREAKING CHANGE: `withSentryConfig` and `SentryBuildOptions` are no longer exported from `@sentry/nextjs`. Import them from `@sentry/nextjs/config`. Refs #22791 Co-Authored-By: Claude Opus 5 (1M context) --- MIGRATION.md | 15 +++ .../nextjs-14/next.config.js | 2 +- .../nextjs-15-basepath/next.config.js | 2 +- .../nextjs-15-intl/next.config.js | 2 +- .../nextjs-15-t3/next.config.js | 2 +- .../nextjs-15/next.config.js | 2 +- .../nextjs-16-bun/next.config.ts | 2 +- .../nextjs-16-cacheComponents/next.config.ts | 2 +- .../nextjs-16-cf-workers/next.config.ts | 2 +- .../next.config.ts | 2 +- .../nextjs-16-streaming/next.config.ts | 2 +- .../nextjs-16-trailing-slash/next.config.mjs | 13 ++ .../nextjs-16-trailing-slash/next.config.ts | 10 -- .../trailing-slash-parameterization.test.ts | 2 +- .../nextjs-16-tunnel/next.config.ts | 2 +- .../nextjs-16-userfeedback/next.config.js | 2 +- .../nextjs-16/next.config.ts | 2 +- .../nextjs-app-dir/next.config.js | 2 +- .../nextjs-orpc/next.config.js | 2 +- .../nextjs-otlp/next.config.ts | 2 +- .../nextjs-pages-dir/next.config.js | 2 +- .../nextjs-sourcemaps/next.config.js | 2 +- .../supabase-nextjs/next.config.js | 2 +- packages/nextjs/package.json | 4 + packages/nextjs/rollup.npm.config.mjs | 2 +- packages/nextjs/src/client/index.ts | 7 -- packages/nextjs/src/common/utils/isBuild.ts | 2 +- packages/nextjs/src/edge/index.ts | 7 -- packages/nextjs/src/index.server.ts | 1 - packages/nextjs/src/index.types.ts | 3 - packages/nextjs/test/configExports.test.ts | 115 ++++++++++++++++++ 31 files changed, 169 insertions(+), 50 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/next.config.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/next.config.ts create mode 100644 packages/nextjs/test/configExports.test.ts diff --git a/MIGRATION.md b/MIGRATION.md index 46d39ebf1085..a8f627e9db15 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1270,6 +1270,21 @@ The legacy per-transaction profiling sampling options were removed. Configure se ### `@sentry/nextjs` +`withSentryConfig` and the `SentryBuildOptions` type moved to the `@sentry/nextjs/config` entry point and are no +longer exported from `@sentry/nextjs`: + +```js +// next.config.mjs + +// before +import { withSentryConfig } from '@sentry/nextjs'; + +// after +import { withSentryConfig } from '@sentry/nextjs/config'; +``` + +The no-op `withSentryConfig` passthroughs that the client and edge builds exported were removed along with it. + The following long-deprecated top-level options in `withSentryConfig` / the `sentry` config were removed. Most of them moved under the `webpack` option in v10; use the replacement listed below instead: diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-14/next.config.js index 1098c2ce5a4f..870b1102aa93 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/next.config.js +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/next.config.js @@ -1,4 +1,4 @@ -const { withSentryConfig } = require('@sentry/nextjs'); +const { withSentryConfig } = require('@sentry/nextjs/config'); /** @type {import('next').NextConfig} */ const nextConfig = {}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/next.config.js index 591aec7c1ce0..2b3ffb9735e2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/next.config.js +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/next.config.js @@ -1,4 +1,4 @@ -const { withSentryConfig } = require('@sentry/nextjs'); +const { withSentryConfig } = require('@sentry/nextjs/config'); /** @type {import('next').NextConfig} */ const nextConfig = { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/next.config.js index edd191e14b38..5de3d1dcd72e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/next.config.js +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/next.config.js @@ -1,4 +1,4 @@ -const { withSentryConfig } = require('@sentry/nextjs'); +const { withSentryConfig } = require('@sentry/nextjs/config'); const createNextIntlPlugin = require('next-intl/plugin'); const withNextIntl = createNextIntlPlugin('./i18n/request.ts'); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/next.config.js index a1ec081d7e72..f08b2288f016 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/next.config.js +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/next.config.js @@ -3,7 +3,7 @@ await import('./src/env.js'); /** @type {import("next").NextConfig} */ const config = {}; -import { withSentryConfig } from '@sentry/nextjs'; +import { withSentryConfig } from '@sentry/nextjs/config'; export default withSentryConfig(config, { webpack: { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-15/next.config.js index 80f077e44167..56ed18eed7eb 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/next.config.js +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/next.config.js @@ -1,4 +1,4 @@ -const { withSentryConfig } = require('@sentry/nextjs'); +const { withSentryConfig } = require('@sentry/nextjs/config'); // Simulate Vercel environment for cron monitoring tests process.env.VERCEL = '1'; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/next.config.ts index 3e7a140e801b..34d17391df28 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/next.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/next.config.ts @@ -1,4 +1,4 @@ -import { withSentryConfig } from '@sentry/nextjs'; +import { withSentryConfig } from '@sentry/nextjs/config'; import type { NextConfig } from 'next'; const nextConfig: NextConfig = {}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/next.config.ts index 2841f1c0c5da..e8f05e7fc695 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/next.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/next.config.ts @@ -1,4 +1,4 @@ -import { withSentryConfig } from '@sentry/nextjs'; +import { withSentryConfig } from '@sentry/nextjs/config'; import type { NextConfig } from 'next'; const nextConfig: NextConfig = { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/next.config.ts index 6699b3dd2c33..28162396e80e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/next.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/next.config.ts @@ -1,4 +1,4 @@ -import { withSentryConfig } from '@sentry/nextjs'; +import { withSentryConfig } from '@sentry/nextjs/config'; import type { NextConfig } from 'next'; const nextConfig: NextConfig = {}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/next.config.ts index 2841f1c0c5da..e8f05e7fc695 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/next.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/next.config.ts @@ -1,4 +1,4 @@ -import { withSentryConfig } from '@sentry/nextjs'; +import { withSentryConfig } from '@sentry/nextjs/config'; import type { NextConfig } from 'next'; const nextConfig: NextConfig = { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/next.config.ts index 8f14934e590c..27ca2d3d6ce6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/next.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/next.config.ts @@ -1,4 +1,4 @@ -import { withSentryConfig } from '@sentry/nextjs'; +import { withSentryConfig } from '@sentry/nextjs/config'; import type { NextConfig } from 'next'; // Simulate Vercel environment for cron monitoring tests diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/next.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/next.config.mjs new file mode 100644 index 000000000000..b1717b717341 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/next.config.mjs @@ -0,0 +1,13 @@ +// Deliberately `.mjs`: Next loads it through Node's own ESM loader rather than compiling it, which is the only +// config format that exercises `@sentry/nextjs/config` as a plain-Node ESM consumer. +// See https://github.com/getsentry/sentry-javascript/issues/22791 +import { withSentryConfig } from '@sentry/nextjs/config'; + +/** @type {import('next').NextConfig} */ +const nextConfig = { + trailingSlash: true, +}; + +export default withSentryConfig(nextConfig, { + silent: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/next.config.ts deleted file mode 100644 index 80946b61ec01..000000000000 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/next.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { withSentryConfig } from '@sentry/nextjs'; -import type { NextConfig } from 'next'; - -const nextConfig: NextConfig = { - trailingSlash: true, -}; - -export default withSentryConfig(nextConfig, { - silent: true, -}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/tests/trailing-slash-parameterization.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/tests/trailing-slash-parameterization.test.ts index a3532c8d08fa..eb47d4881544 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/tests/trailing-slash-parameterization.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/tests/trailing-slash-parameterization.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; // These tests verify that pageload transactions are correctly named when -// trailingSlash: true is enabled in next.config.ts, even when a catch-all +// trailingSlash: true is enabled in next.config.mjs, even when a catch-all // route exists. See: https://github.com/getsentry/sentry-javascript/issues/19241 test('should create a correctly named pageload transaction for a static route', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts index cad68b926a58..ac79f882a572 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts @@ -1,4 +1,4 @@ -import { withSentryConfig } from '@sentry/nextjs'; +import { withSentryConfig } from '@sentry/nextjs/config'; import type { NextConfig } from 'next'; const nextConfig: NextConfig = {}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/next.config.js index 1098c2ce5a4f..870b1102aa93 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/next.config.js +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/next.config.js @@ -1,4 +1,4 @@ -const { withSentryConfig } = require('@sentry/nextjs'); +const { withSentryConfig } = require('@sentry/nextjs/config'); /** @type {import('next').NextConfig} */ const nextConfig = {}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts index b63a26dd6ba1..8f6c5edfbcca 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts @@ -1,4 +1,4 @@ -import { withSentryConfig } from '@sentry/nextjs'; +import { withSentryConfig } from '@sentry/nextjs/config'; import type { NextConfig } from 'next'; // Simulate Vercel environment for cron monitoring tests diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/next.config.js index dce0071a44b9..149a7a773314 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/next.config.js +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/next.config.js @@ -1,4 +1,4 @@ -const { withSentryConfig } = require('@sentry/nextjs'); +const { withSentryConfig } = require('@sentry/nextjs/config'); /** @type {import('next').NextConfig} */ const nextConfig = { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-orpc/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-orpc/next.config.js index 54723b889027..4693824bade6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-orpc/next.config.js +++ b/dev-packages/e2e-tests/test-applications/nextjs-orpc/next.config.js @@ -1,7 +1,7 @@ /** @type {import("next").NextConfig} */ const config = {}; -import { withSentryConfig } from '@sentry/nextjs'; +import { withSentryConfig } from '@sentry/nextjs/config'; export default withSentryConfig(config, { webpack: { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-otlp/next.config.ts index 6699b3dd2c33..28162396e80e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-otlp/next.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/next.config.ts @@ -1,4 +1,4 @@ -import { withSentryConfig } from '@sentry/nextjs'; +import { withSentryConfig } from '@sentry/nextjs/config'; import type { NextConfig } from 'next'; const nextConfig: NextConfig = {}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/next.config.js index ee7efe23508f..df1b0f5f4147 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/next.config.js +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/next.config.js @@ -1,4 +1,4 @@ -const { withSentryConfig } = require('@sentry/nextjs'); +const { withSentryConfig } = require('@sentry/nextjs/config'); /** @type {import('next').NextConfig} */ const nextConfig = { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/next.config.js index 63bb8b443a14..69a0a61a9388 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/next.config.js +++ b/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/next.config.js @@ -1,4 +1,4 @@ -const { withSentryConfig } = require('@sentry/nextjs'); +const { withSentryConfig } = require('@sentry/nextjs/config'); /** @type {import('next').NextConfig} */ const nextConfig = {}; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/next.config.js b/dev-packages/e2e-tests/test-applications/supabase-nextjs/next.config.js index 61672affb524..e8a322596125 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/next.config.js +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/next.config.js @@ -7,7 +7,7 @@ module.exports = nextConfig; // Injected content via Sentry wizard below -const { withSentryConfig } = require('@sentry/nextjs'); +const { withSentryConfig } = require('@sentry/nextjs/config'); module.exports = withSentryConfig(module.exports, { // For all available options, see: diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 69222a2f96c8..3d1e1d11d42a 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -46,6 +46,10 @@ "node": "./build/cjs/index.server.js", "import": "./build/esm/index.server.js" }, + "./config": { + "types": "./build/types/config/index.d.ts", + "default": "./build/cjs/config/index.js" + }, "./async-storage-shim": { "import": { "default": "./build/esm/config/templates/requestAsyncStorageShim.js" diff --git a/packages/nextjs/rollup.npm.config.mjs b/packages/nextjs/rollup.npm.config.mjs index f6f94a3d9883..0b5edc3fa0e3 100644 --- a/packages/nextjs/rollup.npm.config.mjs +++ b/packages/nextjs/rollup.npm.config.mjs @@ -17,7 +17,7 @@ export default [ // prevent this internal nextjs code from ending up in our built package (this doesn't happen automatically because // the name doesn't match an SDK dependency) packageSpecificConfig: { - external: ['next/router', 'next/constants', 'next/headers', 'stacktrace-parser'], + external: ['next/router', 'next/constants.js', 'next/headers', 'stacktrace-parser'], // Next.js and our users are more happy when our client code has the "use client" directive plugins: [ diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index c5de307078c7..5c5d3ffc2c85 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -138,10 +138,3 @@ function getDefaultIntegrations(options: BrowserOptions): Integration[] { return customDefaultIntegrations; } - -/** - * Just a passthrough in case this is imported from the client. - */ -export function withSentryConfig(exportedUserNextConfig: T): T { - return exportedUserNextConfig; -} diff --git a/packages/nextjs/src/common/utils/isBuild.ts b/packages/nextjs/src/common/utils/isBuild.ts index 92b9808f75b9..114c66dc67dd 100644 --- a/packages/nextjs/src/common/utils/isBuild.ts +++ b/packages/nextjs/src/common/utils/isBuild.ts @@ -1,4 +1,4 @@ -import { PHASE_PRODUCTION_BUILD } from 'next/constants'; +import { PHASE_PRODUCTION_BUILD } from 'next/constants.js'; /** * Decide if the currently running process is part of the build phase or happening at runtime. diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 8a5eb7ca8cb8..8dce8ae2f25e 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -203,10 +203,3 @@ export function init(options: VercelEdgeOptions = {}): void { // The statement above can throw because process is not defined on the client } } - -/** - * Just a passthrough in case this is imported from the client. - */ -export function withSentryConfig(exportedUserNextConfig: T): T { - return exportedUserNextConfig; -} diff --git a/packages/nextjs/src/index.server.ts b/packages/nextjs/src/index.server.ts index 133b6ecf1da0..0ce5251aa327 100644 --- a/packages/nextjs/src/index.server.ts +++ b/packages/nextjs/src/index.server.ts @@ -1,2 +1 @@ -export * from './config'; export * from './server'; diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 766eb7837d6d..46b7544ded42 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -11,7 +11,6 @@ import type { ServerComponentContext, VercelCronsConfig } from './common/types'; import type * as edgeSdk from './edge'; import type * as serverSdk from './server'; -export * from './config'; export * from './client'; export * from './server'; export * from './edge'; @@ -52,8 +51,6 @@ export declare const withErrorBoundary: typeof clientSdk.withErrorBoundary; export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; -export { withSentryConfig } from './config'; - /** * Wraps a Next.js Pages Router API route with Sentry error and performance instrumentation. * diff --git a/packages/nextjs/test/configExports.test.ts b/packages/nextjs/test/configExports.test.ts new file mode 100644 index 000000000000..fce97deabf84 --- /dev/null +++ b/packages/nextjs/test/configExports.test.ts @@ -0,0 +1,115 @@ +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { dirname, resolve } from 'node:path'; +import { init, parse } from 'cjs-module-lexer'; +import { beforeAll, describe, expect, it } from 'vitest'; + +const nodeRequire = createRequire(import.meta.url); +const packageExports = (nodeRequire('../package.json') as { exports: Record }).exports; + +/** + * `next.config.mjs` is loaded by a plain Node ESM loader, so build-time config code has to work there. The ESM + * variant of it does not: `build/esm/config/**` relies on `__dirname` to resolve loader and template paths, which + * is a `ReferenceError` in an ES module. So `./config` deliberately serves the CJS build to ESM importers too, + * rather than splitting `import`/`require` like the runtime entries do. + * + * There is no dual-package hazard here because this code runs at build time only and holds no SDK state. + * + * Separating the two entry points is also what unblocks serving real ESM to Node consumers later — see + * https://github.com/getsentry/sentry-javascript/issues/22791 + */ +describe('`./config` subpath export', () => { + const configExport = packageExports['./config']; + + it('resolves to the CJS build for every condition', () => { + expect(configExport).toEqual({ + types: './build/types/config/index.d.ts', + default: './build/cjs/config/index.js', + }); + }); + + it('never points a condition at the ESM config build', () => { + expect(JSON.stringify(configExport)).not.toContain('build/esm'); + }); +}); + +/** + * ESM consumers of a CJS file only get the named exports `cjs-module-lexer` can see statically — anything it misses + * links as `undefined`. So `withSentryConfig` has to stay statically detectable for `import { withSentryConfig } from + * '@sentry/nextjs/config'` to work. + * + * Exercises the generated artifact, so it needs the package built. + */ +describe('`./config` static exports (generated)', () => { + let staticExports: string[]; + + beforeAll(async () => { + await init(); + staticExports = parse(readFileSync(resolve(__dirname, '../build/cjs/config/index.js'), 'utf8')).exports; + }); + + it('statically exports `withSentryConfig`', () => { + expect(staticExports).toContain('withSentryConfig'); + }); +}); + +/** + * The `node` condition can only ever serve ESM if the server runtime entry stays free of build-time config code, so + * guard that here even though `node` still resolves to CommonJS today. A plain `__dirname` reference is the tripwire: + * it survives bundling, only throws once the enclosing function runs (so merely importing the entry would not catch + * it), and every known offender in `src/config` uses one. + * + * Exercises the generated artifact, so it needs the package built. + */ +describe('ESM server build is loadable under a plain Node loader (generated)', () => { + const entry = resolve(__dirname, '../build/esm/index.server.js'); + + /** Every file reachable from `entry` via relative specifiers — i.e. this package's own ESM output. */ + function collectModuleGraph(from: string, seen = new Set()): Set { + if (seen.has(from)) { + return seen; + } + seen.add(from); + + const source = readFileSync(from, 'utf8'); + for (const [, specifier] of source.matchAll(/from\s*'(\.[^']+)'|import\s*'(\.[^']+)'/g)) { + if (specifier) { + collectModuleGraph(resolve(dirname(from), specifier), seen); + } + } + + return seen; + } + + it('reaches no module that references `__dirname`', () => { + const offenders = [...collectModuleGraph(entry)].filter(file => + /(^|[^.\w])__dirname([^\w]|$)/.test(readFileSync(file, 'utf8')), + ); + + expect(offenders).toEqual([]); + }); + + // Catches what the `__dirname` scan cannot: extensionless bare specifiers and require-cycles, which fail at link + // time rather than when some function runs. Needs a real Node loader, hence the child process. + it('imports cleanly, with the full namespace and without `withSentryConfig`', () => { + const script = ` + import * as Sentry from ${JSON.stringify(entry)}; + + const missing = ['init', 'captureException', 'captureMessage', 'setTag', 'addBreadcrumb', 'isEnabled'] + .filter(name => typeof Sentry[name] !== 'function'); + if (missing.length) { + throw new Error('missing exports: ' + missing.join(', ')); + } + + // Build-time only — it lives on \`@sentry/nextjs/config\` and must stay out of the runtime graph. + if ('withSentryConfig' in Sentry) { + throw new Error('the runtime entry still exports withSentryConfig'); + } + `; + + expect(() => + execFileSync(process.execPath, ['--input-type=module', '-e', script], { stdio: 'pipe' }), + ).not.toThrow(); + }); +});