├── tests
├── fixtures
│ ├── swap
│ │ ├── test.js
│ │ ├── test.js.map
│ │ └── visualizer.snap
│ ├── esbuild
│ │ ├── README.md
│ │ ├── example.js
│ │ ├── example.js.map
│ │ └── visualizer.snap
│ └── basic
│ │ ├── test.js
│ │ ├── test.js.map
│ │ └── visualizer.snap
├── fixtures_concat_sourcemap_builder
│ └── empty
│ │ ├── dep1.js
│ │ ├── dep2.js
│ │ ├── dep3.js
│ │ ├── dep2.js.map
│ │ ├── dep1.js.map
│ │ └── dep3.js.map
├── snapshots
│ ├── main__invalid_token_position.snap
│ └── concat_sourcemap_builder__empty.snap
├── concat_sourcemap_builder.rs
├── main.rs
└── tc39_spec_tests.rs
├── .gitignore
├── rust-toolchain.toml
├── .rustfmt.toml
├── .github
├── renovate.json
└── workflows
│ ├── release.yml
│ ├── dprint.yml
│ ├── zizmor.yml
│ ├── copilot-setup-steps.yml
│ ├── benchmark.yml
│ ├── autofix.yml
│ └── ci.yml
├── .typos.toml
├── src
├── lib.rs
├── napi.rs
├── error.rs
├── sourcemap_builder.rs
├── token.rs
├── sourcemap_visualizer.rs
├── decode.rs
├── concat_sourcemap_builder.rs
├── sourcemap.rs
└── encode.rs
├── dprint.json
├── justfile
├── LICENSE
├── benches
└── simple.rs
├── README.md
├── AGENTS.md
├── Cargo.toml
├── CHANGELOG.md
└── Cargo.lock
/tests/fixtures/swap/test.js:
--------------------------------------------------------------------------------
1 | z;
2 | y;
3 | x;
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | tests/source-map-tests/
3 |
--------------------------------------------------------------------------------
/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "1.92.0"
3 | profile = "default"
4 |
--------------------------------------------------------------------------------
/tests/fixtures/esbuild/README.md:
--------------------------------------------------------------------------------
1 | Example code from https://github.com/evanw/source-map-visualization/
2 |
--------------------------------------------------------------------------------
/tests/fixtures_concat_sourcemap_builder/empty/dep1.js:
--------------------------------------------------------------------------------
1 |
2 | //#region src/dep1.js
3 | console.log("dep1");
4 |
5 | //#endregion
6 |
--------------------------------------------------------------------------------
/tests/fixtures_concat_sourcemap_builder/empty/dep2.js:
--------------------------------------------------------------------------------
1 |
2 | //#region src/dep2.js
3 | console.log("dep2");
4 |
5 | //#endregion
6 |
--------------------------------------------------------------------------------
/tests/fixtures_concat_sourcemap_builder/empty/dep3.js:
--------------------------------------------------------------------------------
1 |
2 | //#region src/dep3.js
3 | console.log("dep3");
4 |
5 | //#endregion
6 |
--------------------------------------------------------------------------------
/.rustfmt.toml:
--------------------------------------------------------------------------------
1 | style_edition = "2024"
2 | use_small_heuristics = "Max"
3 | use_field_init_shorthand = true
4 | reorder_modules = true
5 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["github>Boshen/renovate"]
4 | }
5 |
--------------------------------------------------------------------------------
/tests/fixtures/basic/test.js:
--------------------------------------------------------------------------------
1 |
2 | // shared.js
3 | const a = 'shared.js';
4 |
5 | // index.js
6 | const a$1 = 'index.js';
7 | console.log(a$1, a);
8 |
--------------------------------------------------------------------------------
/.typos.toml:
--------------------------------------------------------------------------------
1 | [default]
2 | extend-ignore-re = [
3 | "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$",
4 | "(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on",
5 | ]
6 |
--------------------------------------------------------------------------------
/tests/fixtures_concat_sourcemap_builder/empty/dep2.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"dep2.js","names":[],"sources":["../src/dep2.js"],"sourcesContent":["console.log('dep2');\n"],"mappings":""}
2 |
--------------------------------------------------------------------------------
/tests/fixtures_concat_sourcemap_builder/empty/dep1.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"dep1.js","names":[],"sources":["../src/dep1.js"],"sourcesContent":["console.log('dep1');\n"],"mappings":";;AAAA,QAAQ,IAAI,OAAO"}
2 |
--------------------------------------------------------------------------------
/tests/fixtures_concat_sourcemap_builder/empty/dep3.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"dep3.js","names":[],"sources":["../src/dep3.js"],"sourcesContent":["console.log('dep3');\n"],"mappings":";;AAAA,QAAQ,IAAI,OAAO"}
2 |
--------------------------------------------------------------------------------
/tests/fixtures/swap/test.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "names": ["z", "y", "x"],
4 | "sources": ["test.js"],
5 | "sourcesContent": ["x;\ny;\nz;\n"],
6 | "mappings": "AAEAA,CAAC;AADDC,CAAC;AADDC,CAAC",
7 | "ignoreList": []
8 | }
9 |
--------------------------------------------------------------------------------
/tests/snapshots/main__invalid_token_position.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/main.rs
3 | expression: visualizer_text
4 | snapshot_kind: text
5 | ---
6 | - src.js
7 | (0:0) "abc\n" --> (0:0) "abc\n"
8 | (0:0) --> (0:10) [invalid]
9 | (0:10) [invalid] --> (0:0)
10 |
--------------------------------------------------------------------------------
/tests/fixtures/basic/test.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version":3,
3 | "sources":["shared.js","index.js"],
4 | "sourcesContent":["const a = 'shared.js'\n\nexport { a }","import { a as a2 } from './shared'\nconst a = 'index.js'\nconsole.log(a, a2)\n"],
5 | "names":["a","a$1"],
6 | "mappings":";;AAAA,MAAMA,IAAI;;;ACCV,MAAMC,MAAI;AACV,QAAQ,IAAIA,KAAGD,EAAG"
7 | }
8 |
--------------------------------------------------------------------------------
/tests/fixtures/swap/visualizer.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/oxc_sourcemap/tests/main.rs
3 | input_file: crates/oxc_sourcemap/tests/fixtures/swap/test.js
4 | snapshot_kind: text
5 | ---
6 | - test.js
7 | (2:0) "z" --> (0:0) "z"
8 | (2:1) ";\n" --> (0:1) ";\n"
9 | (1:0) "y" --> (1:0) "y"
10 | (1:1) ";\n" --> (1:1) ";\n"
11 | (0:0) "x" --> (2:0) "x"
12 | (0:1) ";\n" --> (2:1) ";\n"
13 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | permissions: {}
4 |
5 | on:
6 | push:
7 | branches:
8 | - main
9 |
10 | jobs:
11 | release-plz:
12 | name: Release-plz
13 | runs-on: ubuntu-latest
14 | permissions:
15 | pull-requests: write
16 | contents: write
17 | id-token: write
18 | steps:
19 | - uses: oxc-project/release-plz@44b98e8dda1a7783d4ec2ef66e2f37a3e8c1c759 # v1.0.4
20 | with:
21 | PAT: ${{ secrets.OXC_BOT_PAT }}
22 |
--------------------------------------------------------------------------------
/tests/snapshots/concat_sourcemap_builder__empty.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/concat_sourcemap_builder.rs
3 | expression: visualizer_text
4 | snapshot_kind: text
5 | ---
6 | - ../src/dep1.js
7 | (0:0) "console." --> (2:0) "console."
8 | (0:8) "log(" --> (2:8) "log("
9 | (0:12) "'dep1')" --> (2:12) "\"dep1\")"
10 | (0:19) ";\n" --> (2:19) ";\n"
11 | - ../src/dep3.js
12 | (0:0) "console." --> (12:0) "console."
13 | (0:8) "log(" --> (12:8) "log("
14 | (0:12) "'dep3')" --> (12:12) "\"dep3\")"
15 | (0:19) ";\n" --> (12:19) ";\n"
16 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | mod concat_sourcemap_builder;
2 | mod decode;
3 | mod encode;
4 | mod error;
5 | mod sourcemap;
6 | mod sourcemap_builder;
7 | mod sourcemap_visualizer;
8 | mod token;
9 |
10 | #[cfg(feature = "napi")]
11 | pub mod napi;
12 |
13 | pub use concat_sourcemap_builder::ConcatSourceMapBuilder;
14 | pub use decode::JSONSourceMap;
15 | pub use error::Error;
16 | pub use sourcemap::SourceMap;
17 | pub use sourcemap_builder::SourceMapBuilder;
18 | pub use sourcemap_visualizer::SourcemapVisualizer;
19 | pub use token::{SourceViewToken, Token, TokenChunk};
20 |
--------------------------------------------------------------------------------
/dprint.json:
--------------------------------------------------------------------------------
1 | {
2 | "lineWidth": 120,
3 | "typescript": {
4 | "quoteStyle": "preferSingle",
5 | "binaryExpression.operatorPosition": "sameLine"
6 | },
7 | "json": {
8 | "indentWidth": 2
9 | },
10 | "toml": {
11 | },
12 | "excludes": [
13 | "tests/**/*.js"
14 | ],
15 | "plugins": [
16 | "https://plugins.dprint.dev/typescript-0.95.13.wasm",
17 | "https://plugins.dprint.dev/json-0.21.0.wasm",
18 | "https://plugins.dprint.dev/markdown-0.20.0.wasm",
19 | "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm",
20 | "https://plugins.dprint.dev/toml-0.7.0.wasm"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/tests/fixtures/basic/visualizer.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/oxc_sourcemap/tests/main.rs
3 | input_file: crates/oxc_sourcemap/tests/fixtures/basic/test.js
4 | snapshot_kind: text
5 | ---
6 | - shared.js
7 | (0:0) "const " --> (2:0) "const "
8 | (0:6) "a = " --> (2:6) "a = "
9 | (0:10) "'shared.js'\n" --> (2:10) "'shared.js';\n"
10 | - index.js
11 | (1:0) "const " --> (5:0) "const "
12 | (1:6) "a = " --> (5:6) "a$1 = "
13 | (1:10) "'index.js'\n" --> (5:12) "'index.js';\n"
14 | (2:0) "console." --> (6:0) "console."
15 | (2:8) "log(" --> (6:8) "log("
16 | (2:12) "a, " --> (6:12) "a$1, "
17 | (2:15) "a2)" --> (6:17) "a)"
18 | (2:18) "\n" --> (6:19) ";\n"
19 |
--------------------------------------------------------------------------------
/justfile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S just --justfile
2 |
3 | set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
4 | set shell := ["bash", "-cu"]
5 |
6 | _default:
7 | @just --list -u
8 |
9 | alias r := ready
10 |
11 | init:
12 | cargo binstall watchexec-cli typos-cli cargo-shear dprint -y
13 | # Clone tc39 source map tests for spec compliance testing
14 | git clone https://github.com/tc39/source-map-tests.git tests/source-map-tests || true
15 |
16 | ready:
17 | git diff --exit-code --quiet
18 | typos
19 | cargo check --all-targets --all-features
20 | cargo test
21 | cargo clippy --all-targets --all-features
22 | just fmt
23 |
24 | fmt:
25 | -cargo shear --fix # remove all unused dependencies
26 | cargo fmt --all
27 | dprint fmt
28 |
--------------------------------------------------------------------------------
/.github/workflows/dprint.yml:
--------------------------------------------------------------------------------
1 | name: Save dprint plugin cache
2 |
3 | permissions: {}
4 |
5 | on:
6 | workflow_dispatch:
7 | push:
8 | branches:
9 | - main
10 | paths:
11 | - "dprint.json"
12 | - ".github/workflows/dprint.yml"
13 |
14 | jobs:
15 | cache:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1
19 |
20 | - uses: taiki-e/install-action@61e5998d108b2b55a81b9b386c18bd46e4237e4f # v2.63.1
21 | with:
22 | tool: dprint
23 |
24 | - run: dprint check
25 |
26 | - name: Save dprint plugin cache
27 | uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
28 | with:
29 | key: dprint-${{ hashFiles('dprint.json') }}
30 | path: ~/.cache/dprint
31 |
--------------------------------------------------------------------------------
/.github/workflows/zizmor.yml:
--------------------------------------------------------------------------------
1 | name: GitHub Actions Security Analysis
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | types: [opened, synchronize]
7 | paths:
8 | - ".github/workflows/**"
9 | push:
10 | branches:
11 | - main
12 | paths:
13 | - ".github/workflows/**"
14 |
15 | permissions: {}
16 |
17 | jobs:
18 | zizmor:
19 | name: zizmor
20 | runs-on: ubuntu-latest
21 | permissions:
22 | security-events: write
23 | steps:
24 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
25 | with:
26 | persist-credentials: false
27 |
28 | - uses: taiki-e/install-action@61e5998d108b2b55a81b9b386c18bd46e4237e4f # v2.63.1
29 | with:
30 | tool: zizmor
31 |
32 | - run: zizmor --format sarif . > results.sarif
33 | env:
34 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 |
36 | - uses: github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
37 | with:
38 | sarif_file: results.sarif
39 | category: zizmor
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024-present VoidZero Inc. & Contributors
4 | Copyright (c) 2023 Boshen
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/.github/workflows/copilot-setup-steps.yml:
--------------------------------------------------------------------------------
1 | name: Copilot Setup Steps
2 |
3 | # This workflow defines the setup steps that GitHub Copilot agents will use
4 | # to prepare the development environment for the oxc project.
5 | # It preinstalls tools and dependencies needed for Rust and Node.js development.
6 |
7 | on:
8 | workflow_dispatch:
9 | pull_request:
10 | types: [opened, synchronize]
11 | paths:
12 | - .github/workflows/copilot-setup-steps.yml
13 | push:
14 | branches:
15 | - main
16 | paths:
17 | - .github/workflows/copilot-setup-steps.yml
18 |
19 | permissions: {}
20 |
21 | jobs:
22 | copilot-setup-steps:
23 | name: Setup Development Environment for Copilot
24 | runs-on: ubuntu-latest
25 | steps:
26 | # Checkout full repo for git history.
27 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
28 | with:
29 | persist-credentials: false
30 |
31 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11
32 | with:
33 | cache-key: warm
34 | save-cache: false
35 | tools: just,typos-cli,cargo-shear,dprint
36 | components: clippy rust-docs rustfmt
37 |
--------------------------------------------------------------------------------
/tests/fixtures/esbuild/example.js:
--------------------------------------------------------------------------------
1 | // index.tsx
2 | import { h as u, Fragment as l, render as c } from "preact";
3 |
4 | // counter.tsx
5 | import { h as t, Component as i } from "preact";
6 | import { useState as a } from "preact/hooks";
7 | var n = class extends i {
8 | constructor(e) {
9 | super(e);
10 | this.n = () => this.setState({ t: this.state.t + 1 });
11 | this.r = () => this.setState({ t: this.state.t - 1 });
12 | this.state.t = e.e;
13 | }
14 | render() {
15 | return t("div", {
16 | class: "counter"
17 | }, t("h1", null, this.props.label), t("p", null, t("button", {
18 | onClick: this.r
19 | }, "-"), " ", this.state.t, " ", t("button", {
20 | onClick: this.n
21 | }, "+")));
22 | }
23 | }, s = (r) => {
24 | let [o, e] = a(r.e);
25 | return t("div", {
26 | class: "counter"
27 | }, t("h1", null, r.o), t("p", null, t("button", {
28 | onClick: () => e(o - 1)
29 | }, "-"), " ", o, " ", t("button", {
30 | onClick: () => e(o + 1)
31 | }, "+")));
32 | };
33 |
34 | // index.tsx
35 | c(
36 | u(l, null, u(n, {
37 | o: "Counter 1",
38 | e: 100
39 | }), u(s, {
40 | o: "Counter 2",
41 | e: 200
42 | })),
43 | document.getElementById("root")
44 | );
45 | //# sourceMappingURL=example.js.map
46 |
--------------------------------------------------------------------------------
/tests/concat_sourcemap_builder.rs:
--------------------------------------------------------------------------------
1 | use std::fs;
2 |
3 | use oxc_sourcemap::{ConcatSourceMapBuilder, SourceMap, SourcemapVisualizer};
4 |
5 | #[test]
6 | fn concat_sourcemap_builder_with_empty() {
7 | let dir = std::path::Path::new(file!())
8 | .parent()
9 | .unwrap()
10 | .join("fixtures_concat_sourcemap_builder/empty");
11 |
12 | let mut builder = ConcatSourceMapBuilder::default();
13 | let mut source = String::new();
14 |
15 | // dep2.js.map has { mappings: "" }
16 | for filename in ["dep1.js", "dep2.js", "dep3.js"] {
17 | let js = fs::read_to_string(dir.join(filename)).unwrap();
18 | let js_map = fs::read_to_string(dir.join(filename).with_extension("js.map")).unwrap();
19 | let sourcemap = SourceMap::from_json_string(&js_map).unwrap();
20 | builder.add_sourcemap(&sourcemap, source.lines().count() as u32);
21 | source.push_str(&js);
22 | }
23 |
24 | let sourcemap = builder.into_sourcemap();
25 | // encode and decode back to test token chunk serialization
26 | let sourcemap = SourceMap::from_json(sourcemap.to_json()).unwrap();
27 |
28 | let visualizer = SourcemapVisualizer::new(&source, &sourcemap);
29 | let visualizer_text = visualizer.get_text();
30 | insta::assert_snapshot!("empty", visualizer_text);
31 | }
32 |
--------------------------------------------------------------------------------
/tests/main.rs:
--------------------------------------------------------------------------------
1 | use std::fs;
2 |
3 | use oxc_sourcemap::{SourceMap, SourcemapVisualizer, Token};
4 |
5 | #[test]
6 | fn snapshot_sourcemap_visualizer() {
7 | insta::glob!("fixtures/**/*.js", |path| {
8 | let js = fs::read_to_string(path).unwrap();
9 | let js_map = fs::read_to_string(path.with_extension("js.map")).unwrap();
10 | let sourcemap = SourceMap::from_json_string(&js_map).unwrap();
11 | let visualizer = SourcemapVisualizer::new(&js, &sourcemap);
12 | let visualizer_text = visualizer.get_text();
13 | insta::with_settings!({ snapshot_path => path.parent().unwrap(), prepend_module_to_snapshot => false, snapshot_suffix => "", omit_expression => true }, {
14 | insta::assert_snapshot!("visualizer", visualizer_text);
15 | });
16 | });
17 | }
18 |
19 | #[test]
20 | fn invalid_token_position() {
21 | let sourcemap = SourceMap::new(
22 | None,
23 | vec![],
24 | None,
25 | vec!["src.js".into()],
26 | vec![Some("abc\ndef".into())],
27 | vec![
28 | Token::new(0, 0, 0, 0, Some(0), None),
29 | Token::new(0, 10, 0, 0, Some(0), None),
30 | Token::new(0, 0, 0, 10, Some(0), None),
31 | ]
32 | .into_boxed_slice(),
33 | None,
34 | );
35 | let js = "abc\ndef\n";
36 | let visualizer = SourcemapVisualizer::new(js, &sourcemap);
37 | let visualizer_text = visualizer.get_text();
38 | insta::assert_snapshot!("invalid_token_position", visualizer_text);
39 | }
40 |
--------------------------------------------------------------------------------
/.github/workflows/benchmark.yml:
--------------------------------------------------------------------------------
1 | name: Benchmark
2 |
3 | permissions: {}
4 |
5 | on:
6 | workflow_dispatch:
7 | pull_request:
8 | types: [opened, synchronize]
9 | paths:
10 | - "**/*.rs"
11 | - "Cargo.toml"
12 | - "Cargo.lock"
13 | - ".github/workflows/benchmark.yml"
14 | push:
15 | branches:
16 | - main
17 | paths:
18 | - "**/*.rs"
19 | - "Cargo.lock"
20 | - "Cargo.toml"
21 | - ".github/workflows/benchmark.yml"
22 |
23 | concurrency:
24 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
25 | cancel-in-progress: true
26 |
27 | defaults:
28 | run:
29 | shell: bash
30 |
31 | jobs:
32 | benchmark:
33 | name: Benchmark
34 | runs-on: ubuntu-latest
35 | permissions:
36 | id-token: write # required for OIDC authentication with CodSpeed
37 | steps:
38 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1
39 |
40 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11
41 | with:
42 | cache-key: benchmark
43 | save-cache: ${{ github.ref_name == 'main' }}
44 | tools: cargo-codspeed
45 |
46 | - name: Build benchmark
47 | run: cargo codspeed build --features codspeed
48 |
49 | - name: Run benchmark
50 | uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
51 | timeout-minutes: 15
52 | with:
53 | run: cargo codspeed run
54 | mode: instrumentation
55 |
--------------------------------------------------------------------------------
/src/napi.rs:
--------------------------------------------------------------------------------
1 | use napi_derive::napi;
2 |
3 | // Aligned with Rollup's sourcemap input.
4 | //
5 | //
6 | //
7 | // ```
8 | // export interface ExistingRawSourceMap {
9 | // file?: string;
10 | // mappings: string;
11 | // names: string[];
12 | // sourceRoot?: string;
13 | // sources: string[];
14 | // sourcesContent?: string[];
15 | // version: number;
16 | // x_google_ignoreList?: number[];
17 | // }
18 | // ```
19 | #[napi(object)]
20 | pub struct SourceMap {
21 | pub file: Option,
22 | pub mappings: String,
23 | pub names: Vec,
24 | pub source_root: Option,
25 | pub sources: Vec,
26 | pub sources_content: Option>,
27 | pub version: u8,
28 | #[napi(js_name = "x_google_ignoreList")]
29 | pub x_google_ignorelist: Option>,
30 | }
31 |
32 | impl From for SourceMap {
33 | fn from(source_map: crate::SourceMap) -> Self {
34 | let json = source_map.to_json();
35 | Self {
36 | file: json.file,
37 | mappings: json.mappings,
38 | names: json.names,
39 | source_root: json.source_root,
40 | sources: json.sources,
41 | sources_content: json.sources_content.map(|content| {
42 | content.into_iter().map(Option::unwrap_or_default).collect::>()
43 | }),
44 | version: 3,
45 | x_google_ignorelist: None,
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/benches/simple.rs:
--------------------------------------------------------------------------------
1 | use criterion::{Criterion, criterion_group, criterion_main};
2 | use oxc_sourcemap::{SourceMap, SourceMapBuilder};
3 |
4 | pub fn bench(c: &mut Criterion) {
5 | let input = r#"{
6 | "version": 3,
7 | "sources": ["coolstuff.js"],
8 | "sourceRoot": "x",
9 | "names": ["x","alert"],
10 | "mappings": "AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM",
11 | "x_google_ignoreList": [0],
12 | "sourcesContent": ["var x = 1;\nif (x == 2) {\n alert('test');\n}"]
13 | }"#;
14 |
15 | c.bench_function("SourceMap::from_json_string", |b| {
16 | b.iter(|| SourceMap::from_json_string(input).unwrap());
17 | });
18 |
19 | c.bench_function("SourceMap::to_json", |b| {
20 | let sm = SourceMap::from_json_string(input).unwrap();
21 | b.iter(|| sm.to_json());
22 | });
23 |
24 | c.bench_function("SourceMap::to_json_string", |b| {
25 | let sm = SourceMap::from_json_string(input).unwrap();
26 | b.iter(|| sm.to_json_string());
27 | });
28 |
29 | c.bench_function("SourceMap::generate_lookup_table", |b| {
30 | let sm = SourceMap::from_json_string(input).unwrap();
31 | b.iter(|| sm.generate_lookup_table());
32 | });
33 |
34 | c.bench_function("SourceMapBuilder::add_name_add_source_and_content", |b| {
35 | let mut builder = SourceMapBuilder::default();
36 | b.iter(|| {
37 | builder.add_name("foo");
38 | builder.add_source_and_content("test.js", "var x = 1;");
39 | });
40 | });
41 | }
42 |
43 | criterion_group!(sourcemap, bench);
44 | criterion_main!(sourcemap);
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [![Crates.io][crates-badge]][crates-url]
4 | [![Docs.rs][docs-badge]][docs-url]
5 | [![MIT licensed][license-badge]][license-url]
6 | [![Build Status][ci-badge]][ci-url]
7 | [![Sponsors][sponsors-badge]][sponsors-url]
8 | [![Discord chat][discord-badge]][discord-url]
9 |
10 |
11 |
12 |
13 |
14 |
15 | # oxc-sourcemap
16 |
17 | Forked version of [rust-sourcemap](https://github.com/getsentry/rust-sourcemap), modified for Oxc.
18 |
19 | [discord-badge]: https://img.shields.io/discord/1079625926024900739?logo=discord&label=Discord
20 | [discord-url]: https://discord.gg/9uXCAwqQZW
21 | [license-badge]: https://img.shields.io/badge/license-MIT-blue.svg
22 | [license-url]: https://github.com/oxc-project/oxc-sourcemap/blob/main/LICENSE
23 | [ci-badge]: https://github.com/oxc-project/oxc-sourcemap/actions/workflows/ci.yml/badge.svg?event=push&branch=main
24 | [ci-url]: https://github.com/oxc-project/oxc-sourcemap/actions/workflows/ci.yml?query=event%3Apush+branch%3Amain
25 | [code-coverage-badge]: https://codecov.io/github/oxc-project/oxc-sourcemap/branch/main/graph/badge.svg
26 | [code-coverage-url]: https://codecov.io/gh/oxc-project/oxc-sourcemap
27 | [sponsors-badge]: https://img.shields.io/github/sponsors/Boshen
28 | [sponsors-url]: https://github.com/sponsors/Boshen
29 | [codspeed-badge]: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
30 | [codspeed-url]: https://codspeed.io/oxc-project/oxc-sourcemap
31 | [crates-badge]: https://img.shields.io/crates/d/oxc_sourcemap?label=crates.io
32 | [crates-url]: https://crates.io/crates/oxc_sourcemap
33 | [docs-badge]: https://img.shields.io/docsrs/oxc_sourcemap
34 | [docs-url]: https://docs.rs/oxc_sourcemap
35 |
--------------------------------------------------------------------------------
/.github/workflows/autofix.yml:
--------------------------------------------------------------------------------
1 | name: autofix.ci # For security reasons, the workflow in which the autofix.ci action is used must be named "autofix.ci".
2 |
3 | permissions: {}
4 |
5 | on:
6 | pull_request:
7 | types: [opened, synchronize]
8 | paths-ignore:
9 | - "!.github/workflows/ci.yml"
10 | - "!.github/actions/clone-submodules/action.yml"
11 |
12 | concurrency:
13 | group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
14 | cancel-in-progress: ${{ github.ref_name != 'main' }}
15 |
16 | jobs:
17 | autofix:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1
21 |
22 | - name: Restore dprint plugin cache
23 | id: cache-restore
24 | uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
25 | with:
26 | key: dprint-${{ hashFiles('dprint.json') }}
27 | path: ~/.cache/dprint
28 |
29 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11
30 | with:
31 | restore-cache: true
32 | cache-key: warm
33 | tools: just,dprint,cargo-shear@1.3.0
34 | components: rustfmt
35 |
36 | - name: cargo-shear
37 | id: shear
38 | shell: bash
39 | run: |
40 | if ! cargo shear --fix; then
41 | echo "shear=true" >> $GITHUB_OUTPUT
42 | fi
43 |
44 | - name: Check and update lock file
45 | run: cargo check --workspace --all-features --all-targets
46 | if: ${{ steps.shear.outputs.shear == 'true' }}
47 |
48 | - run: just fmt
49 |
50 | - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # v1.3.2
51 | with:
52 | fail-fast: false
53 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | use std::{error, fmt};
2 |
3 | #[derive(Debug)]
4 | pub enum Error {
5 | /// a VLQ string was malformed and data was left over
6 | VlqLeftover,
7 | /// a VLQ string was empty and no values could be decoded.
8 | VlqNoValues,
9 | /// The input encoded a number that didn't fit into i64.
10 | VlqOverflow,
11 | /// `serde_json` parsing failure
12 | BadJson(serde_json::Error),
13 | /// a mapping segment had an unsupported size
14 | BadSegmentSize(u32),
15 | /// a reference to a non existing source was encountered
16 | BadSourceReference(u32),
17 | /// a reference to a non existing name was encountered
18 | BadNameReference(u32),
19 | }
20 | impl fmt::Display for Error {
21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 | match self {
23 | Error::VlqLeftover => write!(f, "VLQ string was malformed and data was left over"),
24 | Error::VlqNoValues => write!(f, "VLQ string was empty and no values could be decoded"),
25 | Error::VlqOverflow => write!(f, "The input encoded a number that didn't fit into i64"),
26 | Error::BadJson(err) => write!(f, "JSON parsing error: {err}"),
27 | Error::BadSegmentSize(size) => {
28 | write!(f, "Mapping segment had an unsupported size of {size}")
29 | }
30 | Error::BadSourceReference(idx) => {
31 | write!(f, "Reference to non-existing source at position {idx}")
32 | }
33 | Error::BadNameReference(idx) => {
34 | write!(f, "Reference to non-existing name at position {idx}")
35 | }
36 | }
37 | }
38 | }
39 |
40 | impl error::Error for Error {
41 | fn source(&self) -> Option<&(dyn error::Error + 'static)> {
42 | if let Self::BadJson(err) = self { Some(err) } else { None }
43 | }
44 | }
45 |
46 | /// The result of decoding.
47 | pub type Result = std::result::Result;
48 |
49 | impl From for Error {
50 | fn from(err: serde_json::Error) -> Error {
51 | Error::BadJson(err)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/AGENTS.md:
--------------------------------------------------------------------------------
1 | # AI Agents
2 |
3 | This document tracks AI agents and their contributions to the oxc-sourcemap project.
4 |
5 | ## Purpose
6 |
7 | This file serves to:
8 |
9 | - Document AI agent contributions to the project
10 | - Provide transparency about automated contributions
11 | - Track the evolution of AI-assisted development in this codebase
12 |
13 | ## Build Instructions
14 |
15 | To build and develop this project, you'll need:
16 |
17 | ### Prerequisites
18 |
19 | - [Rust toolchain](https://rustup.rs/) (version specified in `rust-toolchain.toml`)
20 | - [Just command runner](https://github.com/casey/just) for task automation
21 |
22 | ### Initial Setup
23 |
24 | `just init` has already been run, all tools (`typos-cli`, `cargo-shear`, `dprint`) are already installed, do not run `just init`.
25 |
26 | Rust and `cargo` components `clippy`, `rust-docs` and `rustfmt` has already been installed, do not install them.
27 |
28 | ### Development Workflow
29 |
30 | ```bash
31 | # Run all checks (recommended before committing)
32 | just ready
33 |
34 | # Format code
35 | just fmt
36 |
37 | # Individual commands
38 | cargo check --all-targets --all-features # Check compilation
39 | cargo test # Run tests
40 | cargo clippy --all-targets --all-features # Run linter
41 | typos # Check for typos
42 | ```
43 |
44 | The `just ready` command runs a comprehensive check including git status verification, spell checking, compilation checks, tests, linting, and formatting.
45 |
46 | ## Guidelines for AI Agent Contributions
47 |
48 | When AI agents contribute to this project:
49 |
50 | 1. **Transparency**: All AI-generated contributions should be clearly documented
51 | 2. **Review**: AI contributions must undergo the same review process as human contributions
52 | 3. **Quality**: AI contributions must meet the same quality standards as human contributions
53 | 4. **Attribution**: Significant AI contributions should be properly attributed
54 |
55 | ## Contact
56 |
57 | For questions about AI agent contributions or this documentation, please open an issue in the repository.
58 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | permissions: {}
4 |
5 | on:
6 | workflow_dispatch:
7 | pull_request:
8 | types: [opened, synchronize]
9 | paths-ignore:
10 | - "**/*.md"
11 | - "!.github/workflows/ci.yml"
12 | push:
13 | branches:
14 | - main
15 | - "renovate/**"
16 | paths-ignore:
17 | - "**/*.md"
18 | - "!.github/workflows/ci.yml"
19 |
20 | concurrency:
21 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
22 | cancel-in-progress: ${{ github.ref_name != 'main' }}
23 |
24 | defaults:
25 | run:
26 | shell: bash
27 |
28 | env:
29 | CARGO_INCREMENTAL: 0
30 | RUSTFLAGS: "-D warnings"
31 |
32 | jobs:
33 | test:
34 | name: Test
35 | strategy:
36 | fail-fast: false
37 | matrix:
38 | include:
39 | - os: windows-latest
40 | - os: ubuntu-latest
41 | - os: macos-14
42 | runs-on: ${{ matrix.os }}
43 | steps:
44 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1
45 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11
46 | with:
47 | save-cache: ${{ github.ref_name == 'main' }}
48 | - name: Clone tc39 source-map-tests
49 | run: git clone https://github.com/tc39/source-map-tests.git tests/source-map-tests
50 | - run: cargo check --all-targets --all-features
51 | - run: cargo test
52 |
53 | lint:
54 | name: Clippy
55 | runs-on: ubuntu-latest
56 | steps:
57 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1
58 |
59 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11
60 | with:
61 | components: clippy rust-docs rustfmt
62 |
63 | - run: |
64 | cargo fmt --check
65 | cargo clippy --all-targets --all-features -- -D warnings
66 | RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --document-private-items
67 |
68 | - uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # v1.40.0
69 | with:
70 | files: .
71 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "oxc_sourcemap"
3 | version = "6.0.1"
4 | authors = ["Boshen "]
5 | categories = []
6 | edition = "2024"
7 | include = ["build.rs", "/src", "/benches"]
8 | keywords = ["javascript", "sourcemap", "sourcemaps"]
9 | license = "MIT"
10 | publish = true
11 | readme = "README.md"
12 | repository = "https://github.com/oxc-project/oxc-sourcemap"
13 | rust-version = "1.88.0"
14 | description = "Basic sourcemap handling for Rust"
15 |
16 | #
17 | [lints.rust]
18 | absolute_paths_not_starting_with_crate = "warn"
19 | non_ascii_idents = "warn"
20 | unit-bindings = "warn"
21 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage)', 'cfg(coverage_nightly)'] }
22 |
23 | [lints.clippy]
24 | all = { level = "warn", priority = -1 }
25 | # restriction
26 | dbg_macro = "warn"
27 | todo = "warn"
28 | unimplemented = "warn"
29 | print_stdout = "warn" # Must be opt-in
30 | print_stderr = "warn" # Must be opt-in
31 | allow_attributes = "warn"
32 | # I like the explicitness of this rule as it removes confusion around `clone`.
33 | # This increases readability, avoids `clone` mindlessly and heap allocating by accident.
34 | clone_on_ref_ptr = "warn"
35 | # These two are mutually exclusive, I like `mod.rs` files for better fuzzy searches on module entries.
36 | self_named_module_files = "warn" # "-Wclippy::mod_module_files"
37 | empty_drop = "warn"
38 | empty_structs_with_brackets = "warn"
39 | exit = "warn"
40 | filetype_is_file = "warn"
41 | get_unwrap = "warn"
42 | impl_trait_in_params = "warn"
43 | rc_buffer = "warn"
44 | rc_mutex = "warn"
45 | rest_pat_in_fully_bound_structs = "warn"
46 | unnecessary_safety_comment = "warn"
47 | infinite_loop = "warn"
48 | undocumented_unsafe_blocks = "allow"
49 |
50 | [lib]
51 | doctest = false
52 | crate-type = ["lib", "cdylib"]
53 |
54 | [[bench]]
55 | name = "simple"
56 | harness = false
57 |
58 | [package.metadata.cargo-shear]
59 | ignored = ["napi"]
60 |
61 | [dependencies]
62 | base64-simd = "0.8"
63 | json-escape-simd = "3"
64 | napi = { version = "3", optional = true }
65 | napi-derive = { version = "3", optional = true }
66 | rustc-hash = "2"
67 | serde = { version = "1", features = ["derive"] }
68 | serde_json = { version = "1" }
69 |
70 | [build-dependencies]
71 | napi-build = { version = "2", optional = true }
72 |
73 | [dev-dependencies]
74 | criterion2 = { version = "3.0.2", default-features = false }
75 | insta = { version = "1.43.2", features = ["glob"] }
76 |
77 | [features]
78 | default = []
79 | napi = ["dep:napi", "dep:napi-derive", "dep:napi-build"]
80 | codspeed = ["criterion2/codspeed"]
81 |
82 | [profile.bench]
83 | lto = true
84 | codegen-units = 1
85 |
--------------------------------------------------------------------------------
/tests/fixtures/esbuild/example.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "sources": ["index.tsx", "counter.tsx"],
4 | "sourcesContent": ["import { h, Fragment, render } from 'preact'\nimport { CounterClass, CounterFunction } from './counter'\n\nrender(\n <>\n \n \n >,\n document.getElementById('root')!,\n)\n", "import { h, Component } from 'preact'\nimport { useState } from 'preact/hooks'\n\ninterface CounterProps {\n label_: string\n initialValue_: number\n}\n\ninterface CounterState {\n value_: number\n}\n\nexport class CounterClass extends Component {\n state: CounterState\n\n constructor(props: CounterProps) {\n super(props)\n this.state.value_ = props.initialValue_\n }\n\n increment_ = () => this.setState({ value_: this.state.value_ + 1 })\n decrement_ = () => this.setState({ value_: this.state.value_ - 1 })\n\n render() {\n return \n
{this.props.label}
\n
\n \n {' '}\n {this.state.value_}\n {' '}\n \n
\n
\n }\n}\n\nexport let CounterFunction = (props: CounterProps) => {\n let [value, setValue] = useState(props.initialValue_)\n return \n
{props.label_}
\n
\n \n {' '}\n {value}\n {' '}\n \n
\n
\n}\n"],
5 | "mappings": ";AAAA,SAAS,KAAAA,GAAG,YAAAC,GAAU,UAAAC,SAAc;;;ACApC,SAAS,KAAAC,GAAG,aAAAC,SAAiB;AAC7B,SAAS,YAAAC,SAAgB;AAWlB,IAAMC,IAAN,cAA2BF,EAAsC;AAAA,EAGtE,YAAYG,GAAqB;AAC/B,UAAMA,CAAK;AAIb,SAAAC,IAAa,MAAM,KAAK,SAAS,EAAEC,GAAQ,KAAK,MAAMA,IAAS,EAAE,CAAC;AAClE,SAAAC,IAAa,MAAM,KAAK,SAAS,EAAED,GAAQ,KAAK,MAAMA,IAAS,EAAE,CAAC;AAJhE,SAAK,MAAMA,IAASF,EAAMI;AAAA,EAC5B;AAAA,EAKA,SAAS;AACP,WAAOR,EAAC;AAAA,MAAI,OAAM;AAAA,OAChBA,EAAC,YAAI,KAAK,MAAM,KAAM,GACtBA,EAAC,WACCA,EAAC;AAAA,MAAO,SAAS,KAAKO;AAAA,OAAY,GAAC,GAClC,KACA,KAAK,MAAMD,GACX,KACDN,EAAC;AAAA,MAAO,SAAS,KAAKK;AAAA,OAAY,GAAC,CACrC,CACF;AAAA,EACF;AACF,GAEWI,IAAkB,CAACL,MAAwB;AACpD,MAAI,CAACM,GAAOC,CAAQ,IAAIT,EAASE,EAAMI,CAAa;AACpD,SAAOR,EAAC;AAAA,IAAI,OAAM;AAAA,KAChBA,EAAC,YAAII,EAAMQ,CAAO,GAClBZ,EAAC,WACCA,EAAC;AAAA,IAAO,SAAS,MAAMW,EAASD,IAAQ,CAAC;AAAA,KAAG,GAAC,GAC5C,KACAA,GACA,KACDV,EAAC;AAAA,IAAO,SAAS,MAAMW,EAASD,IAAQ,CAAC;AAAA,KAAG,GAAC,CAC/C,CACF;AACF;;;AD9CAG;AAAA,EACEC,EAAAC,GAAA,MACED,EAACE,GAAA;AAAA,IAAaC,GAAO;AAAA,IAAYC,GAAe;AAAA,GAAK,GACrDJ,EAACK,GAAA;AAAA,IAAgBF,GAAO;AAAA,IAAYC,GAAe;AAAA,GAAK,CAC1D;AAAA,EACA,SAAS,eAAe,MAAM;AAChC;",
6 | "names": ["h", "Fragment", "render", "h", "Component", "useState", "CounterClass", "props", "increment_", "value_", "decrement_", "initialValue_", "CounterFunction", "value", "setValue", "label_", "render", "h", "Fragment", "CounterClass", "label_", "initialValue_", "CounterFunction"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/sourcemap_builder.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use rustc_hash::FxHashMap;
4 |
5 | use crate::{
6 | SourceMap,
7 | token::{Token, TokenChunk},
8 | };
9 |
10 | /// The `SourceMapBuilder` is a helper to generate sourcemap.
11 | #[derive(Debug, Default)]
12 | pub struct SourceMapBuilder {
13 | pub(crate) file: Option>,
14 | pub(crate) names_map: FxHashMap, u32>,
15 | pub(crate) names: Vec>,
16 | pub(crate) sources: Vec>,
17 | pub(crate) sources_map: FxHashMap, u32>,
18 | pub(crate) source_contents: Vec
\n" --> (20:11) ")"
113 | (33:4) "\n" --> (20:12) ");\n"
114 | (33:4) "\n" --> (21:0) " "
115 | (34:2) "}\n" --> (21:2) "}\n"
116 | (35:0) "}\n" --> (22:0) "}, "
117 | (37:11) "CounterFunction = " --> (22:3) "s = "
118 | (37:29) "(" --> (22:7) "("
119 | (37:30) "props: CounterProps) => " --> (22:8) "r) => "
120 | (37:54) "{\n" --> (22:14) "{\n"
121 | (38:2) "let " --> (23:0) " let "
122 | (38:6) "[" --> (23:6) "["
123 | (38:7) "value, " --> (23:7) "o, "
124 | (38:14) "setValue" --> (23:10) "e"
125 | (38:22) "] = " --> (23:11) "] = "
126 | (38:26) "useState(" --> (23:15) "a("
127 | (38:35) "props." --> (23:17) "r."
128 | (38:41) "initialValue_" --> (23:19) "e"
129 | (38:54) ")\n" --> (23:20) ");\n"
130 | (39:2) "return " --> (24:0) " return "
131 | (39:9) "<" --> (24:9) "t("
132 | (39:10) "div " --> (24:11) "\"div\", {\n"
133 | (39:10) "div " --> (25:0) " "
134 | (39:14) "class=" --> (25:4) "class: "
135 | (39:20) "\"counter\">\n" --> (25:11) "\"counter\"\n"
136 | (39:20) "\"counter\">\n" --> (26:0) " }, "
137 | (40:4) "<" --> (26:5) "t("
138 | (40:5) "h1>{" --> (26:7) "\"h1\", null, "
139 | (40:9) "props." --> (26:19) "r."
140 | (40:15) "label_}" --> (26:21) "o"
141 | (40:22) "\n" --> (26:22) "), "
142 | (41:4) "<" --> (26:25) "t("
143 | (41:5) "p>\n" --> (26:27) "\"p\", null, "
144 | (42:6) "<" --> (26:38) "t("
145 | (42:7) "button " --> (26:40) "\"button\", {\n"
146 | (42:7) "button " --> (27:0) " "
147 | (42:14) "onClick={" --> (27:4) "onClick: "
148 | (42:23) "() => " --> (27:13) "() => "
149 | (42:29) "setValue(" --> (27:19) "e("
150 | (42:38) "value - " --> (27:21) "o - "
151 | (42:46) "1" --> (27:25) "1"
152 | (42:47) ")}>" --> (27:26) ")\n"
153 | (42:47) ")}>" --> (28:0) " }, "
154 | (42:50) "-" --> (28:5) "\"-\""
155 | (42:51) "\n" --> (28:8) "), "
156 | (43:7) "' '}\n" --> (28:11) "\" \", "
157 | (44:7) "value}\n" --> (28:16) "o, "
158 | (45:7) "' '}\n" --> (28:19) "\" \", "
159 | (46:6) "<" --> (28:24) "t("
160 | (46:7) "button " --> (28:26) "\"button\", {\n"
161 | (46:7) "button " --> (29:0) " "
162 | (46:14) "onClick={" --> (29:4) "onClick: "
163 | (46:23) "() => " --> (29:13) "() => "
164 | (46:29) "setValue(" --> (29:19) "e("
165 | (46:38) "value + " --> (29:21) "o + "
166 | (46:46) "1" --> (29:25) "1"
167 | (46:47) ")}>" --> (29:26) ")\n"
168 | (46:47) ")}>" --> (30:0) " }, "
169 | (46:50) "+" --> (30:5) "\"+\""
170 | (46:51) "\n" --> (30:8) ")"
171 | (47:4) "\n" --> (30:9) ")"
172 | (48:2) "\n" --> (30:10) ");\n"
173 | (49:0) "}\n" --> (31:0) "};\n"
174 | - index.tsx
175 | (3:0) "render(\n" --> (34:0) "c(\n"
176 | (3:0) "render(\n" --> (35:0) " "
177 | (4:2) "<>\n" --> (35:2) "u("
178 | (4:2) "<>\n" --> (35:4) "l, "
179 | (4:2) "<>\n" --> (35:7) "null, "
180 | (5:4) "<" --> (35:13) "u("
181 | (5:5) "CounterClass " --> (35:15) "n, "
182 | (5:5) "CounterClass " --> (35:18) "{\n"
183 | (5:5) "CounterClass " --> (36:0) " "
184 | (5:18) "label_=" --> (36:4) "o: "
185 | (5:25) "\"Counter 1\" " --> (36:7) "\"Counter 1\",\n"
186 | (5:25) "\"Counter 1\" " --> (37:0) " "
187 | (5:37) "initialValue_={" --> (37:4) "e: "
188 | (5:52) "100} " --> (37:7) "100\n"
189 | (5:52) "100} " --> (38:0) " }"
190 | (5:57) "/>\n" --> (38:3) "), "
191 | (6:4) "<" --> (38:6) "u("
192 | (6:5) "CounterFunction " --> (38:8) "s, "
193 | (6:5) "CounterFunction " --> (38:11) "{\n"
194 | (6:5) "CounterFunction " --> (39:0) " "
195 | (6:21) "label_=" --> (39:4) "o: "
196 | (6:28) "\"Counter 2\" " --> (39:7) "\"Counter 2\",\n"
197 | (6:28) "\"Counter 2\" " --> (40:0) " "
198 | (6:40) "initialValue_={" --> (40:4) "e: "
199 | (6:55) "200} " --> (40:7) "200\n"
200 | (6:55) "200} " --> (41:0) " }"
201 | (6:60) "/>\n" --> (41:3) ")"
202 | (7:2) ">,\n" --> (41:4) "),\n"
203 | (7:2) ">,\n" --> (42:0) " "
204 | (8:2) "document." --> (42:2) "document."
205 | (8:11) "getElementById(" --> (42:11) "getElementById("
206 | (8:26) "'root'" --> (42:26) "\"root\""
207 | (8:32) ")!,\n" --> (42:32) ")\n"
208 | (9:0) ")\n" --> (43:0) ");\n"
209 |
--------------------------------------------------------------------------------
/tests/tc39_spec_tests.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::print_stdout, clippy::print_stderr)]
2 |
3 | use std::{fs, path::PathBuf};
4 |
5 | use oxc_sourcemap::SourceMap;
6 | use serde::Deserialize;
7 |
8 | #[derive(Debug, Deserialize)]
9 | struct TestCase {
10 | name: String,
11 | #[expect(dead_code)]
12 | description: String,
13 | #[serde(rename = "baseFile")]
14 | #[expect(dead_code)]
15 | base_file: String,
16 | #[serde(rename = "sourceMapFile")]
17 | source_map_file: String,
18 | #[serde(rename = "sourceMapIsValid")]
19 | source_map_is_valid: bool,
20 | #[serde(default, rename = "testActions")]
21 | test_actions: Vec,
22 | }
23 |
24 | #[derive(Debug, Deserialize)]
25 | #[serde(tag = "actionType")]
26 | enum TestAction {
27 | #[serde(rename = "checkMapping")]
28 | CheckMapping {
29 | #[serde(rename = "generatedLine")]
30 | generated_line: u32,
31 | #[serde(rename = "generatedColumn")]
32 | generated_column: u32,
33 | #[serde(rename = "originalSource")]
34 | original_source: Option,
35 | #[serde(rename = "originalLine")]
36 | original_line: Option,
37 | #[serde(rename = "originalColumn")]
38 | original_column: Option,
39 | #[serde(rename = "mappedName")]
40 | mapped_name: Option,
41 | },
42 | #[serde(rename = "checkIgnoreList")]
43 | CheckIgnoreList { present: Vec },
44 | #[serde(other)]
45 | Unknown,
46 | }
47 |
48 | #[derive(Debug, Deserialize)]
49 | struct TestSuite {
50 | tests: Vec,
51 | }
52 |
53 | #[test]
54 | fn tc39_source_map_spec_tests() {
55 | let test_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
56 | .join("tests/source-map-tests/source-map-spec-tests.json");
57 | let resources_dir =
58 | PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/source-map-tests/resources");
59 |
60 | // Skip test if tc39 tests haven't been cloned yet
61 | if !test_file.exists() {
62 | eprintln!("Skipping tc39 tests - run 'just init' to clone the test suite");
63 | return;
64 | }
65 |
66 | let test_suite: TestSuite =
67 | serde_json::from_str(&fs::read_to_string(test_file).unwrap()).unwrap();
68 |
69 | let mut passed = 0;
70 | let mut failed = 0;
71 | let mut skipped = 0;
72 |
73 | for test in test_suite.tests {
74 | // Skip tests with null sources - not supported in our API
75 | if test.name == "sourcesAndSourcesContentBothNull"
76 | || test.name == "sourcesNullSourcesContentNonNull"
77 | {
78 | skipped += 1;
79 | continue;
80 | }
81 |
82 | let source_map_path = resources_dir.join(&test.source_map_file);
83 | let source_map_content = match fs::read_to_string(&source_map_path) {
84 | Ok(content) => content,
85 | Err(_) => {
86 | eprintln!("⊘ {}: skipped (file not found)", test.name);
87 | skipped += 1;
88 | continue;
89 | }
90 | };
91 |
92 | let result = SourceMap::from_json_string(&source_map_content);
93 |
94 | // Check if parsing result matches expected validity
95 | let parse_result_valid = result.is_ok();
96 | if parse_result_valid != test.source_map_is_valid {
97 | eprintln!(
98 | "✗ {}: expected valid={}, got valid={}",
99 | test.name, test.source_map_is_valid, parse_result_valid
100 | );
101 | if let Err(e) = result {
102 | eprintln!(" Error: {:?}", e);
103 | }
104 | failed += 1;
105 | continue;
106 | }
107 |
108 | // If the source map is valid, run test actions
109 | if let Ok(source_map) = result
110 | && !run_test_actions(&test, &source_map, &resources_dir)
111 | {
112 | failed += 1;
113 | continue;
114 | }
115 |
116 | passed += 1;
117 | }
118 |
119 | println!("\n{} passed, {} failed, {} skipped", passed, failed, skipped);
120 |
121 | // Don't panic on failures - some tests are expected to fail for unimplemented features
122 | // (index maps, sourceRoot resolution, etc.) and we skip tests with null sources
123 | assert!(passed >= 86, "Expected at least 86 tests to pass, but only {} passed", passed);
124 | }
125 |
126 | fn run_test_actions(test: &TestCase, source_map: &SourceMap, _resources_dir: &PathBuf) -> bool {
127 | for action in &test.test_actions {
128 | match action {
129 | TestAction::CheckMapping {
130 | generated_line,
131 | generated_column,
132 | original_source,
133 | original_line,
134 | original_column,
135 | mapped_name,
136 | } => {
137 | let lookup_table = source_map.generate_lookup_table();
138 | let token = source_map.lookup_source_view_token(
139 | &lookup_table,
140 | *generated_line,
141 | *generated_column,
142 | );
143 |
144 | if let Some(token) = token {
145 | let (source, src_line, src_col, name) = token.to_tuple();
146 |
147 | // Check source
148 | if let Some(expected_source) = original_source {
149 | if source.map(|s| s.as_ref()) != Some(expected_source.as_str()) {
150 | eprintln!(
151 | "✗ {}: mapping check failed - expected source '{}', got {:?}",
152 | test.name,
153 | expected_source,
154 | source.map(|s| s.as_ref())
155 | );
156 | return false;
157 | }
158 | } else if source.is_some() {
159 | eprintln!(
160 | "✗ {}: mapping check failed - expected no source, got {:?}",
161 | test.name,
162 | source.map(|s| s.as_ref())
163 | );
164 | return false;
165 | }
166 |
167 | // Check line and column
168 | if let (Some(exp_line), Some(exp_col)) = (original_line, original_column)
169 | && (src_line != *exp_line || src_col != *exp_col)
170 | {
171 | eprintln!(
172 | "✗ {}: mapping check failed - expected {}:{}, got {}:{}",
173 | test.name, exp_line, exp_col, src_line, src_col
174 | );
175 | return false;
176 | }
177 |
178 | // Check name
179 | let actual_name = name.map(|n| n.as_ref());
180 | let expected_name = mapped_name.as_ref().map(|s| s.as_str());
181 | if actual_name != expected_name {
182 | eprintln!(
183 | "✗ {}: mapping check failed - expected name {:?}, got {:?}",
184 | test.name, expected_name, actual_name
185 | );
186 | return false;
187 | }
188 | } else {
189 | eprintln!(
190 | "✗ {}: mapping check failed - no token found at {}:{}",
191 | test.name, generated_line, generated_column
192 | );
193 | return false;
194 | }
195 | }
196 | TestAction::CheckIgnoreList { present } => {
197 | let ignore_list = source_map.get_x_google_ignore_list();
198 | if let Some(indices) = ignore_list {
199 | for source_name in present {
200 | // Find the index of this source in the sources array
201 | let source_index =
202 | source_map.get_sources().position(|s| s.as_ref() == source_name);
203 |
204 | if let Some(idx) = source_index {
205 | if !indices.contains(&(idx as u32)) {
206 | eprintln!(
207 | "✗ {}: ignore list check failed - '{}' not in ignore list",
208 | test.name, source_name
209 | );
210 | return false;
211 | }
212 | } else {
213 | eprintln!(
214 | "✗ {}: ignore list check failed - source '{}' not found",
215 | test.name, source_name
216 | );
217 | return false;
218 | }
219 | }
220 | } else if !present.is_empty() {
221 | eprintln!("✗ {}: ignore list check failed - no ignore list found", test.name);
222 | return false;
223 | }
224 | }
225 | TestAction::Unknown => {
226 | eprintln!("⊘ {}: unknown test action, skipping", test.name);
227 | }
228 | }
229 | }
230 |
231 | true
232 | }
233 |
--------------------------------------------------------------------------------
/src/decode.rs:
--------------------------------------------------------------------------------
1 | /// Port from https://github.com/getsentry/rust-sourcemap/blob/9.1.0/src/decoder.rs
2 | /// It is a helper for decode vlq soucemap string to `SourceMap`.
3 | use std::sync::Arc;
4 |
5 | use crate::error::{Error, Result};
6 | use crate::token::INVALID_ID;
7 | use crate::{SourceMap, Token};
8 |
9 | /// See .
10 | #[derive(serde::Deserialize)]
11 | #[serde(rename_all = "camelCase")]
12 | pub struct JSONSourceMap {
13 | /// The version field, must be 3.
14 | #[serde(deserialize_with = "deserialize_version")]
15 | pub version: u32,
16 | /// An optional name of the generated code that this source map is associated with.
17 | pub file: Option,
18 | /// A string with the encoded mapping data.
19 | pub mappings: String,
20 | /// An optional source root, useful for relocating source files on a server or removing repeated values in the "sources" entry.
21 | /// This value is prepended to the individual entries in the "source" field.
22 | pub source_root: Option,
23 | /// A list of original sources used by the "mappings" entry.
24 | pub sources: Vec,
25 | /// An optional list of source content, useful when the "source" can't be hosted.
26 | /// The contents are listed in the same order as the sources in line 5. "null" may be used if some original sources should be retrieved by name.
27 | pub sources_content: Option>>,
28 | /// A list of symbol names used by the "mappings" entry.
29 | #[serde(default)]
30 | pub names: Vec,
31 | /// An optional field containing the debugId for this sourcemap.
32 | pub debug_id: Option,
33 | /// Identifies third-party sources (such as framework code or bundler-generated code), allowing developers to avoid code that they don't want to see or step through, without having to configure this beforehand.
34 | /// The `x_google_ignoreList` field refers to the `sources` array, and lists the indices of all the known third-party sources in that source map.
35 | /// When parsing the source map, developer tools can use this to determine sections of the code that the browser loads and runs that could be automatically ignore-listed.
36 | #[serde(rename = "x_google_ignoreList", alias = "ignoreList")]
37 | pub x_google_ignore_list: Option>,
38 | }
39 |
40 | fn deserialize_version<'de, D>(deserializer: D) -> std::result::Result
41 | where
42 | D: serde::Deserializer<'de>,
43 | {
44 | use serde::Deserialize;
45 | let version = u32::deserialize(deserializer)?;
46 | if version != 3 {
47 | return Err(serde::de::Error::custom(format!("unsupported source map version: {version}")));
48 | }
49 | Ok(version)
50 | }
51 |
52 | pub fn decode(json: JSONSourceMap) -> Result {
53 | // Validate x_google_ignore_list indices
54 | if let Some(ref ignore_list) = json.x_google_ignore_list {
55 | for &idx in ignore_list {
56 | if idx >= json.sources.len() as u32 {
57 | return Err(Error::BadSourceReference(idx));
58 | }
59 | }
60 | }
61 |
62 | let tokens = decode_mapping(&json.mappings, json.names.len(), json.sources.len())?;
63 | Ok(SourceMap {
64 | file: json.file.map(Arc::from),
65 | names: json.names.into_iter().map(Arc::from).collect(),
66 | source_root: json.source_root,
67 | sources: json.sources.into_iter().map(Arc::from).collect(),
68 | source_contents: json
69 | .sources_content
70 | .map(|content| content.into_iter().map(|c| c.map(Arc::from)).collect())
71 | .unwrap_or_default(),
72 | tokens: tokens.into_boxed_slice(),
73 | token_chunks: None,
74 | x_google_ignore_list: json.x_google_ignore_list,
75 | debug_id: json.debug_id,
76 | })
77 | }
78 |
79 | pub fn decode_from_string(value: &str) -> Result {
80 | decode(serde_json::from_str(value)?)
81 | }
82 |
83 | fn decode_mapping(mapping: &str, names_len: usize, sources_len: usize) -> Result> {
84 | // Estimate initial capacity: average ~5 chars per token (4 VLQ values + comma/semicolon)
85 | let estimated_tokens = mapping.len() / 5;
86 | let mut tokens = Vec::with_capacity(estimated_tokens);
87 |
88 | let mut dst_col;
89 | let mut src_id = 0;
90 | let mut src_line = 0;
91 | let mut src_col = 0;
92 | let mut name_id = 0;
93 | let mut nums = Vec::with_capacity(6);
94 |
95 | for (dst_line, line) in mapping.split(';').enumerate() {
96 | if line.is_empty() {
97 | continue;
98 | }
99 |
100 | dst_col = 0;
101 |
102 | for segment in line.split(',') {
103 | if segment.is_empty() {
104 | continue;
105 | }
106 |
107 | nums.clear();
108 | parse_vlq_segment_into(segment, &mut nums)?;
109 |
110 | if nums.is_empty() {
111 | return Err(Error::BadSegmentSize(0));
112 | }
113 |
114 | let new_dst_col = i64::from(dst_col) + nums[0];
115 | if new_dst_col < 0 {
116 | return Err(Error::BadSegmentSize(0)); // Negative column
117 | }
118 | dst_col = new_dst_col as u32;
119 |
120 | let mut src = INVALID_ID;
121 | let mut name = INVALID_ID;
122 |
123 | if nums.len() > 1 {
124 | if nums.len() != 4 && nums.len() != 5 {
125 | return Err(Error::BadSegmentSize(nums.len() as u32));
126 | }
127 |
128 | let new_src_id = i64::from(src_id) + nums[1];
129 | if new_src_id < 0 || new_src_id >= sources_len as i64 {
130 | return Err(Error::BadSourceReference(src_id));
131 | }
132 | src_id = new_src_id as u32;
133 | src = src_id;
134 |
135 | let new_src_line = i64::from(src_line) + nums[2];
136 | if new_src_line < 0 {
137 | return Err(Error::BadSegmentSize(0)); // Negative line
138 | }
139 | src_line = new_src_line as u32;
140 |
141 | let new_src_col = i64::from(src_col) + nums[3];
142 | if new_src_col < 0 {
143 | return Err(Error::BadSegmentSize(0)); // Negative column
144 | }
145 | src_col = new_src_col as u32;
146 |
147 | if nums.len() > 4 {
148 | name_id = (i64::from(name_id) + nums[4]) as u32;
149 | if name_id >= names_len as u32 {
150 | return Err(Error::BadNameReference(name_id));
151 | }
152 | name = name_id;
153 | }
154 | }
155 |
156 | tokens.push(Token::new(
157 | dst_line as u32,
158 | dst_col,
159 | src_line,
160 | src_col,
161 | if src == INVALID_ID { None } else { Some(src) },
162 | if name == INVALID_ID { None } else { Some(name) },
163 | ));
164 | }
165 | }
166 |
167 | Ok(tokens)
168 | }
169 |
170 | // Align B64 lookup table on 64-byte boundary for better cache performance
171 | #[repr(align(64))]
172 | struct Aligned64([i8; 256]);
173 |
174 | #[rustfmt::skip]
175 | static B64: Aligned64 = Aligned64([ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ]);
176 |
177 | fn parse_vlq_segment_into(segment: &str, rv: &mut Vec) -> Result<()> {
178 | let mut cur = 0i64;
179 | let mut shift = 0u32;
180 |
181 | for c in segment.bytes() {
182 | // SAFETY: B64 is a 256-element lookup table, and c is a u8 (0-255)
183 | let enc = unsafe { i64::from(*B64.0.get_unchecked(c as usize)) };
184 | let val = enc & 0b11111;
185 | let cont = enc >> 5;
186 |
187 | // Check if shift would overflow before applying
188 | if shift >= 64 {
189 | return Err(Error::VlqOverflow);
190 | }
191 |
192 | // For large shifts, check if the value would fit in 32 bits when decoded
193 | if shift <= 62 {
194 | cur = cur.wrapping_add(val << shift);
195 | } else {
196 | // Beyond 62 bits of shift, we'd overflow i64
197 | return Err(Error::VlqOverflow);
198 | }
199 |
200 | shift += 5;
201 |
202 | if cont == 0 {
203 | let sign = cur & 1;
204 | cur >>= 1;
205 | if sign != 0 {
206 | cur = -cur;
207 | }
208 | rv.push(cur);
209 | cur = 0;
210 | shift = 0;
211 | }
212 | }
213 |
214 | if cur != 0 || shift != 0 {
215 | Err(Error::VlqLeftover)
216 | } else if rv.is_empty() {
217 | Err(Error::VlqNoValues)
218 | } else {
219 | Ok(())
220 | }
221 | }
222 |
223 | #[test]
224 | fn test_decode_sourcemap() {
225 | let input = r#"{
226 | "version": 3,
227 | "sources": ["coolstuff.js"],
228 | "sourceRoot": "x",
229 | "names": ["x","alert"],
230 | "mappings": "AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM",
231 | "x_google_ignoreList": [0]
232 | }"#;
233 | let sm = SourceMap::from_json_string(input).unwrap();
234 | assert_eq!(sm.get_source_root(), Some("x"));
235 | assert_eq!(sm.get_x_google_ignore_list(), Some(&[0][..]));
236 | let mut iter = sm.get_source_view_tokens().filter(|token| token.get_name_id().is_some());
237 | assert_eq!(
238 | iter.next().unwrap().to_tuple(),
239 | (Some(&"coolstuff.js".into()), 0, 4, Some(&"x".into()))
240 | );
241 | assert_eq!(
242 | iter.next().unwrap().to_tuple(),
243 | (Some(&"coolstuff.js".into()), 1, 4, Some(&"x".into()))
244 | );
245 | assert_eq!(
246 | iter.next().unwrap().to_tuple(),
247 | (Some(&"coolstuff.js".into()), 2, 2, Some(&"alert".into()))
248 | );
249 | assert!(iter.next().is_none());
250 | }
251 |
252 | #[test]
253 | fn test_decode_sourcemap_optional_filed() {
254 | let input = r#"{
255 | "version": 3,
256 | "names": [],
257 | "sources": [],
258 | "sourcesContent": [null],
259 | "mappings": ""
260 | }"#;
261 | SourceMap::from_json_string(input).expect("should success");
262 | }
263 |
--------------------------------------------------------------------------------
/src/concat_sourcemap_builder.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use crate::{SourceMap, Token, token::TokenChunk};
4 |
5 | /// The `ConcatSourceMapBuilder` is a helper to concat sourcemaps.
6 | #[derive(Debug, Default)]
7 | pub struct ConcatSourceMapBuilder {
8 | pub(crate) names: Vec>,
9 | pub(crate) sources: Vec>,
10 | pub(crate) source_contents: Vec