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(); 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((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) }; }