diff --git a/crates/oxc/src/napi/transform.rs b/crates/oxc/src/napi/transform.rs index 3d761b2ccee3b..bf4b2e9a5f421 100644 --- a/crates/oxc/src/napi/transform.rs +++ b/crates/oxc/src/napi/transform.rs @@ -8,7 +8,7 @@ use napi::Either; use napi_derive::napi; use rustc_hash::FxHashMap; -use oxc_transformer::{JsxRuntime, RewriteExtensionsMode}; +use oxc_transformer::{EnvOptions, JsxRuntime, RewriteExtensionsMode}; use super::{isolated_declarations::IsolatedDeclarationsOptions, source_map::SourceMap}; @@ -80,6 +80,20 @@ pub struct TransformOptions { /// Configure how TSX and JSX are transformed. pub jsx: Option, + /// Sets the target environment for the generated JavaScript. + /// + /// The lowest target is `es2015`. + /// + /// Example: + /// + /// * 'es2015' + /// * ['es2020', 'chrome58', 'edge16', 'firefox57', 'node12', 'safari11'] + /// + /// @default `esnext` (No transformation) + /// + /// @see [esbuild#target](https://esbuild.github.io/api/#target) + pub target: Option>>, + /// Define Plugin #[napi(ts_type = "Record")] pub define: Option>, @@ -93,6 +107,11 @@ impl TryFrom for oxc_transformer::TransformOptions { type Error = String; fn try_from(options: TransformOptions) -> Result { + let env = match options.target { + Some(Either::A(s)) => EnvOptions::from_target(&s)?, + Some(Either::B(list)) => EnvOptions::from_target_list(&list)?, + _ => EnvOptions::default(), + }; Ok(Self { cwd: options.cwd.map(PathBuf::from).unwrap_or_default(), typescript: options @@ -100,6 +119,7 @@ impl TryFrom for oxc_transformer::TransformOptions { .map(oxc_transformer::TypeScriptOptions::from) .unwrap_or_default(), jsx: options.jsx.map(Into::into).unwrap_or_default(), + env, ..Self::default() }) } diff --git a/crates/oxc_transformer/tests/integrations/es_target.rs b/crates/oxc_transformer/tests/integrations/es_target.rs index 2a486456705ac..53c07e631f381 100644 --- a/crates/oxc_transformer/tests/integrations/es_target.rs +++ b/crates/oxc_transformer/tests/integrations/es_target.rs @@ -13,9 +13,10 @@ fn es_target() { ("es2015", "a ** b"), ("es2016", "async function foo() {}"), ("es2017", "({ ...x })"), - ("es2017", "try {} catch {}"), + ("es2018", "try {} catch {}"), + ("es2019", "a?.b"), ("es2019", "a ?? b"), - ("es2019", "a ||= b"), + ("es2020", "a ||= b"), ("es2019", "1n ** 2n"), // test target error ("es2021", "class foo { static {} }"), ]; diff --git a/crates/oxc_transformer/tests/integrations/snapshots/es_target.snap b/crates/oxc_transformer/tests/integrations/snapshots/es_target.snap index 6bfe409aec951..d667de94c5932 100644 --- a/crates/oxc_transformer/tests/integrations/snapshots/es_target.snap +++ b/crates/oxc_transformer/tests/integrations/snapshots/es_target.snap @@ -30,23 +30,29 @@ function _foo() { import _objectSpread from '@babel/runtime/helpers/objectSpread2'; _objectSpread({}, x); -########## 4 es2017 +########## 4 es2018 try {} catch {} ---------- try {} catch (_unused) {} ########## 5 es2019 +a?.b +---------- +var _a; +(_a = a) === null || _a === void 0 ? void 0 : _a.b; + +########## 6 es2019 a ?? b ---------- var _a; (_a = a) !== null && _a !== void 0 ? _a : b; -########## 6 es2019 +########## 7 es2020 a ||= b ---------- a || (a = b); -########## 7 es2019 +########## 8 es2019 1n ** 2n ---------- @@ -65,7 +71,7 @@ a || (a = b); : ^^ `---- -########## 8 es2021 +########## 9 es2021 class foo { static {} } ---------- class foo { diff --git a/napi/transform/index.d.ts b/napi/transform/index.d.ts index 9fd1aca75d5cb..5e3100e1bf82e 100644 --- a/napi/transform/index.d.ts +++ b/napi/transform/index.d.ts @@ -203,6 +203,21 @@ export interface TransformOptions { typescript?: TypeScriptOptions /** Configure how TSX and JSX are transformed. */ jsx?: JsxOptions + /** + * Sets the target environment for the generated JavaScript. + * + * The lowest target is `es2015`. + * + * Example: + * + * * 'es2015' + * * ['es2020', 'chrome58', 'edge16', 'firefox57', 'node12', 'safari11'] + * + * @default `esnext` (No transformation) + * + * @see [esbuild#target]( /** Define Plugin */ define?: Record /** Inject Plugin */ diff --git a/napi/transform/package.json b/napi/transform/package.json index 7c43dbf2f2ed0..c7efd9a2953c1 100644 --- a/napi/transform/package.json +++ b/napi/transform/package.json @@ -3,10 +3,7 @@ "private": true, "scripts": { "build": "napi build --platform --release", - "test": "vitest run ./test" - }, - "engines": { - "node": ">=14.*" + "test": "vitest --typecheck run ./test" }, "napi": { "binaryName": "transform", diff --git a/napi/transform/test/id.test.ts b/napi/transform/test/id.test.ts index d378d5e79a116..db949e9bce501 100644 --- a/napi/transform/test/id.test.ts +++ b/napi/transform/test/id.test.ts @@ -1,6 +1,6 @@ import { assert, describe, it } from 'vitest'; -import oxc from './index'; +import oxc from '../index'; describe('isolated declaration', () => { const code = ` diff --git a/napi/transform/test/transform.test.ts b/napi/transform/test/transform.test.ts index fa7aeaecd2909..e3505aa1de6e1 100644 --- a/napi/transform/test/transform.test.ts +++ b/napi/transform/test/transform.test.ts @@ -1,12 +1,12 @@ -import { assert, describe, it } from 'vitest'; +import { assert, describe, it, test } from 'vitest'; -import oxc from './index'; +import { transform } from '../index'; describe('simple', () => { const code = 'export class A {}'; it('matches output', () => { - const ret = oxc.transform('test.ts', code, { sourcemap: true }); + const ret = transform('test.ts', code, { sourcemap: true }); assert.deepEqual(ret, { code: 'export class A {}\n', errors: [], @@ -21,12 +21,12 @@ describe('simple', () => { }); it('uses the `lang` option', () => { - const ret = oxc.transform('test.vue', code, { lang: 'ts' }); + const ret = transform('test.vue', code, { lang: 'ts' }); assert.equal(ret.code, 'export class A {}\n'); }); it('uses the `declaration option`', () => { - const ret = oxc.transform('test.ts', code, { typescript: { declaration: true } }); + const ret = transform('test.ts', code, { typescript: { declaration: {} } }); assert.equal(ret.declaration, 'export declare class A {}\n'); }); }); @@ -44,21 +44,48 @@ describe('transform', () => { 'class foo {\n\tstatic {}\n}', ]; for (const code of cases) { - const ret = oxc.transform('test.ts', code); + const ret = transform('test.ts', code); assert.equal(ret.code.trim(), code); } }); }); +describe('target', () => { + const data = [ + ['es2015', 'a ** b;\n'], + ['es2016', 'async function foo() {}\n'], + ['es2017', '({ ...x });\n'], + ['es2017', 'try {} catch {}\n'], + ['es2019', 'a?.b;\n'], + ['es2019', 'a ?? b;\n'], + ['es2021', 'class foo {\n\tstatic {}\n}\n'], + ]; + + test.each(data)('transform %s', (target, code) => { + // Also test array syntax. + const ret = transform('test.js', code, { target: [target] }); + assert(ret.errors.length == 0); + assert(ret.code); + assert.notEqual(ret.code, code); + }); + + test.each(data)('no transform esnext: %s', (_target, code) => { + const ret = transform('test.js', code, { target: 'esnext' }); + assert(ret.errors.length == 0); + assert(ret.code); + assert.equal(ret.code, code); + }); +}); + describe('modules', () => { it('should transform export = and import ', () => { const code = ` export = function foo (): void {} import bar = require('bar') `; - const ret = oxc.transform('test.ts', code, { + const ret = transform('test.ts', code, { typescript: { - declaration: true, + declaration: {}, }, }); assert.deepEqual(ret, { @@ -77,7 +104,7 @@ describe('react refresh plugin', () => { };`; it('matches output', () => { - const ret = oxc.transform('test.tsx', code, { jsx: { refresh: {} } }); + const ret = transform('test.tsx', code, { jsx: { refresh: {} } }); assert.equal( ret.code, `import { useState } from "react"; @@ -103,7 +130,7 @@ $RefreshReg$(_c, "App"); describe('define plugin', () => { it('matches output', () => { const code = 'if (process.env.NODE_ENV === "production") { foo; }'; - const ret = oxc.transform('test.tsx', code, { + const ret = transform('test.tsx', code, { define: { 'process.env.NODE_ENV': '"development"', }, @@ -113,7 +140,7 @@ describe('define plugin', () => { it('handles typescript declare global', () => { const code = 'declare let __TEST_DEFINE__: string; console.log({ __TEST_DEFINE__ });'; - const ret = oxc.transform('test.ts', code, { + const ret = transform('test.ts', code, { define: { '__TEST_DEFINE__': '"replaced"', }, @@ -126,7 +153,7 @@ describe('inject plugin', () => { const code = 'let _ = Object.assign'; it('matches output', () => { - const ret = oxc.transform('test.tsx', code, { + const ret = transform('test.tsx', code, { inject: { 'Object.assign': 'foo', }, diff --git a/napi/transform/tsconfig.json b/napi/transform/tsconfig.json new file mode 100644 index 0000000000000..abb88a3e0ec3e --- /dev/null +++ b/napi/transform/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + } +}