Compare commits

..

7 Commits

Author SHA1 Message Date
Quixos Subtree Publisher e2747066ca Publish quixos-nix-helpers from 1de28b236de076d239752b77553f833dfbdb5b43 2026-09-16 23:49:09 +00:00
Timothy J. Aveni 0c3c9c6ad8 Revert generated-file renaming; retain formatter exclusions 2026-09-15 20:13:10 -07:00
Timothy J. Aveni b3d31b37b3 Mark first-party generated files with .gen; preserve third-party conventions 2026-09-15 17:57:46 -07:00
Timothy J. Aveni 6f3814587f Format authored monorepo code with pinned language formatters 2026-09-15 15:41:24 -07:00
Timothy J. Aveni 1c09bf43c2 Simplify workspace authoring and isolate managed component styles
Use imperative scaffolds without a registry/refresh lifecycle; preserve authored runtime wiring and generate checked descriptors. Consolidate binding options, embed checked TSX/CSS source, queue mutable convergence while allowing parallel immutable checks, and preload real CLI observations. Add Shadow DOM boundaries with scoped overlays and composed-event handling. Update template/toolchain pins and document VM-free authoring, browser, and stateful acceptance; no deployment or default-template selection.
2026-09-14 20:55:32 -07:00
Quixos Subtree Publisher 6657af7ca0 Publish quixos-nix-helpers from 8484d99fcf78a49341a17597bc62211e5f1807dd 2026-09-15 03:49:38 +00:00
Timothy J. Aveni e3080467c9 Simplify workspace authoring and isolate managed component styles 2026-09-14 20:49:38 -07:00
4 changed files with 568 additions and 432 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos", "sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
"sourceCommit": "fc13e9b8411c2210b12db5e558e2b4fa7785505e", "sourceCommit": "1de28b236de076d239752b77553f833dfbdb5b43",
"sourcePath": "quixos-instance/quixos-nix-helpers", "sourcePath": "quixos-instance/quixos-nix-helpers",
"exportName": "quixos-nix-helpers", "exportName": "quixos-nix-helpers",
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git" "mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git"
+52 -17
View File
@@ -12,23 +12,58 @@ const platform = new Map([
["react/jsx-dev-runtime", "/__quixos/platform/react-jsx-dev-runtime/v18.mjs"], ["react/jsx-dev-runtime", "/__quixos/platform/react-jsx-dev-runtime/v18.mjs"],
["@quixos/web-studio-react-runtime", "/__quixos/platform/web-studio-react-runtime/v1.mjs"], ["@quixos/web-studio-react-runtime", "/__quixos/platform/web-studio-react-runtime/v1.mjs"],
]); ]);
await build({...options, plugins: [{name: "quixos-browser-source", setup(api) { await build({
api.onResolve({filter: /\?browser-source$/}, ({path: specifier, resolveDir}) => { ...options,
if (!specifier.startsWith("./") && !specifier.startsWith("../")) throw new Error("Browser source imports must name a relative compiled module"); plugins: [
return {path: path.resolve(resolveDir, specifier.slice(0, -"?browser-source".length)), namespace: "browser-source"}; {
name: "quixos-browser-source",
setup(api) {
api.onResolve({ filter: /\?browser-source$/ }, async ({ path: specifier, resolveDir }) => {
if (!specifier.startsWith("./") && !specifier.startsWith("../"))
throw new Error("Browser source imports must name a relative compiled module");
const resolved = await api.resolve(specifier.slice(0, -"?browser-source".length), {
resolveDir,
kind: "import-statement",
});
if (resolved.errors.length) return { errors: resolved.errors };
return { path: resolved.path, namespace: "browser-source" };
}); });
api.onLoad({ filter: /.*/, namespace: "browser-source" }, async ({ path: entry }) => { api.onLoad({ filter: /.*/, namespace: "browser-source" }, async ({ path: entry }) => {
const browser = await build({entryPoints: [entry], bundle: true, write: false, outfile: "component.mjs", const browser = await build({
format: "esm", platform: "browser", target: "es2022", metafile: true, entryPoints: [entry],
plugins: [{name: "quixos-platform", setup(browserApi) { bundle: true,
browserApi.onResolve({filter: /.*/}, ({path: name}) => platform.has(name) ? {path: platform.get(name), external: true} : undefined); write: false,
}}]}); outfile: "component.mjs",
const js = browser.outputFiles.find(file => file.path.endsWith(".mjs")); format: "esm",
if (!js) throw new Error("Browser source build produced no JavaScript"); platform: "browser",
const css = browser.outputFiles.find(file => file.path.endsWith(".css")); target: "es2022",
if (css && Object.values(browser.metafile.outputs).some(output => output.exports.includes("styles"))) throw new Error("Use either imported CSS or an explicit styles export, not both"); metafile: true,
if (browser.outputFiles.some(file => !file.path.endsWith(".mjs") && !file.path.endsWith(".css"))) throw new Error("Browser assets must be embedded, not emitted as unserved files"); plugins: [
const source = js.text + (css ? `\nexport const styles = ${JSON.stringify(css.text)};\n` : ""); {
return {contents: `export default ${JSON.stringify(source)};`, loader: "js", watchFiles: Object.keys(browser.metafile.inputs)}; name: "quixos-platform",
setup(browserApi) {
browserApi.onResolve({ filter: /.*/ }, ({ path: name }) =>
platform.has(name) ? { path: platform.get(name), external: true } : undefined,
);
},
},
],
});
const js = browser.outputFiles.find((file) => file.path.endsWith(".mjs"));
if (!js) throw new Error("Browser source build produced no JavaScript");
const css = browser.outputFiles.find((file) => file.path.endsWith(".css"));
if (css && Object.values(browser.metafile.outputs).some((output) => output.exports?.includes("styles")))
throw new Error("Use either imported CSS or an explicit styles export, not both");
if (browser.outputFiles.some((file) => !file.path.endsWith(".mjs") && !file.path.endsWith(".css")))
throw new Error("Browser assets must be embedded, not emitted as unserved files");
const source = js.text + (css ? `\nexport const styles = ${JSON.stringify(css.text)};\n` : "");
return {
contents: `export default ${JSON.stringify(source)};`,
loader: "js",
watchFiles: Object.keys(browser.metafile.inputs),
};
});
},
},
],
}); });
}}]});
+32 -9
View File
@@ -1,17 +1,40 @@
import fs from "node:fs"; import fs from "node:fs";
import crypto from "node:crypto"; import crypto from "node:crypto";
const [schemaPath, packageRevisionId, bindingOutput, generatorPath, output] = process.argv.slice(2); const [schemaPath, packageRevisionId, bindingOutput, generatorPath, output] = process.argv.slice(2);
if (!schemaPath || !packageRevisionId || !bindingOutput || !generatorPath || !output) throw new Error("Missing candidate check receipt inputs"); if (!schemaPath || !packageRevisionId || !bindingOutput || !generatorPath || !output)
const canonical = (value) => Array.isArray(value) ? value.map(canonical) : value && typeof value === "object" throw new Error("Missing candidate check receipt inputs");
? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, entry]) => [key, canonical(entry)])) : value; const canonical = (value) =>
const hash = (value) => `sha256:${crypto.createHash("sha256").update(JSON.stringify(canonical(value))).digest("hex")}`; Array.isArray(value)
? value.map(canonical)
: value && typeof value === "object"
? Object.fromEntries(
Object.entries(value)
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
.map(([key, entry]) => [key, canonical(entry)]),
)
: value;
const hash = (value) =>
`sha256:${crypto
.createHash("sha256")
.update(JSON.stringify(canonical(value)))
.digest("hex")}`;
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8")); const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
if (!schema.packages.some((entry) => entry.revisionId === packageRevisionId)) throw new Error("Checked binding schema lacks the package"); if (!schema.packages.some((entry) => entry.revisionId === packageRevisionId))
throw new Error("Checked binding schema lacks the package");
const generated = fs.readFileSync(bindingOutput, "utf8"); const generated = fs.readFileSync(bindingOutput, "utf8");
if (generated !== fs.readFileSync(".qx-checked-bindings", "utf8")) throw new Error("Build replaced candidate-generated bindings; its check is not evidence for this candidate"); if (generated !== fs.readFileSync(".qx-checked-bindings", "utf8"))
throw new Error("Build replaced candidate-generated bindings; its check is not evidence for this candidate");
const receipt = { const receipt = {
schemaVersion: 1, packageRevisionId, success: true, bindingSchema: schema, schemaVersion: 1,
bindingSchemaDigest: hash(schema), generatedDigest: hash(generated), packageRevisionId,
checkerDigest: hash({ generatorPath, compiler: JSON.parse(fs.readFileSync("node_modules/typescript/package.json", "utf8")), lock: fs.readFileSync("yarn.lock", "utf8") }), success: true,
bindingSchema: schema,
bindingSchemaDigest: hash(schema),
generatedDigest: hash(generated),
checkerDigest: hash({
generatorPath,
compiler: JSON.parse(fs.readFileSync("node_modules/typescript/package.json", "utf8")),
lock: fs.readFileSync("yarn.lock", "utf8"),
}),
}; };
fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: "wx" }); fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: "wx" });
+217 -139
View File
@@ -19,23 +19,25 @@ let
schemaMainPathDefault = "dist/quixos-package-schema.js"; schemaMainPathDefault = "dist/quixos-package-schema.js";
schemaPackageName = schemaPackageJson.name or "schema"; schemaPackageName = schemaPackageJson.name or "schema";
selfSchemaName = selfSchemaName =
if pkgs.lib.hasInfix "/" schemaPackageName if pkgs.lib.hasInfix "/" schemaPackageName then
then pkgs.lib.last (pkgs.lib.splitString "/" schemaPackageName) pkgs.lib.last (pkgs.lib.splitString "/" schemaPackageName)
else schemaPackageName; else
schemaPackageName;
flakeText = flakeText = builtins.replaceStrings [ "\n" "\r" "\t" ] [ " " " " " " ] (
builtins.replaceStrings builtins.readFile "${flakeRoot}/flake.nix"
[ "\n" "\r" "\t" ] );
[ " " " " " " ]
(builtins.readFile "${flakeRoot}/flake.nix");
getInputUrl = inputName: getInputUrl =
inputName:
let let
pattern1 = ".*" + inputName + "[[:space:]]*\\.url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*"; pattern1 = ".*" + inputName + "[[:space:]]*\\.url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*";
pattern2 = ".*" + inputName + "[[:space:]]*=[[:space:]]*\\{[^}]*url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*"; pattern2 =
".*" + inputName + "[[:space:]]*=[[:space:]]*\\{[^}]*url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*";
match1 = builtins.match pattern1 flakeText; match1 = builtins.match pattern1 flakeText;
match2 = if match1 != null then match1 else builtins.match pattern2 flakeText; match2 = if match1 != null then match1 else builtins.match pattern2 flakeText;
in if match2 == null then null else builtins.elemAt match2 0; in
if match2 == null then null else builtins.elemAt match2 0;
schemaBase = schemaBase =
(pkgs.callPackage "${schemaDir}/yarn-project.nix" { (pkgs.callPackage "${schemaDir}/yarn-project.nix" {
@@ -65,51 +67,44 @@ let
''; '';
}); });
schemaInputs = schemaInputs = pkgs.lib.filterAttrs (name: _: pkgs.lib.hasPrefix schemaPrefix name) inputs;
pkgs.lib.filterAttrs (name: _: pkgs.lib.hasPrefix schemaPrefix name) inputs;
decodeSchemaInputName = encodedName: decodeSchemaInputName = encodedName: builtins.replaceStrings [ "__" ] [ "." ] encodedName;
builtins.replaceStrings [ "__" ] [ "." ] encodedName;
schemaPackagesFromInputs = schemaPackagesFromInputs = pkgs.lib.mapAttrs' (
pkgs.lib.mapAttrs' name: flake:
(name: flake:
let let
schemaName = schemaName = decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
in in
{ {
name = schemaName; name = schemaName;
value = flake.packages.${system}.schema; value = flake.packages.${system}.schema;
}) }
schemaInputs; ) schemaInputs;
schemaPackages = schemaPackagesFromInputs // { "${selfSchemaName}" = schema; }; schemaPackages = schemaPackagesFromInputs // {
"${selfSchemaName}" = schema;
};
schemaMetaByName = schemaMetaByName = pkgs.lib.mapAttrs' (
pkgs.lib.mapAttrs' name: flake:
(name: flake:
let let
schemaName = schemaName = decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
sourceInfo = if flake ? sourceInfo then flake.sourceInfo else { }; sourceInfo = if flake ? sourceInfo then flake.sourceInfo else { };
sourceRev = sourceInfo.rev or null; sourceRev = sourceInfo.rev or null;
inputUrl = getInputUrl name; inputUrl = getInputUrl name;
urlBase = urlBase = if inputUrl == null then null else builtins.head (pkgs.lib.splitString "?" inputUrl);
if inputUrl == null
then null
else builtins.head (pkgs.lib.splitString "?" inputUrl);
isDisallowed = isDisallowed =
inputUrl == null inputUrl == null
|| pkgs.lib.hasPrefix "path:" inputUrl || pkgs.lib.hasPrefix "path:" inputUrl
|| pkgs.lib.hasPrefix "file:" inputUrl || pkgs.lib.hasPrefix "file:" inputUrl
|| pkgs.lib.hasPrefix "git+file:" inputUrl; || pkgs.lib.hasPrefix "git+file:" inputUrl;
flakeRef = flakeRef =
if isDisallowed || sourceRev == null || urlBase == null if isDisallowed || sourceRev == null || urlBase == null then
then null null
else urlBase + "?rev=" + sourceRev; else
meta = urlBase + "?rev=" + sourceRev;
{ meta = {
name = "@quixos-package-schemas/${schemaName}"; name = "@quixos-package-schemas/${schemaName}";
type = sourceInfo.type or null; type = sourceInfo.type or null;
url = inputUrl; url = inputUrl;
@@ -117,16 +112,16 @@ let
flakeRef = flakeRef; flakeRef = flakeRef;
}; };
in in
if flakeRef == null if flakeRef == null then
then throw "Schema input ${schemaName} must be a git flake with url+rev" throw "Schema input ${schemaName} must be a git flake with url+rev"
else { else
{
name = schemaName; name = schemaName;
value = meta; value = meta;
}) }
schemaInputs; ) schemaInputs;
schemaMetaJsonByName = schemaMetaJsonByName = pkgs.lib.mapAttrs (_: meta: builtins.toJSON meta) schemaMetaByName;
pkgs.lib.mapAttrs (_: meta: builtins.toJSON meta) schemaMetaByName;
schemaExtensionsBlock = pkgs.lib.concatStringsSep "\n" ( schemaExtensionsBlock = pkgs.lib.concatStringsSep "\n" (
[ [
@@ -151,12 +146,11 @@ let
); );
schemaInstallCommands = pkgs.lib.concatStringsSep "\n" ( schemaInstallCommands = pkgs.lib.concatStringsSep "\n" (
pkgs.lib.mapAttrsToList (name: drv: pkgs.lib.mapAttrsToList (
name: drv:
let let
metaJson = metaJson =
if pkgs.lib.hasAttr name schemaMetaJsonByName if pkgs.lib.hasAttr name schemaMetaJsonByName then schemaMetaJsonByName.${name} else null;
then schemaMetaJsonByName.${name}
else null;
in in
'' ''
schema_pkg="$(ls -d ${drv}/libexec/* | head -n1)" schema_pkg="$(ls -d ${drv}/libexec/* | head -n1)"
@@ -166,8 +160,8 @@ let
chmod -R u+w "$dest" chmod -R u+w "$dest"
'' ''
+ ( + (
if metaJson != null if metaJson != null then
then '' ''
schema_main="${schemaMainPathDefault}" schema_main="${schemaMainPathDefault}"
if [ -f "$dest/$schema_main" ]; then if [ -f "$dest/$schema_main" ]; then
block_tmp="$(mktemp -p "$dest")" block_tmp="$(mktemp -p "$dest")"
@@ -205,7 +199,8 @@ EOF
rm -f "$block_tmp" "$tmp_out" rm -f "$block_tmp" "$tmp_out"
fi fi
'' ''
else "" else
""
) )
) schemaPackages ) schemaPackages
); );
@@ -240,7 +235,8 @@ EOF
schemaInstallCommands schemaInstallCommands
updateYarnrcScript updateYarnrcScript
schemaExtensionsBlockEscaped schemaExtensionsBlockEscaped
selfSchemaName; selfSchemaName
;
}; };
mkTsPackageServer = mkTsPackageServer =
@@ -265,9 +261,10 @@ EOF
nodejs' = if nodejs != null then nodejs else pkgs.nodejs_24; nodejs' = if nodejs != null then nodejs else pkgs.nodejs_24;
git' = if git != null then git else pkgs.git; git' = if git != null then git else pkgs.git;
templatePreparePackages' = templatePreparePackages' =
if templatePreparePackages != null if templatePreparePackages != null then
then templatePreparePackages templatePreparePackages
else [ else
[
pkgs.findutils pkgs.findutils
git' git'
pkgs.gnused pkgs.gnused
@@ -291,28 +288,33 @@ EOF
runtimeBinPath = pkgs.lib.makeBinPath extraRuntimePackages; runtimeBinPath = pkgs.lib.makeBinPath extraRuntimePackages;
runtimeLibPath = pkgs.lib.makeLibraryPath extraRuntimePackages; runtimeLibPath = pkgs.lib.makeLibraryPath extraRuntimePackages;
runtimeWrap = runtimeWrap =
if extraRuntimePackages == [ ] if extraRuntimePackages == [ ] then
then "" ""
else '' else
''
wrapProgram "$out/bin/${serverBin}" \ wrapProgram "$out/bin/${serverBin}" \
--prefix PATH : ${runtimeBinPath} \ --prefix PATH : ${runtimeBinPath} \
--prefix LD_LIBRARY_PATH : ${runtimeLibPath} --prefix LD_LIBRARY_PATH : ${runtimeLibPath}
''; '';
server = base.overrideAttrs (old: server = base.overrideAttrs (
old:
let let
extraAttrs = serverOverrides old; extraAttrs = serverOverrides old;
in in
{ {
buildInputs = (old.buildInputs or [ ]) ++ extraBuildInputs; buildInputs = (old.buildInputs or [ ]) ++ extraBuildInputs;
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ nativeBuildInputs =
(old.nativeBuildInputs or [ ])
++ [
pkgs.python3 pkgs.python3
pkgs.gnumake pkgs.gnumake
pkgs.gcc pkgs.gcc
pkgs.pkg-config pkgs.pkg-config
pkgs.makeWrapper pkgs.makeWrapper
] ++ extraNativeBuildInputs; ]
++ extraNativeBuildInputs;
# node-gyp will look at these # node-gyp will look at these
PYTHON = "${pkgs.python3}/bin/python3"; PYTHON = "${pkgs.python3}/bin/python3";
@@ -326,13 +328,14 @@ EOF
buildPhase = buildPhase =
(old.buildPhase or "") (old.buildPhase or "")
+ ( + (
if buildCommand != null if buildCommand != null then
then '' ''
runHook preBuildQuixos runHook preBuildQuixos
${buildCommand} ${buildCommand}
runHook postBuildQuixos runHook postBuildQuixos
'' ''
else "" else
""
); );
postFixup = (old.postFixup or "") + runtimeWrap; postFixup = (old.postFixup or "") + runtimeWrap;
@@ -342,7 +345,8 @@ EOF
mkdir -p "$pkg_root/quixos-package-schemas" mkdir -p "$pkg_root/quixos-package-schemas"
(cd "$pkg_root" && ${schemaSupport.schemaInstallCommands}) (cd "$pkg_root" && ${schemaSupport.schemaInstallCommands})
''; '';
} // extraAttrs }
// extraAttrs
); );
check = server.overrideAttrs (old: { check = server.overrideAttrs (old: {
@@ -360,7 +364,12 @@ EOF
}; };
in in
{ {
inherit server check packageName serverBin; inherit
server
check
packageName
serverBin
;
devShells = { devShells = {
"quixos-prepare" = prepareShell; "quixos-prepare" = prepareShell;
@@ -392,13 +401,18 @@ EOF
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
schemaSupport = mkSchemaSupport { schemaSupport = mkSchemaSupport {
inherit pkgs inputs system schemaDir schemaPrefix self flakeRoot; inherit
pkgs
inputs
system
schemaDir
schemaPrefix
self
flakeRoot
;
}; };
normalizeList = value: normalizeList = value: if builtins.isFunction value then value pkgs else value;
if builtins.isFunction value
then value pkgs
else value;
serverResult = serverBuilder { serverResult = serverBuilder {
inherit pkgs schemaSupport; inherit pkgs schemaSupport;
@@ -408,26 +422,28 @@ EOF
}; };
combinedName = pkgs.lib.strings.sanitizeDerivationName ( combinedName = pkgs.lib.strings.sanitizeDerivationName (
if packageName != null if packageName != null then
then packageName packageName
else if serverResult ? packageName else if serverResult ? packageName then
then serverResult.packageName serverResult.packageName
else "quixos-package" else
"quixos-package"
); );
packageServer = serverResult.server; packageServer = serverResult.server;
packageServerCheck = if serverResult ? check then serverResult.check else null; packageServerCheck = if serverResult ? check then serverResult.check else null;
appProgramFinal = appProgramFinal =
if appProgram != null if appProgram != null then
then appProgram appProgram
else if serverResult ? appProgram else if serverResult ? appProgram then
then serverResult.appProgram serverResult.appProgram
else "${packageServer}/bin/${serverResult.serverBin or "server"}"; else
"${packageServer}/bin/${serverResult.serverBin or "server"}";
devShellHookBase = devShellHookBase =
if enableYarnrcHook if enableYarnrcHook then
then '' ''
find_schema_root() { find_schema_root() {
dir="$PWD" dir="$PWD"
while [ "$dir" != "/" ]; do while [ "$dir" != "/" ]; do
@@ -455,21 +471,26 @@ EOF
fi fi
fi fi
'' ''
else ""; else
"";
devShellHook = devShellHookBase + extraDevShellHook; devShellHook = devShellHookBase + extraDevShellHook;
devShellPackages = normalizeList extraDevShellPackages; devShellPackages = normalizeList extraDevShellPackages;
checks = checks = {
{ schema = schemaSupport.schemaCheck; } schema = schemaSupport.schemaCheck;
}
// (if packageServerCheck != null then { default = packageServerCheck; } else { }); // (if packageServerCheck != null then { default = packageServerCheck; } else { });
extraDevShells = if serverResult ? devShells then serverResult.devShells else { }; extraDevShells = if serverResult ? devShells then serverResult.devShells else { };
in in
{ {
packages.default = pkgs.symlinkJoin { packages.default = pkgs.symlinkJoin {
name = combinedName; name = combinedName;
paths = [ packageServer schemaSupport.schema ]; paths = [
packageServer
schemaSupport.schema
];
}; };
packages.server = packageServer; packages.server = packageServer;
packages.schema = schemaSupport.schema; packages.schema = schemaSupport.schema;
@@ -512,10 +533,19 @@ EOF
# Language-neutral, offline schema compilation. Snapshot directories must be # Language-neutral, offline schema compilation. Snapshot directories must be
# fixed Nix inputs matching the resource lock's complete dependency closure. # fixed Nix inputs matching the resource lock's complete dependency closure.
mkQxBindingSchema = { pkgs, protocol, src, repository, commit, resources ? [ ] }: mkQxBindingSchema =
{
pkgs,
protocol,
src,
repository,
commit,
resources ? [ ],
}:
let let
snapshots = pkgs.writeText "qx-binding-snapshots.json" (builtins.toJSON { inherit resources; }); snapshots = pkgs.writeText "qx-binding-snapshots.json" (builtins.toJSON { inherit resources; });
in pkgs.runCommand "qx-binding-schema.json" { } '' in
pkgs.runCommand "qx-binding-schema.json" { } ''
${protocol}/bin/quixos-resource-compile \ ${protocol}/bin/quixos-resource-compile \
--root ${src} --kind package \ --root ${src} --kind package \
--repository ${pkgs.lib.escapeShellArg repository} \ --repository ${pkgs.lib.escapeShellArg repository} \
@@ -551,23 +581,21 @@ EOF
flake-utils.lib.eachDefaultSystem ( flake-utils.lib.eachDefaultSystem (
system: system:
let let
outputsFor = candidateBindings: outputsFor =
candidateBindings:
let let
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib; lib = pkgs.lib;
nodejs = pkgs.${nodejsAttr}; nodejs = pkgs.${nodejsAttr};
callOption = value: callOption =
if builtins.isFunction value value: if builtins.isFunction value then value { inherit inputs pkgs system; } else value;
then value { inherit inputs pkgs system; }
else value;
packageJson = builtins.fromJSON (builtins.readFile "${packageRoot}/package.json"); packageJson = builtins.fromJSON (builtins.readFile "${packageRoot}/package.json");
packageNameFinal = if packageName != null then packageName else packageJson.name or "quixos-package"; packageNameFinal =
if packageName != null then packageName else packageJson.name or "quixos-package";
promptNameFinal = promptNameFinal =
if promptName != null if promptName != null then promptName else lib.strings.sanitizeDerivationName packageNameFinal;
then promptName
else lib.strings.sanitizeDerivationName packageNameFinal;
sourcePackage = mkCaminoSourcePackage { sourcePackage = mkCaminoSourcePackage {
inherit pkgs; inherit pkgs;
@@ -575,27 +603,43 @@ EOF
name = sourceName; name = sourceName;
}; };
exportsFor = attrs: exportsFor =
attrs:
lib.concatStringsSep "\n" ( lib.concatStringsSep "\n" (
lib.mapAttrsToList lib.mapAttrsToList (name: value: "export ${name}=${lib.escapeShellArg (toString value)}") attrs
(name: value: "export ${name}=${lib.escapeShellArg (toString value)}")
attrs
); );
bindingConfig = if candidateBindings != null then candidateBindings bindingConfig =
else if bindings == null then null else callOption bindings; if candidateBindings != null then
bindingSchema = if bindingConfig == null then null else bindingConfig.schema or (mkQxBindingSchema { candidateBindings
else if bindings == null then
null
else
callOption bindings;
bindingSchema =
if bindingConfig == null then
null
else
bindingConfig.schema or (mkQxBindingSchema {
inherit pkgs; inherit pkgs;
protocol = bindingConfig.generator; protocol = bindingConfig.generator;
src = packageRoot; src = packageRoot;
inherit (bindingConfig) repository commit; inherit (bindingConfig) repository commit;
resources = bindingConfig.resources or [ ]; resources = bindingConfig.resources or [ ];
}); });
bindingOutput = if bindingConfig == null then "src/gen/qx.ts" else bindingConfig.output or "src/gen/qx.ts"; bindingOutput =
if bindingConfig == null then "src/gen/qx.ts" else bindingConfig.output or "src/gen/qx.ts";
authoringCheck = builtins.fromJSON (builtins.readFile "${packageRoot}/quixos.check.json"); authoringCheck = builtins.fromJSON (builtins.readFile "${packageRoot}/quixos.check.json");
bindingOptionsFile = if bindingConfig == null then null else bindingOptionsFile =
if bindingConfig == null then
null
else
pkgs.writeText "qx-typescript-options.json" (builtins.toJSON (authoringCheck.options or { })); pkgs.writeText "qx-typescript-options.json" (builtins.toJSON (authoringCheck.options or { }));
bindingCommand = if bindingConfig == null then "" else '' bindingCommand =
if bindingConfig == null then
""
else
''
mkdir -p ${lib.escapeShellArg (dirOf bindingOutput)} mkdir -p ${lib.escapeShellArg (dirOf bindingOutput)}
${bindingConfig.generator}/bin/quixos-codegen-ts \ ${bindingConfig.generator}/bin/quixos-codegen-ts \
${lib.escapeShellArg (toString bindingSchema)} \ ${lib.escapeShellArg (toString bindingSchema)} \
@@ -617,26 +661,38 @@ EOF
bundleTarget = bundleConfig.target or "node24"; bundleTarget = bundleConfig.target or "node24";
bundleFormat = bundleConfig.format or "esm"; bundleFormat = bundleConfig.format or "esm";
bundleBanner = bundleConfig.banner or nodeRequireBanner; bundleBanner = bundleConfig.banner or nodeRequireBanner;
bundleAliases = bundleConfig.aliases or { bundleAliases =
"@automerge/automerge" = "./node_modules/@automerge/automerge/dist/mjs/entrypoints/fullfat_base64.js"; bundleConfig.aliases or {
"@automerge/automerge" =
"./node_modules/@automerge/automerge/dist/mjs/entrypoints/fullfat_base64.js";
}; };
bundleAliasArgs = lib.concatStringsSep " " ( bundleAliasArgs = lib.concatStringsSep " " (
lib.mapAttrsToList lib.mapAttrsToList (from: to: "--alias:${from}=${lib.escapeShellArg to}") bundleAliases
(from: to: "--alias:${from}=${lib.escapeShellArg to}")
bundleAliases
); );
nodeRequireBanner = "import { createRequire } from 'module';const require = createRequire(import.meta.url);"; nodeRequireBanner = "import { createRequire } from 'module';const require = createRequire(import.meta.url);";
bundleCommand = bundleCommand =
if bundle == null if bundle == null then
then "" ""
else if bundleConfig.browserSources or false then '' else if bundleConfig.browserSources or false then
${nodejs}/bin/node ${./browser-source.mjs} ${lib.escapeShellArg (builtins.toJSON { ''
entryPoints = [ bundleConfig.entry ]; bundle = true; platform = bundlePlatform; ${nodejs}/bin/node ${./browser-source.mjs} ${
target = bundleTarget; format = bundleFormat; alias = bundleAliases; lib.escapeShellArg (
builtins.toJSON {
entryPoints = [ bundleConfig.entry ];
bundle = true;
platform = bundlePlatform;
target = bundleTarget;
format = bundleFormat;
alias = bundleAliases;
preserveSymlinks = bundleConfig.preserveSymlinks or true; preserveSymlinks = bundleConfig.preserveSymlinks or true;
banner.js = bundleBanner; outfile = bundleOutfile; banner.js = bundleBanner;
})} outfile = bundleOutfile;
'' else '' }
)
}
''
else
''
esbuild ${lib.escapeShellArg bundleConfig.entry} \ esbuild ${lib.escapeShellArg bundleConfig.entry} \
--bundle \ --bundle \
--platform=${bundlePlatform} \ --platform=${bundlePlatform} \
@@ -655,18 +711,21 @@ EOF
serverFile = installConfig.serverFile or bundleOutfile; serverFile = installConfig.serverFile or bundleOutfile;
descriptorPath = installConfig.descriptorPath or "descriptor.quixos-package.txtpb"; descriptorPath = installConfig.descriptorPath or "descriptor.quixos-package.txtpb";
extraFiles = installConfig.extraFiles or [ ]; extraFiles = installConfig.extraFiles or [ ];
installExtraFile = file: installExtraFile =
file:
let let
source = toString file.source; source = toString file.source;
target = file.target or (baseNameOf source); target = file.target or (baseNameOf source);
mode = file.mode or "0644"; mode = file.mode or "0644";
in '' in
''
install -Dm${toString mode} ${lib.escapeShellArg source} "$out/libexec/${serverLibexecName}/${target}" install -Dm${toString mode} ${lib.escapeShellArg source} "$out/libexec/${serverLibexecName}/${target}"
''; '';
installServerPhase = installServerPhase =
if installServer == null if installServer == null then
then null null
else '' else
''
runHook preInstall runHook preInstall
install -Dm755 ${lib.escapeShellArg serverFile} "$out/libexec/${serverLibexecName}/${serverFile}" install -Dm755 ${lib.escapeShellArg serverFile} "$out/libexec/${serverLibexecName}/${serverFile}"
${lib.concatMapStringsSep "\n" installExtraFile extraFiles} ${lib.concatMapStringsSep "\n" installExtraFile extraFiles}
@@ -695,7 +754,9 @@ EOF
project = (pkgs.callPackage "${packageRoot}/yarn-project.nix" { inherit nodejs; }) { project = (pkgs.callPackage "${packageRoot}/yarn-project.nix" { inherit nodejs; }) {
src = packageRoot; src = packageRoot;
overrideAttrs = old: { overrideAttrs =
old:
{
nativeBuildInputs = nativeBuildInputs =
(old.nativeBuildInputs or [ ]) (old.nativeBuildInputs or [ ])
++ lib.optional (bundle != null || migrationEntrypoint != null) pkgs.esbuild ++ lib.optional (bundle != null || migrationEntrypoint != null) pkgs.esbuild
@@ -704,9 +765,13 @@ EOF
runHook preBuild runHook preBuild
${exportsFor (callOption buildEnv)} ${exportsFor (callOption buildEnv)}
${bindingCommand} ${bindingCommand}
${lib.optionalString (bindingConfig != null && bundle != null) "${bindingConfig.generator}/bin/quixos-qx bundle-policy src"} ${lib.optionalString (
bindingConfig != null && bundle != null
) "${bindingConfig.generator}/bin/quixos-qx bundle-policy src"}
${lib.optionalString (bindingConfig != null) "yarn exec tsc --noEmit"} ${lib.optionalString (bindingConfig != null) "yarn exec tsc --noEmit"}
${lib.optionalString (bindingConfig != null) "cp ${lib.escapeShellArg bindingOutput} .qx-checked-bindings"} ${lib.optionalString (
bindingConfig != null
) "cp ${lib.escapeShellArg bindingOutput} .qx-checked-bindings"}
${buildCommand} ${buildCommand}
${lib.optionalString (bindingConfig != null) '' ${lib.optionalString (bindingConfig != null) ''
yarn exec tsc --noEmit yarn exec tsc --noEmit
@@ -721,7 +786,8 @@ EOF
''} ''}
runHook postBuild runHook postBuild
''; '';
} // lib.optionalAttrs (installServerPhase != null) { }
// lib.optionalAttrs (installServerPhase != null) {
installPhase = installServerPhase; installPhase = installServerPhase;
}; };
}; };
@@ -737,9 +803,10 @@ EOF
''; '';
maybeServerOutputs = maybeServerOutputs =
if installServer == null if installServer == null then
then { } { }
else { else
{
packages.server = project; packages.server = project;
apps.default = { apps.default = {
type = "app"; type = "app";
@@ -754,16 +821,26 @@ EOF
packages = [ packages = [
nodejs nodejs
project.yarn-freestanding project.yarn-freestanding
] ++ lib.optional (bindingConfig != null) generateBindings ++ callOption devShellPackages; ]
++ lib.optional (bindingConfig != null) generateBindings
++ callOption devShellPackages;
# Explicit command keeps shell entry free of source mutations. # Explicit command keeps shell entry free of source mutations.
shellHook = devShellHookBase + callOption devShellHook; shellHook = devShellHookBase + callOption devShellHook;
}; };
} // maybeServerOutputs; }
in (outputsFor null) // { // maybeServerOutputs;
in
(outputsFor null)
// {
# The workspace supplies a compiler-produced schema for the exact # The workspace supplies a compiler-produced schema for the exact
# candidate graph. Standalone builds may use checked-in authoring types, # candidate graph. Standalone builds may use checked-in authoring types,
# but only this build path regenerates and witnesses candidate contracts. # but only this build path regenerates and witnesses candidate contracts.
quixosPackages.checkedServer = { schema, generator, packageRevisionId }: quixosPackages.checkedServer =
{
schema,
generator,
packageRevisionId,
}:
(outputsFor { inherit schema generator packageRevisionId; }).packages.server; (outputsFor { inherit schema generator packageRevisionId; }).packages.server;
} }
); );
@@ -775,5 +852,6 @@ in
mkSchemaSupport mkSchemaSupport
mkCaminoSourcePackage mkCaminoSourcePackage
mkQxBindingSchema mkQxBindingSchema
mkCaminoTsYarnNixifyFlake; mkCaminoTsYarnNixifyFlake
;
} }