diff --git a/crates/oxc_transformer/src/plugins/module_runner_transform.rs b/crates/oxc_transformer/src/plugins/module_runner_transform.rs index 5bc86951f6338..5f832ad0a744e 100644 --- a/crates/oxc_transformer/src/plugins/module_runner_transform.rs +++ b/crates/oxc_transformer/src/plugins/module_runner_transform.rs @@ -48,13 +48,13 @@ 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_allocator::{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_semantic::{ReferenceFlags, ScopeFlags, ScopeTree, SymbolFlags, SymbolId, SymbolTable}; use oxc_span::SPAN; use oxc_syntax::identifier::is_identifier_name; -use oxc_traverse::{Ancestor, BoundIdentifier, Traverse, TraverseCtx}; +use oxc_traverse::{Ancestor, BoundIdentifier, Traverse, TraverseCtx, traverse_mut}; use crate::utils::ast_builder::{ create_compute_property_access, create_member_callee, create_property_access, @@ -74,7 +74,7 @@ pub struct ModuleRunnerTransform<'a> { dynamic_deps: Vec, } -impl ModuleRunnerTransform<'_> { +impl<'a> ModuleRunnerTransform<'a> { pub fn new() -> Self { Self { import_uid: 0, @@ -83,6 +83,19 @@ impl ModuleRunnerTransform<'_> { dynamic_deps: Vec::default(), } } + + /// Standalone transform + pub fn transform( + mut self, + allocator: &'a Allocator, + program: &mut Program<'a>, + symbols: SymbolTable, + scopes: ScopeTree, + ) -> (Vec, Vec) { + traverse_mut(&mut self, allocator, program, symbols, scopes); + + (self.deps, self.dynamic_deps) + } } const SSR_MODULE_EXPORTS_KEY: Atom<'static> = Atom::new_const("__vite_ssr_exports__"); diff --git a/napi/transform/index.d.ts b/napi/transform/index.d.ts index efa72c3048b57..a3585c253b8cc 100644 --- a/napi/transform/index.d.ts +++ b/napi/transform/index.d.ts @@ -199,6 +199,59 @@ export interface JsxOptions { refresh?: boolean | ReactRefreshOptions } +/** + * Transform JavaScript code to a Vite Node runnable module. + * + * @param filename The name of the file being transformed. + * @param sourceText the source code itself + * @param options The options for the transformation. See {@link + * ModuleRunnerTransformOptions} for more information. + * + * @returns an object containing the transformed code, source maps, and any + * errors that occurred during parsing or transformation. + * + * @deprecated Only works for Vite. + */ +export declare function moduleRunnerTransform(filename: string, sourceText: string, options?: ModuleRunnerTransformOptions | undefined | null): ModuleRunnerTransformResult + +export interface ModuleRunnerTransformOptions { + /** + * Enable source map generation. + * + * When `true`, the `sourceMap` field of transform result objects will be populated. + * + * @default false + * + * @see {@link SourceMap} + */ + sourcemap?: boolean +} + +export interface ModuleRunnerTransformResult { + /** + * The transformed code. + * + * If parsing failed, this will be an empty string. + */ + code: string + /** + * The source map for the transformed code. + * + * This will be set if {@link TransformOptions#sourcemap} is `true`. + */ + map?: SourceMap + deps: Array + dynamicDeps: Array + /** + * Parse and transformation errors. + * + * Oxc's parser recovers from common syntax errors, meaning that + * transformed code may still be available even if there are errors in this + * list. + */ + errors: Array +} + export interface OxcError { severity: Severity message: string diff --git a/napi/transform/index.js b/napi/transform/index.js index 0ddb288679651..2d0e56cfcded7 100644 --- a/napi/transform/index.js +++ b/napi/transform/index.js @@ -372,5 +372,6 @@ if (!nativeBinding) { module.exports.HelperMode = nativeBinding.HelperMode module.exports.isolatedDeclaration = nativeBinding.isolatedDeclaration +module.exports.moduleRunnerTransform = nativeBinding.moduleRunnerTransform module.exports.Severity = nativeBinding.Severity module.exports.transform = nativeBinding.transform diff --git a/napi/transform/src/transformer.rs b/napi/transform/src/transformer.rs index 816fc8e3ea822..78b9f76ccb4e5 100644 --- a/napi/transform/src/transformer.rs +++ b/napi/transform/src/transformer.rs @@ -12,12 +12,16 @@ use rustc_hash::FxHashMap; use oxc::{ CompilerInterface, - codegen::CodegenReturn, + allocator::Allocator, + codegen::{CodeGenerator, CodegenOptions, CodegenReturn}, diagnostics::OxcDiagnostic, + parser::Parser, + semantic::{SemanticBuilder, SemanticBuilderReturn}, span::SourceType, transformer::{ EnvOptions, HelperLoaderMode, HelperLoaderOptions, InjectGlobalVariablesConfig, - InjectImport, JsxRuntime, ReplaceGlobalDefinesConfig, RewriteExtensionsMode, + InjectImport, JsxRuntime, ModuleRunnerTransform, ReplaceGlobalDefinesConfig, + RewriteExtensionsMode, }, }; use oxc_napi::OxcError; @@ -713,3 +717,106 @@ pub fn transform( errors: compiler.errors.into_iter().map(OxcError::from).collect(), } } + +#[derive(Default)] +#[napi(object)] +pub struct ModuleRunnerTransformOptions { + /// Enable source map generation. + /// + /// When `true`, the `sourceMap` field of transform result objects will be populated. + /// + /// @default false + /// + /// @see {@link SourceMap} + pub sourcemap: Option, +} + +#[derive(Default)] +#[napi(object)] +pub struct ModuleRunnerTransformResult { + /// The transformed code. + /// + /// If parsing failed, this will be an empty string. + pub code: String, + + /// The source map for the transformed code. + /// + /// This will be set if {@link TransformOptions#sourcemap} is `true`. + pub map: Option, + + // Import sources collected during transformation. + pub deps: Vec, + + // Dynamic import sources collected during transformation. + pub dynamic_deps: Vec, + + /// Parse and transformation errors. + /// + /// Oxc's parser recovers from common syntax errors, meaning that + /// transformed code may still be available even if there are errors in this + /// list. + pub errors: Vec, +} + +/// Transform JavaScript code to a Vite Node runnable module. +/// +/// @param filename The name of the file being transformed. +/// @param sourceText the source code itself +/// @param options The options for the transformation. See {@link +/// ModuleRunnerTransformOptions} for more information. +/// +/// @returns an object containing the transformed code, source maps, and any +/// errors that occurred during parsing or transformation. +/// +/// @deprecated Only works for Vite. +#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)] +#[napi] +pub fn module_runner_transform( + filename: String, + source_text: String, + options: Option, +) -> ModuleRunnerTransformResult { + let file_path = Path::new(&filename); + let source_type = SourceType::from_path(file_path); + let source_type = match source_type { + Ok(s) => s, + Err(err) => { + return ModuleRunnerTransformResult { + code: String::default(), + map: None, + deps: vec![], + dynamic_deps: vec![], + errors: vec![OxcError::new(err.to_string())], + }; + } + }; + + let allocator = Allocator::default(); + let mut parser_ret = Parser::new(&allocator, &source_text, source_type).parse(); + let mut program = parser_ret.program; + + let SemanticBuilderReturn { semantic, errors } = + SemanticBuilder::new().with_check_syntax_error(true).build(&program); + parser_ret.errors.extend(errors); + + let (symbols, scopes) = semantic.into_symbol_table_and_scope_tree(); + let (deps, dynamic_deps) = + ModuleRunnerTransform::default().transform(&allocator, &mut program, symbols, scopes); + + let CodegenReturn { code, map, .. } = CodeGenerator::new() + .with_options(CodegenOptions { + source_map_path: options.and_then(|opts| { + opts.sourcemap.as_ref().and_then(|s| s.then(|| file_path.to_path_buf())) + }), + ..Default::default() + }) + .build(&program); + + ModuleRunnerTransformResult { + code, + map: map.map(Into::into), + deps, + dynamic_deps, + errors: parser_ret.errors.into_iter().map(OxcError::from).collect(), + } +} diff --git a/napi/transform/test/moduleRunnerTransform.test.ts b/napi/transform/test/moduleRunnerTransform.test.ts new file mode 100644 index 0000000000000..1cba5d9e466ff --- /dev/null +++ b/napi/transform/test/moduleRunnerTransform.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from 'vitest'; +import { moduleRunnerTransform } from '../index'; + +describe('moduleRunnerTransform', () => { + test('dynamic import', async () => { + const result = await moduleRunnerTransform('index.js', `export const i = () => import('./foo')`); + expect(result?.code).toMatchInlineSnapshot(` + "const i = () => __vite_ssr_dynamic_import__("./foo"); + Object.defineProperty(__vite_ssr_exports__, "i", { + enumerable: true, + configurable: true, + get() { + return i; + } + }); + " + `); + expect(result?.deps).toEqual([]); + expect(result?.dynamicDeps).toEqual(['./foo']); + }); + + test('sourcemap', async () => { + const map = ( + moduleRunnerTransform( + 'index.js', + `export const a = 1`, + { + sourcemap: true, + }, + ) + )?.map; + + expect(map).toMatchInlineSnapshot(` + { + "mappings": "AAAO,MAAM,IAAI;AAAjB", + "names": [], + "sources": [ + "index.js", + ], + "sourcesContent": [ + "export const a = 1", + ], + "version": 3, + } + `); + }); +});