nitrojs / nitrojs/nitro

Nitro server build missing `platform: "node"` → CJS `__esModule` interop crash

Open Beginner friendly
#4,113 10 comments 8 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug tanstack v3
Dominant language
TypeScript
Stars
11.2k
Forks
899
Avg merge
2d 24m
Merged PRs (30d)
40

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.mjs

Expected: 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 chain
Note 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 __toESM generate __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)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/build/vite/bundler.ts, especially getBundlerConfig() and its Rolldown branch, then run the reproduction's bun install and bun run build commands. Verify the generated server chunk with the provided Bun import check and confirm that the server renders S3Client loaded: function without the interop crash.

Written by the indexing model from the issue text.

Assessment

Tech stack
bun, typescript, vite
Domain
backend, build-system
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.