Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
6b6ecee0ed | |||
e5b57c894b | |||
ec2db7af72 | |||
9b5668eccb | |||
528a56c358 | |||
7fb2389e3a |
26
changelog.md
26
changelog.md
@@ -1,5 +1,31 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-10-17 - 1.6.1 - fix(plugins)
|
||||||
|
Export child_process.spawn from plugins and use plugins.spawn in spawnPath to remove direct require and unify process spawning
|
||||||
|
|
||||||
|
- Exported spawn from ts/plugins.ts so native child_process.spawn is available via the plugins module
|
||||||
|
- Removed require('child_process') from ts/index.ts and switched to plugins.spawn when spawning child processes in spawnPath
|
||||||
|
- No public API changes; this unifies internal imports and fixes inconsistent spawn usage that could cause runtime issues
|
||||||
|
|
||||||
|
## 2025-10-17 - 1.6.0 - feat(core)
|
||||||
|
Add spawnPath child-process API with timeout/abort/terminate support, export native types, and expand README
|
||||||
|
|
||||||
|
- Implement spawnPath(filePath, fromFileUrl?, options?) in ts/index.ts producing an ITsrunChildProcess with childProcess, stdout, stderr, exitCode, kill() and terminate()
|
||||||
|
- Introduce ISpawnOptions (cwd, env, args, stdio, timeout, signal) and ITsrunChildProcess interfaces for robust process control
|
||||||
|
- Handle timeouts (auto SIGTERM), AbortSignal cancellation, and graceful terminate() (SIGTERM then SIGKILL after 5s)
|
||||||
|
- Export Node types ChildProcess and Readable from ts/plugins.ts for improved typings
|
||||||
|
- Greatly expand README: add badges, table of contents, detailed API docs and examples for runPath, runCli and spawnPath, and troubleshooting guidance
|
||||||
|
- Add local .claude/settings.local.json (environment/settings file)
|
||||||
|
|
||||||
|
## 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)
|
## 2025-10-13 - 1.3.4 - fix(docs)
|
||||||
Update README with expanded docs and examples; add pnpm and CI tooling configs
|
Update README with expanded docs and examples; add pnpm and CI tooling configs
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tsrun",
|
"name": "@git.zone/tsrun",
|
||||||
"version": "1.3.4",
|
"version": "1.6.1",
|
||||||
"description": "run typescript programs efficiently",
|
"description": "run typescript programs efficiently",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
|
321
readme.md
321
readme.md
@@ -1,9 +1,31 @@
|
|||||||
# @git.zone/tsrun
|
# @git.zone/tsrun
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/@git.zone/tsrun)
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](https://nodejs.org/)
|
||||||
|
|
||||||
> Run TypeScript files instantly, without the compilation hassle ⚡
|
> Run TypeScript files instantly, without the compilation hassle ⚡
|
||||||
|
|
||||||
Execute TypeScript programs on-the-fly with zero configuration. Perfect for scripts, prototyping, and development workflows.
|
Execute TypeScript programs on-the-fly with zero configuration. Perfect for scripts, prototyping, and development workflows.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [What is tsrun?](#what-is-tsrun)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [CLI Usage](#-cli-usage)
|
||||||
|
- [Programmatic API](#-programmatic-api)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Why tsrun?](#why-tsrun)
|
||||||
|
- [Common Use Cases](#common-use-cases)
|
||||||
|
- [API Reference](#api-reference)
|
||||||
|
- [Examples](#examples)
|
||||||
|
- [Package Information](#package-information)
|
||||||
|
- [Requirements](#requirements)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [License and Legal Information](#license-and-legal-information)
|
||||||
|
|
||||||
## What is tsrun?
|
## What is tsrun?
|
||||||
|
|
||||||
**tsrun** is a lightweight TypeScript execution tool that lets you run `.ts` files directly—no build step required. It's like running JavaScript with `node`, but for TypeScript. Under the hood, tsrun uses [tsx](https://github.com/esbuild-kit/tsx) for lightning-fast execution while keeping your workflow simple and efficient.
|
**tsrun** is a lightweight TypeScript execution tool that lets you run `.ts` files directly—no build step required. It's like running JavaScript with `node`, but for TypeScript. Under the hood, tsrun uses [tsx](https://github.com/esbuild-kit/tsx) for lightning-fast execution while keeping your workflow simple and efficient.
|
||||||
@@ -40,19 +62,56 @@ All arguments are passed through to your TypeScript program, just as if you were
|
|||||||
|
|
||||||
### 💻 Programmatic API
|
### 💻 Programmatic API
|
||||||
|
|
||||||
Import tsrun in your code for dynamic TypeScript execution:
|
tsrun provides three powerful functions for different execution needs:
|
||||||
|
|
||||||
|
#### runPath() - Simple Execution
|
||||||
|
|
||||||
|
Wait for a script to complete. Perfect for sequential workflows.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { runPath, runCli } from '@git.zone/tsrun';
|
import { runPath } from '@git.zone/tsrun';
|
||||||
|
|
||||||
// Run a TypeScript file from an absolute or relative path
|
// Run a TypeScript file (absolute or relative path)
|
||||||
await runPath('./scripts/myScript.ts');
|
await runPath('./scripts/build.ts');
|
||||||
|
|
||||||
// Run with path resolution relative to a file URL
|
// With path resolution relative to a file URL
|
||||||
await runPath('./myScript.ts', import.meta.url);
|
await runPath('./build.ts', import.meta.url);
|
||||||
|
|
||||||
// Run in CLI mode programmatically (respects process.argv)
|
// With custom working directory
|
||||||
await runCli('./myScript.ts');
|
await runPath('./build.ts', import.meta.url, { cwd: '/path/to/project' });
|
||||||
|
```
|
||||||
|
|
||||||
|
#### runCli() - CLI Mode
|
||||||
|
|
||||||
|
Run with process.argv integration, as if invoked from command line.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { runCli } from '@git.zone/tsrun';
|
||||||
|
|
||||||
|
// Respects process.argv for argument passing
|
||||||
|
await runCli('./script.ts');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### spawnPath() - Advanced Control
|
||||||
|
|
||||||
|
Full process control with stdio access, timeouts, and cancellation.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { spawnPath } from '@git.zone/tsrun';
|
||||||
|
|
||||||
|
// Returns immediately with process handle
|
||||||
|
const proc = spawnPath('./task.ts', import.meta.url, {
|
||||||
|
timeout: 30000,
|
||||||
|
cwd: '/path/to/project',
|
||||||
|
env: { NODE_ENV: 'production' },
|
||||||
|
args: ['--verbose']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Access stdout/stderr streams
|
||||||
|
proc.stdout?.on('data', (chunk) => console.log(chunk.toString()));
|
||||||
|
|
||||||
|
// Wait for completion
|
||||||
|
const exitCode = await proc.exitCode;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -67,6 +126,10 @@ await runCli('./myScript.ts');
|
|||||||
|
|
||||||
🎯 **TypeScript Native** - Full TypeScript support with excellent IntelliSense.
|
🎯 **TypeScript Native** - Full TypeScript support with excellent IntelliSense.
|
||||||
|
|
||||||
|
🔀 **Custom Working Directory** - Execute scripts with different cwds for parallel multi-project workflows.
|
||||||
|
|
||||||
|
🎛️ **Advanced Process Control** - Full control with spawnPath() for stdio access, timeouts, and cancellation.
|
||||||
|
|
||||||
## Why tsrun?
|
## 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:
|
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:
|
||||||
@@ -76,6 +139,84 @@ Sometimes you just want to run a TypeScript file without setting up a build pipe
|
|||||||
- **Development Workflows**: Integrate TypeScript execution into your tooling
|
- **Development Workflows**: Integrate TypeScript execution into your tooling
|
||||||
- **CI/CD**: Run TypeScript-based build scripts without pre-compilation
|
- **CI/CD**: Run TypeScript-based build scripts without pre-compilation
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### Development Scripts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// scripts/dev-setup.ts
|
||||||
|
import { runPath } from '@git.zone/tsrun';
|
||||||
|
|
||||||
|
console.log('Setting up development environment...');
|
||||||
|
await runPath('./install-deps.ts', import.meta.url);
|
||||||
|
await runPath('./init-db.ts', import.meta.url);
|
||||||
|
await runPath('./seed-data.ts', import.meta.url);
|
||||||
|
console.log('✓ Development environment ready!');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Project Builds
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// build-all-projects.ts
|
||||||
|
import { runPath } from '@git.zone/tsrun';
|
||||||
|
|
||||||
|
const projects = [
|
||||||
|
'/workspace/frontend',
|
||||||
|
'/workspace/backend',
|
||||||
|
'/workspace/shared'
|
||||||
|
];
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
projects.map(cwd =>
|
||||||
|
runPath('./build.ts', import.meta.url, { cwd })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Long-Running Tasks with Monitoring
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// monitor-task.ts
|
||||||
|
import { spawnPath } from '@git.zone/tsrun';
|
||||||
|
|
||||||
|
const proc = spawnPath('./data-migration.ts', import.meta.url, {
|
||||||
|
timeout: 300000, // 5 minutes max
|
||||||
|
env: { LOG_LEVEL: 'verbose' }
|
||||||
|
});
|
||||||
|
|
||||||
|
let lineCount = 0;
|
||||||
|
proc.stdout?.on('data', (chunk) => {
|
||||||
|
lineCount++;
|
||||||
|
if (lineCount % 100 === 0) {
|
||||||
|
console.log(`Processed ${lineCount} lines...`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await proc.exitCode;
|
||||||
|
console.log('Migration completed successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Migration failed:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
Choose the right function for your use case:
|
||||||
|
|
||||||
|
| Function | Use When | Returns | Execution Mode |
|
||||||
|
|----------|----------|---------|----------------|
|
||||||
|
| `runPath()` | Simple script execution, sequential workflows | Promise (waits) | In-process (or child with cwd) |
|
||||||
|
| `runCli()` | Need process.argv integration | Promise (waits) | In-process |
|
||||||
|
| `spawnPath()` | Need process control, stdio access, timeout/cancel | Process handle | Child process |
|
||||||
|
|
||||||
|
**Quick decision guide:**
|
||||||
|
- 🎯 **Need to wait for completion?** → Use `runPath()` or `runCli()`
|
||||||
|
- 🎛️ **Need to capture output or control process?** → Use `spawnPath()`
|
||||||
|
- ⚡ **Running multiple scripts in parallel?** → Use `runPath()` with custom `cwd` or `spawnPath()`
|
||||||
|
- ⏱️ **Need timeout or cancellation?** → Use `spawnPath()`
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Simple Script
|
### Simple Script
|
||||||
@@ -141,6 +282,116 @@ 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
|
||||||
|
|
||||||
|
### Advanced Process Control with spawnPath()
|
||||||
|
|
||||||
|
For advanced use cases requiring full process control, stdio access, or timeout/cancellation support, use `spawnPath()`. Unlike `runPath()` which waits for completion, `spawnPath()` returns immediately with a process handle.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { spawnPath } from '@git.zone/tsrun';
|
||||||
|
|
||||||
|
// Basic spawning with output capture
|
||||||
|
const proc = spawnPath('./build.ts', import.meta.url);
|
||||||
|
|
||||||
|
proc.stdout?.on('data', (chunk) => {
|
||||||
|
console.log('Output:', chunk.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.stderr?.on('data', (chunk) => {
|
||||||
|
console.error('Error:', chunk.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitCode = await proc.exitCode;
|
||||||
|
console.log(`Process exited with code ${exitCode}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
**With timeout and custom environment:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const proc = spawnPath('./long-running-task.ts', import.meta.url, {
|
||||||
|
timeout: 30000, // Kill after 30 seconds
|
||||||
|
cwd: '/path/to/project',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
API_KEY: 'secret'
|
||||||
|
},
|
||||||
|
args: ['--mode', 'fast']
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exitCode = await proc.exitCode;
|
||||||
|
console.log('Task completed:', exitCode);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Task failed or timed out:', error.message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**AbortController integration:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const controller = new AbortController();
|
||||||
|
const proc = spawnPath('./task.ts', import.meta.url, {
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel after 5 seconds
|
||||||
|
setTimeout(() => controller.abort(), 5000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await proc.exitCode;
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Process was aborted');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Graceful termination:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const proc = spawnPath('./server.ts', import.meta.url);
|
||||||
|
|
||||||
|
// Later: gracefully shut down
|
||||||
|
// Sends SIGTERM, waits 5s, then SIGKILL if still running
|
||||||
|
await proc.terminate();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key differences from runPath():**
|
||||||
|
|
||||||
|
| Feature | runPath() | spawnPath() |
|
||||||
|
|---------|-----------|-------------|
|
||||||
|
| Returns | Promise (waits) | Process handle (immediate) |
|
||||||
|
| Default execution | In-process (unless cwd) | Always child process |
|
||||||
|
| stdio | 'inherit' (transparent) | 'pipe' (capturable) |
|
||||||
|
| Process control | Limited | Full (streams, signals, timeout) |
|
||||||
|
| Use case | Simple script execution | Complex process management |
|
||||||
|
|
||||||
## Package Information
|
## Package Information
|
||||||
|
|
||||||
- **npmjs**: [@git.zone/tsrun](https://www.npmjs.com/package/@git.zone/tsrun)
|
- **npmjs**: [@git.zone/tsrun](https://www.npmjs.com/package/@git.zone/tsrun)
|
||||||
@@ -152,6 +403,60 @@ for (const script of scripts) {
|
|||||||
- **Node.js**: >= 16.x
|
- **Node.js**: >= 16.x
|
||||||
- **TypeScript**: >= 3.x (automatically handled by tsx)
|
- **TypeScript**: >= 3.x (automatically handled by tsx)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**"Cannot find module" errors**
|
||||||
|
|
||||||
|
Make sure you're using absolute paths or paths relative to `import.meta.url`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Wrong - relative to cwd
|
||||||
|
await runPath('./script.ts');
|
||||||
|
|
||||||
|
// ✅ Correct - relative to current file
|
||||||
|
await runPath('./script.ts', import.meta.url);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Process hangs or doesn't complete**
|
||||||
|
|
||||||
|
When using `spawnPath()`, make sure to await the `exitCode` promise:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const proc = spawnPath('./script.ts', import.meta.url);
|
||||||
|
// Don't forget to await!
|
||||||
|
await proc.exitCode;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Timeout not working**
|
||||||
|
|
||||||
|
Timeouts only work with `spawnPath()`, not with `runPath()`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Wrong - timeout is ignored
|
||||||
|
await runPath('./script.ts', import.meta.url, { timeout: 5000 });
|
||||||
|
|
||||||
|
// ✅ Correct - use spawnPath for timeout support
|
||||||
|
const proc = spawnPath('./script.ts', import.meta.url, { timeout: 5000 });
|
||||||
|
await proc.exitCode;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Environment variables not available**
|
||||||
|
|
||||||
|
The `env` option automatically merges with `process.env` - your custom values override parent values:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Parent env is automatically inherited
|
||||||
|
spawnPath('./script.ts', import.meta.url, {
|
||||||
|
env: {
|
||||||
|
CUSTOM_VAR: 'value' // Added to parent env
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Script will see both process.env AND CUSTOM_VAR
|
||||||
|
```
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||||
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tsrun',
|
name: '@git.zone/tsrun',
|
||||||
version: '1.3.4',
|
version: '1.6.1',
|
||||||
description: 'run typescript programs efficiently'
|
description: 'run typescript programs efficiently'
|
||||||
}
|
}
|
||||||
|
253
ts/index.ts
253
ts/index.ts
@@ -1,14 +1,82 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
const __dirname = plugins.path.dirname(plugins.url.fileURLToPath(import.meta.url));
|
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 interface ISpawnOptions {
|
||||||
|
/** Working directory for the child process */
|
||||||
|
cwd?: string;
|
||||||
|
|
||||||
|
/** Environment variables (merged with parent's env) */
|
||||||
|
env?: Record<string, string>;
|
||||||
|
|
||||||
|
/** Additional CLI arguments to pass to the script */
|
||||||
|
args?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stdio configuration
|
||||||
|
* - 'pipe': Create pipes for stdin/stdout/stderr (default)
|
||||||
|
* - 'inherit': Use parent's stdio
|
||||||
|
* - Array: Custom configuration per stream
|
||||||
|
*/
|
||||||
|
stdio?: 'pipe' | 'inherit' | ['pipe' | 'inherit' | 'ignore', 'pipe' | 'inherit' | 'ignore', 'pipe' | 'inherit' | 'ignore'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional timeout in milliseconds
|
||||||
|
* If provided, process is automatically killed after timeout
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AbortSignal for cancellation support
|
||||||
|
* Allows external cancellation of the process
|
||||||
|
*/
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITsrunChildProcess {
|
||||||
|
/** Direct access to Node's ChildProcess object */
|
||||||
|
childProcess: plugins.ChildProcess;
|
||||||
|
|
||||||
|
/** Readable stream for stdout (null if stdio is 'inherit') */
|
||||||
|
stdout: plugins.Readable | null;
|
||||||
|
|
||||||
|
/** Readable stream for stderr (null if stdio is 'inherit') */
|
||||||
|
stderr: plugins.Readable | null;
|
||||||
|
|
||||||
|
/** Promise that resolves with the exit code when process ends */
|
||||||
|
exitCode: Promise<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send signal to process
|
||||||
|
* Returns true if signal was sent successfully
|
||||||
|
*/
|
||||||
|
kill(signal?: NodeJS.Signals): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully terminate the process
|
||||||
|
* Tries SIGTERM first, waits 5s, then SIGKILL if still running
|
||||||
|
* Returns a promise that resolves when process is terminated
|
||||||
|
*/
|
||||||
|
terminate(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const runPath = async (pathArg: string, fromFileUrl?: string, options?: IRunOptions) => {
|
||||||
pathArg = fromFileUrl
|
pathArg = fromFileUrl
|
||||||
? plugins.path.join(plugins.path.dirname(plugins.url.fileURLToPath(fromFileUrl)), pathArg)
|
? plugins.path.join(plugins.path.dirname(plugins.url.fileURLToPath(fromFileUrl)), pathArg)
|
||||||
: 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
|
// contents of argv array
|
||||||
// process.argv[0] -> node Executable
|
// process.argv[0] -> node Executable
|
||||||
// process.argv[1] -> tsrun executable
|
// process.argv[1] -> tsrun executable
|
||||||
@@ -26,3 +94,182 @@ export const runCli = async (pathArg?: string) => {
|
|||||||
const unregister = tsx.register();
|
const unregister = tsx.register();
|
||||||
await import(absolutePathToTsFile);
|
await import(absolutePathToTsFile);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const runInChildProcess = async (pathArg: string | undefined, cwd: string): Promise<void> => {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const spawnPath = (
|
||||||
|
filePath: string,
|
||||||
|
fromFileUrl?: string | URL,
|
||||||
|
options?: ISpawnOptions
|
||||||
|
): ITsrunChildProcess => {
|
||||||
|
// 1. Resolve path (similar to runPath)
|
||||||
|
const resolvedPath = fromFileUrl
|
||||||
|
? plugins.path.join(
|
||||||
|
plugins.path.dirname(
|
||||||
|
plugins.url.fileURLToPath(
|
||||||
|
typeof fromFileUrl === 'string' ? fromFileUrl : fromFileUrl.href
|
||||||
|
)
|
||||||
|
),
|
||||||
|
filePath
|
||||||
|
)
|
||||||
|
: filePath;
|
||||||
|
|
||||||
|
// 2. Build spawn args
|
||||||
|
const cliChildPath = plugins.path.join(__dirname, '../cli.child.js');
|
||||||
|
const args = [
|
||||||
|
...process.execArgv,
|
||||||
|
cliChildPath,
|
||||||
|
resolvedPath,
|
||||||
|
...(options?.args || [])
|
||||||
|
];
|
||||||
|
|
||||||
|
// 3. Build spawn options
|
||||||
|
const spawnOptions = {
|
||||||
|
cwd: options?.cwd || process.cwd(),
|
||||||
|
env: { ...process.env, ...options?.env },
|
||||||
|
stdio: options?.stdio || 'pipe',
|
||||||
|
shell: false,
|
||||||
|
windowsHide: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Spawn child process
|
||||||
|
const child = plugins.spawn(process.execPath, args, spawnOptions);
|
||||||
|
|
||||||
|
// 5. Set up timeout if provided
|
||||||
|
let timeoutId: NodeJS.Timeout | undefined;
|
||||||
|
let timeoutTriggered = false;
|
||||||
|
|
||||||
|
if (options?.timeout) {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
timeoutTriggered = true;
|
||||||
|
child.kill('SIGTERM');
|
||||||
|
}, options.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Set up AbortSignal if provided
|
||||||
|
let abortHandler: (() => void) | undefined;
|
||||||
|
if (options?.signal) {
|
||||||
|
abortHandler = () => {
|
||||||
|
child.kill('SIGTERM');
|
||||||
|
};
|
||||||
|
options.signal.addEventListener('abort', abortHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Create exitCode promise
|
||||||
|
const exitCodePromise = new Promise<number>((resolve, reject) => {
|
||||||
|
child.on('close', (code: number | null, signal: NodeJS.Signals | null) => {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
if (abortHandler && options?.signal) {
|
||||||
|
options.signal.removeEventListener('abort', abortHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeoutTriggered) {
|
||||||
|
reject(new Error(`Process killed: timeout of ${options?.timeout}ms exceeded`));
|
||||||
|
} else if (options?.signal?.aborted) {
|
||||||
|
reject(new Error('Process killed: aborted by signal'));
|
||||||
|
} else if (signal) {
|
||||||
|
reject(new Error(`Process killed with signal ${signal}`));
|
||||||
|
} else {
|
||||||
|
resolve(code || 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err: Error) => {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
if (abortHandler && options?.signal) {
|
||||||
|
options.signal.removeEventListener('abort', abortHandler);
|
||||||
|
}
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 8. Implement terminate() method
|
||||||
|
const terminate = async (): Promise<void> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (child.killed) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
child.kill('SIGTERM');
|
||||||
|
|
||||||
|
const killTimeout = setTimeout(() => {
|
||||||
|
if (!child.killed) {
|
||||||
|
child.kill('SIGKILL');
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
child.on('close', () => {
|
||||||
|
clearTimeout(killTimeout);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 9. Return ITsrunChildProcess object
|
||||||
|
return {
|
||||||
|
childProcess: child,
|
||||||
|
stdout: child.stdout,
|
||||||
|
stderr: child.stderr,
|
||||||
|
exitCode: exitCodePromise,
|
||||||
|
kill: (signal?: NodeJS.Signals) => child.kill(signal),
|
||||||
|
terminate
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@@ -1,8 +1,12 @@
|
|||||||
// node native
|
// node native
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import type { ChildProcess } from 'child_process';
|
||||||
|
import type { Readable } from 'stream';
|
||||||
|
|
||||||
export { path, url };
|
export { path, url, spawn };
|
||||||
|
export type { ChildProcess, Readable };
|
||||||
|
|
||||||
// @pushrocks scope
|
// @pushrocks scope
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
Reference in New Issue
Block a user