Skip to content

fix(json): preserve .default access on JSON default imports#9568

Merged
graphite-app[bot] merged 1 commit into
mainfrom
05-26-fix_9566
May 28, 2026
Merged

fix(json): preserve .default access on JSON default imports#9568
graphite-app[bot] merged 1 commit into
mainfrom
05-26-fix_9566

Conversation

@IWANABETHATGUY

@IWANABETHATGUY IWANABETHATGUY commented May 26, 2026

Copy link
Copy Markdown
Member

Summary

Closes #9566.

For import data from './x.json', rolldown treats the default-imported binding as a namespace so accesses like data.foo can be optimized to the underlying hoisted key symbol. The shortcut is wrong for data.default: every ESM module's export table maps "default" to the symbol that holds the default export value (the whole JSON object), which is the same symbol data already binds. The shortcut therefore resolves data.default back to data and silently drops the .default access — so data.default.value was being emitted as <whole-object>.value and evaluating to undefined (esbuild emits <whole-object>.default.value).

The fix gates the is_json_import_ns shortcut at setup: when the first property is "default", skip the optimization entirely. JSON resolution is single-level (after cursor == 0 the loop re-derives is_namespace_ref from namespace_object_ref == canonical_ref, ignoring is_json_import_ns), so the check only needs to run once at the top. With no resolved_member_expr_refs entry inserted, the finalizer rewrites the data identifier through the normal path and keeps .default.<rest> as a literal property access on the inlined JSON object.

Tree-shaking of the literal "default" JSON key through this path was never working anyway — json_object_expr_to_esm overwrites named_exports["default"] so it points at default_export_ref (to keep ns.default === data spec-correct for namespace imports), which means the shortcut could only ever resolve to the whole object. So this fix corrects the output without regressing tree-shaking.

Test plan

  • New fixture crates/rolldown/tests/rolldown/issues/9566/ with a runtime assert.strictEqual(data.default.value, 1) — fails before the fix (undefined !== 1), passes after.
  • just dt on the new fixture emits assert.strictEqual({ "default": { "value": 1 } }.default.value, 1); (matches esbuild).
  • Existing JSON regression tests still pass unchanged: crates/rolldown/tests/rolldown/issues/9484, 9484_namespace_default_write, 9484_computed_write.
  • just test-rust — 1726 passing; the single failure (test262::test262_module_code) is a pre-existing meta-test about KNOWN_FAILURES referencing files missing from the test262 submodule, unrelated to this change.

Copy link
Copy Markdown
Member Author

How to use the Graphite Merge Queue

Add the label graphite: merge-when-ready to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@netlify

netlify Bot commented May 26, 2026

Copy link
Copy Markdown

Deploy Preview for rolldown-rs canceled.

Name Link
🔨 Latest commit 5a7a0f8
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/6a17f897878b120008670d2f

@IWANABETHATGUY IWANABETHATGUY changed the title fix: 9566 fix(json): preserve .default access on JSON default imports May 26, 2026
@IWANABETHATGUY IWANABETHATGUY force-pushed the 05-26-fix_9566 branch 3 times, most recently from c6588d5 to 77cdc0d Compare May 28, 2026 06:19
@IWANABETHATGUY IWANABETHATGUY marked this pull request as ready for review May 28, 2026 06:33
@codspeed-hq

codspeed-hq Bot commented May 28, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 4 untouched benchmarks
⏩ 10 skipped benchmarks1


Comparing 05-26-fix_9566 (9808886) with main (990c1de)2

Open in CodSpeed

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (e246cd2) during the generation of this report, so 990c1de was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

IWANABETHATGUY commented May 28, 2026

Copy link
Copy Markdown
Member Author

Merge activity

  • May 28, 8:09 AM UTC: The merge label 'graphite: merge-when-ready' was detected. This PR will be added to the Graphite merge queue once it meets the requirements.
  • May 28, 8:09 AM UTC: IWANABETHATGUY added this pull request to the Graphite merge queue.
  • May 28, 8:15 AM UTC: Merged by the Graphite merge queue.

## Summary

Closes #9566.

For `import data from './x.json'`, rolldown treats the default-imported binding as a namespace so accesses like `data.foo` can be optimized to the underlying hoisted key symbol. The shortcut is wrong for `data.default`: every ESM module's export table maps `"default"` to the symbol that holds the default export value (the whole JSON object), which is the same symbol `data` already binds. The shortcut therefore resolves `data.default` back to `data` and silently drops the `.default` access — so `data.default.value` was being emitted as `<whole-object>.value` and evaluating to `undefined` (esbuild emits `<whole-object>.default.value`).

