UNPKG

@plugjs/plug

Version:
199 lines (198 loc) 7.67 kB
// fork.ts import { fork } from "node:child_process"; import { Console } from "node:console"; import { Writable } from "node:stream"; import { assert, BuildFailure } from "./asserts.mjs"; import { runAsync } from "./async.mjs"; import { Files } from "./files.mjs"; import { $gry, $p, $red, logOptions, NOTICE, WARN } from "./logging.mjs"; import { emit, emitForked } from "./logging/emit.mjs"; import { requireFilename, resolveFile } from "./paths.mjs"; import { Context, install } from "./pipe.mjs"; var ForkingPlug = class { constructor(_scriptFile, _arguments, _exportName) { this._scriptFile = _scriptFile; this._arguments = _arguments; this._exportName = _exportName; } _scriptFile; _arguments; _exportName; pipe(files, context) { const request = { scriptFile: this._scriptFile, exportName: this._exportName, constructorArgs: this._arguments, taskName: context.taskName, buildFile: context.buildFile, filesDir: files.directory, filesList: [...files.absolutePaths()], logIndent: context.log.indent }; const script = requireFilename(import.meta.url); context.log.debug("About to fork plug from", $p(this._scriptFile)); const env = { ...process.env, ...logOptions.forkEnv(context.taskName) }; for (let i = this._arguments.length - 1; i >= 0; i--) { if (this._arguments[i] == null) continue; if (typeof this._arguments[i] === "object") { if (typeof this._arguments[i].coverageDir === "string") { const dir = env.NODE_V8_COVERAGE = context.resolve(this._arguments[i].coverageDir); context.log.debug("Forked process will produce coverage in", $p(dir)); } if (typeof this._arguments[i].forceModule === "string") { const force = env.__TS_LOADER_FORCE_TYPE = this._arguments[i].forceModule; context.log.debug("Forked process will force module type as", $p(force)); } } } const child = fork(script, { stdio: ["ignore", "inherit", "inherit", "ipc"], serialization: "advanced", env }); context.log.info("Running", $p(script), $gry(`(pid=${child.pid})`)); let done = false; return new Promise((resolve, reject) => { let response = void 0; child.on("error", (error) => { context.log.error("Forked plug process error", error); return done || reject(BuildFailure.fail()); }); child.on("message", (message) => { if ("logLevel" in message) { const { logLevel, taskName, lines } = message; lines.forEach((line) => { context.log._emit(logLevel, [line], taskName); }); } else { context.log.debug("Message from forked plug process with PID", child.pid, message); response = message; } }); child.on("exit", (code, signal) => { if (signal) { context.log.error(`Forked plug process exited with signal ${signal}`, $gry(`(pid=${child.pid})`)); return done || reject(BuildFailure.fail()); } else if (code !== 0) { context.log.error(`Forked plug process exited with code ${code}`, $gry(`(pid=${child.pid})`)); return done || reject(BuildFailure.fail()); } else if (!response) { context.log.error("Forked plug process exited with no result", $gry(`(pid=${child.pid})`)); return done || reject(BuildFailure.fail()); } else if (response.failed) { return done || reject(BuildFailure.fail()); } return done || resolve(response.filesDir && response.filesList ? Files.builder(response.filesDir).add(...response.filesList).build() : void 0); }); child.on("spawn", () => { try { child.send(request, (error) => { if (error) { context.log.error("Error sending message to forked plug process (callback failure)", error); reject(BuildFailure.fail()); } }); } catch (error) { context.log.error("Error sending message to forked plug process (exception caught)", error); reject(BuildFailure.fail()); } }); }).finally(() => done = true); } }; if (process.argv[1] === requireFilename(import.meta.url) && process.send) { const originalConsole = globalThis.console; process.on("uncaughtException", (error, origin) => { originalConsole.error( $red("\n= UNCAUGHT EXCEPTION ========================================="), ` Error (${origin}):`, error, ` Node.js ${process.version} (pid=${process.pid}) ` ); process.nextTick(() => process.exit(3)); }); const timeout = setTimeout(() => { originalConsole.error("Fork not initialized in 5 seconds"); process.exit(2); }, 5e3).unref(); process.on("message", (message) => { clearTimeout(timeout); const { scriptFile, exportName, constructorArgs, taskName, buildFile, filesDir, filesList, logIndent } = message; emit.emitter = emitForked; const makeWritable = (level) => new class extends Writable { _write(chunk, _, callback) { const string = chunk.toString(); const message2 = string.endsWith("\n") ? string.slice(0, -1) : string; emit.emitter({ level, taskName }, [message2]); callback(); } }(); globalThis.console = new Console(makeWritable(NOTICE), makeWritable(WARN)); const context = new Context(buildFile, taskName); context.log.indent = logIndent; context.log.debug("Message from parent process for PID", process.pid, message); process.exitCode = 0; const result = runAsync(context, async () => { assert(resolveFile(scriptFile), `Script file ${$p(scriptFile)} not found`); const script = await import(scriptFile); let Ctor; if (exportName === "default") { Ctor = script; while (Ctor && typeof Ctor !== "function") Ctor = Ctor.default; assert(typeof Ctor === "function", `Script ${$p(scriptFile)} does not export a default constructor`); } else { Ctor = script[exportName]; if (!Ctor && script.default) Ctor = script.default[exportName]; assert(typeof Ctor === "function", `Script ${$p(scriptFile)} does not export "${exportName}"`); } const plug = new Ctor(...constructorArgs); const files = Files.builder(filesDir).add(...filesList).build(); return plug.pipe(files, context); }); const promise = result.then((result2) => { const message2 = result2 ? { failed: false, filesDir: result2.directory, filesList: [...result2.absolutePaths()] } : { failed: false }; return new Promise((resolve, reject) => { process.send(message2, (err) => err ? reject(err) : resolve()); }); }, (error) => { context.log.error(error); return new Promise((resolve, reject) => { process.send({ failed: true }, (err) => err ? reject(err) : resolve()); }); }); promise.then(() => { context.log.debug("Forked plug with pid", process.pid, "exiting"); }, (error) => { originalConsole.error("\n\nError sending message back to parent process", error); process.exitCode = 1; }).finally(() => { process.disconnect(); process.exit(process.exitCode); }); }); } function installForking(plugName, scriptFile, exportName = "default") { const ctor = class extends ForkingPlug { constructor(...args) { super(scriptFile, args, exportName); } }; install(plugName, ctor); } export { ForkingPlug, installForking }; //# sourceMappingURL=fork.mjs.map