Skip to content

Commit

Permalink
feat: support js as ts, ts as esm, etc (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
JounQin committed Apr 5, 2022
1 parent 4f49781 commit fd85ccd
Show file tree
Hide file tree
Showing 15 changed files with 219 additions and 102 deletions.
5 changes: 5 additions & 0 deletions .changeset/curly-falcons-deny.md
@@ -0,0 +1,5 @@
---
"synckit": minor
---

feat: support js as ts, ts as esm, etc
1 change: 0 additions & 1 deletion .env

This file was deleted.

9 changes: 5 additions & 4 deletions package.json
Expand Up @@ -41,7 +41,7 @@
"build": "run-p build:*",
"build:r": "r -f cjs",
"build:ts": "tsc -p src",
"jest": "node --experimental-vm-modules node_modules/.bin/jest --setupFiles dotenv/config",
"jest": "node --experimental-vm-modules node_modules/.bin/jest",
"lint": "run-p lint:*",
"lint:es": "eslint . --cache -f friendly --max-warnings 10",
"lint:tsc": "tsc --noEmit",
Expand All @@ -53,10 +53,11 @@
"typecov": "type-coverage"
},
"dependencies": {
"@pkgr/utils": "^2.0.3",
"tslib": "^2.3.1"
},
"devDependencies": {
"@1stg/lib-config": "^5.3.0",
"@1stg/lib-config": "^5.5.0",
"@changesets/changelog-github": "^0.4.4",
"@changesets/cli": "^2.22.0",
"@types/jest": "^27.4.1",
Expand All @@ -72,7 +73,7 @@
"typescript": "^4.6.3"
},
"resolutions": {
"prettier": "^2.6.1",
"prettier": "^2.6.2",
"tslib": "^2.3.1"
},
"commitlint": {
Expand Down Expand Up @@ -105,7 +106,7 @@
]
},
"typeCoverage": {
"atLeast": 99.67,
"atLeast": 99.73,
"cache": true,
"detail": true,
"ignoreAsAssertion": true,
Expand Down
92 changes: 70 additions & 22 deletions src/index.ts
@@ -1,4 +1,6 @@
import { createRequire } from 'module'
import path from 'path'
import { pathToFileURL } from 'url'
import {
MessageChannel,
Worker,
Expand All @@ -7,6 +9,8 @@ import {
parentPort,
} from 'worker_threads'

import { findUp, tryExtensions } from '@pkgr/utils'

import {
AnyAsyncFn,
AnyFn,
Expand All @@ -18,14 +22,7 @@ import {

export * from './types.js'

const {
SYNCKIT_BUFFER_SIZE,
SYNCKIT_TIMEOUT,
SYNCKIT_TS_ESM,
SYNCKIT_EXEC_ARV,
} = process.env

const TS_USE_ESM = !!SYNCKIT_TS_ESM && ['1', 'true'].includes(SYNCKIT_TS_ESM)
const { SYNCKIT_BUFFER_SIZE, SYNCKIT_TIMEOUT, SYNCKIT_EXEC_ARV } = process.env

export const DEFAULT_BUFFER_SIZE = SYNCKIT_BUFFER_SIZE
? +SYNCKIT_BUFFER_SIZE
Expand All @@ -35,6 +32,7 @@ export const DEFAULT_TIMEOUT = SYNCKIT_TIMEOUT ? +SYNCKIT_TIMEOUT : undefined

export const DEFAULT_WORKER_BUFFER_SIZE = DEFAULT_BUFFER_SIZE || 1024

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

const syncFnCache = new Map<string, AnyFn>()
Expand All @@ -50,7 +48,7 @@ export interface SynckitOptions {
// property copying manually.
export const extractProperties = <T>(object?: T): T | undefined => {
if (object && typeof object === 'object') {
const properties = {} as T
const properties = {} as unknown as T
for (const key in object) {
properties[key as keyof T] = object[key]
}
Expand Down Expand Up @@ -84,7 +82,7 @@ export function createSyncFn<R, T extends AnyAsyncFn<R>>(

const syncFn = startWorkerThread<R, T>(
workerPath,
typeof bufferSizeOrOptions === 'number'
/* istanbul ignore next */ typeof bufferSizeOrOptions === 'number'
? { bufferSize: bufferSizeOrOptions, timeout }
: bufferSizeOrOptions,
)
Expand All @@ -94,8 +92,55 @@ export function createSyncFn<R, T extends AnyAsyncFn<R>>(
return syncFn
}

const throwError = (msg: string) => {
throw new Error(msg)
const cjsRequire =
typeof require === 'undefined'
? createRequire(import.meta.url)
: /* istanbul ignore next */ require

const dataUrl = (code: string) =>
new URL(`data:text/javascript,${encodeURIComponent(code)}`)

// eslint-disable-next-line sonarjs/cognitive-complexity
const setupTsNode = (workerPath: string, execArgv: string[]) => {
if (!/[/\\]node_modules[/\\]/.test(workerPath)) {
const ext = path.extname(workerPath)
// TODO: support `.cts` and `.mts` automatically
if (!ext || ext === '.js') {
const found = tryExtensions(
ext ? workerPath.replace(/\.js$/, '') : workerPath,
['.ts', '.js'],
)
if (found) {
workerPath = found
}
}
}

const isTs = /\.[cm]?ts$/.test(workerPath)

// TODO: it does not work for `ts-node` for now
let tsUseEsm = workerPath.endsWith('.mts')

if (isTs) {
if (!tsUseEsm) {
const pkg = findUp(workerPath)
if (pkg) {
tsUseEsm =
(cjsRequire(pkg) as { type?: 'commonjs' | 'module' }).type ===
'module'
}
}
if (tsUseEsm && !execArgv.includes('--loader')) {
execArgv = ['--loader', 'ts-node/esm', ...execArgv]
}
}

return {
isTs,
tsUseEsm,
workerPath,
execArgv,
}
}

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

const isTs = workerPath.endsWith('.ts')
const {
isTs,
tsUseEsm,
workerPath: finalWorkerPath,
execArgv: finalExecArgv,
} = setupTsNode(workerPath, execArgv)

const worker = new Worker(
isTs
? TS_USE_ESM
? throwError(
'Native esm in `.ts` file is not supported yet, please use `.cjs` instead',
)
: `require('ts-node/register');require('${workerPath}')`
: workerPath,
? tsUseEsm
? dataUrl(`import '${String(pathToFileURL(finalWorkerPath))}'`)
: `require('ts-node/register');require('${finalWorkerPath}')`
: finalWorkerPath,
{
eval: isTs,
eval: isTs && !tsUseEsm,
workerData: { workerPort },
transferList: [workerPort],
execArgv,
execArgv: finalExecArgv,
},
)

Expand Down Expand Up @@ -158,7 +206,7 @@ function startWorkerThread<R, T extends AnyAsyncFn<R>>(
}

if (error) {
throw Object.assign(error, properties)
throw Object.assign(error as object, properties)
}

return result!
Expand Down
3 changes: 3 additions & 0 deletions test/cjs/package.json
@@ -0,0 +1,3 @@
{
"name": "cjs-test"
}
9 changes: 9 additions & 0 deletions test/cjs/worker-cjs.ts
@@ -0,0 +1,9 @@
// we're not using `synckit` here because jest can not handle cjs+mjs dual package correctly
const { runAsWorker } =
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
require('../../lib/index.cjs') as typeof import('synckit')

runAsWorker(
<T>(result: T, timeout?: number) =>
new Promise<T>(resolve => setTimeout(() => resolve(result), timeout)),
)
4 changes: 4 additions & 0 deletions test/esm/package.json
@@ -0,0 +1,4 @@
{
"name": "esm-test",
"type": "module"
}
File renamed without changes.
47 changes: 39 additions & 8 deletions test/fn.spec.ts
@@ -1,4 +1,6 @@
import { createRequire } from 'module'
import path from 'path'
import { fileURLToPath } from 'url'

import { jest } from '@jest/globals'

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

delete process.env.SYNCKIT_BUFFER_SIZE
delete process.env.SYNCKIT_TIMEOUT

process.env.SYNCKIT_TS_ESM = '1'
})

const cjsRequire = createRequire(import.meta.url)

const workerEsmTsPath = cjsRequire.resolve('./worker-esm.ts')
const _dirname =
typeof __dirname === 'undefined'
? path.dirname(fileURLToPath(import.meta.url))
: __dirname

const workerCjsTsPath = cjsRequire.resolve('./cjs/worker-cjs.ts')
const workerEsmTsPath = cjsRequire.resolve('./esm/worker-esm.ts')
const workerNoExtAsJsPath = path.resolve(_dirname, './worker-js')
const workerJsAsTsPath = path.resolve(_dirname, './worker.js')
const workerCjsPath = cjsRequire.resolve('./worker.cjs')
const workerMjsPath = cjsRequire.resolve('./worker.mjs')
const workerErrorPath = cjsRequire.resolve('./worker-error.cjs')

test('ts as cjs', () => {
const syncFn = createSyncFn<AsyncWorkerFn>(workerCjsTsPath)
expect(syncFn(1)).toBe(1)
expect(syncFn(2)).toBe(2)
expect(syncFn(5)).toBe(5)
})

test('ts as esm', () => {
const syncFn = createSyncFn<AsyncWorkerFn>(workerEsmTsPath)
expect(syncFn(1)).toBe(1)
expect(syncFn(2)).toBe(2)
expect(syncFn(5)).toBe(5)
})

test('no ext as js (as esm)', () => {
const syncFn = createSyncFn<AsyncWorkerFn>(workerNoExtAsJsPath)
expect(syncFn(1)).toBe(1)
expect(syncFn(2)).toBe(2)
expect(syncFn(5)).toBe(5)
})

test('js as ts (as esm)', () => {
const syncFn = createSyncFn<AsyncWorkerFn>(workerJsAsTsPath)
expect(syncFn(1)).toBe(1)
expect(syncFn(2)).toBe(2)
expect(syncFn(5)).toBe(5)
})

test('createSyncFn', () => {
expect(() => createSyncFn('./fake')).toThrow('`workerPath` must be absolute')
expect(() => createSyncFn(cjsRequire.resolve('eslint'))).not.toThrow()
Expand All @@ -30,10 +66,6 @@ test('createSyncFn', () => {
const syncFn2 = createSyncFn<AsyncWorkerFn>(workerCjsPath)
const syncFn3 = createSyncFn<AsyncWorkerFn>(workerMjsPath)

expect(() => createSyncFn(workerEsmTsPath)).toThrow(
'Native esm in `.ts` file is not supported yet, please use `.cjs` instead',
)

const errSyncFn = createSyncFn<() => Promise<void>>(workerErrorPath)

expect(syncFn1).toBe(syncFn2)
Expand All @@ -58,7 +90,6 @@ test('createSyncFn', () => {
test('timeout', async () => {
process.env.SYNCKIT_BUFFER_SIZE = '0'
process.env.SYNCKIT_TIMEOUT = '1'
process.env.SYNCKIT_TS_ESM = '0'

const { createSyncFn } = await import('synckit')
const syncFn = createSyncFn<AsyncWorkerFn>(workerCjsPath)
Expand Down
2 changes: 1 addition & 1 deletion test/worker-error.cjs
@@ -1,3 +1,3 @@
const { runAsWorker } = require('synckit')
const { runAsWorker } = require('../lib/index.cjs')

runAsWorker(() => Promise.reject(new Error('Worker Error')))
6 changes: 6 additions & 0 deletions test/worker-js.js
@@ -0,0 +1,6 @@
import { runAsWorker } from 'synckit'

runAsWorker(
(result, timeout) =>
new Promise(resolve => setTimeout(() => resolve(result), timeout)),
)
1 change: 1 addition & 0 deletions test/worker.cjs
@@ -1,3 +1,4 @@
// we're not using `synckit` here because jest can not handle cjs+mjs dual package correctly
const { runAsWorker } = require('../lib/index.cjs')

runAsWorker(
Expand Down
2 changes: 1 addition & 1 deletion test/worker.mjs
@@ -1,4 +1,4 @@
import { runAsWorker } from '../lib/index.js'
import { runAsWorker } from 'synckit'

runAsWorker(
(result, timeout) =>
Expand Down
6 changes: 6 additions & 0 deletions test/worker.ts
@@ -0,0 +1,6 @@
import { runAsWorker } from 'synckit'

runAsWorker(
(result: number, timeout: number) =>
new Promise<number>(resolve => setTimeout(() => resolve(result), timeout)),
)

0 comments on commit fd85ccd

Please sign in to comment.