Skip to content

Commit

Permalink
Add --swc shorthand for the built-in swc transpiler; graduate the swc…
Browse files Browse the repository at this point in the history
… transpiler out of experimental status (#1536)

* Add --swc shorthand for the built-in swc transpiler; graduate the swc transpiler out of experimental status

* lint-fix

* fix

* fix tests

* Improve swc / transpiler tests to prove that third-party transpiler was invoked

* lintfix
  • Loading branch information
cspotcode authored Nov 3, 2021
1 parent 3e85ca1 commit 2385967
Show file tree
Hide file tree
Showing 15 changed files with 138 additions and 34 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"./esm.mjs": "./esm.mjs",
"./esm/transpile-only": "./esm/transpile-only.mjs",
"./esm/transpile-only.mjs": "./esm/transpile-only.mjs",
"./transpilers/swc": "./transpilers/swc.js",
"./transpilers/swc-experimental": "./transpilers/swc-experimental.js",
"./node10/tsconfig.json": "./node10/tsconfig.json",
"./node12/tsconfig.json": "./node12/tsconfig.json",
Expand Down
4 changes: 4 additions & 0 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function main(
'--ignore': [String],
'--transpile-only': Boolean,
'--transpiler': String,
'--swc': Boolean,
'--type-check': Boolean,
'--compiler-host': Boolean,
'--pretty': Boolean,
Expand Down Expand Up @@ -116,6 +117,7 @@ export function main(
'--transpile-only': transpileOnly,
'--type-check': typeCheck,
'--transpiler': transpiler,
'--swc': swc,
'--compiler-host': compilerHost,
'--pretty': pretty,
'--skip-project': skipProject,
Expand Down Expand Up @@ -145,6 +147,7 @@ export function main(
--show-config Print resolved configuration and exit
-T, --transpile-only Use TypeScript's faster \`transpileModule\` or a third-party transpiler
--swc Use the swc transpiler
-H, --compiler-host Use TypeScript's compiler host API
-I, --ignore [pattern] Override the path patterns to skip compilation
-P, --project [path] Path to TypeScript JSON project file
Expand Down Expand Up @@ -256,6 +259,7 @@ export function main(
experimentalReplAwait: noExperimentalReplAwait ? false : undefined,
typeCheck,
transpiler,
swc,
compilerHost,
ignore,
preferTsExts,
Expand Down
2 changes: 2 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ function filterRecognizedTsConfigTsNodeOptions(
scopeDir,
moduleTypes,
experimentalReplAwait,
swc,
...unrecognized
} = jsonObject as TsConfigOptions;
const filteredTsConfigOptions = {
Expand All @@ -298,6 +299,7 @@ function filterRecognizedTsConfigTsNodeOptions(
scope,
scopeDir,
moduleTypes,
swc,
};
// Use the typechecker to make sure this implementation has the correct set of properties
const catchExtraneousProps: keyof TsConfigOptions = (null as any) as keyof typeof filteredTsConfigOptions;
Expand Down
38 changes: 32 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ export interface CreateOptions {
* Specify a custom transpiler for use with transpileOnly
*/
transpiler?: string | [string, object];
/**
* Transpile with swc instead of the TypeScript compiler, and skip typechecking.
*
* Equivalent to setting both `transpileOnly: true` and `transpiler: 'ts-node/transpilers/swc'`
*/
swc?: boolean;
/**
* Paths which should not be compiled.
*
Expand Down Expand Up @@ -608,11 +614,33 @@ export function create(rawOptions: CreateOptions = {}): Service {
({ compiler, ts } = loadCompiler(options.compiler, configFilePath));
}

// swc implies two other options
// typeCheck option was implemented specifically to allow overriding tsconfig transpileOnly from the command-line
// So we should allow using typeCheck to override swc
if (options.swc && !options.typeCheck) {
if (options.transpileOnly === false) {
throw new Error(
"Cannot enable 'swc' option with 'transpileOnly: false'. 'swc' implies 'transpileOnly'."
);
}
if (options.transpiler) {
throw new Error(
"Cannot specify both 'swc' and 'transpiler' options. 'swc' uses the built-in swc transpiler."
);
}
}

const readFile = options.readFile || ts.sys.readFile;
const fileExists = options.fileExists || ts.sys.fileExists;
// typeCheck can override transpileOnly, useful for CLI flag to override config file
const transpileOnly =
options.transpileOnly === true && options.typeCheck !== true;
(options.transpileOnly === true || options.swc === true) &&
options.typeCheck !== true;
const transpiler = options.transpiler
? options.transpiler
: options.swc
? require.resolve('./transpilers/swc.js')
: undefined;
const transformers = options.transformers || undefined;
const diagnosticFilters: Array<DiagnosticFilter> = [
{
Expand Down Expand Up @@ -668,17 +696,15 @@ export function create(rawOptions: CreateOptions = {}): Service {
);
}
let customTranspiler: Transpiler | undefined = undefined;
if (options.transpiler) {
if (transpiler) {
if (!transpileOnly)
throw new Error(
'Custom transpiler can only be used when transpileOnly is enabled.'
);
const transpilerName =
typeof options.transpiler === 'string'
? options.transpiler
: options.transpiler[0];
typeof transpiler === 'string' ? transpiler : transpiler[0];
const transpilerOptions =
typeof options.transpiler === 'string' ? {} : options.transpiler[1] ?? {};
typeof transpiler === 'string' ? {} : transpiler[1] ?? {};
// TODO mimic fixed resolution logic from loadCompiler main
// TODO refactor into a more generic "resolve dep relative to project" helper
const transpilerPath = require.resolve(transpilerName, {
Expand Down
41 changes: 26 additions & 15 deletions src/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ test.suite('ts-node', (test) => {
testsDirRequire.resolve('ts-node/esm/transpile-only');
testsDirRequire.resolve('ts-node/esm/transpile-only.mjs');

testsDirRequire.resolve('ts-node/transpilers/swc');
testsDirRequire.resolve('ts-node/transpilers/swc-experimental');

testsDirRequire.resolve('ts-node/node10/tsconfig.json');
Expand Down Expand Up @@ -304,21 +305,31 @@ test.suite('ts-node', (test) => {
expect(err.message).toMatch('error TS1003: Identifier expected');
});

test('should support third-party transpilers via --transpiler', async () => {
const { err, stdout } = await exec(
`${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --transpiler ts-node/transpilers/swc-experimental transpile-only-swc`
);
expect(err).toBe(null);
expect(stdout).toMatch('Hello World!');
});

test('should support third-party transpilers via tsconfig', async () => {
const { err, stdout } = await exec(
`${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} transpile-only-swc-via-tsconfig`
);
expect(err).toBe(null);
expect(stdout).toMatch('Hello World!');
});
for (const flavor of [
'--transpiler ts-node/transpilers/swc transpile-only-swc',
'--transpiler ts-node/transpilers/swc-experimental transpile-only-swc',
'--swc transpile-only-swc',
'transpile-only-swc-via-tsconfig',
'transpile-only-swc-shorthand-via-tsconfig',
]) {
test(`should support swc and third-party transpilers: ${flavor}`, async () => {
const { err, stdout } = await exec(
`${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ${flavor}`,
{
env: {
...process.env,
NODE_OPTIONS: `${
process.env.NODE_OPTIONS || ''
} --require ${require.resolve('../../tests/spy-swc-transpiler')}`,
},
}
);
expect(err).toBe(null);
expect(stdout).toMatch(
'Hello World! swc transpiler invocation count: 1\n'
);
});
}

if (semver.gte(process.version, '12.16.0')) {
test('swc transpiler supports native ESM emit', async () => {
Expand Down
16 changes: 16 additions & 0 deletions tests/spy-swc-transpiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Spy on the swc transpiler so that tests can prove it was used rather than
// TypeScript's `transpileModule`.
const swcTranspiler = require('ts-node/transpilers/swc');

global.swcTranspilerCalls = 0;

const wrappedCreate = swcTranspiler.create;
swcTranspiler.create = function (...args) {
const transpiler = wrappedCreate(...args);
const wrappedTranspile = transpiler.transpile;
transpiler.transpile = function (...args) {
global.swcTranspilerCalls++;
return wrappedTranspile.call(this, ...args);
};
return transpiler;
};
13 changes: 13 additions & 0 deletions tests/transpile-only-swc-shorthand-via-tsconfig/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Test for #1343
const Decorator = function () {};
@Decorator
class World {}

// intentional type errors to check transpile-only ESM loader skips type checking
parseInt(1101, 2);
const x: number = `Hello ${World.name}! swc transpiler invocation count: ${global.swcTranspilerCalls}`;
console.log(x);

// test module type emit
import { readFileSync } from 'fs';
readFileSync;
12 changes: 12 additions & 0 deletions tests/transpile-only-swc-shorthand-via-tsconfig/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"ts-node": {
"swc": true
},
"compilerOptions": {
"target": "ES2018",
"module": "CommonJS",
"allowJs": true,
"jsx": "react",
"experimentalDecorators": true
}
}
2 changes: 1 addition & 1 deletion tests/transpile-only-swc-via-tsconfig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class World {}

// intentional type errors to check transpile-only ESM loader skips type checking
parseInt(1101, 2);
const x: number = `Hello ${World.name}!`;
const x: number = `Hello ${World.name}! swc transpiler invocation count: ${global.swcTranspilerCalls}`;
console.log(x);

// test module type emit
Expand Down
2 changes: 1 addition & 1 deletion tests/transpile-only-swc-via-tsconfig/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"ts-node": {
"transpileOnly": true,
"transpiler": "ts-node/transpilers/swc-experimental"
"transpiler": "ts-node/transpilers/swc"
},
"compilerOptions": {
"target": "ES2018",
Expand Down
2 changes: 1 addition & 1 deletion tests/transpile-only-swc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class World {}

// intentional type errors to check transpile-only ESM loader skips type checking
parseInt(1101, 2);
const x: number = `Hello ${World.name}!`;
const x: number = `Hello ${World.name}! swc transpiler invocation count: ${global.swcTranspilerCalls}`;
console.log(x);

// test module type emit
Expand Down
1 change: 1 addition & 0 deletions transpilers/swc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../dist/transpilers/swc')
1 change: 1 addition & 0 deletions website/docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ _Environment variables, where available, are in `ALL_CAPS`_
- `-I, --ignore [pattern]` Override the path patterns to skip compilation <br/>*Default:* `/node_modules/` <br/>*Environment:* `TS_NODE_IGNORE`
- `--skip-ignore` Skip ignore checks <br/>*Default:* `false` <br/>*Environment:* `TS_NODE_SKIP_IGNORE`
- `-C, --compiler [name]` Specify a custom TypeScript compiler <br/>*Default:* `typescript` <br/>*Environment:* `TS_NODE_COMPILER`
- `--swc` Transpile with [swc](./transpilers.md#swc). Implies `--transpile-only` <br/>*Default:* `false`
- `--transpiler [name]` Specify a third-party, non-typechecking transpiler
- `--prefer-ts-exts` Re-order file extensions so that TypeScript imports are preferred <br/>*Default:* `false` <br/>*Environment:* `TS_NODE_PREFER_TS_EXTS`

Expand Down
2 changes: 1 addition & 1 deletion website/docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ These tricks will make ts-node faster.
It is often better to use `tsc --noEmit` to typecheck once before your tests run or as a lint step. In these cases, ts-node can skip typechecking.

* Enable [`transpileOnly`](./options.md) to skip typechecking
* Use our [`swc` integration](./transpilers.md#bundled-swc-integration)
* Use our [`swc` integration](./transpilers.md#swc)
* This is by far the fastest option

## With typechecking
Expand Down
35 changes: 26 additions & 9 deletions website/docs/transpilers.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Third-party transpilers
title: Transpilers
---

In transpile-only mode, we skip typechecking to speed up execution time. You can go a step further and use a
Expand All @@ -13,12 +13,11 @@ boilerplate.
> For our purposes, a compiler implements TypeScript's API and can perform typechecking.
> A third-party transpiler does not. Both transform TypeScript into JavaScript.
## Bundled `swc` integration
## swc

We have bundled an experimental `swc` integration.
swc support is built-in via the `--swc` flag or `"swc": true` tsconfig option.

[`swc`](https://swc.rs) is a TypeScript-compatible transpiler implemented in Rust. This makes it an order of magnitude faster
than `transpileModule`.
[`swc`](https://swc.rs) is a TypeScript-compatible transpiler implemented in Rust. This makes it an order of magnitude faster than vanilla `transpileOnly`.

To use it, first install `@swc/core` or `@swc/wasm`. If using `importHelpers`, also install `@swc/helpers`. If `target` is less than "es2015" and using either `async`/`await` or generator functions, also install `regenerator-runtime`.

Expand All @@ -31,17 +30,35 @@ Then add the following to your `tsconfig.json`.
```json title="tsconfig.json"
{
"ts-node": {
"transpileOnly": true,
"transpiler": "ts-node/transpilers/swc-experimental"
"swc": true
}
}
```

> `swc` uses `@swc/helpers` instead of `tslib`. If you have enabled `importHelpers`, you must also install `@swc/helpers`.
## Third-party transpilers

The `transpiler` option allows using third-party transpiler integrations with ts-node. `transpiler` must be given the
name of a module which can be `require()`d. The built-in `swc` integration is exposed as `ts-node/transpilers/swc`.

For example, to use a hypothetical "speedy-ts-compiler", first install it into your project: `npm install speedy-ts-compiler`

Then add the following to your tsconfig:

```json title="tsconfig.json"
{
"ts-node": {
"transpileOnly": true,
"transpiler": "speedy-ts-compiler"
}
}
```

## Writing your own integration

To write your own transpiler integration, check our [API docs](https://typestrong.org/ts-node/api/interfaces/TranspilerModule.html).

Integrations are `require()`d, so they can be published to npm. The module must export a `create` function matching the
[`TranspilerModule`](https://typestrong.org/ts-node/api/interfaces/TranspilerModule.html) interface.
Integrations are `require()`d by ts-node, so they can be published to npm for convenience. The module must export a `create` function described by our
[`TranspilerModule`](https://typestrong.org/ts-node/api/interfaces/TranspilerModule.html) interface. `create` is invoked by ts-node
at startup to create the transpiler. The transpiler is used repeatedly to transform TypeScript into JavaScript.

0 comments on commit 2385967

Please sign in to comment.