Build checked component artifacts and shared React session SDK (WI-09)
This commit is contained in:
@@ -127,7 +127,62 @@ export function generatePackageQueryClientBindings(
|
||||
): string {
|
||||
const queries = (pkg.checkedQueries ?? []).map(
|
||||
(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`;
|
||||
}
|
||||
|
||||
/** 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'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ export interface InstalledComponent {
|
||||
readonly subject: string;
|
||||
readonly url: string;
|
||||
readonly exportName: string;
|
||||
readonly styles: readonly string[];
|
||||
readonly compatibility: ComponentArtifact["compatibility"];
|
||||
readonly sharedModules: Readonly<Record<keyof ComponentArtifact["sharedModules"], string>>;
|
||||
}
|
||||
@@ -163,6 +164,7 @@ export async function loadExecutionInstallation(
|
||||
subject: component.subject,
|
||||
compatibility: component.compatibility,
|
||||
exportName: component.exportName,
|
||||
styles: component.files.filter((file) => file.mediaType === "text/css").map(contentArtifactUrl),
|
||||
sharedModules,
|
||||
url: contentArtifactUrl(component.files.find((file) => file.path === component.path)!),
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { generatePortableTypeScriptBindings, generateRemoteTypeScriptBindings }
|
||||
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
|
||||
import { type ArtifactReader, type PackageArtifacts } from "./artifacts.js";
|
||||
import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js";
|
||||
import { generateComponentClientBindings, componentAssetDeclarations } from "./client-contracts.js";
|
||||
|
||||
export interface ArtifactReceipt {
|
||||
schemaVersion: 1;
|
||||
@@ -57,6 +58,16 @@ export function expectedArtifactReceipt(
|
||||
sources.set(path, bytes);
|
||||
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
|
||||
// exporting remote handlers does, and must carry the exact generated source.
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user