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