Skip to content

Commit ee8bbc7

Browse files
feat: range mappings, streaming composition, and remap_streaming
Implement ECMA-426 Stage 2 range mappings across the full stack: - Decode/encode `rangeMappings` field with unsigned VLQ - Range delta lookup in `original_position_for` (cross-line support) - Range mapping preservation through remap and concat - Generator APIs: `add_range_mapping`, `add_named_range_mapping` - StreamingGenerator range mapping support - WASM/NAPI bindings: hasRangeMappings, rangeMappingCount - CLI: range mapping display in `info` and `mappings` Add streaming source map composition: - `remap_streaming()` using MappingsIter + StreamingGenerator - Criterion benchmarks (500/10K/60K mappings) - Updated documentation across all crates and packages
1 parent a29923e commit ee8bbc7

16 files changed

Lines changed: 2184 additions & 98 deletions

File tree

Cargo.lock

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

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ vlq_encode(&mut buf, 42);
137137
Full [ECMA-426](https://tc39.es/ecma426/) (Source Map v3) compliance:
138138

139139
- All standard fields: `version`, `file`, `sourceRoot`, `sources`, `sourcesContent`, `names`, `mappings`
140+
- `rangeMappings` for range-based source mapping (ECMA-426 Stage 2 proposal)
140141
- `ignoreList` for filtering third-party sources
141142
- Indexed source maps with `sections` — flattened with source/name deduplication
142143
- Proper `sourceRoot` resolution
@@ -233,7 +234,7 @@ All commands support `--json` for structured output.
233234

234235
## Internals
235236

236-
- **Flat Mapping struct**24 bytes (6 × u32), cache-friendly contiguous layout
237+
- **Flat Mapping struct**28 bytes (6 × u32 + bool), cache-friendly contiguous layout
237238
- **Inlined VLQ decoder** — single-char fast path for values −15..15 (~85% of real-world VLQ values)
238239
- **Lazy reverse index** — only built on first `generated_position_for` call
239240
- **Binary search lookups** — O(log n) for both forward and reverse queries
@@ -247,6 +248,7 @@ cargo test --workspace # Run all tests
247248
cargo bench -p srcmap-sourcemap # Criterion benchmarks
248249
cargo bench -p srcmap-codec
249250
cargo bench -p srcmap-generator
251+
cargo bench -p srcmap-remapping # remap vs remap_streaming
250252
```
251253

252254
<details>

ROADMAP.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ The `Mapping` struct gains an `is_range_mapping: bool` field (default `false`).
4040

4141
### What to implement
4242

43-
- [ ] Decode `rangeMappings` field and set `is_range_mapping` on `Mapping` structs
44-
- [ ] Encode `rangeMappings` from `Mapping` structs with `is_range_mapping = true`
45-
- [ ] Update `original_position_for` to apply range delta when the matched mapping is a range mapping
46-
- [ ] Update `remap()` to preserve and compose range mappings through transform chains
47-
- [ ] Generator API: `add_range_mapping()` for marking a mapping as a range
48-
- [ ] WASM/NAPI bindings
49-
- [ ] CLI: show range mappings in `srcmap info` and `srcmap mappings`
43+
- [x] Decode `rangeMappings` field and set `is_range_mapping` on `Mapping` structs
44+
- [x] Encode `rangeMappings` from `Mapping` structs with `is_range_mapping = true`
45+
- [x] Update `original_position_for` to apply range delta when the matched mapping is a range mapping
46+
- [x] Update `remap()` to preserve and compose range mappings through transform chains
47+
- [x] Generator API: `add_range_mapping()` for marking a mapping as a range
48+
- [x] WASM/NAPI bindings
49+
- [x] CLI: show range mappings in `srcmap info` and `srcmap mappings`
5050

5151
---
5252

@@ -58,9 +58,11 @@ Rolldown's `collapse_sourcemaps` materializes the entire token stream into `Vec<
5858

5959
### What to implement
6060

61-
- [ ] `remap()` accepts iterators instead of requiring materialized `Vec`s
61+
- [x] `MappingsIter`: lazy iterator over VLQ-encoded mappings (decodes one at a time, no `Vec<Mapping>`)
62+
- [x] `StreamingGenerator`: on-the-fly VLQ encoder that emits mappings in sorted order without collecting
63+
- [x] `remap_streaming()`: composition pipeline that streams through mappings without intermediate allocation
64+
- [x] Criterion benchmarks (500 / 10K / 60K mappings) — 15-20% faster than materialized `remap()`
6265
- [ ] Builder pattern for `SourceMap::new()` that consumes iterators for names/sources/source_contents
63-
- [ ] Composition pipeline that streams through mappings without intermediate allocation
6466
- [ ] Benchmark against Rolldown's current `collapse_sourcemaps`
6567

6668
---

crates/cli/src/main.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ fn cmd_info(file: &PathBuf, json: bool) -> Result<(), CliError> {
398398
"sources": sm.sources.len(),
399399
"names": sm.names.len(),
400400
"mappings": sm.mapping_count(),
401+
"rangeMappings": sm.range_mapping_count(),
401402
"lines": sm.line_count(),
402403
"sourcesWithContent": has_content,
403404
"totalContentSize": content_size,
@@ -416,6 +417,9 @@ fn cmd_info(file: &PathBuf, json: bool) -> Result<(), CliError> {
416417
println!("Sources: {}", sm.sources.len());
417418
println!("Names: {}", sm.names.len());
418419
println!("Mappings: {}", sm.mapping_count());
420+
if sm.has_range_mappings() {
421+
println!(" Range: {} range mappings", sm.range_mapping_count());
422+
}
419423
println!("Lines: {}", sm.line_count());
420424
println!("File size: {}", format_size(raw.len()));
421425

@@ -702,6 +706,7 @@ fn cmd_mappings(
702706
"originalLine": m.original_line,
703707
"originalColumn": m.original_column,
704708
"name": name,
709+
"isRangeMapping": m.is_range_mapping,
705710
})
706711
})
707712
.collect();
@@ -716,10 +721,10 @@ fn cmd_mappings(
716721
println!("{}", serde_json::to_string_pretty(&obj).unwrap());
717722
} else {
718723
println!(
719-
"{:<8} {:<8} {:<30} {:<8} {:<8} name",
720-
"gen.ln", "gen.col", "source", "orig.ln", "orig.col"
724+
"{:<8} {:<8} {:<30} {:<8} {:<8} {:<6} name",
725+
"gen.ln", "gen.col", "source", "orig.ln", "orig.col", "range"
721726
);
722-
println!("{:-<80}", "");
727+
println!("{:-<86}", "");
723728
for m in &filtered {
724729
let source = if m.source != u32::MAX {
725730
sm.source(m.source)
@@ -731,13 +736,15 @@ fn cmd_mappings(
731736
} else {
732737
""
733738
};
739+
let range_marker = if m.is_range_mapping { "R" } else { "" };
734740
println!(
735-
"{:<8} {:<8} {:<30} {:<8} {:<8} {}",
741+
"{:<8} {:<8} {:<30} {:<8} {:<8} {:<6} {}",
736742
m.generated_line,
737743
m.generated_column,
738744
source,
739745
m.original_line,
740746
m.original_column,
747+
range_marker,
741748
name
742749
);
743750
}

crates/generator/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ let json = gen.to_json();
5454
| `add_named_mapping(gen_line, gen_col, src, orig_line, orig_col, name)` | Add a mapping with a name |
5555
| `add_generated_mapping(gen_line, gen_col)` | Add a generated-only mapping (no source) |
5656
| `maybe_add_mapping(gen_line, gen_col, src, orig_line, orig_col) -> bool` | Add only if different from previous |
57+
| `add_range_mapping(gen_line, gen_col, src, orig_line, orig_col)` | Add a range mapping (ECMA-426) |
58+
| `add_named_range_mapping(gen_line, gen_col, src, orig_line, orig_col, name)` | Add a named range mapping |
5759
| `add_to_ignore_list(source_idx)` | Mark a source as ignored (third-party) |
5860
| `to_json() -> String` | Serialize to source map v3 JSON |
61+
| `to_decoded_map() -> SourceMap` | Build a `SourceMap` directly (no JSON roundtrip) |
5962
| `mapping_count() -> usize` | Number of mappings added |
6063

6164
### Parallel encoding
@@ -67,10 +70,28 @@ Enable the `parallel` feature for multi-threaded VLQ encoding via [rayon](https:
6770
srcmap-generator = { version = "0.1", features = ["parallel"] }
6871
```
6972

73+
### `StreamingGenerator`
74+
75+
On-the-fly VLQ encoder that emits mappings as they are added, without collecting into a `Vec`. Ideal for composition pipelines where mappings arrive in sorted order.
76+
77+
| Method | Description |
78+
|--------|-------------|
79+
| `new(file) -> Self` | Create a new streaming generator |
80+
| `add_source(path) -> u32` | Register a source file (deduped) |
81+
| `add_name(name) -> u32` | Register a name (deduped) |
82+
| `add_mapping(...)` | Add a mapping (encoded immediately) |
83+
| `add_named_mapping(...)` | Add a mapping with a name |
84+
| `add_range_mapping(...)` | Add a range mapping (ECMA-426) |
85+
| `add_named_range_mapping(...)` | Add a named range mapping |
86+
| `to_json() -> String` | Serialize to source map v3 JSON |
87+
| `to_decoded_map() -> SourceMap` | Build a `SourceMap` directly |
88+
7089
## Features
7190

7291
- **Automatic deduplication** of sources and names
7392
- **`maybe_add_mapping`** skips redundant mappings to reduce output size
93+
- **Range mappings** (`rangeMappings` field, ECMA-426 Stage 2)
94+
- **Streaming generation** via `StreamingGenerator` — zero-allocation VLQ encoding
7495
- **`ignoreList`** support for filtering third-party sources in DevTools
7596
- **Parallel VLQ encoding** for large maps (opt-in via `parallel` feature)
7697
- **Hand-rolled JSON serialization** — no serde overhead in output path

0 commit comments

Comments
 (0)