Skip to content

Commit ffd485c

Browse files
committed
feat(transformer, napi/transform): expose moduleRunnerTransform function (#9532)
Expose a `moduleRunnerTransform` function that `Vite` can directly use it to speed-up ssr transform. In this way, `Vitest` also benefits without having to wait for `rolldown-vite`.
1 parent a0f6f37 commit ffd485c

File tree

5 files changed

+227
-6
lines changed

5 files changed

+227
-6
lines changed

crates/oxc_transformer/src/plugins/module_runner_transform.rs

+17-4
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ use compact_str::ToCompactString;
4848
use rustc_hash::FxHashMap;
4949
use std::iter;
5050

51-
use oxc_allocator::{Box as ArenaBox, String as ArenaString, Vec as ArenaVec};
51+
use oxc_allocator::{Allocator, Box as ArenaBox, String as ArenaString, Vec as ArenaVec};
5252
use oxc_ast::{NONE, ast::*};
5353
use oxc_ecmascript::BoundNames;
54-
use oxc_semantic::{ReferenceFlags, ScopeFlags, SymbolFlags, SymbolId};
54+
use oxc_semantic::{ReferenceFlags, ScopeFlags, ScopeTree, SymbolFlags, SymbolId, SymbolTable};
5555
use oxc_span::SPAN;
5656
use oxc_syntax::identifier::is_identifier_name;
57-
use oxc_traverse::{Ancestor, BoundIdentifier, Traverse, TraverseCtx};
57+
use oxc_traverse::{Ancestor, BoundIdentifier, Traverse, TraverseCtx, traverse_mut};
5858

5959
use crate::utils::ast_builder::{
6060
create_compute_property_access, create_member_callee, create_property_access,
@@ -74,7 +74,7 @@ pub struct ModuleRunnerTransform<'a> {
7474
dynamic_deps: Vec<String>,
7575
}
7676

77-
impl ModuleRunnerTransform<'_> {
77+
impl<'a> ModuleRunnerTransform<'a> {
7878
pub fn new() -> Self {
7979
Self {
8080
import_uid: 0,
@@ -83,6 +83,19 @@ impl ModuleRunnerTransform<'_> {
8383
dynamic_deps: Vec::default(),
8484
}
8585
}
86+
87+
/// Standalone transform
88+
pub fn transform(
89+
mut self,
90+
allocator: &'a Allocator,
91+
program: &mut Program<'a>,
92+
symbols: SymbolTable,
93+
scopes: ScopeTree,
94+
) -> (Vec<String>, Vec<String>) {
95+
traverse_mut(&mut self, allocator, program, symbols, scopes);
96+
97+
(self.deps, self.dynamic_deps)
98+
}
8699
}
87100

88101
const SSR_MODULE_EXPORTS_KEY: Atom<'static> = Atom::new_const("__vite_ssr_exports__");

napi/transform/index.d.ts

+53
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,59 @@ export interface JsxOptions {
199199
refresh?: boolean | ReactRefreshOptions
200200
}
201201

202+
/**
203+
* Transform JavaScript code to a Vite Node runnable module.
204+
*
205+
* @param filename The name of the file being transformed.
206+
* @param sourceText the source code itself
207+
* @param options The options for the transformation. See {@link
208+
* ModuleRunnerTransformOptions} for more information.
209+
*
210+
* @returns an object containing the transformed code, source maps, and any
211+
* errors that occurred during parsing or transformation.
212+
*
213+
* @deprecated Only works for Vite.
214+
*/
215+
export declare function moduleRunnerTransform(filename: string, sourceText: string, options?: ModuleRunnerTransformOptions | undefined | null): ModuleRunnerTransformResult
216+
217+
export interface ModuleRunnerTransformOptions {
218+
/**
219+
* Enable source map generation.
220+
*
221+
* When `true`, the `sourceMap` field of transform result objects will be populated.
222+
*
223+
* @default false
224+
*
225+
* @see {@link SourceMap}
226+
*/
227+
sourcemap?: boolean
228+
}
229+
230+
export interface ModuleRunnerTransformResult {
231+
/**
232+
* The transformed code.
233+
*
234+
* If parsing failed, this will be an empty string.
235+
*/
236+
code: string
237+
/**
238+
* The source map for the transformed code.
239+
*
240+
* This will be set if {@link TransformOptions#sourcemap} is `true`.
241+
*/
242+
map?: SourceMap
243+
deps: Array<string>
244+
dynamicDeps: Array<string>
245+
/**
246+
* Parse and transformation errors.
247+
*
248+
* Oxc's parser recovers from common syntax errors, meaning that
249+
* transformed code may still be available even if there are errors in this
250+
* list.
251+
*/
252+
errors: Array<OxcError>
253+
}
254+
202255
export interface OxcError {
203256
severity: Severity
204257
message: string

napi/transform/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -372,5 +372,6 @@ if (!nativeBinding) {
372372

373373
module.exports.HelperMode = nativeBinding.HelperMode
374374
module.exports.isolatedDeclaration = nativeBinding.isolatedDeclaration
375+
module.exports.moduleRunnerTransform = nativeBinding.moduleRunnerTransform
375376
module.exports.Severity = nativeBinding.Severity
376377
module.exports.transform = nativeBinding.transform

napi/transform/src/transformer.rs

+109-2
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ use rustc_hash::FxHashMap;
1212

1313
use oxc::{
1414
CompilerInterface,
15-
codegen::CodegenReturn,
15+
allocator::Allocator,
16+
codegen::{CodeGenerator, CodegenOptions, CodegenReturn},
1617
diagnostics::OxcDiagnostic,
18+
parser::Parser,
19+
semantic::{SemanticBuilder, SemanticBuilderReturn},
1720
span::SourceType,
1821
transformer::{
1922
EnvOptions, HelperLoaderMode, HelperLoaderOptions, InjectGlobalVariablesConfig,
20-
InjectImport, JsxRuntime, ReplaceGlobalDefinesConfig, RewriteExtensionsMode,
23+
InjectImport, JsxRuntime, ModuleRunnerTransform, ReplaceGlobalDefinesConfig,
24+
RewriteExtensionsMode,
2125
},
2226
};
2327
use oxc_napi::OxcError;
@@ -713,3 +717,106 @@ pub fn transform(
713717
errors: compiler.errors.into_iter().map(OxcError::from).collect(),
714718
}
715719
}
720+
721+
#[derive(Default)]
722+
#[napi(object)]
723+
pub struct ModuleRunnerTransformOptions {
724+
/// Enable source map generation.
725+
///
726+
/// When `true`, the `sourceMap` field of transform result objects will be populated.
727+
///
728+
/// @default false
729+
///
730+
/// @see {@link SourceMap}
731+
pub sourcemap: Option<bool>,
732+
}
733+
734+
#[derive(Default)]
735+
#[napi(object)]
736+
pub struct ModuleRunnerTransformResult {
737+
/// The transformed code.
738+
///
739+
/// If parsing failed, this will be an empty string.
740+
pub code: String,
741+
742+
/// The source map for the transformed code.
743+
///
744+
/// This will be set if {@link TransformOptions#sourcemap} is `true`.
745+
pub map: Option<SourceMap>,
746+
747+
// Import sources collected during transformation.
748+
pub deps: Vec<String>,
749+
750+
// Dynamic import sources collected during transformation.
751+
pub dynamic_deps: Vec<String>,
752+
753+
/// Parse and transformation errors.
754+
///
755+
/// Oxc's parser recovers from common syntax errors, meaning that
756+
/// transformed code may still be available even if there are errors in this
757+
/// list.
758+
pub errors: Vec<OxcError>,
759+
}
760+
761+
/// Transform JavaScript code to a Vite Node runnable module.
762+
///
763+
/// @param filename The name of the file being transformed.
764+
/// @param sourceText the source code itself
765+
/// @param options The options for the transformation. See {@link
766+
/// ModuleRunnerTransformOptions} for more information.
767+
///
768+
/// @returns an object containing the transformed code, source maps, and any
769+
/// errors that occurred during parsing or transformation.
770+
///
771+
/// @deprecated Only works for Vite.
772+
#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)]
773+
#[napi]
774+
pub fn module_runner_transform(
775+
filename: String,
776+
source_text: String,
777+
options: Option<ModuleRunnerTransformOptions>,
778+
) -> ModuleRunnerTransformResult {
779+
let file_path = Path::new(&filename);
780+
let source_type = SourceType::from_path(file_path);
781+
let source_type = match source_type {
782+
Ok(s) => s,
783+
Err(err) => {
784+
return ModuleRunnerTransformResult {
785+
code: String::default(),
786+
map: None,
787+
deps: vec![],
788+
dynamic_deps: vec![],
789+
errors: vec![OxcError::new(err.to_string())],
790+
};
791+
}
792+
};
793+
794+
let allocator = Allocator::default();
795+
let mut parser_ret = Parser::new(&allocator, &source_text, source_type).parse();
796+
let mut program = parser_ret.program;
797+
798+
let SemanticBuilderReturn { semantic, errors } =
799+
SemanticBuilder::new().with_check_syntax_error(true).build(&program);
800+
parser_ret.errors.extend(errors);
801+
802+
let (symbols, scopes) = semantic.into_symbol_table_and_scope_tree();
803+
let (deps, dynamic_deps) =
804+
ModuleRunnerTransform::default().transform(&allocator, &mut program, symbols, scopes);
805+
806+
let CodegenReturn { code, map, .. } = CodeGenerator::new()
807+
.with_options(CodegenOptions {
808+
source_map_path: options.and_then(|opts| {
809+
opts.sourcemap.as_ref().and_then(|s| s.then(|| file_path.to_path_buf()))
810+
}),
811+
..Default::default()
812+
})
813+
.build(&program);
814+
815+
ModuleRunnerTransformResult {
816+
code,
817+
map: map.map(Into::into),
818+
deps,
819+
dynamic_deps,
820+
errors: parser_ret.errors.into_iter().map(OxcError::from).collect(),
821+
}
822+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { describe, expect, test } from 'vitest';
2+
import { moduleRunnerTransform } from '../index';
3+
4+
describe('moduleRunnerTransform', () => {
5+
test('dynamic import', async () => {
6+
const result = await moduleRunnerTransform('index.js', `export const i = () => import('./foo')`);
7+
expect(result?.code).toMatchInlineSnapshot(`
8+
"const i = () => __vite_ssr_dynamic_import__("./foo");
9+
Object.defineProperty(__vite_ssr_exports__, "i", {
10+
enumerable: true,
11+
configurable: true,
12+
get() {
13+
return i;
14+
}
15+
});
16+
"
17+
`);
18+
expect(result?.deps).toEqual([]);
19+
expect(result?.dynamicDeps).toEqual(['./foo']);
20+
});
21+
22+
test('sourcemap', async () => {
23+
const map = (
24+
moduleRunnerTransform(
25+
'index.js',
26+
`export const a = 1`,
27+
{
28+
sourcemap: true,
29+
},
30+
)
31+
)?.map;
32+
33+
expect(map).toMatchInlineSnapshot(`
34+
{
35+
"mappings": "AAAO,MAAM,IAAI;AAAjB",
36+
"names": [],
37+
"sources": [
38+
"index.js",
39+
],
40+
"sourcesContent": [
41+
"export const a = 1",
42+
],
43+
"version": 3,
44+
}
45+
`);
46+
});
47+
});

0 commit comments

Comments
 (0)