getsentry / getsentry/sentry-javascript
@sentry/nextjs: "node" export condition is a bare CJS path with no import/require split, so ESM consumers get a partial namespace
- 主要言語
- TypeScript
- スター
- 8.7k
- フォーク
- 1.8k
- 平均マージ
- 1日 17時間
- マージ済み PR(30日)
- 515
説明
**Package:** `@sentry/nextjs@10.29.0`
**Node:** v22.18.0
### Summary
`exports["."]["node"]` is a bare string pointing at the CJS build. Every other platform condition in the same map splits `import`/`require`; `node` does not. Because `node` is active for any Node process and is declared *before* `import`, any ESM consumer under a plain-Node loader resolves `build/cjs/index.server.js` rather than `build/esm/index.server.js` — and `cjs-module-lexer` cannot see the bindings that arrive via `export *` from `@sentry/core` / `@sentry/node`, so the resulting namespace is partial.
### The manifest
Read directly from the installed package:
```jsonc
"exports": {
".": {
"types": "./build/types/index.types.d.ts",
"edge": { "import": "./build/esm/edge/index.js", "require": "./build/cjs/edge/index.js" },
"edge-light": { "import": "./build/esm/edge/index.js", "require": "./build/cjs/edge/index.js" },
"worker": { "import": "./build/esm/edge/index.js", "require": "./build/cjs/edge/index.js" },
"workerd": { "import": "./build/esm/edge/index.js", "require": "./build/cjs/edge/index.js" },
"browser": { "import": "./build/esm/index.client.js", "require": "./build/cjs/index.client.js" },
"node": "./build/cjs/index.server.js", // <-- bare string, CJS only
"import": "./build/esm/index.server.js" // <-- unreachable for Node consumers
}
}
```
`browser` and all four edge/worker conditions split `import`/`require`. `node` is the only platform condition that doesn't — which makes the `"import"` entry below it effectively dead code for Node, since `node` always matches first.
### Reproduction
From any project with the package installed — no other setup:
```bash
$ node --input-type=module -e "console.log(import.meta.resolve('@sentry/nextjs'))"
file:///.../@sentry/nextjs/build/cjs/index.server.js # CJS, not ESM
$ node --input-type=module -e "import { addBreadcrumb } from '@sentry/nextjs'"
SyntaxError: Named export 'addBreadcrumb' not found. The requested module
'@sentry/nextjs' is a CommonJS module, which may not support all
module.exports as named exports.
$ node --input-type=module -e "import * as S from '@sentry/nextjs'; console.log(typeof S.init, typeof S.captureException, typeof S.isEnabled)"
function undefined undefined
```
### Impact
Named imports fail loudly at link time, which is the safe case.
**Namespace imports are the dangerous one.** They link cleanly and the missing bindings are `undefined`, so the failure surfaces as `TypeError: X is not a function` at call time, arbitrarily far from the import. The detected/undetected split follows nothing a consumer can predict:
| present | `undefined` |
| -- | -- |
| `init`, `startSpan`, `captureRequestError`, `withSentryConfig` | `captureException`, `captureMessage`, `setTag`, `setContext`, `addBreadcrumb`, `isEnabled`, `isInitialized`, `consoleLoggingIntegration` |
`captureException` is a particularly unhappy landing spot, since it is usually called from inside a `catch` block — so the resulting `TypeError` is frequently swallowed by the very error handler that invoked it, and the error is silently never reported.
This does not affect apps built by Next itself (webpack/turbopack prefer the `import` condition), nor Vite-based loaders. It affects plain-Node ESM tooling: scripts, CLIs, health-check probes and test harnesses that import the SDK outside the bundler.
A default import (`import S from '@sentry/nextjs'`) recovers the full server-build surface, because it reads the runtime `module.exports` object rather than the lexer's static guess. That is a viable workaround, but consumers have to know to apply it, and it is easily "tidied" back into a namespace import by a later reader.
### Suggested fix
Give `node` the same `import`/`require` split its sibling conditions already have:
```jsonc
"node": {
"import": "./build/esm/index.server.js",
"require": "./build/cjs/index.server.js"
}
```
Backwards-compatible for CJS consumers (`require` still resolves the CJS build), and it makes the existing `"import"` entry reachable for ESM ones.
### Possibly related
getsentry/sentry-javascript#20038 — `workerd`/`worker` conditions resolving to `@sentry/node`, breaking Cloudflare Workers. Same class (an export condition resolving to the wrong build), different condition.
### If this is intentional
If `node` deliberately pins CJS — for instance because OpenTelemetry instrumentation patching relies on `require` semantics — that would be very useful to have stated, since it makes the default-import workaround permanent rather than transitional. Given `browser` does split, the asymmetry currently reads as an oversight rather than a decision.
コントリビューションガイド
調査の方向性
Start by locating the @sentry/nextjs package export manifest and inspect the node and import conditions shown in the issue. Run the provided node --input-type=module reproductions to confirm the current resolution and namespace behavior. Done means Node ESM resolves the ESM server build while CommonJS require continues resolving the CJS build, with coverage for both paths if the package has manifest tests.
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- nextjs, nodejs, typescript
- 領域
- developer-experience, tooling
- issue の種類
- バグ
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 活発さ
- 静か
- 明瞭さ
- 明確に書かれている
- 初心者へのやさしさ
- 72/100