Build checked component artifacts and shared React session SDK (WI-09)

This commit is contained in:
Timothy J. Aveni
2026-09-21 16:01:48 -07:00
parent 24073d7f2a
commit 2cf53280e4
9 changed files with 104 additions and 5 deletions
+6
View File
@@ -119,6 +119,12 @@ let
typescript = runtimePackages.camino-replica-core.dependencies; typescript = runtimePackages.camino-replica-core.dependencies;
packageRevisionId = candidate.revision.revisionId; packageRevisionId = candidate.revision.revisionId;
} }
// pkgs.lib.optionalAttrs ((candidate.revision.components or [ ]) != [ ]) {
react = {
sdk = runtimePackages.camino-react;
runtime = runtimePackages.camino-react-runtime;
};
}
else else
{ {
schema = schema =
@@ -56,7 +56,7 @@ export async function executionScaffold(
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
in { in {
quixosPackages.checkedArtifacts = { plan, generator, core, sharedRuntime, typescript, packageRevisionId }: quixosPackages.checkedArtifacts = { plan, generator, core, sharedRuntime, typescript, packageRevisionId }:
(import (helpers + "/portable-artifacts.nix")) { (import (helpers + "/artifacts.nix")) {
inherit pkgs plan core sharedRuntime typescript packageRevisionId; inherit pkgs plan core sharedRuntime typescript packageRevisionId;
protocol = generator; protocol = generator;
src = ./.; src = ./.;
+9
View File
@@ -2692,6 +2692,15 @@ const compileCapabilityResourceSourceInternal = (
externalInterfaces, externalInterfaces,
revision: lowerPackage(state, context, options.source), revision: lowerPackage(state, context, options.source),
}; };
if (resource.revision.components?.length) {
resource.revision.clientInterfaces = Object.fromEntries(
imports.flatMap((entry) => {
if (entry.kind !== "interface") return [];
const contract = options.environment?.interfaces?.get(entry.binding);
return contract && !contract.template ? [[entry.binding, contract.revisionId]] : [];
}),
);
}
} }
if (resource.kind === "interface") { if (resource.kind === "interface") {
+4
View File
@@ -15,6 +15,10 @@ export function validatePresentations(workspace: WorkspaceRevision): CapabilityV
if (!contract || contract.template) fail(path, `Expected an imported closed subject interface: ${id}`); if (!contract || contract.template) fail(path, `Expected an imported closed subject interface: ${id}`);
}; };
for (const [index, pkg] of workspace.packageImports.entries()) { for (const [index, pkg] of workspace.packageImports.entries()) {
for (const [alias, id] of Object.entries(pkg.clientInterfaces ?? {})) {
text(alias, `packageImports[${index}].clientInterfaces`);
subject(id, `packageImports[${index}].clientInterfaces.${alias}`);
}
const ids = new Set(pkg.exports.map((entry) => entry.id as string)); const ids = new Set(pkg.exports.map((entry) => entry.id as string));
const names = new Set([ const names = new Set([
...pkg.exports.map((entry) => entry.displayName), ...pkg.exports.map((entry) => entry.displayName),
+2
View File
@@ -305,6 +305,8 @@ export interface WorkspacePresentation {
} }
export interface PackageRevision { export interface PackageRevision {
/** Closed interface imports exposed to this package's checked component bindings. */
clientInterfaces?: Record<string, InterfaceRevisionId>;
controllers?: PackageController[]; controllers?: PackageController[];
components?: PackageComponent[]; components?: PackageComponent[];
queries?: import("../query/types.js").QueryDeclaration[]; queries?: import("../query/types.js").QueryDeclaration[];
+56 -1
View File
@@ -127,7 +127,62 @@ export function generatePackageQueryClientBindings(
): string { ): string {
const queries = (pkg.checkedQueries ?? []).map( const queries = (pkg.checkedQueries ?? []).map(
(query) => (query) =>
`${JSON.stringify(query.declaration.displayName)}: definePackageQuery<${tsType(query.variables)}, ${tsType(query.output)}>(${JSON.stringify(publicQueryContract(pkg, query.declaration.id))})`, `${JSON.stringify(query.declaration.displayName)}: definePackageQuery<${tsType(query.variables)}, ${tsType(query.output)}, ${JSON.stringify(publicQueryContract(pkg, query.declaration.id).placement)}>(${JSON.stringify(publicQueryContract(pkg, query.declaration.id))})`,
); );
return `// Generated from checked package query contracts. Regenerate; do not edit.\nimport { definePackageQuery, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const queries = {${queries.join(",\n")}} as const;\n`; return `// Generated from checked package query contracts. Regenerate; do not edit.\nimport { definePackageQuery, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const queries = {${queries.join(",\n")}} as const;\n`;
} }
/** Receipt-covered bindings expose the package's imports, component subjects and
* declared queries, never the workspace's complete atom/interface inventory. */
export function generateComponentClientBindings(plan: ExecutionPlan, packageRevision: string): string {
const pkg = plan.definition.workspace.packageImports.find((pkg) => pkg.revisionId === packageRevision);
if (!pkg || !pkg.components?.length)
throw new ExecutionPlanError("NO_COMPONENT_CONTRACT", packageRevision, "Package declares no component entries");
const byId = new Map(plan.definition.workspace.interfaceImports.map((iface) => [iface.revisionId as string, iface]));
const find = (id: string) => {
const contract = byId.get(id);
if (!contract || contract.template)
throw new ExecutionPlanError("OPEN_CLIENT_CONTRACT", id, "Component contracts must be closed");
return contract;
};
const imports = Object.fromEntries(
Object.entries(pkg.clientInterfaces ?? {}).map(([alias, id]) => [alias, find(id)]),
);
const subjects = Object.fromEntries(
pkg.components.map((component) => [component.displayName, find(component.subject)]),
);
const interfaces = generateInterfaceClientBindings(imports, "@quixos/camino-react");
const subjectBindings = generateInterfaceClientBindings(subjects, "@quixos/camino-react")
.split("\n")
.filter((line) => !line.startsWith("import "))
.join("\n")
.replace("export const interfaces =", "export const subjects =");
const queries = generatePackageQueryClientBindings(pkg, "@quixos/camino-react")
.split("\n")
.filter((line) => !line.startsWith("import "))
.join("\n");
return (
interfaces +
'\nimport {definePackageQuery,type ComponentType,type ComponentProps} from "@quixos/camino-react";\n' +
subjectBindings +
"\n" +
queries +
"\nexport interface Components {\n" +
pkg.components
.map(
(component) =>
`${JSON.stringify(component.displayName)}: ComponentType<ComponentProps<${JSON.stringify(component.subject)}>>;`,
)
.join("\n") +
"\n}\n"
);
}
export function componentAssetDeclarations(): string {
return (
"// Generated component asset modules. Regenerate; do not edit.\n" +
["svg", "png", "jpg", "jpeg", "gif", "webp", "avif", "woff", "woff2", "ttf", "ico"]
.map((extension) => `declare module "*.${extension}" { const value: string; export default value; }`)
.join("\n") +
'\ndeclare module "*.css";\n'
);
}
+2
View File
@@ -104,6 +104,7 @@ export interface InstalledComponent {
readonly subject: string; readonly subject: string;
readonly url: string; readonly url: string;
readonly exportName: string; readonly exportName: string;
readonly styles: readonly string[];
readonly compatibility: ComponentArtifact["compatibility"]; readonly compatibility: ComponentArtifact["compatibility"];
readonly sharedModules: Readonly<Record<keyof ComponentArtifact["sharedModules"], string>>; readonly sharedModules: Readonly<Record<keyof ComponentArtifact["sharedModules"], string>>;
} }
@@ -163,6 +164,7 @@ export async function loadExecutionInstallation(
subject: component.subject, subject: component.subject,
compatibility: component.compatibility, compatibility: component.compatibility,
exportName: component.exportName, exportName: component.exportName,
styles: component.files.filter((file) => file.mediaType === "text/css").map(contentArtifactUrl),
sharedModules, sharedModules,
url: contentArtifactUrl(component.files.find((file) => file.path === component.path)!), url: contentArtifactUrl(component.files.find((file) => file.path === component.path)!),
}); });
+11
View File
@@ -3,6 +3,7 @@ import { generatePortableTypeScriptBindings, generateRemoteTypeScriptBindings }
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js"; import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
import { type ArtifactReader, type PackageArtifacts } from "./artifacts.js"; import { type ArtifactReader, type PackageArtifacts } from "./artifacts.js";
import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js"; import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js";
import { generateComponentClientBindings, componentAssetDeclarations } from "./client-contracts.js";
export interface ArtifactReceipt { export interface ArtifactReceipt {
schemaVersion: 1; schemaVersion: 1;
@@ -57,6 +58,16 @@ export function expectedArtifactReceipt(
sources.set(path, bytes); sources.set(path, bytes);
return { target, path, digest: hashBytes(bytes), bytes: bytes.length }; return { target, path, digest: hashBytes(bytes), bytes: bytes.length };
}); });
if (manifest.components?.length) {
for (const [path, source] of [
["share/quixos/generated/client.ts", generateComponentClientBindings(plan, pkg.revision)],
["share/quixos/generated/component-assets.d.ts", componentAssetDeclarations()],
] as const) {
const bytes = new TextEncoder().encode(source);
sources.set(path, bytes);
generated.push({ target: "window", path, digest: hashBytes(bytes), bytes: bytes.length });
}
}
// Custom language services need no TypeScript adapter. A checked JS module // Custom language services need no TypeScript adapter. A checked JS module
// exporting remote handlers does, and must carry the exact generated source. // exporting remote handlers does, and must carry the exact generated source.
if ( if (
+13 -3
View File
@@ -45,6 +45,11 @@ assert.ok(data.ok && data.resource.kind === "interface", JSON.stringify(data.dia
assert.ok(constructors.ok && constructors.resource.kind === "interface", JSON.stringify(constructors.diagnostics)); assert.ok(constructors.ok && constructors.resource.kind === "interface", JSON.stringify(constructors.diagnostics));
const dataInterface = data.resource.revision, const dataInterface = data.resource.revision,
factoriesInterface = constructors.resource.revision; factoriesInterface = constructors.resource.revision;
const ui = compileCapabilityResourceSource(
'import interface Data; package UI id "package:ui" revision "package:ui@1" { component Card id "component:card" subject Data runtime react; }',
{ source, environment: { interfaces: new Map([["Data", dataInterface]]), interfaceClosure: [dataInterface] } },
);
assert.ok(ui.ok && ui.resource.kind === "package", JSON.stringify(ui.diagnostics));
for (const variant of ["base", "added", "gateway", "gateway-next", "session"]) { for (const variant of ["base", "added", "gateway", "gateway-next", "session"]) {
const session = variant === "session", const session = variant === "session",
gateway = session || variant.startsWith("gateway"), gateway = session || variant.startsWith("gateway"),
@@ -57,7 +62,8 @@ package Owner id "package:owner" revision "package:owner@1" {
${gateway ? 'function create id "create" : unit -> atom-ref<Entry> execution transaction targets [server] requires { creation entry id "entry:birth" : Entry input unit; };' : ""} ${gateway ? 'function create id "create" : unit -> atom-ref<Entry> execution transaction targets [server] requires { creation entry id "entry:birth" : Entry input unit; };' : ""}
${ ${
session session
? `function createOne id "create-one" : string -> atom-ref<Entry> execution transaction targets [browser, server] requires { creation entry id "one:birth" : Entry input unit; }; ? `component Editor id "component:owner-editor" subject Data runtime react;
function createOne id "create-one" : string -> atom-ref<Entry> execution transaction targets [browser, server] requires { creation entry id "one:birth" : Entry input unit; };
function createTwo id "create-two" : unit -> atom-ref<Independent> execution transaction targets [browser, server] requires { creation entry id "two:birth" : Independent input unit; }; function createTwo id "create-two" : unit -> atom-ref<Independent> execution transaction targets [browser, server] requires { creation entry id "two:birth" : Independent input unit; };
operation revise id "revise" : record {body:change<string>; notes:change<string>; reject:bool;} -> unit mode call execution transaction targets [browser, server] receiver any requires { operation revise id "revise" : record {body:change<string>; notes:change<string>; reject:bool;} -> unit mode call execution transaction targets [browser, server] receiver any requires {
state body id "revise:body" : string [edit]; state notes id "revise:notes" : string [edit]; state body id "revise:body" : string [edit]; state notes id "revise:notes" : string [edit];
@@ -98,6 +104,7 @@ package Owner id "package:owner" revision "package:owner@1" {
const result = compileCapabilitySource( const result = compileCapabilitySource(
`workspace Managed id "workspace:managed-process" revision "workspace:managed-process@${variant}" commit "${commit}" { `workspace Managed id "workspace:managed-process" revision "workspace:managed-process@${variant}" commit "${commit}" {
import package Owner; import package Owner;
${session ? "import package UI;" : ""}
${ ${
session session
? `import interface Data; import interface Factories; ? `import interface Data; import interface Factories;
@@ -118,8 +125,11 @@ package Owner id "package:owner" revision "package:owner@1" {
}`, }`,
"workspace.qx", "workspace.qx",
{ {
packages: new Map([["Owner", pkg.resource.revision]]), packages: new Map([
packageClosure: [pkg.resource.revision], ["Owner", pkg.resource.revision],
...(session ? [["UI", ui.resource.revision] as const] : []),
]),
packageClosure: [pkg.resource.revision, ...(session ? [ui.resource.revision] : [])],
...(gateway ...(gateway
? { ? {
interfaces: new Map([ interfaces: new Map([