import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { spawnSync } from "node:child_process"; import * as path from "node:path"; import { emitBundle } from "../compiler/codegen.js"; import { loadProject, moduleIdOf, projectDiagnostics } from "../compiler/modules.js"; import { renderDiagnostic, summary } from "../compiler/diagnostics.js"; function entryFileFor(cwd: string, explicit: string | undefined): string { if (explicit) return explicit; const manifest = path.join(cwd, "project.xan"); if (existsSync(manifest)) { const text = readFileSync(manifest, "utf8"); const m = /^\s*entry\s*=\s*"([^"]+)"/m.exec(text); if (m) return m[1] ?? "src/main.xan"; } return "src/main.xan"; } function report(project: ReturnType): boolean { const diags = projectDiagnostics(project); let hasErrors = false; for (const d of diags) { if (d.severity === "error") hasErrors = true; process.stdout.write(renderDiagnostic(d)); } if (diags.length > 0) process.stdout.write(`${summary(diags)}\n`); return !hasErrors; } function build(entry: string, cwd: string): { ok: boolean; code?: string; project?: ReturnType } { const project = loadProject({ entryFile: entry, cwd }); if (!report(project)) return { ok: false, project }; if (!project.entryModuleId) { process.stdout.write("error: no modules were loaded\n"); return { ok: false, project }; } const code = emitBundle(project.modules, { entry: project.entryModuleId, mode: "run", jsImports: project.jsImports, }); return { ok: true, code, project }; } function runNode(code: string, args: string[]): void { const file = path.join(tmpdir(), `ex-${process.pid}-${Date.now()}.mjs`); writeFileSync(file, code); const res = spawnSync(process.execPath, [file, ...args], { stdio: "inherit" }); try { unlinkSync(file); } catch { /* ignore */ } process.exit(res.status ?? 1); } export async function main(argv: string[]): Promise { const cwd = process.cwd(); const [cmd, target, ...rest] = argv; switch (cmd) { case "build": { const outIdx = rest.indexOf("-o"); const outFile = outIdx >= 0 ? rest[outIdx + 1]! : path.join(cwd, "out.mjs"); const res = build(target ?? entryFileFor(cwd, undefined), cwd); if (!res.ok || !res.code) process.exit(1); mkdirSync(path.dirname(path.resolve(outFile)), { recursive: true }); writeFileSync(outFile, res.code); process.stdout.write(`wrote ${outFile}\n`); break; } case "run": { const entry = target ?? entryFileFor(cwd, undefined); const res = build(entry, cwd); if (!res.ok || !res.code) process.exit(1); runNode(res.code, rest); break; } case "test": { const entry = target ?? entryFileFor(cwd, undefined); const project = loadProject({ entryFile: entry, cwd }); if (!report(project)) process.exit(1); if (!project.entryModuleId) process.exit(1); const code = emitBundle(project.modules, { entry: project.entryModuleId, mode: "test", jsImports: project.jsImports, }); runNode(code, rest); break; } case "check": { const entry = target ?? entryFileFor(cwd, undefined); const project = loadProject({ entryFile: entry, cwd }); if (!report(project)) process.exit(1); process.stdout.write("OK: no diagnostics\n"); break; } case "help": case "--help": case "-h": process.stdout.write(`EX Script compiler usage: ex [file] [args...] commands: build [file] [-o out.mjs] compile to a single JS module run [file] [args...] compile and run with node test [file] compile and run test blocks check [file] type-check and report diagnostics `); break; case "version": case "--version": case "-v": process.stdout.write(`exscript 0.1.0\n`); break; default: process.stderr.write(`ex: unknown command '${cmd}' (see 'ex help')\n`); process.exit(2); } }