#[cfg(test)] use oxc_allocator::Allocator; use oxc_ast::ast::{ Argument, BinaryOperator, CallExpression, Expression, ForOfStatement, FunctionBody, ImportExpression, MetaProperty, ObjectExpression, ObjectProperty, ObjectPropertyKind, Program, PropertyKey, PropertyKind, StaticMemberExpression, TemplateLiteral, }; use oxc_ast_visit::{Visit, walk}; #[cfg(test)] use oxc_parser::Parser as OxcParser; #[cfg(test)] use oxc_span::SourceType; use oxc_span::Span; use crate::types::{ DynamicImportRewriteKind, DynamicImportRewriteSite, ModuleAstSpan, ModuleSpecialFormRewriteSites, }; pub(crate) struct ModuleRewriteSites { pub special_forms: ModuleSpecialFormRewriteSites, pub top_level_awaits: Vec, } #[cfg(test)] const SPECIAL_FORM_EXPRESSION_PREFIX: &str = "const __lm_special_form_expr = "; #[cfg(test)] const SPECIAL_FORM_EXPRESSION_SUFFIX: &str = ";"; #[cfg(test)] const SPECIAL_FORM_PARSE_ERROR: &str = "unsupported module special-form syntax"; #[cfg(test)] pub(crate) fn collect_module_special_form_rewrite_sites( source: &str, ) -> std::result::Result { if !source.contains("import") || source_is_only_js_trivia_or_hashbang(source) { return Ok(ModuleSpecialFormRewriteSites::default()); } match collect_module_special_form_rewrite_sites_with_offset(source, 0) { Ok(sites) => Ok(sites), Err(error) if error == SPECIAL_FORM_PARSE_ERROR => { let wrapped = format!("{SPECIAL_FORM_EXPRESSION_PREFIX}{source}{SPECIAL_FORM_EXPRESSION_SUFFIX}"); collect_module_special_form_rewrite_sites_with_offset( &wrapped, SPECIAL_FORM_EXPRESSION_PREFIX.len() as isize, ) } Err(error) => Err(error), } } #[cfg(test)] fn collect_module_special_form_rewrite_sites_with_offset( source: &str, offset: isize, ) -> std::result::Result { let allocator = Allocator::default(); let parsed = OxcParser::new(&allocator, source, SourceType::mjs()).parse(); if !parsed.errors.is_empty() { return Err(SPECIAL_FORM_PARSE_ERROR.to_owned()); } let mut collector = ModuleRewriteSiteCollector::new(source, offset); collector.visit_program(&parsed.program); collector.finish().map(|sites| sites.special_forms) } pub(crate) fn collect_module_rewrite_sites_from_program( source: &str, program: &Program<'_>, ) -> std::result::Result { let mut collector = ModuleRewriteSiteCollector::new(source, 0); collector.visit_program(program); collector.finish() } #[cfg(test)] fn source_is_only_js_trivia_or_hashbang(source: &str) -> bool { let bytes = source.as_bytes(); let mut index = 0; if bytes.starts_with(b"#!") { index = skip_line(bytes, 2); } loop { index = skip_ascii_whitespace(bytes, index); if index >= bytes.len() { return true; } if bytes[index..].starts_with(b"//") { index = skip_line(bytes, index + 2); continue; } if bytes[index..].starts_with(b"/*") { let Some(end) = find_block_comment_end(bytes, index + 2) else { return false; }; index = end; continue; } return false; } } #[cfg(test)] fn skip_ascii_whitespace(bytes: &[u8], mut index: usize) -> usize { while index < bytes.len() && matches!(bytes[index], b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c) { index += 1; } index } #[cfg(test)] fn skip_line(bytes: &[u8], mut index: usize) -> usize { while index < bytes.len() && !matches!(bytes[index], b'\n' | b'\r') { index += 1; } index } #[cfg(test)] fn find_block_comment_end(bytes: &[u8], mut index: usize) -> Option { while index + 1 < bytes.len() { if bytes[index] == b'*' && bytes[index + 1] == b'/' { return Some(index + 2); } index += 1; } None } struct ModuleRewriteSiteCollector<'s> { source: &'s str, offset: isize, special_forms: ModuleSpecialFormRewriteSites, top_level_awaits: Vec, function_depth: usize, errors: Vec, } impl<'s> ModuleRewriteSiteCollector<'s> { fn new(source: &'s str, offset: isize) -> Self { Self { source, offset, special_forms: ModuleSpecialFormRewriteSites::default(), top_level_awaits: Vec::new(), function_depth: 0, errors: Vec::new(), } } fn finish(mut self) -> std::result::Result { if !self.errors.is_empty() { return Err(self.errors.remove(0)); } self.special_forms .import_metas .sort_by_key(|span| (span.start, span.end)); self.special_forms .import_meta_urls .sort_by_key(|span| (span.start, span.end)); self.special_forms .import_meta_resolves .sort_by_key(|site| (site.replace_start, site.replace_end)); self.special_forms .dynamic_imports .sort_by_key(|site| (site.replace_start, site.replace_end)); self.top_level_awaits .sort_by_key(|span| (span.start, span.end)); Ok(ModuleRewriteSites { special_forms: self.special_forms, top_level_awaits: self.top_level_awaits, }) } fn push_unsupported_special_form(&mut self, span: Span, form: &str) { let snippet = self .source .get(span.start as usize..span.end as usize) .unwrap_or(form); self.errors .push(format!("unsupported {form} syntax `{snippet}`")); } fn shifted_span(&self, span: Span) -> Option<(usize, usize)> { let start = span.start as isize - self.offset; let end = span.end as isize - self.offset; if start < 0 || end < start { return None; } Some((start as usize, end as usize)) } fn shifted_module_span(&self, span: Span) -> Option { let (start, end) = self.shifted_span(span)?; Some(ModuleAstSpan { start: start.try_into().ok()?, end: end.try_into().ok()?, }) } fn push_top_level_await(&mut self, span: Span) { if self.function_depth == 0 && let Some(span) = self.shifted_module_span(span) { self.top_level_awaits.push(span); } } } impl<'a> Visit<'a> for ModuleRewriteSiteCollector<'_> { fn visit_static_member_expression(&mut self, expression: &StaticMemberExpression<'a>) { if static_member_is_import_meta_url(expression) { if let Some(span) = self.shifted_module_span(expression.span) { self.special_forms.import_meta_urls.push(span); } return; } walk::walk_static_member_expression(self, expression); } fn visit_call_expression(&mut self, expression: &CallExpression<'a>) { walk::walk_call_expression(self, expression); } fn visit_meta_property(&mut self, meta: &MetaProperty<'a>) { if meta_property_is_import_meta(meta) && let Some(span) = self.shifted_module_span(meta.span) { self.special_forms.import_metas.push(span); } } fn visit_await_expression(&mut self, expression: &oxc_ast::ast::AwaitExpression<'a>) { self.push_top_level_await(expression.span); if let Expression::ImportExpression(import) = &expression.argument { if let Some(site) = dynamic_import_site_from_oxc( self.source, self.offset, expression.span, import, DynamicImportRewriteKind::AwaitedNamespace, ) { self.special_forms.dynamic_imports.push(site); } else if dynamic_import_is_css_import(import) { return; } else { self.push_unsupported_special_form(expression.span, "dynamic import"); } return; } walk::walk_await_expression(self, expression); } fn visit_import_expression(&mut self, expression: &ImportExpression<'a>) { if let Some(site) = dynamic_import_site_from_oxc( self.source, self.offset, expression.span, expression, DynamicImportRewriteKind::Promise, ) { self.special_forms.dynamic_imports.push(site); } else if !dynamic_import_is_css_import(expression) { self.push_unsupported_special_form(expression.span, "dynamic import"); } } fn visit_for_of_statement(&mut self, statement: &ForOfStatement<'a>) { if statement.r#await { self.push_top_level_await(statement.span); } walk::walk_for_of_statement(self, statement); } fn visit_function_body(&mut self, body: &FunctionBody<'a>) { self.function_depth += 1; walk::walk_function_body(self, body); self.function_depth -= 1; } } fn static_member_is_import_meta_url(expression: &StaticMemberExpression<'_>) -> bool { expression.property.name == "url" && expression_is_import_meta(&expression.object) } fn expression_is_import_meta(expression: &Expression<'_>) -> bool { let Expression::MetaProperty(meta) = expression else { return false; }; meta_property_is_import_meta(meta) } fn meta_property_is_import_meta(meta: &MetaProperty<'_>) -> bool { meta.meta.name == "import" && meta.property.name == "meta" } fn dynamic_import_site_from_oxc( source: &str, offset: isize, replace_span: Span, expression: &ImportExpression<'_>, kind: DynamicImportRewriteKind, ) -> Option { let (specifier, resolve_import_meta_first) = dynamic_import_specifier_from_expression(&expression.source)?; let import_type = match &expression.options { Some(options) => dynamic_import_options_type_from_expression(options)?, None => None, }; let (replace_start, replace_end) = shifted_span(replace_span, offset)?; Some(DynamicImportRewriteSite { specifier, resolve_import_meta_first, import_type, replace_start, replace_end, kind, }) .filter(|_| { source .get(replace_span.start as usize..replace_span.end as usize) .is_some() }) } fn dynamic_import_specifier_from_expression(expression: &Expression<'_>) -> Option<(String, bool)> { if let Some(specifier) = static_string_from_expression(expression) { return Some((specifier, false)); } let Expression::CallExpression(call) = expression else { return None; }; if !static_member_is_import_meta_resolve_call(call) { return None; } Some((static_import_meta_resolve_specifier(call)?, true)) } fn static_member_is_import_meta_resolve_call(expression: &CallExpression<'_>) -> bool { let Expression::StaticMemberExpression(member) = &expression.callee else { return false; }; member.property.name == "resolve" && expression_is_import_meta(&member.object) } fn static_import_meta_resolve_specifier(expression: &CallExpression<'_>) -> Option { let Some(argument) = expression.arguments.first() else { return Some("undefined".to_owned()); }; static_string_from_argument(argument) } fn dynamic_import_is_css_import(expression: &ImportExpression<'_>) -> bool { expression .options .as_ref() .and_then(dynamic_import_options_type_from_expression) .flatten() .is_some_and(|import_type| import_type.eq_ignore_ascii_case("css")) } fn shifted_span(span: Span, offset: isize) -> Option<(usize, usize)> { let start = span.start as isize - offset; let end = span.end as isize - offset; if start < 0 || end < start { return None; } Some((start as usize, end as usize)) } fn static_string_from_argument(argument: &Argument<'_>) -> Option { match argument { Argument::StringLiteral(literal) => Some(literal.value.as_str().to_owned()), Argument::TemplateLiteral(template) => static_string_from_template_literal(template), Argument::ParenthesizedExpression(parenthesized) => { static_string_from_expression(&parenthesized.expression) } Argument::BinaryExpression(binary) if binary.operator == BinaryOperator::Addition => { let mut value = static_string_from_expression(&binary.left)?; value.push_str(&static_string_from_expression(&binary.right)?); Some(value) } _ => None, } } fn static_string_from_expression(expression: &Expression<'_>) -> Option { match expression { Expression::StringLiteral(literal) => Some(literal.value.as_str().to_owned()), Expression::TemplateLiteral(template) => static_string_from_template_literal(template), Expression::ParenthesizedExpression(parenthesized) => { static_string_from_expression(&parenthesized.expression) } Expression::BinaryExpression(binary) if binary.operator == BinaryOperator::Addition => { let mut value = static_string_from_expression(&binary.left)?; value.push_str(&static_string_from_expression(&binary.right)?); Some(value) } _ => None, } } fn static_string_from_template_literal(template: &TemplateLiteral<'_>) -> Option { if !template.expressions.is_empty() { return None; } let mut value = String::new(); for quasi in &template.quasis { value.push_str(quasi.value.cooked.as_ref()?.as_str()); } Some(value) } fn dynamic_import_options_type_from_expression( expression: &Expression<'_>, ) -> Option> { let object = static_object_from_expression(expression)?; let mut found_attribute_bag = false; let mut import_type = None; for property in &object.properties { let property = strict_static_object_property(property)?; let key = property_key_static_name(property)?; match key.as_str() { "with" | "assert" => { if found_attribute_bag { return None; } found_attribute_bag = true; import_type = import_attribute_type_from_expression(&property.value)?; } _ => return None, } } Some(import_type) } fn import_attribute_type_from_expression(expression: &Expression<'_>) -> Option> { let object = static_object_from_expression(expression)?; import_attribute_type_from_object(object) } fn import_attribute_type_from_object(object: &ObjectExpression<'_>) -> Option> { let mut import_type = None; for property in &object.properties { let property = strict_static_object_property(property)?; if property_key_static_name(property)? != "type" { return None; } if import_type.is_some() { return None; } import_type = Some(static_string_from_expression(&property.value)?); } Some(import_type) } fn static_object_from_expression<'a>( expression: &'a Expression<'a>, ) -> Option<&'a ObjectExpression<'a>> { match expression { Expression::ObjectExpression(object) => Some(object), Expression::ParenthesizedExpression(parenthesized) => { static_object_from_expression(&parenthesized.expression) } _ => None, } } fn strict_static_object_property<'a>( property: &'a ObjectPropertyKind<'a>, ) -> Option<&'a ObjectProperty<'a>> { let ObjectPropertyKind::ObjectProperty(property) = property else { return None; }; if property.kind != PropertyKind::Init || property.method || property.shorthand || property.computed { return None; } Some(property) } fn property_key_static_name(property: &ObjectProperty<'_>) -> Option { match &property.key { PropertyKey::StaticIdentifier(identifier) => Some(identifier.name.as_str().to_owned()), PropertyKey::StringLiteral(literal) => Some(literal.value.as_str().to_owned()), _ => None, } }