From 0da022e216b26e1cea917d4dd7861e25f15a76d0 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Tue, 22 Sep 2026 13:51:32 -0700 Subject: [PATCH] Build composable Camino packages with explicit type and source dependencies --- artifacts.nix | 3 ++ assemble-package.mjs | 103 +++++++++++++++++++++++++++++++++++++ assemble-package.nix | 29 +++++++++++ build-artifacts.mjs | 81 +++-------------------------- build-components.mjs | 60 +++++++++++++++++++-- build-react-contracts.mjs | 90 ++++++++++++++++++++++++++++++++ build-service.mjs | 88 +++++++++++++++++++++++++++++++ quixos-package-helpers.nix | 6 ++- react-contracts.nix | 21 ++++++++ react-package.nix | 34 ++++++++++++ typescript-package.nix | 41 +++++++++++++++ typescript-service.nix | 30 +++++++++++ 12 files changed, 508 insertions(+), 78 deletions(-) create mode 100644 assemble-package.mjs create mode 100644 assemble-package.nix create mode 100644 build-react-contracts.mjs create mode 100644 build-service.mjs create mode 100644 react-contracts.nix create mode 100644 react-package.nix create mode 100644 typescript-package.nix create mode 100644 typescript-service.nix diff --git a/artifacts.nix b/artifacts.nix index 2f43b76..8e2542a 100644 --- a/artifacts.nix +++ b/artifacts.nix @@ -13,6 +13,7 @@ service ? { }, nodeModules ? null, components ? null, + group ? false, }: let compiler = "${protocol}/libexec/quixos-protocol/execution-world.mjs"; @@ -26,6 +27,7 @@ let sharedRuntime nodeModules service + group ; components = if components == null then @@ -35,6 +37,7 @@ let inherit (components) sdk runtime entries; dependencies = components.sdk.dependencies; bindingOutput = components.bindingOutput or "src/gen/client.ts"; + contracts = components.contracts or [ ]; }; source = src; portable = diff --git a/assemble-package.mjs b/assemble-package.mjs new file mode 100644 index 0000000..56e226e --- /dev/null +++ b/assemble-package.mjs @@ -0,0 +1,103 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { createHash } from "node:crypto"; +import { pathToFileURL } from "node:url"; +import { buildService } from "./build-service.mjs"; + +const config = JSON.parse(await fs.readFile(process.argv[2], "utf8")); +const compiler = await import(pathToFileURL(config.compiler)); +const plan = JSON.parse(await fs.readFile(config.plan, "utf8")); +const pkg = plan.definition.packages.find((pkg) => pkg.revision === config.packageRevisionId); +if (!pkg || compiler.hexDigest(plan.definition) !== plan.digest) throw Error("Invalid package assembly context"); +const output = process.env.out; +if (!output) throw Error("Package assembly requires an output directory"); +const digest = (bytes) => createHash("sha256").update(bytes).digest("hex"); +const generatorDigest = digest(await fs.readFile(config.compiler)); +const implementations = [], + components = [], + files = new Map(), + ids = new Set(); +const recordFile = async (name, mediaType) => { + const bytes = await fs.readFile(path.join(output, compiler.artifactPath(name))); + return { path: name, bytes: bytes.length, digest: digest(bytes), mediaType }; +}; +await fs.mkdir(path.join(output, "share/quixos/generated"), { recursive: true }); +for (const root of config.groups) { + const manifest = compiler.parsePackageArtifacts( + JSON.parse(await fs.readFile(path.join(root, "share/quixos/package-artifacts.json"), "utf8")), + ); + if ( + manifest.packageRevisionId !== pkg.revision || + manifest.bindingDigest !== pkg.bindingDigest || + manifest.sourceDigest !== pkg.sourceDigest || + JSON.stringify(manifest.versions) !== JSON.stringify(plan.definition.versions) + ) + throw Error("Artifact group belongs to a different checked package"); + const receipt = JSON.parse(await fs.readFile(path.join(root, "share/quixos/build-receipt.json"), "utf8")); + const expected = compiler.expectedArtifactReceipt(plan, manifest, generatorDigest); + if (JSON.stringify(receipt) !== JSON.stringify(expected.receipt)) + throw Error("Artifact group has an invalid build receipt"); + const read = await compiler.artifactFileReader({ [pkg.revision]: root }); + for (const artifact of [...manifest.implementations, ...(manifest.components ?? [])]) { + const key = `${artifact.compatibility === "org.quixos.react.esm/1" ? "module" : "implementation"}:${artifact.id}`; + if (ids.has(key)) throw Error("Duplicate artifact group export " + key); + ids.add(key); + for (const file of artifact.files) { + const bytes = await read(pkg.revision, file.path, file.bytes); + if (bytes.length !== file.bytes || digest(bytes) !== file.digest) + throw Error("Modified group asset " + file.path); + const previous = files.get(file.path); + if (previous && JSON.stringify(previous) !== JSON.stringify(file)) + throw Error("Conflicting group asset " + file.path); + if (!previous) { + await fs.mkdir(path.dirname(path.join(output, file.path)), { recursive: true }); + await fs.writeFile(path.join(output, file.path), bytes, { mode: file.path.startsWith("bin/") ? 0o755 : 0o644 }); + files.set(file.path, file); + } + } + } + for (const [name, bytes] of expected.sources) { + const actual = await fs.readFile(path.join(root, name)); + if (!Buffer.from(bytes).equals(actual)) throw Error("Modified group type witness " + name); + await fs.writeFile(path.join(output, name), bytes); + } + implementations.push(...manifest.implementations); + components.push(...(manifest.components ?? [])); +} +if (implementations.filter((entry) => entry.compatibility === "quixos-service-v1").length > 1) + throw Error("A package can have only one process service owner"); +if (!implementations.some((entry) => entry.compatibility === "quixos-service-v1")) + await buildService({ + config: { ...config, service: {} }, + compiler, + pkg, + output, + coreServer: path.join(config.sharedRuntime, "core.mjs"), + sharedFiles: [...files.values()].filter((file) => file.path.startsWith("share/quixos/shared/")), + implementations, + recordFile, + }); +const manifest = compiler.parsePackageArtifacts({ + schemaVersion: 1, + packageRevisionId: pkg.revision, + bindingDigest: pkg.bindingDigest, + sourceDigest: pkg.sourceDigest, + versions: plan.definition.versions, + implementations, + ...(components.length ? { components } : {}), +}); +const { receipt, sources } = compiler.expectedArtifactReceipt(plan, manifest, generatorDigest); +for (const [name, bytes] of sources) { + const actual = await fs.readFile(path.join(output, name)); + if (!Buffer.from(bytes).equals(actual)) throw Error("Assembly lacks checked generated source " + name); +} +await fs.writeFile(path.join(output, "share/quixos/package-artifacts.json"), JSON.stringify(manifest, null, 2)); +await fs.writeFile(path.join(output, "share/quixos/build-receipt.json"), JSON.stringify(receipt, null, 2)); +// Check package coverage with the same public validator used at installation. +const packagePlan = { definition: { ...plan.definition, packages: [pkg] }, digest: "" }; +packagePlan.digest = compiler.hexDigest(packagePlan.definition); +await compiler.checkExecutionWorld( + packagePlan, + [manifest], + await compiler.artifactFileReader({ [pkg.revision]: output }), +); diff --git a/assemble-package.nix b/assemble-package.nix new file mode 100644 index 0000000..8550622 --- /dev/null +++ b/assemble-package.nix @@ -0,0 +1,29 @@ +{ + pkgs, + context, + artifacts, +}: +let + groups = map (artifact: artifact.caminoGroup or artifact) artifacts; + config = pkgs.writeText "camino-package-assembly.json" ( + builtins.toJSON { + inherit groups; + inherit (context) plan packageRevisionId sharedRuntime; + compiler = "${context.generator}/libexec/quixos-protocol/execution-world.mjs"; + node = "${pkgs.nodejs_24}/bin/node"; + esbuild = "${pkgs.esbuild}/bin/esbuild"; + shell = "${pkgs.runtimeShell}"; + serviceAdapter = ./portable-service.mjs; + } + ); +in +pkgs.runCommand "camino-package" + { + passthru = { + caminoGroups = groups; + assemblyConfig = config; + }; + } + '' + ${pkgs.nodejs_24}/bin/node ${./.}/assemble-package.mjs ${config} + '' diff --git a/build-artifacts.mjs b/build-artifacts.mjs index 5000bc7..7565a81 100644 --- a/build-artifacts.mjs +++ b/build-artifacts.mjs @@ -1,3 +1,4 @@ +import { buildService } from "./build-service.mjs"; import { buildComponents } from "./build-components.mjs"; /** Trusted Nix build driver. No package-defined build hooks run between generation and witnessing. */ import fs from "node:fs/promises"; @@ -161,79 +162,13 @@ for (const { target, mode, settings } of builds) { }; implementations.push({ ...body, contentDigest: compiler.implementationDigest(body) }); } -const components = await buildComponents({ config, compiler, plan, pkg, output, recordFile, generated }); -if (pkg.exports.length || config.service.entry) { - await fs.mkdir(path.join(output, "bin"), { recursive: true }); - await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true }); - let serviceEntry; - if (config.service.entry) { - if (!/\.m?js$/.test(config.service.entry)) - throw Error("Custom service entry must be JavaScript; use a separate typed/language build before packaging"); - serviceEntry = path.join(config.source, relative(config.service.entry)); - } else { - const modules = implementations.filter((implementation) => implementation.target === "server"); - if (pkg.exports.some((entry) => !modules.some((module) => module.exports.includes(entry.id)))) - throw Error("Generated service does not cover every package export; provide a custom service"); - if (!modules.length) throw Error("Generated service requires a checked registry"); - serviceEntry = path.resolve("service-entry.mjs"); - const imports = modules - .map( - (module, index) => - `import { ${module.registryExport} as registry${index} } from ${JSON.stringify(path.join(output, module.path))};`, - ) - .join("\n"); - await fs.writeFile( - serviceEntry, - imports + - `\nimport {servePortableRegistry} from ${JSON.stringify(config.serviceAdapter)}; -const registries = [${modules.map((_, index) => "registry" + index).join(",")}]; -const owners = new Map(); -for (const registry of registries) for (const id of registry.exports) { - if (owners.has(id)) throw Error("Duplicate registry export " + id); - owners.set(id, registry); -} -servePortableRegistry({exports: [...owners.keys()], invoke(request, channel, context) { - const registry = owners.get(request.exportId); - if (!registry) throw Error("Unknown export"); - return registry.invoke(request, channel, context); -}});\n`, - ); - } - const serviceModule = "share/quixos/service/main.mjs"; - execFileSync( - config.esbuild, - [ - serviceEntry, - "--bundle", - "--platform=node", - "--format=esm", - "--minify-whitespace", - "--target=es2024", - `--external:${coreServer}`, - `--outfile=${path.join(output, serviceModule)}`, - ], - { stdio: "inherit" }, - ); - await fs.writeFile( - path.join(output, "bin/service"), - `#!${config.shell}\nservice_root=\${0%/*}/..\nexec ${config.node} "$service_root/${serviceModule}" "$@"\n`, - { mode: 0o755 }, - ); - const serviceBody = { - id: "service", - target: "server", - compatibility: "quixos-service-v1", - path: "bin/service", - ports: "quixos-transaction-ports-v1", - exports: pkg.exports.map((e) => e.id), - files: [ - await recordFile("bin/service", "text/x-shellscript"), - await recordFile(serviceModule, "text/javascript"), - ...sharedFiles, - ], - }; - implementations.unshift({ ...serviceBody, contentDigest: compiler.implementationDigest(serviceBody) }); -} +const components = + config.group && !config.components + ? [] + : await buildComponents({ config, compiler, plan, pkg, output, recordFile, generated }); +if (!config.group || config.service.entry) + await buildService({ config, compiler, pkg, output, coreServer, sharedFiles, implementations, recordFile }); + const manifest = compiler.parsePackageArtifacts({ schemaVersion: 1, packageRevisionId: pkg.revision, diff --git a/build-components.mjs b/build-components.mjs index c6fe63f..c74636b 100644 --- a/build-components.mjs +++ b/build-components.mjs @@ -9,7 +9,9 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec if (pkg.components?.length) throw Error("Declared components require checked component build settings"); return []; } - const declarations = pkg.components ?? []; + const declarations = config.group + ? (pkg.components ?? []).filter((entry) => Object.hasOwn(settings.entries, entry.id)) + : (pkg.components ?? []); const entries = Object.entries(settings.entries); if ( entries.length !== declarations.length || @@ -86,6 +88,46 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec await fs.writeFile(path.join(output, "share/quixos/generated/component-assets.d.ts"), assets); generated.set("component-client", { file: path.join(work, bindingPath), source: bindings }); generated.set("component-assets", { file: path.join(work, assetPath), source: assets }); + // Public props are type-only inputs owned by imported interface resources. + // Augment the generated nominal registry without importing an implementation. + const propsImports = [], + propsMembers = [], + seenProps = new Set(); + for (const root of settings.contracts ?? []) { + const companion = JSON.parse(await fs.readFile(path.join(root, "react-contracts.json"), "utf8")); + if (companion.schemaVersion !== 1) throw Error("Unsupported React props companion"); + const contracts = plan.definition.workspace.interfaceImports.filter( + (iface) => + iface.revisionId === companion.interfaceRevisionId || + iface.application?.definitionId === companion.interfaceRevisionId, + ); + if (!contracts.length) continue; + for (const [memberId, entry] of Object.entries(companion.members)) { + if (!/^[$A-Z_a-z][$\w]*$/.test(entry.export)) throw Error("Invalid props type export"); + const alias = `Props${propsImports.length}`; + propsImports.push( + `import type {${entry.export} as ${alias}} from ${JSON.stringify(path.join(root, "types", compiler.artifactPath(entry.path)))};`, + ); + for (const iface of contracts) { + if ( + !(iface.template?.members ?? iface.members).some( + (member) => member.kind === "module" && member.id === memberId, + ) + ) + throw Error("Props companion names an unknown module member"); + const key = JSON.stringify([iface.revisionId, memberId]); + if (seenProps.has(key)) throw Error("Duplicate React props companion for " + key); + seenProps.add(key); + propsMembers.push(`${JSON.stringify(key)}: ${alias};`); + } + } + } + const propsPath = "component-props.d.ts"; + await fs.writeFile( + path.join(work, propsPath), + propsImports.join("\n") + + `\nexport {};\ndeclare module ${JSON.stringify("./" + bindingPath.replace(/\.ts$/, ".js"))} { interface ModuleProps {${propsMembers.join("\n")}} }\n`, + ); const components = []; for (const [id, entry] of entries) { const declaration = declarations.find((component) => component.id === id), @@ -93,10 +135,14 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec if (!/^[$A-Z_a-z][$\w]*$/.test(entry.export)) throw Error("Invalid component export name"); const key = digest(Buffer.from(id)).slice(0, 24), directory = "share/quixos/components/" + key; - const witness = path.join(work, "component-check-" + key + ".ts"); + const witness = path.join(work, "component-check-" + key + ".tsx"); + const module = pkg.modules.find((module) => module.id === id); await fs.writeFile( witness, - `import type {Components} from ${JSON.stringify("./" + bindingPath.replace(/\.ts$/, ".js"))};import {${entry.export} as Component} from ${JSON.stringify("./" + source.replace(/\.[cm]?tsx?$/, ".js"))};const checked:Components[${JSON.stringify(declaration.displayName)}]=Component;export default checked;\n`, + `import type {Modules} from ${JSON.stringify("./" + bindingPath.replace(/\.ts$/, ".js"))};import {${entry.export} as Component} from ${JSON.stringify("./" + source.replace(/\.[cm]?tsx?$/, ".js"))};const checked:Modules[${JSON.stringify(declaration.displayName)}]=Component;export default checked;\n` + + (module.contract === "org.quixos.react.subject-only/1" + ? `import type {ComponentProps} from "@quixos/camino-react";declare const subject:ComponentProps<${JSON.stringify(declaration.subject)}>['subject'];const mount = ;\n` + : ""), ); execFileSync( config.node, @@ -118,15 +164,21 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec "react-jsx", path.basename(witness), assetPath, + propsPath, ], { cwd: work, stdio: "inherit" }, ); const metadata = path.join(work, "component-meta-" + key + ".json"); + const facade = "component-entry-" + key + ".ts"; + await fs.writeFile( + path.join(work, facade), + `export {${entry.export}} from ${JSON.stringify("./" + source.replace(/\.[cm]?tsx?$/, ".js"))};\n`, + ); await fs.mkdir(path.join(output, directory), { recursive: true }); execFileSync( config.esbuild, [ - source, + facade, "--bundle", "--platform=browser", "--format=esm", diff --git a/build-react-contracts.mjs b/build-react-contracts.mjs new file mode 100644 index 0000000..185d858 --- /dev/null +++ b/build-react-contracts.mjs @@ -0,0 +1,90 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +const config = JSON.parse(await fs.readFile(process.argv[2], "utf8")); +const resource = JSON.parse(await fs.readFile(config.interface, "utf8")); +if (resource.kind !== "interface") throw Error("React props belong to an interface resource"); +const iface = resource.revision; +const output = process.env.out; +if (!output) throw Error("Missing Nix output"); +const work = path.resolve("contracts-source"); +const declarations = []; +await fs.cp(config.source, work, { recursive: true }); +const writable = async (directory) => { + await fs.chmod(directory, 0o755); + for (const entry of await fs.readdir(directory, { withFileTypes: true })) { + const file = path.join(directory, entry.name); + if (entry.isSymbolicLink()) throw Error("Contract source cannot contain symlinks"); + if (entry.isDirectory()) await writable(file); + else { + await fs.chmod(file, 0o644); + if (/\.d\.[cm]?ts$/.test(file)) declarations.push(path.relative(work, file)); + } + } +}; +await writable(work); +await fs.writeFile(path.join(work, "package.json"), JSON.stringify({ type: "module" })); +if (config.dependencies) await fs.symlink(config.dependencies, path.join(work, "node_modules")); +const members = Object.create(null); +let index = 0; +for (const [id, entry] of Object.entries(config.members)) { + const member = (iface.template?.members ?? iface.members).find((member) => member.id === id); + if ( + member?.kind !== "module" || + !["org.quixos.react.component/1", "org.quixos.react.subject-only/1"].includes(member.contract) + ) + throw Error("Props entry is not a React module member: " + id); + if ( + !/^[$A-Z_a-z][$\w]*$/.test(entry.export) || + path.isAbsolute(entry.entry) || + entry.entry.split("/").some((part) => part === ".." || !part) + ) + throw Error("Invalid props type entry"); + const filename = `props-${index++}.ts`; + const typeImport = JSON.stringify("./" + entry.entry.replace(/\.[cm]?tsx?$/, ".js")); + await fs.writeFile( + path.join(work, filename), + `import type {${entry.export} as Props} from ${typeImport}; +type Assert = T; +type ObjectProps = Assert; +type ReservedSubject = Assert<"subject" extends keyof Props ? false : true>; +${member.contract === "org.quixos.react.subject-only/1" ? "type SubjectOnly = Assert<{} extends Props ? true : false>;" : ""} +export type {Props};\n`, + ); + members[id] = { path: filename.replace(/\.ts$/, ".d.ts"), export: "Props" }; +} +await fs.mkdir(path.join(output, "types"), { recursive: true }); +if (!Object.keys(members).length) throw Error("React type companion declares no module members"); +execFileSync( + config.node, + [ + config.tsc, + "--declaration", + "--emitDeclarationOnly", + "--strict", + "--target", + "es2024", + "--module", + "nodenext", + "--moduleResolution", + "nodenext", + "--rootDir", + work, + "--outDir", + path.join(output, "types"), + ...Object.values(members).map((entry) => path.join(work, entry.path.replace(/\.d\.ts$/, ".ts"))), + ], + { cwd: work, stdio: "inherit" }, +); +// TypeScript emits declarations for implementation sources, but does not copy +// authored declarations which those emitted files may import. +for (const file of declarations) { + await fs.mkdir(path.dirname(path.join(output, "types", file)), { recursive: true }); + await fs.copyFile(path.join(work, file), path.join(output, "types", file)); +} +await fs.writeFile(path.join(output, "package.json"), JSON.stringify({ type: "module" })); +if (config.dependencies) await fs.symlink(config.dependencies, path.join(output, "node_modules")); +await fs.writeFile( + path.join(output, "react-contracts.json"), + JSON.stringify({ schemaVersion: 1, interfaceRevisionId: iface.revisionId, members }, null, 2), +); diff --git a/build-service.mjs b/build-service.mjs new file mode 100644 index 0000000..5e3ca09 --- /dev/null +++ b/build-service.mjs @@ -0,0 +1,88 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; + +export async function buildService({ + config, + compiler, + pkg, + output, + coreServer, + sharedFiles, + implementations, + recordFile, +}) { + const relative = compiler.artifactPath; + if (pkg.exports.length || config.service.entry) { + await fs.mkdir(path.join(output, "bin"), { recursive: true }); + await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true }); + let serviceEntry; + if (config.service.entry) { + if (!/\.m?js$/.test(config.service.entry)) + throw Error("Custom service entry must be JavaScript; use a separate typed/language build before packaging"); + serviceEntry = path.join(config.source, relative(config.service.entry)); + } else { + const modules = implementations.filter((implementation) => implementation.target === "server"); + if (pkg.exports.some((entry) => !modules.some((module) => module.exports.includes(entry.id)))) + throw Error("Generated service does not cover every package export; provide a custom service"); + if (!modules.length) throw Error("Generated service requires a checked registry"); + serviceEntry = path.resolve("service-entry.mjs"); + const imports = modules + .map( + (module, index) => + `import { ${module.registryExport} as registry${index} } from ${JSON.stringify(path.join(output, module.path))};`, + ) + .join("\n"); + await fs.writeFile( + serviceEntry, + imports + + `\nimport {servePortableRegistry} from ${JSON.stringify(config.serviceAdapter)}; +const registries = [${modules.map((_, index) => "registry" + index).join(",")}]; +const owners = new Map(); +for (const registry of registries) for (const id of registry.exports) { + if (owners.has(id)) throw Error("Duplicate registry export " + id); + owners.set(id, registry); +} +servePortableRegistry({exports: [...owners.keys()], invoke(request, channel, context) { + const registry = owners.get(request.exportId); + if (!registry) throw Error("Unknown export"); + return registry.invoke(request, channel, context); +}});\n`, + ); + } + const serviceModule = "share/quixos/service/main.mjs"; + execFileSync( + config.esbuild, + [ + serviceEntry, + "--bundle", + "--platform=node", + "--format=esm", + "--minify-whitespace", + "--target=es2024", + `--external:${coreServer}`, + `--outfile=${path.join(output, serviceModule)}`, + ], + { stdio: "inherit" }, + ); + await fs.writeFile( + path.join(output, "bin/service"), + `#!${config.shell}\nservice_root=\${0%/*}/..\nexec ${config.node} "$service_root/${serviceModule}" "$@"\n`, + { mode: 0o755 }, + ); + const serviceBody = { + id: "service", + target: "server", + compatibility: "quixos-service-v1", + path: "bin/service", + ports: "quixos-transaction-ports-v1", + exports: pkg.exports.map((e) => e.id), + files: [ + await recordFile("bin/service", "text/x-shellscript"), + await recordFile(serviceModule, "text/javascript"), + ...sharedFiles, + ], + }; + implementations.unshift({ ...serviceBody, contentDigest: compiler.implementationDigest(serviceBody) }); + } +} diff --git a/quixos-package-helpers.nix b/quixos-package-helpers.nix index 346ec3e..3fa177e 100644 --- a/quixos-package-helpers.nix +++ b/quixos-package-helpers.nix @@ -846,7 +846,11 @@ let ); in { - mkCaminoArtifacts = import ./artifacts.nix; + mkCaminoReactPackage = import ./react-package.nix; + mkCaminoReactContracts = import ./react-contracts.nix; + mkCaminoTypeScriptPackage = import ./typescript-package.nix; + mkCaminoTypeScriptService = import ./typescript-service.nix; + assembleCaminoPackage = import ./assemble-package.nix; inherit mkQuixosPackageFlake mkTsPackageServer diff --git a/react-contracts.nix b/react-contracts.nix new file mode 100644 index 0000000..acd5b9b --- /dev/null +++ b/react-contracts.nix @@ -0,0 +1,21 @@ +{ + pkgs, + context, + src, + members, + dependencies ? null, +}: +let + config = pkgs.writeText "camino-react-contracts.json" ( + builtins.toJSON { + source = src; + interface = context.interface; + inherit members dependencies; + node = "${pkgs.nodejs_24}/bin/node"; + tsc = "${context.typescript}/node_modules/typescript/bin/tsc"; + } + ); +in +pkgs.runCommand "camino-react-contracts" { } '' + ${pkgs.nodejs_24}/bin/node ${./.}/build-react-contracts.mjs ${config} +'' diff --git a/react-package.nix b/react-package.nix new file mode 100644 index 0000000..e76ad74 --- /dev/null +++ b/react-package.nix @@ -0,0 +1,34 @@ +{ + pkgs, + context, + src, + modules, + bindingOutput ? "src/gen/client.ts", + dependencies ? null, +}: +let + group = import ./artifacts.nix { + inherit pkgs src; + inherit (context) + plan + core + sharedRuntime + typescript + packageRevisionId + ; + protocol = context.generator; + group = true; + nodeModules = dependencies; + components = { + inherit bindingOutput; + inherit (context.react) sdk runtime; + entries = modules; + contracts = context.react.contracts or [ ]; + }; + }; + package = import ./assemble-package.nix { + inherit pkgs context; + artifacts = [ group ]; + }; +in +package // { caminoGroup = group; } diff --git a/typescript-package.nix b/typescript-package.nix new file mode 100644 index 0000000..950dcbb --- /dev/null +++ b/typescript-package.nix @@ -0,0 +1,41 @@ +{ + pkgs, + context, + src, + entry, + targets ? [ + "browser" + "server" + ], + entries ? { }, + bindingOutput ? "src/gen/qx.ts", + dependencies ? null, +}: +let + group = import ./artifacts.nix { + inherit pkgs src; + inherit (context) + plan + core + sharedRuntime + typescript + packageRevisionId + ; + protocol = context.generator; + group = true; + nodeModules = dependencies; + portable = { + inherit + entry + entries + targets + bindingOutput + ; + }; + }; + package = import ./assemble-package.nix { + inherit pkgs context; + artifacts = [ group ]; + }; +in +package // { caminoGroup = group; } diff --git a/typescript-service.nix b/typescript-service.nix new file mode 100644 index 0000000..61ceea6 --- /dev/null +++ b/typescript-service.nix @@ -0,0 +1,30 @@ +{ + pkgs, + context, + src, + entry, + bindingOutput ? "src/gen/remote.ts", + dependencies ? null, + service ? { }, +}: +let + group = import ./artifacts.nix { + inherit pkgs src service; + inherit (context) + plan + core + sharedRuntime + typescript + packageRevisionId + ; + protocol = context.generator; + group = true; + nodeModules = dependencies; + remote = { inherit entry bindingOutput; }; + }; + package = import ./assemble-package.nix { + inherit pkgs context; + artifacts = [ group ]; + }; +in +package // { caminoGroup = group; }