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",