Skip to content

feat: emit the source map scopes field - #275

Merged
alexander-akait merged 8 commits into
mainfrom
feat/source-map-scopes
Sep 26, 2026
Merged

alexander-akait merged 8 commits into
mainfrom
feat/source-map-scopes

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Sep 8, 2026 •

Copy link
Copy Markdown
Member

Summary

A bundler rewrites an imported binding into a namespace read, and a source map only maps positions, so a debugger cannot resolve the name the source actually used. This adds map({ scopes: true }), emitting the ECMA-426 Scopes field: a source declares what its names read through a new third OriginalSource argument, the chunk stream carries that beside the source content, and the field is built while the mappings are written rather than decoded back out of them. Needed by webpack/webpack#21999, which otherwise has to carry its own VLQ codec and re-parse every asset's mappings.

It also fixes a pre-existing cache bug it would otherwise trip over: CachedSource's fast-path key inspected only columns, so any two option sets differing elsewhere collided — with scopes that meant a map built without the field could be served to a caller asking for it, and vice versa.

What kind of change does this PR introduce?

feat.

Did you add tests for your changes?

Yes — test/scopes.js: the option is opt-in, bindings survive ConcatSource, ReplaceSource and a primed CachedSource, neither cache order leaks or drops the field, and the streaming encoder is asserted byte-identical to decoding a finished map.

Does this PR introduce a breaking change?

No. OnSource gains an optional fourth argument and MapOptions an optional scopes flag; without it nothing is collected or emitted, and the existing 89,895 tests and 1,373 snapshots pass unchanged.

If relevant, what needs to be documented once your changes are merged or what have you already documented in this PR?

map({ scopes: true }), the OriginalSource third constructor argument, and the util.scopes helpers.

Use of AI

Yes. Claude Code was used to move the encoder out of webpack, wire the option through the streaming path, and write the tests. The design — a plain boolean option with the bindings riding on the sources, rather than a callback that a cache key cannot see — was mine, and the CachedSource bugs above were found by testing that decision rather than assumed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C5WxsJzknrdkVzHV3rL1Uv


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Source maps can now include encoded scope information when requested with the scopes option.
    • OriginalSource accepts optional scope bindings, which are preserved through composed and replaced sources.
    • Scope helpers are available through the public utilities export.
  • Documentation
    • Documented the scopes option, scope bindings, and when scope data appears in generated maps.

@changeset-bot

changeset-bot Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 306c28f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack-sources Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov

codecov Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.01%. Comparing base (82a4bf0) to head (306c28f).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #275      +/-   ##
==========================================
+ Coverage   98.91%   99.01%   +0.09%     
==========================================
  Files          26       27       +1     
  Lines        5708     6266     +558     
  Branches      741      791      +50     
==========================================
+ Hits         5646     6204     +558     
  Misses         60       60              
  Partials        2        2              
Flag Coverage Δ
integration 99.01% <100.00%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed

codspeed Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 16 benchmarks

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 24 improved benchmarks
❌ 16 regressed benchmarks
✅ 172 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Memory original-source memory: map({ columns: false }) line-only mappings 107.2 KB 313.4 KB -65.8%
❌ Memory clear-cache memory: shared modules (no visited set — allocates per chunk) 20.9 KB 57.5 KB -63.63%
❌ Memory cached-source memory: warm sourceAndMap() returns cached references 209 B 465 B -55.05%
❌ Memory replace-source memory: map({ columns: true }) splices mappings 1.5 MB 2.4 MB -35.8%
❌ Simulation compat-source: CompatSource.from(SourceLike) 173.5 µs 227 µs -23.58%
❌ Memory clear-cache memory: unique tasks (clearCache default) 3.7 MB 4.9 MB -23.13%
❌ Memory clear-cache memory: unique tasks (no clearCache) 3.7 MB 4.5 MB -18.18%
❌ Memory source-map-source memory: map({ columns: true }) 1.3 KB 1.5 KB -16.33%
❌ Simulation concat-source: getChildren() 300.2 µs 351.2 µs -14.53%
❌ Memory compat-source memory: CompatSource.from() short-circuits on Source 784 B 912 B -14.04%
❌ Simulation concat-source: add() x50 308.5 µs 355.4 µs -13.2%
❌ Memory prefix-source memory: new PrefixSource() 2 KB 2.3 KB -12.16%
❌ Memory concat-source memory: map({ columns: true }) composes child maps 2.5 MB 2.8 MB -11.43%
❌ Memory source-map-source memory: new SourceMapSource(with inner map) 1.3 KB 1.4 KB -10.87%
❌ Simulation concat-source: addAllSkipOptimizing() 280.8 µs 313.2 µs -10.35%
❌ Simulation concat-source: source() (10 raw) 323.3 µs 359.9 µs -10.16%
⚡ Memory replace-source memory: source() concatenates result 70.2 KB 13.2 KB ×5.3
⚡ Memory replace-source memory: construct + 100 insertions 136.6 KB 36.5 KB ×3.7
⚡ Memory concat-source memory: new ConcatSource(...children) 6.6 KB 4.2 KB +57.73%
⚡ Simulation helpers/stringBufferUtils: internString (enabled) 196.5 µs 138.9 µs +41.44%
... ... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing feat/source-map-scopes (306c28f) with main (82a4bf0)

