Publish quixos-nix-helpers from e5aec2bb26110ed474929f353ee51ff46d4f20d6

This commit is contained in:
Quixos Subtree Publisher
2026-10-01 11:03:19 +00:00
21 changed files with 578 additions and 150 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos", "sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
"sourceCommit": "54e443af5dd963f1fa6f83f062f4943b46ad2535", "sourceCommit": "e5aec2bb26110ed474929f353ee51ff46d4f20d6",
"sourcePath": "quixos-instance/quixos-nix-helpers", "sourcePath": "quixos-instance/quixos-nix-helpers",
"exportName": "quixos-nix-helpers", "exportName": "quixos-nix-helpers",
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git" "mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git"
+21 -2
View File
@@ -8,9 +8,9 @@ receipts. It has no migration step or package-defined shell hooks.
```nix ```nix
# Inside a per-system flake output: # Inside a per-system flake output:
quixosPackages.checkedArtifacts = quixosPackages.checkedArtifacts =
{ plan, generator, core, sharedRuntime, typescript, packageRevisionId }: { buildContract, sourceDigest, generator, core, sharedRuntime, typescript, packageRevisionId, ... }:
helpers.mkCaminoArtifacts { helpers.mkCaminoArtifacts {
inherit pkgs plan core sharedRuntime typescript packageRevisionId; inherit pkgs buildContract sourceDigest core sharedRuntime typescript packageRevisionId;
protocol = generator; protocol = generator;
src = ./.; src = ./.;
portable = { portable = {
@@ -22,6 +22,25 @@ quixosPackages.checkedArtifacts =
}; };
``` ```
`buildContract` is a canonical, source-free capability slice. `sourceDigest`
records the exact selected package source in the final installation manifest;
it does not enter the reusable compilation group. Ordinary source dependencies,
SDKs and React props companions remain explicit Nix inputs. Raw QX and resource
lock files enter through the checked contract and evaluated recipe, and are
excluded from the implementation source tree. The `plan` argument on the low-level
helper is a composition convenience for fixtures; it is projected before building.
Package recipes receive the contract directly and can be checked without a workspace.
Inside `checkedArtifacts = context: ...`, the package helpers expose
`camino.packageSet { inherit nixpkgs system context; }` as the usual package set
selection. It shares the workspace composition's evaluated Nixpkgs when the
source content, system, and default import arguments match. Pass ordinary Nixpkgs
arguments such as `config` or `overlays` to use a separate import. A direct
`import nixpkgs { inherit system; }` also works, with additional evaluation cost.
The implementation groups depend on the complete execution compiler bundle and
JavaScript build drivers by content; unrelated checker CLI and publication
metadata changes do not enter those inputs.
The handler entry exports `registry = createRegistry(handlers)` from the generated The handler entry exports `registry = createRegistry(handlers)` from the generated
bindings. One registry contains many exports. The builder always supplies a server bindings. One registry contains many exports. The builder always supplies a server
implementation for the generated process service. A service-only package instead implementation for the generated process service. A service-only package instead
+61 -8
View File
@@ -2,7 +2,25 @@
{ {
pkgs, pkgs,
src, src,
plan, plan ? null,
buildContract ?
(import ./package-context.nix {
inherit
pkgs
protocol
plan
packageRevisionId
;
}).buildContract,
sourceDigest ?
(import ./package-context.nix {
inherit
pkgs
protocol
plan
packageRevisionId
;
}).sourceDigest,
protocol, protocol,
core, core,
sharedRuntime, sharedRuntime,
@@ -16,19 +34,25 @@
group ? false, group ? false,
}: }:
let let
compiler = "${protocol}/libexec/quixos-protocol/execution-world.mjs"; buildTools = import ./build-tools.nix;
# The standalone bundle is the complete code dependency of this driver.
# Changes to unrelated checker CLIs must not invalidate every package group.
compiler = builtins.path {
path = "${protocol}/libexec/quixos-protocol/execution-world.mjs";
name = "execution-world.mjs";
};
config = pkgs.writeText "camino-artifact-build.json" ( config = pkgs.writeText "camino-artifact-build.json" (
builtins.toJSON { builtins.toJSON {
inherit inherit
plan buildContract
compiler compiler
packageRevisionId packageRevisionId
core core
sharedRuntime sharedRuntime
nodeModules nodeModules
service service
group
; ;
group = true;
components = components =
if components == null then if components == null then
null null
@@ -39,7 +63,10 @@ let
bindingOutput = components.bindingOutput or "src/gen/client.ts"; bindingOutput = components.bindingOutput or "src/gen/client.ts";
contracts = components.contracts or [ ]; contracts = components.contracts or [ ];
}; };
source = src; source = import ./implementation-source.nix {
inherit (pkgs) lib;
inherit src;
};
portable = portable =
if portable == null then if portable == null then
null null
@@ -71,7 +98,33 @@ let
serviceAdapter = ./portable-service.mjs; serviceAdapter = ./portable-service.mjs;
} }
); );
compiled =
pkgs.runCommand "camino-compiled-artifacts"
{
passthru.buildConfig = config;
QUIXOS_TRACE_PROFILE = "1";
}
''
${pkgs.nodejs_24}/bin/node ${buildTools}/build-artifacts.mjs ${config}
'';
in in
pkgs.runCommand "camino-checked-artifacts" { passthru.buildConfig = config; } '' if group then
${pkgs.nodejs_24}/bin/node ${./.}/build-artifacts.mjs ${config} compiled
'' else
(import ./assemble-package.nix {
inherit pkgs;
artifacts = [ compiled ];
context = {
inherit
buildContract
sourceDigest
packageRevisionId
sharedRuntime
;
generator = protocol;
};
})
// {
caminoGroup = compiled;
buildConfig = config;
}
+14 -20
View File
@@ -6,9 +6,10 @@ import { buildService } from "./build-service.mjs";
const config = JSON.parse(await fs.readFile(process.argv[2], "utf8")); const config = JSON.parse(await fs.readFile(process.argv[2], "utf8"));
const compiler = await import(pathToFileURL(config.compiler)); const compiler = await import(pathToFileURL(config.compiler));
const plan = JSON.parse(await fs.readFile(config.plan, "utf8")); const contract = JSON.parse(await fs.readFile(config.buildContract, "utf8"));
const pkg = plan.definition.packages.find((pkg) => pkg.revision === config.packageRevisionId); compiler.checkPackageBuildContract(contract);
if (!pkg || compiler.hexDigest(plan.definition) !== plan.digest) throw Error("Invalid package assembly context"); const pkg = contract.definition.package;
if (pkg.revision !== config.packageRevisionId) throw Error("Invalid package assembly context");
const output = process.env.out; const output = process.env.out;
if (!output) throw Error("Package assembly requires an output directory"); if (!output) throw Error("Package assembly requires an output directory");
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex"); const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
@@ -23,18 +24,17 @@ const recordFile = async (name, mediaType) => {
}; };
await fs.mkdir(path.join(output, "share/quixos/generated"), { recursive: true }); await fs.mkdir(path.join(output, "share/quixos/generated"), { recursive: true });
for (const root of config.groups) { for (const root of config.groups) {
const manifest = compiler.parsePackageArtifacts( const manifest = compiler.parseArtifactGroup(
JSON.parse(await fs.readFile(path.join(root, "share/quixos/package-artifacts.json"), "utf8")), JSON.parse(await fs.readFile(path.join(root, "share/quixos/package-artifacts.json"), "utf8")),
); );
if ( if (
manifest.packageRevisionId !== pkg.revision || manifest.packageRevisionId !== pkg.revision ||
manifest.bindingDigest !== pkg.bindingDigest || manifest.bindingDigest !== contract.digest ||
manifest.sourceDigest !== pkg.sourceDigest || JSON.stringify(manifest.versions) !== JSON.stringify(contract.definition.versions)
JSON.stringify(manifest.versions) !== JSON.stringify(plan.definition.versions)
) )
throw Error("Artifact group belongs to a different checked package"); 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 receipt = JSON.parse(await fs.readFile(path.join(root, "share/quixos/build-receipt.json"), "utf8"));
const expected = compiler.expectedArtifactReceipt(plan, manifest, generatorDigest); const expected = compiler.expectedGroupReceipt(contract, manifest, generatorDigest);
if (JSON.stringify(receipt) !== JSON.stringify(expected.receipt)) if (JSON.stringify(receipt) !== JSON.stringify(expected.receipt))
throw Error("Artifact group has an invalid build receipt"); throw Error("Artifact group has an invalid build receipt");
const read = await compiler.artifactFileReader({ [pkg.revision]: root }); const read = await compiler.artifactFileReader({ [pkg.revision]: root });
@@ -80,24 +80,18 @@ if (!implementations.some((entry) => entry.compatibility === "quixos-service-v1"
const manifest = compiler.parsePackageArtifacts({ const manifest = compiler.parsePackageArtifacts({
schemaVersion: 1, schemaVersion: 1,
packageRevisionId: pkg.revision, packageRevisionId: pkg.revision,
bindingDigest: pkg.bindingDigest, bindingDigest: contract.digest,
sourceDigest: pkg.sourceDigest, sourceDigest: config.sourceDigest,
versions: plan.definition.versions, versions: contract.definition.versions,
implementations, implementations,
...(components.length ? { components } : {}), ...(components.length ? { components } : {}),
}); });
const { receipt, sources } = compiler.expectedArtifactReceipt(plan, manifest, generatorDigest); const { receipt, sources } = compiler.expectedPackageReceipt(contract, manifest, config.sourceDigest, generatorDigest);
for (const [name, bytes] of sources) { for (const [name, bytes] of sources) {
const actual = await fs.readFile(path.join(output, name)); const actual = await fs.readFile(path.join(output, name));
if (!Buffer.from(bytes).equals(actual)) throw Error("Assembly lacks checked generated source " + 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/package-artifacts.json"), JSON.stringify(manifest, null, 2));
await fs.writeFile(path.join(output, "share/quixos/build-receipt.json"), JSON.stringify(receipt, 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. // Source selection is attested here; compilation groups retain their own receipt.
const packagePlan = { definition: { ...plan.definition, packages: [pkg] }, digest: "" }; compiler.checkPackageCoverage({ ...pkg, bindingDigest: contract.digest, sourceDigest: config.sourceDigest }, manifest);
packagePlan.digest = compiler.hexDigest(packagePlan.definition);
await compiler.checkExecutionWorld(
packagePlan,
[manifest],
await compiler.artifactFileReader({ [pkg.revision]: output }),
);
+13 -3
View File
@@ -4,12 +4,22 @@
artifacts, artifacts,
}: }:
let let
buildTools = import ./build-tools.nix;
groups = map (artifact: artifact.caminoGroup or artifact) artifacts; groups = map (artifact: artifact.caminoGroup or artifact) artifacts;
compiler = builtins.path {
path = "${context.generator}/libexec/quixos-protocol/execution-world.mjs";
name = "execution-world.mjs";
};
config = pkgs.writeText "camino-package-assembly.json" ( config = pkgs.writeText "camino-package-assembly.json" (
builtins.toJSON { builtins.toJSON {
inherit groups; inherit groups;
inherit (context) plan packageRevisionId sharedRuntime; inherit (context)
compiler = "${context.generator}/libexec/quixos-protocol/execution-world.mjs"; buildContract
sourceDigest
packageRevisionId
sharedRuntime
;
inherit compiler;
node = "${pkgs.nodejs_24}/bin/node"; node = "${pkgs.nodejs_24}/bin/node";
esbuild = "${pkgs.esbuild}/bin/esbuild"; esbuild = "${pkgs.esbuild}/bin/esbuild";
shell = "${pkgs.runtimeShell}"; shell = "${pkgs.runtimeShell}";
@@ -25,5 +35,5 @@ pkgs.runCommand "camino-package"
}; };
} }
'' ''
${pkgs.nodejs_24}/bin/node ${./.}/assemble-package.mjs ${config} ${pkgs.nodejs_24}/bin/node ${buildTools}/assemble-package.mjs ${config}
'' ''
+62 -48
View File
@@ -1,4 +1,5 @@
import { installDependencies } from "./install-dependencies.mjs"; import { installDependencies } from "./install-dependencies.mjs";
import { measureBuild } from "./build-profile.mjs";
import { buildService } from "./build-service.mjs"; import { buildService } from "./build-service.mjs";
import { buildComponents } from "./build-components.mjs"; import { buildComponents } from "./build-components.mjs";
/** Trusted Nix build driver. No package-defined build hooks run between generation and witnessing. */ /** Trusted Nix build driver. No package-defined build hooks run between generation and witnessing. */
@@ -10,9 +11,10 @@ import { execFileSync } from "node:child_process";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
const config = JSON.parse(await fs.readFile(process.argv[2], "utf8")); const config = JSON.parse(await fs.readFile(process.argv[2], "utf8"));
const compiler = await import(pathToFileURL(config.compiler)); const compiler = await import(pathToFileURL(config.compiler));
const plan = JSON.parse(await fs.readFile(config.plan, "utf8")); const contract = JSON.parse(await fs.readFile(config.buildContract, "utf8"));
const pkg = plan.definition.packages.find((p) => p.revision === config.packageRevisionId); compiler.checkPackageBuildContract(contract);
if (!pkg || compiler.hexDigest(plan.definition) !== plan.digest) throw Error("Invalid checked execution plan/package"); const pkg = contract.definition.package;
if (pkg.revision !== config.packageRevisionId) throw Error("Invalid build contract package");
const output = process.env.out; const output = process.env.out;
if (!output) throw Error("Missing Nix output"); if (!output) throw Error("Missing Nix output");
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex"); const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
@@ -55,6 +57,7 @@ const targets = portable ? [...new Set([...portable.targets, "server"])] : [];
const builds = targets.map((target) => ({ target, mode: target, settings: portable })); const builds = targets.map((target) => ({ target, mode: target, settings: portable }));
if (config.remote) builds.push({ target: "server", mode: "remote", settings: config.remote }); if (config.remote) builds.push({ target: "server", mode: "remote", settings: config.remote });
for (const { target, mode, settings } of builds) { for (const { target, mode, settings } of builds) {
const executionTarget = target === "browser" ? "target:org.quixos.execution.javascript@1" : "server";
if (!["browser", "server"].includes(target)) throw Error(`Unsupported JS build target ${target}`); if (!["browser", "server"].includes(target)) throw Error(`Unsupported JS build target ${target}`);
const work = path.resolve(`work-${mode}`); const work = path.resolve(`work-${mode}`);
await fs.cp(config.source, work, { recursive: true, dereference: false }); await fs.cp(config.source, work, { recursive: true, dereference: false });
@@ -85,53 +88,65 @@ for (const { target, mode, settings } of builds) {
const bindingPath = relative(settings.bindingOutput); const bindingPath = relative(settings.bindingOutput);
const source = const source =
mode === "remote" mode === "remote"
? compiler.generateRemoteTypeScriptBindings(plan.definition.bindingSchema, pkg.revision) ? compiler.generateRemoteTypeScriptBindings(contract.definition.bindings, pkg.revision)
: compiler.generatePortableTypeScriptBindings(plan.definition.bindingSchema, pkg.revision, { target }); : compiler.generatePortableTypeScriptBindings(contract.definition.bindings, pkg.revision, {
target: executionTarget,
});
await fs.mkdir(path.dirname(path.join(work, bindingPath)), { recursive: true }); await fs.mkdir(path.dirname(path.join(work, bindingPath)), { recursive: true });
await fs.writeFile(path.join(work, bindingPath), source); await fs.writeFile(path.join(work, bindingPath), source);
generated.set(mode, { file: path.join(work, bindingPath), source }); generated.set(mode, { file: path.join(work, bindingPath), source });
await fs.writeFile(path.join(output, "share/quixos/generated", `${mode}.ts`), source); await fs.writeFile(
path.join(
output,
mode === "remote" ? "share/quixos/generated/remote.ts" : compiler.generatedBindingPath(executionTarget),
),
source,
);
const entry = relative(settings.entries?.[target] ?? settings.entry); const entry = relative(settings.entries?.[target] ?? settings.entry);
execFileSync( measureBuild("portable-typecheck", { package: pkg.revision, target, mode }, () =>
config.node, execFileSync(
[ config.node,
config.tsc, [
"--noEmit", config.tsc,
"--strict", "--noEmit",
"--skipLibCheck", "--strict",
"false", "--skipLibCheck",
"--target", "false",
"es2024", "--target",
"--lib", "es2024",
"es2024,dom,esnext.disposable", "--lib",
"--module", "es2024,dom,esnext.disposable",
"nodenext", "--module",
"--moduleResolution", "nodenext",
"nodenext", "--moduleResolution",
entry, "nodenext",
], entry,
{ cwd: work, stdio: "inherit" }, ],
{ cwd: work, stdio: "inherit" },
),
); );
const bundlePath = const bundlePath =
mode === "remote" ? "share/quixos/remote/server/module.mjs" : `share/quixos/portable/${target}/module.mjs`; mode === "remote" ? "share/quixos/remote/server/module.mjs" : `share/quixos/portable/${target}/module.mjs`;
await fs.mkdir(path.dirname(path.join(output, bundlePath)), { recursive: true }); await fs.mkdir(path.dirname(path.join(output, bundlePath)), { recursive: true });
const coreImport = target === "browser" ? coreURL : coreServer; const coreImport = target === "browser" ? coreURL : coreServer;
const metadata = path.join(work, "bundle-meta.json"); const metadata = path.join(work, "bundle-meta.json");
execFileSync( measureBuild("portable-bundle", { package: pkg.revision, target, mode }, () =>
config.esbuild, execFileSync(
[ config.esbuild,
entry, [
"--bundle", entry,
`--platform=${target === "browser" ? "browser" : "node"}`, "--bundle",
"--format=esm", `--platform=${target === "browser" ? "browser" : "node"}`,
"--minify-whitespace", "--format=esm",
"--target=es2024", "--minify-whitespace",
`--outfile=${path.join(output, bundlePath)}`, "--target=es2024",
`--metafile=${metadata}`, `--outfile=${path.join(output, bundlePath)}`,
`--alias:@quixos/camino-replica-core=${coreImport}`, `--metafile=${metadata}`,
`--external:${coreImport}`, `--alias:@quixos/camino-replica-core=${coreImport}`,
], `--external:${coreImport}`,
{ cwd: work, stdio: "inherit" }, ],
{ cwd: work, stdio: "inherit" },
),
); );
const meta = JSON.parse(await fs.readFile(metadata, "utf8")); const meta = JSON.parse(await fs.readFile(metadata, "utf8"));
const built = Object.values(meta.outputs).find((o) => o.entryPoint); const built = Object.values(meta.outputs).find((o) => o.entryPoint);
@@ -152,12 +167,12 @@ for (const { target, mode, settings } of builds) {
.filter((e) => .filter((e) =>
mode === "remote" mode === "remote"
? e.execution.kind === "remote" ? e.execution.kind === "remote"
: e.execution.kind !== "remote" && e.execution.targets.includes(target), : e.execution.kind !== "remote" && e.execution.targets.includes(executionTarget),
) )
.map((e) => e.id); .map((e) => e.id);
const body = { const body = {
id: `${mode}-module`, id: `${mode}-module`,
target, target: executionTarget,
compatibility: "quixos-js-module-v1", compatibility: "quixos-js-module-v1",
path: bundlePath, path: bundlePath,
ports: "quixos-transaction-ports-v1", ports: "quixos-transaction-ports-v1",
@@ -170,22 +185,21 @@ for (const { target, mode, settings } of builds) {
const components = const components =
config.group && !config.components config.group && !config.components
? [] ? []
: await buildComponents({ config, compiler, plan, pkg, output, recordFile, generated }); : await buildComponents({ config, compiler, contract, pkg, output, recordFile, generated });
if (!config.group || config.service.entry) if (!config.group || config.service.entry)
await buildService({ config, compiler, pkg, output, coreServer, sharedFiles, implementations, recordFile }); await buildService({ config, compiler, pkg, output, coreServer, sharedFiles, implementations, recordFile });
const manifest = compiler.parsePackageArtifacts({ const manifest = compiler.parseArtifactGroup({
schemaVersion: 1, schemaVersion: 1,
packageRevisionId: pkg.revision, packageRevisionId: pkg.revision,
bindingDigest: pkg.bindingDigest, bindingDigest: contract.digest,
sourceDigest: pkg.sourceDigest, versions: contract.definition.versions,
versions: plan.definition.versions,
implementations, implementations,
...(components.length ? { components } : {}), ...(components.length ? { components } : {}),
}); });
for (const { file, source } of generated.values()) for (const { file, source } of generated.values())
if ((await fs.readFile(file, "utf8")) !== source) throw Error("Generated bindings changed during build"); if ((await fs.readFile(file, "utf8")) !== source) throw Error("Generated bindings changed during build");
const { receipt, sources } = compiler.expectedArtifactReceipt(plan, manifest, generatorDigest); const { receipt, sources } = compiler.expectedGroupReceipt(contract, manifest, generatorDigest);
for (const [file, bytes] of sources) for (const [file, bytes] of sources)
if (!Buffer.from(bytes).equals(await fs.readFile(path.join(output, file)))) if (!Buffer.from(bytes).equals(await fs.readFile(path.join(output, file))))
throw Error("Receipt generation differs from compiled bindings"); throw Error("Receipt generation differs from compiled bindings");
+60 -58
View File
@@ -1,10 +1,13 @@
import { installDependencies } from "./install-dependencies.mjs"; import { installDependencies } from "./install-dependencies.mjs";
import { checkComponentTypes } from "./component-types.mjs";
import { measureBuild } from "./build-profile.mjs";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { pathToFileURL } from "node:url";
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex"); const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
export async function buildComponents({ config, compiler, plan, pkg, output, recordFile, generated }) { export async function buildComponents({ config, compiler, contract, pkg, output, recordFile, generated }) {
const settings = config.components; const settings = config.components;
if (!settings) { if (!settings) {
if (pkg.components?.length) throw Error("Declared components require checked component build settings"); if (pkg.components?.length) throw Error("Declared components require checked component build settings");
@@ -100,12 +103,14 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
await fs.writeFile( await fs.writeFile(
shim, shim,
`export * from ${JSON.stringify(url)};` + `export * from ${JSON.stringify(url)};` +
(name === "@quixos/camino-react" ? "" : `export {default} from ${JSON.stringify(url)};`), (["@quixos/camino-react", "@quixos/camino-react/ui"].includes(name)
? ""
: `export {default} from ${JSON.stringify(url)};`),
); );
sharedAliases.push("--alias:" + name + "=" + shim, "--external:" + url); sharedAliases.push("--alias:" + name + "=" + shim, "--external:" + url);
} }
const bindingPath = compiler.artifactPath(settings.bindingOutput), const bindingPath = compiler.artifactPath(settings.bindingOutput),
bindings = compiler.generateComponentClientBindings(plan, pkg.revision), bindings = contract.definition.component,
assets = compiler.componentAssetDeclarations(); assets = compiler.componentAssetDeclarations();
await fs.mkdir(path.dirname(path.join(work, bindingPath)), { recursive: true }); await fs.mkdir(path.dirname(path.join(work, bindingPath)), { recursive: true });
await fs.writeFile(path.join(work, bindingPath), bindings); await fs.writeFile(path.join(work, bindingPath), bindings);
@@ -123,10 +128,9 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
for (const root of settings.contracts ?? []) { for (const root of settings.contracts ?? []) {
const companion = JSON.parse(await fs.readFile(path.join(root, "react-contracts.json"), "utf8")); const companion = JSON.parse(await fs.readFile(path.join(root, "react-contracts.json"), "utf8"));
if (companion.schemaVersion !== 1) throw Error("Unsupported React props companion"); if (companion.schemaVersion !== 1) throw Error("Unsupported React props companion");
const contracts = plan.definition.workspace.interfaceImports.filter( const contracts = contract.definition.moduleInterfaces.filter(
(iface) => (iface) =>
iface.revisionId === companion.interfaceRevisionId || iface.revisionId === companion.interfaceRevisionId || iface.definitionId === companion.interfaceRevisionId,
iface.application?.definitionId === companion.interfaceRevisionId,
); );
if (!contracts.length) continue; if (!contracts.length) continue;
for (const [memberId, entry] of Object.entries(companion.members)) { for (const [memberId, entry] of Object.entries(companion.members)) {
@@ -136,12 +140,7 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
`import type {${entry.export} as ${alias}} from ${JSON.stringify(path.join(root, "types", compiler.artifactPath(entry.path)))};`, `import type {${entry.export} as ${alias}} from ${JSON.stringify(path.join(root, "types", compiler.artifactPath(entry.path)))};`,
); );
for (const iface of contracts) { for (const iface of contracts) {
if ( if (!iface.members.includes(memberId)) throw Error("Props companion names an unknown module member");
!(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]); const key = JSON.stringify([iface.revisionId, memberId]);
if (seenProps.has(key)) throw Error("Duplicate React props companion for " + key); if (seenProps.has(key)) throw Error("Duplicate React props companion for " + key);
seenProps.add(key); seenProps.add(key);
@@ -155,46 +154,41 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
propsImports.join("\n") + propsImports.join("\n") +
`\nexport {};\ndeclare module ${JSON.stringify("./" + bindingPath.replace(/\.ts$/, ".js"))} { interface ModuleProps {${propsMembers.join("\n")}} }\n`, `\nexport {};\ndeclare module ${JSON.stringify("./" + bindingPath.replace(/\.ts$/, ".js"))} { interface ModuleProps {${propsMembers.join("\n")}} }\n`,
); );
const components = []; const witnesses = [];
for (const [id, entry] of entries) { for (const [id, entry] of entries) {
const declaration = declarations.find((component) => component.id === id), const declaration = declarations.find((component) => component.id === id),
source = compiler.artifactPath(entry.entry); source = compiler.artifactPath(entry.entry);
if (!/^[$A-Z_a-z][$\w]*$/.test(entry.export)) throw Error("Invalid component export name"); if (!/^[$A-Z_a-z][$\w]*$/.test(entry.export)) throw Error("Invalid component export name");
const key = digest(Buffer.from(id)).slice(0, 24), const key = digest(Buffer.from(id)).slice(0, 24);
directory = "share/quixos/components/" + key;
const witness = path.join(work, "component-check-" + key + ".tsx"); const witness = path.join(work, "component-check-" + key + ".tsx");
const module = pkg.modules.find((module) => module.id === id); const module = pkg.modules.find((module) => module.id === id);
await fs.writeFile( await fs.writeFile(
witness, witness,
`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` + `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" (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 = <Component subject={subject}/>;\n` ? `import type {ComponentProps} from "react";declare const props:Pick<ComponentProps<Modules[${JSON.stringify(declaration.displayName)}]>,"subject"|"input">;const mount = <Component {...props}/>;\n`
: ""), : ""),
); );
execFileSync( witnesses.push(path.basename(witness));
config.node, }
[ checkComponentTypes({
config.tsc, node: config.node,
"--noEmit", tsc: config.tsc,
"--strict", work,
"--skipLibCheck", witnesses,
"false", assets: assetPath,
"--target", props: propsPath,
"es2024", packageRevision: pkg.revision,
"--lib", });
"es2024,dom,dom.iterable,esnext.disposable", const components = [];
"--module", const { checkControls } = await import(pathToFileURL(path.join(settings.sdk, "lint-controls.mjs")));
"nodenext", const { default: ts } = await import(pathToFileURL(path.resolve(path.dirname(config.tsc), "../lib/typescript.js")));
"--moduleResolution", const checkedControls = new Set();
"nodenext", for (const [id, entry] of entries) {
"--jsx", const declaration = declarations.find((component) => component.id === id),
"react-jsx", source = compiler.artifactPath(entry.entry),
path.basename(witness), key = digest(Buffer.from(id)).slice(0, 24),
assetPath, directory = "share/quixos/components/" + key;
propsPath,
],
{ cwd: work, stdio: "inherit" },
);
const metadata = path.join(work, "component-meta-" + key + ".json"); const metadata = path.join(work, "component-meta-" + key + ".json");
const facade = "component-entry-" + key + ".ts"; const facade = "component-entry-" + key + ".ts";
await fs.writeFile( await fs.writeFile(
@@ -202,27 +196,35 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
`export {${entry.export}} from ${JSON.stringify("./" + source.replace(/\.[cm]?tsx?$/, ".js"))};\n`, `export {${entry.export}} from ${JSON.stringify("./" + source.replace(/\.[cm]?tsx?$/, ".js"))};\n`,
); );
await fs.mkdir(path.join(output, directory), { recursive: true }); await fs.mkdir(path.join(output, directory), { recursive: true });
execFileSync( measureBuild("component-bundle", { package: pkg.revision, component: id }, () =>
config.esbuild, execFileSync(
[ config.esbuild,
facade, [
"--bundle", facade,
"--platform=browser", "--bundle",
"--format=esm", "--platform=browser",
"--target=es2024", "--format=esm",
"--jsx=automatic", "--target=es2024",
'--define:process.env.NODE_ENV="production"', "--jsx=automatic",
"--metafile=" + metadata, '--define:process.env.NODE_ENV="production"',
"--outfile=" + path.join(output, directory, "module.mjs"), "--metafile=" + metadata,
...sharedAliases, "--outfile=" + path.join(output, directory, "module.mjs"),
...["svg", "png", "jpg", "jpeg", "gif", "webp", "avif", "woff", "woff2", "ttf", "ico"].map( ...sharedAliases,
(extension) => "--loader:." + extension + "=dataurl", ...["svg", "png", "jpg", "jpeg", "gif", "webp", "avif", "woff", "woff2", "ttf", "ico"].map(
), (extension) => "--loader:." + extension + "=dataurl",
], ),
{ cwd: work, stdio: "inherit" }, ],
{ cwd: work, stdio: "inherit" },
),
); );
const meta = JSON.parse(await fs.readFile(metadata, "utf8")), const meta = JSON.parse(await fs.readFile(metadata, "utf8")),
outputs = Object.entries(meta.outputs); outputs = Object.entries(meta.outputs);
for (const file of Object.keys(meta.inputs)) {
if (!/\.[cm]?[jt]sx?$/.test(file) || file.includes("node_modules/") || checkedControls.has(file)) continue;
checkedControls.add(file);
const errors = checkControls(ts, await fs.readFile(path.resolve(work, file), "utf8"), file);
if (errors.length) throw Error(errors.join("\n"));
}
const main = outputs.find(([file]) => file.endsWith("/module.mjs"))?.[1]; const main = outputs.find(([file]) => file.endsWith("/module.mjs"))?.[1];
if (!main?.exports.includes(entry.export)) throw Error("Component bundle lacks declared export"); if (!main?.exports.includes(entry.export)) throw Error("Component bundle lacks declared export");
if ( if (
+12
View File
@@ -0,0 +1,12 @@
/** Timing is log data, never part of an artifact or its witness. */
export function measureBuild(phase, details, action) {
const started = performance.now();
try {
return action();
} finally {
if (process.env.QUIXOS_TRACE_PROFILE === "1")
console.error(
`[qx-build-profile] ${JSON.stringify({ phase, ms: Math.round(performance.now() - started), ...details })}`,
);
}
}
+16 -4
View File
@@ -1,6 +1,7 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { installDependencies } from "./install-dependencies.mjs";
const config = JSON.parse(await fs.readFile(process.argv[2], "utf8")); const config = JSON.parse(await fs.readFile(process.argv[2], "utf8"));
const resource = JSON.parse(await fs.readFile(config.interface, "utf8")); const resource = JSON.parse(await fs.readFile(config.interface, "utf8"));
if (resource.kind !== "interface") throw Error("React props belong to an interface resource"); if (resource.kind !== "interface") throw Error("React props belong to an interface resource");
@@ -29,14 +30,24 @@ const metadata = await fs.readFile(packageFile, "utf8").then(JSON.parse, (error)
throw error; throw error;
}); });
await fs.writeFile(packageFile, JSON.stringify({ ...metadata, type: "module" })); await fs.writeFile(packageFile, JSON.stringify({ ...metadata, type: "module" }));
if (config.dependencies) await fs.symlink(config.dependencies, path.join(work, "node_modules")); const modules = path.join(output, "node_modules");
await fs.mkdir(modules, { recursive: true });
await installDependencies({
destination: modules,
application: config.dependencies,
selected: config.reactSdk ? { "@quixos/camino-react": config.reactSdk } : {},
reserved: ["@quixos/camino-react"],
});
await fs.symlink(modules, path.join(work, "node_modules"));
const members = Object.create(null); const members = Object.create(null);
let index = 0; let index = 0;
for (const [id, entry] of Object.entries(config.members)) { for (const [id, entry] of Object.entries(config.members)) {
const member = (iface.template?.members ?? iface.members).find((member) => member.id === id); const member = (iface.template?.members ?? iface.members).find((member) => member.id === id);
if ( if (
member?.kind !== "module" || member?.kind !== "module" ||
!["org.quixos.react.component/1", "org.quixos.react.subject-only/1"].includes(member.contract) !["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes(
member.contract,
)
) )
throw Error("Props entry is not a React module member: " + id); throw Error("Props entry is not a React module member: " + id);
if ( if (
@@ -52,7 +63,7 @@ for (const [id, entry] of Object.entries(config.members)) {
`import type {${entry.export} as Props} from ${typeImport}; `import type {${entry.export} as Props} from ${typeImport};
type Assert<T extends true> = T; type Assert<T extends true> = T;
type ObjectProps = Assert<Props extends object ? true : false>; type ObjectProps = Assert<Props extends object ? true : false>;
type ReservedSubject = Assert<"subject" extends keyof Props ? false : true>; type ReservedRendererProps = Assert<Extract<keyof Props, "subject" | "input" | "navigation" | "usingComponent" | "provider" | "coverage" | "componentCodeFallback"> extends never ? true : false>;
${member.contract === "org.quixos.react.subject-only/1" ? "type SubjectOnly = Assert<{} extends Props ? true : false>;" : ""} ${member.contract === "org.quixos.react.subject-only/1" ? "type SubjectOnly = Assert<{} extends Props ? true : false>;" : ""}
export type {Props};\n`, export type {Props};\n`,
); );
@@ -69,6 +80,8 @@ execFileSync(
"--strict", "--strict",
"--target", "--target",
"es2024", "es2024",
"--lib",
"es2024,dom,dom.iterable,esnext.disposable",
"--module", "--module",
"nodenext", "nodenext",
"--moduleResolution", "--moduleResolution",
@@ -88,7 +101,6 @@ for (const file of declarations) {
await fs.copyFile(path.join(work, file), path.join(output, "types", file)); await fs.copyFile(path.join(work, file), path.join(output, "types", file));
} }
await fs.writeFile(path.join(output, "package.json"), JSON.stringify({ type: "module" })); 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( await fs.writeFile(
path.join(output, "react-contracts.json"), path.join(output, "react-contracts.json"),
JSON.stringify({ schemaVersion: 1, interfaceRevisionId: iface.revisionId, members }, null, 2), JSON.stringify({ schemaVersion: 1, interfaceRevisionId: iface.revisionId, members }, null, 2),
+13
View File
@@ -0,0 +1,13 @@
# Standalone build drivers import sibling JavaScript modules. Their code is an
# input; Nix helper APIs and test sources do not belong to that dependency.
builtins.path {
path = ./.;
name = "camino-build-tools";
filter =
filename: type:
let
name = builtins.baseNameOf filename;
in
type == "directory"
|| (builtins.match ".*\\.mjs" name != null && builtins.match ".*\\.test\\.mjs" name == null);
}
+49
View File
@@ -0,0 +1,49 @@
import path from "node:path";
import { createRequire } from "node:module";
const [tsc, count, ...args] = process.argv.slice(2);
const ts = createRequire(import.meta.url)(path.resolve(path.dirname(tsc), "../lib/typescript.js"));
const parsed = ts.parseCommandLine(args);
const witnesses = parsed.fileNames.slice(0, Number(count));
const shared = parsed.fileNames.slice(Number(count));
const host = ts.createCompilerHost(parsed.options, true);
// These programs only discover source closures; they do not bind or check.
// Sharing immutable parsed source files avoids reading/parsing the same libs
// for every witness. Only the final combined program performs typechecking.
const sources = new Map();
const getSourceFile = host.getSourceFile;
host.getSourceFile = (filename, ...options) => {
if (!sources.has(filename)) sources.set(filename, getSourceFile(filename, ...options));
return sources.get(filename);
};
for (const method of ["fileExists", "directoryExists", "getDirectories", "readFile", "realpath"]) {
if (!host[method]) continue;
const original = host[method],
cache = new Map();
host[method] = (filename) => {
if (!cache.has(filename)) cache.set(filename, original(filename));
return cache.get(filename);
};
}
const closures = witnesses.map(
(witness) => new Set(ts.createProgram([witness, ...shared], parsed.options, host).getSourceFiles()),
);
const common = new Set([...closures[0]].filter((source) => closures.every((closure) => closure.has(source))));
const ambient = (node) =>
ts.isModuleDeclaration(node) || ts.isNamespaceExportDeclaration(node) || ts.forEachChild(node, ambient);
// A file that is visible to just some witnesses must not contribute globals
// or ambient module declarations to the others. Otherwise a combined success
// could hide an independent failure. Script files are conservatively unsafe.
if (
closures.some((closure) =>
[...closure].some((source) => !common.has(source) && (!ts.isExternalModule(source) || ambient(source))),
)
)
process.exit(2);
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
const diagnostics = [...parsed.errors, ...ts.getPreEmitDiagnostics(program)];
if (diagnostics.length) {
console.error(ts.formatDiagnosticsWithColorAndContext(diagnostics, host));
process.exit(1);
}
+51
View File
@@ -0,0 +1,51 @@
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { measureBuild } from "./build-profile.mjs";
// Component witnesses share bindings and usually the same source graph.
// Check a bounded batch once when distinct source closures cannot supply
// ambient declarations to one another. Otherwise use the original checks.
export function checkComponentTypes({ node, tsc, work, witnesses, assets, props, packageRevision }) {
const checkTypes = (files, combined = false) =>
execFileSync(
node,
[
...(combined ? [fileURLToPath(new URL("./component-type-batch.mjs", import.meta.url))] : []),
tsc,
...(combined ? [String(files.length)] : []),
"--noEmit",
"--strict",
"--skipLibCheck",
"false",
"--target",
"es2024",
"--lib",
"es2024,dom,dom.iterable,esnext.disposable",
"--module",
"nodenext",
"--moduleResolution",
"nodenext",
"--jsx",
"react-jsx",
...files,
assets,
props,
],
{ cwd: work, stdio: combined ? "pipe" : "inherit" },
);
measureBuild("component-typecheck", { package: packageRevision, components: witnesses.length }, () => {
for (let start = 0; start < witnesses.length; start += 1000) {
const batch = witnesses.slice(start, start + 1000);
if (batch.length === 1) checkTypes(batch);
else {
try {
checkTypes(batch, true);
} catch (error) {
if (!(error.status > 0) && error.code !== "ENOBUFS") throw error;
console.error("Component batch requires independent witness checks");
for (const witness of batch) checkTypes([witness]);
}
}
}
});
}
+59
View File
@@ -0,0 +1,59 @@
import assert from "node:assert/strict";
import { mkdtemp, writeFile, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { checkComponentTypes } from "./component-types.mjs";
const tsc = process.env.QUIXOS_TYPESCRIPT;
if (!tsc) throw Error("Pass the selected TypeScript compiler path");
async function fixture(context, left, right) {
const work = await mkdtemp(path.join(os.tmpdir(), "component-types-"));
context.after(() => rm(work, { recursive: true, force: true }));
await Promise.all(
Object.entries({ "left.ts": left, "right.ts": right, "assets.d.ts": "", "props.d.ts": "" }).map(([file, source]) =>
writeFile(path.join(work, file), source),
),
);
return {
node: process.execPath,
tsc,
work,
witnesses: ["left.ts", "right.ts"],
assets: "assets.d.ts",
props: "props.d.ts",
packageRevision: "test",
};
}
test("shared component typechecking accepts all valid witnesses", async (context) => {
checkComponentTypes(await fixture(context, "export const left: string = 'ok';", "export const right: number = 1;"));
});
test("independently valid ambient declarations use the full individual check fallback", async (context) => {
checkComponentTypes(
await fixture(
context,
"export {}; declare global { interface Application { value: string; } }",
"export {}; declare global { interface Application { value: number; } }",
),
);
});
test("a type error in any witness still rejects the artifact", async (context) => {
const input = await fixture(context, "export const left: string = 'ok';", "export const right: string = 1;");
assert.throws(() => checkComponentTypes(input));
});
test("one witness's globals cannot make an independently invalid witness pass", async (context) => {
const input = await fixture(
context,
"export {}; declare global { interface Application { value: string; } }",
"export const right: Application = {value: 'ok'};",
);
assert.throws(() => checkComponentTypes(input));
});
test("an ambient module from another source closure cannot resolve a missing import", async (context) => {
const input = await fixture(
context,
"declare module 'missing-component-package' { export const value: string; }",
"import {value} from 'missing-component-package'; export const right = value;",
);
assert.throws(() => checkComponentTypes(input));
});
+30
View File
@@ -0,0 +1,30 @@
# QX and recursive pins enter through the checked contract and build recipe.
# The compiler receives authored implementation files, not resource provenance.
{ lib, src }:
builtins.path {
path = src;
name = "camino-implementation-source";
filter =
path: type:
let
name = builtins.baseNameOf path;
in
if type == "directory" then
!(builtins.elem name [
".git"
".jj"
"node_modules"
])
else
!(builtins.elem name [
"flake.nix"
"flake.lock"
"quixos.lock"
"quixos.check.json"
"quixos.react.json"
"yarn-project.nix"
"components.nix"
"contracts.nix"
])
&& !(lib.hasSuffix ".qx" name);
}
+2
View File
@@ -0,0 +1,2 @@
# The selected protocol owns semantic projection; builders receive its result.
args@{ protocol, ... }: import "${protocol}/share/package-context.nix" args
+57
View File
@@ -0,0 +1,57 @@
{
pkgs,
nixpkgs,
system,
}:
let
packageSet = (import ./quixos-package-helpers.nix).packageSet;
shared = {
marker = "shared";
};
environment = {
inherit system;
nixpkgsSource = toString nixpkgs;
pkgs = shared;
narHash = nixpkgs.narHash;
};
standard = {
inherit nixpkgs system;
context.buildEnvironment = environment;
};
fallback = packageSet { inherit nixpkgs system; };
in
assert packageSet standard == shared;
assert
packageSet (
standard
// {
context.buildEnvironment = environment // {
nixpkgsSource = "same-content-under-another-name";
};
}
) == shared;
assert
(packageSet (
standard
// {
context.buildEnvironment = environment // {
system = "another-system";
};
}
)).path == fallback.path;
assert
(packageSet (
standard
// {
context.buildEnvironment = environment // {
nixpkgsSource = "another-source";
narHash = "another-hash";
};
}
)).path == fallback.path;
assert
(packageSet (standard // { overlays = [ (_: _: { marker = "overlay"; }) ]; })).marker == "overlay";
assert (packageSet (standard // { config.allowBroken = true; })).config.allowBroken;
pkgs.runCommand "camino-package-set-check" { } ''
touch "$out"
''
+34
View File
@@ -847,6 +847,40 @@ let
in in
{ {
mkCaminoReactPackage = import ./react-package.nix; mkCaminoReactPackage = import ./react-package.nix;
# Share the composition's package set only for the same exact Nixpkgs source,
# system and default import arguments. Config/overlay/cross-build choices use
# ordinary Nixpkgs evaluation. No package implementation is substituted.
packageSet =
args@{
nixpkgs,
system,
context ? { },
...
}:
let
authoredArgs = builtins.removeAttrs args [
"nixpkgs"
"context"
];
importArgs = {
config = { };
overlays = [ ];
}
// authoredArgs;
environment = context.buildEnvironment or null;
in
if
builtins.attrNames authoredArgs == [ "system" ]
&& environment != null
&& environment.system == system
&& (
environment.nixpkgsSource == toString nixpkgs
|| ((nixpkgs.narHash or null) != null && nixpkgs.narHash == (environment.narHash or null))
)
then
environment.pkgs
else
import nixpkgs importArgs;
mkCaminoReactContracts = import ./react-contracts.nix; mkCaminoReactContracts = import ./react-contracts.nix;
mkCaminoYarnDependencies = import ./yarn-dependencies.nix; mkCaminoYarnDependencies = import ./yarn-dependencies.nix;
mkCaminoYarnShell = import ./yarn-shell.nix; mkCaminoYarnShell = import ./yarn-shell.nix;
+20 -3
View File
@@ -6,16 +6,33 @@
dependencies ? null, dependencies ? null,
}: }:
let let
buildTools = import ./build-tools.nix;
config = pkgs.writeText "camino-react-contracts.json" ( config = pkgs.writeText "camino-react-contracts.json" (
builtins.toJSON { builtins.toJSON {
source = src; source = import ./implementation-source.nix {
interface = context.interface; inherit (pkgs) lib;
inherit src;
};
interface = builtins.toFile "camino-interface-contract.json" (
builtins.unsafeDiscardStringContext (
builtins.toJSON (
let
resource = builtins.fromJSON (builtins.readFile context.interface);
in
{
kind = resource.kind;
revision = builtins.removeAttrs resource.revision [ "source" ];
}
)
)
);
inherit members dependencies; inherit members dependencies;
reactSdk = context.reactSdk or null;
node = "${pkgs.nodejs_24}/bin/node"; node = "${pkgs.nodejs_24}/bin/node";
tsc = "${context.typescript}/node_modules/typescript/bin/tsc"; tsc = "${context.typescript}/node_modules/typescript/bin/tsc";
} }
); );
in in
pkgs.runCommand "camino-react-contracts" { } '' pkgs.runCommand "camino-react-contracts" { } ''
${pkgs.nodejs_24}/bin/node ${./.}/build-react-contracts.mjs ${config} ${pkgs.nodejs_24}/bin/node ${buildTools}/build-react-contracts.mjs ${config}
'' ''
+1 -1
View File
@@ -10,7 +10,7 @@ let
group = import ./artifacts.nix { group = import ./artifacts.nix {
inherit pkgs src; inherit pkgs src;
inherit (context) inherit (context)
plan buildContract
core core
sharedRuntime sharedRuntime
typescript typescript
+1 -1
View File
@@ -15,7 +15,7 @@ let
group = import ./artifacts.nix { group = import ./artifacts.nix {
inherit pkgs src; inherit pkgs src;
inherit (context) inherit (context)
plan buildContract
core core
sharedRuntime sharedRuntime
typescript typescript
+1 -1
View File
@@ -11,7 +11,7 @@ let
group = import ./artifacts.nix { group = import ./artifacts.nix {
inherit pkgs src service; inherit pkgs src service;
inherit (context) inherit (context)
plan buildContract
core core
sharedRuntime sharedRuntime
typescript typescript