Provide typed React live-field authoring, strict CLI arguments, and timed build stages
This commit is contained in:
@@ -52,6 +52,7 @@
|
|||||||
--bundle --platform=node --target=node24 --format=esm \
|
--bundle --platform=node --target=node24 --format=esm \
|
||||||
--outfile=quixos-lock-check.mjs
|
--outfile=quixos-lock-check.mjs
|
||||||
esbuild dist/src/bindings/cli.js --bundle --platform=node --target=node24 --format=esm --outfile=quixos-codegen-ts.mjs
|
esbuild dist/src/bindings/cli.js --bundle --platform=node --target=node24 --format=esm --outfile=quixos-codegen-ts.mjs
|
||||||
|
esbuild dist/src/bindings/react-platform.js --bundle --platform=node --target=node24 --format=esm --outfile=react-platform.mjs
|
||||||
esbuild dist/src/capability-language/tool-cli.js --bundle --platform=node --target=node24 --format=esm --outfile=quixos-qx.mjs
|
esbuild dist/src/capability-language/tool-cli.js --bundle --platform=node --target=node24 --format=esm --outfile=quixos-qx.mjs
|
||||||
runHook postBuild
|
runHook postBuild
|
||||||
'';
|
'';
|
||||||
@@ -105,6 +106,7 @@ EOF
|
|||||||
chmod +x "$out/bin/quixos-lock-check"
|
chmod +x "$out/bin/quixos-lock-check"
|
||||||
mkdir -p "$out/libexec/quixos-protocol"
|
mkdir -p "$out/libexec/quixos-protocol"
|
||||||
install -m644 client-codegen.mjs "$out/libexec/quixos-protocol/client-codegen.mjs"
|
install -m644 client-codegen.mjs "$out/libexec/quixos-protocol/client-codegen.mjs"
|
||||||
|
install -m644 react-platform.mjs "$out/libexec/quixos-protocol/react-platform.mjs"
|
||||||
install -m644 quixos-codegen-ts.mjs quixos-qx.mjs "$out/libexec/quixos-protocol/"
|
install -m644 quixos-codegen-ts.mjs quixos-qx.mjs "$out/libexec/quixos-protocol/"
|
||||||
install -m644 quixos-descriptor-check.mjs "$out/libexec/quixos-protocol/quixos-descriptor-check.mjs"
|
install -m644 quixos-descriptor-check.mjs "$out/libexec/quixos-protocol/quixos-descriptor-check.mjs"
|
||||||
install -m644 quixos-capability-compile.mjs "$out/libexec/quixos-protocol/quixos-capability-compile.mjs"
|
install -m644 quixos-capability-compile.mjs "$out/libexec/quixos-protocol/quixos-capability-compile.mjs"
|
||||||
|
|||||||
+7
-2
@@ -1,13 +1,18 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { readFile, writeFile } from "node:fs/promises";
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
import { generateTypeScriptBindings } from "./index.js";
|
import { generateTypeScriptBindings } from "./index.js";
|
||||||
|
import path from "node:path";
|
||||||
|
import {reactPlatformTypes} from "./react-platform.js";
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const [schema, revision, output, options, ...rest] = process.argv.slice(2);
|
const [schema, revision, output, options, ...rest] = process.argv.slice(2);
|
||||||
if (!schema || !revision || !output || rest.length) throw new Error(
|
if (!schema || !revision || !output || rest.length) throw new Error(
|
||||||
"usage: quixos-codegen-ts SCHEMA.json PACKAGE_REVISION OUTPUT.ts [OPTIONS.json]");
|
"usage: quixos-codegen-ts SCHEMA.json PACKAGE_REVISION OUTPUT.ts [OPTIONS.json]");
|
||||||
const generated = generateTypeScriptBindings(JSON.parse(await readFile(schema, "utf8")), revision,
|
const config = options ? JSON.parse(await readFile(options, "utf8")) : {};
|
||||||
options ? JSON.parse(await readFile(options, "utf8")) : {});
|
const generated = generateTypeScriptBindings(JSON.parse(await readFile(schema, "utf8")), revision, config);
|
||||||
await writeFile(output, generated);
|
await writeFile(output, generated);
|
||||||
|
if (config.messages?.["org.quixos.web-studio.ReactProps"]) {
|
||||||
|
await writeFile(path.join(path.dirname(output), "web-studio-react-runtime.d.ts"), reactPlatformTypes);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
main().catch((error: unknown) => { console.error(error instanceof Error ? error.message : error); process.exitCode = 1; });
|
main().catch((error: unknown) => { console.error(error instanceof Error ? error.message : error); process.exitCode = 1; });
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/** One authoring contract, distributed by scaffolding and immutable codegen.
|
||||||
|
* Web Studio checks its concrete exports against this contract during its build. */
|
||||||
|
export const reactPlatformTypes = `// Generated by the Quixos React platform contract. Do not edit.
|
||||||
|
declare module "@quixos/web-studio-react-runtime" {
|
||||||
|
import type * as React from "react";
|
||||||
|
export function useComponentOverlayContainer(): HTMLElement;
|
||||||
|
export function useComponentStyleRoot(): ShadowRoot;
|
||||||
|
export type ObjectRef<AtomId extends string> = string & {
|
||||||
|
readonly $quixosAtom: AtomId;
|
||||||
|
};
|
||||||
|
export type LiveFieldProp<T> = {
|
||||||
|
value: T;
|
||||||
|
source: {
|
||||||
|
objectId: string;
|
||||||
|
slotId: string;
|
||||||
|
valueType?: string;
|
||||||
|
storagePolicy?: string;
|
||||||
|
revision?: string | number | bigint;
|
||||||
|
crdtSnapshot?: {
|
||||||
|
type: string;
|
||||||
|
encoding: string;
|
||||||
|
payload: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ReactComponentHostProps<Action> = {
|
||||||
|
onAction?: (action: Action) => void;
|
||||||
|
fallback?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
};
|
||||||
|
export type ReactComponentImplementationProps<CaminoProps, RenderProps, Action> = {
|
||||||
|
camino: CaminoProps;
|
||||||
|
render: RenderProps;
|
||||||
|
dispatch: (action: Action) => void;
|
||||||
|
};
|
||||||
|
export const createWebStudioComponent: <
|
||||||
|
ForObject extends ObjectRef<string>, RenderProps extends object, Action,
|
||||||
|
>(config: { expectedAtomId: string }) => (props: {
|
||||||
|
forObject: ForObject;
|
||||||
|
} & RenderProps & ReactComponentHostProps<Action>) => any;
|
||||||
|
export const invokeCapability: <Result = unknown>(
|
||||||
|
objectId: string,
|
||||||
|
interfaceRevisionId: string,
|
||||||
|
operationId: string,
|
||||||
|
value?: unknown,
|
||||||
|
options?: {clientMutationId?: string},
|
||||||
|
) => Promise<Result>;
|
||||||
|
export const h: typeof React.createElement;
|
||||||
|
export const useLiveField: <T>(
|
||||||
|
field: LiveFieldProp<T>,
|
||||||
|
options?: {
|
||||||
|
reconcileRegister?: (state: {
|
||||||
|
confirmed: T;
|
||||||
|
optimistic: T;
|
||||||
|
pending: boolean;
|
||||||
|
}) => T;
|
||||||
|
},
|
||||||
|
) => readonly [T, (value: T) => Promise<void>];
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -22,6 +22,7 @@ export async function checkAuthoring(start: string, output: string, options: { b
|
|||||||
directory, checker: checkerIdentity(), candidateOnly: true, activationEvidence: false, blockers: [], phase: "convergence", output,
|
directory, checker: checkerIdentity(), candidateOnly: true, activationEvidence: false, blockers: [], phase: "convergence", output,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
console.error(`[${new Date().toISOString()}] Check: capture source and converge dependencies`);
|
||||||
// Serialize only source capture, not the potentially slow Nix build.
|
// Serialize only source capture, not the potentially slow Nix build.
|
||||||
// Repository-scoped agents can check separate immutable candidates in parallel.
|
// Repository-scoped agents can check separate immutable candidates in parallel.
|
||||||
const captured = await promisify(callback)("quixos-qx", ["converge", context.workbench, directory], {
|
const captured = await promisify(callback)("quixos-qx", ["converge", context.workbench, directory], {
|
||||||
@@ -32,6 +33,7 @@ export async function checkAuthoring(start: string, output: string, options: { b
|
|||||||
});
|
});
|
||||||
const converged = JSON.parse(captured.stdout) as Awaited<ReturnType<typeof convergeAuthoring>>;
|
const converged = JSON.parse(captured.stdout) as Awaited<ReturnType<typeof convergeAuthoring>>;
|
||||||
timings.captureMs = Math.round(performance.now() - started);
|
timings.captureMs = Math.round(performance.now() - started);
|
||||||
|
console.error(`[${new Date().toISOString()}] Check: source captured in ${(timings.captureMs / 1000).toFixed(1)}s`);
|
||||||
if (!converged.candidate) {
|
if (!converged.candidate) {
|
||||||
report.phase = converged.worklist.find(entry => entry.phase !== "dependency")?.phase ?? "convergence";
|
report.phase = converged.worklist.find(entry => entry.phase !== "dependency")?.phase ?? "convergence";
|
||||||
throw new Error(converged.worklist.map(entry => `${entry.directory} [${entry.phase}]: ${entry.message}`).join("\n"));
|
throw new Error(converged.worklist.map(entry => `${entry.directory} [${entry.phase}]: ${entry.message}`).join("\n"));
|
||||||
@@ -39,8 +41,13 @@ export async function checkAuthoring(start: string, output: string, options: { b
|
|||||||
report.commit = converged.candidate.commit;
|
report.commit = converged.candidate.commit;
|
||||||
report.phase = "verification";
|
report.phase = "verification";
|
||||||
const buildStarted = performance.now();
|
const buildStarted = performance.now();
|
||||||
|
console.error(`[${new Date().toISOString()}] Check: immutable Nix ${options.contractOnly ? "contract" : "verification"}; build output: ${path.join(output, "nix.log")}`);
|
||||||
|
try {
|
||||||
report.artifactPath = await buildImmutableCandidate(converged.candidate, resource.kind, path.join(output, "nix.log"), options.contractOnly);
|
report.artifactPath = await buildImmutableCandidate(converged.candidate, resource.kind, path.join(output, "nix.log"), options.contractOnly);
|
||||||
|
} finally {
|
||||||
timings.immutableCheckMs = Math.round(performance.now() - buildStarted);
|
timings.immutableCheckMs = Math.round(performance.now() - buildStarted);
|
||||||
|
console.error(`[${new Date().toISOString()}] Check: immutable phase ended after ${(timings.immutableCheckMs / 1000).toFixed(1)}s`);
|
||||||
|
}
|
||||||
const candidateText = await fs.readFile(path.join(report.artifactPath, "candidate.json"), "utf8");
|
const candidateText = await fs.readFile(path.join(report.artifactPath, "candidate.json"), "utf8");
|
||||||
await fs.writeFile(path.join(output, "candidate.json"), candidateText);
|
await fs.writeFile(path.join(output, "candidate.json"), candidateText);
|
||||||
report.compilation = "passed";
|
report.compilation = "passed";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {formatQuixosLock, type GitSource} from "../resource-lock/index.js";
|
|||||||
import {parseQx, walkSyntax} from "./source.js";
|
import {parseQx, walkSyntax} from "./source.js";
|
||||||
import type {StructuralRequest} from "./structural-plan.js";
|
import type {StructuralRequest} from "./structural-plan.js";
|
||||||
import {addImplementation} from "./implementation-edit.js";
|
import {addImplementation} from "./implementation-edit.js";
|
||||||
|
import {reactPlatformTypes} from "../bindings/react-platform.js";
|
||||||
|
|
||||||
type Source = {repository: string; commit: string};
|
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}[]};
|
type PackageEditModel = {generatedBy: "qx-scaffold-v1"; name: string; id: string; revision: string; exports: {name: string; id: string; file: string; migration?: boolean}[]};
|
||||||
@@ -69,6 +70,7 @@ export const scaffoldRecipe = async (root: string, command: "package" | "functio
|
|||||||
create("src/component.tsx", `// Props are opaque at the platform boundary until capability generics exist.\nexport default function Component(_props: {camino: unknown; render: unknown; dispatch: (action: unknown) => void}) {\n return <section><h1>${name}</h1><p>Edit this component, then run qx-workspace check.</p></section>;\n}\n`);
|
create("src/component.tsx", `// Props are opaque at the platform boundary until capability generics exist.\nexport default function Component(_props: {camino: unknown; render: unknown; dispatch: (action: unknown) => void}) {\n return <section><h1>${name}</h1><p>Edit this component, then run qx-workspace check.</p></section>;\n}\n`);
|
||||||
create("src/impl/sourceGet.ts", `import componentSource from "../component.js?browser-source";\nimport type {Implementation} from "../gen/qx.js";\nexport const handler: Implementation["sourceGet"] = () => componentSource;\n`);
|
create("src/impl/sourceGet.ts", `import componentSource from "../component.js?browser-source";\nimport type {Implementation} from "../gen/qx.js";\nexport const handler: Implementation["sourceGet"] = () => componentSource;\n`);
|
||||||
create("src/browser-assets.d.ts", `declare module "*?browser-source" { const source: string; export default source; }\n`);
|
create("src/browser-assets.d.ts", `declare module "*?browser-source" { const source: string; export default source; }\n`);
|
||||||
|
create("src/gen/web-studio-react-runtime.d.ts", reactPlatformTypes);
|
||||||
}
|
}
|
||||||
create(".gitignore", "node_modules/\ndist/\n.quixos/\nresult\n.yarn/install-state.gz\n");
|
create(".gitignore", "node_modules/\ndist/\n.quixos/\nresult\n.yarn/install-state.gz\n");
|
||||||
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`);
|
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`);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {scaffoldRecipe} from "../src/capability-language/scaffold-recipes.js";
|
|||||||
import {planStructure, applyStructure} from "../src/capability-language/structural-plan.js";
|
import {planStructure, applyStructure} from "../src/capability-language/structural-plan.js";
|
||||||
import {contentDigest} from "../src/capability-model/evolution.js";
|
import {contentDigest} from "../src/capability-model/evolution.js";
|
||||||
import {sealMigrations} from "../src/capability-language/migration-seal.js";
|
import {sealMigrations} from "../src/capability-language/migration-seal.js";
|
||||||
|
import {reactPlatformTypes} from "../src/bindings/react-platform.js";
|
||||||
const execFile = promisify(callback);
|
const execFile = promisify(callback);
|
||||||
|
|
||||||
test("React preset applies its browser build script and shared-platform imports", async (context) => {
|
test("React preset applies its browser build script and shared-platform imports", async (context) => {
|
||||||
@@ -20,6 +21,7 @@ test("React preset applies its browser build script and shared-platform imports"
|
|||||||
await applyStructure(await planStructure(root, request));
|
await applyStructure(await planStructure(root, request));
|
||||||
assert.match(await fs.readFile(path.join(root, "src/impl/sourceGet.ts"), "utf8"), /component.js\?browser-source/);
|
assert.match(await fs.readFile(path.join(root, "src/impl/sourceGet.ts"), "utf8"), /component.js\?browser-source/);
|
||||||
assert.match(await fs.readFile(path.join(root, "flake.nix"), "utf8"), /browserSources = true/);
|
assert.match(await fs.readFile(path.join(root, "flake.nix"), "utf8"), /browserSources = true/);
|
||||||
|
assert.equal(await fs.readFile(path.join(root, "src/gen/web-studio-react-runtime.d.ts"), "utf8"), reactPlatformTypes);
|
||||||
assert.equal(JSON.parse(await fs.readFile(path.join(root, "quixos.check.json"), "utf8")).options.messages["org.quixos.web-studio.ReactProps"].export, "opaqueReactPropsBinding");
|
assert.equal(JSON.parse(await fs.readFile(path.join(root, "quixos.check.json"), "utf8")).options.messages["org.quixos.web-studio.ReactProps"].export, "opaqueReactPropsBinding");
|
||||||
await assert.rejects(fs.access(path.join(root, "quixos.scaffold.json")));
|
await assert.rejects(fs.access(path.join(root, "quixos.scaffold.json")));
|
||||||
assert.equal(JSON.parse(await fs.readFile(path.join(root, "tsconfig.json"), "utf8")).compilerOptions.jsx, "react-jsx");
|
assert.equal(JSON.parse(await fs.readFile(path.join(root, "tsconfig.json"), "utf8")).compilerOptions.jsx, "react-jsx");
|
||||||
|
|||||||
Reference in New Issue
Block a user