diff --git a/.cargo/config.toml b/.cargo/config.toml index 239cf0e41e2f8..1394a0b0b1365 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -10,3 +10,31 @@ rule = "run -p rulegen" # Build oxlint in release mode oxlint = "build --release --bin oxlint --features allocator" + +# Fix napi breaking in test environment +# To be able to run unit tests on macOS, support compilation to 'x86_64-apple-darwin'. +[target.'cfg(target_vendor = "apple")'] +rustflags = ["-C", "link-args=-Wl,-undefined,dynamic_lookup,-no_fixup_chains"] + +# To be able to run unit tests on Linux, support compilation to 'x86_64-unknown-linux-gnu'. +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-args=-Wl,--warn-unresolved-symbols"] + +# To be able to run unit tests on Windows, support compilation to 'x86_64-pc-windows-msvc'. +# Use Hybrid CRT to reduce the size of the binary (Coming by default with Windows 10 and later versions). +[target.'cfg(target_os = "windows")'] +rustflags = [ + "-C", + "link-args=/FORCE", + "-C", + "link-args=/NODEFAULTLIB:libucrt.lib", + "-C", + "link-args=/DEFAULTLIB:ucrt.lib", +] + +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] +[target.i686-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] +[target.aarch64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/crates/oxc/src/napi/parse.rs b/crates/oxc/src/napi/parse.rs index 2406a04b417f0..f94496636cb5e 100644 --- a/crates/oxc/src/napi/parse.rs +++ b/crates/oxc/src/napi/parse.rs @@ -1,8 +1,11 @@ use napi_derive::napi; +use rustc_hash::FxHashMap; + +use oxc_span::Span; +use oxc_syntax::module_record::{self, ModuleRecord}; + /// Babel Parser Options -/// -/// #[napi(object)] #[derive(Default)] pub struct ParserOptions { @@ -23,6 +26,7 @@ pub struct ParserOptions { pub struct ParseResult { #[napi(ts_type = "import(\"@oxc-project/types\").Program")] pub program: String, + pub module: EcmaScriptModule, pub comments: Vec, pub errors: Vec, } @@ -35,3 +39,324 @@ pub struct Comment { pub start: u32, pub end: u32, } + +#[napi(object)] +pub struct EcmaScriptModule { + /// Import Statements. + pub static_imports: Vec, + /// Export Statements. + pub static_exports: Vec, +} + +#[napi(object)] +pub struct ValueSpan { + pub value: String, + pub start: u32, + pub end: u32, +} + +#[napi(object)] +pub struct StaticImport { + /// Start of import statement. + pub start: u32, + /// End of import statement. + pub end: u32, + /// Import source. + /// + /// ```js + /// import { foo } from "mod"; + /// // ^^^ + /// ``` + pub module_request: ValueSpan, + /// Import specifiers. + /// + /// Empty for `import "mod"`. + pub entries: Vec, +} + +#[napi(object)] +pub struct ImportEntry { + /// The name under which the desired binding is exported by the module. + /// + /// ```js + /// import { foo } from "mod"; + /// // ^^^ + /// import { foo as bar } from "mod"; + /// // ^^^ + /// ``` + pub import_name: ImportName, + /// The name that is used to locally access the imported value from within the importing module. + /// ```js + /// import { foo } from "mod"; + /// // ^^^ + /// import { foo as bar } from "mod"; + /// // ^^^ + /// ``` + pub local_name: ValueSpan, + + /// Whether this binding is for a TypeScript type-only import. + /// + /// `true` for the following imports: + /// ```ts + /// import type { foo } from "mod"; + /// import { type foo } from "mod"; + /// ``` + pub is_type: bool, +} + +#[napi(string_enum)] +pub enum ImportNameKind { + /// `import { x } from "mod"` + Name, + /// `import * as ns from "mod"` + NamespaceObject, + /// `import defaultExport from "mod"` + Default, +} + +#[napi(object)] +pub struct ImportName { + pub kind: ImportNameKind, + pub name: Option, + pub start: Option, + pub end: Option, +} + +#[napi(object)] +pub struct StaticExport { + pub start: u32, + pub end: u32, + pub entries: Vec, +} + +#[napi(object)] +pub struct ExportEntry { + pub start: u32, + pub end: u32, + pub module_request: Option, + /// The name under which the desired binding is exported by the module`. + pub import_name: ExportImportName, + /// The name used to export this binding by this module. + pub export_name: ExportExportName, + /// The name that is used to locally access the exported value from within the importing module. + pub local_name: ExportLocalName, +} + +#[napi(string_enum)] +pub enum ExportImportNameKind { + /// `export { name } + Name, + /// `export * as ns from "mod"` + All, + /// `export * from "mod"` + AllButDefault, + /// Does not have a specifier. + None, +} + +#[napi(object)] +pub struct ExportImportName { + pub kind: ExportImportNameKind, + pub name: Option, + pub start: Option, + pub end: Option, +} + +#[napi(string_enum)] +pub enum ExportExportNameKind { + /// `export { name } + Name, + /// `export default expression` + Default, + /// `export * from "mod" + None, +} + +#[napi(object)] +pub struct ExportExportName { + pub kind: ExportExportNameKind, + pub name: Option, + pub start: Option, + pub end: Option, +} + +#[napi(object)] +pub struct ExportLocalName { + pub kind: ExportLocalNameKind, + pub name: Option, + pub start: Option, + pub end: Option, +} + +#[napi(string_enum)] +pub enum ExportLocalNameKind { + /// `export { name } + Name, + /// `export default expression` + Default, + /// If the exported value is not locally accessible from within the module. + /// `export default function () {}` + None, +} + +impl From<&ModuleRecord<'_>> for EcmaScriptModule { + fn from(record: &ModuleRecord<'_>) -> Self { + let mut static_imports = record + .requested_modules + .iter() + .flat_map(|(name, requested_modules)| { + requested_modules.iter().filter(|m| m.is_import).map(|m| { + let entries = record + .import_entries + .iter() + .filter(|e| e.statement_span == m.statement_span) + .map(ImportEntry::from) + .collect::>(); + { + StaticImport { + start: m.statement_span.start, + end: m.statement_span.end, + module_request: ValueSpan { + value: name.to_string(), + start: m.span.start, + end: m.span.end, + }, + entries, + } + } + }) + }) + .collect::>(); + static_imports.sort_unstable_by_key(|e| e.start); + + let mut static_exports = record + .local_export_entries + .iter() + .chain(record.indirect_export_entries.iter()) + .chain(record.star_export_entries.iter()) + .map(|e| (e.statement_span, ExportEntry::from(e))) + .collect::>() + .into_iter() + .fold(FxHashMap::>::default(), |mut acc, (span, e)| { + acc.entry(span).or_default().push(e); + acc + }) + .into_iter() + .map(|(span, entries)| StaticExport { start: span.start, end: span.end, entries }) + .collect::>(); + static_exports.sort_unstable_by_key(|e| e.start); + + Self { static_imports, static_exports } + } +} + +impl From<&module_record::ImportEntry<'_>> for ImportEntry { + fn from(e: &module_record::ImportEntry<'_>) -> Self { + Self { + import_name: ImportName::from(&e.import_name), + local_name: ValueSpan::from(&e.local_name), + is_type: e.is_type, + } + } +} + +impl From<&module_record::ImportImportName<'_>> for ImportName { + fn from(e: &module_record::ImportImportName<'_>) -> Self { + let (kind, name, start, end) = match e { + module_record::ImportImportName::Name(name_span) => ( + ImportNameKind::Name, + Some(name_span.name.to_string()), + Some(name_span.span.start), + Some(name_span.span.end), + ), + module_record::ImportImportName::NamespaceObject => { + (ImportNameKind::NamespaceObject, None, None, None) + } + module_record::ImportImportName::Default(span) => { + (ImportNameKind::Default, None, Some(span.start), Some(span.end)) + } + }; + Self { kind, name, start, end } + } +} + +impl From<&module_record::NameSpan<'_>> for ValueSpan { + fn from(name_span: &module_record::NameSpan) -> Self { + Self { + value: name_span.name.to_string(), + start: name_span.span.start, + end: name_span.span.end, + } + } +} + +impl From<&module_record::ExportEntry<'_>> for ExportEntry { + fn from(e: &module_record::ExportEntry) -> Self { + Self { + start: e.span.start, + end: e.span.end, + module_request: e.module_request.as_ref().map(ValueSpan::from), + import_name: ExportImportName::from(&e.import_name), + export_name: ExportExportName::from(&e.export_name), + local_name: ExportLocalName::from(&e.local_name), + } + } +} + +impl From<&module_record::ExportImportName<'_>> for ExportImportName { + fn from(e: &module_record::ExportImportName<'_>) -> Self { + let (kind, name, start, end) = match e { + module_record::ExportImportName::Name(name_span) => ( + ExportImportNameKind::Name, + Some(name_span.name.to_string()), + Some(name_span.span.start), + Some(name_span.span.end), + ), + module_record::ExportImportName::All => (ExportImportNameKind::All, None, None, None), + module_record::ExportImportName::AllButDefault => { + (ExportImportNameKind::AllButDefault, None, None, None) + } + module_record::ExportImportName::Null => (ExportImportNameKind::None, None, None, None), + }; + Self { kind, name, start, end } + } +} + +impl From<&module_record::ExportExportName<'_>> for ExportExportName { + fn from(e: &module_record::ExportExportName<'_>) -> Self { + let (kind, name, start, end) = match e { + module_record::ExportExportName::Name(name_span) => ( + ExportExportNameKind::Name, + Some(name_span.name.to_string()), + Some(name_span.span.start), + Some(name_span.span.end), + ), + module_record::ExportExportName::Default(span) => { + (ExportExportNameKind::Default, None, Some(span.start), Some(span.end)) + } + module_record::ExportExportName::Null => (ExportExportNameKind::None, None, None, None), + }; + Self { kind, name, start, end } + } +} + +impl From<&module_record::ExportLocalName<'_>> for ExportLocalName { + fn from(e: &module_record::ExportLocalName<'_>) -> Self { + let (kind, name, start, end) = match e { + module_record::ExportLocalName::Name(name_span) => ( + ExportLocalNameKind::Name, + Some(name_span.name.to_string()), + Some(name_span.span.start), + Some(name_span.span.end), + ), + module_record::ExportLocalName::Default(name_span) => ( + ExportLocalNameKind::Default, + Some(name_span.name.to_string()), + Some(name_span.span.start), + Some(name_span.span.end), + ), + module_record::ExportLocalName::Null => (ExportLocalNameKind::None, None, None, None), + }; + Self { kind, name, start, end } + } +} diff --git a/crates/oxc_syntax/src/module_record.rs b/crates/oxc_syntax/src/module_record.rs index 38320038e6041..21836d8272c37 100644 --- a/crates/oxc_syntax/src/module_record.rs +++ b/crates/oxc_syntax/src/module_record.rs @@ -166,11 +166,11 @@ pub struct ImportEntry<'a> { /// `ImportName` For `ImportEntry` #[derive(Debug, Clone, PartialEq, Eq)] pub enum ImportImportName<'a> { - /// Name + /// `import { x } from "mod"` Name(NameSpan<'a>), - /// Namespace Object + /// `import * as ns from "mod"` NamespaceObject, - /// Default + /// `import defaultExport from "mod"` Default(Span), } diff --git a/napi/parser/bindings.js b/napi/parser/bindings.js index 4e3a99cf03ac3..e2a65710d14fc 100644 --- a/napi/parser/bindings.js +++ b/napi/parser/bindings.js @@ -361,6 +361,10 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } +module.exports.ExportExportNameKind = nativeBinding.ExportExportNameKind +module.exports.ExportImportNameKind = nativeBinding.ExportImportNameKind +module.exports.ExportLocalNameKind = nativeBinding.ExportLocalNameKind +module.exports.ImportNameKind = nativeBinding.ImportNameKind module.exports.parseAsync = nativeBinding.parseAsync module.exports.parseSync = nativeBinding.parseSync module.exports.parseWithoutReturn = nativeBinding.parseWithoutReturn diff --git a/napi/parser/index.d.ts b/napi/parser/index.d.ts index 323408991814e..3112c64280e33 100644 --- a/napi/parser/index.d.ts +++ b/napi/parser/index.d.ts @@ -9,24 +9,143 @@ export interface Comment { end: number } +export interface EcmaScriptModule { + /** Import Statements. */ + staticImports: Array + /** Export Statements. */ + staticExports: Array +} + +export interface ExportEntry { + start: number + end: number + moduleRequest?: ValueSpan + /** The name under which the desired binding is exported by the module`. */ + importName: ExportImportName + /** The name used to export this binding by this module. */ + exportName: ExportExportName + /** The name that is used to locally access the exported value from within the importing module. */ + localName: ExportLocalName +} + +export interface ExportExportName { + kind: ExportExportNameKind + name?: string + start?: number + end?: number +} + +export declare const enum ExportExportNameKind { + /** `export { name } */ + Name = 'Name', + /** `export default expression` */ + Default = 'Default', + /** `export * from "mod" */ + None = 'None' +} + +export interface ExportImportName { + kind: ExportImportNameKind + name?: string + start?: number + end?: number +} + +export declare const enum ExportImportNameKind { + /** `export { name } */ + Name = 'Name', + /** `export * as ns from "mod"` */ + All = 'All', + /** `export * from "mod"` */ + AllButDefault = 'AllButDefault', + /** Does not have a specifier. */ + None = 'None' +} + +export interface ExportLocalName { + kind: ExportLocalNameKind + name?: string + start?: number + end?: number +} + +export declare const enum ExportLocalNameKind { + /** `export { name } */ + Name = 'Name', + /** `export default expression` */ + Default = 'Default', + /** + * If the exported value is not locally accessible from within the module. + * `export default function () {}` + */ + None = 'None' +} + +export interface ImportEntry { + /** + * The name under which the desired binding is exported by the module. + * + * ```js + * import { foo } from "mod"; + * // ^^^ + * import { foo as bar } from "mod"; + * // ^^^ + * ``` + */ + importName: ImportName + /** + * The name that is used to locally access the imported value from within the importing module. + * ```js + * import { foo } from "mod"; + * // ^^^ + * import { foo as bar } from "mod"; + * // ^^^ + * ``` + */ + localName: ValueSpan + /** + * Whether this binding is for a TypeScript type-only import. + * + * `true` for the following imports: + * ```ts + * import type { foo } from "mod"; + * import { type foo } from "mod"; + * ``` + */ + isType: boolean +} + +export interface ImportName { + kind: ImportNameKind + name?: string + start?: number + end?: number +} + +export declare const enum ImportNameKind { + /** `import { x } from "mod"` */ + Name = 'Name', + /** `import * as ns from "mod"` */ + NamespaceObject = 'NamespaceObject', + /** `import defaultExport from "mod"` */ + Default = 'Default' +} + /** - * # Panics + * Parse asynchronously. * - * * Tokio crashes + * Note: This function can be slower than `parseSync` due to the overhead of spawning a thread. */ export declare function parseAsync(sourceText: string, options?: ParserOptions | undefined | null): Promise export interface ParseResult { program: import("@oxc-project/types").Program + module: EcmaScriptModule comments: Array errors: Array } -/** - * Babel Parser Options - * - * - */ +/** Babel Parser Options */ export interface ParserOptions { sourceType?: 'script' | 'module' | 'unambiguous' | undefined sourceFilename?: string @@ -42,22 +161,47 @@ export interface ParserOptions { preserveParens?: boolean } -/** - * # Panics - * - * * File extension is invalid - * * Serde JSON serialization - */ +/** Parse synchronously. */ export declare function parseSync(sourceText: string, options?: ParserOptions | undefined | null): ParseResult /** * Parse without returning anything. - * This is for benchmark purposes such as measuring napi communication overhead. * - * # Panics - * - * * File extension is invalid - * * Serde JSON serialization + * This is for benchmark purposes such as measuring napi communication overhead. */ export declare function parseWithoutReturn(sourceText: string, options?: ParserOptions | undefined | null): void +export interface StaticExport { + start: number + end: number + entries: Array +} + +export interface StaticImport { + /** Start of import statement. */ + start: number + /** End of import statement. */ + end: number + /** + * Import source. + * + * ```js + * import { foo } from "mod"; + * // ^^^ + * ``` + */ + moduleRequest: ValueSpan + /** + * Import specifiers. + * + * Empty for `import "mod"`. + */ + entries: Array +} + +export interface ValueSpan { + value: string + start: number + end: number +} + diff --git a/napi/parser/package.json b/napi/parser/package.json index 10737c734a0ec..571cfc061572f 100644 --- a/napi/parser/package.json +++ b/napi/parser/package.json @@ -3,6 +3,7 @@ "private": true, "scripts": { "build": "napi build --platform --release --js bindings.js", + "build-dev": "napi build --platform --js bindings.js", "test": "vitest --typecheck run ./test" }, "napi": { diff --git a/napi/parser/src/lib.rs b/napi/parser/src/lib.rs index 2bd8fd724bab4..071afddf852ee 100644 --- a/napi/parser/src/lib.rs +++ b/napi/parser/src/lib.rs @@ -11,7 +11,7 @@ use oxc::{ allocator::Allocator, ast::CommentKind, diagnostics::{Error, NamedSource}, - napi::parse::{Comment, ParseResult, ParserOptions}, + napi::parse::{Comment, EcmaScriptModule, ParseResult, ParserOptions}, parser::{ParseOptions, Parser, ParserReturn}, span::SourceType, }; @@ -81,7 +81,8 @@ fn parse_with_return(source_text: &str, options: &ParserOptions) -> ParseResult }) .collect::>(); - ParseResult { program, comments, errors } + let module = EcmaScriptModule::from(&ret.module_record); + ParseResult { program, module, comments, errors } } /// Parse synchronously. diff --git a/napi/parser/test/__snapshots__/esm.test.ts.snap b/napi/parser/test/__snapshots__/esm.test.ts.snap new file mode 100644 index 0000000000000..b91c93c2b166a --- /dev/null +++ b/napi/parser/test/__snapshots__/esm.test.ts.snap @@ -0,0 +1,1398 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`esm > export * as name1 from "module-name"; 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 37, + "entries": [ + { + "start": 0, + "end": 37, + "moduleRequest": { + "value": "module-name", + "start": 23, + "end": 36 + }, + "importName": { + "kind": "All" + }, + "exportName": { + "kind": "Name", + "name": "name1", + "start": 12, + "end": 17 + }, + "localName": { + "kind": "None" + } + } + ] + } + ] +}" +`; + +exports[`esm > export * from "module-name"; 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 28, + "entries": [ + { + "start": 0, + "end": 28, + "moduleRequest": { + "value": "module-name", + "start": 14, + "end": 27 + }, + "importName": { + "kind": "AllButDefault" + }, + "exportName": { + "kind": "None" + }, + "localName": { + "kind": "None" + } + } + ] + } + ] +}" +`; + +exports[`esm > export { default as name1 } from "module-name"; 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 47, + "entries": [ + { + "start": 9, + "end": 25, + "moduleRequest": { + "value": "module-name", + "start": 33, + "end": 46 + }, + "importName": { + "kind": "Name", + "name": "default", + "start": 9, + "end": 16 + }, + "exportName": { + "kind": "Name", + "name": "name1", + "start": 20, + "end": 25 + }, + "localName": { + "kind": "None" + } + } + ] + } + ] +}" +`; + +exports[`esm > export { default, /* …, */ } from "module-name"; 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 50, + "entries": [ + { + "start": 9, + "end": 16, + "moduleRequest": { + "value": "module-name", + "start": 36, + "end": 49 + }, + "importName": { + "kind": "Name", + "name": "default", + "start": 9, + "end": 16 + }, + "exportName": { + "kind": "Name", + "name": "default", + "start": 9, + "end": 16 + }, + "localName": { + "kind": "None" + } + } + ] + } + ] +}" +`; + +exports[`esm > export { import1 as name1, import2 as name2, /* …, */ nameN } from "module-name"; 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 83, + "entries": [ + { + "start": 9, + "end": 25, + "moduleRequest": { + "value": "module-name", + "start": 69, + "end": 82 + }, + "importName": { + "kind": "Name", + "name": "import1", + "start": 9, + "end": 16 + }, + "exportName": { + "kind": "Name", + "name": "name1", + "start": 20, + "end": 25 + }, + "localName": { + "kind": "None" + } + }, + { + "start": 27, + "end": 43, + "moduleRequest": { + "value": "module-name", + "start": 69, + "end": 82 + }, + "importName": { + "kind": "Name", + "name": "import2", + "start": 27, + "end": 34 + }, + "exportName": { + "kind": "Name", + "name": "name2", + "start": 38, + "end": 43 + }, + "localName": { + "kind": "None" + } + }, + { + "start": 56, + "end": 61, + "moduleRequest": { + "value": "module-name", + "start": 69, + "end": 82 + }, + "importName": { + "kind": "Name", + "name": "nameN", + "start": 56, + "end": 61 + }, + "exportName": { + "kind": "Name", + "name": "nameN", + "start": 56, + "end": 61 + }, + "localName": { + "kind": "None" + } + } + ] + } + ] +}" +`; + +exports[`esm > export { name1 as default /*, … */ }; 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 39, + "entries": [ + { + "start": 9, + "end": 25, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "default", + "start": 18, + "end": 25 + }, + "localName": { + "kind": "Name", + "name": "name1", + "start": 9, + "end": 14 + } + } + ] + } + ] +}" +`; + +exports[`esm > export { name1, /* …, */ nameN } from "module-name"; 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 54, + "entries": [ + { + "start": 9, + "end": 14, + "moduleRequest": { + "value": "module-name", + "start": 40, + "end": 53 + }, + "importName": { + "kind": "Name", + "name": "name1", + "start": 9, + "end": 14 + }, + "exportName": { + "kind": "Name", + "name": "name1", + "start": 9, + "end": 14 + }, + "localName": { + "kind": "None" + } + }, + { + "start": 27, + "end": 32, + "moduleRequest": { + "value": "module-name", + "start": 40, + "end": 53 + }, + "importName": { + "kind": "Name", + "name": "nameN", + "start": 27, + "end": 32 + }, + "exportName": { + "kind": "Name", + "name": "nameN", + "start": 27, + "end": 32 + }, + "localName": { + "kind": "None" + } + } + ] + } + ] +}" +`; + +exports[`esm > export { name1, /* …, */ nameN }; 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 35, + "entries": [ + { + "start": 9, + "end": 14, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "name1", + "start": 9, + "end": 14 + }, + "localName": { + "kind": "Name", + "name": "name1", + "start": 9, + "end": 14 + } + }, + { + "start": 27, + "end": 32, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "nameN", + "start": 27, + "end": 32 + }, + "localName": { + "kind": "Name", + "name": "nameN", + "start": 27, + "end": 32 + } + } + ] + } + ] +}" +`; + +exports[`esm > export { variable1 as "string name" }; 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 38, + "entries": [ + { + "start": 9, + "end": 35, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "string name", + "start": 22, + "end": 35 + }, + "localName": { + "kind": "Name", + "name": "variable1", + "start": 9, + "end": 18 + } + } + ] + } + ] +}" +`; + +exports[`esm > export { variable1 as name1, variable2 as name2, /* …, */ nameN }; 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 68, + "entries": [ + { + "start": 9, + "end": 27, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "name1", + "start": 22, + "end": 27 + }, + "localName": { + "kind": "Name", + "name": "variable1", + "start": 9, + "end": 18 + } + }, + { + "start": 29, + "end": 47, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "name2", + "start": 42, + "end": 47 + }, + "localName": { + "kind": "Name", + "name": "variable2", + "start": 29, + "end": 38 + } + }, + { + "start": 60, + "end": 65, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "nameN", + "start": 60, + "end": 65 + }, + "localName": { + "kind": "Name", + "name": "nameN", + "start": 60, + "end": 65 + } + } + ] + } + ] +}" +`; + +exports[`esm > export class ClassName { /* … */ } 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 36, + "entries": [ + { + "start": 7, + "end": 36, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "ClassName", + "start": 13, + "end": 22 + }, + "localName": { + "kind": "Name", + "name": "ClassName", + "start": 13, + "end": 22 + } + } + ] + } + ] +}" +`; + +exports[`esm > export const [ name1, name2 ] = array; 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 38, + "entries": [ + { + "start": 7, + "end": 38, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "name1", + "start": 15, + "end": 20 + }, + "localName": { + "kind": "Name", + "name": "name1", + "start": 15, + "end": 20 + } + }, + { + "start": 7, + "end": 38, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "name2", + "start": 22, + "end": 27 + }, + "localName": { + "kind": "Name", + "name": "name2", + "start": 22, + "end": 27 + } + } + ] + } + ] +}" +`; + +exports[`esm > export const { name1, name2: bar } = o; 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 39, + "entries": [ + { + "start": 7, + "end": 39, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "name1", + "start": 15, + "end": 20 + }, + "localName": { + "kind": "Name", + "name": "name1", + "start": 15, + "end": 20 + } + }, + { + "start": 7, + "end": 39, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "bar", + "start": 29, + "end": 32 + }, + "localName": { + "kind": "Name", + "name": "bar", + "start": 29, + "end": 32 + } + } + ] + } + ] +}" +`; + +exports[`esm > export const name1 = 1, name2 = 2/*, … */; // also var, let 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 44, + "entries": [ + { + "start": 7, + "end": 44, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "name1", + "start": 13, + "end": 18 + }, + "localName": { + "kind": "Name", + "name": "name1", + "start": 13, + "end": 18 + } + }, + { + "start": 7, + "end": 44, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "name2", + "start": 24, + "end": 29 + }, + "localName": { + "kind": "Name", + "name": "name2", + "start": 24, + "end": 29 + } + } + ] + } + ] +}" +`; + +exports[`esm > export default class { /* … */ } 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 34, + "entries": [ + { + "start": 15, + "end": 34, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Default", + "start": 7, + "end": 14 + }, + "localName": { + "kind": "None" + } + } + ] + } + ] +}" +`; + +exports[`esm > export default class ClassName { /* … */ } 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 44, + "entries": [ + { + "start": 15, + "end": 44, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Default", + "start": 7, + "end": 14 + }, + "localName": { + "kind": "Name", + "name": "ClassName", + "start": 21, + "end": 30 + } + } + ] + } + ] +}" +`; + +exports[`esm > export default expression; 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 26, + "entries": [ + { + "start": 15, + "end": 25, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Default", + "start": 7, + "end": 14 + }, + "localName": { + "kind": "Default", + "name": "expression", + "start": 15, + "end": 25 + } + } + ] + } + ] +}" +`; + +exports[`esm > export default function () { /* … */ } 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 40, + "entries": [ + { + "start": 15, + "end": 40, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Default", + "start": 7, + "end": 14 + }, + "localName": { + "kind": "None" + } + } + ] + } + ] +}" +`; + +exports[`esm > export default function functionName() { /* … */ } 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 52, + "entries": [ + { + "start": 15, + "end": 52, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Default", + "start": 7, + "end": 14 + }, + "localName": { + "kind": "Name", + "name": "functionName", + "start": 24, + "end": 36 + } + } + ] + } + ] +}" +`; + +exports[`esm > export default function* () { /* … */ } 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 41, + "entries": [ + { + "start": 15, + "end": 41, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Default", + "start": 7, + "end": 14 + }, + "localName": { + "kind": "None" + } + } + ] + } + ] +}" +`; + +exports[`esm > export default function* generatorFunctionName() { /* … */ } 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 62, + "entries": [ + { + "start": 15, + "end": 62, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Default", + "start": 7, + "end": 14 + }, + "localName": { + "kind": "Name", + "name": "generatorFunctionName", + "start": 25, + "end": 46 + } + } + ] + } + ] +}" +`; + +exports[`esm > export function functionName() { /* … */ } 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 44, + "entries": [ + { + "start": 7, + "end": 44, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "functionName", + "start": 16, + "end": 28 + }, + "localName": { + "kind": "Name", + "name": "functionName", + "start": 16, + "end": 28 + } + } + ] + } + ] +}" +`; + +exports[`esm > export function* generatorFunctionName() { /* … */ } 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 54, + "entries": [ + { + "start": 7, + "end": 54, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "generatorFunctionName", + "start": 17, + "end": 38 + }, + "localName": { + "kind": "Name", + "name": "generatorFunctionName", + "start": 17, + "end": 38 + } + } + ] + } + ] +}" +`; + +exports[`esm > export let name1, name2/*, … */; // also var 1`] = ` +"{ + "staticImports": [], + "staticExports": [ + { + "start": 0, + "end": 34, + "entries": [ + { + "start": 7, + "end": 34, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "name1", + "start": 11, + "end": 16 + }, + "localName": { + "kind": "Name", + "name": "name1", + "start": 11, + "end": 16 + } + }, + { + "start": 7, + "end": 34, + "importName": { + "kind": "None" + }, + "exportName": { + "kind": "Name", + "name": "name2", + "start": 18, + "end": 23 + }, + "localName": { + "kind": "Name", + "name": "name2", + "start": 18, + "end": 23 + } + } + ] + } + ] +}" +`; + +exports[`esm > import "module-name"; 1`] = ` +"{ + "staticImports": [ + { + "start": 0, + "end": 21, + "moduleRequest": { + "value": "module-name", + "start": 7, + "end": 20 + }, + "entries": [] + } + ], + "staticExports": [] +}" +`; + +exports[`esm > import * as name from "module-name"; 1`] = ` +"{ + "staticImports": [ + { + "start": 0, + "end": 36, + "moduleRequest": { + "value": "module-name", + "start": 22, + "end": 35 + }, + "entries": [ + { + "importName": { + "kind": "NamespaceObject" + }, + "localName": { + "value": "name", + "start": 12, + "end": 16 + }, + "isType": false + } + ] + } + ], + "staticExports": [] +}" +`; + +exports[`esm > import { "string name" as alias } from "module-name"; 1`] = ` +"{ + "staticImports": [ + { + "start": 0, + "end": 53, + "moduleRequest": { + "value": "module-name", + "start": 39, + "end": 52 + }, + "entries": [ + { + "importName": { + "kind": "Name", + "name": "string name", + "start": 9, + "end": 22 + }, + "localName": { + "value": "alias", + "start": 26, + "end": 31 + }, + "isType": false + } + ] + } + ], + "staticExports": [] +}" +`; + +exports[`esm > import { default as alias } from "module-name"; 1`] = ` +"{ + "staticImports": [ + { + "start": 0, + "end": 47, + "moduleRequest": { + "value": "module-name", + "start": 33, + "end": 46 + }, + "entries": [ + { + "importName": { + "kind": "Name", + "name": "default", + "start": 9, + "end": 16 + }, + "localName": { + "value": "alias", + "start": 20, + "end": 25 + }, + "isType": false + } + ] + } + ], + "staticExports": [] +}" +`; + +exports[`esm > import { export1 } from "module-name"; 1`] = ` +"{ + "staticImports": [ + { + "start": 0, + "end": 38, + "moduleRequest": { + "value": "module-name", + "start": 24, + "end": 37 + }, + "entries": [ + { + "importName": { + "kind": "Name", + "name": "export1", + "start": 9, + "end": 16 + }, + "localName": { + "value": "export1", + "start": 9, + "end": 16 + }, + "isType": false + } + ] + } + ], + "staticExports": [] +}" +`; + +exports[`esm > import { export1 as alias1 } from "module-name"; 1`] = ` +"{ + "staticImports": [ + { + "start": 0, + "end": 48, + "moduleRequest": { + "value": "module-name", + "start": 34, + "end": 47 + }, + "entries": [ + { + "importName": { + "kind": "Name", + "name": "export1", + "start": 9, + "end": 16 + }, + "localName": { + "value": "alias1", + "start": 20, + "end": 26 + }, + "isType": false + } + ] + } + ], + "staticExports": [] +}" +`; + +exports[`esm > import { export1, export2 } from "module-name"; 1`] = ` +"{ + "staticImports": [ + { + "start": 0, + "end": 47, + "moduleRequest": { + "value": "module-name", + "start": 33, + "end": 46 + }, + "entries": [ + { + "importName": { + "kind": "Name", + "name": "export1", + "start": 9, + "end": 16 + }, + "localName": { + "value": "export1", + "start": 9, + "end": 16 + }, + "isType": false + }, + { + "importName": { + "kind": "Name", + "name": "export2", + "start": 18, + "end": 25 + }, + "localName": { + "value": "export2", + "start": 18, + "end": 25 + }, + "isType": false + } + ] + } + ], + "staticExports": [] +}" +`; + +exports[`esm > import { export1, export2 as alias2, /* … */ } from "module-name"; 1`] = ` +"{ + "staticImports": [ + { + "start": 0, + "end": 68, + "moduleRequest": { + "value": "module-name", + "start": 54, + "end": 67 + }, + "entries": [ + { + "importName": { + "kind": "Name", + "name": "export1", + "start": 9, + "end": 16 + }, + "localName": { + "value": "export1", + "start": 9, + "end": 16 + }, + "isType": false + }, + { + "importName": { + "kind": "Name", + "name": "export2", + "start": 18, + "end": 25 + }, + "localName": { + "value": "alias2", + "start": 29, + "end": 35 + }, + "isType": false + } + ] + } + ], + "staticExports": [] +}" +`; + +exports[`esm > import defaultExport from "module-name"; 1`] = ` +"{ + "staticImports": [ + { + "start": 0, + "end": 40, + "moduleRequest": { + "value": "module-name", + "start": 26, + "end": 39 + }, + "entries": [ + { + "importName": { + "kind": "Default", + "start": 7, + "end": 20 + }, + "localName": { + "value": "defaultExport", + "start": 7, + "end": 20 + }, + "isType": false + } + ] + } + ], + "staticExports": [] +}" +`; + +exports[`esm > import defaultExport, * as name from "module-name"; 1`] = ` +"{ + "staticImports": [ + { + "start": 0, + "end": 51, + "moduleRequest": { + "value": "module-name", + "start": 37, + "end": 50 + }, + "entries": [ + { + "importName": { + "kind": "Default", + "start": 7, + "end": 20 + }, + "localName": { + "value": "defaultExport", + "start": 7, + "end": 20 + }, + "isType": false + }, + { + "importName": { + "kind": "NamespaceObject" + }, + "localName": { + "value": "name", + "start": 27, + "end": 31 + }, + "isType": false + } + ] + } + ], + "staticExports": [] +}" +`; + +exports[`esm > import defaultExport, { export1, /* … */ } from "module-name"; 1`] = ` +"{ + "staticImports": [ + { + "start": 0, + "end": 64, + "moduleRequest": { + "value": "module-name", + "start": 50, + "end": 63 + }, + "entries": [ + { + "importName": { + "kind": "Default", + "start": 7, + "end": 20 + }, + "localName": { + "value": "defaultExport", + "start": 7, + "end": 20 + }, + "isType": false + }, + { + "importName": { + "kind": "Name", + "name": "export1", + "start": 24, + "end": 31 + }, + "localName": { + "value": "export1", + "start": 24, + "end": 31 + }, + "isType": false + } + ] + } + ], + "staticExports": [] +}" +`; diff --git a/napi/parser/test/esm.test.ts b/napi/parser/test/esm.test.ts new file mode 100644 index 0000000000000..b4a1eef200ed4 --- /dev/null +++ b/napi/parser/test/esm.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it, test } from 'vitest'; + +import * as oxc from '../index.js'; + +describe('esm', () => { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#syntax + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export#syntax + let code = ` +import defaultExport from "module-name"; +import * as name from "module-name"; +import { export1 } from "module-name"; +import { export1 as alias1 } from "module-name"; +import { default as alias } from "module-name"; +import { export1, export2 } from "module-name"; +import { export1, export2 as alias2, /* … */ } from "module-name"; +import { "string name" as alias } from "module-name"; +import defaultExport, { export1, /* … */ } from "module-name"; +import defaultExport, * as name from "module-name"; +import "module-name"; + +export let name1, name2/*, … */; // also var +export const name1 = 1, name2 = 2/*, … */; // also var, let +export function functionName() { /* … */ } +export class ClassName { /* … */ } +export function* generatorFunctionName() { /* … */ } +export const { name1, name2: bar } = o; +export const [ name1, name2 ] = array; + +export { name1, /* …, */ nameN }; +export { variable1 as name1, variable2 as name2, /* …, */ nameN }; +export { variable1 as "string name" }; +export { name1 as default /*, … */ }; + +export default expression; +export default function functionName() { /* … */ } +export default class ClassName { /* … */ } +export default function* generatorFunctionName() { /* … */ } +export default function () { /* … */ } +export default class { /* … */ } +export default function* () { /* … */ } + +export * from "module-name"; +export * as name1 from "module-name"; +export { name1, /* …, */ nameN } from "module-name"; +export { import1 as name1, import2 as name2, /* …, */ nameN } from "module-name"; +export { default, /* …, */ } from "module-name"; +export { default as name1 } from "module-name"; +`.split('\n').map((s) => s.trim()).filter(Boolean); + + test.each(code)('%s', (s) => { + const ret = oxc.parseSync(s, { sourceFilename: 'test.ts' }); + expect(ret.program.body.length).toBeGreaterThan(0); + expect(ret.errors.length).toBe(0); + expect(JSON.stringify(ret.module, null, 2)).toMatchSnapshot(); + if (s.startsWith('import')) { + expect(ret.module.staticImports.length).toBe(1); + expect(ret.module.staticExports.length).toBe(0); + } + if (s.startsWith('export')) { + expect(ret.module.staticImports.length).toBe(0); + expect(ret.module.staticExports.length).toBe(1); + } + }); +});