-
-
Notifications
You must be signed in to change notification settings - Fork 799
Description
Environment
| Package | Version |
|---|---|
| nitro | 3.0.1-20260227-142232-5ccf672a (nitro-nightly) |
| vite | 8.0.0 |
| rolldown | 1.0.0-rc.9 |
| nf3 | 0.3.10 |
| @tanstack/react-start | 1.166.11 |
| @aws-sdk/client-s3 | 3.1009.0 |
| bun | 1.3.10 |
| node | 24.12.0 |
| OS | macOS (darwin arm64) |
Reproduction
git clone git@github.com:FixMyBerlin/_reproduction-tanstack-start-nitro-esm-error.git
cd _reproduction-tanstack-start-nitro-esm-error
bun install
bun run build
# Verify the bug exists in the built output:
cd .output/server
bun -e "import('./_libs/@aws-crypto/crc32+[...].mjs').then(() => console.log('OK')).catch(e => console.error(e.message, '\n', e.stack))"
# Or start the server and visit http://localhost:3000 — the page shows "Something went wrong!":
cd ../..
bun .output/server/index.mjsExpected: Page renders S3Client loaded: function
Actual: Runtime crash from the server-bundled @aws-crypto/crc32 chunk:
TypeError: Cannot destructure property '__extends' from null or undefined value
at .output/server/_libs/@aws-crypto/crc32+[...].mjs:560
Describe the bug
What happens
Nitro's server build (via Rolldown) generates incorrect CJS-to-ESM interop for
modules whose CommonJS exports object sets __esModule: true without
explicitly providing exports.default.
The most common trigger is tslib, which is a UMD/CJS module that sets
Object.defineProperty(exports, "__esModule", { value: true }) but has no
exports.default. It is a transitive dependency of @aws-crypto/* (used by
@aws-sdk/client-s3). (tslib source)
Root cause
Rolldown's __toESM helper has this logic
(runtime helper behavior, helper source):
var __toESM = (mod, isNodeMode, target) => (
target = mod != null ? __create(__getProtoOf(mod)) : {},
__copyProps(
isNodeMode || !mod || !mod.__esModule
? __defProp(target, "default", { value: mod, enumerable: true })
: target,
mod
)
);When isNodeMode is true (or when __esModule is absent), __toESM always
creates a synthetic .default property pointing to the original module. But when
isNodeMode is falsy and __esModule is true, it assumes the module
already provides its own .default — which tslib does not.
Rolldown sets isNodeMode = 1 when platform: "node" is configured
(platform option, input options). Nitro's
server build never sets platform: "node" in its Rolldown config, so
isNodeMode is always undefined. This causes __toESM to skip the synthetic
.default for tslib, but the generated code expects it:
// Generated in .output/server/_libs/@aws-crypto/crc32+[...].mjs:
var { __extends, __assign, ... } = (/* @__PURE__ */ __toESM(require_tslib())).default;
// ^^^^^^^^ undefined!Where the bug is
In Nitro's Vite plugin, getBundlerConfig() builds the rolldownConfig object but never
includes platform: "node". Since the server build targets Node.js/Bun (not the
browser), it should set platform: "node".
The relevant code path is in src/build/vite/bundler.ts (emitted as dist/vite.mjs):
// getBundlerConfig() — rolldown branch (lines ~47–65)
const rolldownConfig: RolldownConfig = defu(
{ transform: { inject: ... }, output: { codeSplitting: ... } },
nitro.options.rolldownConfig,
nitro.options.rollupConfig,
commonConfig
);
// ↑ No `platform: "node"` anywhere in this chainNote on nf3 0.3.11+ masking the bug
In nf3@0.3.11, tslib was added to the NonBundleablePackages list, so
Nitro's dep-tracing auto-externalizes it. This prevents the bug from manifesting
for tslib specifically, but the root cause remains: any other CJS module
with __esModule: true and no .default will still trigger the same crash.
To reproduce with the latest nf3, set noExternals: true in the Nitro config
(as this repo does), or pin nf3@0.3.10.
Additional context
-
Workaround: Externalize the affected packages in
vite.config.ts:nitro({ preset: 'bun', rolldownConfig: { external: ['@aws-sdk/client-s3', /^@aws-crypto\//, /^@smithy\//], }, })
-
Expected fix: Nitro should pass
platform: "node"in its Rolldown config
for server builds. This makes__toESMgenerate__toESM(x, 1)which always
adds the synthetic.default, matching Node.js/Bun CJS interop semantics. -
Rolldown itself behaves correctly — it respects
platform: "node"when told.
The issue is that Nitro doesn't pass this option. -
The
platform: "node"setting is correct even when targeting Bun, as Bun
implements the Node.js module system and CJS interop.
This workaround, reproduction and issue description is generated with the help of Opus 4.6 Thinking in Cursor.
Logs
TypeError: Cannot destructure property '__extends' from null or undefined value
at .output/server/_libs/@aws-crypto/crc32+[...].mjs:560:552
at moduleEvaluation (native:1:11)
at requestImportModule (native:2)