fix(sourcemap): preserve unmapped boundaries during composition#10254
Conversation
How to use the Graphite Merge QueueAdd 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. |
✅ Deploy Preview for rolldown-rs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
b3038d4 to
d862c77
Compare
cf12a71 to
2d55e36
Compare
d862c77 to
c931d1e
Compare
64b4cb3 to
f3b2bcb
Compare
c931d1e to
584ebca
Compare
Merging this PR will not alter performance
Comparing Footnotes
|
@rolldown/browser
@rolldown/debug
rolldown
@rolldown/binding-android-arm64
@rolldown/binding-darwin-arm64
@rolldown/binding-darwin-x64
@rolldown/binding-freebsd-x64
@rolldown/binding-linux-arm-gnueabihf
@rolldown/binding-linux-arm64-gnu
@rolldown/binding-linux-arm64-musl
@rolldown/binding-linux-ppc64-gnu
@rolldown/binding-linux-s390x-gnu
@rolldown/binding-linux-x64-gnu
@rolldown/binding-linux-x64-musl
@rolldown/binding-openharmony-arm64
@rolldown/binding-wasm32-wasi
@rolldown/binding-win32-arm64-msvc
@rolldown/binding-win32-x64-msvc
commit: |
|
If applicable, would you add a test case that covers #6399 case? |
f3b2bcb to
c81e977
Compare
584ebca to
e0bd819
Compare
c81e977 to
3600bc0
Compare
e0bd819 to
84f1aa1
Compare
Merge activity
|
refs #10070. ## The behavior to review This PR makes one choice: when a coarse outer map points to a column before the first mapping on the same line of a more detailed inner map, use that first same-line mapping instead of dropping the generated position. Rollup makes the same choice when tracing segments. The fallback never crosses a line. ## Concrete before and after ### User input `main.js` is the source written by the user: ```js export function app() { globalThis.side = 1; return 42; } ``` ### Generated output Rolldown produces this chunk. Region comments and the `sourceMappingURL` line are omitted here, but the code shape is otherwise the fixture's real output: ```js function app() { globalThis.side = 1; return 42; } export { app }; ``` The generated JavaScript is identical before and after this PR. Only its source map changes. ### Before this PR Asking the composed map where the two generated statements came from returns no original position: ```text generated 3:1 globalThis.side = 1 ──X──> null generated 4:1 return 42 ──X──> null ``` In a source-map visualizer, these generated statements have no colored counterpart in the Original pane. A debugger or stack-trace consumer cannot take these positions back to `main.js`. ### After this PR The same generated positions resolve to the corresponding user-written statements: ```text Generated output User input: main.js 3:1 globalThis.side = 1 ─────────────────> 2:2 globalThis.side = 1 4:1 return 42 ─────────────────> 3:2 return 42 ``` Source-map consumer lines are one-based and columns are zero-based. These are the actual positions asserted by the integration fixture. In a visualizer, each generated statement now shares a color with its corresponding statement in the Original pane. Selecting either side can identify the other side. Debuggers and stack traces can return to the correct original line; the column is the best same-line position available from the coarse map. ## Why the lookup changes The map chain contains two different levels of detail: ```text detailed map: intermediate column 2 -> user source coarse map: final column 0 -> intermediate column 0 ``` Before: ```text final column 0 -> intermediate column 0 -> detailed map has no mapping at or before column 0 -> drop the generated position ``` After: ```text final column 0 -> intermediate column 0 -> detailed map's first mapping on this line is column 2 -> use column 2 and preserve the generated position ``` Lookups between or after existing mappings still use the nearest mapping at or before the requested column. Missing and empty lines still fail. One-field explicitly unmapped segments are skipped in this PR, matching Rollup; #10254 separately preserves those boundaries. #10074 remains necessary because it makes Rolldown-owned mutation maps more precise. This PR covers coarse external maps and other cases where the producer did not provide matching columns. ## Why the snapshots change The snapshot visualizer writes mappings as: ```text (original line:column) "original text" --> (generated line:column) "generated text" ``` Unlike the source-map consumer positions above, these snapshot coordinates are zero-based for both lines and columns. The three snapshots only add mappings; no existing mapping is removed or changed. | Snapshot | Newly restored generated mapping | Previous first mapping on that generated line | Original position used | | --- | --- | --- | --- | | `misc/wrapped_esm` | `60:1 "("`, `60:2 "{"` | `60:3 "e} = "` | `foo.js 4:4` | | `misc/wrapped_esm` | `61:1 "("`, `61:2 "{"` | `61:3 "h: "` | `foo.js 7:2` | | `misc/wrapped_esm` | `73:1 "("`, `73:2 "{"` | `73:3 "destructuring} = "` | `foo.js 30:6` | | `wrapped_esm_default_function` | `19:0 "init_foo("` | `19:9 ");"` | `bar.js 0:23` | | `wrapped_esm_export_named_function` | `18:0 "init_foo("` | `18:9 ");"` | `bar.js 0:27` | There are eight added rows in total: six in `misc/wrapped_esm`, one in each deconflict snapshot. Every addition follows the behavior under review: ```text new generated column < previous first mapped column on that line new original position = previous first mapping's original position ``` For example: ```diff +(0:23) ";\n" --> (19:0) "init_foo(" (0:23) ";\n" --> (19:9) ");\n" ``` The coarse map requested generated column 0, while the detailed map's first mapping was at column 9. The fallback associates column 0 with the same original position already used at column 9. It does not move or replace the existing mapping. ## Suggested review order 1. Review the lookup change and `source_id` guards in `crates/rolldown_sourcemap/src/lib.rs`. 2. Review the Rust test where a column-0 coarse token meets a column-2 detailed token. 3. Review the `renderChunk` fixture, which exercises the public plugin path and asserts the exact positions shown above. 4. Check the snapshot table against the eight added snapshot rows. ## User impact - generated JavaScript, runtime behavior, and bundle size do not change - users of source maps regain original locations for lines previously dropped during composition - the change matters when a plugin or another transform provides a less column-detailed map than Rolldown's preceding map - already resolvable mappings are unchanged ## Validation - `cargo test -p rolldown_sourcemap` - `just test-rust` - `just test-node-rolldown-only fixtures-concurrent.test.ts -t plugin/render-chunk/coarse-sourcemap-composition` - `cargo clippy -p rolldown_sourcemap --all-targets -- --deny warnings`
3600bc0 to
2e29bfc
Compare
## [1.2.0] - 2026-07-15 ### 🚀 Features - dev: skip shipping factories for newly imported top-level modules (#10223) by @h-a-n-a - dev: per-client ship map for HMR patch sizing (#10208) by @h-a-n-a - dev: client-side HMR (#10164) by @h-a-n-a - dev: send a full-reload update to clients when a tsconfig changes (#10262) by @shulaoda - treat `import.meta['url']` and `import.meta['ROLLUP_FILE_URL_*']` as side-effect free (#10267) by @sapphi-red - rewrite `import.meta['url']` (#10251) by @sapphi-red - add `FILE_NOT_FOUND` error (#10220) by @sapphi-red - treat `import.meta.ROLLUP_FILE_URL_*` as side-effect free (#10217) by @sapphi-red ### 🐛 Bug Fixes - sourcemap: preserve unmapped boundaries during composition (#10254) by @hyfdev - `[format]` in `*FileNames` option for ESM format should be `es` instead of `esm` (#10214) by @sapphi-red - sourcemap: preserve coarse mappings during composition (#10249) by @hyfdev - rolldown_plugin_vite_import_glob: support tsconfig paths with `import.meta.glob` (#10167) by @sapphi-red - dev: clear tsconfig caches for bare full builds (#10276) by @shulaoda - dev: force a full rebuild when a tsconfig changes (#10261) by @shulaoda - treat rooted drive-less module ids as absolute in preserveModules naming (#10235) by @IWANABETHATGUY - watch: rebuild when tsconfig files change (#10258) by @shulaoda - watch: drop tsconfig-merged transform options on each rebuild (#10257) by @shulaoda - incorrect `EMPTY_IMPORT_META` warning for `import.meta.ROLLUP_FILE_URL_*` for CJS output (#10221) by @sapphi-red - deconflict: rename CJS locals shadowing wrapped-ESM namespace objects (#9970) by @IWANABETHATGUY - rolldown: drop the unused runtime module after entry-level external flattening (#10237) by @IWANABETHATGUY - rolldown: re-propagate has_dynamic_exports to transitive star importers (#10239) by @IWANABETHATGUY - tree-shaking: tree-shake destructured dynamic import namespace bindings (#10213) by @logaretm - s390x: use json-escape-simd 3.1.1 for big-endian JSON escaping fix (#10211) by @satyamg1620 ### 🚜 Refactor - dev: move full-reload to client side (#10207) by @h-a-n-a - readability follow-ups to the ReplaceWith migration (#10286) by @IWANABETHATGUY - replace take_in-then-write-back with ReplaceWith and by-value moves (#10285) by @Boshen - share the main resolver's cache with the transformer's tsconfig lookups (#10205) by @shulaoda - rolldown: extract the ns star-external __reExport emission rule into LinkingMetadata (#10238) by @IWANABETHATGUY - rolldown: unify link/generate diagnostics into a Diagnostics accumulator (#10234) by @IWANABETHATGUY - sourcemap_filenames: drop dead sourcemap-filename plumbing (#10189) by @IWANABETHATGUY - extract external import symbol merging into a method (#10224) by @IWANABETHATGUY - rolldown: skip CJS namespace merging under strict execution order (#10203) by @hyfdev - resolve the manual tsconfig per file instead of once at startup (#10200) by @shulaoda - rolldown: route interop ESM init emission through a shared init-target view (#10202) by @hyfdev - rolldown: collapse vestigial wrap-kind state and share chunk sort helper (#10201) by @hyfdev ### 📚 Documentation - show plugin kinds in JSDoc and each hook's description (#10218) by @sapphi-red - add an explanation about removing imports from external modules without any messages (#10215) by @sapphi-red ### ⚡ Performance - sourcemap: owned merge in SourceJoiner::join (4005->5 allocs/chunk) (#10250) by @Boshen - avoid redundant sourcemap string copies in collapse and minify paths (#10093) by @Boshen ### 🧪 Testing - code-splitting: establish strict-order review baselines (#10287) by @hyfdev - dev: add hot API test cases (#10181) by @h-a-n-a - code-splitting: normalize strict execution order variants (#10277) by @hyfdev - code-splitting: harden strict execution order coverage (#10252) by @hyfdev - code-splitting: add strict execution order regressions (#10253) by @hyfdev ### ⚙️ Miscellaneous Tasks - deps: update github actions (#10241) by @renovate[bot] - deps: update oxc to 0.140.0 (#10274) by @shulaoda - update Yunfei's GitHub username (#10275) by @hyfdev - deps: update napi (#10260) by @renovate[bot] - deps: update test262 submodule for tests (#10266) by @rolldown-guard[bot] - deps: update dependency vite-plus to v0.2.4 (#10256) by @renovate[bot] - deps: update napi (#10240) by @renovate[bot] - deps: update oxc resolver to v11.24.2 (#10245) by @renovate[bot] - deps: update rust crates (#10244) by @renovate[bot] - disable Renovate updates for idna_adapter (#10248) by @shulaoda - deps: update oxc resolver to v11.24.1 (#10232) by @renovate[bot] - deps: update rust crate oxc_sourcemap to v8.1.1 (#10233) by @renovate[bot] - deps: update dependency rolldown-plugin-dts to ^0.27.0 (#10206) by @renovate[bot] - deps: upgrade sugar_path to v3 (#10230) by @hyfdev - add `dist-*` to `.gitignore` in sourcemap-filenames/hash-final-content fixture (#10216) by @sapphi-red - deps: update dependency rust to v1.97.0 (#10209) by @renovate[bot] ### ❤️ New Contributors * @satyamg1620 made their first contribution in [#10211](#10211) Co-authored-by: shulaoda <[email protected]>
…10368) ### Description The `node-test-windows` job has been failing on every `main` run since 2026-07-15 (for example [this run](https://github.com/rolldown/rolldown/actions/runs/29795703407/job/88527096422)). The two fixtures added in #10249 and #10254 guard their `load` and `transform` hooks with `id.endsWith('/main.js')`. On Windows, module ids use backslashes, so the guard never matches and the hooks are silently skipped. With the hooks skipped, `coarse-sourcemap-composition-issue-6399` bundles the on-disk comment-only `main.js` into an empty chunk whose `map` is `null`, so `new TraceMap(null)` throws `Cannot read properties of null (reading '_decodedMemo')`. In `explicit-unmapped-boundary`, the transform never injects `injected()`, so `code.indexOf('injected()')` returns -1 and the assertion fails. This PR drops the leading slash so the checks become `id.endsWith('main.js')`, which is the pattern every other fixture in the repo already uses and works with both path separators. No production code is touched.

