diff --git a/changelog.md b/changelog.md index f4c0f1a..25d5303 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-10-16 - 1.5.0 - feat(core) +Add cwd option and child-process execution for custom working directory; implement signal-forwarding child runner; update docs and bump package version to 1.4.0 + +- Introduce IRunOptions with cwd support to runPath/runCli +- When cwd is provided, runCli now spawns a child process (runInChildProcess) to execute the script in the specified working directory +- runInChildProcess preserves node execArgv, inherits env and stdio, forwards signals (SIGINT, SIGTERM, SIGHUP) and propagates child exit codes/signals +- Update README with documentation and examples for running scripts with a custom working directory and parallel execution +- Bump package version to 1.4.0 + ## 2025-10-13 - 1.3.4 - fix(docs) Update README with expanded docs and examples; add pnpm and CI tooling configs diff --git a/package.json b/package.json index 2089947..163f071 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@git.zone/tsrun", - "version": "1.3.4", + "version": "1.4.0", "description": "run typescript programs efficiently", "main": "dist_ts/index.js", "typings": "dist_ts/index.d.ts", diff --git a/readme.md b/readme.md index 21ea7c1..5cfc006 100644 --- a/readme.md +++ b/readme.md @@ -67,6 +67,8 @@ await runCli('./myScript.ts'); 🎯 **TypeScript Native** - Full TypeScript support with excellent IntelliSense. +🔀 **Custom Working Directory** - Execute scripts with different cwds for parallel multi-project workflows. + ## Why tsrun? Sometimes you just want to run a TypeScript file without setting up a build pipeline, configuring webpack, or waiting for `tsc` to compile. That's where tsrun shines: @@ -141,6 +143,35 @@ for (const script of scripts) { } ``` +### Running with Custom Working Directory + +Execute TypeScript files with a different working directory using the `cwd` option. This is especially useful for parallel execution across multiple projects: + +```typescript +import { runPath } from '@git.zone/tsrun'; + +// Run with custom cwd +await runPath('./build.ts', undefined, { cwd: '/path/to/project-a' }); + +// Parallel execution with different cwds (safe and isolated) +await Promise.all([ + runPath('./deploy.ts', undefined, { cwd: '/projects/frontend' }), + runPath('./deploy.ts', undefined, { cwd: '/projects/backend' }), + runPath('./deploy.ts', undefined, { cwd: '/projects/api' }) +]); +``` + +**How it works:** +- When `cwd` is provided, the script executes in a **child process** for complete isolation +- Without `cwd`, execution happens **in-process** (faster, less overhead) +- Child processes inherit all environment variables and stdio connections +- Perfect for running the same script across multiple project directories + +**Notes:** +- Output from parallel executions may interleave on the console +- Each child process runs with its own isolated working directory +- Exit codes and signals are properly forwarded + ## Package Information - **npmjs**: [@git.zone/tsrun](https://www.npmjs.com/package/@git.zone/tsrun) diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 56e65b0..ad440e2 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/tsrun', - version: '1.3.4', + version: '1.5.0', description: 'run typescript programs efficiently' } diff --git a/ts/index.ts b/ts/index.ts index ad08fc2..0339093 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -1,14 +1,24 @@ import * as plugins from './plugins.js'; const __dirname = plugins.path.dirname(plugins.url.fileURLToPath(import.meta.url)); -export const runPath = async (pathArg: string, fromFileUrl?: string) => { +export interface IRunOptions { + cwd?: string; +} + +export const runPath = async (pathArg: string, fromFileUrl?: string, options?: IRunOptions) => { pathArg = fromFileUrl ? plugins.path.join(plugins.path.dirname(plugins.url.fileURLToPath(fromFileUrl)), pathArg) : pathArg; - await runCli(pathArg); + await runCli(pathArg, options); }; -export const runCli = async (pathArg?: string) => { +export const runCli = async (pathArg?: string, options?: IRunOptions) => { + // CRITICAL: Branch BEFORE splicing argv to avoid corruption + if (options?.cwd) { + return runInChildProcess(pathArg, options.cwd); + } + + // Existing in-process execution // contents of argv array // process.argv[0] -> node Executable // process.argv[1] -> tsrun executable @@ -26,3 +36,62 @@ export const runCli = async (pathArg?: string) => { const unregister = tsx.register(); await import(absolutePathToTsFile); }; + +const runInChildProcess = async (pathArg: string | undefined, cwd: string): Promise => { + const { spawn } = await import('child_process'); + + // Resolve cli.child.js relative to this file + const cliChildPath = plugins.path.join(__dirname, '../cli.child.js'); + + // Build args: [Node flags, entry point, script path, script args] + const args = [ + ...process.execArgv, // Preserve --inspect, etc. + cliChildPath, + ...process.argv.slice(2) // Original CLI args (not spliced) + ]; + + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, args, { + cwd: cwd, + env: process.env, + stdio: 'inherit', + shell: false, + windowsHide: false + }); + + // Signal forwarding with cleanup + const signalHandler = (signal: NodeJS.Signals) => { + try { child.kill(signal); } catch {} + }; + + const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGHUP']; + signals.forEach(sig => process.on(sig, signalHandler)); + + child.on('error', (err) => { + signals.forEach(sig => process.off(sig, signalHandler)); + reject(err); + }); + + child.on('close', (code, signal) => { + // Clean up signal handlers + signals.forEach(sig => process.off(sig, signalHandler)); + + if (signal) { + // Child was terminated by signal + // On POSIX: try to exit with same signal + // On Windows: exit with convention (128 + signal number) + try { + process.kill(process.pid, signal); + } catch { + // Fallback to exit code + const signalExitCode = signal === 'SIGINT' ? 130 : 128; + process.exit(signalExitCode); + } + } else if (code !== null && code !== 0) { + process.exit(code); + } else { + resolve(); + } + }); + }); +};