Make Camino examples and scaffolds complete Yarn and Nix packages

This commit is contained in:
Timothy J. Aveni
2026-09-22 15:31:42 -07:00
parent 82a4542746
commit 152f58a3c6
9 changed files with 193 additions and 32 deletions
+27
View File
@@ -0,0 +1,27 @@
import assert from "node:assert/strict";
import test from "node:test";
import { componentEntries } from "../src/capability-language/component-nix.js";
test("component edit surface parses literal Nix without evaluating arbitrary expressions", async () => {
assert.deepEqual(
{
...(await componentEntries(
`{\n # retained author comment\n "component:card" = { export = "Card"; entry = "src/card.tsx"; };\n}\n`,
)),
},
{ "component:card": { entry: "src/card.tsx", export: "Card" } },
);
await assert.rejects(
componentEntries('let x = builtins.abort "must not evaluate"; in x'),
/supported literal-entry shape|literal strings|supported scaffold shape/,
);
await assert.rejects(
componentEntries(
'{\n "component:card" = { entry = "${builtins.abort "must not evaluate"}"; export = "Card"; };\n}\n',
),
/literal strings|supported literal-entry shape/,
);
await assert.rejects(
componentEntries('{\n "component:card" = { entry = "src/card.tsx"; export = "Card"; extra = true; };\n}\n'),
/Unsupported component entry property/,
);
});
+29 -4
View File
@@ -5,6 +5,7 @@ import os from "node:os";
import path from "node:path";
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { componentEntries } from "../src/capability-language/component-nix.js";
import { scaffoldRecipe } from "../src/capability-language/scaffold-recipes.js";
import { planStructure, applyStructure } from "../src/capability-language/structural-plan.js";
const execFile = promisify(callback);
@@ -31,12 +32,23 @@ test("checked component scaffolding composes with execution handlers without rep
id: "component:card",
declaration: 'module Card id "component:card" implements View.render;',
});
assert.deepEqual(JSON.parse(await fs.readFile(path.join(root, "quixos.components.json"), "utf8")), {
"component:card": { entry: "src/components/Card.tsx", export: "Card" },
});
assert.deepEqual(
{ ...(await componentEntries(await fs.readFile(path.join(root, "components.nix"), "utf8"))) },
{
"component:card": { entry: "src/components/Card.tsx", export: "Card" },
},
);
const card = path.join(root, "src/components/Card.tsx");
await fs.appendFile(card, "\n// authored extension\n");
const retained = await fs.readFile(card, "utf8");
const flake = path.join(root, "flake.nix");
await fs.appendFile(flake, "\n# retained authored toolchain configuration\n");
const retainedFlake = await fs.readFile(flake, "utf8");
const entriesFile = path.join(root, "components.nix");
await fs.writeFile(
entriesFile,
(await fs.readFile(entriesFile, "utf8")).replace("{\n", "{\n # retained entry comment\n"),
);
await apply("function", {
name: "echo",
id: "echo",
@@ -48,8 +60,10 @@ test("checked component scaffolding composes with execution handlers without rep
declaration: 'module Details id "component:details" implements View.render;',
});
assert.equal(await fs.readFile(card, "utf8"), retained);
assert.equal(await fs.readFile(flake, "utf8"), retainedFlake);
assert.match(await fs.readFile(entriesFile, "utf8"), /retained entry comment/);
assert.match(await fs.readFile(path.join(root, "src/server.ts"), "utf8"), /echo/);
assert.match(await fs.readFile(path.join(root, "flake.nix"), "utf8"), /react \? null/);
assert.match(await fs.readFile(path.join(root, "flake.nix"), "utf8"), /import \.\/package\.nix/);
await assert.rejects(
apply("component", {
name: "Card",
@@ -67,6 +81,17 @@ test("checked component scaffolding composes with execution handlers without rep
}),
/exactly one/,
);
const beforeUnsupported = await fs.readFile(packageFile, "utf8");
await fs.writeFile(entriesFile, '{\n custom = builtins.abort "must not evaluate";\n}\n');
await assert.rejects(
apply("component", {
name: "Unsupported",
id: "component:unsupported",
declaration: 'module Unsupported id "component:unsupported" implements View.render;',
}),
/literal-entry shape/,
);
assert.equal(await fs.readFile(packageFile, "utf8"), beforeUnsupported);
});
test("controller scaffolding preserves existing registrations and rejects duplicate identities", async (context) => {
+1 -1
View File
@@ -47,7 +47,7 @@ test("execution scaffolds route typed handlers by declared effect and target wit
assert.match(await read("src/impl/asyncWork.ts"), /RemoteImplementation/);
assert.match(await read("src/impl/both.ts"), /PortableImplementation/);
assert.match(await read("flake.nix"), /checkedArtifacts/);
assert.match(await read("flake.nix"), /entries.browser/);
assert.match(await read("package.nix"), /entries.browser/);
await assert.rejects(add("both", "transaction targets [browser, server]"), /unique/);
await assert.rejects(add("invalid", "snapshot targets [remote]"), /browser\/server/);
await assert.rejects(apply("function", { name: "missing", id: "export:missing" }), /explicit execution/);
+24
View File
@@ -61,4 +61,28 @@ test("structural plans validate the graph, journal originals, and reject stale e
await applyStructure(draft);
assert.match(await fs.readFile(path.join(root, "workspace.qx"), "utf8"), /import interface NotImplementedYet/);
await assert.rejects(() => planStructure(root, { ...provisional, validation: "resource-graph" }));
const plugin = ".yarn/plugins/yarn-plugin-nixify.cjs";
await applyStructure(
await planStructure(root, {
kind: "workspace",
validation: "syntax",
files: [
{ file: plugin, create: "module.exports = {};\n" },
{ file: "yarn.lock", create: "__metadata:\n version: 8\n" },
{ file: "flake.lock", create: '{"version":7}\n' },
],
}),
);
assert.equal(await fs.readFile(path.join(root, plugin), "utf8"), "module.exports = {};\n");
for (const file of [".yarn/../escape.cjs", ".yarn/cache/entry.cjs", ".git/hooks/hook.cjs"]) {
await assert.rejects(
() =>
planStructure(root, {
kind: "workspace",
validation: "syntax",
files: [{ file, create: "" }],
}),
/Unsafe scaffold path/,
);
}
});