Open in CodSpeed

alexander-akait and others added 7 commits September 26, 2026 11:35
A bundler rewrites an imported binding into a namespace read, and a
source map only maps positions, so a debugger cannot resolve the name
the source used. The Scopes proposal answers that by naming, per scope,
the expression each variable evaluates to.

Add map({ scopes: true }). A source declares what its names read through
a new OriginalSource argument, the chunk stream carries it beside the
source content, and the field is built while the mappings are written
rather than decoded back out of them.
Requiring it from the two map assembly helpers put the encoder and its
lookup table in the module graph of every caller, which the memory
benchmarks measured as a constant cost on sources that never emit the
field.
The streaming writer reaches none of it, so a map handed in from
elsewhere went untested: the early returns, a segment naming no source
or one the map lacks, a source resumed after another, a name the map
already carries, and a value too large for one base64 digit.
The cases required the helper module directly, so the getter that
exposes it on `util` was never run.
Also choose the mapping callback once instead of testing for the scopes
writer per segment. Interleaved against main on one machine, that test
cost OriginalSource.map({ columns: true }) a reproducible 1-3%; with the
callback chosen up front the same measurement is neutral.
`tsc` emits `@private` as a TypeScript `private` member, which makes
classes from two installed copies incompatible; `main` dropped these tags
when it moved to generated types (#291). Regenerate `types/` for the
scopes changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01ThsbPWvW5vnYWEaFs2Q22F
`main` runs the tests with `node:test` (#292); convert `expect` to
`assert` so the file runs under `node --test` and the Jest adapter.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01ThsbPWvW5vnYWEaFs2Q22F
@linux-foundation-easycla

linux-foundation-easycla Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

CLA Not Signed

One or more co-authors of this pull request were not found. You must specify co-authors in commit message trailer via:

Co-authored-by: name <email>

Supported Co-authored-by: formats include:

  1. Anything <[email protected]> - it will locate your GitHub user by id part.
  2. Anything <[email protected]> - it will locate your GitHub user by login part.
  3. Anything <public-email> - it will locate your GitHub user by public-email part. Note that this email must be made public on Github.
  4. Anything <other-email> - it will locate your GitHub user by other-email part but only if that email was used before for any other CLA as a main commit author.
  5. login <any-valid-email> - it will locate your GitHub user by login part, note that login part must be at least 3 characters long.

Alternatively, if the co-author should not be included, remove the Co-authored-by: line from the commit message.

Please update your commit message(s) by doing git commit --amend and then git push [--force] and then request re-running CLA check via commenting on this pull request:

/easycla

@coderabbitai

coderabbitai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

Next included review available in 18 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: f61af587-e858-4b92-a2b8-f5f2cb0d55ab

📥 Commits

Reviewing files that changed from the base of the PR and between a894b28 and 306c28f.

📒 Files selected for processing (5)
  • lib/helpers/getFromStreamChunks.js
  • lib/helpers/scopes.js
  • lib/helpers/streamAndGetSourceAndMap.js
  • test/scopes.js
  • types/helpers/scopes.d.ts

Walkthrough

The change adds optional scope bindings to OriginalSource and forwards them through composed sources. When scopes is enabled, map-generation helpers collect mapping and binding data and encode it in the source map. The change also updates caching behavior, public types and exports, documentation, and tests.

Priority: ⬇️ Low

Merge Risk: 🟡 Moderate · up to a894b

Scope maps can be invalid when generated output lacks a trailing newline, or report incorrect expressions when composed sources share a name. Fix these opt-in map-generation cases before merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to a894b

Scope metadata can be lost when a cached source is composed again, or associated with the wrong code when distinct sources share a name. No security exploit or new external exposure was established.

Retained concerns

  • Medium · architecture · inferred: A repeated scopes-enabled map request can lose a cached child’s bindings: the first stream forwards them, but cached-map replay registers sources without them.
  • Medium · architecture · inferred: Distinct composed sources with the same name share one source index and the first source’s bindings. The new scopes field can therefore omit or misassociate later bindings; whether differing bindings under one name are contractually valid remains unclear.
Security review details

Security Blast Radius

  • inferred — Any exposure of binding expressions depends on callers requesting and distributing the resulting map; the reviewed library path establishes map output, not a deployment or publication sink.

Trust Boundaries and Controls

  • observed — Scope emission is opt-in, and the cache key separates scoped from unscoped map requests. These controls do not preserve bindings during cached-map replay.

Hardening Proposals

  • proposed — If generated maps are published, apply the same confidentiality policy to supplied binding expressions as to other source-map content.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding support to emit the source map scopes field.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 1…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 37868bec-e87d-41be-8bb7-63c915bcbc7e

📥 Commits

Reviewing files that changed from the base of the PR and between 82a4bf0 and a894b28.

📒 Files selected for processing (21)
  • .changeset/source-map-scopes.md
  • README.md
  • lib/CachedSource.js
  • lib/ConcatSource.js
  • lib/OriginalSource.js
  • lib/ReplaceSource.js
  • lib/Source.js
  • lib/helpers/getFromStreamChunks.js
  • lib/helpers/scopes.js
  • lib/helpers/streamAndGetSourceAndMap.js
  • lib/helpers/streamChunks.js
  • lib/helpers/streamChunksOfCombinedSourceMap.js
  • lib/index.js
  • test/scopes.js
  • types.d.ts
  • types/OriginalSource.d.ts
  • types/Source.d.ts
  • types/helpers/scopes.d.ts
  • types/helpers/streamAndGetSourceAndMap.d.ts
  • types/helpers/streamChunks.d.ts
  • types/index.d.ts
💤 Files with no reviewable changes (1)
  • types/helpers/streamAndGetSourceAndMap.d.ts

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

Comment thread lib/ConcatSource.js Resolved
Comment thread lib/helpers/scopes.js Resolved
The scopes writer closed the open range at column 0 of the last line.
When the output does not end with a newline, a range that starts on that
line began after that point, so its end encoded a negative column delta,
which the unsigned VLQ writer turned into 4294967292 and an invalid
`scopes` field. Pass the generated end column through `finish`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01ThsbPWvW5vnYWEaFs2Q22F

alexander-akait commented Sep 26, 2026 •

Copy link
Copy Markdown
Member Author

CodSpeed Performance Analysis is red, but not because of this PR. The first report compared against fba8312, because main's tip had no finished run yet. Compared against main's own run at 82a4bf0, CodSpeed's environment check marks every flagged benchmark as having run on different hardware:

  • memory runs moved from AMD EPYC 9V74 to EPYC 7763;
  • simulation runs moved from AMD EPYC 7763 to Intel Xeon 8573C.

The one re-run also mixed environments. It shows 16 regressions and 24 improvements, a different set each time.

To check without the runner differences, I measured main (82a4bf0) and this branch (306c28f) on one machine, on the fixture the benchmarks use:

path allocated per call, main → PR time, main → PR (interleaved, 40 rounds)
OriginalSource.map({ columns: false }) 6.6–8.3 KB → 7.0–9.4 KB 25.6 → 25.6 µs, 27.0 → 26.1 µs
OriginalSource.map({ columns: true }) 9.2 → 9.3 KB 120.8 → 123.0 µs, 124.0 → 122.2 µs
OriginalSource.sourceAndMap({ columns: false }) n/a 32.1 → 31.3 µs, 30.6 → 30.0 µs
ConcatSource.map({ columns: true }) 18.6 → 18.6 KB n/a
ReplaceSource.map({ columns: true }) 8.2 → 8.2 KB n/a
warm CachedSource.sourceAndMap() 0.1 → 0.1 KB n/a

Retained memory is identical for all of these. Nothing reproduces the 3× memory growth or the 10–18% slowdowns CodSpeed reports, and several of those are in constructors this PR doesn't change. The regressions can be acknowledged on CodSpeed.


Generated by Claude Code

@alexander-akait
alexander-akait merged commit 2bd50f3 into main Sep 26, 2026
37 of 39 checks passed
@alexander-akait
alexander-akait deleted the feat/source-map-scopes branch September 26, 2026 14:17
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.

2 participants