Skip to content

Commit 70c402b

Browse files
committed
feat(napi/transform): add async transform function (#13881)
## Summary This PR adds an async transform function to the NAPI transform package, following the same pattern used in the parser package for consistency. Closes #10900 ## Changes - Added `transformAsync` function that returns a Promise - Implemented `TransformTask` struct with `napi::Task` trait - Reuses existing transform logic from the synchronous version - Added comprehensive tests to verify async behavior ## Test Plan Added tests in `test/transform.test.ts` that verify: - Async function works correctly - Produces identical results to sync version - Properly handles errors All existing tests continue to pass. 🤖 Generated with [Claude Code](https://claude.ai/code)
1 parent b52389a commit 70c402b

File tree

6 files changed

+129
-3
lines changed

6 files changed

+129
-3
lines changed

napi/transform/index.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,22 @@ export interface StyledComponentsOptions {
436436
*/
437437
export declare function transform(filename: string, sourceText: string, options?: TransformOptions | undefined | null): TransformResult
438438

439+
/**
440+
* Transpile a JavaScript or TypeScript into a target ECMAScript version, asynchronously.
441+
*
442+
* Note: This function can be slower than `transform` due to the overhead of spawning a thread.
443+
*
444+
* @param filename The name of the file being transformed. If this is a
445+
* relative path, consider setting the {@link TransformOptions#cwd} option.
446+
* @param sourceText the source code itself
447+
* @param options The options for the transformation. See {@link
448+
* TransformOptions} for more information.
449+
*
450+
* @returns a promise that resolves to an object containing the transformed code,
451+
* source maps, and any errors that occurred during parsing or transformation.
452+
*/
453+
export declare function transformAsync(filename: string, sourceText: string, options?: TransformOptions | undefined | null): Promise<TransformResult>
454+
439455
/**
440456
* Options for transforming a JavaScript or TypeScript file.
441457
*

napi/transform/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,9 +565,10 @@ if (!nativeBinding) {
565565
throw new Error(`Failed to load native binding`)
566566
}
567567

568-
const { Severity, HelperMode, isolatedDeclaration, moduleRunnerTransform, transform } = nativeBinding
568+
const { Severity, HelperMode, isolatedDeclaration, moduleRunnerTransform, transform, transformAsync } = nativeBinding
569569
export { Severity }
570570
export { HelperMode }
571571
export { isolatedDeclaration }
572572
export { moduleRunnerTransform }
573573
export { transform }
574+
export { transformAsync }

napi/transform/src/transformer.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66
path::{Path, PathBuf},
77
};
88

9-
use napi::Either;
9+
use napi::{Either, Task, bindgen_prelude::AsyncTask};
1010
use napi_derive::napi;
1111
use rustc_hash::FxHashMap;
1212

@@ -902,6 +902,74 @@ pub fn transform(
902902
}
903903
}
904904

905+
pub struct TransformTask {
906+
filename: String,
907+
source_text: String,
908+
options: Option<TransformOptions>,
909+
}
910+
911+
#[napi]
912+
impl Task for TransformTask {
913+
type JsValue = TransformResult;
914+
type Output = TransformResult;
915+
916+
fn compute(&mut self) -> napi::Result<Self::Output> {
917+
let source_path = Path::new(&self.filename);
918+
919+
let source_type = get_source_type(
920+
&self.filename,
921+
self.options.as_ref().and_then(|options| options.lang.as_deref()),
922+
self.options.as_ref().and_then(|options| options.source_type.as_deref()),
923+
);
924+
925+
let mut compiler = match Compiler::new(self.options.take()) {
926+
Ok(compiler) => compiler,
927+
Err(errors) => {
928+
return Ok(TransformResult {
929+
errors: OxcError::from_diagnostics(&self.filename, &self.source_text, errors),
930+
..Default::default()
931+
});
932+
}
933+
};
934+
935+
compiler.compile(&self.source_text, source_type, source_path);
936+
937+
Ok(TransformResult {
938+
code: compiler.printed,
939+
map: compiler.printed_sourcemap,
940+
declaration: compiler.declaration,
941+
declaration_map: compiler.declaration_map,
942+
helpers_used: compiler.helpers_used,
943+
errors: OxcError::from_diagnostics(&self.filename, &self.source_text, compiler.errors),
944+
})
945+
}
946+
947+
fn resolve(&mut self, _: napi::Env, result: Self::Output) -> napi::Result<Self::JsValue> {
948+
Ok(result)
949+
}
950+
}
951+
952+
/// Transpile a JavaScript or TypeScript into a target ECMAScript version, asynchronously.
953+
///
954+
/// Note: This function can be slower than `transform` due to the overhead of spawning a thread.
955+
///
956+
/// @param filename The name of the file being transformed. If this is a
957+
/// relative path, consider setting the {@link TransformOptions#cwd} option.
958+
/// @param sourceText the source code itself
959+
/// @param options The options for the transformation. See {@link
960+
/// TransformOptions} for more information.
961+
///
962+
/// @returns a promise that resolves to an object containing the transformed code,
963+
/// source maps, and any errors that occurred during parsing or transformation.
964+
#[napi]
965+
pub fn transform_async(
966+
filename: String,
967+
source_text: String,
968+
options: Option<TransformOptions>,
969+
) -> AsyncTask<TransformTask> {
970+
AsyncTask::new(TransformTask { filename, source_text, options })
971+
}
972+
905973
#[derive(Default)]
906974
#[napi(object)]
907975
pub struct ModuleRunnerTransformOptions {

napi/transform/test/transform.test.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Worker } from 'node:worker_threads';
22
import { describe, expect, it, test } from 'vitest';
33

4-
import { HelperMode, transform } from '../index';
4+
import { HelperMode, transform, transformAsync } from '../index';
55

66
describe('simple', () => {
77
const code = 'export class A<T> {}';
@@ -45,6 +45,45 @@ describe('simple', () => {
4545
});
4646
});
4747

48+
describe('transformAsync', () => {
49+
const code = 'export class A<T> {}';
50+
51+
it('should work asynchronously', async () => {
52+
const ret = await transformAsync('test.ts', code, { sourcemap: true });
53+
expect(ret).toMatchObject({
54+
code: 'export class A {}\n',
55+
errors: [],
56+
helpersUsed: {},
57+
map: {
58+
names: [],
59+
sources: ['test.ts'],
60+
sourcesContent: ['export class A<T> {}'],
61+
version: 3,
62+
},
63+
});
64+
});
65+
66+
it('should produce the same result as sync transform', async () => {
67+
const sourceCode = `
68+
const add = (a, b) => a + b;
69+
console.log(add(1, 2));
70+
`;
71+
72+
const syncResult = transform('test.js', sourceCode, { target: 'es2015' });
73+
const asyncResult = await transformAsync('test.js', sourceCode, { target: 'es2015' });
74+
75+
expect(asyncResult.code).toEqual(syncResult.code);
76+
expect(asyncResult.errors).toEqual(syncResult.errors);
77+
expect(asyncResult.helpersUsed).toEqual(syncResult.helpersUsed);
78+
});
79+
80+
it('should handle errors properly', async () => {
81+
const invalidCode = 'export class { invalid syntax';
82+
const ret = await transformAsync('test.ts', invalidCode);
83+
expect(ret.errors.length).toBeGreaterThan(0);
84+
});
85+
});
86+
4887
describe('transform', () => {
4988
it('should not transform by default', () => {
5089
const cases = [

napi/transform/transform.wasi-browser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ export const HelperMode = __napiModule.exports.HelperMode
6161
export const isolatedDeclaration = __napiModule.exports.isolatedDeclaration
6262
export const moduleRunnerTransform = __napiModule.exports.moduleRunnerTransform
6363
export const transform = __napiModule.exports.transform
64+
export const transformAsync = __napiModule.exports.transformAsync

napi/transform/transform.wasi.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,4 @@ module.exports.HelperMode = __napiModule.exports.HelperMode
113113
module.exports.isolatedDeclaration = __napiModule.exports.isolatedDeclaration
114114
module.exports.moduleRunnerTransform = __napiModule.exports.moduleRunnerTransform
115115
module.exports.transform = __napiModule.exports.transform
116+
module.exports.transformAsync = __napiModule.exports.transformAsync

0 commit comments

Comments
 (0)