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.
209 lines
12 KiB
TypeScript
209 lines
12 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
|
|
import type { StructuralRequest } from "./structural-plan.js";
|
|
import { parseQx, walkSyntax } from "./source.js";
|
|
import { addImplementation } from "./implementation-edit.js";
|
|
import { formatQuixosLock } from "../resource-lock/index.js";
|
|
import { javascriptTarget } from "../execution-world/native-backends.js";
|
|
const json = (value: unknown) => JSON.stringify(value, null, 2) + "\n";
|
|
const q = (value: string) => JSON.stringify(value);
|
|
const nixString = (value: string) => q(value).replaceAll("${", "\\${");
|
|
export async function isExecutionScaffold(root: string, prefix: string): Promise<boolean> {
|
|
try {
|
|
return JSON.parse(await fs.readFile(path.join(root, prefix, "quixos.check.json"), "utf8")).backend === "execution";
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
throw error;
|
|
}
|
|
}
|
|
/** Insert ordinary authored code; declarations remain the only export inventory. */
|
|
export async function executionScaffold(
|
|
root: string,
|
|
command: string,
|
|
spec: ScaffoldRecipe,
|
|
prefix: string,
|
|
nixSource: (source: NonNullable<ScaffoldRecipe["tools"]>["helpers"]) => string,
|
|
): Promise<StructuralRequest> {
|
|
if (!spec.name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) || !spec.id)
|
|
throw new Error("Execution scaffold requires a simple name and an ID");
|
|
if (spec.initialFiles && command !== "package") throw new Error("Initial files are only for new packages");
|
|
const files: StructuralRequest["files"] = [];
|
|
const create = (file: string, content: string) => files.push({ file: prefix + file, create: content });
|
|
if (command === "package") {
|
|
if (!spec.tools || !spec.revision) throw new Error("Execution package requires exact tool sources and a revision");
|
|
create(
|
|
"package.qx",
|
|
`symbol JavaScript id ${q(javascriptTarget)};\npackage ${spec.name} id ${q(spec.id)} revision ${q(spec.revision)} {\n}\n`,
|
|
);
|
|
create(
|
|
"quixos.lock",
|
|
formatQuixosLock({ formatVersion: 1, quixos: { resolver: "git", ...spec.tools.quixos }, resources: [] }),
|
|
);
|
|
create("quixos.check.json", json({ backend: "execution" }));
|
|
create("components.nix", "{\n}\n");
|
|
create(
|
|
"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"])
|
|
create(
|
|
`src/${lane}.ts`,
|
|
`import { createRegistry } from "./gen/${lane === "remote" ? "remote" : "qx"}.js";\nexport const registry = createRegistry({});\n`,
|
|
);
|
|
create(
|
|
"flake.nix",
|
|
`{
|
|
inputs.protocol.url = ${nixString(nixSource(spec.tools.protocol))};
|
|
inputs.nixpkgs.follows = "protocol/nixpkgs";
|
|
inputs.flake-utils.follows = "protocol/flake-utils";
|
|
inputs.helpers = { url = ${nixString(nixSource(spec.tools.helpers))}; flake = false; };
|
|
outputs = { nixpkgs, flake-utils, helpers, ... }:
|
|
flake-utils.lib.eachDefaultSystem (system: let
|
|
pkgs = import nixpkgs { inherit system; };
|
|
camino = import (helpers + "/quixos-package-helpers.nix");
|
|
in {
|
|
devShells.default = (import (helpers + "/quixos-package-helpers.nix")).mkCaminoYarnShell { inherit pkgs; };
|
|
quixosPackages.checkedArtifacts = context:
|
|
let
|
|
pkgs = camino.packageSet { inherit nixpkgs system context; };
|
|
contract = builtins.fromJSON (builtins.readFile context.buildContract);
|
|
package = contract.definition.package;
|
|
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 (map (target: if target == "target:org.quixos.execution.javascript@1" then "browser" else if target == "server" then "server" else throw "Unsupported TypeScript backend target") (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 = ./.;
|
|
});
|
|
};
|
|
});
|
|
}
|
|
`,
|
|
);
|
|
create(
|
|
"README.md",
|
|
`# ${spec.name}\n\nThis execution package exports checked browser/server modules and a process service.\nUse scaffold function with an explicit execution contract to add a typed handler.\nSnapshot/transaction targets choose browser.ts and/or server.ts; remote handlers\nuse remote.ts. Shared helpers are ordinary imports. Generated bindings are build\noutputs, not authored source. The Nix composition supplies the canonical package contract\nand explicit runtime dependencies to checkedArtifacts.\n\nRun qx-workspace check from this package to build it independently, or from a\nworkspace to also check installed bindings. Activate the resulting report separately through the workspace\nhost. A source-only structural check is not runtime qualification.\n`,
|
|
);
|
|
} else if (command === "function") {
|
|
if (!spec.declaration)
|
|
throw new Error("Execution functions require a declaration with explicit execution effect and targets");
|
|
const wrapped = `package Scaffold id "scaffold" revision "scaffold@1" { ${spec.declaration} }`;
|
|
const parsed = parseQx(wrapped);
|
|
const pkg = parsed.root.children.find((node) => node.kind === "packageResourceDecl");
|
|
const exports = [...walkSyntax(parsed.root)].filter((node) =>
|
|
["packageFunctionExport", "packageOperationExport"].includes(node.kind),
|
|
);
|
|
const declaration = exports[0];
|
|
if (
|
|
parsed.diagnostics.length ||
|
|
exports.length !== 1 ||
|
|
!declaration ||
|
|
parsed.root.children.length !== 1 ||
|
|
pkg?.children.filter((node) => node.kind === "packageExport").length !== 1 ||
|
|
pkg?.children.some((node) => !["identifier", "stringLiteral", "packageExport"].includes(node.kind))
|
|
)
|
|
throw new Error("Expected one function or operation export with an execution contract");
|
|
const text = (node: { start: number; end: number }) => wrapped.slice(node.start, node.end);
|
|
const name = declaration.children.find((node) => node.kind === "identifier");
|
|
const id = declaration.children.find((node) => node.kind === "stringLiteral");
|
|
const execution = declaration.children.find((node) => node.kind === "executionClause");
|
|
if (!name || text(name) !== spec.name || !id || JSON.parse(text(id)) !== spec.id || !execution)
|
|
throw new Error("Declaration name/ID must match; an explicit execution contract is required");
|
|
const tokens = parsed.tokens
|
|
.filter((token) => !token.trivia && token.start >= execution.start && token.end <= execution.end)
|
|
.map((token) => token.text);
|
|
const remote = tokens[1] === "remote";
|
|
const declaredTargets = remote
|
|
? ["remote"]
|
|
: [...walkSyntax(execution)].filter((node) => node.kind === "identifier").map(text);
|
|
const targets = declaredTargets.length ? declaredTargets : ["server"];
|
|
if (
|
|
!targets.length ||
|
|
new Set(targets).size !== targets.length ||
|
|
targets.some((target) => !(remote ? ["remote"] : ["JavaScript", "server"]).includes(target))
|
|
)
|
|
throw new Error("This TypeScript scaffold supports the JavaScript target, server, and remote service handlers");
|
|
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
|
|
const syntax = parseQx(authored);
|
|
const packageNode = syntax.root.children.find((node) => node.kind === "packageResourceDecl");
|
|
if (syntax.diagnostics.length || !packageNode) throw new Error("Fix package.qx before adding a handler");
|
|
for (const node of [...walkSyntax(packageNode)].filter((node) =>
|
|
["packageFunctionExport", "packageOperationExport", "packageConstructorExport"].includes(node.kind),
|
|
)) {
|
|
const identifier = node.children.find((child) => child.kind === "identifier");
|
|
const literal = node.children.find((child) => child.kind === "stringLiteral");
|
|
if (
|
|
(identifier && authored.slice(identifier.start, identifier.end) === spec.name) ||
|
|
(literal && JSON.parse(authored.slice(literal.start, literal.end)) === spec.id)
|
|
)
|
|
throw new Error("New export requires a unique name and ID");
|
|
}
|
|
const packageID = packageNode.children.find((node) => node.kind === "stringLiteral")!;
|
|
files.push({
|
|
file: prefix + "package.qx",
|
|
edits: [
|
|
{
|
|
operation: "append",
|
|
parent: { kind: "packageResourceDecl", id: JSON.parse(authored.slice(packageID.start, packageID.end)) },
|
|
source: spec.declaration,
|
|
},
|
|
],
|
|
});
|
|
create(
|
|
`src/impl/${spec.name}.ts`,
|
|
`import type { ${remote ? "Remote" : "Portable"}Implementation } from "../gen/${remote ? "remote" : "qx"}.js";\nexport const handler: ${remote ? "Remote" : "Portable"}Implementation[${q(spec.name)}] = async (_context) => { throw new Error(${q("Implement " + spec.name)}); };\n`,
|
|
);
|
|
for (const target of targets) {
|
|
const file = prefix + `src/${target === "JavaScript" ? "browser" : target}.ts`;
|
|
const before = await fs.readFile(path.join(root, file), "utf8");
|
|
files.push({
|
|
file,
|
|
expected: before,
|
|
replace: addImplementation(before, "createRegistry", spec.name, `./impl/${spec.name}.js`),
|
|
});
|
|
}
|
|
} else
|
|
throw new Error("Execution packages use ordinary factory transactions; migration scaffolding is not supported");
|
|
if (spec.initialFiles) {
|
|
for (const [file, content] of Object.entries(spec.initialFiles)) {
|
|
if (
|
|
typeof content !== "string" ||
|
|
!/^(?: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.split("/").some((part) => part === ".." || part === "." || part === "gen")
|
|
)
|
|
throw new Error("Not an initial authored execution file: " + file);
|
|
const previous = files.findIndex((entry) => entry.file === prefix + file);
|
|
if (previous >= 0) files.splice(previous, 1);
|
|
create(file, content);
|
|
}
|
|
}
|
|
return { kind: "package", source: spec.source, resourceRoot: spec.directory, validation: "syntax", files };
|
|
}
|