The fix gates the `is_json_import_ns` shortcut at setup: when the first property is `"default"`, skip the optimization entirely. JSON resolution is single-level (after `cursor == 0` the loop re-derives `is_namespace_ref` from `namespace_object_ref == canonical_ref`, ignoring `is_json_import_ns`), so the check only needs to run once at the top. With no `resolved_member_expr_refs` entry inserted, the finalizer rewrites the `data` identifier through the normal path and keeps `.default.<rest>` as a literal property access on the inlined JSON object.

Tree-shaking of the literal `"default"` JSON key through this path was never working anyway — `json_object_expr_to_esm` overwrites `named_exports["default"]` so it points at `default_export_ref` (to keep `ns.default === data` spec-correct for namespace imports), which means the shortcut could only ever resolve to the whole object. So this fix corrects the output without regressing tree-shaking.

## Test plan

- [x] New fixture `crates/rolldown/tests/rolldown/issues/9566/` with a runtime `assert.strictEqual(data.default.value, 1)` — fails before the fix (`undefined !== 1`), passes after.
- [x] `just dt` on the new fixture emits `assert.strictEqual({ "default": { "value": 1 } }.default.value, 1);` (matches esbuild).
- [x] Existing JSON regression tests still pass unchanged: `crates/rolldown/tests/rolldown/issues/9484`, `9484_namespace_default_write`, `9484_computed_write`.
- [x] `just test-rust` — 1726 passing; the single failure (`test262::test262_module_code`) is a pre-existing meta-test about `KNOWN_FAILURES` referencing files missing from the test262 submodule, unrelated to this change.
@graphite-app graphite-app Bot merged commit 5a7a0f8 into main May 28, 2026
34 checks passed
@graphite-app graphite-app Bot deleted the 05-26-fix_9566 branch May 28, 2026 08:15
@rolldown-guard rolldown-guard Bot mentioned this pull request Jun 3, 2026
shulaoda added a commit that referenced this pull request Jun 3, 2026
> [!IMPORTANT]
> **This is a minor release.** Two changes alter default behavior compared to `1.0.3`. Please read this section before upgrading. Everything else is additive (new features, fixes, deps).

## ⚠️ Notable behavior changes

### 1. `experimental.lazyBarrel` is now enabled by default (#9632)

**What changed.** `experimental.lazyBarrel` now defaults to `true`. When a barrel module is recognized as side-effect-free, Rolldown skips compiling the re-exported modules that are never actually used.

**Impact.** For codebases with large barrel files (component libraries such as Ant Design, `@mui/icons-material`, etc.) this is a meaningful build-time speedup, and for the vast majority of projects the emitted output is unchanged. In rare cases where a barrel is *incorrectly* treated as side-effect-free, the optimization could drop a module that was being relied on for its side effects.

**How to opt out (backward compatible).**

```js
// rolldown.config.js
export default {
  experimental: { lazyBarrel: false },
}
```

> Note: this opt-out flag is planned to be removed in a future release. If you have a case where you must turn it off, please open an issue so we can fix the underlying detection instead.

---

### 2. `tsconfig` project-reference resolution now aligns with TypeScript

