Files
2026-09-17 21:54:33 +08:00

81 lines
3.4 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { build } from "esbuild";
import JSZip from "jszip";
import { spawn } from "node:child_process";
import { projectRoot, workspaceRoot } from "./storage";
import { validateArtifact } from "./validation";
const activeOperations = new Set<string>();
export function isBuildActive(id: string) { return activeOperations.has(id); }
export async function buildProject(id: string) {
if (activeOperations.has(id)) throw new Error("Project is already busy");
activeOperations.add(id);
const workspace = workspaceRoot(id);
const dist = path.join(projectRoot(id), "dist");
try {
await fs.rm(dist, { recursive: true, force: true });
await fs.mkdir(path.join(dist, "assets"), { recursive: true });
await build({
absWorkingDir: workspace,
entryPoints: ["src/App.jsx"],
bundle: true,
outfile: path.join(dist, "assets", "main.js"),
format: "iife",
target: ["es2017", "chrome61"],
minify: true,
sourcemap: false,
legalComments: "inline",
define: { "process.env.NODE_ENV": '"production"' },
nodePaths: [path.join(process.cwd(), "node_modules"), path.join(workspace, "node_modules")],
logLevel: "silent",
});
await fs.copyFile(path.join(workspace, "src", "index.html"), path.join(dist, "index.html"));
await fs.copyFile(path.join(workspace, "src", "style.css"), path.join(dist, "assets", "style.css"));
return { dist, report: await validateArtifact(dist) };
} finally {
activeOperations.delete(id);
}
}
export async function installProjectDependencies(id: string) {
if (activeOperations.has(id)) throw new Error("Project is already busy");
activeOperations.add(id);
const workspace = workspaceRoot(id);
try {
await fs.access(path.join(workspace, "package.json"));
const output = await new Promise<string>((resolve, reject) => {
const child = spawn("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund"], { cwd: workspace, env: { ...process.env, npm_config_update_notifier: "false" } });
let text = "";
child.stdout.on("data", (chunk) => { text += chunk; });
child.stderr.on("data", (chunk) => { text += chunk; });
child.on("error", reject);
child.on("close", (code) => code === 0 ? resolve(text.trim()) : reject(new Error(`npm install failed (${code}): ${text.trim()}`)));
});
return { output };
} finally {
activeOperations.delete(id);
}
}
async function addDirectoryToZip(zip: JSZip, root: string, dir = root) {
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) await addDirectoryToZip(zip, root, full);
else zip.file(path.relative(root, full).split(path.sep).join("/"), await fs.readFile(full));
}
}
export async function packageProject(id: string) {
const { dist, report } = await buildProject(id);
if (report.status === "failed") return { report };
const artifacts = path.join(projectRoot(id), "artifacts");
await fs.mkdir(artifacts, { recursive: true });
const zipPath = path.join(artifacts, "minitool.zip");
const zip = new JSZip();
await addDirectoryToZip(zip, dist);
await fs.writeFile(zipPath, await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", compressionOptions: { level: 9 } }));
return { zipPath, report: await validateArtifact(dist, zipPath) };
}