Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions crates/oxc_transformer/src/plugins/module_runner_transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -74,7 +74,7 @@ pub struct ModuleRunnerTransform<'a> {
dynamic_deps: Vec<String>,
}

impl ModuleRunnerTransform<'_> {
impl<'a> ModuleRunnerTransform<'a> {
pub fn new() -> Self {
Self {
import_uid: 0,
Expand All @@ -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<String>, Vec<String>) {
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__");
Expand Down
53 changes: 53 additions & 0 deletions napi/transform/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
dynamicDeps: Array<string>
/**
* 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<OxcError>
}

export interface OxcError {
severity: Severity
message: string
Expand Down
1 change: 1 addition & 0 deletions napi/transform/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
111 changes: 109 additions & 2 deletions napi/transform/src/transformer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<bool>,
}

#[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<SourceMap>,

// Import sources collected during transformation.
pub deps: Vec<String>,

// Dynamic import sources collected during transformation.
pub dynamic_deps: Vec<String>,

/// 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<OxcError>,
}

/// 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<ModuleRunnerTransformOptions>,
) -> 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(),
}
}
47 changes: 47 additions & 0 deletions napi/transform/test/moduleRunnerTransform.test.ts
Original file line number Diff line number Diff line change
@@ -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,
}
`);
});
});
Loading