├── 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>>, 19 | pub(crate) tokens: Vec, 20 | pub(crate) token_chunks: Option>, 21 | } 22 | 23 | impl SourceMapBuilder { 24 | /// Add item to `SourceMap::name`. 25 | pub fn add_name(&mut self, name: &str) -> u32 { 26 | if let Some(&id) = self.names_map.get(name) { 27 | return id; 28 | } 29 | let count = self.names.len() as u32; 30 | let name = Arc::from(name); 31 | self.names_map.insert(Arc::clone(&name), count); 32 | self.names.push(name); 33 | count 34 | } 35 | 36 | /// Add item to `SourceMap::sources` and `SourceMap::source_contents`. 37 | /// If `source` maybe duplicate, please use it. 38 | pub fn add_source_and_content(&mut self, source: &str, source_content: &str) -> u32 { 39 | let count = self.sources.len() as u32; 40 | let id = *self.sources_map.entry(source.into()).or_insert(count); 41 | if id == count { 42 | self.sources.push(source.into()); 43 | self.source_contents.push(Some(source_content.into())); 44 | } 45 | id 46 | } 47 | 48 | /// Add item to `SourceMap::sources` and `SourceMap::source_contents`. 49 | /// If `source` hasn't duplicate,it will avoid extra hash calculation. 50 | pub fn set_source_and_content(&mut self, source: &str, source_content: &str) -> u32 { 51 | let count = self.sources.len() as u32; 52 | self.sources.push(source.into()); 53 | self.source_contents.push(Some(source_content.into())); 54 | count 55 | } 56 | 57 | /// Add item to `SourceMap::tokens`. 58 | pub fn add_token( 59 | &mut self, 60 | dst_line: u32, 61 | dst_col: u32, 62 | src_line: u32, 63 | src_col: u32, 64 | src_id: Option, 65 | name_id: Option, 66 | ) { 67 | self.tokens.push(Token::new(dst_line, dst_col, src_line, src_col, src_id, name_id)); 68 | } 69 | 70 | pub fn set_file(&mut self, file: &str) { 71 | self.file = Some(file.into()); 72 | } 73 | 74 | /// Set the `SourceMap::token_chunks` to make the sourcemap to vlq mapping at parallel. 75 | pub fn set_token_chunks(&mut self, token_chunks: Vec) { 76 | self.token_chunks = Some(token_chunks); 77 | } 78 | 79 | pub fn into_sourcemap(mut self) -> SourceMap { 80 | // Trade performance for memory. 81 | // The tokens array take enormously large amount of data, 82 | // which is not ideal for large applications. 83 | self.names_map.shrink_to_fit(); 84 | self.names.shrink_to_fit(); 85 | self.sources.shrink_to_fit(); 86 | self.sources_map.shrink_to_fit(); 87 | // For checker.ts, capacity for `tokens` before and after are 262144 and 171174 respectively. 88 | self.tokens.shrink_to_fit(); 89 | if let Some(c) = self.token_chunks.as_mut() { 90 | c.shrink_to_fit() 91 | } 92 | SourceMap::new( 93 | self.file, 94 | self.names, 95 | None, 96 | self.sources, 97 | self.source_contents, 98 | self.tokens.into_boxed_slice(), 99 | self.token_chunks, 100 | ) 101 | } 102 | } 103 | 104 | #[test] 105 | fn test_sourcemap_builder() { 106 | let mut builder = SourceMapBuilder::default(); 107 | builder.set_source_and_content("baz.js", ""); 108 | builder.add_name("x"); 109 | builder.set_file("file"); 110 | 111 | let sm = builder.into_sourcemap(); 112 | assert_eq!(sm.get_source(0).map(|s| s.as_ref()), Some("baz.js")); 113 | assert_eq!(sm.get_name(0).map(|s| s.as_ref()), Some("x")); 114 | assert_eq!(sm.get_file().map(|s| s.as_ref()), Some("file")); 115 | 116 | let expected = r#"{"version":3,"file":"file","names":["x"],"sources":["baz.js"],"sourcesContent":[""],"mappings":""}"#; 117 | assert_eq!(expected, sm.to_json_string()); 118 | } 119 | -------------------------------------------------------------------------------- /src/token.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::SourceMap; 4 | 5 | /// Sentinel value representing an invalid/missing ID for source or name. 6 | /// Used when a token doesn't have an associated source file or name. 7 | pub(crate) const INVALID_ID: u32 = u32::MAX; 8 | 9 | /// The `Token` is used to generate vlq `mappings`. 10 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 11 | pub struct Token { 12 | pub(crate) dst_line: u32, 13 | pub(crate) dst_col: u32, 14 | pub(crate) src_line: u32, 15 | pub(crate) src_col: u32, 16 | source_id: u32, 17 | name_id: u32, 18 | } 19 | 20 | impl Token { 21 | pub fn new( 22 | dst_line: u32, 23 | dst_col: u32, 24 | src_line: u32, 25 | src_col: u32, 26 | source_id: Option, 27 | name_id: Option, 28 | ) -> Self { 29 | Self { 30 | dst_line, 31 | dst_col, 32 | src_line, 33 | src_col, 34 | source_id: source_id.unwrap_or(INVALID_ID), 35 | name_id: name_id.unwrap_or(INVALID_ID), 36 | } 37 | } 38 | 39 | pub fn get_dst_line(&self) -> u32 { 40 | self.dst_line 41 | } 42 | 43 | pub fn get_dst_col(&self) -> u32 { 44 | self.dst_col 45 | } 46 | 47 | pub fn get_src_line(&self) -> u32 { 48 | self.src_line 49 | } 50 | 51 | pub fn get_src_col(&self) -> u32 { 52 | self.src_col 53 | } 54 | 55 | pub fn get_name_id(&self) -> Option { 56 | if self.name_id == INVALID_ID { None } else { Some(self.name_id) } 57 | } 58 | 59 | pub fn get_source_id(&self) -> Option { 60 | if self.source_id == INVALID_ID { None } else { Some(self.source_id) } 61 | } 62 | } 63 | 64 | #[derive(Debug, Clone, Default, PartialEq, Eq)] 65 | pub struct TokenChunk { 66 | pub start: u32, 67 | pub end: u32, 68 | pub prev_dst_line: u32, 69 | pub prev_dst_col: u32, 70 | pub prev_src_line: u32, 71 | pub prev_src_col: u32, 72 | pub prev_name_id: u32, 73 | pub prev_source_id: u32, 74 | } 75 | 76 | impl TokenChunk { 77 | #[expect(clippy::too_many_arguments)] 78 | pub fn new( 79 | start: u32, 80 | end: u32, 81 | prev_dst_line: u32, 82 | prev_dst_col: u32, 83 | prev_src_line: u32, 84 | prev_src_col: u32, 85 | prev_name_id: u32, 86 | prev_source_id: u32, 87 | ) -> Self { 88 | Self { 89 | start, 90 | end, 91 | prev_dst_line, 92 | prev_dst_col, 93 | prev_src_line, 94 | prev_src_col, 95 | prev_name_id, 96 | prev_source_id, 97 | } 98 | } 99 | } 100 | 101 | /// The `SourceViewToken` provider extra `source` and `source_content` value. 102 | #[derive(Debug, Clone, Copy)] 103 | pub struct SourceViewToken<'a> { 104 | pub(crate) token: Token, 105 | pub(crate) sourcemap: &'a SourceMap, 106 | } 107 | 108 | impl<'a> SourceViewToken<'a> { 109 | pub fn new(token: Token, sourcemap: &'a SourceMap) -> Self { 110 | Self { token, sourcemap } 111 | } 112 | 113 | pub fn get_dst_line(&self) -> u32 { 114 | self.token.dst_line 115 | } 116 | 117 | pub fn get_dst_col(&self) -> u32 { 118 | self.token.dst_col 119 | } 120 | 121 | pub fn get_src_line(&self) -> u32 { 122 | self.token.src_line 123 | } 124 | 125 | pub fn get_src_col(&self) -> u32 { 126 | self.token.src_col 127 | } 128 | 129 | pub fn get_name_id(&self) -> Option { 130 | if self.token.name_id == INVALID_ID { None } else { Some(self.token.name_id) } 131 | } 132 | 133 | pub fn get_source_id(&self) -> Option { 134 | if self.token.source_id == INVALID_ID { None } else { Some(self.token.source_id) } 135 | } 136 | 137 | pub fn get_name(&self) -> Option<&Arc> { 138 | if self.token.name_id == INVALID_ID { 139 | None 140 | } else { 141 | self.sourcemap.get_name(self.token.name_id) 142 | } 143 | } 144 | 145 | pub fn get_source(&self) -> Option<&Arc> { 146 | if self.token.source_id == INVALID_ID { 147 | None 148 | } else { 149 | self.sourcemap.get_source(self.token.source_id) 150 | } 151 | } 152 | 153 | pub fn get_source_content(&self) -> Option<&Arc> { 154 | if self.token.source_id == INVALID_ID { 155 | None 156 | } else { 157 | self.sourcemap.get_source_content(self.token.source_id) 158 | } 159 | } 160 | 161 | pub fn get_source_and_content(&self) -> Option<(&Arc, &Arc)> { 162 | if self.token.source_id == INVALID_ID { 163 | None 164 | } else { 165 | self.sourcemap.get_source_and_content(self.token.source_id) 166 | } 167 | } 168 | 169 | pub fn to_tuple(&self) -> (Option<&Arc>, u32, u32, Option<&Arc>) { 170 | (self.get_source(), self.get_src_line(), self.get_src_col(), self.get_name()) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/sourcemap_visualizer.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use crate::SourceMap; 4 | 5 | /// The `SourcemapVisualizer` is a helper for sourcemap testing. 6 | /// It print the mapping of original content and final content tokens. 7 | pub struct SourcemapVisualizer<'a> { 8 | code: &'a str, 9 | sourcemap: &'a SourceMap, 10 | } 11 | 12 | impl<'a> SourcemapVisualizer<'a> { 13 | pub fn new(code: &'a str, sourcemap: &'a SourceMap) -> Self { 14 | Self { code, sourcemap } 15 | } 16 | 17 | pub fn get_url(&self) -> String { 18 | let result = self.sourcemap.to_json_string(); 19 | let s = format!("{}\0{}{}\0{}", self.code.len(), self.code, result.len(), result); 20 | let hash = base64_simd::STANDARD.encode_to_string(s); 21 | format!("https://evanw.github.io/source-map-visualization/#{hash}") 22 | } 23 | 24 | pub fn get_text(&self) -> String { 25 | let mut s = String::new(); 26 | let source_contents = &self.sourcemap.source_contents; 27 | if self.sourcemap.source_contents.is_empty() { 28 | s.push_str("[no source contents]\n"); 29 | return s; 30 | } 31 | 32 | let source_contents_lines_map: Vec>> = source_contents 33 | .iter() 34 | .filter_map(|content| { 35 | let content = content.as_ref()?; 36 | Some(Self::generate_line_utf16_tables(content)) 37 | }) 38 | .collect(); 39 | 40 | let output_lines = Self::generate_line_utf16_tables(self.code); 41 | 42 | let tokens = &self.sourcemap.tokens; 43 | 44 | let mut last_source: Option<&str> = None; 45 | for i in 0..tokens.len() { 46 | let t = &tokens[i]; 47 | let Some(source_id) = t.get_source_id() else { 48 | continue; 49 | }; 50 | let Some(source) = self.sourcemap.get_source(source_id) else { continue }; 51 | let source_lines = &source_contents_lines_map[source_id as usize]; 52 | 53 | // Print source 54 | if last_source != Some(source) { 55 | s.push('-'); 56 | s.push(' '); 57 | s.push_str(source); 58 | s.push('\n'); 59 | last_source = Some(source); 60 | } 61 | 62 | // validate token position 63 | let dst_invalid = t.dst_line as usize >= output_lines.len() 64 | || (t.dst_col as usize) >= output_lines[t.dst_line as usize].len(); 65 | let src_invalid = t.src_line as usize >= source_lines.len() 66 | || (t.src_col as usize) >= source_lines[t.src_line as usize].len(); 67 | if dst_invalid || src_invalid { 68 | s.push_str(&format!( 69 | "({}:{}){} --> ({}:{}){}\n", 70 | t.src_line, 71 | t.src_col, 72 | if src_invalid { " [invalid]" } else { "" }, 73 | t.dst_line, 74 | t.dst_col, 75 | if dst_invalid { " [invalid]" } else { "" }, 76 | )); 77 | continue; 78 | } 79 | 80 | // find next dst column or EOL 81 | let dst_end_col = { 82 | match tokens.get(i + 1) { 83 | Some(t2) if t2.dst_line == t.dst_line => t2.dst_col, 84 | _ => output_lines[t.dst_line as usize].len() as u32, 85 | } 86 | }; 87 | 88 | // find next src column or EOL 89 | let src_end_col = 'result: { 90 | for t2 in &tokens[i + 1..] { 91 | if t2.get_source_id() == t.get_source_id() && t2.src_line == t.src_line { 92 | // skip duplicate or backward 93 | if t2.src_col <= t.src_col { 94 | continue; 95 | } 96 | break 'result t2.src_col; 97 | } 98 | break; 99 | } 100 | source_lines[t.src_line as usize].len() as u32 101 | }; 102 | 103 | s.push_str(&format!( 104 | "({}:{}) {:?} --> ({}:{}) {:?}\n", 105 | t.src_line, 106 | t.src_col, 107 | Self::str_slice_by_token(source_lines, t.src_line, t.src_col, src_end_col), 108 | t.dst_line, 109 | t.dst_col, 110 | Self::str_slice_by_token(&output_lines, t.dst_line, t.dst_col, dst_end_col) 111 | )); 112 | } 113 | 114 | s 115 | } 116 | 117 | fn generate_line_utf16_tables(content: &str) -> Vec> { 118 | let mut tables = vec![]; 119 | let mut line_byte_offset = 0; 120 | for (i, ch) in content.char_indices() { 121 | match ch { 122 | '\r' | '\n' | '\u{2028}' | '\u{2029}' => { 123 | // Handle Windows-specific "\r\n" newlines 124 | if ch == '\r' && content.chars().nth(i + 1) == Some('\n') { 125 | continue; 126 | } 127 | tables.push(content[line_byte_offset..=i].encode_utf16().collect::>()); 128 | line_byte_offset = i + 1; 129 | } 130 | _ => {} 131 | } 132 | } 133 | tables.push(content[line_byte_offset..].encode_utf16().collect::>()); 134 | tables 135 | } 136 | 137 | fn str_slice_by_token(buff: &[Vec], line: u32, start: u32, end: u32) -> Cow<'_, str> { 138 | let line = line as usize; 139 | let start = start as usize; 140 | let end = end as usize; 141 | let s = &buff[line]; 142 | String::from_utf16(&s[start.min(end).min(s.len())..start.max(end).min(s.len())]) 143 | .unwrap() 144 | .replace("\r", "") 145 | .into() 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /tests/fixtures/esbuild/visualizer.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/oxc_sourcemap/tests/main.rs 3 | input_file: crates/oxc_sourcemap/tests/fixtures/esbuild/example.js 4 | snapshot_kind: text 5 | --- 6 | - index.tsx 7 | (0:0) "import { " --> (1:0) "import { " 8 | (0:9) "h, " --> (1:9) "h as " 9 | (0:9) "h, " --> (1:14) "u, " 10 | (0:12) "Fragment, " --> (1:17) "Fragment as " 11 | (0:12) "Fragment, " --> (1:29) "l, " 12 | (0:22) "render } from " --> (1:32) "render as " 13 | (0:22) "render } from " --> (1:42) "c } from " 14 | (0:36) "'preact'\n" --> (1:51) "\"preact\";\n" 15 | - counter.tsx 16 | (0:0) "import { " --> (4:0) "import { " 17 | (0:9) "h, " --> (4:9) "h as " 18 | (0:9) "h, " --> (4:14) "t, " 19 | (0:12) "Component } from " --> (4:17) "Component as " 20 | (0:12) "Component } from " --> (4:30) "i } from " 21 | (0:29) "'preact'\n" --> (4:39) "\"preact\";\n" 22 | (1:0) "import { " --> (5:0) "import { " 23 | (1:9) "useState } from " --> (5:9) "useState as " 24 | (1:9) "useState } from " --> (5:21) "a } from " 25 | (1:25) "'preact/hooks'\n" --> (5:30) "\"preact/hooks\";\n" 26 | (12:7) "class " --> (6:0) "var " 27 | (12:13) "CounterClass extends " --> (6:4) "n = " 28 | (12:7) "class CounterClass extends " --> (6:8) "class extends " 29 | (12:34) "Component " --> (6:22) "i " 30 | (12:72) "{\n" --> (6:24) "{\n" 31 | (12:72) "{\n" --> (7:0) " " 32 | (15:2) "constructor(" --> (7:2) "constructor(" 33 | (15:14) "props: CounterProps) " --> (7:14) "e) " 34 | (15:35) "{\n" --> (7:17) "{\n" 35 | (16:4) "super(" --> (8:0) " super(" 36 | (16:10) "props" --> (8:10) "e" 37 | (16:15) ")\n" --> (8:11) ");\n" 38 | (20:2) "increment_ = " --> (9:0) " this." 39 | (20:2) "increment_ = " --> (9:9) "n = " 40 | (20:15) "() => " --> (9:13) "() => " 41 | (20:21) "this." --> (9:19) "this." 42 | (20:26) "setState(" --> (9:24) "setState(" 43 | (20:35) "{ " --> (9:33) "{ " 44 | (20:37) "value_: " --> (9:35) "t: " 45 | (20:45) "this." --> (9:38) "this." 46 | (20:50) "state." --> (9:43) "state." 47 | (20:56) "value_ + " --> (9:49) "t + " 48 | (20:65) "1 " --> (9:53) "1 " 49 | (20:67) "}" --> (9:55) "}" 50 | (20:68) ")\n" --> (9:56) ");\n" 51 | (21:2) "decrement_ = " --> (10:0) " this." 52 | (21:2) "decrement_ = " --> (10:9) "r = " 53 | (21:15) "() => " --> (10:13) "() => " 54 | (21:21) "this." --> (10:19) "this." 55 | (21:26) "setState(" --> (10:24) "setState(" 56 | (21:35) "{ " --> (10:33) "{ " 57 | (21:37) "value_: " --> (10:35) "t: " 58 | (21:45) "this." --> (10:38) "this." 59 | (21:50) "state." --> (10:43) "state." 60 | (21:56) "value_ - " --> (10:49) "t - " 61 | (21:65) "1 " --> (10:53) "1 " 62 | (21:67) "}" --> (10:55) "}" 63 | (21:68) ")\n" --> (10:56) ");\n" 64 | (17:4) "this." --> (11:0) " this." 65 | (17:9) "state." --> (11:9) "state." 66 | (17:15) "value_ = " --> (11:15) "t = " 67 | (17:24) "props." --> (11:19) "e." 68 | (17:30) "initialValue_\n" --> (11:21) "e;\n" 69 | (17:30) "initialValue_\n" --> (12:0) " " 70 | (18:2) "}\n" --> (12:2) "}\n" 71 | (18:2) "}\n" --> (13:0) " " 72 | (23:2) "render() " --> (13:2) "render() " 73 | (23:11) "{\n" --> (13:11) "{\n" 74 | (24:4) "return " --> (14:0) " return " 75 | (24:11) "<" --> (14:11) "t(" 76 | (24:12) "div " --> (14:13) "\"div\", {\n" 77 | (24:12) "div " --> (15:0) " " 78 | (24:16) "class=" --> (15:6) "class: " 79 | (24:22) "\"counter\">\n" --> (15:13) "\"counter\"\n" 80 | (24:22) "\"counter\">\n" --> (16:0) " }, " 81 | (25:6) "<" --> (16:7) "t(" 82 | (25:7) "h1>{" --> (16:9) "\"h1\", null, " 83 | (25:11) "this." --> (16:21) "this." 84 | (25:16) "props." --> (16:26) "props." 85 | (25:22) "label}" --> (16:32) "label" 86 | (25:28) "\n" --> (16:37) "), " 87 | (26:6) "<" --> (16:40) "t(" 88 | (26:7) "p>\n" --> (16:42) "\"p\", null, " 89 | (27:8) "<" --> (16:53) "t(" 90 | (27:9) "button " --> (16:55) "\"button\", {\n" 91 | (27:9) "button " --> (17:0) " " 92 | (27:16) "onClick={" --> (17:6) "onClick: " 93 | (27:25) "this." --> (17:15) "this." 94 | (27:30) "decrement_}>" --> (17:20) "r\n" 95 | (27:30) "decrement_}>" --> (18:0) " }, " 96 | (27:42) "-" --> (18:7) "\"-\"" 97 | (27:43) "\n" --> (18:10) "), " 98 | (28:9) "' '}\n" --> (18:13) "\" \", " 99 | (29:9) "this." --> (18:18) "this." 100 | (29:14) "state." --> (18:23) "state." 101 | (29:20) "value_}\n" --> (18:29) "t, " 102 | (30:9) "' '}\n" --> (18:32) "\" \", " 103 | (31:8) "<" --> (18:37) "t(" 104 | (31:9) "button " --> (18:39) "\"button\", {\n" 105 | (31:9) "button " --> (19:0) " " 106 | (31:16) "onClick={" --> (19:6) "onClick: " 107 | (31:25) "this." --> (19:15) "this." 108 | (31:30) "increment_}>" --> (19:20) "n\n" 109 | (31:30) "increment_}>" --> (20:0) " }, " 110 | (31:42) "+" --> (20:7) "\"+\"" 111 | (31:43) "\n" --> (20:10) ")" 112 | (32:6) "

\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>>, 11 | pub(crate) tokens: Vec, 12 | /// The `token_chunks` is used for encode tokens to vlq mappings at parallel. 13 | pub(crate) token_chunks: Vec, 14 | pub(crate) token_chunk_prev_source_id: u32, 15 | pub(crate) token_chunk_prev_name_id: u32, 16 | } 17 | 18 | impl ConcatSourceMapBuilder { 19 | /// Create new `ConcatSourceMapBuilder` with pre-allocated capacity. 20 | /// 21 | /// Allocating capacity before adding sourcemaps with `add_sourcemap` avoids memory copies 22 | /// and increases performance. 23 | /// 24 | /// Alternatively, use `from_sourcemaps`. 25 | pub fn with_capacity( 26 | names_len: usize, 27 | sources_len: usize, 28 | tokens_len: usize, 29 | token_chunks_len: usize, 30 | ) -> Self { 31 | Self { 32 | names: Vec::with_capacity(names_len), 33 | sources: Vec::with_capacity(sources_len), 34 | source_contents: Vec::with_capacity(sources_len), 35 | tokens: Vec::with_capacity(tokens_len), 36 | token_chunks: Vec::with_capacity(token_chunks_len), 37 | token_chunk_prev_source_id: 0, 38 | token_chunk_prev_name_id: 0, 39 | } 40 | } 41 | 42 | /// Create new `ConcatSourceMapBuilder` from an array of `SourceMap`s and line offsets. 43 | /// 44 | /// This avoids memory copies versus creating builder with `ConcatSourceMapBuilder::default()` 45 | /// and then adding sourcemaps individually with `add_sourcemap`. 46 | /// 47 | /// # Example 48 | /// ``` 49 | /// let builder = ConcatSourceMapBuilder::from_sourcemaps(&[ 50 | /// (&sourcemap1, 0), 51 | /// (&sourcemap2, 100), 52 | /// ]); 53 | /// let combined_sourcemap = builder.into_sourcemap(); 54 | /// ``` 55 | pub fn from_sourcemaps(sourcemap_and_line_offsets: &[(&SourceMap, u32)]) -> Self { 56 | // Calculate length of `Vec`s required 57 | let mut names_len = 0; 58 | let mut sources_len = 0; 59 | let mut tokens_len = 0; 60 | for (sourcemap, _) in sourcemap_and_line_offsets { 61 | names_len += sourcemap.names.len(); 62 | sources_len += sourcemap.sources.len(); 63 | tokens_len += sourcemap.tokens.len(); 64 | } 65 | 66 | let mut builder = Self::with_capacity( 67 | names_len, 68 | sources_len, 69 | tokens_len, 70 | sourcemap_and_line_offsets.len(), 71 | ); 72 | 73 | for (sourcemap, line_offset) in sourcemap_and_line_offsets.iter().copied() { 74 | builder.add_sourcemap(sourcemap, line_offset); 75 | } 76 | 77 | builder 78 | } 79 | 80 | pub fn add_sourcemap(&mut self, sourcemap: &SourceMap, line_offset: u32) { 81 | let source_offset = self.sources.len() as u32; 82 | let name_offset = self.names.len() as u32; 83 | let start_token_idx = self.tokens.len() as u32; 84 | 85 | // Capture prev_name_id and prev_source_id before they get updated during token mapping 86 | let chunk_prev_name_id = self.token_chunk_prev_name_id; 87 | let chunk_prev_source_id = self.token_chunk_prev_source_id; 88 | 89 | // Extend `sources` and `source_contents`. 90 | self.sources.extend(sourcemap.get_sources().map(Arc::clone)); 91 | 92 | // Clone `Arc` instead of generating a new `Arc` and copying string data because 93 | // source texts are generally long strings. Cost of copying a large string is higher 94 | // than cloning an `Arc`. 95 | self.source_contents.extend(sourcemap.source_contents.iter().cloned()); 96 | 97 | // Extend `names`. 98 | self.names.reserve(sourcemap.names.len()); 99 | self.names.extend(sourcemap.get_names().map(Arc::clone)); 100 | 101 | // Extend `tokens`. 102 | self.tokens.reserve(sourcemap.tokens.len()); 103 | let tokens: Vec = sourcemap 104 | .get_tokens() 105 | .map(|token| { 106 | Token::new( 107 | token.get_dst_line() + line_offset, 108 | token.get_dst_col(), 109 | token.get_src_line(), 110 | token.get_src_col(), 111 | token.get_source_id().map(|x| { 112 | self.token_chunk_prev_source_id = x + source_offset; 113 | self.token_chunk_prev_source_id 114 | }), 115 | token.get_name_id().map(|x| { 116 | self.token_chunk_prev_name_id = x + name_offset; 117 | self.token_chunk_prev_name_id 118 | }), 119 | ) 120 | }) 121 | .collect(); 122 | 123 | // Skip first token if it's identical to the last existing token to avoid duplicates 124 | let tokens_to_add = if let Some(last_token) = self.tokens.last() { 125 | if let Some(first_new) = tokens.first() { 126 | if last_token == first_new { 127 | &tokens[1..] // Skip duplicate 128 | } else { 129 | &tokens[..] 130 | } 131 | } else { 132 | &tokens[..] 133 | } 134 | } else { 135 | &tokens[..] 136 | }; 137 | 138 | self.tokens.extend_from_slice(tokens_to_add); 139 | 140 | // Add `token_chunks` after tokens are added so we know the actual end index 141 | let end_token_idx = self.tokens.len() as u32; 142 | 143 | if start_token_idx > 0 { 144 | // Not the first sourcemap - use previous token's state 145 | let prev_token = &self.tokens[start_token_idx as usize - 1]; 146 | self.token_chunks.push(TokenChunk::new( 147 | start_token_idx, 148 | end_token_idx, 149 | prev_token.get_dst_line(), 150 | prev_token.get_dst_col(), 151 | prev_token.get_src_line(), 152 | prev_token.get_src_col(), 153 | chunk_prev_name_id, 154 | chunk_prev_source_id, 155 | )); 156 | } else { 157 | // First sourcemap - use zeros 158 | self.token_chunks.push(TokenChunk::new(0, end_token_idx, 0, 0, 0, 0, 0, 0)); 159 | } 160 | } 161 | 162 | pub fn into_sourcemap(self) -> SourceMap { 163 | SourceMap::new( 164 | None, 165 | self.names, 166 | None, 167 | self.sources, 168 | self.source_contents, 169 | self.tokens.into_boxed_slice(), 170 | Some(self.token_chunks), 171 | ) 172 | } 173 | } 174 | 175 | #[test] 176 | fn test_concat_sourcemap_builder() { 177 | run_test(|sourcemap_and_line_offsets| { 178 | let mut builder = ConcatSourceMapBuilder::default(); 179 | for (sourcemap, line_offset) in sourcemap_and_line_offsets.iter().copied() { 180 | builder.add_sourcemap(sourcemap, line_offset); 181 | } 182 | builder 183 | }); 184 | } 185 | 186 | #[test] 187 | fn test_concat_sourcemap_builder_from_sourcemaps() { 188 | run_test(ConcatSourceMapBuilder::from_sourcemaps); 189 | } 190 | 191 | #[cfg(test)] 192 | fn run_test(create_builder: F) 193 | where 194 | F: Fn(&[(&SourceMap, u32)]) -> ConcatSourceMapBuilder, 195 | { 196 | let sm1 = SourceMap::new( 197 | None, 198 | vec!["foo".into(), "foo2".into()], 199 | None, 200 | vec!["foo.js".into()], 201 | vec![], 202 | vec![Token::new(1, 1, 1, 1, Some(0), Some(0))].into_boxed_slice(), 203 | None, 204 | ); 205 | let sm2 = SourceMap::new( 206 | None, 207 | vec!["bar".into()], 208 | None, 209 | vec!["bar.js".into()], 210 | vec![], 211 | vec![Token::new(1, 1, 1, 1, Some(0), Some(0))].into_boxed_slice(), 212 | None, 213 | ); 214 | let sm3 = SourceMap::new( 215 | None, 216 | vec!["abc".into()], 217 | None, 218 | vec!["abc.js".into()], 219 | vec![], 220 | vec![Token::new(1, 2, 2, 2, Some(0), Some(0))].into_boxed_slice(), 221 | None, 222 | ); 223 | 224 | let builder = create_builder(&[(&sm1, 0), (&sm2, 2), (&sm3, 2)]); 225 | 226 | let sm = SourceMap::new( 227 | None, 228 | vec!["foo".into(), "foo2".into(), "bar".into(), "abc".into()], 229 | None, 230 | vec!["foo.js".into(), "bar.js".into(), "abc.js".into()], 231 | vec![], 232 | vec![ 233 | Token::new(1, 1, 1, 1, Some(0), Some(0)), 234 | Token::new(3, 1, 1, 1, Some(1), Some(2)), 235 | Token::new(3, 2, 2, 2, Some(2), Some(3)), 236 | ] 237 | .into_boxed_slice(), 238 | None, 239 | ); 240 | let concat_sm = builder.into_sourcemap(); 241 | 242 | assert_eq!(concat_sm.tokens, sm.tokens); 243 | assert_eq!(concat_sm.sources, sm.sources); 244 | assert_eq!(concat_sm.names, sm.names); 245 | assert_eq!( 246 | concat_sm.token_chunks, 247 | Some(vec![ 248 | TokenChunk::new(0, 1, 0, 0, 0, 0, 0, 0,), 249 | TokenChunk::new(1, 2, 1, 1, 1, 1, 0, 0,), 250 | TokenChunk::new(2, 3, 3, 1, 1, 1, 2, 1,) 251 | ]) 252 | ); 253 | 254 | assert_eq!(sm.to_json().mappings, concat_sm.to_json().mappings); 255 | } 256 | 257 | #[test] 258 | fn test_concat_sourcemap_builder_deduplicates_tokens() { 259 | // Test that duplicate tokens at concatenation boundaries are removed 260 | // For tokens to be truly identical after concatenation, they must have: 261 | // - Same dst_line (after line_offset) 262 | // - Same dst_col 263 | // - Same src_line, src_col 264 | // - Same source_id and name_id (after source_offset/name_offset) 265 | 266 | // This is difficult to create naturally, so we test the scenario where 267 | // no deduplication should happen (tokens are different) 268 | let sm1 = SourceMap::new( 269 | None, 270 | vec!["name1".into()], 271 | None, 272 | vec!["file1.js".into()], 273 | vec![], 274 | vec![Token::new(1, 1, 1, 1, Some(0), Some(0)), Token::new(2, 5, 2, 5, Some(0), Some(0))] 275 | .into_boxed_slice(), 276 | None, 277 | ); 278 | 279 | // sm2 has different source_id/name_id after offset, so won't deduplicate 280 | let sm2 = SourceMap::new( 281 | None, 282 | vec!["name2".into()], 283 | None, 284 | vec!["file2.js".into()], 285 | vec![], 286 | vec![ 287 | Token::new(2, 5, 2, 5, Some(0), Some(0)), // Different source/name after offset 288 | Token::new(3, 10, 3, 10, Some(0), Some(0)), 289 | ] 290 | .into_boxed_slice(), 291 | None, 292 | ); 293 | 294 | let mut builder = ConcatSourceMapBuilder::default(); 295 | builder.add_sourcemap(&sm1, 0); 296 | builder.add_sourcemap(&sm2, 0); 297 | 298 | let concat_sm = builder.into_sourcemap(); 299 | 300 | // Should have 4 tokens (no deduplication because source_id/name_id differ) 301 | assert_eq!(concat_sm.tokens.len(), 4); 302 | } 303 | -------------------------------------------------------------------------------- /src/sourcemap.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::{ 4 | SourceViewToken, 5 | decode::{JSONSourceMap, decode, decode_from_string}, 6 | encode::{encode, encode_to_string}, 7 | error::Result, 8 | token::{Token, TokenChunk}, 9 | }; 10 | 11 | #[derive(Debug, Clone, Default)] 12 | pub struct SourceMap { 13 | pub(crate) file: Option>, 14 | pub(crate) names: Vec>, 15 | pub(crate) source_root: Option, 16 | pub(crate) sources: Vec>, 17 | pub(crate) source_contents: Vec>>, 18 | pub(crate) tokens: Box<[Token]>, 19 | pub(crate) token_chunks: Option>, 20 | /// 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. 21 | /// 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. 22 | /// 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. 23 | pub(crate) x_google_ignore_list: Option>, 24 | pub(crate) debug_id: Option, 25 | } 26 | 27 | impl SourceMap { 28 | pub fn new( 29 | file: Option>, 30 | names: Vec>, 31 | source_root: Option, 32 | sources: Vec>, 33 | source_contents: Vec>>, 34 | tokens: Box<[Token]>, 35 | token_chunks: Option>, 36 | ) -> Self { 37 | Self { 38 | file, 39 | names, 40 | source_root, 41 | sources, 42 | source_contents, 43 | tokens, 44 | token_chunks, 45 | x_google_ignore_list: None, 46 | debug_id: None, 47 | } 48 | } 49 | 50 | /// Convert the vlq sourcemap to to `SourceMap`. 51 | /// # Errors 52 | /// 53 | /// The `serde_json` deserialize Error. 54 | pub fn from_json(value: JSONSourceMap) -> Result { 55 | decode(value) 56 | } 57 | 58 | /// Convert the vlq sourcemap string to `SourceMap`. 59 | /// # Errors 60 | /// 61 | /// The `serde_json` deserialize Error. 62 | pub fn from_json_string(value: &str) -> Result { 63 | decode_from_string(value) 64 | } 65 | 66 | /// Convert `SourceMap` to vlq sourcemap. 67 | pub fn to_json(&self) -> JSONSourceMap { 68 | encode(self) 69 | } 70 | 71 | /// Convert `SourceMap` to vlq sourcemap string. 72 | pub fn to_json_string(&self) -> String { 73 | encode_to_string(self) 74 | } 75 | 76 | /// Convert `SourceMap` to vlq sourcemap data url. 77 | pub fn to_data_url(&self) -> String { 78 | let base_64_str = base64_simd::STANDARD.encode_to_string(self.to_json_string().as_bytes()); 79 | format!("data:application/json;charset=utf-8;base64,{base_64_str}") 80 | } 81 | 82 | pub fn get_file(&self) -> Option<&Arc> { 83 | self.file.as_ref() 84 | } 85 | 86 | pub fn set_file(&mut self, file: &str) { 87 | self.file = Some(file.into()); 88 | } 89 | 90 | pub fn get_source_root(&self) -> Option<&str> { 91 | self.source_root.as_deref() 92 | } 93 | 94 | pub fn get_x_google_ignore_list(&self) -> Option<&[u32]> { 95 | self.x_google_ignore_list.as_deref() 96 | } 97 | 98 | /// Set `x_google_ignoreList`. 99 | pub fn set_x_google_ignore_list(&mut self, x_google_ignore_list: Vec) { 100 | self.x_google_ignore_list = Some(x_google_ignore_list); 101 | } 102 | 103 | pub fn set_debug_id(&mut self, debug_id: &str) { 104 | self.debug_id = Some(debug_id.into()); 105 | } 106 | 107 | pub fn get_debug_id(&self) -> Option<&str> { 108 | self.debug_id.as_deref() 109 | } 110 | 111 | pub fn get_names(&self) -> impl Iterator> { 112 | self.names.iter() 113 | } 114 | 115 | /// Adjust `sources`. 116 | pub fn set_sources, I: IntoIterator>(&mut self, sources: I) { 117 | self.sources = sources.into_iter().map(|s| s.as_ref().into()).collect(); 118 | } 119 | 120 | pub fn get_sources(&self) -> impl Iterator> { 121 | self.sources.iter() 122 | } 123 | 124 | /// Adjust `source_content`. 125 | pub fn set_source_contents(&mut self, source_contents: Vec>) { 126 | self.source_contents = 127 | source_contents.into_iter().map(|v| v.map(Arc::from)).collect::>(); 128 | } 129 | 130 | pub fn get_source_contents(&self) -> impl Iterator>> { 131 | self.source_contents.iter().map(|item| item.as_ref()) 132 | } 133 | 134 | pub fn get_token(&self, index: u32) -> Option { 135 | self.tokens.get(index as usize).copied() 136 | } 137 | 138 | pub fn get_source_view_token(&self, index: u32) -> Option> { 139 | self.tokens.get(index as usize).copied().map(|token| SourceViewToken::new(token, self)) 140 | } 141 | 142 | /// Get raw tokens. 143 | pub fn get_tokens(&self) -> impl Iterator { 144 | self.tokens.iter().copied() 145 | } 146 | 147 | /// Get source view tokens. See [`SourceViewToken`] for more information. 148 | pub fn get_source_view_tokens(&self) -> impl Iterator> { 149 | self.tokens.iter().map(|&token| SourceViewToken::new(token, self)) 150 | } 151 | 152 | pub fn get_name(&self, id: u32) -> Option<&Arc> { 153 | self.names.get(id as usize) 154 | } 155 | 156 | pub fn get_source(&self, id: u32) -> Option<&Arc> { 157 | self.sources.get(id as usize) 158 | } 159 | 160 | pub fn get_source_content(&self, id: u32) -> Option<&Arc> { 161 | self.source_contents.get(id as usize).and_then(|item| item.as_ref()) 162 | } 163 | 164 | pub fn get_source_and_content(&self, id: u32) -> Option<(&Arc, &Arc)> { 165 | let source = self.get_source(id)?; 166 | let content = self.get_source_content(id)?; 167 | Some((source, content)) 168 | } 169 | 170 | /// Generate a lookup table, it will be used at `lookup_token` or `lookup_source_view_token`. 171 | pub fn generate_lookup_table<'a>(&'a self) -> Vec> { 172 | // The dst line/dst col always has increasing order. 173 | if let Some(last_token) = self.tokens.last() { 174 | let mut table = vec![&self.tokens[..0]; last_token.dst_line as usize + 1]; 175 | let mut prev_start_idx = 0u32; 176 | let mut prev_dst_line = 0u32; 177 | for (idx, token) in self.tokens.iter().enumerate() { 178 | if token.dst_line != prev_dst_line { 179 | table[prev_dst_line as usize] = &self.tokens[prev_start_idx as usize..idx]; 180 | prev_start_idx = idx as u32; 181 | prev_dst_line = token.dst_line; 182 | } 183 | } 184 | table[prev_dst_line as usize] = &self.tokens[prev_start_idx as usize..]; 185 | table 186 | } else { 187 | vec![] 188 | } 189 | } 190 | 191 | /// Lookup a token by line and column, it will used at remapping. 192 | pub fn lookup_token( 193 | &self, 194 | lookup_table: &[LineLookupTable], 195 | line: u32, 196 | col: u32, 197 | ) -> Option { 198 | // If the line is greater than the number of lines in the lookup table, it hasn't corresponding origin token. 199 | if line >= lookup_table.len() as u32 { 200 | return None; 201 | } 202 | let token = greatest_lower_bound(lookup_table[line as usize], &(line, col), |token| { 203 | (token.dst_line, token.dst_col) 204 | })?; 205 | Some(*token) 206 | } 207 | 208 | /// Lookup a token by line and column, it will used at remapping. See `SourceViewToken`. 209 | pub fn lookup_source_view_token( 210 | &self, 211 | lookup_table: &[LineLookupTable], 212 | line: u32, 213 | col: u32, 214 | ) -> Option> { 215 | self.lookup_token(lookup_table, line, col).map(|token| SourceViewToken::new(token, self)) 216 | } 217 | } 218 | 219 | type LineLookupTable<'a> = &'a [Token]; 220 | 221 | fn greatest_lower_bound<'a, T, K: Ord, F: Fn(&'a T) -> K>( 222 | slice: &'a [T], 223 | key: &K, 224 | map: F, 225 | ) -> Option<&'a T> { 226 | let mut idx = match slice.binary_search_by_key(key, &map) { 227 | Ok(index) => index, 228 | Err(index) => { 229 | // If there is no match, then we know for certain that the index is where we should 230 | // insert a new token, and that the token directly before is the greatest lower bound. 231 | return slice.get(index.checked_sub(1)?); 232 | } 233 | }; 234 | 235 | // If we get an exact match, then we need to continue looking at previous tokens to see if 236 | // they also match. We use a linear search because the number of exact matches is generally 237 | // very small, and almost certainly smaller than the number of tokens before the index. 238 | for i in (0..idx).rev() { 239 | if map(&slice[i]) == *key { 240 | idx = i; 241 | } else { 242 | break; 243 | } 244 | } 245 | slice.get(idx) 246 | } 247 | 248 | #[test] 249 | fn test_sourcemap_lookup_token() { 250 | let input = r#"{ 251 | "version": 3, 252 | "sources": ["coolstuff.js"], 253 | "sourceRoot": "x", 254 | "names": ["x","alert"], 255 | "mappings": "AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM" 256 | }"#; 257 | let sm = SourceMap::from_json_string(input).unwrap(); 258 | let lookup_table = sm.generate_lookup_table(); 259 | assert_eq!( 260 | sm.lookup_source_view_token(&lookup_table, 0, 0).unwrap().to_tuple(), 261 | (Some(&"coolstuff.js".into()), 0, 0, None) 262 | ); 263 | assert_eq!( 264 | sm.lookup_source_view_token(&lookup_table, 0, 3).unwrap().to_tuple(), 265 | (Some(&"coolstuff.js".into()), 0, 4, Some(&"x".into())) 266 | ); 267 | assert_eq!( 268 | sm.lookup_source_view_token(&lookup_table, 0, 24).unwrap().to_tuple(), 269 | (Some(&"coolstuff.js".into()), 2, 8, None) 270 | ); 271 | 272 | // Lines continue out to infinity 273 | assert_eq!( 274 | sm.lookup_source_view_token(&lookup_table, 0, 1000).unwrap().to_tuple(), 275 | (Some(&"coolstuff.js".into()), 2, 8, None) 276 | ); 277 | 278 | assert!(sm.lookup_source_view_token(&lookup_table, 1000, 0).is_none()); 279 | } 280 | 281 | #[test] 282 | fn test_sourcemap_source_view_token() { 283 | let sm = SourceMap::new( 284 | None, 285 | vec!["foo".into()], 286 | None, 287 | vec!["foo.js".into()], 288 | vec![], 289 | vec![Token::new(1, 1, 1, 1, Some(0), Some(0))].into_boxed_slice(), 290 | None, 291 | ); 292 | let mut source_view_tokens = sm.get_source_view_tokens(); 293 | assert_eq!( 294 | source_view_tokens.next().unwrap().to_tuple(), 295 | (Some(&"foo.js".into()), 1, 1, Some(&"foo".into())) 296 | ); 297 | } 298 | 299 | #[test] 300 | fn test_mut_sourcemap() { 301 | let mut sm = SourceMap::default(); 302 | sm.set_file("index.js"); 303 | sm.set_sources(vec!["foo.js"]); 304 | sm.set_source_contents(vec![Some("foo")]); 305 | 306 | assert_eq!(sm.get_file().map(|s| s.as_ref()), Some("index.js")); 307 | assert_eq!(sm.get_source(0).map(|s| s.as_ref()), Some("foo.js")); 308 | assert_eq!(sm.get_source_content(0).map(|s| s.as_ref()), Some("foo")); 309 | } 310 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [6.0.1](https://github.com/oxc-project/oxc-sourcemap/compare/v6.0.0...v6.0.1) - 2025-11-06 11 | 12 | ### Other 13 | 14 | - _(deps)_ bump napi; bump msrv to 1.88.0 for napi ([#213](https://github.com/oxc-project/oxc-sourcemap/pull/213)) 15 | 16 | ## [6.0.0](https://github.com/oxc-project/oxc-sourcemap/compare/v5.0.1...v6.0.0) - 2025-10-15 17 | 18 | ### Other 19 | 20 | - loosen the type constraint on set_sources ([#199](https://github.com/oxc-project/oxc-sourcemap/pull/199)) 21 | 22 | ## [5.0.1](https://github.com/oxc-project/oxc-sourcemap/compare/v5.0.0...v5.0.1) - 2025-10-15 23 | 24 | ### Fixed 25 | 26 | - remove debug_assert in sourcemap encoding ([#197](https://github.com/oxc-project/oxc-sourcemap/pull/197)) 27 | 28 | ## [5.0.0](https://github.com/oxc-project/oxc-sourcemap/compare/v4.2.1...v5.0.0) - 2025-10-14 29 | 30 | ### Added 31 | 32 | - integrate tc39/source-map-tests for spec compliance testing ([#190](https://github.com/oxc-project/oxc-sourcemap/pull/190)) 33 | 34 | ### Other 35 | 36 | - bump json-escape-simd ([#195](https://github.com/oxc-project/oxc-sourcemap/pull/195)) 37 | - _(deps)_ lock file maintenance rust crates ([#194](https://github.com/oxc-project/oxc-sourcemap/pull/194)) 38 | - remove token equality check from encoding hot loop ([#189](https://github.com/oxc-project/oxc-sourcemap/pull/189)) 39 | - optimize VLQ decode and capacity estimation 40 | 41 | ## [4.2.1](https://github.com/oxc-project/oxc-sourcemap/compare/v4.2.0...v4.2.1) - 2025-09-27 42 | 43 | ### Other 44 | 45 | - reduce memory usage by replacing Vec with Box<[Token]> 46 | - replace !0 with INVALID_ID constant for better readability ([#175](https://github.com/oxc-project/oxc-sourcemap/pull/175)) 47 | - remove outdated comment for `TokenChunk` 48 | 49 | ## [4.2.0](https://github.com/oxc-project/oxc-sourcemap/compare/v4.1.6...v4.2.0) - 2025-09-26 50 | 51 | ### Added 52 | 53 | - reduce peak memory usage with `.shrink_to_fit()` ([#172](https://github.com/oxc-project/oxc-sourcemap/pull/172)) 54 | 55 | ## [4.1.6](https://github.com/oxc-project/oxc-sourcemap/compare/v4.1.5...v4.1.6) - 2025-09-25 56 | 57 | ### Fixed 58 | 59 | - take `;` into account when estimating mappings length ([#167](https://github.com/oxc-project/oxc-sourcemap/pull/167)) 60 | 61 | ### Other 62 | 63 | - tweak `x_google_ignore_list` comment and add assertion ([#168](https://github.com/oxc-project/oxc-sourcemap/pull/168)) 64 | 65 | ## [4.1.5](https://github.com/oxc-project/oxc-sourcemap/compare/v4.1.4...v4.1.5) - 2025-09-24 66 | 67 | ### Fixed 68 | 69 | - x_google_ignoreList encode format ([#165](https://github.com/oxc-project/oxc-sourcemap/pull/165)) 70 | 71 | ## [4.1.4](https://github.com/oxc-project/oxc-sourcemap/compare/v4.1.3...v4.1.4) - 2025-09-24 72 | 73 | ### Other 74 | 75 | - reduce string allocation in sourcemap encoding ([#163](https://github.com/oxc-project/oxc-sourcemap/pull/163)) 76 | 77 | ## [4.1.3](https://github.com/oxc-project/oxc-sourcemap/compare/v4.1.2...v4.1.3) - 2025-09-23 78 | 79 | ### Other 80 | 81 | - use simd optimized json string escape impl ([#158](https://github.com/oxc-project/oxc-sourcemap/pull/158)) 82 | - enable fat lto in bench profile ([#161](https://github.com/oxc-project/oxc-sourcemap/pull/161)) 83 | 84 | ## [4.1.2](https://github.com/oxc-project/oxc-sourcemap/compare/v4.1.1...v4.1.2) - 2025-09-18 85 | 86 | ### Other 87 | 88 | - simplify LineLookupTable structure ([#151](https://github.com/oxc-project/oxc-sourcemap/pull/151)) 89 | - remove sort in generate_lookup_table ([#150](https://github.com/oxc-project/oxc-sourcemap/pull/150)) 90 | 91 | ## [4.1.1](https://github.com/oxc-project/oxc-sourcemap/compare/v4.1.0...v4.1.1) - 2025-09-11 92 | 93 | ### Other 94 | 95 | - _(sourcemap)_ optimize escape_json_string to avoid serde overhead ([#141](https://github.com/oxc-project/oxc-sourcemap/pull/141)) 96 | 97 | ## [4.1.0](https://github.com/oxc-project/oxc-sourcemap/compare/v4.0.5...v4.1.0) - 2025-08-18 98 | 99 | ### Added 100 | 101 | - add `SourcemapVisualizer::get_url` method ([#126](https://github.com/oxc-project/oxc-sourcemap/pull/126)) 102 | 103 | ## [4.0.5](https://github.com/oxc-project/oxc-sourcemap/compare/v4.0.4...v4.0.5) - 2025-08-03 104 | 105 | ### Other 106 | 107 | - make `Token` Copy ([#108](https://github.com/oxc-project/oxc-sourcemap/pull/108)) 108 | - change some APIs to return `&Arc` ([#107](https://github.com/oxc-project/oxc-sourcemap/pull/107)) 109 | 110 | ## [4.0.4](https://github.com/oxc-project/oxc-sourcemap/compare/v4.0.3...v4.0.4) - 2025-07-31 111 | 112 | ### Fixed 113 | 114 | - fix 115 | 116 | ### Other 117 | 118 | - change some APIs to return `&Arc` ([#105](https://github.com/oxc-project/oxc-sourcemap/pull/105)) 119 | - avoid string allocation in `SourceMapBuilder::add_name` ([#103](https://github.com/oxc-project/oxc-sourcemap/pull/103)) 120 | - add [bench] to Cargo.toml 121 | - add benchmark ([#100](https://github.com/oxc-project/oxc-sourcemap/pull/100)) 122 | 123 | ## [4.0.3](https://github.com/oxc-project/oxc-sourcemap/compare/v4.0.2...v4.0.3) - 2025-07-28 124 | 125 | ### Other 126 | 127 | - remove outdated text from README ([#95](https://github.com/oxc-project/oxc-sourcemap/pull/95)) 128 | - _(deps)_ lock file maintenance ([#97](https://github.com/oxc-project/oxc-sourcemap/pull/97)) 129 | - _(justfile)_ add `dprint` ([#94](https://github.com/oxc-project/oxc-sourcemap/pull/94)) 130 | - add auto format ([#92](https://github.com/oxc-project/oxc-sourcemap/pull/92)) 131 | 132 | # Changelog 133 | 134 | All notable changes to this package will be documented in this file. 135 | 136 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project does not adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) until v1.0.0. 137 | 138 | ## [4.0.2](https://github.com/oxc-project/oxc-sourcemap/compare/v4.0.1...v4.0.2) - 2025-07-18 139 | 140 | ### Other 141 | 142 | - return `&Arc` instead `&str` for source content ([#88](https://github.com/oxc-project/oxc-sourcemap/pull/88)) 143 | - reduce size of token from 32 bytes to 24 ([#86](https://github.com/oxc-project/oxc-sourcemap/pull/86)) 144 | 145 | ## [4.0.1](https://github.com/oxc-project/oxc-sourcemap/compare/v4.0.0...v4.0.1) - 2025-07-18 146 | 147 | ### Other 148 | 149 | - _(deps)_ napi v3 ([#84](https://github.com/oxc-project/oxc-sourcemap/pull/84)) 150 | 151 | ## [4.0.0](https://github.com/oxc-project/oxc-sourcemap/compare/v3.0.2...v4.0.0) - 2025-07-17 152 | 153 | ### Other 154 | 155 | - _(deps)_ bump deps ([#83](https://github.com/oxc-project/oxc-sourcemap/pull/83)) 156 | - remove `rayon` feature ([#81](https://github.com/oxc-project/oxc-sourcemap/pull/81)) 157 | 158 | ## [3.0.2](https://github.com/oxc-project/oxc-sourcemap/compare/v3.0.1...v3.0.2) - 2025-05-19 159 | 160 | ### Other 161 | 162 | - napi beta 163 | 164 | ## [3.0.1](https://github.com/oxc-project/oxc-sourcemap/compare/v3.0.0...v3.0.1) - 2025-05-10 165 | 166 | ### Fixed 167 | 168 | - sources_content should be Vec>> ([#50](https://github.com/oxc-project/oxc-sourcemap/pull/50)) 169 | 170 | ## [3.0.0](https://github.com/oxc-project/oxc-sourcemap/compare/v2.0.2...v3.0.0) - 2025-03-03 171 | 172 | ### Added 173 | 174 | - support `x_google_ignoreList` in more places ([#30](https://github.com/oxc-project/oxc-sourcemap/pull/30)) 175 | 176 | ## [2.0.2](https://github.com/oxc-project/oxc-sourcemap/compare/v2.0.1...v2.0.2) - 2025-02-22 177 | 178 | ### Other 179 | 180 | - Rust Edition 2024 ([#24](https://github.com/oxc-project/oxc-sourcemap/pull/24)) 181 | 182 | ## [2.0.1](https://github.com/oxc-project/oxc-sourcemap/compare/v2.0.0...v2.0.1) - 2025-02-21 183 | 184 | ### Other 185 | 186 | - include build.rs 187 | 188 | ## [2.0.0](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.7...v2.0.0) - 2025-02-21 189 | 190 | ### Fixed 191 | 192 | - broken cargo features 193 | 194 | ## [1.0.7](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.6...v1.0.7) - 2025-02-11 195 | 196 | ### Fixed 197 | 198 | - add napi build.rs (#19) 199 | 200 | ## [1.0.6](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.5...v1.0.6) - 2024-12-15 201 | 202 | ### Fixed 203 | 204 | - handle non existing token position in visualizer (#14) 205 | 206 | ## [1.0.5](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.4...v1.0.5) - 2024-12-11 207 | 208 | ### Fixed 209 | 210 | - _(lookup_token)_ should be None if original tokens hasn't the line (#9) 211 | 212 | ## [1.0.4](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.3...v1.0.4) - 2024-12-10 213 | 214 | ### Fixed 215 | 216 | - fix wrong source id when concatenating empty source map (#7) 217 | 218 | ### Other 219 | 220 | - update README 221 | 222 | ## [1.0.3](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.2...v1.0.3) - 2024-12-03 223 | 224 | ### Other 225 | 226 | - rename feature `concurrent` to `rayon` 227 | 228 | ## [1.0.2](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.1...v1.0.2) - 2024-12-03 229 | 230 | ### Other 231 | 232 | - `pub mod napi` 233 | 234 | ## [1.0.1](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.0...v1.0.1) - 2024-12-03 235 | 236 | ### Other 237 | 238 | - remove unused lint 239 | - add `napi` feature 240 | 241 | ## [0.37.0] - 2024-11-21 242 | 243 | ### Bug Fixes 244 | 245 | - 3d66929 sourcemap: Improve source map visualizer (#7386) (Hiroshi Ogawa) 246 | 247 | ## [0.31.0] - 2024-10-08 248 | 249 | ### Features 250 | 251 | - f6e42b6 sourcemap: Add support for sourcemap debug IDs (#6221) (Tim Fish) 252 | 253 | ## [0.30.4] - 2024-09-28 254 | 255 | ### Bug Fixes 256 | 257 | - 6f98aad sourcemap: Align sourcemap type with Rollup (#6133) (Boshen) 258 | 259 | ## [0.29.0] - 2024-09-13 260 | 261 | ### Performance 262 | 263 | - d18c896 rust: Use `cow_utils` instead (#5664) (dalaoshu) 264 | 265 | ## [0.28.0] - 2024-09-11 266 | 267 | ### Documentation 268 | 269 | - fefbbc1 sourcemap: Add trailing newline to README (#5539) (overlookmotel) 270 | 271 | ## [0.24.3] - 2024-08-18 272 | 273 | ### Refactor 274 | 275 | - 5fd1701 sourcemap: Lower the `msrv`. (#4873) (rzvxa) 276 | 277 | ## [0.24.0] - 2024-08-08 278 | 279 | ### Features 280 | 281 | - e42ac3a sourcemap: Add `ConcatSourceMapBuilder::from_sourcemaps` (#4639) (overlookmotel) 282 | 283 | ### Performance 284 | 285 | - ff43dff sourcemap: Speed up VLQ encoding (#4633) (overlookmotel) 286 | - a330773 sourcemap: Reduce string copying in `ConcatSourceMapBuilder` (#4638) (overlookmotel) 287 | - 372316b sourcemap: `ConcatSourceMapBuilder` extend `source_contents` in separate loop (#4634) (overlookmotel) 288 | - c7f1d48 sourcemap: Keep local copy of previous token in VLQ encode (#4596) (overlookmotel) 289 | - 590d795 sourcemap: Shorten main loop encoding VLQ (#4586) (overlookmotel) 290 | 291 | ## [0.23.1] - 2024-08-06 292 | 293 | ### Features 294 | 295 | - e42ac3a sourcemap: Add `ConcatSourceMapBuilder::from_sourcemaps` (#4639) (overlookmotel) 296 | 297 | ### Performance 298 | 299 | - ff43dff sourcemap: Speed up VLQ encoding (#4633) (overlookmotel) 300 | - a330773 sourcemap: Reduce string copying in `ConcatSourceMapBuilder` (#4638) (overlookmotel) 301 | - 372316b sourcemap: `ConcatSourceMapBuilder` extend `source_contents` in separate loop (#4634) (overlookmotel) 302 | - c7f1d48 sourcemap: Keep local copy of previous token in VLQ encode (#4596) (overlookmotel) 303 | - 590d795 sourcemap: Shorten main loop encoding VLQ (#4586) (overlookmotel) 304 | 305 | ## [0.23.0] - 2024-08-01 306 | 307 | - 27fd062 sourcemap: [**BREAKING**] Avoid passing `Result`s (#4541) (overlookmotel) 308 | 309 | ### Performance 310 | 311 | - d00014e sourcemap: Elide bounds checks in VLQ encoding (#4583) (overlookmotel) 312 | - 1fd9dd0 sourcemap: Use simd to escape JSON string (#4487) (Brooooooklyn) 313 | 314 | ### Refactor 315 | 316 | - 7c42ffc sourcemap: Align Base64 chars lookup table to cache line (#4535) (overlookmotel) 317 | 318 | ## [0.22.1] - 2024-07-27 319 | 320 | ### Bug Fixes 321 | 322 | - 5db7bed sourcemap: Fix pre-calculation of required segments for building JSON (#4490) (overlookmotel) 323 | 324 | ### Performance 325 | 326 | - 705e19f sourcemap: Reduce memory copies encoding JSON (#4489) (overlookmotel) 327 | - 4d10c6c sourcemap: Pre allocate String buf while encoding (#4476) (Brooooooklyn) 328 | 329 | ### Refactor 330 | 331 | - c958a55 sourcemap: `push_list` method for building JSON (#4486) (overlookmotel) 332 | 333 | ## [0.22.0] - 2024-07-23 334 | 335 | ### Bug Fixes 336 | 337 | - 4cd5df0 sourcemap: Avoid negative line if token_chunks has same prev_dst_line (#4348) (underfin) 338 | 339 | ## [0.21.0] - 2024-07-18 340 | 341 | ### Features 342 | 343 | - 205c259 sourcemap: Support SourceMapBuilder#token_chunks (#4220) (underfin) 344 | 345 | ## [0.16.0] - 2024-06-26 346 | 347 | ### Features 348 | 349 | - 01572f0 sourcemap: Impl `std::fmt::Display` for `Error` (#3902) (DonIsaac)- d3cd3ea Oxc transform binding (#3896) (underfin) 350 | 351 | ## [0.13.1] - 2024-05-22 352 | 353 | ### Features 354 | 355 | - 90d2d09 sourcemap: Add Sourcemap#from_json method (#3361) (underfin) 356 | 357 | ### Bug Fixes 358 | 359 | - 899a52b Fix some nightly warnings (Boshen) 360 | 361 | ## [0.13.0] - 2024-05-14 362 | 363 | ### Features 364 | 365 | - f6daf0b sourcemap: Add feature "sourcemap_concurrent" (Boshen) 366 | - 7363e14 sourcemap: Add "rayon" feature (#3198) (Boshen) 367 | 368 | ## [0.12.3] - 2024-04-11 369 | 370 | ### Features 371 | 372 | - 8662f4f sourcemap: Add x_google_ignoreList (#2928) (underfin) 373 | - 5cb3991 sourcemap: Add sourceRoot (#2926) (underfin) 374 | 375 | ## [0.12.2] - 2024-04-08 376 | 377 | ### Features 378 | 379 | - 96f02e6 sourcemap: Optional JSONSourceMap fields (#2910) (underfin) 380 | - d87cf17 sourcemap: Add methods to mutate SourceMap (#2909) (underfin) 381 | - 74aca1c sourcemap: Add SourceMapBuilder file (#2908) (underfin) 382 | 383 | ## [0.12.1] - 2024-04-03 384 | 385 | ### Bug Fixes 386 | 387 | - 28fae2e sourcemap: Using serde_json::to_string to quote sourcemap string (#2889) (underfin) 388 | 389 | ## [0.11.0] - 2024-03-30 390 | 391 | ### Features 392 | 393 | - b199cb8 Add oxc sourcemap crate (#2825) (underfin) 394 | 395 | ### Bug Fixes 396 | 397 | - 6177c2f codegen: Sourcemap token name should be original name (#2843) (underfin) 398 | -------------------------------------------------------------------------------- /src/encode.rs: -------------------------------------------------------------------------------- 1 | //! Ported and modified from 2 | 3 | use std::ops::{Deref, DerefMut}; 4 | 5 | use json_escape_simd::escape_into; 6 | 7 | use crate::JSONSourceMap; 8 | use crate::{SourceMap, Token, token::TokenChunk}; 9 | 10 | pub fn encode(sourcemap: &SourceMap) -> JSONSourceMap { 11 | JSONSourceMap { 12 | version: 3, 13 | file: sourcemap.get_file().map(ToString::to_string), 14 | mappings: { 15 | let mut mappings = String::with_capacity(estimate_mappings_length(sourcemap)); 16 | serialize_sourcemap_mappings(sourcemap, &mut mappings); 17 | mappings 18 | }, 19 | source_root: sourcemap.get_source_root().map(ToString::to_string), 20 | sources: sourcemap.sources.iter().map(ToString::to_string).collect(), 21 | sources_content: Some( 22 | sourcemap 23 | .source_contents 24 | .iter() 25 | .map(|v| v.as_ref().map(|item| item.to_string())) 26 | .collect(), 27 | ), 28 | names: sourcemap.names.iter().map(ToString::to_string).collect(), 29 | debug_id: sourcemap.get_debug_id().map(ToString::to_string), 30 | x_google_ignore_list: sourcemap.get_x_google_ignore_list().map(|x| x.to_vec()), 31 | } 32 | } 33 | 34 | pub fn encode_to_string(sourcemap: &SourceMap) -> String { 35 | // Worst-case capacity accounting: 36 | // - escape_into may write up to (len * 2 + 2) for each string 37 | // - include commas between items and constant JSON punctuation/keys 38 | let mut max_segments = 0usize; 39 | 40 | // {"version":3, 41 | max_segments += 13; 42 | 43 | // Optional "file":"...", 44 | if let Some(file) = sourcemap.get_file() { 45 | max_segments += 8 /* "file":" */ + file.as_ref().len() + 2 /* ", */; 46 | } 47 | 48 | // Optional "sourceRoot":"...", 49 | if let Some(source_root) = sourcemap.get_source_root() { 50 | max_segments += 14 /* "sourceRoot":" */ + source_root.len() + 2 /* ", */; 51 | } 52 | 53 | // Calculate string lengths in a single pass for better cache locality 54 | let names_count = sourcemap.names.len(); 55 | let sources_count = sourcemap.sources.len(); 56 | let sc_count = sourcemap.source_contents.len(); 57 | 58 | // Accumulate total string bytes across all collections 59 | let mut total_string_bytes = 0usize; 60 | 61 | for name in &sourcemap.names { 62 | total_string_bytes += name.len(); 63 | } 64 | 65 | for source in &sourcemap.sources { 66 | total_string_bytes += source.len(); 67 | } 68 | 69 | for content in &sourcemap.source_contents { 70 | total_string_bytes += content.as_ref().map_or(/*"null"*/ 4, |s| s.len()); 71 | } 72 | 73 | // Calculate total capacity needed 74 | max_segments += 9 + 13 + 20; // "names":[ + ],"sources":[ + ],"sourcesContent":[ 75 | max_segments += 6 * total_string_bytes; // worst-case escaping (* 6), \0 -> \\u0000 76 | max_segments += 2 * (names_count + sources_count + sc_count); // quotes around each item 77 | 78 | // Commas between array items 79 | let comma_count = names_count.saturating_sub(1) 80 | + sources_count.saturating_sub(1) 81 | + sc_count.saturating_sub(1); 82 | max_segments += comma_count; 83 | 84 | // Optional ],"x_google_ignoreList":[ 85 | if let Some(x_google_ignore_list) = &sourcemap.x_google_ignore_list { 86 | max_segments += 25; // ],"x_google_ignoreList":[ 87 | 88 | let ig_count = x_google_ignore_list.len(); 89 | // guess 10 digits per item, 100_000_000 maximum per element 90 | max_segments += 10 * ig_count; 91 | } 92 | 93 | // ],"mappings":" 94 | max_segments += 14; 95 | max_segments += estimate_mappings_length(sourcemap); 96 | 97 | // Optional ,"debugId":"..." 98 | if let Some(debug_id) = sourcemap.get_debug_id() { 99 | max_segments += 13 /* ,"debugId":" */ + debug_id.len(); 100 | } 101 | 102 | // "} 103 | max_segments += 2; 104 | let mut contents = PreAllocatedString::new(max_segments); 105 | 106 | contents.push("{\"version\":3,"); 107 | if let Some(file) = sourcemap.get_file() { 108 | contents.push("\"file\":\""); 109 | contents.push(file.as_ref()); 110 | contents.push("\","); 111 | } 112 | 113 | if let Some(source_root) = sourcemap.get_source_root() { 114 | contents.push("\"sourceRoot\":\""); 115 | contents.push(source_root); 116 | contents.push("\","); 117 | } 118 | 119 | contents.push("\"names\":["); 120 | contents.push_list(sourcemap.names.iter(), escape_into); 121 | 122 | contents.push("],\"sources\":["); 123 | contents.push_list(sourcemap.sources.iter(), escape_into); 124 | 125 | // Quote `source_content` in parallel 126 | let source_contents = &sourcemap.source_contents; 127 | contents.push("],\"sourcesContent\":["); 128 | contents.push_list(source_contents.iter().map(|v| v.as_deref().unwrap_or("null")), escape_into); 129 | 130 | if let Some(x_google_ignore_list) = &sourcemap.x_google_ignore_list { 131 | contents.push("],\"x_google_ignoreList\":["); 132 | contents.push_list(x_google_ignore_list.iter(), |s, output| { 133 | output.extend_from_slice(s.to_string().as_bytes()); 134 | }); 135 | } 136 | 137 | contents.push("],\"mappings\":\""); 138 | serialize_sourcemap_mappings(sourcemap, &mut contents); 139 | 140 | if let Some(debug_id) = sourcemap.get_debug_id() { 141 | contents.push("\",\"debugId\":\""); 142 | contents.push(debug_id); 143 | } 144 | 145 | contents.push("\"}"); 146 | 147 | // Check we calculated number of segments required correctly 148 | debug_assert!(contents.len() <= max_segments); 149 | 150 | contents.consume() 151 | } 152 | 153 | fn estimate_mappings_length(sourcemap: &SourceMap) -> usize { 154 | sourcemap 155 | .token_chunks 156 | .as_ref() 157 | .map(|chunks| { 158 | // Increased from 10 to 12 to account for worst-case VLQ encoding and separators 159 | // Add prev_dst_line for each chunk as those become semicolons 160 | chunks 161 | .iter() 162 | .map(|t| (t.end - t.start) as usize * 12 + t.prev_dst_line as usize) 163 | .sum::() 164 | }) 165 | .unwrap_or_else(|| { 166 | sourcemap.tokens.len() * 12 + sourcemap.tokens.last().map_or(0, |t| t.dst_line as usize) 167 | }) 168 | } 169 | 170 | fn serialize_sourcemap_mappings(sm: &SourceMap, output: &mut String) { 171 | if let Some(token_chunks) = sm.token_chunks.as_ref() { 172 | token_chunks.iter().for_each(|token_chunk| { 173 | serialize_mappings(&sm.tokens, token_chunk, output); 174 | }) 175 | } else { 176 | serialize_mappings( 177 | &sm.tokens, 178 | &TokenChunk::new(0, sm.tokens.len() as u32, 0, 0, 0, 0, 0, 0), 179 | output, 180 | ); 181 | } 182 | } 183 | 184 | // Max length of a single VLQ encoding 185 | const MAX_VLQ_BYTES: usize = 7; 186 | 187 | fn serialize_mappings(tokens: &[Token], token_chunk: &TokenChunk, output: &mut String) { 188 | let TokenChunk { 189 | start, 190 | end, 191 | mut prev_dst_line, 192 | mut prev_dst_col, 193 | mut prev_src_line, 194 | mut prev_src_col, 195 | mut prev_name_id, 196 | mut prev_source_id, 197 | } = *token_chunk; 198 | 199 | let mut prev_token = if start == 0 { None } else { Some(&tokens[start as usize - 1]) }; 200 | 201 | for token in &tokens[start as usize..end as usize] { 202 | // Max length of a single VLQ encoding is 7 bytes. Max number of calls to `encode_vlq_diff` is 5. 203 | // Also need 1 byte for each line number difference, or 1 byte if no line num difference. 204 | // Reserve this amount of capacity in `rv` early, so can skip bounds checks in code below. 205 | // As well as skipping the bounds checks, this also removes a function call to 206 | // `alloc::raw_vec::RawVec::grow_one` for every byte that's pushed. 207 | // https://godbolt.org/z/44G8jjss3 208 | const MAX_TOTAL_VLQ_BYTES: usize = 5 * MAX_VLQ_BYTES; 209 | 210 | let num_line_breaks = token.get_dst_line() - prev_dst_line; 211 | if num_line_breaks != 0 { 212 | let required = MAX_TOTAL_VLQ_BYTES + num_line_breaks as usize; 213 | if output.capacity() - output.len() < required { 214 | output.reserve(required); 215 | } 216 | // SAFETY: We have reserved sufficient capacity for `num_line_breaks` bytes 217 | unsafe { push_bytes_unchecked(output, b';', num_line_breaks) }; 218 | prev_dst_col = 0; 219 | prev_dst_line += num_line_breaks; 220 | } else if prev_token.is_some() { 221 | let required = MAX_TOTAL_VLQ_BYTES + 1; 222 | if output.capacity() - output.len() < required { 223 | output.reserve(required); 224 | } 225 | // SAFETY: We have reserved sufficient capacity for 1 byte 226 | unsafe { push_byte_unchecked(output, b',') }; 227 | } 228 | 229 | // SAFETY: We have reserved enough capacity above to satisfy safety contract 230 | // of `encode_vlq_diff` for all calls below 231 | unsafe { 232 | encode_vlq_diff(output, token.get_dst_col(), prev_dst_col); 233 | prev_dst_col = token.get_dst_col(); 234 | 235 | if let Some(source_id) = token.get_source_id() { 236 | encode_vlq_diff(output, source_id, prev_source_id); 237 | prev_source_id = source_id; 238 | encode_vlq_diff(output, token.get_src_line(), prev_src_line); 239 | prev_src_line = token.get_src_line(); 240 | encode_vlq_diff(output, token.get_src_col(), prev_src_col); 241 | prev_src_col = token.get_src_col(); 242 | if let Some(name_id) = token.get_name_id() { 243 | encode_vlq_diff(output, name_id, prev_name_id); 244 | prev_name_id = name_id; 245 | } 246 | } 247 | } 248 | 249 | prev_token = Some(token); 250 | } 251 | } 252 | 253 | /// Encode diff as VLQ and push encoding into `out`. 254 | /// Will push between 1 byte (num = 0) and 7 bytes (num = -u32::MAX). 255 | /// 256 | /// # SAFETY 257 | /// Caller must ensure at least 7 bytes spare capacity in `out`, 258 | /// as this function does not perform any bounds checks. 259 | #[inline] 260 | unsafe fn encode_vlq_diff(out: &mut String, a: u32, b: u32) { 261 | unsafe { 262 | encode_vlq(out, i64::from(a) - i64::from(b)); 263 | } 264 | } 265 | 266 | // Align chars lookup table on 64 so occupies a single cache line 267 | #[repr(align(64))] 268 | struct Aligned64([u8; 64]); 269 | 270 | static B64_CHARS: Aligned64 = Aligned64([ 271 | b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', 272 | b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', b'Z', b'a', b'b', b'c', b'd', b'e', b'f', 273 | b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', 274 | b'w', b'x', b'y', b'z', b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'+', b'/', 275 | ]); 276 | 277 | /// Encode number as VLQ and push encoding into `out`. 278 | /// Will push between 1 byte (num = 0) and 7 bytes (num = -u32::MAX). 279 | /// 280 | /// # SAFETY 281 | /// Caller must ensure at least 7 bytes spare capacity in `out`, 282 | /// as this function does not perform any bounds checks. 283 | unsafe fn encode_vlq(out: &mut String, num: i64) { 284 | unsafe { 285 | let mut num = if num < 0 { ((-num) << 1) + 1 } else { num << 1 }; 286 | 287 | // Breaking out of loop early when have reached last char (rather than conditionally adding 288 | // 32 for last char within the loop) removes 3 instructions from the loop. 289 | // https://godbolt.org/z/Es4Pavh9j 290 | // This translates to a 16% speed-up for VLQ encoding. 291 | let mut digit; 292 | loop { 293 | digit = num & 0b11111; 294 | num >>= 5; 295 | if num == 0 { 296 | break; 297 | } 298 | 299 | let b = B64_CHARS.0[digit as usize + 32]; 300 | // SAFETY: 301 | // * This loop can execute a maximum of 7 times, and on last turn will exit before getting here. 302 | // Caller promises there are at least 7 bytes spare capacity in `out` at start. We only 303 | // push 1 byte on each turn, so guaranteed there is at least 1 byte capacity in `out` here. 304 | // * All values in `B64_CHARS` lookup table are ASCII bytes. 305 | push_byte_unchecked(out, b); 306 | } 307 | 308 | let b = B64_CHARS.0[digit as usize]; 309 | // SAFETY: 310 | // * The loop above pushes max 6 bytes. Caller promises there are at least 7 bytes spare capacity 311 | // in `out` at start. So guaranteed there is at least 1 byte capacity in `out` here. 312 | // * All values in `B64_CHARS` lookup table are ASCII bytes. 313 | push_byte_unchecked(out, b); 314 | } 315 | } 316 | 317 | /// Push a byte to `out` without bounds checking. 318 | /// 319 | /// # SAFETY 320 | /// * `out` must have at least 1 byte spare capacity. 321 | /// * `b` must be an ASCII byte (i.e. not `>= 128`). 322 | // 323 | // `#[inline(always)]` to ensure that `len` is stored in a register during `encode_vlq`'s loop. 324 | #[expect(clippy::inline_always)] 325 | #[inline(always)] 326 | unsafe fn push_byte_unchecked(out: &mut String, b: u8) { 327 | unsafe { 328 | debug_assert!(out.len() < out.capacity()); 329 | debug_assert!(b.is_ascii()); 330 | 331 | let out = out.as_mut_vec(); 332 | let len = out.len(); 333 | let ptr = out.as_mut_ptr().add(len); 334 | ptr.write(b); 335 | out.set_len(len + 1); 336 | } 337 | } 338 | 339 | /// Push a byte to `out` a number of times without bounds checking. 340 | /// 341 | /// # SAFETY 342 | /// * `out` must have at least `repeats` bytes spare capacity. 343 | /// * `b` must be an ASCII byte (i.e. not `>= 128`). 344 | #[inline] 345 | unsafe fn push_bytes_unchecked(out: &mut String, b: u8, repeats: u32) { 346 | unsafe { 347 | debug_assert!(out.capacity() - out.len() >= repeats as usize); 348 | debug_assert!(b.is_ascii()); 349 | 350 | let out = out.as_mut_vec(); 351 | let len = out.len(); 352 | let mut ptr = out.as_mut_ptr().add(len); 353 | for _ in 0..repeats { 354 | ptr.write(b); 355 | ptr = ptr.add(1); 356 | } 357 | out.set_len(len + repeats as usize); 358 | } 359 | } 360 | 361 | /// A helper for pre-allocate string buffer. 362 | /// 363 | /// Pre-allocate a Cow<'a, str> buffer, and push the segment into it. 364 | /// Finally, convert it to a pre-allocated length String. 365 | #[repr(transparent)] 366 | struct PreAllocatedString(String); 367 | 368 | impl Deref for PreAllocatedString { 369 | type Target = String; 370 | 371 | fn deref(&self) -> &Self::Target { 372 | &self.0 373 | } 374 | } 375 | 376 | impl DerefMut for PreAllocatedString { 377 | fn deref_mut(&mut self) -> &mut Self::Target { 378 | &mut self.0 379 | } 380 | } 381 | 382 | impl PreAllocatedString { 383 | fn new(max_segments: usize) -> Self { 384 | Self(String::with_capacity(max_segments)) 385 | } 386 | 387 | #[inline] 388 | fn push(&mut self, s: &str) { 389 | self.0.push_str(s); 390 | } 391 | 392 | #[inline] 393 | fn push_list(&mut self, mut iter: I, encode: impl Fn(S, &mut Vec)) 394 | where 395 | I: Iterator, 396 | { 397 | let Some(first) = iter.next() else { 398 | return; 399 | }; 400 | encode(first, self.as_mut_vec()); 401 | 402 | for other in iter { 403 | self.0.push(','); 404 | encode(other, self.as_mut_vec()); 405 | } 406 | } 407 | 408 | fn as_mut_vec(&mut self) -> &mut Vec { 409 | // SAFETY: we are sure that the string is not shared 410 | unsafe { self.0.as_mut_vec() } 411 | } 412 | 413 | #[inline] 414 | fn consume(self) -> String { 415 | self.0 416 | } 417 | } 418 | 419 | #[test] 420 | fn test_encode() { 421 | let input = r#"{ 422 | "version": 3, 423 | "sources": ["coolstuff.js"], 424 | "sourceRoot": "x", 425 | "names": ["x","alert"], 426 | "mappings": "AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM", 427 | "x_google_ignoreList": [0] 428 | }"#; 429 | let sm = SourceMap::from_json_string(input).unwrap(); 430 | let sm2 = SourceMap::from_json_string(&sm.to_json_string()).unwrap(); 431 | 432 | for (tok1, tok2) in sm.get_tokens().zip(sm2.get_tokens()) { 433 | assert_eq!(tok1, tok2); 434 | } 435 | 436 | // spellchecker:off 437 | let input = r#"{ 438 | "version": 3, 439 | "file": "index.js", 440 | "names": [ 441 | "text", 442 | "text" 443 | ], 444 | "sources": [ 445 | "../../hmr.js", 446 | "../../main.js", 447 | "../../index.html" 448 | ], 449 | "sourcesContent": [ 450 | "export const foo = 'hello'\n\ntext('.hmr', foo)\n\nfunction text(el, text) {\n document.querySelector(el).textContent = text\n}\n\nimport.meta.hot?.accept((mod) =\u003E {\n if (mod) {\n text('.hmr', mod.foo)\n }\n})\n", 451 | "import './hmr.js'\n\ntext('.app', 'hello')\n\nfunction text(el, text) {\n document.querySelector(el).textContent = text\n}\n", 452 | "\u003Ch1\u003EHMR Full Bundle Mode\u003C/h1\u003E\n\n\u003Cdiv class=\"app\"\u003E\u003C/div\u003E\n\u003Cdiv class=\"hmr\"\u003E\u003C/div\u003E\n\n\u003Cscript type=\"module\" src=\"./main.js\"\u003E\u003C/script\u003E\n" 453 | ], 454 | "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,MAAa,MAAM;AAEnBA,OAAK,QAAQ,IAAI;AAEjB,SAASA,OAAK,IAAI,QAAM;AACtB,UAAS,cAAc,GAAG,CAAC,cAAcA;;SAG1B,QAAQ,QAAQ;AAC/B,KAAI,KAAK;AACP,SAAK,QAAQ,IAAI,IAAI;;EAEvB;;;;;;;ACVF,KAAK,QAAQ,QAAQ;AAErB,SAAS,KAAK,IAAI,QAAM;AACtB,UAAS,cAAc,GAAG,CAAC,cAAcC" 455 | }"#; 456 | // spellchecker:on 457 | let sm = SourceMap::from_json_string(input).unwrap(); 458 | let sm2 = SourceMap::from_json_string(&sm.to_json_string()).unwrap(); 459 | 460 | for (tok1, tok2) in sm.get_tokens().zip(sm2.get_tokens()) { 461 | assert_eq!(tok1, tok2); 462 | } 463 | } 464 | 465 | #[test] 466 | fn test_encode_escape_string() { 467 | // '\0' should be escaped. 468 | let mut sm = SourceMap::new( 469 | None, 470 | vec!["name_length_greater_than_16_\0".into()], 471 | None, 472 | vec!["\0".into()], 473 | vec![Some("emoji-👀-\0".into())], 474 | vec![].into_boxed_slice(), 475 | None, 476 | ); 477 | sm.set_x_google_ignore_list(vec![0]); 478 | sm.set_debug_id("56431d54-c0a6-451d-8ea2-ba5de5d8ca2e"); 479 | assert_eq!( 480 | sm.to_json_string(), 481 | r#"{"version":3,"names":["name_length_greater_than_16_\u0000"],"sources":["\u0000"],"sourcesContent":["emoji-👀-\u0000"],"x_google_ignoreList":[0],"mappings":"","debugId":"56431d54-c0a6-451d-8ea2-ba5de5d8ca2e"}"# 482 | ); 483 | } 484 | 485 | #[test] 486 | fn test_vlq_encode_diff() { 487 | // Most import tests here are that with maximum values, `encode_vlq_diff` pushes maximum of 7 bytes. 488 | // This invariant is essential to safety of `encode_vlq_diff`. 489 | #[rustfmt::skip] 490 | const FIXTURES: &[(u32, u32, &str)] = &[ 491 | (0, 0, "A"), 492 | (1, 0, "C"), 493 | (2, 0, "E"), 494 | (15, 0, "e"), 495 | (16, 0, "gB"), 496 | (511, 0, "+f"), 497 | (512, 0, "ggB"), 498 | (16_383, 0, "+/f"), 499 | (16_384, 0, "gggB"), 500 | (524_287, 0, "+//f"), 501 | (524_288, 0, "ggggB"), 502 | (16_777_215, 0, "+///f"), 503 | (16_777_216, 0, "gggggB"), 504 | (536_870_911, 0, "+////f"), 505 | (536_870_912, 0, "ggggggB"), 506 | (u32::MAX, 0, "+/////H"), // 7 bytes 507 | 508 | (0, 1, "D"), 509 | (0, 2, "F"), 510 | (0, 15, "f"), 511 | (0, 16, "hB"), 512 | (0, 511, "/f"), 513 | (0, 512, "hgB"), 514 | (0, 16_383, "//f"), 515 | (0, 16_384, "hggB"), 516 | (0, 524_287, "///f"), 517 | (0, 524_288, "hgggB"), 518 | (0, 16_777_215, "////f"), 519 | (0, 16_777_216, "hggggB"), 520 | (0, 536_870_911, "/////f"), 521 | (0, 536_870_912, "hgggggB"), 522 | (0, u32::MAX, "//////H"), // 7 bytes 523 | ]; 524 | 525 | for (a, b, res) in FIXTURES.iter().copied() { 526 | let mut out = String::with_capacity(MAX_VLQ_BYTES); 527 | // SAFETY: `out` has 7 bytes spare capacity 528 | unsafe { encode_vlq_diff(&mut out, a, b) }; 529 | assert_eq!(&out, res); 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anes" 16 | version = "0.2.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "dc43e46599f3d77fcf2f2ca89e4d962910b0c19c44e7b58679cbbdfd1820a662" 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.100" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 25 | 26 | [[package]] 27 | name = "approx" 28 | version = "0.5.1" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" 31 | dependencies = [ 32 | "num-traits", 33 | ] 34 | 35 | [[package]] 36 | name = "autocfg" 37 | version = "1.5.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 40 | 41 | [[package]] 42 | name = "base64-simd" 43 | version = "0.8.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" 46 | dependencies = [ 47 | "outref", 48 | "vsimd", 49 | ] 50 | 51 | [[package]] 52 | name = "bincode" 53 | version = "1.3.3" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 56 | dependencies = [ 57 | "serde", 58 | ] 59 | 60 | [[package]] 61 | name = "bitflags" 62 | version = "2.10.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 65 | 66 | [[package]] 67 | name = "bpaf" 68 | version = "0.9.20" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "473976d7a8620bb1e06dcdd184407c2363fe4fec8e983ee03ed9197222634a31" 71 | 72 | [[package]] 73 | name = "bstr" 74 | version = "1.12.1" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" 77 | dependencies = [ 78 | "memchr", 79 | "serde", 80 | ] 81 | 82 | [[package]] 83 | name = "bumpalo" 84 | version = "3.19.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 87 | 88 | [[package]] 89 | name = "cast" 90 | version = "0.3.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 93 | 94 | [[package]] 95 | name = "cfg-if" 96 | version = "1.0.4" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 99 | 100 | [[package]] 101 | name = "cfg_aliases" 102 | version = "0.2.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 105 | 106 | [[package]] 107 | name = "ciborium" 108 | version = "0.2.2" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 111 | dependencies = [ 112 | "ciborium-io", 113 | "ciborium-ll", 114 | "serde", 115 | ] 116 | 117 | [[package]] 118 | name = "ciborium-io" 119 | version = "0.2.2" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 122 | 123 | [[package]] 124 | name = "ciborium-ll" 125 | version = "0.2.2" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 128 | dependencies = [ 129 | "ciborium-io", 130 | "half", 131 | ] 132 | 133 | [[package]] 134 | name = "codspeed" 135 | version = "3.0.5" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "35584c5fcba8059780748866387fb97c5a203bcfc563fc3d0790af406727a117" 138 | dependencies = [ 139 | "anyhow", 140 | "bincode", 141 | "colored 2.2.0", 142 | "glob", 143 | "libc", 144 | "nix", 145 | "serde", 146 | "serde_json", 147 | "statrs", 148 | "uuid", 149 | ] 150 | 151 | [[package]] 152 | name = "colored" 153 | version = "2.2.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" 156 | dependencies = [ 157 | "lazy_static", 158 | "windows-sys 0.59.0", 159 | ] 160 | 161 | [[package]] 162 | name = "colored" 163 | version = "3.0.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" 166 | dependencies = [ 167 | "windows-sys 0.59.0", 168 | ] 169 | 170 | [[package]] 171 | name = "console" 172 | version = "0.15.11" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 175 | dependencies = [ 176 | "encode_unicode", 177 | "libc", 178 | "once_cell", 179 | "windows-sys 0.59.0", 180 | ] 181 | 182 | [[package]] 183 | name = "convert_case" 184 | version = "0.10.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" 187 | dependencies = [ 188 | "unicode-segmentation", 189 | ] 190 | 191 | [[package]] 192 | name = "criterion2" 193 | version = "3.0.2" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "77cd1059d67baa066c334993d8d6e757ad257d21030db6a9a945dddbb559d4fe" 196 | dependencies = [ 197 | "anes", 198 | "bpaf", 199 | "cast", 200 | "ciborium", 201 | "codspeed", 202 | "colored 3.0.0", 203 | "num-traits", 204 | "oorandom", 205 | "serde", 206 | "serde_json", 207 | "walkdir", 208 | ] 209 | 210 | [[package]] 211 | name = "crunchy" 212 | version = "0.2.4" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" 215 | 216 | [[package]] 217 | name = "ctor" 218 | version = "0.6.1" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "3ffc71fcdcdb40d6f087edddf7f8f1f8f79e6cf922f555a9ee8779752d4819bd" 221 | dependencies = [ 222 | "ctor-proc-macro", 223 | "dtor", 224 | ] 225 | 226 | [[package]] 227 | name = "ctor-proc-macro" 228 | version = "0.0.7" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" 231 | 232 | [[package]] 233 | name = "dtor" 234 | version = "0.1.1" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" 237 | dependencies = [ 238 | "dtor-proc-macro", 239 | ] 240 | 241 | [[package]] 242 | name = "dtor-proc-macro" 243 | version = "0.0.6" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" 246 | 247 | [[package]] 248 | name = "encode_unicode" 249 | version = "1.0.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 252 | 253 | [[package]] 254 | name = "futures" 255 | version = "0.3.31" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 258 | dependencies = [ 259 | "futures-channel", 260 | "futures-core", 261 | "futures-executor", 262 | "futures-io", 263 | "futures-sink", 264 | "futures-task", 265 | "futures-util", 266 | ] 267 | 268 | [[package]] 269 | name = "futures-channel" 270 | version = "0.3.31" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 273 | dependencies = [ 274 | "futures-core", 275 | "futures-sink", 276 | ] 277 | 278 | [[package]] 279 | name = "futures-core" 280 | version = "0.3.31" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 283 | 284 | [[package]] 285 | name = "futures-executor" 286 | version = "0.3.31" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 289 | dependencies = [ 290 | "futures-core", 291 | "futures-task", 292 | "futures-util", 293 | ] 294 | 295 | [[package]] 296 | name = "futures-io" 297 | version = "0.3.31" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 300 | 301 | [[package]] 302 | name = "futures-macro" 303 | version = "0.3.31" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 306 | dependencies = [ 307 | "proc-macro2", 308 | "quote", 309 | "syn", 310 | ] 311 | 312 | [[package]] 313 | name = "futures-sink" 314 | version = "0.3.31" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 317 | 318 | [[package]] 319 | name = "futures-task" 320 | version = "0.3.31" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 323 | 324 | [[package]] 325 | name = "futures-util" 326 | version = "0.3.31" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 329 | dependencies = [ 330 | "futures-channel", 331 | "futures-core", 332 | "futures-io", 333 | "futures-macro", 334 | "futures-sink", 335 | "futures-task", 336 | "memchr", 337 | "pin-project-lite", 338 | "pin-utils", 339 | "slab", 340 | ] 341 | 342 | [[package]] 343 | name = "getrandom" 344 | version = "0.3.4" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 347 | dependencies = [ 348 | "cfg-if", 349 | "libc", 350 | "r-efi", 351 | "wasip2", 352 | ] 353 | 354 | [[package]] 355 | name = "glob" 356 | version = "0.3.3" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 359 | 360 | [[package]] 361 | name = "globset" 362 | version = "0.4.16" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" 365 | dependencies = [ 366 | "aho-corasick", 367 | "bstr", 368 | "log", 369 | "regex-automata", 370 | "regex-syntax", 371 | ] 372 | 373 | [[package]] 374 | name = "half" 375 | version = "2.7.1" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" 378 | dependencies = [ 379 | "cfg-if", 380 | "crunchy", 381 | "zerocopy", 382 | ] 383 | 384 | [[package]] 385 | name = "insta" 386 | version = "1.44.3" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698" 389 | dependencies = [ 390 | "console", 391 | "globset", 392 | "once_cell", 393 | "similar", 394 | "walkdir", 395 | ] 396 | 397 | [[package]] 398 | name = "itoa" 399 | version = "1.0.15" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 402 | 403 | [[package]] 404 | name = "js-sys" 405 | version = "0.3.82" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" 408 | dependencies = [ 409 | "once_cell", 410 | "wasm-bindgen", 411 | ] 412 | 413 | [[package]] 414 | name = "json-escape-simd" 415 | version = "3.0.1" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "a3c2a6c0b4b5637c41719973ef40c6a1cf564f9db6958350de6193fbee9c23f5" 418 | 419 | [[package]] 420 | name = "lazy_static" 421 | version = "1.5.0" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 424 | 425 | [[package]] 426 | name = "libc" 427 | version = "0.2.177" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 430 | 431 | [[package]] 432 | name = "libloading" 433 | version = "0.9.0" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" 436 | dependencies = [ 437 | "cfg-if", 438 | "windows-link", 439 | ] 440 | 441 | [[package]] 442 | name = "log" 443 | version = "0.4.28" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 446 | 447 | [[package]] 448 | name = "memchr" 449 | version = "2.7.6" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 452 | 453 | [[package]] 454 | name = "napi" 455 | version = "3.7.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "f27a163b545fd2184d2efdccf3d3df56acdb63465f2fcfebcaee0463c1e91783" 458 | dependencies = [ 459 | "bitflags", 460 | "ctor", 461 | "futures", 462 | "napi-build", 463 | "napi-sys", 464 | "nohash-hasher", 465 | "rustc-hash", 466 | ] 467 | 468 | [[package]] 469 | name = "napi-build" 470 | version = "2.3.1" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" 473 | 474 | [[package]] 475 | name = "napi-derive" 476 | version = "3.4.0" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "47cffa09ea668c4cc5d7b1198780882e28780ed1804a903b80680725426223d9" 479 | dependencies = [ 480 | "convert_case", 481 | "ctor", 482 | "napi-derive-backend", 483 | "proc-macro2", 484 | "quote", 485 | "syn", 486 | ] 487 | 488 | [[package]] 489 | name = "napi-derive-backend" 490 | version = "4.0.0" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "5e186227ec22f4675267a176d98dffecb27e6cc88926cbb7efb5427268565c0f" 493 | dependencies = [ 494 | "convert_case", 495 | "proc-macro2", 496 | "quote", 497 | "semver", 498 | "syn", 499 | ] 500 | 501 | [[package]] 502 | name = "napi-sys" 503 | version = "3.2.1" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "8eb602b84d7c1edae45e50bbf1374696548f36ae179dfa667f577e384bb90c2b" 506 | dependencies = [ 507 | "libloading", 508 | ] 509 | 510 | [[package]] 511 | name = "nix" 512 | version = "0.29.0" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 515 | dependencies = [ 516 | "bitflags", 517 | "cfg-if", 518 | "cfg_aliases", 519 | "libc", 520 | ] 521 | 522 | [[package]] 523 | name = "nohash-hasher" 524 | version = "0.2.0" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" 527 | 528 | [[package]] 529 | name = "num-traits" 530 | version = "0.2.19" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 533 | dependencies = [ 534 | "autocfg", 535 | ] 536 | 537 | [[package]] 538 | name = "once_cell" 539 | version = "1.21.3" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 542 | 543 | [[package]] 544 | name = "oorandom" 545 | version = "11.1.5" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" 548 | 549 | [[package]] 550 | name = "outref" 551 | version = "0.5.2" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" 554 | 555 | [[package]] 556 | name = "oxc_sourcemap" 557 | version = "6.0.1" 558 | dependencies = [ 559 | "base64-simd", 560 | "criterion2", 561 | "insta", 562 | "json-escape-simd", 563 | "napi", 564 | "napi-build", 565 | "napi-derive", 566 | "rustc-hash", 567 | "serde", 568 | "serde_json", 569 | ] 570 | 571 | [[package]] 572 | name = "pin-project-lite" 573 | version = "0.2.16" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 576 | 577 | [[package]] 578 | name = "pin-utils" 579 | version = "0.1.0" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 582 | 583 | [[package]] 584 | name = "proc-macro2" 585 | version = "1.0.103" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 588 | dependencies = [ 589 | "unicode-ident", 590 | ] 591 | 592 | [[package]] 593 | name = "quote" 594 | version = "1.0.42" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 597 | dependencies = [ 598 | "proc-macro2", 599 | ] 600 | 601 | [[package]] 602 | name = "r-efi" 603 | version = "5.3.0" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 606 | 607 | [[package]] 608 | name = "regex-automata" 609 | version = "0.4.13" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 612 | dependencies = [ 613 | "aho-corasick", 614 | "memchr", 615 | "regex-syntax", 616 | ] 617 | 618 | [[package]] 619 | name = "regex-syntax" 620 | version = "0.8.8" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 623 | 624 | [[package]] 625 | name = "rustc-hash" 626 | version = "2.1.1" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 629 | 630 | [[package]] 631 | name = "rustversion" 632 | version = "1.0.22" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 635 | 636 | [[package]] 637 | name = "ryu" 638 | version = "1.0.20" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 641 | 642 | [[package]] 643 | name = "same-file" 644 | version = "1.0.6" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 647 | dependencies = [ 648 | "winapi-util", 649 | ] 650 | 651 | [[package]] 652 | name = "semver" 653 | version = "1.0.27" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 656 | 657 | [[package]] 658 | name = "serde" 659 | version = "1.0.228" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 662 | dependencies = [ 663 | "serde_core", 664 | "serde_derive", 665 | ] 666 | 667 | [[package]] 668 | name = "serde_core" 669 | version = "1.0.228" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 672 | dependencies = [ 673 | "serde_derive", 674 | ] 675 | 676 | [[package]] 677 | name = "serde_derive" 678 | version = "1.0.228" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 681 | dependencies = [ 682 | "proc-macro2", 683 | "quote", 684 | "syn", 685 | ] 686 | 687 | [[package]] 688 | name = "serde_json" 689 | version = "1.0.145" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 692 | dependencies = [ 693 | "itoa", 694 | "memchr", 695 | "ryu", 696 | "serde", 697 | "serde_core", 698 | ] 699 | 700 | [[package]] 701 | name = "similar" 702 | version = "2.7.0" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" 705 | 706 | [[package]] 707 | name = "slab" 708 | version = "0.4.11" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 711 | 712 | [[package]] 713 | name = "statrs" 714 | version = "0.18.0" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" 717 | dependencies = [ 718 | "approx", 719 | "num-traits", 720 | ] 721 | 722 | [[package]] 723 | name = "syn" 724 | version = "2.0.109" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" 727 | dependencies = [ 728 | "proc-macro2", 729 | "quote", 730 | "unicode-ident", 731 | ] 732 | 733 | [[package]] 734 | name = "unicode-ident" 735 | version = "1.0.22" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 738 | 739 | [[package]] 740 | name = "unicode-segmentation" 741 | version = "1.12.0" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 744 | 745 | [[package]] 746 | name = "uuid" 747 | version = "1.18.1" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" 750 | dependencies = [ 751 | "getrandom", 752 | "js-sys", 753 | "wasm-bindgen", 754 | ] 755 | 756 | [[package]] 757 | name = "vsimd" 758 | version = "0.8.0" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" 761 | 762 | [[package]] 763 | name = "walkdir" 764 | version = "2.5.0" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 767 | dependencies = [ 768 | "same-file", 769 | "winapi-util", 770 | ] 771 | 772 | [[package]] 773 | name = "wasip2" 774 | version = "1.0.1+wasi-0.2.4" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 777 | dependencies = [ 778 | "wit-bindgen", 779 | ] 780 | 781 | [[package]] 782 | name = "wasm-bindgen" 783 | version = "0.2.105" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" 786 | dependencies = [ 787 | "cfg-if", 788 | "once_cell", 789 | "rustversion", 790 | "wasm-bindgen-macro", 791 | "wasm-bindgen-shared", 792 | ] 793 | 794 | [[package]] 795 | name = "wasm-bindgen-macro" 796 | version = "0.2.105" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" 799 | dependencies = [ 800 | "quote", 801 | "wasm-bindgen-macro-support", 802 | ] 803 | 804 | [[package]] 805 | name = "wasm-bindgen-macro-support" 806 | version = "0.2.105" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" 809 | dependencies = [ 810 | "bumpalo", 811 | "proc-macro2", 812 | "quote", 813 | "syn", 814 | "wasm-bindgen-shared", 815 | ] 816 | 817 | [[package]] 818 | name = "wasm-bindgen-shared" 819 | version = "0.2.105" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" 822 | dependencies = [ 823 | "unicode-ident", 824 | ] 825 | 826 | [[package]] 827 | name = "winapi-util" 828 | version = "0.1.11" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 831 | dependencies = [ 832 | "windows-sys 0.61.2", 833 | ] 834 | 835 | [[package]] 836 | name = "windows-link" 837 | version = "0.2.1" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 840 | 841 | [[package]] 842 | name = "windows-sys" 843 | version = "0.59.0" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 846 | dependencies = [ 847 | "windows-targets", 848 | ] 849 | 850 | [[package]] 851 | name = "windows-sys" 852 | version = "0.61.2" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 855 | dependencies = [ 856 | "windows-link", 857 | ] 858 | 859 | [[package]] 860 | name = "windows-targets" 861 | version = "0.52.6" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 864 | dependencies = [ 865 | "windows_aarch64_gnullvm", 866 | "windows_aarch64_msvc", 867 | "windows_i686_gnu", 868 | "windows_i686_gnullvm", 869 | "windows_i686_msvc", 870 | "windows_x86_64_gnu", 871 | "windows_x86_64_gnullvm", 872 | "windows_x86_64_msvc", 873 | ] 874 | 875 | [[package]] 876 | name = "windows_aarch64_gnullvm" 877 | version = "0.52.6" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 880 | 881 | [[package]] 882 | name = "windows_aarch64_msvc" 883 | version = "0.52.6" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 886 | 887 | [[package]] 888 | name = "windows_i686_gnu" 889 | version = "0.52.6" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 892 | 893 | [[package]] 894 | name = "windows_i686_gnullvm" 895 | version = "0.52.6" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 898 | 899 | [[package]] 900 | name = "windows_i686_msvc" 901 | version = "0.52.6" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 904 | 905 | [[package]] 906 | name = "windows_x86_64_gnu" 907 | version = "0.52.6" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 910 | 911 | [[package]] 912 | name = "windows_x86_64_gnullvm" 913 | version = "0.52.6" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 916 | 917 | [[package]] 918 | name = "windows_x86_64_msvc" 919 | version = "0.52.6" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 922 | 923 | [[package]] 924 | name = "wit-bindgen" 925 | version = "0.46.0" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 928 | 929 | [[package]] 930 | name = "zerocopy" 931 | version = "0.8.27" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" 934 | dependencies = [ 935 | "zerocopy-derive", 936 | ] 937 | 938 | [[package]] 939 | name = "zerocopy-derive" 940 | version = "0.8.27" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" 943 | dependencies = [ 944 | "proc-macro2", 945 | "quote", 946 | "syn", 947 | ] 948 | --------------------------------------------------------------------------------