├── rustfmt.toml ├── .prettierignore ├── examples ├── wasm │ ├── index.html │ ├── index.js │ └── package.json └── index.js ├── node ├── build.rs ├── tsconfig.json ├── npm │ ├── darwin-x64 │ │ ├── README.md │ │ └── package.json │ ├── android-arm64 │ │ ├── README.md │ │ └── package.json │ ├── darwin-arm64 │ │ ├── README.md │ │ └── package.json │ ├── freebsd-x64 │ │ ├── README.md │ │ └── package.json │ ├── linux-x64-gnu │ │ ├── README.md │ │ └── package.json │ ├── win32-ia32-msvc │ │ ├── README.md │ │ └── package.json │ ├── win32-x64-msvc │ │ ├── README.md │ │ └── package.json │ ├── android-arm-eabi │ │ ├── README.md │ │ └── package.json │ ├── linux-arm64-gnu │ │ ├── README.md │ │ └── package.json │ ├── linux-x64-musl │ │ ├── README.md │ │ └── package.json │ ├── win32-arm64-msvc │ │ ├── README.md │ │ └── package.json │ ├── linux-arm64-musl │ │ ├── README.md │ │ └── package.json │ └── linux-arm-gnueabihf │ │ ├── README.md │ │ └── package.json ├── Cargo.toml ├── prof │ └── index.js ├── index.js ├── package.json ├── index.d.ts ├── src │ └── lib.rs ├── binding.js └── tests │ └── MagicString.spec.ts ├── core ├── README.md ├── tests │ ├── magic_string.rs │ ├── is_empty.rs │ ├── trim.rs │ ├── remove.rs │ ├── overwrite.rs │ ├── pend.rs │ ├── move.rs │ └── generate_map.rs ├── src │ ├── lib.rs │ ├── source_map.rs │ ├── utils.rs │ ├── result.rs │ ├── chunk.rs │ ├── mapping.rs │ └── magic_string.rs └── Cargo.toml ├── Cargo.toml ├── wasm ├── src │ └── lib.rs ├── package.json └── Cargo.toml ├── benchmark ├── package.json ├── tsconfig.json └── bench.ts ├── .cargo └── config.toml ├── .gitignore ├── .github └── workflows │ ├── Test.yaml │ ├── lint.yaml │ └── CI.yaml ├── LICENSE ├── package.json └── README.md /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | target 2 | node_modules 3 | -------------------------------------------------------------------------------- /examples/wasm/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /node/build.rs: -------------------------------------------------------------------------------- 1 | extern crate napi_build; 2 | 3 | fn main() { 4 | napi_build::setup(); 5 | } 6 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | const magicString = require('../node') 2 | 3 | console.log(magicString.init(123)) 4 | -------------------------------------------------------------------------------- /node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "moduleResolution": "Node" 5 | } 6 | } -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | # magic_string 2 | 3 | ## 📃 Documentation 4 | 5 | [doc.rs](https://docs.rs/magic_string/latest/magic_string) 6 | -------------------------------------------------------------------------------- /examples/wasm/index.js: -------------------------------------------------------------------------------- 1 | import init, { create } from '../../wasm/pkg/wasm.js' 2 | 3 | await init() 4 | console.log(create(123)) 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["core", "node"] 3 | 4 | [profile.release] 5 | codegen-units = 1 6 | lto = true 7 | opt-level = 3 8 | -------------------------------------------------------------------------------- /wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | #[wasm_bindgen] 4 | pub fn create(val: u32) -> Result { 5 | Ok(val) 6 | } 7 | -------------------------------------------------------------------------------- /examples/wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm-demo", 3 | "private": true, 4 | "scripts": { 5 | "serve": "pnpx http-server" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /node/npm/darwin-x64/README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/magic-string-darwin-x64` 2 | 3 | This is the **x86_64-apple-darwin** binary for `@napi-rs/magic-string` 4 | -------------------------------------------------------------------------------- /node/npm/android-arm64/README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/magic-string-android-arm64` 2 | 3 | This is the **aarch64-linux-android** binary for `@napi-rs/magic-string` 4 | -------------------------------------------------------------------------------- /node/npm/darwin-arm64/README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/magic-string-darwin-arm64` 2 | 3 | This is the **aarch64-apple-darwin** binary for `@napi-rs/magic-string` 4 | -------------------------------------------------------------------------------- /node/npm/freebsd-x64/README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/magic-string-freebsd-x64` 2 | 3 | This is the **x86_64-unknown-freebsd** binary for `@napi-rs/magic-string` 4 | -------------------------------------------------------------------------------- /node/npm/linux-x64-gnu/README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/magic-string-linux-x64-gnu` 2 | 3 | This is the **x86_64-unknown-linux-gnu** binary for `@napi-rs/magic-string` 4 | -------------------------------------------------------------------------------- /node/npm/win32-ia32-msvc/README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/magic-string-win32-ia32-msvc` 2 | 3 | This is the **i686-pc-windows-msvc** binary for `@napi-rs/magic-string` 4 | -------------------------------------------------------------------------------- /node/npm/win32-x64-msvc/README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/magic-string-win32-x64-msvc` 2 | 3 | This is the **x86_64-pc-windows-msvc** binary for `@napi-rs/magic-string` 4 | -------------------------------------------------------------------------------- /node/npm/android-arm-eabi/README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/magic-string-android-arm-eabi` 2 | 3 | This is the **armv7-linux-androideabi** binary for `@napi-rs/magic-string` 4 | -------------------------------------------------------------------------------- /node/npm/linux-arm64-gnu/README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/magic-string-linux-arm64-gnu` 2 | 3 | This is the **aarch64-unknown-linux-gnu** binary for `@napi-rs/magic-string` 4 | -------------------------------------------------------------------------------- /node/npm/linux-x64-musl/README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/magic-string-linux-x64-musl` 2 | 3 | This is the **x86_64-unknown-linux-musl** binary for `@napi-rs/magic-string` 4 | -------------------------------------------------------------------------------- /node/npm/win32-arm64-msvc/README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/magic-string-win32-arm64-msvc` 2 | 3 | This is the **aarch64-pc-windows-msvc** binary for `@napi-rs/magic-string` 4 | -------------------------------------------------------------------------------- /node/npm/linux-arm64-musl/README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/magic-string-linux-arm64-musl` 2 | 3 | This is the **aarch64-unknown-linux-musl** binary for `@napi-rs/magic-string` 4 | -------------------------------------------------------------------------------- /node/npm/linux-arm-gnueabihf/README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/magic-string-linux-arm-gnueabihf` 2 | 3 | This is the **armv7-unknown-linux-gnueabihf** binary for `@napi-rs/magic-string` 4 | -------------------------------------------------------------------------------- /core/tests/magic_string.rs: -------------------------------------------------------------------------------- 1 | // tests are copied from `magic-string`: https://github.com/Rich-Harris/magic-string/blob/master/test/MagicString.js 2 | 3 | #[cfg(test)] 4 | mod magic_string {} 5 | -------------------------------------------------------------------------------- /benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bench", 3 | "private": true, 4 | "license": "MIT", 5 | "version": "0.0.0", 6 | "dependencies": { 7 | "benny": "^3.7.1", 8 | "magic-string": "^0.25.7" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /benchmark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "outDir": "dist", 7 | "target": "ESNext", 8 | "rootDir": "." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | 3 | #[macro_use] 4 | extern crate serde; 5 | 6 | mod mapping; 7 | mod utils; 8 | 9 | mod chunk; 10 | mod source_map; 11 | 12 | pub mod magic_string; 13 | pub mod result; 14 | 15 | pub use crate::magic_string::*; 16 | pub use crate::result::*; 17 | pub use crate::source_map::SourceMap; 18 | -------------------------------------------------------------------------------- /wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magic-string-wasm", 3 | "version": "0.0.0", 4 | "main": "./pkg/magic_string_wasm.js", 5 | "module": "./pkg/magic_string_wasm.js", 6 | "typings": "./pkg/magic_string_wasm.d.ts", 7 | "scripts": { 8 | "build": "wasm-pack build --release --target web", 9 | "test": "wasm-pack test --headless --chrome" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["h-a-n-a "] 3 | edition = "2021" 4 | name = "magic-string-wasm" 5 | version = "0.1.0" 6 | publish = false 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [dependencies] 14 | magic_string = {path = "../core"} 15 | wasm-bindgen = "0.2.63" 16 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-gnu] 2 | linker = "aarch64-linux-gnu-gcc" 3 | 4 | [target.armv7-unknown-linux-gnueabihf] 5 | linker = "arm-linux-gnueabihf-gcc" 6 | 7 | [target.x86_64-unknown-linux-musl] 8 | rustflags = [ 9 | "-C", 10 | "target-feature=-crt-static", 11 | ] 12 | 13 | [target.aarch64-unknown-linux-musl] 14 | linker = "aarch64-linux-musl-gcc" 15 | rustflags = ["-C", "target-feature=-crt-static"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | 3 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 4 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 5 | Cargo.lock 6 | 7 | # These are backup files generated by rustfmt 8 | **/*.rs.bk 9 | 10 | # IDE 11 | .idea 12 | 13 | 14 | # Added by cargo 15 | /target 16 | 17 | 18 | 19 | # Web 20 | node_modules 21 | yarn-error.log 22 | dist 23 | 24 | # Node addon 25 | *.node -------------------------------------------------------------------------------- /core/tests/is_empty.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod empty { 3 | use magic_string::{MagicString, OverwriteOptions, Result}; 4 | 5 | #[test] 6 | fn should_support_is_empty() -> Result { 7 | let str = " abcde fghijkl "; 8 | let mut s = MagicString::new(str); 9 | 10 | assert!(!s.is_empty()); 11 | 12 | s.prepend(" ")?; 13 | 14 | s.overwrite(0, str.len() as i64, "", OverwriteOptions::default())?; 15 | 16 | assert!(s.is_empty()); 17 | 18 | Ok(()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/Test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags-ignore: 8 | - '**' 9 | pull_request: 10 | 11 | jobs: 12 | Test: 13 | name: Test 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Setup node 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 14 22 | 23 | - name: Install 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: stable 27 | profile: minimal 28 | override: true 29 | 30 | - name: Cargo test 31 | run: cargo test -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["h-a-n-a "] 3 | description = "magic string" 4 | edition = "2021" 5 | license = "MIT" 6 | name = "magic_string" 7 | repository = "https://github.com/h-a-n-a/magic-string-rs" 8 | version = "0.3.4" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [features] 13 | node-api = ["napi", "napi-derive"] 14 | 15 | [dependencies] 16 | base64 = "0.13" 17 | serde = {version = "1.0", features = ["derive"]} 18 | serde_json = "1.0" 19 | vlq = "0.5.1" 20 | regex = "1.4.2" 21 | 22 | [dependencies.napi] 23 | optional = true 24 | version = "2.0.0-beta.5" 25 | 26 | [dependencies.napi-derive] 27 | optional = true 28 | version = "2.0.0-beta.5" 29 | -------------------------------------------------------------------------------- /node/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["h-a-n-a "] 3 | edition = "2021" 4 | name = "magic-string-node" 5 | publish = false 6 | version = "0.1.0" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | crate-type = ["cdylib"] 12 | 13 | [dependencies] 14 | magic_string = {path = "../core", features = ["node-api"]} 15 | napi = {version = "2.0.1", features = ["napi3", "serde-json"]} 16 | napi-derive = "2.0.1" 17 | serde = {version = "1", features = ["derive"]} 18 | serde_json = "1" 19 | 20 | [target.'cfg(all(any(windows, unix), target_arch = "x86_64", not(target_env = "musl")))'.dependencies] 21 | mimalloc = {version = "0.1"} 22 | 23 | [build-dependencies] 24 | napi-build = "1" 25 | -------------------------------------------------------------------------------- /node/prof/index.js: -------------------------------------------------------------------------------- 1 | const b = require("benny") 2 | 3 | const { MagicString: MagicStringRust } = require("../index"); 4 | const MagicString = require("magic-string") 5 | 6 | 7 | const BANNER = `/*! 8 | * Vue.js v2.6.14 9 | * (c) 2014-2021 Evan You 10 | * Released under the MIT License. 11 | */ 12 | ` 13 | 14 | // for (let i = 0; i < 100000; i++) { 15 | // const s = new MagicString(BANNER) 16 | // s.prepend(BANNER) 17 | // s.generateMap().toString() 18 | // } 19 | 20 | b.suite( 21 | 'add banner#generateMap', 22 | b.add('MagicString', () => { 23 | const m = new MagicString(`export const foo = 'bar'`) 24 | m.prepend(BANNER) 25 | m.generateMap() 26 | }), 27 | b.add('MagicStringRust', () => { 28 | const m = new MagicStringRust(`export const foo = 'bar'`) 29 | m.prepend(BANNER) 30 | m.generateMap().toMap() 31 | }), 32 | b.cycle(), 33 | b.complete(), 34 | ) -------------------------------------------------------------------------------- /node/npm/darwin-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string-darwin-x64", 3 | "version": "0.3.4", 4 | "os": [ 5 | "darwin" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "magic-string-rs.darwin-x64.node", 11 | "files": [ 12 | "magic-string-rs.darwin-x64.node" 13 | ], 14 | "description": "rusty magic-string", 15 | "keywords": [ 16 | "magic-string", 17 | "string", 18 | "napi", 19 | "N-API", 20 | "napi-rs" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "h-a-n-a", 25 | "email": "andywangsy@gmail.com" 26 | }, 27 | { 28 | "name": "LongYinan", 29 | "email": "github@lyn.one" 30 | } 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/", 38 | "access": "public" 39 | }, 40 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git" 41 | } -------------------------------------------------------------------------------- /node/npm/freebsd-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string-freebsd-x64", 3 | "version": "0.3.4", 4 | "os": [ 5 | "freebsd" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "magic-string-rs.freebsd-x64.node", 11 | "files": [ 12 | "magic-string-rs.freebsd-x64.node" 13 | ], 14 | "description": "rusty magic-string", 15 | "keywords": [ 16 | "magic-string", 17 | "string", 18 | "napi", 19 | "N-API", 20 | "napi-rs" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "h-a-n-a", 25 | "email": "andywangsy@gmail.com" 26 | }, 27 | { 28 | "name": "LongYinan", 29 | "email": "github@lyn.one" 30 | } 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/", 38 | "access": "public" 39 | }, 40 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git" 41 | } -------------------------------------------------------------------------------- /node/npm/darwin-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string-darwin-arm64", 3 | "version": "0.3.4", 4 | "os": [ 5 | "darwin" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "magic-string-rs.darwin-arm64.node", 11 | "files": [ 12 | "magic-string-rs.darwin-arm64.node" 13 | ], 14 | "description": "rusty magic-string", 15 | "keywords": [ 16 | "magic-string", 17 | "string", 18 | "napi", 19 | "N-API", 20 | "napi-rs" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "h-a-n-a", 25 | "email": "andywangsy@gmail.com" 26 | }, 27 | { 28 | "name": "LongYinan", 29 | "email": "github@lyn.one" 30 | } 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/", 38 | "access": "public" 39 | }, 40 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git" 41 | } -------------------------------------------------------------------------------- /node/npm/linux-x64-gnu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string-linux-x64-gnu", 3 | "version": "0.3.4", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "magic-string-rs.linux-x64-gnu.node", 11 | "files": [ 12 | "magic-string-rs.linux-x64-gnu.node" 13 | ], 14 | "description": "rusty magic-string", 15 | "keywords": [ 16 | "magic-string", 17 | "string", 18 | "napi", 19 | "N-API", 20 | "napi-rs" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "h-a-n-a", 25 | "email": "andywangsy@gmail.com" 26 | }, 27 | { 28 | "name": "LongYinan", 29 | "email": "github@lyn.one" 30 | } 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/", 38 | "access": "public" 39 | }, 40 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git" 41 | } -------------------------------------------------------------------------------- /node/npm/android-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string-android-arm64", 3 | "version": "0.3.4", 4 | "os": [ 5 | "android" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "magic-string-rs.android-arm64.node", 11 | "files": [ 12 | "magic-string-rs.android-arm64.node" 13 | ], 14 | "description": "rusty magic-string", 15 | "keywords": [ 16 | "magic-string", 17 | "string", 18 | "napi", 19 | "N-API", 20 | "napi-rs" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "h-a-n-a", 25 | "email": "andywangsy@gmail.com" 26 | }, 27 | { 28 | "name": "LongYinan", 29 | "email": "github@lyn.one" 30 | } 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/", 38 | "access": "public" 39 | }, 40 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git" 41 | } -------------------------------------------------------------------------------- /node/npm/linux-x64-musl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string-linux-x64-musl", 3 | "version": "0.3.4", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "magic-string-rs.linux-x64-musl.node", 11 | "files": [ 12 | "magic-string-rs.linux-x64-musl.node" 13 | ], 14 | "description": "rusty magic-string", 15 | "keywords": [ 16 | "magic-string", 17 | "string", 18 | "napi", 19 | "N-API", 20 | "napi-rs" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "h-a-n-a", 25 | "email": "andywangsy@gmail.com" 26 | }, 27 | { 28 | "name": "LongYinan", 29 | "email": "github@lyn.one" 30 | } 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/", 38 | "access": "public" 39 | }, 40 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git" 41 | } -------------------------------------------------------------------------------- /node/npm/win32-x64-msvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string-win32-x64-msvc", 3 | "version": "0.3.4", 4 | "os": [ 5 | "win32" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "magic-string-rs.win32-x64-msvc.node", 11 | "files": [ 12 | "magic-string-rs.win32-x64-msvc.node" 13 | ], 14 | "description": "rusty magic-string", 15 | "keywords": [ 16 | "magic-string", 17 | "string", 18 | "napi", 19 | "N-API", 20 | "napi-rs" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "h-a-n-a", 25 | "email": "andywangsy@gmail.com" 26 | }, 27 | { 28 | "name": "LongYinan", 29 | "email": "github@lyn.one" 30 | } 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/", 38 | "access": "public" 39 | }, 40 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git" 41 | } -------------------------------------------------------------------------------- /node/npm/linux-arm64-gnu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string-linux-arm64-gnu", 3 | "version": "0.3.4", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "magic-string-rs.linux-arm64-gnu.node", 11 | "files": [ 12 | "magic-string-rs.linux-arm64-gnu.node" 13 | ], 14 | "description": "rusty magic-string", 15 | "keywords": [ 16 | "magic-string", 17 | "string", 18 | "napi", 19 | "N-API", 20 | "napi-rs" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "h-a-n-a", 25 | "email": "andywangsy@gmail.com" 26 | }, 27 | { 28 | "name": "LongYinan", 29 | "email": "github@lyn.one" 30 | } 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/", 38 | "access": "public" 39 | }, 40 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git" 41 | } -------------------------------------------------------------------------------- /node/npm/win32-ia32-msvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string-win32-ia32-msvc", 3 | "version": "0.3.4", 4 | "os": [ 5 | "win32" 6 | ], 7 | "cpu": [ 8 | "ia32" 9 | ], 10 | "main": "magic-string-rs.win32-ia32-msvc.node", 11 | "files": [ 12 | "magic-string-rs.win32-ia32-msvc.node" 13 | ], 14 | "description": "rusty magic-string", 15 | "keywords": [ 16 | "magic-string", 17 | "string", 18 | "napi", 19 | "N-API", 20 | "napi-rs" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "h-a-n-a", 25 | "email": "andywangsy@gmail.com" 26 | }, 27 | { 28 | "name": "LongYinan", 29 | "email": "github@lyn.one" 30 | } 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/", 38 | "access": "public" 39 | }, 40 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git" 41 | } -------------------------------------------------------------------------------- /node/npm/android-arm-eabi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string-android-arm-eabi", 3 | "version": "0.3.4", 4 | "os": [ 5 | "android" 6 | ], 7 | "cpu": [ 8 | "arm" 9 | ], 10 | "main": "magic-string-rs.android-arm-eabi.node", 11 | "files": [ 12 | "magic-string-rs.android-arm-eabi.node" 13 | ], 14 | "description": "rusty magic-string", 15 | "keywords": [ 16 | "magic-string", 17 | "string", 18 | "napi", 19 | "N-API", 20 | "napi-rs" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "h-a-n-a", 25 | "email": "andywangsy@gmail.com" 26 | }, 27 | { 28 | "name": "LongYinan", 29 | "email": "github@lyn.one" 30 | } 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/", 38 | "access": "public" 39 | }, 40 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git" 41 | } -------------------------------------------------------------------------------- /node/npm/linux-arm64-musl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string-linux-arm64-musl", 3 | "version": "0.3.4", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "magic-string-rs.linux-arm64-musl.node", 11 | "files": [ 12 | "magic-string-rs.linux-arm64-musl.node" 13 | ], 14 | "description": "rusty magic-string", 15 | "keywords": [ 16 | "magic-string", 17 | "string", 18 | "napi", 19 | "N-API", 20 | "napi-rs" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "h-a-n-a", 25 | "email": "andywangsy@gmail.com" 26 | }, 27 | { 28 | "name": "LongYinan", 29 | "email": "github@lyn.one" 30 | } 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/", 38 | "access": "public" 39 | }, 40 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git" 41 | } -------------------------------------------------------------------------------- /node/npm/win32-arm64-msvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string-win32-arm64-msvc", 3 | "version": "0.3.4", 4 | "os": [ 5 | "win32" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "magic-string-rs.win32-arm64-msvc.node", 11 | "files": [ 12 | "magic-string-rs.win32-arm64-msvc.node" 13 | ], 14 | "description": "rusty magic-string", 15 | "keywords": [ 16 | "magic-string", 17 | "string", 18 | "napi", 19 | "N-API", 20 | "napi-rs" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "h-a-n-a", 25 | "email": "andywangsy@gmail.com" 26 | }, 27 | { 28 | "name": "LongYinan", 29 | "email": "github@lyn.one" 30 | } 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/", 38 | "access": "public" 39 | }, 40 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git" 41 | } -------------------------------------------------------------------------------- /node/npm/linux-arm-gnueabihf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string-linux-arm-gnueabihf", 3 | "version": "0.3.4", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "arm" 9 | ], 10 | "main": "magic-string-rs.linux-arm-gnueabihf.node", 11 | "files": [ 12 | "magic-string-rs.linux-arm-gnueabihf.node" 13 | ], 14 | "description": "rusty magic-string", 15 | "keywords": [ 16 | "magic-string", 17 | "string", 18 | "napi", 19 | "N-API", 20 | "napi-rs" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "h-a-n-a", 25 | "email": "andywangsy@gmail.com" 26 | }, 27 | { 28 | "name": "LongYinan", 29 | "email": "github@lyn.one" 30 | } 31 | ], 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/", 38 | "access": "public" 39 | }, 40 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git" 41 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags-ignore: 8 | - '**' 9 | pull_request: 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Setup node 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 14 22 | 23 | - name: Install 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: stable 27 | profile: minimal 28 | override: true 29 | components: rustfmt, clippy 30 | 31 | - name: Cache NPM dependencies 32 | uses: actions/cache@v1 33 | with: 34 | path: node/node_modules 35 | key: npm-cache-lint-node@14-${{ hashFiles('yarn.lock') }} 36 | 37 | - name: Cargo fmt 38 | run: cargo fmt -- --check 39 | 40 | - name: Clippy 41 | run: cargo clippy 42 | 43 | - name: 'Install dependencies' 44 | run: yarn install --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 45 | 46 | - name: ESLint 47 | run: yarn --cwd node lint 48 | -------------------------------------------------------------------------------- /node/index.js: -------------------------------------------------------------------------------- 1 | const { MagicString: MagicStringNative } = require('./binding') 2 | 3 | module.exports.MagicString = class MagicString extends MagicStringNative { 4 | overwrite(start, end, content, options) { 5 | options = { 6 | contentOnly: false, 7 | ...options 8 | } 9 | return super.overwrite(start, end, content, options) 10 | } 11 | generateMap(options) { 12 | options = { 13 | file: null, 14 | source: null, 15 | sourceRoot: null, 16 | includeContent: false, 17 | hires: false, 18 | ...options, 19 | } 20 | 21 | const toString = () => super.toSourcemapString(options) 22 | const toUrl = () => super.toSourcemapUrl(options) 23 | const toMap = () => JSON.parse(toString(options)) 24 | 25 | return { 26 | toString, 27 | toUrl, 28 | toMap, 29 | } 30 | } 31 | generateDecodedMap(options) { 32 | options = { 33 | file: null, 34 | source: null, 35 | sourceRoot: null, 36 | includeContent: false, 37 | hires: false, 38 | ...options, 39 | } 40 | 41 | return JSON.parse(super.generateDecodedMap(options)) 42 | } 43 | toSourcemapString() { 44 | throw new Error( 45 | '[magic-string] This is an internal API, you may refer to `generateMap`', 46 | ) 47 | } 48 | toSourcemapUrl() { 49 | throw new Error( 50 | '[magic-string] This is an internal API, you may refer to `generateMap`', 51 | ) 52 | } 53 | } 54 | 55 | Object.assign(exports, '__esModule', { 56 | value: true, 57 | }) 58 | module.exports.default = module.exports.MagicString 59 | -------------------------------------------------------------------------------- /node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/magic-string", 3 | "version": "0.3.4", 4 | "description": "rusty magic-string", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git", 8 | "authors": [ 9 | { 10 | "name": "h-a-n-a", 11 | "email": "andywangsy@gmail.com" 12 | }, 13 | { 14 | "name": "LongYinan", 15 | "email": "github@lyn.one" 16 | } 17 | ], 18 | "license": "MIT", 19 | "publishConfig": { 20 | "registry": "https://registry.npmjs.org/", 21 | "access": "public" 22 | }, 23 | "keywords": [ 24 | "magic-string", 25 | "string", 26 | "napi", 27 | "N-API", 28 | "napi-rs" 29 | ], 30 | "files": [ 31 | "index.js", 32 | "index.d.ts", 33 | "binding.js" 34 | ], 35 | "scripts": { 36 | "artifacts": "napi artifacts", 37 | "build": "napi build --platform --release --js binding.js --pipe \"prettier -w\"", 38 | "build:debug": "napi build --platform --js binding.js --pipe \"prettier -w\"", 39 | "prepublishOnly": "napi prepublish -t npm && esbuild --minify --outfile=binding.js --allow-overwrite binding.js", 40 | "prof": "node --cpu-prof prof/index.js", 41 | "lint": "echo lint", 42 | "version": "napi version" 43 | }, 44 | "napi": { 45 | "name": "magic-string-rs", 46 | "triples": { 47 | "defaults": true, 48 | "additional": [ 49 | "x86_64-unknown-linux-musl", 50 | "aarch64-unknown-linux-gnu", 51 | "i686-pc-windows-msvc", 52 | "armv7-unknown-linux-gnueabihf", 53 | "aarch64-apple-darwin", 54 | "x86_64-unknown-freebsd", 55 | "aarch64-unknown-linux-musl", 56 | "aarch64-pc-windows-msvc", 57 | "aarch64-linux-android", 58 | "armv7-linux-androideabi" 59 | ] 60 | } 61 | }, 62 | "engines": { 63 | "node": ">= 10" 64 | }, 65 | "dependencies": {} 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magic-string-rs", 3 | "version": "0.0.0", 4 | "description": "https://github.com/Rich-Harris/magic-string Rust port", 5 | "main": "index.js", 6 | "repository": "https://github.com/h-a-n-a/magic-string-rs.git", 7 | "authors": [ 8 | { 9 | "name": "h-a-n-a", 10 | "email": "andywangsy@gmail.com" 11 | }, 12 | { 13 | "name": "LongYinan", 14 | "email": "github@lyn.one" 15 | } 16 | ], 17 | "license": "MIT", 18 | "private": true, 19 | "workspaces": [ 20 | "node", 21 | "benchmark" 22 | ], 23 | "scripts": { 24 | "bench": "node -r @swc-node/register benchmark/bench.ts", 25 | "build": "yarn --cwd node build", 26 | "format": "prettier . -w", 27 | "test": "mocha -r @swc-node/register node/tests/**/*.spec.ts" 28 | }, 29 | "devDependencies": { 30 | "@napi-rs/cli": "^2.0.0", 31 | "@swc-node/register": "^1.4.0", 32 | "@types/mocha": "^9.0.0", 33 | "@types/node": "^16.11.12", 34 | "@types/source-map": "^0.5.7", 35 | "@typescript-eslint/eslint-plugin": "^4.33.0", 36 | "@typescript-eslint/parser": "^4.31.2", 37 | "chalk": "^4.1.2", 38 | "esbuild": "^0.14.2", 39 | "eslint": "^7.32.0", 40 | "eslint-config-prettier": "^8.3.0", 41 | "eslint-plugin-import": "^2.25.3", 42 | "eslint-plugin-prettier": "^4.0.0", 43 | "eslint-plugin-sonarjs": "^0.10.0", 44 | "husky": "^7.0.4", 45 | "lint-staged": "^12.0.2", 46 | "magic-string": "^0.25.7", 47 | "mocha": "^9.1.3", 48 | "npm-run-all": "^4.1.5", 49 | "prettier": "^2.5.1", 50 | "source-map": "0.6.1", 51 | "typescript": "^4.4.4" 52 | }, 53 | "prettier": { 54 | "printWidth": 80, 55 | "semi": false, 56 | "singleQuote": true, 57 | "trailingComma": "all", 58 | "arrowParens": "always" 59 | }, 60 | "ava": { 61 | "extensions": [ 62 | "ts" 63 | ], 64 | "require": [ 65 | "@swc-node/register" 66 | ], 67 | "files": [ 68 | "**/*.spec.ts" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /node/index.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | 4 | /* auto-generated by NAPI-RS */ 5 | 6 | export class ExternalObject { 7 | readonly '': { 8 | readonly '': unique symbol 9 | [K: symbol]: T 10 | } 11 | } 12 | /** Only for .d.ts type generation */ 13 | export interface DecodedMap { 14 | file?: string | undefined | null 15 | sources: Array 16 | sourceRoot?: string | undefined | null 17 | sourcesContent: Array 18 | names: Array 19 | mappings: Array>> 20 | } 21 | /** Only for .d.ts generation */ 22 | export interface GenerateDecodedMapOptions { 23 | file?: string | undefined | null 24 | sourceRoot?: string | undefined | null 25 | source?: string | undefined | null 26 | includeContent: boolean 27 | hires: boolean 28 | } 29 | /** Only for .d.ts generation */ 30 | export interface OverwriteOptions { 31 | contentOnly: boolean 32 | } 33 | export class MagicString { 34 | constructor(originalStr: string) 35 | append(input: string): this 36 | prepend(input: string): this 37 | appendLeft(index: number, input: string): this 38 | appendRight(index: number, input: string): this 39 | prependLeft(index: number, input: string): this 40 | prependRight(index: number, input: string): this 41 | overwrite( 42 | start: number, 43 | end: number, 44 | content: string, 45 | options?: OverwriteOptions, 46 | ): this 47 | trim(pattern?: string | undefined | null): this 48 | trimStart(pattern?: string | undefined | null): this 49 | trimEnd(pattern?: string | undefined | null): this 50 | trimLines(): this 51 | remove(start: number, end: number): this 52 | move(start: number, end: number, index: number): this 53 | isEmpty(): boolean 54 | generateMap(options?: Partial): { 55 | toString: () => string 56 | toUrl: () => string 57 | toMap: () => { 58 | version: number 59 | file?: string 60 | sources: string[] 61 | sourcesContent: string[] 62 | names: string[] 63 | mappings: string 64 | sourceRoot?: string 65 | } 66 | } 67 | generateDecodedMap(options?: Partial): DecodedMap 68 | toString(): string 69 | length(): number 70 | } 71 | -------------------------------------------------------------------------------- /core/tests/trim.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod trim { 3 | use magic_string::{MagicString, OverwriteOptions, Result}; 4 | 5 | #[test] 6 | fn should_trim_original_content() -> Result { 7 | assert_eq!( 8 | MagicString::new(" abcdefghijkl ") 9 | .trim(None)? 10 | .to_string(), 11 | "abcdefghijkl" 12 | ); 13 | 14 | assert_eq!( 15 | MagicString::new(" abcdefghijkl").trim(None)?.to_string(), 16 | "abcdefghijkl" 17 | ); 18 | 19 | assert_eq!( 20 | MagicString::new("abcdefghijkl ").trim(None)?.to_string(), 21 | "abcdefghijkl" 22 | ); 23 | 24 | Ok(()) 25 | } 26 | 27 | #[test] 28 | fn should_trim_replaced_content() -> Result { 29 | let mut s = MagicString::new("abcdefghijkl"); 30 | 31 | s.overwrite(0, 3, " ", OverwriteOptions::default())?; 32 | s.overwrite(9, 12, " ", OverwriteOptions::default())?; 33 | 34 | s.trim(None)?; 35 | 36 | assert_eq!(s.to_string(), "defghi"); 37 | 38 | Ok(()) 39 | } 40 | 41 | #[test] 42 | fn should_trim_prepended_appended_content() -> Result { 43 | let mut s = MagicString::new(" abcdefghijkl "); 44 | 45 | s.prepend(" ")?; 46 | s.append(" ")?; 47 | s.trim(None)?; 48 | 49 | assert_eq!(s.to_string(), "abcdefghijkl"); 50 | 51 | Ok(()) 52 | } 53 | 54 | #[test] 55 | fn should_trim_empty_string() -> Result { 56 | let mut s = MagicString::new(" "); 57 | 58 | s.trim(None)?; 59 | 60 | assert_eq!(s.to_string(), ""); 61 | 62 | Ok(()) 63 | } 64 | 65 | #[test] 66 | fn should_return_self() -> Result { 67 | let mut s = MagicString::new(" "); 68 | 69 | let result = s.trim(None)?; 70 | 71 | let result_ptr = result as *mut _; 72 | let s_ptr = &s as *const _; 73 | 74 | assert_eq!(s_ptr, result_ptr); 75 | 76 | Ok(()) 77 | } 78 | 79 | #[test] 80 | fn should_support_trimming_chunks_with_intro_and_outro() -> Result { 81 | let mut s = MagicString::new(" \n"); 82 | s.append_right(4, "test")?; 83 | s.trim(None)?; 84 | 85 | assert_eq!(s.to_string(), "test"); 86 | 87 | Ok(()) 88 | } 89 | 90 | #[test] 91 | fn should_support_trimming_with_given_pattern() -> Result { 92 | let mut s = MagicString::new(" \n\n\t abc \n\t "); 93 | s.trim(Some("\\s|\t"))?; 94 | 95 | assert_eq!(s.to_string(), "abc"); 96 | 97 | Ok(()) 98 | } 99 | } 100 | 101 | #[cfg(test)] 102 | mod trim_lines { 103 | use magic_string::{MagicString, Result}; 104 | 105 | #[test] 106 | fn should_trim_original_content() -> Result { 107 | let mut s = MagicString::new("\n\n abcdefghijkl \n\n"); 108 | s.trim_lines()?; 109 | 110 | assert_eq!(s.to_string(), " abcdefghijkl "); 111 | 112 | Ok(()) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /core/src/source_map.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use crate::magic_string::DecodedMap; 4 | use crate::mapping::Mapping; 5 | use crate::result::Result; 6 | 7 | // current specification version 8 | static VERSION: u8 = 3; 9 | 10 | #[derive(Debug, Serialize, Clone)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct SourceMap { 13 | pub version: u8, 14 | pub mappings: String, 15 | pub names: Vec, 16 | pub sources: Vec>, 17 | pub sources_content: Vec>, 18 | pub file: Option, 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | #[serde(default)] 21 | pub source_root: Option, 22 | } 23 | 24 | impl SourceMap { 25 | pub fn new( 26 | mappings: &str, 27 | file: Option<&str>, 28 | names: Vec<&str>, 29 | sources_content: Vec>, 30 | source_root: Option<&str>, 31 | sources: Vec>, 32 | ) -> Self { 33 | Self { 34 | version: VERSION, 35 | mappings: String::from(mappings), 36 | file: file.map(|f| f.to_owned()), 37 | names: names.iter().map(|&n| n.to_owned()).collect::>(), 38 | sources_content: sources_content 39 | .iter() 40 | .map(|s| s.map(|s| s.to_owned())) 41 | .collect(), 42 | source_root: source_root.map(|s| s.to_owned()), 43 | sources: sources.iter().map(|s| s.map(|s| s.to_owned())).collect(), 44 | } 45 | } 46 | 47 | /// ## Create a SourceMap instance from a decoded map 48 | /// 49 | /// `DecodedMap` can be created by utilizing `generate_decoded_map`. 50 | /// 51 | /// Example: 52 | /// ``` 53 | /// use magic_string::{MagicString, GenerateDecodedMapOptions, SourceMap}; 54 | /// 55 | /// let mut s = MagicString::new("export default React"); 56 | /// s.prepend("import React from 'react'\n"); 57 | /// 58 | /// let decoded_map = s.generate_decoded_map(GenerateDecodedMapOptions { 59 | /// file: Some("index.js".to_owned()), 60 | /// source: Some("index.ts".to_owned()), 61 | /// source_root: Some("./".to_owned()), 62 | /// include_content: true, 63 | /// hires: false, 64 | /// }).expect("failed to generate decoded map"); 65 | /// 66 | /// SourceMap::new_from_decoded(decoded_map); 67 | /// ``` 68 | pub fn new_from_decoded(decoded_map: DecodedMap) -> Result { 69 | Ok(Self { 70 | version: VERSION, 71 | file: decoded_map.file, 72 | mappings: Mapping::generate_encoded_mappings(&decoded_map.mappings)?, 73 | names: decoded_map.names, 74 | source_root: decoded_map.source_root, 75 | sources_content: decoded_map.sources_content, 76 | sources: decoded_map.sources, 77 | }) 78 | } 79 | 80 | /// ## Generate SourceMap in JSON format 81 | pub fn to_string(&self) -> Result { 82 | Ok(serde_json::to_string(self)?) 83 | } 84 | 85 | /// ## Generate inline SourceMap 86 | pub fn to_url(&self) -> Result { 87 | let str = Self::to_string(self)?; 88 | 89 | Ok(format!( 90 | "data:application/json;charset=utf-8;base64,{}", 91 | base64::encode(str) 92 | )) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /core/tests/remove.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod remove { 3 | use magic_string::{MagicString, OverwriteOptions, Result}; 4 | 5 | #[test] 6 | fn should_remove_characters_from_the_original_string() -> Result { 7 | let mut s = MagicString::new("abcdefghijkl"); 8 | 9 | s.remove(1, 5)?; 10 | assert_eq!(s.to_string(), "afghijkl"); 11 | 12 | s.remove(9, 12)?; 13 | assert_eq!(s.to_string(), "afghi"); 14 | 15 | Ok(()) 16 | } 17 | 18 | #[test] 19 | fn should_remove_from_the_start() -> Result { 20 | let mut s = MagicString::new("abcdefghijkl"); 21 | 22 | s.remove(0, 6)?; 23 | assert_eq!(s.to_string(), "ghijkl"); 24 | 25 | Ok(()) 26 | } 27 | 28 | #[test] 29 | fn should_remove_from_the_end() -> Result { 30 | let mut s = MagicString::new("abcdefghijkl"); 31 | 32 | s.remove(6, 12)?; 33 | assert_eq!(s.to_string(), "abcdef"); 34 | 35 | Ok(()) 36 | } 37 | 38 | #[test] 39 | fn should_treat_zero_length_removals_as_a_no_op() -> Result { 40 | let mut s = MagicString::new("abcdefghijkl"); 41 | 42 | s.remove(0, 0)?; 43 | s.remove(6, 6)?; 44 | s.remove(9, -3)?; 45 | assert_eq!(s.to_string(), "abcdefghijkl"); 46 | 47 | Ok(()) 48 | } 49 | 50 | #[test] 51 | fn should_remove_overlapping_ranges() -> Result { 52 | let mut s = MagicString::new("abcdefghijkl"); 53 | 54 | s.remove(3, 7)?; 55 | s.remove(5, 9)?; 56 | assert_eq!(s.to_string(), "abcjkl"); 57 | 58 | let mut s = MagicString::new("abcdefghijkl"); 59 | 60 | s.remove(3, 7)?; 61 | s.remove(4, 6)?; 62 | assert_eq!(s.to_string(), "abchijkl"); 63 | 64 | Ok(()) 65 | } 66 | 67 | #[test] 68 | fn should_remove_overlapping_ranges_redux() -> Result { 69 | let mut s = MagicString::new("abccde"); 70 | 71 | s.remove(2, 3)?; // c 72 | s.remove(1, 3)?; // bc 73 | assert_eq!(s.to_string(), "acde"); 74 | 75 | Ok(()) 76 | } 77 | 78 | #[test] 79 | fn should_remove_modified_ranges() -> Result { 80 | let mut s = MagicString::new("abcdefghi"); 81 | 82 | s.overwrite(3, 6, "DEF", OverwriteOptions::default())?; 83 | s.remove(2, 7)?; // cDEFg 84 | 85 | // assert_eq!(s.slice(1, 8), "bh"); // To be implemented 86 | assert_eq!(s.to_string(), "abhi"); 87 | 88 | Ok(()) 89 | } 90 | 91 | #[test] 92 | fn should_not_remove_content_inserted_after_the_end_of_removed_range() -> Result { 93 | let mut s = MagicString::new("ab.c;"); 94 | 95 | s.prepend_right(0, "(")?; 96 | s.prepend_right(4, ")")?; 97 | s.remove(2, 4)?; 98 | 99 | assert_eq!(s.to_string(), "(ab);"); 100 | 101 | Ok(()) 102 | } 103 | 104 | #[test] 105 | fn should_remove_interior_inserts() -> Result { 106 | let mut s = MagicString::new("abc;"); 107 | 108 | s.append_left(1, "[")?; 109 | s.prepend_right(1, "(")?; 110 | s.append_left(2, ")")?; 111 | s.prepend_right(2, "]")?; 112 | 113 | s.remove(1, 2)?; 114 | 115 | assert_eq!(s.to_string(), "a[]c;"); 116 | 117 | Ok(()) 118 | } 119 | 120 | // #[test] 121 | // fn should_remove_across_moved_content() -> Result { 122 | // let mut s = MagicString::new("abcdefghijkl"); 123 | // 124 | // // to be implemented 125 | // // s.move(6, 9, 3); 126 | // s.remove(5, 7)?; 127 | // 128 | // assert.equal(s.toString(), "abchidejkl"); 129 | // 130 | // Ok(()) 131 | // } 132 | 133 | #[test] 134 | fn should_return_self() -> Result { 135 | let mut s = MagicString::new(""); 136 | let result = s.remove(0, 0)?; 137 | 138 | let result_ptr = result as *mut _; 139 | let s_ptr = &s as *const _; 140 | 141 | assert_eq!(s_ptr, result_ptr); 142 | 143 | let mut s = MagicString::new("abcdefg"); 144 | let result = s.remove(1, 2)?; 145 | 146 | let result_ptr = result as *mut _; 147 | let s_ptr = &s as *const _; 148 | 149 | assert_eq!(s_ptr, result_ptr); 150 | 151 | Ok(()) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /core/tests/overwrite.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod overwrite { 3 | use magic_string::{MagicString, OverwriteOptions, Result}; 4 | 5 | #[test] 6 | fn should_replace_characters() -> Result { 7 | let mut s = MagicString::new("abcdefghijkl"); 8 | 9 | s.overwrite(5, 8, "FGH", OverwriteOptions::default())?; 10 | assert_eq!(s.to_string(), "abcdeFGHijkl"); 11 | 12 | Ok(()) 13 | } 14 | 15 | #[test] 16 | #[should_panic] 17 | fn should_panic_if_overlapping_replacements_are_attempted() { 18 | let mut s = MagicString::new("abcdefghijkl"); 19 | 20 | assert!(s 21 | .overwrite(7, 11, "xx", OverwriteOptions::default()) 22 | .is_ok()); 23 | 24 | assert!(s 25 | .overwrite(8, 12, "yy", OverwriteOptions::default()) 26 | .is_ok()); 27 | } 28 | 29 | #[test] 30 | fn should_allow_contiguous_but_non_overlapping_replacements() -> Result { 31 | let mut s = MagicString::new("abcdefghijkl"); 32 | 33 | s.overwrite(3, 6, "DEF", OverwriteOptions::default())?; 34 | assert_eq!(s.to_string(), "abcDEFghijkl"); 35 | 36 | s.overwrite(6, 9, "GHI", OverwriteOptions::default())?; 37 | assert_eq!(s.to_string(), "abcDEFGHIjkl"); 38 | 39 | s.overwrite(0, 3, "ABC", OverwriteOptions::default())?; 40 | assert_eq!(s.to_string(), "ABCDEFGHIjkl"); 41 | 42 | s.overwrite(9, 12, "JKL", OverwriteOptions::default())?; 43 | assert_eq!(s.to_string(), "ABCDEFGHIJKL"); 44 | 45 | Ok(()) 46 | } 47 | 48 | #[test] 49 | fn replaces_zero_length_inserts_inside_overwrite() -> Result { 50 | let mut s = MagicString::new("abcdefghijkl"); 51 | 52 | s.append_left(6, "XXX")?; 53 | s.overwrite(3, 9, "DEFGHI", OverwriteOptions::default())?; 54 | 55 | assert_eq!(s.to_string(), "abcDEFGHIjkl"); 56 | 57 | Ok(()) 58 | } 59 | 60 | #[test] 61 | fn replaces_non_zero_length_inserts_inside_overwrite() -> Result { 62 | let mut s = MagicString::new("abcdefghijkl"); 63 | 64 | s.overwrite(3, 4, "XXX", OverwriteOptions::default())?; 65 | s.overwrite(3, 5, "DE", OverwriteOptions::default())?; 66 | assert_eq!(s.to_string(), "abcDEfghijkl"); 67 | 68 | s.overwrite(7, 8, "YYY", OverwriteOptions::default())?; 69 | s.overwrite(6, 8, "GH", OverwriteOptions::default())?; 70 | assert_eq!(s.to_string(), "abcDEfGHijkl"); 71 | 72 | Ok(()) 73 | } 74 | 75 | #[test] 76 | fn should_not_replace_zero_length_inserts_at_overwrite_start_location() -> Result { 77 | let mut s = MagicString::new("abcdefghijkl"); 78 | 79 | s.remove(0, 6)?; 80 | s.append_left(6, "DEF")?; 81 | s.overwrite(6, 9, "GHI", OverwriteOptions::default())?; 82 | 83 | assert_eq!(s.to_string(), "DEFGHIjkl"); 84 | 85 | Ok(()) 86 | } 87 | 88 | #[test] 89 | fn should_return_self() -> Result { 90 | let mut s = MagicString::new("abcdefghijkl"); 91 | 92 | let result = s.overwrite(3, 4, "D", OverwriteOptions::default())?; 93 | 94 | let result_ptr = result as *mut _; 95 | let s_ptr = &s as *const _; 96 | 97 | assert_eq!(s_ptr, result_ptr); 98 | 99 | Ok(()) 100 | } 101 | 102 | #[test] 103 | fn replaces_interior_inserts() -> Result { 104 | let mut s = MagicString::new("abcdefghijkl"); 105 | 106 | s.append_left(1, "&")?; 107 | s.prepend_right(1, "^")?; 108 | s.append_left(3, "!")?; 109 | s.prepend_right(3, "?")?; 110 | s.overwrite(1, 3, "...", OverwriteOptions::default())?; 111 | 112 | assert_eq!(s.to_string(), "a&...?defghijkl"); 113 | 114 | Ok(()) 115 | } 116 | 117 | #[test] 118 | fn preserves_interior_inserts_with_content_only_true() -> Result { 119 | let mut s = MagicString::new("abcdefghijkl"); 120 | 121 | s.append_left(1, "&")?; 122 | s.prepend_right(1, "^")?; 123 | s.append_left(3, "!")?; 124 | s.prepend_right(3, "?")?; 125 | s.overwrite(1, 3, "...", OverwriteOptions { content_only: true })?; 126 | 127 | assert_eq!(s.to_string(), "a&^...!?defghijkl"); 128 | 129 | Ok(()) 130 | } 131 | 132 | #[test] 133 | fn allows_later_insertions_at_the_end() -> Result { 134 | let mut s = MagicString::new("abcdefg"); 135 | 136 | s.append_left(4, "(")?; 137 | s.overwrite(2, 7, "", OverwriteOptions::default())?; 138 | s.append_left(7, "h")?; 139 | 140 | assert_eq!(s.to_string(), "abh"); 141 | 142 | Ok(()) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /core/tests/pend.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod pend { 3 | use magic_string::{MagicString, Result}; 4 | 5 | #[test] 6 | fn preserves_intended_order() -> Result { 7 | let mut s = MagicString::new("0123456789"); 8 | 9 | s.append_left(5, "A")?; 10 | s.prepend_right(5, "a")?; 11 | s.prepend_right(5, "b")?; 12 | s.append_left(5, "B")?; 13 | s.append_left(5, "C")?; 14 | s.prepend_right(5, "c")?; 15 | assert_eq!(s.to_string(), "01234ABCcba56789"); 16 | 17 | s.append_right(7, "}")?; 18 | s.append_right(7, "{")?; 19 | assert_eq!(s.to_string(), "01234ABCcba56}{789"); 20 | 21 | s.prepend_left(7, "]")?; 22 | s.prepend_left(7, "[")?; 23 | assert_eq!(s.to_string(), "01234ABCcba56[]}{789"); 24 | 25 | Ok(()) 26 | } 27 | 28 | #[test] 29 | fn preserves_intended_order_at_beginning_of_string() -> Result { 30 | let mut s = MagicString::new("x"); 31 | 32 | s.append_left(0, "1")?; 33 | s.prepend_left(0, "2")?; 34 | s.append_left(0, "3")?; 35 | s.prepend_left(0, "4")?; 36 | 37 | assert_eq!(s.to_string(), "4213x"); 38 | 39 | Ok(()) 40 | } 41 | 42 | #[test] 43 | fn preserves_intended_order_at_end_of_string() -> Result { 44 | let mut s = MagicString::new("x"); 45 | 46 | s.append_right(1, "1")?; 47 | s.prepend_right(1, "2")?; 48 | s.append_right(1, "3")?; 49 | s.prepend_right(1, "4")?; 50 | 51 | assert_eq!(s.to_string(), "x4213"); 52 | 53 | Ok(()) 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod prepend { 59 | use magic_string::{MagicString, Result}; 60 | 61 | #[test] 62 | fn should_prepend_content() -> Result { 63 | let mut s = MagicString::new("abcdefghijkl"); 64 | 65 | s.prepend("xyz")?; 66 | assert_eq!(s.to_string(), "xyzabcdefghijkl"); 67 | 68 | s.prepend("123")?; 69 | assert_eq!(s.to_string(), "123xyzabcdefghijkl"); 70 | 71 | Ok(()) 72 | } 73 | 74 | #[test] 75 | fn should_return_self() -> Result { 76 | let mut s = MagicString::new(""); 77 | let result = s.prepend("s")?; 78 | 79 | let result_ptr = result as *mut _; 80 | let s_ptr = &s as *const _; 81 | 82 | assert_eq!(s_ptr, result_ptr); 83 | 84 | Ok(()) 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod prepend_left { 90 | use magic_string::{MagicString, Result}; 91 | 92 | #[test] 93 | fn should_return_self() -> Result { 94 | let mut s = MagicString::new(""); 95 | let result = s.prepend_left(0, "s")?; 96 | 97 | let result_ptr = result as *mut _; 98 | let s_ptr = &s as *const _; 99 | 100 | assert_eq!(s_ptr, result_ptr); 101 | 102 | Ok(()) 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod prepend_right { 108 | use magic_string::{MagicString, Result}; 109 | 110 | #[test] 111 | fn should_return_self() -> Result { 112 | let mut s = MagicString::new(""); 113 | let result = s.prepend_right(0, "s")?; 114 | 115 | let result_ptr = result as *mut _; 116 | let s_ptr = &s as *const _; 117 | 118 | assert_eq!(s_ptr, result_ptr); 119 | 120 | Ok(()) 121 | } 122 | } 123 | 124 | #[cfg(test)] 125 | mod append { 126 | use magic_string::{MagicString, Result}; 127 | 128 | #[test] 129 | fn should_prepend_content() -> Result { 130 | let mut s = MagicString::new("abcdefghijkl"); 131 | 132 | s.append("xyz")?; 133 | assert_eq!(s.to_string(), "abcdefghijklxyz"); 134 | 135 | s.append("123")?; 136 | assert_eq!(s.to_string(), "abcdefghijklxyz123"); 137 | 138 | Ok(()) 139 | } 140 | 141 | #[test] 142 | fn should_return_self() -> Result { 143 | let mut s = MagicString::new(""); 144 | let result = s.prepend("s")?; 145 | 146 | let result_ptr = result as *mut _; 147 | let s_ptr = &s as *const _; 148 | 149 | assert_eq!(s_ptr, result_ptr); 150 | 151 | Ok(()) 152 | } 153 | } 154 | 155 | #[cfg(test)] 156 | mod append_left { 157 | use magic_string::{MagicString, Result}; 158 | 159 | #[test] 160 | fn should_return_self() -> Result { 161 | let mut s = MagicString::new(""); 162 | let result = s.append_left(0, "s")?; 163 | 164 | let result_ptr = result as *mut _; 165 | let s_ptr = &s as *const _; 166 | 167 | assert_eq!(s_ptr, result_ptr); 168 | 169 | Ok(()) 170 | } 171 | } 172 | 173 | #[cfg(test)] 174 | mod append_right { 175 | use magic_string::{MagicString, Result}; 176 | 177 | #[test] 178 | fn should_return_self() -> Result { 179 | let mut s = MagicString::new(""); 180 | let result = s.append_right(0, "s")?; 181 | 182 | let result_ptr = result as *mut _; 183 | let s_ptr = &s as *const _; 184 | 185 | assert_eq!(s_ptr, result_ptr); 186 | 187 | Ok(()) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /core/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub mod locator { 2 | #[allow(dead_code)] 3 | #[derive(Debug, Clone)] 4 | pub struct Locator { 5 | original_lines: Vec, 6 | line_offsets: Vec, 7 | } 8 | 9 | type Location = (u32, u32); 10 | 11 | impl Locator { 12 | pub fn new(original: &str) -> Self { 13 | let original_lines = original 14 | .split('\n') 15 | .map(|line| line.to_owned()) 16 | .collect::>(); 17 | 18 | let mut line_offsets: Vec = vec![]; 19 | 20 | let mut pos_in_original = 0; 21 | for line in original_lines.iter() { 22 | line_offsets.push(pos_in_original); 23 | pos_in_original += line.len() as u32 + 1; 24 | } 25 | 26 | Locator { 27 | original_lines, 28 | line_offsets, 29 | } 30 | } 31 | 32 | pub fn locate(&self, index: u32) -> Location { 33 | let mut i = 0; 34 | let mut j = self.line_offsets.len(); 35 | 36 | while i < j { 37 | let m = (i + j) >> 1; 38 | if index < self.line_offsets[m] { 39 | j = m; 40 | } else { 41 | i = m + 1; 42 | } 43 | } 44 | let line = (i - 1) as u32; 45 | let column = index - self.line_offsets[line as usize]; 46 | 47 | (line, column) 48 | } 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::Locator; 54 | 55 | #[test] 56 | fn test() { 57 | let locator = Locator::new("magic\nstring\nrs"); 58 | 59 | assert_eq!(locator.original_lines[0], "magic"); 60 | assert_eq!(locator.original_lines[1], "string"); 61 | assert_eq!(locator.original_lines[2], "rs"); 62 | 63 | assert_eq!(locator.line_offsets[0], 0); 64 | assert_eq!(locator.line_offsets[1], 6); 65 | assert_eq!(locator.line_offsets[2], 13); 66 | 67 | assert_eq!(locator.locate(2), (0, 2)); 68 | assert_eq!(locator.locate(8), (1, 2)); 69 | assert_eq!(locator.locate(14), (2, 1)); 70 | } 71 | } 72 | } 73 | 74 | pub mod trim { 75 | use regex::Regex; 76 | 77 | use crate::Result; 78 | 79 | pub fn trim_start_regexp<'a>(s: &'a str, reg_pat: &'a str) -> Result<&'a str> { 80 | if s.is_empty() { 81 | return Ok(s); 82 | } 83 | 84 | let matcher = Regex::new(reg_pat)?; 85 | let chars = s.chars().collect::>(); 86 | 87 | let mut pos = 0; 88 | 89 | while pos < s.len() { 90 | let c = chars.get(pos).unwrap(); 91 | if !matcher.is_match(c.to_string().as_str()) { 92 | break; 93 | } 94 | pos += 1; 95 | } 96 | 97 | Ok(&s[pos..]) 98 | } 99 | 100 | pub fn trim_end_regexp<'a>(s: &'a str, reg_pat: &'a str) -> Result<&'a str> { 101 | if s.is_empty() { 102 | return Ok(s); 103 | } 104 | 105 | let matcher = Regex::new(reg_pat)?; 106 | let chars = s.chars().collect::>(); 107 | 108 | let mut pos = (s.len() - 1) as i32; 109 | 110 | while pos >= 0 { 111 | let c = chars.get(pos as usize).unwrap(); 112 | if !matcher.is_match(c.to_string().as_str()) { 113 | break; 114 | } 115 | pos -= 1; 116 | } 117 | 118 | Ok(&s[..(pos + 1) as usize]) 119 | } 120 | 121 | #[test] 122 | fn should_trim_start() -> Result { 123 | assert_eq!(trim_start_regexp(" abc ", "\\s")?, "abc "); 124 | assert_eq!(trim_start_regexp("\t\t\tabc\t\t", "\\t")?, "abc\t\t"); 125 | assert_eq!(trim_start_regexp("\n\nabc\t\t", "\n")?, "abc\t\t"); 126 | assert_eq!(trim_start_regexp("\n\n\n", "\n")?, ""); 127 | 128 | Ok(()) 129 | } 130 | 131 | #[test] 132 | fn should_trim_end() -> Result { 133 | assert_eq!(trim_end_regexp(" abc ", "\\s")?, " abc"); 134 | assert_eq!(trim_end_regexp("\t\t\tabc\t\t", "\\t")?, "\t\t\tabc"); 135 | assert_eq!(trim_end_regexp("\t\tabc\n\n", "\n")?, "\t\tabc"); 136 | assert_eq!(trim_end_regexp("\n\n\n", "\n")?, ""); 137 | 138 | Ok(()) 139 | } 140 | 141 | #[test] 142 | fn should_not_trim_unrelated_contents() -> Result { 143 | assert_eq!(trim_start_regexp("\\s\\sabc", "\\s")?, "\\s\\sabc"); 144 | assert_eq!(trim_end_regexp("abc\\t\\t", "\\t")?, "abc\\t\\t"); 145 | 146 | Ok(()) 147 | } 148 | } 149 | 150 | use crate::{Error, MagicStringErrorType, Result}; 151 | 152 | pub fn normalize_index(s: &str, index: i64) -> Result { 153 | let len = s.len() as i64; 154 | 155 | let index = if index < 0 { index + len } else { index }; 156 | 157 | if index < 0 || index > len { 158 | return Err(Error::new_with_reason( 159 | MagicStringErrorType::MagicStringOutOfRangeError, 160 | "index out of range", 161 | )); 162 | } 163 | 164 | Ok(index as usize) 165 | } 166 | -------------------------------------------------------------------------------- /core/tests/move.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod _move { 3 | use magic_string::{MagicString, OverwriteOptions, Result}; 4 | 5 | #[test] 6 | fn should_move_from_start() -> Result { 7 | let mut s = MagicString::new("abcdefghijkl"); 8 | 9 | s._move(0, 3, 6)?; 10 | assert_eq!(s.to_string(), "defabcghijkl"); 11 | Ok(()) 12 | } 13 | 14 | #[test] 15 | fn should_move_to_start() -> Result { 16 | let mut s = MagicString::new("abcdefghijkl"); 17 | 18 | s._move(3, 6, 0)?; 19 | 20 | assert_eq!(s.to_string(), "defabcghijkl"); 21 | Ok(()) 22 | } 23 | 24 | #[test] 25 | fn should_move_from_end() -> Result { 26 | let mut s = MagicString::new("abcdefghijkl"); 27 | 28 | s._move(9, 12, 3)?; 29 | 30 | assert_eq!(s.to_string(), "abcjkldefghi"); 31 | Ok(()) 32 | } 33 | #[test] 34 | fn should_move_to_end() -> Result { 35 | let mut s = MagicString::new("abcdefghijkl"); 36 | 37 | s._move(3, 6, 12)?; 38 | 39 | assert_eq!(s.to_string(), "abcghijkldef"); 40 | Ok(()) 41 | } 42 | 43 | #[test] 44 | fn should_move_and_remove() -> Result { 45 | let mut s = MagicString::new("abcdefghijkl"); 46 | 47 | s._move(3, 6, 12)?; 48 | s._move(3, 5, 0)?; 49 | 50 | assert_eq!(s.to_string(), "deabcghijklf"); 51 | 52 | Ok(()) 53 | } 54 | 55 | #[test] 56 | fn should_move_after_insert() -> Result { 57 | let mut s = MagicString::new("abcdefghijk"); 58 | 59 | s.prepend("xyz")?; 60 | s.append("mn")?; 61 | s.prepend_left(4, "A")?; 62 | s.append_left(4, "B")?; 63 | s.prepend_right(4, "C")?; 64 | s.append_right(4, "D")?; 65 | s._move(0, 3, 6)?; 66 | assert_eq!(s.to_string(), "xyzdABCDefabcghijkmn"); 67 | Ok(()) 68 | } 69 | 70 | #[test] 71 | fn should_ignores_redundant_move() -> Result { 72 | let mut s = MagicString::new("abcdefghijkl"); 73 | s.prepend_right(9, "X")?; 74 | s._move(9, 12, 6)?; 75 | s.append_left(12, "Y")?; 76 | s._move(6, 9, 12)?; // this is redundant – [6,9] is already after [9,12] 77 | 78 | assert_eq!(s.to_string(), "abcdefXjklYghi"); 79 | 80 | Ok(()) 81 | } 82 | 83 | #[test] 84 | fn should_move_content_to_middle() -> Result { 85 | let mut s = MagicString::new("abcdefghijkl"); 86 | s._move(3, 6, 9)?; 87 | 88 | assert_eq!(s.to_string(), "abcghidefjkl"); 89 | Ok(()) 90 | } 91 | 92 | #[test] 93 | fn should_handles_multiple_moves_of_same_snippet() -> Result { 94 | let mut s = MagicString::new("abcdefghijkl"); 95 | s._move(0, 3, 6)?; 96 | assert_eq!(s.to_string(), "defabcghijkl"); 97 | 98 | s._move(0, 3, 9)?; 99 | assert_eq!(s.to_string(), "defghiabcjkl"); 100 | 101 | Ok(()) 102 | } 103 | #[test] 104 | fn should_handles_moves_of_adjacent_snippets() -> Result { 105 | let mut s = MagicString::new("abcdefghijkl"); 106 | s._move(0, 2, 6)?; 107 | assert_eq!(s.to_string(), "cdefabghijkl"); 108 | 109 | s._move(2, 4, 6)?; 110 | assert_eq!(s.to_string(), "efabcdghijkl"); 111 | 112 | Ok(()) 113 | } 114 | #[test] 115 | fn should_handles_moves_to_same_index() -> Result { 116 | let mut s = MagicString::new("abcdefghijkl"); 117 | s._move(0, 2, 6)?._move(3, 5, 6)?; 118 | assert_eq!(s.to_string(), "cfabdeghijkl"); 119 | 120 | Ok(()) 121 | } 122 | #[test] 123 | fn should_allows_edits_of_moved_content() -> Result { 124 | let mut s = MagicString::new("abcdefghijkl"); 125 | s._move(3, 6, 9)?; 126 | s.overwrite(3, 6, "DEF", OverwriteOptions::default())?; 127 | assert_eq!(s.to_string(), "abcghiDEFjkl"); 128 | 129 | let mut s = MagicString::new("abcdefghijkl"); 130 | 131 | s._move(3, 6, 9)?; 132 | s.overwrite(4, 5, "E", OverwriteOptions::default())?; 133 | assert_eq!(s.to_string(), "abcghidEfjkl"); 134 | Ok(()) 135 | } 136 | // #[test] 137 | // fn should_move_follows_inserts() -> Result { 138 | // let mut s = MagicString::new("abcdefghijkl"); 139 | // s._move(3, 6, 9)?; 140 | 141 | // assert_eq!(s.to_string(), "abcghidefjkl"); 142 | // Ok(()) 143 | // } 144 | #[test] 145 | fn should_moves_content_inserted_at_end_of_range() -> Result { 146 | let mut s = MagicString::new("abcdefghijkl"); 147 | s.append_left(6, "X")?._move(3, 6, 9)?; 148 | 149 | assert_eq!(s.to_string(), "abcghidefXjkl"); 150 | 151 | Ok(()) 152 | } 153 | #[test] 154 | fn should_returns_this() -> Result { 155 | let mut s = MagicString::new("abcdefghijkl"); 156 | 157 | let result = s._move(3, 6, 9)?; 158 | let result_ptr = result as *mut _; 159 | let s_ptr = &s as *const _; 160 | 161 | assert_eq!(s_ptr, result_ptr); 162 | Ok(()) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /benchmark/bench.ts: -------------------------------------------------------------------------------- 1 | import b from 'benny' 2 | import MagicString from 'magic-string' 3 | 4 | import { MagicString as MagicStringRust } from '../node' 5 | 6 | const BANNER = `/*! 7 | * Vue.js v2.6.14 8 | * (c) 2014-2021 Evan You 9 | * Released under the MIT License. 10 | */ 11 | ` 12 | 13 | const EXPORT_STATEMENT = `export default foo` 14 | 15 | b.suite( 16 | 'overwrite', 17 | b.add('MagicString', () => { 18 | const m = new MagicString(`export const foo = 'bar'`) 19 | m.overwrite(13, 16, "bar") 20 | }), 21 | b.add('MagicStringRust', () => { 22 | const m = new MagicStringRust(`export const foo = 'bar'`) 23 | m.overwrite(13, 16, "bar") 24 | }), 25 | b.cycle(), 26 | b.complete(), 27 | ) 28 | 29 | b.suite( 30 | 'prepend|append', 31 | b.add('MagicString', () => { 32 | const m = new MagicString(`export const foo = 'bar'`) 33 | m.prepend(BANNER) 34 | m.append(EXPORT_STATEMENT) 35 | }), 36 | b.add('MagicStringRust', () => { 37 | const m = new MagicStringRust(`export const foo = 'bar'`) 38 | m.prepend(BANNER) 39 | m.append(EXPORT_STATEMENT) 40 | }), 41 | b.cycle(), 42 | b.complete(), 43 | ) 44 | 45 | b.suite( 46 | 'add banner#toString', 47 | b.add('MagicString', () => { 48 | const m = new MagicString(`export const foo = 'bar'`) 49 | m.prepend(BANNER) 50 | m.toString() 51 | }), 52 | b.add('MagicStringRust', () => { 53 | const m = new MagicStringRust(`export const foo = 'bar'`) 54 | m.prepend(BANNER) 55 | m.toString() 56 | }), 57 | b.cycle(), 58 | b.complete(), 59 | ) 60 | 61 | b.suite( 62 | 'add banner#generateDecodedMap', 63 | b.add('MagicString', () => { 64 | const m = new MagicString(`export const foo = 'bar'`) 65 | m.prepend(BANNER) 66 | m.generateDecodedMap() 67 | }), 68 | b.add('MagicStringRust', () => { 69 | const m = new MagicStringRust(`export const foo = 'bar'`) 70 | m.prepend(BANNER) 71 | m.generateDecodedMap() 72 | }), 73 | b.cycle(), 74 | b.complete(), 75 | ) 76 | 77 | b.suite( 78 | 'add banner#generateMapHires', 79 | b.add('MagicString', () => { 80 | const m = new MagicString(`export const foo = 'bar'`) 81 | m.prepend(BANNER) 82 | m.generateMap({ 83 | hires: true 84 | }) 85 | }), 86 | b.add('MagicStringRust', () => { 87 | const m = new MagicStringRust(`export const foo = 'bar'`) 88 | m.prepend(BANNER) 89 | m.generateMap({ 90 | hires: true 91 | }).toMap() 92 | }), 93 | b.cycle(), 94 | b.complete(), 95 | ) 96 | 97 | b.suite( 98 | 'add banner#generateMap', 99 | b.add('MagicString', () => { 100 | const m = new MagicString(`export const foo = 'bar'`) 101 | m.prepend(BANNER) 102 | m.generateMap() 103 | }), 104 | b.add('MagicStringRust', () => { 105 | const m = new MagicStringRust(`export const foo = 'bar'`) 106 | m.prepend(BANNER) 107 | m.generateMap().toMap() 108 | }), 109 | b.cycle(), 110 | b.complete(), 111 | ) 112 | 113 | b.suite( 114 | 'add banner#generateMap.toString', 115 | b.add('MagicString', () => { 116 | const m = new MagicString(`export const foo = 'bar'`) 117 | m.prepend(BANNER) 118 | m.generateMap().toString() 119 | }), 120 | b.add('MagicStringRust', () => { 121 | const m = new MagicStringRust(`export const foo = 'bar'`) 122 | m.prepend(BANNER) 123 | m.generateMap().toString() 124 | }), 125 | b.cycle(), 126 | b.complete(), 127 | ) 128 | 129 | b.suite( 130 | 'add banner#generateMapHires.toString', 131 | b.add('MagicString', () => { 132 | const m = new MagicString(`export const foo = 'bar'`) 133 | m.prepend(BANNER) 134 | m.generateMap({ 135 | hires: true 136 | }).toString() 137 | }), 138 | b.add('MagicStringRust', () => { 139 | const m = new MagicStringRust(`export const foo = 'bar'`) 140 | m.prepend(BANNER) 141 | m.generateMap({ 142 | hires: true 143 | }).toString() 144 | }), 145 | b.cycle(), 146 | b.complete(), 147 | ) 148 | 149 | b.suite( 150 | 'add banner#generateMap.toUrl', 151 | b.add('MagicString', () => { 152 | const m = new MagicString(`export const foo = 'bar'`) 153 | m.prepend(BANNER) 154 | m.generateMap().toUrl() 155 | }), 156 | b.add('MagicStringRust', () => { 157 | const m = new MagicStringRust(`export const foo = 'bar'`) 158 | m.prepend(BANNER) 159 | m.generateMap().toUrl() 160 | }), 161 | b.cycle(), 162 | b.complete(), 163 | ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

