Files
hapi/cli/src/utils/spawnHappyCLI.ts
T
2025-12-16 15:03:50 +08:00

104 lines
4.1 KiB
TypeScript

/**
* Cross-platform Happy CLI spawning utility
*
* ## Background
*
* We built a command-line JavaScript program with the entrypoint at `dist/index.mjs`.
* This needs to be run with `node`, but we want to hide deprecation warnings and other
* noise from end users by passing specific flags: `--no-warnings --no-deprecation`.
*
* Users don't care about these technical details - they just want a clean experience
* with no warning output when using Happy.
*
* ## The Wrapper Strategy
*
* We created a wrapper script `bin/happy.mjs` with a shebang `#!/usr/bin/env node`.
* This allows direct execution on Unix systems and NPM automatically generates
* Windows-specific wrapper scripts (`happy.cmd` and `happy.ps1`) when it sees
* the `bin` field in package.json pointing to a JavaScript file with a shebang.
*
* The wrapper script either directly execs `dist/index.mjs` with the flags we want,
* or imports it directly if Node.js already has the right flags.
*
* ## Execution Chains
*
* **Unix/Linux/macOS:**
* 1. User runs `happy` command
* 2. Shell directly executes `bin/happy.mjs` (shebang: `#!/usr/bin/env node`)
* 3. `bin/happy.mjs` either execs `node --no-warnings --no-deprecation dist/index.mjs` or imports `dist/index.mjs` directly
*
* **Windows:**
* 1. User runs `happy` command
* 2. NPM wrapper (`happy.cmd`) calls `node bin/happy.mjs`
* 3. `bin/happy.mjs` either execs `node --no-warnings --no-deprecation dist/index.mjs` or imports `dist/index.mjs` directly
*
* ## The Spawning Problem
*
* When our code needs to spawn Happy cli as a subprocess (for daemon processes),
* we were trying to execute `bin/happy.mjs` directly. This fails on Windows
* because Windows doesn't understand shebangs - you get an `EFTYPE` error.
*
* ## The Solution
*
* Since we know exactly what needs to happen (run `dist/index.mjs` with specific
* Node.js flags), we can bypass all the wrapper layers and do it directly:
*
* `spawn('node', ['--no-warnings', '--no-deprecation', 'dist/index.mjs', ...args])`
*
* This works on all platforms and achieves the same result without any of the
* middleman steps that were providing workarounds for Windows vs Linux differences.
*/
import { spawn, SpawnOptions, type ChildProcess } from 'child_process';
import { join } from 'node:path';
import { projectPath } from '@/projectPath';
import { logger } from '@/ui/logger';
import { existsSync } from 'node:fs';
/**
* Spawn the Happy CLI with the given arguments in a cross-platform way.
*
* This function bypasses the wrapper script (bin/happy.mjs) and spawns the
* actual CLI entrypoint (dist/index.mjs) directly with Node.js, ensuring
* compatibility across all platforms including Windows.
*
* @param args - Arguments to pass to the Happy CLI
* @param options - Spawn options (same as child_process.spawn)
* @returns ChildProcess instance
*/
export function spawnHappyCLI(args: string[], options: SpawnOptions = {}): ChildProcess {
const projectRoot = projectPath();
const entrypoint = join(projectRoot, 'dist', 'index.mjs');
let directory: string | URL | undefined;
if ('cwd' in options) {
directory = options.cwd
} else {
directory = process.cwd()
}
// Note: We're actually executing 'node' with the calculated entrypoint path below,
// bypassing the 'happy' wrapper that would normally be found in the shell's PATH.
// However, we log it as 'happy' here because other engineers are typically looking
// for when "happy" was started and don't care about the underlying node process
// details and flags we use to achieve the same result.
const fullCommand = `hapi ${args.join(' ')}`;
logger.debug(`[SPAWN HAPI CLI] Spawning: ${fullCommand} in ${directory}`);
// Use the same Node.js flags that the wrapper script uses
const nodeArgs = [
'--no-warnings',
'--no-deprecation',
entrypoint,
...args
];
// Sanity check of the entrypoint path exists
if (!existsSync(entrypoint)) {
const errorMessage = `Entrypoint ${entrypoint} does not exist`;
logger.debug(`[SPAWN HAPI CLI] ${errorMessage}`);
throw new Error(errorMessage);
}
return spawn('node', nodeArgs, options);
}