diff --git a/Cargo.lock b/Cargo.lock index 153ab2d1ffb4c..227354b5ec094 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1580,6 +1580,16 @@ dependencies = [ "oxc_tasks_common", ] +[[package]] +name = "oxc_module_lexer" +version = "0.0.0" +dependencies = [ + "oxc_allocator", + "oxc_ast", + "oxc_parser", + "oxc_span", +] + [[package]] name = "oxc_parser" version = "0.9.0" @@ -1612,6 +1622,7 @@ dependencies = [ "oxc_allocator", "oxc_ast", "oxc_diagnostics", + "oxc_module_lexer", "oxc_parser", "oxc_span", "serde", diff --git a/Cargo.toml b/Cargo.toml index bb821424fb90a..16e2fe65caa93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,9 @@ oxc_prettier = { path = "crates/oxc_prettier" } oxc_tasks_common = { path = "tasks/common" } oxc_language_server = { path = "crates/oxc_language_server" } +napi = { version = "2" } +napi-derive = { version = "2" } + assert-unchecked = { version = "0.1.2" } bpaf = { version = "0.9.9" } bitflags = { version = "2.4.2" } diff --git a/crates/oxc_module_lexer/Cargo.toml b/crates/oxc_module_lexer/Cargo.toml new file mode 100644 index 0000000000000..74c11240c1730 --- /dev/null +++ b/crates/oxc_module_lexer/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "oxc_module_lexer" +version = "0.0.0" +publish = false +authors.workspace = true +description.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +categories.workspace = true + +[lints] +workspace = true + +[lib] +test = false +doctest = false + +[dependencies] +oxc_ast = { workspace = true } +oxc_span = { workspace = true } + +[dev-dependencies] +oxc_allocator = { workspace = true } +oxc_parser = { workspace = true } diff --git a/crates/oxc_module_lexer/README.md b/crates/oxc_module_lexer/README.md new file mode 100644 index 0000000000000..0159482e9f8a7 --- /dev/null +++ b/crates/oxc_module_lexer/README.md @@ -0,0 +1,24 @@ +# Oxc Module Lexer + +This is not a lexer. The name "lexer" is used for easier recognition. + +## [es-module-lexer](https://github.com/guybedford/es-module-lexer) + +Outputs the list of exports and locations of import specifiers, including dynamic import and import meta handling. + +Does not have any [limitations](https://github.com/guybedford/es-module-lexer?tab=readme-ov-file#limitations) mentioned in `es-module-lexer`. + +- [ ] get imported variables https://github.com/guybedford/es-module-lexer/issues/163 +- [ ] track star exports as imports as well https://github.com/guybedford/es-module-lexer/issues/76 +- [ ] TypeScript specific syntax +- [ ] TypeScript `type` import / export keyword + +## [cjs-module-lexer](https://github.com/nodejs/cjs-module-lexer) + +- [ ] TODO + +## Benchmark + +This is 2 times slower than `es-module-lexer`, but will be significantly faster when TypeScript is processed. + +The difference is around 10ms vs 20ms on a large file (700k). diff --git a/crates/oxc_module_lexer/examples/module_lexer.rs b/crates/oxc_module_lexer/examples/module_lexer.rs new file mode 100644 index 0000000000000..53878b4913750 --- /dev/null +++ b/crates/oxc_module_lexer/examples/module_lexer.rs @@ -0,0 +1,46 @@ +use std::{env, path::Path}; + +use oxc_allocator::Allocator; +use oxc_module_lexer::ModuleLexer; +use oxc_parser::Parser; +use oxc_span::SourceType; + +// Instruction: +// * create a `test.js` +// * `just example module_lexer + +fn main() -> Result<(), String> { + let name = env::args().nth(1).unwrap_or_else(|| "test.js".to_string()); + let path = Path::new(&name); + let source_text = std::fs::read_to_string(path).map_err(|_| format!("Missing '{name}'"))?; + let allocator = Allocator::default(); + let source_type = SourceType::from_path(path).unwrap(); + let ret = Parser::new(&allocator, &source_text, source_type).parse(); + + println!("source:"); + println!("{source_text}"); + + for error in ret.errors { + let error = error.with_source_code(source_text.clone()); + println!("{error:?}"); + println!("Parsed with Errors."); + } + + let ModuleLexer { imports, exports, facade, has_module_syntax } = + ModuleLexer::new().build(&ret.program); + + println!("\nimports:"); + for import in imports { + println!("{import:?}"); + } + + println!("\nexports:"); + for export in exports { + println!("{export:?}"); + } + + println!("\nfacade: {facade}"); + println!("has_module_syntax {has_module_syntax}"); + + Ok(()) +} diff --git a/crates/oxc_module_lexer/src/lib.rs b/crates/oxc_module_lexer/src/lib.rs new file mode 100644 index 0000000000000..e9aa059bfbb0b --- /dev/null +++ b/crates/oxc_module_lexer/src/lib.rs @@ -0,0 +1,269 @@ +//! ESM module lexer +//! +//! * + +#[allow(clippy::wildcard_imports)] +use oxc_ast::{ast::*, syntax_directed_operations::BoundNames, AstKind, Visit}; +use oxc_span::{Atom, GetSpan}; + +#[derive(Debug, Clone)] +pub struct ImportSpecifier<'a> { + /// Module name + /// + /// To handle escape sequences in specifier strings, the .n field of imported specifiers will be provided where possible. + /// + /// For dynamic import expressions, this field will be empty if not a valid JS string. + pub n: Option>, + + /// Start of module specifier + pub s: u32, + + /// End of module specifier + pub e: u32, + + /// Start of import statement + pub ss: u32, + + /// End of import statement + pub se: u32, + + /// Dynamic import / Static import / `import.meta` + pub d: ImportType, + + /// If this import has an import assertion, this is the start value + pub a: Option, +} + +#[derive(Debug, Clone)] +pub struct ExportSpecifier<'a> { + /// Exported name + pub n: Atom<'a>, + + /// Local name, or undefined. + pub ln: Option>, + + /// Start of exported name + pub s: u32, + + /// End of exported name + pub e: u32, + + /// Start of local name + pub ls: Option, + + /// End of local name + pub le: Option, +} + +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +pub enum ImportType { + /// If this import keyword is a dynamic import, this is the start value. + DynamicImport(u32), + /// If this import keyword is a static import + #[default] + StaticImport, + /// If this import keyword is an import.meta expresion + ImportMeta, +} + +impl ImportType { + pub fn as_dynamic_import(&self) -> Option { + match self { + Self::DynamicImport(start) => Some(*start), + Self::StaticImport | Self::ImportMeta => None, + } + } +} + +pub struct ModuleLexer<'a> { + pub imports: Vec>, + + pub exports: Vec>, + + /// ESM syntax detection + /// + /// The use of ESM syntax: import / export statements and `import.meta` + pub has_module_syntax: bool, + + /// Facade modules that only use import / export syntax + pub facade: bool, +} + +impl<'a> Default for ModuleLexer<'a> { + fn default() -> Self { + Self::new() + } +} + +impl<'a> ModuleLexer<'a> { + #[must_use] + pub fn new() -> Self { + Self { imports: vec![], exports: vec![], has_module_syntax: false, facade: true } + } + + #[must_use] + pub fn build(mut self, program: &Program<'a>) -> Self { + self.visit_program(program); + self + } +} + +impl<'a> Visit<'a> for ModuleLexer<'a> { + fn enter_node(&mut self, kind: AstKind<'a>) { + match kind { + kind if self.facade && kind.is_statement() => { + self.facade = false; + } + AstKind::ModuleDeclaration(_) if !self.has_module_syntax => { + self.has_module_syntax = true; + } + // import.meta + AstKind::MetaProperty(prop) => { + if !self.has_module_syntax { + self.has_module_syntax = true; + } + if prop.meta.name == "import" && prop.property.name == "meta" { + self.imports.push(ImportSpecifier { + n: None, + s: prop.span.start, + e: prop.span.end, + ss: prop.span.start, + se: prop.span.end, + d: ImportType::ImportMeta, + a: None, + }); + } + } + // import("foo") + AstKind::ImportExpression(expr) => { + let (source, source_span_start, source_span_end) = + if let Expression::StringLiteral(s) = &expr.source { + (Some(s.value.clone()), s.span.start, s.span.end) + } else { + let span = expr.source.span(); + (None, span.start, span.end) + }; + self.imports.push(ImportSpecifier { + n: source, + s: source_span_start, + e: source_span_end, + ss: expr.span.start, + se: expr.span.end, + d: ImportType::DynamicImport(expr.span.start + 6), + a: expr.arguments.first().map(|e| e.span().start), + }); + } + AstKind::ImportDeclaration(decl) => { + let assertions = decl + .with_clause + .as_ref() + .filter(|c| c.with_entries.first().is_some_and(|a| a.key.as_atom() == "type")) + .map(|c| c.span.start); + self.imports.push(ImportSpecifier { + n: Some(decl.source.value.clone()), + s: decl.source.span.start + 1, // +- 1 for removing string quotes + e: decl.source.span.end - 1, + ss: decl.span.start, + se: decl.span.end, + d: ImportType::StaticImport, + a: assertions, + }); + } + AstKind::ExportNamedDeclaration(decl) => { + if let Some(source) = &decl.source { + // export { named } from 'foo' + self.imports.push(ImportSpecifier { + n: Some(source.value.clone()), + s: source.span.start + 1, + e: source.span.end - 1, + ss: decl.span.start, + se: decl.span.end, + d: ImportType::StaticImport, + a: None, + }); + } + + // export const/let/var/function/class ... + if let Some(decl) = &decl.declaration { + if self.facade { + self.facade = false; + } + decl.bound_names(&mut |ident| { + self.exports.push(ExportSpecifier { + n: ident.name.clone(), + ln: Some(ident.name.clone()), + s: ident.span.start, + e: ident.span.end, + ls: None, + le: None, + }); + }); + } + + // export { named } + self.exports.extend(decl.specifiers.iter().map(|s| { + let (exported_start, exported_end) = match &s.exported { + ModuleExportName::Identifier(ident) => (ident.span.start, ident.span.end), + // +1 -1 to remove the string quotes + ModuleExportName::StringLiteral(s) => (s.span.start + 1, s.span.end - 1), + }; + ExportSpecifier { + n: s.exported.name().clone(), + ln: decl.source.is_none().then(|| s.local.name().clone()), + s: exported_start, + e: exported_end, + ls: Some(s.local.span().start), + le: Some(s.local.span().end), + } + })); + } + // export default foo + AstKind::ExportDefaultDeclaration(decl) => { + if self.facade { + self.facade = false; + } + let ln = match &decl.declaration { + ExportDefaultDeclarationKind::FunctionDeclaration(func) => func.id.as_ref(), + ExportDefaultDeclarationKind::ClassDeclaration(class) => class.id.as_ref(), + ExportDefaultDeclarationKind::Expression(_) + | ExportDefaultDeclarationKind::TSInterfaceDeclaration(_) + | ExportDefaultDeclarationKind::TSEnumDeclaration(_) => None, + }; + self.exports.push(ExportSpecifier { + n: decl.exported.name().clone(), + ln: ln.map(|id| id.name.clone()), + s: decl.exported.span().start, + e: decl.exported.span().end, + ls: None, + le: None, + }); + } + AstKind::ExportAllDeclaration(decl) => { + // export * as ns from 'foo' + if let Some(exported) = &decl.exported { + let n = exported.name().clone(); + let s = exported.span().start; + let e = exported.span().end; + self.exports.push(ExportSpecifier { + n: n.clone(), + ln: None, + s, + e, + ls: None, + le: None, + }); + self.imports.push(ImportSpecifier { + n: Some(n), + s, + e, + ss: decl.span.start, + se: decl.span.end, + d: ImportType::StaticImport, + a: None, + }); + } + } + _ => {} + } + } +} diff --git a/crates/oxc_module_lexer/tests/esm.rs b/crates/oxc_module_lexer/tests/esm.rs new file mode 100644 index 0000000000000..514d08b0251b1 --- /dev/null +++ b/crates/oxc_module_lexer/tests/esm.rs @@ -0,0 +1,1648 @@ +//! + +use oxc_allocator::Allocator; +use oxc_parser::Parser; +use oxc_span::SourceType; + +use oxc_module_lexer::ImportType; + +#[derive(Debug, Clone)] +struct ImportSpecifier { + n: Option, + s: u32, + e: u32, + ss: u32, + se: u32, + d: ImportType, + a: Option, +} + +#[derive(Debug, Clone)] +pub struct ExportSpecifier { + n: String, + ln: Option, + s: u32, + e: u32, + ls: Option, + le: Option, +} + +#[non_exhaustive] +struct ModuleLexer { + imports: Vec, + exports: Vec, + has_module_syntax: bool, + facade: bool, +} + +fn parse(source: &str) -> ModuleLexer { + let allocator = Allocator::default(); + let source_type = SourceType::default().with_module(true); + let ret = Parser::new(&allocator, source, source_type).parse(); + assert!(ret.errors.is_empty(), "{source} should not produce errors.\n{:?}", ret.errors); + let module_lexer = oxc_module_lexer::ModuleLexer::new().build(&ret.program); + // Copy data over because `ModuleLexer<'a>` can't be returned + ModuleLexer { + imports: module_lexer + .imports + .into_iter() + .map(|i| ImportSpecifier { + n: i.n.map(|n| n.to_string()), + s: i.s, + e: i.e, + ss: i.ss, + se: i.se, + d: i.d, + a: i.a, + }) + .collect(), + exports: module_lexer + .exports + .into_iter() + .map(|e| ExportSpecifier { + n: e.n.to_string(), + ln: e.ln.map(|ln| ln.to_string()), + s: e.s, + e: e.e, + ls: e.ls, + le: e.le, + }) + .collect(), + has_module_syntax: module_lexer.has_module_syntax, + facade: module_lexer.facade, + } +} + +trait Slice { + fn slice(&self, start: u32, end: u32) -> &'static str; +} + +impl Slice for &'static str { + fn slice(&self, start: u32, end: u32) -> &'static str { + &self[start as usize..end as usize] + } +} + +#[allow(clippy::needless_pass_by_value, clippy::similar_names)] +fn assert_export_is( + source: &str, + actual: &ExportSpecifier, + expected_n: &str, + expected_ln: Option<&str>, +) { + // Commented out because: + // * there are no tests hitting the true branch + // * `&source[s..e]` is a Rust string, `expected.n.as_str()` is a escaped JavaScript string, + // which will never be cause for escaped strings. + // let s = actual.s as usize; + // let e = actual.e as usize; + // if matches!(&source[s..s], "\"" | "'") { + // assert_eq!(&source[s..s], &source[e - 1..e - 1]); + // } else { + // assert_eq!(&source[s..e], &expected.n.as_str()); + // } + + let ls = actual.ls; + let le = actual.le; + if let Some(expected_ln) = expected_ln { + if expected_ln.is_empty() { + assert_eq!(ls, None); + assert_eq!(le, None); + } else if let Some(ls) = ls { + let ls = ls as usize; + let le = le.unwrap() as usize; + // Commented out because "true" branch never got him. + // if matches!(&source[ls..ls], "\"" | "'") { + // assert_eq!(&source[ls..ls], &source[le - 1..le - 1]); + // } else { + assert_eq!(&source[ls..le], expected_ln); + // } + } + } + assert_eq!(actual.n, expected_n, "n"); + assert_eq!(actual.ln.as_deref(), expected_ln, "ln"); +} + +/* Added by Oxc */ + +#[test] +fn named_imports() { + let source = "import { a, b, c } from 'foo'"; + let imports = &parse(source).imports; + assert_eq!(imports.len(), 1); + // assert_eq!(source.slice(impt.ss, impt.se), r#"import(("asdf"))"#); + // assert_eq!(source.slice(impt.s, impt.e), r#"("asdf")"#); +} + +/* Suite Lexer */ + +#[test] +fn dynamic_import_expression_range() { + let source = r#"import(("asdf"))"#; + let impt = &parse(source).imports[0]; + assert_eq!(source.slice(impt.ss, impt.se), r#"import(("asdf"))"#); + assert_eq!(source.slice(impt.s, impt.e), r#"("asdf")"#); +} + +#[test] +fn dynamic_import_expression_range_2() { + let source = r"import(/* comment */ `asdf` /* comment */)"; + let impt = &parse(source).imports[0]; + assert_eq!(source.slice(impt.ss, impt.se), r"import(/* comment */ `asdf` /* comment */)"); + assert_eq!(source.slice(impt.s, impt.e), r"`asdf`"); +} + +#[test] +fn dynamic_import_expression_range_3() { + let source = "import(`asdf` // comment\n)"; + let impt = &parse(source).imports[0]; + assert_eq!(source.slice(impt.ss, impt.se), "import(`asdf` // comment\n)"); + assert_eq!(source.slice(impt.s, impt.e), "`asdf`"); +} + +#[test] +fn dynamic_import_expression_range_4() { + let source = "import(\"foo\" + /* comment */ \"bar\")"; + let impt = &parse(source).imports[0]; + assert_eq!(source.slice(impt.ss, impt.se), "import(\"foo\" + /* comment */ \"bar\")"); + assert_eq!(source.slice(impt.s, impt.e), "\"foo\" + /* comment */ \"bar\""); +} + +#[test] +fn dynamic_import_expression_range_5() { + let source = "import((() => { return \"foo\" })() /* comment */)"; + let impt = &parse(source).imports[0]; + assert_eq!( + source.slice(impt.ss, impt.se), + "import((() => { return \"foo\" })() /* comment */)" + ); + assert_eq!(source.slice(impt.s, impt.e), "(() => { return \"foo\" })()"); +} + +#[test] +fn dynamic_import_expression_range_6() { + let source = "import(/* comment */ `asdf` /* comment */ /* comment 2 */)"; + let impt = &parse(source).imports[0]; + assert_eq!( + source.slice(impt.ss, impt.se), + "import(/* comment */ `asdf` /* comment */ /* comment 2 */)" + ); + assert_eq!(source.slice(impt.s, impt.e), "`asdf`"); +} + +#[test] +fn simple_export_destructuring() { + let source = " + export const{URI,Utils,...Another}=LIB + export var p, { z } = {}; + + export var { aa, qq: { z } } = { qq: {} }, pp = {}; + "; + let exports = parse(source).exports; + assert_eq!( + exports.iter().map(|e| e.n.clone()).collect::>(), + // NOTE: esm-module-lexer does not have "Another", "z", and "pp", and has an extra "qq" + // vec!["URI", "Utils", "p", "aa", "qq"] + vec!["URI", "Utils", "Another", "p", "z", "aa", "z", "pp"] + ); +} + +#[test] +fn export_default_cases() { + let source = " + export default \"export default a\" + export default \"export default 'a'\" + export default \"export function foo() {}\" + export default \"export function foo() {return bar}\" + "; + let exports = parse(source).exports; + assert_eq!( + exports.iter().map(|expt| expt.n.clone()).collect::>(), + vec!["default", "default", "default", "default"] + ); +} + +#[test] +fn import_meta_spread() { + let source = "console.log(...import.meta.obj);"; + let impts = parse(source).imports; + assert_eq!(impts.len(), 1); + assert_eq!(source.slice(impts[0].s, impts[0].e), "import.meta"); +} + +#[test] +fn template_string_default_bracket() { + let source = "export default{};"; + let expts = &parse(source).exports; + let expt = &expts[0]; + assert_eq!(source.slice(expt.s, expt.e), "default"); + assert_eq!(expt.ls, None); + assert_eq!(expt.le, None); + assert_eq!(expt.n, "default"); + assert_eq!(expt.ln, None); +} + +#[test] +fn template_string_default() { + let source = "const css = String.raw; +export default css`:host { solid 1px black }`;"; + let expts = &parse(source).exports; + let expt = &expts[0]; + assert_eq!(source.slice(expt.s, expt.e), "default"); + assert_eq!(expt.ls, None); + assert_eq!(expt.le, None); + assert_eq!(expt.n, "default"); + assert_eq!(expt.ln, None); +} + +#[test] +fn class_fn_asi() { + parse("class a{friendlyName;import}n();"); +} + +#[test] +fn division_const_after_class_parse_case() { + let source = "class a{}const Ti=a/yi;"; + parse(source); +} + +#[test] +fn multiline_dynamic_import_on_windows() { + let source = "import(\n\"./statehash\\u{1011}.js\"\r)"; + let imports = parse(source).imports; + assert_eq!(imports.len(), 1); + assert_eq!(source.slice(imports[0].s, imports[0].e), "\"./statehash\\u{1011}.js\""); +} + +#[test] +fn basic_nested_dynamic_import_support() { + let source = "await import (await import ('foo'))"; + let imports = parse(source).imports; + assert_eq!(imports.len(), 2); + // We can't obtain the left of `(` + // assert_eq!(source.slice(imports[0].ss, imports[0].d.as_dynamic_import().unwrap()), "import "); + assert_eq!(source.slice(imports[0].ss, imports[0].se), "import (await import ('foo'))"); + assert_eq!(source.slice(imports[0].s, imports[0].e), "await import ('foo')"); + // We can't obtain the left of `(` + // assert_eq!(source.slice(imports[1].ss, imports[1].d.as_dynamic_import().unwrap()), "import "); + assert_eq!(source.slice(imports[1].ss, imports[1].se), "import ('foo')"); + assert_eq!(source.slice(imports[1].s, imports[1].e), "'foo'"); +} + +#[test] +fn import_assertions() { + let source = r#" +import json from "./foo.json" assert { type: "json" }; +import("foo.json", { assert: { type: "json" } }); + +import test from './asdf' assert { not: 'an assertion!' } +export var p = 5; +"#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 3); + assert_eq!(source.slice(imports[0].s, imports[0].e), "./foo.json"); + assert_eq!(source.slice(imports[0].a.unwrap(), imports[0].se), "{ type: \"json\" };"); + assert_eq!( + source.slice(imports[1].a.unwrap(), imports[1].se), + "{ assert: { type: \"json\" } })" + ); + assert_eq!(source.slice(imports[1].s, imports[1].e), "\"foo.json\""); + assert_eq!(imports[1].n.as_ref().unwrap(), "foo.json"); + assert_eq!(imports[2].n.as_ref().unwrap(), "./asdf"); + assert_eq!(imports[2].a, None); + assert_eq!(exports.len(), 1); + assert_export_is(source, &exports[0], "p", Some("p")); +} + +#[test] +fn import_attributes() { + let source = " +import json from \"./foo.json\" with { type: \"json\" }; +import(\"foo.json\", { with: { type: \"json\" } }); + +import test from './asdf' +with { not: 'an assertion!' } +export var p = 5; +"; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 3); + assert_eq!(source.slice(imports[0].s, imports[0].e), "./foo.json"); + assert_eq!(source.slice(imports[0].a.unwrap(), imports[0].se), "{ type: \"json\" };"); + assert_eq!(source.slice(imports[1].a.unwrap(), imports[1].se), "{ with: { type: \"json\" } })"); + assert_eq!(source.slice(imports[1].s, imports[1].e), "\"foo.json\""); + assert_eq!(imports[1].n.clone().unwrap(), "foo.json"); + assert_eq!(imports[2].n.clone().unwrap(), "./asdf"); + assert_eq!(imports[2].a, None); + assert_eq!(exports.len(), 1); + assert_export_is(source, &exports[0], "p", Some("p")); +} + +#[test] +fn import_meta_inside_dynamic_import() { + let source = "import(import.meta.url)"; + let imports = parse(source).imports; + assert_eq!(imports.len(), 2); + assert_eq!(source.slice(imports[0].s, imports[0].e), "import.meta.url"); +} + +#[test] +fn export() { + let source = "export var p=5"; + let exports = parse(source).exports; + assert_export_is(source, &exports[0], "p", Some("p")); +} + +#[test] +fn string_encoding() { + let imports = parse( + " + import './\\x61\\x62\\x63.js'; + import './\\u{20204}.js'; + import('./\\u{20204}.js'); + import('./\\u{20204}.js' + dyn); + import('./\\u{20204}.js' ); + import('./\\u{20204}.js' ()); + ", + ) + .imports; + assert_eq!(imports.len(), 6); + assert_eq!(imports[0].n.clone().unwrap(), "./abc.js"); + assert_eq!(imports[1].n.clone().unwrap(), "./𠈄.js"); + assert_eq!(imports[2].n.clone().unwrap(), "./𠈄.js"); + assert_eq!(imports[3].n, None); + assert_eq!(imports[4].n.clone().unwrap(), "./𠈄.js"); + assert_eq!(imports[5].n, None); +} + +#[test] +fn regexp_case() { + parse( + " + class Number { + + } + + /(\"|')(?(\\\\(\\1)|[^\\1])*)?(\\1)/.exec(`'\\\\\"\\\\'aa'`); + + const x = `\"${label.replace(/\"/g, \"\\\\\\\"\")}\"`; + ", + ); +} + +#[test] +fn regexp_keyword_prefixes() { + let imports = parse( + " + x: while (true) { + if (foo) break + /import(\"a\")/.test(bar) || baz() + if (foo) continue + /import(\"b\")/.test(bar) || baz() + if (foo) break x + /import(\"c\")/.test(bar) || baz() + if (foo) continue x + /import(\"d\")/.test(bar) || baz() + } + ", + ) + .imports; + assert_eq!(imports.len(), 0); +} + +#[test] +fn regexp_division() { + parse("\nconst x = num / /'/.exec(l)[0].slice(1, -1)//'"); +} + +#[test] +fn multiline_string_escapes() { + parse("const str = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wAAAAAzJ3zzAAAGTElEQV\\\n\t\tRIx+VXe1BU1xn/zjn7ugvL4sIuQnll5U0ELAQxig7WiQYz6NRHa6O206qdSXXSxs60dTK200zNY9q0dcRpMs1jkrRNWmaijCVoaU';\n"); +} + +#[test] +fn dotted_number() { + parse( + " + const x = 5. / 10; + ", + ); +} + +#[test] +fn division_operator_case() { + parse(" + function log(r){ + if(g>=0){u[g++]=m;g>=n.logSz&&(g=0)}else{u.push(m);u.length>=n.logSz&&(g=0)}/^(DBG|TICK): /.test(r)||t.Ticker.tick(454,o.slice(0,200)); + } + + (function(n){ + })(); + "); +} + +#[test] +fn single_parse_cases() { + parse("export { x }"); + parse("'asdf'"); + parse("/asdf/"); + parse("`asdf`"); + parse("/**/\n"); + parse(" //"); +} + +#[test] +fn simple_export_with_unicode_conversions() { + let source = "export var p𓀀s,q"; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 0); + assert_eq!(exports.len(), 2); + assert_export_is(source, &exports[0], "p𓀀s", Some("p𓀀s")); + assert_export_is(source, &exports[1], "q", Some("q")); +} + +#[test] +fn simple_import() { + let source = " + import test from \"test\"; + console.log(test); + "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 1); + let import = &imports[0]; + assert_eq!(import.d, ImportType::StaticImport); + assert_eq!(import.n.clone().unwrap(), "test"); + assert_eq!(source.slice(import.ss, import.se), "import test from \"test\";"); + assert_eq!(exports.len(), 0); +} + +#[test] +fn empty_single_quote_string_import() { + let source = "import ''"; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 1); + let import = &imports[0]; + assert_eq!(import.d, ImportType::StaticImport); + assert_eq!(source.slice(import.s, import.e), ""); + assert_eq!(source.slice(import.ss, import.se), "import ''"); + assert_eq!(exports.len(), 0); +} + +#[test] +fn empty_double_quote_string_import() { + let source = "import \"\""; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 1); + let import = &imports[0]; + assert_eq!(import.d, ImportType::StaticImport); + assert_eq!(source.slice(import.s, import.e), ""); + assert_eq!(source.slice(import.ss, import.se), "import \"\""); + assert_eq!(exports.len(), 0); +} + +#[test] +fn import_export_with_comments() { + let source = " + +import /* 'x' */ + 'a'; + +import /* 'x' */ + 'b'; + +export var z /* */ + export { + a, +// b, +/* c */ + d + }; + "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 2); + assert_eq!(source.slice(imports[0].s, imports[0].e), "a"); + assert_eq!(source.slice(imports[0].ss, imports[0].se), "import /* 'x' */\n 'a';"); + assert_eq!(source.slice(imports[1].s, imports[1].e), "b"); + assert_eq!(source.slice(imports[1].ss, imports[1].se), "import /* 'x' */\n 'b';"); + assert_eq!(exports.len(), 3); + assert_export_is(source, &exports[0], "z", Some("z")); + assert_export_is(source, &exports[1], "a", Some("a")); + assert_export_is(source, &exports[2], "d", Some("d")); +} + +#[test] +fn exported_function_and_class() { + let source = " + export function a𓀀 () { + + } + export class Q{ + + } + "; + let exports = parse(source).exports; + assert_eq!(exports.len(), 2); + assert_export_is(source, &exports[0], "a𓀀", Some("a𓀀")); + assert_export_is(source, &exports[1], "Q", Some("Q")); +} + +#[test] +fn export_destructuring() { + let source = " + export const { a, b } = foo; + + export { ok }; + "; + let exports = parse(source).exports; + assert_eq!(exports.len(), 3); + assert_export_is(source, &exports[0], "a", Some("a")); +} + +#[test] +fn minified_import_syntax() { + let source = r#"import{TemplateResult as t}from"lit-html";import{a as e}from"./chunk-4be41b30.js";export{j as SVGTemplateResult,i as TemplateResult,g as html,h as svg}from"./chunk-4be41b30.js";window.JSCompiler_renameProperty='asdf';"#; + let imports = parse(source).imports; + assert_eq!(imports.len(), 3); + assert_eq!(imports[0].s, 32); + assert_eq!(imports[0].e, 40); + assert_eq!(imports[0].ss, 0); + assert_eq!(imports[0].se, 42); + assert_eq!(imports[1].s, 61); + assert_eq!(imports[1].e, 80); + assert_eq!(imports[1].ss, 42); + assert_eq!(imports[1].se, 82); + assert_eq!(imports[2].s, 156); + assert_eq!(imports[2].e, 175); + assert_eq!(imports[2].ss, 82); + assert_eq!(imports[2].se, 177); +} + +#[test] +fn more_minified_imports() { + let source = r#"import"some/import.js";"#; + let imports = parse(source).imports; + assert_eq!(imports.len(), 1); + assert_eq!(imports[0].s, 7); + assert_eq!(imports[0].e, 21); + assert_eq!(imports[0].ss, 0); + assert_eq!(imports[0].se, 23); +} + +#[test] +fn plus_plus_division() { + parse( + "\ +tick++/fetti;f=(1)+\")\"; +", + ); +} + +#[test] +fn return_bracket_division() { + let source = "function variance(){return s/(a-1)}"; + parse(source); +} + +#[test] +fn simple_reexport() { + let source = r#"export { hello as default } from "test-dep";"#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 1); + let import = &imports[0]; + assert_eq!(import.d, ImportType::StaticImport); + assert_eq!(source.slice(import.s, import.e), "test-dep"); + assert_eq!( + source.slice(import.ss, import.se), + "export { hello as default } from \"test-dep\";" + ); + assert_eq!(exports.len(), 1); + assert_export_is(source, &exports[0], "default", None); +} + +#[test] +fn import_meta() { + let source = r" + export var hello = 'world'; + console.log(import.meta.url); + "; + let imports = parse(source).imports; + assert_eq!(imports.len(), 1); + let import = &imports[0]; + assert_eq!(import.d, ImportType::ImportMeta); + assert_eq!(import.ss, 53); + assert_eq!(import.se, 64); + assert_eq!(source.slice(import.s, import.e), "import.meta"); +} + +#[test] +fn import_meta_edge_cases() { + let source = r" + // Import meta + import. + meta + // Not import meta + a. + import. + meta + "; + let imports = parse(source).imports; + assert_eq!(imports.len(), 1); + let import = &imports[0]; + assert_eq!(import.d, ImportType::ImportMeta); + assert_eq!(import.ss, 28); + assert_eq!(import.se, 47); + assert_eq!(source.slice(import.s, import.e), "import.\n meta"); +} + +#[test] +fn dynamic_import_method() { + let source = r" + class A { + import() { + } + } + "; + let imports = parse(source).imports; + assert_eq!(imports.len(), 0); +} + +#[test] +fn dynamic_import_edge_cases() { + let source = r" + ({ + // not a dynamic import! + import(not1) {} + }); + { + // is a dynamic import! + import(is1); + } + a. + // not a dynamic import! + import(not2); + a. + b() + // is a dynamic import! + import(is2); + + const myObject = { + import: ()=> import(some_url) + } + "; + let imports = parse(source).imports; + assert_eq!(imports.len(), 3); + let imp = &imports[0]; + assert_eq!(imp.ss + 6, imp.d.as_dynamic_import().unwrap()); + assert_eq!(imp.se, imp.e + 1); + assert_eq!(source.slice(imp.d.as_dynamic_import().unwrap(), imp.se), "(is1)"); + assert_eq!(source.slice(imp.s, imp.e), "is1"); + + let imp = &imports[1]; + assert_eq!(imp.ss + 6, imp.d.as_dynamic_import().unwrap()); + assert_eq!(imp.se, imp.e + 1); + assert_eq!(source.slice(imp.s, imp.e), "is2"); + + let imp = &imports[2]; + assert_eq!(imp.ss + 6, imp.d.as_dynamic_import().unwrap()); + assert_eq!(imp.se, imp.e + 1); + assert_eq!(source.slice(imp.s, imp.e), "some_url"); +} + +#[test] +fn import_after_code() { + let source = "\ +export function f () { +g(); +} + +import { g } from './test-circular2.js'; +"; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 1); + let import = &imports[0]; + assert_eq!(import.d, ImportType::StaticImport); + assert_eq!(source.slice(import.s, import.e), "./test-circular2.js"); + assert_eq!(source.slice(import.ss, import.se), "import { g } from './test-circular2.js';"); + assert_eq!(exports.len(), 1); + assert_export_is(source, &exports[0], "f", Some("f")); +} + +#[test] +fn comments() { + let source = " /*\n VERSION\n */\nimport util from 'util';\n\n//\nfunction x() {\n}\n\n/**/\n// '\n/* / */\n/*\n\n * export { b }\n\\*/\nexport { a }\n\n function () {\n/***/\n }\n "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 1); + assert_eq!(source.slice(imports[0].s, imports[0].e), "util"); + assert_eq!(source.slice(imports[0].ss, imports[0].se), "import util from 'util';"); + assert_eq!(exports.len(), 1); + assert_export_is(source, &exports[0], "a", Some("a")); +} + +#[test] +fn strings() { + let source = r#" + ""; + ` + ${ + import(`test/${ import(b)}`) + } + ` + export { a } + "#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 2); + assert_ne!(imports[0].d, ImportType::StaticImport); + assert_eq!(imports[0].ss + 6, imports[0].d.as_dynamic_import().unwrap()); + assert_eq!(imports[0].se, imports[0].e + 1); + assert_eq!(source.slice(imports[0].ss, imports[0].s), "import("); + assert_ne!(imports[1].d, ImportType::StaticImport); + assert_eq!(imports[1].ss + 6, imports[1].d.as_dynamic_import().unwrap()); + assert_eq!(imports[1].se, imports[1].e + 1); + assert_eq!(source.slice(imports[1].ss, imports[1].d.as_dynamic_import().unwrap()), "import"); + assert_eq!(exports.len(), 1); + assert_export_is(source, &exports[0], "a", Some("a")); +} + +#[test] +fn bracket_matching() { + parse( + " + instance.extend('parseExprAtom', function (nextMethod) { + return function () { + function parseExprAtom(refDestructuringErrors) { + if (this.type === tt._import) { + return parseDynamicImport.call(this); + } + return c(refDestructuringErrors); + } + }(); + }); + export { a } + ", + ); +} + +#[test] +fn division_regex_ambiguity() { + let source = r" + /as)df/; x(); + a / 2; ' / ' + while (true) + /test'/ + x-/a'/g + try {} + finally{}/a'/g + (x);{f()}/d'export { b }/g + ;{}/e'/g; + {}/f'/g + a / 'b' / c; + /a'/ - /b'/; + +{} /g -'/g' + ('a')/h -'/g' + if //x + ('a')/i'/g; + /asdf/ / /as'df/; // ' + p = `\${/test/ + 5}`; + /regex/ / x; + function m() { + return /*asdf8*// 5/; + } + export { a }; + "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 0); + assert_eq!(exports.len(), 1); + assert_export_is(source, &exports[0], "a", Some("a")); +} + +#[test] +fn template_string_expression_ambiguity() { + let source = r" + `$` + import 'a'; + `` + export { b }; + `a$b` + import(`$`); + `{$}` + "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 2); + assert_eq!(exports.len(), 1); + assert_export_is(source, &exports[0], "b", Some("b")); +} + +#[test] +fn many_exports() { + let exports = parse( + r" + export { _iconsCache as fas, prefix, faAbacus, faAcorn, faAd, faAddressBook, faAddressCard, faAdjust, faAirFreshener, faAlarmClock, faAlarmExclamation, faAlarmPlus, faAlarmSnooze, faAlicorn, faAlignCenter, faAlignJustify, faAlignLeft, faAlignRight, faAlignSlash, faAllergies, faAmbulance, faAmericanSignLanguageInterpreting, faAnalytics, faAnchor, faAngel, faAngleDoubleDown, faAngleDoubleLeft, faAngleDoubleRight, faAngleDoubleUp, faAngleDown, faAngleLeft, faAngleRight, faAngleUp, faAngry, faAnkh, faAppleAlt, faAppleCrate, faArchive, faArchway, faArrowAltCircleDown, faArrowAltCircleLeft, faArrowAltCircleRight, faArrowAltCircleUp, faArrowAltDown, faArrowAltFromBottom, faArrowAltFromLeft, faArrowAltFromRight, faArrowAltFromTop, faArrowAltLeft, faArrowAltRight, faArrowAltSquareDown, faArrowAltSquareLeft, faArrowAltSquareRight, faArrowAltSquareUp, faArrowAltToBottom, faArrowAltToLeft, faArrowAltToRight, faArrowAltToTop, faArrowAltUp, faArrowCircleDown, faArrowCircleLeft, faArrowCircleRight, faArrowCircleUp, faArrowDown, faArrowFromBottom, faArrowFromLeft, faArrowFromRight, faArrowFromTop, faArrowLeft, faArrowRight, faArrowSquareDown, faArrowSquareLeft, faArrowSquareRight, faArrowSquareUp, faArrowToBottom, faArrowToLeft, faArrowToRight, faArrowToTop, faArrowUp, faArrows, faArrowsAlt, faArrowsAltH, faArrowsAltV, faArrowsH, faArrowsV, faAssistiveListeningSystems, faAsterisk, faAt, faAtlas, faAtom, faAtomAlt, faAudioDescription, faAward, faAxe, faAxeBattle, faBaby, faBabyCarriage, faBackpack, faBackspace, faBackward, faBacon, faBadge, faBadgeCheck, faBadgeDollar, faBadgePercent, faBadgerHoney, faBagsShopping, faBalanceScale, faBalanceScaleLeft, faBalanceScaleRight, faBallPile, faBallot, faBallotCheck, faBan, faBandAid, faBarcode, faBarcodeAlt, faBarcodeRead, faBarcodeScan, faBars, faBaseball, faBaseballBall, faBasketballBall, faBasketballHoop, faBat, faBath, faBatteryBolt, faBatteryEmpty, faBatteryFull, faBatteryHalf, faBatteryQuarter, faBatterySlash, faBatteryThreeQuarters, faBed, faBeer, faBell, faBellExclamation, faBellPlus, faBellSchool, faBellSchoolSlash, faBellSlash, faBells, faBezierCurve, faBible, faBicycle, faBiking, faBikingMountain, faBinoculars, faBiohazard, faBirthdayCake, faBlanket, faBlender, faBlenderPhone, faBlind, faBlog, faBold, faBolt, faBomb, faBone, faBoneBreak, faBong, faBook, faBookAlt, faBookDead, faBookHeart, faBookMedical, faBookOpen, faBookReader, faBookSpells, faBookUser, faBookmark, faBooks, faBooksMedical, faBoot, faBoothCurtain, faBorderAll, faBorderBottom, faBorderCenterH, faBorderCenterV, faBorderInner, faBorderLeft, faBorderNone, faBorderOuter, faBorderRight, faBorderStyle, faBorderStyleAlt, faBorderTop, faBowArrow, faBowlingBall, faBowlingPins, faBox, faBoxAlt, faBoxBallot, faBoxCheck, faBoxFragile, faBoxFull, faBoxHeart, faBoxOpen, faBoxUp, faBoxUsd, faBoxes, faBoxesAlt, faBoxingGlove, faBrackets, faBracketsCurly, faBraille, faBrain, faBreadLoaf, faBreadSlice, faBriefcase, faBriefcaseMedical, faBringForward, faBringFront, faBroadcastTower, faBroom, faBrowser, faBrush, faBug, faBuilding, faBullhorn, faBullseye, faBullseyeArrow, faBullseyePointer, faBurgerSoda, faBurn, faBurrito, faBus, faBusAlt, faBusSchool, faBusinessTime, faCabinetFiling, faCalculator, faCalculatorAlt, faCalendar, faCalendarAlt, faCalendarCheck, faCalendarDay, faCalendarEdit, faCalendarExclamation, faCalendarMinus, faCalendarPlus, faCalendarStar, faCalendarTimes, faCalendarWeek, faCamera, faCameraAlt, faCameraRetro, faCampfire, faCampground, faCandleHolder, faCandyCane, faCandyCorn, faCannabis, faCapsules, faCar, faCarAlt, faCarBattery, faCarBuilding, faCarBump, faCarBus, faCarCrash, faCarGarage, faCarMechanic, faCarSide, faCarTilt, faCarWash, faCaretCircleDown, faCaretCircleLeft, faCaretCircleRight, faCaretCircleUp, faCaretDown, faCaretLeft, faCaretRight, faCaretSquareDown, faCaretSquareLeft, faCaretSquareRight, faCaretSquareUp, faCaretUp, faCarrot, faCars, faCartArrowDown, faCartPlus, faCashRegister, faCat, faCauldron, faCertificate, faChair, faChairOffice, faChalkboard, faChalkboardTeacher, faChargingStation, faChartArea, faChartBar, faChartLine, faChartLineDown, faChartNetwork, faChartPie, faChartPieAlt, faChartScatter, faCheck, faCheckCircle, faCheckDouble, faCheckSquare, faCheese, faCheeseSwiss, faCheeseburger, faChess, faChessBishop, faChessBishopAlt, faChessBoard, faChessClock, faChessClockAlt, faChessKing, faChessKingAlt, faChessKnight, faChessKnightAlt, faChessPawn, faChessPawnAlt, faChessQueen, faChessQueenAlt, faChessRook, faChessRookAlt, faChevronCircleDown, faChevronCircleLeft, faChevronCircleRight, faChevronCircleUp, faChevronDoubleDown, faChevronDoubleLeft, faChevronDoubleRight, faChevronDoubleUp, faChevronDown, faChevronLeft, faChevronRight, faChevronSquareDown, faChevronSquareLeft, faChevronSquareRight, faChevronSquareUp, faChevronUp, faChild, faChimney, faChurch, faCircle, faCircleNotch, faCity, faClawMarks, faClinicMedical, faClipboard, faClipboardCheck, faClipboardList, faClipboardListCheck, faClipboardPrescription, faClipboardUser, faClock, faClone, faClosedCaptioning, faCloud, faCloudDownload, faCloudDownloadAlt, faCloudDrizzle, faCloudHail, faCloudHailMixed, faCloudMeatball, faCloudMoon, faCloudMoonRain, faCloudRain, faCloudRainbow, faCloudShowers, faCloudShowersHeavy, faCloudSleet, faCloudSnow, faCloudSun, faCloudSunRain, faCloudUpload, faCloudUploadAlt, faClouds, faCloudsMoon, faCloudsSun, faClub, faCocktail, faCode, faCodeBranch, faCodeCommit, faCodeMerge, faCoffee, faCoffeeTogo, faCoffin, faCog, faCogs, faCoin, faCoins, faColumns, faComment, faCommentAlt, faCommentAltCheck, faCommentAltDollar, faCommentAltDots, faCommentAltEdit, faCommentAltExclamation, faCommentAltLines, faCommentAltMedical, faCommentAltMinus, faCommentAltPlus, faCommentAltSlash, faCommentAltSmile, faCommentAltTimes, faCommentCheck, faCommentDollar, faCommentDots, faCommentEdit, faCommentExclamation, faCommentLines, faCommentMedical, faCommentMinus, faCommentPlus, faCommentSlash, faCommentSmile, faCommentTimes, faComments, faCommentsAlt, faCommentsAltDollar, faCommentsDollar, faCompactDisc, faCompass, faCompassSlash, faCompress, faCompressAlt, faCompressArrowsAlt, faCompressWide, faConciergeBell, faConstruction, faContainerStorage, faConveyorBelt, faConveyorBeltAlt, faCookie, faCookieBite, faCopy, faCopyright, faCorn, faCouch, faCow, faCreditCard, faCreditCardBlank, faCreditCardFront, faCricket, faCroissant, faCrop, faCropAlt, faCross, faCrosshairs, faCrow, faCrown, faCrutch, faCrutches, faCube, faCubes, faCurling, faCut, faDagger, faDatabase, faDeaf, faDebug, faDeer, faDeerRudolph, faDemocrat, faDesktop, faDesktopAlt, faDewpoint, faDharmachakra, faDiagnoses, faDiamond, faDice, faDiceD10, faDiceD12, faDiceD20, faDiceD4, faDiceD6, faDiceD8, faDiceFive, faDiceFour, faDiceOne, faDiceSix, faDiceThree, faDiceTwo, faDigging, faDigitalTachograph, faDiploma, faDirections, faDisease, faDivide, faDizzy, faDna, faDoNotEnter, faDog, faDogLeashed, faDollarSign, faDolly, faDollyEmpty, faDollyFlatbed, faDollyFlatbedAlt, faDollyFlatbedEmpty, faDonate, faDoorClosed, faDoorOpen, faDotCircle, faDove, faDownload, faDraftingCompass, faDragon, faDrawCircle, faDrawPolygon, faDrawSquare, faDreidel, faDrone, faDroneAlt, faDrum, faDrumSteelpan, faDrumstick, faDrumstickBite, faDryer, faDryerAlt, faDuck, faDumbbell, faDumpster, faDumpsterFire, faDungeon, faEar, faEarMuffs, faEclipse, faEclipseAlt, faEdit, faEgg, faEggFried, faEject, faElephant, faEllipsisH, faEllipsisHAlt, faEllipsisV, faEllipsisVAlt, faEmptySet, faEngineWarning, faEnvelope, faEnvelopeOpen, faEnvelopeOpenDollar, faEnvelopeOpenText, faEnvelopeSquare, faEquals, faEraser, faEthernet, faEuroSign, faExchange, faExchangeAlt, faExclamation, faExclamationCircle, faExclamationSquare, faExclamationTriangle, faExpand, faExpandAlt, faExpandArrows, faExpandArrowsAlt, faExpandWide, faExternalLink, faExternalLinkAlt, faExternalLinkSquare, faExternalLinkSquareAlt, faEye, faEyeDropper, faEyeEvil, faEyeSlash, faFan, faFarm, faFastBackward, faFastForward, faFax, faFeather, faFeatherAlt, faFemale, faFieldHockey, faFighterJet, faFile, faFileAlt, faFileArchive, faFileAudio, faFileCertificate, faFileChartLine, faFileChartPie, faFileCheck, faFileCode, faFileContract, faFileCsv, faFileDownload, faFileEdit, faFileExcel, faFileExclamation, faFileExport, faFileImage, faFileImport, faFileInvoice, faFileInvoiceDollar, faFileMedical, faFileMedicalAlt, faFileMinus, faFilePdf, faFilePlus, faFilePowerpoint, faFilePrescription, faFileSearch, faFileSignature, faFileSpreadsheet, faFileTimes, faFileUpload, faFileUser, faFileVideo, faFileWord, faFilesMedical, faFill, faFillDrip, faFilm, faFilmAlt, faFilter, faFingerprint, faFire, faFireAlt, faFireExtinguisher, faFireSmoke, faFireplace, faFirstAid, faFish, faFishCooked, faFistRaised, faFlag, faFlagAlt, faFlagCheckered, faFlagUsa, faFlame, faFlask, faFlaskPoison, faFlaskPotion, faFlower, faFlowerDaffodil, faFlowerTulip, faFlushed, faFog, faFolder, faFolderMinus, faFolderOpen, faFolderPlus, faFolderTimes, faFolderTree, faFolders, faFont, faFontAwesomeLogoFull, faFontCase, faFootballBall, faFootballHelmet, faForklift, faForward, faFragile, faFrenchFries, faFrog, faFrostyHead, faFrown, faFrownOpen, faFunction, faFunnelDollar, faFutbol, faGameBoard, faGameBoardAlt, faGamepad, faGasPump, faGasPumpSlash, faGavel, faGem, faGenderless, faGhost, faGift, faGiftCard, faGifts, faGingerbreadMan, faGlass, faGlassChampagne, faGlassCheers, faGlassCitrus, faGlassMartini, faGlassMartiniAlt, faGlassWhiskey, faGlassWhiskeyRocks, faGlasses, faGlassesAlt, faGlobe, faGlobeAfrica, faGlobeAmericas, faGlobeAsia, faGlobeEurope, faGlobeSnow, faGlobeStand, faGolfBall, faGolfClub, faGopuram, faGraduationCap, faGreaterThan, faGreaterThanEqual, faGrimace, faGrin, faGrinAlt, faGrinBeam, faGrinBeamSweat, faGrinHearts, faGrinSquint, faGrinSquintTears, faGrinStars, faGrinTears, faGrinTongue, faGrinTongueSquint, faGrinTongueWink, faGrinWink, faGripHorizontal, faGripLines, faGripLinesVertical, faGripVertical, faGuitar, faHSquare, faH1, faH2, faH3, faH4, faHamburger, faHammer, faHammerWar, faHamsa, faHandHeart, faHandHolding, faHandHoldingBox, faHandHoldingHeart, faHandHoldingMagic, faHandHoldingSeedling, faHandHoldingUsd, faHandHoldingWater, faHandLizard, faHandMiddleFinger, faHandPaper, faHandPeace, faHandPointDown, faHandPointLeft, faHandPointRight, faHandPointUp, faHandPointer, faHandReceiving, faHandRock, faHandScissors, faHandSpock, faHands, faHandsHeart, faHandsHelping, faHandsUsd, faHandshake, faHandshakeAlt, faHanukiah, faHardHat, faHashtag, faHatChef, faHatSanta, faHatWinter, faHatWitch, faHatWizard, faHaykal, faHdd, faHeadSide, faHeadSideBrain, faHeadSideMedical, faHeadVr, faHeading, faHeadphones, faHeadphonesAlt, faHeadset, faHeart, faHeartBroken, faHeartCircle, faHeartRate, faHeartSquare, faHeartbeat, faHelicopter, faHelmetBattle, faHexagon, faHighlighter, faHiking, faHippo, faHistory, faHockeyMask, faHockeyPuck, faHockeySticks, faHollyBerry, faHome, faHomeAlt, faHomeHeart, faHomeLg, faHomeLgAlt, faHoodCloak, faHorizontalRule, faHorse, faHorseHead, faHospital, faHospitalAlt, faHospitalSymbol, faHospitalUser, faHospitals, faHotTub, faHotdog, faHotel, faHourglass, faHourglassEnd, faHourglassHalf, faHourglassStart, faHouseDamage, faHouseFlood, faHryvnia, faHumidity, faHurricane, faICursor, faIceCream, faIceSkate, faIcicles, faIcons, faIconsAlt, faIdBadge, faIdCard, faIdCardAlt, faIgloo, faImage, faImages, faInbox, faInboxIn, faInboxOut, faIndent, faIndustry, faIndustryAlt, faInfinity, faInfo, faInfoCircle, faInfoSquare, faInhaler, faIntegral, faIntersection, faInventory, faIslandTropical, faItalic, faJackOLantern, faJedi, faJoint, faJournalWhills, faKaaba, faKerning, faKey, faKeySkeleton, faKeyboard, faKeynote, faKhanda, faKidneys, faKiss, faKissBeam, faKissWinkHeart, faKite, faKiwiBird, faKnifeKitchen, faLambda, faLamp, faLandmark, faLandmarkAlt, faLanguage, faLaptop, faLaptopCode, faLaptopMedical, faLaugh, faLaughBeam, faLaughSquint, faLaughWink, faLayerGroup, faLayerMinus, faLayerPlus, faLeaf, faLeafHeart, faLeafMaple, faLeafOak, faLemon, faLessThan, faLessThanEqual, faLevelDown, faLevelDownAlt, faLevelUp, faLevelUpAlt, faLifeRing, faLightbulb, faLightbulbDollar, faLightbulbExclamation, faLightbulbOn, faLightbulbSlash, faLightsHoliday, faLineColumns, faLineHeight, faLink, faLips, faLiraSign, faList, faListAlt, faListOl, faListUl, faLocation, faLocationArrow, faLocationCircle, faLocationSlash, faLock, faLockAlt, faLockOpen, faLockOpenAlt, faLongArrowAltDown, faLongArrowAltLeft, faLongArrowAltRight, faLongArrowAltUp, faLongArrowDown, faLongArrowLeft, faLongArrowRight, faLongArrowUp, faLoveseat, faLowVision, faLuchador, faLuggageCart, faLungs, faMace, faMagic, faMagnet, faMailBulk, faMailbox, faMale, faMandolin, faMap, faMapMarked, faMapMarkedAlt, faMapMarker, faMapMarkerAlt, faMapMarkerAltSlash, faMapMarkerCheck, faMapMarkerEdit, faMapMarkerExclamation, faMapMarkerMinus, faMapMarkerPlus, faMapMarkerQuestion, faMapMarkerSlash, faMapMarkerSmile, faMapMarkerTimes, faMapPin, faMapSigns, faMarker, faMars, faMarsDouble, faMarsStroke, faMarsStrokeH, faMarsStrokeV, faMask, faMeat, faMedal, faMedkit, faMegaphone, faMeh, faMehBlank, faMehRollingEyes, faMemory, faMenorah, faMercury, faMeteor, faMicrochip, faMicrophone, faMicrophoneAlt, faMicrophoneAltSlash, faMicrophoneSlash, faMicroscope, faMindShare, faMinus, faMinusCircle, faMinusHexagon, faMinusOctagon, faMinusSquare, faMistletoe, faMitten, faMobile, faMobileAlt, faMobileAndroid, faMobileAndroidAlt, faMoneyBill, faMoneyBillAlt, faMoneyBillWave, faMoneyBillWaveAlt, faMoneyCheck, faMoneyCheckAlt, faMoneyCheckEdit, faMoneyCheckEditAlt, faMonitorHeartRate, faMonkey, faMonument, faMoon, faMoonCloud, faMoonStars, faMortarPestle, faMosque, faMotorcycle, faMountain, faMountains, faMousePointer, faMug, faMugHot, faMugMarshmallows, faMugTea, faMusic, faNarwhal, faNetworkWired, faNeuter, faNewspaper, faNotEqual, faNotesMedical, faObjectGroup, faObjectUngroup, faOctagon, faOilCan, faOilTemp, faOm, faOmega, faOrnament, faOtter, faOutdent, faOverline, faPageBreak, faPager, faPaintBrush, faPaintBrushAlt, faPaintRoller, faPalette, faPallet, faPalletAlt, faPaperPlane, faPaperclip, faParachuteBox, faParagraph, faParagraphRtl, faParking, faParkingCircle, faParkingCircleSlash, faParkingSlash, faPassport, faPastafarianism, faPaste, faPause, faPauseCircle, faPaw, faPawAlt, faPawClaws, faPeace, faPegasus, faPen, faPenAlt, faPenFancy, faPenNib, faPenSquare, faPencil, faPencilAlt, faPencilPaintbrush, faPencilRuler, faPennant, faPeopleCarry, faPepperHot, faPercent, faPercentage, faPersonBooth, faPersonCarry, faPersonDolly, faPersonDollyEmpty, faPersonSign, faPhone, faPhoneAlt, faPhoneLaptop, faPhoneOffice, faPhonePlus, faPhoneSlash, faPhoneSquare, faPhoneSquareAlt, faPhoneVolume, faPhotoVideo, faPi, faPie, faPig, faPiggyBank, faPills, faPizza, faPizzaSlice, faPlaceOfWorship, faPlane, faPlaneAlt, faPlaneArrival, faPlaneDeparture, faPlay, faPlayCircle, faPlug, faPlus, faPlusCircle, faPlusHexagon, faPlusOctagon, faPlusSquare, faPodcast, faPodium, faPodiumStar, faPoll, faPollH, faPollPeople, faPoo, faPooStorm, faPoop, faPopcorn, faPortrait, faPoundSign, faPowerOff, faPray, faPrayingHands, faPrescription, faPrescriptionBottle, faPrescriptionBottleAlt, faPresentation, faPrint, faPrintSearch, faPrintSlash, faProcedures, faProjectDiagram, faPumpkin, faPuzzlePiece, faQrcode, faQuestion, faQuestionCircle, faQuestionSquare, faQuidditch, faQuoteLeft, faQuoteRight, faQuran, faRabbit, faRabbitFast, faRacquet, faRadiation, faRadiationAlt, faRainbow, faRaindrops, faRam, faRampLoading, faRandom, faReceipt, faRectangleLandscape, faRectanglePortrait, faRectangleWide, faRecycle, faRedo, faRedoAlt, faRegistered, faRemoveFormat, faRepeat, faRepeat1, faRepeat1Alt, faRepeatAlt, faReply, faReplyAll, faRepublican, faRestroom, faRetweet, faRetweetAlt, faRibbon, faRing, faRingsWedding, faRoad, faRobot, faRocket, faRoute, faRouteHighway, faRouteInterstate, faRss, faRssSquare, faRubleSign, faRuler, faRulerCombined, faRulerHorizontal, faRulerTriangle, faRulerVertical, faRunning, faRupeeSign, faRv, faSack, faSackDollar, faSadCry, faSadTear, faSalad, faSandwich, faSatellite, faSatelliteDish, faSausage, faSave, faScalpel, faScalpelPath, faScanner, faScannerKeyboard, faScannerTouchscreen, faScarecrow, faScarf, faSchool, faScrewdriver, faScroll, faScrollOld, faScrubber, faScythe, faSdCard, faSearch, faSearchDollar, faSearchLocation, faSearchMinus, faSearchPlus, faSeedling, faSendBack, faSendBackward, faServer, faShapes, faShare, faShareAll, faShareAlt, faShareAltSquare, faShareSquare, faSheep, faShekelSign, faShield, faShieldAlt, faShieldCheck, faShieldCross, faShip, faShippingFast, faShippingTimed, faShishKebab, faShoePrints, faShoppingBag, faShoppingBasket, faShoppingCart, faShovel, faShovelSnow, faShower, faShredder, faShuttleVan, faShuttlecock, faSickle, faSigma, faSign, faSignIn, faSignInAlt, faSignLanguage, faSignOut, faSignOutAlt, faSignal, faSignal1, faSignal2, faSignal3, faSignal4, faSignalAlt, faSignalAlt1, faSignalAlt2, faSignalAlt3, faSignalAltSlash, faSignalSlash, faSignature, faSimCard, faSitemap, faSkating, faSkeleton, faSkiJump, faSkiLift, faSkiing, faSkiingNordic, faSkull, faSkullCrossbones, faSlash, faSledding, faSleigh, faSlidersH, faSlidersHSquare, faSlidersV, faSlidersVSquare, faSmile, faSmileBeam, faSmilePlus, faSmileWink, faSmog, faSmoke, faSmoking, faSmokingBan, faSms, faSnake, faSnooze, faSnowBlowing, faSnowboarding, faSnowflake, faSnowflakes, faSnowman, faSnowmobile, faSnowplow, faSocks, faSolarPanel, faSort, faSortAlphaDown, faSortAlphaDownAlt, faSortAlphaUp, faSortAlphaUpAlt, faSortAlt, faSortAmountDown, faSortAmountDownAlt, faSortAmountUp, faSortAmountUpAlt, faSortDown, faSortNumericDown, faSortNumericDownAlt, faSortNumericUp, faSortNumericUpAlt, faSortShapesDown, faSortShapesDownAlt, faSortShapesUp, faSortShapesUpAlt, faSortSizeDown, faSortSizeDownAlt, faSortSizeUp, faSortSizeUpAlt, faSortUp, faSoup, faSpa, faSpaceShuttle, faSpade, faSparkles, faSpellCheck, faSpider, faSpiderBlackWidow, faSpiderWeb, faSpinner, faSpinnerThird, faSplotch, faSprayCan, faSquare, faSquareFull, faSquareRoot, faSquareRootAlt, faSquirrel, faStaff, faStamp, faStar, faStarAndCrescent, faStarChristmas, faStarExclamation, faStarHalf, faStarHalfAlt, faStarOfDavid, faStarOfLife, faStars, faSteak, faSteeringWheel, faStepBackward, faStepForward, faStethoscope, faStickyNote, faStocking, faStomach, faStop, faStopCircle, faStopwatch, faStore, faStoreAlt, faStream, faStreetView, faStretcher, faStrikethrough, faStroopwafel, faSubscript, faSubway, faSuitcase, faSuitcaseRolling, faSun, faSunCloud, faSunDust, faSunHaze, faSunglasses, faSunrise, faSunset, faSuperscript, faSurprise, faSwatchbook, faSwimmer, faSwimmingPool, faSword, faSwords, faSynagogue, faSync, faSyncAlt, faSyringe, faTable, faTableTennis, faTablet, faTabletAlt, faTabletAndroid, faTabletAndroidAlt, faTabletRugged, faTablets, faTachometer, faTachometerAlt, faTachometerAltAverage, faTachometerAltFast, faTachometerAltFastest, faTachometerAltSlow, faTachometerAltSlowest, faTachometerAverage, faTachometerFast, faTachometerFastest, faTachometerSlow, faTachometerSlowest, faTaco, faTag, faTags, faTally, faTanakh, faTape, faTasks, faTasksAlt, faTaxi, faTeeth, faTeethOpen, faTemperatureFrigid, faTemperatureHigh, faTemperatureHot, faTemperatureLow, faTenge, faTennisBall, faTerminal, faText, faTextHeight, faTextSize, faTextWidth, faTh, faThLarge, faThList, faTheaterMasks, faThermometer, faThermometerEmpty, faThermometerFull, faThermometerHalf, faThermometerQuarter, faThermometerThreeQuarters, faTheta, faThumbsDown, faThumbsUp, faThumbtack, faThunderstorm, faThunderstormMoon, faThunderstormSun, faTicket, faTicketAlt, faTilde, faTimes, faTimesCircle, faTimesHexagon, faTimesOctagon, faTimesSquare, faTint, faTintSlash, faTire, faTireFlat, faTirePressureWarning, faTireRugged, faTired, faToggleOff, faToggleOn, faToilet, faToiletPaper, faToiletPaperAlt, faTombstone, faTombstoneAlt, faToolbox, faTools, faTooth, faToothbrush, faTorah, faToriiGate, faTornado, faTractor, faTrademark, faTrafficCone, faTrafficLight, faTrafficLightGo, faTrafficLightSlow, faTrafficLightStop, faTrain, faTram, faTransgender, faTransgenderAlt, faTrash, faTrashAlt, faTrashRestore, faTrashRestoreAlt, faTrashUndo, faTrashUndoAlt, faTreasureChest, faTree, faTreeAlt, faTreeChristmas, faTreeDecorated, faTreeLarge, faTreePalm, faTrees, faTriangle, faTrophy, faTrophyAlt, faTruck, faTruckContainer, faTruckCouch, faTruckLoading, faTruckMonster, faTruckMoving, faTruckPickup, faTruckPlow, faTruckRamp, faTshirt, faTty, faTurkey, faTurtle, faTv, faTvRetro, faUmbrella, faUmbrellaBeach, faUnderline, faUndo, faUndoAlt, faUnicorn, faUnion, faUniversalAccess, faUniversity, faUnlink, faUnlock, faUnlockAlt, faUpload, faUsdCircle, faUsdSquare, faUser, faUserAlt, faUserAltSlash, faUserAstronaut, faUserChart, faUserCheck, faUserCircle, faUserClock, faUserCog, faUserCrown, faUserEdit, faUserFriends, faUserGraduate, faUserHardHat, faUserHeadset, faUserInjured, faUserLock, faUserMd, faUserMdChat, faUserMinus, faUserNinja, faUserNurse, faUserPlus, faUserSecret, faUserShield, faUserSlash, faUserTag, faUserTie, faUserTimes, faUsers, faUsersClass, faUsersCog, faUsersCrown, faUsersMedical, faUtensilFork, faUtensilKnife, faUtensilSpoon, faUtensils, faUtensilsAlt, faValueAbsolute, faVectorSquare, faVenus, faVenusDouble, faVenusMars, faVial, faVials, faVideo, faVideoPlus, faVideoSlash, faVihara, faVoicemail, faVolcano, faVolleyballBall, faVolume, faVolumeDown, faVolumeMute, faVolumeOff, faVolumeSlash, faVolumeUp, faVoteNay, faVoteYea, faVrCardboard, faWalker, faWalking, faWallet, faWand, faWandMagic, faWarehouse, faWarehouseAlt, faWasher, faWatch, faWatchFitness, faWater, faWaterLower, faWaterRise, faWaveSine, faWaveSquare, faWaveTriangle, faWebcam, faWebcamSlash, faWeight, faWeightHanging, faWhale, faWheat, faWheelchair, faWhistle, faWifi, faWifi1, faWifi2, faWifiSlash, faWind, faWindTurbine, faWindWarning, faWindow, faWindowAlt, faWindowClose, faWindowMaximize, faWindowMinimize, faWindowRestore, faWindsock, faWineBottle, faWineGlass, faWineGlassAlt, faWonSign, faWreath, faWrench, faXRay, faYenSign, faYinYang }; + ", + ).exports; + assert_eq!(exports.len(), 1651); +} + +#[test] +fn empty_export() { + let source = r" + export {}; + "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 0); + assert_eq!(exports.len(), 0); +} + +#[test] +fn export_star_as() { + let source = r" + export * as X from './asdf'; + export * as yy from './g'; + "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 2); + assert_eq!(exports.len(), 2); + assert_export_is(source, &exports[0], "X", None); + assert_export_is(source, &exports[1], "yy", None); +} + +/* Suite Import From */ + +#[test] +fn non_identifier_string_double_quote() { + let source = r#" + import { "~123" as foo0 } from './mod0.js'; + import { "ab cd" as foo1 } from './mod1.js'; + import { "not identifier" as foo2 } from './mod2.js'; + import { "-notidentifier" as foo3 } from './mod3.js'; + import { "%notidentifier" as foo4 } from './mod4.js'; + import { "@notidentifier" as foo5 } from './mod5.js'; + import { " notidentifier" as foo6 } from './mod6.js'; + import { "notidentifier " as foo7 } from './mod7.js'; + import { " notidentifier " as foo8 } from './mod8.js'; + import LionCombobox from './src/LionCombobox.js'; // assuming LionCombobox is imported directly + "#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(exports.len(), 0); + assert_eq!(imports.len(), 10); + assert_eq!(imports[0].n.clone().unwrap(), "./mod0.js"); + assert_eq!(imports[1].n.clone().unwrap(), "./mod1.js"); + assert_eq!(imports[2].n.clone().unwrap(), "./mod2.js"); + assert_eq!(imports[3].n.clone().unwrap(), "./mod3.js"); + assert_eq!(imports[4].n.clone().unwrap(), "./mod4.js"); + assert_eq!(imports[5].n.clone().unwrap(), "./mod5.js"); + assert_eq!(imports[6].n.clone().unwrap(), "./mod6.js"); + assert_eq!(imports[7].n.clone().unwrap(), "./mod7.js"); + assert_eq!(imports[8].n.clone().unwrap(), "./mod8.js"); +} + +#[test] +fn non_identifier_string_single_quote() { + let source = r" + import { '~123' as foo0 } from './mod0.js'; + import { 'ab cd' as foo1 } from './mod1.js'; + import { 'not identifier' as foo2 } from './mod2.js'; + import { '-notidentifier' as foo3 } from './mod3.js'; + import { '%notidentifier' as foo4 } from './mod4.js'; + import { '@notidentifier' as foo5 } from './mod5.js'; + import { ' notidentifier' as foo6 } from './mod6.js'; + import { 'notidentifier ' as foo7 } from './mod7.js'; + import { ' notidentifier ' as foo8 } from './mod8.js'; + "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(exports.len(), 0); + assert_eq!(imports.len(), 9); + assert_eq!(imports[0].n.clone().unwrap(), "./mod0.js"); + assert_eq!(imports[1].n.clone().unwrap(), "./mod1.js"); + assert_eq!(imports[2].n.clone().unwrap(), "./mod2.js"); + assert_eq!(imports[3].n.clone().unwrap(), "./mod3.js"); + assert_eq!(imports[4].n.clone().unwrap(), "./mod4.js"); + assert_eq!(imports[5].n.clone().unwrap(), "./mod5.js"); + assert_eq!(imports[6].n.clone().unwrap(), "./mod6.js"); + assert_eq!(imports[7].n.clone().unwrap(), "./mod7.js"); + assert_eq!(imports[8].n.clone().unwrap(), "./mod8.js"); +} + +#[test] +fn with_backslash_keywords_double_quote() { + let source = r#" + import { " slash\\ " as foo0 } from './mod0.js'; + import { " quote\" " as foo1 } from './mod1.js'; + import { " quote\\\" " as foo2 } from './mod2.js'; + import { " quote' " as foo3 } from './mod3.js'; + "#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(exports.len(), 0); + assert_eq!(imports.len(), 4); + assert_eq!(imports[0].n.clone().unwrap(), "./mod0.js"); + assert_eq!(imports[1].n.clone().unwrap(), "./mod1.js"); + assert_eq!(imports[2].n.clone().unwrap(), "./mod2.js"); + assert_eq!(imports[3].n.clone().unwrap(), "./mod3.js"); +} + +#[test] +fn with_backslash_keywords_single_quote() { + let source = r" + import { ' slash\\ ' as foo0 } from './mod0.js'; + import { ' quote\' ' as foo1 } from './mod1.js'; + import { ' quote\\\' ' as foo2 } from './mod2.js'; + import { ' quote\' ' as foo3 } from './mod3.js'; + "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(exports.len(), 0); + assert_eq!(imports.len(), 4); + assert_eq!(imports[0].n.clone().unwrap(), "./mod0.js"); + assert_eq!(imports[1].n.clone().unwrap(), "./mod1.js"); + assert_eq!(imports[2].n.clone().unwrap(), "./mod2.js"); + assert_eq!(imports[3].n.clone().unwrap(), "./mod3.js"); +} + +#[test] +fn with_emoji_as() { + let source = r#" + import { "hm🤔" as foo0 } from './mod0.js'; + import { " 🚀rocket space " as foo1 } from './mod1.js'; + "#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(exports.len(), 0); + assert_eq!(imports.len(), 2); + assert_eq!(imports[0].n.clone().unwrap(), "./mod0.js"); + assert_eq!(imports[1].n.clone().unwrap(), "./mod1.js"); +} + +#[test] +fn double_quotes_and_curly_bracket() { + // cannot be parsed + // let source = " + // import { asdf as \"b} from 'wrong'\" } from 'mod0';"; + let source = " + import { asdf as x } from 'mod0';"; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(exports.len(), 0); + assert_eq!(imports.len(), 1); + assert_eq!(imports[0].n.clone().unwrap(), "mod0"); +} + +#[test] +fn single_quotes_and_curly_bracket() { + // cannot be parsed + // let source = " + // import { asdf as 'b} from \"wrong\"' } from 'mod0';"; + let source = " + import { asdf as x } from 'mod0';"; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(exports.len(), 0); + assert_eq!(imports.len(), 1); + assert_eq!(imports[0].n.clone().unwrap(), "mod0"); +} + +/* Export From */ + +#[test] +fn identifier_only() { + let source = " + export { x } from './asdf'; + export { x1, x2 } from './g'; + export { foo, x2 as bar, zoo } from './g2'; + export { + /** @type{HTMLElement} */ + LionCombobox + } from './src/LionCombobox.js';"; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 4); + assert_eq!(exports.len(), 7); + assert_export_is(source, &exports[0], "x", None); + assert_export_is(source, &exports[1], "x1", None); + assert_export_is(source, &exports[2], "x2", None); + assert_export_is(source, &exports[3], "foo", None); + assert_export_is(source, &exports[4], "bar", None); + assert_export_is(source, &exports[5], "zoo", None); + assert_export_is(source, &exports[6], "LionCombobox", None); +} + +#[test] +fn non_identifier_string_as_variable_double_quote() { + let source = " + export { \"~123\" as foo0 } from './mod0.js'; + export { \"ab cd\" as foo1 } from './mod1.js'; + export { \"not identifier\" as foo2 } from './mod2.js'; + export { \"-notidentifier\" as foo3 } from './mod3.js'; + export { \"%notidentifier\" as foo4 } from './mod4.js'; + export { \"@notidentifier\" as foo5 } from './mod5.js'; + export { \" notidentifier\" as foo6 } from './mod6.js'; + export { \"notidentifier \" as foo7 } from './mod7.js'; + export { \" notidentifier \" as foo8 } from './mod8.js';"; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 9); + assert_eq!(exports.len(), 9); + assert_export_is(source, &exports[0], "foo0", None); + assert_export_is(source, &exports[1], "foo1", None); + assert_export_is(source, &exports[2], "foo2", None); + assert_export_is(source, &exports[3], "foo3", None); + assert_export_is(source, &exports[4], "foo4", None); + assert_export_is(source, &exports[5], "foo5", None); + assert_export_is(source, &exports[6], "foo6", None); + assert_export_is(source, &exports[7], "foo7", None); + assert_export_is(source, &exports[8], "foo8", None); +} + +#[test] +fn non_identifier_string_as_variable_single_quote() { + let source = " + export { '~123' as foo0 } from './mod0.js'; + export { 'ab cd' as foo1 } from './mod1.js'; + export { 'not identifier' as foo2 } from './mod2.js'; + export { '-notidentifier' as foo3 } from './mod3.js'; + export { '%notidentifier' as foo4 } from './mod4.js'; + export { '@notidentifier' as foo5 } from './mod5.js'; + export { ' notidentifier' as foo6 } from './mod6.js'; + export { 'notidentifier ' as foo7 } from './mod7.js'; + export { ' notidentifier ' as foo8 } from './mod8.js';"; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 9); + assert_eq!(exports.len(), 9); + assert_export_is(source, &exports[0], "foo0", None); + assert_export_is(source, &exports[1], "foo1", None); + assert_export_is(source, &exports[2], "foo2", None); + assert_export_is(source, &exports[3], "foo3", None); + assert_export_is(source, &exports[4], "foo4", None); + assert_export_is(source, &exports[5], "foo5", None); + assert_export_is(source, &exports[6], "foo6", None); + assert_export_is(source, &exports[7], "foo7", None); + assert_export_is(source, &exports[8], "foo8", None); +} + +#[test] +fn with_backslash_keywords_as_variable_double_quote() { + let source = r#" + export { " slash\\ " as foo0 } from './mod0.js'; + export { " quote\" " as foo1 } from './mod1.js'; + export { " quote\\\" " as foo2 } from './mod2.js'; + export { " quote' " as foo3 } from './mod3.js';"#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 4); + assert_eq!(exports.len(), 4); + assert_export_is(source, &exports[0], "foo0", None); + assert_export_is(source, &exports[1], "foo1", None); + assert_export_is(source, &exports[2], "foo2", None); + assert_export_is(source, &exports[3], "foo3", None); +} + +#[test] +fn with_backslash_keywords_as_variable_single_quote() { + let source = r" + export { ' slash\\ ' as foo0 } from './mod0.js'; + export { ' quote\' ' as foo1 } from './mod1.js'; + export { ' quote\\\' ' as foo2 } from './mod2.js'; + export { ' quote\' ' as foo3 } from './mod3.js';"; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 4); + assert_eq!(exports.len(), 4); + assert_export_is(source, &exports[0], "foo0", None); + assert_export_is(source, &exports[1], "foo1", None); + assert_export_is(source, &exports[2], "foo2", None); + assert_export_is(source, &exports[3], "foo3", None); +} + +#[test] +fn with_emoji_as_2() { + let source = r#" + export { "hm🤔" as foo0 } from './mod0.js'; + export { " 🚀rocket space " as foo1 } from './mod1.js';"#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 2); + assert_eq!(exports.len(), 2); + assert_export_is(source, &exports[0], "foo0", None); + assert_export_is(source, &exports[1], "foo1", None); +} + +#[test] +fn non_identifier_string_double_quote_2() { + let source = r#" + export { "~123" } from './mod0.js'; + export { "ab cd" } from './mod1.js'; + export { "not identifier" } from './mod2.js'; + export { "-notidentifier" } from './mod3.js'; + export { "%notidentifier" } from './mod4.js'; + export { "@notidentifier" } from './mod5.js'; + export { " notidentifier" } from './mod6.js'; + export { "notidentifier " } from './mod7.js'; + export { " notidentifier " } from './mod8.js';"#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 9); + assert_eq!(exports.len(), 9); + assert_export_is(source, &exports[0], "~123", None); + assert_export_is(source, &exports[1], "ab cd", None); + assert_export_is(source, &exports[2], "not identifier", None); + assert_export_is(source, &exports[3], "-notidentifier", None); + assert_export_is(source, &exports[4], "%notidentifier", None); + assert_export_is(source, &exports[5], "@notidentifier", None); + assert_export_is(source, &exports[6], " notidentifier", None); + assert_export_is(source, &exports[7], "notidentifier ", None); + assert_export_is(source, &exports[8], " notidentifier ", None); +} + +#[test] +fn non_identifier_string_single_quote_2() { + let source = r" + export { '~123' } from './mod0.js'; + export { 'ab cd' } from './mod1.js'; + export { 'not identifier' } from './mod2.js'; + export { '-notidentifier' } from './mod3.js'; + export { '%notidentifier' } from './mod4.js'; + export { '@notidentifier' } from './mod5.js'; + export { ' notidentifier' } from './mod6.js'; + export { 'notidentifier ' } from './mod7.js'; + export { ' notidentifier ' } from './mod8.js';"; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 9); + assert_eq!(exports.len(), 9); + assert_export_is(source, &exports[0], "~123", None); + assert_export_is(source, &exports[1], "ab cd", None); + assert_export_is(source, &exports[2], "not identifier", None); + assert_export_is(source, &exports[3], "-notidentifier", None); + assert_export_is(source, &exports[4], "%notidentifier", None); + assert_export_is(source, &exports[5], "@notidentifier", None); + assert_export_is(source, &exports[6], " notidentifier", None); + assert_export_is(source, &exports[7], "notidentifier ", None); + assert_export_is(source, &exports[8], " notidentifier ", None); +} + +#[test] +fn with_backslash_keywords_double_quote_2() { + let source = r#" + export { " slash\\ " } from './mod0.js'; + export { " quote\" " } from './mod1.js'; + export { " quote\\\" " } from './mod2.js'; + export { " quote' " } from './mod3.js'; + "#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 4); + assert_eq!(exports.len(), 4); + assert_export_is(source, &exports[0], " slash\\ ", None); + assert_export_is(source, &exports[1], " quote\" ", None); + assert_export_is(source, &exports[2], " quote\\\" ", None); + assert_export_is(source, &exports[3], " quote' ", None); +} + +#[test] +fn with_backslash_keywords_single_quote_2() { + let source = r" + export { ' slash\\ ' } from './mod0.js'; + export { ' quote\' ' } from './mod1.js'; + export { ' quote\\\' ' } from './mod2.js'; + export { ' quote\' ' } from './mod3.js'; + "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 4); + assert_eq!(exports.len(), 4); + assert_export_is(source, &exports[0], " slash\\ ", None); + assert_export_is(source, &exports[1], " quote' ", None); + assert_export_is(source, &exports[2], " quote\\' ", None); + assert_export_is(source, &exports[3], " quote' ", None); +} + +#[test] +fn variable_as_non_identifier_string_double_quote() { + let source = r#" + export { foo0 as "~123" } from './mod0.js'; + export { foo1 as "ab cd" } from './mod1.js'; + export { foo2 as "not identifier" } from './mod2.js'; + export { foo3 as "-notidentifier" } from './mod3.js'; + export { foo4 as "%notidentifier" } from './mod4.js'; + export { foo5 as "@notidentifier" } from './mod5.js'; + export { foo6 as " notidentifier" } from './mod6.js'; + export { foo7 as "notidentifier " } from './mod7.js'; + export { foo8 as " notidentifier " } from './mod8.js';"#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 9); + assert_eq!(exports.len(), 9); + assert_export_is(source, &exports[0], "~123", None); + assert_export_is(source, &exports[1], "ab cd", None); + assert_export_is(source, &exports[2], "not identifier", None); + assert_export_is(source, &exports[3], "-notidentifier", None); + assert_export_is(source, &exports[4], "%notidentifier", None); + assert_export_is(source, &exports[5], "@notidentifier", None); + assert_export_is(source, &exports[6], " notidentifier", None); + assert_export_is(source, &exports[7], "notidentifier ", None); + assert_export_is(source, &exports[8], " notidentifier ", None); +} + +#[test] +fn variable_as_non_identifier_string_single_quote() { + let source = r" + export { foo0 as '~123' } from './mod0.js'; + export { foo1 as 'ab cd' } from './mod1.js'; + export { foo2 as 'not identifier' } from './mod2.js'; + export { foo3 as '-notidentifier' } from './mod3.js'; + export { foo4 as '%notidentifier' } from './mod4.js'; + export { foo5 as '@notidentifier' } from './mod5.js'; + export { foo6 as ' notidentifier' } from './mod6.js'; + export { foo7 as 'notidentifier ' } from './mod7.js'; + export { foo8 as ' notidentifier ' } from './mod8.js';"; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 9); + assert_eq!(exports.len(), 9); + assert_export_is(source, &exports[0], "~123", None); + assert_export_is(source, &exports[1], "ab cd", None); + assert_export_is(source, &exports[2], "not identifier", None); + assert_export_is(source, &exports[3], "-notidentifier", None); + assert_export_is(source, &exports[4], "%notidentifier", None); + assert_export_is(source, &exports[5], "@notidentifier", None); + assert_export_is(source, &exports[6], " notidentifier", None); + assert_export_is(source, &exports[7], "notidentifier ", None); + assert_export_is(source, &exports[8], " notidentifier ", None); +} + +#[test] +fn variable_as_with_backslash_keywords_double_quote() { + let source = r#" + export { foo0 as " slash\\ " } from './mod0.js'; + export { foo1 as " quote\" " } from './mod1.js'; + export { foo2 as " quote\\\" " } from './mod2.js'; + export { foo3 as " quote' " } from './mod3.js'; + "#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 4); + assert_eq!(exports.len(), 4); + assert_export_is(source, &exports[0], " slash\\ ", None); + assert_export_is(source, &exports[1], " quote\" ", None); + assert_export_is(source, &exports[2], " quote\\\" ", None); + assert_export_is(source, &exports[3], " quote' ", None); +} + +#[test] +fn variable_as_with_backslash_keywords_single_quote() { + let source = r" + export { foo0 as ' slash\\ ' } from './mod0.js'; + export { foo1 as ' quote\' ' } from './mod1.js'; + export { foo2 as ' quote\\\' ' } from './mod2.js'; + export { foo3 as ' quote\' ' } from './mod3.js'; + "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 4); + assert_eq!(exports.len(), 4); + assert_export_is(source, &exports[0], " slash\\ ", None); + assert_export_is(source, &exports[1], " quote' ", None); + assert_export_is(source, &exports[2], " quote\\' ", None); + assert_export_is(source, &exports[3], " quote' ", None); +} + +#[test] +fn non_identifier_string_as_non_identifier_string_double_quote() { + let source = r#" + export { "~123" as "~123" } from './mod0.js'; + export { "ab cd" as "ab cd" } from './mod1.js'; + export { "not identifier" as "not identifier" } from './mod2.js'; + export { "-notidentifier" as "-notidentifier" } from './mod3.js'; + export { "%notidentifier" as "%notidentifier" } from './mod4.js'; + export { "@notidentifier" as "@notidentifier" } from './mod5.js'; + export { " notidentifier" as " notidentifier" } from './mod6.js'; + export { "notidentifier " as "notidentifier " } from './mod7.js'; + export { " notidentifier " as " notidentifier " } from './mod8.js'; + "#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 9); + assert_eq!(exports.len(), 9); + assert_export_is(source, &exports[0], "~123", None); + assert_export_is(source, &exports[1], "ab cd", None); + assert_export_is(source, &exports[2], "not identifier", None); + assert_export_is(source, &exports[3], "-notidentifier", None); + assert_export_is(source, &exports[4], "%notidentifier", None); + assert_export_is(source, &exports[5], "@notidentifier", None); + assert_export_is(source, &exports[6], " notidentifier", None); + assert_export_is(source, &exports[7], "notidentifier ", None); + assert_export_is(source, &exports[8], " notidentifier ", None); +} + +#[test] +fn non_identifier_string_as_non_identifier_string_single_quote() { + let source = r" + export { '~123' as '~123' } from './mod0.js'; + export { 'ab cd' as 'ab cd' } from './mod1.js'; + export { 'not identifier' as 'not identifier' } from './mod2.js'; + export { '-notidentifier' as '-notidentifier' } from './mod3.js'; + export { '%notidentifier' as '%notidentifier' } from './mod4.js'; + export { '@notidentifier' as '@notidentifier' } from './mod5.js'; + export { ' notidentifier' as ' notidentifier' } from './mod6.js'; + export { 'notidentifier ' as 'notidentifier ' } from './mod7.js'; + export { ' notidentifier ' as ' notidentifier ' } from './mod8.js'; + "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 9); + assert_eq!(exports.len(), 9); + assert_export_is(source, &exports[0], "~123", None); + assert_export_is(source, &exports[1], "ab cd", None); + assert_export_is(source, &exports[2], "not identifier", None); + assert_export_is(source, &exports[3], "-notidentifier", None); + assert_export_is(source, &exports[4], "%notidentifier", None); + assert_export_is(source, &exports[5], "@notidentifier", None); + assert_export_is(source, &exports[6], " notidentifier", None); + assert_export_is(source, &exports[7], "notidentifier ", None); + assert_export_is(source, &exports[8], " notidentifier ", None); +} + +#[test] +fn with_backslash_keywords_as_with_backslash_keywords_double_quote() { + let source = r#" + export { " slash\\ " as " slash\\ " } from './mod0.js'; + export { " quote\"" as " quote\" " } from './mod1.js' + export { " quote\\\" " as " quote\\\" " } from './mod2.js'; + export { " quote' " as " quote' " } from './mod3.js'; + "#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 4); + assert_eq!(exports.len(), 4); + assert_export_is(source, &exports[0], " slash\\ ", None); + assert_export_is(source, &exports[1], " quote\" ", None); + assert_export_is(source, &exports[2], " quote\\\" ", None); + assert_export_is(source, &exports[3], " quote' ", None); +} + +#[test] +fn with_backslash_keywords_as_with_backslash_keywords_single_quote() { + let source = r" + export { ' slash\\ ' as ' slash\\ ' } from './mod0.js'; + export { ' quote\'' as ' quote\' ' } from './mod1.js' + export { ' quote\\\' ' as ' quote\\\' ' } from './mod2.js'; + export { ' quote\' ' as ' quote\' ' } from './mod3.js'; + "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 4); + assert_eq!(exports.len(), 4); + assert_export_is(source, &exports[0], " slash\\ ", None); + assert_export_is(source, &exports[1], " quote' ", None); + assert_export_is(source, &exports[2], " quote\\' ", None); + assert_export_is(source, &exports[3], " quote' ", None); +} + +#[test] +fn curly_brace_double_quote() { + let source = r#" + export { " right-curlybrace} " } from './mod0.js'; + export { " {left-curlybrace " } from './mod1.js'; + export { " {curlybrackets} " } from './mod2.js'; + export { ' right-curlybrace} ' } from './mod0.js'; + export { ' {left-curlybrace ' } from './mod1.js'; + export { ' {curlybrackets} ' } from './mod2.js'; + "#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 6); + assert_eq!(exports.len(), 6); + assert_export_is(source, &exports[0], " right-curlybrace} ", None); + assert_export_is(source, &exports[1], " {left-curlybrace ", None); + assert_export_is(source, &exports[2], " {curlybrackets} ", None); + assert_export_is(source, &exports[3], " right-curlybrace} ", None); + assert_export_is(source, &exports[4], " {left-curlybrace ", None); + assert_export_is(source, &exports[5], " {curlybrackets} ", None); +} + +#[test] +fn as_curly_brace_double_quote() { + let source = r#" + export { foo as " right-curlybrace} " } from './mod0.js'; + export { foo as " {left-curlybrace " } from './mod1.js'; + export { foo as " {curlybrackets} " } from './mod2.js'; + export { foo as ' right-curlybrace} ' } from './mod0.js'; + export { foo as ' {left-curlybrace ' } from './mod1.js'; + export { foo as ' {curlybrackets} ' } from './mod2.js'; + "#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 6); + assert_eq!(exports.len(), 6); + assert_export_is(source, &exports[0], " right-curlybrace} ", None); + assert_export_is(source, &exports[1], " {left-curlybrace ", None); + assert_export_is(source, &exports[2], " {curlybrackets} ", None); + assert_export_is(source, &exports[3], " right-curlybrace} ", None); + assert_export_is(source, &exports[4], " {left-curlybrace ", None); + assert_export_is(source, &exports[5], " {curlybrackets} ", None); +} + +#[test] +fn curly_brace_as_curly_brace_double_quote() { + let source = r#" + export { " right-curlybrace} " as " right-curlybrace} " } from './mod0.js'; + export { " {left-curlybrace " as " {left-curlybrace " } from './mod1.js'; + export { " {curlybrackets} " as " {curlybrackets} " } from './mod2.js'; + export { ' right-curlybrace} ' as ' right-curlybrace} ' } from './mod0.js'; + export { ' {left-curlybrace ' as ' {left-curlybrace ' } from './mod1.js'; + export { ' {curlybrackets} ' as ' {curlybrackets} ' } from './mod2.js'; + "#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 6); + assert_eq!(exports.len(), 6); + assert_export_is(source, &exports[0], " right-curlybrace} ", None); + assert_export_is(source, &exports[1], " {left-curlybrace ", None); + assert_export_is(source, &exports[2], " {curlybrackets} ", None); + assert_export_is(source, &exports[3], " right-curlybrace} ", None); + assert_export_is(source, &exports[4], " {left-curlybrace ", None); + assert_export_is(source, &exports[5], " {curlybrackets} ", None); +} + +#[test] +fn complex_and_edge_cases() { + let source = r#" + export { + foo, + foo1 as foo2, + " {left-curlybrace ", + " {curly-brackets}" as "@notidentifier", + "?" as "identifier", + } from './mod0.js'; + export { "p as 'z' from 'asdf'" as "z'" } from 'asdf'; + export { "z'" as "p as 'z' from 'asdf'" } from 'asdf'; + "#; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 3); + assert_eq!(exports.len(), 7); + assert_export_is(source, &exports[0], "foo", None); + assert_export_is(source, &exports[1], "foo2", None); + assert_export_is(source, &exports[2], " {left-curlybrace ", None); + assert_export_is(source, &exports[3], "@notidentifier", None); + assert_export_is(source, &exports[4], "identifier", None); + assert_export_is(source, &exports[5], "z'", None); + assert_export_is(source, &exports[6], "p as 'z' from 'asdf'", None); +} + +#[test] +fn export_default() { + let source = r" + export default async function example () {}; + export const a = '1'; + export default a; + export default function example1() {}; + export default function() {}; + export default class className {/* ... */}; + export default class {} + export default function* generatorFunctionName(){/* ... */}; + export default function* () {}; + const async = 1 + export default async + + function x() {} + + const asyncVar = 1 + export default asyncVar + + function functionName () {}; + export default functionName; + "; + let ModuleLexer { imports, exports, .. } = parse(source); + assert_eq!(imports.len(), 0); + assert_eq!(exports.len(), 12); + assert_export_is(source, &exports[0], "default", Some("example")); + assert_export_is(source, &exports[1], "a", Some("a")); + assert_export_is(source, &exports[2], "default", None); + assert_export_is(source, &exports[3], "default", Some("example1")); + assert_export_is(source, &exports[4], "default", None); + assert_export_is(source, &exports[5], "default", Some("className")); + assert_export_is(source, &exports[6], "default", None); + assert_export_is(source, &exports[7], "default", Some("generatorFunctionName")); + assert_export_is(source, &exports[8], "default", None); + assert_export_is(source, &exports[9], "default", None); + assert_export_is(source, &exports[10], "default", None); + assert_export_is(source, &exports[11], "default", None); +} + +/* Suite Invalid Syntax */ + +fn expect_parse_error(source: &str) { + let allocator = Allocator::default(); + let source_type = SourceType::default().with_module(true); + let ret = Parser::new(&allocator, source, source_type).parse(); + assert!(!ret.errors.is_empty()); +} + +#[test] +fn unterminated_object() { + let source = r" + const foo = }; + const bar = {}; + "; + expect_parse_error(source); +} + +#[test] +fn invalid_string() { + let source = r"import './export.js'; + + import d from './export.js'; + + import { s as p } from './reexport1.js'; + + import { z, q as r } from './reexport2.js'; + + ' + + import * as q from './reexport1.js'; + + export { d as a, p as b, z as c, r as d, q }`"; + expect_parse_error(source); +} + +#[test] +fn invalid_export() { + let source = "export { a = }"; + expect_parse_error(source); +} + +/* has_module_syntax */ + +#[test] +fn has_module_syntax_import1() { + let has_module_syntax = parse(r#"import foo from "./foo""#).has_module_syntax; + assert!(has_module_syntax); +} + +#[test] +fn has_module_syntax_import2() { + let has_module_syntax = parse(r#"const foo = "import""#).has_module_syntax; + assert!(!has_module_syntax); +} + +#[test] +fn has_module_syntax_import3() { + let has_module_syntax = parse(r#"import("./foo")"#).has_module_syntax; + // dynamic imports can be used in non-ESM files as well + assert!(!has_module_syntax); +} + +#[test] +fn has_module_syntax_import4() { + let has_module_syntax = parse(r"import.meta.url").has_module_syntax; + assert!(has_module_syntax); +} + +#[test] +fn has_module_syntax_export1() { + let has_module_syntax = parse(r#"export const foo = "foo""#).has_module_syntax; + assert!(has_module_syntax); +} + +#[test] +fn has_module_syntax_export2() { + let has_module_syntax = parse(r"export {}").has_module_syntax; + assert!(has_module_syntax); +} + +#[test] +fn has_module_syntax_export3() { + let has_module_syntax = parse(r#"export * from "./foo""#).has_module_syntax; + assert!(has_module_syntax); +} + +/* facade */ + +#[test] +fn facade() { + let facade = parse( + r" + export * from 'external'; + import * as ns from 'external2'; + export { a as b } from 'external3'; + export { ns }; + ", + ) + .facade; + assert!(facade); +} + +#[test] +fn facade_default() { + let facade = parse( + r" + import * as ns from 'external'; + export default ns; + ", + ) + .facade; + assert!(!facade); +} + +#[test] +fn facade_declaration1() { + let facade = parse(r"export function p () {}").facade; + assert!(!facade); +} + +#[test] +fn facade_declaration2() { + let facade = parse(r"export var p").facade; + assert!(!facade); +} + +#[test] +fn facade_declaration3() { + let facade = parse(r"export {}").facade; + assert!(facade); +} + +#[test] +fn facade_declaration4() { + let facade = parse(r"export class Q{}").facade; + assert!(!facade); +} + +#[test] +fn facade_side_effect() { + let facade = parse(r"console.log('any non esm syntax')").facade; + assert!(!facade); +} diff --git a/crates/oxc_parser/src/js/module.rs b/crates/oxc_parser/src/js/module.rs index 790663f2f8bb3..5422f24c80fca 100644 --- a/crates/oxc_parser/src/js/module.rs +++ b/crates/oxc_parser/src/js/module.rs @@ -136,7 +136,6 @@ impl<'a> ParserImpl<'a> { /// [Import Attributes](https://tc39.es/proposal-import-attributes) fn parse_import_attributes(&mut self) -> Result>> { - let span = self.start_span(); let attributes_keyword = match self.cur_kind() { Kind::Assert if !self.cur_token().is_on_new_line => self.parse_identifier_name()?, Kind::With => self.parse_identifier_name()?, @@ -144,7 +143,7 @@ impl<'a> ParserImpl<'a> { return Ok(None); } }; - + let span = self.start_span(); let ctx = self.ctx; self.ctx = Context::default(); let with_entries = AssertEntries::parse(self)?.elements; diff --git a/napi/parser/Cargo.toml b/napi/parser/Cargo.toml index d077852177b0c..1148b7e5c1a7d 100644 --- a/napi/parser/Cargo.toml +++ b/napi/parser/Cargo.toml @@ -26,14 +26,15 @@ oxc_parser = { workspace = true } oxc_ast = { workspace = true, features = ["serde"] } oxc_span = { workspace = true } oxc_diagnostics = { workspace = true } +oxc_module_lexer = { path = "../../crates/oxc_module_lexer" } serde = { workspace = true } serde_json = { workspace = true } flexbuffers = { version = "2.0.0" } tokio = { workspace = true } -napi = { version = "2", features = ["serde-json", "async"] } -napi-derive = { version = "2" } +napi = { workspace = true, features = ["serde-json", "async"] } +napi-derive = { workspace = true } [build-dependencies] napi-build = "2" diff --git a/napi/parser/index.d.ts b/napi/parser/index.d.ts index 3cd8724934e36..7a39b57c597a1 100644 --- a/napi/parser/index.d.ts +++ b/napi/parser/index.d.ts @@ -3,6 +3,77 @@ /* auto-generated by NAPI-RS */ +export interface ImportSpecifier { + /** + * Module name + * + * To handle escape sequences in specifier strings, the .n field of imported specifiers will be provided where possible. + * + * For dynamic import expressions, this field will be empty if not a valid JS string. + */ + n?: string + /** Start of module specifier */ + s: number + /** End of module specifier */ + e: number + /** Start of import statement */ + ss: number + /** End of import statement */ + se: number + /** + * Import Type + * * If this import keyword is a dynamic import, this is the start value. + * * If this import keyword is a static import, this is -1. + * * If this import keyword is an import.meta expresion, this is -2. + */ + d: number + /** + * If this import has an import assertion, this is the start value + * Otherwise this is `-1`. + */ + a: number +} +export interface ExportSpecifier { + /** Exported name */ + n: string + /** Local name, or undefined. */ + ln?: string + /** Start of exported name */ + s: number + /** End of exported name */ + e: number + /** Start of local name */ + ls?: number + /** End of local name */ + le?: number +} +export interface ModuleLexer { + imports: Array + exports: Array + /** + * ESM syntax detection + * + * The use of ESM syntax: import / export statements and `import.meta` + */ + hasModuleSyntax: boolean + /** Facade modules that only use import / export syntax */ + facade: boolean +} +/** + * Outputs the list of exports and locations of import specifiers, + * including dynamic import and import meta handling. + * + * # Panics + * + * * File extension is invalid + */ +export function moduleLexerSync(sourceText: string, options?: ParserOptions | undefined | null): ModuleLexer +/** + * # Panics + * + * * Tokio crashes + */ +export function moduleLexerAsync(sourceText: string, options?: ParserOptions | undefined | null): Promise /** * Babel Parser Options * @@ -53,6 +124,7 @@ export function parseSync(sourceText: string, options?: ParserOptions | undefine /** * Returns a binary AST in flexbuffers format. * This is a POC API. Error handling is not done yet. + * * # Panics * * * File extension is invalid diff --git a/napi/parser/index.js b/napi/parser/index.js index 4b1b9d040c71a..5ea0192837715 100644 --- a/napi/parser/index.js +++ b/napi/parser/index.js @@ -295,8 +295,10 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { parseWithoutReturn, parseSync, parseSyncBuffer, parseAsync } = nativeBinding +const { moduleLexerSync, moduleLexerAsync, parseWithoutReturn, parseSync, parseSyncBuffer, parseAsync } = nativeBinding +module.exports.moduleLexerSync = moduleLexerSync +module.exports.moduleLexerAsync = moduleLexerAsync module.exports.parseWithoutReturn = parseWithoutReturn module.exports.parseSync = parseSync module.exports.parseSyncBuffer = parseSyncBuffer diff --git a/napi/parser/package.json b/napi/parser/package.json index 12a42dfbf646a..473f07198d2d4 100644 --- a/napi/parser/package.json +++ b/napi/parser/package.json @@ -7,6 +7,7 @@ }, "devDependencies": { "@napi-rs/cli": "^2.18.0", + "es-module-lexer": "^1.4.1", "flatbuffers": "^23.5.26" }, "engines": { diff --git a/napi/parser/pnpm-lock.yaml b/napi/parser/pnpm-lock.yaml index 0050cba45c9b0..743e5e4599e22 100644 --- a/napi/parser/pnpm-lock.yaml +++ b/napi/parser/pnpm-lock.yaml @@ -1,13 +1,12 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - devDependencies: '@napi-rs/cli': specifier: ^2.18.0 version: 2.18.0 + es-module-lexer: + specifier: ^1.4.1 + version: 1.4.1 flatbuffers: specifier: ^23.5.26 version: 23.5.26 @@ -20,6 +19,14 @@ packages: hasBin: true dev: true + /es-module-lexer@1.4.1: + resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} + dev: true + /flatbuffers@23.5.26: resolution: {integrity: sha512-vE+SI9vrJDwi1oETtTIFldC/o9GsVKRM+s6EL0nQgxXlYV1Vc4Tk30hj4xGICftInKQKj1F3up2n8UbIVobISQ==} dev: true + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/napi/parser/src/lib.rs b/napi/parser/src/lib.rs index 3db2b8e9fa12d..7b32900449658 100644 --- a/napi/parser/src/lib.rs +++ b/napi/parser/src/lib.rs @@ -1,19 +1,23 @@ #![allow(clippy::trailing_empty_array)] +mod module_lexer; + use std::sync::Arc; use flexbuffers::FlexbufferSerializer; use napi::bindgen_prelude::Buffer; use napi_derive::napi; -use oxc_ast::CommentKind; use serde::Serialize; use oxc_allocator::Allocator; pub use oxc_ast::ast::Program; +use oxc_ast::CommentKind; use oxc_diagnostics::miette::NamedSource; use oxc_parser::{Parser, ParserReturn}; use oxc_span::SourceType; +pub use crate::module_lexer::*; + /// Babel Parser Options /// /// @@ -129,6 +133,7 @@ pub fn parse_sync(source_text: String, options: Option) -> ParseR /// Returns a binary AST in flexbuffers format. /// This is a POC API. Error handling is not done yet. +/// /// # Panics /// /// * File extension is invalid diff --git a/napi/parser/src/module_lexer.rs b/napi/parser/src/module_lexer.rs new file mode 100644 index 0000000000000..c635b5b638d20 --- /dev/null +++ b/napi/parser/src/module_lexer.rs @@ -0,0 +1,146 @@ +use napi_derive::napi; + +use oxc_allocator::Allocator; +use oxc_module_lexer::ImportType; + +use crate::{parse, ParserOptions}; + +#[napi(object)] +pub struct ImportSpecifier { + /// Module name + /// + /// To handle escape sequences in specifier strings, the .n field of imported specifiers will be provided where possible. + /// + /// For dynamic import expressions, this field will be empty if not a valid JS string. + pub n: Option, + + /// Start of module specifier + pub s: u32, + + /// End of module specifier + pub e: u32, + + /// Start of import statement + pub ss: u32, + + /// End of import statement + pub se: u32, + + /// Import Type + /// * If this import keyword is a dynamic import, this is the start value. + /// * If this import keyword is a static import, this is -1. + /// * If this import keyword is an import.meta expresion, this is -2. + pub d: i64, + + /// If this import has an import assertion, this is the start value + /// Otherwise this is `-1`. + pub a: i64, +} + +#[napi(object)] +pub struct ExportSpecifier { + /// Exported name + pub n: String, + + /// Local name, or undefined. + pub ln: Option, + + /// Start of exported name + pub s: u32, + + /// End of exported name + pub e: u32, + + /// Start of local name + pub ls: Option, + + /// End of local name + pub le: Option, +} + +impl<'a> From> for ImportSpecifier { + #[allow(clippy::cast_lossless)] + fn from(i: oxc_module_lexer::ImportSpecifier) -> Self { + Self { + n: i.n.map(|n| n.to_string()), + s: i.s, + e: i.e, + ss: i.ss, + se: i.se, + d: match i.d { + ImportType::DynamicImport(start) => start as i64, + ImportType::StaticImport => -1, + ImportType::ImportMeta => -2, + }, + a: i.a.map_or(-1, |a| a as i64), + } + } +} + +impl<'a> From> for ExportSpecifier { + fn from(e: oxc_module_lexer::ExportSpecifier) -> Self { + Self { + n: e.n.to_string(), + ln: e.ln.map(|ln| ln.to_string()), + s: e.s, + e: e.e, + ls: e.ls, + le: e.le, + } + } +} + +#[napi(object)] +pub struct ModuleLexer { + pub imports: Vec, + + pub exports: Vec, + + /// ESM syntax detection + /// + /// The use of ESM syntax: import / export statements and `import.meta` + pub has_module_syntax: bool, + + /// Facade modules that only use import / export syntax + pub facade: bool, +} + +#[allow(clippy::needless_pass_by_value)] +fn module_lexer(source_text: String, options: Option) -> ModuleLexer { + let options = options.unwrap_or_default(); + let allocator = Allocator::default(); + let ret = parse(&allocator, &source_text, &options); + let module_lexer = oxc_module_lexer::ModuleLexer::new().build(&ret.program); + let imports = module_lexer.imports.into_iter().map(ImportSpecifier::from).collect(); + let exports = module_lexer.exports.into_iter().map(ExportSpecifier::from).collect(); + ModuleLexer { + imports, + exports, + has_module_syntax: module_lexer.has_module_syntax, + facade: module_lexer.facade, + } +} + +/// Outputs the list of exports and locations of import specifiers, +/// including dynamic import and import meta handling. +/// +/// # Panics +/// +/// * File extension is invalid +#[napi] +#[allow(clippy::needless_pass_by_value)] +pub fn module_lexer_sync(source_text: String, options: Option) -> ModuleLexer { + module_lexer(source_text, options) +} + +/// # Panics +/// +/// * Tokio crashes +#[napi] +#[allow(clippy::needless_pass_by_value)] +pub async fn module_lexer_async( + source_text: String, + options: Option, +) -> ModuleLexer { + tokio::spawn(async move { module_lexer(source_text, options) }).await.unwrap() +} diff --git a/napi/parser/test_module_lexer.mjs b/napi/parser/test_module_lexer.mjs new file mode 100644 index 0000000000000..e208a3a3fb89d --- /dev/null +++ b/napi/parser/test_module_lexer.mjs @@ -0,0 +1,103 @@ +/* + * Test and bench against `es-module-lexer` + */ +import fs from "fs"; +import assert from "assert"; + +import oxc from "./index.js"; +import * as esModuleLexer from "es-module-lexer"; + +const root = "/Users/boshen/github/es-module-lexer/test/samples"; + +main(); + +async function main() { + await esModuleLexer.init; + + const files = fs + .readdirSync(root) + .filter((x) => x.endsWith(".js")) + .map((file) => { + const source = fs.readFileSync(`${root}/${file}`); + return { + file, + code: source.toString(), + size: source.byteLength, + }; + }); + + function test(source) { + const [imports, exports, facade, hasModuleSyntax] = + esModuleLexer.parse(source); + const moduleLexer = oxc.moduleLexerSync(source); + assert(moduleLexer.imports.length == imports.length); + assert(moduleLexer.exports.length == exports.length); + assert(moduleLexer.facade == facade); + assert(moduleLexer.hasModuleSyntax == hasModuleSyntax); + } + + function testAll(files) { + for (const file of files) { + test(file.code); + } + } + + testAll(files); + + const n = 25; + + bench("es-module-lexer", esModuleLexer.parse); + bench("oxc", oxc.moduleLexerSync); + + function bench(name, fn) { + console.log("-----------"); + console.log(name); + + doRun(); + + function timeRun(code) { + const start = process.hrtime.bigint(); + fn(code); + const end = process.hrtime.bigint(); + return Math.round(Number(end - start) / 1e6); + } + + function doRun() { + console.log("Cold Run, All Samples"); + let totalSize = 0; + { + let total = 0; + files.forEach(({ code, size }) => { + totalSize += size; + total += timeRun(code); + }); + console.log(`test/samples/*.js (${Math.round(totalSize / 1e3)} KiB)`); + console.log(`> ${total + "ms"}`); + } + + console.log(`\nWarm Runs (average of ${n} runs)`); + files.forEach(({ file, code, size }) => { + console.log(`${file} (${Math.round(size / 1e3)} KiB)`); + + let total = 0; + for (let i = 0; i < n; i++) { + total += timeRun(code); + } + + console.log(`> ${total / n + "ms"}`); + }); + + console.log(`\nWarm Runs, All Samples (average of ${n} runs)`); + { + let total = 0; + for (let i = 0; i < n; i++) { + files.forEach(({ code }) => { + total += timeRun(code); + }); + } + console.log(`test/samples/*.js (${Math.round(totalSize / 1e3)} KiB)`); + console.log(`> ${total / n + "ms"}`); + } + } + } +}