Why
This PR is stacked on #10249.
A source-map segment changes the mapping state from its generated column until the next segment. It is not just a label for one character. A segment with a source starts a mapped range; a one-field segment with only a generated column starts an explicitly unmapped range.
For example, the transform fixture returns
AAAA,Kfor this generated line:The intended ranges are therefore:
What a source-map visualizer shows
The blocks below represent the colored spans that a visualizer associates with the original source.
█is mapped and░is uncolored or explicitly unmapped.Before this PR, composition dropped the one-field segment at column 5:
With no boundary after column 0, the previous mapping remains active to the end of the line.
injected()is colored as if it came fromfoo(); asking a source-map consumer for the original position ofinjected()returns the earliermain.jsmapping.After this PR, composition preserves the boundary:
The generated
foo()remains associated with the original source, while the semicolon andinjected()have no original position. A visualizer leaves that range uncolored, and a debugger or other source-map consumer no longer attributes it tofoo().The same rule applies when the unmapped boundary appears in an intermediate map instead of the final map.
Rollup currently drops these one-field segments while collapsing maps. This PR deliberately differs from that behavior so an explicit unmapped range remains explicit after composition.
What changed
AAAA,KValidation
cargo test -p rolldown_sourcemapjust test-rustjust test-node-rolldown-only fixtures-concurrent.test.ts -t plugin/transform/explicit-unmapped-boundarycargo clippy -p rolldown_sourcemap --all-targets -- --deny warnings