magic-string-rs

4 | 5 |

6 | CI 7 | crates 8 | NPM version 9 |

10 | 11 |

12 | MagicString port for Node and modern browsers, also, for rust, of course. 13 |

14 | 15 |
16 | 17 | ## 🔧 Installation 18 | 19 | ### Rust 20 | 21 | Add it as a dependency in a Cargo project. 22 | 23 | ```toml 24 | # Cargo.toml 25 | 26 | [dependency] 27 | magic_string = "x.x.x" 28 | ``` 29 | 30 | ### Node 31 | 32 | ```bash 33 | $ npm install @napi-rs/magic-string 34 | ``` 35 | 36 | Note: Web-Assembly is currently not supported, but it's on the plan. 37 | 38 | ## Performance 39 | 40 | ### Hardware info 41 | 42 | ``` 43 | Model Name: MacBook Pro 44 | Model Identifier: MacBookPro17,1 45 | Chip: Apple M1 46 | Total Number of Cores: 8 (4 performance and 4 efficiency) 47 | Memory: 16 GB 48 | ``` 49 | 50 | ### Benchmark 51 | 52 | ``` 53 | Running "overwrite" suite... 54 | Progress: 100% 55 | 56 | MagicString: 57 | 238 584 ops/s, ±0.34% | slowest, 50.7% slower 58 | 59 | MagicStringRust: 60 | 483 950 ops/s, ±2.13% | fastest 61 | 62 | Finished 2 cases! 63 | Fastest: MagicStringRust 64 | Slowest: MagicString 65 | 66 | Running "prepend|append" suite... 67 | Progress: 100% 68 | 69 | MagicString: 70 | 290 244 ops/s, ±1.35% | slowest, 48.35% slower 71 | 72 | MagicStringRust: 73 | 561 981 ops/s, ±6.71% | fastest 74 | 75 | Finished 2 cases! 76 | Fastest: MagicStringRust 77 | Slowest: MagicString 78 | 79 | Running "add banner#toString" suite... 80 | Progress: 100% 81 | 82 | MagicString: 83 | 301 467 ops/s, ±0.29% | slowest, 37.66% slower 84 | 85 | MagicStringRust: 86 | 483 586 ops/s, ±5.50% | fastest 87 | 88 | Finished 2 cases! 89 | Fastest: MagicStringRust 90 | Slowest: MagicString 91 | 92 | Running "add banner#generateDecodedMap" suite... 93 | Progress: 100% 94 | 95 | MagicString: 96 | 233 702 ops/s, ±0.76% | fastest 97 | 98 | MagicStringRust: 99 | 229 899 ops/s, ±2.68% | slowest, 1.63% slower 100 | 101 | Finished 2 cases! 102 | Fastest: MagicString 103 | Slowest: MagicStringRust 104 | 105 | Running "add banner#generateMapHires" suite... 106 | Progress: 100% 107 | 108 | MagicString: 109 | 177 783 ops/s, ±1.84% | fastest 110 | 111 | MagicStringRust: 112 | 90 780 ops/s, ±1.00% | slowest, 48.94% slower 113 | 114 | Finished 2 cases! 115 | Fastest: MagicString 116 | Slowest: MagicStringRust 117 | 118 | Running "add banner#generateMap" suite... 119 | Progress: 100% 120 | 121 | MagicString: 122 | 227 594 ops/s, ±0.68% | slowest, 0.42% slow 123 | er 124 | 125 | MagicStringRust: 126 | 228 545 ops/s, ±0.82% | fastest 127 | 128 | Finished 2 cases! 129 | Fastest: MagicStringRust 130 | Slowest: MagicString 131 | 132 | Running "add banner#generateMap.toString" suite... 133 | Progress: 100% 134 | 135 | MagicString: 136 | 201 272 ops/s, ±0.47% | slowest, 21.86% slower 137 | 138 | MagicStringRust: 139 | 257 577 ops/s, ±2.38% | fastest 140 | 141 | Finished 2 cases! 142 | Fastest: MagicStringRust 143 | Slowest: MagicString 144 | 145 | Running "add banner#generateMapHires.toString" suite... 146 | Progress: 100% 147 | 148 | MagicString: 149 | 157 685 ops/s, ±0.18% | fastest 150 | 151 | MagicStringRust: 152 | 95 510 ops/s, ±1.00% | slowest, 39.43% slower 153 | 154 | Finished 2 cases! 155 | Fastest: MagicString 156 | Slowest: MagicStringRust 157 | 158 | Running "add banner#generateMap.toUrl" suite... 159 | Progress: 100% 160 | 161 | MagicString: 162 | 182 161 ops/s, ±0.65% | slowest, 25.04% slower 163 | 164 | MagicStringRust: 165 | 243 019 ops/s, ±0.98% | fastest 166 | 167 | Finished 2 cases! 168 | Fastest: MagicStringRust 169 | Slowest: MagicString 170 | ``` 171 | 172 | ## 📃 Documentation 173 | 174 | [doc.rs](https://docs.rs/magic_string/latest/magic_string) 175 | 176 | ## Supported APIs 177 | 178 | - [x] generateMap: Note that there is a huge overhead for rust for implementing the same API in Node, for more detail please refer to [this](./node/index.d.ts) 179 | - [x] generateDecodedMap 180 | - [x] toString 181 | - [x] prepend 182 | - [x] append 183 | - [x] prependLeft 184 | - [x] prependRight 185 | - [x] appendLeft 186 | - [x] appendRight 187 | - [x] overwrite 188 | - [x] trim 189 | - [x] trimStart 190 | - [x] trimEnd 191 | - [x] trimLines 192 | - [x] isEmpty 193 | - [x] remove 194 | - [ ] move 195 | - [ ] indent 196 | - [ ] addSourcemapLocation 197 | - [ ] clone 198 | - [ ] slice 199 | - [ ] snip 200 | 201 | ## Credits 202 | 203 | The original project [magic-string](https://github.com/Rich-Harris/magic-string) is really awesome, you should check it out and we made this project even furthur for better performance. 204 | 205 | ## License 206 | 207 | MIT 208 | -------------------------------------------------------------------------------- /core/src/result.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Formatter; 2 | use std::{ 3 | fmt, io, result, 4 | string::{self, FromUtf8Error}, 5 | }; 6 | 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub enum MagicStringErrorType { 9 | IOError, 10 | UTF8Error, 11 | 12 | JSONSerializationError, 13 | 14 | VlqUnexpectedEof, 15 | VlqInvalidBase64, 16 | VlqOverflow, 17 | 18 | RegexSyntaxError, 19 | RegexCompiledTooBig, 20 | RegexUnknownError, 21 | 22 | MagicStringOutOfRangeError, 23 | MagicStringCrossChunkError, 24 | MagicStringDoubleSplitError, 25 | MagicStringDoubleEditError, 26 | MagicStringUnknownError, 27 | 28 | Default, 29 | } 30 | 31 | pub type Result = result::Result; 32 | 33 | #[derive(Debug, PartialEq)] 34 | pub struct Error { 35 | pub error_type: MagicStringErrorType, 36 | pub reason: Option, 37 | } 38 | 39 | impl Default for Error { 40 | fn default() -> Self { 41 | Self { 42 | error_type: MagicStringErrorType::Default, 43 | reason: None, 44 | } 45 | } 46 | } 47 | 48 | impl Error { 49 | pub fn new(error_type: MagicStringErrorType) -> Self { 50 | Self { 51 | error_type, 52 | reason: None, 53 | } 54 | } 55 | 56 | pub fn new_with_reason(error_type: MagicStringErrorType, reason: &str) -> Self { 57 | Self { 58 | error_type, 59 | reason: Some(String::from(reason)), 60 | } 61 | } 62 | } 63 | 64 | impl fmt::Display for Error { 65 | #[inline] 66 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 67 | if let Some(ref reason) = self.reason { 68 | write!(f, "{:?}, {}", self.error_type, reason) 69 | } else { 70 | write!(f, "{:?}", self.error_type) 71 | } 72 | } 73 | } 74 | 75 | impl From for Error { 76 | #[inline] 77 | fn from(_: io::Error) -> Self { 78 | Error::new(MagicStringErrorType::IOError) 79 | } 80 | } 81 | 82 | impl From for Error { 83 | #[inline] 84 | fn from(err: vlq::Error) -> Self { 85 | match err { 86 | vlq::Error::UnexpectedEof => Error::new(MagicStringErrorType::VlqUnexpectedEof), 87 | vlq::Error::InvalidBase64(_) => Error::new(MagicStringErrorType::VlqInvalidBase64), 88 | vlq::Error::Overflow => Error::new(MagicStringErrorType::VlqOverflow), 89 | } 90 | } 91 | } 92 | 93 | impl From for Error { 94 | #[inline] 95 | fn from(err: regex::Error) -> Self { 96 | match err { 97 | regex::Error::Syntax(_) => Error::new(MagicStringErrorType::RegexSyntaxError), 98 | regex::Error::CompiledTooBig(_) => Error::new(MagicStringErrorType::RegexCompiledTooBig), 99 | _ => Error::new(MagicStringErrorType::RegexUnknownError), 100 | } 101 | } 102 | } 103 | 104 | impl From for Error { 105 | #[inline] 106 | fn from(_: FromUtf8Error) -> Self { 107 | Error::new(MagicStringErrorType::UTF8Error) 108 | } 109 | } 110 | 111 | impl From for Error { 112 | #[inline] 113 | fn from(_: serde_json::Error) -> Self { 114 | Error::new(MagicStringErrorType::JSONSerializationError) 115 | } 116 | } 117 | 118 | #[cfg(feature = "node-api")] 119 | impl From for napi::Error { 120 | #[inline] 121 | fn from(err: Error) -> Self { 122 | let mut reason = String::from("[magic-string] "); 123 | 124 | match err.error_type { 125 | MagicStringErrorType::IOError => { 126 | reason.push_str("IO Error"); 127 | } 128 | MagicStringErrorType::UTF8Error => { 129 | reason.push_str("UTF8 Encoding Error"); 130 | } 131 | 132 | MagicStringErrorType::JSONSerializationError => { 133 | reason.push_str("JSON Serialization Error"); 134 | } 135 | 136 | MagicStringErrorType::VlqUnexpectedEof => { 137 | reason.push_str("Vlq Unexpected Eof"); 138 | } 139 | MagicStringErrorType::VlqInvalidBase64 => { 140 | reason.push_str("Vlq Unexpected Base64"); 141 | } 142 | MagicStringErrorType::VlqOverflow => { 143 | reason.push_str("Vlq Overflow"); 144 | } 145 | 146 | MagicStringErrorType::RegexSyntaxError => { 147 | reason.push_str("Regex Syntax Error"); 148 | } 149 | MagicStringErrorType::RegexCompiledTooBig => { 150 | reason.push_str("Regex Compiled Too Big"); 151 | } 152 | MagicStringErrorType::RegexUnknownError => { 153 | reason.push_str("Regex Unknown Error"); 154 | } 155 | 156 | MagicStringErrorType::MagicStringOutOfRangeError => { 157 | reason.push_str("Magic String Out of Range Error"); 158 | } 159 | MagicStringErrorType::MagicStringCrossChunkError => { 160 | reason.push_str("Magic String Cross Chunk Error"); 161 | } 162 | MagicStringErrorType::MagicStringDoubleSplitError => { 163 | reason.push_str("Magic String Double Split Error"); 164 | } 165 | MagicStringErrorType::MagicStringUnknownError => { 166 | reason.push_str("Magic encountered an unknown error, please file an issue"); 167 | } 168 | MagicStringErrorType::MagicStringDoubleEditError => { 169 | reason.push_str("Magic String Double Edit Error"); 170 | } 171 | 172 | MagicStringErrorType::Default => { 173 | reason.push_str( 174 | "Default Error should never been thrown to the user end, please file an issue.", 175 | ); 176 | } 177 | } 178 | 179 | if let Some(r) = err.reason { 180 | reason.push_str(", "); 181 | reason.push_str(&r[..]); 182 | } 183 | 184 | napi::Error::new(napi::Status::GenericFailure, reason) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /node/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate napi; 2 | #[macro_use] 3 | extern crate napi_derive; 4 | 5 | use napi::bindgen_prelude::*; 6 | use napi::Result; 7 | 8 | // use magic_string::SourceMap; 9 | 10 | #[napi] 11 | pub struct MagicString(magic_string::MagicString); 12 | 13 | pub fn create_external(value: T) -> External { 14 | External::new(value) 15 | } 16 | 17 | #[napi] 18 | impl MagicString { 19 | #[napi(constructor)] 20 | pub fn new(original_str: String) -> Self { 21 | MagicString(magic_string::MagicString::new(original_str.as_str())) 22 | } 23 | 24 | #[napi] 25 | pub fn append(&mut self, input: String) -> Result<&Self> { 26 | self.0.append(input.as_str())?; 27 | Ok(self) 28 | } 29 | 30 | #[napi] 31 | pub fn prepend(&mut self, input: String) -> Result<&Self> { 32 | self.0.prepend(input.as_str())?; 33 | Ok(self) 34 | } 35 | 36 | #[napi] 37 | pub fn append_left(&mut self, index: u32, input: String) -> Result<&Self> { 38 | self.0.append_left(index, input.as_str())?; 39 | Ok(self) 40 | } 41 | 42 | #[napi] 43 | pub fn append_right(&mut self, index: u32, input: String) -> Result<&Self> { 44 | self.0.append_right(index, input.as_str())?; 45 | Ok(self) 46 | } 47 | 48 | #[napi] 49 | pub fn prepend_left(&mut self, index: u32, input: String) -> Result<&Self> { 50 | self.0.prepend_left(index, input.as_str())?; 51 | Ok(self) 52 | } 53 | 54 | #[napi] 55 | pub fn prepend_right(&mut self, index: u32, input: String) -> Result<&Self> { 56 | self.0.prepend_right(index, input.as_str())?; 57 | Ok(self) 58 | } 59 | 60 | #[napi(ts_args_type = r" 61 | start: number, 62 | end: number, 63 | content: string, 64 | options?: OverwriteOptions 65 | ")] 66 | pub fn overwrite( 67 | &mut self, 68 | start: i64, 69 | end: i64, 70 | content: String, 71 | options: magic_string::OverwriteOptions, 72 | ) -> Result<&Self> { 73 | self.0.overwrite(start, end, content.as_str(), options)?; 74 | Ok(self) 75 | } 76 | 77 | #[napi] 78 | pub fn trim(&mut self, pattern: Option) -> Result<&Self> { 79 | self.0.trim(pattern.as_deref())?; 80 | Ok(self) 81 | } 82 | 83 | #[napi] 84 | pub fn trim_start(&mut self, pattern: Option) -> Result<&Self> { 85 | self.0.trim_start(pattern.as_deref())?; 86 | Ok(self) 87 | } 88 | 89 | #[napi] 90 | pub fn trim_end(&mut self, pattern: Option) -> Result<&Self> { 91 | self.0.trim_end(pattern.as_deref())?; 92 | Ok(self) 93 | } 94 | 95 | #[napi] 96 | pub fn trim_lines(&mut self) -> Result<&Self> { 97 | self.0.trim_lines()?; 98 | Ok(self) 99 | } 100 | 101 | #[napi] 102 | pub fn remove(&mut self, start: i64, end: i64) -> Result<&Self> { 103 | self.0.remove(start, end)?; 104 | Ok(self) 105 | } 106 | 107 | #[napi] 108 | pub fn _move(&mut self, start: i64, end: i64, index: i64) -> Result<&Self> { 109 | self.0._move(start, end, index)?; 110 | Ok(self) 111 | } 112 | #[napi] 113 | pub fn is_empty(&self) -> Result { 114 | Ok(self.0.is_empty()) 115 | } 116 | 117 | #[napi( 118 | ts_args_type = "options?: Partial", 119 | ts_return_type = r"{ 120 | toString: () => string; 121 | toUrl: () => string; 122 | toMap: () => { 123 | version: number; 124 | file?: string; 125 | sources: string[]; 126 | sourcesContent: string[]; 127 | names: string[]; 128 | mappings: string; 129 | sourceRoot?: string; 130 | } 131 | }" 132 | )] 133 | pub fn generate_map(&self) -> Result<()> { 134 | // only for .d.ts generation 135 | Ok(()) 136 | } 137 | 138 | #[napi(skip_typescript)] 139 | pub fn to_sourcemap_string( 140 | &self, 141 | options: Option, 142 | ) -> Result { 143 | Ok( 144 | self 145 | .0 146 | .generate_map(options.unwrap_or_default())? 147 | .to_string()?, 148 | ) 149 | } 150 | 151 | #[napi(skip_typescript)] 152 | pub fn to_sourcemap_url( 153 | &self, 154 | options: Option, 155 | ) -> Result { 156 | Ok(self.0.generate_map(options.unwrap_or_default())?.to_url()?) 157 | } 158 | 159 | #[napi( 160 | ts_args_type = "options?: Partial", 161 | ts_return_type = "DecodedMap" 162 | )] 163 | pub fn generate_decoded_map( 164 | &self, 165 | options: Option, 166 | ) -> Result { 167 | let decoded = self.0.generate_decoded_map(options.unwrap_or_default())?; 168 | Ok(serde_json::to_string(&decoded)?) 169 | } 170 | 171 | #[napi] 172 | #[allow(clippy::inherent_to_string)] 173 | pub fn to_string(&self) -> String { 174 | self.0.to_string() 175 | } 176 | 177 | #[napi] 178 | pub fn length(&self) -> u32 { 179 | self.0.len() as u32 180 | } 181 | } 182 | 183 | #[napi(object)] 184 | /// Only for .d.ts type generation 185 | pub struct DecodedMap { 186 | pub file: Option, 187 | pub sources: Vec>, 188 | pub source_root: Option, 189 | pub sources_content: Vec>, 190 | pub names: Vec, 191 | pub mappings: Vec>>, 192 | } 193 | 194 | /// Only for .d.ts generation 195 | #[napi(object)] 196 | pub struct GenerateDecodedMapOptions { 197 | pub file: Option, 198 | pub source_root: Option, 199 | pub source: Option, 200 | pub include_content: bool, 201 | pub hires: bool, 202 | } 203 | /// Only for .d.ts generation 204 | #[napi(object)] 205 | pub struct OverwriteOptions { 206 | pub content_only: bool, 207 | } 208 | -------------------------------------------------------------------------------- /core/src/chunk.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use crate::utils::trim; 4 | use crate::Result; 5 | 6 | #[derive(Debug, PartialEq, Eq, Clone)] 7 | pub struct Chunk { 8 | pub start: u32, 9 | pub end: u32, 10 | pub original_str: String, 11 | 12 | pub content: String, 13 | 14 | pub intro: String, 15 | pub outro: String, 16 | 17 | pub next: Option>>, 18 | pub prev: Option>>, 19 | } 20 | 21 | impl Chunk { 22 | pub fn new(start: u32, end: u32, content: &str) -> Chunk { 23 | Chunk { 24 | start, 25 | end, 26 | original_str: String::from(content), 27 | content: String::from(content), 28 | 29 | intro: String::default(), 30 | outro: String::default(), 31 | 32 | next: None, 33 | prev: None, 34 | } 35 | } 36 | 37 | // The original `MagicString`'s naming looks a little bit weird to me 38 | // So I have to change this, please forgive me... 39 | 40 | pub fn append_outro(&mut self, content: &str) { 41 | self.outro = format!("{}{}", self.outro, content); 42 | } 43 | 44 | pub fn prepend_outro(&mut self, content: &str) { 45 | self.outro = format!("{}{}", content, self.outro); 46 | } 47 | 48 | pub fn append_intro(&mut self, content: &str) { 49 | self.intro = format!("{}{}", self.intro, content); 50 | } 51 | 52 | pub fn prepend_intro(&mut self, content: &str) { 53 | self.intro = format!("{}{}", content, self.intro); 54 | } 55 | 56 | pub fn trim_start_regexp(&mut self, pat: &str) -> Result { 57 | let trimmed_intro = trim::trim_start_regexp(self.intro.as_str(), pat)?.to_owned(); 58 | self.intro = trimmed_intro.to_owned(); 59 | if !trimmed_intro.is_empty() { 60 | return Ok(()); 61 | } 62 | 63 | let trimmed_content = trim::trim_start_regexp(self.content.as_str(), pat)?.to_owned(); 64 | self.content = trimmed_content.to_owned(); 65 | if !trimmed_content.is_empty() { 66 | return Ok(()); 67 | } 68 | 69 | let trimmed_outro = trim::trim_start_regexp(self.outro.as_str(), pat)?.to_owned(); 70 | self.outro = trimmed_outro; 71 | 72 | Ok(()) 73 | } 74 | 75 | pub fn trim_end_regexp(&mut self, pat: &str) -> Result { 76 | let trimmed_outro = trim::trim_end_regexp(self.outro.as_str(), pat)?.to_owned(); 77 | self.outro = trimmed_outro.to_owned(); 78 | 79 | if !trimmed_outro.is_empty() { 80 | return Ok(()); 81 | } 82 | 83 | let trimmed_content = trim::trim_end_regexp(self.content.as_str(), pat)?.to_owned(); 84 | self.content = trimmed_content.to_owned(); 85 | if !trimmed_content.is_empty() { 86 | return Ok(()); 87 | } 88 | 89 | let trimmed_intro = trim::trim_end_regexp(self.intro.as_str(), pat)?.to_owned(); 90 | self.intro = trimmed_intro; 91 | 92 | Ok(()) 93 | } 94 | 95 | pub fn is_content_edited(&self) -> bool { 96 | self.original_str.len() != self.content.len() || self.original_str != self.content 97 | } 98 | 99 | pub fn try_each_next(chunk: Rc>, mut f: F) -> Result 100 | where 101 | F: FnMut(Rc>) -> Result, 102 | { 103 | let mut curr = Some(chunk); 104 | while let Some(value) = curr { 105 | match f(Rc::clone(&value)) { 106 | Ok(should_yield) => { 107 | if should_yield { 108 | break; 109 | } 110 | } 111 | Err(e) => { 112 | return Err(e); 113 | } 114 | } 115 | curr = value.borrow().next.as_ref().map(Rc::clone); 116 | } 117 | Ok(()) 118 | } 119 | 120 | pub fn try_each_prev(chunk: Rc>, mut f: F) -> Result 121 | where 122 | F: FnMut(Rc>) -> Result, 123 | { 124 | let mut curr = Some(chunk); 125 | while let Some(value) = curr { 126 | match f(Rc::clone(&value)) { 127 | Ok(should_yield) => { 128 | if should_yield { 129 | break; 130 | } 131 | } 132 | Err(e) => return Err(e), 133 | } 134 | curr = value.borrow().prev.as_ref().map(Rc::clone); 135 | } 136 | Ok(()) 137 | } 138 | 139 | pub fn contains(&self, index: u32) -> bool { 140 | index >= self.start && index < self.end 141 | } 142 | 143 | pub fn split(chunk: Rc>, index: u32) -> Rc> { 144 | let mut curr_chunk = chunk.borrow_mut(); 145 | 146 | let chunk_mid = (index - curr_chunk.start) as usize; 147 | let chunk_str = curr_chunk.original_str[0..chunk_mid].to_owned(); 148 | let next_chunk_str = curr_chunk.original_str[chunk_mid..].to_owned(); 149 | 150 | let next_chunk = Rc::new(RefCell::new(Chunk::new( 151 | index, 152 | curr_chunk.end, 153 | next_chunk_str.as_str(), 154 | ))); 155 | 156 | // `outro` of the current chunk will be moved to the newly created one and we need to reset the current one 157 | next_chunk.borrow_mut().outro = curr_chunk.outro.to_owned(); 158 | curr_chunk.outro = String::default(); 159 | 160 | if curr_chunk.is_content_edited() { 161 | next_chunk.borrow_mut().content = String::default(); 162 | curr_chunk.content = String::default(); 163 | } else { 164 | curr_chunk.content = chunk_str.to_owned(); 165 | } 166 | 167 | curr_chunk.original_str = chunk_str; 168 | curr_chunk.end = index; 169 | 170 | next_chunk.borrow_mut().next = { 171 | if curr_chunk.next.is_some() { 172 | Some(Rc::clone(curr_chunk.next.as_ref().unwrap())) 173 | } else { 174 | None 175 | } 176 | }; 177 | 178 | curr_chunk.next = Some(Rc::clone(&next_chunk)); 179 | 180 | next_chunk.borrow_mut().prev = Some(Rc::clone(&chunk)); 181 | 182 | next_chunk 183 | } 184 | } 185 | 186 | impl ToString for Chunk { 187 | fn to_string(&self) -> String { 188 | format!("{}{}{}", self.intro, self.content, self.outro) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /core/tests/generate_map.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod sourcemap { 3 | use magic_string::{GenerateDecodedMapOptions, MagicString, OverwriteOptions, Result}; 4 | 5 | #[test] 6 | fn should_generate_a_sourcemap() -> Result { 7 | let mut s = MagicString::new("abcdefghijkl"); 8 | s.remove(3, 9)?; 9 | 10 | let map = s.generate_map(GenerateDecodedMapOptions { 11 | file: Some("output.md".to_owned()), 12 | source_root: Some("./".to_owned()), 13 | source: Some("input.md".to_owned()), 14 | include_content: true, 15 | hires: false, 16 | })?; 17 | 18 | assert_eq!(map.version, 3); 19 | assert_eq!(map.file, Some("output.md".to_owned())); 20 | assert_eq!(map.sources, vec![Some("input.md".to_owned())]); 21 | assert_eq!(map.source_root, Some("./".to_owned())); 22 | assert_eq!(map.sources_content, vec![Some("abcdefghijkl".to_owned())]); 23 | assert_eq!(map.mappings, "AAAA,GAAS".to_owned()); 24 | 25 | assert_eq!(map.to_string().unwrap(), "{\"version\":3,\"mappings\":\"AAAA,GAAS\",\"names\":[],\"sources\":[\"input.md\"],\"sourcesContent\":[\"abcdefghijkl\"],\"file\":\"output.md\",\"sourceRoot\":\"./\"}".to_owned()); 26 | assert_eq!(map.to_url().unwrap(), "data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQUEsR0FBUyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyJpbnB1dC5tZCJdLCJzb3VyY2VzQ29udGVudCI6WyJhYmNkZWZnaGlqa2wiXSwiZmlsZSI6Im91dHB1dC5tZCIsInNvdXJjZVJvb3QiOiIuLyJ9".to_owned()); 27 | 28 | let map = s.generate_map(GenerateDecodedMapOptions { 29 | file: Some("output.md".to_owned()), 30 | source_root: Some("./".to_owned()), 31 | source: Some("input.md".to_owned()), 32 | include_content: true, 33 | hires: true, 34 | })?; 35 | 36 | println!("{}{}", map.to_string()?, s.to_string()); 37 | assert_eq!(map.mappings, "AAAA,CAAC,CAAC,CAAO,CAAC,CAAC".to_owned()); 38 | 39 | Ok(()) 40 | } 41 | 42 | #[test] 43 | fn should_generate_a_correct_sourcemap_for_prepend_content_when_hires_equals_to_false() -> Result 44 | { 45 | let mut s = MagicString::new("x\nq"); 46 | 47 | s.prepend("y\n")?; 48 | 49 | let map = s.generate_map(GenerateDecodedMapOptions { 50 | include_content: true, 51 | ..GenerateDecodedMapOptions::default() 52 | })?; 53 | 54 | assert_eq!(map.mappings, ";AAAA;AACA"); 55 | 56 | Ok(()) 57 | } 58 | 59 | #[test] 60 | fn should_generate_a_correct_sourcemap_for_prepend_content_when_hires_equals_to_true() -> Result { 61 | let mut s = MagicString::new("x\nq"); 62 | 63 | s.prepend("y\n")?; 64 | 65 | let map = s.generate_map(GenerateDecodedMapOptions { 66 | include_content: true, 67 | hires: true, 68 | ..GenerateDecodedMapOptions::default() 69 | })?; 70 | 71 | println!("{}", s.to_string()); 72 | assert_eq!(map.mappings, ";AAAA,CAAC;AACD"); 73 | 74 | Ok(()) 75 | } 76 | 77 | #[test] 78 | fn should_correctly_map_inserted_content() -> Result { 79 | let mut s = MagicString::new("function Foo () {}"); 80 | 81 | s.overwrite(9, 12, "Bar", OverwriteOptions::default())?; 82 | 83 | let map = s.generate_map(GenerateDecodedMapOptions { 84 | include_content: true, 85 | ..GenerateDecodedMapOptions::default() 86 | })?; 87 | 88 | assert_eq!(map.to_string().unwrap(), "{\"version\":3,\"mappings\":\"AAAA,SAAS,GAAG\",\"names\":[],\"sources\":[null],\"sourcesContent\":[\"function Foo () {}\"],\"file\":null}".to_owned()); 89 | 90 | Ok(()) 91 | } 92 | 93 | #[test] 94 | fn should_correctly_map_inserted_multi_lines_content() -> Result { 95 | let mut s = MagicString::new("function Foo () {}"); 96 | 97 | s.overwrite(15, 16, "\n", OverwriteOptions::default())?; 98 | 99 | let map = s.generate_map(GenerateDecodedMapOptions { 100 | include_content: true, 101 | ..GenerateDecodedMapOptions::default() 102 | })?; 103 | 104 | assert_eq!(map.to_string().unwrap(), "{\"version\":3,\"mappings\":\"AAAA;AAAgB\",\"names\":[],\"sources\":[null],\"sourcesContent\":[\"function Foo () {}\"],\"file\":null}".to_owned()); 105 | 106 | let mut s = MagicString::new("function Foo () {}"); 107 | 108 | s.overwrite( 109 | 15, 110 | 17, 111 | " {\n console.log(\"bar\")\n", 112 | OverwriteOptions::default(), 113 | )?; 114 | 115 | let map = s.generate_map(GenerateDecodedMapOptions { 116 | include_content: true, 117 | ..GenerateDecodedMapOptions::default() 118 | })?; 119 | 120 | assert_eq!(map.to_string().unwrap(), "{\"version\":3,\"mappings\":\"AAAA,eAAe;AAAA;AAAE\",\"names\":[],\"sources\":[null],\"sourcesContent\":[\"function Foo () {}\"],\"file\":null}".to_owned()); 121 | 122 | Ok(()) 123 | } 124 | 125 | #[test] 126 | fn should_generate_one_segment_per_replacement() -> Result { 127 | let mut s = MagicString::new("var answer = 42"); 128 | 129 | s.overwrite(4, 10, "number", OverwriteOptions::default())?; 130 | 131 | let options = GenerateDecodedMapOptions { 132 | include_content: true, 133 | ..GenerateDecodedMapOptions::default() 134 | }; 135 | 136 | let map = s.generate_decoded_map(options.to_owned())?; 137 | 138 | assert_eq!(map.mappings.len(), 1); 139 | assert_eq!(map.mappings.get(0).unwrap().len(), 3); 140 | 141 | let map = s.generate_map(options)?; 142 | 143 | assert_eq!(map.mappings.split(",").collect::>().len(), 3); 144 | 145 | Ok(()) 146 | } 147 | 148 | #[test] 149 | fn skips_empty_segments_at_the_start() -> Result { 150 | let mut s = MagicString::new("abcdefghijkl"); 151 | 152 | s.remove(0, 3)?; 153 | s.remove(3, 6)?; 154 | 155 | let map = s.generate_map(GenerateDecodedMapOptions { 156 | include_content: true, 157 | ..GenerateDecodedMapOptions::default() 158 | })?; 159 | 160 | let decoded_map = s.generate_decoded_map(GenerateDecodedMapOptions { 161 | include_content: true, 162 | ..GenerateDecodedMapOptions::default() 163 | })?; 164 | 165 | assert_eq!(map.mappings, "AAAM".to_owned()); 166 | assert_eq!(decoded_map.mappings, vec![vec![vec![0, 0, 0, 6]]]); 167 | 168 | Ok(()) 169 | } 170 | 171 | #[test] 172 | fn should_correctly_generate_a_map_with_trimmed_content() -> Result { 173 | let mut s = MagicString::new("abcdefghijkl "); 174 | s.trim(None)?; 175 | 176 | let map = s.generate_map(GenerateDecodedMapOptions::default())?; 177 | 178 | assert_eq!(map.mappings, "AAAA"); 179 | 180 | let mut s = MagicString::new(" abcdefghijkl"); 181 | s.trim(None)?; 182 | 183 | let map = s.generate_map(GenerateDecodedMapOptions::default())?; 184 | 185 | // This should be "AAAC" if we want to make `trim` more precise. 186 | assert_eq!(map.mappings, "AAAA"); 187 | 188 | Ok(()) 189 | } 190 | 191 | #[test] 192 | fn should_yield_consistent_result_between_append_left_and_prepend_right() -> Result { 193 | let mut s1 = MagicString::new("abcdefghijkl"); 194 | s1.append_left(6, "X")?; 195 | let mut s2 = MagicString::new("abcdefghijkl"); 196 | s2.prepend_right(6, "X")?; 197 | 198 | assert_eq!( 199 | s1.generate_map(GenerateDecodedMapOptions::default())? 200 | .to_string(), 201 | s2.generate_map(GenerateDecodedMapOptions::default())? 202 | .to_string() 203 | ); 204 | 205 | Ok(()) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /node/binding.js: -------------------------------------------------------------------------------- 1 | const { existsSync, readFileSync } = require('fs') 2 | const { join } = require('path') 3 | 4 | const { platform, arch } = process 5 | 6 | let nativeBinding = null 7 | let localFileExisted = false 8 | let isMusl = false 9 | let loadError = null 10 | 11 | switch (platform) { 12 | case 'android': 13 | if (arch !== 'arm64') { 14 | throw new Error(`Unsupported architecture on Android ${arch}`) 15 | } 16 | localFileExisted = existsSync( 17 | join(__dirname, 'magic-string-rs.android-arm64.node'), 18 | ) 19 | try { 20 | if (localFileExisted) { 21 | nativeBinding = require('./magic-string-rs.android-arm64.node') 22 | } else { 23 | nativeBinding = require('@napi-rs/magic-string-android-arm64') 24 | } 25 | } catch (e) { 26 | loadError = e 27 | } 28 | break 29 | case 'win32': 30 | switch (arch) { 31 | case 'x64': 32 | localFileExisted = existsSync( 33 | join(__dirname, 'magic-string-rs.win32-x64-msvc.node'), 34 | ) 35 | try { 36 | if (localFileExisted) { 37 | nativeBinding = require('./magic-string-rs.win32-x64-msvc.node') 38 | } else { 39 | nativeBinding = require('@napi-rs/magic-string-win32-x64-msvc') 40 | } 41 | } catch (e) { 42 | loadError = e 43 | } 44 | break 45 | case 'ia32': 46 | localFileExisted = existsSync( 47 | join(__dirname, 'magic-string-rs.win32-ia32-msvc.node'), 48 | ) 49 | try { 50 | if (localFileExisted) { 51 | nativeBinding = require('./magic-string-rs.win32-ia32-msvc.node') 52 | } else { 53 | nativeBinding = require('@napi-rs/magic-string-win32-ia32-msvc') 54 | } 55 | } catch (e) { 56 | loadError = e 57 | } 58 | break 59 | case 'arm64': 60 | localFileExisted = existsSync( 61 | join(__dirname, 'magic-string-rs.win32-arm64-msvc.node'), 62 | ) 63 | try { 64 | if (localFileExisted) { 65 | nativeBinding = require('./magic-string-rs.win32-arm64-msvc.node') 66 | } else { 67 | nativeBinding = require('@napi-rs/magic-string-win32-arm64-msvc') 68 | } 69 | } catch (e) { 70 | loadError = e 71 | } 72 | break 73 | default: 74 | throw new Error(`Unsupported architecture on Windows: ${arch}`) 75 | } 76 | break 77 | case 'darwin': 78 | switch (arch) { 79 | case 'x64': 80 | localFileExisted = existsSync( 81 | join(__dirname, 'magic-string-rs.darwin-x64.node'), 82 | ) 83 | try { 84 | if (localFileExisted) { 85 | nativeBinding = require('./magic-string-rs.darwin-x64.node') 86 | } else { 87 | nativeBinding = require('@napi-rs/magic-string-darwin-x64') 88 | } 89 | } catch (e) { 90 | loadError = e 91 | } 92 | break 93 | case 'arm64': 94 | localFileExisted = existsSync( 95 | join(__dirname, 'magic-string-rs.darwin-arm64.node'), 96 | ) 97 | try { 98 | if (localFileExisted) { 99 | nativeBinding = require('./magic-string-rs.darwin-arm64.node') 100 | } else { 101 | nativeBinding = require('@napi-rs/magic-string-darwin-arm64') 102 | } 103 | } catch (e) { 104 | loadError = e 105 | } 106 | break 107 | default: 108 | throw new Error(`Unsupported architecture on macOS: ${arch}`) 109 | } 110 | break 111 | case 'freebsd': 112 | if (arch !== 'x64') { 113 | throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) 114 | } 115 | localFileExisted = existsSync( 116 | join(__dirname, 'magic-string-rs.freebsd-x64.node'), 117 | ) 118 | try { 119 | if (localFileExisted) { 120 | nativeBinding = require('./magic-string-rs.freebsd-x64.node') 121 | } else { 122 | nativeBinding = require('@napi-rs/magic-string-freebsd-x64') 123 | } 124 | } catch (e) { 125 | loadError = e 126 | } 127 | break 128 | case 'linux': 129 | switch (arch) { 130 | case 'x64': 131 | isMusl = readFileSync('/usr/bin/ldd', 'utf8').includes('musl') 132 | if (isMusl) { 133 | localFileExisted = existsSync( 134 | join(__dirname, 'magic-string-rs.linux-x64-musl.node'), 135 | ) 136 | try { 137 | if (localFileExisted) { 138 | nativeBinding = require('./magic-string-rs.linux-x64-musl.node') 139 | } else { 140 | nativeBinding = require('@napi-rs/magic-string-linux-x64-musl') 141 | } 142 | } catch (e) { 143 | loadError = e 144 | } 145 | } else { 146 | localFileExisted = existsSync( 147 | join(__dirname, 'magic-string-rs.linux-x64-gnu.node'), 148 | ) 149 | try { 150 | if (localFileExisted) { 151 | nativeBinding = require('./magic-string-rs.linux-x64-gnu.node') 152 | } else { 153 | nativeBinding = require('@napi-rs/magic-string-linux-x64-gnu') 154 | } 155 | } catch (e) { 156 | loadError = e 157 | } 158 | } 159 | break 160 | case 'arm64': 161 | isMusl = readFileSync('/usr/bin/ldd', 'utf8').includes('musl') 162 | if (isMusl) { 163 | localFileExisted = existsSync( 164 | join(__dirname, 'magic-string-rs.linux-arm64-musl.node'), 165 | ) 166 | try { 167 | if (localFileExisted) { 168 | nativeBinding = require('./magic-string-rs.linux-arm64-musl.node') 169 | } else { 170 | nativeBinding = require('@napi-rs/magic-string-linux-arm64-musl') 171 | } 172 | } catch (e) { 173 | loadError = e 174 | } 175 | } else { 176 | localFileExisted = existsSync( 177 | join(__dirname, 'magic-string-rs.linux-arm64-gnu.node'), 178 | ) 179 | try { 180 | if (localFileExisted) { 181 | nativeBinding = require('./magic-string-rs.linux-arm64-gnu.node') 182 | } else { 183 | nativeBinding = require('@napi-rs/magic-string-linux-arm64-gnu') 184 | } 185 | } catch (e) { 186 | loadError = e 187 | } 188 | } 189 | break 190 | case 'arm': 191 | localFileExisted = existsSync( 192 | join(__dirname, 'magic-string-rs.linux-arm-gnueabihf.node'), 193 | ) 194 | try { 195 | if (localFileExisted) { 196 | nativeBinding = require('./magic-string-rs.linux-arm-gnueabihf.node') 197 | } else { 198 | nativeBinding = require('@napi-rs/magic-string-linux-arm-gnueabihf') 199 | } 200 | } catch (e) { 201 | loadError = e 202 | } 203 | break 204 | default: 205 | throw new Error(`Unsupported architecture on Linux: ${arch}`) 206 | } 207 | break 208 | default: 209 | throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) 210 | } 211 | 212 | if (!nativeBinding) { 213 | if (loadError) { 214 | throw loadError 215 | } 216 | throw new Error(`Failed to load native binding`) 217 | } 218 | 219 | const { MagicString } = nativeBinding 220 | 221 | module.exports.MagicString = MagicString 222 | -------------------------------------------------------------------------------- /core/src/mapping.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use crate::chunk::Chunk; 4 | use crate::result::Result; 5 | 6 | static SOURCE_INDEX: u8 = 0; 7 | 8 | pub type Segment = Vec; 9 | pub type Line = Vec; 10 | pub type Mappings = Vec; 11 | 12 | #[derive(Debug)] 13 | pub struct Mapping { 14 | generated_code_line: u32, 15 | generated_code_column: u32, 16 | hires: bool, 17 | 18 | absolute_mappings: Mappings, 19 | } 20 | 21 | impl Mapping { 22 | pub fn new(hires: bool) -> Self { 23 | Self { 24 | hires, 25 | 26 | generated_code_line: 0, 27 | generated_code_column: 0, 28 | // all lines and columns are absolutely related 29 | // , which is a middle-island for us to convert it to relative mapping later (sourcemap specification) 30 | absolute_mappings: vec![], 31 | } 32 | } 33 | 34 | pub fn add_chunk( 35 | &mut self, 36 | chunk: Rc>, 37 | (original_line, original_column): (u32, u32), 38 | ) { 39 | let chunk_content_edited = chunk.borrow().is_content_edited(); 40 | self.advance(chunk.borrow().intro.as_str()); 41 | 42 | if chunk_content_edited { 43 | let content_str = chunk.borrow().content.to_owned(); 44 | let content_lines = content_str.split('\n').collect::>(); 45 | 46 | // In some edge case where `content` contains a line-break, which can be created through `overwrite`, 47 | // we must regard the content as a multi-line string. 48 | for (index, &s) in content_lines.iter().enumerate() { 49 | if !s.is_empty() { 50 | let segment: Vec = vec![ 51 | self.generated_code_column.into(), 52 | SOURCE_INDEX.into(), 53 | original_line.into(), 54 | original_column.into(), 55 | ]; 56 | 57 | if let Some(line) = self 58 | .absolute_mappings 59 | .get_mut(self.generated_code_line as usize) 60 | { 61 | line.push(segment) 62 | } else { 63 | self.absolute_mappings.push(vec![segment]) 64 | } 65 | } 66 | 67 | if index != content_lines.len() - 1 { 68 | // We are not at the ending yet, so we have to reset all stuff for new generated lines 69 | self.generated_code_line += 1; 70 | self.generated_code_column = 0; 71 | } else { 72 | // We are currently at the last piece, this is the next starting piece. 73 | // So we have to set the next starting column for later use. 74 | self.generated_code_column += s.len() as u32; 75 | } 76 | } 77 | } else { 78 | let mut original_line = original_line as i64; 79 | let mut original_column = original_column as i64; 80 | 81 | let original_str = chunk.borrow().original_str.to_owned(); 82 | let mut first = true; 83 | 84 | for char in original_str.chars() { 85 | if self.hires || first { 86 | let segment: Segment = vec![ 87 | self.generated_code_column.into(), 88 | SOURCE_INDEX.into(), 89 | original_line, 90 | original_column, 91 | ]; 92 | if let Some(line) = self 93 | .absolute_mappings 94 | .get_mut(self.generated_code_line as usize) 95 | { 96 | line.push(segment) 97 | } else { 98 | self.absolute_mappings.push(vec![segment]) 99 | } 100 | } 101 | 102 | match char { 103 | '\n' => { 104 | original_line += 1; 105 | original_column = 0; 106 | self.generated_code_line += 1; 107 | self.generated_code_column = 0; 108 | first = true; 109 | } 110 | _ => { 111 | original_column += 1; 112 | self.generated_code_column += 1; 113 | first = false; 114 | } 115 | } 116 | } 117 | } 118 | 119 | self.advance(chunk.borrow().outro.as_str()); 120 | } 121 | 122 | pub fn advance(&mut self, str: &str) { 123 | if str.is_empty() { 124 | return; 125 | } 126 | 127 | let lines = str.split('\n').collect::>(); 128 | 129 | let mut i = lines.len(); 130 | while i > 1 { 131 | self.absolute_mappings.push(Vec::default()); 132 | self.generated_code_column = 0; 133 | i -= 1; 134 | } 135 | 136 | self.generated_code_line += (lines.len() - 1) as u32; 137 | 138 | // save starting column for later use 139 | self.generated_code_column += lines.last().unwrap().len() as u32; 140 | } 141 | 142 | // absolute to relative 143 | pub fn get_decoded_mappings(&mut self) -> Mappings { 144 | let mut source_index: i64 = 0; 145 | let mut original_line: i64 = 0; 146 | let mut original_column: i64 = 0; 147 | 148 | let decoded_mappings = self 149 | .absolute_mappings 150 | .iter() 151 | .map(|line| { 152 | let mut generated_column: i64 = 0; 153 | 154 | line 155 | .iter() 156 | .map(|segment| { 157 | let generated_column_offset = segment[0] - generated_column; 158 | let source_index_offset = segment[1] - source_index; 159 | let original_line_offset = segment[2] - original_line; 160 | let original_column_offset = segment[3] - original_column; 161 | 162 | generated_column = segment[0]; 163 | source_index = segment[1]; 164 | original_line = segment[2]; 165 | original_column = segment[3]; 166 | 167 | vec![ 168 | generated_column_offset, 169 | source_index_offset, 170 | original_line_offset, 171 | original_column_offset, 172 | ] 173 | }) 174 | .collect::() 175 | }) 176 | .collect::(); 177 | 178 | decoded_mappings 179 | } 180 | 181 | // generate encoded mappings, mappings are encoded relatively 182 | #[allow(clippy::ptr_arg)] 183 | pub fn generate_encoded_mappings(decoded_mappings: &Mappings) -> Result { 184 | let mut encoded_mappings: Vec = vec![]; 185 | 186 | for line in decoded_mappings.iter() { 187 | let mut line_str: Vec = vec![]; 188 | 189 | for segment in line.iter() { 190 | let mut segment_str: Vec = vec![]; 191 | 192 | for item in segment.iter() { 193 | let mut vlq_output: Vec = vec![]; 194 | 195 | vlq::encode(item.to_owned(), &mut vlq_output)?; 196 | segment_str.push(String::from_utf8(vlq_output)?); 197 | } 198 | 199 | line_str.push(segment_str.join("")); 200 | } 201 | 202 | encoded_mappings.push(line_str.join(",")); 203 | } 204 | 205 | let encoded_mappings_str = encoded_mappings.join(";"); 206 | 207 | Ok(encoded_mappings_str) 208 | } 209 | } 210 | 211 | #[cfg(test)] 212 | mod tests { 213 | use super::Mapping; 214 | 215 | #[test] 216 | fn absolute_mapping_to_relative_mapping() { 217 | let mut mapping = Mapping::new(false); 218 | 219 | mapping 220 | .absolute_mappings 221 | .push(vec![vec![3, 1, 0, 1], vec![4, 1, 0, 1]]); 222 | mapping 223 | .absolute_mappings 224 | .push(vec![vec![5, 5, 2, 9], vec![6, 6, 3, 10]]); 225 | 226 | let decoded_mappings = mapping.get_decoded_mappings(); 227 | 228 | assert_eq!( 229 | &decoded_mappings, 230 | &vec![ 231 | vec![vec![3, 1, 0, 1], vec![1, 0, 0, 0]], 232 | vec![vec![5, 4, 2, 8], vec![1, 1, 1, 1]], 233 | ] 234 | ) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /.github/workflows/CI.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | env: 3 | DEBUG: napi:* 4 | APP_NAME: magic-string-rs 5 | MACOSX_DEPLOYMENT_TARGET: '10.13' 6 | 'on': 7 | push: 8 | branches: 9 | - main 10 | tags-ignore: 11 | - '**' 12 | paths-ignore: 13 | - '**/*.md' 14 | - LICENSE 15 | - '**/*.gitignore' 16 | - .editorconfig 17 | - docs/** 18 | pull_request: null 19 | jobs: 20 | build: 21 | if: "!contains(github.event.head_commit.message, 'skip ci')" 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | settings: 26 | - host: macos-latest 27 | target: x86_64-apple-darwin 28 | architecture: x64 29 | build: | 30 | yarn build 31 | strip -x node/*.node 32 | - host: windows-latest 33 | build: yarn build 34 | target: x86_64-pc-windows-msvc 35 | architecture: x64 36 | - host: windows-latest 37 | build: | 38 | yarn build --target i686-pc-windows-msvc 39 | yarn test 40 | target: i686-pc-windows-msvc 41 | architecture: x86 42 | - host: ubuntu-latest 43 | target: x86_64-unknown-linux-gnu 44 | architecture: x64 45 | docker: | 46 | docker pull $DOCKER_REGISTRY_URL/napi-rs/napi-rs/nodejs-rust:lts-debian 47 | docker tag $DOCKER_REGISTRY_URL/napi-rs/napi-rs/nodejs-rust:lts-debian builder 48 | build: | 49 | docker run --rm -v ~/.cargo/git:/root/.cargo/git -v ~/.cargo/registry:/root/.cargo/registry -v $(pwd):/build -w /build builder yarn build && strip node/magic-string-rs.linux-x64-gnu.node 50 | - host: ubuntu-latest 51 | target: x86_64-unknown-linux-musl 52 | architecture: x64 53 | docker: | 54 | docker pull $DOCKER_REGISTRY_URL/napi-rs/napi-rs/nodejs-rust:lts-alpine 55 | docker tag $DOCKER_REGISTRY_URL/napi-rs/napi-rs/nodejs-rust:lts-alpine builder 56 | build: docker run --rm -v ~/.cargo/git:/root/.cargo/git -v ~/.cargo/registry:/root/.cargo/registry -v $(pwd):/build -w /build builder yarn build && strip node/magic-string-rs.linux-x64-musl.node 57 | - host: macos-latest 58 | target: aarch64-apple-darwin 59 | build: | 60 | yarn build --target=aarch64-apple-darwin 61 | strip -x node/*.node 62 | - host: ubuntu-latest 63 | architecture: x64 64 | target: aarch64-unknown-linux-gnu 65 | setup: | 66 | sudo apt-get update 67 | sudo apt-get install g++-aarch64-linux-gnu gcc-aarch64-linux-gnu -y 68 | build: | 69 | yarn build --target=aarch64-unknown-linux-gnu 70 | aarch64-linux-gnu-strip node/magic-string-rs.linux-arm64-gnu.node 71 | - host: ubuntu-latest 72 | architecture: x64 73 | target: armv7-unknown-linux-gnueabihf 74 | setup: | 75 | sudo apt-get update 76 | sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y 77 | build: | 78 | yarn build --target=armv7-unknown-linux-gnueabihf 79 | arm-linux-gnueabihf-strip node/magic-string-rs.linux-arm-gnueabihf.node 80 | - host: ubuntu-latest 81 | architecture: x64 82 | target: aarch64-linux-android 83 | build: | 84 | export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" 85 | export CC="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" 86 | export CXX="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang++" 87 | export PATH="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin:${PATH}" 88 | yarn build --target aarch64-linux-android 89 | ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip node/*.node 90 | - host: ubuntu-latest 91 | architecture: x64 92 | target: armv7-linux-androideabi 93 | build: | 94 | export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang" 95 | export CC="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang" 96 | export CXX="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang++" 97 | export PATH="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin:${PATH}" 98 | yarn build --target armv7-linux-androideabi 99 | ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/arm-linux-androideabi-strip node/*.node 100 | - host: ubuntu-latest 101 | architecture: x64 102 | target: aarch64-unknown-linux-musl 103 | downloadTarget: aarch64-unknown-linux-musl 104 | docker: | 105 | docker pull ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine 106 | docker tag ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine builder 107 | build: | 108 | docker run --rm -v ~/.cargo/git:/root/.cargo/git -v ~/.cargo/registry:/root/.cargo/registry -v $(pwd):/build -w /build builder sh -c "yarn build --target=aarch64-unknown-linux-musl && /aarch64-linux-musl-cross/bin/aarch64-linux-musl-strip node/magic-string-rs.linux-arm64-musl.node" 109 | - host: windows-latest 110 | architecture: x64 111 | target: aarch64-pc-windows-msvc 112 | build: yarn build --target aarch64-pc-windows-msvc 113 | name: stable - ${{ matrix.settings.target }} - node@16 114 | runs-on: ${{ matrix.settings.host }} 115 | steps: 116 | - uses: actions/checkout@v2 117 | - name: Setup node 118 | uses: actions/setup-node@v2 119 | with: 120 | node-version: 16 121 | check-latest: true 122 | cache: yarn 123 | architecture: ${{ matrix.settings.architecture }} 124 | - name: Install 125 | uses: actions-rs/toolchain@v1 126 | with: 127 | profile: minimal 128 | override: true 129 | toolchain: stable 130 | target: ${{ matrix.settings.target }} 131 | - name: Generate Cargo.lock 132 | uses: actions-rs/cargo@v1 133 | with: 134 | command: generate-lockfile 135 | - name: Cache cargo registry 136 | uses: actions/cache@v2 137 | with: 138 | path: ~/.cargo/registry 139 | key: ${{ matrix.settings.target }}-node@16-cargo-registry-trimmed-${{ hashFiles('**/Cargo.lock') }} 140 | - name: Cache cargo index 141 | uses: actions/cache@v2 142 | with: 143 | path: ~/.cargo/git 144 | key: ${{ matrix.settings.target }}-node@16-cargo-index-trimmed-${{ hashFiles('**/Cargo.lock') }} 145 | - name: Cache NPM dependencies 146 | uses: actions/cache@v2 147 | with: 148 | path: node_modules 149 | key: npm-cache-${{ matrix.settings.target }}-node@16-${{ hashFiles('yarn.lock') }} 150 | - name: Pull latest image 151 | run: ${{ matrix.settings.docker }} 152 | env: 153 | DOCKER_REGISTRY_URL: ghcr.io 154 | if: ${{ matrix.settings.docker }} 155 | - name: Setup toolchain 156 | run: ${{ matrix.settings.setup }} 157 | if: ${{ matrix.settings.setup }} 158 | shell: bash 159 | - name: Install dependencies 160 | run: yarn install --ignore-scripts --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 161 | - name: Build 162 | run: ${{ matrix.settings.build }} 163 | shell: bash 164 | - name: Upload artifact 165 | uses: actions/upload-artifact@v2 166 | with: 167 | name: bindings-${{ matrix.settings.target }} 168 | path: node/${{ env.APP_NAME }}.*.node 169 | if-no-files-found: error 170 | build-freebsd: 171 | runs-on: macos-10.15 172 | name: Build FreeBSD 173 | steps: 174 | - uses: actions/checkout@v2 175 | - name: Build 176 | id: build 177 | uses: vmactions/freebsd-vm@v0.1.5 178 | env: 179 | DEBUG: napi:* 180 | RUSTUP_HOME: /usr/local/rustup 181 | CARGO_HOME: /usr/local/cargo 182 | RUSTUP_IO_THREADS: 1 183 | with: 184 | envs: DEBUG RUSTUP_HOME CARGO_HOME RUSTUP_IO_THREADS 185 | usesh: true 186 | mem: 3000 187 | prepare: | 188 | pkg install -y curl node14 python2 189 | curl -qL https://www.npmjs.com/install.sh | sh 190 | npm install -g yarn 191 | curl https://sh.rustup.rs -sSf --output rustup.sh 192 | sh rustup.sh -y --profile minimal --default-toolchain stable 193 | export PATH="/usr/local/cargo/bin:$PATH" 194 | echo "~~~~ rustc --version ~~~~" 195 | rustc --version 196 | echo "~~~~ node -v ~~~~" 197 | node -v 198 | echo "~~~~ yarn --version ~~~~" 199 | yarn --version 200 | run: | 201 | export PATH="/usr/local/cargo/bin:$PATH" 202 | pwd 203 | ls -lah 204 | whoami 205 | env 206 | freebsd-version 207 | yarn install --ignore-scripts --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 208 | yarn --cwd node build 209 | strip -x node/*.node 210 | yarn test 211 | rm -rf node_modules 212 | rm -rf target 213 | - name: Upload artifact 214 | uses: actions/upload-artifact@v2 215 | with: 216 | name: bindings-freebsd 217 | path: node/${{ env.APP_NAME }}.*.node 218 | if-no-files-found: error 219 | test-macOS-windows-binding: 220 | name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} 221 | needs: 222 | - build 223 | strategy: 224 | fail-fast: false 225 | matrix: 226 | settings: 227 | - host: windows-latest 228 | target: x86_64-pc-windows-msvc 229 | node: 230 | - '12' 231 | - '14' 232 | - '16' 233 | runs-on: ${{ matrix.settings.host }} 234 | steps: 235 | - uses: actions/checkout@v2 236 | - name: Setup node 237 | uses: actions/setup-node@v2 238 | with: 239 | node-version: ${{ matrix.node }} 240 | check-latest: true 241 | cache: yarn 242 | - name: Cache NPM dependencies 243 | uses: actions/cache@v2 244 | with: 245 | path: node_modules 246 | key: npm-cache-test-${{ matrix.settings.target }}-${{ matrix.node }}-${{ hashFiles('yarn.lock') }} 247 | - name: Install dependencies 248 | run: yarn install --ignore-scripts --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 249 | - name: Download artifacts 250 | uses: actions/download-artifact@v2 251 | with: 252 | name: bindings-${{ matrix.settings.target }} 253 | path: ./node 254 | - name: List packages 255 | run: ls -R . 256 | shell: bash 257 | - name: Test bindings 258 | run: yarn test 259 | test-linux-x64-gnu-binding: 260 | name: Test bindings on Linux-x64-gnu - node@${{ matrix.node }} 261 | needs: 262 | - build 263 | strategy: 264 | fail-fast: false 265 | matrix: 266 | node: 267 | - '12' 268 | - '14' 269 | - '16' 270 | runs-on: ubuntu-latest 271 | steps: 272 | - uses: actions/checkout@v2 273 | - name: Setup node 274 | uses: actions/setup-node@v2 275 | with: 276 | node-version: ${{ matrix.node }} 277 | check-latest: true 278 | cache: yarn 279 | - name: Cache NPM dependencies 280 | uses: actions/cache@v2 281 | with: 282 | path: node_modules 283 | key: npm-cache-test-linux-x64-gnu-${{ matrix.node }}-${{ hashFiles('yarn.lock') }} 284 | - name: Install dependencies 285 | run: yarn install --ignore-scripts --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 286 | - name: Download artifacts 287 | uses: actions/download-artifact@v2 288 | with: 289 | name: bindings-x86_64-unknown-linux-gnu 290 | path: ./node 291 | - name: List packages 292 | run: ls -R . 293 | shell: bash 294 | - name: Test bindings 295 | run: docker run --rm -v $(pwd):/magic-string-rs -w /magic-string-rs node:${{ matrix.node }}-slim yarn test 296 | test-linux-x64-musl-binding: 297 | name: Test bindings on x86_64-unknown-linux-musl - node@${{ matrix.node }} 298 | needs: 299 | - build 300 | strategy: 301 | fail-fast: false 302 | matrix: 303 | node: 304 | - '12' 305 | - '14' 306 | - '16' 307 | runs-on: ubuntu-latest 308 | steps: 309 | - uses: actions/checkout@v2 310 | - name: Setup node 311 | uses: actions/setup-node@v2 312 | with: 313 | node-version: ${{ matrix.node }} 314 | check-latest: true 315 | cache: yarn 316 | - name: Cache NPM dependencies 317 | uses: actions/cache@v2 318 | with: 319 | path: node_modules 320 | key: npm-cache-test-x86_64-unknown-linux-musl-${{ matrix.node }}-${{ hashFiles('yarn.lock') }} 321 | - name: Install dependencies 322 | run: yarn install --ignore-scripts --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 323 | - name: Download artifacts 324 | uses: actions/download-artifact@v2 325 | with: 326 | name: bindings-x86_64-unknown-linux-musl 327 | path: ./node 328 | - name: List packages 329 | run: ls -R . 330 | shell: bash 331 | - name: Test bindings 332 | run: docker run --rm -v $(pwd):/magic-string-rs -w /magic-string-rs node:${{ matrix.node }}-alpine yarn test 333 | test-linux-aarch64-gnu-binding: 334 | name: Test bindings on aarch64-unknown-linux-gnu - node@${{ matrix.node }} 335 | needs: 336 | - build 337 | strategy: 338 | fail-fast: false 339 | matrix: 340 | node: 341 | - '12' 342 | - '14' 343 | - '16' 344 | runs-on: ubuntu-latest 345 | steps: 346 | - run: docker run --rm --privileged multiarch/qemu-user-static:register --reset 347 | - uses: actions/checkout@v2 348 | - name: Download artifacts 349 | uses: actions/download-artifact@v2 350 | with: 351 | name: bindings-aarch64-unknown-linux-gnu 352 | path: ./node 353 | - name: List packages 354 | run: ls -R . 355 | shell: bash 356 | - name: Cache NPM dependencies 357 | uses: actions/cache@v2 358 | with: 359 | path: node_modules 360 | key: npm-cache-test-linux-aarch64-gnu-${{ matrix.node }}-${{ hashFiles('yarn.lock') }} 361 | - name: Install dependencies 362 | run: yarn install --ignore-scripts --ignore-platform --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 363 | - name: Setup and run tests 364 | uses: addnab/docker-run-action@v3 365 | with: 366 | image: ghcr.io/napi-rs/napi-rs/nodejs:aarch64-${{ matrix.node }} 367 | options: '-v ${{ github.workspace }}:/build -w /build' 368 | run: | 369 | set -e 370 | yarn test 371 | ls -la 372 | test-linux-aarch64-musl-binding: 373 | name: Test bindings on aarch64-unknown-linux-musl - node@${{ matrix.node }} 374 | needs: 375 | - build 376 | runs-on: ubuntu-latest 377 | steps: 378 | - run: docker run --rm --privileged multiarch/qemu-user-static:register --reset 379 | - uses: actions/checkout@v2 380 | - name: Download artifacts 381 | uses: actions/download-artifact@v2 382 | with: 383 | name: bindings-aarch64-unknown-linux-musl 384 | path: ./node 385 | - name: List packages 386 | run: ls -R . 387 | shell: bash 388 | - name: Cache NPM dependencies 389 | uses: actions/cache@v2 390 | with: 391 | path: node_modules 392 | key: npm-cache-test-linux-aarch64-musl-${{ matrix.node }}-${{ hashFiles('yarn.lock') }} 393 | - name: Install dependencies 394 | run: yarn install --ignore-scripts --ignore-platform --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 395 | - name: Setup and run tests 396 | uses: addnab/docker-run-action@v3 397 | with: 398 | image: multiarch/alpine:aarch64-latest-stable 399 | options: '-v ${{ github.workspace }}:/build -w /build' 400 | run: | 401 | set -e 402 | apk add nodejs npm yarn 403 | yarn test 404 | test-linux-arm-gnueabihf-binding: 405 | name: Test bindings on armv7-unknown-linux-gnueabihf - node@${{ matrix.node }} 406 | needs: 407 | - build 408 | strategy: 409 | fail-fast: false 410 | matrix: 411 | node: 412 | - '12' 413 | - '14' 414 | - '16' 415 | runs-on: ubuntu-latest 416 | steps: 417 | - run: docker run --rm --privileged multiarch/qemu-user-static:register --reset 418 | - uses: actions/checkout@v2 419 | - name: Download artifacts 420 | uses: actions/download-artifact@v2 421 | with: 422 | name: bindings-armv7-unknown-linux-gnueabihf 423 | path: ./node 424 | - name: List packages 425 | run: ls -R . 426 | shell: bash 427 | - name: Cache NPM dependencies 428 | uses: actions/cache@v2 429 | with: 430 | path: node_modules 431 | key: npm-cache-test-linux-arm-gnueabihf-${{ matrix.node }}-${{ hashFiles('yarn.lock') }} 432 | - name: Install dependencies 433 | run: yarn install --ignore-scripts --ignore-platform --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 434 | - name: Setup and run tests 435 | uses: addnab/docker-run-action@v3 436 | with: 437 | image: ghcr.io/napi-rs/napi-rs/nodejs:armhf-${{ matrix.node }} 438 | options: '-v ${{ github.workspace }}:/build -w /build' 439 | run: | 440 | set -e 441 | yarn test 442 | ls -la 443 | publish: 444 | name: Publish 445 | runs-on: ubuntu-latest 446 | needs: 447 | - build-freebsd 448 | - test-macOS-windows-binding 449 | - test-linux-x64-gnu-binding 450 | - test-linux-x64-musl-binding 451 | - test-linux-aarch64-gnu-binding 452 | - test-linux-aarch64-musl-binding 453 | - test-linux-arm-gnueabihf-binding 454 | steps: 455 | - uses: actions/checkout@v2 456 | - name: Setup node 457 | uses: actions/setup-node@v2 458 | with: 459 | node-version: 16 460 | check-latest: true 461 | cache: yarn 462 | - name: Cache NPM dependencies 463 | uses: actions/cache@v2 464 | with: 465 | path: node_modules 466 | key: npm-cache-publish-ubuntu-latest-${{ hashFiles('yarn.lock') }} 467 | - name: Install dependencies 468 | run: yarn install --ignore-scripts --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 469 | - name: Download all artifacts 470 | uses: actions/download-artifact@v2 471 | with: 472 | path: node/artifacts 473 | - name: Move artifacts 474 | run: yarn --cwd node artifacts 475 | - name: List packages 476 | run: ls -R ./node/npm 477 | shell: bash 478 | - name: Publish 479 | run: | 480 | if git log -1 --pretty=%B | grep "^chore(release): v[0-9]\+\.[0-9]\+\.[0-9]\+$"; 481 | then 482 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 483 | cd node 484 | npm publish --access public 485 | elif git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+"; 486 | then 487 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 488 | cd node 489 | npm publish --tag next --access public 490 | else 491 | echo "Not a release, skipping publish" 492 | fi 493 | env: 494 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 495 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 496 | -------------------------------------------------------------------------------- /core/src/magic_string.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::HashMap, rc::Rc, string::ToString}; 2 | 3 | use crate::utils::{normalize_index, trim}; 4 | 5 | #[cfg(feature = "node-api")] 6 | use napi_derive::napi; 7 | 8 | use crate::{ 9 | chunk::Chunk, 10 | mapping::{Mapping, Mappings}, 11 | result::Result, 12 | source_map::SourceMap, 13 | utils::locator::Locator, 14 | Error, MagicStringErrorType, 15 | }; 16 | 17 | #[cfg(feature = "node-api")] 18 | #[napi(object)] 19 | #[derive(Debug, Default, Clone)] 20 | pub struct GenerateDecodedMapOptions { 21 | pub file: Option, 22 | pub source_root: Option, 23 | pub source: Option, 24 | pub include_content: bool, 25 | pub hires: bool, 26 | } 27 | 28 | #[cfg(not(feature = "node-api"))] 29 | #[derive(Debug, Default, Clone)] 30 | pub struct GenerateDecodedMapOptions { 31 | pub file: Option, 32 | pub source_root: Option, 33 | pub source: Option, 34 | pub include_content: bool, 35 | pub hires: bool, 36 | } 37 | 38 | #[cfg(feature = "node-api")] 39 | #[napi(object)] 40 | #[derive(Debug, Default, Clone)] 41 | pub struct OverwriteOptions { 42 | pub content_only: bool, 43 | } 44 | 45 | #[cfg(not(feature = "node-api"))] 46 | #[derive(Debug, Default, Clone)] 47 | pub struct OverwriteOptions { 48 | pub content_only: bool, 49 | } 50 | 51 | #[derive(Debug, Serialize)] 52 | pub struct DecodedMap { 53 | pub file: Option, 54 | pub sources: Vec>, 55 | pub source_root: Option, 56 | pub sources_content: Vec>, 57 | pub names: Vec, 58 | pub mappings: Mappings, 59 | } 60 | 61 | #[derive(Debug, Clone)] 62 | pub struct MagicString { 63 | original_str: String, 64 | original_str_locator: Locator, 65 | 66 | intro: String, 67 | outro: String, 68 | 69 | chunk_by_start: HashMap>>, 70 | chunk_by_end: HashMap>>, 71 | 72 | last_searched_chunk: Rc>, 73 | first_chunk: Rc>, 74 | last_chunk: Rc>, 75 | } 76 | 77 | impl MagicString { 78 | /// ## Create a new `MagicString` instance 79 | /// 80 | /// Example: 81 | /// ``` 82 | /// use magic_string::MagicString; 83 | /// 84 | /// let mut s = MagicString::new("import React from 'react'"); 85 | /// 86 | /// assert_eq!(s.to_string(), "import React from 'react'"); 87 | /// ``` 88 | /// 89 | /// 90 | pub fn new(str: &str) -> MagicString { 91 | let original_chunk = Rc::new(RefCell::new(Chunk::new(0u32, str.len() as u32, str))); 92 | 93 | MagicString { 94 | original_str: String::from(str), 95 | 96 | intro: String::default(), 97 | outro: String::default(), 98 | 99 | chunk_by_start: HashMap::default(), 100 | chunk_by_end: HashMap::default(), 101 | 102 | first_chunk: Rc::clone(&original_chunk), 103 | last_chunk: Rc::clone(&original_chunk), 104 | last_searched_chunk: Rc::clone(&original_chunk), 105 | 106 | original_str_locator: Locator::new(str), 107 | } 108 | } 109 | 110 | /// ## Append `string` 111 | /// 112 | /// Appends the specified content to the end of the string. Returns `self`. 113 | /// 114 | /// Example: 115 | /// ``` 116 | /// use magic_string::MagicString; 117 | /// 118 | /// let mut s = MagicString::new("import React from 'react'"); 119 | /// 120 | /// s.append("\nexport default React"); 121 | /// 122 | /// assert_eq!(s.to_string(), "import React from 'react'\nexport default React"); 123 | /// 124 | /// ``` 125 | pub fn append(&mut self, str: &str) -> Result<&mut Self> { 126 | self.outro = format!("{}{}", self.outro, str); 127 | 128 | Ok(self) 129 | } 130 | 131 | /// ## Prepend `string` 132 | /// 133 | /// Prepends the string with the specified content. Returns `self`. 134 | /// 135 | /// Example: 136 | /// ``` 137 | /// use magic_string::MagicString; 138 | /// 139 | /// let mut s = MagicString::new("export default React"); 140 | /// 141 | /// s.prepend("import React from 'react'\n"); 142 | /// 143 | /// assert_eq!(s.to_string(), "import React from 'react'\nexport default React"); 144 | /// 145 | /// ``` 146 | pub fn prepend(&mut self, str: &str) -> Result<&mut Self> { 147 | self.intro = format!("{}{}", str, self.intro); 148 | 149 | Ok(self) 150 | } 151 | 152 | /// ## Prepend left 153 | /// 154 | /// Same as `s.append_left(...)`, except that the inserted content will go before any previous appends or prepends at index. Returns `self`. 155 | pub fn prepend_left(&mut self, index: u32, str: &str) -> Result<&mut Self> { 156 | self._split_at_index(index)?; 157 | 158 | if let Some(chunk) = self.chunk_by_end.get(&index) { 159 | chunk.borrow_mut().prepend_outro(str); 160 | } else { 161 | self.intro = format!("{}{}", str, self.intro) 162 | }; 163 | 164 | Ok(self) 165 | } 166 | 167 | /// ## Prepend right 168 | /// 169 | /// Same as `s.append_right(...)`, except that the inserted content will go before any previous appends or prepends at index. Returns `self`. 170 | pub fn prepend_right(&mut self, index: u32, str: &str) -> Result<&mut Self> { 171 | self._split_at_index(index)?; 172 | 173 | if let Some(chunk) = self.chunk_by_start.get(&index) { 174 | chunk.borrow_mut().prepend_intro(str); 175 | } else { 176 | self.outro = format!("{}{}", str, self.outro) 177 | }; 178 | 179 | Ok(self) 180 | } 181 | 182 | /// ## Append left 183 | /// 184 | /// Appends the specified content at the index in the original string. 185 | /// If a range ending with index is subsequently moved, the insert will be moved with it. Returns this. See also `s.prepend_left(...)`. Returns `self`. 186 | pub fn append_left(&mut self, index: u32, str: &str) -> Result<&mut Self> { 187 | self._split_at_index(index)?; 188 | 189 | if let Some(chunk) = self.chunk_by_end.get(&index) { 190 | chunk.borrow_mut().append_outro(str); 191 | } else { 192 | self.intro = format!("{}{}", self.intro, str); 193 | }; 194 | 195 | Ok(self) 196 | } 197 | 198 | /// ## Append right 199 | /// 200 | /// Appends the specified content at the index in the original string. 201 | /// If a range starting with index is subsequently moved, the insert will be moved with it. Returns this. See also `s.prepend_right(...)`. Returns `self`. 202 | pub fn append_right(&mut self, index: u32, str: &str) -> Result<&mut Self> { 203 | self._split_at_index(index)?; 204 | 205 | if let Some(chunk) = self.chunk_by_start.get(&index) { 206 | chunk.borrow_mut().append_intro(str); 207 | } else { 208 | self.append(str)?; 209 | }; 210 | 211 | Ok(self) 212 | } 213 | 214 | /// ## Overwrite 215 | /// 216 | /// Replaces the characters from start to end with content. Returns `self`. 217 | /// The fourth argument is optional. 218 | /// - and a `content_only` property which determines whether only the content is overwritten, or anything that was appended/prepended to the range as well. 219 | /// 220 | /// Example: 221 | /// ``` 222 | /// use magic_string::{MagicString, OverwriteOptions}; 223 | /// 224 | /// 225 | /// let mut s = MagicString::new("abcdefg"); 226 | /// 227 | /// s.overwrite(1,4, "z", OverwriteOptions::default()); 228 | /// assert_eq!(s.to_string(), "azefg") 229 | /// 230 | /// ``` 231 | /// 232 | pub fn overwrite( 233 | &mut self, 234 | start: i64, 235 | end: i64, 236 | content: &str, 237 | options: OverwriteOptions, 238 | ) -> Result<&mut Self> { 239 | let content_only = options.content_only; 240 | let start = normalize_index(self.original_str.as_str(), start)?; 241 | let end = normalize_index(self.original_str.as_str(), end)?; 242 | 243 | let start = start as u32; 244 | let end = end as u32; 245 | 246 | if start == end { 247 | return Err(Error::new_with_reason( 248 | MagicStringErrorType::MagicStringOutOfRangeError, 249 | "Start and end should not be the same. Please consider using `append_(left|right)` or `prepend_(left|right)` instead", 250 | )); 251 | } 252 | 253 | if start > end { 254 | return Err(Error::new_with_reason( 255 | MagicStringErrorType::MagicStringOutOfRangeError, 256 | "Start must be greater than end.", 257 | )); 258 | } 259 | 260 | self._split_at_index(start)?; 261 | self._split_at_index(end)?; 262 | 263 | let start_chunk: Option>> = self.chunk_by_start.get(&start).map(Rc::clone); 264 | let end_chunk: Option>> = self.chunk_by_end.get(&end).map(Rc::clone); 265 | 266 | if let Some(start_chunk) = start_chunk { 267 | // Note: This original implementation looks a little bit weird to me. 268 | // It should check whether the latter chunks had been edited(not only for content-wise, but also for intro and outro) or not, 269 | // then we could return the Error. But for now, It's been doing just fine. 270 | if start_chunk.borrow().end < end 271 | && (start_chunk.borrow().next 272 | != self 273 | .chunk_by_start 274 | .get(&start_chunk.borrow().end) 275 | .map(Rc::clone)) 276 | { 277 | return Err(Error::new_with_reason( 278 | MagicStringErrorType::MagicStringCrossChunkError, 279 | "unable to edit overlapped chunks", 280 | )); 281 | } 282 | 283 | Chunk::try_each_next(Rc::clone(&start_chunk), |chunk| { 284 | if start_chunk == chunk { 285 | start_chunk.borrow_mut().content = content.to_owned(); 286 | if !content_only { 287 | start_chunk.borrow_mut().intro = String::default(); 288 | start_chunk.borrow_mut().outro = String::default(); 289 | } 290 | 291 | return Ok(false); 292 | } 293 | 294 | if end_chunk.is_some() 295 | && chunk.borrow().start 296 | >= (end_chunk.as_ref().map(Rc::clone).unwrap() as Rc>) 297 | .borrow() 298 | .end 299 | { 300 | return Ok(true); 301 | } 302 | 303 | chunk.borrow_mut().content = String::default(); 304 | if !content_only { 305 | chunk.borrow_mut().intro = String::default(); 306 | chunk.borrow_mut().outro = String::default(); 307 | } 308 | 309 | Ok(false) 310 | })? 311 | } 312 | 313 | Ok(self) 314 | } 315 | 316 | /// ## Trim start and end 317 | /// 318 | /// Trims content matching `pattern` (defaults to '\s', i.e. whitespace) from the start and the end. Returns `self`. 319 | /// Note that in Rust, '\t'(char) and "\\t"(string) are different types, whereas they are regarded the same pattern in Regex, which means you can pass eiter one of them to `pattern` argument. 320 | /// 321 | /// Example: 322 | /// ``` 323 | /// use magic_string::MagicString; 324 | /// 325 | /// let mut s = MagicString::new(" abc "); 326 | /// s.trim(None); 327 | /// 328 | /// assert_eq!(s.to_string(), "abc"); 329 | /// 330 | /// let mut s = MagicString::new("\t\t abc \t\t"); 331 | /// s.trim(Some("\t")); 332 | /// 333 | /// assert_eq!(s.to_string(), " abc "); 334 | /// 335 | /// let mut s = MagicString::new("\t\t abc \t\t"); 336 | /// s.trim(Some("\t|\\s")); 337 | /// 338 | /// assert_eq!(s.to_string(), "abc"); 339 | /// ``` 340 | pub fn trim(&mut self, pattern: Option<&str>) -> Result<&mut Self> { 341 | self.trim_start(pattern)?.trim_end(pattern) 342 | } 343 | 344 | /// ## Trim start 345 | /// 346 | /// Trims content matching `pattern` (defaults to '\s', i.e. whitespace) from the start. Returns `self`. 347 | /// 348 | /// Example: 349 | /// ``` 350 | /// use magic_string::MagicString; 351 | /// 352 | /// let mut s = MagicString::new(" abc"); 353 | /// s.trim_start(None); 354 | /// 355 | /// assert_eq!(s.to_string(), "abc"); 356 | /// 357 | /// let mut s = MagicString::new(" abc"); 358 | /// s.prepend(" "); 359 | /// s.trim_start(None); 360 | /// 361 | /// assert_eq!(s.to_string(), "abc"); 362 | /// 363 | /// let mut s = MagicString::new(" abc"); 364 | /// s.prepend("\t\ta"); 365 | /// s.trim_start(Some("\t")); 366 | /// 367 | /// assert_eq!(s.to_string(), "a abc"); 368 | /// ``` 369 | pub fn trim_start(&mut self, pattern: Option<&str>) -> Result<&mut Self> { 370 | let pattern = pattern.unwrap_or("\\s"); 371 | 372 | self.intro = trim::trim_start_regexp(self.intro.as_str(), pattern)?.to_owned(); 373 | 374 | if !self.intro.is_empty() { 375 | return Ok(self); 376 | } 377 | 378 | let error = Error::default(); 379 | 380 | Chunk::try_each_next(Rc::clone(&self.first_chunk), |chunk| { 381 | self.last_searched_chunk = Rc::clone(&chunk); 382 | if let Err(e) = chunk.borrow_mut().trim_start_regexp(pattern) { 383 | return Err(e); 384 | } 385 | 386 | Ok(!chunk.borrow().to_string().is_empty()) 387 | })?; 388 | 389 | if error != Error::default() { 390 | return Err(error); 391 | } 392 | 393 | if self.last_searched_chunk == self.last_chunk 394 | && self.last_chunk.borrow().content.to_string().is_empty() 395 | { 396 | self.outro = trim::trim_start_regexp(self.outro.as_str(), pattern)?.to_owned() 397 | } 398 | 399 | Ok(self) 400 | } 401 | 402 | /// ## Trim end 403 | /// 404 | /// Trims content matching `pattern` (defaults to '\s', i.e. whitespace) from the end. Returns `self`. 405 | /// 406 | /// Example: 407 | /// ``` 408 | /// use magic_string::MagicString; 409 | /// 410 | /// let mut s = MagicString::new("abc "); 411 | /// s.trim_end(None); 412 | /// 413 | /// assert_eq!(s.to_string(), "abc"); 414 | /// 415 | /// let mut s = MagicString::new("abc "); 416 | /// s.append(" "); 417 | /// s.trim_end(None); 418 | /// 419 | /// assert_eq!(s.to_string(), "abc"); 420 | /// 421 | /// let mut s = MagicString::new("abc"); 422 | /// s.append(" a\t\t"); 423 | /// s.trim_end(Some("\t")); 424 | /// 425 | /// assert_eq!(s.to_string(), "abc a"); 426 | pub fn trim_end(&mut self, pattern: Option<&str>) -> Result<&mut Self> { 427 | let pattern = pattern.unwrap_or("\\s"); 428 | 429 | self.outro = trim::trim_end_regexp(self.outro.as_str(), pattern)?.to_owned(); 430 | 431 | if !self.outro.is_empty() { 432 | return Ok(self); 433 | } 434 | 435 | Chunk::try_each_prev(Rc::clone(&self.last_chunk), |chunk| { 436 | self.last_searched_chunk = Rc::clone(&chunk); 437 | if let Err(e) = chunk.borrow_mut().trim_end_regexp(pattern) { 438 | return Err(e); 439 | } 440 | 441 | Ok(!chunk.borrow().to_string().is_empty()) 442 | })?; 443 | 444 | if self.last_searched_chunk == self.first_chunk 445 | && self.first_chunk.borrow().content.to_string().is_empty() 446 | { 447 | self.intro = trim::trim_end_regexp(self.intro.as_str(), pattern)?.to_owned() 448 | } 449 | 450 | Ok(self) 451 | } 452 | 453 | /// ## Trim lines 454 | /// 455 | /// Removes empty lines from the start and end. Returns `self`. 456 | /// 457 | /// Example: 458 | /// ``` 459 | /// use magic_string::MagicString; 460 | /// 461 | /// let mut s = MagicString::new("\n\nabc\n"); 462 | /// s.append("\n"); 463 | /// s.prepend("\n"); 464 | /// 465 | /// s.trim_lines(); 466 | /// 467 | /// assert_eq!(s.to_string(), "abc") 468 | /// ``` 469 | pub fn trim_lines(&mut self) -> Result<&mut Self> { 470 | self.trim_start(Some("\n"))?.trim_end(Some("\n")) 471 | } 472 | 473 | /// ## Remove 474 | /// 475 | /// Removes the characters from start to end (of the original string, not the generated string). 476 | /// Removing the same content twice, or making removals that partially overlap, will cause an error. Returns `self`. 477 | /// 478 | /// Example: 479 | /// ``` 480 | /// use magic_string::MagicString; 481 | /// let mut s = MagicString::new("abcdefghijkl"); 482 | /// 483 | /// s.remove(1, 5); 484 | /// assert_eq!(s.to_string(), "afghijkl"); 485 | /// 486 | /// s.remove(9, 12); 487 | /// assert_eq!(s.to_string(), "afghi"); 488 | /// 489 | /// ``` 490 | pub fn remove(&mut self, start: i64, end: i64) -> Result<&mut Self> { 491 | let start = normalize_index(self.original_str.as_str(), start)?; 492 | let end = normalize_index(self.original_str.as_str(), end)?; 493 | 494 | let start = start as u32; 495 | let end = end as u32; 496 | 497 | if start == end { 498 | // according to the original implementation, this is a noop. 499 | return Ok(self); 500 | } 501 | 502 | if start > end { 503 | return Err(Error::new_with_reason( 504 | MagicStringErrorType::MagicStringOutOfRangeError, 505 | "Start must be greater than end.", 506 | )); 507 | } 508 | 509 | self._split_at_index(start)?; 510 | self._split_at_index(end)?; 511 | 512 | let start_chunk = self.chunk_by_start.get(&start); 513 | let end_chunk = self.chunk_by_end.get(&end); 514 | 515 | if start_chunk.is_some() { 516 | Chunk::try_each_next(start_chunk.map(Rc::clone).unwrap(), |chunk| { 517 | chunk.borrow_mut().content = String::default(); 518 | chunk.borrow_mut().intro = String::default(); 519 | chunk.borrow_mut().outro = String::default(); 520 | 521 | Ok(chunk == Rc::clone(end_chunk.unwrap())) 522 | })?; 523 | } 524 | 525 | Ok(self) 526 | } 527 | 528 | /// ## Is empty 529 | /// 530 | /// Returns `true` if the resulting source is empty (disregarding white space). 531 | /// 532 | /// Example: 533 | /// ``` 534 | /// use magic_string::MagicString; 535 | /// 536 | /// let mut s = MagicString::new(""); 537 | /// 538 | /// assert_eq!(s.is_empty(), true); 539 | /// 540 | /// let mut s = MagicString::new("abc"); 541 | /// 542 | /// assert_eq!(s.is_empty(), false); 543 | /// ``` 544 | pub fn is_empty(&self) -> bool { 545 | self.to_string().trim().is_empty() 546 | } 547 | 548 | /// ## Length 549 | /// 550 | /// Returns the length of the modified string. 551 | pub fn len(&self) -> usize { 552 | self.to_string().len() 553 | } 554 | 555 | /// ## Generate decoded map 556 | /// 557 | /// Generates a sourcemap object with raw mappings in array form, rather than encoded as a string. 558 | /// See generate_map documentation below for options details. 559 | /// Useful if you need to manipulate the sourcemap further, but most of the time you will use generateMap instead. 560 | /// 561 | /// Notice: All decoded mappings are positioned absolutely. 562 | /// 563 | /// Example 564 | /// ``` 565 | /// use magic_string::{MagicString, GenerateDecodedMapOptions}; 566 | /// 567 | /// let mut s = MagicString::new("export default React"); 568 | /// s.prepend("import React from 'react'\n"); 569 | /// 570 | /// s.generate_decoded_map(GenerateDecodedMapOptions { 571 | /// file: Some("index.js".to_owned()), 572 | /// source: Some("index.ts".to_owned()), 573 | /// source_root: Some("./".to_owned()), 574 | /// include_content: true, 575 | /// hires: false, 576 | /// }); 577 | /// ``` 578 | pub fn generate_decoded_map(&self, options: GenerateDecodedMapOptions) -> Result { 579 | let mut map = Mapping::new(options.hires); 580 | let locator = &self.original_str_locator; 581 | 582 | map.advance(self.intro.as_str()); 583 | 584 | Chunk::try_each_next(Rc::clone(&self.first_chunk), |chunk| { 585 | let loc = locator.locate(chunk.borrow().start); 586 | map.add_chunk(Rc::clone(&chunk), loc); 587 | Ok(false) 588 | })?; 589 | 590 | map.advance(self.outro.as_str()); 591 | 592 | Ok(DecodedMap { 593 | file: options.file.to_owned(), 594 | mappings: map.get_decoded_mappings(), 595 | source_root: options.source_root.to_owned(), 596 | sources: vec![options.source], 597 | names: Vec::default(), 598 | sources_content: { 599 | if options.include_content { 600 | vec![Some(self.original_str.to_owned())] 601 | } else { 602 | Default::default() 603 | } 604 | }, 605 | }) 606 | } 607 | 608 | /// ## Generate Map 609 | /// 610 | /// Generates a version 3 sourcemap. All options are optional, see `GenerateDecodedMapOptions` for detailed document. 611 | /// 612 | /// ``` 613 | /// use magic_string::{MagicString, GenerateDecodedMapOptions}; 614 | /// 615 | /// let mut s = MagicString::new("export default React"); 616 | /// s.prepend("import React from 'react'\n"); 617 | /// 618 | /// let generated_map = s.generate_map(GenerateDecodedMapOptions { 619 | /// file: Some("index.js".to_owned()), 620 | /// source: Some("index.ts".to_owned()), 621 | /// source_root: Some("./".to_owned()), 622 | /// include_content: true, 623 | /// hires: true, 624 | /// }).expect("fail to generate map"); 625 | /// 626 | /// generated_map.to_string(); // generates v3 sourcemap in JSON format 627 | /// generated_map.to_url(); // generates v3 inline sourcemap 628 | /// ``` 629 | pub fn generate_map(&self, options: GenerateDecodedMapOptions) -> Result { 630 | let decoded_map = self.generate_decoded_map(options)?; 631 | SourceMap::new_from_decoded(decoded_map) 632 | } 633 | 634 | /// ## Move 635 | /// Moves the string between start and end to the specified position.Return `self`. 636 | /// 637 | /// Example: 638 | /// ``` 639 | /// use magic_string::MagicString; 640 | /// let mut s = MagicString::new("abcdefghijkl"); 641 | /// s._move(3, 6, 9); 642 | /// assert_eq!(s.to_string(), "abcghidefjkl"); 643 | /// 644 | /// ``` 645 | /// 646 | pub fn _move(&mut self, start: i64, end: i64, index: i64) -> Result<&mut Self> { 647 | let start = normalize_index(self.original_str.as_str(), start)?; 648 | let end = normalize_index(self.original_str.as_str(), end)?; 649 | let index = normalize_index(self.original_str.as_str(), index)?; 650 | 651 | let start = start as u32; 652 | let end = end as u32; 653 | let index = index as u32; 654 | 655 | if index >= start && index <= end { 656 | return Err(Error::new_with_reason( 657 | MagicStringErrorType::MagicStringUnknownError, 658 | "Cannot move a selection inside itself", 659 | )); 660 | } 661 | 662 | if start > end { 663 | return Err(Error::new_with_reason( 664 | MagicStringErrorType::MagicStringOutOfRangeError, 665 | "Start must be greater than end.", 666 | )); 667 | } 668 | 669 | self._split_at_index(start)?; 670 | self._split_at_index(end)?; 671 | self._split_at_index(index)?; 672 | 673 | let first = self.chunk_by_start.get(&start).map(Rc::clone).unwrap(); 674 | let last = self.chunk_by_end.get(&end).map(Rc::clone).unwrap(); 675 | 676 | let old_left = first.borrow().clone().prev; 677 | let old_right = last.borrow().clone().next; 678 | 679 | let new_right = self.chunk_by_start.get(&index).map(Rc::clone); 680 | let new_left = match new_right.clone() { 681 | Some(l) => Rc::clone(&l).borrow().clone().prev, 682 | None => Some(Rc::clone(&self.last_chunk)), 683 | }; 684 | let clone_old_left = old_left.clone(); 685 | 686 | match old_left { 687 | Some(old_left) => { 688 | old_left.borrow_mut().next = old_right.clone(); 689 | } 690 | None => self.first_chunk = old_right.clone().unwrap(), 691 | } 692 | 693 | match old_right { 694 | Some(old_right) => old_right.borrow_mut().prev = clone_old_left, 695 | None => self.last_chunk = clone_old_left.unwrap(), 696 | } 697 | 698 | match new_left { 699 | Some(new_left) => { 700 | new_left.borrow_mut().next = Some(Rc::clone(&first)); 701 | first.borrow_mut().prev = Some(new_left); 702 | } 703 | None => { 704 | let first = Rc::clone(&first); 705 | self.first_chunk.borrow_mut().prev = Some(Rc::clone(&first)); 706 | last.borrow_mut().next = Some(Rc::clone(&self.first_chunk)); 707 | self.first_chunk = first; 708 | } 709 | } 710 | 711 | match new_right { 712 | Some(new_right) => { 713 | new_right.borrow_mut().prev = Some(Rc::clone(&last)); 714 | last.borrow_mut().next = Some(new_right); 715 | } 716 | None => { 717 | let last = Rc::clone(&last); 718 | self.last_chunk.borrow_mut().next = Some(Rc::clone(&last)); 719 | first.borrow_mut().prev = Some(Rc::clone(&self.last_chunk)); 720 | last.borrow_mut().next = None; 721 | self.last_chunk = last; 722 | } 723 | } 724 | 725 | Ok(self) 726 | } 727 | 728 | fn _split_at_index(&mut self, index: u32) -> Result { 729 | if self.chunk_by_end.contains_key(&index) || self.chunk_by_start.contains_key(&index) { 730 | // early bail-out if it's already split 731 | return Ok(()); 732 | } 733 | 734 | let chunk = Rc::clone(&self.last_searched_chunk); 735 | 736 | let search_forward = index > chunk.borrow().start; 737 | 738 | let mut curr = Some(chunk); 739 | while let Some(c) = curr { 740 | if c.borrow().contains(index) { 741 | self._split_chunk_at_index(c, index)?; 742 | return Ok(()); 743 | } else { 744 | curr = { 745 | if search_forward { 746 | self.chunk_by_start.get(&c.borrow().end).map(Rc::clone) 747 | } else { 748 | self.chunk_by_end.get(&c.borrow().start).map(Rc::clone) 749 | } 750 | } 751 | } 752 | } 753 | 754 | Ok(()) 755 | } 756 | 757 | fn _split_chunk_at_index(&mut self, chunk: Rc>, index: u32) -> Result { 758 | // Zero-length edited chunks can be split into different chunks, cause split chunks are the same. 759 | if chunk.borrow().is_content_edited() && !chunk.borrow().content.is_empty() { 760 | return Err(Error::new( 761 | MagicStringErrorType::MagicStringDoubleSplitError, 762 | )); 763 | } 764 | let next_chunk = chunk.borrow().clone().next; 765 | let new_chunk = Chunk::split(Rc::clone(&chunk), index); 766 | 767 | if let Some(next_chunk) = next_chunk { 768 | next_chunk.borrow_mut().prev = Some(Rc::clone(&new_chunk)); 769 | } 770 | 771 | let new_chunk_original = new_chunk.borrow(); 772 | self.chunk_by_end.insert(index, Rc::clone(&chunk)); 773 | 774 | self.chunk_by_start.insert(index, Rc::clone(&new_chunk)); 775 | self 776 | .chunk_by_end 777 | .insert(new_chunk_original.end, Rc::clone(&new_chunk)); 778 | 779 | if self.last_chunk == chunk { 780 | self.last_chunk = Rc::clone(&new_chunk); 781 | } 782 | 783 | self.last_searched_chunk = Rc::clone(&chunk); 784 | 785 | Ok(()) 786 | } 787 | } 788 | 789 | impl ToString for MagicString { 790 | /// ## To string 791 | /// 792 | /// Returns a modified string. 793 | /// 794 | /// Example: 795 | /// ``` 796 | /// use magic_string::MagicString; 797 | /// 798 | /// let mut s = MagicString::new("abc"); 799 | /// 800 | /// assert_eq!(s.to_string(), "abc"); 801 | /// ``` 802 | fn to_string(&self) -> String { 803 | let mut str = self.intro.to_owned(); 804 | 805 | Chunk::try_each_next(Rc::clone(&self.first_chunk), |chunk| { 806 | str = format!("{}{}", str, chunk.borrow().to_string()); 807 | Ok(false) 808 | }) 809 | .unwrap(); 810 | 811 | format!("{}{}", str, self.outro) 812 | } 813 | } 814 | -------------------------------------------------------------------------------- /node/tests/MagicString.spec.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { SourceMapConsumer } from 'source-map' 3 | 4 | import { MagicString } from '../' 5 | 6 | describe('MagicString', () => { 7 | // describe('options', () => { 8 | // it('stores source file information', () => { 9 | // const s = new MagicString('abc', { 10 | // filename: 'foo.js', 11 | // }) 12 | 13 | // assert.equal(s.filename, 'foo.js') 14 | // }) 15 | // }) 16 | 17 | describe('append', () => { 18 | it('should append content', () => { 19 | const s = new MagicString('abcdefghijkl') 20 | 21 | s.append('xyz') 22 | assert.equal(s.toString(), 'abcdefghijklxyz') 23 | 24 | s.append('xyz') 25 | assert.equal(s.toString(), 'abcdefghijklxyzxyz') 26 | }) 27 | 28 | it('should return this', () => { 29 | const s = new MagicString('abcdefghijkl') 30 | assert.strictEqual(s.append('xyz'), s) 31 | }) 32 | 33 | it('should throw when given non-string content', () => { 34 | const s = new MagicString('') 35 | // @ts-expect-error 36 | assert.throws(() => s.append([])) 37 | }) 38 | }) 39 | 40 | describe('(ap|pre)pend(Left|Right)', () => { 41 | it('preserves intended order', () => { 42 | const s = new MagicString('0123456789') 43 | 44 | s.appendLeft(5, 'A') 45 | s.prependRight(5, 'a') 46 | s.prependRight(5, 'b') 47 | s.appendLeft(5, 'B') 48 | s.appendLeft(5, 'C') 49 | s.prependRight(5, 'c') 50 | 51 | assert.equal(s.toString(), '01234ABCcba56789') 52 | // assert.equal(s.slice(0, 5), '01234ABC') 53 | // assert.equal(s.slice(5), 'cba56789') 54 | 55 | s.prependLeft(5, '<') 56 | s.prependLeft(5, '{') 57 | assert.equal(s.toString(), '01234{') 60 | s.appendRight(5, '}') 61 | assert.equal(s.toString(), '01234{}56789') 62 | 63 | s.appendLeft(5, '(') 64 | s.appendLeft(5, '[') 65 | assert.equal(s.toString(), '01234{}56789') 66 | 67 | s.prependRight(5, ')') 68 | s.prependRight(5, ']') 69 | assert.equal(s.toString(), '01234{}56789') 70 | 71 | // impl later 72 | // assert.equal(s.slice(0, 5), '01234{}56789') 74 | }) 75 | 76 | it('preserves intended order at beginning of string', () => { 77 | const s = new MagicString('x') 78 | 79 | s.appendLeft(0, '1') 80 | s.prependLeft(0, '2') 81 | s.appendLeft(0, '3') 82 | s.prependLeft(0, '4') 83 | 84 | assert.equal(s.toString(), '4213x') 85 | }) 86 | 87 | it('preserves intended order at end of string', () => { 88 | const s = new MagicString('x') 89 | 90 | s.appendRight(1, '1') 91 | s.prependRight(1, '2') 92 | s.appendRight(1, '3') 93 | s.prependRight(1, '4') 94 | 95 | assert.equal(s.toString(), 'x4213') 96 | }) 97 | }) 98 | 99 | describe('appendLeft', () => { 100 | it('should return this', () => { 101 | const s = new MagicString('abcdefghijkl') 102 | assert.strictEqual(s.appendLeft(0, 'a'), s) 103 | }) 104 | }) 105 | 106 | describe('appendRight', () => { 107 | it('should return this', () => { 108 | const s = new MagicString('abcdefghijkl') 109 | assert.strictEqual(s.appendRight(0, 'a'), s) 110 | }) 111 | }) 112 | 113 | // describe('clone', () => { 114 | // it('should clone a magic string', () => { 115 | // const s = new MagicString('abcdefghijkl') 116 | 117 | // s.overwrite(3, 9, 'XYZ') 118 | // const c = s.clone() 119 | 120 | // assert.notEqual(s, c) 121 | // assert.equal(c.toString(), 'abcXYZjkl') 122 | // }) 123 | 124 | // it('should clone filename info', () => { 125 | // const s = new MagicString('abcdefghijkl', { filename: 'foo.js' }) 126 | // const c = s.clone() 127 | 128 | // assert.equal(c.filename, 'foo.js') 129 | // }) 130 | 131 | // it('should clone indentExclusionRanges', () => { 132 | // const array = [3, 6] 133 | // const source = new MagicString('abcdefghijkl', { 134 | // filename: 'foo.js', 135 | // indentExclusionRanges: array, 136 | // }) 137 | 138 | // const clone = source.clone() 139 | 140 | // assert.notStrictEqual( 141 | // source.indentExclusionRanges, 142 | // clone.indentExclusionRanges, 143 | // ) 144 | // assert.deepEqual( 145 | // source.indentExclusionRanges, 146 | // clone.indentExclusionRanges, 147 | // ) 148 | // }) 149 | 150 | // it('should clone complex indentExclusionRanges', () => { 151 | // const array = [ 152 | // [3, 6], 153 | // [7, 9], 154 | // ] 155 | // const source = new MagicString('abcdefghijkl', { 156 | // filename: 'foo.js', 157 | // indentExclusionRanges: array, 158 | // }) 159 | 160 | // const clone = source.clone() 161 | 162 | // assert.notStrictEqual( 163 | // source.indentExclusionRanges, 164 | // clone.indentExclusionRanges, 165 | // ) 166 | // assert.deepEqual( 167 | // source.indentExclusionRanges, 168 | // clone.indentExclusionRanges, 169 | // ) 170 | // }) 171 | 172 | // it('should clone sourcemapLocations', () => { 173 | // const source = new MagicString('abcdefghijkl', { 174 | // filename: 'foo.js', 175 | // }) 176 | 177 | // source.addSourcemapLocation(3) 178 | 179 | // const clone = source.clone() 180 | 181 | // assert.notStrictEqual(source.sourcemapLocations, clone.sourcemapLocations) 182 | // assert.deepEqual(source.sourcemapLocations, clone.sourcemapLocations) 183 | // }) 184 | 185 | // it('should clone intro and outro', () => { 186 | // const source = new MagicString('defghi') 187 | 188 | // source.prepend('abc') 189 | // source.append('jkl') 190 | 191 | // const clone = source.clone() 192 | 193 | // assert.equal(source.toString(), clone.toString()) 194 | // }) 195 | // }) 196 | 197 | describe('generateMap', () => { 198 | it('should generate a sourcemap', () => { 199 | const s = new MagicString('abcdefghijkl').remove(3, 9) 200 | 201 | const map = s.generateMap({ 202 | file: 'output.md', 203 | source: 'input.md', 204 | includeContent: true, 205 | hires: true, 206 | }) 207 | 208 | const mapObject = map.toMap() 209 | 210 | assert.equal(mapObject.version, 3) 211 | assert.equal(mapObject.file, 'output.md') 212 | assert.deepEqual(mapObject.sources, ['input.md']) 213 | assert.deepEqual(mapObject.sourcesContent, ['abcdefghijkl']) 214 | assert.equal(mapObject.mappings, 'AAAA,CAAC,CAAC,CAAO,CAAC,CAAC') 215 | 216 | assert.equal( 217 | map.toString(), 218 | '{"version":3,"mappings":"AAAA,CAAC,CAAC,CAAO,CAAC,CAAC","names":[],"sources":["input.md"],"sourcesContent":["abcdefghijkl"],"file":"output.md"}', 219 | ) 220 | assert.equal( 221 | map.toUrl(), 222 | 'data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQUEsQ0FBQyxDQUFDLENBQU8sQ0FBQyxDQUFDIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbImlucHV0Lm1kIl0sInNvdXJjZXNDb250ZW50IjpbImFiY2RlZmdoaWprbCJdLCJmaWxlIjoib3V0cHV0Lm1kIn0=', 223 | ) 224 | 225 | const smc = new SourceMapConsumer(mapObject) 226 | let loc 227 | 228 | loc = smc.originalPositionFor({ line: 1, column: 0 }) 229 | assert.equal(loc.line, 1) 230 | assert.equal(loc.column, 0) 231 | 232 | loc = smc.originalPositionFor({ line: 1, column: 1 }) 233 | assert.equal(loc.line, 1) 234 | assert.equal(loc.column, 1) 235 | 236 | loc = smc.originalPositionFor({ line: 1, column: 4 }) 237 | assert.equal(loc.line, 1) 238 | assert.equal(loc.column, 10) 239 | }) 240 | 241 | it('should generate a correct sourcemap for prepend content when hires = false', () => { 242 | const s = new MagicString('x\nq') 243 | 244 | s.prepend('y\n') 245 | 246 | const map = s.generateMap({ 247 | includeContent: true, 248 | }).toMap() 249 | 250 | assert.equal(map.mappings, ';AAAA;AACA') 251 | }) 252 | 253 | // it('should generate a correct sourcemap for indented content', () => { 254 | // const s = new MagicString( 255 | // 'var answer = 42;\nconsole.log("the answer is %s", answer);', 256 | // ) 257 | 258 | // s.prepend("'use strict';\n\n") 259 | // s.indent('\t').prepend('(function () {\n').append('\n}).call(global);') 260 | 261 | // const map = s.generateMap({ 262 | // source: 'input.md', 263 | // includeContent: true, 264 | // hires: true, 265 | // }) 266 | 267 | // const smc = new SourceMapConsumer(map) 268 | 269 | // const originLoc = smc.originalPositionFor({ line: 5, column: 1 }) 270 | // assert.equal(originLoc.line, 2) 271 | // assert.equal(originLoc.column, 0) 272 | // }) 273 | 274 | // it('should generate a sourcemap using specified locations', () => { 275 | // const s = new MagicString('abcdefghijkl') 276 | 277 | // s.addSourcemapLocation(0) 278 | // s.addSourcemapLocation(3) 279 | // s.addSourcemapLocation(10) 280 | 281 | // s.remove(6, 9) 282 | // const map = s.generateMap({ 283 | // file: 'output.md', 284 | // source: 'input.md', 285 | // includeContent: true, 286 | // }) 287 | 288 | // assert.equal(map.version, 3) 289 | // assert.equal(map.file, 'output.md') 290 | // assert.deepEqual(map.sources, ['input.md']) 291 | // assert.deepEqual(map.sourcesContent, ['abcdefghijkl']) 292 | 293 | // assert.equal( 294 | // map.toString(), 295 | // '{"version":3,"file":"output.md","sources":["input.md"],"sourcesContent":["abcdefghijkl"],"names":[],"mappings":"AAAA,GAAG,GAAM,CAAC"}', 296 | // ) 297 | // assert.equal( 298 | // map.toUrl(), 299 | // 'data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3V0cHV0Lm1kIiwic291cmNlcyI6WyJpbnB1dC5tZCJdLCJzb3VyY2VzQ29udGVudCI6WyJhYmNkZWZnaGlqa2wiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsR0FBRyxHQUFNLENBQUMifQ==', 300 | // ) 301 | 302 | // const smc = new SourceMapConsumer(map) 303 | // let loc 304 | 305 | // loc = smc.originalPositionFor({ line: 1, column: 0 }) 306 | // assert.equal(loc.line, 1) 307 | // assert.equal(loc.column, 0) 308 | 309 | // loc = smc.originalPositionFor({ line: 1, column: 3 }) 310 | // assert.equal(loc.line, 1) 311 | // assert.equal(loc.column, 3) 312 | 313 | // loc = smc.originalPositionFor({ line: 1, column: 7 }) 314 | // assert.equal(loc.line, 1) 315 | // assert.equal(loc.column, 10) 316 | // }) 317 | 318 | it('should correctly map inserted content', () => { 319 | const s = new MagicString('function Foo () {}') 320 | 321 | s.overwrite(9, 12, 'Bar') 322 | 323 | const map = s.generateMap({ 324 | file: 'output.js', 325 | source: 'input.js', 326 | includeContent: true, 327 | }).toMap() 328 | 329 | const smc = new SourceMapConsumer(map) 330 | 331 | const loc = smc.originalPositionFor({ line: 1, column: 9 }) 332 | assert.equal(loc.line, 1) 333 | assert.equal(loc.column, 9) 334 | }) 335 | 336 | it('should yield consistent results between appendLeft and prependRight', () => { 337 | const s1 = new MagicString('abcdefghijkl') 338 | s1.appendLeft(6, 'X') 339 | 340 | const s2 = new MagicString('abcdefghijkl') 341 | s2.prependRight(6, 'X') 342 | 343 | const m1 = s1.generateMap({ 344 | file: 'output', 345 | source: 'input', 346 | includeContent: true, 347 | }).toMap() 348 | const m2 = s2.generateMap({ 349 | file: 'output', 350 | source: 'input', 351 | includeContent: true, 352 | }).toMap() 353 | 354 | assert.deepEqual(m1, m2) 355 | }) 356 | 357 | // it('should recover original names', () => { 358 | // const s = new MagicString('function Foo () {}') 359 | 360 | // s.overwrite(9, 12, 'Bar', { storeName: true }) 361 | 362 | // const map = s.generateMap({ 363 | // file: 'output.js', 364 | // source: 'input.js', 365 | // includeContent: true, 366 | // }) 367 | 368 | // const smc = new SourceMapConsumer(map) 369 | 370 | // const loc = smc.originalPositionFor({ line: 1, column: 9 }) 371 | // assert.equal(loc.name, 'Foo') 372 | // }) 373 | 374 | // it('should generate one segment per replacement', () => { 375 | // const s = new MagicString('var answer = 42') 376 | // s.overwrite(4, 10, 'number', { storeName: true }) 377 | 378 | // const map = s.generateMap({ 379 | // file: 'output.js', 380 | // source: 'input.js', 381 | // includeContent: true, 382 | // }) 383 | 384 | // const smc = new SourceMapConsumer(map) 385 | 386 | // let numMappings = 0 387 | // smc.eachMapping(() => (numMappings += 1)) 388 | 389 | // assert.equal(numMappings, 3) // one at 0, one at the edit, one afterwards 390 | // }) 391 | 392 | // it('should generate a sourcemap that correctly locates moved content', () => { 393 | // const s = new MagicString('abcdefghijkl') 394 | // s.move(3, 6, 9) 395 | 396 | // const result = s.toString() 397 | // const map = s.generateMap({ 398 | // file: 'output.js', 399 | // source: 'input.js', 400 | // includeContent: true, 401 | // hires: true, 402 | // }) 403 | 404 | // const smc = new SourceMapConsumer(map) 405 | 406 | // 'abcdefghijkl'.split('').forEach((letter, i) => { 407 | // const column = result.indexOf(letter) 408 | // const loc = smc.originalPositionFor({ line: 1, column }) 409 | 410 | // assert.equal(loc.line, 1) 411 | // assert.equal(loc.column, i) 412 | // }) 413 | // }) 414 | 415 | // TODO: better trim algorithm 416 | // it('generates a map with trimmed content (#53)', () => { 417 | // const s1 = new MagicString('abcdefghijkl ').trim() 418 | // const map1 = s1.generateMap({ 419 | // file: 'output', 420 | // source: 'input', 421 | // includeContent: true, 422 | // hires: true, 423 | // }) 424 | // 425 | // const smc1 = new SourceMapConsumer(map1) 426 | // const loc1 = smc1.originalPositionFor({ line: 1, column: 11 }) 427 | // 428 | // assert.equal(loc1.column, 11) 429 | // 430 | // const s2 = new MagicString(' abcdefghijkl').trim() 431 | // const map2 = s2.generateMap({ 432 | // file: 'output', 433 | // source: 'input', 434 | // includeContent: true, 435 | // hires: true, 436 | // }) 437 | // 438 | // const smc2 = new SourceMapConsumer(map2) 439 | // const loc2 = smc2.originalPositionFor({ line: 1, column: 1 }) 440 | // 441 | // assert.equal(loc2.column, 2) 442 | // }) 443 | 444 | it('skips empty segments at the start', () => { 445 | const s = new MagicString('abcdefghijkl') 446 | s.remove(0, 3).remove(3, 6) 447 | 448 | const map = s.generateMap().toMap() 449 | const smc = new SourceMapConsumer(map) 450 | const loc = smc.originalPositionFor({ line: 1, column: 6 }) 451 | 452 | assert.equal(loc.column, 6) 453 | }) 454 | 455 | // it('skips indentation at the start', () => { 456 | // const s = new MagicString('abcdefghijkl') 457 | // s.indent(' ') 458 | 459 | // const map = s.generateMap() 460 | // assert.equal(map.mappings, 'IAAA') 461 | // }) 462 | }) 463 | 464 | // describe('getIndentString', () => { 465 | // it('should guess the indent string', () => { 466 | // const s = new MagicString('abc\n def\nghi') 467 | // assert.equal(s.getIndentString(), ' ') 468 | // }) 469 | 470 | // it('should return a tab if no lines are indented', () => { 471 | // const s = new MagicString('abc\ndef\nghi') 472 | // assert.equal(s.getIndentString(), '\t') 473 | // }) 474 | // }) 475 | 476 | // describe('indent', () => { 477 | // it('should indent content with a single tab character by default', () => { 478 | // const s = new MagicString('abc\ndef\nghi\njkl') 479 | 480 | // s.indent() 481 | // assert.equal(s.toString(), '\tabc\n\tdef\n\tghi\n\tjkl') 482 | 483 | // s.indent() 484 | // assert.equal(s.toString(), '\t\tabc\n\t\tdef\n\t\tghi\n\t\tjkl') 485 | // }) 486 | 487 | // it('should indent content, using existing indentation as a guide', () => { 488 | // const s = new MagicString('abc\n def\n ghi\n jkl') 489 | 490 | // s.indent() 491 | // assert.equal(s.toString(), ' abc\n def\n ghi\n jkl') 492 | 493 | // s.indent() 494 | // assert.equal(s.toString(), ' abc\n def\n ghi\n jkl') 495 | // }) 496 | 497 | // it('should disregard single-space indentation when auto-indenting', () => { 498 | // const s = new MagicString('abc\n/**\n *comment\n */') 499 | 500 | // s.indent() 501 | // assert.equal(s.toString(), '\tabc\n\t/**\n\t *comment\n\t */') 502 | // }) 503 | 504 | // it('should indent content using the supplied indent string', () => { 505 | // const s = new MagicString('abc\ndef\nghi\njkl') 506 | 507 | // s.indent(' ') 508 | // assert.equal(s.toString(), ' abc\n def\n ghi\n jkl') 509 | 510 | // s.indent('>>') 511 | // assert.equal(s.toString(), '>> abc\n>> def\n>> ghi\n>> jkl') 512 | // }) 513 | 514 | // it('should indent content using the empty string if specified (i.e. noop)', () => { 515 | // const s = new MagicString('abc\ndef\nghi\njkl') 516 | 517 | // s.indent('') 518 | // assert.equal(s.toString(), 'abc\ndef\nghi\njkl') 519 | // }) 520 | 521 | // it('should prevent excluded characters from being indented', () => { 522 | // const s = new MagicString('abc\ndef\nghi\njkl') 523 | 524 | // s.indent(' ', { exclude: [7, 15] }) 525 | // assert.equal(s.toString(), ' abc\n def\nghi\njkl') 526 | 527 | // s.indent('>>', { exclude: [7, 15] }) 528 | // assert.equal(s.toString(), '>> abc\n>> def\nghi\njkl') 529 | // }) 530 | 531 | // it('should not add characters to empty lines', () => { 532 | // const s = new MagicString('\n\nabc\ndef\n\nghi\njkl') 533 | 534 | // s.indent() 535 | // assert.equal(s.toString(), '\n\n\tabc\n\tdef\n\n\tghi\n\tjkl') 536 | 537 | // s.indent() 538 | // assert.equal(s.toString(), '\n\n\t\tabc\n\t\tdef\n\n\t\tghi\n\t\tjkl') 539 | // }) 540 | 541 | // it('should not add characters to empty lines, even on Windows', () => { 542 | // const s = new MagicString('\r\n\r\nabc\r\ndef\r\n\r\nghi\r\njkl') 543 | 544 | // s.indent() 545 | // assert.equal(s.toString(), '\r\n\r\n\tabc\r\n\tdef\r\n\r\n\tghi\r\n\tjkl') 546 | 547 | // s.indent() 548 | // assert.equal( 549 | // s.toString(), 550 | // '\r\n\r\n\t\tabc\r\n\t\tdef\r\n\r\n\t\tghi\r\n\t\tjkl', 551 | // ) 552 | // }) 553 | 554 | // it('should indent content with removals', () => { 555 | // const s = new MagicString('/* remove this line */\nvar foo = 1;') 556 | 557 | // s.remove(0, 23) 558 | // s.indent() 559 | 560 | // assert.equal(s.toString(), '\tvar foo = 1;') 561 | // }) 562 | 563 | // it('should not indent patches in the middle of a line', () => { 564 | // const s = new MagicString('class Foo extends Bar {}') 565 | 566 | // s.overwrite(18, 21, 'Baz') 567 | // assert.equal(s.toString(), 'class Foo extends Baz {}') 568 | 569 | // s.indent() 570 | // assert.equal(s.toString(), '\tclass Foo extends Baz {}') 571 | // }) 572 | 573 | // it('should return this', () => { 574 | // const s = new MagicString('abcdefghijkl') 575 | // assert.strictEqual(s.indent(), s) 576 | // }) 577 | 578 | // it('should return this on noop', () => { 579 | // const s = new MagicString('abcdefghijkl') 580 | // assert.strictEqual(s.indent(''), s) 581 | // }) 582 | // }) 583 | 584 | // describe('insert', () => { 585 | // it('is deprecated', () => { 586 | // const s = new MagicString('abcdefghijkl') 587 | // assert.throws(() => s.insert(6, 'X'), /deprecated/) 588 | // }) 589 | 590 | // TODO move this into prependRight and appendLeft tests 591 | 592 | // it( 'should insert characters in the correct location', () => { 593 | // const s = new MagicString( 'abcdefghijkl' ); 594 | // 595 | // s.insert( 0, '>>>' ); 596 | // s.insert( 6, '***' ); 597 | // s.insert( 12, '<<<' ); 598 | // 599 | // assert.equal( s.toString(), '>>>abcdef***ghijkl<<<' ); 600 | // }); 601 | // 602 | // it( 'should return this', () => { 603 | // const s = new MagicString( 'abcdefghijkl' ); 604 | // assert.strictEqual( s.insert( 0, 'a' ), s ); 605 | // }); 606 | // 607 | // it( 'should insert repeatedly at the same position correctly', () => { 608 | // const s = new MagicString( 'ab' ); 609 | // assert.equal( s.insert(1, '1').toString(), 'a1b' ); 610 | // assert.equal( s.insert(1, '2').toString(), 'a12b' ); 611 | // }); 612 | // 613 | // it( 'should insert repeatedly at the beginning correctly', () => { 614 | // const s = new MagicString( 'ab' ); 615 | // assert.equal( s.insert(0, '1').toString(), '1ab' ); 616 | // assert.equal( s.insert(0, '2').toString(), '12ab' ); 617 | // }); 618 | // 619 | // it( 'should throw when given non-string content', () => { 620 | // const s = new MagicString( '' ); 621 | // assert.throws( 622 | // function () { s.insert( 0, [] ); }, 623 | // TypeError 624 | // ); 625 | // }); 626 | // 627 | // it( 'should allow inserting after removed range', () => { 628 | // const s = new MagicString( 'abcd' ); 629 | // s.remove( 1, 2 ); 630 | // s.insert( 2, 'z' ); 631 | // assert.equal( s.toString(), 'azcd' ); 632 | // }); 633 | }) 634 | 635 | describe("GenerateMap", () => { 636 | it("should correctly map inserted multi-lines content", () => { 637 | let s = new MagicString("function Foo () {}") 638 | 639 | s.overwrite(15, 16, "\n", { 640 | contentOnly: false 641 | }) 642 | 643 | let map = s.generateMap({ 644 | includeContent: true 645 | }) 646 | 647 | assert.equal(map.toString(), "{\"version\":3,\"mappings\":\"AAAA;AAAgB\",\"names\":[],\"sources\":[null],\"sourcesContent\":[\"function Foo () {}\"],\"file\":null}") 648 | 649 | s = new MagicString("function Foo () {}") 650 | 651 | s.overwrite( 652 | 15, 17, " {\n console.log(\"bar\")\n", 653 | { 654 | contentOnly: false 655 | } 656 | ) 657 | 658 | map = s.generateMap({ 659 | includeContent: true 660 | }) 661 | 662 | assert.equal(map.toString(), "{\"version\":3,\"mappings\":\"AAAA,eAAe;AAAA;AAAE\",\"names\":[],\"sources\":[null],\"sourcesContent\":[\"function Foo () {}\"],\"file\":null}") 663 | }) 664 | 665 | it("should correctly map inserted content", () => { 666 | const s = new MagicString("function Foo () {}") 667 | 668 | s.overwrite(9, 12, "Bar", { 669 | contentOnly: false 670 | }) 671 | 672 | const map = s.generateMap({ 673 | includeContent: true 674 | }) 675 | 676 | assert.equal(map.toString(), "{\"version\":3,\"mappings\":\"AAAA,SAAS,GAAG\",\"names\":[],\"sources\":[null],\"sourcesContent\":[\"function Foo () {}\"],\"file\":null}") 677 | }) 678 | 679 | it("should correctly map to JSON", () => { 680 | const s = new MagicString("function Foo () {}") 681 | 682 | let map = s.generateMap({ 683 | includeContent: true 684 | }).toMap() 685 | 686 | assert.equal(map.sourcesContent[0], "function Foo () {}") 687 | assert.equal(map.file, null) 688 | assert.equal(map.sourceRoot, undefined) 689 | assert.equal(map.names.length, 0) 690 | assert.equal(map.sources[0], null) 691 | assert.equal(map.version, 3) 692 | assert.equal(map.mappings, "AAAA") 693 | }) 694 | }) 695 | 696 | describe('move', () => { 697 | it('moves content from the start', () => { 698 | const s = new MagicString('abcdefghijkl') 699 | s.move(0, 3, 6) 700 | 701 | assert.equal(s.toString(), 'defabcghijkl') 702 | }) 703 | 704 | it('moves content to the start', () => { 705 | const s = new MagicString('abcdefghijkl') 706 | s.move(3, 6, 0) 707 | 708 | assert.equal(s.toString(), 'defabcghijkl') 709 | }) 710 | 711 | it('moves content from the end', () => { 712 | const s = new MagicString('abcdefghijkl') 713 | s.move(9, 12, 6) 714 | 715 | assert.equal(s.toString(), 'abcdefjklghi') 716 | }) 717 | 718 | it('moves content to the end', () => { 719 | const s = new MagicString('abcdefghijkl') 720 | s.move(6, 9, 12) 721 | 722 | assert.equal(s.toString(), 'abcdefjklghi') 723 | }) 724 | 725 | it('ignores redundant move', () => { 726 | const s = new MagicString('abcdefghijkl') 727 | s.prependRight(9, 'X') 728 | s.move(9, 12, 6) 729 | s.appendLeft(12, 'Y') 730 | s.move(6, 9, 12) // this is redundant – [6,9] is already after [9,12] 731 | 732 | assert.equal(s.toString(), 'abcdefXjklYghi') 733 | }) 734 | 735 | it('moves content to the middle', () => { 736 | const s = new MagicString('abcdefghijkl') 737 | s.move(3, 6, 9) 738 | 739 | assert.equal(s.toString(), 'abcghidefjkl') 740 | }) 741 | 742 | it('handles multiple moves of the same snippet', () => { 743 | const s = new MagicString('abcdefghijkl') 744 | 745 | s.move(0, 3, 6) 746 | assert.equal(s.toString(), 'defabcghijkl') 747 | 748 | s.move(0, 3, 9) 749 | assert.equal(s.toString(), 'defghiabcjkl') 750 | }) 751 | 752 | it('handles moves of adjacent snippets', () => { 753 | const s = new MagicString('abcdefghijkl') 754 | 755 | s.move(0, 2, 6) 756 | assert.equal(s.toString(), 'cdefabghijkl') 757 | 758 | s.move(2, 4, 6) 759 | assert.equal(s.toString(), 'efabcdghijkl') 760 | }) 761 | 762 | it('handles moves to same index', () => { 763 | const s = new MagicString('abcdefghijkl') 764 | s.move(0, 2, 6).move(3, 5, 6) 765 | 766 | assert.equal(s.toString(), 'cfabdeghijkl') 767 | }) 768 | 769 | it('refuses to move a selection to inside itself', () => { 770 | const s = new MagicString('abcdefghijkl') 771 | 772 | assert.throws( 773 | () => s.move(3, 6, 3), 774 | /Cannot move a selection inside itself/, 775 | ) 776 | 777 | assert.throws( 778 | () => s.move(3, 6, 4), 779 | /Cannot move a selection inside itself/, 780 | ) 781 | 782 | assert.throws( 783 | () => s.move(3, 6, 6), 784 | /Cannot move a selection inside itself/, 785 | ) 786 | }) 787 | 788 | it('allows edits of moved content', () => { 789 | const s1 = new MagicString('abcdefghijkl') 790 | 791 | s1.move(3, 6, 9) 792 | s1.overwrite(3, 6, 'DEF') 793 | 794 | assert.equal(s1.toString(), 'abcghiDEFjkl') 795 | 796 | const s2 = new MagicString('abcdefghijkl') 797 | 798 | s2.move(3, 6, 9) 799 | s2.overwrite(4, 5, 'E') 800 | 801 | assert.equal(s2.toString(), 'abcghidEfjkl') 802 | }) 803 | 804 | // it( 'move follows inserts', () => { 805 | // const s = new MagicString( 'abcdefghijkl' ); 806 | // 807 | // s.appendLeft( 3, 'X' ).move( 6, 9, 3 ); 808 | // assert.equal( s.toString(), 'abcXghidefjkl' ); 809 | // }); 810 | // 811 | // it( 'inserts follow move', () => { 812 | // const s = new MagicString( 'abcdefghijkl' ); 813 | // 814 | // s.insert( 3, 'X' ).move( 6, 9, 3 ).insert( 3, 'Y' ); 815 | // assert.equal( s.toString(), 'abcXghiYdefjkl' ); 816 | // }); 817 | // 818 | // it( 'discards inserts at end of move by default', () => { 819 | // const s = new MagicString( 'abcdefghijkl' ); 820 | // 821 | // s.insert( 6, 'X' ).move( 3, 6, 9 ); 822 | // assert.equal( s.toString(), 'abcXghidefjkl' ); 823 | // }); 824 | 825 | it('moves content inserted at end of range', () => { 826 | const s = new MagicString('abcdefghijkl') 827 | 828 | s.appendLeft(6, 'X').move(3, 6, 9) 829 | assert.equal(s.toString(), 'abcghidefXjkl') 830 | }) 831 | 832 | it('returns this', () => { 833 | const s = new MagicString('abcdefghijkl') 834 | assert.strictEqual(s.move(3, 6, 9), s) 835 | }) 836 | }) 837 | 838 | describe('overwrite', () => { 839 | it('should replace characters', () => { 840 | const s = new MagicString('abcdefghijkl') 841 | 842 | s.overwrite(5, 8, 'FGH') 843 | assert.equal(s.toString(), 'abcdeFGHijkl') 844 | }) 845 | 846 | it('should throw an error if overlapping replacements are attempted', () => { 847 | const s = new MagicString('abcdefghijkl') 848 | 849 | s.overwrite(7, 11, 'xx') 850 | 851 | assert.throws(() => s.overwrite(8, 12, 'yy')) 852 | 853 | assert.equal(s.toString(), 'abcdefgxxl') 854 | 855 | s.overwrite(6, 12, 'yes') 856 | assert.equal(s.toString(), 'abcdefyes') 857 | }) 858 | 859 | it('should allow contiguous but non-overlapping replacements', () => { 860 | const s = new MagicString('abcdefghijkl') 861 | 862 | s.overwrite(3, 6, 'DEF') 863 | assert.equal(s.toString(), 'abcDEFghijkl') 864 | 865 | s.overwrite(6, 9, 'GHI') 866 | assert.equal(s.toString(), 'abcDEFGHIjkl') 867 | 868 | s.overwrite(0, 3, 'ABC') 869 | assert.equal(s.toString(), 'ABCDEFGHIjkl') 870 | 871 | s.overwrite(9, 12, 'JKL') 872 | assert.equal(s.toString(), 'ABCDEFGHIJKL') 873 | }) 874 | 875 | it('does not replace zero-length inserts at overwrite start location', () => { 876 | const s = new MagicString('abcdefghijkl') 877 | 878 | s.remove(0, 6) 879 | s.appendLeft(6, 'DEF') 880 | s.overwrite(6, 9, 'GHI') 881 | assert.equal(s.toString(), 'DEFGHIjkl') 882 | }) 883 | 884 | it('replaces zero-length inserts inside overwrite', () => { 885 | const s = new MagicString('abcdefghijkl') 886 | 887 | s.appendLeft(6, 'XXX') 888 | s.overwrite(3, 9, 'DEFGHI') 889 | assert.equal(s.toString(), 'abcDEFGHIjkl') 890 | }) 891 | 892 | it('replaces non-zero-length inserts inside overwrite', () => { 893 | const s = new MagicString('abcdefghijkl') 894 | 895 | s.overwrite(3, 4, 'XXX') 896 | s.overwrite(3, 5, 'DE') 897 | assert.equal(s.toString(), 'abcDEfghijkl') 898 | 899 | s.overwrite(7, 8, 'YYY') 900 | s.overwrite(6, 8, 'GH') 901 | assert.equal(s.toString(), 'abcDEfGHijkl') 902 | }) 903 | 904 | it('should return this', () => { 905 | const s = new MagicString('abcdefghijkl') 906 | assert.strictEqual(s.overwrite(3, 4, 'D'), s) 907 | }) 908 | 909 | it('should disallow overwriting zero-length ranges', () => { 910 | const s = new MagicString('x') 911 | assert.throws(() => s.overwrite(0, 0, 'anything')) 912 | }) 913 | 914 | it('should throw when given non-string content', () => { 915 | const s = new MagicString('') 916 | // @ts-expect-error 917 | assert.throws(() => s.overwrite(0, 1, [])) 918 | }) 919 | 920 | it('replaces interior inserts', () => { 921 | const s = new MagicString('abcdefghijkl') 922 | 923 | s.appendLeft(1, '&') 924 | s.prependRight(1, '^') 925 | s.appendLeft(3, '!') 926 | s.prependRight(3, '?') 927 | s.overwrite(1, 3, '...') 928 | assert.equal(s.toString(), 'a&...?defghijkl') 929 | }) 930 | 931 | it('preserves interior inserts with `contentOnly: true`', () => { 932 | const s = new MagicString('abcdefghijkl') 933 | 934 | s.appendLeft(1, '&') 935 | s.prependRight(1, '^') 936 | s.appendLeft(3, '!') 937 | s.prependRight(3, '?') 938 | s.overwrite(1, 3, '...', { contentOnly: true }) 939 | assert.equal(s.toString(), 'a&^...!?defghijkl') 940 | }) 941 | 942 | // it('disallows overwriting across moved content', () => { 943 | // const s = new MagicString('abcdefghijkl') 944 | 945 | // s.move(6, 9, 3) 946 | // assert.throws( 947 | // () => s.overwrite(5, 7, 'XX'), 948 | // /Cannot overwrite across a split point/, 949 | // ) 950 | // }) 951 | 952 | it('allows later insertions at the end', () => { 953 | const s = new MagicString('abcdefg') 954 | 955 | s.appendLeft(4, '(') 956 | s.overwrite(2, 7, '') 957 | s.appendLeft(7, 'h') 958 | assert.equal(s.toString(), 'abh') 959 | }) 960 | }) 961 | 962 | describe('prepend', () => { 963 | it('should prepend content', () => { 964 | const s = new MagicString('abcdefghijkl') 965 | 966 | s.prepend('xyz') 967 | assert.equal(s.toString(), 'xyzabcdefghijkl') 968 | 969 | s.prepend('123') 970 | assert.equal(s.toString(), '123xyzabcdefghijkl') 971 | }) 972 | 973 | it('should return this', () => { 974 | const s = new MagicString('abcdefghijkl') 975 | assert.strictEqual(s.prepend('xyz'), s) 976 | }) 977 | }) 978 | 979 | describe('prependLeft', () => { 980 | it('should return this', () => { 981 | const s = new MagicString('abcdefghijkl') 982 | assert.strictEqual(s.prependLeft(0, 'a'), s) 983 | }) 984 | }) 985 | 986 | describe('prependRight', () => { 987 | it('should return this', () => { 988 | const s = new MagicString('abcdefghijkl') 989 | assert.strictEqual(s.prependRight(0, 'a'), s) 990 | }) 991 | }) 992 | 993 | describe('remove', () => { 994 | it('should remove characters from the original string', () => { 995 | const s = new MagicString('abcdefghijkl') 996 | 997 | s.remove(1, 5) 998 | assert.equal(s.toString(), 'afghijkl') 999 | 1000 | s.remove(9, 12) 1001 | assert.equal(s.toString(), 'afghi') 1002 | }) 1003 | 1004 | it('should remove from the start', () => { 1005 | const s = new MagicString('abcdefghijkl') 1006 | 1007 | s.remove(0, 6) 1008 | assert.equal(s.toString(), 'ghijkl') 1009 | }) 1010 | 1011 | it('should remove from the end', () => { 1012 | const s = new MagicString('abcdefghijkl') 1013 | 1014 | s.remove(6, 12) 1015 | assert.equal(s.toString(), 'abcdef') 1016 | }) 1017 | 1018 | it('should treat zero-length removals as a no-op', () => { 1019 | const s = new MagicString('abcdefghijkl') 1020 | 1021 | s.remove(0, 0).remove(6, 6).remove(9, -3) 1022 | assert.equal(s.toString(), 'abcdefghijkl') 1023 | }) 1024 | 1025 | it('should remove overlapping ranges', () => { 1026 | const s1 = new MagicString('abcdefghijkl') 1027 | 1028 | s1.remove(3, 7).remove(5, 9) 1029 | assert.equal(s1.toString(), 'abcjkl') 1030 | 1031 | const s2 = new MagicString('abcdefghijkl') 1032 | 1033 | s2.remove(3, 7).remove(4, 6) 1034 | assert.equal(s2.toString(), 'abchijkl') 1035 | }) 1036 | 1037 | it('should remove overlapping ranges, redux', () => { 1038 | const s = new MagicString('abccde') 1039 | 1040 | s.remove(2, 3) // c 1041 | s.remove(1, 3) // bc 1042 | assert.equal(s.toString(), 'acde') 1043 | }) 1044 | 1045 | // it('should remove modified ranges', () => { 1046 | // const s = new MagicString('abcdefghi') 1047 | 1048 | // s.overwrite(3, 6, 'DEF') 1049 | // s.remove(2, 7) // cDEFg 1050 | // assert.equal(s.slice(1, 8), 'bh') 1051 | // assert.equal(s.toString(), 'abhi') 1052 | // }) 1053 | 1054 | it('should not remove content inserted after the end of removed range', () => { 1055 | const s = new MagicString('ab.c;') 1056 | 1057 | s.prependRight(0, '(') 1058 | s.prependRight(4, ')') 1059 | s.remove(2, 4) 1060 | assert.equal(s.toString(), '(ab);') 1061 | }) 1062 | 1063 | it('should remove interior inserts', () => { 1064 | const s = new MagicString('abc;') 1065 | 1066 | s.appendLeft(1, '[') 1067 | s.prependRight(1, '(') 1068 | s.appendLeft(2, ')') 1069 | s.prependRight(2, ']') 1070 | s.remove(1, 2) 1071 | assert.equal(s.toString(), 'a[]c;') 1072 | }) 1073 | 1074 | it('should provide a useful error when illegal removals are attempted', () => { 1075 | const s = new MagicString('abcdefghijkl') 1076 | 1077 | s.overwrite(5, 7, 'XX') 1078 | 1079 | assert.throws(() => s.remove(4, 6)) 1080 | }) 1081 | 1082 | it('should return this', () => { 1083 | const s = new MagicString('abcdefghijkl') 1084 | assert.strictEqual(s.remove(3, 4), s) 1085 | }) 1086 | 1087 | // it('removes across moved content', () => { 1088 | // const s = new MagicString('abcdefghijkl') 1089 | 1090 | // s.move(6, 9, 3) 1091 | // s.remove(5, 7) 1092 | 1093 | // assert.equal(s.toString(), 'abchidejkl') 1094 | // }) 1095 | }) 1096 | 1097 | // describe('slice', () => { 1098 | // it('should return the generated content between the specified original characters', () => { 1099 | // const s = new MagicString('abcdefghijkl') 1100 | 1101 | // assert.equal(s.slice(3, 9), 'defghi') 1102 | // s.overwrite(4, 8, 'XX') 1103 | // assert.equal(s.slice(3, 9), 'dXXi') 1104 | // s.overwrite(2, 10, 'ZZ') 1105 | // assert.equal(s.slice(1, 11), 'bZZk') 1106 | // assert.equal(s.slice(2, 10), 'ZZ') 1107 | 1108 | // assert.throws(() => s.slice(3, 9)) 1109 | // }) 1110 | 1111 | // it('defaults `end` to the original string length', () => { 1112 | // const s = new MagicString('abcdefghijkl') 1113 | // assert.equal(s.slice(3), 'defghijkl') 1114 | // }) 1115 | 1116 | // it('allows negative numbers as arguments', () => { 1117 | // const s = new MagicString('abcdefghijkl') 1118 | // assert.equal(s.slice(-3), 'jkl') 1119 | // assert.equal(s.slice(0, -3), 'abcdefghi') 1120 | // }) 1121 | 1122 | // it('includes inserted characters, respecting insertion direction', () => { 1123 | // const s = new MagicString('abefij') 1124 | 1125 | // s.prependRight(2, 'cd') 1126 | // s.appendLeft(4, 'gh') 1127 | 1128 | // assert.equal(s.slice(), 'abcdefghij') 1129 | // assert.equal(s.slice(1, 5), 'bcdefghi') 1130 | // assert.equal(s.slice(2, 4), 'cdefgh') 1131 | // assert.equal(s.slice(3, 4), 'fgh') 1132 | // assert.equal(s.slice(0, 2), 'ab') 1133 | // assert.equal(s.slice(0, 3), 'abcde') 1134 | // assert.equal(s.slice(4, 6), 'ij') 1135 | // assert.equal(s.slice(3, 6), 'fghij') 1136 | // }) 1137 | 1138 | // it('supports characters moved outward', () => { 1139 | // const s = new MagicString('abcdEFghIJklmn') 1140 | 1141 | // s.move(4, 6, 2) 1142 | // s.move(8, 10, 12) 1143 | // assert.equal(s.toString(), 'abEFcdghklIJmn') 1144 | 1145 | // assert.equal(s.slice(1, -1), 'bEFcdghklIJm') 1146 | // assert.equal(s.slice(2, -2), 'cdghkl') 1147 | // assert.equal(s.slice(3, -3), 'dghk') 1148 | // assert.equal(s.slice(4, -4), 'EFcdghklIJ') 1149 | // assert.equal(s.slice(5, -5), 'FcdghklI') 1150 | // assert.equal(s.slice(6, -6), 'gh') 1151 | // }) 1152 | 1153 | // it('supports characters moved inward', () => { 1154 | // const s = new MagicString('abCDefghijKLmn') 1155 | 1156 | // s.move(2, 4, 6) 1157 | // s.move(10, 12, 8) 1158 | // assert.equal(s.toString(), 'abefCDghKLijmn') 1159 | 1160 | // assert.equal(s.slice(1, -1), 'befCDghKLijm') 1161 | // assert.equal(s.slice(2, -2), 'CDghKL') 1162 | // assert.equal(s.slice(3, -3), 'DghK') 1163 | // assert.equal(s.slice(4, -4), 'efCDghKLij') 1164 | // assert.equal(s.slice(5, -5), 'fCDghKLi') 1165 | // assert.equal(s.slice(6, -6), 'gh') 1166 | // }) 1167 | 1168 | // it('supports characters moved opposing', () => { 1169 | // const s = new MagicString('abCDefghIJkl') 1170 | 1171 | // s.move(2, 4, 8) 1172 | // s.move(8, 10, 4) 1173 | // assert.equal(s.toString(), 'abIJefghCDkl') 1174 | 1175 | // assert.equal(s.slice(1, -1), 'bIJefghCDk') 1176 | // assert.equal(s.slice(2, -2), '') 1177 | // assert.equal(s.slice(3, -3), '') 1178 | // assert.equal(s.slice(-3, 3), 'JefghC') 1179 | // assert.equal(s.slice(4, -4), 'efgh') 1180 | // assert.equal(s.slice(0, 3), 'abIJefghC') 1181 | // assert.equal(s.slice(3), 'Dkl') 1182 | // assert.equal(s.slice(0, -3), 'abI') 1183 | // assert.equal(s.slice(-3), 'JefghCDkl') 1184 | // }) 1185 | 1186 | // it('errors if replaced characters are used as slice anchors', () => { 1187 | // const s = new MagicString('abcdef') 1188 | // s.overwrite(2, 4, 'CD') 1189 | 1190 | // assert.throws(() => s.slice(2, 3), /slice end anchor/) 1191 | 1192 | // assert.throws(() => s.slice(3, 4), /slice start anchor/) 1193 | 1194 | // assert.throws(() => s.slice(3, 5), /slice start anchor/) 1195 | 1196 | // assert.equal(s.slice(1, 5), 'bCDe') 1197 | // }) 1198 | 1199 | // it('does not error if slice is after removed characters', () => { 1200 | // const s = new MagicString('abcdef') 1201 | 1202 | // s.remove(0, 2) 1203 | 1204 | // assert.equal(s.slice(2, 4), 'cd') 1205 | // }) 1206 | // }) 1207 | 1208 | // describe('snip', () => { 1209 | // it('should return a clone with content outside `start` and `end` removed', () => { 1210 | // const s = new MagicString('abcdefghijkl', { 1211 | // filename: 'foo.js', 1212 | // }) 1213 | 1214 | // s.overwrite(6, 9, 'GHI') 1215 | 1216 | // const snippet = s.snip(3, 9) 1217 | // assert.equal(snippet.toString(), 'defGHI') 1218 | // assert.equal(snippet.filename, 'foo.js') 1219 | // }) 1220 | 1221 | // it('should snip from the start', () => { 1222 | // const s = new MagicString('abcdefghijkl') 1223 | // const snippet = s.snip(0, 6) 1224 | 1225 | // assert.equal(snippet.toString(), 'abcdef') 1226 | // }) 1227 | 1228 | // it('should snip from the end', () => { 1229 | // const s = new MagicString('abcdefghijkl') 1230 | // const snippet = s.snip(6, 12) 1231 | 1232 | // assert.equal(snippet.toString(), 'ghijkl') 1233 | // }) 1234 | 1235 | // it('should respect original indices', () => { 1236 | // const s = new MagicString('abcdefghijkl') 1237 | // const snippet = s.snip(3, 9) 1238 | 1239 | // snippet.overwrite(6, 9, 'GHI') 1240 | // assert.equal(snippet.toString(), 'defGHI') 1241 | // }) 1242 | // }) 1243 | 1244 | describe('trim', () => { 1245 | it('should trim original content', () => { 1246 | assert.equal( 1247 | new MagicString(' abcdefghijkl ').trim().toString(), 1248 | 'abcdefghijkl', 1249 | ) 1250 | assert.equal( 1251 | new MagicString(' abcdefghijkl').trim().toString(), 1252 | 'abcdefghijkl', 1253 | ) 1254 | assert.equal( 1255 | new MagicString('abcdefghijkl ').trim().toString(), 1256 | 'abcdefghijkl', 1257 | ) 1258 | }) 1259 | 1260 | it('should trim replaced content', () => { 1261 | const s = new MagicString('abcdefghijkl') 1262 | 1263 | s.overwrite(0, 3, ' ').overwrite(9, 12, ' ').trim() 1264 | assert.equal(s.toString(), 'defghi') 1265 | }) 1266 | 1267 | it('should trim original content before replaced content', () => { 1268 | const s = new MagicString('abc def') 1269 | 1270 | s.remove(6, 9) 1271 | assert.equal(s.toString(), 'abc ') 1272 | 1273 | s.trim() 1274 | assert.equal(s.toString(), 'abc') 1275 | }) 1276 | 1277 | it('should trim original content after replaced content', () => { 1278 | const s = new MagicString('abc def') 1279 | 1280 | s.remove(0, 3) 1281 | assert.equal(s.toString(), ' def') 1282 | 1283 | s.trim() 1284 | assert.equal(s.toString(), 'def') 1285 | }) 1286 | 1287 | it('should trim original content before and after replaced content', () => { 1288 | const s = new MagicString('abc def ghi') 1289 | 1290 | s.remove(0, 3) 1291 | s.remove(12, 15) 1292 | assert.equal(s.toString(), ' def ') 1293 | 1294 | s.trim() 1295 | assert.equal(s.toString(), 'def') 1296 | }) 1297 | 1298 | it('should trim appended/prepended content', () => { 1299 | const s = new MagicString(' abcdefghijkl ') 1300 | 1301 | s.prepend(' ').append(' ').trim() 1302 | assert.equal(s.toString(), 'abcdefghijkl') 1303 | }) 1304 | 1305 | it('should trim empty string', () => { 1306 | const s = new MagicString(' ') 1307 | 1308 | assert.equal(s.trim().toString(), '') 1309 | }) 1310 | 1311 | it('should return this', () => { 1312 | const s = new MagicString(' abcdefghijkl ') 1313 | assert.strictEqual(s.trim(), s) 1314 | }) 1315 | 1316 | it('should support trimming chunks with intro and outro', () => { 1317 | const s = new MagicString(' \n') 1318 | s.appendRight(4, 'test') 1319 | assert.strictEqual(s.trim().toString(), 'test') 1320 | }) 1321 | }) 1322 | 1323 | describe('trimLines', () => { 1324 | it('should trim original content', () => { 1325 | const s = new MagicString('\n\n abcdefghijkl \n\n') 1326 | 1327 | s.trimLines() 1328 | assert.equal(s.toString(), ' abcdefghijkl ') 1329 | }) 1330 | }) 1331 | 1332 | describe('isEmpty', () => { 1333 | it('should support isEmpty', () => { 1334 | const s = new MagicString(' abcde fghijkl ') 1335 | 1336 | assert.equal(s.isEmpty(), false) 1337 | 1338 | s.prepend(' ') 1339 | s.append(' ') 1340 | s.remove(1, 6) 1341 | s.remove(9, 15) 1342 | 1343 | assert.equal(s.isEmpty(), false) 1344 | 1345 | s.remove(15, 16) 1346 | 1347 | assert.equal(s.isEmpty(), true) 1348 | }) 1349 | }) 1350 | 1351 | // Original length implementation does not count `intro / outro` 1352 | // describe('length', () => { 1353 | // it('should support length', () => { 1354 | // const str = ' abcde fghijkl ' 1355 | // const s = new MagicString(str) 1356 | // 1357 | // assert.equal(s.length(), 17) 1358 | // 1359 | // s.prepend(' ') 1360 | // s.append(' ') 1361 | // s.remove(1, 6) 1362 | // s.remove(9, 15) 1363 | // 1364 | // assert.equal(s.length(), 6) 1365 | // 1366 | // s.remove(15, 16) 1367 | // 1368 | // assert.equal(s.length(), 5) 1369 | // }) 1370 | // }) 1371 | 1372 | // describe('lastLine', () => { 1373 | // it('should support lastLine', () => { 1374 | // const s = new MagicString(' abcde\nfghijkl ') 1375 | 1376 | // assert.equal(s.lastLine(), 'fghijkl ') 1377 | 1378 | // s.prepend(' ') 1379 | // s.append(' ') 1380 | // s.remove(1, 6) 1381 | // s.remove(9, 15) 1382 | 1383 | // assert.equal(s.lastLine(), 'fg ') 1384 | 1385 | // s.overwrite(7, 8, '\n') 1386 | 1387 | // assert.equal(s.lastLine(), 'g ') 1388 | 1389 | // s.append('\n//lastline') 1390 | 1391 | // assert.equal(s.lastLine(), '//lastline') 1392 | // }) 1393 | // }) 1394 | // }) 1395 | --------------------------------------------------------------------------------