Compare commits
43 Commits
81e55657d9
...
exported
| Author | SHA1 | Date | |
|---|---|---|---|
| db5e4a4aa8 | |||
| f34ce87f55 | |||
| 2a7034fdd2 | |||
| 785c54992f | |||
| 0da022e216 | |||
| 85734e3003 | |||
| 0d7d5daea6 | |||
| daa20ca79f | |||
| 95c9c3db7a | |||
| 66512e84bb | |||
| ca0efe7252 | |||
| 9257198eec | |||
| f352ac0c3b | |||
| e2747066ca | |||
| 0c3c9c6ad8 | |||
| b3d31b37b3 | |||
| 6f3814587f | |||
| 1c09bf43c2 | |||
| 6657af7ca0 | |||
| e3080467c9 | |||
| e84b62f4d6 | |||
| 06c668b587 | |||
| 56fa26acc8 | |||
| cf7945abb6 | |||
| 74b6ff2ace | |||
| 5861e91298 | |||
| d109410617 | |||
| 8b2a224698 | |||
| 80b65b6f0c | |||
| 32652d5279 | |||
| 7e4fb60155 | |||
| b6cd2f3cd7 | |||
| 2f66f25153 | |||
| 2ee30c177c | |||
| 7177130c03 | |||
| 1965741625 | |||
| 220aaaa08c | |||
| 56bbcd22d1 | |||
| bb57488dbc | |||
| 2413926ad8 | |||
| 1f0c39b015 | |||
| fc0fdb09a4 | |||
| f85b2e3d7b |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos.git",
|
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
|
||||||
"sourceCommit": "eb407e5a3bbd4ae2f810a0cee0d45b5c7e8d7db1",
|
"sourceCommit": "54e443af5dd963f1fa6f83f062f4943b46ad2535",
|
||||||
"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"
|
||||||
|
|||||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,74 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# 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}
|
||||||
|
''
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
import { buildService } from "./build-service.mjs";
|
||||||
|
|
||||||
|
const config = JSON.parse(await fs.readFile(process.argv[2], "utf8"));
|
||||||
|
const compiler = await import(pathToFileURL(config.compiler));
|
||||||
|
const plan = JSON.parse(await fs.readFile(config.plan, "utf8"));
|
||||||
|
const pkg = plan.definition.packages.find((pkg) => pkg.revision === config.packageRevisionId);
|
||||||
|
if (!pkg || compiler.hexDigest(plan.definition) !== plan.digest) throw Error("Invalid package assembly context");
|
||||||
|
const output = process.env.out;
|
||||||
|
if (!output) throw Error("Package assembly requires an output directory");
|
||||||
|
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||||
|
const generatorDigest = digest(await fs.readFile(config.compiler));
|
||||||
|
const implementations = [],
|
||||||
|
components = [],
|
||||||
|
files = new Map(),
|
||||||
|
ids = new Set();
|
||||||
|
const recordFile = async (name, mediaType) => {
|
||||||
|
const bytes = await fs.readFile(path.join(output, compiler.artifactPath(name)));
|
||||||
|
return { path: name, bytes: bytes.length, digest: digest(bytes), mediaType };
|
||||||
|
};
|
||||||
|
await fs.mkdir(path.join(output, "share/quixos/generated"), { recursive: true });
|
||||||
|
for (const root of config.groups) {
|
||||||
|
const manifest = compiler.parsePackageArtifacts(
|
||||||
|
JSON.parse(await fs.readFile(path.join(root, "share/quixos/package-artifacts.json"), "utf8")),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
manifest.packageRevisionId !== pkg.revision ||
|
||||||
|
manifest.bindingDigest !== pkg.bindingDigest ||
|
||||||
|
manifest.sourceDigest !== pkg.sourceDigest ||
|
||||||
|
JSON.stringify(manifest.versions) !== JSON.stringify(plan.definition.versions)
|
||||||
|
)
|
||||||
|
throw Error("Artifact group belongs to a different checked package");
|
||||||
|
const receipt = JSON.parse(await fs.readFile(path.join(root, "share/quixos/build-receipt.json"), "utf8"));
|
||||||
|
const expected = compiler.expectedArtifactReceipt(plan, manifest, generatorDigest);
|
||||||
|
if (JSON.stringify(receipt) !== JSON.stringify(expected.receipt))
|
||||||
|
throw Error("Artifact group has an invalid build receipt");
|
||||||
|
const read = await compiler.artifactFileReader({ [pkg.revision]: root });
|
||||||
|
for (const artifact of [...manifest.implementations, ...(manifest.components ?? [])]) {
|
||||||
|
const key = `${artifact.compatibility === "org.quixos.react.esm/1" ? "module" : "implementation"}:${artifact.id}`;
|
||||||
|
if (ids.has(key)) throw Error("Duplicate artifact group export " + key);
|
||||||
|
ids.add(key);
|
||||||
|
for (const file of artifact.files) {
|
||||||
|
const bytes = await read(pkg.revision, file.path, file.bytes);
|
||||||
|
if (bytes.length !== file.bytes || digest(bytes) !== file.digest)
|
||||||
|
throw Error("Modified group asset " + file.path);
|
||||||
|
const previous = files.get(file.path);
|
||||||
|
if (previous && JSON.stringify(previous) !== JSON.stringify(file))
|
||||||
|
throw Error("Conflicting group asset " + file.path);
|
||||||
|
if (!previous) {
|
||||||
|
await fs.mkdir(path.dirname(path.join(output, file.path)), { recursive: true });
|
||||||
|
await fs.writeFile(path.join(output, file.path), bytes, { mode: file.path.startsWith("bin/") ? 0o755 : 0o644 });
|
||||||
|
files.set(file.path, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [name, bytes] of expected.sources) {
|
||||||
|
const actual = await fs.readFile(path.join(root, name));
|
||||||
|
if (!Buffer.from(bytes).equals(actual)) throw Error("Modified group type witness " + name);
|
||||||
|
await fs.writeFile(path.join(output, name), bytes);
|
||||||
|
}
|
||||||
|
implementations.push(...manifest.implementations);
|
||||||
|
components.push(...(manifest.components ?? []));
|
||||||
|
}
|
||||||
|
if (implementations.filter((entry) => entry.compatibility === "quixos-service-v1").length > 1)
|
||||||
|
throw Error("A package can have only one process service owner");
|
||||||
|
if (!implementations.some((entry) => entry.compatibility === "quixos-service-v1"))
|
||||||
|
await buildService({
|
||||||
|
config: { ...config, service: {} },
|
||||||
|
compiler,
|
||||||
|
pkg,
|
||||||
|
output,
|
||||||
|
coreServer: path.join(config.sharedRuntime, "core.mjs"),
|
||||||
|
sharedFiles: [...files.values()].filter((file) => file.path.startsWith("share/quixos/shared/")),
|
||||||
|
implementations,
|
||||||
|
recordFile,
|
||||||
|
});
|
||||||
|
const manifest = compiler.parsePackageArtifacts({
|
||||||
|
schemaVersion: 1,
|
||||||
|
packageRevisionId: pkg.revision,
|
||||||
|
bindingDigest: pkg.bindingDigest,
|
||||||
|
sourceDigest: pkg.sourceDigest,
|
||||||
|
versions: plan.definition.versions,
|
||||||
|
implementations,
|
||||||
|
...(components.length ? { components } : {}),
|
||||||
|
});
|
||||||
|
const { receipt, sources } = compiler.expectedArtifactReceipt(plan, manifest, generatorDigest);
|
||||||
|
for (const [name, bytes] of sources) {
|
||||||
|
const actual = await fs.readFile(path.join(output, name));
|
||||||
|
if (!Buffer.from(bytes).equals(actual)) throw Error("Assembly lacks checked generated source " + name);
|
||||||
|
}
|
||||||
|
await fs.writeFile(path.join(output, "share/quixos/package-artifacts.json"), JSON.stringify(manifest, null, 2));
|
||||||
|
await fs.writeFile(path.join(output, "share/quixos/build-receipt.json"), JSON.stringify(receipt, null, 2));
|
||||||
|
// Check package coverage with the same public validator used at installation.
|
||||||
|
const packagePlan = { definition: { ...plan.definition, packages: [pkg] }, digest: "" };
|
||||||
|
packagePlan.digest = compiler.hexDigest(packagePlan.definition);
|
||||||
|
await compiler.checkExecutionWorld(
|
||||||
|
packagePlan,
|
||||||
|
[manifest],
|
||||||
|
await compiler.artifactFileReader({ [pkg.revision]: output }),
|
||||||
|
);
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
context,
|
||||||
|
artifacts,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
groups = map (artifact: artifact.caminoGroup or artifact) artifacts;
|
||||||
|
config = pkgs.writeText "camino-package-assembly.json" (
|
||||||
|
builtins.toJSON {
|
||||||
|
inherit groups;
|
||||||
|
inherit (context) plan packageRevisionId sharedRuntime;
|
||||||
|
compiler = "${context.generator}/libexec/quixos-protocol/execution-world.mjs";
|
||||||
|
node = "${pkgs.nodejs_24}/bin/node";
|
||||||
|
esbuild = "${pkgs.esbuild}/bin/esbuild";
|
||||||
|
shell = "${pkgs.runtimeShell}";
|
||||||
|
serviceAdapter = ./portable-service.mjs;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
in
|
||||||
|
pkgs.runCommand "camino-package"
|
||||||
|
{
|
||||||
|
passthru = {
|
||||||
|
caminoGroups = groups;
|
||||||
|
assemblyConfig = config;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${pkgs.nodejs_24}/bin/node ${./.}/assemble-package.mjs ${config}
|
||||||
|
''
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
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));
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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),
|
||||||
|
);
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
|
||||||
|
export async function buildService({
|
||||||
|
config,
|
||||||
|
compiler,
|
||||||
|
pkg,
|
||||||
|
output,
|
||||||
|
coreServer,
|
||||||
|
sharedFiles,
|
||||||
|
implementations,
|
||||||
|
recordFile,
|
||||||
|
}) {
|
||||||
|
const relative = compiler.artifactPath;
|
||||||
|
if (pkg.exports.length || config.service.entry) {
|
||||||
|
await fs.mkdir(path.join(output, "bin"), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true });
|
||||||
|
let serviceEntry;
|
||||||
|
if (config.service.entry) {
|
||||||
|
if (!/\.m?js$/.test(config.service.entry))
|
||||||
|
throw Error("Custom service entry must be JavaScript; use a separate typed/language build before packaging");
|
||||||
|
serviceEntry = path.join(config.source, relative(config.service.entry));
|
||||||
|
} else {
|
||||||
|
const modules = implementations.filter((implementation) => implementation.target === "server");
|
||||||
|
if (pkg.exports.some((entry) => !modules.some((module) => module.exports.includes(entry.id))))
|
||||||
|
throw Error("Generated service does not cover every package export; provide a custom service");
|
||||||
|
if (!modules.length) throw Error("Generated service requires a checked registry");
|
||||||
|
serviceEntry = path.resolve("service-entry.mjs");
|
||||||
|
const imports = modules
|
||||||
|
.map(
|
||||||
|
(module, index) =>
|
||||||
|
`import { ${module.registryExport} as registry${index} } from ${JSON.stringify(path.join(output, module.path))};`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
await fs.writeFile(
|
||||||
|
serviceEntry,
|
||||||
|
imports +
|
||||||
|
`\nimport {servePortableRegistry} from ${JSON.stringify(config.serviceAdapter)};
|
||||||
|
const registries = [${modules.map((_, index) => "registry" + index).join(",")}];
|
||||||
|
const owners = new Map();
|
||||||
|
for (const registry of registries) for (const id of registry.exports) {
|
||||||
|
if (owners.has(id)) throw Error("Duplicate registry export " + id);
|
||||||
|
owners.set(id, registry);
|
||||||
|
}
|
||||||
|
servePortableRegistry({exports: [...owners.keys()], invoke(request, channel, context) {
|
||||||
|
const registry = owners.get(request.exportId);
|
||||||
|
if (!registry) throw Error("Unknown export");
|
||||||
|
return registry.invoke(request, channel, context);
|
||||||
|
}});\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const serviceModule = "share/quixos/service/main.mjs";
|
||||||
|
execFileSync(
|
||||||
|
config.esbuild,
|
||||||
|
[
|
||||||
|
serviceEntry,
|
||||||
|
"--bundle",
|
||||||
|
"--platform=node",
|
||||||
|
"--format=esm",
|
||||||
|
"--minify-whitespace",
|
||||||
|
"--target=es2024",
|
||||||
|
`--external:${coreServer}`,
|
||||||
|
`--outfile=${path.join(output, serviceModule)}`,
|
||||||
|
],
|
||||||
|
{ stdio: "inherit" },
|
||||||
|
);
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(output, "bin/service"),
|
||||||
|
`#!${config.shell}\nservice_root=\${0%/*}/..\nexec ${config.node} "$service_root/${serviceModule}" "$@"\n`,
|
||||||
|
{ mode: 0o755 },
|
||||||
|
);
|
||||||
|
const serviceBody = {
|
||||||
|
id: "service",
|
||||||
|
target: "server",
|
||||||
|
compatibility: "quixos-service-v1",
|
||||||
|
path: "bin/service",
|
||||||
|
ports: "quixos-transaction-ports-v1",
|
||||||
|
exports: pkg.exports.map((e) => e.id),
|
||||||
|
files: [
|
||||||
|
await recordFile("bin/service", "text/x-shellscript"),
|
||||||
|
await recordFile(serviceModule, "text/javascript"),
|
||||||
|
...sharedFiles,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
implementations.unshift({ ...serviceBody, contentDigest: compiler.implementationDigest(serviceBody) });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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" });
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
/** 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 });
|
||||||
|
}
|
||||||
+315
-130
@@ -19,23 +19,25 @@ let
|
|||||||
schemaMainPathDefault = "dist/quixos-package-schema.js";
|
schemaMainPathDefault = "dist/quixos-package-schema.js";
|
||||||
schemaPackageName = schemaPackageJson.name or "schema";
|
schemaPackageName = schemaPackageJson.name or "schema";
|
||||||
selfSchemaName =
|
selfSchemaName =
|
||||||
if pkgs.lib.hasInfix "/" schemaPackageName
|
if pkgs.lib.hasInfix "/" schemaPackageName then
|
||||||
then pkgs.lib.last (pkgs.lib.splitString "/" schemaPackageName)
|
pkgs.lib.last (pkgs.lib.splitString "/" schemaPackageName)
|
||||||
else schemaPackageName;
|
else
|
||||||
|
schemaPackageName;
|
||||||
|
|
||||||
flakeText =
|
flakeText = builtins.replaceStrings [ "\n" "\r" "\t" ] [ " " " " " " ] (
|
||||||
builtins.replaceStrings
|
builtins.readFile "${flakeRoot}/flake.nix"
|
||||||
[ "\n" "\r" "\t" ]
|
);
|
||||||
[ " " " " " " ]
|
|
||||||
(builtins.readFile "${flakeRoot}/flake.nix");
|
|
||||||
|
|
||||||
getInputUrl = inputName:
|
getInputUrl =
|
||||||
|
inputName:
|
||||||
let
|
let
|
||||||
pattern1 = ".*" + inputName + "[[:space:]]*\\.url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*";
|
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;
|
match1 = builtins.match pattern1 flakeText;
|
||||||
match2 = if match1 != null then match1 else builtins.match pattern2 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 =
|
schemaBase =
|
||||||
(pkgs.callPackage "${schemaDir}/yarn-project.nix" {
|
(pkgs.callPackage "${schemaDir}/yarn-project.nix" {
|
||||||
@@ -65,51 +67,44 @@ let
|
|||||||
'';
|
'';
|
||||||
});
|
});
|
||||||
|
|
||||||
schemaInputs =
|
schemaInputs = pkgs.lib.filterAttrs (name: _: pkgs.lib.hasPrefix schemaPrefix name) inputs;
|
||||||
pkgs.lib.filterAttrs (name: _: pkgs.lib.hasPrefix schemaPrefix name) inputs;
|
|
||||||
|
|
||||||
decodeSchemaInputName = encodedName:
|
decodeSchemaInputName = encodedName: builtins.replaceStrings [ "__" ] [ "." ] encodedName;
|
||||||
builtins.replaceStrings [ "__" ] [ "." ] encodedName;
|
|
||||||
|
|
||||||
schemaPackagesFromInputs =
|
schemaPackagesFromInputs = pkgs.lib.mapAttrs' (
|
||||||
pkgs.lib.mapAttrs'
|
name: flake:
|
||||||
(name: flake:
|
|
||||||
let
|
let
|
||||||
schemaName =
|
schemaName = decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
|
||||||
decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
name = schemaName;
|
name = schemaName;
|
||||||
value = flake.packages.${system}.schema;
|
value = flake.packages.${system}.schema;
|
||||||
})
|
}
|
||||||
schemaInputs;
|
) schemaInputs;
|
||||||
|
|
||||||
schemaPackages = schemaPackagesFromInputs // { "${selfSchemaName}" = schema; };
|
schemaPackages = schemaPackagesFromInputs // {
|
||||||
|
"${selfSchemaName}" = schema;
|
||||||
|
};
|
||||||
|
|
||||||
schemaMetaByName =
|
schemaMetaByName = pkgs.lib.mapAttrs' (
|
||||||
pkgs.lib.mapAttrs'
|
name: flake:
|
||||||
(name: flake:
|
|
||||||
let
|
let
|
||||||
schemaName =
|
schemaName = decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
|
||||||
decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
|
|
||||||
sourceInfo = if flake ? sourceInfo then flake.sourceInfo else { };
|
sourceInfo = if flake ? sourceInfo then flake.sourceInfo else { };
|
||||||
sourceRev = sourceInfo.rev or null;
|
sourceRev = sourceInfo.rev or null;
|
||||||
inputUrl = getInputUrl name;
|
inputUrl = getInputUrl name;
|
||||||
urlBase =
|
urlBase = if inputUrl == null then null else builtins.head (pkgs.lib.splitString "?" inputUrl);
|
||||||
if inputUrl == null
|
|
||||||
then null
|
|
||||||
else builtins.head (pkgs.lib.splitString "?" inputUrl);
|
|
||||||
isDisallowed =
|
isDisallowed =
|
||||||
inputUrl == null
|
inputUrl == null
|
||||||
|| pkgs.lib.hasPrefix "path:" inputUrl
|
|| pkgs.lib.hasPrefix "path:" inputUrl
|
||||||
|| pkgs.lib.hasPrefix "file:" inputUrl
|
|| pkgs.lib.hasPrefix "file:" inputUrl
|
||||||
|| pkgs.lib.hasPrefix "git+file:" inputUrl;
|
|| pkgs.lib.hasPrefix "git+file:" inputUrl;
|
||||||
flakeRef =
|
flakeRef =
|
||||||
if isDisallowed || sourceRev == null || urlBase == null
|
if isDisallowed || sourceRev == null || urlBase == null then
|
||||||
then null
|
null
|
||||||
else urlBase + "?rev=" + sourceRev;
|
else
|
||||||
meta =
|
urlBase + "?rev=" + sourceRev;
|
||||||
{
|
meta = {
|
||||||
name = "@quixos-package-schemas/${schemaName}";
|
name = "@quixos-package-schemas/${schemaName}";
|
||||||
type = sourceInfo.type or null;
|
type = sourceInfo.type or null;
|
||||||
url = inputUrl;
|
url = inputUrl;
|
||||||
@@ -117,16 +112,16 @@ let
|
|||||||
flakeRef = flakeRef;
|
flakeRef = flakeRef;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
if flakeRef == null
|
if flakeRef == null then
|
||||||
then throw "Schema input ${schemaName} must be a git flake with url+rev"
|
throw "Schema input ${schemaName} must be a git flake with url+rev"
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
name = schemaName;
|
name = schemaName;
|
||||||
value = meta;
|
value = meta;
|
||||||
})
|
}
|
||||||
schemaInputs;
|
) schemaInputs;
|
||||||
|
|
||||||
schemaMetaJsonByName =
|
schemaMetaJsonByName = pkgs.lib.mapAttrs (_: meta: builtins.toJSON meta) schemaMetaByName;
|
||||||
pkgs.lib.mapAttrs (_: meta: builtins.toJSON meta) schemaMetaByName;
|
|
||||||
|
|
||||||
schemaExtensionsBlock = pkgs.lib.concatStringsSep "\n" (
|
schemaExtensionsBlock = pkgs.lib.concatStringsSep "\n" (
|
||||||
[
|
[
|
||||||
@@ -151,12 +146,11 @@ let
|
|||||||
);
|
);
|
||||||
|
|
||||||
schemaInstallCommands = pkgs.lib.concatStringsSep "\n" (
|
schemaInstallCommands = pkgs.lib.concatStringsSep "\n" (
|
||||||
pkgs.lib.mapAttrsToList (name: drv:
|
pkgs.lib.mapAttrsToList (
|
||||||
|
name: drv:
|
||||||
let
|
let
|
||||||
metaJson =
|
metaJson =
|
||||||
if pkgs.lib.hasAttr name schemaMetaJsonByName
|
if pkgs.lib.hasAttr name schemaMetaJsonByName then schemaMetaJsonByName.${name} else null;
|
||||||
then schemaMetaJsonByName.${name}
|
|
||||||
else null;
|
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
schema_pkg="$(ls -d ${drv}/libexec/* | head -n1)"
|
schema_pkg="$(ls -d ${drv}/libexec/* | head -n1)"
|
||||||
@@ -166,15 +160,15 @@ let
|
|||||||
chmod -R u+w "$dest"
|
chmod -R u+w "$dest"
|
||||||
''
|
''
|
||||||
+ (
|
+ (
|
||||||
if metaJson != null
|
if metaJson != null then
|
||||||
then ''
|
''
|
||||||
schema_main="${schemaMainPathDefault}"
|
schema_main="${schemaMainPathDefault}"
|
||||||
if [ -f "$dest/$schema_main" ]; then
|
if [ -f "$dest/$schema_main" ]; then
|
||||||
block_tmp="$(mktemp -p "$dest")"
|
block_tmp="$(mktemp -p "$dest")"
|
||||||
tmp_out="$(mktemp -p "$dest")"
|
tmp_out="$(mktemp -p "$dest")"
|
||||||
cat > "$block_tmp" <<'EOF'
|
cat > "$block_tmp" <<'EOF'
|
||||||
packageSchema.__quixos = ${metaJson};
|
packageSchema.__quixos = ${metaJson};
|
||||||
EOF
|
EOF
|
||||||
chmod u+w "$dest/$schema_main"
|
chmod u+w "$dest/$schema_main"
|
||||||
if grep -q '^export default ' "$dest/$schema_main"; then
|
if grep -q '^export default ' "$dest/$schema_main"; then
|
||||||
awk -v blockFile="$block_tmp" '
|
awk -v blockFile="$block_tmp" '
|
||||||
@@ -205,7 +199,8 @@ EOF
|
|||||||
rm -f "$block_tmp" "$tmp_out"
|
rm -f "$block_tmp" "$tmp_out"
|
||||||
fi
|
fi
|
||||||
''
|
''
|
||||||
else ""
|
else
|
||||||
|
""
|
||||||
)
|
)
|
||||||
) schemaPackages
|
) schemaPackages
|
||||||
);
|
);
|
||||||
@@ -240,7 +235,8 @@ EOF
|
|||||||
schemaInstallCommands
|
schemaInstallCommands
|
||||||
updateYarnrcScript
|
updateYarnrcScript
|
||||||
schemaExtensionsBlockEscaped
|
schemaExtensionsBlockEscaped
|
||||||
selfSchemaName;
|
selfSchemaName
|
||||||
|
;
|
||||||
};
|
};
|
||||||
|
|
||||||
mkTsPackageServer =
|
mkTsPackageServer =
|
||||||
@@ -265,9 +261,10 @@ EOF
|
|||||||
nodejs' = if nodejs != null then nodejs else pkgs.nodejs_24;
|
nodejs' = if nodejs != null then nodejs else pkgs.nodejs_24;
|
||||||
git' = if git != null then git else pkgs.git;
|
git' = if git != null then git else pkgs.git;
|
||||||
templatePreparePackages' =
|
templatePreparePackages' =
|
||||||
if templatePreparePackages != null
|
if templatePreparePackages != null then
|
||||||
then templatePreparePackages
|
templatePreparePackages
|
||||||
else [
|
else
|
||||||
|
[
|
||||||
pkgs.findutils
|
pkgs.findutils
|
||||||
git'
|
git'
|
||||||
pkgs.gnused
|
pkgs.gnused
|
||||||
@@ -291,28 +288,33 @@ EOF
|
|||||||
runtimeBinPath = pkgs.lib.makeBinPath extraRuntimePackages;
|
runtimeBinPath = pkgs.lib.makeBinPath extraRuntimePackages;
|
||||||
runtimeLibPath = pkgs.lib.makeLibraryPath extraRuntimePackages;
|
runtimeLibPath = pkgs.lib.makeLibraryPath extraRuntimePackages;
|
||||||
runtimeWrap =
|
runtimeWrap =
|
||||||
if extraRuntimePackages == [ ]
|
if extraRuntimePackages == [ ] then
|
||||||
then ""
|
""
|
||||||
else ''
|
else
|
||||||
|
''
|
||||||
wrapProgram "$out/bin/${serverBin}" \
|
wrapProgram "$out/bin/${serverBin}" \
|
||||||
--prefix PATH : ${runtimeBinPath} \
|
--prefix PATH : ${runtimeBinPath} \
|
||||||
--prefix LD_LIBRARY_PATH : ${runtimeLibPath}
|
--prefix LD_LIBRARY_PATH : ${runtimeLibPath}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
server = base.overrideAttrs (old:
|
server = base.overrideAttrs (
|
||||||
|
old:
|
||||||
let
|
let
|
||||||
extraAttrs = serverOverrides old;
|
extraAttrs = serverOverrides old;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
buildInputs = (old.buildInputs or [ ]) ++ extraBuildInputs;
|
buildInputs = (old.buildInputs or [ ]) ++ extraBuildInputs;
|
||||||
|
|
||||||
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
|
nativeBuildInputs =
|
||||||
|
(old.nativeBuildInputs or [ ])
|
||||||
|
++ [
|
||||||
pkgs.python3
|
pkgs.python3
|
||||||
pkgs.gnumake
|
pkgs.gnumake
|
||||||
pkgs.gcc
|
pkgs.gcc
|
||||||
pkgs.pkg-config
|
pkgs.pkg-config
|
||||||
pkgs.makeWrapper
|
pkgs.makeWrapper
|
||||||
] ++ extraNativeBuildInputs;
|
]
|
||||||
|
++ extraNativeBuildInputs;
|
||||||
|
|
||||||
# node-gyp will look at these
|
# node-gyp will look at these
|
||||||
PYTHON = "${pkgs.python3}/bin/python3";
|
PYTHON = "${pkgs.python3}/bin/python3";
|
||||||
@@ -326,13 +328,14 @@ EOF
|
|||||||
buildPhase =
|
buildPhase =
|
||||||
(old.buildPhase or "")
|
(old.buildPhase or "")
|
||||||
+ (
|
+ (
|
||||||
if buildCommand != null
|
if buildCommand != null then
|
||||||
then ''
|
''
|
||||||
runHook preBuildQuixos
|
runHook preBuildQuixos
|
||||||
${buildCommand}
|
${buildCommand}
|
||||||
runHook postBuildQuixos
|
runHook postBuildQuixos
|
||||||
''
|
''
|
||||||
else ""
|
else
|
||||||
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
postFixup = (old.postFixup or "") + runtimeWrap;
|
postFixup = (old.postFixup or "") + runtimeWrap;
|
||||||
@@ -342,7 +345,8 @@ EOF
|
|||||||
mkdir -p "$pkg_root/quixos-package-schemas"
|
mkdir -p "$pkg_root/quixos-package-schemas"
|
||||||
(cd "$pkg_root" && ${schemaSupport.schemaInstallCommands})
|
(cd "$pkg_root" && ${schemaSupport.schemaInstallCommands})
|
||||||
'';
|
'';
|
||||||
} // extraAttrs
|
}
|
||||||
|
// extraAttrs
|
||||||
);
|
);
|
||||||
|
|
||||||
check = server.overrideAttrs (old: {
|
check = server.overrideAttrs (old: {
|
||||||
@@ -360,7 +364,12 @@ EOF
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit server check packageName serverBin;
|
inherit
|
||||||
|
server
|
||||||
|
check
|
||||||
|
packageName
|
||||||
|
serverBin
|
||||||
|
;
|
||||||
|
|
||||||
devShells = {
|
devShells = {
|
||||||
"quixos-prepare" = prepareShell;
|
"quixos-prepare" = prepareShell;
|
||||||
@@ -392,13 +401,18 @@ EOF
|
|||||||
pkgs = import nixpkgs { inherit system; };
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
|
||||||
schemaSupport = mkSchemaSupport {
|
schemaSupport = mkSchemaSupport {
|
||||||
inherit pkgs inputs system schemaDir schemaPrefix self flakeRoot;
|
inherit
|
||||||
|
pkgs
|
||||||
|
inputs
|
||||||
|
system
|
||||||
|
schemaDir
|
||||||
|
schemaPrefix
|
||||||
|
self
|
||||||
|
flakeRoot
|
||||||
|
;
|
||||||
};
|
};
|
||||||
|
|
||||||
normalizeList = value:
|
normalizeList = value: if builtins.isFunction value then value pkgs else value;
|
||||||
if builtins.isFunction value
|
|
||||||
then value pkgs
|
|
||||||
else value;
|
|
||||||
|
|
||||||
serverResult = serverBuilder {
|
serverResult = serverBuilder {
|
||||||
inherit pkgs schemaSupport;
|
inherit pkgs schemaSupport;
|
||||||
@@ -408,26 +422,28 @@ EOF
|
|||||||
};
|
};
|
||||||
|
|
||||||
combinedName = pkgs.lib.strings.sanitizeDerivationName (
|
combinedName = pkgs.lib.strings.sanitizeDerivationName (
|
||||||
if packageName != null
|
if packageName != null then
|
||||||
then packageName
|
packageName
|
||||||
else if serverResult ? packageName
|
else if serverResult ? packageName then
|
||||||
then serverResult.packageName
|
serverResult.packageName
|
||||||
else "quixos-package"
|
else
|
||||||
|
"quixos-package"
|
||||||
);
|
);
|
||||||
|
|
||||||
packageServer = serverResult.server;
|
packageServer = serverResult.server;
|
||||||
packageServerCheck = if serverResult ? check then serverResult.check else null;
|
packageServerCheck = if serverResult ? check then serverResult.check else null;
|
||||||
|
|
||||||
appProgramFinal =
|
appProgramFinal =
|
||||||
if appProgram != null
|
if appProgram != null then
|
||||||
then appProgram
|
appProgram
|
||||||
else if serverResult ? appProgram
|
else if serverResult ? appProgram then
|
||||||
then serverResult.appProgram
|
serverResult.appProgram
|
||||||
else "${packageServer}/bin/${serverResult.serverBin or "server"}";
|
else
|
||||||
|
"${packageServer}/bin/${serverResult.serverBin or "server"}";
|
||||||
|
|
||||||
devShellHookBase =
|
devShellHookBase =
|
||||||
if enableYarnrcHook
|
if enableYarnrcHook then
|
||||||
then ''
|
''
|
||||||
find_schema_root() {
|
find_schema_root() {
|
||||||
dir="$PWD"
|
dir="$PWD"
|
||||||
while [ "$dir" != "/" ]; do
|
while [ "$dir" != "/" ]; do
|
||||||
@@ -455,21 +471,26 @@ EOF
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
''
|
''
|
||||||
else "";
|
else
|
||||||
|
"";
|
||||||
|
|
||||||
devShellHook = devShellHookBase + extraDevShellHook;
|
devShellHook = devShellHookBase + extraDevShellHook;
|
||||||
|
|
||||||
devShellPackages = normalizeList extraDevShellPackages;
|
devShellPackages = normalizeList extraDevShellPackages;
|
||||||
|
|
||||||
checks =
|
checks = {
|
||||||
{ schema = schemaSupport.schemaCheck; }
|
schema = schemaSupport.schemaCheck;
|
||||||
|
}
|
||||||
// (if packageServerCheck != null then { default = packageServerCheck; } else { });
|
// (if packageServerCheck != null then { default = packageServerCheck; } else { });
|
||||||
extraDevShells = if serverResult ? devShells then serverResult.devShells else { };
|
extraDevShells = if serverResult ? devShells then serverResult.devShells else { };
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages.default = pkgs.symlinkJoin {
|
packages.default = pkgs.symlinkJoin {
|
||||||
name = combinedName;
|
name = combinedName;
|
||||||
paths = [ packageServer schemaSupport.schema ];
|
paths = [
|
||||||
|
packageServer
|
||||||
|
schemaSupport.schema
|
||||||
|
];
|
||||||
};
|
};
|
||||||
packages.server = packageServer;
|
packages.server = packageServer;
|
||||||
packages.schema = schemaSupport.schema;
|
packages.schema = schemaSupport.schema;
|
||||||
@@ -510,6 +531,29 @@ EOF
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# 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 =
|
mkCaminoTsYarnNixifyFlake =
|
||||||
{
|
{
|
||||||
inputs,
|
inputs,
|
||||||
@@ -521,8 +565,13 @@ EOF
|
|||||||
promptName ? null,
|
promptName ? null,
|
||||||
nodejsAttr ? "nodejs_24",
|
nodejsAttr ? "nodejs_24",
|
||||||
buildCommand ? "yarn build",
|
buildCommand ? "yarn build",
|
||||||
sourcePortals ? { },
|
|
||||||
buildEnv ? { },
|
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 ? [ ],
|
nativeBuildInputs ? [ ],
|
||||||
devShellPackages ? [ ],
|
devShellPackages ? [ ],
|
||||||
devShellHook ? "",
|
devShellHook ? "",
|
||||||
@@ -531,22 +580,22 @@ EOF
|
|||||||
}:
|
}:
|
||||||
flake-utils.lib.eachDefaultSystem (
|
flake-utils.lib.eachDefaultSystem (
|
||||||
system:
|
system:
|
||||||
|
let
|
||||||
|
outputsFor =
|
||||||
|
candidateBindings:
|
||||||
let
|
let
|
||||||
pkgs = import nixpkgs { inherit system; };
|
pkgs = import nixpkgs { inherit system; };
|
||||||
lib = pkgs.lib;
|
lib = pkgs.lib;
|
||||||
nodejs = pkgs.${nodejsAttr};
|
nodejs = pkgs.${nodejsAttr};
|
||||||
|
|
||||||
callOption = value:
|
callOption =
|
||||||
if builtins.isFunction value
|
value: if builtins.isFunction value then value { inherit inputs pkgs system; } else value;
|
||||||
then value { inherit inputs pkgs system; }
|
|
||||||
else value;
|
|
||||||
|
|
||||||
packageJson = builtins.fromJSON (builtins.readFile "${packageRoot}/package.json");
|
packageJson = builtins.fromJSON (builtins.readFile "${packageRoot}/package.json");
|
||||||
packageNameFinal = if packageName != null then packageName else packageJson.name or "quixos-package";
|
packageNameFinal =
|
||||||
|
if packageName != null then packageName else packageJson.name or "quixos-package";
|
||||||
promptNameFinal =
|
promptNameFinal =
|
||||||
if promptName != null
|
if promptName != null then promptName else lib.strings.sanitizeDerivationName packageNameFinal;
|
||||||
then promptName
|
|
||||||
else lib.strings.sanitizeDerivationName packageNameFinal;
|
|
||||||
|
|
||||||
sourcePackage = mkCaminoSourcePackage {
|
sourcePackage = mkCaminoSourcePackage {
|
||||||
inherit pkgs;
|
inherit pkgs;
|
||||||
@@ -554,19 +603,57 @@ EOF
|
|||||||
name = sourceName;
|
name = sourceName;
|
||||||
};
|
};
|
||||||
|
|
||||||
exportsFor = attrs:
|
exportsFor =
|
||||||
|
attrs:
|
||||||
lib.concatStringsSep "\n" (
|
lib.concatStringsSep "\n" (
|
||||||
lib.mapAttrsToList
|
lib.mapAttrsToList (name: value: "export ${name}=${lib.escapeShellArg (toString value)}") attrs
|
||||||
(name: value: "export ${name}=${lib.escapeShellArg (toString value)}")
|
|
||||||
attrs
|
|
||||||
);
|
);
|
||||||
|
|
||||||
portalLinksFor = attrs:
|
bindingConfig =
|
||||||
lib.concatStringsSep "\n" (
|
if candidateBindings != null then
|
||||||
lib.mapAttrsToList
|
candidateBindings
|
||||||
(target: source: "ln -sfn ${source} ${lib.escapeShellArg target}")
|
else if bindings == null then
|
||||||
attrs
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
bundleConfig = if bundle == null then { } else bundle;
|
bundleConfig = if bundle == null then { } else bundle;
|
||||||
bundleOutfile = bundleConfig.outfile or "server.mjs";
|
bundleOutfile = bundleConfig.outfile or "server.mjs";
|
||||||
@@ -574,16 +661,44 @@ EOF
|
|||||||
bundleTarget = bundleConfig.target or "node24";
|
bundleTarget = bundleConfig.target or "node24";
|
||||||
bundleFormat = bundleConfig.format or "esm";
|
bundleFormat = bundleConfig.format or "esm";
|
||||||
bundleBanner = bundleConfig.banner or nodeRequireBanner;
|
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);";
|
nodeRequireBanner = "import { createRequire } from 'module';const require = createRequire(import.meta.url);";
|
||||||
bundleCommand =
|
bundleCommand =
|
||||||
if bundle == null
|
if bundle == null then
|
||||||
then ""
|
""
|
||||||
else ''
|
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} \
|
esbuild ${lib.escapeShellArg bundleConfig.entry} \
|
||||||
--bundle \
|
--bundle \
|
||||||
--platform=${bundlePlatform} \
|
--platform=${bundlePlatform} \
|
||||||
--target=${bundleTarget} \
|
--target=${bundleTarget} \
|
||||||
--format=${bundleFormat} \
|
--format=${bundleFormat} \
|
||||||
|
${bundleAliasArgs} \
|
||||||
${lib.optionalString (bundleConfig.preserveSymlinks or true) "--preserve-symlinks \\"}
|
${lib.optionalString (bundleConfig.preserveSymlinks or true) "--preserve-symlinks \\"}
|
||||||
--banner:js=${lib.escapeShellArg bundleBanner} \
|
--banner:js=${lib.escapeShellArg bundleBanner} \
|
||||||
--outfile=${lib.escapeShellArg bundleOutfile}
|
--outfile=${lib.escapeShellArg bundleOutfile}
|
||||||
@@ -595,42 +710,84 @@ EOF
|
|||||||
installConfig.libexecName or (lib.strings.sanitizeDerivationName packageNameFinal);
|
installConfig.libexecName or (lib.strings.sanitizeDerivationName packageNameFinal);
|
||||||
serverFile = installConfig.serverFile or bundleOutfile;
|
serverFile = installConfig.serverFile or bundleOutfile;
|
||||||
descriptorPath = installConfig.descriptorPath or "descriptor.quixos-package.txtpb";
|
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 =
|
installServerPhase =
|
||||||
if installServer == null
|
if installServer == null then
|
||||||
then null
|
null
|
||||||
else ''
|
else
|
||||||
|
''
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
install -Dm755 ${lib.escapeShellArg serverFile} "$out/libexec/${serverLibexecName}/${serverFile}"
|
install -Dm755 ${lib.escapeShellArg serverFile} "$out/libexec/${serverLibexecName}/${serverFile}"
|
||||||
|
${lib.concatMapStringsSep "\n" installExtraFile extraFiles}
|
||||||
mkdir -p "$out/bin"
|
mkdir -p "$out/bin"
|
||||||
cat > "$out/bin/${serverBin}" <<EOF
|
cat > "$out/bin/${serverBin}" <<EOF
|
||||||
#!${pkgs.runtimeShell}
|
#!${pkgs.runtimeShell}
|
||||||
exec ${nodejs}/bin/node "$out/libexec/${serverLibexecName}/${serverFile}" "\$@"
|
exec ${nodejs}/bin/node "$out/libexec/${serverLibexecName}/${serverFile}" "\$@"
|
||||||
EOF
|
EOF
|
||||||
chmod +x "$out/bin/${serverBin}"
|
chmod +x "$out/bin/${serverBin}"
|
||||||
${lib.optionalString (installConfig ? descriptorPath && descriptorPath != null) ''
|
${lib.optionalString (installConfig ? descriptorPath && descriptorPath != null) ''
|
||||||
cp ${lib.escapeShellArg descriptorPath} "$out/${descriptorPath}"
|
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
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
|
|
||||||
project = (pkgs.callPackage "${packageRoot}/yarn-project.nix" { inherit nodejs; }) {
|
project = (pkgs.callPackage "${packageRoot}/yarn-project.nix" { inherit nodejs; }) {
|
||||||
src = packageRoot;
|
src = packageRoot;
|
||||||
overrideAttrs = old: {
|
overrideAttrs =
|
||||||
|
old:
|
||||||
|
{
|
||||||
nativeBuildInputs =
|
nativeBuildInputs =
|
||||||
(old.nativeBuildInputs or [ ])
|
(old.nativeBuildInputs or [ ])
|
||||||
++ lib.optional (bundle != null) pkgs.esbuild
|
++ lib.optional (bundle != null || migrationEntrypoint != null) pkgs.esbuild
|
||||||
++ callOption nativeBuildInputs;
|
++ callOption nativeBuildInputs;
|
||||||
preConfigure = (old.preConfigure or "") + ''
|
|
||||||
${portalLinksFor (callOption sourcePortals)}
|
|
||||||
'';
|
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
runHook preBuild
|
runHook preBuild
|
||||||
${exportsFor (callOption buildEnv)}
|
${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}
|
${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}
|
${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
|
runHook postBuild
|
||||||
'';
|
'';
|
||||||
} // lib.optionalAttrs (installServerPhase != null) {
|
}
|
||||||
|
// lib.optionalAttrs (installServerPhase != null) {
|
||||||
installPhase = installServerPhase;
|
installPhase = installServerPhase;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -646,9 +803,10 @@ EOF
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
maybeServerOutputs =
|
maybeServerOutputs =
|
||||||
if installServer == null
|
if installServer == null then
|
||||||
then { }
|
{ }
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
packages.server = project;
|
packages.server = project;
|
||||||
apps.default = {
|
apps.default = {
|
||||||
type = "app";
|
type = "app";
|
||||||
@@ -662,18 +820,45 @@ EOF
|
|||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
packages = [
|
packages = [
|
||||||
nodejs
|
nodejs
|
||||||
pkgs.yarn-berry_4
|
project.yarn-freestanding
|
||||||
] ++ callOption devShellPackages;
|
]
|
||||||
|
++ lib.optional (bindingConfig != null) generateBindings
|
||||||
|
++ callOption devShellPackages;
|
||||||
|
# Explicit command keeps shell entry free of source mutations.
|
||||||
shellHook = devShellHookBase + callOption devShellHook;
|
shellHook = devShellHookBase + callOption devShellHook;
|
||||||
};
|
};
|
||||||
} // maybeServerOutputs
|
}
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
in
|
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
|
inherit
|
||||||
mkQuixosPackageFlake
|
mkQuixosPackageFlake
|
||||||
mkTsPackageServer
|
mkTsPackageServer
|
||||||
mkSchemaSupport
|
mkSchemaSupport
|
||||||
mkCaminoSourcePackage
|
mkCaminoSourcePackage
|
||||||
mkCaminoTsYarnNixifyFlake;
|
mkQxBindingSchema
|
||||||
|
mkCaminoTsYarnNixifyFlake
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
context,
|
||||||
|
src,
|
||||||
|
members,
|
||||||
|
dependencies ? null,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
config = pkgs.writeText "camino-react-contracts.json" (
|
||||||
|
builtins.toJSON {
|
||||||
|
source = src;
|
||||||
|
interface = context.interface;
|
||||||
|
inherit members dependencies;
|
||||||
|
node = "${pkgs.nodejs_24}/bin/node";
|
||||||
|
tsc = "${context.typescript}/node_modules/typescript/bin/tsc";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
in
|
||||||
|
pkgs.runCommand "camino-react-contracts" { } ''
|
||||||
|
${pkgs.nodejs_24}/bin/node ${./.}/build-react-contracts.mjs ${config}
|
||||||
|
''
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
context,
|
||||||
|
src,
|
||||||
|
modules,
|
||||||
|
bindingOutput ? "src/gen/client.ts",
|
||||||
|
dependencies ? null,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
group = import ./artifacts.nix {
|
||||||
|
inherit pkgs src;
|
||||||
|
inherit (context)
|
||||||
|
plan
|
||||||
|
core
|
||||||
|
sharedRuntime
|
||||||
|
typescript
|
||||||
|
packageRevisionId
|
||||||
|
;
|
||||||
|
protocol = context.generator;
|
||||||
|
group = true;
|
||||||
|
nodeModules = dependencies;
|
||||||
|
components = {
|
||||||
|
inherit bindingOutput;
|
||||||
|
inherit (context.react) sdk runtime;
|
||||||
|
entries = modules;
|
||||||
|
contracts = context.react.contracts or [ ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
package = import ./assemble-package.nix {
|
||||||
|
inherit pkgs context;
|
||||||
|
artifacts = [ group ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
package // { caminoGroup = group; }
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
context,
|
||||||
|
src,
|
||||||
|
entry,
|
||||||
|
targets ? [
|
||||||
|
"browser"
|
||||||
|
"server"
|
||||||
|
],
|
||||||
|
entries ? { },
|
||||||
|
bindingOutput ? "src/gen/qx.ts",
|
||||||
|
dependencies ? null,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
group = import ./artifacts.nix {
|
||||||
|
inherit pkgs src;
|
||||||
|
inherit (context)
|
||||||
|
plan
|
||||||
|
core
|
||||||
|
sharedRuntime
|
||||||
|
typescript
|
||||||
|
packageRevisionId
|
||||||
|
;
|
||||||
|
protocol = context.generator;
|
||||||
|
group = true;
|
||||||
|
nodeModules = dependencies;
|
||||||
|
portable = {
|
||||||
|
inherit
|
||||||
|
entry
|
||||||
|
entries
|
||||||
|
targets
|
||||||
|
bindingOutput
|
||||||
|
;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
package = import ./assemble-package.nix {
|
||||||
|
inherit pkgs context;
|
||||||
|
artifacts = [ group ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
package // { caminoGroup = group; }
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
context,
|
||||||
|
src,
|
||||||
|
entry,
|
||||||
|
bindingOutput ? "src/gen/remote.ts",
|
||||||
|
dependencies ? null,
|
||||||
|
service ? { },
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
group = import ./artifacts.nix {
|
||||||
|
inherit pkgs src service;
|
||||||
|
inherit (context)
|
||||||
|
plan
|
||||||
|
core
|
||||||
|
sharedRuntime
|
||||||
|
typescript
|
||||||
|
packageRevisionId
|
||||||
|
;
|
||||||
|
protocol = context.generator;
|
||||||
|
group = true;
|
||||||
|
nodeModules = dependencies;
|
||||||
|
remote = { inherit entry bindingOutput; };
|
||||||
|
};
|
||||||
|
package = import ./assemble-package.nix {
|
||||||
|
inherit pkgs context;
|
||||||
|
artifacts = [ group ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
package // { caminoGroup = group; }
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
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
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{ 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
|
||||||
|
'';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user