import fs from "node:fs/promises"; import path from "node:path"; import { execFileSync } from "node:child_process"; export async function buildService({ config, compiler, pkg, output, coreServer, sharedFiles, implementations, recordFile, }) { const relative = compiler.artifactPath; if (pkg.exports.length || config.service.entry) { await fs.mkdir(path.join(output, "bin"), { recursive: true }); await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true }); let serviceEntry; if (config.service.entry) { if (!/\.m?js$/.test(config.service.entry)) throw Error("Custom service entry must be JavaScript; use a separate typed/language build before packaging"); serviceEntry = path.join(config.source, relative(config.service.entry)); } else { const modules = implementations.filter((implementation) => implementation.target === "server"); if (pkg.exports.some((entry) => !modules.some((module) => module.exports.includes(entry.id)))) throw Error("Generated service does not cover every package export; provide a custom service"); if (!modules.length) throw Error("Generated service requires a checked registry"); serviceEntry = path.resolve("service-entry.mjs"); const imports = modules .map( (module, index) => `import { ${module.registryExport} as registry${index} } from ${JSON.stringify(path.join(output, module.path))};`, ) .join("\n"); await fs.writeFile( serviceEntry, imports + `\nimport {servePortableRegistry} from ${JSON.stringify(config.serviceAdapter)}; const registries = [${modules.map((_, index) => "registry" + index).join(",")}]; const owners = new Map(); for (const registry of registries) for (const id of registry.exports) { if (owners.has(id)) throw Error("Duplicate registry export " + id); owners.set(id, registry); } servePortableRegistry({exports: [...owners.keys()], invoke(request, channel, context) { const registry = owners.get(request.exportId); if (!registry) throw Error("Unknown export"); return registry.invoke(request, channel, context); }});\n`, ); } const serviceModule = "share/quixos/service/main.mjs"; execFileSync( config.esbuild, [ serviceEntry, "--bundle", "--platform=node", "--format=esm", "--minify-whitespace", "--target=es2024", `--external:${coreServer}`, `--outfile=${path.join(output, serviceModule)}`, ], { stdio: "inherit" }, ); await fs.writeFile( path.join(output, "bin/service"), `#!${config.shell}\nservice_root=\${0%/*}/..\nexec ${config.node} "$service_root/${serviceModule}" "$@"\n`, { mode: 0o755 }, ); const serviceBody = { id: "service", target: "server", compatibility: "quixos-service-v1", path: "bin/service", ports: "quixos-transaction-ports-v1", exports: pkg.exports.map((e) => e.id), files: [ await recordFile("bin/service", "text/x-shellscript"), await recordFile(serviceModule, "text/javascript"), ...sharedFiles, ], }; implementations.unshift({ ...serviceBody, contentDigest: compiler.implementationDigest(serviceBody) }); } }