1#!/usr/bin/env node 2// The Worker as the deploy uploads it: worker/ with a node_modules of its 3// own, holding exactly its dependencies' closure, so `wrangler deploy` in a 4// copy of it outside the repo resolves @simplewebauthn/server and 5// everything that needs. 6// 7// worker-closure.mjs <installed worker dir> <out dir> 8// 9// Run in nix/strudel.nix after pnpm has installed the workspace (worker/ is 10// a workspace package). pnpm's layout is already self-contained per 11// package: `node_modules/.pnpm/<entry>/node_modules/` holds the package and 12// relative symlinks to its dependencies' entries beside it. So the closure 13// is those entries, found by following the symlinks from worker's own 14// node_modules, copied with their symlinks as they are, plus worker's 15// top-level links pointing into the copy. `pnpm deploy` would do this, but 16// in pnpm 10 it either needs injected workspace packages (changing how the 17// whole workspace links) or re-resolves every workspace package's 18// 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'; 21 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}`; 29// 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} 36 37// 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} 44 45// 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}); 54 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`);