jevstrudel.git / nix / worker-closure.mjs
1#!/usr/bin/env node

The Worker as the deploy uploads it: worker/ with a node_modules of its own, holding exactly its dependencies' closure, so wrangler deploy in a copy of it outside the repo resolves @simplewebauthn/server and everything that needs.

worker-closure.mjs <installed worker dir> <out dir>

Run in nix/strudel.nix after pnpm has installed the workspace (worker/ is a workspace package). pnpm's layout is already self-contained per package: node_modules/.pnpm/<entry>/node_modules/ holds the package and relative symlinks to its dependencies' entries beside it. So the closure is those entries, found by following the symlinks from worker's own node_modules, copied with their symlinks as they are, plus worker's top-level links pointing into the copy. pnpm deploy would do this, but in pnpm 10 it either needs injected workspace packages (changing how the whole workspace links) or re-resolves every workspace package's dependencies from the registry (--legacy), which the sandbox cannot.

19import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, realpathSync, symlinkSync } from 'node:fs';
20import { dirname, join, relative, resolve, sep } from 'node:path';
22const [from, out] = process.argv.slice(2).map((p) => resolve(p));
23if (!from || !out) {
24  console.error('usage: worker-closure.mjs <installed worker dir> <out dir>');
25  process.exit(2);
26}
27
28const STORE = `${sep}node_modules${sep}.pnpm${sep}`;

The .pnpm entry a real path is inside: [store dir, entry name].

30function entryOf(real) {
31  const at = real.lastIndexOf(STORE);
32  if (at < 0) throw new Error(`not in a pnpm store: ${real}`);
33  const store = real.slice(0, at + STORE.length - 1);
34  return [store, real.slice(at + STORE.length).split(sep)[0]];
35}

The package names in a node_modules directory (name and @scope/name).

38function packages(dir) {
39  if (!existsSync(dir)) return [];
40  return readdirSync(dir)
41    .filter((n) => !n.startsWith('.'))
42    .flatMap((n) => (n.startsWith('@') ? readdirSync(join(dir, n)).map((m) => `${n}/${m}`) : [n]));
43}

worker's own files, without what an install or a dev run leaves in it

46cpSync(from, out, {
47  recursive: true,
48  verbatimSymlinks: true,
49  filter: (src) => {
50    const rel = relative(from, src).split(sep)[0];
51    return rel !== 'node_modules' && rel !== '.wrangler' && rel !== 'worker-configuration.d.ts';
52  },
53});
55const outStore = join(out, 'node_modules', '.pnpm');
56const seen = new Set();
57const queue = [];
58const top = packages(join(from, 'node_modules'));
59for (const name of top) {
60  const real = realpathSync(join(from, 'node_modules', name));
61  const [store, entry] = entryOf(real);
62  queue.push([store, entry]);
63  const link = join(out, 'node_modules', name);
64  mkdirSync(dirname(link), { recursive: true });
65  symlinkSync(relative(dirname(link), join(outStore, entry, relative(join(store, entry), real))), link);
66}
67while (queue.length) {
68  const [store, entry] = queue.pop();
69  if (seen.has(entry)) continue;
70  seen.add(entry);
71  cpSync(join(store, entry), join(outStore, entry), { recursive: true, verbatimSymlinks: true });
72  const modules = join(store, entry, 'node_modules');
73  for (const name of packages(modules)) {
74    const path = join(modules, name);
75    if (!lstatSync(path).isSymbolicLink()) continue; // the package itself
76    const target = resolve(dirname(path), readlinkSync(path));
77    const [depStore, dep] = entryOf(target);
78    if (depStore !== store) throw new Error(`${entry} links outside its store: ${name}`);
79    queue.push([store, dep]);
80  }
81}
82console.error(`worker-closure: ${top.length} dependencies, ${seen.size} packages in their closure`);