Generate typed QX bindings and add source editing tools
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user