feat(napi/parser): add source map API (#8584)

This commit is contained in:
Boshen 2025-01-18 23:06:42 +08:00 committed by GitHub
parent 62f1881ec4
commit 1bef911e59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 152 additions and 19 deletions

1
Cargo.lock generated
View file

@ -1947,6 +1947,7 @@ dependencies = [
"oxc_ast",
"oxc_data_structures",
"oxc_napi",
"oxc_sourcemap",
"rustc-hash",
"self_cell",
"serde_json",

View file

@ -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 }

View file

@ -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<GenerateDecodedMapOptions>): {
toString: () => string;
toUrl: () => string;
toMap: () => {
file?: string
mappings: string
names: Array<string>
sourceRoot?: string
sources: Array<string>
sourcesContent?: Array<string>
version: number
x_google_ignoreList?: Array<number>
}
}
}
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<string>
sourceRoot?: string
sources: Array<string>
sourcesContent?: Array<string>
version: number
x_google_ignoreList?: Array<number>
}
export interface SourceMapOptions {
includeContent?: boolean
source?: string

View file

@ -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;
},
};

View file

@ -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<bool>,
}
#[napi(object)]
pub struct GenerateDecodedMapOptions {
/// The filename of the file containing the original source.
pub source: Option<String>,
/// 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<bool, String>,
}
impl Default for GenerateDecodedMapOptions {
fn default() -> Self {
Self { source: None, include_content: false, hires: Either::A(false) }
}
}
impl From<GenerateDecodedMapOptions> 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<SourceMapOptions>) -> 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<GenerateDecodedMapOptions>",
ts_return_type = r"{
toString: () => string;
toUrl: () => string;
toMap: () => {
file?: string
mappings: string
names: Array<string>
sourceRoot?: string
sources: Array<string>
sourcesContent?: Array<string>
version: number
x_google_ignoreList?: Array<number>
}
}"
)]
pub fn generate_map(&self) {
// only for .d.ts generation
}
#[napi(skip_typescript)]
pub fn to_sourcemap_string(&self, options: Option<GenerateDecodedMapOptions>) -> String {
self.get_sourcemap(options).to_json_string()
}
#[napi(skip_typescript)]
pub fn to_sourcemap_url(&self, options: Option<GenerateDecodedMapOptions>) -> String {
self.get_sourcemap(options).to_data_url()
}
#[napi(skip_typescript)]
pub fn to_sourcemap_object(
&self,
options: Option<GenerateDecodedMapOptions>,
) -> oxc_sourcemap::napi::SourceMap {
oxc_sourcemap::napi::SourceMap::from(self.get_sourcemap(options))
}
fn get_sourcemap(
&self,
options: Option<GenerateDecodedMapOptions>,
) -> oxc_sourcemap::SourceMap {
self.cell
.borrow_dependent()
.source_map(string_wizard::SourceMapOptions::from(options.unwrap_or_default()))
}
}

View file

@ -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,
});
});
});

View file

@ -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);
});
});