diff --git a/.quixos-subtree-source.json b/.quixos-subtree-source.json index 46a170c..1eb5a92 100644 --- a/.quixos-subtree-source.json +++ b/.quixos-subtree-source.json @@ -1,7 +1,7 @@ { "version": 1, "sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos", - "sourceCommit": "8484d99fcf78a49341a17597bc62211e5f1807dd", + "sourceCommit": "1de28b236de076d239752b77553f833dfbdb5b43", "sourcePath": "quixos-instance/quixos-nix-helpers", "exportName": "quixos-nix-helpers", "mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git" diff --git a/browser-source.mjs b/browser-source.mjs index 5d9cbd4..28ba749 100644 --- a/browser-source.mjs +++ b/browser-source.mjs @@ -1,10 +1,10 @@ -import {createRequire} from "node:module"; +import { createRequire } from "node:module"; import path from "node:path"; // Resolve build dependencies from the package's locked node_modules, not from // the helper's own Nix-store location. This program runs only during the build. const require = createRequire(path.join(process.cwd(), "package.json")); -const {build} = require("esbuild"); +const { build } = require("esbuild"); const options = JSON.parse(process.argv[2]); const platform = new Map([ ["react", "/__quixos/platform/react/v18.mjs"], @@ -12,25 +12,58 @@ const platform = new Map([ ["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"], ]); -await build({...options, plugins: [{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}) => { - const browser = await build({entryPoints: [entry], bundle: true, write: false, outfile: "component.mjs", - format: "esm", platform: "browser", target: "es2022", metafile: true, - plugins: [{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)}; - }); -}}]}); +await build({ + ...options, + plugins: [ + { + 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 }) => { + const browser = await build({ + entryPoints: [entry], + bundle: true, + write: false, + outfile: "component.mjs", + format: "esm", + platform: "browser", + target: "es2022", + metafile: true, + plugins: [ + { + 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), + }; + }); + }, + }, + ], +}); diff --git a/check-receipt.mjs b/check-receipt.mjs index 5e2d602..5c6694e 100644 --- a/check-receipt.mjs +++ b/check-receipt.mjs @@ -1,17 +1,40 @@ import fs from "node:fs"; import crypto from "node:crypto"; const [schemaPath, packageRevisionId, bindingOutput, generatorPath, output] = process.argv.slice(2); -if (!schemaPath || !packageRevisionId || !bindingOutput || !generatorPath || !output) throw new Error("Missing candidate check receipt inputs"); -const canonical = (value) => 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")}`; +if (!schemaPath || !packageRevisionId || !bindingOutput || !generatorPath || !output) + throw new Error("Missing candidate check receipt inputs"); +const canonical = (value) => + 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")); -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"); -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 = { - schemaVersion: 1, packageRevisionId, 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") }), + schemaVersion: 1, + packageRevisionId, + 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" }); diff --git a/quixos-package-helpers.nix b/quixos-package-helpers.nix index b4a5fe7..c042e22 100644 --- a/quixos-package-helpers.nix +++ b/quixos-package-helpers.nix @@ -19,23 +19,25 @@ let schemaMainPathDefault = "dist/quixos-package-schema.js"; schemaPackageName = schemaPackageJson.name or "schema"; selfSchemaName = - if pkgs.lib.hasInfix "/" schemaPackageName - then pkgs.lib.last (pkgs.lib.splitString "/" schemaPackageName) - else schemaPackageName; + if pkgs.lib.hasInfix "/" schemaPackageName then + pkgs.lib.last (pkgs.lib.splitString "/" schemaPackageName) + else + schemaPackageName; - flakeText = - builtins.replaceStrings - [ "\n" "\r" "\t" ] - [ " " " " " " ] - (builtins.readFile "${flakeRoot}/flake.nix"); + flakeText = builtins.replaceStrings [ "\n" "\r" "\t" ] [ " " " " " " ] ( + builtins.readFile "${flakeRoot}/flake.nix" + ); - getInputUrl = inputName: + getInputUrl = + inputName: let 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; 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 = (pkgs.callPackage "${schemaDir}/yarn-project.nix" { @@ -65,68 +67,61 @@ let ''; }); - schemaInputs = - pkgs.lib.filterAttrs (name: _: pkgs.lib.hasPrefix schemaPrefix name) inputs; + schemaInputs = pkgs.lib.filterAttrs (name: _: pkgs.lib.hasPrefix schemaPrefix name) inputs; - decodeSchemaInputName = encodedName: - builtins.replaceStrings [ "__" ] [ "." ] encodedName; + decodeSchemaInputName = encodedName: builtins.replaceStrings [ "__" ] [ "." ] encodedName; - schemaPackagesFromInputs = - pkgs.lib.mapAttrs' - (name: flake: - let - schemaName = - decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name); - in - { - name = schemaName; - value = flake.packages.${system}.schema; - }) - schemaInputs; + schemaPackagesFromInputs = pkgs.lib.mapAttrs' ( + name: flake: + let + schemaName = decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name); + in + { + name = schemaName; + value = flake.packages.${system}.schema; + } + ) schemaInputs; - schemaPackages = schemaPackagesFromInputs // { "${selfSchemaName}" = schema; }; + schemaPackages = schemaPackagesFromInputs // { + "${selfSchemaName}" = schema; + }; - schemaMetaByName = - pkgs.lib.mapAttrs' - (name: flake: - let - schemaName = - decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name); - sourceInfo = if flake ? sourceInfo then flake.sourceInfo else { }; - sourceRev = sourceInfo.rev or null; - inputUrl = getInputUrl name; - urlBase = - if inputUrl == null - then null - else builtins.head (pkgs.lib.splitString "?" inputUrl); - isDisallowed = - inputUrl == null - || pkgs.lib.hasPrefix "path:" inputUrl - || pkgs.lib.hasPrefix "file:" inputUrl - || pkgs.lib.hasPrefix "git+file:" inputUrl; - flakeRef = - if isDisallowed || sourceRev == null || urlBase == null - then null - else urlBase + "?rev=" + sourceRev; - meta = - { - name = "@quixos-package-schemas/${schemaName}"; - type = sourceInfo.type or null; - url = inputUrl; - rev = sourceRev; - flakeRef = flakeRef; - }; - in - if flakeRef == null - then throw "Schema input ${schemaName} must be a git flake with url+rev" - else { - name = schemaName; - value = meta; - }) - schemaInputs; + schemaMetaByName = pkgs.lib.mapAttrs' ( + name: flake: + let + schemaName = decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name); + sourceInfo = if flake ? sourceInfo then flake.sourceInfo else { }; + sourceRev = sourceInfo.rev or null; + inputUrl = getInputUrl name; + urlBase = if inputUrl == null then null else builtins.head (pkgs.lib.splitString "?" inputUrl); + isDisallowed = + inputUrl == null + || pkgs.lib.hasPrefix "path:" inputUrl + || pkgs.lib.hasPrefix "file:" inputUrl + || pkgs.lib.hasPrefix "git+file:" inputUrl; + flakeRef = + if isDisallowed || sourceRev == null || urlBase == null then + null + else + urlBase + "?rev=" + sourceRev; + meta = { + name = "@quixos-package-schemas/${schemaName}"; + type = sourceInfo.type or null; + url = inputUrl; + rev = sourceRev; + flakeRef = flakeRef; + }; + in + if flakeRef == null then + throw "Schema input ${schemaName} must be a git flake with url+rev" + else + { + name = schemaName; + value = meta; + } + ) schemaInputs; - schemaMetaJsonByName = - pkgs.lib.mapAttrs (_: meta: builtins.toJSON meta) schemaMetaByName; + schemaMetaJsonByName = pkgs.lib.mapAttrs (_: meta: builtins.toJSON meta) schemaMetaByName; schemaExtensionsBlock = pkgs.lib.concatStringsSep "\n" ( [ @@ -151,12 +146,11 @@ let ); schemaInstallCommands = pkgs.lib.concatStringsSep "\n" ( - pkgs.lib.mapAttrsToList (name: drv: + pkgs.lib.mapAttrsToList ( + name: drv: let metaJson = - if pkgs.lib.hasAttr name schemaMetaJsonByName - then schemaMetaJsonByName.${name} - else null; + if pkgs.lib.hasAttr name schemaMetaJsonByName then schemaMetaJsonByName.${name} else null; in '' schema_pkg="$(ls -d ${drv}/libexec/* | head -n1)" @@ -166,46 +160,47 @@ let chmod -R u+w "$dest" '' + ( - if metaJson != null - then '' - schema_main="${schemaMainPathDefault}" - if [ -f "$dest/$schema_main" ]; then - block_tmp="$(mktemp -p "$dest")" - tmp_out="$(mktemp -p "$dest")" - cat > "$block_tmp" <<'EOF' -packageSchema.__quixos = ${metaJson}; -EOF - chmod u+w "$dest/$schema_main" - if grep -q '^export default ' "$dest/$schema_main"; then - awk -v blockFile="$block_tmp" ' - BEGIN { - while ((getline line < blockFile) > 0) { - block = block line "\n" - } - } - !inserted && /^export default / { printf "%s", block; inserted=1 } - { print } - ' "$dest/$schema_main" > "$tmp_out" - cat "$tmp_out" > "$dest/$schema_main" - elif grep -q '^//# sourceMappingURL=' "$dest/$schema_main"; then - awk -v blockFile="$block_tmp" ' - BEGIN { - while ((getline line < blockFile) > 0) { - block = block line "\n" - } - } - /^\/\/# sourceMappingURL=/ { printf "%s", block; print; next } - { print } - ' "$dest/$schema_main" > "$tmp_out" - cat "$tmp_out" > "$dest/$schema_main" - else - printf '\n' >> "$dest/$schema_main" - cat "$block_tmp" >> "$dest/$schema_main" - fi - rm -f "$block_tmp" "$tmp_out" - fi - '' - else "" + if metaJson != null then + '' + schema_main="${schemaMainPathDefault}" + if [ -f "$dest/$schema_main" ]; then + block_tmp="$(mktemp -p "$dest")" + tmp_out="$(mktemp -p "$dest")" + cat > "$block_tmp" <<'EOF' + packageSchema.__quixos = ${metaJson}; + EOF + chmod u+w "$dest/$schema_main" + if grep -q '^export default ' "$dest/$schema_main"; then + awk -v blockFile="$block_tmp" ' + BEGIN { + while ((getline line < blockFile) > 0) { + block = block line "\n" + } + } + !inserted && /^export default / { printf "%s", block; inserted=1 } + { print } + ' "$dest/$schema_main" > "$tmp_out" + cat "$tmp_out" > "$dest/$schema_main" + elif grep -q '^//# sourceMappingURL=' "$dest/$schema_main"; then + awk -v blockFile="$block_tmp" ' + BEGIN { + while ((getline line < blockFile) > 0) { + block = block line "\n" + } + } + /^\/\/# sourceMappingURL=/ { printf "%s", block; print; next } + { print } + ' "$dest/$schema_main" > "$tmp_out" + cat "$tmp_out" > "$dest/$schema_main" + else + printf '\n' >> "$dest/$schema_main" + cat "$block_tmp" >> "$dest/$schema_main" + fi + rm -f "$block_tmp" "$tmp_out" + fi + '' + else + "" ) ) schemaPackages ); @@ -240,7 +235,8 @@ EOF schemaInstallCommands updateYarnrcScript schemaExtensionsBlockEscaped - selfSchemaName; + selfSchemaName + ; }; mkTsPackageServer = @@ -265,15 +261,16 @@ EOF nodejs' = if nodejs != null then nodejs else pkgs.nodejs_24; git' = if git != null then git else pkgs.git; templatePreparePackages' = - if templatePreparePackages != null - then templatePreparePackages - else [ - pkgs.findutils - git' - pkgs.gnused - nodejs' - pkgs.yarn-berry_4 - ]; + if templatePreparePackages != null then + templatePreparePackages + else + [ + pkgs.findutils + git' + pkgs.gnused + nodejs' + pkgs.yarn-berry_4 + ]; packageJson = builtins.fromJSON (builtins.readFile "${srcDir}/package.json"); packageName = packageJson.name or "quixos-package"; @@ -291,28 +288,33 @@ EOF runtimeBinPath = pkgs.lib.makeBinPath extraRuntimePackages; runtimeLibPath = pkgs.lib.makeLibraryPath extraRuntimePackages; runtimeWrap = - if extraRuntimePackages == [ ] - then "" - else '' - wrapProgram "$out/bin/${serverBin}" \ - --prefix PATH : ${runtimeBinPath} \ - --prefix LD_LIBRARY_PATH : ${runtimeLibPath} - ''; + if extraRuntimePackages == [ ] then + "" + else + '' + wrapProgram "$out/bin/${serverBin}" \ + --prefix PATH : ${runtimeBinPath} \ + --prefix LD_LIBRARY_PATH : ${runtimeLibPath} + ''; - server = base.overrideAttrs (old: + server = base.overrideAttrs ( + old: let extraAttrs = serverOverrides old; in { buildInputs = (old.buildInputs or [ ]) ++ extraBuildInputs; - nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ - pkgs.python3 - pkgs.gnumake - pkgs.gcc - pkgs.pkg-config - pkgs.makeWrapper - ] ++ extraNativeBuildInputs; + nativeBuildInputs = + (old.nativeBuildInputs or [ ]) + ++ [ + pkgs.python3 + pkgs.gnumake + pkgs.gcc + pkgs.pkg-config + pkgs.makeWrapper + ] + ++ extraNativeBuildInputs; # node-gyp will look at these PYTHON = "${pkgs.python3}/bin/python3"; @@ -326,13 +328,14 @@ EOF buildPhase = (old.buildPhase or "") + ( - if buildCommand != null - then '' - runHook preBuildQuixos - ${buildCommand} - runHook postBuildQuixos - '' - else "" + if buildCommand != null then + '' + runHook preBuildQuixos + ${buildCommand} + runHook postBuildQuixos + '' + else + "" ); postFixup = (old.postFixup or "") + runtimeWrap; @@ -342,7 +345,8 @@ EOF mkdir -p "$pkg_root/quixos-package-schemas" (cd "$pkg_root" && ${schemaSupport.schemaInstallCommands}) ''; - } // extraAttrs + } + // extraAttrs ); check = server.overrideAttrs (old: { @@ -360,7 +364,12 @@ EOF }; in { - inherit server check packageName serverBin; + inherit + server + check + packageName + serverBin + ; devShells = { "quixos-prepare" = prepareShell; @@ -392,13 +401,18 @@ EOF pkgs = import nixpkgs { inherit system; }; schemaSupport = mkSchemaSupport { - inherit pkgs inputs system schemaDir schemaPrefix self flakeRoot; + inherit + pkgs + inputs + system + schemaDir + schemaPrefix + self + flakeRoot + ; }; - normalizeList = value: - if builtins.isFunction value - then value pkgs - else value; + normalizeList = value: if builtins.isFunction value then value pkgs else value; serverResult = serverBuilder { inherit pkgs schemaSupport; @@ -408,68 +422,75 @@ EOF }; combinedName = pkgs.lib.strings.sanitizeDerivationName ( - if packageName != null - then packageName - else if serverResult ? packageName - then serverResult.packageName - else "quixos-package" + if packageName != null then + packageName + else if serverResult ? packageName then + serverResult.packageName + else + "quixos-package" ); packageServer = serverResult.server; packageServerCheck = if serverResult ? check then serverResult.check else null; appProgramFinal = - if appProgram != null - then appProgram - else if serverResult ? appProgram - then serverResult.appProgram - else "${packageServer}/bin/${serverResult.serverBin or "server"}"; + if appProgram != null then + appProgram + else if serverResult ? appProgram then + serverResult.appProgram + else + "${packageServer}/bin/${serverResult.serverBin or "server"}"; devShellHookBase = - if enableYarnrcHook - then '' - find_schema_root() { - dir="$PWD" - while [ "$dir" != "/" ]; do - if [ -f "$dir/src/.yarnrc.yml" ] && [ -d "$dir/schema" ]; then - printf "%s\n" "$dir" - return 0 + if enableYarnrcHook then + '' + find_schema_root() { + dir="$PWD" + while [ "$dir" != "/" ]; do + if [ -f "$dir/src/.yarnrc.yml" ] && [ -d "$dir/schema" ]; then + printf "%s\n" "$dir" + return 0 + fi + dir="$(dirname "$dir")" + done + return 1 + } + + ${schemaSupport.updateYarnrcScript} + + link_schemas() { + mkdir -p quixos-package-schemas + ${schemaSupport.schemaLinkCommands} + } + + base="$(find_schema_root || true)" + if [ -n "$base" ]; then + (cd "$base/src" && link_schemas && quixos_package_schemas_update_yarnrc) + if [ -d "$base/schema" ]; then + ln -sfn "$base/schema" "$base/src/quixos-package-schemas/${schemaSupport.selfSchemaName}" fi - dir="$(dirname "$dir")" - done - return 1 - } - - ${schemaSupport.updateYarnrcScript} - - link_schemas() { - mkdir -p quixos-package-schemas - ${schemaSupport.schemaLinkCommands} - } - - base="$(find_schema_root || true)" - if [ -n "$base" ]; then - (cd "$base/src" && link_schemas && quixos_package_schemas_update_yarnrc) - if [ -d "$base/schema" ]; then - ln -sfn "$base/schema" "$base/src/quixos-package-schemas/${schemaSupport.selfSchemaName}" fi - fi - '' - else ""; + '' + else + ""; devShellHook = devShellHookBase + extraDevShellHook; devShellPackages = normalizeList extraDevShellPackages; - checks = - { schema = schemaSupport.schemaCheck; } - // (if packageServerCheck != null then { default = packageServerCheck; } else { }); + checks = { + schema = schemaSupport.schemaCheck; + } + // (if packageServerCheck != null then { default = packageServerCheck; } else { }); extraDevShells = if serverResult ? devShells then serverResult.devShells else { }; in { packages.default = pkgs.symlinkJoin { name = combinedName; - paths = [ packageServer schemaSupport.schema ]; + paths = [ + packageServer + schemaSupport.schema + ]; }; packages.server = packageServer; packages.schema = schemaSupport.schema; @@ -512,10 +533,19 @@ EOF # Language-neutral, offline schema compilation. Snapshot directories must be # 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 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 \ --root ${src} --kind package \ --repository ${pkgs.lib.escapeShellArg repository} \ @@ -551,219 +581,266 @@ EOF flake-utils.lib.eachDefaultSystem ( system: let - outputsFor = candidateBindings: - let - pkgs = import nixpkgs { inherit system; }; - lib = pkgs.lib; - nodejs = pkgs.${nodejsAttr}; - - callOption = value: - if builtins.isFunction value - then value { inherit inputs pkgs system; } - else value; - - packageJson = builtins.fromJSON (builtins.readFile "${packageRoot}/package.json"); - packageNameFinal = if packageName != null then packageName else packageJson.name or "quixos-package"; - promptNameFinal = - if promptName != null - then promptName - else lib.strings.sanitizeDerivationName packageNameFinal; - - sourcePackage = mkCaminoSourcePackage { - inherit pkgs; - src = packageRoot; - name = sourceName; - }; - - exportsFor = attrs: - lib.concatStringsSep "\n" ( - lib.mapAttrsToList - (name: value: "export ${name}=${lib.escapeShellArg (toString value)}") - attrs - ); - - bindingConfig = if candidateBindings != null then candidateBindings - else if bindings == null then null else callOption bindings; - bindingSchema = if bindingConfig == null then null else bindingConfig.schema or (mkQxBindingSchema { - inherit pkgs; - protocol = bindingConfig.generator; - src = packageRoot; - inherit (bindingConfig) repository commit; - resources = bindingConfig.resources or [ ]; - }); - 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"); - bindingOptionsFile = if bindingConfig == null then null else - pkgs.writeText "qx-typescript-options.json" (builtins.toJSON (authoringCheck.options or { })); - bindingCommand = if bindingConfig == null then "" else '' - mkdir -p ${lib.escapeShellArg (dirOf bindingOutput)} - ${bindingConfig.generator}/bin/quixos-codegen-ts \ - ${lib.escapeShellArg (toString bindingSchema)} \ - ${lib.escapeShellArg bindingConfig.packageRevisionId} \ - ${lib.escapeShellArg bindingOutput} ${bindingOptionsFile} - ${lib.optionalString (installServer != null && descriptorPath != null) '' - ${bindingConfig.generator}/bin/quixos-qx package-descriptor ${lib.escapeShellArg (toString bindingSchema)} \ - ${lib.escapeShellArg bindingConfig.packageRevisionId} > ${lib.escapeShellArg descriptorPath} - ''} - ''; - generateBindings = pkgs.writeShellApplication { - name = "qx-generate-bindings"; - text = bindingCommand; - }; - - bundleConfig = if bundle == null then { } else bundle; - bundleOutfile = bundleConfig.outfile or "server.mjs"; - bundlePlatform = bundleConfig.platform or "node"; - bundleTarget = bundleConfig.target or "node24"; - bundleFormat = bundleConfig.format or "esm"; - bundleBanner = bundleConfig.banner or nodeRequireBanner; - bundleAliases = bundleConfig.aliases or { - "@automerge/automerge" = "./node_modules/@automerge/automerge/dist/mjs/entrypoints/fullfat_base64.js"; - }; - bundleAliasArgs = lib.concatStringsSep " " ( - lib.mapAttrsToList - (from: to: "--alias:${from}=${lib.escapeShellArg to}") - bundleAliases - ); - nodeRequireBanner = "import { createRequire } from 'module';const require = createRequire(import.meta.url);"; - bundleCommand = - if bundle == null - 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; - target = bundleTarget; format = bundleFormat; alias = bundleAliases; - preserveSymlinks = bundleConfig.preserveSymlinks or true; - banner.js = bundleBanner; outfile = bundleOutfile; - })} - '' else '' - esbuild ${lib.escapeShellArg bundleConfig.entry} \ - --bundle \ - --platform=${bundlePlatform} \ - --target=${bundleTarget} \ - --format=${bundleFormat} \ - ${bundleAliasArgs} \ - ${lib.optionalString (bundleConfig.preserveSymlinks or true) "--preserve-symlinks \\"} - --banner:js=${lib.escapeShellArg bundleBanner} \ - --outfile=${lib.escapeShellArg bundleOutfile} - ''; - - installConfig = if installServer == null then { } else installServer; - serverBin = installConfig.binName or "server"; - serverLibexecName = - installConfig.libexecName or (lib.strings.sanitizeDerivationName packageNameFinal); - serverFile = installConfig.serverFile or bundleOutfile; - descriptorPath = installConfig.descriptorPath or "descriptor.quixos-package.txtpb"; - extraFiles = installConfig.extraFiles or [ ]; - installExtraFile = file: + outputsFor = + candidateBindings: let - source = toString file.source; - target = file.target or (baseNameOf source); - mode = file.mode or "0644"; - in '' - install -Dm${toString mode} ${lib.escapeShellArg source} "$out/libexec/${serverLibexecName}/${target}" - ''; - installServerPhase = - if installServer == null - then null - else '' - runHook preInstall - install -Dm755 ${lib.escapeShellArg serverFile} "$out/libexec/${serverLibexecName}/${serverFile}" - ${lib.concatMapStringsSep "\n" installExtraFile extraFiles} - mkdir -p "$out/bin" - cat > "$out/bin/${serverBin}" < "$out/bin/migrate" < ${lib.escapeShellArg descriptorPath} + ''} + ''; + generateBindings = pkgs.writeShellApplication { + name = "qx-generate-bindings"; + text = bindingCommand; + }; + + bundleConfig = if bundle == null then { } else bundle; + bundleOutfile = bundleConfig.outfile or "server.mjs"; + bundlePlatform = bundleConfig.platform or "node"; + bundleTarget = bundleConfig.target or "node24"; + bundleFormat = bundleConfig.format or "esm"; + bundleBanner = bundleConfig.banner or nodeRequireBanner; + bundleAliases = + bundleConfig.aliases or { + "@automerge/automerge" = + "./node_modules/@automerge/automerge/dist/mjs/entrypoints/fullfat_base64.js"; + }; + bundleAliasArgs = lib.concatStringsSep " " ( + lib.mapAttrsToList (from: to: "--alias:${from}=${lib.escapeShellArg to}") bundleAliases + ); + nodeRequireBanner = "import { createRequire } from 'module';const require = createRequire(import.meta.url);"; + bundleCommand = + if bundle == null 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; + target = bundleTarget; + format = bundleFormat; + alias = bundleAliases; + preserveSymlinks = bundleConfig.preserveSymlinks or true; + banner.js = bundleBanner; + outfile = bundleOutfile; + } + ) + } + '' + else + '' + esbuild ${lib.escapeShellArg bundleConfig.entry} \ + --bundle \ + --platform=${bundlePlatform} \ + --target=${bundleTarget} \ + --format=${bundleFormat} \ + ${bundleAliasArgs} \ + ${lib.optionalString (bundleConfig.preserveSymlinks or true) "--preserve-symlinks \\"} + --banner:js=${lib.escapeShellArg bundleBanner} \ + --outfile=${lib.escapeShellArg bundleOutfile} + ''; + + installConfig = if installServer == null then { } else installServer; + serverBin = installConfig.binName or "server"; + serverLibexecName = + installConfig.libexecName or (lib.strings.sanitizeDerivationName packageNameFinal); + serverFile = installConfig.serverFile or bundleOutfile; + descriptorPath = installConfig.descriptorPath or "descriptor.quixos-package.txtpb"; + extraFiles = installConfig.extraFiles or [ ]; + installExtraFile = + file: + let + source = toString file.source; + target = file.target or (baseNameOf source); + mode = file.mode or "0644"; + in + '' + install -Dm${toString mode} ${lib.escapeShellArg source} "$out/libexec/${serverLibexecName}/${target}" + ''; + installServerPhase = + if installServer == null then + null + else + '' + runHook preInstall + install -Dm755 ${lib.escapeShellArg serverFile} "$out/libexec/${serverLibexecName}/${serverFile}" + ${lib.concatMapStringsSep "\n" installExtraFile extraFiles} + mkdir -p "$out/bin" + cat > "$out/bin/${serverBin}" < "$out/bin/migrate" <