Instrument and improve incremental workspace checks
Profile source capture, Nix evaluation/realization, resource/query checking, and implementation builds without putting timings into immutable outputs. Keep modular repositories; bound independent capture work, snapshot jj once per observation, and retain final concurrent-edit and Git completeness checks. Compose exact resource/closure/frontend/toolchain checks as memoized Nix derivations. Preserve whole-workspace validation and the full compiler path when no checked dependency is supplied. Share canonical Nixpkgs by content, index workspace build contexts once, and narrow lock/driver/compiler inputs with conservative complete-source fallback. Batch React witness checks only when distinct source closures cannot share ambient declarations; otherwise run the original independent TypeScript checks. Seed local authoring toolchains with exact canonical fetch identities when they match the selected inputs; retain store-path inputs for overrides. Add generic correctness checks rather than permanent Reader timing/content fixtures. Verify actual Reader edits and rejection/recovery in a private workbench, preserving the original user's workbench and runtime. Query checking currently caches at resource/closure boundaries, not individual query semantic dependency boundaries. Template helper pins and reproducible fixture snapshots follow in a separate publication change.
This commit is contained in:
@@ -31,6 +31,16 @@ excluded from the implementation source tree. The `plan` argument on the low-lev
|
|||||||
helper is a composition convenience for fixtures; it is projected before building.
|
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.
|
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
|
||||||
|
|||||||
+16
-4
@@ -34,7 +34,13 @@
|
|||||||
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
|
||||||
@@ -92,9 +98,15 @@ let
|
|||||||
serviceAdapter = ./portable-service.mjs;
|
serviceAdapter = ./portable-service.mjs;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
compiled = pkgs.runCommand "camino-compiled-artifacts" { passthru.buildConfig = config; } ''
|
compiled =
|
||||||
${pkgs.nodejs_24}/bin/node ${./.}/build-artifacts.mjs ${config}
|
pkgs.runCommand "camino-compiled-artifacts"
|
||||||
'';
|
{
|
||||||
|
passthru.buildConfig = config;
|
||||||
|
QUIXOS_TRACE_PROFILE = "1";
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${pkgs.nodejs_24}/bin/node ${buildTools}/build-artifacts.mjs ${config}
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
if group then
|
if group then
|
||||||
compiled
|
compiled
|
||||||
|
|||||||
@@ -4,7 +4,12 @@
|
|||||||
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;
|
||||||
@@ -14,7 +19,7 @@ let
|
|||||||
packageRevisionId
|
packageRevisionId
|
||||||
sharedRuntime
|
sharedRuntime
|
||||||
;
|
;
|
||||||
compiler = "${context.generator}/libexec/quixos-protocol/execution-world.mjs";
|
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}";
|
||||||
@@ -30,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}
|
||||||
''
|
''
|
||||||
|
|||||||
+39
-34
@@ -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. */
|
||||||
@@ -102,46 +103,50 @@ for (const { target, mode, settings } of builds) {
|
|||||||
source,
|
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);
|
||||||
|
|||||||
+44
-48
@@ -1,4 +1,6 @@
|
|||||||
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";
|
||||||
@@ -152,16 +154,12 @@ export async function buildComponents({ config, compiler, contract, pkg, output,
|
|||||||
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 = [];
|
||||||
const { checkControls } = await import(pathToFileURL(path.join(settings.sdk, "lint-controls.mjs")));
|
|
||||||
const { default: ts } = await import(pathToFileURL(path.resolve(path.dirname(config.tsc), "../lib/typescript.js")));
|
|
||||||
const checkedControls = new Set();
|
|
||||||
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(
|
||||||
@@ -171,30 +169,26 @@ export async function buildComponents({ config, compiler, contract, pkg, output,
|
|||||||
? `import type {ComponentProps} from "react";declare const props:Pick<ComponentProps<Modules[${JSON.stringify(declaration.displayName)}]>,"subject"|"input">;const mount = <Component {...props}/>;\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,24 +196,26 @@ export async function buildComponents({ config, compiler, contract, pkg, output,
|
|||||||
`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);
|
||||||
|
|||||||
@@ -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 })}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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;
|
||||||
|
|||||||
+2
-1
@@ -6,6 +6,7 @@
|
|||||||
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 = import ./implementation-source.nix {
|
source = import ./implementation-source.nix {
|
||||||
@@ -33,5 +34,5 @@ let
|
|||||||
);
|
);
|
||||||
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}
|
||||||
''
|
''
|
||||||
|
|||||||
Reference in New Issue
Block a user