Skip to content

Commit ffcf174

Browse files
authoredJul 16, 2022
feat: support more ts runners for TypeScript files (#90)
1 parent acc1235 commit ffcf174

18 files changed

+483
-187
lines changed
 

‎.changeset/calm-bees-swim.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"synckit": minor
3+
---
4+
5+
build!: drop Node 12 support, remove testing on Node 14

‎.changeset/kind-dolls-ring.md

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"synckit": minor
3+
---
4+
5+
feat: support more ts runners for TypeScript files
6+
7+
- https://github.com/TypeStrong/ts-node
8+
- https://github.com/egoist/esbuild-register
9+
- https://github.com/folke/esbuild-runner
10+
- https://github.com/esbuild-kit/tsx
11+
12+
Feel free to PR to add more runner support like [`swc`](https://github.com/swc-project/swc) if you want

‎.github/workflows/ci.yml

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ jobs:
1010
strategy:
1111
matrix:
1212
node:
13-
- 14
1413
- 16
1514
- 18
1615
os:

‎README.md

+19
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ Perform async work synchronously in Node.js using `worker_threads` with first-cl
2121
- [API](#api)
2222
- [Envs](#envs)
2323
- [TypeScript](#typescript)
24+
- [`ts-node`](#ts-node)
25+
- [`esbuild-register`](#esbuild-register)
26+
- [`esbuild-runner`](#esbuild-runner)
27+
- [`tsx`](#tsx)
2428
- [Benchmark](#benchmark)
2529
- [Sponsors](#sponsors)
2630
- [Backers](#backers)
@@ -69,15 +73,30 @@ You must make sure, the `result` is serializable by [`Structured Clone Algorithm
6973
1. `SYNCKIT_BUFFER_SIZE`: `bufferSize` to create `SharedArrayBuffer` for `worker_threads` (default as `1024`)
7074
2. `SYNCKIT_TIMEOUT`: `timeout` for performing the async job (no default)
7175
3. `SYNCKIT_EXEC_ARGV`: List of node CLI options passed to the worker, split with comma `,`. (default as `[]`), see also [`node` docs](https://nodejs.org/api/worker_threads.html)
76+
4. `SYNCKIT_TS_RUNNER`: Which TypeScript runner to be used, it could be very useful for development, could be `'ts-node' | 'esbuild-register' | 'esbuild-runner' | 'tsx'`, `'ts-node'` is used by default, make sure you have installed them already
7277

7378
### TypeScript
7479

80+
#### `ts-node`
81+
7582
If you want to use `ts-node` for worker file (a `.ts` file), it is supported out of box!
7683

7784
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.
7885

7986
If you want to integrate with [tsconfig-paths](https://www.npmjs.com/package/tsconfig-paths), please view [ts-node](https://github.com/TypeStrong/ts-node#paths-and-baseurl) for more details.
8087

88+
#### `esbuild-register`
89+
90+
Please view <https://github.com/egoist/esbuild-register> for its document
91+
92+
#### `esbuild-runner`
93+
94+
Please view <https://github.com/folke/esbuild-runner> for its document
95+
96+
#### `tsx`
97+
98+
Please view <https://github.com/esbuild-kit/tsx> for its document
99+
81100
## Benchmark
82101

83102
It is about 20x faster than [`sync-threads`](https://github.com/lambci/sync-threads) but 3x slower than native for reading the file content itself 1000 times during runtime, and 18x faster than `sync-threads` but 4x slower than native for total time.

‎package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"funding": "https://opencollective.com/unts",
4040
"license": "MIT",
4141
"engines": {
42-
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
42+
"node": "^14.18.0 || >=16.0.0"
4343
},
4444
"main": "./lib/index.cjs",
4545
"module": "./lib/index.js",
@@ -94,12 +94,15 @@
9494
"@types/node": "^18.0.4",
9595
"clean-publish": "^4.0.1",
9696
"deasync": "^0.1.27",
97+
"esbuild-register": "^3.3.3",
98+
"esbuild-runner": "^2.2.1",
9799
"jest": "^28.1.3",
98100
"patch-package": "^6.4.7",
99101
"sync-threads": "^1.0.1",
100102
"ts-expect": "^1.3.0",
101103
"ts-jest": "^28.0.6",
102104
"ts-node": "^10.9.1",
105+
"tsx": "^3.8.0",
103106
"type-coverage": "^2.22.0",
104107
"typescript": "^4.7.4",
105108
"yarn-deduplicate": "^5.0.0"

‎src/index.ts

+119-39
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,32 @@ import {
1717
AnyFn,
1818
MainToWorkerMessage,
1919
Syncify,
20+
ValueOf,
2021
WorkerData,
2122
WorkerToMainMessage,
2223
} from './types.js'
2324

2425
export * from './types.js'
2526

26-
const { SYNCKIT_BUFFER_SIZE, SYNCKIT_TIMEOUT, SYNCKIT_EXEC_ARV } = process.env
27+
export const TsRunner = {
28+
// https://github.com/TypeStrong/ts-node
29+
TsNode: 'ts-node',
30+
// https://github.com/egoist/esbuild-register
31+
EsbuildRegister: 'esbuild-register',
32+
// https://github.com/folke/esbuild-runner
33+
EsbuildRunner: 'esbuild-runner',
34+
// https://github.com/esbuild-kit/tsx
35+
TSX: 'tsx',
36+
} as const
37+
38+
export type TsRunner = ValueOf<typeof TsRunner>
39+
40+
const {
41+
SYNCKIT_BUFFER_SIZE,
42+
SYNCKIT_TIMEOUT,
43+
SYNCKIT_EXEC_ARV,
44+
SYNCKIT_TS_RUNNER,
45+
} = process.env
2746

2847
export const DEFAULT_BUFFER_SIZE = SYNCKIT_BUFFER_SIZE
2948
? +SYNCKIT_BUFFER_SIZE
@@ -34,14 +53,18 @@ export const DEFAULT_TIMEOUT = SYNCKIT_TIMEOUT ? +SYNCKIT_TIMEOUT : undefined
3453
export const DEFAULT_WORKER_BUFFER_SIZE = DEFAULT_BUFFER_SIZE || 1024
3554

3655
/* istanbul ignore next */
37-
export const DEFAULT_EXEC_ARGV = SYNCKIT_EXEC_ARV?.split(',') ?? []
56+
export const DEFAULT_EXEC_ARGV = SYNCKIT_EXEC_ARV?.split(',') || []
57+
58+
export const DEFAULT_TS_RUNNER = (SYNCKIT_TS_RUNNER ||
59+
TsRunner.TsNode) as TsRunner
3860

3961
const syncFnCache = new Map<string, AnyFn>()
4062

4163
export interface SynckitOptions {
4264
bufferSize?: number
4365
timeout?: number
4466
execArgv?: string[]
67+
tsRunner?: TsRunner
4568
}
4669

4770
// MessagePort doesn't copy the properties of Error objects. We still want
@@ -101,30 +124,34 @@ const cjsRequire =
101124
const dataUrl = (code: string) =>
102125
new URL(`data:text/javascript,${encodeURIComponent(code)}`)
103126

104-
// eslint-disable-next-line sonarjs/cognitive-complexity
105-
const setupTsNode = (workerPath: string, execArgv: string[]) => {
106-
if (!/[/\\]node_modules[/\\]/.test(workerPath)) {
107-
const ext = path.extname(workerPath)
108-
if (!ext || /\.[cm]?js$/.test(ext)) {
109-
const workPathWithoutExt = ext
110-
? workerPath.slice(0, -ext.length)
111-
: workerPath
112-
let extensions: string[]
113-
switch (ext) {
114-
case '.cjs':
115-
extensions = ['cts', 'cjs']
116-
break
117-
case '.mjs':
118-
extensions = ['mts', 'mjs']
119-
break
120-
default:
121-
extensions = ['.ts', '.js']
122-
break
123-
}
124-
const found = tryExtensions(workPathWithoutExt, extensions)
125-
if (found && (!ext || found !== workPathWithoutExt)) {
126-
workerPath = found
127-
}
127+
const setupTsRunner = (
128+
workerPath: string,
129+
{ execArgv, tsRunner }: { execArgv: string[]; tsRunner: TsRunner }, // eslint-disable-next-line sonarjs/cognitive-complexity
130+
) => {
131+
const ext = path.extname(workerPath)
132+
133+
if (
134+
!/[/\\]node_modules[/\\]/.test(workerPath) &&
135+
(!ext || /^\.[cm]?js$/.test(ext))
136+
) {
137+
const workPathWithoutExt = ext
138+
? workerPath.slice(0, -ext.length)
139+
: workerPath
140+
let extensions: string[]
141+
switch (ext) {
142+
case '.cjs':
143+
extensions = ['.cts', '.cjs']
144+
break
145+
case '.mjs':
146+
extensions = ['.mts', '.mjs']
147+
break
148+
default:
149+
extensions = ['.ts', '.js']
150+
break
151+
}
152+
const found = tryExtensions(workPathWithoutExt, extensions)
153+
if (found && (!ext || found !== workPathWithoutExt)) {
154+
workerPath = found
128155
}
129156
}
130157

@@ -141,12 +168,43 @@ const setupTsNode = (workerPath: string, execArgv: string[]) => {
141168
'module'
142169
}
143170
}
144-
if (tsUseEsm && !execArgv.includes('--loader')) {
145-
execArgv = ['--loader', 'ts-node/esm', ...execArgv]
171+
switch (tsRunner) {
172+
case TsRunner.TsNode: {
173+
if (tsUseEsm) {
174+
if (!execArgv.includes('--loader')) {
175+
execArgv = ['--loader', `${TsRunner.TsNode}/esm`, ...execArgv]
176+
}
177+
} else if (!execArgv.includes('-r')) {
178+
execArgv = ['-r', `${TsRunner.TsNode}/register`, ...execArgv]
179+
}
180+
break
181+
}
182+
case TsRunner.EsbuildRegister: {
183+
if (!execArgv.includes('-r')) {
184+
execArgv = ['-r', TsRunner.EsbuildRegister, ...execArgv]
185+
}
186+
break
187+
}
188+
case TsRunner.EsbuildRunner: {
189+
if (!execArgv.includes('-r')) {
190+
execArgv = ['-r', `${TsRunner.EsbuildRunner}/register`, ...execArgv]
191+
}
192+
break
193+
}
194+
case TsRunner.TSX: {
195+
if (!execArgv.includes('--loader')) {
196+
execArgv = ['--loader', TsRunner.TSX, ...execArgv]
197+
}
198+
break
199+
}
200+
default: {
201+
throw new Error(`Unknown ts runner: ${String(tsRunner)}`)
202+
}
146203
}
147204
}
148205

149206
return {
207+
ext,
150208
isTs,
151209
tsUseEsm,
152210
workerPath,
@@ -160,28 +218,50 @@ function startWorkerThread<R, T extends AnyAsyncFn<R>>(
160218
bufferSize = DEFAULT_WORKER_BUFFER_SIZE,
161219
timeout = DEFAULT_TIMEOUT,
162220
execArgv = DEFAULT_EXEC_ARGV,
221+
tsRunner = DEFAULT_TS_RUNNER,
163222
}: SynckitOptions = {},
164223
) {
165224
const { port1: mainPort, port2: workerPort } = new MessageChannel()
166225

167226
const {
168-
isTs,
227+
ext,
169228
tsUseEsm,
170229
workerPath: finalWorkerPath,
171230
execArgv: finalExecArgv,
172-
} = setupTsNode(workerPath, execArgv)
231+
} = setupTsRunner(workerPath, { execArgv, tsRunner })
232+
233+
const workerPathUrl = pathToFileURL(finalWorkerPath)
234+
235+
if (
236+
/\.[cm]ts$/.test(finalWorkerPath) &&
237+
(
238+
[
239+
// https://github.com/egoist/esbuild-register/issues/79
240+
TsRunner.EsbuildRegister,
241+
// https://github.com/folke/esbuild-runner/issues/67
242+
TsRunner.EsbuildRunner,
243+
] as TsRunner[]
244+
).includes(tsRunner)
245+
) {
246+
throw new Error(
247+
`${tsRunner} is not supported for ${ext} files yet, you can try [tsx](https://github.com/esbuild-kit/tsx) instead`,
248+
)
249+
}
173250

174251
const worker = new Worker(
175-
isTs
176-
? tsUseEsm
177-
? dataUrl(`import '${String(pathToFileURL(finalWorkerPath))}'`)
178-
: `require('ts-node/register');require('${finalWorkerPath.replace(
179-
/\\/g,
180-
'\\\\',
181-
)}')`
182-
: pathToFileURL(finalWorkerPath),
252+
tsUseEsm &&
253+
(
254+
[
255+
TsRunner.TsNode,
256+
// https://github.com/egoist/esbuild-register/issues/79
257+
// TsRunner.EsbuildRegister,
258+
// https://github.com/folke/esbuild-runner/issues/67
259+
// TsRunner.EsbuildRunner
260+
] as TsRunner[]
261+
).includes(tsRunner)
262+
? dataUrl(`import '${String(workerPathUrl)}'`)
263+
: workerPathUrl,
183264
{
184-
eval: isTs && !tsUseEsm,
185265
workerData: { workerPort },
186266
transferList: [workerPort],
187267
execArgv: finalExecArgv,

‎src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export type PromiseType<T extends AnyPromise> = T extends Promise<infer R>
1919
? R
2020
: never
2121

22+
export type ValueOf<T> = T[keyof T]
23+
2224
export interface MainToWorkerMessage<T extends unknown[]> {
2325
sharedBuffer: SharedArrayBuffer
2426
id: number
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { runAsWorker } from 'synckit'
2+
3+
runAsWorker(
4+
(result: number, timeout: number) =>
5+
new Promise<number>(resolve => setTimeout(() => resolve(result), timeout)),
6+
)

‎test/esbuild-register.worker.mjs

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { runAsWorker } from 'synckit'
2+
3+
runAsWorker(
4+
(result, timeout) =>
5+
new Promise(resolve => setTimeout(() => resolve(result), timeout)),
6+
)

‎test/esbuild-runner-error.worker.mts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { runAsWorker } from 'synckit'
2+
3+
runAsWorker(
4+
(result: number, timeout: number) =>
5+
new Promise<number>(resolve => setTimeout(() => resolve(result), timeout)),
6+
)

‎test/esbuild-runner.worker.mjs

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { runAsWorker } from 'synckit'
2+
3+
runAsWorker(
4+
(result, timeout) =>
5+
new Promise(resolve => setTimeout(() => resolve(result), timeout)),
6+
)

‎test/fn.spec.ts

+10-15
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { createRequire } from 'node:module'
22
import path from 'node:path'
3-
import { fileURLToPath } from 'node:url'
43

54
import { jest } from '@jest/globals'
65

7-
import { createSyncFn, extractProperties } from 'synckit'
6+
import { _dirname } from './helpers.js'
7+
import type { AsyncWorkerFn } from './types.js'
88

9-
type AsyncWorkerFn<T = number> = (result: T, timeout?: number) => Promise<T>
9+
import { createSyncFn, extractProperties } from 'synckit'
1010

1111
beforeEach(() => {
1212
jest.resetModules()
@@ -17,18 +17,13 @@ beforeEach(() => {
1717

1818
const cjsRequire = createRequire(import.meta.url)
1919

20-
const _dirname =
21-
typeof __dirname === 'undefined'
22-
? path.dirname(fileURLToPath(import.meta.url))
23-
: __dirname
24-
25-
const workerCjsTsPath = cjsRequire.resolve('./cjs/worker-cjs.ts')
26-
const workerEsmTsPath = cjsRequire.resolve('./esm/worker-esm.ts')
27-
const workerNoExtAsJsPath = path.resolve(_dirname, './worker-js')
28-
const workerJsAsTsPath = path.resolve(_dirname, './worker.js')
29-
const workerCjsPath = cjsRequire.resolve('./worker.cjs')
30-
const workerMjsPath = cjsRequire.resolve('./worker.mjs')
31-
const workerErrorPath = cjsRequire.resolve('./worker-error.cjs')
20+
const workerCjsTsPath = path.resolve(_dirname, 'cjs/worker-cjs.ts')
21+
const workerEsmTsPath = path.resolve(_dirname, 'esm/worker-esm.ts')
22+
const workerNoExtAsJsPath = path.resolve(_dirname, 'worker-js')
23+
const workerJsAsTsPath = path.resolve(_dirname, 'worker.js')
24+
const workerCjsPath = path.resolve(_dirname, 'worker.cjs')
25+
const workerMjsPath = path.resolve(_dirname, 'worker.mjs')
26+
const workerErrorPath = path.resolve(_dirname, 'worker-error.cjs')
3227

3328
test('ts as cjs', () => {
3429
const syncFn = createSyncFn<AsyncWorkerFn>(workerCjsTsPath)

‎test/helpers.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import path from 'node:path'
2+
import { fileURLToPath } from 'node:url'
3+
4+
export const _dirname = path.dirname(fileURLToPath(import.meta.url))

‎test/ts-runner.spec.ts

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/* eslint-disable jest/valid-title */
2+
3+
import path from 'node:path'
4+
5+
import { jest } from '@jest/globals'
6+
7+
import { _dirname } from './helpers.js'
8+
import type { AsyncWorkerFn } from './types.js'
9+
10+
import { TsRunner } from 'synckit'
11+
12+
beforeEach(() => {
13+
jest.resetModules()
14+
})
15+
16+
it(TsRunner.EsbuildRegister, async () => {
17+
const { createSyncFn } = await import('synckit')
18+
const syncFn = createSyncFn<AsyncWorkerFn>(
19+
path.resolve(_dirname, 'esbuild-register.worker.mjs'),
20+
{
21+
tsRunner: TsRunner.EsbuildRegister,
22+
},
23+
)
24+
expect(syncFn(1)).toBe(1)
25+
expect(syncFn(2)).toBe(2)
26+
expect(syncFn(5)).toBe(5)
27+
28+
expect(() =>
29+
createSyncFn<AsyncWorkerFn>(
30+
path.resolve(_dirname, 'esbuild-register-error.worker.mts'),
31+
{
32+
tsRunner: TsRunner.EsbuildRegister,
33+
},
34+
),
35+
).toThrowErrorMatchingInlineSnapshot(
36+
`"esbuild-register is not supported for .mts files yet, you can try [tsx](https://github.com/esbuild-kit/tsx) instead"`,
37+
)
38+
})
39+
40+
it(TsRunner.EsbuildRunner, async () => {
41+
const { createSyncFn } = await import('synckit')
42+
const syncFn = createSyncFn<AsyncWorkerFn>(
43+
path.resolve(_dirname, 'esbuild-runner.worker.mjs'),
44+
{
45+
tsRunner: TsRunner.EsbuildRunner,
46+
},
47+
)
48+
expect(syncFn(1)).toBe(1)
49+
expect(syncFn(2)).toBe(2)
50+
expect(syncFn(5)).toBe(5)
51+
52+
expect(() =>
53+
createSyncFn<AsyncWorkerFn>(
54+
path.resolve(_dirname, 'esbuild-runner-error.worker.mts'),
55+
{
56+
tsRunner: TsRunner.EsbuildRunner,
57+
},
58+
),
59+
).toThrowErrorMatchingInlineSnapshot(
60+
`"esbuild-runner is not supported for .mts files yet, you can try [tsx](https://github.com/esbuild-kit/tsx) instead"`,
61+
)
62+
})
63+
64+
it(TsRunner.TSX, async () => {
65+
const { createSyncFn } = await import('synckit')
66+
const syncFn = createSyncFn<AsyncWorkerFn>(
67+
path.resolve(_dirname, 'tsx.worker.mjs'),
68+
{
69+
tsRunner: TsRunner.TSX,
70+
},
71+
)
72+
expect(syncFn(1)).toBe(1)
73+
expect(syncFn(2)).toBe(2)
74+
expect(syncFn(5)).toBe(5)
75+
})
76+
77+
it('unknown ts runner', async () => {
78+
const { createSyncFn } = await import('synckit')
79+
80+
expect(() =>
81+
// @ts-expect-error
82+
createSyncFn<AsyncWorkerFn>(path.resolve(_dirname, 'worker.ts'), {
83+
tsRunner: 'unknown',
84+
}),
85+
).toThrowErrorMatchingInlineSnapshot(`"Unknown ts runner: unknown"`)
86+
})

‎test/tsx.worker.mts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { runAsWorker } from 'synckit'
2+
3+
runAsWorker(
4+
(result: number, timeout: number) =>
5+
new Promise<number>(resolve => setTimeout(() => resolve(result), timeout)),
6+
)

‎test/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type AsyncWorkerFn<T = number> = (
2+
result: T,
3+
timeout?: number,
4+
) => Promise<T>

‎tsconfig.json

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
{
22
"extends": "@1stg/tsconfig/lib",
33
"compilerOptions": {
4-
"baseUrl": ".",
54
"paths": {
6-
"synckit": ["src"]
5+
"synckit": ["./src"]
76
}
8-
},
9-
"ts-node": {
10-
"require": ["tsconfig-paths/register"]
117
}
128
}

‎yarn.lock

+187-126
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.