Skip to content

Commit fd85ccd

Browse files
authoredApr 5, 2022
feat: support js as ts, ts as esm, etc (#78)
1 parent 4f49781 commit fd85ccd

15 files changed

+219
-102
lines changed
 

‎.changeset/curly-falcons-deny.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"synckit": minor
3+
---
4+
5+
feat: support js as ts, ts as esm, etc

‎.env

-1
This file was deleted.

‎package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"build": "run-p build:*",
4242
"build:r": "r -f cjs",
4343
"build:ts": "tsc -p src",
44-
"jest": "node --experimental-vm-modules node_modules/.bin/jest --setupFiles dotenv/config",
44+
"jest": "node --experimental-vm-modules node_modules/.bin/jest",
4545
"lint": "run-p lint:*",
4646
"lint:es": "eslint . --cache -f friendly --max-warnings 10",
4747
"lint:tsc": "tsc --noEmit",
@@ -53,10 +53,11 @@
5353
"typecov": "type-coverage"
5454
},
5555
"dependencies": {
56+
"@pkgr/utils": "^2.0.3",
5657
"tslib": "^2.3.1"
5758
},
5859
"devDependencies": {
59-
"@1stg/lib-config": "^5.3.0",
60+
"@1stg/lib-config": "^5.5.0",
6061
"@changesets/changelog-github": "^0.4.4",
6162
"@changesets/cli": "^2.22.0",
6263
"@types/jest": "^27.4.1",
@@ -72,7 +73,7 @@
7273
"typescript": "^4.6.3"
7374
},
7475
"resolutions": {
75-
"prettier": "^2.6.1",
76+
"prettier": "^2.6.2",
7677
"tslib": "^2.3.1"
7778
},
7879
"commitlint": {
@@ -105,7 +106,7 @@
105106
]
106107
},
107108
"typeCoverage": {
108-
"atLeast": 99.67,
109+
"atLeast": 99.73,
109110
"cache": true,
110111
"detail": true,
111112
"ignoreAsAssertion": true,

‎src/index.ts

+70-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { createRequire } from 'module'
12
import path from 'path'
3+
import { pathToFileURL } from 'url'
24
import {
35
MessageChannel,
46
Worker,
@@ -7,6 +9,8 @@ import {
79
parentPort,
810
} from 'worker_threads'
911

12+
import { findUp, tryExtensions } from '@pkgr/utils'
13+
1014
import {
1115
AnyAsyncFn,
1216
AnyFn,
@@ -18,14 +22,7 @@ import {
1822

1923
export * from './types.js'
2024

21-
const {
22-
SYNCKIT_BUFFER_SIZE,
23-
SYNCKIT_TIMEOUT,
24-
SYNCKIT_TS_ESM,
25-
SYNCKIT_EXEC_ARV,
26-
} = process.env
27-
28-
const TS_USE_ESM = !!SYNCKIT_TS_ESM && ['1', 'true'].includes(SYNCKIT_TS_ESM)
25+
const { SYNCKIT_BUFFER_SIZE, SYNCKIT_TIMEOUT, SYNCKIT_EXEC_ARV } = process.env
2926

3027
export const DEFAULT_BUFFER_SIZE = SYNCKIT_BUFFER_SIZE
3128
? +SYNCKIT_BUFFER_SIZE
@@ -35,6 +32,7 @@ export const DEFAULT_TIMEOUT = SYNCKIT_TIMEOUT ? +SYNCKIT_TIMEOUT : undefined
3532

3633
export const DEFAULT_WORKER_BUFFER_SIZE = DEFAULT_BUFFER_SIZE || 1024
3734

35+
/* istanbul ignore next */
3836
export const DEFAULT_EXEC_ARGV = SYNCKIT_EXEC_ARV?.split(',') ?? []
3937

4038
const syncFnCache = new Map<string, AnyFn>()
@@ -50,7 +48,7 @@ export interface SynckitOptions {
5048
// property copying manually.
5149
export const extractProperties = <T>(object?: T): T | undefined => {
5250
if (object && typeof object === 'object') {
53-
const properties = {} as T
51+
const properties = {} as unknown as T
5452
for (const key in object) {
5553
properties[key as keyof T] = object[key]
5654
}
@@ -84,7 +82,7 @@ export function createSyncFn<R, T extends AnyAsyncFn<R>>(
8482

8583
const syncFn = startWorkerThread<R, T>(
8684
workerPath,
87-
typeof bufferSizeOrOptions === 'number'
85+
/* istanbul ignore next */ typeof bufferSizeOrOptions === 'number'
8886
? { bufferSize: bufferSizeOrOptions, timeout }
8987
: bufferSizeOrOptions,
9088
)
@@ -94,8 +92,55 @@ export function createSyncFn<R, T extends AnyAsyncFn<R>>(
9492
return syncFn
9593
}
9694

97-
const throwError = (msg: string) => {
98-
throw new Error(msg)
95+
const cjsRequire =
96+
typeof require === 'undefined'
97+
? createRequire(import.meta.url)
98+
: /* istanbul ignore next */ require
99+
100+
const dataUrl = (code: string) =>
101+
new URL(`data:text/javascript,${encodeURIComponent(code)}`)
102+
103+
// eslint-disable-next-line sonarjs/cognitive-complexity
104+
const setupTsNode = (workerPath: string, execArgv: string[]) => {
105+
if (!/[/\\]node_modules[/\\]/.test(workerPath)) {
106+
const ext = path.extname(workerPath)
107+
// TODO: support `.cts` and `.mts` automatically
108+
if (!ext || ext === '.js') {
109+
const found = tryExtensions(
110+
ext ? workerPath.replace(/\.js$/, '') : workerPath,
111+
['.ts', '.js'],
112+
)
113+
if (found) {
114+
workerPath = found
115+
}
116+
}
117+
}
118+
119+
const isTs = /\.[cm]?ts$/.test(workerPath)
120+
121+
// TODO: it does not work for `ts-node` for now
122+
let tsUseEsm = workerPath.endsWith('.mts')
123+
124+
if (isTs) {
125+
if (!tsUseEsm) {
126+
const pkg = findUp(workerPath)
127+
if (pkg) {
128+
tsUseEsm =
129+
(cjsRequire(pkg) as { type?: 'commonjs' | 'module' }).type ===
130+
'module'
131+
}
132+
}
133+
if (tsUseEsm && !execArgv.includes('--loader')) {
134+
execArgv = ['--loader', 'ts-node/esm', ...execArgv]
135+
}
136+
}
137+
138+
return {
139+
isTs,
140+
tsUseEsm,
141+
workerPath,
142+
execArgv,
143+
}
99144
}
100145

101146
function startWorkerThread<R, T extends AnyAsyncFn<R>>(
@@ -108,21 +153,24 @@ function startWorkerThread<R, T extends AnyAsyncFn<R>>(
108153
) {
109154
const { port1: mainPort, port2: workerPort } = new MessageChannel()
110155

111-
const isTs = workerPath.endsWith('.ts')
156+
const {
157+
isTs,
158+
tsUseEsm,
159+
workerPath: finalWorkerPath,
160+
execArgv: finalExecArgv,
161+
} = setupTsNode(workerPath, execArgv)
112162

113163
const worker = new Worker(
114164
isTs
115-
? TS_USE_ESM
116-
? throwError(
117-
'Native esm in `.ts` file is not supported yet, please use `.cjs` instead',
118-
)
119-
: `require('ts-node/register');require('${workerPath}')`
120-
: workerPath,
165+
? tsUseEsm
166+
? dataUrl(`import '${String(pathToFileURL(finalWorkerPath))}'`)
167+
: `require('ts-node/register');require('${finalWorkerPath}')`
168+
: finalWorkerPath,
121169
{
122-
eval: isTs,
170+
eval: isTs && !tsUseEsm,
123171
workerData: { workerPort },
124172
transferList: [workerPort],
125-
execArgv,
173+
execArgv: finalExecArgv,
126174
},
127175
)
128176

@@ -158,7 +206,7 @@ function startWorkerThread<R, T extends AnyAsyncFn<R>>(
158206
}
159207

160208
if (error) {
161-
throw Object.assign(error, properties)
209+
throw Object.assign(error as object, properties)
162210
}
163211

164212
return result!

‎test/cjs/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "cjs-test"
3+
}

‎test/cjs/worker-cjs.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// we're not using `synckit` here because jest can not handle cjs+mjs dual package correctly
2+
const { runAsWorker } =
3+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
4+
require('../../lib/index.cjs') as typeof import('synckit')
5+
6+
runAsWorker(
7+
<T>(result: T, timeout?: number) =>
8+
new Promise<T>(resolve => setTimeout(() => resolve(result), timeout)),
9+
)

‎test/esm/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "esm-test",
3+
"type": "module"
4+
}
File renamed without changes.

‎test/fn.spec.ts

+39-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { createRequire } from 'module'
2+
import path from 'path'
3+
import { fileURLToPath } from 'url'
24

35
import { jest } from '@jest/globals'
46

@@ -11,17 +13,51 @@ beforeEach(() => {
1113

1214
delete process.env.SYNCKIT_BUFFER_SIZE
1315
delete process.env.SYNCKIT_TIMEOUT
14-
15-
process.env.SYNCKIT_TS_ESM = '1'
1616
})
1717

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

20-
const workerEsmTsPath = cjsRequire.resolve('./worker-esm.ts')
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')
2129
const workerCjsPath = cjsRequire.resolve('./worker.cjs')
2230
const workerMjsPath = cjsRequire.resolve('./worker.mjs')
2331
const workerErrorPath = cjsRequire.resolve('./worker-error.cjs')
2432

33+
test('ts as cjs', () => {
34+
const syncFn = createSyncFn<AsyncWorkerFn>(workerCjsTsPath)
35+
expect(syncFn(1)).toBe(1)
36+
expect(syncFn(2)).toBe(2)
37+
expect(syncFn(5)).toBe(5)
38+
})
39+
40+
test('ts as esm', () => {
41+
const syncFn = createSyncFn<AsyncWorkerFn>(workerEsmTsPath)
42+
expect(syncFn(1)).toBe(1)
43+
expect(syncFn(2)).toBe(2)
44+
expect(syncFn(5)).toBe(5)
45+
})
46+
47+
test('no ext as js (as esm)', () => {
48+
const syncFn = createSyncFn<AsyncWorkerFn>(workerNoExtAsJsPath)
49+
expect(syncFn(1)).toBe(1)
50+
expect(syncFn(2)).toBe(2)
51+
expect(syncFn(5)).toBe(5)
52+
})
53+
54+
test('js as ts (as esm)', () => {
55+
const syncFn = createSyncFn<AsyncWorkerFn>(workerJsAsTsPath)
56+
expect(syncFn(1)).toBe(1)
57+
expect(syncFn(2)).toBe(2)
58+
expect(syncFn(5)).toBe(5)
59+
})
60+
2561
test('createSyncFn', () => {
2662
expect(() => createSyncFn('./fake')).toThrow('`workerPath` must be absolute')
2763
expect(() => createSyncFn(cjsRequire.resolve('eslint'))).not.toThrow()
@@ -30,10 +66,6 @@ test('createSyncFn', () => {
3066
const syncFn2 = createSyncFn<AsyncWorkerFn>(workerCjsPath)
3167
const syncFn3 = createSyncFn<AsyncWorkerFn>(workerMjsPath)
3268

33-
expect(() => createSyncFn(workerEsmTsPath)).toThrow(
34-
'Native esm in `.ts` file is not supported yet, please use `.cjs` instead',
35-
)
36-
3769
const errSyncFn = createSyncFn<() => Promise<void>>(workerErrorPath)
3870

3971
expect(syncFn1).toBe(syncFn2)
@@ -58,7 +90,6 @@ test('createSyncFn', () => {
5890
test('timeout', async () => {
5991
process.env.SYNCKIT_BUFFER_SIZE = '0'
6092
process.env.SYNCKIT_TIMEOUT = '1'
61-
process.env.SYNCKIT_TS_ESM = '0'
6293

6394
const { createSyncFn } = await import('synckit')
6495
const syncFn = createSyncFn<AsyncWorkerFn>(workerCjsPath)

‎test/worker-error.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
const { runAsWorker } = require('synckit')
1+
const { runAsWorker } = require('../lib/index.cjs')
22

33
runAsWorker(() => Promise.reject(new Error('Worker Error')))

‎test/worker-js.js

+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/worker.cjs

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// we're not using `synckit` here because jest can not handle cjs+mjs dual package correctly
12
const { runAsWorker } = require('../lib/index.cjs')
23

34
runAsWorker(

‎test/worker.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { runAsWorker } from '../lib/index.js'
1+
import { runAsWorker } from 'synckit'
22

33
runAsWorker(
44
(result, timeout) =>

‎test/worker.ts

+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+
)

0 commit comments

Comments
 (0)
Please sign in to comment.