From d0caef5c3b6f6fadf5ce8dac813302d29dd7b63d Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Fri, 24 Nov 2023 20:41:53 +0900 Subject: [PATCH 01/28] wip --- src/cli.ts | 11 ++++++++++- tests/specs/cli.ts | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 45e6e408f..b6d58614d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,4 @@ -import type { ChildProcess } from 'child_process'; +import type { ChildProcess, Serializable } from 'child_process'; import { cli } from 'cleye'; import { transformSync as esbuildTransformSync, @@ -188,6 +188,15 @@ cli({ relaySignals(childProcess); + if (process.send) { + childProcess.on('message', (message) => { + process.send!(message); + }); + process.on('message', (message) => { + childProcess.send(message as Serializable); + }); + } + childProcess.on( 'close', code => process.exit(code!), diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts index 6b080a5bc..2cc6de70c 100644 --- a/tests/specs/cli.ts +++ b/tests/specs/cli.ts @@ -2,11 +2,11 @@ import path from 'path'; import { setTimeout } from 'timers/promises'; import { testSuite, expect } from 'manten'; import { createFixture } from 'fs-fixture'; +import { execa } from 'execa'; import packageJson from '../../package.json'; -import { tsxPath } from '../utils/tsx.js'; import { ptyShell, isWindows } from '../utils/pty-shell/index'; import { expectMatchInOrder } from '../utils/expect-match-in-order.js'; -import type { NodeApis } from '../utils/tsx.js'; +import { tsxPath, type NodeApis } from '../utils/tsx.js'; import { compareNodeVersion, type Version } from '../../src/utils/node-features.js'; export default testSuite(({ describe }, node: NodeApis) => { @@ -304,5 +304,34 @@ export default testSuite(({ describe }, node: NodeApis) => { }, 10_000); }); }); + + // Relays to child + test('relays messages to child', async ({ onTestFinish }) => { + const fixture = await createFixture({ + 'file.js': ` + console.log('READY'); + process.on('message', (received) => { + console.log({ received }); + process.send(received); + }); + `, + }); + + onTestFinish(async () => await fixture.rm()); + + const tsx = execa(tsxPath, ['file.js'], { + cwd: fixture.path, + stdio: ['ipc'], + reject: false, + }); + + tsx.on('message', (message) => { + console.log('from test', message); + tsx.kill(); + }); + tsx.send('hello'); + + console.log(await tsx); + }); }); }); From 65e64b525255e241bc43e98c21bc87561f832255 Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sat, 25 Nov 2023 10:18:14 +0900 Subject: [PATCH 02/28] wip --- package.json | 2 +- src/cjs/index.ts | 10 ++++-- src/cli.ts | 33 ++++++++++-------- src/esm/loaders.ts | 11 +++--- src/esm/register.ts | 10 ++++-- src/preflight.cts | 61 ++++++++++++++++++++-------------- src/run.ts | 9 +---- src/utils/ipc/client.ts | 28 ++++++++++++++++ src/utils/ipc/get-pipe-path.ts | 11 ++++++ src/utils/ipc/server.ts | 39 ++++++++++++++++++++++ src/utils/tmp-dir.ts | 22 ++++++++++++ src/utils/transform/cache.ts | 27 +++------------ src/watch/index.ts | 53 ++++++++++++++--------------- 13 files changed, 211 insertions(+), 105 deletions(-) create mode 100644 src/utils/ipc/client.ts create mode 100644 src/utils/ipc/get-pipe-path.ts create mode 100644 src/utils/ipc/server.ts create mode 100644 src/utils/tmp-dir.ts diff --git a/package.json b/package.json index 4ec787d6f..d887fb152 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "bin": "./dist/cli.mjs", "scripts": { "prepare": "pnpm simple-git-hooks", - "build": "pkgroll --target=node12.19 --minify", + "build": "pkgroll --target=node12.19", "lint": "eslint --cache .", "type-check": "tsc --noEmit", "test": "pnpm build && node ./dist/cli.mjs tests/index.ts", diff --git a/src/cjs/index.ts b/src/cjs/index.ts index 4299192b8..3d8e6a7b5 100644 --- a/src/cjs/index.ts +++ b/src/cjs/index.ts @@ -13,6 +13,7 @@ import { transformSync } from '../utils/transform/index.js'; import { transformDynamicImport } from '../utils/transform/transform-dynamic-import.js'; import { resolveTsPath } from '../utils/resolve-ts-path.js'; import { isESM } from '../utils/esm-pattern.js'; +import { creatingClient, type SendToClient} from '../utils/ipc/client.js'; const isRelativePathPattern = /^\.{1,2}\//; const isTsFilePatten = /\.[cm]?tsx?$/; @@ -49,13 +50,18 @@ const transformExtensions = [ '.mjs', ]; +let sendToClient: SendToClient | undefined; +creatingClient.then((c) => { + sendToClient = c; +}); + const transformer = ( module: Module, filePath: string, ) => { // For tracking dependencies in watch mode - if (process.send) { - process.send({ + if (sendToClient) { + sendToClient({ type: 'dependency', path: filePath, }); diff --git a/src/cli.ts b/src/cli.ts index 45e6e408f..a292ad52f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -11,22 +11,24 @@ import { ignoreAfterArgument, } from './remove-argv-flags.js'; import { testRunnerGlob } from './utils/node-features.js'; +import { createIpcServer } from './utils/ipc/server.js'; +import type { Server } from 'net'; const relaySignals = ( childProcess: ChildProcess, + ipcSocket: Server, ) => { let waitForSignal: undefined | ((signal: NodeJS.Signals) => void); - childProcess.on( - 'message', - ( - data: { type: string; signal: NodeJS.Signals }, - ) => { - if (data && data.type === 'kill' && waitForSignal) { - waitForSignal(data.signal); - } - }, - ); + ipcSocket.on('data', (data: { type: string; signal: NodeJS.Signals }) => { + if ( + data + && data.type === 'kill' + && waitForSignal + ) { + waitForSignal(data.signal); + } + }); const waitForSignalFromChild = () => { const p = new Promise<NodeJS.Signals | undefined>((resolve) => { @@ -113,7 +115,7 @@ cli({ }, help: false, ignoreArgv: ignoreAfterArgument(), -}, (argv) => { +}, async (argv) => { if (argv.flags.version) { process.stdout.write(`tsx v${version}\nnode `); } else if (argv.flags.help) { @@ -178,6 +180,8 @@ cli({ argvsToRun.push('**/{test,test/**/*,test-*,*[.-_]test}.?(c|m)@(t|j)s'); } + const ipc = await createIpcServer(); + const childProcess = run( argvsToRun, { @@ -186,10 +190,13 @@ cli({ }, ); - relaySignals(childProcess); + relaySignals(childProcess, ipc); childProcess.on( 'close', - code => process.exit(code!), + code => { + // ipc.close(); + process.exit(code!); + }, ); }); diff --git a/src/esm/loaders.ts b/src/esm/loaders.ts index 821477150..c5d24cc01 100644 --- a/src/esm/loaders.ts +++ b/src/esm/loaders.ts @@ -61,14 +61,17 @@ export const globalPreload: GlobalPreloadHook = ({ port }) => { return ` const require = getBuiltin('module').createRequire("${import.meta.url}"); - require('tsx/source-map').installSourceMapSupport(); - if (process.send) { + require('../source-map.cjs').installSourceMapSupport(); + // TODO pkgroll needs to build import maps as entry points + const { creatingClient } = require('#ipc/client.js'); + console.log(creatingClient); + creatingClient.then((sendToParent) => { port.addListener('message', (message) => { if (message.type === 'dependency') { - process.send(message); + sendToParent(message); } }); - } + }); port.unref(); // Allows process to exit without waiting for port to close `; }; diff --git a/src/esm/register.ts b/src/esm/register.ts index 19c7f9ee8..fdbfdd696 100644 --- a/src/esm/register.ts +++ b/src/esm/register.ts @@ -1,18 +1,21 @@ import module from 'node:module'; import { MessageChannel } from 'node:worker_threads'; import { installSourceMapSupport } from '../source-map.js'; +import { creatingClient } from '../utils/ipc/client.js'; export const registerLoader = () => { const { port1, port2 } = new MessageChannel(); installSourceMapSupport(); - if (process.send) { + + creatingClient.then((sendToClient) => { + console.log('create clikent'); port1.addListener('message', (message) => { if (message.type === 'dependency') { - process.send!(message); + sendToClient(message); } }); - } + }); // Allows process to exit without waiting for port to close port1.unref(); @@ -25,6 +28,7 @@ export const registerLoader = () => { port: port2, }, transferList: [port2], + // TODO: Strip preflight }, ); }; diff --git a/src/preflight.cts b/src/preflight.cts index 529113232..0ecd0348e 100644 --- a/src/preflight.cts +++ b/src/preflight.cts @@ -1,5 +1,7 @@ import { constants as osConstants } from 'os'; -import './suppress-warnings.cts'; +import { creatingClient } from './utils/ipc/client.js'; +import { isMainThread } from 'node:worker_threads'; +import './suppress-warnings.cjs'; type BaseEventListener = () => void; @@ -16,27 +18,13 @@ type BaseEventListener = () => void; // eslint-disable-next-line import/no-unresolved require('./cjs/index.cjs'); -// If a parent process is detected -if (process.send) { - const relaySignal = (signal: NodeJS.Signals) => { - process.send!({ - type: 'kill', - signal, - }); - - /** - * Since we're setting a custom signal handler, we need to emulate the - * default behavior when there are no other handlers set - */ - if (process.listenerCount(signal) === 0) { - process.exit(128 + osConstants.signals[signal]); - } - }; - - const relaySignals = ['SIGINT', 'SIGTERM'] as const; - type RelaySignals = typeof relaySignals[number]; - for (const signal of relaySignals) { - process.on(signal, relaySignal); +const bindHiddenSignalsHandler = ( + signals: NodeJS.Signals[], + handler: NodeJS.SignalsListener, +) => { + type RelaySignals = typeof signals[number]; + for (const signal of signals) { + process.on(signal, handler); } /** @@ -46,7 +34,7 @@ if (process.send) { process.listenerCount = function (eventName) { let count = Reflect.apply(listenerCount, this, arguments); - if (relaySignals.includes(eventName as RelaySignals)) { + if (signals.includes(eventName as RelaySignals)) { count -= 1; } return count; @@ -54,9 +42,32 @@ if (process.send) { process.listeners = function (eventName) { const result: BaseEventListener[] = Reflect.apply(listeners, this, arguments); - if (relaySignals.includes(eventName as RelaySignals)) { - return result.filter(listener => listener !== relaySignal); + if (signals.includes(eventName as RelaySignals)) { + return result.filter(listener => listener !== handler); } return result; }; +}; + +// ESM Loader spawns child with same flags as parent +// TODO: maybe we can also remove these flags? +if (isMainThread) { + (async () => { + const sendToClient = await creatingClient; + + bindHiddenSignalsHandler(['SIGINT', 'SIGTERM'], (signal: NodeJS.Signals) => { + sendToClient({ + type: 'kill', + signal, + }); + + /** + * If the user has not registered a signal handler, we need to emulate + * the default behavior when there are no other handlers set + */ + if (process.listenerCount(signal) === 0) { + process.exit(128 + osConstants.signals[signal]); + } + }); + })(); } diff --git a/src/run.ts b/src/run.ts index 958ff5cee..7ea4b000e 100644 --- a/src/run.ts +++ b/src/run.ts @@ -1,4 +1,3 @@ -import type { StdioOptions } from 'child_process'; import { pathToFileURL } from 'url'; import spawn from 'cross-spawn'; import { supportsModuleRegister } from './utils/node-features'; @@ -12,12 +11,6 @@ export const run = ( }, ) => { const environment = { ...process.env }; - const stdio: StdioOptions = [ - 'inherit', // stdin - 'inherit', // stdout - 'inherit', // stderr - 'ipc', // parent-child communication - ]; if (options) { if (options.noCache) { @@ -41,7 +34,7 @@ export const run = ( ...argv, ], { - stdio, + stdio: 'inherit', env: environment, }, ); diff --git a/src/utils/ipc/client.ts b/src/utils/ipc/client.ts new file mode 100644 index 000000000..903750123 --- /dev/null +++ b/src/utils/ipc/client.ts @@ -0,0 +1,28 @@ +import net from 'net'; +import { getPipePath } from './get-pipe-path.js'; + +export type SendToClient = (data: Record<string, unknown>) => void; + +// TODO: Handle when the loader is called directly +const createIpcClient = () => new Promise<SendToClient>((resolve, reject) => { + const pipePath = getPipePath(process.ppid); + const socket: net.Socket = net.createConnection( + pipePath, + () => { + const send: SendToClient = (data) => { + const messageBuffer = Buffer.from(JSON.stringify(data)); + const lengthBuffer = Buffer.alloc(4); + lengthBuffer.writeInt32BE(messageBuffer.length, 0); + socket.write(Buffer.concat([lengthBuffer, messageBuffer])); + }; + resolve(send); + }, + ); + + socket.on('error', reject); + + // Prevent Node from waiting for this socket to close before exiting + socket.unref(); +}); + +export const creatingClient = createIpcClient(); diff --git a/src/utils/ipc/get-pipe-path.ts b/src/utils/ipc/get-pipe-path.ts new file mode 100644 index 000000000..0be7359f6 --- /dev/null +++ b/src/utils/ipc/get-pipe-path.ts @@ -0,0 +1,11 @@ +import path from 'path'; +import { tmpdir } from '../tmp-dir.js'; + +export const getPipePath = (processId: number) => { + const pipePath = path.join(tmpdir, `tsx_${processId}`); + return ( + process.platform === 'win32' + ? '\\\\.\\pipe\\' + pipePath + : pipePath + ); +}; diff --git a/src/utils/ipc/server.ts b/src/utils/ipc/server.ts new file mode 100644 index 000000000..04a3f47e5 --- /dev/null +++ b/src/utils/ipc/server.ts @@ -0,0 +1,39 @@ +import net from 'net'; +import { getPipePath } from './get-pipe-path.js'; + +export const createIpcServer = () => new Promise<net.Server>((resolve, reject) => { + const pipePath = getPipePath(process.pid); + const server = net.createServer((socket) => { + let buffer = Buffer.alloc(0); + + const handleIncomingMessage = (message: Buffer) => { + const data = JSON.parse(message.toString()); + server.emit('data', data); + }; + + socket.on('data', (data) => { + buffer = Buffer.concat([buffer, data]); + + while (buffer.length > 4) { + const messageLength = buffer.readInt32BE(0); + if (buffer.length >= 4 + messageLength) { + const message = buffer.slice(4, 4 + messageLength); + handleIncomingMessage(message); + buffer = buffer.slice(4 + messageLength); + } else { + break; + } + } + + // console.log({ data, string: data.toString() }); + // server.emit('data', JSON.parse(data)); + }); + }); + server.listen(pipePath, () => resolve(server)); + server.on('error', reject); + + // // Prevent Node from waiting for this socket to close before exiting + // server.unref(); + + // TODO: servver close +}); diff --git a/src/utils/tmp-dir.ts b/src/utils/tmp-dir.ts new file mode 100644 index 000000000..7801e0899 --- /dev/null +++ b/src/utils/tmp-dir.ts @@ -0,0 +1,22 @@ +import path from 'path'; +import os from 'os'; + +/** + * Cache directory is based on the user's identifier + * to avoid permission issues when accessed by a different user + */ +const { geteuid } = process; +const userId = ( + geteuid + // For Linux users with virtual users on CI (e.g. Docker) + ? geteuid() + + // Use username on Windows because it doesn't have id + : os.userInfo().username +); + +/** + * This ensures that the cache directory is unique per user + * and has the appropriate permissions + */ +export const tmpdir = path.join(os.tmpdir(), `tsx-${userId}`); diff --git a/src/utils/transform/cache.ts b/src/utils/transform/cache.ts index 2bd28986d..f123e9a6e 100644 --- a/src/utils/transform/cache.ts +++ b/src/utils/transform/cache.ts @@ -3,25 +3,10 @@ import path from 'path'; import os from 'os'; import { readJsonFile } from '../read-json-file.js'; import type { Transformed } from './apply-transformers.js'; +import { tmpdir } from '../tmp-dir.js'; -const getTime = () => Math.floor(Date.now() / 1e8); - -const tmpdir = os.tmpdir(); const noop = () => {}; - -/** - * Cache directory is based on the user's identifier - * to avoid permission issues when accessed by a different user - */ -const { geteuid } = process; -const userId = ( - geteuid - // For Linux users with virtual users on CI (e.g. Docker) - ? geteuid() - - // Use username on Windows because it doesn't have id - : os.userInfo().username -); +const getTime = () => Math.floor(Date.now() / 1e8); class FileCache<ReturnType> extends Map<string, ReturnType> { /** @@ -34,14 +19,10 @@ class FileCache<ReturnType> extends Map<string, ReturnType> { * Note on Windows, temp files are not cleaned up automatically. * https://superuser.com/a/1599897 */ - cacheDirectory = path.join( - // Write permissions by anyone - tmpdir, - `tsx-${userId}`, - ); + cacheDirectory = tmpdir; // Maintained so we can remove it on Windows - oldCacheDirectory = path.join(tmpdir, 'tsx'); + oldCacheDirectory = path.join(os.tmpdir(), 'tsx'); cacheFiles: { time: number; diff --git a/src/watch/index.ts b/src/watch/index.ts index 8c18c94bd..f89f89be3 100644 --- a/src/watch/index.ts +++ b/src/watch/index.ts @@ -15,6 +15,7 @@ import { debounce, log, } from './utils.js'; +import { createIpcServer } from '../utils/ipc/server.js'; const flags = { noCache: { @@ -52,7 +53,7 @@ export const watchCommand = command({ * Remove once cleye supports error callbacks on missing arguments */ ignoreArgv: ignoreAfterArgument(false), -}, (argv) => { +}, async (argv) => { const rawArgvs = removeArgvFlags(flags, process.argv.slice(3)); const options = { noCache: argv.flags.noCache, @@ -65,36 +66,36 @@ export const watchCommand = command({ let runProcess: ChildProcess | undefined; let exiting = false; + const server = await createIpcServer(); + + server.on('data', (data) => { + // Collect run-time dependencies to watch + if ( + data + && typeof data === 'object' + && 'type' in data + && data.type === 'dependency' + && 'path' in data + && typeof data.path === 'string' + ) { + const dependencyPath = ( + data.path.startsWith('file:') + ? fileURLToPath(data.path) + : data.path + ); + + if (path.isAbsolute(dependencyPath)) { + watcher.add(dependencyPath); + } + } + }); + const spawnProcess = () => { if (exiting) { return; } - const childProcess = run(rawArgvs, options); - - childProcess.on('message', (data) => { - // Collect run-time dependencies to watch - if ( - data - && typeof data === 'object' - && 'type' in data - && data.type === 'dependency' - && 'path' in data - && typeof data.path === 'string' - ) { - const dependencyPath = ( - data.path.startsWith('file:') - ? fileURLToPath(data.path) - : data.path - ); - - if (path.isAbsolute(dependencyPath)) { - watcher.add(dependencyPath); - } - } - }); - - return childProcess; + return run(rawArgvs, options); }; let waitingChildExit = false; From b50adcc58b631ced547dc8f9ee1d129b6f1f19fc Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sat, 25 Nov 2023 10:18:44 +0900 Subject: [PATCH 03/28] wip --- src/cjs/index.ts | 2 +- src/cli.ts | 4 ++-- src/preflight.cts | 6 +++--- src/utils/ipc/get-pipe-path.ts | 2 +- src/utils/ipc/server.ts | 2 +- src/utils/transform/cache.ts | 2 +- src/watch/index.ts | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cjs/index.ts b/src/cjs/index.ts index 3d8e6a7b5..bb6346c8a 100644 --- a/src/cjs/index.ts +++ b/src/cjs/index.ts @@ -13,7 +13,7 @@ import { transformSync } from '../utils/transform/index.js'; import { transformDynamicImport } from '../utils/transform/transform-dynamic-import.js'; import { resolveTsPath } from '../utils/resolve-ts-path.js'; import { isESM } from '../utils/esm-pattern.js'; -import { creatingClient, type SendToClient} from '../utils/ipc/client.js'; +import { creatingClient, type SendToClient } from '../utils/ipc/client.js'; const isRelativePathPattern = /^\.{1,2}\//; const isTsFilePatten = /\.[cm]?tsx?$/; diff --git a/src/cli.ts b/src/cli.ts index a292ad52f..8519b7aff 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,5 @@ import type { ChildProcess } from 'child_process'; +import type { Server } from 'net'; import { cli } from 'cleye'; import { transformSync as esbuildTransformSync, @@ -12,7 +13,6 @@ import { } from './remove-argv-flags.js'; import { testRunnerGlob } from './utils/node-features.js'; import { createIpcServer } from './utils/ipc/server.js'; -import type { Server } from 'net'; const relaySignals = ( childProcess: ChildProcess, @@ -194,7 +194,7 @@ cli({ childProcess.on( 'close', - code => { + (code) => { // ipc.close(); process.exit(code!); }, diff --git a/src/preflight.cts b/src/preflight.cts index 0ecd0348e..79f695a6c 100644 --- a/src/preflight.cts +++ b/src/preflight.cts @@ -1,6 +1,6 @@ import { constants as osConstants } from 'os'; -import { creatingClient } from './utils/ipc/client.js'; import { isMainThread } from 'node:worker_threads'; +import { creatingClient } from './utils/ipc/client.js'; import './suppress-warnings.cjs'; type BaseEventListener = () => void; @@ -54,13 +54,13 @@ const bindHiddenSignalsHandler = ( if (isMainThread) { (async () => { const sendToClient = await creatingClient; - + bindHiddenSignalsHandler(['SIGINT', 'SIGTERM'], (signal: NodeJS.Signals) => { sendToClient({ type: 'kill', signal, }); - + /** * If the user has not registered a signal handler, we need to emulate * the default behavior when there are no other handlers set diff --git a/src/utils/ipc/get-pipe-path.ts b/src/utils/ipc/get-pipe-path.ts index 0be7359f6..d27e744ea 100644 --- a/src/utils/ipc/get-pipe-path.ts +++ b/src/utils/ipc/get-pipe-path.ts @@ -5,7 +5,7 @@ export const getPipePath = (processId: number) => { const pipePath = path.join(tmpdir, `tsx_${processId}`); return ( process.platform === 'win32' - ? '\\\\.\\pipe\\' + pipePath + ? `\\\\.\\pipe\\${pipePath}` : pipePath ); }; diff --git a/src/utils/ipc/server.ts b/src/utils/ipc/server.ts index 04a3f47e5..82a23c4c3 100644 --- a/src/utils/ipc/server.ts +++ b/src/utils/ipc/server.ts @@ -28,7 +28,7 @@ export const createIpcServer = () => new Promise<net.Server>((resolve, reject) = // console.log({ data, string: data.toString() }); // server.emit('data', JSON.parse(data)); }); - }); + }); server.listen(pipePath, () => resolve(server)); server.on('error', reject); diff --git a/src/utils/transform/cache.ts b/src/utils/transform/cache.ts index f123e9a6e..cf4033802 100644 --- a/src/utils/transform/cache.ts +++ b/src/utils/transform/cache.ts @@ -2,8 +2,8 @@ import fs from 'fs'; import path from 'path'; import os from 'os'; import { readJsonFile } from '../read-json-file.js'; -import type { Transformed } from './apply-transformers.js'; import { tmpdir } from '../tmp-dir.js'; +import type { Transformed } from './apply-transformers.js'; const noop = () => {}; const getTime = () => Math.floor(Date.now() / 1e8); diff --git a/src/watch/index.ts b/src/watch/index.ts index f89f89be3..634579fdf 100644 --- a/src/watch/index.ts +++ b/src/watch/index.ts @@ -10,12 +10,12 @@ import { removeArgvFlags, ignoreAfterArgument, } from '../remove-argv-flags.js'; +import { createIpcServer } from '../utils/ipc/server.js'; import { clearScreen, debounce, log, } from './utils.js'; -import { createIpcServer } from '../utils/ipc/server.js'; const flags = { noCache: { From 4b608266aa03b40dbfdb0957c2c67598abc8d750 Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 10:53:38 +0900 Subject: [PATCH 04/28] wip --- src/preflight.cts | 6 ++++-- src/utils/tmp-dir.ts | 22 ---------------------- 2 files changed, 4 insertions(+), 24 deletions(-) delete mode 100644 src/utils/tmp-dir.ts diff --git a/src/preflight.cts b/src/preflight.cts index 35ddbe80d..5fe248585 100644 --- a/src/preflight.cts +++ b/src/preflight.cts @@ -36,8 +36,10 @@ const bindHiddenSignalsHandler = ( }; }; -// ESM Loader spawns child with same flags as parent -// TODO: maybe we can also remove these flags? +/** + * Seems module.register() calls the loader with the same Node arguments + * which causes this preflight to be loaded in the loader thread + */ if (isMainThread) { /** * Hook require() to transform to CJS diff --git a/src/utils/tmp-dir.ts b/src/utils/tmp-dir.ts deleted file mode 100644 index 7801e0899..000000000 --- a/src/utils/tmp-dir.ts +++ /dev/null @@ -1,22 +0,0 @@ -import path from 'path'; -import os from 'os'; - -/** - * Cache directory is based on the user's identifier - * to avoid permission issues when accessed by a different user - */ -const { geteuid } = process; -const userId = ( - geteuid - // For Linux users with virtual users on CI (e.g. Docker) - ? geteuid() - - // Use username on Windows because it doesn't have id - : os.userInfo().username -); - -/** - * This ensures that the cache directory is unique per user - * and has the appropriate permissions - */ -export const tmpdir = path.join(os.tmpdir(), `tsx-${userId}`); From 5dd6cd86c09a63cdbeca7d8c4c9213ddaeb4017d Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 11:27:29 +0900 Subject: [PATCH 05/28] wip --- src/esm/register.ts | 10 +++++----- src/utils/ipc/get-pipe-path.ts | 2 +- src/utils/ipc/server.ts | 11 +++++++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/esm/register.ts b/src/esm/register.ts index fdbfdd696..7d902ff4e 100644 --- a/src/esm/register.ts +++ b/src/esm/register.ts @@ -9,16 +9,16 @@ export const registerLoader = () => { installSourceMapSupport(); creatingClient.then((sendToClient) => { - console.log('create clikent'); - port1.addListener('message', (message) => { + port1.on('message', (message) => { if (message.type === 'dependency') { sendToClient(message); } }); - }); - // Allows process to exit without waiting for port to close - port1.unref(); + // Allows process to exit without waiting for port to close + // Has to be called after .on() + port1.unref(); + }); module.register( './index.mjs', diff --git a/src/utils/ipc/get-pipe-path.ts b/src/utils/ipc/get-pipe-path.ts index 8da62a5a5..f09d1a2d3 100644 --- a/src/utils/ipc/get-pipe-path.ts +++ b/src/utils/ipc/get-pipe-path.ts @@ -2,7 +2,7 @@ import path from 'path'; import { tmpdir } from '../temporary-directory.js'; export const getPipePath = (processId: number) => { - const pipePath = path.join(tmpdir, `tsx_${processId}`); + const pipePath = path.join(tmpdir, `${processId}.pipe`); return ( process.platform === 'win32' ? `\\\\.\\pipe\\${pipePath}` diff --git a/src/utils/ipc/server.ts b/src/utils/ipc/server.ts index 82a23c4c3..ac93b49e6 100644 --- a/src/utils/ipc/server.ts +++ b/src/utils/ipc/server.ts @@ -1,8 +1,9 @@ import net from 'net'; +import fs from 'fs/promises'; +import { tmpdir } from '../temporary-directory.js'; import { getPipePath } from './get-pipe-path.js'; -export const createIpcServer = () => new Promise<net.Server>((resolve, reject) => { - const pipePath = getPipePath(process.pid); +export const createIpcServer = () => new Promise<net.Server>(async (resolve, reject) => { const server = net.createServer((socket) => { let buffer = Buffer.alloc(0); @@ -29,6 +30,12 @@ export const createIpcServer = () => new Promise<net.Server>((resolve, reject) = // server.emit('data', JSON.parse(data)); }); }); + + await fs.mkdir(tmpdir, { + recursive: true, + }); + + const pipePath = getPipePath(process.pid); server.listen(pipePath, () => resolve(server)); server.on('error', reject); From e2b46789d242b62f1ed9c0ae8583069df9ced523 Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 11:52:22 +0900 Subject: [PATCH 06/28] wip --- src/cjs/index.ts | 4 +-- src/esm/loaders.ts | 33 +++++------------------ src/esm/register.ts | 22 +++++++-------- src/utils/ipc/client.ts | 6 ++--- tests/index.ts | 2 +- tests/specs/cli.ts | 60 ++++++++++++++++++++--------------------- 6 files changed, 54 insertions(+), 73 deletions(-) diff --git a/src/cjs/index.ts b/src/cjs/index.ts index bb6346c8a..d5dd4b5b2 100644 --- a/src/cjs/index.ts +++ b/src/cjs/index.ts @@ -13,7 +13,7 @@ import { transformSync } from '../utils/transform/index.js'; import { transformDynamicImport } from '../utils/transform/transform-dynamic-import.js'; import { resolveTsPath } from '../utils/resolve-ts-path.js'; import { isESM } from '../utils/esm-pattern.js'; -import { creatingClient, type SendToClient } from '../utils/ipc/client.js'; +import { creatingClient, type SendToParent } from '../utils/ipc/client.js'; const isRelativePathPattern = /^\.{1,2}\//; const isTsFilePatten = /\.[cm]?tsx?$/; @@ -50,7 +50,7 @@ const transformExtensions = [ '.mjs', ]; -let sendToClient: SendToClient | undefined; +let sendToClient: SendToParent | undefined; creatingClient.then((c) => { sendToClient = c; }); diff --git a/src/esm/loaders.ts b/src/esm/loaders.ts index c5d24cc01..4dbc81eec 100644 --- a/src/esm/loaders.ts +++ b/src/esm/loaders.ts @@ -9,6 +9,7 @@ import { transformDynamicImport } from '../utils/transform/transform-dynamic-imp import { resolveTsPath } from '../utils/resolve-ts-path.js'; import { installSourceMapSupport } from '../source-map.js'; import { importAttributes } from '../utils/node-features.js'; +import { creatingClient, type SendToParent } from '../utils/ipc/client.js'; import { tsconfigPathsMatcher, fileMatcher, @@ -36,45 +37,20 @@ type resolve = ( recursiveCall?: boolean, ) => MaybePromise<ResolveFnOutput>; -type SendToParent = (data: { - type: 'dependency'; - path: string; -}) => void; - -let sendToParent: SendToParent | undefined = process.send ? process.send.bind(process) : undefined; - export const initialize: InitializeHook = async (data) => { if (!data) { throw new Error('tsx must be loaded with --import instead of --loader\nThe --loader flag was deprecated in Node v20.6.0'); } - - const { port } = data; - sendToParent = port.postMessage.bind(port); }; /** * Technically globalPreload is deprecated so it should be in loaders-deprecated * but it shares a closure with the new load hook */ -export const globalPreload: GlobalPreloadHook = ({ port }) => { - sendToParent = port.postMessage.bind(port); - - return ` +export const globalPreload: GlobalPreloadHook = ({ port }) => ` const require = getBuiltin('module').createRequire("${import.meta.url}"); require('../source-map.cjs').installSourceMapSupport(); - // TODO pkgroll needs to build import maps as entry points - const { creatingClient } = require('#ipc/client.js'); - console.log(creatingClient); - creatingClient.then((sendToParent) => { - port.addListener('message', (message) => { - if (message.type === 'dependency') { - sendToParent(message); - } - }); - }); - port.unref(); // Allows process to exit without waiting for port to close `; -}; const resolveExplicitPath = async ( defaultResolve: NextResolve, @@ -245,6 +221,11 @@ export const resolve: resolve = async function ( } }; +let sendToParent: SendToParent | undefined; +creatingClient.then((c) => { + sendToParent = c; +}); + const contextAttributesProperty = importAttributes ? 'importAttributes' : 'importAssertions'; export const load: LoadHook = async function ( diff --git a/src/esm/register.ts b/src/esm/register.ts index 7d902ff4e..74580f6d6 100644 --- a/src/esm/register.ts +++ b/src/esm/register.ts @@ -1,24 +1,24 @@ import module from 'node:module'; import { MessageChannel } from 'node:worker_threads'; import { installSourceMapSupport } from '../source-map.js'; -import { creatingClient } from '../utils/ipc/client.js'; +// import { creatingClient } from '../utils/ipc/client.js'; export const registerLoader = () => { const { port1, port2 } = new MessageChannel(); installSourceMapSupport(); - creatingClient.then((sendToClient) => { - port1.on('message', (message) => { - if (message.type === 'dependency') { - sendToClient(message); - } - }); + // creatingClient.then((sendToClient) => { + // port1.on('message', (message) => { + // if (message.type === 'dependency') { + // sendToClient(message); + // } + // }); - // Allows process to exit without waiting for port to close - // Has to be called after .on() - port1.unref(); - }); + // // Allows process to exit without waiting for port to close + // // Has to be called after .on() + // port1.unref(); + // }); module.register( './index.mjs', diff --git a/src/utils/ipc/client.ts b/src/utils/ipc/client.ts index 903750123..9d322036f 100644 --- a/src/utils/ipc/client.ts +++ b/src/utils/ipc/client.ts @@ -1,15 +1,15 @@ import net from 'net'; import { getPipePath } from './get-pipe-path.js'; -export type SendToClient = (data: Record<string, unknown>) => void; +export type SendToParent = (data: Record<string, unknown>) => void; // TODO: Handle when the loader is called directly -const createIpcClient = () => new Promise<SendToClient>((resolve, reject) => { +const createIpcClient = () => new Promise<SendToParent>((resolve, reject) => { const pipePath = getPipePath(process.ppid); const socket: net.Socket = net.createConnection( pipePath, () => { - const send: SendToClient = (data) => { + const send: SendToParent = (data) => { const messageBuffer = Buffer.from(JSON.stringify(data)); const lengthBuffer = Buffer.alloc(4); lengthBuffer.writeInt32BE(messageBuffer.length, 0); diff --git a/tests/index.ts b/tests/index.ts index ac959be87..d9eb7609c 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -10,7 +10,7 @@ import { nodeVersions } from './utils/node-versions'; for (const nodeVersion of nodeVersions) { const node = await createNode(nodeVersion); await describe(`Node ${node.version}`, async ({ runTestSuite }) => { - await runTestSuite(import('./specs/cli'), node); + // await runTestSuite(import('./specs/cli'), node); await runTestSuite(import('./specs/watch'), node); await runTestSuite( import('./specs/smoke'), diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts index 6b080a5bc..22c4fff8b 100644 --- a/tests/specs/cli.ts +++ b/tests/specs/cli.ts @@ -273,36 +273,36 @@ export default testSuite(({ describe }, node: NodeApis) => { await tsxProcess; }, 10_000); - describe('Ctrl + C', ({ test }) => { - test('Exit code', async () => { - const output = await ptyShell( - [ - `${node.path} ${tsxPath} ${path.join(fixture.path, 'keep-alive.js')}\r`, - stdout => stdout.includes('READY') && '\u0003', - `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, - ], - ); - expect(output).toMatch(/EXIT_CODE:\s+130/); - }, 10_000); - - test('Catchable', async () => { - const output = await ptyShell( - [ - `${node.path} ${tsxPath} ${path.join(fixture.path, 'catch-signals.js')}\r`, - stdout => stdout.includes('READY') && '\u0003', - `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, - ], - ); - - expectMatchInOrder(output, [ - 'READY\r\n', - process.platform === 'win32' ? '' : '^C', - 'SIGINT\r\n', - 'SIGINT HANDLER COMPLETED\r\n', - /EXIT_CODE:\s+200/, - ]); - }, 10_000); - }); + // describe('Ctrl + C', ({ test }) => { + // test('Exit code', async () => { + // const output = await ptyShell( + // [ + // `${tsxPath} ${path.join(fixture.path, 'keep-alive.js')}\r`, + // stdout => stdout.includes('READY') && '\u0003', + // `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, + // ], + // ); + // expect(output).toMatch(/EXIT_CODE:\s+130/); + // }, 10_000); + + // test('Catchable', async () => { + // const output = await ptyShell( + // [ + // `${tsxPath} ${path.join(fixture.path, 'catch-signals.js')}\r`, + // stdout => stdout.includes('READY') && '\u0003', + // `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, + // ], + // ); + + // expectMatchInOrder(output, [ + // 'READY\r\n', + // process.platform === 'win32' ? '' : '^C', + // 'SIGINT\r\n', + // 'SIGINT HANDLER COMPLETED\r\n', + // /EXIT_CODE:\s+200/, + // ]); + // }, 10_000); + // }); }); }); }); From b48b7fbe69c20719177d09a167a1d33d0d6fd3ac Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 12:23:16 +0900 Subject: [PATCH 07/28] wip --- src/esm/loaders.ts | 8 ++++---- src/esm/register.ts | 22 +--------------------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/esm/loaders.ts b/src/esm/loaders.ts index 4dbc81eec..400c12dc4 100644 --- a/src/esm/loaders.ts +++ b/src/esm/loaders.ts @@ -47,10 +47,10 @@ export const initialize: InitializeHook = async (data) => { * Technically globalPreload is deprecated so it should be in loaders-deprecated * but it shares a closure with the new load hook */ -export const globalPreload: GlobalPreloadHook = ({ port }) => ` - const require = getBuiltin('module').createRequire("${import.meta.url}"); - require('../source-map.cjs').installSourceMapSupport(); - `; +export const globalPreload: GlobalPreloadHook = () => ` +const require = getBuiltin('module').createRequire("${import.meta.url}"); +require('../source-map.cjs').installSourceMapSupport(); +`; const resolveExplicitPath = async ( defaultResolve: NextResolve, diff --git a/src/esm/register.ts b/src/esm/register.ts index 74580f6d6..963de6bcd 100644 --- a/src/esm/register.ts +++ b/src/esm/register.ts @@ -1,34 +1,14 @@ import module from 'node:module'; -import { MessageChannel } from 'node:worker_threads'; import { installSourceMapSupport } from '../source-map.js'; -// import { creatingClient } from '../utils/ipc/client.js'; export const registerLoader = () => { - const { port1, port2 } = new MessageChannel(); - installSourceMapSupport(); - // creatingClient.then((sendToClient) => { - // port1.on('message', (message) => { - // if (message.type === 'dependency') { - // sendToClient(message); - // } - // }); - - // // Allows process to exit without waiting for port to close - // // Has to be called after .on() - // port1.unref(); - // }); - module.register( './index.mjs', { parentURL: import.meta.url, - data: { - port: port2, - }, - transferList: [port2], - // TODO: Strip preflight + data: true, }, ); }; From 3ee6546917c26978f784257d10dd90f30a1284be Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 12:26:23 +0900 Subject: [PATCH 08/28] wip --- tests/index.ts | 2 +- tests/specs/cli.ts | 62 +++++++++++++++++++++++----------------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/tests/index.ts b/tests/index.ts index d9eb7609c..ac959be87 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -10,7 +10,7 @@ import { nodeVersions } from './utils/node-versions'; for (const nodeVersion of nodeVersions) { const node = await createNode(nodeVersion); await describe(`Node ${node.version}`, async ({ runTestSuite }) => { - // await runTestSuite(import('./specs/cli'), node); + await runTestSuite(import('./specs/cli'), node); await runTestSuite(import('./specs/watch'), node); await runTestSuite( import('./specs/smoke'), diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts index 22c4fff8b..bc96494b3 100644 --- a/tests/specs/cli.ts +++ b/tests/specs/cli.ts @@ -107,38 +107,38 @@ export default testSuite(({ describe }, node: NodeApis) => { const cliTestFlag = compareNodeVersion([18, 1, 0], nodeVersion) >= 0; const testRunnerGlob = compareNodeVersion([21, 0, 0], nodeVersion) >= 0; if (cliTestFlag) { - test('Node.js test runner', async ({ onTestFinish }) => { - const fixture = await createFixture({ - 'test.ts': ` - import { test } from 'node:test'; - import assert from 'assert'; - - test('some passing test', () => { - assert.strictEqual(1, 1); - }); - `, - }); - onTestFinish(async () => await fixture.rm()); - - const tsxProcess = await tsx( - [ - '--test', - ...( - testRunnerGlob - ? [] - : ['test.ts'] - ), - ], - fixture.path, - ); + // test('Node.js test runner', async ({ onTestFinish }) => { + // const fixture = await createFixture({ + // 'test.ts': ` + // import { test } from 'node:test'; + // import assert from 'assert'; + + // test('some passing test', () => { + // assert.strictEqual(1, 1); + // }); + // `, + // }); + // onTestFinish(async () => await fixture.rm()); + + // const tsxProcess = await tsx( + // [ + // '--test', + // ...( + // testRunnerGlob + // ? [] + // : ['test.ts'] + // ), + // ], + // fixture.path, + // ); - expect(tsxProcess.exitCode).toBe(0); - if (testRunnerGlob) { - expect(tsxProcess.stdout).toMatch('some passing test\n'); - } else { - expect(tsxProcess.stdout).toMatch('# pass 1\n'); - } - }, 10_000); + // expect(tsxProcess.exitCode).toBe(0); + // if (testRunnerGlob) { + // expect(tsxProcess.stdout).toMatch('some passing test\n'); + // } else { + // expect(tsxProcess.stdout).toMatch('# pass 1\n'); + // } + // }, 10_000); } describe('Signals', async ({ describe, onFinish }) => { From 7d6c6fdeb3e09b433ba85cdd7821b8c1f1deffd1 Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 12:46:33 +0900 Subject: [PATCH 09/28] wip --- src/utils/ipc/client.ts | 24 +++++++++------- tests/specs/cli.ts | 64 ++++++++++++++++++++--------------------- 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/src/utils/ipc/client.ts b/src/utils/ipc/client.ts index 9d322036f..7c76d4c4c 100644 --- a/src/utils/ipc/client.ts +++ b/src/utils/ipc/client.ts @@ -4,22 +4,24 @@ import { getPipePath } from './get-pipe-path.js'; export type SendToParent = (data: Record<string, unknown>) => void; // TODO: Handle when the loader is called directly -const createIpcClient = () => new Promise<SendToParent>((resolve, reject) => { +const createIpcClient = () => new Promise<SendToParent | void>((resolve) => { const pipePath = getPipePath(process.ppid); const socket: net.Socket = net.createConnection( pipePath, - () => { - const send: SendToParent = (data) => { - const messageBuffer = Buffer.from(JSON.stringify(data)); - const lengthBuffer = Buffer.alloc(4); - lengthBuffer.writeInt32BE(messageBuffer.length, 0); - socket.write(Buffer.concat([lengthBuffer, messageBuffer])); - }; - resolve(send); - }, + () => resolve((data) => { + const messageBuffer = Buffer.from(JSON.stringify(data)); + const lengthBuffer = Buffer.alloc(4); + lengthBuffer.writeInt32BE(messageBuffer.length, 0); + socket.write(Buffer.concat([lengthBuffer, messageBuffer])); + }), ); - socket.on('error', reject); + /** + * Ignore error: + * - Called as a loader + * - Nested process when using --test + */ + socket.on('error', () => resolve()); // Prevent Node from waiting for this socket to close before exiting socket.unref(); diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts index bc96494b3..d0450159b 100644 --- a/tests/specs/cli.ts +++ b/tests/specs/cli.ts @@ -107,38 +107,38 @@ export default testSuite(({ describe }, node: NodeApis) => { const cliTestFlag = compareNodeVersion([18, 1, 0], nodeVersion) >= 0; const testRunnerGlob = compareNodeVersion([21, 0, 0], nodeVersion) >= 0; if (cliTestFlag) { - // test('Node.js test runner', async ({ onTestFinish }) => { - // const fixture = await createFixture({ - // 'test.ts': ` - // import { test } from 'node:test'; - // import assert from 'assert'; - - // test('some passing test', () => { - // assert.strictEqual(1, 1); - // }); - // `, - // }); - // onTestFinish(async () => await fixture.rm()); - - // const tsxProcess = await tsx( - // [ - // '--test', - // ...( - // testRunnerGlob - // ? [] - // : ['test.ts'] - // ), - // ], - // fixture.path, - // ); - - // expect(tsxProcess.exitCode).toBe(0); - // if (testRunnerGlob) { - // expect(tsxProcess.stdout).toMatch('some passing test\n'); - // } else { - // expect(tsxProcess.stdout).toMatch('# pass 1\n'); - // } - // }, 10_000); + test('Node.js test runner', async ({ onTestFinish }) => { + const fixture = await createFixture({ + 'test.ts': ` + import { test } from 'node:test'; + import assert from 'assert'; + + test('some passing test', () => { + assert.strictEqual(1, 1 as number); + }); + `, + }); + onTestFinish(async () => await fixture.rm()); + + const tsxProcess = await tsx( + [ + '--test', + ...( + testRunnerGlob + ? [] + : ['test.ts'] + ), + ], + fixture.path, + ); + + if (testRunnerGlob) { + expect(tsxProcess.stdout).toMatch('some passing test\n'); + } else { + expect(tsxProcess.stdout).toMatch('# pass 1\n'); + } + expect(tsxProcess.exitCode).toBe(0); + }, 10_000); } describe('Signals', async ({ describe, onFinish }) => { From 6e990ab21aa2fc51fde26f25be89bc3cfb3f73ac Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 12:51:55 +0900 Subject: [PATCH 10/28] wip --- src/cjs/index.ts | 4 ++-- src/esm/loaders.ts | 6 +++--- src/preflight.cts | 32 +++++++++++++++++--------------- src/utils/ipc/client.ts | 4 ++-- tests/specs/cli.ts | 24 ++++++++++++------------ 5 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/cjs/index.ts b/src/cjs/index.ts index d5dd4b5b2..801ef9155 100644 --- a/src/cjs/index.ts +++ b/src/cjs/index.ts @@ -13,7 +13,7 @@ import { transformSync } from '../utils/transform/index.js'; import { transformDynamicImport } from '../utils/transform/transform-dynamic-import.js'; import { resolveTsPath } from '../utils/resolve-ts-path.js'; import { isESM } from '../utils/esm-pattern.js'; -import { creatingClient, type SendToParent } from '../utils/ipc/client.js'; +import { connectingToServer, type SendToParent } from '../utils/ipc/client.js'; const isRelativePathPattern = /^\.{1,2}\//; const isTsFilePatten = /\.[cm]?tsx?$/; @@ -51,7 +51,7 @@ const transformExtensions = [ ]; let sendToClient: SendToParent | undefined; -creatingClient.then((c) => { +connectingToServer.then((c) => { sendToClient = c; }); diff --git a/src/esm/loaders.ts b/src/esm/loaders.ts index 400c12dc4..b82f7365c 100644 --- a/src/esm/loaders.ts +++ b/src/esm/loaders.ts @@ -9,7 +9,7 @@ import { transformDynamicImport } from '../utils/transform/transform-dynamic-imp import { resolveTsPath } from '../utils/resolve-ts-path.js'; import { installSourceMapSupport } from '../source-map.js'; import { importAttributes } from '../utils/node-features.js'; -import { creatingClient, type SendToParent } from '../utils/ipc/client.js'; +import { connectingToServer, type SendToParent } from '../utils/ipc/client.js'; import { tsconfigPathsMatcher, fileMatcher, @@ -221,8 +221,8 @@ export const resolve: resolve = async function ( } }; -let sendToParent: SendToParent | undefined; -creatingClient.then((c) => { +let sendToParent: SendToParent | void; +connectingToServer.then((c) => { sendToParent = c; }); diff --git a/src/preflight.cts b/src/preflight.cts index 5fe248585..df8f98793 100644 --- a/src/preflight.cts +++ b/src/preflight.cts @@ -1,6 +1,6 @@ import { constants as osConstants } from 'os'; import { isMainThread } from 'node:worker_threads'; -import { creatingClient } from './utils/ipc/client.js'; +import { connectingToServer } from './utils/ipc/client.js'; import './suppress-warnings.cjs'; type BaseEventListener = () => void; @@ -55,21 +55,23 @@ if (isMainThread) { require('./cjs/index.cjs'); (async () => { - const sendToClient = await creatingClient; + const sendToClient = await connectingToServer; - bindHiddenSignalsHandler(['SIGINT', 'SIGTERM'], (signal: NodeJS.Signals) => { - sendToClient({ - type: 'kill', - signal, + if (sendToClient) { + bindHiddenSignalsHandler(['SIGINT', 'SIGTERM'], (signal: NodeJS.Signals) => { + sendToClient({ + type: 'kill', + signal, + }); + + /** + * If the user has not registered a signal handler, we need to emulate + * the default behavior when there are no other handlers set + */ + if (process.listenerCount(signal) === 0) { + process.exit(128 + osConstants.signals[signal]); + } }); - - /** - * If the user has not registered a signal handler, we need to emulate - * the default behavior when there are no other handlers set - */ - if (process.listenerCount(signal) === 0) { - process.exit(128 + osConstants.signals[signal]); - } - }); + } })(); } diff --git a/src/utils/ipc/client.ts b/src/utils/ipc/client.ts index 7c76d4c4c..770995659 100644 --- a/src/utils/ipc/client.ts +++ b/src/utils/ipc/client.ts @@ -4,7 +4,7 @@ import { getPipePath } from './get-pipe-path.js'; export type SendToParent = (data: Record<string, unknown>) => void; // TODO: Handle when the loader is called directly -const createIpcClient = () => new Promise<SendToParent | void>((resolve) => { +const connectToServer = () => new Promise<SendToParent | void>((resolve) => { const pipePath = getPipePath(process.ppid); const socket: net.Socket = net.createConnection( pipePath, @@ -27,4 +27,4 @@ const createIpcClient = () => new Promise<SendToParent | void>((resolve) => { socket.unref(); }); -export const creatingClient = createIpcClient(); +export const connectingToServer = connectToServer(); diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts index d0450159b..109e326b3 100644 --- a/tests/specs/cli.ts +++ b/tests/specs/cli.ts @@ -273,17 +273,17 @@ export default testSuite(({ describe }, node: NodeApis) => { await tsxProcess; }, 10_000); - // describe('Ctrl + C', ({ test }) => { - // test('Exit code', async () => { - // const output = await ptyShell( - // [ - // `${tsxPath} ${path.join(fixture.path, 'keep-alive.js')}\r`, - // stdout => stdout.includes('READY') && '\u0003', - // `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, - // ], - // ); - // expect(output).toMatch(/EXIT_CODE:\s+130/); - // }, 10_000); + describe('Ctrl + C', ({ test }) => { + test('Exit code', async () => { + const output = await ptyShell( + [ + `${tsxPath} ${path.join(fixture.path, 'keep-alive.js')}\r`, + stdout => stdout.includes('READY') && '\u0003', + `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, + ], + ); + expect(output).toMatch(/EXIT_CODE:\s+130/); + }, 10_000); // test('Catchable', async () => { // const output = await ptyShell( @@ -302,7 +302,7 @@ export default testSuite(({ describe }, node: NodeApis) => { // /EXIT_CODE:\s+200/, // ]); // }, 10_000); - // }); + }); }); }); }); From d63d2a86c0200f3294e79bcb741324f0785c0cfb Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 12:52:18 +0900 Subject: [PATCH 11/28] wip --- tests/specs/cli.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts index 109e326b3..11fac794e 100644 --- a/tests/specs/cli.ts +++ b/tests/specs/cli.ts @@ -285,23 +285,23 @@ export default testSuite(({ describe }, node: NodeApis) => { expect(output).toMatch(/EXIT_CODE:\s+130/); }, 10_000); - // test('Catchable', async () => { - // const output = await ptyShell( - // [ - // `${tsxPath} ${path.join(fixture.path, 'catch-signals.js')}\r`, - // stdout => stdout.includes('READY') && '\u0003', - // `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, - // ], - // ); - - // expectMatchInOrder(output, [ - // 'READY\r\n', - // process.platform === 'win32' ? '' : '^C', - // 'SIGINT\r\n', - // 'SIGINT HANDLER COMPLETED\r\n', - // /EXIT_CODE:\s+200/, - // ]); - // }, 10_000); + test('Catchable', async () => { + const output = await ptyShell( + [ + `${tsxPath} ${path.join(fixture.path, 'catch-signals.js')}\r`, + stdout => stdout.includes('READY') && '\u0003', + `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, + ], + ); + + expectMatchInOrder(output, [ + 'READY\r\n', + process.platform === 'win32' ? '' : '^C', + 'SIGINT\r\n', + 'SIGINT HANDLER COMPLETED\r\n', + /EXIT_CODE:\s+200/, + ]); + }, 10_000); }); }); }); From f1f4d70b29628287fb19e46b3950cab7131e71af Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 12:55:32 +0900 Subject: [PATCH 12/28] wip --- src/cjs/index.ts | 10 +++++----- src/esm/loaders.ts | 4 ++-- tests/specs/cli.ts | 5 +++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/cjs/index.ts b/src/cjs/index.ts index 801ef9155..b7fe64e8c 100644 --- a/src/cjs/index.ts +++ b/src/cjs/index.ts @@ -50,9 +50,9 @@ const transformExtensions = [ '.mjs', ]; -let sendToClient: SendToParent | undefined; -connectingToServer.then((c) => { - sendToClient = c; +let sendToParent: SendToParent | void; +connectingToServer.then((_sendToParent) => { + sendToParent = _sendToParent; }); const transformer = ( @@ -60,8 +60,8 @@ const transformer = ( filePath: string, ) => { // For tracking dependencies in watch mode - if (sendToClient) { - sendToClient({ + if (sendToParent) { + sendToParent({ type: 'dependency', path: filePath, }); diff --git a/src/esm/loaders.ts b/src/esm/loaders.ts index b82f7365c..ba894c775 100644 --- a/src/esm/loaders.ts +++ b/src/esm/loaders.ts @@ -222,8 +222,8 @@ export const resolve: resolve = async function ( }; let sendToParent: SendToParent | void; -connectingToServer.then((c) => { - sendToParent = c; +connectingToServer.then((_sendToParent) => { + sendToParent = _sendToParent; }); const contextAttributesProperty = importAttributes ? 'importAttributes' : 'importAssertions'; diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts index 11fac794e..d95750ab5 100644 --- a/tests/specs/cli.ts +++ b/tests/specs/cli.ts @@ -274,11 +274,12 @@ export default testSuite(({ describe }, node: NodeApis) => { }, 10_000); describe('Ctrl + C', ({ test }) => { + const CtrlC = '\u0003'; test('Exit code', async () => { const output = await ptyShell( [ `${tsxPath} ${path.join(fixture.path, 'keep-alive.js')}\r`, - stdout => stdout.includes('READY') && '\u0003', + stdout => stdout.includes('READY') && CtrlC, `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, ], ); @@ -289,7 +290,7 @@ export default testSuite(({ describe }, node: NodeApis) => { const output = await ptyShell( [ `${tsxPath} ${path.join(fixture.path, 'catch-signals.js')}\r`, - stdout => stdout.includes('READY') && '\u0003', + stdout => stdout.includes('READY') && CtrlC, `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, ], ); From bf4595e718e65f46d0a3c7b165278da27e76e8b0 Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 12:58:39 +0900 Subject: [PATCH 13/28] wip --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 524781597..c0e6ed16d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,6 +58,6 @@ jobs: if: ${{ matrix.os == 'ubuntu-latest' }} run: pnpm type-check - - name: Lint - if: ${{ matrix.os == 'ubuntu-latest' }} - run: pnpm lint + # - name: Lint + # if: ${{ matrix.os == 'ubuntu-latest' }} + # run: pnpm lint From f473f79e4ebc7bd1e3d628b59e3b357dd3ef5a3b Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 13:02:17 +0900 Subject: [PATCH 14/28] wip --- src/preflight.cts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/preflight.cts b/src/preflight.cts index df8f98793..e5035cb1f 100644 --- a/src/preflight.cts +++ b/src/preflight.cts @@ -55,11 +55,11 @@ if (isMainThread) { require('./cjs/index.cjs'); (async () => { - const sendToClient = await connectingToServer; + const sendToParent = await connectingToServer; - if (sendToClient) { + if (sendToParent) { bindHiddenSignalsHandler(['SIGINT', 'SIGTERM'], (signal: NodeJS.Signals) => { - sendToClient({ + sendToParent({ type: 'kill', signal, }); From 75cefc74124834fdf8edcee7307a1d50466a5c13 Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 15:36:08 +0900 Subject: [PATCH 15/28] wip --- src/preflight.cts | 2 +- tests/specs/cli.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/preflight.cts b/src/preflight.cts index e5035cb1f..bd4d70434 100644 --- a/src/preflight.cts +++ b/src/preflight.cts @@ -63,7 +63,7 @@ if (isMainThread) { type: 'kill', signal, }); - + /** * If the user has not registered a signal handler, we need to emulate * the default behavior when there are no other handlers set diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts index d95750ab5..cad6e0549 100644 --- a/tests/specs/cli.ts +++ b/tests/specs/cli.ts @@ -278,7 +278,8 @@ export default testSuite(({ describe }, node: NodeApis) => { test('Exit code', async () => { const output = await ptyShell( [ - `${tsxPath} ${path.join(fixture.path, 'keep-alive.js')}\r`, + // Windows doesn't support shebangs + `${node.path} ${tsxPath} ${path.join(fixture.path, 'keep-alive.js')}\r`, stdout => stdout.includes('READY') && CtrlC, `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, ], @@ -289,7 +290,7 @@ export default testSuite(({ describe }, node: NodeApis) => { test('Catchable', async () => { const output = await ptyShell( [ - `${tsxPath} ${path.join(fixture.path, 'catch-signals.js')}\r`, + `${node.path} ${tsxPath} ${path.join(fixture.path, 'catch-signals.js')}\r`, stdout => stdout.includes('READY') && CtrlC, `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, ], From 7ade2327f284ae3c71c1c99371a320f0ba45ed2e Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 17:18:51 +0900 Subject: [PATCH 16/28] wip --- tests/specs/cli.ts | 38 ++++++++++++++++++++++++++++------ tests/utils/pty-shell/index.ts | 13 ++++++------ 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts index cad6e0549..1d303a5e6 100644 --- a/tests/specs/cli.ts +++ b/tests/specs/cli.ts @@ -9,6 +9,14 @@ import { expectMatchInOrder } from '../utils/expect-match-in-order.js'; import type { NodeApis } from '../utils/tsx.js'; import { compareNodeVersion, type Version } from '../../src/utils/node-features.js'; +const isProcessAlive = (pid: number) => { + try { + process.kill(pid, 0); + return true; + } catch {} + return false; +}; + export default testSuite(({ describe }, node: NodeApis) => { const { tsx } = node; describe('CLI', ({ describe, test }) => { @@ -239,10 +247,13 @@ export default testSuite(({ describe }, node: NodeApis) => { forceKillAfterTimeout: false, }); - await tsxProcess; + const result = await tsxProcess; + + // TODO: Is this correct? + expect(result.exitCode).toBe(0); // Enforce that child process is killed - expect(() => process.kill(childPid!, 0)).toThrow(); + expect(isProcessAlive(childPid!)).toBe(false); }, 10_000); test('Doesn\'t kill child when responsive (ignores signal)', async () => { @@ -265,12 +276,15 @@ export default testSuite(({ describe }, node: NodeApis) => { if (process.platform === 'win32') { // Enforce that child process is killed - expect(() => process.kill(childPid!, 0)).toThrow(); + expect(isProcessAlive(childPid!)).toBe(false); } else { - // Kill child process - expect(() => process.kill(childPid!, 'SIGKILL')).not.toThrow(); + expect(isProcessAlive(childPid!)).toBe(true); + process.kill(childPid!, 'SIGKILL'); } - await tsxProcess; + const result = await tsxProcess; + + // TODO: Is this correct? + expect(result.exitCode).toBe(0); }, 10_000); describe('Ctrl + C', ({ test }) => { @@ -304,6 +318,18 @@ export default testSuite(({ describe }, node: NodeApis) => { /EXIT_CODE:\s+200/, ]); }, 10_000); + + test('Infinite loop', async () => { + const output = await ptyShell( + [ + // Windows doesn't support shebangs + `${node.path} ${tsxPath} ${path.join(fixture.path, 'infinite-loop.js')}\r`, + stdout => /\d+\r\n/.test(stdout) && CtrlC, + `echo EXIT_CODE: ${isWindows ? '$LastExitCode' : '$?'}\r`, + ], + ); + expect(output).toMatch(/EXIT_CODE:\s+0/); + }, 10_000); }); }); }); diff --git a/tests/utils/pty-shell/index.ts b/tests/utils/pty-shell/index.ts index eeef888f2..f6557c8de 100644 --- a/tests/utils/pty-shell/index.ts +++ b/tests/utils/pty-shell/index.ts @@ -27,7 +27,7 @@ export const ptyShell = ( fileURLToPath(new URL('node-pty.mjs', import.meta.url)), [shell], { - stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + stdio: 'pipe', }, ); @@ -35,10 +35,10 @@ export const ptyShell = ( let currentStdin = getStdin(stdins); - const output: Buffer[] = []; + let buffer = Buffer.alloc(0); childProcess.stdout!.on('data', (data) => { - output.push(data); - const outString = data.toString(); + buffer = Buffer.concat([buffer, data]); + const outString = stripAnsi(data.toString()); if (currentStdin) { const stdin = currentStdin(outString); @@ -47,7 +47,7 @@ export const ptyShell = ( currentStdin = getStdin(stdins); } } else if (outString.includes(commandCaret)) { - childProcess.kill('SIGTERM'); + childProcess.kill('SIGKILL'); } }); @@ -56,8 +56,7 @@ export const ptyShell = ( }); childProcess.on('exit', () => { - let outString = Buffer.concat(output).toString(); - outString = stripAnsi(outString); + const outString = stripAnsi(buffer.toString()); resolve(outString); }); }); From ff01de69a26d40c2e8a241f8a574d9c39e573b00 Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Sun, 26 Nov 2023 22:15:49 +0900 Subject: [PATCH 17/28] wip --- src/cli.ts | 2 +- src/preflight.cts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 1145bc456..3641b9333 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -24,7 +24,7 @@ const relaySignals = ( ipcSocket.on('data', (data: { type: string; signal: NodeJS.Signals }) => { if ( data - && data.type === 'kill' + && data.type === 'signal' && waitForSignal ) { waitForSignal(data.signal); diff --git a/src/preflight.cts b/src/preflight.cts index bd4d70434..34d5b2df3 100644 --- a/src/preflight.cts +++ b/src/preflight.cts @@ -60,7 +60,7 @@ if (isMainThread) { if (sendToParent) { bindHiddenSignalsHandler(['SIGINT', 'SIGTERM'], (signal: NodeJS.Signals) => { sendToParent({ - type: 'kill', + type: 'signal', signal, }); From 13ec7052100e8b97f103852576a7c03610babee6 Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Mon, 27 Nov 2023 02:36:47 +0900 Subject: [PATCH 18/28] wip --- src/cli.ts | 1 - src/utils/ipc/client.ts | 20 ++++++---- src/utils/ipc/get-pipe-path.ts | 1 + src/utils/ipc/server.ts | 71 +++++++++++++++++++--------------- 4 files changed, 54 insertions(+), 39 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 3641b9333..b9f1acbe0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -215,7 +215,6 @@ cli({ childProcess.on( 'close', (exitCode) => { - // ipc.close(); // If there's no exit code, it's likely killed by a signal // https://nodejs.org/api/process.html#process_exit_codes diff --git a/src/utils/ipc/client.ts b/src/utils/ipc/client.ts index 770995659..aed1089cd 100644 --- a/src/utils/ipc/client.ts +++ b/src/utils/ipc/client.ts @@ -8,12 +8,15 @@ const connectToServer = () => new Promise<SendToParent | void>((resolve) => { const pipePath = getPipePath(process.ppid); const socket: net.Socket = net.createConnection( pipePath, - () => resolve((data) => { - const messageBuffer = Buffer.from(JSON.stringify(data)); - const lengthBuffer = Buffer.alloc(4); - lengthBuffer.writeInt32BE(messageBuffer.length, 0); - socket.write(Buffer.concat([lengthBuffer, messageBuffer])); - }), + () => { + console.log('connected to server!'); + resolve((data) => { + const messageBuffer = Buffer.from(JSON.stringify(data)); + const lengthBuffer = Buffer.alloc(4); + lengthBuffer.writeInt32BE(messageBuffer.length, 0); + socket.write(Buffer.concat([lengthBuffer, messageBuffer])); + }); + }, ); /** @@ -21,7 +24,10 @@ const connectToServer = () => new Promise<SendToParent | void>((resolve) => { * - Called as a loader * - Nested process when using --test */ - socket.on('error', () => resolve()); + socket.on('error', (error) => { + console.warn(error); + resolve(); + }); // Prevent Node from waiting for this socket to close before exiting socket.unref(); diff --git a/src/utils/ipc/get-pipe-path.ts b/src/utils/ipc/get-pipe-path.ts index f09d1a2d3..86ab48a6e 100644 --- a/src/utils/ipc/get-pipe-path.ts +++ b/src/utils/ipc/get-pipe-path.ts @@ -1,6 +1,7 @@ import path from 'path'; import { tmpdir } from '../temporary-directory.js'; +console.log(tmpdir); export const getPipePath = (processId: number) => { const pipePath = path.join(tmpdir, `${processId}.pipe`); return ( diff --git a/src/utils/ipc/server.ts b/src/utils/ipc/server.ts index ac93b49e6..cd611dc6b 100644 --- a/src/utils/ipc/server.ts +++ b/src/utils/ipc/server.ts @@ -1,46 +1,55 @@ import net from 'net'; -import fs from 'fs/promises'; +import fs from 'fs'; import { tmpdir } from '../temporary-directory.js'; import { getPipePath } from './get-pipe-path.js'; +type OnMessage = (message: Buffer) => void; + +const bufferData = ( + onMessage: OnMessage, +) => { + let buffer = Buffer.alloc(0); + return (data: Buffer) => { + buffer = Buffer.concat([buffer, data]); + + while (buffer.length > 4) { + const messageLength = buffer.readInt32BE(0); + if (buffer.length >= 4 + messageLength) { + const message = buffer.slice(4, 4 + messageLength); + onMessage(message); + buffer = buffer.slice(4 + messageLength); + } else { + break; + } + } + }; +}; + export const createIpcServer = () => new Promise<net.Server>(async (resolve, reject) => { const server = net.createServer((socket) => { - let buffer = Buffer.alloc(0); - - const handleIncomingMessage = (message: Buffer) => { + socket.on('data', bufferData((message: Buffer) => { const data = JSON.parse(message.toString()); server.emit('data', data); - }; - - socket.on('data', (data) => { - buffer = Buffer.concat([buffer, data]); - - while (buffer.length > 4) { - const messageLength = buffer.readInt32BE(0); - if (buffer.length >= 4 + messageLength) { - const message = buffer.slice(4, 4 + messageLength); - handleIncomingMessage(message); - buffer = buffer.slice(4 + messageLength); - } else { - break; - } - } - - // console.log({ data, string: data.toString() }); - // server.emit('data', JSON.parse(data)); - }); - }); - - await fs.mkdir(tmpdir, { - recursive: true, + })); }); const pipePath = getPipePath(process.pid); - server.listen(pipePath, () => resolve(server)); + + await fs.promises.mkdir(tmpdir, { recursive: true }); + server.listen(pipePath, () => { + resolve(server); + }); server.on('error', reject); - // // Prevent Node from waiting for this socket to close before exiting - // server.unref(); + // Prevent Node from waiting for this socket to close before exiting + server.unref(); - // TODO: servver close + process.on('exit', () => { + server.close(); + try { + fs.rmSync(pipePath); + } catch (error) { + console.log('Failed to remove pipe', error); + } + }); }); From ce3f5605e1f50e3e86b4a98466ed0bd56c25ce5e Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Mon, 27 Nov 2023 02:55:50 +0900 Subject: [PATCH 19/28] wip --- src/utils/ipc/client.ts | 4 +--- src/utils/ipc/get-pipe-path.ts | 1 - src/utils/ipc/server.ts | 17 ++++++++--------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/utils/ipc/client.ts b/src/utils/ipc/client.ts index aed1089cd..a7e4a1d67 100644 --- a/src/utils/ipc/client.ts +++ b/src/utils/ipc/client.ts @@ -9,7 +9,6 @@ const connectToServer = () => new Promise<SendToParent | void>((resolve) => { const socket: net.Socket = net.createConnection( pipePath, () => { - console.log('connected to server!'); resolve((data) => { const messageBuffer = Buffer.from(JSON.stringify(data)); const lengthBuffer = Buffer.alloc(4); @@ -24,8 +23,7 @@ const connectToServer = () => new Promise<SendToParent | void>((resolve) => { * - Called as a loader * - Nested process when using --test */ - socket.on('error', (error) => { - console.warn(error); + socket.on('error', () => { resolve(); }); diff --git a/src/utils/ipc/get-pipe-path.ts b/src/utils/ipc/get-pipe-path.ts index 86ab48a6e..f09d1a2d3 100644 --- a/src/utils/ipc/get-pipe-path.ts +++ b/src/utils/ipc/get-pipe-path.ts @@ -1,7 +1,6 @@ import path from 'path'; import { tmpdir } from '../temporary-directory.js'; -console.log(tmpdir); export const getPipePath = (processId: number) => { const pipePath = path.join(tmpdir, `${processId}.pipe`); return ( diff --git a/src/utils/ipc/server.ts b/src/utils/ipc/server.ts index cd611dc6b..5f7097dbe 100644 --- a/src/utils/ipc/server.ts +++ b/src/utils/ipc/server.ts @@ -38,18 +38,17 @@ export const createIpcServer = () => new Promise<net.Server>(async (resolve, rej await fs.promises.mkdir(tmpdir, { recursive: true }); server.listen(pipePath, () => { resolve(server); + + process.on('exit', () => { + server.close(); + + try { + fs.rmSync(pipePath); + } catch {} + }); }); server.on('error', reject); // Prevent Node from waiting for this socket to close before exiting server.unref(); - - process.on('exit', () => { - server.close(); - try { - fs.rmSync(pipePath); - } catch (error) { - console.log('Failed to remove pipe', error); - } - }); }); From cf704e3b1bfa3a2432d34ef29f4dbbd6acc76d48 Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Mon, 27 Nov 2023 03:00:06 +0900 Subject: [PATCH 20/28] wip --- src/utils/ipc/server.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils/ipc/server.ts b/src/utils/ipc/server.ts index 5f7097dbe..23509648c 100644 --- a/src/utils/ipc/server.ts +++ b/src/utils/ipc/server.ts @@ -44,7 +44,11 @@ export const createIpcServer = () => new Promise<net.Server>(async (resolve, rej try { fs.rmSync(pipePath); - } catch {} + } catch (err) { + if (process.platform === 'win32') { + console.log(111, err); + } + } }); }); server.on('error', reject); From 1869febd23cb23b754612f6214034f2cacf50588 Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Mon, 27 Nov 2023 13:12:01 +0900 Subject: [PATCH 21/28] wip --- src/utils/ipc/client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/ipc/client.ts b/src/utils/ipc/client.ts index a7e4a1d67..cdd6f23af 100644 --- a/src/utils/ipc/client.ts +++ b/src/utils/ipc/client.ts @@ -19,9 +19,9 @@ const connectToServer = () => new Promise<SendToParent | void>((resolve) => { ); /** - * Ignore error: - * - Called as a loader - * - Nested process when using --test + * Ignore error when: + * - Called as a loader and there is no server + * - Nested process when using --test and the ppid is incorrect */ socket.on('error', () => { resolve(); From ae1505c49b194bfff875eb78ead7e315b5c745ad Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Mon, 27 Nov 2023 16:44:16 +0900 Subject: [PATCH 22/28] style: lint fix --- src/cjs/index.ts | 9 ++++--- src/esm/loaders.ts | 9 ++++--- src/utils/ipc/server.ts | 56 +++++++++++++++++++++-------------------- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/src/cjs/index.ts b/src/cjs/index.ts index b7fe64e8c..57f273724 100644 --- a/src/cjs/index.ts +++ b/src/cjs/index.ts @@ -51,9 +51,12 @@ const transformExtensions = [ ]; let sendToParent: SendToParent | void; -connectingToServer.then((_sendToParent) => { - sendToParent = _sendToParent; -}); +connectingToServer.then( + (_sendToParent) => { + sendToParent = _sendToParent; + }, + () => {}, +); const transformer = ( module: Module, diff --git a/src/esm/loaders.ts b/src/esm/loaders.ts index ba894c775..95db51fff 100644 --- a/src/esm/loaders.ts +++ b/src/esm/loaders.ts @@ -222,9 +222,12 @@ export const resolve: resolve = async function ( }; let sendToParent: SendToParent | void; -connectingToServer.then((_sendToParent) => { - sendToParent = _sendToParent; -}); +connectingToServer.then( + (_sendToParent) => { + sendToParent = _sendToParent; + }, + () => {}, +); const contextAttributesProperty = importAttributes ? 'importAttributes' : 'importAssertions'; diff --git a/src/utils/ipc/server.ts b/src/utils/ipc/server.ts index 29ca51ed4..73eb10076 100644 --- a/src/utils/ipc/server.ts +++ b/src/utils/ipc/server.ts @@ -25,7 +25,7 @@ const bufferData = ( }; }; -export const createIpcServer = () => new Promise<net.Server>(async (resolve, reject) => { +export const createIpcServer = async () => { const server = net.createServer((socket) => { socket.on('data', bufferData((message: Buffer) => { const data = JSON.parse(message.toString()); @@ -34,35 +34,37 @@ export const createIpcServer = () => new Promise<net.Server>(async (resolve, rej }); const pipePath = getPipePath(process.pid); - await fs.promises.mkdir(tmpdir, { recursive: true }); - server.listen(pipePath, () => { - resolve(server); - - process.on('exit', () => { - server.close(); - /** - * Only clean on Unix - * - * https://nodejs.org/api/net.html#ipc-support: - * On Windows, the local domain is implemented using a named pipe. - * The path must refer to an entry in \\?\pipe\ or \\.\pipe\. - * Any characters are permitted, but the latter may do some processing - * of pipe names, such as resolving .. sequences. Despite how it might - * look, the pipe namespace is flat. Pipes will not persist. They are - * removed when the last reference to them is closed. Unlike Unix domain - * sockets, Windows will close and remove the pipe when the owning process exits. - */ - if (process.platform !== 'win32') { - try { - fs.rmSync(pipePath); - } catch {} - } - }); + await new Promise<void>((resolve, reject) => { + server.listen(pipePath, resolve); + server.on('error', reject); }); - server.on('error', reject); // Prevent Node from waiting for this socket to close before exiting server.unref(); -}); + + process.on('exit', () => { + server.close(); + + /** + * Only clean on Unix + * + * https://nodejs.org/api/net.html#ipc-support: + * On Windows, the local domain is implemented using a named pipe. + * The path must refer to an entry in \\?\pipe\ or \\.\pipe\. + * Any characters are permitted, but the latter may do some processing + * of pipe names, such as resolving .. sequences. Despite how it might + * look, the pipe namespace is flat. Pipes will not persist. They are + * removed when the last reference to them is closed. Unlike Unix domain + * sockets, Windows will close and remove the pipe when the owning process exits. + */ + if (process.platform !== 'win32') { + try { + fs.rmSync(pipePath); + } catch {} + } + }); + + return server; +}; From b8d468ff7e371c754cc0f016fea0e8f41e6ba5b6 Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Mon, 27 Nov 2023 22:55:12 +0900 Subject: [PATCH 23/28] wip --- .github/workflows/test.yml | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c0e6ed16d..524781597 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,6 +58,6 @@ jobs: if: ${{ matrix.os == 'ubuntu-latest' }} run: pnpm type-check - # - name: Lint - # if: ${{ matrix.os == 'ubuntu-latest' }} - # run: pnpm lint + - name: Lint + if: ${{ matrix.os == 'ubuntu-latest' }} + run: pnpm lint diff --git a/package.json b/package.json index 0dc3c05c6..c82e3b6ad 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "bin": "./dist/cli.mjs", "scripts": { "prepare": "pnpm simple-git-hooks", - "build": "pkgroll --target=node12.19", + "build": "pkgroll --target=node12.19 --minify", "lint": "eslint --cache .", "type-check": "tsc --noEmit", "test": "pnpm build && node ./dist/cli.mjs tests/index.ts", From 617bc63f50e65cbfe173b37b9fab7bfe8ff9365c Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Mon, 27 Nov 2023 23:07:26 +0900 Subject: [PATCH 24/28] wip --- src/run.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/run.ts b/src/run.ts index 7ea4b000e..958ff5cee 100644 --- a/src/run.ts +++ b/src/run.ts @@ -1,3 +1,4 @@ +import type { StdioOptions } from 'child_process'; import { pathToFileURL } from 'url'; import spawn from 'cross-spawn'; import { supportsModuleRegister } from './utils/node-features'; @@ -11,6 +12,12 @@ export const run = ( }, ) => { const environment = { ...process.env }; + const stdio: StdioOptions = [ + 'inherit', // stdin + 'inherit', // stdout + 'inherit', // stderr + 'ipc', // parent-child communication + ]; if (options) { if (options.noCache) { @@ -34,7 +41,7 @@ export const run = ( ...argv, ], { - stdio: 'inherit', + stdio, env: environment, }, ); From cdb382ccc3fc2d09dc25526dac03b0749d8f1340 Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Mon, 27 Nov 2023 23:54:27 +0900 Subject: [PATCH 25/28] wip --- src/cli.ts | 3 +++ src/run.ts | 6 +++++- tests/specs/cli.ts | 19 ++++++++----------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index d7259ccd8..e75cfea04 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -216,6 +216,9 @@ cli({ childProcess.on('message', (message) => { process.send!(message); }); + } + + if (childProcess.send) { process.on('message', (message) => { childProcess.send(message as Serializable); }); diff --git a/src/run.ts b/src/run.ts index 958ff5cee..f8fef0c7f 100644 --- a/src/run.ts +++ b/src/run.ts @@ -16,9 +16,13 @@ export const run = ( 'inherit', // stdin 'inherit', // stdout 'inherit', // stderr - 'ipc', // parent-child communication ]; + // If parent process spawns tsx with ipc, spawn child with ipc + if (process.send) { + stdio.push('ipc'); + } + if (options) { if (options.noCache) { environment.TSX_DISABLE_CACHE = '1'; diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts index ff354815c..e35c7a801 100644 --- a/tests/specs/cli.ts +++ b/tests/specs/cli.ts @@ -345,20 +345,17 @@ export default testSuite(({ describe }, node: NodeApis) => { }); }); - // Relays to child test('relays messages to child', async ({ onTestFinish }) => { const fixture = await createFixture({ 'file.js': ` - console.log('READY'); process.on('message', (received) => { - console.log({ received }); - process.send(received); + process.send('goodbye'); + process.exit(); }); `, }); - console.log(fixture); - // onTestFinish(async () => await fixture.rm()); + onTestFinish(async () => await fixture.rm()); const tsxProcess = execa(tsxPath, ['file.js'], { cwd: fixture.path, @@ -366,13 +363,13 @@ export default testSuite(({ describe }, node: NodeApis) => { reject: false, }); - tsxProcess.on('message', (message) => { - console.log('from test', message); - // tsxProcess.kill(); - }); tsxProcess.send('hello'); + const received = await new Promise((resolve) => { + tsxProcess.once('message', resolve); + }); + expect(received).toBe('goodbye'); - console.log(await tsxProcess); + await tsxProcess; }); }); }); From 6c970ecc8e701f341d6e147acda1a61b0bac7759 Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Tue, 28 Nov 2023 08:10:53 +0900 Subject: [PATCH 26/28] wip --- tests/specs/cli.ts | 5 ++--- tests/utils/tsx.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts index e35c7a801..21f17311e 100644 --- a/tests/specs/cli.ts +++ b/tests/specs/cli.ts @@ -2,7 +2,6 @@ import path from 'path'; import { setTimeout } from 'timers/promises'; import { testSuite, expect } from 'manten'; import { createFixture } from 'fs-fixture'; -import { execa } from 'execa'; import packageJson from '../../package.json'; import { ptyShell, isWindows } from '../utils/pty-shell/index'; import { expectMatchInOrder } from '../utils/expect-match-in-order.js'; @@ -345,7 +344,7 @@ export default testSuite(({ describe }, node: NodeApis) => { }); }); - test('relays messages to child', async ({ onTestFinish }) => { + test('relays ipc message to child and back', async ({ onTestFinish }) => { const fixture = await createFixture({ 'file.js': ` process.on('message', (received) => { @@ -357,7 +356,7 @@ export default testSuite(({ describe }, node: NodeApis) => { onTestFinish(async () => await fixture.rm()); - const tsxProcess = execa(tsxPath, ['file.js'], { + const tsxProcess = tsx(['file.js'], { cwd: fixture.path, stdio: ['ipc'], reject: false, diff --git a/tests/utils/tsx.ts b/tests/utils/tsx.ts index 7722b69b2..35dd8508a 100644 --- a/tests/utils/tsx.ts +++ b/tests/utils/tsx.ts @@ -1,5 +1,5 @@ import { fileURLToPath } from 'url'; -import { execaNode } from 'execa'; +import { execaNode, type NodeOptions } from 'execa'; import getNode from 'get-node'; import { compareNodeVersion, type Version } from './node-features.js'; @@ -62,12 +62,11 @@ export const createNode = async ( tsx: ( args: string[], - cwd?: string, + cwdOrOptions?: string | NodeOptions, ) => execaNode( tsxPath, args, { - cwd, env: { TSX_DISABLE_CACHE: '1', DEBUG: '1', @@ -76,6 +75,11 @@ export const createNode = async ( nodeOptions: [], reject: false, all: true, + ...( + typeof cwdOrOptions === 'string' + ? { cwd: cwdOrOptions } + : cwdOrOptions + ), }, ), From 254bcc1a8456d402c4306590523f25eb09172bbd Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Tue, 28 Nov 2023 08:21:10 +0900 Subject: [PATCH 27/28] wip --- src/utils/ipc/client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/ipc/client.ts b/src/utils/ipc/client.ts index cdd6f23af..a395d174a 100644 --- a/src/utils/ipc/client.ts +++ b/src/utils/ipc/client.ts @@ -3,7 +3,6 @@ import { getPipePath } from './get-pipe-path.js'; export type SendToParent = (data: Record<string, unknown>) => void; -// TODO: Handle when the loader is called directly const connectToServer = () => new Promise<SendToParent | void>((resolve) => { const pipePath = getPipePath(process.ppid); const socket: net.Socket = net.createConnection( From 64fb7830b56734c75d36887745bff8c0e8c22f9c Mon Sep 17 00:00:00 2001 From: Hiroki Osame <hiroki.osame@gmail.com> Date: Tue, 28 Nov 2023 08:40:44 +0900 Subject: [PATCH 28/28] wip --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0dc3c05c6..c82e3b6ad 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "bin": "./dist/cli.mjs", "scripts": { "prepare": "pnpm simple-git-hooks", - "build": "pkgroll --target=node12.19", + "build": "pkgroll --target=node12.19 --minify", "lint": "eslint --cache .", "type-check": "tsc --noEmit", "test": "pnpm build && node ./dist/cli.mjs tests/index.ts",