diff --git a/Cargo.lock b/Cargo.lock index 0a8690882dea7..84bd3596defba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -958,6 +958,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "index_vec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44faf5bb8861a9c72e20d3fb0fdbd59233e43056e2b80475ab0aacdc2e781355" + [[package]] name = "indexmap" version = "2.6.0" @@ -1011,7 +1017,7 @@ dependencies = [ "handlebars", "lazy_static", "oxc_tasks_common", - "rustc-hash", + "rustc-hash 2.0.0", "serde", ] @@ -1078,7 +1084,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -1406,11 +1412,11 @@ dependencies = [ "oxc_parser", "oxc_regular_expression", "oxc_semantic", - "oxc_sourcemap", + "oxc_sourcemap 0.38.0", "oxc_span", "oxc_syntax", "oxc_transformer", - "rustc-hash", + "rustc-hash 2.0.0", ] [[package]] @@ -1421,7 +1427,7 @@ checksum = "d240f6572a29895f324ad834a42a0c4bad0739269a08d0181ad7d6db7537e0d1" dependencies = [ "js-sys", "nom", - "rustc-hash", + "rustc-hash 2.0.0", "serde", "serde-wasm-bindgen", "serde_json", @@ -1505,7 +1511,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 2.0.0", "serde", "serde_json", "syn", @@ -1524,7 +1530,7 @@ dependencies = [ "oxc_parser", "oxc_prettier", "oxc_semantic", - "oxc_sourcemap", + "oxc_sourcemap 0.38.0", "oxc_span", "oxc_tasks_common", "oxc_transformer", @@ -1542,7 +1548,7 @@ dependencies = [ "oxc_index", "oxc_syntax", "petgraph", - "rustc-hash", + "rustc-hash 2.0.0", ] [[package]] @@ -1560,11 +1566,11 @@ dependencies = [ "oxc_index", "oxc_mangler", "oxc_parser", - "oxc_sourcemap", + "oxc_sourcemap 0.38.0", "oxc_span", "oxc_syntax", "pico-args", - "rustc-hash", + "rustc-hash 2.0.0", ] [[package]] @@ -1597,7 +1603,7 @@ dependencies = [ "pico-args", "rayon", "regex", - "rustc-hash", + "rustc-hash 2.0.0", "saphyr", "serde", "serde_json", @@ -1618,7 +1624,7 @@ name = "oxc_diagnostics" version = "0.38.0" dependencies = [ "oxc-miette", - "rustc-hash", + "rustc-hash 2.0.0", ] [[package]] @@ -1664,7 +1670,7 @@ dependencies = [ "oxc_parser", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.0.0", ] [[package]] @@ -1685,7 +1691,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "ropey", - "rustc-hash", + "rustc-hash 2.0.0", "serde", "serde_json", "tokio", @@ -1731,7 +1737,7 @@ dependencies = [ "rayon", "regex", "rust-lapper", - "rustc-hash", + "rustc-hash 2.0.0", "schemars", "serde", "serde_json", @@ -1806,7 +1812,7 @@ dependencies = [ "oxc_parser", "oxc_span", "oxc_tasks_common", - "rustc-hash", + "rustc-hash 2.0.0", ] [[package]] @@ -1837,7 +1843,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "pico-args", - "rustc-hash", + "rustc-hash 2.0.0", "seq-macro", "serde_json", ] @@ -1851,7 +1857,9 @@ dependencies = [ "napi-derive", "oxc", "oxc_module_lexer", + "self_cell", "serde_json", + "string_wizard", ] [[package]] @@ -1877,7 +1885,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "pico-args", - "rustc-hash", + "rustc-hash 2.0.0", ] [[package]] @@ -1891,7 +1899,7 @@ dependencies = [ "oxc_span", "oxc_tasks_common", "pico-args", - "rustc-hash", + "rustc-hash 2.0.0", "walkdir", ] @@ -1905,7 +1913,7 @@ dependencies = [ "oxc_estree", "oxc_span", "phf", - "rustc-hash", + "rustc-hash 2.0.0", "serde", "unicode-id-start", ] @@ -1921,7 +1929,7 @@ dependencies = [ "indexmap", "json-strip-comments", "once_cell", - "rustc-hash", + "rustc-hash 2.0.0", "serde", "serde_json", "simdutf8", @@ -1947,13 +1955,26 @@ dependencies = [ "oxc_span", "oxc_syntax", "phf", - "rustc-hash", + "rustc-hash 2.0.0", "serde", "serde_json", "tsify", "wasm-bindgen", ] +[[package]] +name = "oxc_sourcemap" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd8e81ebe12ff1c13de9e44cc3d0d187de138cd23e9992959653ecd003d0261" +dependencies = [ + "base64-simd", + "cfg-if", + "rustc-hash 2.0.0", + "serde", + "serde_json", +] + [[package]] name = "oxc_sourcemap" version = "0.38.0" @@ -1963,7 +1984,7 @@ dependencies = [ "cow-utils", "insta", "rayon", - "rustc-hash", + "rustc-hash 2.0.0", "serde", "serde_json", ] @@ -1995,7 +2016,7 @@ dependencies = [ "oxc_index", "oxc_span", "phf", - "rustc-hash", + "rustc-hash 2.0.0", "ryu-js", "serde", "unicode-id-start", @@ -2024,7 +2045,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.0.0", ] [[package]] @@ -2077,7 +2098,7 @@ dependencies = [ "oxc_traverse", "pico-args", "ropey", - "rustc-hash", + "rustc-hash 2.0.0", "serde", "serde_json", "sha1", @@ -2097,7 +2118,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.0.0", ] [[package]] @@ -2448,7 +2469,7 @@ dependencies = [ "oxc_span", "oxc_tasks_common", "regex", - "rustc-hash", + "rustc-hash 2.0.0", "serde", "ureq", ] @@ -2468,6 +2489,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.0.0" @@ -2599,6 +2626,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + [[package]] name = "semver" version = "1.0.23" @@ -2807,6 +2840,17 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9557cb6521e8d009c51a8666f09356f4b817ba9ba0981a305bd86aee47bd35c" +[[package]] +name = "string_wizard" +version = "0.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dda97f92b3c192214daba2a9d2e6c046edec247901234636159f2169ee7150a" +dependencies = [ + "index_vec", + "oxc_sourcemap 0.25.0", + "rustc-hash 1.1.0", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3373,7 +3417,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/napi/parser/Cargo.toml b/napi/parser/Cargo.toml index c524b64ea580f..3cc027c38b635 100644 --- a/napi/parser/Cargo.toml +++ b/napi/parser/Cargo.toml @@ -26,7 +26,9 @@ oxc_module_lexer = { workspace = true } napi = { workspace = true, features = ["async"] } napi-derive = { workspace = true } +self_cell = "1.0.4" serde_json = { workspace = true } +string_wizard = { version = "0.0.22", features = ["source_map"] } [package.metadata.cargo-shear] ignored = ["napi"] diff --git a/napi/parser/bindings.js b/napi/parser/bindings.js index 2b09b39f8c35a..b95ccde1f62e2 100644 --- a/napi/parser/bindings.js +++ b/napi/parser/bindings.js @@ -361,6 +361,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } +module.exports.ParserBuilder = nativeBinding.ParserBuilder module.exports.moduleLexerAsync = nativeBinding.moduleLexerAsync module.exports.moduleLexerSync = nativeBinding.moduleLexerSync module.exports.parseAsync = nativeBinding.parseAsync diff --git a/napi/parser/index.d.ts b/napi/parser/index.d.ts index a36ba4bec6090..c5865fb628a33 100644 --- a/napi/parser/index.d.ts +++ b/napi/parser/index.d.ts @@ -2,6 +2,30 @@ /* eslint-disable */ export * from '@oxc-project/types'; +/** A Parser instance that holds a MagicString. */ +export declare class ParserBuilder { + constructor(sourceText: string) + /** + * # Panics + * + * * File extension is invalid + * * Serde JSON serialization + */ + parseSync(options?: ParserOptions | undefined | null): ParseResult + sourceText(start: number, end: number): string + toString(): string + len(): number + append(source: string): void + appendLeft(textIndex: number, content: string): void + appendRight(textIndex: number, content: string): void + indent(): void + prepend(source: string): void + prependLeft(textIndex: number, content: string): void + relocate(start: number, end: number, to: number): void + remove(start: number, end: number): void + update(start: number, end: number, content: string): void +} + export interface Comment { type: 'Line' | 'Block' value: string @@ -89,7 +113,7 @@ export declare function moduleLexerSync(sourceText: string, options?: ParserOpti /** * # Panics * - * * Tokio crashes + * * Internal runtime (tokio) crashes */ export declare function parseAsync(sourceText: string, options?: ParserOptions | undefined | null): Promise diff --git a/napi/parser/index.js b/napi/parser/index.js index fcedfd9cf3a0f..88f9760ca5212 100644 --- a/napi/parser/index.js +++ b/napi/parser/index.js @@ -14,3 +14,11 @@ module.exports.parseSync = function parseSync(...args) { result.program = JSON.parse(result.program); return result; }; + +const parseSyncFn = bindings.ParserBuilder.prototype.parseSync; +bindings.ParserBuilder.prototype.parseSync = function parseSync(...args) { + const result = parseSyncFn.apply(this, args); + result.program = JSON.parse(result.program); + return result; +}; +module.exports.ParserBuilder = bindings.ParserBuilder; diff --git a/napi/parser/src/lib.rs b/napi/parser/src/lib.rs index 08add6965e2fd..0d80578af8f03 100644 --- a/napi/parser/src/lib.rs +++ b/napi/parser/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::needless_pass_by_value)] + mod module_lexer; use std::sync::Arc; @@ -5,6 +7,8 @@ use std::sync::Arc; use napi::{bindgen_prelude::AsyncTask, Task}; use napi_derive::napi; +use self_cell::self_cell; + use oxc::{ allocator::Allocator, ast::CommentKind, @@ -15,6 +19,152 @@ use oxc::{ }; pub use crate::module_lexer::*; +use string_wizard::MagicString; + +/// A Parser instance that holds a MagicString. +#[napi] +pub struct ParserBuilder { + cell: ParserBuilderImpl, +} + +self_cell!( + struct ParserBuilderImpl { + owner: String, + + #[covariant] + dependent: MagicString, + } +); + +#[napi] +impl ParserBuilder { + #[napi(constructor)] + pub fn new(source_text: String) -> Self { + Self { cell: ParserBuilderImpl::new(source_text, |s| MagicString::new(s)) } + } + + /// # Panics + /// + /// * File extension is invalid + /// * Serde JSON serialization + #[napi] + pub fn parse_sync(&mut self, options: Option) -> ParseResult { + let options = options.unwrap_or_default(); + // TODO: update magic string filename here. + parse_with_return(self.cell.borrow_owner(), &options) + } + + #[napi] + pub fn source_text(&self, start: u32, end: u32) -> &str { + &self.cell.borrow_owner()[start as usize..end as usize] + } + + #[napi] + #[allow(clippy::inherent_to_string)] + pub fn to_string(&self) -> String { + self.cell.borrow_dependent().to_string() + } + + #[napi] + #[allow(clippy::len_without_is_empty, clippy::cast_possible_truncation)] + pub fn len(&self) -> u32 { + self.cell.borrow_dependent().len() as u32 + } + + // #[napi] + // pub fn generate_map(&self) -> oxc::napi::source_map::SourceMap { + // let json = self.cell.borrow_dependent().source_map(SourceMapOptions::default()).to_json(); + // oxc::napi::source_map::SourceMap { + // file: json.file, + // mappings: json.mappings, + // names: json.names, + // source_root: json.source_root, + // sources: json.sources, + // sources_content: json.sources_content.map(|content| { + // content.into_iter().map(Option::unwrap_or_default).collect::>() + // }), + // version: 3, + // x_google_ignorelist: None, + // } + // } + + #[napi] + pub fn append(&mut self, source: String) { + self.cell.with_dependent_mut(|_, s| { + s.append(source); + }); + } + + #[napi] + pub fn append_left(&mut self, text_index: u32, content: String) { + self.cell.with_dependent_mut(|_, s| { + s.append_left(text_index as usize, content); + }); + } + + #[napi] + pub fn append_right(&mut self, text_index: u32, content: String) { + self.cell.with_dependent_mut(|_, s| { + s.append_left(text_index as usize, content); + }); + } + + #[napi] + pub fn indent(&mut self) { + self.cell.with_dependent_mut(|_, s| { + s.indent(); + }); + } + + // #[napi] + // pub fn indent_with(&mut self, opts: string_wizard::IndentOptions<'_, '_>) { + // self.cell.with_dependent_mut(|_, s| { + // s.indent_with(&content, opts); + // }); + // } + + #[napi] + pub fn prepend(&mut self, source: String) { + self.cell.with_dependent_mut(|_, s| { + s.prepend(source); + }); + } + + #[napi] + pub fn prepend_left(&mut self, text_index: u32, content: String) { + self.cell.with_dependent_mut(|_, s| { + s.prepend_left(text_index as usize, content); + }); + } + + #[napi] + pub fn relocate(&mut self, start: u32, end: u32, to: u32) { + self.cell.with_dependent_mut(|_, s| { + s.relocate(start as usize, end as usize, to as usize); + }); + } + + #[napi] + pub fn remove(&mut self, start: u32, end: u32) { + self.cell.with_dependent_mut(|_, s| { + s.remove(start as usize, end as usize); + }); + } + + #[napi] + pub fn update(&mut self, start: u32, end: u32, content: String) { + self.cell.with_dependent_mut(|_, s| { + s.update(start as usize, end as usize, content); + }); + } + + // #[napi] + // pub fn update_with(&mut self, start: u32, end: u32, content: String, opts: UpdateOptions) { + // self.cell.with_dependent_mut(|_, s| { + // s.update_with(start as usize, end as usize, content, opts); + // }); + // } +} fn parse<'a>( allocator: &'a Allocator, @@ -46,7 +196,6 @@ fn parse<'a>( /// /// * File extension is invalid /// * Serde JSON serialization -#[allow(clippy::needless_pass_by_value)] #[napi] pub fn parse_without_return(source_text: String, options: Option) { let options = options.unwrap_or_default(); @@ -94,7 +243,6 @@ fn parse_with_return<'a>(source_text: &'a str, options: &ParserOptions) -> Parse /// /// * File extension is invalid /// * Serde JSON serialization -#[allow(clippy::needless_pass_by_value)] #[napi] pub fn parse_sync(source_text: String, options: Option) -> ParseResult { let options = options.unwrap_or_default(); @@ -122,8 +270,7 @@ impl Task for ResolveTask { /// # Panics /// -/// * Tokio crashes -#[allow(clippy::needless_pass_by_value)] +/// * Internal runtime (tokio) crashes #[napi] pub fn parse_async(source_text: String, options: Option) -> AsyncTask { let options = options.unwrap_or_default(); diff --git a/napi/parser/test/magic_string.test.ts b/napi/parser/test/magic_string.test.ts new file mode 100644 index 0000000000000..20db304b8e379 --- /dev/null +++ b/napi/parser/test/magic_string.test.ts @@ -0,0 +1,28 @@ +import { assert, describe, it } from 'vitest'; + +import type { StringLiteral, VariableDeclaration } from '../index.js'; +import { ParserBuilder } from '../index.js'; + +describe('simple', () => { + const code = 'const s: String = "测试"'; + + it('calls magic string APIs', () => { + // `oxc` holds a magic string instance on the Rust side. + const oxc = new ParserBuilder(code); + + const ast = oxc.parseSync({ sourceFilename: 'test.ts' }).program; + const declaration = ast.body[0] as VariableDeclaration; + const stringLiteral = declaration.declarations[0].init as StringLiteral; + + // These spans are in utf8 offsets. + const start = stringLiteral.start + 1; + const end = stringLiteral.end - 1; + + // Access source text by utf8 offset. + assert.equal(oxc.sourceText(start, end), '测试'); + + // Magic string manipulation. + oxc.remove(start, end); + assert.equal(oxc.toString(), 'const s: String = ""'); + }); +}); diff --git a/napi/transform/index.d.ts b/napi/transform/index.d.ts index 5e3100e1bf82e..abac5894f1fa9 100644 --- a/napi/transform/index.d.ts +++ b/napi/transform/index.d.ts @@ -215,7 +215,7 @@ export interface TransformOptions { * * @default `esnext` (No transformation) * - * @see [esbuild#target]( /** Define Plugin */