@tsed/cli
Version:
CLI to bootstrap your Ts.ED project
179 lines (178 loc) • 5.58 kB
JavaScript
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath, pathToFileURL } from "node:url";
const RUN_MODE = "TSED_VITE_RUN_MODE";
const DEFAULT_ENTRY_MODULE = "src/index.ts";
const runnerFile = fileURLToPath(import.meta.url);
const configFile = path.resolve(process.cwd(), "vite.config.ts");
function parseWatchValue(args) {
if (args.includes("--no-watch")) {
return false;
}
const watchIndex = args.findIndex((arg) => arg === "--watch" || arg.startsWith("--watch="));
if (watchIndex === -1) {
return true;
}
const watchArg = args[watchIndex];
if (watchArg === "--watch") {
const nextValue = args[watchIndex + 1];
if (!nextValue || nextValue.startsWith("-")) {
return true;
}
return nextValue !== "false";
}
if (watchArg === "--watch=false") {
return false;
}
if (watchArg === "--watch=true") {
return true;
}
return true;
}
export function resolveEntryModulePath(args) {
for (let index = 0; index < args.length; index++) {
const arg = args[index];
if (arg === "--watch") {
const nextValue = args[index + 1];
if (nextValue && !nextValue.startsWith("-")) {
index++;
}
continue;
}
if (arg === "--no-watch" || arg.startsWith("--watch=") || arg.startsWith("-")) {
continue;
}
return `/${arg.replace(/\\/g, "/").replace(/^\.?\//, "")}`;
}
return `/${DEFAULT_ENTRY_MODULE}`;
}
export function shouldIgnoreWatchFile(file, watcher) {
if (!file || file.includes("node_modules") || file.includes(".git") || file.includes("/dist/")) {
return true;
}
const isIgnored = watcher?._isIgnored;
return typeof isIgnored === "function" ? isIgnored(file) : false;
}
function assertViteProject() {
if (!existsSync(configFile)) {
throw new Error("tsed dev is only available for ViteRuntime projects. Missing vite.config.ts in the current directory.");
}
}
async function createViteDevServer() {
// @ts-ignore
const { createServer } = await import("vite");
return createServer({
configFile,
server: {
middlewareMode: "ssr",
hmr: false,
ws: false
}
});
}
async function runViteApp(rawArgs) {
const vite = await createViteDevServer();
const shutdown = async () => {
await vite.close();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
await vite.ssrLoadModule(resolveEntryModulePath(rawArgs));
await new Promise(() => { });
}
async function runViteController(rawArgs) {
const watch = parseWatchValue(rawArgs);
const vite = await createViteDevServer();
let childProcess;
let childStarted = false;
let restarting = false;
let queued = false;
const startChild = () => {
if (childStarted) {
return;
}
childStarted = true;
childProcess = spawn(process.execPath, [runnerFile, ...rawArgs], {
env: {
...process.env,
[RUN_MODE]: "app"
},
stdio: "inherit"
});
childProcess.once("exit", () => {
childStarted = false;
childProcess = undefined;
});
};
const stopChild = async () => {
if (!childProcess || childProcess.killed || childProcess.exitCode !== null || childProcess.signalCode !== null) {
childStarted = false;
childProcess = undefined;
return;
}
await new Promise((resolve) => {
childProcess.once("exit", resolve);
childProcess.kill("SIGTERM");
});
childStarted = false;
};
const restartChild = async (reason, file = "") => {
if (restarting) {
queued = true;
return;
}
restarting = true;
const suffix = file ? `: ${file}` : "";
vite.config.logger.info(`[tsed-dev] restart (${reason})${suffix}`);
await stopChild();
startChild();
restarting = false;
if (queued) {
queued = false;
await restartChild("queued");
}
};
if (watch) {
vite.watcher.on("all", async (event, file) => {
if (shouldIgnoreWatchFile(file, vite.watcher)) {
return;
}
if (["add", "change", "unlink"].includes(event)) {
await restartChild(event, file);
}
});
}
vite.watcher.once("ready", () => {
startChild();
});
// Fallback: some environments can miss/lag watcher "ready" when Vite re-optimizes deps.
setTimeout(() => {
if (!childStarted) {
startChild();
}
}, 2500);
const shutdown = async () => {
await stopChild();
await vite.close();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
await new Promise(() => { });
}
export async function dev(rawArgs = process.argv.slice(2)) {
if (process.env[RUN_MODE] === "app") {
await runViteApp(rawArgs);
return;
}
await runViteController(rawArgs);
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
dev().catch((error) => {
console.error(error);
process.exit(1);
});
}