Upgrading `oxc_resolver` (`11.19.1` → `11.20.0` in #9549, then `→ 11.21.0` in #9634) changes how a *solution-style* `tsconfig.json` (one that only lists `references` and delegates the real settings to `tsconfig.app.json` / `tsconfig.node.json`, as Vite scaffolds) is resolved, bringing it **in line with how TypeScript (`tsc`) itself behaves**:

- **Reference match priority** (oxc-resolver [#1151](oxc-project/oxc-resolver#1151)): when the root has `references`, a referenced project that includes the file now **takes precedence over the root**, instead of the root matching it first (this is what TypeScript already does). So that project's `compilerOptions.paths` now apply.
- **`allowJs`** (oxc-resolver [#1198](oxc-project/oxc-resolver#1198)): whether a `.js`/`.jsx`/`.mjs`/`.cjs` file is included is now decided by **each referenced project's own** `allowJs`, not the root's (again matching TypeScript). So `tsconfig.app.json` with `allowJs: true` + `paths` now resolves aliases for `.js` files even when the root doesn't set `allowJs`.

For most projects this is a fix (the standard Vite `paths` aliases now resolve, closes #8468), but it **is** a behavior change if you relied on the previous behavior, where the root's `paths` / `allowJs` took precedence.

**If you relied on the old "root wins" behavior.** There is no exact toggle back, because the old behavior was the bug being fixed. The recommended path is to align your config with TypeScript: declare the `paths` / `allowJs` on the referenced project that actually owns the files.

If you must keep the old precedence while still using `references`: a referenced project's match wins, and **the first matching `references` entry takes priority** (the root is only a fallback when no reference claims the file). So extract the old root settings into their own config and list it **first**:

```jsonc
// tsconfig.json (solution root)
{
  "files": [],
  "references": [
    { "path": "./tsconfig.base.json" }, // old root paths/allowJs — listed first, so it wins
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}
```

`tsconfig.base.json` should carry the `paths` you previously declared on the root, plus `allowJs: true` if it needs to claim `.js` files (the extension is checked against each config's own `allowJs`). With no `include`, it defaults to `**/*` under its directory and claims every file first.

Alternatively, bypass reference resolution entirely by pointing the top-level `tsconfig` option at a single config: `export default { tsconfig: './tsconfig.app.json' }`.

---

## [1.1.0] - 2026-06-03

### 🚀 Features

- enable `experimental.lazyBarrel` by default (#9632) by @shulaoda
- `import.meta.glob` support `caseSensitive` option (#9594) by @btea
- add `SOURCEMAP_BROKEN` warning for renderChunk hook (#9601) by @sapphi-red
- add `SOURCEMAP_BROKEN` warning for transform hook (#9600) by @sapphi-red
- add `@__NO_SIDE_EFFECTS__` hint for invalid `@__PURE__` before function declarations (#9505) by @Copilot
- code-splitting: support group-local `includeDependenciesRecursively` (#9587) by @hyf0

### 🐛 Bug Fixes

- report TSCONFIG_ERROR instead of UNHANDLEABLE_ERROR for a missing tsconfig file (#9633) by @shulaoda
- browser: add missing exports and ensure consistency with `rolldown` package (#9629) by @sapphi-red
- should build test-dev-server when test-node (#9610) by @situ2001
- chunk-optimizer: refuse asymmetric merge for cyclic dynamic entries (#9320) (#9322) by @aminpaks
- dev: handle the remaining errors in dev (#9570) by @h-a-n-a
- handle slash-normalized ids with preserveModulesRoot (#9595) by @IWANABETHATGUY
- json: preserve .default access on JSON default imports (#9568) by @IWANABETHATGUY
- testing: remove unintended trigger_full_build from test harness (#9573) by @hyf0

### 🚜 Refactor

- js-regex: use regress native replace/replace_all (#9607) by @IWANABETHATGUY
- remove never-constructed `ImportStatus` variants (#9606) by @Boshen

### 📚 Documentation

- clarify that `RolldownBuild::close` method should be called in most cases (#9619) by @sapphi-red

### ⚡ Performance

- avoid unnecessary intermediate sourcemaps (#9599) by @sapphi-red

### 🧪 Testing

- add unit test for collapsing module sourcemap (#9626) by @sapphi-red
- cover vite-alias regex capture-group expansion (#9602) (#9608) by @IWANABETHATGUY

### ⚙️ Miscellaneous Tasks

- deps: update oxc_resolver to 11.21.0 (#9634) by @shulaoda
- update invalid option diagnostic link to point to Rolldown docs (#9631) by @sapphi-red
- deps: update vite+ to v0.1.24 (#9628) by @renovate[bot]
- deps: update oxc resolver to v11.20.0 (#9549) by @renovate[bot]
- deps: update dependency vite-plus to v0.1.24 (#9470) by @renovate[bot]
- deps: update npm packages (#9614) by @renovate[bot]
- deps: upgrade oxc to 0.134.0 (#9625) by @shulaoda
- deps: update crate-ci/typos action to v1.47.0 (#9620) by @renovate[bot]
- deps: update rollup submodule for tests to v4.61.0 (#9623) by @rolldown-guard[bot]
- deps: update github actions (#9613) by @renovate[bot]
- deps: update pnpm to v11.4.0 (#9616) by @renovate[bot]
- deps: update rust crates (#9615) by @renovate[bot]
- deps: update test262 submodule for tests (#9624) by @rolldown-guard[bot]
- deps: update dependency @napi-rs/cli to v3.7.0 (#9588) by @renovate[bot]
- deps: update dependency rust to v1.96.0 (#9596) by @renovate[bot]
- re-enable WASI testing with proper infrastructure (#9397) by @Boshen

### ❤️ New Contributors

* @aminpaks made their first contribution in [#9322](#9322)

Co-authored-by: shulaoda <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: default export of json files are mapped to the default property rather than the whole json

3 participants