From 1bef911e59e2aecf7ecd78f8d20a07e73e9fde18 Mon Sep 17 00:00:00 2001 From: Boshen Date: Sat, 18 Jan 2025 23:06:42 +0800 Subject: [PATCH] feat(napi/parser): add source map API (#8584) --- Cargo.lock | 1 + napi/parser/Cargo.toml | 2 +- napi/parser/index.d.ts | 34 +++++++++ napi/parser/index.js | 8 +- napi/parser/src/magic_string.rs | 102 ++++++++++++++++++++++---- napi/parser/test/magic_string.test.ts | 21 ++++++ napi/parser/test/parse.test.ts | 3 - 7 files changed, 152 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7cd787fae9a6..83ccc17067397 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1947,6 +1947,7 @@ dependencies = [ "oxc_ast", "oxc_data_structures", "oxc_napi", + "oxc_sourcemap", "rustc-hash", "self_cell", "serde_json", diff --git a/napi/parser/Cargo.toml b/napi/parser/Cargo.toml index e426efedfbf08..bc3d0c4d4ef05 100644 --- a/napi/parser/Cargo.toml +++ b/napi/parser/Cargo.toml @@ -25,12 +25,12 @@ oxc = { workspace = true } oxc_ast = { workspace = true, features = ["serialize"] } # enable feature only oxc_data_structures = { workspace = true } oxc_napi = { workspace = true } +oxc_sourcemap = { workspace = true, features = ["napi"] } rustc-hash = { workspace = true } self_cell = { workspace = true } serde_json = { workspace = true } string_wizard = { workspace = true, features = ["sourcemap", "serde"] } -# oxc_sourcemap = { workspace = true, features = ["napi"] } napi = { workspace = true, features = ["async"] } napi-derive = { workspace = true } diff --git a/napi/parser/index.d.ts b/napi/parser/index.d.ts index a15b100cb10df..0961371b7ad4a 100644 --- a/napi/parser/index.d.ts +++ b/napi/parser/index.d.ts @@ -20,6 +20,20 @@ export declare class MagicString { prependRight(index: number, input: string): this relocate(start: number, end: number, to: number): this remove(start: number, end: number): this + generateMap(options?: Partial): { + toString: () => string; + toUrl: () => string; + toMap: () => { + file?: string + mappings: string + names: Array + sourceRoot?: string + sources: Array + sourcesContent?: Array + version: number + x_google_ignoreList?: Array + } + } } export declare class ParseResult { @@ -121,6 +135,15 @@ export declare const enum ExportLocalNameKind { None = 'None' } +export interface GenerateDecodedMapOptions { + /** The filename of the file containing the original source. */ + source?: string + /** Whether to include the original content in the map's `sourcesContent` array. */ + includeContent: boolean + /** Whether the mapping should be high-resolution. */ + hires: boolean | 'boundary' +} + export interface ImportName { kind: ImportNameKind name?: string @@ -192,6 +215,17 @@ export declare const enum Severity { Advice = 'Advice' } +export interface SourceMap { + file?: string + mappings: string + names: Array + sourceRoot?: string + sources: Array + sourcesContent?: Array + version: number + x_google_ignoreList?: Array +} + export interface SourceMapOptions { includeContent?: boolean source?: string diff --git a/napi/parser/index.js b/napi/parser/index.js index 2cdb1fe05ed24..c2bcf920136c0 100644 --- a/napi/parser/index.js +++ b/napi/parser/index.js @@ -1,6 +1,5 @@ const bindings = require('./bindings.js'); -module.exports.MagicString = bindings.MagicString; module.exports.ParseResult = bindings.ParseResult; module.exports.ExportExportNameKind = bindings.ExportExportNameKind; module.exports.ExportImportNameKind = bindings.ExportImportNameKind; @@ -30,6 +29,13 @@ function wrap(result) { }, get magicString() { if (!magicString) magicString = result.magicString; + magicString.generateMap = function generateMap(options) { + return { + toString: () => magicString.toSourcemapString(options), + toUrl: () => magicString.toSourcemapUrl(options), + toMap: () => magicString.toSourcemapObject(options), + }; + }; return magicString; }, }; diff --git a/napi/parser/src/magic_string.rs b/napi/parser/src/magic_string.rs index 4ff652c871fee..e727fb4669efb 100644 --- a/napi/parser/src/magic_string.rs +++ b/napi/parser/src/magic_string.rs @@ -1,12 +1,12 @@ #![allow(clippy::cast_possible_truncation)] -// use std::sync::Arc; +use std::sync::Arc; +use napi::Either; use napi_derive::napi; use self_cell::self_cell; -use string_wizard::MagicString as MS; +use string_wizard::{Hires, MagicString as MS}; use oxc_data_structures::rope::{get_line_column, Rope}; -// use oxc_sourcemap::napi::SourceMap; #[napi] pub struct MagicString { @@ -49,6 +49,43 @@ pub struct SourceMapOptions { pub hires: Option, } +#[napi(object)] +pub struct GenerateDecodedMapOptions { + /// The filename of the file containing the original source. + pub source: Option, + /// Whether to include the original content in the map's `sourcesContent` array. + pub include_content: bool, + /// Whether the mapping should be high-resolution. + #[napi(ts_type = "boolean | 'boundary'")] + pub hires: Either, +} + +impl Default for GenerateDecodedMapOptions { + fn default() -> Self { + Self { source: None, include_content: false, hires: Either::A(false) } + } +} + +impl From for string_wizard::SourceMapOptions { + fn from(o: GenerateDecodedMapOptions) -> Self { + Self { + source: Arc::from(o.source.unwrap_or_default()), + include_content: o.include_content, + hires: match o.hires { + Either::A(true) => Hires::True, + Either::A(false) => Hires::False, + Either::B(s) => { + if s == "boundary" { + Hires::Boundary + } else { + Hires::False + } + } + }, + } + } +} + #[napi] impl MagicString { /// Get source text from utf8 offset. @@ -85,17 +122,6 @@ impl MagicString { self.cell.borrow_dependent().to_string() } - // #[napi] - // pub fn source_map(&self, options: Option) -> SourceMap { - // let options = options.map(|o| string_wizard::SourceMapOptions { - // include_content: o.include_content.unwrap_or_default(), - // source: o.source.map(Arc::from).unwrap_or_default(), - // hires: o.hires.unwrap_or_default(), - // }); - // let map = self.cell.borrow_dependent().source_map(options.unwrap_or_default()); - // oxc_sourcemap::napi::SourceMap::from(map) - // } - #[napi] pub fn append(&mut self, input: String) -> &Self { self.cell.with_dependent_mut(|_, ms| { @@ -167,4 +193,52 @@ impl MagicString { }); self } + + #[napi( + ts_args_type = "options?: Partial", + ts_return_type = r"{ + toString: () => string; + toUrl: () => string; + toMap: () => { + file?: string + mappings: string + names: Array + sourceRoot?: string + sources: Array + sourcesContent?: Array + version: number + x_google_ignoreList?: Array + } + }" + )] + pub fn generate_map(&self) { + // only for .d.ts generation + } + + #[napi(skip_typescript)] + pub fn to_sourcemap_string(&self, options: Option) -> String { + self.get_sourcemap(options).to_json_string() + } + + #[napi(skip_typescript)] + pub fn to_sourcemap_url(&self, options: Option) -> String { + self.get_sourcemap(options).to_data_url() + } + + #[napi(skip_typescript)] + pub fn to_sourcemap_object( + &self, + options: Option, + ) -> oxc_sourcemap::napi::SourceMap { + oxc_sourcemap::napi::SourceMap::from(self.get_sourcemap(options)) + } + + fn get_sourcemap( + &self, + options: Option, + ) -> oxc_sourcemap::SourceMap { + self.cell + .borrow_dependent() + .source_map(string_wizard::SourceMapOptions::from(options.unwrap_or_default())) + } } diff --git a/napi/parser/test/magic_string.test.ts b/napi/parser/test/magic_string.test.ts index 6a8541c5186f1..23a4e4a8b110b 100644 --- a/napi/parser/test/magic_string.test.ts +++ b/napi/parser/test/magic_string.test.ts @@ -32,4 +32,25 @@ describe('simple', () => { ms.remove(start, end).append(';'); expect(ms.toString()).toEqual('const s: String = /* 🤨 */ "";'); }); + + it('returns sourcemap', () => { + const { magicString: ms } = parseSync('test.ts', code); + ms.indent(); + const map = ms.generateMap({ + source: 'test.ts', + includeContent: true, + hires: true, + }); + expect(map.toUrl()).toBeTypeOf('string'); + expect(map.toString()).toBeTypeOf('string'); + console.log(map.toMap()); + expect(map.toMap()).toEqual({ + mappings: + 'CAAA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC', + names: [], + sources: ['test.ts'], + sourcesContent: ['const s: String = /* 🤨 */ "测试"'], + version: 3, + }); + }); }); diff --git a/napi/parser/test/parse.test.ts b/napi/parser/test/parse.test.ts index 1afc623dc90e3..85ba729308e30 100644 --- a/napi/parser/test/parse.test.ts +++ b/napi/parser/test/parse.test.ts @@ -25,9 +25,6 @@ describe('parse', () => { 'value': ' comment ', }); expect(code.substring(comment.start, comment.end)).toBe('/*' + comment.value + '*/'); - - const ret2 = await parseAsync('test.js', code); - expect(ret).toEqual(ret2); }); });