Make Camino examples and scaffolds complete Yarn and Nix packages
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
pkgs.jujutsu
|
pkgs.jujutsu
|
||||||
pkgs.gnutar
|
pkgs.gnutar
|
||||||
pkgs.util-linux
|
pkgs.util-linux
|
||||||
|
pkgs.nix
|
||||||
];
|
];
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
runHook preBuild
|
runHook preBuild
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { execFile as callback } from "node:child_process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
const execFile = promisify(callback);
|
||||||
|
export const nixLiteral = (value: string): string => JSON.stringify(value).replace(/\$\{/g, "\\${");
|
||||||
|
|
||||||
|
/** Recognize the deliberately small scaffold edit surface, never evaluate Nix.
|
||||||
|
* Authors may replace it with arbitrary Nix; then scaffolding declines the edit. */
|
||||||
|
export async function componentEntries(source: string): Promise<Record<string, { entry: string; export: string }>> {
|
||||||
|
const { stdout } = await execFile("nix-instantiate", ["--parse", "--expr", source], { maxBuffer: 1024 * 1024 });
|
||||||
|
const tokens = stdout.match(/"(?:\\.|[^"\\])*"|[A-Za-z_][A-Za-z0-9_-]*|[{}=;]/g) ?? [];
|
||||||
|
if (stdout.replace(/"(?:\\.|[^"\\])*"|[A-Za-z_][A-Za-z0-9_-]*|[{}=;]|\s+/g, ""))
|
||||||
|
throw Error("components.nix is outside the supported literal-entry shape");
|
||||||
|
let cursor = 0;
|
||||||
|
const expect = (token: string) => {
|
||||||
|
if (tokens[cursor++] !== token) throw Error("components.nix is outside the supported literal-entry shape");
|
||||||
|
};
|
||||||
|
const string = () => {
|
||||||
|
const token = tokens[cursor++];
|
||||||
|
if (!token?.startsWith('"')) throw Error("Component entries must use literal strings");
|
||||||
|
const json = token.replace(/(\\*)\$\{/g, (_match, escapes: string) => {
|
||||||
|
if (escapes.length % 2 === 0) throw Error("Component entries must use literal strings");
|
||||||
|
return escapes.slice(1) + "${";
|
||||||
|
});
|
||||||
|
return JSON.parse(json) as string;
|
||||||
|
};
|
||||||
|
const entries: Record<string, { entry: string; export: string }> = Object.create(null);
|
||||||
|
expect("{");
|
||||||
|
while (tokens[cursor] !== "}") {
|
||||||
|
const id = tokens[cursor]?.startsWith('"') ? string() : tokens[cursor++];
|
||||||
|
if (!id) throw Error("Missing component identity");
|
||||||
|
if (Object.hasOwn(entries, id)) throw Error("Duplicate component entry");
|
||||||
|
expect("=");
|
||||||
|
expect("{");
|
||||||
|
const properties: Record<string, string> = Object.create(null);
|
||||||
|
while (tokens[cursor] !== "}") {
|
||||||
|
const key = tokens[cursor]?.startsWith('"') ? string() : tokens[cursor++];
|
||||||
|
if (!key || !["entry", "export"].includes(key) || Object.hasOwn(properties, key))
|
||||||
|
throw Error("Unsupported component entry property");
|
||||||
|
expect("=");
|
||||||
|
properties[key] = string();
|
||||||
|
expect(";");
|
||||||
|
}
|
||||||
|
expect("}");
|
||||||
|
expect(";");
|
||||||
|
if (!properties.entry || !properties.export) throw Error("Component entry needs entry and export");
|
||||||
|
entries[id] = { entry: properties.entry, export: properties.export };
|
||||||
|
}
|
||||||
|
expect("}");
|
||||||
|
if (cursor !== tokens.length || !/\n}\s*$/.test(source))
|
||||||
|
throw Error("components.nix is outside the supported scaffold shape");
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import path from "node:path";
|
|||||||
import { parseQx, walkSyntax } from "./source.js";
|
import { parseQx, walkSyntax } from "./source.js";
|
||||||
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
|
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
|
||||||
import type { StructuralRequest } from "./structural-plan.js";
|
import type { StructuralRequest } from "./structural-plan.js";
|
||||||
|
import { componentEntries, nixLiteral } from "./component-nix.js";
|
||||||
|
|
||||||
/** Add one ordinary component entry without regenerating existing application code. */
|
/** Add one ordinary component entry without regenerating existing application code. */
|
||||||
export async function scaffoldComponent(
|
export async function scaffoldComponent(
|
||||||
@@ -46,12 +47,13 @@ export async function scaffoldComponent(
|
|||||||
)
|
)
|
||||||
throw Error("Component requires a unique name and ID");
|
throw Error("Component requires a unique name and ID");
|
||||||
}
|
}
|
||||||
const filename = "quixos.components.json";
|
const filename = "components.nix";
|
||||||
const before = await fs.readFile(path.join(root, prefix, filename), "utf8");
|
const before = await fs.readFile(path.join(root, prefix, filename), "utf8");
|
||||||
const entries: Record<string, { entry: string; export: string }> = JSON.parse(before);
|
const entries = await componentEntries(before);
|
||||||
if (!entries || typeof entries !== "object" || Array.isArray(entries) || Object.hasOwn(entries, spec.id))
|
if (!entries || typeof entries !== "object" || Array.isArray(entries) || Object.hasOwn(entries, spec.id))
|
||||||
throw Error("Invalid or duplicate component entry");
|
throw Error("Invalid or duplicate component entry");
|
||||||
entries[spec.id] = { entry: `src/components/${spec.name}.tsx`, export: spec.name };
|
const insertion = ` ${nixLiteral(spec.id)} = { entry = ${nixLiteral(`src/components/${spec.name}.tsx`)}; export = ${nixLiteral(spec.name)}; };\n`;
|
||||||
|
const after = before.replace(/\n}(\s*)$/, (_match, trailing) => "\n" + insertion + "}" + trailing);
|
||||||
const parentID = parent.children.find((node) => node.kind === "stringLiteral")!;
|
const parentID = parent.children.find((node) => node.kind === "stringLiteral")!;
|
||||||
return {
|
return {
|
||||||
kind: "package",
|
kind: "package",
|
||||||
@@ -69,10 +71,10 @@ export async function scaffoldComponent(
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ file: prefix + filename, expected: before, replace: JSON.stringify(entries, null, 2) + "\n" },
|
{ file: prefix + filename, expected: before, replace: after },
|
||||||
{
|
{
|
||||||
file: prefix + `src/components/${spec.name}.tsx`,
|
file: prefix + `src/components/${spec.name}.tsx`,
|
||||||
create: `import type { Components } from "../gen/client.js";\n\n// Add explicit query admission and named interface operations from the generated client.\nexport const ${spec.name}: Components[${JSON.stringify(spec.name)}] = ({subject}) => <p>{subject.object}</p>;\n`,
|
create: `import type { Modules } from "../gen/client.js";\n\n// Add explicit query admission and named interface operations from the generated client.\nexport const ${spec.name}: Modules[${JSON.stringify(spec.name)}] = ({subject}) => <p>{subject.object}</p>;\n`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,9 +37,25 @@ export async function executionScaffold(
|
|||||||
formatQuixosLock({ formatVersion: 1, quixos: { resolver: "git", ...spec.tools.quixos }, resources: [] }),
|
formatQuixosLock({ formatVersion: 1, quixos: { resolver: "git", ...spec.tools.quixos }, resources: [] }),
|
||||||
);
|
);
|
||||||
create("quixos.check.json", json({ backend: "execution" }));
|
create("quixos.check.json", json({ backend: "execution" }));
|
||||||
create("quixos.components.json", "{}\n");
|
create("components.nix", "{\n}\n");
|
||||||
create("package.json", json({ name: `@quixos/${spec.name.toLowerCase()}`, private: true, type: "module" }));
|
create(
|
||||||
create(".gitignore", "node_modules/\ndist/\nsrc/gen/\n.quixos/\nresult\n");
|
"package.json",
|
||||||
|
json({
|
||||||
|
name: `@quixos/${spec.name.toLowerCase()}`,
|
||||||
|
private: true,
|
||||||
|
type: "module",
|
||||||
|
packageManager: "yarn@4.18.0",
|
||||||
|
scripts: { typecheck: "qx-workspace check" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
create(
|
||||||
|
".gitignore",
|
||||||
|
"node_modules/\ndist/\nsrc/gen/\n.quixos/\nresult\n.yarn/cache/\n.yarn/install-state.gz\n.yarn/unplugged/\n",
|
||||||
|
);
|
||||||
|
create(
|
||||||
|
".yarnrc.yml",
|
||||||
|
'enableScripts: true\n\ngenerateDefaultNix: false\n\nindividualNixPackaging: true\n\nnodeLinker: node-modules\n\nsupportedArchitectures:\n os:\n - current\n - linux\n cpu:\n - current\n - x64\n - arm64\n libc:\n - current\n - glibc\n\nplugins:\n - checksum: 271de9a785321a37c564b8e2a6b5cd689850b20572d5fee8ec0a6b8e7a5d4cfb59e533524af2b4a537e00da12828abd062eaec6978556e3dd9149a020db2591b\n path: .yarn/plugins/yarn-plugin-nixify.cjs\n spec: "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/yarn-plugin-nixify-patched/raw/commit/4528fdd20b30d869262443b3f044549810e75fb8/dist/yarn-plugin-nixify.js"\n',
|
||||||
|
);
|
||||||
for (const lane of ["browser", "server", "remote"])
|
for (const lane of ["browser", "server", "remote"])
|
||||||
create(
|
create(
|
||||||
`src/${lane}.ts`,
|
`src/${lane}.ts`,
|
||||||
@@ -56,22 +72,36 @@ export async function executionScaffold(
|
|||||||
flake-utils.lib.eachDefaultSystem (system: let
|
flake-utils.lib.eachDefaultSystem (system: let
|
||||||
pkgs = import nixpkgs { inherit system; };
|
pkgs = import nixpkgs { inherit system; };
|
||||||
in {
|
in {
|
||||||
quixosPackages.checkedArtifacts = { plan, generator, core, sharedRuntime, typescript, packageRevisionId, react ? null, ... }:
|
devShells.default = (import (helpers + "/quixos-package-helpers.nix")).mkCaminoYarnShell { inherit pkgs; };
|
||||||
(import (helpers + "/artifacts.nix")) {
|
quixosPackages.checkedArtifacts = context:
|
||||||
inherit pkgs plan core sharedRuntime typescript packageRevisionId;
|
import ./package.nix { inherit pkgs context; camino = import (helpers + "/quixos-package-helpers.nix"); };
|
||||||
protocol = generator;
|
});
|
||||||
src = ./.;
|
}
|
||||||
components = let entries = builtins.fromJSON (builtins.readFile ./quixos.components.json); in
|
`,
|
||||||
if entries == {} then null else
|
);
|
||||||
if react == null then throw "Component entries require the workspace-selected React runtime" else
|
create(
|
||||||
{ inherit (react) sdk runtime; inherit entries; };
|
"package.nix",
|
||||||
portable = {
|
`{ pkgs, camino, context }:
|
||||||
entry = "src/server.ts";
|
let
|
||||||
entries.browser = "src/browser.ts";
|
plan = builtins.fromJSON (builtins.readFile context.plan);
|
||||||
bindingOutput = "src/gen/qx.ts";
|
package = pkgs.lib.findFirst (entry: entry.revision == context.packageRevisionId)
|
||||||
};
|
(throw "Package missing from checked plan") plan.definition.packages;
|
||||||
remote = { entry = "src/remote.ts"; bindingOutput = "src/gen/remote.ts"; };
|
portable = builtins.filter (entry: entry.execution.kind != "remote") package.exports;
|
||||||
};
|
remote = builtins.filter (entry: entry.execution.kind == "remote") package.exports;
|
||||||
|
modules = import ./components.nix;
|
||||||
|
dependencies = if builtins.pathExists ./yarn-project.nix
|
||||||
|
then camino.mkCaminoYarnDependencies { inherit pkgs; src = ./.; } else null;
|
||||||
|
in camino.assembleCaminoPackage {
|
||||||
|
inherit pkgs context;
|
||||||
|
artifacts =
|
||||||
|
pkgs.lib.optional (portable != []) (camino.mkCaminoTypeScriptPackage {
|
||||||
|
inherit pkgs context dependencies; src = ./.;
|
||||||
|
entry = "src/server.ts"; entries.browser = "src/browser.ts";
|
||||||
|
targets = pkgs.lib.unique (pkgs.lib.concatMap (entry: entry.execution.targets) portable);
|
||||||
|
}) ++ pkgs.lib.optional (remote != []) (camino.mkCaminoTypeScriptService {
|
||||||
|
inherit pkgs context dependencies; src = ./.; entry = "src/remote.ts";
|
||||||
|
}) ++ pkgs.lib.optional (modules != {}) (camino.mkCaminoReactPackage {
|
||||||
|
inherit pkgs context dependencies modules; src = ./.;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -163,7 +193,7 @@ export async function executionScaffold(
|
|||||||
for (const [file, content] of Object.entries(spec.initialFiles)) {
|
for (const [file, content] of Object.entries(spec.initialFiles)) {
|
||||||
if (
|
if (
|
||||||
typeof content !== "string" ||
|
typeof content !== "string" ||
|
||||||
!/^(?:package\.qx|quixos\.components\.json|quixos\.controllers\.json|README\.md|(?:src|queries)\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_.-]+)$/.test(
|
!/^(?:package\.qx|(?:package|components|flake|yarn-project)\.nix|package\.json|yarn\.lock|\.yarnrc\.yml|\.gitignore|\.yarn\/plugins\/[A-Za-z0-9_.-]+\.cjs|quixos\.controllers\.json|README\.md|(?:src|queries)\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_.-]+)$/.test(
|
||||||
file,
|
file,
|
||||||
) ||
|
) ||
|
||||||
file.split("/").some((part) => part === ".." || part === "." || part === "gen")
|
file.split("/").some((part) => part === ".." || part === "." || part === "gen")
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ type Change = { file: string; before: string | null; after: string; mode: number
|
|||||||
type Journal = { schemaVersion: 1; id: string; root: string; phase: "prepared" | "complete"; changes: Change[] };
|
type Journal = { schemaVersion: 1; id: string; root: string; phase: "prepared" | "complete"; changes: Change[] };
|
||||||
const safeFile = (file: string) => {
|
const safeFile = (file: string) => {
|
||||||
if (
|
if (
|
||||||
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc.yml|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|graphql|lock|ts|tsx|css|mjs|json|nix|txtpb|md))$/.test(
|
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc\.yml|\.yarn\/plugins\/[A-Za-z0-9_-][A-Za-z0-9_.-]*\.cjs|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|graphql|lock|ts|tsx|css|mjs|json|nix|txtpb|md))$/.test(
|
||||||
file,
|
file,
|
||||||
) ||
|
) ||
|
||||||
file.split("/").some((part) => [".git", ".jj", ".quixos", "node_modules"].includes(part))
|
file.split("/").some((part) => [".git", ".jj", ".quixos", "node_modules"].includes(part))
|
||||||
@@ -141,7 +141,7 @@ export const planStructure = async (rootPath: string, request: StructuralRequest
|
|||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
if (change.file.endsWith(".qx") && parseQx(change.after, change.file).diagnostics.length)
|
if (change.file.endsWith(".qx") && parseQx(change.after, change.file).diagnostics.length)
|
||||||
throw new Error(`Invalid QX syntax in ${change.file}`);
|
throw new Error(`Invalid QX syntax in ${change.file}`);
|
||||||
if (change.file.endsWith(".lock") && !parseQuixosLockDocument(change.after, change.file).ok)
|
if (path.basename(change.file) === "quixos.lock" && !parseQuixosLockDocument(change.after, change.file).ok)
|
||||||
throw new Error(`Invalid lock syntax in ${change.file}`);
|
throw new Error(`Invalid lock syntax in ${change.file}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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/,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { execFile as callback } from "node:child_process";
|
import { execFile as callback } from "node:child_process";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
|
import { componentEntries } from "../src/capability-language/component-nix.js";
|
||||||
import { scaffoldRecipe } from "../src/capability-language/scaffold-recipes.js";
|
import { scaffoldRecipe } from "../src/capability-language/scaffold-recipes.js";
|
||||||
import { planStructure, applyStructure } from "../src/capability-language/structural-plan.js";
|
import { planStructure, applyStructure } from "../src/capability-language/structural-plan.js";
|
||||||
const execFile = promisify(callback);
|
const execFile = promisify(callback);
|
||||||
@@ -31,12 +32,23 @@ test("checked component scaffolding composes with execution handlers without rep
|
|||||||
id: "component:card",
|
id: "component:card",
|
||||||
declaration: 'module Card id "component:card" implements View.render;',
|
declaration: 'module Card id "component:card" implements View.render;',
|
||||||
});
|
});
|
||||||
assert.deepEqual(JSON.parse(await fs.readFile(path.join(root, "quixos.components.json"), "utf8")), {
|
assert.deepEqual(
|
||||||
"component:card": { entry: "src/components/Card.tsx", export: "Card" },
|
{ ...(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");
|
const card = path.join(root, "src/components/Card.tsx");
|
||||||
await fs.appendFile(card, "\n// authored extension\n");
|
await fs.appendFile(card, "\n// authored extension\n");
|
||||||
const retained = await fs.readFile(card, "utf8");
|
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", {
|
await apply("function", {
|
||||||
name: "echo",
|
name: "echo",
|
||||||
id: "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;',
|
declaration: 'module Details id "component:details" implements View.render;',
|
||||||
});
|
});
|
||||||
assert.equal(await fs.readFile(card, "utf8"), retained);
|
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, "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(
|
await assert.rejects(
|
||||||
apply("component", {
|
apply("component", {
|
||||||
name: "Card",
|
name: "Card",
|
||||||
@@ -67,6 +81,17 @@ test("checked component scaffolding composes with execution handlers without rep
|
|||||||
}),
|
}),
|
||||||
/exactly one/,
|
/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) => {
|
test("controller scaffolding preserves existing registrations and rejects duplicate identities", async (context) => {
|
||||||
|
|||||||
@@ -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/asyncWork.ts"), /RemoteImplementation/);
|
||||||
assert.match(await read("src/impl/both.ts"), /PortableImplementation/);
|
assert.match(await read("src/impl/both.ts"), /PortableImplementation/);
|
||||||
assert.match(await read("flake.nix"), /checkedArtifacts/);
|
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("both", "transaction targets [browser, server]"), /unique/);
|
||||||
await assert.rejects(add("invalid", "snapshot targets [remote]"), /browser\/server/);
|
await assert.rejects(add("invalid", "snapshot targets [remote]"), /browser\/server/);
|
||||||
await assert.rejects(apply("function", { name: "missing", id: "export:missing" }), /explicit execution/);
|
await assert.rejects(apply("function", { name: "missing", id: "export:missing" }), /explicit execution/);
|
||||||
|
|||||||
@@ -61,4 +61,28 @@ test("structural plans validate the graph, journal originals, and reject stale e
|
|||||||
await applyStructure(draft);
|
await applyStructure(draft);
|
||||||
assert.match(await fs.readFile(path.join(root, "workspace.qx"), "utf8"), /import interface NotImplementedYet/);
|
assert.match(await fs.readFile(path.join(root, "workspace.qx"), "utf8"), /import interface NotImplementedYet/);
|
||||||
await assert.rejects(() => planStructure(root, { ...provisional, validation: "resource-graph" }));
|
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/,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user