Make Camino examples and scaffolds complete Yarn and Nix packages

This commit is contained in:
Timothy J. Aveni
2026-09-22 14:42:54 -07:00
parent 0da022e216
commit 785c54992f
8 changed files with 164 additions and 20 deletions
File diff suppressed because one or more lines are too long
+13 -8
View File
@@ -1,3 +1,4 @@
import { installDependencies } from "./install-dependencies.mjs";
import { buildService } from "./build-service.mjs";
import { buildComponents } from "./build-components.mjs";
/** Trusted Nix build driver. No package-defined build hooks run between generation and witnessing. */
@@ -69,14 +70,18 @@ for (const { target, mode, settings } of builds) {
await writable(work);
const modules = path.join(work, "node_modules");
await fs.mkdir(path.join(modules, "@quixos"), { recursive: true });
if (config.nodeModules) {
for (const item of await fs.readdir(config.nodeModules, { withFileTypes: true })) {
if (item.name === "@quixos") throw Error("Application dependencies cannot substitute the checked SDK");
await fs.symlink(path.join(config.nodeModules, item.name), path.join(modules, item.name));
}
}
await fs.symlink(config.core, path.join(modules, "@quixos/camino-replica-core"));
await fs.writeFile(path.join(work, "package.json"), JSON.stringify({ type: "module" }));
await installDependencies({
destination: modules,
application: config.nodeModules,
selected: { "@quixos/camino-replica-core": config.core },
reserved: ["@quixos/camino-replica-core", "@quixos/camino-replica-client", "@quixos/camino-replica-engine"],
});
const packageFile = path.join(work, "package.json");
const packageMetadata = await fs.readFile(packageFile, "utf8").then(JSON.parse, (error) => {
if (error.code === "ENOENT") return {};
throw error;
});
await fs.writeFile(packageFile, JSON.stringify({ ...packageMetadata, type: "module" }));
const bindingPath = relative(settings.bindingOutput);
const source =
mode === "remote"
+38 -11
View File
@@ -1,3 +1,4 @@
import { installDependencies } from "./install-dependencies.mjs";
import fs from "node:fs/promises";
import path from "node:path";
import { createHash } from "node:crypto";
@@ -66,17 +67,43 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
await writable(work);
const modules = path.join(work, "node_modules");
await fs.mkdir(path.join(modules, "@quixos"), { recursive: true });
if (config.nodeModules)
for (const item of await fs.readdir(config.nodeModules, { withFileTypes: true })) {
if (["@quixos", "react", "react-dom", "scheduler", "@types", "csstype"].includes(item.name))
throw Error("Application dependencies cannot substitute selected React/client types");
await fs.symlink(path.join(config.nodeModules, item.name), path.join(modules, item.name));
}
for (const item of await fs.readdir(path.join(settings.dependencies, "node_modules"))) {
await fs.symlink(path.join(settings.dependencies, "node_modules", item), path.join(modules, item));
await installDependencies({
destination: modules,
application: config.nodeModules,
defaults: [path.join(settings.dependencies, "node_modules")],
selected: { "@quixos/camino-react": settings.sdk },
reserved: [
"react",
"react-dom",
"scheduler",
"csstype",
"@types/react",
"@types/react-dom",
"@types/prop-types",
"@quixos/camino-react",
"@quixos/camino-replica-core",
"@quixos/camino-replica-client",
"@quixos/camino-replica-engine",
],
});
const packageFile = path.join(work, "package.json");
const packageMetadata = await fs.readFile(packageFile, "utf8").then(JSON.parse, (error) => {
if (error.code === "ENOENT") return {};
throw error;
});
await fs.writeFile(packageFile, JSON.stringify({ ...packageMetadata, type: "module" }));
// CommonJS dependencies may require React. An ESM bridge lets esbuild lower
// that require while retaining the one host-owned runtime instance.
const sharedAliases = [];
for (const [name, url] of Object.entries(imports)) {
const shim = path.join(work, "shared-" + digest(Buffer.from(name)).slice(0, 16) + ".mjs");
await fs.writeFile(
shim,
`export * from ${JSON.stringify(url)};` +
(name === "@quixos/camino-react" ? "" : `export {default} from ${JSON.stringify(url)};`),
);
sharedAliases.push("--alias:" + name + "=" + shim, "--external:" + url);
}
await fs.symlink(settings.sdk, path.join(modules, "@quixos/camino-react"));
await fs.writeFile(path.join(work, "package.json"), JSON.stringify({ type: "module" }));
const bindingPath = compiler.artifactPath(settings.bindingOutput),
bindings = compiler.generateComponentClientBindings(plan, pkg.revision),
assets = compiler.componentAssetDeclarations();
@@ -187,7 +214,7 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
'--define:process.env.NODE_ENV="production"',
"--metafile=" + metadata,
"--outfile=" + path.join(output, directory, "module.mjs"),
...Object.entries(imports).flatMap(([name, url]) => ["--alias:" + name + "=" + url, "--external:" + url]),
...sharedAliases,
...["svg", "png", "jpg", "jpeg", "gif", "webp", "avif", "woff", "woff2", "ttf", "ico"].map(
(extension) => "--loader:." + extension + "=dataurl",
),
+6 -1
View File
@@ -23,7 +23,12 @@ const writable = async (directory) => {
}
};
await writable(work);
await fs.writeFile(path.join(work, "package.json"), JSON.stringify({ type: "module" }));
const packageFile = path.join(work, "package.json");
const metadata = await fs.readFile(packageFile, "utf8").then(JSON.parse, (error) => {
if (error.code === "ENOENT") return {};
throw error;
});
await fs.writeFile(packageFile, JSON.stringify({ ...metadata, type: "module" }));
if (config.dependencies) await fs.symlink(config.dependencies, path.join(work, "node_modules"));
const members = Object.create(null);
let index = 0;
+53
View File
@@ -0,0 +1,53 @@
import fs from "node:fs/promises";
import path from "node:path";
import { createHash } from "node:crypto";
async function packages(directory) {
const result = new Map();
for (const entry of await fs.readdir(directory)) {
if (entry.startsWith(".")) continue;
if (entry.startsWith("@")) {
for (const name of await fs.readdir(path.join(directory, entry)))
result.set(`${entry}/${name}`, path.join(directory, entry, name));
} else result.set(entry, path.join(directory, entry));
}
return result;
}
async function packageDigest(directory) {
const digest = createHash("sha256");
async function visit(relative) {
for (const entry of (await fs.readdir(path.join(directory, relative), { withFileTypes: true })).sort((a, b) =>
a.name.localeCompare(b.name),
)) {
if (entry.name === "node_modules") continue;
const name = path.join(relative, entry.name),
file = path.join(directory, name);
if (entry.isDirectory()) await visit(name);
else {
const bytes = await fs.readFile(file);
digest.update(JSON.stringify([name, bytes.length]));
digest.update(bytes);
}
}
}
await visit("");
return digest.digest("hex");
}
/** Merge at package granularity: a scope such as @types is not a reserved package. */
export async function installDependencies({ destination, application, defaults = [], selected = {}, reserved = [] }) {
const entries = new Map();
for (const root of defaults) for (const [name, file] of await packages(root)) entries.set(name, file);
for (const [name, file] of Object.entries(selected)) entries.set(name, file);
if (application)
for (const [name, file] of await packages(application)) {
if (reserved.includes(name)) {
const canonical = entries.get(name);
if (!canonical || (await packageDigest(file)) !== (await packageDigest(canonical)))
throw Error("Application dependencies cannot substitute selected React/client types or checked SDK: " + name);
} else entries.set(name, file);
}
for (const [name, file] of entries) {
await fs.mkdir(path.dirname(path.join(destination, name)), { recursive: true });
await fs.symlink(file, path.join(destination, name));
}
}
+2
View File
@@ -848,6 +848,8 @@ in
{
mkCaminoReactPackage = import ./react-package.nix;
mkCaminoReactContracts = import ./react-contracts.nix;
mkCaminoYarnDependencies = import ./yarn-dependencies.nix;
mkCaminoYarnShell = import ./yarn-shell.nix;
mkCaminoTypeScriptPackage = import ./typescript-package.nix;
mkCaminoTypeScriptService = import ./typescript-service.nix;
assembleCaminoPackage = import ./assemble-package.nix;
+29
View File
@@ -0,0 +1,29 @@
{
pkgs,
src,
nodejs ? pkgs.nodejs_24,
}:
let
project = (pkgs.callPackage (src + "/yarn-project.nix") { inherit nodejs; }) {
inherit src;
overrideAttrs = _: {
buildPhase = "runHook preBuild; runHook postBuild";
installPhase = ''
runHook preInstall
test -d node_modules
mkdir -p "$out"
cp -a node_modules/. "$out/"
runHook postInstall
'';
};
};
in
project
// {
devShell = pkgs.mkShell {
packages = [
nodejs
project.yarn-freestanding
];
};
}
+22
View File
@@ -0,0 +1,22 @@
{ pkgs }:
let
yarnCli = pkgs.fetchurl {
url = "https://repo.yarnpkg.com/4.18.0/packages/yarnpkg-cli/bin/yarn.js";
hash = "sha512-/Lhxb+fNDuzhQf/Bi5IZOp35IEwbqDGJwoiDUiP8C75kr0c7qw1emSen2utcryuwfrJ4fMkzjKBA6hJfKh8vfg==";
};
yarn = pkgs.writeShellScriptBin "yarn" ''
exec ${pkgs.nodejs_24}/bin/node ${yarnCli} "$@"
'';
in
pkgs.mkShell {
packages = [
pkgs.nodejs_24
yarn
];
shellHook = ''
if [ ! -f .yarn/plugins/yarn-plugin-nixify.cjs ]; then
mkdir -p .yarn/plugins
cp ${./.yarn/plugins/yarn-plugin-nixify.cjs} .yarn/plugins/yarn-plugin-nixify.cjs
fi
'';
}