83 lines
2.9 KiB
TypeScript
83 lines
2.9 KiB
TypeScript
import { lstat, open, rename, unlink } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { randomUUID } from "node:crypto";
|
|
import { addWorkspaceImport } from "./source.js";
|
|
import { readQxSource } from "./source-loader.js";
|
|
import { compileWorkspaceRepository, type CapabilityRepositoryResolver } from "./assembly.js";
|
|
|
|
export const planAtomScaffold = (workspace: string, name: string, id: string) => {
|
|
if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) throw new Error("Atom name must be PascalCase");
|
|
if (!id.trim()) throw new Error("Atom ID must not be empty");
|
|
const fileName = `${name}.qx`;
|
|
return {
|
|
before: workspace,
|
|
workspace: addWorkspaceImport(workspace, fileName),
|
|
fileName,
|
|
fragment: `fragment {\n atom ${name} id ${JSON.stringify(id)};\n}\n`,
|
|
};
|
|
};
|
|
|
|
/** Validate an in-memory proposal before creating files. Never replace a fragment. */
|
|
export const scaffoldAtom = async (options: {
|
|
root: string;
|
|
name: string;
|
|
id: string;
|
|
write: boolean;
|
|
resolveResource: CapabilityRepositoryResolver;
|
|
}) => {
|
|
const before = await readQxSource(options.root, "workspace.qx");
|
|
const plan = planAtomScaffold(before, options.name, options.id);
|
|
const fragmentPath = path.join(options.root, plan.fileName);
|
|
try {
|
|
await lstat(fragmentPath);
|
|
throw new Error(`Scaffold target already exists: ${plan.fileName}`);
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
}
|
|
const observed = new Map<string, string>();
|
|
await compileWorkspaceRepository({
|
|
rootDirectory: options.root,
|
|
resolveResource: options.resolveResource,
|
|
readSource: async (name) => {
|
|
if (name === "workspace.qx") return plan.workspace;
|
|
if (name === plan.fileName) return plan.fragment;
|
|
const text = await readQxSource(options.root, name);
|
|
observed.set(name, text);
|
|
return text;
|
|
},
|
|
});
|
|
if (!options.write) return plan;
|
|
const workspacePath = path.join(options.root, "workspace.qx");
|
|
const temporary = path.join(options.root, `.qx-scaffold-${randomUUID()}.tmp`);
|
|
let createdFragment = false;
|
|
let createdTemporary = false;
|
|
try {
|
|
const fragment = await open(fragmentPath, "wx");
|
|
createdFragment = true;
|
|
try {
|
|
await fragment.writeFile(plan.fragment);
|
|
} finally {
|
|
await fragment.close();
|
|
}
|
|
const file = await open(temporary, "wx", (await lstat(workspacePath)).mode);
|
|
createdTemporary = true;
|
|
try {
|
|
await file.writeFile(plan.workspace);
|
|
} finally {
|
|
await file.close();
|
|
}
|
|
observed.set("workspace.qx", before);
|
|
for (const [name, text] of observed) {
|
|
if ((await readQxSource(options.root, name)) !== text)
|
|
throw new Error(`QX source changed during scaffolding: ${name}`);
|
|
}
|
|
await rename(temporary, workspacePath);
|
|
createdTemporary = false;
|
|
createdFragment = false;
|
|
} finally {
|
|
if (createdTemporary) await unlink(temporary);
|
|
if (createdFragment) await unlink(fragmentPath);
|
|
}
|
|
return plan;
|
|
};
|