├── .github ├── renovate.json └── workflows │ ├── ci.yml │ ├── release.yml │ └── zizmor.yml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── justfile ├── rust-toolchain.toml ├── src ├── concat_sourcemap_builder.rs ├── decode.rs ├── encode.rs ├── error.rs ├── lib.rs ├── napi.rs ├── sourcemap.rs ├── sourcemap_builder.rs ├── sourcemap_visualizer.rs └── token.rs └── tests ├── concat_sourcemap_builder.rs ├── fixtures ├── basic │ ├── test.js │ ├── test.js.map │ └── visualizer.snap ├── esbuild │ ├── README.md │ ├── example.js │ ├── example.js.map │ └── visualizer.snap └── swap │ ├── test.js │ ├── test.js.map │ └── visualizer.snap ├── fixtures_concat_sourcemap_builder └── empty │ ├── dep1.js │ ├── dep1.js.map │ ├── dep2.js │ ├── dep2.js.map │ ├── dep3.js │ └── dep3.js.map ├── main.rs └── snapshots ├── concat_sourcemap_builder__empty.snap └── main__invalid_token_position.snap /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>Boshen/renovate"] 4 | } 5 | -------------------------------------------------------------------------------- /.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@cd82e1efec7fef815e2c23d296756f31c7cdc03d # v1.0.0 46 | with: 47 | save-cache: ${{ github.ref_name == 'main' }} 48 | - run: cargo check --all-targets --all-features 49 | - run: cargo test 50 | 51 | lint: 52 | name: Clippy 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 56 | 57 | - uses: oxc-project/setup-rust@cd82e1efec7fef815e2c23d296756f31c7cdc03d # v1.0.0 58 | with: 59 | components: clippy rust-docs rustfmt 60 | 61 | - run: | 62 | cargo fmt --check 63 | cargo clippy --all-targets --all-features -- -D warnings 64 | RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --document-private-items 65 | 66 | - uses: crate-ci/typos@b1ae8d918b6e85bd611117d3d9a3be4f903ee5e4 # v1.33.1 67 | with: 68 | files: . 69 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**/*.md' 9 | - '**/*.yml' 10 | - '!.github/workflows/release.yml' 11 | 12 | jobs: 13 | release-plz: 14 | name: Release-plz 15 | runs-on: ubuntu-latest 16 | permissions: 17 | pull-requests: write 18 | contents: write 19 | steps: 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | with: 22 | fetch-depth: 0 23 | token: ${{ secrets.OXC_BOT_PAT }} 24 | persist-credentials: true # required by release-plz 25 | 26 | - uses: oxc-project/setup-rust@cd82e1efec7fef815e2c23d296756f31c7cdc03d # v1.0.0 27 | 28 | - uses: MarcoIeni/release-plz-action@dde7b63054529c440305a924e5849c68318bcc9a # v0.5.107 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.OXC_BOT_PAT }} 31 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 32 | -------------------------------------------------------------------------------- /.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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | with: 26 | persist-credentials: false 27 | 28 | - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4 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@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 37 | with: 38 | sarif_file: results.sarif 39 | category: zizmor 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | style_edition = "2024" 2 | use_small_heuristics = "Max" 3 | use_field_init_shorthand = true 4 | reorder_modules = true 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this package will be documented in this file. 4 | 5 | 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. 6 | 7 | ## [3.0.2](https://github.com/oxc-project/oxc-sourcemap/compare/v3.0.1...v3.0.2) - 2025-05-19 8 | 9 | ### Other 10 | 11 | - napi beta 12 | 13 | ## [3.0.1](https://github.com/oxc-project/oxc-sourcemap/compare/v3.0.0...v3.0.1) - 2025-05-10 14 | 15 | ### Fixed 16 | 17 | - sources_content should be Vec>> ([#50](https://github.com/oxc-project/oxc-sourcemap/pull/50)) 18 | 19 | ## [3.0.0](https://github.com/oxc-project/oxc-sourcemap/compare/v2.0.2...v3.0.0) - 2025-03-03 20 | 21 | ### Added 22 | 23 | - support `x_google_ignoreList` in more places ([#30](https://github.com/oxc-project/oxc-sourcemap/pull/30)) 24 | 25 | ## [2.0.2](https://github.com/oxc-project/oxc-sourcemap/compare/v2.0.1...v2.0.2) - 2025-02-22 26 | 27 | ### Other 28 | 29 | - Rust Edition 2024 ([#24](https://github.com/oxc-project/oxc-sourcemap/pull/24)) 30 | 31 | ## [2.0.1](https://github.com/oxc-project/oxc-sourcemap/compare/v2.0.0...v2.0.1) - 2025-02-21 32 | 33 | ### Other 34 | 35 | - include build.rs 36 | 37 | ## [2.0.0](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.7...v2.0.0) - 2025-02-21 38 | 39 | ### Fixed 40 | 41 | - broken cargo features 42 | 43 | ## [1.0.7](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.6...v1.0.7) - 2025-02-11 44 | 45 | ### Fixed 46 | 47 | - add napi build.rs (#19) 48 | 49 | ## [1.0.6](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.5...v1.0.6) - 2024-12-15 50 | 51 | ### Fixed 52 | 53 | - handle non existing token position in visualizer (#14) 54 | 55 | ## [1.0.5](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.4...v1.0.5) - 2024-12-11 56 | 57 | ### Fixed 58 | 59 | - *(lookup_token)* should be None if original tokens hasn't the line (#9) 60 | 61 | ## [1.0.4](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.3...v1.0.4) - 2024-12-10 62 | 63 | ### Fixed 64 | 65 | - fix wrong source id when concatenating empty source map (#7) 66 | 67 | ### Other 68 | 69 | - update README 70 | 71 | ## [1.0.3](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.2...v1.0.3) - 2024-12-03 72 | 73 | ### Other 74 | 75 | - rename feature `concurrent` to `rayon` 76 | 77 | ## [1.0.2](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.1...v1.0.2) - 2024-12-03 78 | 79 | ### Other 80 | 81 | - `pub mod napi` 82 | 83 | ## [1.0.1](https://github.com/oxc-project/oxc-sourcemap/compare/v1.0.0...v1.0.1) - 2024-12-03 84 | 85 | ### Other 86 | 87 | - remove unused lint 88 | - add `napi` feature 89 | 90 | ## [0.37.0] - 2024-11-21 91 | 92 | ### Bug Fixes 93 | 94 | - 3d66929 sourcemap: Improve source map visualizer (#7386) (Hiroshi Ogawa) 95 | 96 | ## [0.31.0] - 2024-10-08 97 | 98 | ### Features 99 | 100 | - f6e42b6 sourcemap: Add support for sourcemap debug IDs (#6221) (Tim Fish) 101 | 102 | ## [0.30.4] - 2024-09-28 103 | 104 | ### Bug Fixes 105 | 106 | - 6f98aad sourcemap: Align sourcemap type with Rollup (#6133) (Boshen) 107 | 108 | ## [0.29.0] - 2024-09-13 109 | 110 | ### Performance 111 | 112 | - d18c896 rust: Use `cow_utils` instead (#5664) (dalaoshu) 113 | 114 | ## [0.28.0] - 2024-09-11 115 | 116 | ### Documentation 117 | 118 | - fefbbc1 sourcemap: Add trailing newline to README (#5539) (overlookmotel) 119 | 120 | ## [0.24.3] - 2024-08-18 121 | 122 | ### Refactor 123 | 124 | - 5fd1701 sourcemap: Lower the `msrv`. (#4873) (rzvxa) 125 | 126 | ## [0.24.0] - 2024-08-08 127 | 128 | ### Features 129 | 130 | - e42ac3a sourcemap: Add `ConcatSourceMapBuilder::from_sourcemaps` (#4639) (overlookmotel) 131 | 132 | ### Performance 133 | 134 | - ff43dff sourcemap: Speed up VLQ encoding (#4633) (overlookmotel) 135 | - a330773 sourcemap: Reduce string copying in `ConcatSourceMapBuilder` (#4638) (overlookmotel) 136 | - 372316b sourcemap: `ConcatSourceMapBuilder` extend `source_contents` in separate loop (#4634) (overlookmotel) 137 | - c7f1d48 sourcemap: Keep local copy of previous token in VLQ encode (#4596) (overlookmotel) 138 | - 590d795 sourcemap: Shorten main loop encoding VLQ (#4586) (overlookmotel) 139 | 140 | ## [0.23.1] - 2024-08-06 141 | 142 | ### Features 143 | 144 | - e42ac3a sourcemap: Add `ConcatSourceMapBuilder::from_sourcemaps` (#4639) (overlookmotel) 145 | 146 | ### Performance 147 | 148 | - ff43dff sourcemap: Speed up VLQ encoding (#4633) (overlookmotel) 149 | - a330773 sourcemap: Reduce string copying in `ConcatSourceMapBuilder` (#4638) (overlookmotel) 150 | - 372316b sourcemap: `ConcatSourceMapBuilder` extend `source_contents` in separate loop (#4634) (overlookmotel) 151 | - c7f1d48 sourcemap: Keep local copy of previous token in VLQ encode (#4596) (overlookmotel) 152 | - 590d795 sourcemap: Shorten main loop encoding VLQ (#4586) (overlookmotel) 153 | 154 | ## [0.23.0] - 2024-08-01 155 | 156 | - 27fd062 sourcemap: [**BREAKING**] Avoid passing `Result`s (#4541) (overlookmotel) 157 | 158 | ### Performance 159 | 160 | - d00014e sourcemap: Elide bounds checks in VLQ encoding (#4583) (overlookmotel) 161 | - 1fd9dd0 sourcemap: Use simd to escape JSON string (#4487) (Brooooooklyn) 162 | 163 | ### Refactor 164 | 165 | - 7c42ffc sourcemap: Align Base64 chars lookup table to cache line (#4535) (overlookmotel) 166 | 167 | ## [0.22.1] - 2024-07-27 168 | 169 | ### Bug Fixes 170 | 171 | - 5db7bed sourcemap: Fix pre-calculation of required segments for building JSON (#4490) (overlookmotel) 172 | 173 | ### Performance 174 | 175 | - 705e19f sourcemap: Reduce memory copies encoding JSON (#4489) (overlookmotel) 176 | - 4d10c6c sourcemap: Pre allocate String buf while encoding (#4476) (Brooooooklyn) 177 | 178 | ### Refactor 179 | 180 | - c958a55 sourcemap: `push_list` method for building JSON (#4486) (overlookmotel) 181 | 182 | ## [0.22.0] - 2024-07-23 183 | 184 | ### Bug Fixes 185 | 186 | - 4cd5df0 sourcemap: Avoid negative line if token_chunks has same prev_dst_line (#4348) (underfin) 187 | 188 | ## [0.21.0] - 2024-07-18 189 | 190 | ### Features 191 | 192 | - 205c259 sourcemap: Support SourceMapBuilder#token_chunks (#4220) (underfin) 193 | 194 | ## [0.16.0] - 2024-06-26 195 | 196 | ### Features 197 | 198 | - 01572f0 sourcemap: Impl `std::fmt::Display` for `Error` (#3902) (DonIsaac)- d3cd3ea Oxc transform binding (#3896) (underfin) 199 | 200 | ## [0.13.1] - 2024-05-22 201 | 202 | ### Features 203 | 204 | - 90d2d09 sourcemap: Add Sourcemap#from_json method (#3361) (underfin) 205 | 206 | ### Bug Fixes 207 | - 899a52b Fix some nightly warnings (Boshen) 208 | 209 | ## [0.13.0] - 2024-05-14 210 | 211 | ### Features 212 | 213 | - f6daf0b sourcemap: Add feature "sourcemap_concurrent" (Boshen) 214 | - 7363e14 sourcemap: Add "rayon" feature (#3198) (Boshen) 215 | 216 | ## [0.12.3] - 2024-04-11 217 | 218 | ### Features 219 | 220 | - 8662f4f sourcemap: Add x_google_ignoreList (#2928) (underfin) 221 | - 5cb3991 sourcemap: Add sourceRoot (#2926) (underfin) 222 | 223 | ## [0.12.2] - 2024-04-08 224 | 225 | ### Features 226 | 227 | - 96f02e6 sourcemap: Optional JSONSourceMap fields (#2910) (underfin) 228 | - d87cf17 sourcemap: Add methods to mutate SourceMap (#2909) (underfin) 229 | - 74aca1c sourcemap: Add SourceMapBuilder file (#2908) (underfin) 230 | 231 | ## [0.12.1] - 2024-04-03 232 | 233 | ### Bug Fixes 234 | 235 | - 28fae2e sourcemap: Using serde_json::to_string to quote sourcemap string (#2889) (underfin) 236 | 237 | ## [0.11.0] - 2024-03-30 238 | 239 | ### Features 240 | - b199cb8 Add oxc sourcemap crate (#2825) (underfin) 241 | 242 | ### Bug Fixes 243 | 244 | - 6177c2f codegen: Sourcemap token name should be original name (#2843) (underfin) 245 | 246 | -------------------------------------------------------------------------------- /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.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "base64-simd" 16 | version = "0.8.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" 19 | dependencies = [ 20 | "outref", 21 | "vsimd", 22 | ] 23 | 24 | [[package]] 25 | name = "bitflags" 26 | version = "2.9.1" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 29 | 30 | [[package]] 31 | name = "bstr" 32 | version = "1.12.0" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" 35 | dependencies = [ 36 | "memchr", 37 | "serde", 38 | ] 39 | 40 | [[package]] 41 | name = "cfg-if" 42 | version = "1.0.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 45 | 46 | [[package]] 47 | name = "console" 48 | version = "0.15.11" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 51 | dependencies = [ 52 | "encode_unicode", 53 | "libc", 54 | "once_cell", 55 | "windows-sys", 56 | ] 57 | 58 | [[package]] 59 | name = "convert_case" 60 | version = "0.8.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" 63 | dependencies = [ 64 | "unicode-segmentation", 65 | ] 66 | 67 | [[package]] 68 | name = "cow-utils" 69 | version = "0.1.3" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" 72 | 73 | [[package]] 74 | name = "crossbeam-deque" 75 | version = "0.8.6" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 78 | dependencies = [ 79 | "crossbeam-epoch", 80 | "crossbeam-utils", 81 | ] 82 | 83 | [[package]] 84 | name = "crossbeam-epoch" 85 | version = "0.9.18" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 88 | dependencies = [ 89 | "crossbeam-utils", 90 | ] 91 | 92 | [[package]] 93 | name = "crossbeam-utils" 94 | version = "0.8.21" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 97 | 98 | [[package]] 99 | name = "ctor" 100 | version = "0.4.2" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "a4735f265ba6a1188052ca32d461028a7d1125868be18e287e756019da7607b5" 103 | dependencies = [ 104 | "ctor-proc-macro", 105 | "dtor", 106 | ] 107 | 108 | [[package]] 109 | name = "ctor-proc-macro" 110 | version = "0.0.5" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d" 113 | 114 | [[package]] 115 | name = "dtor" 116 | version = "0.0.6" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8" 119 | dependencies = [ 120 | "dtor-proc-macro", 121 | ] 122 | 123 | [[package]] 124 | name = "dtor-proc-macro" 125 | version = "0.0.5" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" 128 | 129 | [[package]] 130 | name = "either" 131 | version = "1.15.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 134 | 135 | [[package]] 136 | name = "encode_unicode" 137 | version = "1.0.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 140 | 141 | [[package]] 142 | name = "globset" 143 | version = "0.4.16" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" 146 | dependencies = [ 147 | "aho-corasick", 148 | "bstr", 149 | "log", 150 | "regex-automata", 151 | "regex-syntax", 152 | ] 153 | 154 | [[package]] 155 | name = "insta" 156 | version = "1.43.1" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" 159 | dependencies = [ 160 | "console", 161 | "globset", 162 | "once_cell", 163 | "similar", 164 | "walkdir", 165 | ] 166 | 167 | [[package]] 168 | name = "itoa" 169 | version = "1.0.15" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 172 | 173 | [[package]] 174 | name = "libc" 175 | version = "0.2.172" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 178 | 179 | [[package]] 180 | name = "libloading" 181 | version = "0.8.8" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" 184 | dependencies = [ 185 | "cfg-if", 186 | "windows-targets", 187 | ] 188 | 189 | [[package]] 190 | name = "log" 191 | version = "0.4.27" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 194 | 195 | [[package]] 196 | name = "memchr" 197 | version = "2.7.4" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 200 | 201 | [[package]] 202 | name = "napi" 203 | version = "3.0.0-beta.4" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "268c14cbac213177cf5f9a2756250683c47255537aa3f44a8a2e996a12bcacc7" 206 | dependencies = [ 207 | "bitflags", 208 | "ctor", 209 | "napi-build", 210 | "napi-sys", 211 | "nohash-hasher", 212 | "rustc-hash", 213 | ] 214 | 215 | [[package]] 216 | name = "napi-build" 217 | version = "2.2.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "03acbfa4f156a32188bfa09b86dc11a431b5725253fc1fc6f6df5bed273382c4" 220 | 221 | [[package]] 222 | name = "napi-derive" 223 | version = "3.0.0-beta.4" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "bcb9450364ef33372209a6343f4989f58eaacb12f7051e528b0db30fafb07189" 226 | dependencies = [ 227 | "convert_case", 228 | "ctor", 229 | "napi-derive-backend", 230 | "proc-macro2", 231 | "quote", 232 | "syn", 233 | ] 234 | 235 | [[package]] 236 | name = "napi-derive-backend" 237 | version = "2.0.0-beta.4" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "35941ecaf293d55fa7edec7e68f2d5dcb9025b376e808766a8b69fe1c284d70e" 240 | dependencies = [ 241 | "convert_case", 242 | "proc-macro2", 243 | "quote", 244 | "semver", 245 | "syn", 246 | ] 247 | 248 | [[package]] 249 | name = "napi-sys" 250 | version = "3.0.0-alpha.3" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "c4401c63f866b42d673a8b213d5662c84a0701b0f6c3acff7e2b9fc439f1675d" 253 | dependencies = [ 254 | "libloading", 255 | ] 256 | 257 | [[package]] 258 | name = "nohash-hasher" 259 | version = "0.2.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" 262 | 263 | [[package]] 264 | name = "once_cell" 265 | version = "1.21.3" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 268 | 269 | [[package]] 270 | name = "outref" 271 | version = "0.5.2" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" 274 | 275 | [[package]] 276 | name = "oxc_sourcemap" 277 | version = "3.0.2" 278 | dependencies = [ 279 | "base64-simd", 280 | "cfg-if", 281 | "cow-utils", 282 | "insta", 283 | "napi", 284 | "napi-build", 285 | "napi-derive", 286 | "rayon", 287 | "rustc-hash", 288 | "serde", 289 | "serde_json", 290 | ] 291 | 292 | [[package]] 293 | name = "proc-macro2" 294 | version = "1.0.95" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 297 | dependencies = [ 298 | "unicode-ident", 299 | ] 300 | 301 | [[package]] 302 | name = "quote" 303 | version = "1.0.40" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 306 | dependencies = [ 307 | "proc-macro2", 308 | ] 309 | 310 | [[package]] 311 | name = "rayon" 312 | version = "1.10.0" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 315 | dependencies = [ 316 | "either", 317 | "rayon-core", 318 | ] 319 | 320 | [[package]] 321 | name = "rayon-core" 322 | version = "1.12.1" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 325 | dependencies = [ 326 | "crossbeam-deque", 327 | "crossbeam-utils", 328 | ] 329 | 330 | [[package]] 331 | name = "regex-automata" 332 | version = "0.4.9" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 335 | dependencies = [ 336 | "aho-corasick", 337 | "memchr", 338 | "regex-syntax", 339 | ] 340 | 341 | [[package]] 342 | name = "regex-syntax" 343 | version = "0.8.5" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 346 | 347 | [[package]] 348 | name = "rustc-hash" 349 | version = "2.1.1" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 352 | 353 | [[package]] 354 | name = "ryu" 355 | version = "1.0.20" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 358 | 359 | [[package]] 360 | name = "same-file" 361 | version = "1.0.6" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 364 | dependencies = [ 365 | "winapi-util", 366 | ] 367 | 368 | [[package]] 369 | name = "semver" 370 | version = "1.0.26" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 373 | 374 | [[package]] 375 | name = "serde" 376 | version = "1.0.219" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 379 | dependencies = [ 380 | "serde_derive", 381 | ] 382 | 383 | [[package]] 384 | name = "serde_derive" 385 | version = "1.0.219" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 388 | dependencies = [ 389 | "proc-macro2", 390 | "quote", 391 | "syn", 392 | ] 393 | 394 | [[package]] 395 | name = "serde_json" 396 | version = "1.0.140" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 399 | dependencies = [ 400 | "itoa", 401 | "memchr", 402 | "ryu", 403 | "serde", 404 | ] 405 | 406 | [[package]] 407 | name = "similar" 408 | version = "2.7.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" 411 | 412 | [[package]] 413 | name = "syn" 414 | version = "2.0.101" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 417 | dependencies = [ 418 | "proc-macro2", 419 | "quote", 420 | "unicode-ident", 421 | ] 422 | 423 | [[package]] 424 | name = "unicode-ident" 425 | version = "1.0.18" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 428 | 429 | [[package]] 430 | name = "unicode-segmentation" 431 | version = "1.12.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 434 | 435 | [[package]] 436 | name = "vsimd" 437 | version = "0.8.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" 440 | 441 | [[package]] 442 | name = "walkdir" 443 | version = "2.5.0" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 446 | dependencies = [ 447 | "same-file", 448 | "winapi-util", 449 | ] 450 | 451 | [[package]] 452 | name = "winapi-util" 453 | version = "0.1.9" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 456 | dependencies = [ 457 | "windows-sys", 458 | ] 459 | 460 | [[package]] 461 | name = "windows-sys" 462 | version = "0.59.0" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 465 | dependencies = [ 466 | "windows-targets", 467 | ] 468 | 469 | [[package]] 470 | name = "windows-targets" 471 | version = "0.52.6" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 474 | dependencies = [ 475 | "windows_aarch64_gnullvm", 476 | "windows_aarch64_msvc", 477 | "windows_i686_gnu", 478 | "windows_i686_gnullvm", 479 | "windows_i686_msvc", 480 | "windows_x86_64_gnu", 481 | "windows_x86_64_gnullvm", 482 | "windows_x86_64_msvc", 483 | ] 484 | 485 | [[package]] 486 | name = "windows_aarch64_gnullvm" 487 | version = "0.52.6" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 490 | 491 | [[package]] 492 | name = "windows_aarch64_msvc" 493 | version = "0.52.6" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 496 | 497 | [[package]] 498 | name = "windows_i686_gnu" 499 | version = "0.52.6" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 502 | 503 | [[package]] 504 | name = "windows_i686_gnullvm" 505 | version = "0.52.6" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 508 | 509 | [[package]] 510 | name = "windows_i686_msvc" 511 | version = "0.52.6" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 514 | 515 | [[package]] 516 | name = "windows_x86_64_gnu" 517 | version = "0.52.6" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 520 | 521 | [[package]] 522 | name = "windows_x86_64_gnullvm" 523 | version = "0.52.6" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 526 | 527 | [[package]] 528 | name = "windows_x86_64_msvc" 529 | version = "0.52.6" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 532 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oxc_sourcemap" 3 | version = "3.0.2" 4 | publish = true 5 | authors = ["Boshen "] 6 | edition = "2024" 7 | keywords = ["javascript", "sourcemap", "sourcemaps"] 8 | description = "Basic sourcemap handling for Rust" 9 | categories = [] 10 | readme = "README.md" 11 | license = "MIT" 12 | repository = "https://github.com/oxc-project/oxc-sourcemap" 13 | rust-version = "1.85.0" 14 | include = ["/src", "build.rs"] 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 | [dependencies] 55 | base64-simd = "0.8" 56 | cfg-if = "1.0.0" 57 | cow-utils = "0.1.3" 58 | rustc-hash = "2" 59 | serde = { version = "1", features = ["derive"] } 60 | serde_json = { version = "1" } 61 | rayon = { version = "1", optional = true } 62 | napi = { version = "3.0.0-beta", optional = true } 63 | napi-derive = { version = "3.0.0-beta" , optional = true } 64 | 65 | [dev-dependencies] 66 | insta = { version = "1.43.1", features = ["glob"] } 67 | 68 | [build-dependencies] 69 | napi-build = { version = "2", optional = true } 70 | 71 | [features] 72 | default = [] 73 | rayon = ["dep:rayon"] 74 | napi = ["dep:napi", "dep:napi-derive", "dep:napi-build"] 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | # oxc-sourcemap 15 | 16 | Forked version of [rust-sourcemap](https://github.com/getsentry/rust-sourcemap), but has some different with it. 17 | 18 | - Encode sourcemap at parallel, including quote `sourceContent` and encode token to `vlq` mappings. 19 | - Avoid `Sourcemap` some methods overhead, like `SourceMap::tokens()`. 20 | 21 | [discord-badge]: https://img.shields.io/discord/1079625926024900739?logo=discord&label=Discord 22 | [discord-url]: https://discord.gg/9uXCAwqQZW 23 | [license-badge]: https://img.shields.io/badge/license-MIT-blue.svg 24 | [license-url]: https://github.com/oxc-project/oxc-sourcemap/blob/main/LICENSE 25 | [ci-badge]: https://github.com/oxc-project/oxc-sourcemap/actions/workflows/ci.yml/badge.svg?event=push&branch=main 26 | [ci-url]: https://github.com/oxc-project/oxc-sourcemap/actions/workflows/ci.yml?query=event%3Apush+branch%3Amain 27 | [code-coverage-badge]: https://codecov.io/github/oxc-project/oxc-sourcemap/branch/main/graph/badge.svg 28 | [code-coverage-url]: https://codecov.io/gh/oxc-project/oxc-sourcemap 29 | [sponsors-badge]: https://img.shields.io/github/sponsors/Boshen 30 | [sponsors-url]: https://github.com/sponsors/Boshen 31 | [codspeed-badge]: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json 32 | [codspeed-url]: https://codspeed.io/oxc-project/oxc-sourcemap 33 | [crates-badge]: https://img.shields.io/crates/d/oxc_sourcemap?label=crates.io 34 | [crates-url]: https://crates.io/crates/oxc_sourcemap 35 | [docs-badge]: https://img.shields.io/docsrs/oxc_sourcemap 36 | [docs-url]: https://docs.rs/oxc_sourcemap 37 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "napi")] 3 | { 4 | napi_build::setup(); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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 | ready: 12 | cargo check --all-targets --all-features 13 | cargo test 14 | cargo clippy --all-targets --all-features 15 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.87.0" 3 | profile = "default" 4 | -------------------------------------------------------------------------------- /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 | 84 | // Add `token_chunks`, See `TokenChunk`. 85 | if let Some(last_token) = self.tokens.last() { 86 | self.token_chunks.push(TokenChunk::new( 87 | self.tokens.len() as u32, 88 | self.tokens.len() as u32 + sourcemap.tokens.len() as u32, 89 | last_token.get_dst_line(), 90 | last_token.get_dst_col(), 91 | last_token.get_src_line(), 92 | last_token.get_src_col(), 93 | self.token_chunk_prev_name_id, 94 | self.token_chunk_prev_source_id, 95 | )); 96 | } else { 97 | self.token_chunks.push(TokenChunk::new( 98 | 0, 99 | sourcemap.tokens.len() as u32, 100 | 0, 101 | 0, 102 | 0, 103 | 0, 104 | 0, 105 | 0, 106 | )); 107 | } 108 | 109 | // Extend `sources` and `source_contents`. 110 | self.sources.extend(sourcemap.get_sources().map(Into::into)); 111 | 112 | // Clone `Arc` instead of generating a new `Arc` and copying string data because 113 | // source texts are generally long strings. Cost of copying a large string is higher 114 | // than cloning an `Arc`. 115 | self.source_contents.extend(sourcemap.source_contents.iter().cloned()); 116 | 117 | // Extend `names`. 118 | self.names.reserve(sourcemap.names.len()); 119 | self.names.extend(sourcemap.get_names().map(Into::into)); 120 | 121 | // Extend `tokens`. 122 | self.tokens.reserve(sourcemap.tokens.len()); 123 | let tokens = sourcemap.get_tokens().map(|token| { 124 | Token::new( 125 | token.get_dst_line() + line_offset, 126 | token.get_dst_col(), 127 | token.get_src_line(), 128 | token.get_src_col(), 129 | token.get_source_id().map(|x| { 130 | self.token_chunk_prev_source_id = x + source_offset; 131 | self.token_chunk_prev_source_id 132 | }), 133 | token.get_name_id().map(|x| { 134 | self.token_chunk_prev_name_id = x + name_offset; 135 | self.token_chunk_prev_name_id 136 | }), 137 | ) 138 | }); 139 | self.tokens.extend(tokens); 140 | } 141 | 142 | pub fn into_sourcemap(self) -> SourceMap { 143 | SourceMap::new( 144 | None, 145 | self.names, 146 | None, 147 | self.sources, 148 | self.source_contents, 149 | self.tokens, 150 | Some(self.token_chunks), 151 | ) 152 | } 153 | } 154 | 155 | #[test] 156 | fn test_concat_sourcemap_builder() { 157 | run_test(|sourcemap_and_line_offsets| { 158 | let mut builder = ConcatSourceMapBuilder::default(); 159 | for (sourcemap, line_offset) in sourcemap_and_line_offsets.iter().copied() { 160 | builder.add_sourcemap(sourcemap, line_offset); 161 | } 162 | builder 163 | }); 164 | } 165 | 166 | #[test] 167 | fn test_concat_sourcemap_builder_from_sourcemaps() { 168 | run_test(ConcatSourceMapBuilder::from_sourcemaps); 169 | } 170 | 171 | #[cfg(test)] 172 | fn run_test(create_builder: F) 173 | where 174 | F: Fn(&[(&SourceMap, u32)]) -> ConcatSourceMapBuilder, 175 | { 176 | let sm1 = SourceMap::new( 177 | None, 178 | vec!["foo".into(), "foo2".into()], 179 | None, 180 | vec!["foo.js".into()], 181 | vec![], 182 | vec![Token::new(1, 1, 1, 1, Some(0), Some(0))], 183 | None, 184 | ); 185 | let sm2 = SourceMap::new( 186 | None, 187 | vec!["bar".into()], 188 | None, 189 | vec!["bar.js".into()], 190 | vec![], 191 | vec![Token::new(1, 1, 1, 1, Some(0), Some(0))], 192 | None, 193 | ); 194 | let sm3 = SourceMap::new( 195 | None, 196 | vec!["abc".into()], 197 | None, 198 | vec!["abc.js".into()], 199 | vec![], 200 | vec![Token::new(1, 2, 2, 2, Some(0), Some(0))], 201 | None, 202 | ); 203 | 204 | let builder = create_builder(&[(&sm1, 0), (&sm2, 2), (&sm3, 2)]); 205 | 206 | let sm = SourceMap::new( 207 | None, 208 | vec!["foo".into(), "foo2".into(), "bar".into(), "abc".into()], 209 | None, 210 | vec!["foo.js".into(), "bar.js".into(), "abc.js".into()], 211 | vec![], 212 | vec![ 213 | Token::new(1, 1, 1, 1, Some(0), Some(0)), 214 | Token::new(3, 1, 1, 1, Some(1), Some(2)), 215 | Token::new(3, 2, 2, 2, Some(2), Some(3)), 216 | ], 217 | None, 218 | ); 219 | let concat_sm = builder.into_sourcemap(); 220 | 221 | assert_eq!(concat_sm.tokens, sm.tokens); 222 | assert_eq!(concat_sm.sources, sm.sources); 223 | assert_eq!(concat_sm.names, sm.names); 224 | assert_eq!( 225 | concat_sm.token_chunks, 226 | Some(vec![ 227 | TokenChunk::new(0, 1, 0, 0, 0, 0, 0, 0,), 228 | TokenChunk::new(1, 2, 1, 1, 1, 1, 0, 0,), 229 | TokenChunk::new(2, 3, 3, 1, 1, 1, 2, 1,) 230 | ]) 231 | ); 232 | 233 | assert_eq!(sm.to_json().mappings, concat_sm.to_json().mappings); 234 | } 235 | -------------------------------------------------------------------------------- /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::{SourceMap, Token}; 7 | 8 | /// See . 9 | #[derive(serde::Deserialize, Default)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct JSONSourceMap { 12 | /// An optional name of the generated code that this source map is associated with. 13 | pub file: Option, 14 | /// A string with the encoded mapping data. 15 | pub mappings: String, 16 | /// An optional source root, useful for relocating source files on a server or removing repeated values in the “sources” entry. 17 | /// This value is prepended to the individual entries in the “source” field. 18 | pub source_root: Option, 19 | /// A list of original sources used by the “mappings” entry. 20 | pub sources: Vec, 21 | /// An optional list of source content, useful when the “source” can’t be hosted. 22 | /// 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. 23 | pub sources_content: Option>>, 24 | /// A list of symbol names used by the “mappings” entry. 25 | pub names: Vec, 26 | /// An optional field containing the debugId for this sourcemap. 27 | pub debug_id: Option, 28 | /// 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. 29 | /// 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. 30 | /// 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. 31 | #[serde(rename = "x_google_ignoreList")] 32 | pub x_google_ignore_list: Option>, 33 | } 34 | 35 | pub fn decode(json: JSONSourceMap) -> Result { 36 | let tokens = decode_mapping(&json.mappings, json.names.len(), json.sources.len())?; 37 | Ok(SourceMap { 38 | file: json.file.map(Arc::from), 39 | names: json.names.into_iter().map(Arc::from).collect(), 40 | source_root: json.source_root, 41 | sources: json.sources.into_iter().map(Arc::from).collect(), 42 | source_contents: json 43 | .sources_content 44 | .map(|content| content.into_iter().map(|c| c.map(Arc::from)).collect()) 45 | .unwrap_or_default(), 46 | tokens, 47 | token_chunks: None, 48 | x_google_ignore_list: json.x_google_ignore_list, 49 | debug_id: json.debug_id, 50 | }) 51 | } 52 | 53 | pub fn decode_from_string(value: &str) -> Result { 54 | decode(serde_json::from_str(value)?) 55 | } 56 | 57 | fn decode_mapping(mapping: &str, names_len: usize, sources_len: usize) -> Result> { 58 | let mut tokens = vec![]; 59 | 60 | let mut dst_col; 61 | let mut src_id = 0; 62 | let mut src_line = 0; 63 | let mut src_col = 0; 64 | let mut name_id = 0; 65 | let mut nums = Vec::with_capacity(6); 66 | 67 | for (dst_line, line) in mapping.split(';').enumerate() { 68 | if line.is_empty() { 69 | continue; 70 | } 71 | 72 | dst_col = 0; 73 | 74 | for segment in line.split(',') { 75 | if segment.is_empty() { 76 | continue; 77 | } 78 | 79 | nums.clear(); 80 | parse_vlq_segment_into(segment, &mut nums)?; 81 | dst_col = (i64::from(dst_col) + nums[0]) as u32; 82 | 83 | let mut src = !0; 84 | let mut name = !0; 85 | 86 | if nums.len() > 1 { 87 | if nums.len() != 4 && nums.len() != 5 { 88 | return Err(Error::BadSegmentSize(nums.len() as u32)); 89 | } 90 | src_id = (i64::from(src_id) + nums[1]) as u32; 91 | if src_id >= sources_len as u32 { 92 | return Err(Error::BadSourceReference(src_id)); 93 | } 94 | 95 | src = src_id; 96 | src_line = (i64::from(src_line) + nums[2]) as u32; 97 | src_col = (i64::from(src_col) + nums[3]) as u32; 98 | 99 | if nums.len() > 4 { 100 | name_id = (i64::from(name_id) + nums[4]) as u32; 101 | if name_id >= names_len as u32 { 102 | return Err(Error::BadNameReference(name_id)); 103 | } 104 | name = name_id; 105 | } 106 | } 107 | 108 | tokens.push(Token::new( 109 | dst_line as u32, 110 | dst_col, 111 | src_line, 112 | src_col, 113 | if src == !0 { None } else { Some(src) }, 114 | if name == !0 { None } else { Some(name) }, 115 | )); 116 | } 117 | } 118 | 119 | Ok(tokens) 120 | } 121 | 122 | #[rustfmt::skip] 123 | const B64: [i8; 256] = [ -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, -1, ]; 124 | 125 | fn parse_vlq_segment_into(segment: &str, rv: &mut Vec) -> Result<()> { 126 | let mut cur = 0; 127 | let mut shift = 0; 128 | 129 | for c in segment.bytes() { 130 | let enc = i64::from(B64[c as usize]); 131 | let val = enc & 0b11111; 132 | let cont = enc >> 5; 133 | cur += val.checked_shl(shift).ok_or(Error::VlqOverflow)?; 134 | shift += 5; 135 | 136 | if cont == 0 { 137 | let sign = cur & 1; 138 | cur >>= 1; 139 | if sign != 0 { 140 | cur = -cur; 141 | } 142 | rv.push(cur); 143 | cur = 0; 144 | shift = 0; 145 | } 146 | } 147 | 148 | if cur != 0 || shift != 0 { 149 | Err(Error::VlqLeftover) 150 | } else if rv.is_empty() { 151 | Err(Error::VlqNoValues) 152 | } else { 153 | Ok(()) 154 | } 155 | } 156 | 157 | #[test] 158 | fn test_decode_sourcemap() { 159 | let input = r#"{ 160 | "version": 3, 161 | "sources": ["coolstuff.js"], 162 | "sourceRoot": "x", 163 | "names": ["x","alert"], 164 | "mappings": "AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM", 165 | "x_google_ignoreList": [0] 166 | }"#; 167 | let sm = SourceMap::from_json_string(input).unwrap(); 168 | assert_eq!(sm.get_source_root(), Some("x")); 169 | assert_eq!(sm.get_x_google_ignore_list(), Some(&[0][..])); 170 | let mut iter = sm.get_source_view_tokens().filter(|token| token.get_name_id().is_some()); 171 | assert_eq!(iter.next().unwrap().to_tuple(), (Some("coolstuff.js"), 0, 4, Some("x"))); 172 | assert_eq!(iter.next().unwrap().to_tuple(), (Some("coolstuff.js"), 1, 4, Some("x"))); 173 | assert_eq!(iter.next().unwrap().to_tuple(), (Some("coolstuff.js"), 2, 2, Some("alert"))); 174 | assert!(iter.next().is_none()); 175 | } 176 | 177 | #[test] 178 | fn test_decode_sourcemap_optional_filed() { 179 | let input = r#"{ 180 | "names": [], 181 | "sources": [], 182 | "sourcesContent": [null], 183 | "mappings": "" 184 | }"#; 185 | SourceMap::from_json_string(input).expect("should success"); 186 | } 187 | -------------------------------------------------------------------------------- /src/encode.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | #[cfg(feature = "rayon")] 4 | use rayon::prelude::*; 5 | 6 | use crate::JSONSourceMap; 7 | /// Port from https://github.com/getsentry/rust-sourcemap/blob/9.1.0/src/encoder.rs 8 | /// It is a helper for encode `SourceMap` to vlq sourcemap string, but here some different. 9 | /// - Quote `source_content` at parallel. 10 | /// - If you using `ConcatSourceMapBuilder`, serialize `tokens` to vlq `mappings` at parallel. 11 | use crate::{SourceMap, Token, token::TokenChunk}; 12 | 13 | pub fn encode(sourcemap: &SourceMap) -> JSONSourceMap { 14 | JSONSourceMap { 15 | file: sourcemap.get_file().map(ToString::to_string), 16 | mappings: serialize_sourcemap_mappings(sourcemap), 17 | source_root: sourcemap.get_source_root().map(ToString::to_string), 18 | sources: sourcemap.sources.iter().map(ToString::to_string).collect(), 19 | sources_content: Some( 20 | sourcemap 21 | .source_contents 22 | .iter() 23 | .map(|v| v.as_ref().map(|item| item.to_string())) 24 | .collect(), 25 | ), 26 | names: sourcemap.names.iter().map(ToString::to_string).collect(), 27 | debug_id: sourcemap.get_debug_id().map(ToString::to_string), 28 | x_google_ignore_list: sourcemap.get_x_google_ignore_list().map(|x| x.to_vec()), 29 | } 30 | } 31 | fn escape_optional_json_string>(s: &Option) -> String { 32 | s.as_ref().map(escape_json_string).unwrap_or_else(|| String::from("null")) 33 | } 34 | 35 | // Here using `serde_json` to serialize `names` / `source_contents` / `sources`. 36 | // It will escape the string to avoid invalid JSON string. 37 | pub fn encode_to_string(sourcemap: &SourceMap) -> String { 38 | let max_segments = 12 39 | + sourcemap.names.len() * 2 40 | + sourcemap.sources.len() * 2 41 | + sourcemap.source_contents.len() * 2 42 | + 1 43 | + sourcemap.x_google_ignore_list.as_ref().map_or(0, |x| x.len() * 2 + 1); 44 | let mut contents = PreAllocatedString::new(max_segments); 45 | 46 | contents.push("{\"version\":3,".into()); 47 | if let Some(file) = sourcemap.get_file() { 48 | contents.push("\"file\":\"".into()); 49 | contents.push(file.into()); 50 | contents.push("\",".into()); 51 | } 52 | 53 | if let Some(source_root) = sourcemap.get_source_root() { 54 | contents.push("\"sourceRoot\":\"".into()); 55 | contents.push(source_root.into()); 56 | contents.push("\",".into()); 57 | } 58 | 59 | contents.push("\"names\":[".into()); 60 | contents.push_list(sourcemap.names.iter().map(escape_json_string)); 61 | 62 | contents.push("],\"sources\":[".into()); 63 | contents.push_list(sourcemap.sources.iter().map(escape_json_string)); 64 | 65 | // Quote `source_content` in parallel 66 | let source_contents = &sourcemap.source_contents; 67 | contents.push("],\"sourcesContent\":[".into()); 68 | cfg_if::cfg_if! { 69 | if #[cfg(feature = "rayon")] { 70 | let quoted_source_contents: Vec<_> = source_contents 71 | .par_iter() 72 | .map(escape_optional_json_string) 73 | .collect(); 74 | contents.push_list(quoted_source_contents.into_iter()); 75 | } else { 76 | contents.push_list(source_contents.iter().map(escape_optional_json_string)); 77 | } 78 | }; 79 | 80 | if let Some(x_google_ignore_list) = &sourcemap.x_google_ignore_list { 81 | contents.push("],\"x_google_ignoreList\":[".into()); 82 | contents.push_list(x_google_ignore_list.iter().map(ToString::to_string)); 83 | } 84 | 85 | contents.push("],\"mappings\":\"".into()); 86 | contents.push(serialize_sourcemap_mappings(sourcemap).into()); 87 | 88 | if let Some(debug_id) = sourcemap.get_debug_id() { 89 | contents.push("\",\"debugId\":\"".into()); 90 | contents.push(debug_id.into()); 91 | } 92 | 93 | contents.push("\"}".into()); 94 | 95 | // Check we calculated number of segments required correctly 96 | debug_assert!(contents.num_segments() <= max_segments); 97 | 98 | contents.consume() 99 | } 100 | 101 | fn serialize_sourcemap_mappings(sm: &SourceMap) -> String { 102 | sm.token_chunks.as_ref().map_or_else( 103 | || { 104 | serialize_mappings( 105 | &sm.tokens, 106 | &TokenChunk::new(0, sm.tokens.len() as u32, 0, 0, 0, 0, 0, 0), 107 | ) 108 | }, 109 | |token_chunks| { 110 | // Serialize `tokens` to vlq `mappings` at parallel. 111 | cfg_if::cfg_if! { 112 | if #[cfg(feature = "rayon")] { 113 | token_chunks 114 | .par_iter() 115 | .map(|token_chunk| serialize_mappings(&sm.tokens, token_chunk)) 116 | .collect::() 117 | } else { 118 | token_chunks 119 | .iter() 120 | .map(|token_chunk| serialize_mappings(&sm.tokens, token_chunk)) 121 | .collect::() 122 | } 123 | } 124 | }, 125 | ) 126 | } 127 | 128 | // Max length of a single VLQ encoding 129 | const MAX_VLQ_BYTES: usize = 7; 130 | 131 | fn serialize_mappings(tokens: &[Token], token_chunk: &TokenChunk) -> String { 132 | let TokenChunk { 133 | start, 134 | end, 135 | mut prev_dst_line, 136 | mut prev_dst_col, 137 | mut prev_src_line, 138 | mut prev_src_col, 139 | mut prev_name_id, 140 | mut prev_source_id, 141 | } = *token_chunk; 142 | 143 | let capacity = ((end - start) * 10) as usize; 144 | 145 | let mut rv = String::with_capacity(capacity); 146 | 147 | let mut prev_token = if start == 0 { None } else { Some(&tokens[start as usize - 1]) }; 148 | 149 | for token in &tokens[start as usize..end as usize] { 150 | // Max length of a single VLQ encoding is 7 bytes. Max number of calls to `encode_vlq_diff` is 5. 151 | // Also need 1 byte for each line number difference, or 1 byte if no line num difference. 152 | // Reserve this amount of capacity in `rv` early, so can skip bounds checks in code below. 153 | // As well as skipping the bounds checks, this also removes a function call to 154 | // `alloc::raw_vec::RawVec::grow_one` for every byte that's pushed. 155 | // https://godbolt.org/z/44G8jjss3 156 | const MAX_TOTAL_VLQ_BYTES: usize = 5 * MAX_VLQ_BYTES; 157 | 158 | let num_line_breaks = token.get_dst_line() - prev_dst_line; 159 | if num_line_breaks != 0 { 160 | rv.reserve(MAX_TOTAL_VLQ_BYTES + num_line_breaks as usize); 161 | // SAFETY: We have reserved sufficient capacity for `num_line_breaks` bytes 162 | unsafe { push_bytes_unchecked(&mut rv, b';', num_line_breaks) }; 163 | prev_dst_col = 0; 164 | prev_dst_line += num_line_breaks; 165 | } else if let Some(prev_token) = prev_token { 166 | if prev_token == token { 167 | continue; 168 | } 169 | rv.reserve(MAX_TOTAL_VLQ_BYTES + 1); 170 | // SAFETY: We have reserved sufficient capacity for 1 byte 171 | unsafe { push_byte_unchecked(&mut rv, b',') }; 172 | } 173 | 174 | // SAFETY: We have reserved enough capacity above to satisfy safety contract 175 | // of `encode_vlq_diff` for all calls below 176 | unsafe { 177 | encode_vlq_diff(&mut rv, token.get_dst_col(), prev_dst_col); 178 | prev_dst_col = token.get_dst_col(); 179 | 180 | if let Some(source_id) = token.get_source_id() { 181 | encode_vlq_diff(&mut rv, source_id, prev_source_id); 182 | prev_source_id = source_id; 183 | encode_vlq_diff(&mut rv, token.get_src_line(), prev_src_line); 184 | prev_src_line = token.get_src_line(); 185 | encode_vlq_diff(&mut rv, token.get_src_col(), prev_src_col); 186 | prev_src_col = token.get_src_col(); 187 | if let Some(name_id) = token.get_name_id() { 188 | encode_vlq_diff(&mut rv, name_id, prev_name_id); 189 | prev_name_id = name_id; 190 | } 191 | } 192 | } 193 | 194 | prev_token = Some(token); 195 | } 196 | 197 | rv 198 | } 199 | 200 | /// Encode diff as VLQ and push encoding into `out`. 201 | /// Will push between 1 byte (num = 0) and 7 bytes (num = -u32::MAX). 202 | /// 203 | /// # SAFETY 204 | /// Caller must ensure at least 7 bytes spare capacity in `out`, 205 | /// as this function does not perform any bounds checks. 206 | #[inline] 207 | unsafe fn encode_vlq_diff(out: &mut String, a: u32, b: u32) { 208 | unsafe { 209 | encode_vlq(out, i64::from(a) - i64::from(b)); 210 | } 211 | } 212 | 213 | // Align chars lookup table on 64 so occupies a single cache line 214 | #[repr(align(64))] 215 | struct Aligned64([u8; 64]); 216 | 217 | static B64_CHARS: Aligned64 = Aligned64([ 218 | 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', 219 | 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', 220 | 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', 221 | 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'/', 222 | ]); 223 | 224 | /// Encode number as VLQ and push encoding into `out`. 225 | /// Will push between 1 byte (num = 0) and 7 bytes (num = -u32::MAX). 226 | /// 227 | /// # SAFETY 228 | /// Caller must ensure at least 7 bytes spare capacity in `out`, 229 | /// as this function does not perform any bounds checks. 230 | unsafe fn encode_vlq(out: &mut String, num: i64) { 231 | unsafe { 232 | let mut num = if num < 0 { ((-num) << 1) + 1 } else { num << 1 }; 233 | 234 | // Breaking out of loop early when have reached last char (rather than conditionally adding 235 | // 32 for last char within the loop) removes 3 instructions from the loop. 236 | // https://godbolt.org/z/Es4Pavh9j 237 | // This translates to a 16% speed-up for VLQ encoding. 238 | let mut digit; 239 | loop { 240 | digit = num & 0b11111; 241 | num >>= 5; 242 | if num == 0 { 243 | break; 244 | } 245 | 246 | let b = B64_CHARS.0[digit as usize + 32]; 247 | // SAFETY: 248 | // * This loop can execute a maximum of 7 times, and on last turn will exit before getting here. 249 | // Caller promises there are at least 7 bytes spare capacity in `out` at start. We only 250 | // push 1 byte on each turn, so guaranteed there is at least 1 byte capacity in `out` here. 251 | // * All values in `B64_CHARS` lookup table are ASCII bytes. 252 | push_byte_unchecked(out, b); 253 | } 254 | 255 | let b = B64_CHARS.0[digit as usize]; 256 | // SAFETY: 257 | // * The loop above pushes max 6 bytes. Caller promises there are at least 7 bytes spare capacity 258 | // in `out` at start. So guaranteed there is at least 1 byte capacity in `out` here. 259 | // * All values in `B64_CHARS` lookup table are ASCII bytes. 260 | push_byte_unchecked(out, b); 261 | } 262 | } 263 | 264 | /// Push a byte to `out` without bounds checking. 265 | /// 266 | /// # SAFETY 267 | /// * `out` must have at least 1 byte spare capacity. 268 | /// * `b` must be an ASCII byte (i.e. not `>= 128`). 269 | // 270 | // `#[inline(always)]` to ensure that `len` is stored in a register during `encode_vlq`'s loop. 271 | #[expect(clippy::inline_always)] 272 | #[inline(always)] 273 | unsafe fn push_byte_unchecked(out: &mut String, b: u8) { 274 | unsafe { 275 | debug_assert!(out.len() < out.capacity()); 276 | debug_assert!(b.is_ascii()); 277 | 278 | let out = out.as_mut_vec(); 279 | let len = out.len(); 280 | let ptr = out.as_mut_ptr().add(len); 281 | ptr.write(b); 282 | out.set_len(len + 1); 283 | } 284 | } 285 | 286 | /// Push a byte to `out` a number of times without bounds checking. 287 | /// 288 | /// # SAFETY 289 | /// * `out` must have at least `repeats` bytes spare capacity. 290 | /// * `b` must be an ASCII byte (i.e. not `>= 128`). 291 | #[inline] 292 | unsafe fn push_bytes_unchecked(out: &mut String, b: u8, repeats: u32) { 293 | unsafe { 294 | debug_assert!(out.capacity() - out.len() >= repeats as usize); 295 | debug_assert!(b.is_ascii()); 296 | 297 | let out = out.as_mut_vec(); 298 | let len = out.len(); 299 | let mut ptr = out.as_mut_ptr().add(len); 300 | for _ in 0..repeats { 301 | ptr.write(b); 302 | ptr = ptr.add(1); 303 | } 304 | out.set_len(len + repeats as usize); 305 | } 306 | } 307 | 308 | /// A helper for pre-allocate string buffer. 309 | /// 310 | /// Pre-allocate a Cow<'a, str> buffer, and push the segment into it. 311 | /// Finally, convert it to a pre-allocated length String. 312 | struct PreAllocatedString<'a> { 313 | buf: Vec>, 314 | len: usize, 315 | } 316 | 317 | impl<'a> PreAllocatedString<'a> { 318 | fn new(max_segments: usize) -> Self { 319 | Self { buf: Vec::with_capacity(max_segments), len: 0 } 320 | } 321 | 322 | #[inline] 323 | fn push(&mut self, s: Cow<'a, str>) { 324 | self.len += s.len(); 325 | self.buf.push(s); 326 | } 327 | 328 | #[inline] 329 | fn push_list(&mut self, mut iter: I) 330 | where 331 | I: Iterator, 332 | { 333 | let Some(first) = iter.next() else { 334 | return; 335 | }; 336 | self.push(Cow::Owned(first)); 337 | 338 | for other in iter { 339 | self.push(Cow::Borrowed(",")); 340 | self.push(Cow::Owned(other)); 341 | } 342 | } 343 | 344 | #[inline] 345 | fn consume(self) -> String { 346 | let mut buf = String::with_capacity(self.len); 347 | buf.extend(self.buf); 348 | buf 349 | } 350 | 351 | fn num_segments(&self) -> usize { 352 | self.buf.len() 353 | } 354 | } 355 | 356 | fn escape_json_string>(s: S) -> String { 357 | let s = s.as_ref(); 358 | let mut escaped_buf = Vec::with_capacity(s.len() * 2 + 2); 359 | // This call is infallible as only error it can return is if the writer errors. 360 | // Writing to a `Vec` is infallible, so that's not possible here. 361 | serde::Serialize::serialize(s, &mut serde_json::Serializer::new(&mut escaped_buf)).unwrap(); 362 | // Safety: `escaped_buf` is valid utf8. 363 | unsafe { String::from_utf8_unchecked(escaped_buf) } 364 | } 365 | 366 | #[test] 367 | fn test_escape_json_string() { 368 | const FIXTURES: &[(char, &str)] = &[ 369 | ('n', "\"n\""), 370 | ('"', "\"\\\"\""), 371 | ('\\', "\"\\\\\""), 372 | ('/', "\"/\""), 373 | ('\x08', "\"\\b\""), 374 | ('\x0C', "\"\\f\""), 375 | ('\n', "\"\\n\""), 376 | ('\r', "\"\\r\""), 377 | ('\t', "\"\\t\""), 378 | ('\x0B', "\"\\u000b\""), 379 | ('虎', "\"虎\""), 380 | ('\u{3A3}', "\"\u{3A3}\""), 381 | ]; 382 | 383 | for (c, expected) in FIXTURES { 384 | let mut input = String::new(); 385 | input.push(*c); 386 | assert_eq!(escape_json_string(input), *expected); 387 | } 388 | } 389 | 390 | #[test] 391 | fn test_encode() { 392 | let input = r#"{ 393 | "version": 3, 394 | "sources": ["coolstuff.js"], 395 | "sourceRoot": "x", 396 | "names": ["x","alert"], 397 | "mappings": "AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM", 398 | "x_google_ignoreList": [0] 399 | }"#; 400 | let sm = SourceMap::from_json_string(input).unwrap(); 401 | let sm2 = SourceMap::from_json_string(&sm.to_json_string()).unwrap(); 402 | 403 | for (tok1, tok2) in sm.get_tokens().zip(sm2.get_tokens()) { 404 | assert_eq!(tok1, tok2); 405 | } 406 | } 407 | 408 | #[test] 409 | fn test_encode_escape_string() { 410 | // '\0' should be escaped. 411 | let mut sm = SourceMap::new( 412 | None, 413 | vec!["name_length_greater_than_16_\0".into()], 414 | None, 415 | vec!["\0".into()], 416 | vec![Some("emoji-👀-\0".into())], 417 | vec![], 418 | None, 419 | ); 420 | sm.set_x_google_ignore_list(vec![0]); 421 | sm.set_debug_id("56431d54-c0a6-451d-8ea2-ba5de5d8ca2e"); 422 | assert_eq!( 423 | sm.to_json_string(), 424 | 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"}"# 425 | ); 426 | } 427 | 428 | #[test] 429 | fn test_vlq_encode_diff() { 430 | // Most import tests here are that with maximum values, `encode_vlq_diff` pushes maximum of 7 bytes. 431 | // This invariant is essential to safety of `encode_vlq_diff`. 432 | #[rustfmt::skip] 433 | const FIXTURES: &[(u32, u32, &str)] = &[ 434 | (0, 0, "A"), 435 | (1, 0, "C"), 436 | (2, 0, "E"), 437 | (15, 0, "e"), 438 | (16, 0, "gB"), 439 | (511, 0, "+f"), 440 | (512, 0, "ggB"), 441 | (16_383, 0, "+/f"), 442 | (16_384, 0, "gggB"), 443 | (524_287, 0, "+//f"), 444 | (524_288, 0, "ggggB"), 445 | (16_777_215, 0, "+///f"), 446 | (16_777_216, 0, "gggggB"), 447 | (536_870_911, 0, "+////f"), 448 | (536_870_912, 0, "ggggggB"), 449 | (u32::MAX, 0, "+/////H"), // 7 bytes 450 | 451 | (0, 1, "D"), 452 | (0, 2, "F"), 453 | (0, 15, "f"), 454 | (0, 16, "hB"), 455 | (0, 511, "/f"), 456 | (0, 512, "hgB"), 457 | (0, 16_383, "//f"), 458 | (0, 16_384, "hggB"), 459 | (0, 524_287, "///f"), 460 | (0, 524_288, "hgggB"), 461 | (0, 16_777_215, "////f"), 462 | (0, 16_777_216, "hggggB"), 463 | (0, 536_870_911, "/////f"), 464 | (0, 536_870_912, "hgggggB"), 465 | (0, u32::MAX, "//////H"), // 7 bytes 466 | ]; 467 | 468 | for (a, b, res) in FIXTURES.iter().copied() { 469 | let mut out = String::with_capacity(MAX_VLQ_BYTES); 470 | // SAFETY: `out` has 7 bytes spare capacity 471 | unsafe { encode_vlq_diff(&mut out, a, b) }; 472 | assert_eq!(&out, res); 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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: Vec, 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: Vec, 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<&str> { 83 | self.file.as_deref() 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().map(AsRef::as_ref) 113 | } 114 | 115 | /// Adjust `sources`. 116 | pub fn set_sources(&mut self, sources: Vec<&str>) { 117 | self.sources = sources.into_iter().map(Into::into).collect(); 118 | } 119 | 120 | pub fn get_sources(&self) -> impl Iterator { 121 | self.sources.iter().map(AsRef::as_ref) 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().map(AsRef::as_ref)) 132 | } 133 | 134 | pub fn get_token(&self, index: u32) -> Option<&Token> { 135 | self.tokens.get(index as usize) 136 | } 137 | 138 | pub fn get_source_view_token(&self, index: u32) -> Option> { 139 | self.tokens.get(index as usize).map(|token| SourceViewToken::new(token, self)) 140 | } 141 | 142 | /// Get raw tokens. 143 | pub fn get_tokens(&self) -> impl Iterator { 144 | self.tokens.iter() 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<&str> { 153 | self.names.get(id as usize).map(AsRef::as_ref) 154 | } 155 | 156 | pub fn get_source(&self, id: u32) -> Option<&str> { 157 | self.sources.get(id as usize).map(AsRef::as_ref) 158 | } 159 | 160 | pub fn get_source_content(&self, id: u32) -> Option<&str> { 161 | self.source_contents.get(id as usize).and_then(|item| item.as_ref().map(AsRef::as_ref)) 162 | } 163 | 164 | pub fn get_source_and_content(&self, id: u32) -> Option<(&str, &str)> { 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(&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![vec![]; last_token.dst_line as usize + 1]; 175 | for (idx, token) in self.tokens.iter().enumerate() { 176 | table[token.dst_line as usize].push((token.dst_line, token.dst_col, idx as u32)); 177 | } 178 | table.iter_mut().for_each(|line| line.sort_unstable()); 179 | table 180 | } else { 181 | vec![] 182 | } 183 | } 184 | 185 | /// Lookup a token by line and column, it will used at remapping. 186 | pub fn lookup_token( 187 | &self, 188 | lookup_table: &[LineLookupTable], 189 | line: u32, 190 | col: u32, 191 | ) -> Option<&Token> { 192 | // If the line is greater than the number of lines in the lookup table, it hasn't corresponding origin token. 193 | if line >= lookup_table.len() as u32 { 194 | return None; 195 | } 196 | let table = greatest_lower_bound(&lookup_table[line as usize], &(line, col), |table| { 197 | (table.0, table.1) 198 | })?; 199 | self.get_token(table.2) 200 | } 201 | 202 | /// Lookup a token by line and column, it will used at remapping. See `SourceViewToken`. 203 | pub fn lookup_source_view_token( 204 | &self, 205 | lookup_table: &[LineLookupTable], 206 | line: u32, 207 | col: u32, 208 | ) -> Option> { 209 | self.lookup_token(lookup_table, line, col).map(|token| SourceViewToken::new(token, self)) 210 | } 211 | } 212 | 213 | type LineLookupTable = Vec<(u32, u32, u32)>; 214 | 215 | fn greatest_lower_bound<'a, T, K: Ord, F: Fn(&'a T) -> K>( 216 | slice: &'a [T], 217 | key: &K, 218 | map: F, 219 | ) -> Option<&'a T> { 220 | let mut idx = match slice.binary_search_by_key(key, &map) { 221 | Ok(index) => index, 222 | Err(index) => { 223 | // If there is no match, then we know for certain that the index is where we should 224 | // insert a new token, and that the token directly before is the greatest lower bound. 225 | return slice.get(index.checked_sub(1)?); 226 | } 227 | }; 228 | 229 | // If we get an exact match, then we need to continue looking at previous tokens to see if 230 | // they also match. We use a linear search because the number of exact matches is generally 231 | // very small, and almost certainly smaller than the number of tokens before the index. 232 | for i in (0..idx).rev() { 233 | if map(&slice[i]) == *key { 234 | idx = i; 235 | } else { 236 | break; 237 | } 238 | } 239 | slice.get(idx) 240 | } 241 | 242 | #[test] 243 | fn test_sourcemap_lookup_token() { 244 | let input = r#"{ 245 | "version": 3, 246 | "sources": ["coolstuff.js"], 247 | "sourceRoot": "x", 248 | "names": ["x","alert"], 249 | "mappings": "AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM" 250 | }"#; 251 | let sm = SourceMap::from_json_string(input).unwrap(); 252 | let lookup_table = sm.generate_lookup_table(); 253 | assert_eq!( 254 | sm.lookup_source_view_token(&lookup_table, 0, 0).unwrap().to_tuple(), 255 | (Some("coolstuff.js"), 0, 0, None) 256 | ); 257 | assert_eq!( 258 | sm.lookup_source_view_token(&lookup_table, 0, 3).unwrap().to_tuple(), 259 | (Some("coolstuff.js"), 0, 4, Some("x")) 260 | ); 261 | assert_eq!( 262 | sm.lookup_source_view_token(&lookup_table, 0, 24).unwrap().to_tuple(), 263 | (Some("coolstuff.js"), 2, 8, None) 264 | ); 265 | 266 | // Lines continue out to infinity 267 | assert_eq!( 268 | sm.lookup_source_view_token(&lookup_table, 0, 1000).unwrap().to_tuple(), 269 | (Some("coolstuff.js"), 2, 8, None) 270 | ); 271 | 272 | assert!(sm.lookup_source_view_token(&lookup_table, 1000, 0).is_none()); 273 | } 274 | 275 | #[test] 276 | fn test_sourcemap_source_view_token() { 277 | let sm = SourceMap::new( 278 | None, 279 | vec!["foo".into()], 280 | None, 281 | vec!["foo.js".into()], 282 | vec![], 283 | vec![Token::new(1, 1, 1, 1, Some(0), Some(0))], 284 | None, 285 | ); 286 | let mut source_view_tokens = sm.get_source_view_tokens(); 287 | assert_eq!(source_view_tokens.next().unwrap().to_tuple(), (Some("foo.js"), 1, 1, Some("foo"))); 288 | } 289 | 290 | #[test] 291 | fn test_mut_sourcemap() { 292 | let mut sm = SourceMap::default(); 293 | sm.set_file("index.js"); 294 | sm.set_sources(vec!["foo.js"]); 295 | sm.set_source_contents(vec![Some("foo")]); 296 | 297 | assert_eq!(sm.get_file(), Some("index.js")); 298 | assert_eq!(sm.get_source(0), Some("foo.js")); 299 | assert_eq!(sm.get_source_content(0), Some("foo")); 300 | } 301 | -------------------------------------------------------------------------------- /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 | let count = self.names.len() as u32; 27 | let id = *self.names_map.entry(name.into()).or_insert(count); 28 | if id == count { 29 | self.names.push(name.into()); 30 | } 31 | id 32 | } 33 | 34 | /// Add item to `SourceMap::sources` and `SourceMap::source_contents`. 35 | /// If `source` maybe duplicate, please use it. 36 | pub fn add_source_and_content(&mut self, source: &str, source_content: &str) -> u32 { 37 | let count = self.sources.len() as u32; 38 | let id = *self.sources_map.entry(source.into()).or_insert(count); 39 | if id == count { 40 | self.sources.push(source.into()); 41 | self.source_contents.push(Some(source_content.into())); 42 | } 43 | id 44 | } 45 | 46 | /// Add item to `SourceMap::sources` and `SourceMap::source_contents`. 47 | /// If `source` hasn't duplicate,it will avoid extra hash calculation. 48 | pub fn set_source_and_content(&mut self, source: &str, source_content: &str) -> u32 { 49 | let count = self.sources.len() as u32; 50 | self.sources.push(source.into()); 51 | self.source_contents.push(Some(source_content.into())); 52 | count 53 | } 54 | 55 | /// Add item to `SourceMap::tokens`. 56 | pub fn add_token( 57 | &mut self, 58 | dst_line: u32, 59 | dst_col: u32, 60 | src_line: u32, 61 | src_col: u32, 62 | src_id: Option, 63 | name_id: Option, 64 | ) { 65 | self.tokens.push(Token::new(dst_line, dst_col, src_line, src_col, src_id, name_id)); 66 | } 67 | 68 | pub fn set_file(&mut self, file: &str) { 69 | self.file = Some(file.into()); 70 | } 71 | 72 | /// Set the `SourceMap::token_chunks` to make the sourcemap to vlq mapping at parallel. 73 | pub fn set_token_chunks(&mut self, token_chunks: Vec) { 74 | self.token_chunks = Some(token_chunks); 75 | } 76 | 77 | pub fn into_sourcemap(self) -> SourceMap { 78 | SourceMap::new( 79 | self.file, 80 | self.names, 81 | None, 82 | self.sources, 83 | self.source_contents, 84 | self.tokens, 85 | self.token_chunks, 86 | ) 87 | } 88 | } 89 | 90 | #[test] 91 | fn test_sourcemap_builder() { 92 | let mut builder = SourceMapBuilder::default(); 93 | builder.set_source_and_content("baz.js", ""); 94 | builder.add_name("x"); 95 | builder.set_file("file"); 96 | 97 | let sm = builder.into_sourcemap(); 98 | assert_eq!(sm.get_source(0), Some("baz.js")); 99 | assert_eq!(sm.get_name(0), Some("x")); 100 | assert_eq!(sm.get_file(), Some("file")); 101 | 102 | let expected = r#"{"version":3,"file":"file","names":["x"],"sources":["baz.js"],"sourcesContent":[""],"mappings":""}"#; 103 | assert_eq!(expected, sm.to_json_string()); 104 | } 105 | -------------------------------------------------------------------------------- /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 | output: &'a str, 9 | sourcemap: &'a SourceMap, 10 | } 11 | 12 | impl<'a> SourcemapVisualizer<'a> { 13 | pub fn new(output: &'a str, sourcemap: &'a SourceMap) -> Self { 14 | Self { output, sourcemap } 15 | } 16 | 17 | pub fn into_visualizer_text(self) -> String { 18 | let mut s = String::new(); 19 | let source_contents = &self.sourcemap.source_contents; 20 | if self.sourcemap.source_contents.is_empty() { 21 | s.push_str("[no source contents]\n"); 22 | return s; 23 | } 24 | 25 | let source_contents_lines_map: Vec>> = source_contents 26 | .iter() 27 | .filter_map(|content| { 28 | let content = content.as_ref()?; 29 | Some(Self::generate_line_utf16_tables(content)) 30 | }) 31 | .collect(); 32 | 33 | let output_lines = Self::generate_line_utf16_tables(self.output); 34 | 35 | let tokens = &self.sourcemap.tokens; 36 | 37 | let mut last_source: Option<&str> = None; 38 | for i in 0..tokens.len() { 39 | let t = &tokens[i]; 40 | let Some(source_id) = t.source_id else { 41 | continue; 42 | }; 43 | let Some(source) = self.sourcemap.get_source(source_id) else { continue }; 44 | let source_lines = &source_contents_lines_map[source_id as usize]; 45 | 46 | // Print source 47 | if last_source != Some(source) { 48 | s.push('-'); 49 | s.push(' '); 50 | s.push_str(source); 51 | s.push('\n'); 52 | last_source = Some(source); 53 | } 54 | 55 | // validate token position 56 | let dst_invalid = t.dst_line as usize >= output_lines.len() 57 | || (t.dst_col as usize) >= output_lines[t.dst_line as usize].len(); 58 | let src_invalid = t.src_line as usize >= source_lines.len() 59 | || (t.src_col as usize) >= source_lines[t.src_line as usize].len(); 60 | if dst_invalid || src_invalid { 61 | s.push_str(&format!( 62 | "({}:{}){} --> ({}:{}){}\n", 63 | t.src_line, 64 | t.src_col, 65 | if src_invalid { " [invalid]" } else { "" }, 66 | t.dst_line, 67 | t.dst_col, 68 | if dst_invalid { " [invalid]" } else { "" }, 69 | )); 70 | continue; 71 | } 72 | 73 | // find next dst column or EOL 74 | let dst_end_col = { 75 | match tokens.get(i + 1) { 76 | Some(t2) if t2.dst_line == t.dst_line => t2.dst_col, 77 | _ => output_lines[t.dst_line as usize].len() as u32, 78 | } 79 | }; 80 | 81 | // find next src column or EOL 82 | let src_end_col = 'result: { 83 | for t2 in &tokens[i + 1..] { 84 | if t2.source_id == t.source_id && t2.src_line == t.src_line { 85 | // skip duplicate or backward 86 | if t2.src_col <= t.src_col { 87 | continue; 88 | } 89 | break 'result t2.src_col; 90 | } 91 | break; 92 | } 93 | source_lines[t.src_line as usize].len() as u32 94 | }; 95 | 96 | s.push_str(&format!( 97 | "({}:{}) {:?} --> ({}:{}) {:?}\n", 98 | t.src_line, 99 | t.src_col, 100 | Self::str_slice_by_token(source_lines, t.src_line, t.src_col, src_end_col), 101 | t.dst_line, 102 | t.dst_col, 103 | Self::str_slice_by_token(&output_lines, t.dst_line, t.dst_col, dst_end_col) 104 | )); 105 | } 106 | 107 | s 108 | } 109 | 110 | fn generate_line_utf16_tables(content: &str) -> Vec> { 111 | let mut tables = vec![]; 112 | let mut line_byte_offset = 0; 113 | for (i, ch) in content.char_indices() { 114 | match ch { 115 | '\r' | '\n' | '\u{2028}' | '\u{2029}' => { 116 | // Handle Windows-specific "\r\n" newlines 117 | if ch == '\r' && content.chars().nth(i + 1) == Some('\n') { 118 | continue; 119 | } 120 | tables.push(content[line_byte_offset..=i].encode_utf16().collect::>()); 121 | line_byte_offset = i + 1; 122 | } 123 | _ => {} 124 | } 125 | } 126 | tables.push(content[line_byte_offset..].encode_utf16().collect::>()); 127 | tables 128 | } 129 | 130 | fn str_slice_by_token(buff: &[Vec], line: u32, start: u32, end: u32) -> Cow<'_, str> { 131 | let line = line as usize; 132 | let start = start as usize; 133 | let end = end as usize; 134 | let s = &buff[line]; 135 | String::from_utf16(&s[start.min(end).min(s.len())..start.max(end).min(s.len())]) 136 | .unwrap() 137 | .replace("\r", "") 138 | .into() 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/token.rs: -------------------------------------------------------------------------------- 1 | use crate::SourceMap; 2 | 3 | /// The `Token` is used to generate vlq `mappings`. 4 | #[derive(Debug, PartialEq, Eq, Clone)] 5 | pub struct Token { 6 | pub(crate) dst_line: u32, 7 | pub(crate) dst_col: u32, 8 | pub(crate) src_line: u32, 9 | pub(crate) src_col: u32, 10 | pub(crate) source_id: Option, 11 | pub(crate) name_id: Option, 12 | } 13 | 14 | impl Token { 15 | pub fn new( 16 | dst_line: u32, 17 | dst_col: u32, 18 | src_line: u32, 19 | src_col: u32, 20 | source_id: Option, 21 | name_id: Option, 22 | ) -> Self { 23 | Self { dst_line, dst_col, src_line, src_col, source_id, name_id } 24 | } 25 | 26 | pub fn get_dst_line(&self) -> u32 { 27 | self.dst_line 28 | } 29 | 30 | pub fn get_dst_col(&self) -> u32 { 31 | self.dst_col 32 | } 33 | 34 | pub fn get_src_line(&self) -> u32 { 35 | self.src_line 36 | } 37 | 38 | pub fn get_src_col(&self) -> u32 { 39 | self.src_col 40 | } 41 | 42 | pub fn get_name_id(&self) -> Option { 43 | self.name_id 44 | } 45 | 46 | pub fn get_source_id(&self) -> Option { 47 | self.source_id 48 | } 49 | } 50 | 51 | /// The `TokenChunk` used by encode tokens to vlq mappings at parallel. 52 | /// It is a slice of `SourceMap::tokens`, it is a unit of parallel. 53 | #[derive(Debug, Clone, Default, PartialEq, Eq)] 54 | pub struct TokenChunk { 55 | pub start: u32, 56 | pub end: u32, 57 | pub prev_dst_line: u32, 58 | pub prev_dst_col: u32, 59 | pub prev_src_line: u32, 60 | pub prev_src_col: u32, 61 | pub prev_name_id: u32, 62 | pub prev_source_id: u32, 63 | } 64 | 65 | impl TokenChunk { 66 | #[expect(clippy::too_many_arguments)] 67 | pub fn new( 68 | start: u32, 69 | end: u32, 70 | prev_dst_line: u32, 71 | prev_dst_col: u32, 72 | prev_src_line: u32, 73 | prev_src_col: u32, 74 | prev_name_id: u32, 75 | prev_source_id: u32, 76 | ) -> Self { 77 | Self { 78 | start, 79 | end, 80 | prev_dst_line, 81 | prev_dst_col, 82 | prev_src_line, 83 | prev_src_col, 84 | prev_name_id, 85 | prev_source_id, 86 | } 87 | } 88 | } 89 | 90 | /// The `SourceViewToken` provider extra `source` and `source_content` value. 91 | #[derive(Debug, Clone, Copy)] 92 | pub struct SourceViewToken<'a> { 93 | pub(crate) token: &'a Token, 94 | pub(crate) sourcemap: &'a SourceMap, 95 | } 96 | 97 | impl<'a> SourceViewToken<'a> { 98 | pub fn new(token: &'a Token, sourcemap: &'a SourceMap) -> Self { 99 | Self { token, sourcemap } 100 | } 101 | 102 | pub fn get_dst_line(&self) -> u32 { 103 | self.token.dst_line 104 | } 105 | 106 | pub fn get_dst_col(&self) -> u32 { 107 | self.token.dst_col 108 | } 109 | 110 | pub fn get_src_line(&self) -> u32 { 111 | self.token.src_line 112 | } 113 | 114 | pub fn get_src_col(&self) -> u32 { 115 | self.token.src_col 116 | } 117 | 118 | pub fn get_name_id(&self) -> Option { 119 | self.token.name_id 120 | } 121 | 122 | pub fn get_source_id(&self) -> Option { 123 | self.token.source_id 124 | } 125 | 126 | pub fn get_name(&self) -> Option<&str> { 127 | self.token.name_id.and_then(|id| self.sourcemap.get_name(id)) 128 | } 129 | 130 | pub fn get_source(&self) -> Option<&str> { 131 | self.token.source_id.and_then(|id| self.sourcemap.get_source(id)) 132 | } 133 | 134 | pub fn get_source_content(&self) -> Option<&str> { 135 | self.token.source_id.and_then(|id| self.sourcemap.get_source_content(id)) 136 | } 137 | 138 | pub fn get_source_and_content(&self) -> Option<(&str, &str)> { 139 | self.token.source_id.and_then(|id| self.sourcemap.get_source_and_content(id)) 140 | } 141 | 142 | pub fn to_tuple(&self) -> (Option<&str>, u32, u32, Option<&str>) { 143 | (self.get_source(), self.get_src_line(), self.get_src_col(), self.get_name()) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /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.into_visualizer_text(); 30 | insta::assert_snapshot!("empty", visualizer_text); 31 | } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /tests/fixtures/esbuild/README.md: -------------------------------------------------------------------------------- 1 | Example code from https://github.com/evanw/source-map-visualization/ 2 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/fixtures/swap/test.js: -------------------------------------------------------------------------------- 1 | z; 2 | y; 3 | x; 4 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/dep2.js: -------------------------------------------------------------------------------- 1 | 2 | //#region src/dep2.js 3 | console.log("dep2"); 4 | 5 | //#endregion 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/dep3.js: -------------------------------------------------------------------------------- 1 | 2 | //#region src/dep3.js 3 | console.log("dep3"); 4 | 5 | //#endregion 6 | -------------------------------------------------------------------------------- /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/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.into_visualizer_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 | None, 33 | ); 34 | let js = "abc\ndef\n"; 35 | let visualizer = SourcemapVisualizer::new(js, &sourcemap); 36 | let visualizer_text = visualizer.into_visualizer_text(); 37 | insta::assert_snapshot!("invalid_token_position", visualizer_text); 38 | } 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------