2026-09-10 18:27:41 -07:00
import fs from "node:fs/promises" ;
import path from "node:path" ;
2026-09-15 15:23:24 -07:00
import { contentDigest } from "../capability-model/evolution.js" ;
import {
validateMigrationCatalog ,
type MigrationCatalog ,
type MigrationDeclaration ,
} from "../capability-model/migrations.js" ;
import { formatQuixosLock , type GitSource } from "../resource-lock/index.js" ;
import { parseQx , walkSyntax } from "./source.js" ;
import type { StructuralRequest } from "./structural-plan.js" ;
import { addImplementation } from "./implementation-edit.js" ;
import { reactPlatformTypes } from "../bindings/react-platform.js" ;
2026-09-21 00:44:47 -07:00
import { executionScaffold , isExecutionScaffold } from "./execution-scaffold.js" ;
2026-09-10 18:27:41 -07:00
2026-09-15 15:23:24 -07:00
type Source = { repository : string ; commit : string };
type PackageEditModel = {
generatedBy : "qx-scaffold-v1" ;
name : string ;
id : string ;
revision : string ;
exports : { name : string ; id : string ; file : string ; migration? : boolean }[];
};
2026-09-10 18:27:41 -07:00
export type ScaffoldRecipe = {
2026-09-21 00:44:47 -07:00
template ?: "typescript" | "typescript-react" | "typescript-execution" ;
2026-09-15 15:23:24 -07:00
source : Source ;
directory? : string ;
name? : string ;
id? : string ;
revision? : string ;
2026-09-10 18:27:41 -07:00
declaration? : string ;
2026-09-15 13:45:42 -07:00
/** Initial authored files for a composed recipe; only used for a new package. */
initialFiles? : Record < string , string >;
2026-09-15 15:23:24 -07:00
tools ?: { quixos : Source ; protocol : Source ; helpers : Source ; sdk : Source };
2026-09-10 18:27:41 -07:00
nixifyPluginUrl? : string ;
2026-09-15 15:23:24 -07:00
migration? : Omit < MigrationDeclaration , "implementation" > & { contracts : Record < string , unknown > };
2026-09-10 18:27:41 -07:00
};
const json = ( value : unknown ) => ` ${ JSON . stringify ( value , null , 2 ) } \ n` ;
const source = ( value : Source ) : GitSource => {
2026-09-15 15:23:24 -07:00
if ( ! value || typeof value . repository !== "string" || typeof value . commit !== "string" )
throw new Error ( "Scaffold requires an exact source identity; use qx-workspace from a registered repository" );
2026-09-10 18:27:41 -07:00
const url = new URL ( value . repository );
2026-09-15 15:23:24 -07:00
if (
url . protocol !== "https:" ||
url . username ||
url . password ||
url . search ||
url . hash ||
! /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/ . test ( value . commit )
)
throw new Error ( "Scaffolds require credential-free HTTPS sources and full exact commits" );
return { resolver : "git" , ... value };
2026-09-10 18:27:41 -07:00
};
2026-09-15 15:23:24 -07:00
const nixSource = ( value : Source ) =>
`git+ ${ source ( value ). repository } ?ref=refs/tags/quixos-reachability/ ${ value . commit } &rev= ${ value . commit } ` ;
2026-09-10 18:27:41 -07:00
const nixString = ( value : string ) => JSON . stringify ( value ). replaceAll ( "${" , "\\${" );
const safeName = ( name : string | undefined ) : string => {
if ( ! name || ! /^[A-Za-z_][A-Za-z0-9_]*$/ . test ( name )) throw new Error ( "Scaffold requires a simple authored name" );
return name ;
};
const ownedJson = async < T >( root : string , file : string ) : Promise < T > => {
const target = path . join ( root , file );
2026-09-15 15:23:24 -07:00
if ( ! ( await fs . realpath ( target )). startsWith ( ` ${ await fs . realpath ( root ) } /` ))
throw new Error ( "Scaffold input escapes repository" );
2026-09-10 18:27:41 -07:00
const value = JSON . parse ( await fs . readFile ( target , "utf8" ));
if ( value . generatedBy !== "qx-scaffold-v1" ) throw new Error ( `Not scaffold-owned: ${ file } ` );
return value ;
};
/** Recipes describe structural edits; planStructure owns validation/journaling.
2026-09-14 20:35:53 -07:00
* Implementation files are created once; later edits preserve authored wiring. */
2026-09-15 15:23:24 -07:00
export const scaffoldRecipe = async (
root : string ,
command : "package" | "function" | "migration" | "refresh" ,
spec : ScaffoldRecipe ,
) : Promise < StructuralRequest > => {
if ( command === "refresh" )
throw new Error (
"Scaffold refresh has been removed. Edit declarations and typed server wiring directly, then run qx-workspace check. Use scaffold function to add a declaration and handler together." ,
);
2026-09-10 18:27:41 -07:00
source ( spec . source );
2026-09-15 15:23:24 -07:00
if ( spec . directory && ! /^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/ . test ( spec . directory ))
throw new Error ( "Scaffold directory must be contained" );
2026-09-10 18:27:41 -07:00
const prefix = spec . directory ? ` ${ spec . directory } /` : "" ;
2026-09-21 00:44:47 -07:00
if (
spec . template === "typescript-execution" ||
( command !== "package" && ( await isExecutionScaffold ( root , prefix )))
) {
if ( spec . tools ) Object . values ( spec . tools ). forEach ( source );
return executionScaffold ( root , command , spec , prefix , nixSource );
}
2026-09-10 18:27:41 -07:00
const files : StructuralRequest [ "files" ] = [];
2026-09-15 15:23:24 -07:00
const create = ( file : string , content : string ) => files . push ({ file : prefix + file , create : content });
const generated = ( file : string , content : string ) => files . push ({ file : prefix + file , generated : content });
2026-09-14 20:35:53 -07:00
let packageModel : PackageEditModel ;
2026-09-15 15:23:24 -07:00
let catalog : MigrationCatalog & { generatedBy : "qx-scaffold-v1" };
2026-09-10 18:27:41 -07:00
if ( command === "package" ) {
const name = safeName ( spec . name );
2026-09-15 15:23:24 -07:00
if ( spec . template && ! [ "typescript" , "typescript-react" ]. includes ( spec . template ))
throw new Error ( "Unknown package template" );
2026-09-13 22:07:18 -07:00
const react = spec . template === "typescript-react" ;
2026-09-15 15:23:24 -07:00
if ( ! spec . id || ! spec . revision || ! spec . tools )
throw new Error ( "Package scaffold requires id, revision, and exact quixos/protocol/helpers/sdk tool sources" );
2026-09-10 18:27:41 -07:00
Object . values ( spec . tools ). forEach ( source );
2026-09-15 15:23:24 -07:00
packageModel = { generatedBy : "qx-scaffold-v1" , name , id : spec.id , revision : spec.revision , exports : [] };
catalog = { generatedBy : "qx-scaffold-v1" , schemaVersion : 1 , contracts : {}, migrations : [] };
if ( react )
packageModel . exports . push ({ name : "sourceGet" , id : `export: ${ name } :source` , file : "src/impl/sourceGet.ts" });
create (
"package.qx" ,
`package ${ name } id ${ JSON . stringify ( spec . id ) } revision ${ JSON . stringify ( spec . revision ) } { \ n ${ react ? ` function sourceGet id ${ JSON . stringify ( `export: ${ name } :source` ) } : unit -> string; \ n` : "" } } \ n` ,
);
create ( "quixos.lock" , formatQuixosLock ({ formatVersion : 1 , quixos : source ( spec . tools . quixos ), resources : [] }));
create (
"package.json" ,
json ({
name : `@quixos/ ${ name . toLowerCase () } ` ,
version : "0.1.0" ,
private : true ,
type : "module" ,
packageManager : "yarn@4.18.0" ,
scripts : { build : "tsc -p tsconfig.json" , typecheck : "tsc --noEmit" },
dependencies : {
"@quixos/camino-package-runtime" : ` ${ spec . tools . sdk . repository } #commit= ${ spec . tools . sdk . commit } ` ,
},
devDependencies : {
"@types/node" : "^24" ,
typescript : "^7.0.2" ,
...( react ? { react : "^18.3.1" , "@types/react" : "^18.3.12" , esbuild : "^0.25.12" } : {}),
},
}),
);
create (
"tsconfig.json" ,
json ({
compilerOptions : {
target : "ES2023" ,
module : "NodeNext" ,
moduleResolution : "NodeNext" ,
strict : true ,
types : [ "node" , ...( react ? [ "react" ] : [])],
outDir : "dist" ,
rootDir : "src" ,
skipLibCheck : true ,
...( react ? { jsx : "react-jsx" , esModuleInterop : true } : {}),
},
include : [ "src/**/*.ts" , "src/**/*.tsx" ],
}),
);
2026-09-13 22:07:18 -07:00
if ( react ) {
2026-09-15 15:23:24 -07:00
create (
"src/component.tsx" ,
2026-09-16 11:25:20 -07:00
`// Add a checked props export, select it in quixos.check.json options.react, \ n// then use its generated ReactResults type. Scaffold bundle does this for you. \ nexport default function Component(_props: {camino: Record<string, never>; render: unknown; dispatch: (action: unknown) => void}) { \ n return <section><h1> ${ name } </h1></section>; \ n} \ n` ,
2026-09-15 15:23:24 -07:00
);
create (
"src/impl/sourceGet.ts" ,
2026-09-15 20:13:10 -07:00
`import componentSource from "../component.js?browser-source"; \ nimport type {Implementation} from "../gen/qx.js"; \ nexport const handler: Implementation["sourceGet"] = () => componentSource; \ n` ,
2026-09-15 15:23:24 -07:00
);
create (
"src/browser-assets.d.ts" ,
`declare module "*?browser-source" { const source: string; export default source; } \ ndeclare module "*.css" {} \ n` ,
);
2026-09-15 20:13:10 -07:00
create ( "src/gen/web-studio-react-runtime.d.ts" , reactPlatformTypes );
2026-09-13 22:07:18 -07:00
}
2026-09-10 18:27:41 -07:00
create ( ".gitignore" , "node_modules/\ndist/\n.quixos/\nresult\n.yarn/install-state.gz\n" );
2026-09-15 15:23:24 -07:00
create (
".yarnrc.yml" ,
`nodeLinker: node-modules \ nenableScripts: true \ nnpmMinimalAgeGate: 0 \ napprovedGitRepositories: \ n - ${ JSON . stringify ( spec . tools . sdk . repository ) } \ nsupportedArchitectures: \ n os: [current, linux] \ n cpu: [current, x64, arm64] \ n libc: [current, glibc] \ n` ,
);
const nixifyPluginUrl =
spec . nixifyPluginUrl ??
"https://gitea-external.egads.tutti.syntaxblitz.net/quixos/yarn-plugin-nixify-patched/raw/commit/4528fdd20b30d869262443b3f044549810e75fb8/dist/yarn-plugin-nixify.js" ;
2026-09-10 18:27:41 -07:00
const plugin = new URL ( nixifyPluginUrl );
2026-09-15 15:23:24 -07:00
if (
plugin . protocol !== "https:" ||
plugin . username ||
plugin . password ||
plugin . search ||
plugin . hash ||
! /\/commit\/[a-f0-9]{40,64}\// . test ( plugin . pathname )
)
throw new Error ( "Nixify plugin must have an exact credential-free HTTPS commit URL" );
generated ( "quixos.toolchain.json" , json ({ generatedBy : "qx-scaffold-v1" , nixifyPluginUrl }));
create (
"quixos.check.json" ,
json ({
backend : "typescript" ,
2026-09-15 20:13:10 -07:00
bindingOutput : "src/gen/qx.ts" ,
2026-09-15 15:23:24 -07:00
...( react
? {
2026-09-16 11:25:20 -07:00
options : { react : { propsExports : [] } },
2026-09-15 15:23:24 -07:00
}
: {}),
}),
);
create (
"flake.nix" ,
`{
2026-09-10 18:27:41 -07:00
inputs.protocol.url = ${ nixString ( nixSource ( spec . tools . protocol )) } ;
inputs.nixpkgs.follows = "protocol/nixpkgs";
inputs.flake-utils.follows = "protocol/flake-utils";
inputs.helpers = { url = ${ nixString ( nixSource ( spec . tools . helpers )) } ; flake = false; };
outputs = inputs@{ self, protocol, nixpkgs, flake-utils, helpers, ... }:
(import (toString helpers + "/quixos-package-helpers.nix")).mkCaminoTsYarnNixifyFlake {
inherit inputs nixpkgs flake-utils; packageRoot = ./.;
2026-09-14 20:35:53 -07:00
bundle = { entry = ${ JSON . stringify ( react ? "src/server.ts" : "dist/server.js" ) } ; ${ react ? "browserSources = true;" : "" } };
2026-09-10 18:27:41 -07:00
migrationEntrypoint = "dist/migrate.js";
2026-09-15 20:13:10 -07:00
installServer = { libexecName = ${ JSON . stringify ( name . toLowerCase ()) } ; descriptorPath = "descriptor.quixos-package.txtpb"; };
2026-09-10 18:27:41 -07:00
};
2026-09-15 15:23:24 -07:00
} \ n` ,
);
2026-09-10 18:27:41 -07:00
} else {
catalog = await ownedJson < typeof catalog >( root , prefix + "quixos.migrations.json" );
2026-09-14 20:35:53 -07:00
// Derive the edit model from authored declarations; it is never persisted.
2026-09-14 15:24:37 -07:00
const authored = await fs . readFile ( path . join ( root , prefix , "package.qx" ), "utf8" );
const syntax = parseQx ( authored );
2026-09-15 15:23:24 -07:00
if ( syntax . diagnostics . length )
throw new Error ( "Cannot scaffold into an invalid package.qx; fix the reported syntax first" );
const declaration = [... walkSyntax ( syntax . root )]. find (( node ) => node . kind === "packageResourceDecl" );
2026-09-14 15:24:37 -07:00
if ( ! declaration ) throw new Error ( "Expected a package declaration" );
const text = ( node : typeof declaration ) => authored . slice ( node . start , node . end );
2026-09-15 15:23:24 -07:00
const literals = declaration . children . filter (( node ) => node . kind === "stringLiteral" );
packageModel = {
generatedBy : "qx-scaffold-v1" ,
name : text ( declaration . children . find (( node ) => node . kind === "identifier" ) ! ),
id : JSON.parse ( text ( literals [ 0 ])),
revision : JSON.parse ( text ( literals [ 1 ])),
exports : [],
};
2026-09-14 15:24:37 -07:00
for ( const node of walkSyntax ( declaration )) {
2026-09-15 15:23:24 -07:00
if ( ! [ "packageFunctionExport" , "packageOperationExport" , "packageConstructorExport" ]. includes ( node . kind ))
continue ;
const name = safeName ( text ( node . children . find (( child ) => child . kind === "identifier" ) ! ));
const id = JSON . parse ( text ( node . children . find (( child ) => child . kind === "stringLiteral" ) ! ));
if ( packageModel . exports . some (( entry ) => entry . id === id || entry . name === name ))
throw new Error ( "Duplicate package export name or ID" );
const migration = catalog . migrations . find (( entry ) => entry . implementation . exportId === id );
packageModel . exports . push ({
name ,
id ,
file : migration?.implementation.file ?? `src/impl/ ${ name } .ts` ,
...( migration ? { migration : true } : {}),
});
2026-09-14 15:24:37 -07:00
}
2026-09-14 20:35:53 -07:00
{
2026-09-10 18:27:41 -07:00
const name = safeName ( spec . name );
2026-09-15 15:23:24 -07:00
if ( ! spec . id || packageModel . exports . some (( entry ) => entry . id === spec . id || entry . name === name ))
throw new Error ( "New export requires a unique name and ID" );
2026-09-10 18:27:41 -07:00
const declaration = spec . declaration ?? `function ${ name } id ${ JSON . stringify ( spec . id ) } : unit -> unit;` ;
const parsed = parseQx ( `package Scaffold id "scaffold" revision "scaffold@1" { ${ declaration } }` );
2026-09-15 15:23:24 -07:00
const exports = [... walkSyntax ( parsed . root )]. filter (( node ) =>
[ "packageFunctionExport" , "packageOperationExport" , "packageConstructorExport" ]. includes ( node . kind ),
);
if ( parsed . diagnostics . length || exports . length !== 1 )
throw new Error ( "Expected one valid package export declaration" );
2026-09-10 18:27:41 -07:00
const wrapped = `package Scaffold id "scaffold" revision "scaffold@1" { ${ declaration } }` ;
const node = exports [ 0 ];
const derived = [... walkSyntax ( node )]. some (( entry ) => entry . kind === "eventClause" );
2026-09-15 15:23:24 -07:00
if ( command === "migration" && ( node . kind !== "packageFunctionExport" || spec . declaration ))
throw new Error (
"Migration exports use the scaffold's unit function declaration and dedicated migration entrypoint" ,
);
if (
wrapped . slice (
node . children . find (( child ) => child . kind === "identifier" ) ! . start ,
node . children . find (( child ) => child . kind === "identifier" ) ! . end ,
) !== name ||
JSON . parse (
wrapped . slice (
node . children . find (( child ) => child . kind === "stringLiteral" ) ! . start ,
node . children . find (( child ) => child . kind === "stringLiteral" ) ! . end ,
),
) !== spec . id
)
throw new Error ( "Declaration name/ID must match its registration" );
files . push ({
file : prefix + "package.qx" ,
edits : [
{ operation : "append" , parent : { kind : "packageResourceDecl" , id : packageModel.id }, source : declaration },
],
});
2026-09-10 18:27:41 -07:00
const file = `src/ ${ command === "migration" ? "migrations" : "impl" } / ${ name } .ts` ;
2026-09-15 15:23:24 -07:00
const implementation =
command === "migration"
? `import type {MigrationContext} from "@quixos/camino-package-runtime"; \ nexport const handler = async (_context: MigrationContext): Promise<void> => { throw new Error( ${ JSON . stringify ( `Implement migration ${ name } ` ) } ); }; \ n`
2026-09-15 20:13:10 -07:00
: `import type {Implementation} from "../gen/qx.js"; \ nexport const handler: Implementation[ ${ JSON . stringify ( name ) } ] = ${ derived ? '{kind: "derived", get: ' : "" } async (_context) => { throw new Error( ${ JSON . stringify ( `Implement ${ name } ` ) } ); } ${ derived ? "}" : "" } ; \ n` ;
2026-09-10 18:27:41 -07:00
create ( file , implementation );
2026-09-15 15:23:24 -07:00
packageModel . exports . push ({ name , id : spec.id , file , ...( command === "migration" ? { migration : true } : {}) });
2026-09-10 18:27:41 -07:00
if ( command === "migration" ) {
2026-09-15 15:23:24 -07:00
if ( ! spec . migration )
throw new Error ( "Migration scaffold requires retained contracts and an explicit transition" );
const { contracts , ... transition } = spec . migration ;
2026-09-10 18:27:41 -07:00
for ( const [ digest , contract ] of Object . entries ( contracts )) {
2026-09-15 15:23:24 -07:00
if (
contentDigest ( contract ) !== digest ||
( catalog . contracts [ digest ] && contentDigest ( catalog . contracts [ digest ]) !== digest )
)
throw new Error ( "Retained migration contract mismatch" );
2026-09-10 18:27:41 -07:00
catalog . contracts [ digest ] = contract ;
}
2026-09-15 15:23:24 -07:00
catalog . migrations . push ({
... transition ,
implementation : { exportId : spec.id , file , digest : contentDigest ( implementation ) },
});
2026-09-10 18:27:41 -07:00
}
}
}
2026-09-14 20:35:53 -07:00
validateMigrationCatalog ( catalog , new Set ( packageModel . exports . map (( entry ) => entry . id )));
2026-09-10 18:27:41 -07:00
generated ( "quixos.migrations.json" , json ( catalog ));
2026-09-14 20:35:53 -07:00
if ( command !== "package" ) {
const entry = packageModel . exports . at ( - 1 ) ! ;
const file = prefix + "src/server.ts" ;
const before = await fs . readFile ( path . join ( root , file ), "utf8" );
2026-09-15 15:23:24 -07:00
files . push ({
file ,
expected : before ,
replace : addImplementation (
before ,
"createRuntime" ,
entry . name ,
`./ ${ entry . file . slice ( 4 , - 3 ) } .js` ,
!! entry . migration ,
),
});
2026-09-14 20:35:53 -07:00
if ( entry . migration ) {
const file = prefix + "src/migrate.ts" ;
const before = await fs . readFile ( path . join ( root , file ), "utf8" );
2026-09-15 15:23:24 -07:00
files . push ({
file ,
expected : before ,
replace : addImplementation ( before , "serveMigration" , entry . id , `./ ${ entry . file . slice ( 4 , - 3 ) } .js` ),
});
2026-09-14 20:35:53 -07:00
}
} else {
2026-09-15 15:23:24 -07:00
create (
"src/server.ts" ,
2026-09-15 20:13:10 -07:00
`import {servePackageRuntime} from "@quixos/camino-package-runtime"; \ nimport {createRuntime} from "./gen/qx.js"; \ n` +
2026-09-15 15:23:24 -07:00
packageModel . exports
. filter (( entry ) => ! entry . migration )
. map (
( entry , index ) =>
`import {handler as impl ${ index } } from ${ JSON . stringify ( `./ ${ entry . file . slice ( 4 , - 3 ) } .js` ) } ; \ n` ,
)
. join ( "" ) +
`servePackageRuntime(createRuntime({ \ n` +
packageModel . exports
. map (
( entry ) =>
` ${ JSON . stringify ( entry . name ) } : ${ entry . migration ? 'async () => { throw new Error("Migration-only export"); }' : `impl ${ packageModel . exports . filter (( value ) => ! value . migration ). indexOf ( entry ) } ` } ,` ,
)
. join ( "\n" ) +
` \ n})); \ n` ,
);
const migrations = packageModel . exports . filter (( entry ) => entry . migration );
create (
"src/migrate.ts" ,
`import {serveMigration} from "@quixos/camino-package-runtime"; \ n` +
migrations
. map (
( entry , index ) =>
`import {handler as impl ${ index } } from ${ JSON . stringify ( `./ ${ entry . file . slice ( 4 , - 3 ) } .js` ) } ; \ n` ,
)
. join ( "" ) +
`await serveMigration({ ${ migrations . map (( entry , index ) => ` ${ JSON . stringify ( entry . id ) } : impl ${ index } ` ). join ( ", " ) } }); \ n` ,
);
2026-09-14 20:35:53 -07:00
}
2026-09-15 15:23:24 -07:00
generated (
2026-09-15 20:13:10 -07:00
"descriptor.quixos-package.txtpb" ,
2026-09-15 15:23:24 -07:00
`# Generated by qx-scaffold-v1 \ npackage_id: ${ JSON . stringify ( packageModel . id ) } \ npackage_revision_id: ${ JSON . stringify ( packageModel . revision ) } \ nruntime_protocol_version: "quixos-capabilities-v1" \ n` +
packageModel . exports
. map (
( entry ) =>
`exports: { export_id: ${ JSON . stringify ( entry . id ) } runtime_symbol: ${ JSON . stringify ( entry . name ) } } \ n` ,
)
. join ( "" ),
);
2026-09-15 13:45:42 -07:00
if ( spec . initialFiles ) {
if ( command !== "package" ) throw new Error ( "initialFiles is only valid when creating a package" );
const authored = spec . initialFiles [ "package.qx" ];
if ( authored !== undefined ) {
const parsed = parseQx ( authored );
2026-09-15 15:23:24 -07:00
const declaration = [... walkSyntax ( parsed . root )]. find (( n ) => n . kind === "packageResourceDecl" );
const literals = declaration ? . children . filter (( n ) => n . kind === "stringLiteral" ) ?? [];
const identifier = declaration ? . children . find (( n ) => n . kind === "identifier" );
if (
parsed . diagnostics . length ||
literals . length !== 2 ||
! identifier ||
authored . slice ( identifier . start , identifier . end ) !== spec . name ||
JSON . parse ( authored . slice ( literals [ 0 ]. start , literals [ 0 ]. end )) !== spec . id ||
JSON . parse ( authored . slice ( literals [ 1 ]. start , literals [ 1 ]. end )) !== spec . revision
)
2026-09-15 13:45:42 -07:00
throw new Error ( "Initial package declaration must match the provisioned name, ID and revision" );
}
for ( const [ file , content ] of Object . entries ( spec . initialFiles )) {
2026-09-15 15:23:24 -07:00
if (
typeof content !== "string" ||
2026-09-16 11:25:20 -07:00
[ "quixos.lock" , "flake.nix" , "quixos.toolchain.json" , "package.json" ]. includes ( file )
2026-09-15 15:23:24 -07:00
)
throw new Error ( `Not an initial authored file: ${ file } ` );
2026-09-16 11:25:20 -07:00
if ( file === "quixos.check.json" ) {
const check = JSON . parse ( content );
if (
check . backend !== "typescript" ||
check . bindingOutput !== "src/gen/qx.ts" ||
Object . keys ( check ). some (( key ) => ! [ "backend" , "bindingOutput" , "options" ]. includes ( key ))
)
throw new Error (
"Initial check configuration may customize binding options, not the scaffold verification backend or output" ,
);
}
2026-09-15 15:23:24 -07:00
const existing = files . findIndex (( entry ) => entry . file === prefix + file );
2026-09-15 13:45:42 -07:00
if ( existing >= 0 ) files . splice ( existing , 1 );
create ( file , content );
}
// A composed React recipe supplies its own modules and complete server.
if ( reactRecipe ( spec )) {
2026-09-15 20:13:10 -07:00
for ( const file of [ "src/component.tsx" , "src/impl/sourceGet.ts" , "descriptor.quixos-package.txtpb" ])
2026-09-15 15:23:24 -07:00
if ( ! ( file in spec . initialFiles )) {
const index = files . findIndex (( entry ) => entry . file === prefix + file );
if ( index >= 0 ) files . splice ( index , 1 );
}
2026-09-15 13:45:42 -07:00
}
}
2026-09-15 15:23:24 -07:00
return { kind : "package" , source : spec.source , resourceRoot : spec.directory , validation : "syntax" , files };
2026-09-10 18:27:41 -07:00
};
2026-09-15 13:45:42 -07:00
2026-09-15 15:23:24 -07:00
const reactRecipe = ( spec : ScaffoldRecipe ) =>
spec . template === "typescript-react" && spec . initialFiles ? .[ "package.qx" ] && spec . initialFiles ? .[ "src/server.ts" ];