feat: add Source Map Scopes Proposal support#210
Conversation
Port the Scopes Proposal plumbing from the `sokra/sources-proposal` branch on top of current `main`. Adds two optional `streamChunks` callbacks — `onOriginalScope` and `onGeneratedRange` — and threads them through every derived source so that `originalScopes`/`generatedRanges` on an input sourceMap round-trip out to `map()`/`sourceAndMap()`: - SourceMapSource reads scopes/ranges from its source map and emits them - ConcatSource forwards them with generated-line offsets per child - PrefixSource shifts generated-range column offsets for the prefix - CachedSource passes them through cache-miss and cache-hit paths - ReplaceSource forwards original scopes as-is (positions are stable) and drops generated ranges (their generated columns would need remapping) - RawSource / OriginalSource accept the callbacks as no-ops (no scopes) - streamChunksOfCombinedSourceMap accepts and drops them (combined maps don't remap scope/range coordinates today) - getFromStreamChunks / streamAndGetSourceAndMap collect the emitted scopes/ranges and emit them back onto the output source map New helpers: `vlq` (extracted token reader/encoder), `readOriginalScopes` / `readAllOriginalScopes` / `readGeneratedRanges`, `createOriginalScopesSerializer` / `createGeneratedRangesSerializer`. Typedefs for `OnOriginalScope`, `OnGeneratedRange`, `DefinitionReference`, `Callsite`, `Binding`, and the `originalScopes`/`generatedRanges` fields on `RawSourceMap` live in Source.js and are surfaced in the generated `types.d.ts`. Adds `test/scopes.js` covering VLQ round-trip, serializer↔reader round-trip, `readAllOriginalScopes`, SourceMapSource scope/range pass-through via `map()` and `streamChunks`, and ConcatSource generated range line-offset shifting. All 89,825 pre-existing tests continue to pass; 19 new tests added. https://claude.ai/code/session_014ZzvnYVFk8TSyuPA643z3v
|
🦋 Changeset detectedLatest commit: 3bfb891 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #210 +/- ##
==========================================
- Coverage 97.77% 96.88% -0.89%
==========================================
Files 25 26 +1
Lines 1932 2346 +414
Branches 606 734 +128
==========================================
+ Hits 1889 2273 +384
- Misses 41 70 +29
- Partials 2 3 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Merging this PR will degrade performance by 13.27%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | compat-source: source() (wrapping SourceLike) |
154.5 µs | 130.4 µs | +18.5% |
| ⚡ | compat-source: sourceAndMap() |
237.6 µs | 184 µs | +29.13% |
| ❌ | compat-source: map() |
167.5 µs | 193.2 µs | -13.27% |
| ⚡ | source-map-source: streamChunks({finalSource:true}) |
7.1 ms | 6.1 ms | +17.54% |
Comparing claude/investigate-sources-proposal-nxQ3b (3bfb891) with main (e130653)
Based on review feedback, flatten the six scope helpers into a single `lib/helpers/scopes.js` module, optimize the VLQ encoder/decoder hot paths, expand the JSDoc, mark the API as experimental in TypeScript- visible typedefs (@experimental), and add a dedicated experimental section to the README. Simplifications - `lib/helpers/vlq.js`, `readOriginalScopes.js`, `readAllOriginalScopes.js`, `readGeneratedRanges.js`, `createOriginalScopesSerializer.js`, and `createGeneratedRangesSerializer.js` are gone - All their exports (plus HAS_NAME_FLAG / HAS_DEFINITION_FLAG / HAS_CALLSITE_FLAG) now live in `scopes.js` - The three internal consumers (streamChunksOfSourceMap, getFromStreamChunks, streamAndGetSourceAndMap) import from the consolidated module - Net diff: -384 lines across the port Performance - Decoders (`readOriginalScopes`, `readGeneratedRanges`) inline the VLQ state machine instead of going through a per-token `onToken` closure, saving one closure call per sextet on the hot path - Encoder (`valueAsToken`) has a single-sextet fast path for the common case of |value| < 16, skipping the continuation loop entirely - Generated-ranges serializer uses a plain `";"` literal for single-line gaps instead of `.repeat(1)` (a test pins this) Docs & experimental status - Source.js typedefs (RawSourceMap.originalScopes/generatedRanges, DefinitionReference, Callsite, Binding, OnOriginalScope, OnGeneratedRange) gain prominent "Experimental — Source Map Scopes Proposal" prefixes and links to the TC39 proposal - Dedicated "Source Map Scopes Proposal (experimental)" section in README.md explaining the fields, propagation rules across derived sources, the two optional streamChunks callbacks, and the low-level helpers — all with a clear warning that the wire format may change - Changeset updated to call out the experimental status - `test/scopes.js` drops the internal VLQ-alphabet unit tests (they were exercising private implementation) and gains three targeted round-trip tests for the fast/slow VLQ paths and the single-`;` optimization All 89,844 tests still pass; npm run lint / tsc / tsc types test / format-file-header / generate-types all clean. https://claude.ai/code/session_014ZzvnYVFk8TSyuPA643z3v
Add 24 new tests (34 total, up from 10) covering the previously
under-exercised paths in lib/helpers/scopes.js and in the scope
propagation through every derived Source.
Encoder / decoder edges
- Single-sextet fast path (|value| <= 15)
- Multi-sextet continuation loop (|value| up to 2_000_000)
- Same-line comma separator (",") path in the serializer
- Multi-line ";" gap branch via `.repeat`
- HAS_CALLSITE_FLAG round-trip including source-change reset
- HAS_DEFINITION_FLAG source-change reset
- Simple (no-subrange) bindings round-trip
- Subrange line-delta wire format (pinned behavior)
- INVALID char (space) skipped silently in both readers
- Out-of-table char (charCode > 'z') skipped silently
- Tail flush when input ends without a terminator
- Non-string / null / number input is a no-op
- HAS_NAME_FLAG=0 path skips the name field
Source propagation
- PrefixSource shifts non-line-start generated-range columns by the
prefix length and leaves column-0 starts at column 0
- CachedSource preserves scope/range data on both the cache-miss and
cache-hit paths
- ReplaceSource keeps originalScopes but drops generatedRanges
- SourceMapSource with an inner map silently drops scopes (no throw,
no emitted events, no retained map fields)
- RawSource and OriginalSource accept the callbacks as no-ops
- ConcatSource remaps scope sourceIndex when a second child contributes
a distinct source, and remaps scope variable name indices to the
global name table
- The `streamChunks` dispatcher fallback (for sources without their own
streamChunks method) routes scope data through sourceAndMap +
streamChunksOfSourceMap
Coverage deltas
- scopes.js: 72% -> 98.5% lines, 52% -> 88% branches
- ConcatSource.js: 46% -> 50% lines
- PrefixSource.js: 0% -> 60% lines
- ReplaceSource.js: 0% -> 46% lines
- streamAndGetSourceAndMap: 43% -> 85% lines
- streamChunks.js: 55% -> 88% lines
- streamChunksOfSourceMap: 24% -> 57% lines
- streamChunksOfCombinedSourceMap: 3% -> 48% lines
All 89,859 tests pass; lint + tsc + tsc-types-test + tooling lint:special
clean.
https://claude.ai/code/session_014ZzvnYVFk8TSyuPA643z3v
The helpers in lib/helpers/scopes.js are internal and not part of the stable package surface. Documenting them in the README implied they were user-facing; remove the section to avoid that implication. https://claude.ai/code/session_014ZzvnYVFk8TSyuPA643z3v
Summary
Port the Source Map Scopes Proposal plumbing from the stale
sokra/sources-proposalbranch onto the currentmain. The proposal branch is about a year old and shares no merge base withmain, so this is a fresh re-application rather than a merge.The goal of this PR is narrow and forward-compatible: let an input
SourceMapSourcecarryoriginalScopes/generatedRanges, thread them through thestreamChunkspipeline (with coordinate shifts where needed), and emit them back out in the map produced bygetFromStreamChunks/streamAndGetSourceAndMap. Every existingstreamChunkssignature gains two optional trailing callbacks (onOriginalScope,onGeneratedRange) — nothing else changes for callers that don't care about scopes.What's new
New helpers under
lib/helpers/:vlq.js— shared VLQ token reader (readTokens) andvalueAsTokenencoder, extracted so both mappings and scopes/ranges use one decoderreadOriginalScopes.js/readAllOriginalScopes.js— decodeoriginalScopesper-source VLQ stringsreadGeneratedRanges.js— decode thegeneratedRangesVLQ string (definitions, callsites, bindings, subranges)createOriginalScopesSerializer.js/createGeneratedRangesSerializer.js— stateful serializers used bygetFromStreamChunks/streamAndGetSourceAndMapSource-class plumbing:
SourceMapSourcereadsoriginalScopes/generatedRangesfrom its source map and emits them via the new callbacksConcatSourceforwards scopes verbatim and shifts generated-range line/column coordinates by the cumulative offset of each childPrefixSourceshifts generated-range columns by the prefix length on non-line-start positionsCachedSourcepasses the callbacks through both the cache-miss (streamAndGetSourceAndMap) and cache-hit (streamChunksOfSourceMap) pathsReplaceSourceforwards original scopes (their positions refer to original sources, not generated columns, so they're stable across replacements) but drops generated ranges (their columns would need remapping through each replacement — out of scope here)RawSource,OriginalSource,streamChunksOfCombinedSourceMapaccept the new callbacks as no-ops since they have no scope data to emitTypes:
Source.jsgainsOnOriginalScope,OnGeneratedRange,DefinitionReference,Callsite,Bindingtypedefs, andRawSourceMappicks up optionaloriginalScopes/generatedRangesfieldstypes.d.tsregenerated viafix:specialto surface the new optional parameters on everystreamChunksWhat's intentionally not in this PR
streamChunksOfCombinedSourceMapdoes not remap scopes/ranges through the combined coordinate transform — it silently drops them. Supporting that is a larger design question (and was marked asthrowon the proposal branch).ReplaceSourcedrops generated ranges for the same reason.Test plan
test/scopes.jscover:,/;controlsoriginalScopesserializer ↔ reader round-trip (nested scope with named function + variables)generatedRangesserializer ↔ reader round-trip (range withHAS_DEFINITIONflag)readAllOriginalScopeswalks each per-source string; ignores non-array inputSourceMapSource.map()preservesoriginalScopes/generatedRangesverbatim when no inner mapSourceMapSource.streamChunksfires the scope/range callbacks with correct positionsConcatSourceshifts generated-range line offsets for non-first childrennpm run lint(eslint, tsc, tsc -p tsconfig.types.test.json, lint:special) cleantypes.d.tsregenerated and committedhttps://claude.ai/code/session_014ZzvnYVFk8TSyuPA643z3v
Generated by Claude Code