Build and activate checked component packages through normal source authoring (WI-12)
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { parseQx, walkSyntax } from "./source.js";
|
||||
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
|
||||
import type { StructuralRequest } from "./structural-plan.js";
|
||||
|
||||
/** Add one ordinary component entry without regenerating existing application code. */
|
||||
export async function scaffoldComponent(
|
||||
root: string,
|
||||
prefix: string,
|
||||
spec: ScaffoldRecipe,
|
||||
): Promise<StructuralRequest> {
|
||||
if (!spec.declaration || !spec.name || !/^[A-Z][A-Za-z0-9_]*$/.test(spec.name) || !spec.id)
|
||||
throw Error("Component scaffold requires a PascalCase name, ID and QX declaration");
|
||||
const source = `package Draft id "draft" revision "draft@1" { ${spec.declaration} }`;
|
||||
const syntax = parseQx(source);
|
||||
const declarations = [...walkSyntax(syntax.root)].filter((node) => node.kind === "componentDecl");
|
||||
const pkg = syntax.root.children.find((node) => node.kind === "packageResourceDecl");
|
||||
if (
|
||||
syntax.diagnostics.length ||
|
||||
syntax.root.children.length !== 1 ||
|
||||
declarations.length !== 1 ||
|
||||
pkg?.children.filter((node) => node.kind === "packageExport").length !== 1
|
||||
)
|
||||
throw Error("Expected exactly one component declaration");
|
||||
const declaration = declarations[0]!;
|
||||
const identifier = declaration.children.find((node) => node.kind === "identifier");
|
||||
const id = declaration.children.find((node) => node.kind === "stringLiteral");
|
||||
if (
|
||||
!identifier ||
|
||||
source.slice(identifier.start, identifier.end) !== spec.name ||
|
||||
!id ||
|
||||
JSON.parse(source.slice(id.start, id.end)) !== spec.id
|
||||
)
|
||||
throw Error("Component declaration name/ID must match its registration");
|
||||
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
|
||||
const existing = parseQx(authored);
|
||||
const parent = existing.root.children.find((node) => node.kind === "packageResourceDecl");
|
||||
if (existing.diagnostics.length || !parent) throw Error("Fix package.qx before adding a component");
|
||||
for (const item of parent.children.filter((node) => node.kind === "packageExport").flatMap((node) => node.children)) {
|
||||
const name = item.children.find((node) => node.kind === "identifier");
|
||||
const literal = item.children.find((node) => node.kind === "stringLiteral");
|
||||
if (
|
||||
(name && authored.slice(name.start, name.end) === spec.name) ||
|
||||
(literal && JSON.parse(authored.slice(literal.start, literal.end)) === spec.id)
|
||||
)
|
||||
throw Error("Component requires a unique name and ID");
|
||||
}
|
||||
const filename = "quixos.components.json";
|
||||
const before = await fs.readFile(path.join(root, prefix, filename), "utf8");
|
||||
const entries: Record<string, { entry: string; export: string }> = JSON.parse(before);
|
||||
if (!entries || typeof entries !== "object" || Array.isArray(entries) || Object.hasOwn(entries, spec.id))
|
||||
throw Error("Invalid or duplicate component entry");
|
||||
entries[spec.id] = { entry: `src/components/${spec.name}.tsx`, export: spec.name };
|
||||
const parentID = parent.children.find((node) => node.kind === "stringLiteral")!;
|
||||
return {
|
||||
kind: "package",
|
||||
source: spec.source,
|
||||
resourceRoot: spec.directory,
|
||||
validation: "syntax",
|
||||
files: [
|
||||
{
|
||||
file: prefix + "package.qx",
|
||||
edits: [
|
||||
{
|
||||
operation: "append",
|
||||
parent: { kind: "packageResourceDecl", id: JSON.parse(authored.slice(parentID.start, parentID.end)) },
|
||||
source: spec.declaration,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ file: prefix + filename, expected: before, replace: JSON.stringify(entries, null, 2) + "\n" },
|
||||
{
|
||||
file: prefix + `src/components/${spec.name}.tsx`,
|
||||
create: `import type { Components } from "../gen/client.js";\n\n// Add explicit query admission and named interface operations from the generated client.\nexport const ${spec.name}: Components[${JSON.stringify(spec.name)}] = ({subject}) => <p>{subject.object}</p>;\n`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user