Skip to content

ksmth/rolldown-cjs-class-field-repro

Repository files navigation

Rolldown CJS class-field lowering bug — minimal repro

[email protected] (which pins [email protected]) miscompiles class fields that live in a CJS-classified source module when build.target is below ES2022. The downleveled _defineProperty(this, …) call cannot resolve its helper at module-init time, throwing:

Uncaught (in promise) TypeError: _defineProperty is not a function
    at new EraParser (assets/index-XXXXXXXX.js:…)

Root cause (what the bundle actually contains)

Inside every CJS-classified module that uses class fields, Rolldown emits:

var require_EraParser = /* @__PURE__ */ __commonJSMin(((exports) => {
    var _defineProperty = (init_defineProperty(), __toCommonJS(defineProperty_exports));
    //                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    // __toCommonJS returns the namespace { __esModule: true, default: <fn> },
    // not the function itself.  The next line throws.
    var EraParser = class extends _Parser.Parser {
        constructor(..._args) {
            super(..._args);
            _defineProperty(this, "priority", 140);   // TypeError here
            
        }
    };
}));

Compare with the helper definition Rolldown emits at the top of the bundle:

var defineProperty_exports = /* @__PURE__ */ __exportAll({ default: () => _defineProperty });
function _defineProperty(e, r, t) {  }

So _defineProperty exists as a real function — but the wrapper in each CJS-classified module re-binds the name to the namespace object instead of to that function. The local _defineProperty shadow is broken.

In an ESM-classified module Rolldown calls the function directly, with no __toCommonJS indirection, and the build works. The bug only fires inside a __commonJSMin wrapper.

Reproduce

npm install
npm run build
npm run preview     # open http://localhost:4173

In the browser console you will see:

TypeError: _defineProperty is not a function
    at new EraParser (assets/index-XXXXXXXX.js:1112:4)

The bundle is unminified (build.minify: false) so the broken expression is plainly visible — see the grep snippet below.

Confirm the exact bug pattern in dist/

# This expression must exist at least once for the bug to fire:
grep -c '(init_defineProperty(), __toCommonJS(defineProperty_exports))' dist/assets/*.js
# In this repro it appears 32 times (once per CJS-wrapped class-field module).

# And the parser sources that triggered it must resolve to .cjs files:
grep -o 'parse/_lib/parsers/[A-Za-z]*\.\(cjs\|js\)' dist/assets/*.js.map | sort -u
# → all results end in `.cjs`

If dist/ lacks the literal (init_defineProperty(), __toCommonJS(defineProperty_exports)) substring, you are not hitting this bug — you are probably looking at rolldown#7843 (an eval-mangler shadow bug fixed in 1.0.0-rc.13), which only manifests when build.minify is on.

Why this minimal repro forces the require condition explicitly

The vite.config.ts in this repo sets:

resolve: { conditions: ["require", "node", "default"] }

[email protected]'s exports map is

"./parse": {
  "require": { "default": "./parse.cjs" },
  "import":  { "default": "./parse.js"  }
}

With Vite's default browser conditions, Rolldown selects the import branch and the .js parsers are bundled as ESM — the bug stays hidden. The repro pins require to deterministically resolve to the .cjs files (which is what real consumers hit when their dep graph reaches date-fns through a transitive CJS package, or when a plugin/preset narrows resolve.conditions).

If you want to demonstrate the bug without changing resolve.conditions, you can also add a local .cjs file with class fields and import it from your entry — Rolldown classifies any .cjs source the same way.

The three "make it stop" knobs

1. Bump build.target to es2022

npx vite build --config vite.config.es2022.ts
grep -c '(init_defineProperty(), __toCommonJS(defineProperty_exports))' dist-es2022/assets/*.js   # → 0
grep -c '_defineProperty(this'                                          dist-es2022/assets/*.js   # → 0

Class fields are emitted natively, so there is nothing to downlevel and the broken helper binding is never generated.

2. Force-ESM resolve via a tiny plugin (target unchanged)

npx vite build --config vite.config.force-esm.ts
grep -c '(init_defineProperty(), __toCommonJS(defineProperty_exports))' dist-force-esm/assets/*.js   # → 0
grep -c '_defineProperty(this'                                          dist-force-esm/assets/*.js   # → 67

vite.config.force-esm.ts rewrites every resolution that lands on a date-fns/**/*.cjs file to its sibling .js. Class fields are still downleveled (we kept target: ['safari13']), but _defineProperty is now hoisted as a plain top-level function and called directly — no CJS wrapper, no __toCommonJS, no bug.

3. Downgrade to vite@7 (Rollup, not Rolldown)

npm i -D vite@7

The Rollup-based build path does not have this transform, so the bug disappears. Useful as a sanity check that the regression is in Rolldown, not in date-fns.

Files

  • package.json — pins [email protected] and [email protected].
  • vite.config.ts — the broken-by-default config (target safari13, minify: false, resolve.conditions: ["require", …]).
  • vite.config.es2022.ts — knob #1.
  • vite.config.force-esm.ts — knob #2.
  • index.html, src/entry.js — minimal entry that imports date-fns/parse and uses the result so it isn't tree-shaken.

Versions tested

package version
vite 8.0.10
rolldown 1.0.0-rc.17
date-fns 4.1.0
@oxc-project/runtime (helpers) 0.127.0

To try a newer rolldown rc:

npm i -D 'https://pkg.pr.new/rolldown@HEAD'
npm run build

About

Minimal repro for Rolldown class-field-in-CJS lowering bug ([email protected] / [email protected])

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors