2026-09-21 16:59:35 -07:00
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 );
2026-09-22 13:16:23 -07:00
const declarations = [... walkSyntax ( syntax . root )]. filter (( node ) => node . kind === "moduleExport" );
2026-09-21 16:59:35 -07:00
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` ,
},
],
};
}