@plugjs/plug
Version:
PlugJS Build System ===================
194 lines (193 loc) • 7.56 kB
JavaScript
// 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;
}
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);
});
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