no1b4me's picture
Upload 5037 files
95f4e64 verified
import { context } from "esbuild";
import { spawn } from "child_process";
import findInputFile from "../tools/findInputFile.js";
import { fileConstantsPlugin } from "../plugins/fileConstants.js";
import path, { posix } from "path";
import { unlinkSync, writeFileSync } from "fs";
import { findBinDirectory } from "../tools/findBinDirectory.js";
import { importRequire } from "../tools/importRequire.js";
import { grub } from "@digitak/grubber";
export default class Runner {
constructor(inputFile, options) {
this.outputFile = undefined; // temporary output file
this.output = "";
this.stdout = "";
this.stderr = "";
this.outputCode = "";
this.args = [];
this.nodeOptions = {};
this.dependencies = [];
this.inputFile = findInputFile(inputFile);
this.args = options?.args ?? [];
this.sudo = options?.sudo ?? false;
this.watched = options?.watch ?? false;
this.preserveConsole = options?.preserveConsole ?? false;
this.inspect = options?.inspect ?? false;
this.fileConstants = options?.fileConstants ?? true;
this.tsConfigFile = options?.tsConfigFile;
this.interProcessCommunication =
options?.interProcessCommunication ?? false;
this.makeAllPackagesExternal = options?.makeAllPackagesExternal ?? true;
this.exitAfterExecution = options?.exitAfterExecution ?? true;
this.beforeRun = options?.beforeRun;
this.afterRun = options?.afterRun;
this.nodeOptions = options?.nodeOptions ?? {};
this.nodeOptions = options?.nodeOptions ?? {};
this.esbuildOptions = options?.esbuildOptions ?? {};
this.sendCodeMode =
options?.sendCodeMode ?? process.platform === "win32"
? "temporaryFile"
: "cliParameters";
}
getDependencies() {
return this.dependencies;
}
retrieveDependencies() {
return Object.keys(this.buildOutput?.metafile?.inputs ?? []).map((input) => posix.resolve(input));
}
async run() {
try {
await this.build();
const status = await this.execute();
if (this.exitAfterExecution) {
process.exit(status);
}
}
catch (error) {
console.error(error);
process.exit(1);
}
}
async build(buildOptions = {}) {
const plugins = [];
if (this.fileConstants) {
plugins.push(fileConstantsPlugin());
}
try {
this.buildContext = await context({
entryPoints: [this.inputFile],
bundle: true,
platform: "node",
format: "esm",
plugins,
sourcemap: "inline",
sourceRoot: process.cwd(),
tsconfig: this.tsConfigFile,
external: this.makeAllPackagesExternal
? [
"./node_modules/*",
"../node_modules/*",
"../../node_modules/*",
"../../../node_modules/*",
"../../../../node_modules/*",
"../../../../../node_modules/*",
"../../../../../../node_modules/*",
"../../../../../../../node_modules/*",
"../../../../../../../../node_modules/*",
"../../../../../../../../../node_modules/*",
"../../../../../../../../../../node_modules/*",
]
: [],
...this.esbuildOptions,
...buildOptions,
write: false,
metafile: true,
});
this.buildOutput = await this.buildContext?.rebuild();
this.outputCode = this.getOutputCode();
// remove shebangs
this.outputCode = grub(this.outputCode).replace({ from: /^#!.*$/gm, to: "" });
this.dependencies = this.retrieveDependencies();
}
catch (error) {
// No need to log the error as it has already been done by esbuild.
this.buildOutput = undefined;
this.outputCode = "";
}
}
async transform(transformer) {
this.outputCode = await transformer(this.outputCode);
}
async execute() {
this.output = this.stdout = this.stderr = "";
if (!this.buildOutput)
return 1;
let code = this.outputCode;
let command = "node";
let commandArgs = ["--enable-source-maps"];
for (const nodeOption in this.nodeOptions) {
let argument = `--${nodeOption}`;
const parameters = this.nodeOptions[nodeOption];
if (Array.isArray(parameters)) {
argument += `=${parameters.join(",")}`;
}
commandArgs.push(argument);
}
if (this.inspect) {
commandArgs.push("--inspect");
code = `setTimeout(() => console.log("Process timeout"), 3_600_000);\n${code}`;
}
const evalArgs = [];
if (this.sendCodeMode === "temporaryFile") {
// we create a temporary file that we will execute
const binDirectory = findBinDirectory();
const uniqueId = Date.now();
this.outputFile = path.normalize(posix.join(binDirectory, `esrun-${uniqueId}.tmp.mjs`));
if (binDirectory && binDirectory !== ".") {
code = code
.replace(/(?:^|;)import (.*?) from "..\//gm, 'import $1 from "../../../')
.replace(/(?:^|;)import (.*?) from ".\//gm, 'import $1 from "../../');
}
code = importRequire(code, this.outputFile);
code = `process.argv = [process.argv[0], ...process.argv.slice(3)];\n${code}`;
writeFileSync(this.outputFile, code);
evalArgs.push(this.outputFile);
}
else {
code = importRequire(code, posix.resolve("index.js"));
// we pass the code directly from the command line
evalArgs.push("--input-type=module", "--eval", code);
}
commandArgs.push(...evalArgs, "--", this.inputFile, ...this.args);
if (this.sudo) {
commandArgs = [command, ...commandArgs];
command = "sudo";
}
await this.beforeRun?.();
try {
this.childProcess = spawn(command, commandArgs, {
stdio: this.interProcessCommunication
? ["pipe", "pipe", "pipe", "ipc"]
: "inherit",
});
if (this.interProcessCommunication) {
this.childProcess?.on("message", (message) => {
this.output += message.toString();
});
this.childProcess?.stdout?.on("data", (data) => {
this.stdout += data.toString();
});
this.childProcess?.stderr?.on("data", (data) => {
this.stderr += data.toString();
});
}
return new Promise((resolve) => {
const done = async (code) => {
await this.afterRun?.();
if (this.outputFile) {
unlinkSync(this.outputFile);
}
resolve(code ?? 0);
};
this.childProcess?.on("close", done);
this.childProcess?.on("error", async (error) => {
console.error(error);
done(1);
});
});
}
catch (error) {
console.error(error);
return 1;
}
}
getOutputCode() {
return this.buildOutput?.outputFiles?.[0]?.text || "";
}
}