Skip to content

Commit 61bb225

Browse files
feat: add ECMA-426 scopes support and publish WASM packages
- New srcmap-scopes crate: full encode/decode for the ECMA-426 scopes proposal (original scope trees, generated ranges, bindings, call sites) - Unsigned VLQ primitives added to srcmap-codec - Integration with srcmap-sourcemap (scopes field parsed automatically) - Integration with srcmap-generator (set_scopes + scopes in JSON output) - Published @srcmap/generator-wasm and @srcmap/remapping-wasm to npm - Updated release workflow for new crates and WASM packages - Fixed repository.url across all package.json files - Added pkg/ to .gitignore for wasm-pack build output
1 parent fa1d488 commit 61bb225

20 files changed

Lines changed: 2079 additions & 17 deletions

File tree

.github/workflows/release.yml

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ jobs:
3131
- name: Wait for crates.io index
3232
run: sleep 30
3333

34+
- name: Publish srcmap-scopes
35+
run: cargo publish -p srcmap-scopes || echo "Already published, skipping"
36+
env:
37+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
38+
39+
- name: Wait for crates.io index
40+
run: sleep 30
41+
3442
- name: Publish srcmap-sourcemap
3543
run: cargo publish -p srcmap-sourcemap || echo "Already published, skipping"
3644
env:
@@ -159,17 +167,42 @@ jobs:
159167
- name: Install wasm-pack
160168
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
161169

162-
- name: Build WASM package
170+
- name: Build sourcemap-wasm
163171
run: wasm-pack build --target nodejs --release
164172
working-directory: packages/sourcemap-wasm
165173

166-
- name: Upload WASM artifact
174+
- name: Build generator-wasm
175+
run: wasm-pack build --target nodejs --release
176+
working-directory: packages/generator-wasm
177+
178+
- name: Build remapping-wasm
179+
run: wasm-pack build --target nodejs --release
180+
working-directory: packages/remapping-wasm
181+
182+
- name: Remove wasm-pack .gitignore files
183+
run: rm -f packages/sourcemap-wasm/pkg/.gitignore packages/generator-wasm/pkg/.gitignore packages/remapping-wasm/pkg/.gitignore
184+
185+
- name: Upload sourcemap-wasm artifact
167186
uses: actions/upload-artifact@v4
168187
with:
169-
name: wasm-package
188+
name: wasm-sourcemap-package
170189
path: packages/sourcemap-wasm/pkg/
171190
if-no-files-found: error
172191

192+
- name: Upload generator-wasm artifact
193+
uses: actions/upload-artifact@v4
194+
with:
195+
name: wasm-generator-package
196+
path: packages/generator-wasm/pkg/
197+
if-no-files-found: error
198+
199+
- name: Upload remapping-wasm artifact
200+
uses: actions/upload-artifact@v4
201+
with:
202+
name: wasm-remapping-package
203+
path: packages/remapping-wasm/pkg/
204+
if-no-files-found: error
205+
173206
publish-npm:
174207
name: Publish npm packages
175208
runs-on: ubuntu-latest
@@ -204,12 +237,24 @@ jobs:
204237
pattern: sourcemap-bindings-*
205238
merge-multiple: false
206239

207-
- name: Download WASM artifact
240+
- name: Download sourcemap-wasm artifact
208241
uses: actions/download-artifact@v4
209242
with:
210-
name: wasm-package
243+
name: wasm-sourcemap-package
211244
path: packages/sourcemap-wasm/pkg
212245

246+
- name: Download generator-wasm artifact
247+
uses: actions/download-artifact@v4
248+
with:
249+
name: wasm-generator-package
250+
path: packages/generator-wasm/pkg
251+
252+
- name: Download remapping-wasm artifact
253+
uses: actions/download-artifact@v4
254+
with:
255+
name: wasm-remapping-package
256+
path: packages/remapping-wasm/pkg
257+
213258
- name: Move codec artifacts
214259
run: napi artifacts -d artifacts
215260
working-directory: packages/codec
@@ -244,6 +289,14 @@ jobs:
244289
run: npm publish --access public --provenance --ignore-scripts || echo "Already published, skipping"
245290
working-directory: packages/sourcemap-wasm
246291

292+
- name: Publish @srcmap/generator-wasm
293+
run: npm publish --access public --provenance --ignore-scripts || echo "Already published, skipping"
294+
working-directory: packages/generator-wasm
295+
296+
- name: Publish @srcmap/remapping-wasm
297+
run: npm publish --access public --provenance --ignore-scripts || echo "Already published, skipping"
298+
working-directory: packages/remapping-wasm
299+
247300
create-release:
248301
name: Create GitHub Release
249302
runs-on: ubuntu-latest

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@ node_modules/
66
# Coverage reports
77
coverage/
88

