diff --git a/.changeset/good-walls-brush.md b/.changeset/good-walls-brush.md new file mode 100644 index 000000000..df38d26ca --- /dev/null +++ b/.changeset/good-walls-brush.md @@ -0,0 +1,15 @@ +--- +"synckit": minor +--- + +feat: add support for `--experimental-strip-types` + +Introducing the `node` runner, which will replace `ts-node` as the new default: + +- when running on Node 22 with the `--experimental-strip-types` + flag enabled via `NODE_OPTIONS` env or cli args +- or when running on Node 23+ without `--no-experimental-strip-types` + flag enabled via `NODE_OPTIONS` env or cli args + +An error will be thrown when attempting to run with `node` on unsupported versions (<22). +On these versions, the default runner remains `ts-node` when available. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ecccf61d..b7b5ca277 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,5 @@ jobs: - name: Codecov uses: codecov/codecov-action@v5 - if: ${{ matrix.node == 18 || matrix.node == 18.18 }} with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index 153ae783b..d3766fe5d 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,8 @@ Perform async work synchronously in Node.js using `worker_threads` with first-cl - [Options](#options) - [Envs](#envs) - [TypeScript](#typescript) - - [`ts-node`](#ts-node) + - [`node` (Default, Node 23+)](#node-default-node-23) + - [`ts-node` (Default)](#ts-node-default) - [`esbuild-register`](#esbuild-register) - [`esbuild-runner`](#esbuild-runner) - [`swc`](#swc) @@ -53,7 +54,7 @@ import { createSyncFn } from 'synckit' // the worker path must be absolute const syncFn = createSyncFn(require.resolve('./worker'), { - tsRunner: 'tsx', // optional, can be `'ts-node' | 'esbuild-register' | 'esbuild-runner' | 'tsx'` + tsRunner: 'tsx', // optional, can be `'node' | 'ts-node' | 'esbuild-register' | 'esbuild-runner' | 'tsx'` }) // do whatever you want, you will get the result synchronously! @@ -126,9 +127,17 @@ export interface GlobalShim { ### TypeScript -#### `ts-node` +#### `node` (Default, Node 23+) -If you want to use `ts-node` for worker file (a `.ts` file), it is supported out of box! +On recent Node versions, you may select this runner to execute your worker file (a `.ts` file) in the native runtime. + +As of Node v23, this feature is supported out of the box. To enable it in the current LTS, you can pass the [`--experimental-strip-types`](https://nodejs.org/docs/latest-v22.x/api/typescript.html#type-stripping) flag to the process. Visit the [documentation](https://nodejs.org/docs/latest/api/typescript.html#type-stripping) to learn more. + +When `synckit` detects the process to be running with this flag, it will execute the worker file with the `node` runner by default. + +#### `ts-node` (Default) + +Prior to Node v23, you may want to use `ts-node` to execute your worker file (a `.ts` file). If you want to use a custom tsconfig as project instead of default `tsconfig.json`, use `TS_NODE_PROJECT` env. Please view [ts-node](https://github.com/TypeStrong/ts-node#tsconfig) for more details. diff --git a/src/index.ts b/src/index.ts index 9b991f700..e0035c6cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,8 @@ const INT32_BYTES = 4 export * from './types.js' export const TsRunner = { + // https://nodejs.org/docs/latest/api/typescript.html#type-stripping + Node: 'node', // https://github.com/TypeStrong/ts-node TsNode: 'ts-node', // https://github.com/egoist/esbuild-register @@ -55,7 +57,23 @@ const { SYNCKIT_TS_RUNNER, } = process.env -const IS_NODE_20 = Number(process.versions.node.split('.')[0]) >= 20 +export const MTS_SUPPORTED_NODE_VERSION = 16 +export const LOADER_SUPPORTED_NODE_VERSION = 20 +export const STRIP_TYPES_DEFAULT_NODE_VERSION = 23 +export const STRIP_TYPES_SUPPORTED_NODE_VERSION = 22 + +const NODE_VERSION = Number.parseFloat(process.versions.node) +const STRIP_TYPES_FLAG = '--experimental-strip-types' +const NO_STRIP_TYPES_FLAG = '--no-experimental-strip-types' +const IS_TYPE_STRIPPING_ENABLED = + (NODE_VERSION >= STRIP_TYPES_DEFAULT_NODE_VERSION && + !( + NODE_OPTIONS?.includes(NO_STRIP_TYPES_FLAG) || + process.argv.includes(NO_STRIP_TYPES_FLAG) + )) || + (NODE_VERSION >= STRIP_TYPES_SUPPORTED_NODE_VERSION && + (NODE_OPTIONS?.includes(STRIP_TYPES_FLAG) || + process.argv.includes(STRIP_TYPES_FLAG))) export const DEFAULT_TIMEOUT = SYNCKIT_TIMEOUT ? +SYNCKIT_TIMEOUT : undefined @@ -80,8 +98,6 @@ export const DEFAULT_GLOBAL_SHIMS_PRESET: GlobalShim[] = [ }, ] -export const MTS_SUPPORTED_NODE_VERSION = 16 - let syncFnCache: Map | undefined export interface SynckitOptions { @@ -205,11 +221,27 @@ const setupTsRunner = ( } } - if (tsRunner == null && isPkgAvailable(TsRunner.TsNode)) { - tsRunner = TsRunner.TsNode + if (tsRunner == null) { + if (IS_TYPE_STRIPPING_ENABLED) { + tsRunner = TsRunner.Node + } else if (isPkgAvailable(TsRunner.TsNode)) { + tsRunner = TsRunner.TsNode + } } switch (tsRunner) { + case TsRunner.Node: { + if (NODE_VERSION < STRIP_TYPES_SUPPORTED_NODE_VERSION) { + throw new Error( + 'type stripping is not supported in this node version', + ) + } + execArgv = + NODE_VERSION >= STRIP_TYPES_DEFAULT_NODE_VERSION + ? execArgv.filter(arg => arg !== NO_STRIP_TYPES_FLAG) + : [STRIP_TYPES_FLAG, ...execArgv] + break + } case TsRunner.TsNode: { if (tsUseEsm) { if (!execArgv.includes('--loader')) { @@ -283,7 +315,7 @@ const setupTsRunner = ( // https://github.com/un-ts/synckit/issues/123 resolvedPnpLoaderPath = pathToFileURL(pnpLoaderPath).toString() - if (!IS_NODE_20) { + if (NODE_VERSION < LOADER_SUPPORTED_NODE_VERSION) { execArgv = [ '--experimental-loader', resolvedPnpLoaderPath, @@ -452,8 +484,7 @@ function startWorkerThread>( if (/\.[cm]ts$/.test(finalWorkerPath)) { const isTsxSupported = - !tsUseEsm || - Number.parseFloat(process.versions.node) >= MTS_SUPPORTED_NODE_VERSION + !tsUseEsm || NODE_VERSION >= MTS_SUPPORTED_NODE_VERSION /* istanbul ignore if */ if (!finalTsRunner) { throw new Error('No ts runner specified, ts worker path is not supported') @@ -604,7 +635,7 @@ export function runAsWorker< const { workerPort, sharedBuffer, pnpLoaderPath } = workerData as WorkerData - if (pnpLoaderPath && IS_NODE_20) { + if (pnpLoaderPath && NODE_VERSION >= LOADER_SUPPORTED_NODE_VERSION) { module.register(pnpLoaderPath) } diff --git a/test/ts-runner.spec.ts b/test/ts-runner.spec.ts index 813fe5b02..97de79771 100644 --- a/test/ts-runner.spec.ts +++ b/test/ts-runner.spec.ts @@ -7,7 +7,12 @@ import { jest } from '@jest/globals' import { _dirname, nodeVersion, tsUseEsmSupported } from './helpers.js' import type { AsyncWorkerFn } from './types.js' -import { MTS_SUPPORTED_NODE_VERSION, TsRunner } from 'synckit' +import { + MTS_SUPPORTED_NODE_VERSION, + STRIP_TYPES_DEFAULT_NODE_VERSION, + STRIP_TYPES_SUPPORTED_NODE_VERSION, + TsRunner, +} from 'synckit' beforeEach(() => { jest.resetModules() @@ -130,6 +135,44 @@ test(TsRunner.TSX, async () => { expect(syncFn(5)).toBe(5) }) +test(TsRunner.Node, async () => { + const { createSyncFn } = await import('synckit') + + if (nodeVersion < STRIP_TYPES_SUPPORTED_NODE_VERSION) { + // eslint-disable-next-line jest/no-conditional-expect + expect(() => + createSyncFn(workerMtsPath, { + tsRunner: TsRunner.Node, + }), + ).toThrow('type stripping is not supported in this node version') + return + } + + let syncFn = createSyncFn(workerJsPath, { + tsRunner: + nodeVersion >= STRIP_TYPES_DEFAULT_NODE_VERSION + ? undefined + : TsRunner.Node, + }) + expect(syncFn(1)).toBe(1) + expect(syncFn(2)).toBe(2) + expect(syncFn(5)).toBe(5) + + if (!tsUseEsmSupported) { + return + } + + syncFn = createSyncFn(workerMtsPath, { + tsRunner: + nodeVersion >= STRIP_TYPES_DEFAULT_NODE_VERSION + ? undefined + : TsRunner.Node, + }) + expect(syncFn(1)).toBe(1) + expect(syncFn(2)).toBe(2) + expect(syncFn(5)).toBe(5) +}) + test('unknown ts runner', async () => { const { createSyncFn } = await import('synckit')