Simplify workspace authoring and isolate managed component styles
This commit is contained in:
+3
-2
@@ -5,11 +5,11 @@
|
|||||||
"packageManager": "yarn@4.18.0",
|
"packageManager": "yarn@4.18.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"quixos-qx": "dist/src/capability-language/tool-cli.js",
|
|
||||||
"quixos-codegen-ts": "dist/src/bindings/cli.js",
|
|
||||||
"quixos-capability-compile": "dist/src/capability-language/cli.js",
|
"quixos-capability-compile": "dist/src/capability-language/cli.js",
|
||||||
|
"quixos-codegen-ts": "dist/src/bindings/cli.js",
|
||||||
"quixos-descriptor-check": "dist/src/descriptor-check.js",
|
"quixos-descriptor-check": "dist/src/descriptor-check.js",
|
||||||
"quixos-lock-check": "dist/src/resource-lock/cli.js",
|
"quixos-lock-check": "dist/src/resource-lock/cli.js",
|
||||||
|
"quixos-qx": "dist/src/capability-language/tool-cli.js",
|
||||||
"quixos-resource-compile": "dist/src/capability-language/resource-cli.js",
|
"quixos-resource-compile": "dist/src/capability-language/resource-cli.js",
|
||||||
"quixos-workspace-compile": "dist/src/capability-language/workspace-cli.js"
|
"quixos-workspace-compile": "dist/src/capability-language/workspace-cli.js"
|
||||||
},
|
},
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
"typecheck": "yarn generate && tsc --noEmit"
|
"typecheck": "yarn generate && tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.28.0",
|
||||||
"@bufbuild/protobuf": "^2.12.1",
|
"@bufbuild/protobuf": "^2.12.1",
|
||||||
"@bufbuild/protoc-gen-es": "^2.12.1",
|
"@bufbuild/protoc-gen-es": "^2.12.1",
|
||||||
"antlr4ng": "^3.0.16"
|
"antlr4ng": "^3.0.16"
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import {parse} from "@babel/parser";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
/** Enforce the authored bundled-code contract, not a security sandbox. */
|
||||||
|
export function bundlePolicyErrors(source: string, filename: string): string[] {
|
||||||
|
const ast = parse(source, {sourceType: "module", plugins: ["typescript", "jsx"]});
|
||||||
|
const errors: string[] = [];
|
||||||
|
const visit = (value: unknown) => {
|
||||||
|
if (Array.isArray(value)) {value.forEach(visit); return;}
|
||||||
|
if (!value || typeof value !== "object") return;
|
||||||
|
const n = value as Record<string, any>;
|
||||||
|
if (typeof n.type !== "string") return;
|
||||||
|
const fail = (message: string) => errors.push(`${filename}:${n.loc?.start.line ?? 1}: ${message}`);
|
||||||
|
if (n.type === "MetaProperty" && n.meta.name === "import") fail("Bundled package code cannot use import.meta; import packaged assets statically");
|
||||||
|
if (n.type === "Identifier" && ["__dirname", "__filename"].includes(n.name)) fail("Bundled package code cannot depend on module filesystem locations");
|
||||||
|
if (["CallExpression", "NewExpression"].includes(n.type)) {
|
||||||
|
if (n.callee?.type === "Identifier" && ["eval", "Function"].includes(n.callee.name)) fail("Dynamic code generation is unsupported in bundled package code");
|
||||||
|
if ((n.callee?.type === "Import" || (n.callee?.type === "Identifier" && n.callee.name === "require")) &&
|
||||||
|
(n.arguments.length !== 1 || n.arguments[0].type !== "StringLiteral")) fail("Module imports must have a static string specifier");
|
||||||
|
}
|
||||||
|
if (n.type === "ImportExpression" && n.source.type !== "StringLiteral") fail("Module imports must have a static string specifier");
|
||||||
|
Object.values(n).forEach(visit);
|
||||||
|
};
|
||||||
|
visit(ast);
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
export async function checkBundleSources(directory: string): Promise<void> {
|
||||||
|
const errors: string[] = [];
|
||||||
|
async function walk(current: string) {
|
||||||
|
for (const entry of await fs.readdir(current, {withFileTypes: true})) {
|
||||||
|
if (["gen", "node_modules"].includes(entry.name)) continue;
|
||||||
|
const file = path.join(current, entry.name);
|
||||||
|
if (entry.isSymbolicLink()) throw new Error(`Authored source symlinks are unsupported: ${file}`);
|
||||||
|
if (entry.isDirectory()) await walk(file);
|
||||||
|
else if (/\.[cm]?[jt]sx?$/.test(entry.name) && !entry.name.endsWith(".d.ts")) errors.push(...bundlePolicyErrors(await fs.readFile(file, "utf8"), file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await walk(directory);
|
||||||
|
if (errors.length) throw new Error(errors.join("\n"));
|
||||||
|
}
|
||||||
@@ -18,6 +18,12 @@ export type TypeScriptBindingOptions = {
|
|||||||
/** Each export must implement MessageBinding<T>, providing both TS type and wire codec. */
|
/** Each export must implement MessageBinding<T>, providing both TS type and wire codec. */
|
||||||
messages?: Record<string, { module: string; export: string }>;
|
messages?: Record<string, { module: string; export: string }>;
|
||||||
};
|
};
|
||||||
|
export function generatePackageDescriptor(schema: BindingSchema, revisionId: string): string {
|
||||||
|
const pkg = schema.packages.find(entry => entry.revisionId === revisionId);
|
||||||
|
if (!pkg) throw new Error(`Unknown package revision ${revisionId}`);
|
||||||
|
return `# Generated from the checked package contract\npackage_id: ${JSON.stringify(pkg.packageId)}\npackage_revision_id: ${JSON.stringify(pkg.revisionId)}\nruntime_protocol_version: "quixos-capabilities-v1"\n` +
|
||||||
|
pkg.exports.map(entry => `exports: { export_id: ${JSON.stringify(entry.id)} runtime_symbol: ${JSON.stringify(entry.displayName)} }\n`).join("");
|
||||||
|
}
|
||||||
const q = JSON.stringify;
|
const q = JSON.stringify;
|
||||||
const object = (entries: [string, string][]) => `{ ${entries.map(([key, value]) => `${q(key)}: ${value}`).join("; ")} }`;
|
const object = (entries: [string, string][]) => `{ ${entries.map(([key, value]) => `${q(key)}: ${value}`).join("; ")} }`;
|
||||||
const unit = (type: ValueType) => type.kind === "builtin" && type.name === "unit";
|
const unit = (type: ValueType) => type.kind === "builtin" && type.name === "unit";
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { planEvolution, type WorkspaceRevision, type EvolutionReview } from "../
|
|||||||
|
|
||||||
export const checkRecordName = (directory: string) => createHash("sha256").update(directory).digest("hex") + ".json";
|
export const checkRecordName = (directory: string) => createHash("sha256").update(directory).digest("hex") + ".json";
|
||||||
export async function checkAuthoring(start: string, output: string, options: { baseline?: string; reviews?: string; contractOnly?: boolean } = {}) {
|
export async function checkAuthoring(start: string, output: string, options: { baseline?: string; reviews?: string; contractOnly?: boolean } = {}) {
|
||||||
|
const started = performance.now();
|
||||||
|
const timings: Record<string, number> = {};
|
||||||
const context = await authoringContext(start);
|
const context = await authoringContext(start);
|
||||||
const location = await fs.realpath(start);
|
const location = await fs.realpath(start);
|
||||||
const directory = location === context.workbench ? "root" : path.relative(context.workbench, location);
|
const directory = location === context.workbench ? "root" : path.relative(context.workbench, location);
|
||||||
@@ -29,13 +31,16 @@ export async function checkAuthoring(start: string, output: string, options: { b
|
|||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
const converged = JSON.parse(captured.stdout) as Awaited<ReturnType<typeof convergeAuthoring>>;
|
const converged = JSON.parse(captured.stdout) as Awaited<ReturnType<typeof convergeAuthoring>>;
|
||||||
|
timings.captureMs = Math.round(performance.now() - started);
|
||||||
if (!converged.candidate) {
|
if (!converged.candidate) {
|
||||||
report.phase = converged.worklist.find(entry => entry.phase !== "dependency")?.phase ?? "convergence";
|
report.phase = converged.worklist.find(entry => entry.phase !== "dependency")?.phase ?? "convergence";
|
||||||
throw new Error(converged.worklist.map(entry => `${entry.directory} [${entry.phase}]: ${entry.message}`).join("\n"));
|
throw new Error(converged.worklist.map(entry => `${entry.directory} [${entry.phase}]: ${entry.message}`).join("\n"));
|
||||||
}
|
}
|
||||||
report.commit = converged.candidate.commit;
|
report.commit = converged.candidate.commit;
|
||||||
report.phase = "verification";
|
report.phase = "verification";
|
||||||
|
const buildStarted = performance.now();
|
||||||
report.artifactPath = await buildImmutableCandidate(converged.candidate, resource.kind, path.join(output, "nix.log"), options.contractOnly);
|
report.artifactPath = await buildImmutableCandidate(converged.candidate, resource.kind, path.join(output, "nix.log"), options.contractOnly);
|
||||||
|
timings.immutableCheckMs = Math.round(performance.now() - buildStarted);
|
||||||
const candidateText = await fs.readFile(path.join(report.artifactPath, "candidate.json"), "utf8");
|
const candidateText = await fs.readFile(path.join(report.artifactPath, "candidate.json"), "utf8");
|
||||||
await fs.writeFile(path.join(output, "candidate.json"), candidateText);
|
await fs.writeFile(path.join(output, "candidate.json"), candidateText);
|
||||||
report.compilation = "passed";
|
report.compilation = "passed";
|
||||||
@@ -59,6 +64,8 @@ export async function checkAuthoring(start: string, output: string, options: { b
|
|||||||
}
|
}
|
||||||
if (!report.blockers.length) report.phase = options.contractOnly ? "contract-only" : "checked";
|
if (!report.blockers.length) report.phase = options.contractOnly ? "contract-only" : "checked";
|
||||||
} catch (error) { report.blockers.push(String(error instanceof Error ? error.message : error)); }
|
} catch (error) { report.blockers.push(String(error instanceof Error ? error.message : error)); }
|
||||||
|
timings.totalMs = Math.round(performance.now() - started);
|
||||||
|
Object.assign(report, {timings});
|
||||||
await fs.writeFile(path.join(output, "report.json"), JSON.stringify(report, null, 2));
|
await fs.writeFile(path.join(output, "report.json"), JSON.stringify(report, null, 2));
|
||||||
if (options.contractOnly) return report;
|
if (options.contractOnly) return report;
|
||||||
const records = path.join(context.workbench, ".quixos/checks");
|
const records = path.join(context.workbench, ".quixos/checks");
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import {parse} from "@babel/parser";
|
||||||
|
type Node = {type: string; start: number; end: number; [key: string]: unknown};
|
||||||
|
const node = (value: unknown): value is Node => !!value && typeof value === "object" && typeof (value as Node).type === "string";
|
||||||
|
|
||||||
|
/** One requested insertion into ordinary authored TypeScript, not regeneration.
|
||||||
|
* Ambiguous/custom wiring is left alone with an actionable error. */
|
||||||
|
export function addImplementation(text: string, factory: "createRuntime" | "serveMigration", key: string, importPath: string, migrationOnly = false): string {
|
||||||
|
const ast = parse(text, {sourceType: "module", plugins: ["typescript"]});
|
||||||
|
const objects: Node[] = [];
|
||||||
|
const names = new Set<string>();
|
||||||
|
const visit = (value: unknown) => {
|
||||||
|
if (Array.isArray(value)) { value.forEach(visit); return; }
|
||||||
|
if (!node(value)) return;
|
||||||
|
if (value.type === "Identifier") names.add(String(value.name));
|
||||||
|
if (value.type === "CallExpression" && node(value.callee) && value.callee.type === "Identifier" && value.callee.name === factory &&
|
||||||
|
Array.isArray(value.arguments) && value.arguments.length === 1 && node(value.arguments[0]) && value.arguments[0].type === "ObjectExpression") objects.push(value.arguments[0]);
|
||||||
|
Object.values(value).forEach(visit);
|
||||||
|
};
|
||||||
|
visit(ast);
|
||||||
|
if (objects.length !== 1) throw new Error(`Cannot safely add implementation: expected one ${factory}({...}) literal. Wire the handler in your authored server code instead.`);
|
||||||
|
const object = objects[0];
|
||||||
|
if ((object.properties as Node[]).some(p => node(p.key) && !p.computed && (p.key.name === key || p.key.value === key))) throw new Error(`Implementation already exists for ${key}`);
|
||||||
|
let alias = "qxImplementation";
|
||||||
|
for (let index = 1; names.has(alias); index++) alias = `qxImplementation${index}`;
|
||||||
|
const value = migrationOnly ? 'async () => { throw new Error("Migration-only export"); }' : alias;
|
||||||
|
const insertion = object.start + 1;
|
||||||
|
const edited = text.slice(0, insertion) + `\n ${JSON.stringify(key)}: ${value},\n` + text.slice(insertion);
|
||||||
|
return (migrationOnly ? "" : `import {handler as ${alias}} from ${JSON.stringify(importPath)};\n`) + edited;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import {contentDigest} from "../capability-model/evolution.js";
|
||||||
|
import {validateMigrationCatalog, type MigrationCatalog} from "../capability-model/migrations.js";
|
||||||
|
import {applyStructure, planStructure} from "./structural-plan.js";
|
||||||
|
|
||||||
|
/** Explicitly acknowledge edited migration code. This does not change retained
|
||||||
|
* contracts or grant activation approval; the immutable checker verifies it. */
|
||||||
|
export async function sealMigrations(directory: string) {
|
||||||
|
const root = await fs.realpath(directory);
|
||||||
|
const file = "quixos.migrations.json";
|
||||||
|
const before = await fs.readFile(path.join(root, file), "utf8");
|
||||||
|
const catalog = JSON.parse(before) as MigrationCatalog;
|
||||||
|
for (const migration of catalog.migrations) {
|
||||||
|
const implementation = await fs.realpath(path.join(root, migration.implementation.file));
|
||||||
|
if (!implementation.startsWith(root + path.sep)) throw new Error("Migration implementation escapes package");
|
||||||
|
migration.implementation.digest = contentDigest(await fs.readFile(implementation, "utf8"));
|
||||||
|
}
|
||||||
|
validateMigrationCatalog(catalog);
|
||||||
|
return applyStructure(await planStructure(root, {kind: "package", validation: "syntax", files: [
|
||||||
|
{file, expected: before, replace: JSON.stringify(catalog, null, 2) + "\n"},
|
||||||
|
]}));
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import {validateMigrationCatalog, type MigrationCatalog, type MigrationDeclarati
|
|||||||
import {formatQuixosLock, type GitSource} from "../resource-lock/index.js";
|
import {formatQuixosLock, type GitSource} from "../resource-lock/index.js";
|
||||||
import {parseQx, walkSyntax} from "./source.js";
|
import {parseQx, walkSyntax} from "./source.js";
|
||||||
import type {StructuralRequest} from "./structural-plan.js";
|
import type {StructuralRequest} from "./structural-plan.js";
|
||||||
|
import {addImplementation} from "./implementation-edit.js";
|
||||||
|
|
||||||
type Source = {repository: string; commit: string};
|
type Source = {repository: string; commit: string};
|
||||||
type Registry = {generatedBy: "qx-scaffold-v1"; name: string; id: string; revision: string; exports: {name: string; id: string; file: string; migration?: boolean}[]};
|
type Registry = {generatedBy: "qx-scaffold-v1"; name: string; id: string; revision: string; exports: {name: string; id: string; file: string; migration?: boolean}[]};
|
||||||
@@ -19,6 +20,7 @@ export type ScaffoldRecipe = {
|
|||||||
const marker = "// Generated by qx-scaffold-v1\n";
|
const marker = "// Generated by qx-scaffold-v1\n";
|
||||||
const json = (value: unknown) => `${JSON.stringify(value, null, 2)}\n`;
|
const json = (value: unknown) => `${JSON.stringify(value, null, 2)}\n`;
|
||||||
const source = (value: Source): GitSource => {
|
const source = (value: Source): GitSource => {
|
||||||
|
if (!value || typeof value.repository !== "string" || typeof value.commit !== "string") throw new Error("Scaffold requires an exact source identity; use qx-workspace from a registered repository");
|
||||||
const url = new URL(value.repository);
|
const url = new URL(value.repository);
|
||||||
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(value.commit)) throw new Error("Scaffolds require credential-free HTTPS sources and full exact commits");
|
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(value.commit)) throw new Error("Scaffolds require credential-free HTTPS sources and full exact commits");
|
||||||
return {resolver: "git", ...value};
|
return {resolver: "git", ...value};
|
||||||
@@ -40,6 +42,7 @@ const ownedJson = async <T>(root: string, file: string): Promise<T> => {
|
|||||||
/** Recipes describe structural edits; planStructure owns validation/journaling.
|
/** Recipes describe structural edits; planStructure owns validation/journaling.
|
||||||
* Implementation files are created once and never rewritten by refresh. */
|
* Implementation files are created once and never rewritten by refresh. */
|
||||||
export const scaffoldRecipe = async (root: string, command: "package" | "function" | "migration" | "refresh", spec: ScaffoldRecipe): Promise<StructuralRequest> => {
|
export const scaffoldRecipe = async (root: string, command: "package" | "function" | "migration" | "refresh", spec: ScaffoldRecipe): Promise<StructuralRequest> => {
|
||||||
|
if (command === "refresh") throw new Error("Scaffold refresh has been removed. Edit declarations and typed server wiring directly, then run qx-workspace check. Use scaffold function to add a declaration and handler together.");
|
||||||
source(spec.source);
|
source(spec.source);
|
||||||
if (spec.directory && !/^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/.test(spec.directory)) throw new Error("Scaffold directory must be contained");
|
if (spec.directory && !/^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/.test(spec.directory)) throw new Error("Scaffold directory must be contained");
|
||||||
const prefix = spec.directory ? `${spec.directory}/` : "";
|
const prefix = spec.directory ? `${spec.directory}/` : "";
|
||||||
@@ -60,13 +63,13 @@ export const scaffoldRecipe = async (root: string, command: "package" | "functio
|
|||||||
create("package.qx", `package ${name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n${react ? ` function sourceGet id ${JSON.stringify(`export:${name}:source`)} : unit -> string;\n` : ""}}\n`);
|
create("package.qx", `package ${name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n${react ? ` function sourceGet id ${JSON.stringify(`export:${name}:source`)} : unit -> string;\n` : ""}}\n`);
|
||||||
create("quixos.lock", formatQuixosLock({formatVersion: 1, quixos: source(spec.tools.quixos), resources: []}));
|
create("quixos.lock", formatQuixosLock({formatVersion: 1, quixos: source(spec.tools.quixos), resources: []}));
|
||||||
create("package.json", json({name: `@quixos/${name.toLowerCase()}`, version: "0.1.0", private: true, type: "module", packageManager: "yarn@4.18.0",
|
create("package.json", json({name: `@quixos/${name.toLowerCase()}`, version: "0.1.0", private: true, type: "module", packageManager: "yarn@4.18.0",
|
||||||
scripts: {build: `tsc -p tsconfig.json${react ? " && node scripts/build-component.mjs" : ""}`, typecheck: "tsc --noEmit"}, dependencies: {"@quixos/camino-package-runtime": `${spec.tools.sdk.repository}#commit=${spec.tools.sdk.commit}`},
|
scripts: {build: "tsc -p tsconfig.json", typecheck: "tsc --noEmit"}, dependencies: {"@quixos/camino-package-runtime": `${spec.tools.sdk.repository}#commit=${spec.tools.sdk.commit}`},
|
||||||
devDependencies: {"@types/node": "^24", typescript: "^7.0.2", ...(react ? {react: "^18.3.1", "@types/react": "^18.3.12", esbuild: "^0.25.12"} : {})}}));
|
devDependencies: {"@types/node": "^24", typescript: "^7.0.2", ...(react ? {react: "^18.3.1", "@types/react": "^18.3.12", esbuild: "^0.25.12"} : {})}}));
|
||||||
create("tsconfig.json", json({compilerOptions: {target: "ES2023", module: "NodeNext", moduleResolution: "NodeNext", strict: true, types: ["node", ...(react ? ["react"] : [])], outDir: "dist", rootDir: "src", skipLibCheck: true, ...(react ? {jsx: "react-jsx", esModuleInterop: true} : {})}, include: ["src/**/*.ts", "src/**/*.tsx"]}));
|
create("tsconfig.json", json({compilerOptions: {target: "ES2023", module: "NodeNext", moduleResolution: "NodeNext", strict: true, types: ["node", ...(react ? ["react"] : [])], outDir: "dist", rootDir: "src", skipLibCheck: true, ...(react ? {jsx: "react-jsx", esModuleInterop: true} : {})}, include: ["src/**/*.ts", "src/**/*.tsx"]}));
|
||||||
if (react) {
|
if (react) {
|
||||||
create("src/component.tsx", `// Props are opaque at the platform boundary until capability generics exist.\nexport default function Component(_props: {camino: unknown; render: unknown; dispatch: (action: unknown) => void}) {\n return <section><h1>${name}</h1><p>Edit this component, then run qx-workspace check.</p></section>;\n}\n`);
|
create("src/component.tsx", `// Props are opaque at the platform boundary until capability generics exist.\nexport default function Component(_props: {camino: unknown; render: unknown; dispatch: (action: unknown) => void}) {\n return <section><h1>${name}</h1><p>Edit this component, then run qx-workspace check.</p></section>;\n}\n`);
|
||||||
create("src/impl/sourceGet.ts", `import {readFile} from "node:fs/promises";\nimport type {Implementation} from "../gen/qx.js";\nexport const handler: Implementation["sourceGet"] = () => readFile(new URL("./component.mjs", import.meta.url), "utf8");\n`);
|
create("src/impl/sourceGet.ts", `import componentSource from "../component.js?browser-source";\nimport type {Implementation} from "../gen/qx.js";\nexport const handler: Implementation["sourceGet"] = () => componentSource;\n`);
|
||||||
create("scripts/build-component.mjs", `import {build} from "esbuild";\nconst platform = new Map([\n ["react", "/__quixos/platform/react/v18.mjs"],\n ["react/jsx-runtime", "/__quixos/platform/react-jsx-runtime/v18.mjs"],\n ["react/jsx-dev-runtime", "/__quixos/platform/react-jsx-dev-runtime/v18.mjs"],\n ["@quixos/web-studio-react-runtime", "/__quixos/platform/web-studio-react-runtime/v1.mjs"],\n]);\nawait build({entryPoints: ["dist/component.js"], outfile: "dist/component.mjs", bundle: true, format: "esm", platform: "browser", target: "es2022", plugins: [{name: "quixos-platform", setup(api) {api.onResolve({filter: /.*/}, ({path}) => platform.has(path) ? {path: platform.get(path), external: true} : undefined);}}]});\n`);
|
create("src/browser-assets.d.ts", `declare module "*?browser-source" { const source: string; export default source; }\n`);
|
||||||
}
|
}
|
||||||
create(".gitignore", "node_modules/\ndist/\n.quixos/\nresult\n.yarn/install-state.gz\n");
|
create(".gitignore", "node_modules/\ndist/\n.quixos/\nresult\n.yarn/install-state.gz\n");
|
||||||
create(".yarnrc.yml", `nodeLinker: node-modules\nenableScripts: true\nnpmMinimalAgeGate: 0\napprovedGitRepositories:\n - ${JSON.stringify(spec.tools.sdk.repository)}\nsupportedArchitectures:\n os: [current, linux]\n cpu: [current, x64, arm64]\n libc: [current, glibc]\n`);
|
create(".yarnrc.yml", `nodeLinker: node-modules\nenableScripts: true\nnpmMinimalAgeGate: 0\napprovedGitRepositories:\n - ${JSON.stringify(spec.tools.sdk.repository)}\nsupportedArchitectures:\n os: [current, linux]\n cpu: [current, x64, arm64]\n libc: [current, glibc]\n`);
|
||||||
@@ -74,7 +77,7 @@ export const scaffoldRecipe = async (root: string, command: "package" | "functio
|
|||||||
const plugin = new URL(nixifyPluginUrl);
|
const plugin = new URL(nixifyPluginUrl);
|
||||||
if (plugin.protocol !== "https:" || plugin.username || plugin.password || plugin.search || plugin.hash || !/\/commit\/[a-f0-9]{40,64}\//.test(plugin.pathname)) throw new Error("Nixify plugin must have an exact credential-free HTTPS commit URL");
|
if (plugin.protocol !== "https:" || plugin.username || plugin.password || plugin.search || plugin.hash || !/\/commit\/[a-f0-9]{40,64}\//.test(plugin.pathname)) throw new Error("Nixify plugin must have an exact credential-free HTTPS commit URL");
|
||||||
generated("quixos.toolchain.json", json({generatedBy: "qx-scaffold-v1", nixifyPluginUrl}));
|
generated("quixos.toolchain.json", json({generatedBy: "qx-scaffold-v1", nixifyPluginUrl}));
|
||||||
create("quixos.check.json", json({backend: "typescript", bindingOutput: "src/gen/qx.ts"}));
|
create("quixos.check.json", json({backend: "typescript", bindingOutput: "src/gen/qx.ts", ...(react ? {options: {messages: {"org.quixos.web-studio.ReactProps": {module: "@quixos/camino-package-runtime", export: "opaqueReactPropsBinding"}}}} : {})}));
|
||||||
create("flake.nix", `{
|
create("flake.nix", `{
|
||||||
inputs.protocol.url = ${nixString(nixSource(spec.tools.protocol))};
|
inputs.protocol.url = ${nixString(nixSource(spec.tools.protocol))};
|
||||||
inputs.nixpkgs.follows = "protocol/nixpkgs";
|
inputs.nixpkgs.follows = "protocol/nixpkgs";
|
||||||
@@ -83,45 +86,32 @@ export const scaffoldRecipe = async (root: string, command: "package" | "functio
|
|||||||
outputs = inputs@{ self, protocol, nixpkgs, flake-utils, helpers, ... }:
|
outputs = inputs@{ self, protocol, nixpkgs, flake-utils, helpers, ... }:
|
||||||
(import (toString helpers + "/quixos-package-helpers.nix")).mkCaminoTsYarnNixifyFlake {
|
(import (toString helpers + "/quixos-package-helpers.nix")).mkCaminoTsYarnNixifyFlake {
|
||||||
inherit inputs nixpkgs flake-utils; packageRoot = ./.;
|
inherit inputs nixpkgs flake-utils; packageRoot = ./.;
|
||||||
bundle = { entry = "dist/server.js"; };
|
bundle = { entry = "dist/server.js"; ${react ? "browserSources = true;" : ""} };
|
||||||
migrationEntrypoint = "dist/migrate.js";
|
migrationEntrypoint = "dist/migrate.js";
|
||||||
installServer = { libexecName = ${JSON.stringify(name.toLowerCase())}; descriptorPath = "descriptor.quixos-package.txtpb"; ${react ? 'extraFiles = [ { source = "dist/component.mjs"; target = "component.mjs"; } ];' : ""} };
|
installServer = { libexecName = ${JSON.stringify(name.toLowerCase())}; descriptorPath = "descriptor.quixos-package.txtpb"; };
|
||||||
};
|
};
|
||||||
}\n`);
|
}\n`);
|
||||||
} else {
|
} else {
|
||||||
registry = await ownedJson<Registry>(root, prefix + "quixos.scaffold.json");
|
|
||||||
catalog = await ownedJson<typeof catalog>(root, prefix + "quixos.migrations.json");
|
catalog = await ownedJson<typeof catalog>(root, prefix + "quixos.migrations.json");
|
||||||
// package.qx is authoritative. The registry remembers implementation paths,
|
// Derive the edit model from authored declarations; it is never persisted.
|
||||||
// not a second declaration list that can erase an author's new exports.
|
|
||||||
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
|
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
|
||||||
const syntax = parseQx(authored);
|
const syntax = parseQx(authored);
|
||||||
if (syntax.diagnostics.length) throw new Error("Cannot refresh an invalid package.qx; fix the reported syntax first");
|
if (syntax.diagnostics.length) throw new Error("Cannot scaffold into an invalid package.qx; fix the reported syntax first");
|
||||||
const declaration = [...walkSyntax(syntax.root)].find(node => node.kind === "packageResourceDecl");
|
const declaration = [...walkSyntax(syntax.root)].find(node => node.kind === "packageResourceDecl");
|
||||||
if (!declaration) throw new Error("Expected a package declaration");
|
if (!declaration) throw new Error("Expected a package declaration");
|
||||||
const text = (node: typeof declaration) => authored.slice(node.start, node.end);
|
const text = (node: typeof declaration) => authored.slice(node.start, node.end);
|
||||||
const literals = declaration.children.filter(node => node.kind === "stringLiteral");
|
const literals = declaration.children.filter(node => node.kind === "stringLiteral");
|
||||||
registry.name = text(declaration.children.find(node => node.kind === "identifier")!);
|
registry = {generatedBy: "qx-scaffold-v1", name: text(declaration.children.find(node => node.kind === "identifier")!),
|
||||||
registry.id = JSON.parse(text(literals[0]));
|
id: JSON.parse(text(literals[0])), revision: JSON.parse(text(literals[1])), exports: []};
|
||||||
registry.revision = JSON.parse(text(literals[1]));
|
|
||||||
const previousExports = registry.exports;
|
|
||||||
registry.exports = [];
|
|
||||||
for (const node of walkSyntax(declaration)) {
|
for (const node of walkSyntax(declaration)) {
|
||||||
if (!["packageFunctionExport", "packageOperationExport", "packageConstructorExport"].includes(node.kind)) continue;
|
if (!["packageFunctionExport", "packageOperationExport", "packageConstructorExport"].includes(node.kind)) continue;
|
||||||
const name = safeName(text(node.children.find(child => child.kind === "identifier")!));
|
const name = safeName(text(node.children.find(child => child.kind === "identifier")!));
|
||||||
const id = JSON.parse(text(node.children.find(child => child.kind === "stringLiteral")!));
|
const id = JSON.parse(text(node.children.find(child => child.kind === "stringLiteral")!));
|
||||||
if (registry.exports.some(entry => entry.id === id || entry.name === name)) throw new Error("Duplicate package export name or ID");
|
if (registry.exports.some(entry => entry.id === id || entry.name === name)) throw new Error("Duplicate package export name or ID");
|
||||||
const old = previousExports.find(entry => entry.id === id);
|
const migration = catalog.migrations.find(entry => entry.implementation.exportId === id);
|
||||||
const file = old?.file ?? `src/impl/${name}.ts`;
|
registry.exports.push({name, id, file: migration?.implementation.file ?? `src/impl/${name}.ts`, ...(migration ? {migration: true} : {})});
|
||||||
if (!old) {
|
|
||||||
const exists = await fs.access(path.join(root, prefix, file)).then(() => true, () => false);
|
|
||||||
if (!exists) {
|
|
||||||
const derived = [...walkSyntax(node)].some(child => child.kind === "eventClause");
|
|
||||||
create(file, `import type {Implementation} from "../gen/qx.js";\nexport const handler: Implementation[${JSON.stringify(name)}] = ${derived ? '{kind: "derived", get: ' : ""}async (_context) => { throw new Error(${JSON.stringify(`Implement ${name}`)}); }${derived ? "}" : ""};\n`);
|
|
||||||
}
|
}
|
||||||
}
|
{
|
||||||
registry.exports.push({...old, name, id, file});
|
|
||||||
}
|
|
||||||
if (command !== "refresh") {
|
|
||||||
const name = safeName(spec.name);
|
const name = safeName(spec.name);
|
||||||
if (!spec.id || registry.exports.some((entry) => entry.id === spec.id || entry.name === name)) throw new Error("New export requires a unique name and ID");
|
if (!spec.id || registry.exports.some((entry) => entry.id === spec.id || entry.name === name)) throw new Error("New export requires a unique name and ID");
|
||||||
const declaration = spec.declaration ?? `function ${name} id ${JSON.stringify(spec.id)} : unit -> unit;`;
|
const declaration = spec.declaration ?? `function ${name} id ${JSON.stringify(spec.id)} : unit -> unit;`;
|
||||||
@@ -150,19 +140,25 @@ export const scaffoldRecipe = async (root: string, command: "package" | "functio
|
|||||||
catalog.migrations.push({...transition, implementation: {exportId: spec.id, file, digest: contentDigest(implementation)}});
|
catalog.migrations.push({...transition, implementation: {exportId: spec.id, file, digest: contentDigest(implementation)}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (command === "refresh") for (const migration of catalog.migrations) {
|
|
||||||
const file = path.join(root, prefix, migration.implementation.file);
|
|
||||||
if (!(await fs.realpath(file)).startsWith(`${await fs.realpath(path.join(root, prefix))}/`)) throw new Error("Migration implementation escapes package");
|
|
||||||
migration.implementation.digest = contentDigest(await fs.readFile(file, "utf8"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
validateMigrationCatalog(catalog, new Set(registry.exports.map((entry) => entry.id)));
|
validateMigrationCatalog(catalog, new Set(registry.exports.map((entry) => entry.id)));
|
||||||
generated("quixos.scaffold.json", json(registry));
|
|
||||||
generated("quixos.migrations.json", json(catalog));
|
generated("quixos.migrations.json", json(catalog));
|
||||||
|
if (command !== "package") {
|
||||||
|
const entry = registry.exports.at(-1)!;
|
||||||
|
const file = prefix + "src/server.ts";
|
||||||
|
const before = await fs.readFile(path.join(root, file), "utf8");
|
||||||
|
files.push({file, expected: before, replace: addImplementation(before, "createRuntime", entry.name, `./${entry.file.slice(4, -3)}.js`, !!entry.migration)});
|
||||||
|
if (entry.migration) {
|
||||||
|
const file = prefix + "src/migrate.ts";
|
||||||
|
const before = await fs.readFile(path.join(root, file), "utf8");
|
||||||
|
files.push({file, expected: before, replace: addImplementation(before, "serveMigration", entry.id, `./${entry.file.slice(4, -3)}.js`)});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
generated("src/server.ts", marker + `import {servePackageRuntime} from "@quixos/camino-package-runtime";\nimport {createRuntime} from "./gen/qx.js";\n` + registry.exports.filter((entry) => !entry.migration).map((entry, index) => `import {handler as impl${index}} from ${JSON.stringify(`./${entry.file.slice(4, -3)}.js`)};\n`).join("") +
|
generated("src/server.ts", marker + `import {servePackageRuntime} from "@quixos/camino-package-runtime";\nimport {createRuntime} from "./gen/qx.js";\n` + registry.exports.filter((entry) => !entry.migration).map((entry, index) => `import {handler as impl${index}} from ${JSON.stringify(`./${entry.file.slice(4, -3)}.js`)};\n`).join("") +
|
||||||
`servePackageRuntime(createRuntime({\n` + registry.exports.map((entry) => ` ${JSON.stringify(entry.name)}: ${entry.migration ? 'async () => { throw new Error("Migration-only export"); }' : `impl${registry.exports.filter((value) => !value.migration).indexOf(entry)}`},`).join("\n") + `\n}));\n`);
|
`servePackageRuntime(createRuntime({\n` + registry.exports.map((entry) => ` ${JSON.stringify(entry.name)}: ${entry.migration ? 'async () => { throw new Error("Migration-only export"); }' : `impl${registry.exports.filter((value) => !value.migration).indexOf(entry)}`},`).join("\n") + `\n}));\n`);
|
||||||
const migrations = registry.exports.filter((entry) => entry.migration);
|
const migrations = registry.exports.filter((entry) => entry.migration);
|
||||||
generated("src/migrate.ts", marker + `import {serveMigration} from "@quixos/camino-package-runtime";\n` + migrations.map((entry, index) => `import {handler as impl${index}} from ${JSON.stringify(`./${entry.file.slice(4, -3)}.js`)};\n`).join("") + `await serveMigration({${migrations.map((entry, index) => `${JSON.stringify(entry.id)}: impl${index}`).join(", ")}});\n`);
|
generated("src/migrate.ts", marker + `import {serveMigration} from "@quixos/camino-package-runtime";\n` + migrations.map((entry, index) => `import {handler as impl${index}} from ${JSON.stringify(`./${entry.file.slice(4, -3)}.js`)};\n`).join("") + `await serveMigration({${migrations.map((entry, index) => `${JSON.stringify(entry.id)}: impl${index}`).join(", ")}});\n`);
|
||||||
|
}
|
||||||
generated("descriptor.quixos-package.txtpb", `# Generated by qx-scaffold-v1\npackage_id: ${JSON.stringify(registry.id)}\npackage_revision_id: ${JSON.stringify(registry.revision)}\nruntime_protocol_version: "quixos-capabilities-v1"\n` + registry.exports.map((entry) => `exports: { export_id: ${JSON.stringify(entry.id)} runtime_symbol: ${JSON.stringify(entry.name)} }\n`).join(""));
|
generated("descriptor.quixos-package.txtpb", `# Generated by qx-scaffold-v1\npackage_id: ${JSON.stringify(registry.id)}\npackage_revision_id: ${JSON.stringify(registry.revision)}\nruntime_protocol_version: "quixos-capabilities-v1"\n` + registry.exports.map((entry) => `exports: { export_id: ${JSON.stringify(entry.id)} runtime_symbol: ${JSON.stringify(entry.name)} }\n`).join(""));
|
||||||
return {kind: "package", source: spec.source, resourceRoot: spec.directory, files};
|
return {kind: "package", source: spec.source, resourceRoot: spec.directory, validation: "syntax", files};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export type StructuralRequest = {
|
|||||||
source?: {repository: string; commit: string};
|
source?: {repository: string; commit: string};
|
||||||
resourceRoot?: string;
|
resourceRoot?: string;
|
||||||
validation?: "syntax" | "resource-graph";
|
validation?: "syntax" | "resource-graph";
|
||||||
files: ({file: string; edits: StructuralEdit[]} | {file: string; create: string} | {file: string; generated: string})[];
|
files: ({file: string; edits: StructuralEdit[]} | {file: string; create: string} | {file: string; generated: string} | {file: string; expected: string; replace: string})[];
|
||||||
};
|
};
|
||||||
type Change = {file: string; before: string | null; after: string; mode: number};
|
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[]};
|
||||||
@@ -70,6 +70,9 @@ export const planStructure = async (rootPath: string, request: StructuralRequest
|
|||||||
if ("create" in input) {
|
if ("create" in input) {
|
||||||
if (before !== null || typeof input.create !== "string") throw new Error("Scaffold creation cannot replace an existing file");
|
if (before !== null || typeof input.create !== "string") throw new Error("Scaffold creation cannot replace an existing file");
|
||||||
after = input.create;
|
after = input.create;
|
||||||
|
} else if ("replace" in input) {
|
||||||
|
if (before !== input.expected || typeof input.replace !== "string") throw new Error(`Stale imperative edit: ${input.file}`);
|
||||||
|
after = input.replace;
|
||||||
} else if ("generated" in input) {
|
} else if ("generated" in input) {
|
||||||
const generated = (text: string) => text.startsWith("// Generated by qx-scaffold-v1\n") || text.startsWith("# Generated by qx-scaffold-v1\n") || (() => {try {return JSON.parse(text).generatedBy === "qx-scaffold-v1";} catch {return false;}})();
|
const generated = (text: string) => text.startsWith("// Generated by qx-scaffold-v1\n") || text.startsWith("# Generated by qx-scaffold-v1\n") || (() => {try {return JSON.parse(text).generatedBy === "qx-scaffold-v1";} catch {return false;}})();
|
||||||
if (typeof input.generated !== "string" || !generated(input.generated) || (before !== null && !generated(before))) throw new Error("Only scaffold-owned generated files may be regenerated");
|
if (typeof input.generated !== "string" || !generated(input.generated) || (before !== null && !generated(before))) throw new Error("Only scaffold-owned generated files may be regenerated");
|
||||||
@@ -79,17 +82,6 @@ export const planStructure = async (rootPath: string, request: StructuralRequest
|
|||||||
after = input.edits.reduce(editStructure, before);
|
after = input.edits.reduce(editStructure, before);
|
||||||
}
|
}
|
||||||
if (Buffer.byteLength(after) > 1024 * 1024) throw new Error("Scaffold file exceeds 1 MiB");
|
if (Buffer.byteLength(after) > 1024 * 1024) throw new Error("Scaffold file exceeds 1 MiB");
|
||||||
if (input.file.endsWith("package.qx")) {
|
|
||||||
let registry;
|
|
||||||
try { registry = JSON.parse(await fs.readFile(path.join(root, path.dirname(input.file), "quixos.scaffold.json"), "utf8")); }
|
|
||||||
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
|
|
||||||
if (registry?.generatedBy === "qx-scaffold-v1") {
|
|
||||||
const declaration = parseQx(after).root.children.find(node => node.kind === "packageResourceDecl");
|
|
||||||
const literal = declaration?.children.find(node => node.kind === "stringLiteral");
|
|
||||||
if (literal && JSON.parse(after.slice(literal.start, literal.end)) !== registry.id)
|
|
||||||
throw new Error("Cannot change a scaffold-owned package identity independently of its registry; create a new managed package instead");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const mode = before === null ? 0o644 : (await fs.stat(path.join(root, input.file))).mode & 0o777;
|
const mode = before === null ? 0o644 : (await fs.stat(path.join(root, input.file))).mode & 0o777;
|
||||||
changes.push({file: input.file, before, after, mode});
|
changes.push({file: input.file, before, after, mode});
|
||||||
await containedParent(snapshot.directory, input.file);
|
await containedParent(snapshot.directory, input.file);
|
||||||
@@ -109,9 +101,7 @@ export const planStructure = async (rootPath: string, request: StructuralRequest
|
|||||||
if (request.kind === "workspace") await compileWorkspaceRepository({rootDirectory: resourceRoot, resolveResource});
|
if (request.kind === "workspace") await compileWorkspaceRepository({rootDirectory: resourceRoot, resolveResource});
|
||||||
else if (["package", "interface"].includes(request.kind) && request.source) {
|
else if (["package", "interface"].includes(request.kind) && request.source) {
|
||||||
const compiled = await compileCapabilityResourceRepository({rootDirectory: resourceRoot, kind: request.kind as "package" | "interface", source: {resolver: "git", ...request.source}, resolveResource});
|
const compiled = await compileCapabilityResourceRepository({rootDirectory: resourceRoot, kind: request.kind as "package" | "interface", source: {resolver: "git", ...request.source}, resolveResource});
|
||||||
let scaffoldOwned = false;
|
if (compiled.resource.kind === "package") {
|
||||||
try { scaffoldOwned = JSON.parse(await fs.readFile(path.join(resourceRoot, "quixos.scaffold.json"), "utf8")).generatedBy === "qx-scaffold-v1"; } catch { /* ordinary resource, no generated package scaffolding */ }
|
|
||||||
if (scaffoldOwned && compiled.resource.kind === "package") {
|
|
||||||
const configuration = JSON.parse(await fs.readFile(path.join(resourceRoot, "quixos.check.json"), "utf8"));
|
const configuration = JSON.parse(await fs.readFile(path.join(resourceRoot, "quixos.check.json"), "utf8"));
|
||||||
const artifacts = [
|
const artifacts = [
|
||||||
{file: configuration.bindingOutput as string, after: generateTypeScriptBindings(bindingSchema(compiled), compiled.resource.revision.revisionId, configuration.options)},
|
{file: configuration.bindingOutput as string, after: generateTypeScriptBindings(bindingSchema(compiled), compiled.resource.revision.revisionId, configuration.options)},
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import os from "node:os";
|
|||||||
import {spawnSync} from "node:child_process";
|
import {spawnSync} from "node:child_process";
|
||||||
import { planStructure, applyStructure, resumeStructure, type StructuralRequest } from "./structural-plan.js";
|
import { planStructure, applyStructure, resumeStructure, type StructuralRequest } from "./structural-plan.js";
|
||||||
import {scaffoldRecipe, type ScaffoldRecipe} from "./scaffold-recipes.js";
|
import {scaffoldRecipe, type ScaffoldRecipe} from "./scaffold-recipes.js";
|
||||||
|
import {checkBundleSources} from "../bindings/bundle-policy.js";
|
||||||
|
import {sealMigrations} from "./migration-seal.js";
|
||||||
|
import {generatePackageDescriptor} from "../bindings/index.js";
|
||||||
import {buildCheckedPackage, buildImmutableCandidate, snapshotCommit} from "./checked-build.js";
|
import {buildCheckedPackage, buildImmutableCandidate, snapshotCommit} from "./checked-build.js";
|
||||||
import {formatQuixosLock, loadQuixosLock, parseQuixosLockDocument} from "../resource-lock/index.js";
|
import {formatQuixosLock, loadQuixosLock, parseQuixosLockDocument} from "../resource-lock/index.js";
|
||||||
import { walkSyntax } from "./source.js";
|
import { walkSyntax } from "./source.js";
|
||||||
@@ -38,6 +41,32 @@ const planSummary = (plan: Awaited<ReturnType<typeof planStructure>>) => ({
|
|||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const [command, ...args] = process.argv.slice(2);
|
const [command, ...args] = process.argv.slice(2);
|
||||||
|
if (command === "package-identity") {
|
||||||
|
if (args.length !== 1) throw new Error("usage: quixos-qx package-identity PACKAGE_QX");
|
||||||
|
const authored = await readFile(args[0], "utf8");
|
||||||
|
const syntax = parseQx(authored);
|
||||||
|
const declarations = [...walkSyntax(syntax.root)].filter(node => node.kind === "packageResourceDecl");
|
||||||
|
if (syntax.diagnostics.length || declarations.length !== 1) throw new Error("Expected one valid package declaration");
|
||||||
|
const literals = declarations[0].children.filter(node => node.kind === "stringLiteral");
|
||||||
|
process.stdout.write(JSON.stringify({id: JSON.parse(authored.slice(literals[0].start, literals[0].end)),
|
||||||
|
revision: JSON.parse(authored.slice(literals[1].start, literals[1].end))}) + "\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "package-descriptor") {
|
||||||
|
if (args.length !== 2) throw new Error("usage: quixos-qx package-descriptor SCHEMA REVISION_ID");
|
||||||
|
process.stdout.write(generatePackageDescriptor(JSON.parse(await readFile(args[0], "utf8")), args[1]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "migration-seal") {
|
||||||
|
if (args.length !== 1) throw new Error("usage: quixos-qx migration-seal PACKAGE_DIRECTORY");
|
||||||
|
process.stdout.write(JSON.stringify(await sealMigrations(args[0])) + "\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "bundle-policy") {
|
||||||
|
if (args.length !== 1) throw new Error("usage: quixos-qx bundle-policy SOURCE_DIRECTORY");
|
||||||
|
await checkBundleSources(args[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (command === "worklist" && !args.includes("--help")) {
|
if (command === "worklist" && !args.includes("--help")) {
|
||||||
if (args.length !== 1) throw new Error("usage: quixos-qx worklist WORKBENCH");
|
if (args.length !== 1) throw new Error("usage: quixos-qx worklist WORKBENCH");
|
||||||
process.stdout.write(`${JSON.stringify(await authoringWorklist(args[0]), null, 2)}\n`);
|
process.stdout.write(`${JSON.stringify(await authoringWorklist(args[0]), null, 2)}\n`);
|
||||||
@@ -62,10 +91,10 @@ const main = async () => {
|
|||||||
if (!args.length || args.length > 2) throw new Error("usage: quixos-qx converge WORKBENCH [REGISTERED_DIRECTORY] (join package writers first)");
|
if (!args.length || args.length > 2) throw new Error("usage: quixos-qx converge WORKBENCH [REGISTERED_DIRECTORY] (join package writers first)");
|
||||||
const context = await authoringContext(args[0]);
|
const context = await authoringContext(args[0]);
|
||||||
if (command === "converge") {
|
if (command === "converge") {
|
||||||
const result = spawnSync("flock", ["--exclusive", "--nonblock", "--conflict-exit-code", "75", path.join(context.workbench, ".quixos/converge.lock"),
|
const result = spawnSync("flock", ["--exclusive", "--timeout", "120", "--conflict-exit-code", "75", path.join(context.workbench, ".quixos/converge.lock"),
|
||||||
process.execPath, process.argv[1], "_converge", context.workbench, ...(args[1] ? [args[1]] : [])], {stdio: "inherit"});
|
process.execPath, process.argv[1], "_converge", context.workbench, ...(args[1] ? [args[1]] : [])], {stdio: "inherit"});
|
||||||
if (result.error) throw result.error;
|
if (result.error) throw result.error;
|
||||||
if (result.status === 75) process.stderr.write("Another source coordinator is running; retry when it finishes.\n");
|
if (result.status === 75) process.stderr.write("Timed out after 120 seconds waiting for source capture; inspect the active coordinator. No build lock is held.\n");
|
||||||
process.exitCode = result.status ?? 1; return;
|
process.exitCode = result.status ?? 1; return;
|
||||||
}
|
}
|
||||||
const result = await convergeAuthoring(context.workbench, args[1]);
|
const result = await convergeAuthoring(context.workbench, args[1]);
|
||||||
@@ -130,7 +159,7 @@ const main = async () => {
|
|||||||
const parsed = parseQuixosLockDocument(await readFile(path.join(root, file), "utf8"));
|
const parsed = parseQuixosLockDocument(await readFile(path.join(root, file), "utf8"));
|
||||||
if (parsed.ok && parsed.document.resources.some(entry => entry.kind === kind && entry.binding === name)) target = file;
|
if (parsed.ok && parsed.document.resources.some(entry => entry.kind === kind && entry.binding === name)) target = file;
|
||||||
}
|
}
|
||||||
const request: StructuralRequest = {kind: entrypoint, source: await authorSource(root), files: [
|
const request: StructuralRequest = {kind: entrypoint, source: await authorSource(root), validation: "syntax", files: [
|
||||||
{file: `${entrypoint}.qx`, edits: [{operation: "import", kind: resourceKind, name}]},
|
{file: `${entrypoint}.qx`, edits: [{operation: "import", kind: resourceKind, name}]},
|
||||||
{file: target, edits: [{operation: "dependency", kind: resourceKind, name, source: {repository, commit}}]},
|
{file: target, edits: [{operation: "dependency", kind: resourceKind, name, source: {repository, commit}}]},
|
||||||
]};
|
]};
|
||||||
@@ -180,12 +209,17 @@ const main = async () => {
|
|||||||
process.stdout.write(`${JSON.stringify(publish ? await applyPinUpgrades(plan, resume, undefined, {acceptEdits}) : plan, null, 2)}\n`);
|
process.stdout.write(`${JSON.stringify(publish ? await applyPinUpgrades(plan, resume, undefined, {acceptEdits}) : plan, null, 2)}\n`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (["scaffold-package", "scaffold-function", "scaffold-migration", "scaffold-refresh"].includes(command)) {
|
if (command === "scaffold-refresh") throw new Error("Refresh is no longer required: edit declarations and typed implementation wiring, then run qx-workspace check. Dependency installation uses scaffold install.");
|
||||||
|
if (["scaffold-package", "scaffold-function", "scaffold-migration", "scaffold-install"].includes(command)) {
|
||||||
const [root, specFile, ...flags] = args;
|
const [root, specFile, ...flags] = args;
|
||||||
let spec: ScaffoldRecipe;
|
let spec: ScaffoldRecipe;
|
||||||
if (command === "scaffold-function" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(specFile ?? "")) {
|
if (command === "scaffold-function" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(specFile ?? "")) {
|
||||||
const registry = JSON.parse(await readFile(path.join(root, "quixos.scaffold.json"), "utf8"));
|
const authored = parseQx(await readFile(path.join(root, "package.qx"), "utf8"));
|
||||||
spec = {source: await authorSource(root), name: specFile, id: `export:${registry.name}:${specFile}`};
|
const declarationNode = [...walkSyntax(authored.root)].find(node => node.kind === "packageResourceDecl");
|
||||||
|
const nameNode = declarationNode?.children.find(node => node.kind === "identifier");
|
||||||
|
if (!nameNode) throw new Error("Expected a package declaration");
|
||||||
|
const name = authored.source.slice(nameNode.start, nameNode.end);
|
||||||
|
spec = {source: await authorSource(root), name: specFile, id: `export:${name}:${specFile}`};
|
||||||
const declaration = flags.indexOf("--declaration");
|
const declaration = flags.indexOf("--declaration");
|
||||||
if (declaration >= 0) {
|
if (declaration >= 0) {
|
||||||
if (!flags[declaration + 1]) throw new Error("--declaration requires a QX declaration file");
|
if (!flags[declaration + 1]) throw new Error("--declaration requires a QX declaration file");
|
||||||
@@ -201,9 +235,9 @@ const main = async () => {
|
|||||||
}
|
}
|
||||||
} else spec = await readSpec(specFile) as ScaffoldRecipe;
|
} else spec = await readSpec(specFile) as ScaffoldRecipe;
|
||||||
if (!root || !specFile || flags.some((flag) => !["--write", "--install"].includes(flag)) || (flags.includes("--install") && !flags.includes("--write"))) throw new Error("usage: quixos-qx scaffold-package|function|migration|refresh ROOT SPEC_JSON [--write [--install]]");
|
if (!root || !specFile || flags.some((flag) => !["--write", "--install"].includes(flag)) || (flags.includes("--install") && !flags.includes("--write"))) throw new Error("usage: quixos-qx scaffold-package|function|migration|refresh ROOT SPEC_JSON [--write [--install]]");
|
||||||
const request = await scaffoldRecipe(root, command.slice(9) as "package" | "function" | "migration" | "refresh", spec);
|
const plan = command === "scaffold-install" ? undefined : await planStructure(root,
|
||||||
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
|
await scaffoldRecipe(root, command.slice(9) as "package" | "function" | "migration", spec), process.env.QUIXOS_SNAPSHOT_MAP);
|
||||||
const applied = flags.includes("--write") ? await applyStructure(plan) : undefined;
|
const applied = plan && flags.includes("--write") ? await applyStructure(plan) : undefined;
|
||||||
if (flags.includes("--install")) {
|
if (flags.includes("--install")) {
|
||||||
const cwd = path.resolve(root, spec.directory ?? "");
|
const cwd = path.resolve(root, spec.directory ?? "");
|
||||||
const toolchain = JSON.parse(await readFile(path.join(cwd, "quixos.toolchain.json"), "utf8"));
|
const toolchain = JSON.parse(await readFile(path.join(cwd, "quixos.toolchain.json"), "utf8"));
|
||||||
@@ -218,7 +252,7 @@ const main = async () => {
|
|||||||
const locked = spawnSync("nix", ["flake", "lock"], {cwd, stdio: ["inherit", 2, 2]});
|
const locked = spawnSync("nix", ["flake", "lock"], {cwd, stdio: ["inherit", 2, 2]});
|
||||||
if (locked.error || locked.status !== 0) throw new Error("Scaffold files retained; nix flake lock failed");
|
if (locked.error || locked.status !== 0) throw new Error("Scaffold files retained; nix flake lock failed");
|
||||||
}
|
}
|
||||||
process.stdout.write(`${JSON.stringify({...planSummary(plan), applied}, null, 2)}\n`);
|
process.stdout.write(`${JSON.stringify({...plan ? planSummary(plan) : {installed: true}, applied}, null, 2)}\n`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (command === "scaffold-structure") {
|
if (command === "scaffold-structure") {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import {bundlePolicyErrors} from "../src/bindings/bundle-policy.js";
|
||||||
|
import {addImplementation} from "../src/capability-language/implementation-edit.js";
|
||||||
|
|
||||||
|
test("bundled source policy rejects location and dynamic-loading assumptions, not static assets or runtime I/O", () => {
|
||||||
|
for (const code of ['new URL("../x", import.meta.url)', '__dirname', '__filename', 'import(name)', 'require(name)', 'eval(code)', 'new Function(code)'])
|
||||||
|
assert.ok(bundlePolicyErrors(code, "source.ts").length, code);
|
||||||
|
assert.deepEqual(bundlePolicyErrors('import source from "./component.js?browser-source"; import fs from "node:fs"; fs.readFile(userSelectedPath);', "source.ts"), []);
|
||||||
|
assert.deepEqual(bundlePolicyErrors('// import.meta.url\nconst text = "__dirname";', "source.ts"), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("imperative handler insertion preserves arbitrary existing code and rejects ambiguous targets", () => {
|
||||||
|
const original = 'const keep = "createRuntime({fake:1})";\nservePackageRuntime(createRuntime({ existing: customHandler }));\n';
|
||||||
|
const edited = addImplementation(original, "createRuntime", "newHandler", "./impl/new.js");
|
||||||
|
assert.match(edited, /existing: customHandler/);
|
||||||
|
assert.match(edited, /const keep =/);
|
||||||
|
assert.match(edited, /"newHandler": qxImplementation/);
|
||||||
|
assert.throws(() => addImplementation(original, "createRuntime", "existing", "./x.js"), /already exists/);
|
||||||
|
assert.throws(() => addImplementation('createRuntime(one);', "createRuntime", "x", "./x.js"), /Cannot safely/);
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ import {promisify} from "node:util";
|
|||||||
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";
|
||||||
import {contentDigest} from "../src/capability-model/evolution.js";
|
import {contentDigest} from "../src/capability-model/evolution.js";
|
||||||
|
import {sealMigrations} from "../src/capability-language/migration-seal.js";
|
||||||
const execFile = promisify(callback);
|
const execFile = promisify(callback);
|
||||||
|
|
||||||
test("React preset applies its browser build script and shared-platform imports", async (context) => {
|
test("React preset applies its browser build script and shared-platform imports", async (context) => {
|
||||||
@@ -17,11 +18,14 @@ test("React preset applies its browser build script and shared-platform imports"
|
|||||||
const source = {repository: "https://example.test/react.git", commit: "a".repeat(40)};
|
const source = {repository: "https://example.test/react.git", commit: "a".repeat(40)};
|
||||||
const request = await scaffoldRecipe(root, "package", {source, name: "React", id: "package:react", revision: "package:react@1", template: "typescript-react", tools: {quixos: source, protocol: source, helpers: source, sdk: source}});
|
const request = await scaffoldRecipe(root, "package", {source, name: "React", id: "package:react", revision: "package:react@1", template: "typescript-react", tools: {quixos: source, protocol: source, helpers: source, sdk: source}});
|
||||||
await applyStructure(await planStructure(root, request));
|
await applyStructure(await planStructure(root, request));
|
||||||
assert.match(await fs.readFile(path.join(root, "scripts/build-component.mjs"), "utf8"), /__quixos\/platform\/react/);
|
assert.match(await fs.readFile(path.join(root, "src/impl/sourceGet.ts"), "utf8"), /component.js\?browser-source/);
|
||||||
|
assert.match(await fs.readFile(path.join(root, "flake.nix"), "utf8"), /browserSources = true/);
|
||||||
|
assert.equal(JSON.parse(await fs.readFile(path.join(root, "quixos.check.json"), "utf8")).options.messages["org.quixos.web-studio.ReactProps"].export, "opaqueReactPropsBinding");
|
||||||
|
await assert.rejects(fs.access(path.join(root, "quixos.scaffold.json")));
|
||||||
assert.equal(JSON.parse(await fs.readFile(path.join(root, "tsconfig.json"), "utf8")).compilerOptions.jsx, "react-jsx");
|
assert.equal(JSON.parse(await fs.readFile(path.join(root, "tsconfig.json"), "utf8")).compilerOptions.jsx, "react-jsx");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("package/function/migration scaffolds register implementations and refresh code digests without overwriting code", async (context) => {
|
test("imperative scaffolds preserve authored wiring; migration sealing is explicit and separate", async (context) => {
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-recipes-"));
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-recipes-"));
|
||||||
context.after(() => fs.rm(root, {recursive: true, force: true}));
|
context.after(() => fs.rm(root, {recursive: true, force: true}));
|
||||||
await execFile("git", ["-C", root, "init"]);
|
await execFile("git", ["-C", root, "init"]);
|
||||||
@@ -38,31 +42,23 @@ test("package/function/migration scaffolds register implementations and refresh
|
|||||||
await apply("migration", {...base, name: "upgrade", id: "export:upgrade", migration: {id: "upgrade-v2", scopeId: "board", from, to, predecessors: [], ports: [], contracts: {[from]: old, [to]: next}}});
|
await apply("migration", {...base, name: "upgrade", id: "export:upgrade", migration: {id: "upgrade-v2", scopeId: "board", from, to, predecessors: [], ports: [], contracts: {[from]: old, [to]: next}}});
|
||||||
const migrationFile = path.join(root, base.directory, "src/migrations/upgrade.ts");
|
const migrationFile = path.join(root, base.directory, "src/migrations/upgrade.ts");
|
||||||
await fs.appendFile(migrationFile, "\n// authored migration change\n");
|
await fs.appendFile(migrationFile, "\n// authored migration change\n");
|
||||||
await apply("refresh", base);
|
await sealMigrations(path.join(root, base.directory));
|
||||||
assert.equal(await fs.readFile(filename, "utf8"), edited);
|
assert.equal(await fs.readFile(filename, "utf8"), edited);
|
||||||
const catalog = JSON.parse(await fs.readFile(path.join(root, base.directory, "quixos.migrations.json"), "utf8"));
|
const catalog = JSON.parse(await fs.readFile(path.join(root, base.directory, "quixos.migrations.json"), "utf8"));
|
||||||
assert.equal(catalog.migrations[0].implementation.digest, contentDigest(await fs.readFile(migrationFile, "utf8")));
|
assert.equal(catalog.migrations[0].implementation.digest, contentDigest(await fs.readFile(migrationFile, "utf8")));
|
||||||
const bindings = await fs.readFile(path.join(root, base.directory, "src/gen/qx.ts"), "utf8");
|
|
||||||
assert.match(bindings, /export:play/);
|
|
||||||
assert.match(await fs.readFile(path.join(root, base.directory, "src/migrate.ts"), "utf8"), /export:upgrade/);
|
assert.match(await fs.readFile(path.join(root, base.directory, "src/migrate.ts"), "utf8"), /export:upgrade/);
|
||||||
if (process.env.QX_SCAFFOLD_TEST_SDK) {
|
|
||||||
const sdk = await fs.realpath(process.env.QX_SCAFFOLD_TEST_SDK);
|
|
||||||
const packageRoot = path.join(root, base.directory);
|
|
||||||
await fs.mkdir(path.join(packageRoot, "node_modules/@quixos"), {recursive: true});
|
|
||||||
await fs.symlink(sdk, path.join(packageRoot, "node_modules/@quixos/camino-package-runtime"));
|
|
||||||
await fs.symlink(path.join(sdk, "node_modules/@types"), path.join(packageRoot, "node_modules/@types"));
|
|
||||||
await execFile(path.join(sdk, "node_modules/.bin/tsc"), ["--noEmit"], {cwd: packageRoot});
|
|
||||||
await execFile("nix-instantiate", ["--parse", path.join(packageRoot, "flake.nix")]);
|
|
||||||
}
|
|
||||||
await assert.rejects(() => apply("function", {...base, name: "play", id: "export:play"}), /unique/);
|
await assert.rejects(() => apply("function", {...base, name: "play", id: "export:play"}), /unique/);
|
||||||
const declarations = path.join(root, base.directory, "package.qx");
|
const declarations = path.join(root, base.directory, "package.qx");
|
||||||
await fs.writeFile(declarations, (await fs.readFile(declarations, "utf8")).replace(/}\s*$/, ' function authored id "export:authored" : unit -> unit;\n}\n'));
|
await fs.writeFile(declarations, (await fs.readFile(declarations, "utf8")).replace(/}\s*$/, ' function authored id "export:authored" : unit -> unit;\n}\n'));
|
||||||
await apply("refresh", base);
|
const server = path.join(root, base.directory, "src/server.ts");
|
||||||
assert.match(await fs.readFile(path.join(root, base.directory, "src/server.ts"), "utf8"), /"authored":/);
|
await fs.appendFile(server, "\n// authored comment must survive\n");
|
||||||
assert.match(await fs.readFile(path.join(root, base.directory, "src/impl/authored.ts"), "utf8"), /Implement authored/);
|
await apply("function", {...base, name: "another", id: "export:another"});
|
||||||
|
assert.match(await fs.readFile(server, "utf8"), /authored comment must survive/);
|
||||||
|
await assert.rejects(() => apply("refresh", base), /removed/);
|
||||||
|
await assert.rejects(fs.access(path.join(root, base.directory, "src/impl/authored.ts")));
|
||||||
assert.equal(await fs.readFile(filename, "utf8"), edited);
|
assert.equal(await fs.readFile(filename, "utf8"), edited);
|
||||||
await assert.rejects(() => planStructure(root, {kind: "package", source, resourceRoot: base.directory, validation: "syntax", files: [{
|
await planStructure(root, {kind: "package", source, resourceRoot: base.directory, validation: "syntax", files: [{
|
||||||
file: `${base.directory}/package.qx`, edits: [{operation: "replace", target: {kind: "packageResourceDecl", id: "package:chess"},
|
file: `${base.directory}/package.qx`, edits: [{operation: "replace", target: {kind: "packageResourceDecl", id: "package:chess"},
|
||||||
source: 'package Other id "package:other" revision "package:other@1" {}'}],
|
source: 'package Other id "package:other" revision "package:other@1" {}'}],
|
||||||
}]}), /scaffold-owned package identity/);
|
}]});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -156,6 +156,10 @@ let
|
|||||||
overriddenProject = optionalOverride overrideAttrs project;
|
overriddenProject = optionalOverride overrideAttrs project;
|
||||||
|
|
||||||
cacheEntries = {
|
cacheEntries = {
|
||||||
|
"@babel/helper-string-parser@npm:7.29.7" = { filename = "@babel-helper-string-parser-npm-7.29.7-87998d618e-194bc0f171.zip"; hash = "sha512-GUvA8XFuOW1f/eVq1hGXRfuVV2YsmGEVkOXkVJBng6TMshzpMFa462mkkJBEg05F2W5QrGlbvp4yIWSP4DPAbA=="; };
|
||||||
|
"@babel/helper-validator-identifier@npm:7.29.7" = { filename = "@babel-helper-validator-identifier-npm-7.29.7-9939aac13d-4795354e7a.zip"; hash = "sha512-R5U1Tnrg3K+nLeHNBOxRJS3BSYUXFwvq8BngPv/Ft78TxrIaOUmnfge4Elvn8QbtETE1DY69RWauh0CUpybWKw=="; };
|
||||||
|
"@babel/parser@npm:7.29.8" = { filename = "@babel-parser-npm-7.29.8-d8ac19f8b3-acc890c5e6.zip"; hash = "sha512-rMiQxeam3UCGOke1C6wRHXGF7m+74WPr4R1SFIVMoq25AUYq1NcYplCQ74S9IjDp6KtFouDKzMaF8fV6sLseKA=="; };
|
||||||
|
"@babel/types@npm:7.29.8" = { filename = "@babel-types-npm-7.29.8-3f9597fc63-be7c279f0a.zip"; hash = "sha512-vnwnnwq/KghsYz4htJx8qAJ10FKDzFomi2enCMmRS9DJRPFCKz6zyzdoKir11WCr9SDM+bAbU+y/5rcfvD/d5g=="; };
|
||||||
"@bufbuild/protobuf@npm:2.14.1" = { filename = "@bufbuild-protobuf-npm-2.14.1-78e9ea56a2-3f913aca03.zip"; hash = "sha512-P5E6ygPfhBnEsh2ADqVNf2yDalND5l1vaGJ57DVmxJB3KZOO8vlKXifZLo0/StzW4CcRi6Kbp2TSHhxjfN3HAg=="; };
|
"@bufbuild/protobuf@npm:2.14.1" = { filename = "@bufbuild-protobuf-npm-2.14.1-78e9ea56a2-3f913aca03.zip"; hash = "sha512-P5E6ygPfhBnEsh2ADqVNf2yDalND5l1vaGJ57DVmxJB3KZOO8vlKXifZLo0/StzW4CcRi6Kbp2TSHhxjfN3HAg=="; };
|
||||||
"@bufbuild/protoc-gen-es@npm:2.14.1" = { filename = "@bufbuild-protoc-gen-es-npm-2.14.1-230b1181a2-79f9fad1d5.zip"; hash = "sha512-efn60dVmXaptZUmyh4oPY/6/rbGHuFNAg/vrI4Ydwizz6mmmYKEvD45Ps8AtJO6ALxcYRBVt5C+0sXEuOgZ1aQ=="; };
|
"@bufbuild/protoc-gen-es@npm:2.14.1" = { filename = "@bufbuild-protoc-gen-es-npm-2.14.1-230b1181a2-79f9fad1d5.zip"; hash = "sha512-efn60dVmXaptZUmyh4oPY/6/rbGHuFNAg/vrI4Ydwizz6mmmYKEvD45Ps8AtJO6ALxcYRBVt5C+0sXEuOgZ1aQ=="; };
|
||||||
"@bufbuild/protoplugin@npm:2.14.1" = { filename = "@bufbuild-protoplugin-npm-2.14.1-ca1a20a987-6a727aa5a8.zip"; hash = "sha512-anJ6pahI4FA13tkPUOE65Sx3NXutalhHalbU/3ANyBPKxmTgzDC1IBNU5cFY6//rANyDUxbWaqLDhKLp2qXf4w=="; };
|
"@bufbuild/protoplugin@npm:2.14.1" = { filename = "@bufbuild-protoplugin-npm-2.14.1-ca1a20a987-6a727aa5a8.zip"; hash = "sha512-anJ6pahI4FA13tkPUOE65Sx3NXutalhHalbU/3ANyBPKxmTgzDC1IBNU5cFY6//rANyDUxbWaqLDhKLp2qXf4w=="; };
|
||||||
|
|||||||
@@ -5,6 +5,41 @@ __metadata:
|
|||||||
version: 10
|
version: 10
|
||||||
cacheKey: 10c0
|
cacheKey: 10c0
|
||||||
|
|
||||||
|
"@babel/helper-string-parser@npm:^7.29.7":
|
||||||
|
version: 7.29.7
|
||||||
|
resolution: "@babel/helper-string-parser@npm:7.29.7"
|
||||||
|
checksum: 10c0/194bc0f1716e396d5ffde56ad6119745fb9557662c98611590e5e454906783a4ccb21ce93056b8eb69a4909044834e45d96e50ac695bbe9e3221648fe033c06c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier@npm:^7.29.7":
|
||||||
|
version: 7.29.7
|
||||||
|
resolution: "@babel/helper-validator-identifier@npm:7.29.7"
|
||||||
|
checksum: 10c0/4795354e7ae0dcafa72de1cd04ec51252dc1498517170beaf019e03effc5b7bf13c6b21a3949a77e07b8125be7f106ed1131350d8ebd4566ae874094a726d62b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@babel/parser@npm:^7.28.0":
|
||||||
|
version: 7.29.8
|
||||||
|
resolution: "@babel/parser@npm:7.29.8"
|
||||||
|
dependencies:
|
||||||
|
"@babel/types": "npm:^7.29.8"
|
||||||
|
bin:
|
||||||
|
parser: ./bin/babel-parser.js
|
||||||
|
checksum: 10c0/acc890c5e6a6dd40863a47b50bac111d7185ee6fbbe163ebe11d5214854ca2adb901462ad4d718a65090ef84bd2230e9e8ab45a2e0caccc685f1f57ab0bb1e28
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@babel/types@npm:^7.29.8":
|
||||||
|
version: 7.29.8
|
||||||
|
resolution: "@babel/types@npm:7.29.8"
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-string-parser": "npm:^7.29.7"
|
||||||
|
"@babel/helper-validator-identifier": "npm:^7.29.7"
|
||||||
|
checksum: 10c0/be7c279f0abf2a086c633e21b49c7ca80275d05283cc5a268b67a708c9914bd0c944f1422b3eb3cb37682a2af5d560abf520ccf9b01b53ecbfe6b71fbc3fdde6
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@bufbuild/protobuf@npm:2.14.1, @bufbuild/protobuf@npm:^2.12.1":
|
"@bufbuild/protobuf@npm:2.14.1, @bufbuild/protobuf@npm:^2.12.1":
|
||||||
version: 2.14.1
|
version: 2.14.1
|
||||||
resolution: "@bufbuild/protobuf@npm:2.14.1"
|
resolution: "@bufbuild/protobuf@npm:2.14.1"
|
||||||
@@ -44,6 +79,7 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@quixos/quixos-protocol@workspace:."
|
resolution: "@quixos/quixos-protocol@workspace:."
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@babel/parser": "npm:^7.28.0"
|
||||||
"@bufbuild/protobuf": "npm:^2.12.1"
|
"@bufbuild/protobuf": "npm:^2.12.1"
|
||||||
"@bufbuild/protoc-gen-es": "npm:^2.12.1"
|
"@bufbuild/protoc-gen-es": "npm:^2.12.1"
|
||||||
"@types/node": "npm:^24"
|
"@types/node": "npm:^24"
|
||||||
|
|||||||
Reference in New Issue
Block a user