9+
# wasm-pack build output (rebuilt on publish)
10+
packages/*/pkg/
11+
912
# napi-rs native binaries (rebuilt on publish)
1013
# Note: index.js and index.d.ts are tracked — they're hand-written loaders

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ srcmap-codec = { version = "0.1.2", path = "crates/codec" }
1313
srcmap-sourcemap = { version = "0.1.2", path = "crates/sourcemap" }
1414
srcmap-generator = { version = "0.1.2", path = "crates/generator" }
1515
srcmap-remapping = { version = "0.1.2", path = "crates/remapping" }
16+
srcmap-scopes = { version = "0.1.2", path = "crates/scopes" }
1617

1718
[profile.release]
1819
lto = true

ROADMAP.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,13 @@ Agent-friendly command-line interface for inspecting, validating, composing, and
138138
- [x] Publish `srcmap-sourcemap` to crates.io (0.1.2)
139139
- [x] Publish `srcmap-generator` to crates.io (0.1.2)
140140
- [x] Publish `srcmap-remapping` to crates.io (0.1.2)
141+
- [ ] Publish `srcmap-scopes` to crates.io
141142
- [ ] Publish `srcmap-cli` to crates.io
142143
- [x] Publish `@srcmap/codec` to npm (0.1.2)
143144
- [x] Publish `@srcmap/sourcemap` to npm (0.1.2)
144145
- [x] Publish `@srcmap/sourcemap-wasm` to npm (0.1.2)
145-
- [ ] Publish `@srcmap/generator-wasm` to npm
146-
- [ ] Publish `@srcmap/remapping-wasm` to npm
146+
- [x] Publish `@srcmap/generator-wasm` to npm (0.1.3)
147+
- [x] Publish `@srcmap/remapping-wasm` to npm (0.1.3)
147148

148149
## Phase 6: WASM Bindings for Generator + Remapping ✅
149150

@@ -153,12 +154,28 @@ Node.js WASM bindings completing the full source map pipeline.
153154
- [x] `@srcmap/remapping-wasm` — ConcatBuilder + remap() with JS callback for upstream map loading
154155
- [x] Test suites for both packages (25 tests)
155156

157+
## Phase 7: Scopes & Variables (ECMA-426 Scopes Proposal) ✅
158+
159+
First Rust implementation of the ECMA-426 scopes proposal (Stage 3). Enables debuggers to reconstruct original scope trees, variable bindings, and inlined function call sites.
160+
161+
- [x] New `srcmap-scopes` crate with full encode/decode
162+
- [x] Tag-based VLQ encoding: B (scope start), C (scope end), D (variables), E (range start), F (range end), G (bindings), H (sub-range bindings), I (call site)
163+
- [x] Unsigned VLQ primitives added to `srcmap-codec`
164+
- [x] Original scope trees with nesting, names, kinds, stack frame flags, and variable lists
165+
- [x] Generated ranges with nesting, definition references, bindings, sub-range bindings, and call sites
166+
- [x] Delta/relative encoding for positions, names, kinds, variables, and definitions
167+
- [x] Integration with `srcmap-sourcemap` parser (`scopes` field parsed automatically)
168+
- [x] Integration with `srcmap-generator` (`set_scopes()` + `scopes` field in JSON output)
169+
- [x] Full roundtrip encode → decode tests (14 scopes tests + 2 integration tests)
170+
- [x] Definition resolution helper (`original_scope_for_definition`)
171+
156172
## Future
157173

158174
- [x] Debug ID support (`debugId` field, part of ECMA-426)
175+
- [x] Scopes & variables support (ECMA-426 scopes proposal) — **first Rust implementation**
159176
- [ ] NAPI bindings for generator and remapping
160177
- [ ] WASM build target for browser (devtools, playgrounds, edge runtimes)
161-
- [ ] Scopes & variables support (ECMA-426 proposal — no library supports this yet)
178+
- [ ] WASM bindings for scopes decode/encode
162179
- [ ] Streaming/lazy decode for very large source maps
163180

164181
## Non-goals

crates/codec/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub use decode::decode;
4343
pub use encode::encode;
4444
#[cfg(feature = "parallel")]
4545
pub use encode::encode_parallel;
46-
pub use vlq::{vlq_decode, vlq_encode};
46+
pub use vlq::{vlq_decode, vlq_decode_unsigned, vlq_encode, vlq_encode_unsigned};
4747

4848
use std::fmt;
4949

crates/codec/src/vlq.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,71 @@ pub fn vlq_decode(input: &[u8], pos: usize) -> Result<(i64, usize), DecodeError>
123123
Ok((value, i - pos))
124124
}
125125

126+
/// Encode a single unsigned VLQ value, appending base64 chars to the output buffer.
127+
///
128+
/// Unlike signed VLQ, no sign bit is used — all 5 bits per character are data.
129+
/// Used by the ECMA-426 scopes proposal for tags, flags, and unsigned values.
130+
#[inline]
131+
pub fn vlq_encode_unsigned(out: &mut Vec<u8>, value: u64) {
132+
let mut vlq = value;
133+
loop {
134+
let mut digit = vlq & VLQ_BASE_MASK;
135+
vlq >>= VLQ_BASE_SHIFT;
136+
if vlq > 0 {
137+
digit |= VLQ_CONTINUATION_BIT;
138+
}
139+
out.push(BASE64_ENCODE[digit as usize]);
140+
if vlq == 0 {
141+
break;
142+
}
143+
}
144+
}
145+
146+
/// Decode a single unsigned VLQ value from the input bytes starting at the given position.
147+
///
148+
/// Returns `(decoded_value, bytes_consumed)` or a [`DecodeError`].
149+
/// Unlike signed VLQ, no sign bit extraction is performed.
150+
#[inline]
151+
pub fn vlq_decode_unsigned(input: &[u8], pos: usize) -> Result<(u64, usize), DecodeError> {
152+
let mut result: u64 = 0;
153+
let mut shift: u32 = 0;
154+
let mut i = pos;
155+
156+
loop {
157+
if i >= input.len() {
158+
return Err(DecodeError::UnexpectedEof { offset: i });
159+
}
160+
161+
let byte = input[i];
162+
163+
if byte >= 128 {
164+
return Err(DecodeError::InvalidBase64 { byte, offset: i });
165+
}
166+
167+
let digit = BASE64_DECODE[byte as usize];
168+
169+
if digit == 255 {
170+
return Err(DecodeError::InvalidBase64 { byte, offset: i });
171+
}
172+
173+
let digit = digit as u64;
174+
i += 1;
175+
176+
if shift >= VLQ_MAX_SHIFT {
177+
return Err(DecodeError::VlqOverflow { offset: pos });
178+
}
179+
180+
result += (digit & VLQ_BASE_MASK) << shift;
181+
shift += VLQ_BASE_SHIFT;
182+
183+
if (digit & VLQ_CONTINUATION_BIT) == 0 {
184+
break;
185+
}
186+
}
187+
188+
Ok((result, i - pos))
189+
}
190+
126191
#[cfg(test)]
127192
mod tests {
128193
use super::*;
@@ -240,4 +305,52 @@ mod tests {
240305
let err = vlq_decode(b"", 0).unwrap_err();
241306
assert_eq!(err, DecodeError::UnexpectedEof { offset: 0 });
242307
}
308+
309+
// --- Unsigned VLQ tests ---
310+
311+
#[test]
312+
fn unsigned_encode_zero() {
313+
let mut buf = Vec::new();
314+
vlq_encode_unsigned(&mut buf, 0);
315+
assert_eq!(&buf, b"A");
316+
}
317+
318+
#[test]
319+
fn unsigned_encode_small_values() {
320+
// Value 1 → 'B', value 2 → 'C', ..., value 31 → base64[31] = 'f'
321+
let mut buf = Vec::new();
322+
vlq_encode_unsigned(&mut buf, 1);
323+
assert_eq!(&buf, b"B");
324+
325+
buf.clear();
326+
vlq_encode_unsigned(&mut buf, 8);
327+
assert_eq!(&buf, b"I");
328+
}
329+
330+
#[test]
331+
fn unsigned_roundtrip() {
332+
let values: [u64; 10] = [0, 1, 8, 15, 16, 31, 32, 100, 1000, 100_000];
333+
for &v in &values {
334+
let mut buf = Vec::new();
335+
vlq_encode_unsigned(&mut buf, v);
336+
let (decoded, consumed) = vlq_decode_unsigned(&buf, 0).unwrap();
337+
assert_eq!(decoded, v, "unsigned roundtrip failed for {v}");
338+
assert_eq!(consumed, buf.len());
339+
}
340+
}
341+
342+
#[test]
343+
fn unsigned_multi_char() {
344+
let mut buf = Vec::new();
345+
vlq_encode_unsigned(&mut buf, 500);
346+
assert!(buf.len() > 1, "500 should need multiple chars");
347+
let (decoded, _) = vlq_decode_unsigned(&buf, 0).unwrap();
348+
assert_eq!(decoded, 500);
349+
}
350+
351+
#[test]
352+
fn unsigned_decode_empty() {
353+
let err = vlq_decode_unsigned(b"", 0).unwrap_err();
354+
assert_eq!(err, DecodeError::UnexpectedEof { offset: 0 });
355+
}
243356
}

crates/generator/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ crate-type = ["lib"]
1818

1919
[dependencies]
2020
srcmap-codec = { workspace = true }
21+
srcmap-scopes = { workspace = true }
2122
serde = { version = "1", features = ["derive"] }
2223
serde_json = "1"
2324
rayon = { version = "1", optional = true }

0 commit comments

Comments
 (0)