UNPKG

mono-runner

Version:

A script runner for mono repos that just works. Use Mono to run separate compilers and dev servers for your packages at once, build only dependent packages or format everything at once.

374 lines (345 loc) 8.54 kB
import { spawn } from "node:child_process"; import { LocalPackage, getLocalPackages, rootPath } from "./packages"; import { join } from "node:path"; import { lstat, readdir } from "node:fs/promises"; import { drawConsole } from "./out"; import { getPackageConfig } from "./config"; import { rmSync } from "node:fs"; export type ExecOptions = { parallel?: boolean; noWait?: boolean; ignoreCode?: boolean; quiet?: boolean; }; export type StreamInfo = { level: "out" | "err"; stream: any; package: string; }; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const holdBeforeFolderExists = async (filePath: string, timeout: number) => { await sleep(1000); timeout = timeout < 1000 ? 1000 : timeout; const end = Date.now() + timeout; return new Promise((resolve) => { const check = async () => { if (end < Date.now()) { resolve(false); return; } const isDir = await lstat(filePath) .then((stat) => stat.isDirectory()) .catch(() => false); if (isDir) { const fileCount = await readdir(filePath) .then((files) => { return files?.length || 0; }) .catch(() => 0); if (fileCount > 0) { resolve(true); return; } } setTimeout(check, 100); }; setTimeout(check, 100); }); }; const findNodeBin = async ( path: string, name: string ): Promise<string | false> => { let current = path; while (current.length >= rootPath.length) { const binPath = join(current, "node_modules", ".bin", name); if ( await lstat(binPath) .then(() => true) .catch(() => false) ) { return binPath; } current = join(current, ".."); } return false; }; type ScriptInfo = { exec?: string; args: string[]; envs: Record<string, string>; }; const parseScript = (script: string): ScriptInfo => { if (!script) { return { exec: undefined, args: [], envs: {}, }; } const parts = script.split(" "); const hasAnd = script.includes("&"); const envs: Record<string, string> = {}; for (let idx = 0; idx < parts.length; idx++) { if (parts[idx].includes("=")) { const [env, value] = parts[idx].split("="); envs[env] = value; } else { if (hasAnd) { return { exec: undefined, args: parts.slice(idx), envs, }; } else { return { exec: parts[idx], args: parts.slice(idx + 1), envs, }; } } } return { exec: undefined, args: parts, envs, }; }; const spawnWorker = async ( runtime: string, pcg: LocalPackage, command: string, args: string[] = [], noWait: boolean, quiet: boolean ) => { return new Promise<{ out: any; err: any; ready: Promise<boolean>; exit: Promise<number>; kill: () => void; }>(async (resolve) => { const currentDir = join(rootPath, pcg.path); const script = pcg.scriptCommands[command]; let { exec, args: execArgs, envs } = parseScript(script.trim()); const env: any = { ...process.env, ...envs, }; delete env.NODE_CHANNEL_FD; const bin = exec ? await findNodeBin(currentDir, exec) : false; if (!bin) { if (!quiet) { console.log( `Could not directly start ${pcg.name} - starting using ${runtime}` ); } execArgs = ["run", command, ...args]; } if (!quiet) { console.log( `Running ${pcg.name} - ${command}: ${bin || runtime} ${execArgs.join( " " )}` ); } const p = spawn(bin || runtime, execArgs, { cwd: currentDir, stdio: ["pipe", "pipe", "pipe", "ipc"], env: { ...env, MONO_ROOT: rootPath, }, }); const exit = new Promise<number>((resolve) => { p.on("exit", (e) => { if (e !== 0) { console.error(`Error in ${pcg.name} - ${e}`); } resolve(e || 0); }); }); const ready = new Promise<boolean>((resolve) => { if (noWait) { resolve(true); } else { p.on("message", (e) => { if (e && e.event === "READY") { resolve(true); } }); } }); p.on("spawn", () => { resolve({ out: p.stdout as any, err: p.stderr as any, ready, exit, kill: () => { p.kill(); }, }); }); }); }; const isReady = async ( packageName: string, packagePath: string, packageDeps: string[], ready?: Promise<boolean> ) => { const config = await getPackageConfig({ name: packageName, path: packagePath, dependencies: packageDeps, }); if (config && config.readyIPC) { // timeout after 15 seconds const res = await Promise.any([ ready?.then(() => true), sleep(15000).then(() => false), ]); if (res === false) { console.log(`${packageName} timed out - continuing...`); } // some compilers emit the ready hook before writing the files await sleep(1000); return true; } else if (config && config.outPath) { const path = join(rootPath, packagePath, config.outPath); rmSync(path, { recursive: true, force: true }); await sleep(1000); const exists = await holdBeforeFolderExists(path, 10000); if (exists) { return true; } else { return false; } } else { return; } }; const startScriptAndDeps = async ( runtime: string, localPackage: LocalPackage | LocalPackage[], command: string, args: string[], options: ExecOptions ) => { const started: Map<string, Promise<void>> = new Map(); const { parallel = false, noWait = false, ignoreCode = false, quiet = false, } = options; let exitCode = 0; const allExits: Promise<number>[] = []; const startScript = async (pcg: LocalPackage) => { const deps = pcg.localDependencies.map(async (dep) => { if (started.has(dep.name)) { return started.get(dep.name); } const scr = startScript(dep); started.set(dep.name, scr); return await scr; }); if (!noWait) { await Promise.all(deps); } if (!pcg.scripts.includes(command)) { return; } const workerPrm = spawnWorker(runtime, pcg, command, args, noWait, quiet); workerPrm.then((worker) => { drawConsole( [ { level: "out", stream: worker.out, package: pcg.name, }, { level: "err", stream: worker.err, package: pcg.name, }, ], quiet ); }); if (parallel) { const worker = await workerPrm; await isReady(pcg.name, pcg.path, pcg.dependencies, worker.ready); if (pcg === localPackage) { exitCode = Math.max(exitCode, await worker.exit); if (exitCode !== 0 && !ignoreCode) { process.exit(exitCode); } } else if (Array.isArray(localPackage)) { allExits.push(worker.exit); } } else { exitCode = Math.max(await (await workerPrm).exit); if (exitCode !== 0 && !ignoreCode) { process.exit(exitCode); } } }; if (Array.isArray(localPackage)) { localPackage.forEach((pcg) => { started.set(pcg.name, Promise.resolve()); }); await Promise.all(localPackage.map((pcg) => startScript(pcg))); const code = (await Promise.all(allExits)).reduce( (a, b) => Math.max(a, b), 0 ); return code; } else { await startScript(localPackage); } return exitCode; }; export const execute = async ( runtime: string, pcg: string, command: string, args: string[], options: ExecOptions ) => { // packet finden, // script für deps starten .... // script für packet starten // ausgabe const packages = await getLocalPackages(); if (pcg === "") { const exitCode = await startScriptAndDeps( runtime, packages, command, args, options ); process.exit(exitCode); } else { const localPackage = packages.find((p) => p.name === pcg); if (!localPackage) { console.error(`Package ${pcg} not found`); return; } const exitCode = await startScriptAndDeps( runtime, localPackage, command, args, options ); if (options.parallel) { process.exit(exitCode); } } };