Compare commits

..

4 Commits

Author SHA1 Message Date
Timothy J. Aveni 8dde1f6b82 Add Camino TypeScript package helper 2026-07-12 12:21:21 -07:00
Timothy J. Aveni e0874093f2 Drop perl from package prepare shell 2026-06-27 20:33:06 -07:00
Timothy J. Aveni 0b159fcb81 Add perl to package prepare shell 2026-06-27 20:31:20 -07:00
Timothy J. Aveni f33eee054f Initial commit
germanium cutoff
2026-05-24 15:25:38 -07:00
21 changed files with 318 additions and 1951 deletions
-8
View File
@@ -1,8 +0,0 @@
{
"version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
"sourceCommit": "54e443af5dd963f1fa6f83f062f4943b46ad2535",
"sourcePath": "quixos-instance/quixos-nix-helpers",
"exportName": "quixos-nix-helpers",
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git"
}
File diff suppressed because one or more lines are too long
-74
View File
@@ -1,74 +0,0 @@
# Checked capability and component artifacts
`mkCaminoArtifacts` is the fresh package builder. It regenerates candidate
bindings, typechecks the entry's dependency closure, bundles declared targets,
generates a process service when requested, and emits content manifests and
receipts. It has no migration step or package-defined shell hooks.
```nix
# Inside a per-system flake output:
quixosPackages.checkedArtifacts =
{ plan, generator, core, sharedRuntime, typescript, packageRevisionId }:
helpers.mkCaminoArtifacts {
inherit pkgs plan core sharedRuntime typescript packageRevisionId;
protocol = generator;
src = ./.;
portable = {
entry = "src/portable.ts";
bindingOutput = "src/gen/qx.ts";
registryExport = "registry";
targets = [ "browser" "server" ];
};
};
```
The handler entry exports `registry = createRegistry(handlers)` from the generated
bindings. One registry contains many exports. The builder always supplies a server
implementation for the generated process service. A service-only package instead
omits `portable` and supplies `service.entry`, an ordinary JavaScript process entry
using the versioned stdin/stdout protocol. Custom services own their lifecycle and
may hold external resources. This first helper supports JavaScript custom service
entries; other languages can emit the same checked artifact contract independently.
Additional portable source dependencies can be provided as a locked `nodeModules` directory.
The workspace supplies the exact SDK and compiler; packages cannot replace those
with their own versions. Authored source symlinks and non-bundled portable imports
other than the SDK and server Node builtins are rejected. This is build closure
checking, not a purity proof or security sandbox.
The core/Automerge bundle and Replicache bundle are built once. Browser modules
import the core through a content-addressed gateway URL, independent of package
identity. Each manifest covers the shared bytes it uses. The gateway retains its
workspace authentication and explicit file registry; it never interprets an HTTP
path as a Nix store path. Applications cannot import Replicache through the SDK.
`checked-artifacts.nix` evaluates a package's `checkedArtifacts` output without
activation. During the checked Nix build, `loadCheckedArtifactWorld` verifies manifest bytes, the selected
generator identity, and exact regenerated binding sources. Receipts are consistency
evidence from a trusted Nix build, not cryptographic attestations of remote builds.
The build stores the checked plan, kernel/query programs, their digests and original
compiler provenance in its immutable candidate output. Runtime loading validates
that output's identities, artifact contents and supported runtime contracts. It
does not regenerate bindings or programs with the currently running compiler.
Nix owns rebuilding and cache reuse when a toolchain input changes.
The root `camino-artifact-builds` check exercises actual reader/service outputs,
module and process execution, browser shared imports, rejection recovery, and
altered receipt rejection at the build boundary. The managed workspace host loads
the resulting checked candidate before activation. No adapter pretends this ABI
works in the old server dispatcher.
Component packages add `components = { inherit (react) sdk runtime; entries; }`.
The ordinary execution candidate checker supplies the selected React build inputs
when the package declares components. Each entry maps a component ID to
`{ entry = "src/card.tsx"; export = "Card"; }`. See the
[React SDK](../camino-react/README.md) for bindings and hooks. CSS is emitted as a
checked artifact; local image/font imports are inlined. The helper rejects
undeclared external assets and substituted React/client dependencies.
Component-only packages produce no backend service.
`camino-component-artifacts` checks bad subject/SDK/dependency/asset inputs and
proves that a component-only edit preserves a mixed package's service digest.
Run `nix run .#camino-component-source-qualification` from the monorepo to
materialize separate retained Git resources and compile an ordinary Nix candidate.
This is a source/build qualification harness; it does not deploy a workspace.
-77
View File
@@ -1,77 +0,0 @@
# Fresh artifact builder. Candidate plans and runtime dependencies are supplied by the workspace compiler.
{
pkgs,
src,
plan,
protocol,
core,
sharedRuntime,
typescript ? core.dependencies,
packageRevisionId,
portable ? null,
remote ? null,
service ? { },
nodeModules ? null,
components ? null,
group ? false,
}:
let
compiler = "${protocol}/libexec/quixos-protocol/execution-world.mjs";
config = pkgs.writeText "camino-artifact-build.json" (
builtins.toJSON {
inherit
plan
compiler
packageRevisionId
core
sharedRuntime
nodeModules
service
group
;
components =
if components == null then
null
else
{
inherit (components) sdk runtime entries;
dependencies = components.sdk.dependencies;
bindingOutput = components.bindingOutput or "src/gen/client.ts";
contracts = components.contracts or [ ];
};
source = src;
portable =
if portable == null then
null
else
{
inherit (portable) entry;
entries = portable.entries or { };
registryExport = portable.registryExport or "registry";
bindingOutput = portable.bindingOutput or "src/gen/qx.ts";
targets =
portable.targets or [
"browser"
"server"
];
};
remote =
if remote == null then
null
else
{
inherit (remote) entry;
registryExport = remote.registryExport or "registry";
bindingOutput = remote.bindingOutput or "src/gen/remote.ts";
};
node = "${pkgs.nodejs_24}/bin/node";
tsc = "${typescript}/node_modules/typescript/bin/tsc";
esbuild = "${pkgs.esbuild}/bin/esbuild";
shell = "${pkgs.runtimeShell}";
serviceAdapter = ./portable-service.mjs;
}
);
in
pkgs.runCommand "camino-checked-artifacts" { passthru.buildConfig = config; } ''
${pkgs.nodejs_24}/bin/node ${./.}/build-artifacts.mjs ${config}
''
-103
View File
@@ -1,103 +0,0 @@
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 }),
);
-29
View File
@@ -1,29 +0,0 @@
{
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}
''
-69
View File
@@ -1,69 +0,0 @@
import { createRequire } from "node:module";
import path from "node:path";
// Resolve build dependencies from the package's locked node_modules, not from
// the helper's own Nix-store location. This program runs only during the build.
const require = createRequire(path.join(process.cwd(), "package.json"));
const { build } = require("esbuild");
const options = JSON.parse(process.argv[2]);
const platform = new Map([
["react", "/__quixos/platform/react/v18.mjs"],
["react/jsx-runtime", "/__quixos/platform/react-jsx-runtime/v18.mjs"],
["react/jsx-dev-runtime", "/__quixos/platform/react-jsx-dev-runtime/v18.mjs"],
["@quixos/web-studio-react-runtime", "/__quixos/platform/web-studio-react-runtime/v1.mjs"],
]);
await build({
...options,
plugins: [
{
name: "quixos-browser-source",
setup(api) {
api.onResolve({ filter: /\?browser-source$/ }, async ({ path: specifier, resolveDir }) => {
if (!specifier.startsWith("./") && !specifier.startsWith("../"))
throw new Error("Browser source imports must name a relative compiled module");
const resolved = await api.resolve(specifier.slice(0, -"?browser-source".length), {
resolveDir,
kind: "import-statement",
});
if (resolved.errors.length) return { errors: resolved.errors };
return { path: resolved.path, namespace: "browser-source" };
});
api.onLoad({ filter: /.*/, namespace: "browser-source" }, async ({ path: entry }) => {
const browser = await build({
entryPoints: [entry],
bundle: true,
write: false,
outfile: "component.mjs",
format: "esm",
platform: "browser",
target: "es2022",
metafile: true,
plugins: [
{
name: "quixos-platform",
setup(browserApi) {
browserApi.onResolve({ filter: /.*/ }, ({ path: name }) =>
platform.has(name) ? { path: platform.get(name), external: true } : undefined,
);
},
},
],
});
const js = browser.outputFiles.find((file) => file.path.endsWith(".mjs"));
if (!js) throw new Error("Browser source build produced no JavaScript");
const css = browser.outputFiles.find((file) => file.path.endsWith(".css"));
if (css && Object.values(browser.metafile.outputs).some((output) => output.exports?.includes("styles")))
throw new Error("Use either imported CSS or an explicit styles export, not both");
if (browser.outputFiles.some((file) => !file.path.endsWith(".mjs") && !file.path.endsWith(".css")))
throw new Error("Browser assets must be embedded, not emitted as unserved files");
const source = js.text + (css ? `\nexport const styles = ${JSON.stringify(css.text)};\n` : "");
return {
contents: `export default ${JSON.stringify(source)};`,
loader: "js",
watchFiles: Object.keys(browser.metafile.inputs),
};
});
},
},
],
});
-193
View File
@@ -1,193 +0,0 @@
import { installDependencies } from "./install-dependencies.mjs";
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";
import path from "node:path";
import { createHash } from "node:crypto";
import { builtinModules } from "node:module";
import { execFileSync } from "node:child_process";
import { pathToFileURL } from "node:url";
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((p) => p.revision === config.packageRevisionId);
if (!pkg || compiler.hexDigest(plan.definition) !== plan.digest) throw Error("Invalid checked execution plan/package");
const output = process.env.out;
if (!output) throw Error("Missing Nix output");
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
const generatorDigest = digest(await fs.readFile(config.compiler));
const runtime =
config.portable || config.remote
? JSON.parse(await fs.readFile(path.join(config.sharedRuntime, "runtime.json"), "utf8"))
: { files: [] };
const relative = (p) => compiler.artifactPath(p);
const implementations = [];
const generated = new Map();
const recordFile = async (p, mediaType) => {
const bytes = await fs.readFile(path.join(output, relative(p)));
return { path: p, digest: digest(bytes), bytes: bytes.length, mediaType };
};
const copyRuntime = async () => {
await fs.mkdir(path.join(output, "share/quixos/shared"), { recursive: true });
for (const file of runtime.files) {
relative(file.path);
const bytes = await fs.readFile(path.join(config.sharedRuntime, file.path));
if (bytes.length !== file.bytes || digest(bytes) !== file.digest) throw Error("Shared runtime mismatch");
await fs.writeFile(path.join(output, "share/quixos/shared", file.path), bytes);
}
};
await copyRuntime();
const sharedFiles = await Promise.all(
runtime.files.map((f) => recordFile(`share/quixos/shared/${f.path}`, f.mediaType)),
);
const coreFile = sharedFiles.find((f) => f.path.endsWith("/core.mjs"));
const coreURL = coreFile ? compiler.contentArtifactUrl(coreFile) : null;
const coreServer = path.join(config.sharedRuntime, "core.mjs");
await fs.mkdir(path.join(output, "share/quixos/generated"), { recursive: true });
const portable = config.portable;
for (const target of Object.keys(portable?.entries ?? {}))
if (!["browser", "server"].includes(target)) throw Error(`Unsupported portable entry target ${target}`);
for (const settings of [portable, config.remote].filter(Boolean))
if (!/^[$A-Z_a-z][$\w]*$/.test(settings.registryExport))
throw Error("Registry export must be a JavaScript identifier");
const targets = portable ? [...new Set([...portable.targets, "server"])] : [];
const builds = targets.map((target) => ({ target, mode: target, settings: portable }));
if (config.remote) builds.push({ target: "server", mode: "remote", settings: config.remote });
for (const { target, mode, settings } of builds) {
if (!["browser", "server"].includes(target)) throw Error(`Unsupported JS build target ${target}`);
const work = path.resolve(`work-${mode}`);
await fs.cp(config.source, work, { recursive: true, dereference: false });
const writable = async (directory) => {
await fs.chmod(directory, 0o755);
for (const item of await fs.readdir(directory, { withFileTypes: true })) {
const file = path.join(directory, item.name);
if (item.isSymbolicLink()) throw Error("Authored package source cannot contain symlinks");
if (item.isDirectory()) await writable(file);
else await fs.chmod(file, 0o644);
}
};
await writable(work);
const modules = path.join(work, "node_modules");
await fs.mkdir(path.join(modules, "@quixos"), { recursive: true });
await installDependencies({
destination: modules,
application: config.nodeModules,
selected: { "@quixos/camino-replica-core": config.core },
reserved: ["@quixos/camino-replica-core", "@quixos/camino-replica-client", "@quixos/camino-replica-engine"],
});
const packageFile = path.join(work, "package.json");
const packageMetadata = await fs.readFile(packageFile, "utf8").then(JSON.parse, (error) => {
if (error.code === "ENOENT") return {};
throw error;
});
await fs.writeFile(packageFile, JSON.stringify({ ...packageMetadata, type: "module" }));
const bindingPath = relative(settings.bindingOutput);
const source =
mode === "remote"
? compiler.generateRemoteTypeScriptBindings(plan.definition.bindingSchema, pkg.revision)
: compiler.generatePortableTypeScriptBindings(plan.definition.bindingSchema, pkg.revision, { target });
await fs.mkdir(path.dirname(path.join(work, bindingPath)), { recursive: true });
await fs.writeFile(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);
const entry = relative(settings.entries?.[target] ?? settings.entry);
execFileSync(
config.node,
[
config.tsc,
"--noEmit",
"--strict",
"--skipLibCheck",
"false",
"--target",
"es2024",
"--lib",
"es2024,dom,esnext.disposable",
"--module",
"nodenext",
"--moduleResolution",
"nodenext",
entry,
],
{ cwd: work, stdio: "inherit" },
);
const bundlePath =
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 });
const coreImport = target === "browser" ? coreURL : coreServer;
const metadata = path.join(work, "bundle-meta.json");
execFileSync(
config.esbuild,
[
entry,
"--bundle",
`--platform=${target === "browser" ? "browser" : "node"}`,
"--format=esm",
"--minify-whitespace",
"--target=es2024",
`--outfile=${path.join(output, bundlePath)}`,
`--metafile=${metadata}`,
`--alias:@quixos/camino-replica-core=${coreImport}`,
`--external:${coreImport}`,
],
{ cwd: work, stdio: "inherit" },
);
const meta = JSON.parse(await fs.readFile(metadata, "utf8"));
const built = Object.values(meta.outputs).find((o) => o.entryPoint);
if (!built?.exports.includes(settings.registryExport)) throw Error("Bundle lacks the declared registry export");
// Portable modules must contain their ordinary source dependencies and share only the checked SDK.
if (
Object.values(meta.outputs).some((o) =>
o.imports.some(
(i) =>
i.external &&
i.path !== coreImport &&
!(target === "server" && builtinModules.includes(i.path.replace(/^node:/, ""))),
),
)
)
throw Error("Unexpected external import in portable module");
const exports = pkg.exports
.filter((e) =>
mode === "remote"
? e.execution.kind === "remote"
: e.execution.kind !== "remote" && e.execution.targets.includes(target),
)
.map((e) => e.id);
const body = {
id: `${mode}-module`,
target,
compatibility: "quixos-js-module-v1",
path: bundlePath,
ports: "quixos-transaction-ports-v1",
registryExport: settings.registryExport,
exports,
files: [await recordFile(bundlePath, "text/javascript"), ...sharedFiles],
};
implementations.push({ ...body, contentDigest: compiler.implementationDigest(body) });
}
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,
bindingDigest: pkg.bindingDigest,
sourceDigest: pkg.sourceDigest,
versions: plan.definition.versions,
implementations,
...(components.length ? { components } : {}),
});
for (const { file, source } of generated.values())
if ((await fs.readFile(file, "utf8")) !== source) throw Error("Generated bindings changed during build");
const { receipt, sources } = compiler.expectedArtifactReceipt(plan, manifest, generatorDigest);
for (const [file, bytes] of sources)
if (!Buffer.from(bytes).equals(await fs.readFile(path.join(output, file))))
throw Error("Receipt generation differs from compiled bindings");
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));
-262
View File
@@ -1,262 +0,0 @@
import { installDependencies } from "./install-dependencies.mjs";
import fs from "node:fs/promises";
import path from "node:path";
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
export async function buildComponents({ config, compiler, plan, pkg, output, recordFile, generated }) {
const settings = config.components;
if (!settings) {
if (pkg.components?.length) throw Error("Declared components require checked component build settings");
return [];
}
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 ||
entries.some(([id]) => !declarations.some((component) => component.id === id))
)
throw Error("Component build entries differ from package declarations");
const runtime = JSON.parse(await fs.readFile(path.join(settings.runtime, "runtime.json"), "utf8"));
if (
runtime.sdkCodeDigest !== digest(await fs.readFile(path.join(settings.sdk, "dist/index.js"))) ||
runtime.sdkTypeDigest !== digest(await fs.readFile(path.join(settings.sdk, "dist/index.d.ts")))
)
throw Error("Component SDK differs from selected shared runtime");
const sharedFiles = [];
for (const file of runtime.files) {
compiler.artifactPath(file.path);
const bytes = await fs.readFile(path.join(settings.runtime, file.path));
if (bytes.length !== file.bytes || digest(bytes) !== file.digest) throw Error("Modified component runtime");
const target = "share/quixos/component-shared/" + file.path;
await fs.mkdir(path.dirname(path.join(output, target)), { recursive: true });
await fs.writeFile(path.join(output, target), bytes);
sharedFiles.push(await recordFile(target, file.mediaType));
}
const sharedModules = Object.fromEntries(
compiler.componentSharedModules.map((name) => {
const file = sharedFiles.find((file) => file.path === "share/quixos/component-shared/" + runtime.modules[name]);
if (!file) throw Error("Missing shared component module " + name);
return [name, file.path];
}),
);
const imports = Object.fromEntries(
Object.entries(sharedModules).map(([name, file]) => [
name,
compiler.contentArtifactUrl(sharedFiles.find((value) => value.path === file)),
]),
);
const core = runtime.files.find((file) => file.path === "core.mjs");
const selected = JSON.parse(await fs.readFile(path.join(config.sharedRuntime, "runtime.json"), "utf8")).files.find(
(file) => file.path === "core.mjs",
);
if (core?.digest !== selected?.digest) throw Error("Component runtime core differs from portable runtime core");
const work = path.resolve("work-components");
await fs.cp(config.source, work, { recursive: true, dereference: false });
const writable = async (directory) => {
await fs.chmod(directory, 0o755);
for (const item of await fs.readdir(directory, { withFileTypes: true })) {
const file = path.join(directory, item.name);
if (item.isSymbolicLink()) throw Error("Authored component source cannot contain symlinks");
if (item.isDirectory()) await writable(file);
else await fs.chmod(file, 0o644);
}
};
await writable(work);
const modules = path.join(work, "node_modules");
await fs.mkdir(path.join(modules, "@quixos"), { recursive: true });
await installDependencies({
destination: modules,
application: config.nodeModules,
defaults: [path.join(settings.dependencies, "node_modules")],
selected: { "@quixos/camino-react": settings.sdk },
reserved: [
"react",
"react-dom",
"scheduler",
"csstype",
"@types/react",
"@types/react-dom",
"@types/prop-types",
"@quixos/camino-react",
"@quixos/camino-replica-core",
"@quixos/camino-replica-client",
"@quixos/camino-replica-engine",
],
});
const packageFile = path.join(work, "package.json");
const packageMetadata = await fs.readFile(packageFile, "utf8").then(JSON.parse, (error) => {
if (error.code === "ENOENT") return {};
throw error;
});
await fs.writeFile(packageFile, JSON.stringify({ ...packageMetadata, type: "module" }));
// CommonJS dependencies may require React. An ESM bridge lets esbuild lower
// that require while retaining the one host-owned runtime instance.
const sharedAliases = [];
for (const [name, url] of Object.entries(imports)) {
const shim = path.join(work, "shared-" + digest(Buffer.from(name)).slice(0, 16) + ".mjs");
await fs.writeFile(
shim,
`export * from ${JSON.stringify(url)};` +
(name === "@quixos/camino-react" ? "" : `export {default} from ${JSON.stringify(url)};`),
);
sharedAliases.push("--alias:" + name + "=" + shim, "--external:" + url);
}
const bindingPath = compiler.artifactPath(settings.bindingOutput),
bindings = compiler.generateComponentClientBindings(plan, pkg.revision),
assets = compiler.componentAssetDeclarations();
await fs.mkdir(path.dirname(path.join(work, bindingPath)), { recursive: true });
await fs.writeFile(path.join(work, bindingPath), bindings);
const assetPath = "component-assets.d.ts";
await fs.writeFile(path.join(work, assetPath), assets);
await fs.writeFile(path.join(output, "share/quixos/generated/client.ts"), bindings);
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),
source = compiler.artifactPath(entry.entry);
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 + ".tsx");
const module = pkg.modules.find((module) => module.id === id);
await fs.writeFile(
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` +
(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`
: ""),
);
execFileSync(
config.node,
[
config.tsc,
"--noEmit",
"--strict",
"--skipLibCheck",
"false",
"--target",
"es2024",
"--lib",
"es2024,dom,dom.iterable,esnext.disposable",
"--module",
"nodenext",
"--moduleResolution",
"nodenext",
"--jsx",
"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,
[
facade,
"--bundle",
"--platform=browser",
"--format=esm",
"--target=es2024",
"--jsx=automatic",
'--define:process.env.NODE_ENV="production"',
"--metafile=" + metadata,
"--outfile=" + path.join(output, directory, "module.mjs"),
...sharedAliases,
...["svg", "png", "jpg", "jpeg", "gif", "webp", "avif", "woff", "woff2", "ttf", "ico"].map(
(extension) => "--loader:." + extension + "=dataurl",
),
],
{ cwd: work, stdio: "inherit" },
);
const meta = JSON.parse(await fs.readFile(metadata, "utf8")),
outputs = Object.entries(meta.outputs);
const main = outputs.find(([file]) => file.endsWith("/module.mjs"))?.[1];
if (!main?.exports.includes(entry.export)) throw Error("Component bundle lacks declared export");
if (
Object.keys(meta.inputs).some((file) =>
/camino-replica-engine|camino-replica-client|replicache|node_modules\/(?:react|react-dom|scheduler)(?:\/|$)/.test(
file,
),
)
)
throw Error("Component bundled an internal engine/client or duplicate React");
if (
outputs.some(([, value]) =>
value.imports.some((imported) => imported.external && !Object.values(imports).includes(imported.path)),
)
)
throw Error("Component has an undeclared external module or asset");
const files = [...sharedFiles];
for (const [file] of outputs) {
const absolute = path.resolve(work, file),
relative = path.relative(output, absolute);
compiler.artifactPath(relative);
if (!relative.startsWith(directory + "/")) throw Error("Component output escaped declared artifact closure");
files.push(await recordFile(relative, relative.endsWith(".css") ? "text/css" : "text/javascript"));
}
const body = {
id,
subject: declaration.subject,
compatibility: "org.quixos.react.esm/1",
path: directory + "/module.mjs",
exportName: entry.export,
sharedModules,
files,
};
components.push({ ...body, contentDigest: compiler.componentArtifactDigest(body) });
}
return components;
}
-95
View File
@@ -1,95 +0,0 @@
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);
const packageFile = path.join(work, "package.json");
const metadata = await fs.readFile(packageFile, "utf8").then(JSON.parse, (error) => {
if (error.code === "ENOENT") return {};
throw error;
});
await fs.writeFile(packageFile, JSON.stringify({ ...metadata, 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 extends true> = T;
type ObjectProps = Assert<Props extends object ? true : false>;
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),
);
-88
View File
@@ -1,88 +0,0 @@
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) });
}
}
-40
View File
@@ -1,40 +0,0 @@
import fs from "node:fs";
import crypto from "node:crypto";
const [schemaPath, packageRevisionId, bindingOutput, generatorPath, output] = process.argv.slice(2);
if (!schemaPath || !packageRevisionId || !bindingOutput || !generatorPath || !output)
throw new Error("Missing candidate check receipt inputs");
const canonical = (value) =>
Array.isArray(value)
? value.map(canonical)
: value && typeof value === "object"
? Object.fromEntries(
Object.entries(value)
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
.map(([key, entry]) => [key, canonical(entry)]),
)
: value;
const hash = (value) =>
`sha256:${crypto
.createHash("sha256")
.update(JSON.stringify(canonical(value)))
.digest("hex")}`;
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
if (!schema.packages.some((entry) => entry.revisionId === packageRevisionId))
throw new Error("Checked binding schema lacks the package");
const generated = fs.readFileSync(bindingOutput, "utf8");
if (generated !== fs.readFileSync(".qx-checked-bindings", "utf8"))
throw new Error("Build replaced candidate-generated bindings; its check is not evidence for this candidate");
const receipt = {
schemaVersion: 1,
packageRevisionId,
success: true,
bindingSchema: schema,
bindingSchemaDigest: hash(schema),
generatedDigest: hash(generated),
checkerDigest: hash({
generatorPath,
compiler: JSON.parse(fs.readFileSync("node_modules/typescript/package.json", "utf8")),
lock: fs.readFileSync("yarn.lock", "utf8"),
}),
};
fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: "wx" });
-61
View File
@@ -1,61 +0,0 @@
import fs from "node:fs/promises";
import path from "node:path";
import { createHash } from "node:crypto";
async function packages(directory) {
const result = new Map();
for (const entry of await fs.readdir(directory)) {
if (entry.startsWith(".")) continue;
if (entry.startsWith("@")) {
for (const name of await fs.readdir(path.join(directory, entry)))
result.set(`${entry}/${name}`, path.join(directory, entry, name));
} else result.set(entry, path.join(directory, entry));
}
return result;
}
async function packageDigest(directory) {
const digest = createHash("sha256");
async function visit(relative) {
for (const entry of (await fs.readdir(path.join(directory, relative), { withFileTypes: true })).sort((a, b) =>
a.name.localeCompare(b.name),
)) {
if (entry.name === "node_modules") continue;
const name = path.join(relative, entry.name),
file = path.join(directory, name);
if (entry.isDirectory()) await visit(name);
else {
const bytes = await fs.readFile(file);
digest.update(JSON.stringify([name, bytes.length]));
digest.update(bytes);
}
}
}
await visit("");
return digest.digest("hex");
}
/** Merge at package granularity: a scope such as @types is not a reserved package. */
export async function installDependencies({ destination, application, defaults = [], selected = {}, reserved = [] }) {
const entries = new Map();
const authored = new Set();
for (const root of defaults) for (const [name, file] of await packages(root)) entries.set(name, file);
for (const [name, file] of Object.entries(selected)) entries.set(name, file);
if (application)
for (const [name, file] of await packages(application)) {
if (reserved.includes(name)) {
const canonical = entries.get(name);
if (!canonical || (await packageDigest(file)) !== (await packageDigest(canonical)))
throw Error("Application dependencies cannot substitute selected React/client types or checked SDK: " + name);
} else {
entries.set(name, file);
authored.add(name);
}
}
for (const [name, file] of entries) {
await fs.mkdir(path.dirname(path.join(destination, name)), { recursive: true });
// Application packages resolve siblings from this assembled node_modules.
// Keep SDK links canonical: copying those would duplicate branded types and
// React declarations from their own retained dependency closures.
if (authored.has(name)) await fs.cp(file, path.join(destination, name), { recursive: true, dereference: true });
else await fs.symlink(file, path.join(destination, name));
}
}
-171
View File
@@ -1,171 +0,0 @@
/** Generated-service transport. The supervisor owns process isolation, deadlines and termination.
* stdout is reserved for this protocol; application diagnostics belong on stderr. */
export function servePortableRegistry(
registry,
{ maxBytes = 4 * 1024 * 1024, maxInvocations = 64, maxPortCalls = 10000 } = {},
) {
const abi = "quixos-transaction-ports-v1";
const active = new Map();
const pending = new Map();
let buffer = Buffer.alloc(0),
stopped = false;
const send = (message) => {
const encoded = JSON.stringify(message);
if (Buffer.byteLength(encoded) > maxBytes) throw Error("SERVICE_MESSAGE_LIMIT");
// A slow or absent coordinator must not cause an unbounded stdout queue.
if (process.stdout.writableLength + Buffer.byteLength(encoded) > maxBytes * 2) throw Error("SERVICE_BACKPRESSURE");
process.stdout.write(encoded + "\n");
};
const errorBody = (error) => ({
code: typeof error?.code === "string" ? error.code.slice(0, 1024) : "PACKAGE_ERROR",
message: String(error?.message ?? error).slice(0, 4096),
});
const rejectPorts = (id, reason) => {
for (const [key, call] of pending)
if (call.invocation === id) {
pending.delete(key);
call.reject(reason);
}
};
const cancel = (id, reason) => {
const token = active.get(id);
if (token) {
token.cancelled = true;
token.controller.abort(reason);
}
rejectPorts(id, reason);
};
const fatal = (error) => {
if (stopped) return;
stopped = true;
for (const id of active.keys()) cancel(id, error);
process.stderr.write(String(error?.message ?? error) + "\n");
process.exitCode = 1;
process.stdin.destroy();
};
const receive = (message) => {
if (!message || message.abi !== abi) throw Error("SERVICE_ABI_MISMATCH");
if (message.kind === "shutdown") {
for (const id of active.keys()) cancel(id, Error("SERVICE_SHUTDOWN"));
stopped = true;
process.stdin.destroy();
return;
}
if (message.kind === "cancel") {
const token = active.get(message.invocation);
if (token && token.frame !== message.frame) throw Error("INVALID_CANCELLATION_FRAME");
cancel(message.invocation, Error("INVOCATION_CANCELLED"));
// A completed result may already be in flight. Acknowledge without resurrecting its context.
if (!token) send({ abi, kind: "cancelled", invocation: message.invocation, frame: message.frame });
return;
}
if (message.kind === "port-result") {
const key = JSON.stringify([message.invocation, message.sequence]);
const call = pending.get(key);
if (!call && (!active.has(message.invocation) || active.get(message.invocation).cancelled)) return;
if (!call || message.frame !== call.frame) throw Error("UNEXPECTED_PORT_RESULT");
pending.delete(key);
if (message.status === "returned") call.resolve(message.output);
else if (message.status === "failed")
call.reject(
Object.assign(Error(String(message.error?.message ?? "Port failed")), {
code: String(message.error?.code ?? "PORT_FAILED"),
}),
);
else throw Error("INVALID_PORT_RESULT");
return;
}
if (
message.kind !== "invoke" ||
typeof message.invocation !== "string" ||
typeof message.frame !== "string" ||
!message.invocation ||
!message.frame ||
message.invocation.length > 4096 ||
message.frame.length > 4096
)
throw Error("INVALID_INVOCATION");
if (active.has(message.invocation) || active.size >= maxInvocations) throw Error("INVOCATION_LIMIT_OR_DUPLICATE");
const token = { frame: message.frame, controller: new AbortController(), cancelled: false };
active.set(message.invocation, token);
let sequence = 0;
const channel = (request) => {
if (active.get(message.invocation) !== token || token.cancelled)
return Promise.reject(Error("EXPIRED_INVOCATION"));
if (++sequence > maxPortCalls || pending.size >= maxPortCalls) return Promise.reject(Error("PORT_CALL_LIMIT"));
const key = JSON.stringify([message.invocation, sequence]);
return new Promise((resolve, reject) => {
pending.set(key, { invocation: message.invocation, frame: message.frame, resolve, reject });
try {
send({
abi,
kind: "port",
frame: message.frame,
invocation: message.invocation,
sequence,
portId: request.portId,
action: { ...request.action, ...(request.key === undefined ? {} : { key: request.key }) },
input: request.input,
});
} catch (error) {
pending.delete(key);
reject(error);
}
});
};
void registry
.invoke(
{ exportId: message.exportId, workspace: message.workspace, receiver: message.receiver, input: message.input },
channel,
Object.freeze({ signal: token.controller.signal, effect: message.effect }),
)
.then(
(output) => {
if (active.get(message.invocation) === token && !token.cancelled)
send({ abi, invocation: message.invocation, status: "returned", output });
},
(error) => {
if (active.get(message.invocation) === token && !token.cancelled)
send({ abi, invocation: message.invocation, status: "failed", error: errorBody(error) });
},
)
.catch(fatal)
.finally(() => {
if (active.get(message.invocation) === token) {
active.delete(message.invocation);
token.controller.abort(Error("INVOCATION_ENDED"));
rejectPorts(message.invocation, Error("INVOCATION_ENDED"));
if (token.cancelled && !stopped)
send({ abi, kind: "cancelled", invocation: message.invocation, frame: token.frame });
}
})
.catch(fatal);
};
process.stdin.on("data", (bytes) => {
if (stopped) return;
try {
// Consume each line before retaining the remainder; combined transport chunks may contain many frames.
let offset = 0;
while (offset < bytes.length) {
const newline = bytes.indexOf(10, offset);
const end = newline < 0 ? bytes.length : newline;
if (buffer.length + end - offset > maxBytes) throw Error("SERVICE_MESSAGE_LIMIT");
buffer = Buffer.concat([buffer, bytes.subarray(offset, end)]);
if (newline < 0) break;
receive(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffer)));
buffer = Buffer.alloc(0);
offset = newline + 1;
if (stopped) break;
}
} catch (error) {
fatal(error);
}
});
process.stdin.on("end", () => {
if (buffer.length) fatal(Error("TRUNCATED_SERVICE_MESSAGE"));
for (const id of active.keys()) cancel(id, Error("COORDINATOR_DISCONNECTED"));
});
process.stdin.on("error", fatal);
process.stdout.on("error", fatal);
send({ abi, kind: "ready", exports: registry.exports });
}
+318 -503
View File
@@ -19,25 +19,23 @@ let
schemaMainPathDefault = "dist/quixos-package-schema.js";
schemaPackageName = schemaPackageJson.name or "schema";
selfSchemaName =
if pkgs.lib.hasInfix "/" schemaPackageName then
pkgs.lib.last (pkgs.lib.splitString "/" schemaPackageName)
else
schemaPackageName;
if pkgs.lib.hasInfix "/" schemaPackageName
then pkgs.lib.last (pkgs.lib.splitString "/" schemaPackageName)
else schemaPackageName;
flakeText = builtins.replaceStrings [ "\n" "\r" "\t" ] [ " " " " " " ] (
builtins.readFile "${flakeRoot}/flake.nix"
);
flakeText =
builtins.replaceStrings
[ "\n" "\r" "\t" ]
[ " " " " " " ]
(builtins.readFile "${flakeRoot}/flake.nix");
getInputUrl =
inputName:
getInputUrl = inputName:
let
pattern1 = ".*" + inputName + "[[:space:]]*\\.url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*";
pattern2 =
".*" + inputName + "[[:space:]]*=[[:space:]]*\\{[^}]*url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*";
pattern2 = ".*" + inputName + "[[:space:]]*=[[:space:]]*\\{[^}]*url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*";
match1 = builtins.match pattern1 flakeText;
match2 = if match1 != null then match1 else builtins.match pattern2 flakeText;
in
if match2 == null then null else builtins.elemAt match2 0;
in if match2 == null then null else builtins.elemAt match2 0;
schemaBase =
(pkgs.callPackage "${schemaDir}/yarn-project.nix" {
@@ -67,61 +65,68 @@ let
'';
});
schemaInputs = pkgs.lib.filterAttrs (name: _: pkgs.lib.hasPrefix schemaPrefix name) inputs;
schemaInputs =
pkgs.lib.filterAttrs (name: _: pkgs.lib.hasPrefix schemaPrefix name) inputs;
decodeSchemaInputName = encodedName: builtins.replaceStrings [ "__" ] [ "." ] encodedName;
decodeSchemaInputName = encodedName:
builtins.replaceStrings [ "__" ] [ "." ] encodedName;
schemaPackagesFromInputs = pkgs.lib.mapAttrs' (
name: flake:
let
schemaName = decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
in
{
name = schemaName;
value = flake.packages.${system}.schema;
}
) schemaInputs;
schemaPackagesFromInputs =
pkgs.lib.mapAttrs'
(name: flake:
let
schemaName =
decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
in
{
name = schemaName;
value = flake.packages.${system}.schema;
})
schemaInputs;
schemaPackages = schemaPackagesFromInputs // {
"${selfSchemaName}" = schema;
};
schemaPackages = schemaPackagesFromInputs // { "${selfSchemaName}" = schema; };
schemaMetaByName = pkgs.lib.mapAttrs' (
name: flake:
let
schemaName = decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
sourceInfo = if flake ? sourceInfo then flake.sourceInfo else { };
sourceRev = sourceInfo.rev or null;
inputUrl = getInputUrl name;
urlBase = if inputUrl == null then null else builtins.head (pkgs.lib.splitString "?" inputUrl);
isDisallowed =
inputUrl == null
|| pkgs.lib.hasPrefix "path:" inputUrl
|| pkgs.lib.hasPrefix "file:" inputUrl
|| pkgs.lib.hasPrefix "git+file:" inputUrl;
flakeRef =
if isDisallowed || sourceRev == null || urlBase == null then
null
else
urlBase + "?rev=" + sourceRev;
meta = {
name = "@quixos-package-schemas/${schemaName}";
type = sourceInfo.type or null;
url = inputUrl;
rev = sourceRev;
flakeRef = flakeRef;
};
in
if flakeRef == null then
throw "Schema input ${schemaName} must be a git flake with url+rev"
else
{
name = schemaName;
value = meta;
}
) schemaInputs;
schemaMetaByName =
pkgs.lib.mapAttrs'
(name: flake:
let
schemaName =
decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
sourceInfo = if flake ? sourceInfo then flake.sourceInfo else { };
sourceRev = sourceInfo.rev or null;
inputUrl = getInputUrl name;
urlBase =
if inputUrl == null
then null
else builtins.head (pkgs.lib.splitString "?" inputUrl);
isDisallowed =
inputUrl == null
|| pkgs.lib.hasPrefix "path:" inputUrl
|| pkgs.lib.hasPrefix "file:" inputUrl
|| pkgs.lib.hasPrefix "git+file:" inputUrl;
flakeRef =
if isDisallowed || sourceRev == null || urlBase == null
then null
else urlBase + "?rev=" + sourceRev;
meta =
{
name = "@quixos-package-schemas/${schemaName}";
type = sourceInfo.type or null;
url = inputUrl;
rev = sourceRev;
flakeRef = flakeRef;
};
in
if flakeRef == null
then throw "Schema input ${schemaName} must be a git flake with url+rev"
else {
name = schemaName;
value = meta;
})
schemaInputs;
schemaMetaJsonByName = pkgs.lib.mapAttrs (_: meta: builtins.toJSON meta) schemaMetaByName;
schemaMetaJsonByName =
pkgs.lib.mapAttrs (_: meta: builtins.toJSON meta) schemaMetaByName;
schemaExtensionsBlock = pkgs.lib.concatStringsSep "\n" (
[
@@ -146,11 +151,12 @@ let
);
schemaInstallCommands = pkgs.lib.concatStringsSep "\n" (
pkgs.lib.mapAttrsToList (
name: drv:
pkgs.lib.mapAttrsToList (name: drv:
let
metaJson =
if pkgs.lib.hasAttr name schemaMetaJsonByName then schemaMetaJsonByName.${name} else null;
if pkgs.lib.hasAttr name schemaMetaJsonByName
then schemaMetaJsonByName.${name}
else null;
in
''
schema_pkg="$(ls -d ${drv}/libexec/* | head -n1)"
@@ -160,47 +166,46 @@ let
chmod -R u+w "$dest"
''
+ (
if metaJson != null then
''
schema_main="${schemaMainPathDefault}"
if [ -f "$dest/$schema_main" ]; then
block_tmp="$(mktemp -p "$dest")"
tmp_out="$(mktemp -p "$dest")"
cat > "$block_tmp" <<'EOF'
packageSchema.__quixos = ${metaJson};
EOF
chmod u+w "$dest/$schema_main"
if grep -q '^export default ' "$dest/$schema_main"; then
awk -v blockFile="$block_tmp" '
BEGIN {
while ((getline line < blockFile) > 0) {
block = block line "\n"
}
}
!inserted && /^export default / { printf "%s", block; inserted=1 }
{ print }
' "$dest/$schema_main" > "$tmp_out"
cat "$tmp_out" > "$dest/$schema_main"
elif grep -q '^//# sourceMappingURL=' "$dest/$schema_main"; then
awk -v blockFile="$block_tmp" '
BEGIN {
while ((getline line < blockFile) > 0) {
block = block line "\n"
}
}
/^\/\/# sourceMappingURL=/ { printf "%s", block; print; next }
{ print }
' "$dest/$schema_main" > "$tmp_out"
cat "$tmp_out" > "$dest/$schema_main"
else
printf '\n' >> "$dest/$schema_main"
cat "$block_tmp" >> "$dest/$schema_main"
fi
rm -f "$block_tmp" "$tmp_out"
fi
''
else
""
if metaJson != null
then ''
schema_main="${schemaMainPathDefault}"
if [ -f "$dest/$schema_main" ]; then
block_tmp="$(mktemp -p "$dest")"
tmp_out="$(mktemp -p "$dest")"
cat > "$block_tmp" <<'EOF'
packageSchema.__quixos = ${metaJson};
EOF
chmod u+w "$dest/$schema_main"
if grep -q '^export default ' "$dest/$schema_main"; then
awk -v blockFile="$block_tmp" '
BEGIN {
while ((getline line < blockFile) > 0) {
block = block line "\n"
}
}
!inserted && /^export default / { printf "%s", block; inserted=1 }
{ print }
' "$dest/$schema_main" > "$tmp_out"
cat "$tmp_out" > "$dest/$schema_main"
elif grep -q '^//# sourceMappingURL=' "$dest/$schema_main"; then
awk -v blockFile="$block_tmp" '
BEGIN {
while ((getline line < blockFile) > 0) {
block = block line "\n"
}
}
/^\/\/# sourceMappingURL=/ { printf "%s", block; print; next }
{ print }
' "$dest/$schema_main" > "$tmp_out"
cat "$tmp_out" > "$dest/$schema_main"
else
printf '\n' >> "$dest/$schema_main"
cat "$block_tmp" >> "$dest/$schema_main"
fi
rm -f "$block_tmp" "$tmp_out"
fi
''
else ""
)
) schemaPackages
);
@@ -235,8 +240,7 @@ let
schemaInstallCommands
updateYarnrcScript
schemaExtensionsBlockEscaped
selfSchemaName
;
selfSchemaName;
};
mkTsPackageServer =
@@ -261,16 +265,15 @@ let
nodejs' = if nodejs != null then nodejs else pkgs.nodejs_24;
git' = if git != null then git else pkgs.git;
templatePreparePackages' =
if templatePreparePackages != null then
templatePreparePackages
else
[
pkgs.findutils
git'
pkgs.gnused
nodejs'
pkgs.yarn-berry_4
];
if templatePreparePackages != null
then templatePreparePackages
else [
pkgs.findutils
git'
pkgs.gnused
nodejs'
pkgs.yarn-berry_4
];
packageJson = builtins.fromJSON (builtins.readFile "${srcDir}/package.json");
packageName = packageJson.name or "quixos-package";
@@ -288,33 +291,28 @@ let
runtimeBinPath = pkgs.lib.makeBinPath extraRuntimePackages;
runtimeLibPath = pkgs.lib.makeLibraryPath extraRuntimePackages;
runtimeWrap =
if extraRuntimePackages == [ ] then
""
else
''
wrapProgram "$out/bin/${serverBin}" \
--prefix PATH : ${runtimeBinPath} \
--prefix LD_LIBRARY_PATH : ${runtimeLibPath}
'';
if extraRuntimePackages == [ ]
then ""
else ''
wrapProgram "$out/bin/${serverBin}" \
--prefix PATH : ${runtimeBinPath} \
--prefix LD_LIBRARY_PATH : ${runtimeLibPath}
'';
server = base.overrideAttrs (
old:
server = base.overrideAttrs (old:
let
extraAttrs = serverOverrides old;
in
{
buildInputs = (old.buildInputs or [ ]) ++ extraBuildInputs;
nativeBuildInputs =
(old.nativeBuildInputs or [ ])
++ [
pkgs.python3
pkgs.gnumake
pkgs.gcc
pkgs.pkg-config
pkgs.makeWrapper
]
++ extraNativeBuildInputs;
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
pkgs.python3
pkgs.gnumake
pkgs.gcc
pkgs.pkg-config
pkgs.makeWrapper
] ++ extraNativeBuildInputs;
# node-gyp will look at these
PYTHON = "${pkgs.python3}/bin/python3";
@@ -328,14 +326,13 @@ let
buildPhase =
(old.buildPhase or "")
+ (
if buildCommand != null then
''
runHook preBuildQuixos
${buildCommand}
runHook postBuildQuixos
''
else
""
if buildCommand != null
then ''
runHook preBuildQuixos
${buildCommand}
runHook postBuildQuixos
''
else ""
);
postFixup = (old.postFixup or "") + runtimeWrap;
@@ -345,8 +342,7 @@ let
mkdir -p "$pkg_root/quixos-package-schemas"
(cd "$pkg_root" && ${schemaSupport.schemaInstallCommands})
'';
}
// extraAttrs
} // extraAttrs
);
check = server.overrideAttrs (old: {
@@ -364,12 +360,7 @@ let
};
in
{
inherit
server
check
packageName
serverBin
;
inherit server check packageName serverBin;
devShells = {
"quixos-prepare" = prepareShell;
@@ -401,18 +392,13 @@ let
pkgs = import nixpkgs { inherit system; };
schemaSupport = mkSchemaSupport {
inherit
pkgs
inputs
system
schemaDir
schemaPrefix
self
flakeRoot
;
inherit pkgs inputs system schemaDir schemaPrefix self flakeRoot;
};
normalizeList = value: if builtins.isFunction value then value pkgs else value;
normalizeList = value:
if builtins.isFunction value
then value pkgs
else value;
serverResult = serverBuilder {
inherit pkgs schemaSupport;
@@ -422,75 +408,68 @@ let
};
combinedName = pkgs.lib.strings.sanitizeDerivationName (
if packageName != null then
packageName
else if serverResult ? packageName then
serverResult.packageName
else
"quixos-package"
if packageName != null
then packageName
else if serverResult ? packageName
then serverResult.packageName
else "quixos-package"
);
packageServer = serverResult.server;
packageServerCheck = if serverResult ? check then serverResult.check else null;
appProgramFinal =
if appProgram != null then
appProgram
else if serverResult ? appProgram then
serverResult.appProgram
else
"${packageServer}/bin/${serverResult.serverBin or "server"}";
if appProgram != null
then appProgram
else if serverResult ? appProgram
then serverResult.appProgram
else "${packageServer}/bin/${serverResult.serverBin or "server"}";
devShellHookBase =
if enableYarnrcHook then
''
find_schema_root() {
dir="$PWD"
while [ "$dir" != "/" ]; do
if [ -f "$dir/src/.yarnrc.yml" ] && [ -d "$dir/schema" ]; then
printf "%s\n" "$dir"
return 0
fi
dir="$(dirname "$dir")"
done
return 1
}
${schemaSupport.updateYarnrcScript}
link_schemas() {
mkdir -p quixos-package-schemas
${schemaSupport.schemaLinkCommands}
}
base="$(find_schema_root || true)"
if [ -n "$base" ]; then
(cd "$base/src" && link_schemas && quixos_package_schemas_update_yarnrc)
if [ -d "$base/schema" ]; then
ln -sfn "$base/schema" "$base/src/quixos-package-schemas/${schemaSupport.selfSchemaName}"
if enableYarnrcHook
then ''
find_schema_root() {
dir="$PWD"
while [ "$dir" != "/" ]; do
if [ -f "$dir/src/.yarnrc.yml" ] && [ -d "$dir/schema" ]; then
printf "%s\n" "$dir"
return 0
fi
dir="$(dirname "$dir")"
done
return 1
}
${schemaSupport.updateYarnrcScript}
link_schemas() {
mkdir -p quixos-package-schemas
${schemaSupport.schemaLinkCommands}
}
base="$(find_schema_root || true)"
if [ -n "$base" ]; then
(cd "$base/src" && link_schemas && quixos_package_schemas_update_yarnrc)
if [ -d "$base/schema" ]; then
ln -sfn "$base/schema" "$base/src/quixos-package-schemas/${schemaSupport.selfSchemaName}"
fi
''
else
"";
fi
''
else "";
devShellHook = devShellHookBase + extraDevShellHook;
devShellPackages = normalizeList extraDevShellPackages;
checks = {
schema = schemaSupport.schemaCheck;
}
// (if packageServerCheck != null then { default = packageServerCheck; } else { });
checks =
{ schema = schemaSupport.schemaCheck; }
// (if packageServerCheck != null then { default = packageServerCheck; } else { });
extraDevShells = if serverResult ? devShells then serverResult.devShells else { };
in
{
packages.default = pkgs.symlinkJoin {
name = combinedName;
paths = [
packageServer
schemaSupport.schema
];
paths = [ packageServer schemaSupport.schema ];
};
packages.server = packageServer;
packages.schema = schemaSupport.schema;
@@ -531,29 +510,6 @@ let
'';
};
# Language-neutral, offline schema compilation. Snapshot directories must be
# fixed Nix inputs matching the resource lock's complete dependency closure.
mkQxBindingSchema =
{
pkgs,
protocol,
src,
repository,
commit,
resources ? [ ],
}:
let
snapshots = pkgs.writeText "qx-binding-snapshots.json" (builtins.toJSON { inherit resources; });
in
pkgs.runCommand "qx-binding-schema.json" { } ''
${protocol}/bin/quixos-resource-compile \
--root ${src} --kind package \
--repository ${pkgs.lib.escapeShellArg repository} \
--commit ${pkgs.lib.escapeShellArg commit} \
--checkout-root "$TMPDIR/checkouts" \
--snapshot-map ${snapshots} --snapshot-only true --schema-out "$out" > /dev/null
'';
mkCaminoTsYarnNixifyFlake =
{
inputs,
@@ -565,13 +521,8 @@ let
promptName ? null,
nodejsAttr ? "nodejs_24",
buildCommand ? "yarn build",
sourcePortals ? { },
buildEnv ? { },
# An exact, compiler-produced BindingSchema JSON artifact and a backend.
# Other language helpers can consume the same schema with their own generator/runtime.
bindings ? null,
# A dedicated entrypoint calling SDK serveMigration; never start the
# normal package server in the isolated migration execution boundary.
migrationEntrypoint ? null,
nativeBuildInputs ? [ ],
devShellPackages ? [ ],
devShellHook ? "",
@@ -581,284 +532,148 @@ let
flake-utils.lib.eachDefaultSystem (
system:
let
outputsFor =
candidateBindings:
let
pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib;
nodejs = pkgs.${nodejsAttr};
pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib;
nodejs = pkgs.${nodejsAttr};
callOption =
value: if builtins.isFunction value then value { inherit inputs pkgs system; } else value;
callOption = value:
if builtins.isFunction value
then value { inherit inputs pkgs system; }
else value;
packageJson = builtins.fromJSON (builtins.readFile "${packageRoot}/package.json");
packageNameFinal =
if packageName != null then packageName else packageJson.name or "quixos-package";
promptNameFinal =
if promptName != null then promptName else lib.strings.sanitizeDerivationName packageNameFinal;
packageJson = builtins.fromJSON (builtins.readFile "${packageRoot}/package.json");
packageNameFinal = if packageName != null then packageName else packageJson.name or "quixos-package";
promptNameFinal =
if promptName != null
then promptName
else lib.strings.sanitizeDerivationName packageNameFinal;
sourcePackage = mkCaminoSourcePackage {
inherit pkgs;
src = packageRoot;
name = sourceName;
};
sourcePackage = mkCaminoSourcePackage {
inherit pkgs;
src = packageRoot;
name = sourceName;
};
exportsFor =
attrs:
lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: value: "export ${name}=${lib.escapeShellArg (toString value)}") attrs
);
exportsFor = attrs:
lib.concatStringsSep "\n" (
lib.mapAttrsToList
(name: value: "export ${name}=${lib.escapeShellArg (toString value)}")
attrs
);
bindingConfig =
if candidateBindings != null then
candidateBindings
else if bindings == null then
null
else
callOption bindings;
bindingSchema =
if bindingConfig == null then
null
else
bindingConfig.schema or (mkQxBindingSchema {
inherit pkgs;
protocol = bindingConfig.generator;
src = packageRoot;
inherit (bindingConfig) repository commit;
resources = bindingConfig.resources or [ ];
});
bindingOutput =
if bindingConfig == null then "src/gen/qx.ts" else bindingConfig.output or "src/gen/qx.ts";
authoringCheck = builtins.fromJSON (builtins.readFile "${packageRoot}/quixos.check.json");
bindingOptionsFile =
if bindingConfig == null then
null
else
pkgs.writeText "qx-typescript-options.json" (builtins.toJSON (authoringCheck.options or { }));
bindingCommand =
if bindingConfig == null then
""
else
''
mkdir -p ${lib.escapeShellArg (dirOf bindingOutput)}
${bindingConfig.generator}/bin/quixos-codegen-ts \
${lib.escapeShellArg (toString bindingSchema)} \
${lib.escapeShellArg bindingConfig.packageRevisionId} \
${lib.escapeShellArg bindingOutput} ${bindingOptionsFile}
${lib.optionalString (installServer != null && descriptorPath != null) ''
${bindingConfig.generator}/bin/quixos-qx package-descriptor ${lib.escapeShellArg (toString bindingSchema)} \
${lib.escapeShellArg bindingConfig.packageRevisionId} > ${lib.escapeShellArg descriptorPath}
''}
'';
generateBindings = pkgs.writeShellApplication {
name = "qx-generate-bindings";
text = bindingCommand;
};
portalLinksFor = attrs:
lib.concatStringsSep "\n" (
lib.mapAttrsToList
(target: source: "ln -sfn ${source} ${lib.escapeShellArg target}")
attrs
);
bundleConfig = if bundle == null then { } else bundle;
bundleOutfile = bundleConfig.outfile or "server.mjs";
bundlePlatform = bundleConfig.platform or "node";
bundleTarget = bundleConfig.target or "node24";
bundleFormat = bundleConfig.format or "esm";
bundleBanner = bundleConfig.banner or nodeRequireBanner;
bundleAliases =
bundleConfig.aliases or {
"@automerge/automerge" =
"./node_modules/@automerge/automerge/dist/mjs/entrypoints/fullfat_base64.js";
};
bundleAliasArgs = lib.concatStringsSep " " (
lib.mapAttrsToList (from: to: "--alias:${from}=${lib.escapeShellArg to}") bundleAliases
);
nodeRequireBanner = "import { createRequire } from 'module';const require = createRequire(import.meta.url);";
bundleCommand =
if bundle == null then
""
else if bundleConfig.browserSources or false then
''
${nodejs}/bin/node ${./browser-source.mjs} ${
lib.escapeShellArg (
builtins.toJSON {
entryPoints = [ bundleConfig.entry ];
bundle = true;
platform = bundlePlatform;
target = bundleTarget;
format = bundleFormat;
alias = bundleAliases;
preserveSymlinks = bundleConfig.preserveSymlinks or true;
banner.js = bundleBanner;
outfile = bundleOutfile;
}
)
}
''
else
''
esbuild ${lib.escapeShellArg bundleConfig.entry} \
--bundle \
--platform=${bundlePlatform} \
--target=${bundleTarget} \
--format=${bundleFormat} \
${bundleAliasArgs} \
${lib.optionalString (bundleConfig.preserveSymlinks or true) "--preserve-symlinks \\"}
--banner:js=${lib.escapeShellArg bundleBanner} \
--outfile=${lib.escapeShellArg bundleOutfile}
'';
bundleConfig = if bundle == null then { } else bundle;
bundleOutfile = bundleConfig.outfile or "server.mjs";
bundlePlatform = bundleConfig.platform or "node";
bundleTarget = bundleConfig.target or "node24";
bundleFormat = bundleConfig.format or "esm";
bundleBanner = bundleConfig.banner or nodeRequireBanner;
nodeRequireBanner = "import { createRequire } from 'module';const require = createRequire(import.meta.url);";
bundleCommand =
if bundle == null
then ""
else ''
esbuild ${lib.escapeShellArg bundleConfig.entry} \
--bundle \
--platform=${bundlePlatform} \
--target=${bundleTarget} \
--format=${bundleFormat} \
${lib.optionalString (bundleConfig.preserveSymlinks or true) "--preserve-symlinks \\"}
--banner:js=${lib.escapeShellArg bundleBanner} \
--outfile=${lib.escapeShellArg bundleOutfile}
'';
installConfig = if installServer == null then { } else installServer;
serverBin = installConfig.binName or "server";
serverLibexecName =
installConfig.libexecName or (lib.strings.sanitizeDerivationName packageNameFinal);
serverFile = installConfig.serverFile or bundleOutfile;
descriptorPath = installConfig.descriptorPath or "descriptor.quixos-package.txtpb";
extraFiles = installConfig.extraFiles or [ ];
installExtraFile =
file:
let
source = toString file.source;
target = file.target or (baseNameOf source);
mode = file.mode or "0644";
in
''
install -Dm${toString mode} ${lib.escapeShellArg source} "$out/libexec/${serverLibexecName}/${target}"
'';
installServerPhase =
if installServer == null then
null
else
''
runHook preInstall
install -Dm755 ${lib.escapeShellArg serverFile} "$out/libexec/${serverLibexecName}/${serverFile}"
${lib.concatMapStringsSep "\n" installExtraFile extraFiles}
mkdir -p "$out/bin"
cat > "$out/bin/${serverBin}" <<EOF
#!${pkgs.runtimeShell}
exec ${nodejs}/bin/node "$out/libexec/${serverLibexecName}/${serverFile}" "\$@"
EOF
chmod +x "$out/bin/${serverBin}"
${lib.optionalString (installConfig ? descriptorPath && descriptorPath != null) ''
cp ${lib.escapeShellArg descriptorPath} "$out/${descriptorPath}"
''}
${lib.optionalString (bindingConfig != null) ''
install -m 0444 quixos-check.json "$out/quixos-check.json"
''}
${lib.optionalString (migrationEntrypoint != null) ''
install -Dm444 migration.mjs "$out/libexec/${serverLibexecName}/migration.mjs"
cat > "$out/bin/migrate" <<EOF
#!${pkgs.runtimeShell}
exec ${nodejs}/bin/node --max-old-space-size=256 "$out/libexec/${serverLibexecName}/migration.mjs" "\$@"
EOF
chmod +x "$out/bin/migrate"
''}
runHook postInstall
'';
installConfig = if installServer == null then { } else installServer;
serverBin = installConfig.binName or "server";
serverLibexecName =
installConfig.libexecName or (lib.strings.sanitizeDerivationName packageNameFinal);
serverFile = installConfig.serverFile or bundleOutfile;
descriptorPath = installConfig.descriptorPath or "descriptor.quixos-package.txtpb";
installServerPhase =
if installServer == null
then null
else ''
runHook preInstall
install -Dm755 ${lib.escapeShellArg serverFile} "$out/libexec/${serverLibexecName}/${serverFile}"
mkdir -p "$out/bin"
cat > "$out/bin/${serverBin}" <<EOF
#!${pkgs.runtimeShell}
exec ${nodejs}/bin/node "$out/libexec/${serverLibexecName}/${serverFile}" "\$@"
EOF
chmod +x "$out/bin/${serverBin}"
${lib.optionalString (installConfig ? descriptorPath && descriptorPath != null) ''
cp ${lib.escapeShellArg descriptorPath} "$out/${descriptorPath}"
''}
runHook postInstall
'';
project = (pkgs.callPackage "${packageRoot}/yarn-project.nix" { inherit nodejs; }) {
src = packageRoot;
overrideAttrs =
old:
{
nativeBuildInputs =
(old.nativeBuildInputs or [ ])
++ lib.optional (bundle != null || migrationEntrypoint != null) pkgs.esbuild
++ callOption nativeBuildInputs;
buildPhase = ''
runHook preBuild
${exportsFor (callOption buildEnv)}
${bindingCommand}
${lib.optionalString (
bindingConfig != null && bundle != null
) "${bindingConfig.generator}/bin/quixos-qx bundle-policy src"}
${lib.optionalString (bindingConfig != null) "yarn exec tsc --noEmit"}
${lib.optionalString (
bindingConfig != null
) "cp ${lib.escapeShellArg bindingOutput} .qx-checked-bindings"}
${buildCommand}
${lib.optionalString (bindingConfig != null) ''
yarn exec tsc --noEmit
node ${./check-receipt.mjs} ${lib.escapeShellArg (toString bindingSchema)} \
${lib.escapeShellArg bindingConfig.packageRevisionId} ${lib.escapeShellArg bindingOutput} \
${lib.escapeShellArg (toString bindingConfig.generator)} quixos-check.json
''}
${bundleCommand}
${lib.optionalString (migrationEntrypoint != null) ''
esbuild ${lib.escapeShellArg migrationEntrypoint} --bundle --platform=node --target=node24 --format=esm \
${bundleAliasArgs} --preserve-symlinks --banner:js=${lib.escapeShellArg nodeRequireBanner} --outfile=migration.mjs
''}
runHook postBuild
'';
}
// lib.optionalAttrs (installServerPhase != null) {
installPhase = installServerPhase;
};
};
devShellHookBase = ''
if [ -n "''${PS1-}" ]; then
if [ -n "''${QX_DEV_SHELL_PROMPT-}" ]; then
PS1="''${PS1#\[qx:''${QX_DEV_SHELL_PROMPT}\] }"
fi
export QX_DEV_SHELL_PROMPT=${lib.escapeShellArg promptNameFinal}
PS1="[qx:${promptNameFinal}] $PS1"
fi
project = (pkgs.callPackage "${packageRoot}/yarn-project.nix" { inherit nodejs; }) {
src = packageRoot;
overrideAttrs = old: {
nativeBuildInputs =
(old.nativeBuildInputs or [ ])
++ lib.optional (bundle != null) pkgs.esbuild
++ callOption nativeBuildInputs;
preConfigure = (old.preConfigure or "") + ''
${portalLinksFor (callOption sourcePortals)}
'';
buildPhase = ''
runHook preBuild
${exportsFor (callOption buildEnv)}
${buildCommand}
${bundleCommand}
runHook postBuild
'';
} // lib.optionalAttrs (installServerPhase != null) {
installPhase = installServerPhase;
};
};
maybeServerOutputs =
if installServer == null then
{ }
else
{
packages.server = project;
apps.default = {
type = "app";
program = "${project}/bin/${serverBin}";
};
};
in
{
packages.default = project;
packages.source = sourcePackage;
devShells.default = pkgs.mkShell {
packages = [
nodejs
project.yarn-freestanding
]
++ lib.optional (bindingConfig != null) generateBindings
++ callOption devShellPackages;
# Explicit command keeps shell entry free of source mutations.
shellHook = devShellHookBase + callOption devShellHook;
devShellHookBase = ''
if [ -n "''${PS1-}" ]; then
if [ -n "''${QX_DEV_SHELL_PROMPT-}" ]; then
PS1="''${PS1#\[qx:''${QX_DEV_SHELL_PROMPT}\] }"
fi
export QX_DEV_SHELL_PROMPT=${lib.escapeShellArg promptNameFinal}
PS1="[qx:${promptNameFinal}] $PS1"
fi
'';
maybeServerOutputs =
if installServer == null
then { }
else {
packages.server = project;
apps.default = {
type = "app";
program = "${project}/bin/${serverBin}";
};
}
// maybeServerOutputs;
};
in
(outputsFor null)
// {
# The workspace supplies a compiler-produced schema for the exact
# candidate graph. Standalone builds may use checked-in authoring types,
# but only this build path regenerates and witnesses candidate contracts.
quixosPackages.checkedServer =
{
schema,
generator,
packageRevisionId,
}:
(outputsFor { inherit schema generator packageRevisionId; }).packages.server;
}
{
packages.default = project;
packages.source = sourcePackage;
devShells.default = pkgs.mkShell {
packages = [
nodejs
pkgs.yarn-berry_4
] ++ callOption devShellPackages;
shellHook = devShellHookBase + callOption devShellHook;
};
} // maybeServerOutputs
);
in
{
mkCaminoReactPackage = import ./react-package.nix;
mkCaminoReactContracts = import ./react-contracts.nix;
mkCaminoYarnDependencies = import ./yarn-dependencies.nix;
mkCaminoYarnShell = import ./yarn-shell.nix;
mkCaminoTypeScriptPackage = import ./typescript-package.nix;
mkCaminoTypeScriptService = import ./typescript-service.nix;
assembleCaminoPackage = import ./assemble-package.nix;
inherit
mkQuixosPackageFlake
mkTsPackageServer
mkSchemaSupport
mkCaminoSourcePackage
mkQxBindingSchema
mkCaminoTsYarnNixifyFlake
;
mkCaminoTsYarnNixifyFlake;
}
-21
View File
@@ -1,21 +0,0 @@
{
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}
''
-34
View File
@@ -1,34 +0,0 @@
{
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; }
-41
View File
@@ -1,41 +0,0 @@
{
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; }
-30
View File
@@ -1,30 +0,0 @@
{
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; }
-29
View File
@@ -1,29 +0,0 @@
{
pkgs,
src,
nodejs ? pkgs.nodejs_24,
}:
let
project = (pkgs.callPackage (src + "/yarn-project.nix") { inherit nodejs; }) {
inherit src;
overrideAttrs = _: {
buildPhase = "runHook preBuild; runHook postBuild";
installPhase = ''
runHook preInstall
test -d node_modules
mkdir -p "$out"
cp -a node_modules/. "$out/"
runHook postInstall
'';
};
};
in
project
// {
devShell = pkgs.mkShell {
packages = [
nodejs
project.yarn-freestanding
];
};
}
-22
View File
@@ -1,22 +0,0 @@
{ pkgs }:
let
yarnCli = pkgs.fetchurl {
url = "https://repo.yarnpkg.com/4.18.0/packages/yarnpkg-cli/bin/yarn.js";
hash = "sha512-/Lhxb+fNDuzhQf/Bi5IZOp35IEwbqDGJwoiDUiP8C75kr0c7qw1emSen2utcryuwfrJ4fMkzjKBA6hJfKh8vfg==";
};
yarn = pkgs.writeShellScriptBin "yarn" ''
exec ${pkgs.nodejs_24}/bin/node ${yarnCli} "$@"
'';
in
pkgs.mkShell {
packages = [
pkgs.nodejs_24
yarn
];
shellHook = ''
if [ ! -f .yarn/plugins/yarn-plugin-nixify.cjs ]; then
mkdir -p .yarn/plugins
cp ${./.yarn/plugins/yarn-plugin-nixify.cjs} .yarn/plugins/yarn-plugin-nixify.cjs
fi
'';
}