From c26955ee90290e92109e15b26e80edfe93fd9bff Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Fri, 21 Feb 2025 13:30:07 +0000 Subject: [PATCH] fix(wasm): transfer AST to JS as JSON string in `oxc-wasm` --- Cargo.lock | 1 - crates/oxc_wasm/Cargo.toml | 1 - crates/oxc_wasm/src/lib.rs | 25 +++++---------- crates/oxc_wasm/update-bindings.mjs | 50 +++++++++++++++++++++++++++++ justfile | 1 + napi/parser/index.js | 4 +-- npm/oxc-wasm/oxc_wasm.d.ts | 3 +- npm/oxc-wasm/oxc_wasm.js | 32 +++++++++++++++--- npm/oxc-wasm/oxc_wasm_bg.wasm.d.ts | 2 +- wasm/parser/update-bindings.mjs | 4 +-- 10 files changed, 94 insertions(+), 29 deletions(-) create mode 100644 crates/oxc_wasm/update-bindings.mjs diff --git a/Cargo.lock b/Cargo.lock index 64941e17fc544..21f3a04fb3151 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2246,7 +2246,6 @@ dependencies = [ "oxc_prettier", "serde", "serde-wasm-bindgen", - "serde_json", "tsify", "wasm-bindgen", ] diff --git a/crates/oxc_wasm/Cargo.toml b/crates/oxc_wasm/Cargo.toml index 07cb02ac5fcd6..bbf1250313057 100644 --- a/crates/oxc_wasm/Cargo.toml +++ b/crates/oxc_wasm/Cargo.toml @@ -28,6 +28,5 @@ oxc_prettier = { workspace = true } console_error_panic_hook = { workspace = true } serde = { workspace = true } serde-wasm-bindgen = { workspace = true } -serde_json = { workspace = true } tsify = { workspace = true } wasm-bindgen = { workspace = true } diff --git a/crates/oxc_wasm/src/lib.rs b/crates/oxc_wasm/src/lib.rs index 40ec2bea9482d..cff4605e2d395 100644 --- a/crates/oxc_wasm/src/lib.rs +++ b/crates/oxc_wasm/src/lib.rs @@ -42,9 +42,14 @@ export * from "@oxc-project/types"; #[derive(Default, Tsify)] #[serde(rename_all = "camelCase")] pub struct Oxc { - #[wasm_bindgen(readonly, skip_typescript)] + // Dummy field, only present to make `tsify` include it in the type definition for `Oxc`. + // The getter for this field in WASM bindings is generated by `update-bindings.mjs` script. + #[wasm_bindgen(skip)] #[tsify(type = "Program")] - pub ast: JsValue, + pub ast: (), + + #[wasm_bindgen(readonly, skip_typescript, js_name = astJson)] + pub ast_json: String, #[wasm_bindgen(readonly, skip_typescript)] pub ir: String, @@ -466,22 +471,8 @@ impl Oxc { } fn convert_ast(&mut self, program: &mut Program) { - use serde::Deserialize; - Utf8ToUtf16::new().convert(program); - - // Convert: - // 1. `Program` to JSON string using `ESTree`. - // 2. JSON string to `serde_json::Value`. - // 3. `serde_json::Value` to `wasm_bindgen::JsValue`. - // TODO: There has to be a better way! - let json = program.to_json(); - let s = serde_json::de::StrRead::new(&json); - let mut deserializer = serde_json::Deserializer::new(s); - let value = serde_json::Value::deserialize(&mut deserializer).unwrap(); - deserializer.end().unwrap(); - self.ast = value.serialize(&self.serializer).unwrap(); - + self.ast_json = program.to_pretty_json(); self.comments = Self::map_comments(program.source_text, &program.comments); } diff --git a/crates/oxc_wasm/update-bindings.mjs b/crates/oxc_wasm/update-bindings.mjs new file mode 100644 index 0000000000000..952115bc0e069 --- /dev/null +++ b/crates/oxc_wasm/update-bindings.mjs @@ -0,0 +1,50 @@ +// Script to inject code for an extra `ast` getter on `class Oxc` in WASM binding file. + +import assert from 'assert'; +import { readFileSync, writeFileSync } from 'fs'; +import { join as pathJoin } from 'path'; +import { fileURLToPath } from 'url'; + +const path = pathJoin(fileURLToPath(import.meta.url), '../../../npm/oxc-wasm/oxc_wasm.js'); + +// Extra getter on `Oxc` `get ast() { ... }` that gets the AST as JSON string, +// and parses it to a `Program` object. +// +// JSON parsing uses a reviver function that sets `value` field of `Literal`s for `BigInt`s and `RegExp`s. +// This is not possible to do on Rust side, as neither can be represented correctly in JSON. +// Invalid regexp, or valid regexp using syntax not supported by the platform is ignored. +// +// Note: This code is repeated in `napi/parser/index.js` and `wasm/parser/update-bindings.mjs`. +// Any changes should be applied in those 2 places too. +// +// Unlike `wasm/parser/update-bindings.mjs`, the getter does not cache the `JSON.parse`-ed value, +// because I (@overlookmotel) believe that the `Oxc` class instance is used as a singleton in playground, +// and the value of `astJson` may change after the source text is changed. +// TODO: Check this assumption is correct. +const getterCode = ` + get ast() { + return JSON.parse(this.astJson, function(key, value) { + if (value === null && key === 'value' && Object.hasOwn(this, 'type') && this.type === 'Literal') { + if (Object.hasOwn(this, 'bigint')) { + return BigInt(this.bigint); + } + if (Object.hasOwn(this, 'regex')) { + const { regex } = this; + try { + return RegExp(regex.pattern, regex.flags); + } catch (_err) {} + } + } + return value; + }); + } +`.trimEnd().replace(/ /g, ' '); + +const insertGetterAfter = 'export class Oxc {'; + +const code = readFileSync(path, 'utf8'); +const parts = code.split(insertGetterAfter); +assert(parts.length === 2); +const [before, after] = parts; +const updatedCode = [before, insertGetterAfter, getterCode, after].join(''); +writeFileSync(path, updatedCode); diff --git a/justfile b/justfile index b7e4746ba658d..36d693e9cf188 100755 --- a/justfile +++ b/justfile @@ -161,6 +161,7 @@ build-wasm mode="release": wasm-pack build crates/oxc_wasm --no-pack --target web --scope oxc --out-dir ../../npm/oxc-wasm --{{mode}} cp crates/oxc_wasm/package.json npm/oxc-wasm/package.json rm npm/oxc-wasm/.gitignore + node ./crates/oxc_wasm/update-bindings.mjs # Generate the JavaScript global variables. See `tasks/javascript_globals` javascript-globals: diff --git a/napi/parser/index.js b/napi/parser/index.js index 5a0f6658d4378..dad8d1a6dd690 100644 --- a/napi/parser/index.js +++ b/napi/parser/index.js @@ -13,8 +13,8 @@ function wrap(result) { return { get program() { if (!program) { - // Note: This code is repeated in `wasm/parser/update-bindings.mjs`. - // Any changes should be applied in both places. + // Note: This code is repeated in `wasm/parser/update-bindings.mjs` and `crates/oxc-wasm/update-bindings.mjs`. + // Any changes should be applied in those 2 scripts too. program = JSON.parse(result.program, function(key, value) { // Set `value` field of `Literal`s for `BigInt`s and `RegExp`s. // This is not possible to do on Rust side, as neither can be represented correctly in JSON. diff --git a/npm/oxc-wasm/oxc_wasm.d.ts b/npm/oxc-wasm/oxc_wasm.d.ts index 0f43ad47e4b99..a3639b0191b87 100644 --- a/npm/oxc-wasm/oxc_wasm.d.ts +++ b/npm/oxc-wasm/oxc_wasm.d.ts @@ -70,6 +70,7 @@ export * from "@oxc-project/types"; export interface Oxc { ast: Program; + astJson: string; ir: string; controlFlowGraph: string; symbols: any; @@ -146,7 +147,7 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl export interface InitOutput { readonly memory: WebAssembly.Memory; readonly __wbg_oxc_free: (a: number, b: number) => void; - readonly __wbg_get_oxc_ast: (a: number) => any; + readonly __wbg_get_oxc_astJson: (a: number) => [number, number]; readonly __wbg_get_oxc_ir: (a: number) => [number, number]; readonly __wbg_get_oxc_controlFlowGraph: (a: number) => [number, number]; readonly __wbg_get_oxc_symbols: (a: number) => any; diff --git a/npm/oxc-wasm/oxc_wasm.js b/npm/oxc-wasm/oxc_wasm.js index adca2d5c68f7c..1828bdc12712a 100644 --- a/npm/oxc-wasm/oxc_wasm.js +++ b/npm/oxc-wasm/oxc_wasm.js @@ -203,6 +203,22 @@ const OxcFinalization = (typeof FinalizationRegistry === 'undefined') : new FinalizationRegistry(ptr => wasm.__wbg_oxc_free(ptr >>> 0, 1)); export class Oxc { + get ast() { + return JSON.parse(this.astJson, function(key, value) { + if (value === null && key === 'value' && Object.hasOwn(this, 'type') && this.type === 'Literal') { + if (Object.hasOwn(this, 'bigint')) { + return BigInt(this.bigint); + } + if (Object.hasOwn(this, 'regex')) { + const { regex } = this; + try { + return RegExp(regex.pattern, regex.flags); + } catch (_err) {} + } + } + return value; + }); + } __destroy_into_raw() { const ptr = this.__wbg_ptr; @@ -216,11 +232,19 @@ export class Oxc { wasm.__wbg_oxc_free(ptr, 0); } /** - * @returns {any} + * @returns {string} */ - get ast() { - const ret = wasm.__wbg_get_oxc_ast(this.__wbg_ptr); - return ret; + get astJson() { + let deferred1_0; + let deferred1_1; + try { + const ret = wasm.__wbg_get_oxc_astJson(this.__wbg_ptr); + deferred1_0 = ret[0]; + deferred1_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + } finally { + wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); + } } /** * @returns {string} diff --git a/npm/oxc-wasm/oxc_wasm_bg.wasm.d.ts b/npm/oxc-wasm/oxc_wasm_bg.wasm.d.ts index e123cfaa2cfe1..e683dbc7bca5d 100644 --- a/npm/oxc-wasm/oxc_wasm_bg.wasm.d.ts +++ b/npm/oxc-wasm/oxc_wasm_bg.wasm.d.ts @@ -2,7 +2,7 @@ /* eslint-disable */ export const memory: WebAssembly.Memory; export const __wbg_oxc_free: (a: number, b: number) => void; -export const __wbg_get_oxc_ast: (a: number) => any; +export const __wbg_get_oxc_astJson: (a: number) => [number, number]; export const __wbg_get_oxc_ir: (a: number) => [number, number]; export const __wbg_get_oxc_controlFlowGraph: (a: number) => [number, number]; export const __wbg_get_oxc_symbols: (a: number) => any; diff --git a/wasm/parser/update-bindings.mjs b/wasm/parser/update-bindings.mjs index 55ab9e627726e..66aad63235e25 100644 --- a/wasm/parser/update-bindings.mjs +++ b/wasm/parser/update-bindings.mjs @@ -18,8 +18,8 @@ const bindingFilename = 'oxc_parser_wasm.js'; // // The getter caches the result to avoid re-parsing JSON every time `result.program` is accessed. // -// Note: This code is repeated in `napi/parser/index.js`. -// Any changes should be applied in both places. +// Note: This code is repeated in `napi/parser/index.js` and `crates/oxc-wasm/update-bindings.mjs`. +// Any changes should be applied in those 2 places too. const getterCode = ` __program;