diff --git a/Cargo.lock b/Cargo.lock index 01d7181af2..decd51f895 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2228,12 +2228,14 @@ dependencies = [ "oxc_sourcemap", "oxc_span", "oxc_syntax", + "oxc_tasks_common", "oxc_traverse", "pico-args", "rustc-hash", "serde", "serde_json", "sha1", + "similar", ] [[package]] diff --git a/crates/oxc_transformer/Cargo.toml b/crates/oxc_transformer/Cargo.toml index 0036c7fc15..b8d9700983 100644 --- a/crates/oxc_transformer/Cargo.toml +++ b/crates/oxc_transformer/Cargo.toml @@ -50,7 +50,9 @@ insta = { workspace = true } oxc_codegen = { workspace = true } oxc_parser = { workspace = true } oxc_sourcemap = { workspace = true } +oxc_tasks_common = { workspace = true } pico-args = { workspace = true } +similar = { workspace = true } [features] default = [] diff --git a/crates/oxc_transformer/src/decorator/legacy/metadata.rs b/crates/oxc_transformer/src/decorator/legacy/metadata.rs index 221e94bdba..d873330411 100644 --- a/crates/oxc_transformer/src/decorator/legacy/metadata.rs +++ b/crates/oxc_transformer/src/decorator/legacy/metadata.rs @@ -375,7 +375,7 @@ impl<'a> LegacyDecoratorMetadata<'a, '_> { let binding = MaybeBoundIdentifier::from_identifier_reference(ident, ctx); let ident1 = binding.create_read_expression(ctx); let ident2 = binding.create_read_expression(ctx); - let member = create_property_access(ident1, &qualified.right.name, ctx); + let member = create_property_access(SPAN, ident1, &qualified.right.name, ctx); Self::create_checked_value(ident2, member, ctx) } else { // `A.B.C` -> `typeof A !== "undefined" && (_a = A.B) !== void 0 && _a.C` @@ -401,7 +401,7 @@ impl<'a> LegacyDecoratorMetadata<'a, '_> { ); let object = binding.create_read_expression(ctx); - let member = create_property_access(object, &qualified.right.name, ctx); + let member = create_property_access(SPAN, object, &qualified.right.name, ctx); ctx.ast.expression_logical(SPAN, left, LogicalOperator::And, member) } } diff --git a/crates/oxc_transformer/src/plugins/mod.rs b/crates/oxc_transformer/src/plugins/mod.rs index fdf6d786e0..d47d1e1e91 100644 --- a/crates/oxc_transformer/src/plugins/mod.rs +++ b/crates/oxc_transformer/src/plugins/mod.rs @@ -1,5 +1,7 @@ mod inject_global_variables; +mod module_runner_transform; mod replace_global_defines; pub use inject_global_variables::*; +pub use module_runner_transform::*; pub use replace_global_defines::*; diff --git a/crates/oxc_transformer/src/plugins/module_runner_transform.rs b/crates/oxc_transformer/src/plugins/module_runner_transform.rs new file mode 100644 index 0000000000..47a3724814 --- /dev/null +++ b/crates/oxc_transformer/src/plugins/module_runner_transform.rs @@ -0,0 +1,2087 @@ +/// Module runner transform +/// +/// This plugin is used to transform import statement to import by `__vite_ssr_import__` +/// and export statement to export by `__vite_ssr_exports__`, these functions will be +/// injected by Vite node. +/// +/// ## Example +/// +/// Input: +/// ```js +/// import { foo } from 'vue'; +/// import vue from 'vue'; +/// import * as vue from 'vue'; +/// +/// foo(); +/// console.log(vue.version); +/// console.log(vue.zoo()); +/// ``` +/// +/// Output: +/// ```js +/// const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['foo'] }); +/// const __vite_ssr_import_1__ = await __vite_ssr_import__('vue', { importedNames: ['default'] }); +/// const __vite_ssr_import_2__ = await __vite_ssr_import__('vue'); +/// (0, __vite_ssr_import_0__.foo)(); +/// console.log(__vite_ssr_import_2__.version); +/// console.log(__vite_ssr_import_2__.zoo()); +/// ``` +/// +/// ## Implementation +/// +/// Based on [Vite](https://github.com/vitejs/vite/blob/00deea4ff88e30e299cb40a801b5dc0205ac913d/packages/vite/src/node/ssr/ssrTransform.ts)'s ssrTransform. +/// +/// All tests are copy-pasted from (ssrTransform.spec.ts)[https://github.com/vitejs/vite/blob/00deea4ff88e30e299cb40a801b5dc0205ac913d/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts] +/// +/// ## Integrate into main `Transformer` in future +/// +/// There are few problems to integrate this transform into the main transformer: +/// +/// 1. In Vite, it will collect import deps and dynamic import deps during the transform process, and return them +/// at the end of function. We can do this, but how to pass them into the js side? +/// +/// 2. In case other plugins will insert imports/exports, we must transform them in `exit_program`, but it will pose +/// another problem: how to transform identifiers which refer to imports? We must collect some information from imports, +/// but it is already at the end of the visitor. To solve this, we may introduce a new visitor to transform identifiers, +/// dynamic imports, and import meta. +use compact_str::ToCompactString; +use rustc_hash::FxHashMap; +use std::iter; + +use oxc_allocator::{Box as ArenaBox, String as ArenaString, Vec as ArenaVec}; +use oxc_ast::{NONE, ast::*}; +use oxc_ecmascript::BoundNames; +use oxc_semantic::{ReferenceFlags, ScopeFlags, SymbolFlags, SymbolId}; +use oxc_span::SPAN; +use oxc_syntax::identifier::is_identifier_name; +use oxc_traverse::{Ancestor, BoundIdentifier, Traverse, TraverseCtx}; + +use crate::utils::ast_builder::{ + create_compute_property_access, create_member_callee, create_property_access, +}; + +#[derive(Debug, Default)] +pub struct ModuleRunnerTransform<'a> { + /// Uid for generating import binding names. + import_uid: u32, + /// Import bindings used to determine which identifiers should be transformed. + /// The key is a symbol id that belongs to the import binding. + /// The value is a tuple of (Binding, Property). + import_bindings: FxHashMap, Option>)>, + + // Collect deps and dynamic deps for Vite + deps: Vec, + dynamic_deps: Vec, +} + +impl ModuleRunnerTransform<'_> { + pub fn new() -> Self { + Self { + import_uid: 0, + import_bindings: FxHashMap::default(), + deps: Vec::default(), + dynamic_deps: Vec::default(), + } + } +} + +const SSR_MODULE_EXPORTS_KEY: Atom<'static> = Atom::new_const("__vite_ssr_exports__"); +const SSR_IMPORT_KEY: Atom<'static> = Atom::new_const("__vite_ssr_import__"); +const SSR_DYNAMIC_IMPORT_KEY: Atom<'static> = Atom::new_const("__vite_ssr_dynamic_import__"); +const SSR_EXPORT_ALL_KEY: Atom<'static> = Atom::new_const("__vite_ssr_exportAll__"); +const SSR_IMPORT_META_KEY: Atom<'static> = Atom::new_const("__vite_ssr_import_meta__"); +const DEFAULT: Atom<'static> = Atom::new_const("default"); + +impl<'a> Traverse<'a> for ModuleRunnerTransform<'a> { + #[inline] + fn enter_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + self.transform_imports_and_exports(program, ctx); + } + + #[inline] + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + match expr { + Expression::Identifier(_) => self.transform_identifier(expr, ctx), + Expression::MetaProperty(_) => Self::transform_meta_property(expr, ctx), + Expression::ImportExpression(_) => self.transform_dynamic_import(expr, ctx), + _ => {} + } + } +} + +impl<'a> ModuleRunnerTransform<'a> { + /// Transform import and export declarations. + fn transform_imports_and_exports( + &mut self, + program: &mut Program<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let should_transform = program.body.iter().any(Self::should_transform_statement); + if !should_transform { + return; + } + + // Reserve enough space for new statements + let mut new_stmts: ArenaVec<'a, Statement<'a>> = + ctx.ast.vec_with_capacity(program.body.len() * 2); + + let mut hoist_index = None; + let mut hoist_imports = Vec::with_capacity(program.body.len()); + + for stmt in program.body.drain(..) { + match stmt { + Statement::ImportDeclaration(import) => { + let ImportDeclaration { span, source, specifiers, .. } = import.unbox(); + let import_statement = self.transform_import(span, source, specifiers, ctx); + // Needs to hoist import statements to the above of the other statements + if hoist_index.is_some() { + hoist_imports.push(import_statement); + } else { + new_stmts.push(import_statement); + } + continue; + } + Statement::ExportAllDeclaration(export) => { + self.transform_export_all_declaration(&mut new_stmts, export, ctx); + continue; + } + Statement::ExportNamedDeclaration(export) => { + self.transform_export_named_declaration(&mut new_stmts, export, ctx); + } + Statement::ExportDefaultDeclaration(export) => { + Self::transform_export_default_declaration(&mut new_stmts, export, ctx); + } + _ => { + new_stmts.push(stmt); + } + } + + // Found that non-import statements, we should start hoisting import statements after this + if hoist_index.is_none() { + hoist_index.replace(new_stmts.len() - 1); + } + } + + if let Some(index) = hoist_index { + if !hoist_imports.is_empty() { + new_stmts.splice(index..index, hoist_imports); + } + } + + program.body = new_stmts; + } + + /// Transform `identifier` to point correctly imported binding. + /// + /// - Import without renaming + /// ```js + /// import { foo } from 'vue'; + /// foo; + /// // to + /// __vite_ssr_import_0__.foo; + /// ``` + /// + /// - Import with renaming + /// ```js + /// import { "arbitrary string" as bar } from 'vue'; + /// bar; + /// // to + /// __vite_ssr_import_0__["arbitrary string"]; + /// ``` + /// + /// - The identifier is a callee of a call expression + /// ```js + /// import { foo } from 'vue'; + /// foo(); + /// // to + /// (0, __vite_ssr_import_0__.foo)(); + /// ``` + fn transform_identifier(&self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + let Expression::Identifier(ident) = expr else { + unreachable!(); + }; + + let Some((binding, property)) = ident + .reference_id + .get() + .and_then(|id| ctx.symbols().get_reference(id).symbol_id()) + .and_then(|id| self.import_bindings.get(&id)) + else { + return; + }; + + let object = binding.create_read_expression(ctx); + *expr = if let Some(property) = property { + // TODO(improvement): It looks like here could always return a computed member expression, + // so that we don't need to check if it's an identifier name. + // __vite_ssr_import_0__.foo + let expr = if is_identifier_name(property) { + create_property_access(ident.span, object, property, ctx) + } else { + // __vite_ssr_import_0__['arbitrary string'] + create_compute_property_access(ident.span, object, property, ctx) + }; + + if matches!(ctx.parent(), Ancestor::CallExpressionCallee(_)) { + // wrap with (0, ...) to avoid method binding `this` + // + let zero = + ctx.ast.expression_numeric_literal(SPAN, 0f64, None, NumberBase::Decimal); + let expressions = ctx.ast.vec_from_array([zero, expr]); + ctx.ast.expression_sequence(ident.span, expressions) + } else { + expr + } + } else { + object + }; + } + + /// Transform `import(source, ...arguments)` to `__vite_ssr_dynamic_import__(source, ...arguments)`. + #[inline] + fn transform_dynamic_import(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + let Expression::ImportExpression(import_expr) = ctx.ast.move_expression(expr) else { + unreachable!(); + }; + + let ImportExpression { span, source, arguments, .. } = import_expr.unbox(); + + if let Expression::StringLiteral(source) = &source { + self.dynamic_deps.push(source.value.to_string()); + } + + let flags = ReferenceFlags::Read; + let callee = ctx.create_unbound_ident_expr(SPAN, SSR_DYNAMIC_IMPORT_KEY, flags); + let arguments = arguments.into_iter().map(Argument::from); + let arguments = ctx.ast.vec_from_iter(iter::once(Argument::from(source)).chain(arguments)); + *expr = ctx.ast.expression_call(span, callee, NONE, arguments, false); + } + + /// Transform `import.meta` to `__vite_ssr_import_meta__`. + #[inline] + fn transform_meta_property(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + let Expression::MetaProperty(meta) = expr else { + unreachable!(); + }; + + *expr = ctx.create_unbound_ident_expr(meta.span, SSR_IMPORT_META_KEY, ReferenceFlags::Read); + } + + /// Transform import declaration (`import { foo } from 'vue'`). + /// + /// - Import specifier + /// ```js + /// import { foo, bar } from 'vue'; + /// // to + /// const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['foo', 'bar'] }); + /// ``` + /// + /// - Import default specifier + /// ```js + /// import vue from 'vue'; + /// // to + /// const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['default'] }); + /// ``` + /// + /// - Import namespace specifier + /// ```js + /// import * as vue from 'vue'; + /// // to + /// const __vite_ssr_import_0__ = await __vite_ssr_import__('vue'); + /// ``` + fn transform_import( + &mut self, + span: Span, + source: StringLiteral<'a>, + specifiers: Option>>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + self.deps.push(source.value.to_string()); + + // ['vue', { importedNames: ['foo'] }]` + let mut arguments = ctx.ast.vec_with_capacity(1 + usize::from(specifiers.is_some())); + arguments.push(Argument::from(Expression::StringLiteral(ctx.ast.alloc(source)))); + let pattern = if let Some(mut specifiers) = specifiers { + // `import * as vue from 'vue';` -> `const __vite_ssr_import_0__ = await __vite_ssr_import__('vue');` + if matches!( + specifiers.last(), + Some(ImportDeclarationSpecifier::ImportNamespaceSpecifier(_)) + ) { + let Some(ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier)) = + specifiers.pop() + else { + unreachable!() + }; + + // Reuse the `vue` binding identifier by renaming it to `__vite_ssr_import_0__` + let mut local = specifier.unbox().local; + local.name = self.generate_import_binding_name(ctx); + let binding = BoundIdentifier::from_binding_ident(&local); + ctx.symbols_mut().set_name(binding.symbol_id, &binding.name); + self.import_bindings.insert(binding.symbol_id, (binding, None)); + + let kind = BindingPatternKind::BindingIdentifier(ctx.alloc(local)); + ctx.ast.binding_pattern(kind, NONE, false) + } else { + let binding = self.generate_import_binding(ctx); + arguments.push(self.transform_import_specifiers(&binding, specifiers, ctx)); + binding.create_binding_pattern(ctx) + } + } else { + let binding = self.generate_import_binding(ctx); + binding.create_binding_pattern(ctx) + }; + + Self::create_import(span, pattern, arguments, ctx) + } + + /// Transform named export declaration (`export function foo() {}`). + /// + /// - Export a declaration + /// ```js + /// export function foo() {} + /// // to + /// function foo() {} + /// Object.defineProperty(__vite_ssr_exports__, 'foo', { enumerable: true, configurable: true, get() { return foo; }}); + /// ``` + /// + /// - Export specifiers + /// ```js + /// export { foo, bar }; + /// // to + /// Object.defineProperty(__vite_ssr_exports__, 'foo', { enumerable: true, configurable: true, get() { return foo; }}); + /// Object.defineProperty(__vite_ssr_exports__, 'bar', { enumerable: true, configurable: true, get() { return bar; }}); + /// ``` + /// + /// - Export specifiers from module + /// ```js + /// export { foo, bar } from 'vue'; + /// // to + /// const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['foo', 'bar'] }); + /// Object.defineProperty(__vite_ssr_exports__, 'foo', { enumerable: true, configurable: true, get() { return __vite_ssr_import_0__.foo; }}); + /// Object.defineProperty(__vite_ssr_exports__, 'bar', { enumerable: true, configurable: true, get() { return __vite_ssr_import_0__.bar; }}); + /// ``` + /// + /// - Export specifiers with renaming + /// ```js + /// export { foo as bar }; + /// // to + /// Object.defineProperty(__vite_ssr_exports__, 'bar', { enumerable: true, configurable: true, get() { return foo; }}); + /// ``` + fn transform_export_named_declaration( + &mut self, + new_stmts: &mut ArenaVec<'a, Statement<'a>>, + export: ArenaBox<'a, ExportNamedDeclaration<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + let ExportNamedDeclaration { span, source, specifiers, declaration, .. } = export.unbox(); + + if let Some(declaration) = declaration { + let export_expression = match &declaration { + // `export const [foo, bar] = [1, 2];` + Declaration::VariableDeclaration(variable) => { + let new_stmts_index = new_stmts.len(); + variable.bound_names(&mut |ident| { + let binding = BoundIdentifier::from_binding_ident(ident); + let ident = binding.create_read_expression(ctx); + new_stmts.push(Self::create_export(span, ident, binding.name, ctx)); + }); + // Should be inserted before the exports + new_stmts.insert(new_stmts_index, Statement::from(declaration)); + return; + } + // `export function foo() {}` + Declaration::FunctionDeclaration(func) => { + let binding = BoundIdentifier::from_binding_ident(func.id.as_ref().unwrap()); + let ident = binding.create_read_expression(ctx); + Self::create_export(span, ident, binding.name, ctx) + } + // `export class Foo {}` + Declaration::ClassDeclaration(class) => { + let binding = BoundIdentifier::from_binding_ident(class.id.as_ref().unwrap()); + let ident = binding.create_read_expression(ctx); + Self::create_export(span, ident, binding.name, ctx) + } + _ => { + unreachable!( + "Unsupported for transforming typescript declaration in named export" + ); + } + }; + new_stmts.extend([Statement::from(declaration), export_expression]); + } else { + // If the source is Some, then we need to import the module first and then export them. + let import_binding = source.map(|source| { + let binding = self.generate_import_binding(ctx); + let pattern = binding.create_binding_pattern(ctx); + let imported_names = ctx.ast.vec_from_iter(specifiers.iter().map(|specifier| { + let local_name = specifier.local.name(); + let local_name_expr = ctx.ast.expression_string_literal(SPAN, local_name, None); + ArrayExpressionElement::from(local_name_expr) + })); + let arguments = ctx.ast.vec_from_array([ + Argument::from(Expression::StringLiteral(ctx.ast.alloc(source))), + Self::create_imported_names_object(imported_names, ctx), + ]); + new_stmts.push(Self::create_import(SPAN, pattern, arguments, ctx)); + binding + }); + + new_stmts.extend(specifiers.into_iter().map(|specifier| { + let ExportSpecifier { span, exported, local, .. } = specifier; + let expr = if let Some(import_binding) = &import_binding { + let object = import_binding.create_read_expression(ctx); + let property = local.name(); + // TODO(improvement): It looks like here could always return a computed member expression, + // so that we don't need to check if it's an identifier name. + if is_identifier_name(&property) { + create_property_access(SPAN, object, &property, ctx) + } else { + create_compute_property_access(SPAN, object, &property, ctx) + } + } else { + let ModuleExportName::IdentifierReference(ident) = local else { + unreachable!() + }; + Expression::Identifier(ctx.ast.alloc(ident)) + }; + Self::create_export(span, expr, exported.name(), ctx) + })); + } + } + + /// Transform export all declaration (`export * from 'vue'`). + /// + /// - Without renamed export: + /// ```js + /// export * from 'vue'; + /// // to + /// const __vite_ssr_import_0__ = await __vite_ssr_import__('vue'); + /// Object.defineProperty(__vite_ssr_exports__, 'default', { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__ } }); + /// ``` + /// + /// - Renamed export: + /// ```js + /// export * as foo from 'vue'; + /// // to + /// const __vite_ssr_import_0__ = await __vite_ssr_import__('vue'); + /// Object.defineProperty(__vite_ssr_exports__, 'foo', { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__ } }); + /// ``` + fn transform_export_all_declaration( + &mut self, + new_stmts: &mut ArenaVec<'a, Statement<'a>>, + export: ArenaBox<'a, ExportAllDeclaration<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + let ExportAllDeclaration { span, source, exported, .. } = export.unbox(); + let binding = self.generate_import_binding(ctx); + let pattern = binding.create_binding_pattern(ctx); + let arguments = + ctx.ast.vec1(Argument::from(Expression::StringLiteral(ctx.ast.alloc(source)))); + let import = Self::create_import(span, pattern, arguments, ctx); + + let ident = binding.create_read_expression(ctx); + + let export = if let Some(exported) = exported { + // `export * as foo from 'vue'` -> + // `Object.defineProperty(__vite_ssr_exports__, 'foo', { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__ } });` + Self::create_export(span, ident, exported.name(), ctx) + } else { + let callee = ctx.ast.expression_identifier(SPAN, SSR_EXPORT_ALL_KEY); + let arguments = ctx.ast.vec1(Argument::from(ident)); + // `export * from 'vue'` -> `__vite_ssr_exportAll__(__vite_ssr_import_0__);` + let call = ctx.ast.expression_call(SPAN, callee, NONE, arguments, false); + ctx.ast.statement_expression(span, call) + }; + new_stmts.extend([import, export]); + } + + /// Transform export default declaration (`export default function foo() {}`). + /// + /// - Named function declaration + /// ```js + /// export default function foo() {} + /// // to + /// function foo() {} + /// Object.defineProperty(__vite_ssr_exports__, 'default', { enumerable: true, configurable: true, get(){ return foo } }); + /// ``` + /// + /// - Named class declaration + /// ```js + /// export default class Foo {} + /// // to + /// class Foo {} + /// Object.defineProperty(__vite_ssr_exports__, 'default', { enumerable: true, configurable: true, get(){ return Foo } }); + /// ``` + /// + /// - Without named declaration and expression + /// ```js + /// export default function () {} + /// export default {} + /// // to + /// Object.defineProperty(__vite_ssr_exports__, 'default', { enumerable: true, configurable: true, get(){ return expr } }); + /// Object.defineProperty(__vite_ssr_exports__, 'default', { enumerable: true, configurable: true, get(){ return {} } }); + /// ``` + fn transform_export_default_declaration( + new_stmts: &mut ArenaVec<'a, Statement<'a>>, + export: ArenaBox<'a, ExportDefaultDeclaration<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + let ExportDefaultDeclaration { span, declaration, .. } = export.unbox(); + let expr = match declaration { + ExportDefaultDeclarationKind::FunctionDeclaration(mut func) => { + if let Some(id) = &func.id { + let ident = BoundIdentifier::from_binding_ident(id).create_read_expression(ctx); + new_stmts.extend([ + Statement::FunctionDeclaration(func), + Self::create_export(span, ident, DEFAULT, ctx), + ]); + } else { + func.r#type = FunctionType::FunctionExpression; + let right = Expression::FunctionExpression(func); + new_stmts.push(Self::create_export_default_assignment(span, right, ctx)); + } + return; + } + ExportDefaultDeclarationKind::ClassDeclaration(mut class) => { + if let Some(id) = &class.id { + let ident = BoundIdentifier::from_binding_ident(id).create_read_expression(ctx); + new_stmts.extend([ + Statement::ClassDeclaration(class), + Self::create_export(span, ident, DEFAULT, ctx), + ]); + } else { + class.r#type = ClassType::ClassExpression; + let right = Expression::ClassExpression(class); + new_stmts.push(Self::create_export_default_assignment(span, right, ctx)); + } + return; + } + ExportDefaultDeclarationKind::TSInterfaceDeclaration(_) => { + // Do nothing for `export default interface Foo {}` + return; + } + expr @ match_expression!(ExportDefaultDeclarationKind) => expr.into_expression(), + }; + + new_stmts.push(Self::create_export(span, expr, DEFAULT, ctx)); + } + + /// Transform import specifiers, and return an imported names object. + fn transform_import_specifiers( + &mut self, + binding: &BoundIdentifier<'a>, + specifiers: ArenaVec<'a, ImportDeclarationSpecifier<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Argument<'a> { + let elements_iter = + ctx.ast.vec_from_iter(specifiers.into_iter().map(|specifier| match specifier { + ImportDeclarationSpecifier::ImportSpecifier(specifier) => { + let ImportSpecifier { span, local, imported, .. } = specifier.unbox(); + self.insert_import_binding(span, binding, local, imported.name(), ctx) + } + ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => { + let ImportDefaultSpecifier { span, local } = specifier.unbox(); + self.insert_import_binding(span, binding, local, DEFAULT, ctx) + } + ImportDeclarationSpecifier::ImportNamespaceSpecifier(_) => { + unreachable!() + } + })); + let elements = ctx.ast.vec_from_iter(elements_iter); + Self::create_imported_names_object(elements, ctx) + } + + /// Insert an import binding into the import bindings map and then return an imported name. + fn insert_import_binding( + &mut self, + span: Span, + binding: &BoundIdentifier<'a>, + ident: BindingIdentifier<'a>, + key: Atom<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> ArrayExpressionElement<'a> { + let BindingIdentifier { name, symbol_id, .. } = ident; + + let scopes = ctx.scopes_mut(); + scopes.remove_binding(scopes.root_scope_id(), &name); + + let symbol_id = symbol_id.get().unwrap(); + // Do not need to insert if there no identifiers that point to this symbol + if !ctx.symbols().get_resolved_reference_ids(symbol_id).is_empty() { + self.import_bindings.insert(symbol_id, (binding.clone(), Some(key))); + } + + ArrayExpressionElement::from(ctx.ast.expression_string_literal(span, key, None)) + } + + #[inline] + fn should_transform_statement(statement: &Statement<'a>) -> bool { + matches!( + statement, + Statement::ImportDeclaration(_) + | Statement::ExportAllDeclaration(_) + | Statement::ExportNamedDeclaration(_) + | Statement::ExportDefaultDeclaration(_) + ) + } + + /// Generate a unique import binding name like `__vite_ssr_import_{uid}__`. + fn generate_import_binding_name(&mut self, ctx: &TraverseCtx<'a>) -> Atom<'a> { + let uid = self.import_uid.to_compact_string(); + self.import_uid += 1; + let capacity = 20 + uid.len(); + let mut string = ArenaString::with_capacity_in(capacity, ctx.ast.allocator); + string.push_str("__vite_ssr_import_"); + string.push_str(&uid); + string.push_str("__"); + debug_assert_eq!(string.len(), capacity); + Atom::from(string) + } + + /// Generate a unique import binding whose name is like `__vite_ssr_import_{uid}__`. + #[inline] + fn generate_import_binding(&mut self, ctx: &mut TraverseCtx<'a>) -> BoundIdentifier<'a> { + let name = self.generate_import_binding_name(ctx); + ctx.generate_binding_in_current_scope(name, SymbolFlags::BlockScopedVariable) + } + + // { importedNames: ['foo', 'bar'] } + fn create_imported_names_object( + elements: ArenaVec<'a, ArrayExpressionElement<'a>>, + ctx: &TraverseCtx<'a>, + ) -> Argument<'a> { + let value = ctx.ast.expression_array(SPAN, elements, None); + let key = ctx.ast.property_key_static_identifier(SPAN, Atom::from("importedNames")); + let imported_names = ctx.ast.object_property_kind_object_property( + SPAN, + PropertyKind::Init, + key, + value, + false, + false, + false, + ); + Argument::from(ctx.ast.expression_object(SPAN, ctx.ast.vec1(imported_names), None)) + } + + // `const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['foo'] });` + fn create_import( + span: Span, + pattern: BindingPattern<'a>, + arguments: ArenaVec<'a, Argument<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let callee = ctx.create_unbound_ident_expr(SPAN, SSR_IMPORT_KEY, ReferenceFlags::Read); + let call = ctx.ast.expression_call(SPAN, callee, NONE, arguments, false); + let init = ctx.ast.expression_await(SPAN, call); + + let kind = VariableDeclarationKind::Const; + let declarator = ctx.ast.variable_declarator(SPAN, kind, pattern, Some(init), false); + let declaration = ctx.ast.declaration_variable(span, kind, ctx.ast.vec1(declarator), false); + Statement::from(declaration) + } + + // `Object.defineProperty(...arguments)` + fn create_define_property( + arguments: ArenaVec<'a, Argument<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let object = + ctx.create_unbound_ident_expr(SPAN, Atom::from("Object"), ReferenceFlags::Read); + let member = create_member_callee(object, "defineProperty", ctx); + ctx.ast.expression_call(SPAN, member, NONE, arguments, false) + } + + // `key: value` or `key() {}` + fn create_object_property( + key: &str, + value: Option>, + ctx: &TraverseCtx<'a>, + ) -> ObjectPropertyKind<'a> { + let is_method = value.is_some(); + ctx.ast.object_property_kind_object_property( + SPAN, + PropertyKind::Init, + ctx.ast.property_key_static_identifier(SPAN, key), + value.unwrap_or_else(|| ctx.ast.expression_boolean_literal(SPAN, true)), + is_method, + false, + false, + ) + } + + /// `{ enumerable: true, configurable: true, get(){ return expr } }` + fn create_function_with_return_statement( + expr: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let kind = FormalParameterKind::FormalParameter; + let params = ctx.ast.formal_parameters(SPAN, kind, ctx.ast.vec(), NONE); + let statement = ctx.ast.statement_return(SPAN, Some(expr)); + let body = ctx.ast.function_body(SPAN, ctx.ast.vec(), ctx.ast.vec1(statement)); + let r#type = FunctionType::FunctionExpression; + let scope_id = ctx.create_child_scope(ctx.scopes().root_scope_id(), ScopeFlags::Function); + let function = ctx.ast.alloc_function_with_scope_id( + SPAN, + r#type, + None, + false, + false, + false, + NONE, + NONE, + params, + NONE, + Some(body), + scope_id, + ); + Expression::FunctionExpression(function) + } + + // `Object.defineProperty(__vite_ssr_exports__, 'foo', {enumerable: true, configurable: true, get(){ return foo }});` + fn create_export( + span: Span, + expr: Expression<'a>, + exported_name: Atom<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let getter = Self::create_function_with_return_statement(expr, ctx); + let object = ctx.ast.expression_object( + SPAN, + ctx.ast.vec_from_array([ + Self::create_object_property("enumerable", None, ctx), + Self::create_object_property("configurable", None, ctx), + Self::create_object_property("get", Some(getter), ctx), + ]), + None, + ); + + let arguments = ctx.ast.vec_from_array([ + Argument::from(ctx.create_unbound_ident_expr( + SPAN, + SSR_MODULE_EXPORTS_KEY, + ReferenceFlags::Read, + )), + Argument::from(ctx.ast.expression_string_literal(SPAN, exported_name, None)), + Argument::from(object), + ]); + + ctx.ast.statement_expression(span, Self::create_define_property(arguments, ctx)) + } + + // __vite_ssr_exports__.default = right; + fn create_export_default_assignment( + span: Span, + right: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let object = + ctx.create_unbound_ident_expr(SPAN, SSR_MODULE_EXPORTS_KEY, ReferenceFlags::Read); + let property = ctx.ast.identifier_name(SPAN, DEFAULT); + let member = ctx.ast.member_expression_static(SPAN, object, property, false); + let target = AssignmentTarget::from(member); + let operator = AssignmentOperator::Assign; + let assignment = ctx.ast.expression_assignment(SPAN, operator, target, right); + ctx.ast.statement_expression(span, assignment) + } +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use oxc_ast::AstBuilder; + use similar::TextDiff; + + use oxc_allocator::Allocator; + use oxc_codegen::{CodeGenerator, CodegenOptions}; + use oxc_diagnostics::OxcDiagnostic; + use oxc_parser::Parser; + use oxc_semantic::SemanticBuilder; + use oxc_span::SourceType; + use oxc_tasks_common::print_diff_in_terminal; + use oxc_traverse::traverse_mut; + + use crate::{JsxOptions, JsxRuntime, TransformOptions, context::TransformCtx, jsx::Jsx}; + + use super::ModuleRunnerTransform; + + fn transform(source_text: &str, is_jsx: bool) -> Result> { + let source_type = SourceType::default().with_jsx(is_jsx); + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, source_text, source_type).parse(); + let mut program = ret.program; + let (mut symbols, mut scopes) = + SemanticBuilder::new().build(&program).semantic.into_symbol_table_and_scope_tree(); + if is_jsx { + let mut jsx_options = JsxOptions::enable(); + jsx_options.runtime = JsxRuntime::Classic; + jsx_options.jsx_plugin = true; + let ctx = TransformCtx::new(Path::new(""), &TransformOptions::default()); + let mut jsx = Jsx::new(jsx_options, None, AstBuilder::new(&allocator), &ctx); + + (symbols, scopes) = traverse_mut(&mut jsx, &allocator, &mut program, symbols, scopes); + } + let mut module_runner_transform = ModuleRunnerTransform::default(); + traverse_mut(&mut module_runner_transform, &allocator, &mut program, symbols, scopes); + + if !ret.errors.is_empty() { + return Err(ret.errors); + } + let code = CodeGenerator::new() + .with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() }) + .build(&program) + .code; + Ok(code) + } + + fn format_expected_code(source_text: &str) -> String { + let source_type = SourceType::default(); + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, source_text, source_type).parse(); + + CodeGenerator::new() + .with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() }) + .build(&ret.program) + .code + } + + fn test_same(source_text: &str, expected: &str) { + let expected = format_expected_code(expected); + let result = transform(source_text, false).unwrap(); + if result != expected { + let diff = TextDiff::from_lines(&expected, &result); + print_diff_in_terminal(&diff); + panic!("Expected code does not match the result"); + } + } + + fn test_same_jsx(source_text: &str, expected: &str) { + let expected = format_expected_code(expected); + let result = transform(source_text, true).unwrap(); + if result != expected { + let diff = TextDiff::from_lines(&expected, &result); + print_diff_in_terminal(&diff); + panic!("Expected code does not match the result"); + } + } + + #[test] + fn default_import() { + test_same( + "import foo from 'vue';console.log(foo.bar)", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['default'] }); +console.log(__vite_ssr_import_0__.default.bar);", + ); + } + + #[test] + fn named_import() { + test_same( + "import { ref } from 'vue';function foo() { return ref(0) }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['ref'] }); +function foo() { + return (0, __vite_ssr_import_0__.ref)(0); +}", + ); + } + + #[test] + fn named_import_arbitrary_module_namespace() { + test_same( + "import { \"some thing\" as ref } from 'vue';function foo() { return ref(0) }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['some thing'] }); +function foo() { + return (0, __vite_ssr_import_0__['some thing'])(0); +}", + ); + } + + #[test] + fn namespace_import() { + test_same( + "import * as vue from 'vue';function foo() { return vue.ref(0) }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue'); +function foo() { + return __vite_ssr_import_0__.ref(0); +}", + ); + } + + #[test] + fn export_function_declaration() { + test_same( + "export function foo() {}", + "function foo() {} +Object.defineProperty(__vite_ssr_exports__, 'foo', { + enumerable: true, + configurable: true, + get() { + return foo; + } +});", + ); + } + + #[test] + fn export_class_declaration() { + test_same( + "export class foo {}", + "class foo {} +Object.defineProperty(__vite_ssr_exports__, 'foo', { + enumerable: true, + configurable: true, + get() { + return foo; + } +});", + ); + } + + #[test] + fn export_var_declaration() { + test_same( + "export const a = 1, b = 2", + "const a = 1, b = 2; +Object.defineProperty(__vite_ssr_exports__, 'a', { + enumerable: true, + configurable: true, + get() { + return a; + } +}); +Object.defineProperty(__vite_ssr_exports__, 'b', { + enumerable: true, + configurable: true, + get() { + return b; + } +});", + ); + } + + #[test] + fn export_named() { + test_same( + "const a = 1, b = 2; export { a, b as c }", + "const a = 1, b = 2; +Object.defineProperty(__vite_ssr_exports__, 'a', { + enumerable: true, + configurable: true, + get() { + return a; + } +}); +Object.defineProperty(__vite_ssr_exports__, 'c', { + enumerable: true, + configurable: true, + get() { + return b; + } +});", + ); + } + + #[test] + fn export_named_from() { + test_same( + "export { ref, computed as c } from 'vue'", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['ref', 'computed'] }); +Object.defineProperty(__vite_ssr_exports__, 'ref', { + enumerable: true, + configurable: true, + get() { + return __vite_ssr_import_0__.ref; + } +}); +Object.defineProperty(__vite_ssr_exports__, 'c', { + enumerable: true, + configurable: true, + get() { + return __vite_ssr_import_0__.computed; + } +});", + ); + } + + #[test] + fn export_named_imported_binding() { + test_same( + " + import { createApp } from 'vue'; + export { createApp } + ", + " + const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['createApp'] }); + Object.defineProperty(__vite_ssr_exports__, 'createApp', { + enumerable: true, + configurable: true, + get() { + return __vite_ssr_import_0__.createApp; + } + }); + ", + ); + } + + #[test] + fn export_star_from() { + test_same( + "export * from 'vue'\nexport * from 'react'", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue'); +__vite_ssr_exportAll__(__vite_ssr_import_0__); +const __vite_ssr_import_1__ = await __vite_ssr_import__('react'); +__vite_ssr_exportAll__(__vite_ssr_import_1__);", + ); + } + + #[test] + fn export_star_as_from() { + test_same( + "export * as foo from 'vue'", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue'); +Object.defineProperty(__vite_ssr_exports__, 'foo', { + enumerable: true, + configurable: true, + get() { + return __vite_ssr_import_0__; + } +});", + ); + } + + #[test] + fn re_export_by_imported_name() { + test_same( + "import * as foo from 'foo'; export * as foo from 'foo'", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('foo'); +const __vite_ssr_import_1__ = await __vite_ssr_import__('foo'); +Object.defineProperty(__vite_ssr_exports__, 'foo', { + enumerable: true, + configurable: true, + get() { + return __vite_ssr_import_1__; + } +});", + ); + + test_same( + "import { foo } from 'foo'; export { foo } from 'foo'", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('foo', { importedNames: ['foo'] }); +const __vite_ssr_import_1__ = await __vite_ssr_import__('foo', { importedNames: ['foo'] }); +Object.defineProperty(__vite_ssr_exports__, 'foo', { + enumerable: true, + configurable: true, + get() { + return __vite_ssr_import_1__.foo; + } +});", + ); + } + + #[test] + fn export_arbitrary_namespace() { + test_same( + "export * as \"arbitrary string\" from 'vue'", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue'); +Object.defineProperty(__vite_ssr_exports__, 'arbitrary string', { + enumerable: true, + configurable: true, + get() { + return __vite_ssr_import_0__; + } +});", + ); + + test_same( + "const something = \"Something\"; export { something as \"arbitrary string\" }", + "const something = 'Something'; +Object.defineProperty(__vite_ssr_exports__, 'arbitrary string', { + enumerable: true, + configurable: true, + get() { + return something; + } +});", + ); + + test_same( + "export { \"arbitrary string2\" as \"arbitrary string\" } from 'vue'", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['arbitrary string2'] }); +Object.defineProperty(__vite_ssr_exports__, 'arbitrary string', { + enumerable: true, + configurable: true, + get() { + return __vite_ssr_import_0__['arbitrary string2']; + } +});", + ); + } + + #[test] + fn export_default() { + test_same( + "export default {}", + "Object.defineProperty(__vite_ssr_exports__, 'default', { + enumerable: true, + configurable: true, + get() { + return {}; + } +});", + ); + } + + #[test] + fn export_then_import_minified() { + test_same( + "export * from 'vue';import {createApp} from 'vue'", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue'); +__vite_ssr_exportAll__(__vite_ssr_import_0__); +const __vite_ssr_import_1__ = await __vite_ssr_import__('vue', { importedNames: ['createApp'] });", + ); + } + + #[test] + fn hoist_import_to_top() { + test_same( + "path.resolve('server.js');import path from 'node:path';", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('node:path', { importedNames: ['default'] }); +__vite_ssr_import_0__.default.resolve('server.js');", + ); + } + + #[test] + fn import_meta() { + test_same("console.log(import.meta.url)", "console.log(__vite_ssr_import_meta__.url);"); + } + + #[test] + fn dynamic_import() { + test_same( + "export const i = () => import('./foo')", + "const i = () => __vite_ssr_dynamic_import__('./foo'); +Object.defineProperty(__vite_ssr_exports__, 'i', { + enumerable: true, + configurable: true, + get() { + return i; + } +});", + ); + } + + #[test] + fn do_not_rewrite_method_definition() { + test_same( + "import { fn } from 'vue';class A { fn() { fn() } }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['fn'] }); +class A { + fn() { + (0, __vite_ssr_import_0__.fn)(); + } +}", + ); + } + + #[test] + fn do_not_rewrite_when_variable_in_scope() { + test_same( + "import { fn } from 'vue';function A(){ const fn = () => {}; return { fn }; }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['fn'] }); +function A() { + const fn = () => {}; + return { fn }; +}", + ); + } + + #[test] + fn do_not_rewrite_destructuring_object() { + test_same( + "import { fn } from 'vue';function A(){ let {fn, test} = {fn: 'foo', test: 'bar'}; return { fn }; }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['fn'] }); +function A() { + let { fn, test } = { + fn: 'foo', + test: 'bar' + }; + return { fn }; +}", + ); + } + + #[test] + fn do_not_rewrite_destructuring_array() { + test_same( + "import { fn } from 'vue';function A(){ let [fn, test] = ['foo', 'bar']; return { fn }; }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['fn'] }); +function A() { + let [fn, test] = ['foo', 'bar']; + return { fn }; +}", + ); + } + + #[test] + fn do_not_rewrite_catch_clause() { + test_same( + "import {error} from './dependency';try {} catch(error) {}", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('./dependency', { importedNames: ['error'] }); +try {} catch (error) {}", + ); + } + + #[test] + fn should_declare_imported_super_class() { + test_same( + "import { Foo } from './dependency'; class A extends Foo {}", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('./dependency', { importedNames: ['Foo'] }); +class A extends __vite_ssr_import_0__.Foo {}", + ); + } + + #[test] + fn should_declare_exported_super_class() { + test_same( + " + import { Foo } from './dependency'; + export default class A extends Foo {} + export class B extends Foo {} + ", + " + const __vite_ssr_import_0__ = await __vite_ssr_import__('./dependency', { importedNames: ['Foo'] }); + class A extends __vite_ssr_import_0__.Foo {} + Object.defineProperty(__vite_ssr_exports__, 'default', { + enumerable: true, + configurable: true, + get() { + return A; + } + }); + class B extends __vite_ssr_import_0__.Foo {} + Object.defineProperty(__vite_ssr_exports__, 'B', { + enumerable: true, + configurable: true, + get() { + return B; + } + }); + ", + ); + } + + /// + #[test] + fn should_handle_default_export_variants() { + // default anonymous functions + test_same("export default function() {}", "__vite_ssr_exports__.default = function() {};"); + + // default anonymous class + test_same("export default class {}", "__vite_ssr_exports__.default = class {};"); + + // default named functions + test_same( + "export default function foo() {}\nfoo.prototype = Object.prototype;", + "function foo() {} +Object.defineProperty(__vite_ssr_exports__, 'default', { + enumerable: true, + configurable: true, + get() { + return foo; + } +}); +foo.prototype = Object.prototype;", + ); + + // default named classes + test_same( + "export default class A {}\nexport class B extends A {}", + "class A {} +Object.defineProperty(__vite_ssr_exports__, 'default', { + enumerable: true, + configurable: true, + get() { + return A; + } +}); +class B extends A {} +Object.defineProperty(__vite_ssr_exports__, 'B', { + enumerable: true, + configurable: true, + get() { + return B; + } +});", + ); + } + + #[test] + fn overwrite_bindings() { + test_same( + "import { inject } from 'vue'; + const a = { inject } + const b = { test: inject } + function c() { const { test: inject } = { test: true }; console.log(inject) } + const d = inject + function f() { console.log(inject) } + function e() { const { inject } = { inject: true } } + function g() { const f = () => { const inject = true }; console.log(inject) }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['inject'] }); +const a = { inject: __vite_ssr_import_0__.inject }; +const b = { test: __vite_ssr_import_0__.inject }; +function c() { + const { test: inject } = { test: true }; + console.log(inject); +} +const d = __vite_ssr_import_0__.inject; +function f() { + console.log(__vite_ssr_import_0__.inject); +} +function e() { + const { inject } = { inject: true }; +} +function g() { + const f = () => { + const inject = true; + }; + console.log(__vite_ssr_import_0__.inject); +}", + ); + } + + #[test] + fn empty_array_pattern() { + test_same("const [, LHS, RHS] = inMatch;", "const [, LHS, RHS] = inMatch;"); + } + + #[test] + fn function_argument_destructure() { + test_same( + " + import { foo, bar } from 'foo' + const a = ({ _ = foo() }) => {} + function b({ _ = bar() }) {} + function c({ _ = bar() + foo() }) {}", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('foo', { importedNames: ['foo', 'bar'] }); +const a = ({ _ = (0, __vite_ssr_import_0__.foo)() }) => {}; +function b({ _ = (0, __vite_ssr_import_0__.bar)() }) {} +function c({ _ = (0, __vite_ssr_import_0__.bar)() + (0, __vite_ssr_import_0__.foo)() }) {}", + ); + } + + #[test] + fn object_destructure_alias() { + test_same( + " + import { n } from 'foo' + const a = () => { + const { type: n = 'bar' } = {} + console.log(n) + }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('foo', { importedNames: ['n'] }); +const a = () => { + const { type: n = 'bar' } = {}; + console.log(n); +};", + ); + + // https://github.com/vitejs/vite/issues/9585 + test_same( + " + import { n, m } from 'foo' + const foo = {} + { + const { [n]: m } = foo + }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('foo', { importedNames: ['n', 'm'] }); +const foo = {}; +{ + const { [__vite_ssr_import_0__.n]: m } = foo; +}", + ); + } + + #[test] + fn nested_object_destructure_alias() { + test_same( + "import { remove, add, get, set, rest, objRest } from 'vue' + + function a() { + const { + o: { remove }, + a: { b: { c: [ add ] }}, + d: [{ get }, set, ...rest], + ...objRest + } = foo + + remove() + add() + get() + set() + rest() + objRest() + } + + remove() + add() + get() + set() + rest() + objRest()", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['remove', 'add', 'get', 'set', 'rest', 'objRest'] }); + +function a() { + const { + o: { remove }, + a: { b: { c: [ add ] }}, + d: [{ get }, set, ...rest], + ...objRest + } = foo; + + remove(); + add(); + get(); + set(); + rest(); + objRest(); +} + +(0, __vite_ssr_import_0__.remove)(); +(0, __vite_ssr_import_0__.add)(); +(0, __vite_ssr_import_0__.get)(); +(0, __vite_ssr_import_0__.set)(); +(0, __vite_ssr_import_0__.rest)(); +(0, __vite_ssr_import_0__.objRest)(); +", + ); + } + + #[test] + fn object_props_and_methods() { + test_same( + "import foo from 'foo' + + const bar = 'bar' + + const obj = { + foo() {}, + [foo]() {}, + [bar]() {}, + foo: () => {}, + [foo]: () => {}, + [bar]: () => {}, + bar(foo) {} + }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('foo', { importedNames: ['default'] }); + +const bar = 'bar'; + +const obj = { + foo() {}, + [__vite_ssr_import_0__.default]() {}, + [bar]() {}, + foo: () => {}, + [__vite_ssr_import_0__.default]: () => {}, + [bar]: () => {}, + bar(foo) {} +};", + ); + } + + #[test] + fn class_props() { + test_same( + "import { remove, add } from 'vue' + class A { + remove = 1 + add = null + }", + " + const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['remove', 'add'] }); + class A { + remove = 1; + add = null; + } + ", + ); + } + + #[test] + fn class_methods() { + test_same( + "import foo from 'foo' + + const bar = 'bar' + + class A { + foo() {} + [foo]() {} + [bar]() {} + #foo() {} + bar(foo) {} + }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('foo', { importedNames: ['default'] }); + +const bar = 'bar'; + +class A { + foo() {} + [__vite_ssr_import_0__.default]() {} + [bar]() {} + #foo() {} + bar(foo) {} +}", + ); + } + + #[test] + fn declare_scope() { + test_same( + "import { aaa, bbb, ccc, ddd } from 'vue' + + function foobar() { + ddd() + + const aaa = () => { + bbb(ccc) + ddd() + } + const bbb = () => { + console.log('hi') + } + const ccc = 1 + function ddd() {} + + aaa() + bbb() + ccc() + } + + aaa() + bbb()", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('vue', { importedNames: ['aaa', 'bbb', 'ccc', 'ddd'] }); + +function foobar() { + ddd(); + + const aaa = () => { + bbb(ccc); + ddd(); + }; + const bbb = () => { + console.log('hi'); + }; + const ccc = 1; + function ddd() {} + + aaa(); + bbb(); + ccc(); +} + +(0, __vite_ssr_import_0__.aaa)(); +(0, __vite_ssr_import_0__.bbb)(); +", + ); + } + + #[test] + fn jsx() { + test_same_jsx( + "import React from 'react' + import { Foo, Slot } from 'foo' + + function Bar({ Slot = }) { + return ( + <> + + + ) + }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('react', { importedNames: ['default'] }); +const __vite_ssr_import_1__ = await __vite_ssr_import__('foo', { importedNames: ['Foo', 'Slot'] }); +function Bar({ Slot = __vite_ssr_import_0__.default.createElement(__vite_ssr_import_1__.Foo, null) }) { + return __vite_ssr_import_0__.default.createElement(__vite_ssr_import_0__.default.Fragment, null, __vite_ssr_import_0__.default.createElement(Slot, null)); +}", + ); + } + + #[test] + fn continuous_exports() { + test_same( + "export function fn1() { + }export function fn2() { + }", + "function fn1() { +} +Object.defineProperty(__vite_ssr_exports__, 'fn1', { + enumerable: true, + configurable: true, + get() { + return fn1; + } +}); +function fn2() { +} +Object.defineProperty(__vite_ssr_exports__, 'fn2', { + enumerable: true, + configurable: true, + get() { + return fn2; + } +});", + ); + } + + // https://github.com/vitest-dev/vitest/issues/1141 + #[test] + fn export_default_expression() { + // esbuild transform result of following TS code + // export default function getRandom() { + // return Math.random() + // } + test_same( + "export default (function getRandom() { + return Math.random(); + });", + "Object.defineProperty(__vite_ssr_exports__, 'default', { + enumerable: true, + configurable: true, + get() { + return function getRandom() { + return Math.random(); + }; + } +});", + ); + + test_same( + "export default (class A {});", + "Object.defineProperty(__vite_ssr_exports__, 'default', { + enumerable: true, + configurable: true, + get() { + return class A {}; + } +});", + ); + } + + // https://github.com/vitejs/vite/issues/8002 + #[test] + fn with_hashbang() { + test_same( + "#!/usr/bin/env node + console.log(\"it can parse the hashbang\")", + "#!/usr/bin/env node +console.log(\"it can parse the hashbang\");", + ); + } + + #[test] + fn import_hoisted_after_hashbang() { + test_same( + "#!/usr/bin/env node + console.log(foo); + import foo from 'foo'", + "#!/usr/bin/env node + const __vite_ssr_import_0__ = await __vite_ssr_import__('foo', { importedNames: ['default'] }); + console.log(__vite_ssr_import_0__.default); + ", + ); + } + + #[test] + fn identity_function_helper_injected_after_hashbang() { + test_same( + "#!/usr/bin/env node + import { foo } from 'foo'; + foo();", + "#!/usr/bin/env node + const __vite_ssr_import_0__ = await __vite_ssr_import__('foo', { importedNames: ['foo'] }); + (0, __vite_ssr_import_0__.foo)(); + ", + ); + } + + // https://github.com/vitejs/vite/issues/10289 + #[test] + fn track_scope_by_class_function_condition_blocks() { + test_same( + "import { foo, bar } from 'foobar' + if (false) { + const foo = 'foo' + console.log(foo) + } else if (false) { + const [bar] = ['bar'] + console.log(bar) + } else { + console.log(foo) + console.log(bar) + } + export class Test { + constructor() { + if (false) { + const foo = 'foo' + console.log(foo) + } else if (false) { + const [bar] = ['bar'] + console.log(bar) + } else { + console.log(foo) + console.log(bar) + } + } + }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('foobar', { importedNames: ['foo', 'bar'] }); +if (false) { + const foo = 'foo'; + console.log(foo); +} else if (false) { + const [bar] = ['bar']; + console.log(bar); +} else { + console.log(__vite_ssr_import_0__.foo); + console.log(__vite_ssr_import_0__.bar); +} +class Test { + constructor() { + if (false) { + const foo = 'foo'; + console.log(foo); + } else if (false) { + const [bar] = ['bar']; + console.log(bar); + } else { + console.log(__vite_ssr_import_0__.foo); + console.log(__vite_ssr_import_0__.bar); + } + } +} +Object.defineProperty(__vite_ssr_exports__, 'Test', { + enumerable: true, + configurable: true, + get() { + return Test; + } +});", + ); + } + + // https://github.com/vitejs/vite/issues/10386 + #[test] + fn track_var_scope_by_function() { + test_same( + "import { foo, bar } from 'foobar' + function test() { + if (true) { + var foo = () => { var why = 'would' }, bar = 'someone' + } + return [foo, bar] + }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('foobar', { importedNames: ['foo', 'bar'] }); +function test() { + if (true) { + var foo = () => { var why = 'would'; }, bar = 'someone'; + } + return [foo, bar]; +}", + ); + } + + // https://github.com/vitejs/vite/issues/11806 + #[test] + fn track_scope_by_blocks() { + test_same( + "import { foo, bar, baz } from 'foobar' + function test() { + [foo]; + { + let foo = 10; + let bar = 10; + } + try {} catch (baz){ baz } + return bar; + }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('foobar', { importedNames: ['foo', 'bar', 'baz'] }); +function test() { + [__vite_ssr_import_0__.foo]; + { + let foo = 10; + let bar = 10; + } + try {} catch (baz) { baz; } + return __vite_ssr_import_0__.bar; +}", + ); + } + + #[test] + fn track_scope_in_for_loops() { + test_same( + "import { test } from './test.js' + + for (const test of tests) { + console.log(test) + } + + for (let test = 0; test < 10; test++) { + console.log(test) + } + + for (const test in tests) { + console.log(test) + }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('./test.js', { importedNames: ['test'] }); +for (const test of tests) { + console.log(test); +} +for (let test = 0; test < 10; test++) { + console.log(test); +} +for (const test in tests) { + console.log(test); +}", + ); + } + + #[test] + fn avoid_binding_class_expression() { + test_same( + "import Foo, { Bar } from './foo'; + + console.log(Foo, Bar); + const obj = { + foo: class Foo {}, + bar: class Bar {} + } + const Baz = class extends Foo {}", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('./foo', { importedNames: ['default', 'Bar'] }); +console.log(__vite_ssr_import_0__.default, __vite_ssr_import_0__.Bar); +const obj = { + foo: class Foo {}, + bar: class Bar {} +}; +const Baz = class extends __vite_ssr_import_0__.default {};", + ); + } + + #[test] + fn import_assertion_attribute() { + test_same( + "import * as foo from './foo.json' with { type: 'json' }; + import('./bar.json', { with: { type: 'json' } });", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('./foo.json'); +__vite_ssr_dynamic_import__('./bar.json', { with: { type: 'json' } });", + ); + } + + #[test] + fn import_and_export_ordering() { + // Given all imported modules logs `mod ${mod}` on execution, + // and `foo` is `bar`, the logging order should be: + // "mod a", "mod foo", "mod b", "bar1", "bar2" + test_same( + "console.log(foo + 1) + export * from './a' + import { foo } from './foo' + export * from './b' + console.log(foo + 2)", + "const __vite_ssr_import_1__ = await __vite_ssr_import__('./foo', { importedNames: ['foo']}); +console.log(__vite_ssr_import_1__.foo + 1); +const __vite_ssr_import_0__ = await __vite_ssr_import__('./a');__vite_ssr_exportAll__(__vite_ssr_import_0__); +const __vite_ssr_import_2__ = await __vite_ssr_import__('./b');__vite_ssr_exportAll__(__vite_ssr_import_2__); +console.log(__vite_ssr_import_1__.foo + 2);", + ); + } + + #[test] + fn identity_function_is_declared_before_used() { + test_same( + "import { foo } from './foo' + export default foo() + export * as bar from './bar' + console.log(bar)", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('./foo', { importedNames: ['foo'] }); +Object.defineProperty(__vite_ssr_exports__, 'default', { + enumerable: true, + configurable: true, + get() { + return (0, __vite_ssr_import_0__.foo)(); + } +}); +const __vite_ssr_import_1__ = await __vite_ssr_import__('./bar'); +Object.defineProperty(__vite_ssr_exports__, 'bar', { + enumerable: true, + configurable: true, + get() { + return __vite_ssr_import_1__; + } +}); +console.log(bar);", + ); + } + + #[test] + fn inject_semicolon_call_wrapper() { + let code = "import { f } from './f' + + let x = 0; + + x + f() + + if (1) + x + f() + + if (1) + x + else + x + f() + + + let y = x + f() + + x /*;;*/ /*;;*/ + f() + + function z() { + x + f() + + if (1) { + x + f() + } + } + + let a = {} + f() + + let b = () => {} + f() + + function c() { + } + f() + + class D { + } + f() + + { + x + } + f() + + switch (1) { + case 1: + x + f() + break + } + + if(0){}f() + + if(0){}else{}f() + + switch(1){}f() + + {}f(1)"; + + let expected = " + + const __vite_ssr_import_0__ = await __vite_ssr_import__('./f', {importedNames:['f']}); + + let x = 0; + + x; + (0,__vite_ssr_import_0__.f)(); + + if (1) + x; + (0,__vite_ssr_import_0__.f)(); + + if (1) + x + else + x; + (0,__vite_ssr_import_0__.f)(); + + + let y = x; + (0,__vite_ssr_import_0__.f)(); + + x; /*;;*/ /*;;*/ + (0,__vite_ssr_import_0__.f)(); + + function z() { + x; + (0,__vite_ssr_import_0__.f)(); + + if (1) { + x; + (0,__vite_ssr_import_0__.f)() + } + } + + let a = {}; + (0,__vite_ssr_import_0__.f)(); + + let b = () => {}; + (0,__vite_ssr_import_0__.f)(); + + function c() { + } + (0,__vite_ssr_import_0__.f)(); + + class D { + } + (0,__vite_ssr_import_0__.f)(); + + { + x + } + (0,__vite_ssr_import_0__.f)(); + + switch (1) { + case 1: + x; + (0,__vite_ssr_import_0__.f)(); + break + } + + if(0){}(0,__vite_ssr_import_0__.f)(); + + if(0){}else{}(0,__vite_ssr_import_0__.f)(); + + switch(1){}(0,__vite_ssr_import_0__.f)(); + + {}(0,__vite_ssr_import_0__.f)(1) + "; + test_same(code, expected); + } + + #[test] + fn does_not_break_minified_code() { + // Based on https://unpkg.com/@headlessui/vue@1.7.23/dist/components/transitions/transition.js + test_same( + "import O from 'a'; + const c = () => { + if(true){return}O(1,{}) + }", + "const __vite_ssr_import_0__ = await __vite_ssr_import__('a', { importedNames: ['default'] }); +const c = () => { + if (true) { + return; + } + (0, __vite_ssr_import_0__.default)(1, {}); +};", + ); + } +} diff --git a/crates/oxc_transformer/src/utils/ast_builder.rs b/crates/oxc_transformer/src/utils/ast_builder.rs index 05773bc097..4945f169b0 100644 --- a/crates/oxc_transformer/src/utils/ast_builder.rs +++ b/crates/oxc_transformer/src/utils/ast_builder.rs @@ -84,10 +84,22 @@ pub fn create_prototype_member<'a>( /// `object` -> `object.a`. pub fn create_property_access<'a>( + span: Span, object: Expression<'a>, property: &str, ctx: &TraverseCtx<'a>, ) -> Expression<'a> { let property = ctx.ast.identifier_name(SPAN, ctx.ast.atom(property)); - Expression::from(ctx.ast.member_expression_static(SPAN, object, property, false)) + Expression::from(ctx.ast.member_expression_static(span, object, property, false)) +} + +/// `object` -> `object['a']`. +pub fn create_compute_property_access<'a>( + span: Span, + object: Expression<'a>, + property: &str, + ctx: &TraverseCtx<'a>, +) -> Expression<'a> { + let expression = ctx.ast.expression_string_literal(SPAN, ctx.ast.atom(property), None); + Expression::from(ctx.ast.member_expression_computed(span, object, expression, false)) }