1#!/usr/bin/env node
2const fs = require("fs");
3const path = require("path");
4
5async function asyncFilter(arr, pred) {
6 const filtered = [];
7 for (const elem of arr) {
8 if (await pred(elem)) {
9 filtered.push(elem);
10 }
11 }
12 return filtered;
13}
14
15// Get a list of all _unmanaged_ files in node_modules.
16// This means every file in node_modules that is _not_ a symlink to the Nix store.
17async function getUnmanagedFiles(storePrefix, files) {
18 return await asyncFilter(files, async (file) => {
19 const filePath = path.join("node_modules", file);
20
21 // Is file a symlink
22 const stat = await fs.promises.lstat(filePath);
23 if (!stat.isSymbolicLink()) {
24 return true;
25 }
26
27 // Is file in the store
28 const linkTarget = await fs.promises.readlink(filePath);
29 return !linkTarget.startsWith(storePrefix);
30 });
31}
32
33async function main() {
34 const args = process.argv.slice(2);
35 const storePrefix = args[0];
36 const sourceModules = args[1];
37
38 // Ensure node_modules exists
39 try {
40 await fs.promises.mkdir("node_modules");
41 } catch (err) {
42 if (err.code !== "EEXIST") {
43 throw err;
44 }
45 }
46
47 const files = await fs.promises.readdir("node_modules");
48
49 // Get deny list of files that we don't manage.
50 // We do manage nix store symlinks, but not other files.
51 // For example: If a .vite was present in both our
52 // source node_modules and the non-store node_modules we don't want to overwrite
53 // the non-store one.
54 const unmanaged = await getUnmanagedFiles(storePrefix, files);
55 const managed = new Set(files.filter((file) => ! unmanaged.includes(file)));
56
57 const sourceFiles = await fs.promises.readdir(sourceModules);
58 await Promise.all(
59 sourceFiles.map(async (file) => {
60 const sourcePath = path.join(sourceModules, file);
61 const targetPath = path.join("node_modules", file);
62
63 // Skip file if it's not a symlink to a store path
64 if (unmanaged.includes(file)) {
65 console.log(`'${targetPath}' exists, cowardly refusing to link.`);
66 return;
67 }
68
69 // Don't unlink this file, we just wrote it.
70 managed.delete(file);
71
72 // Link file
73 try {
74 await fs.promises.symlink(sourcePath, targetPath);
75 } catch (err) {
76 // If the target file already exists remove it and try again
77 if (err.code !== "EEXIST") {
78 throw err;
79 }
80 await fs.promises.unlink(targetPath);
81 await fs.promises.symlink(sourcePath, targetPath);
82 }
83 })
84 );
85
86 // Clean up store symlinks not included in this generation of node_modules
87 await Promise.all(
88 Array.from(managed).map((file) =>
89 fs.promises.unlink(path.join("node_modules", file)),
90 )
91 );
92}
93
94main();