History

Simplify self-host Docker runtime packaging

Rhys Sullivan committed 4 months ago

Commit 84df8de1 parent a65590b2 files · +241 −129Browse files

Showing the changes to scripts (1 of 2 files). Show all

Unified · Split

scripts/package-selfhost-runtime.ts+236−0
@@ -0,0 +1,236 @@
1+import {
2+ copyFileSync,
3+ existsSync,
4+ lstatSync,
5+ mkdirSync,
6+ readdirSync,
7+ readFileSync,
8+ readlinkSync,
9+ rmSync,
10+ symlinkSync,
11+} from "node:fs";
12+import { dirname, join, relative } from "node:path";
13+
14+const root = process.cwd();
15+const out = join(root, ".selfhost-runtime");
16+const appName = "@executor-js/host-selfhost";
17+
18+const packageRoots = ["apps", "packages", "examples"] as const;
19+const packagePruneExtensions = [".map", ".d.ts", ".d.mts", ".d.cts", ".md", ".markdown"];
20+const nodeModulePruneExtensions = [...packagePruneExtensions, ".ts", ".tsx"];
21+const sourcePruneExtensions = [".test.ts", ".test.tsx"];
22+const prunedDirNames = new Set([
23+ ".git",
24+ ".turbo",
25+ "test",
26+ "tests",
27+ "__tests__",
28+ "fixtures",
29+ "fixture",
30+ "examples",
31+ "__snapshots__",
32+]);
33+const packageConfigFiles = new Set([
34+ "tsconfig.json",
35+ "tsup.config.ts",
36+ "vite.config.ts",
37+ "vitest.config.ts",
38+ "CHANGELOG.md",
39+]);
40+
41+const nodeModulesPackagePrunes = [
42+ /^@ant-design\+/,
43+ /^@base-ui\+/,
44+ /^@cloudflare\+workers-types@/,
45+ /^@electric-sql\+pglite@/,
46+ /^@emotion\+react@/,
47+ /^@emoji-mart\+/,
48+ /^@esbuild\+.*linux-x64-musl/,
49+ /^@esbuild\+/,
50+ /^@floating-ui\+react@/,
51+ /^@dnd-kit\+/,
52+ /^@hookform\+/,
53+ /^@lobehub\+/,
54+ /^@mermaid-js\+/,
55+ /^@pierre\+diffs@/,
56+ /^@pierre\+theme@/,
57+ /^@primer\+octicons@/,
58+ /^@rc-component\+/,
59+ /^@rolldown\+binding-/,
60+ /^@reduxjs\+/,
61+ /^@shikijs\+/,
62+ /^@splinetool\+/,
63+ /^@tanstack\+start-plugin-core@/,
64+ /^@tanstack\+react-router@/,
65+ /^@tanstack\+router-core@/,
66+ /^@types\+/,
67+ /^ahooks@/,
68+ /^antd@/,
69+ /^better-sqlite3@/,
70+ /^bun-types@/,
71+ /^caniuse-lite@/,
72+ /^chroma-js@/,
73+ /^cytoscape/,
74+ /^d3@/,
75+ /^date-fns/,
76+ /^drizzle-kit@/,
77+ /^dompurify@/,
78+ /^emoji-mart@/,
79+ /^framer-motion@/,
80+ /^jiti@/,
81+ /^katex@/,
82+ /^layout-base@/,
83+ /^leva@/,
84+ /^lightningcss-/,
85+ /^lit-html@/,
86+ /^lucide-react@/,
87+ /^mermaid@/,
88+ /^motion@/,
89+ /^motion-/,
90+ /^polished@/,
91+ /^prettier@/,
92+ /^react-day-picker@/,
93+ /^react-hook-form@/,
94+ /^react@/,
95+ /^react-dom@/,
96+ /^react-dropzone@/,
97+ /^recharts@/,
98+ /^rolldown@/,
99+ /^shiki@/,
100+ /^solid-js@/,
101+ /^source-map@/,
102+ /^tailwind-merge@/,
103+ /^tsx@/,
104+ /^@upsetjs\+venn\.js@/,
105+ /^victory-vendor@/,
106+ /^virtua@/,
107+ /^typeorm@/,
108+ /^vite@/,
109+ /^vitest@/,
110+];
111+
112+type PackageJson = {
113+ readonly name?: string;
114+ readonly dependencies?: Record<string, string>;
115+ readonly optionalDependencies?: Record<string, string>;
116+};
117+
118+type WorkspacePackage = {
119+ readonly name: string;
120+ readonly dir: string;
121+ readonly packageJson: PackageJson;
122+};
123+
124+const readJson = <T>(path: string): T => JSON.parse(readFileSync(path, "utf8")) as T;
125+
126+const copyRecursive = (
127+ source: string,
128+ target: string,
129+ shouldSkip: (
130+ source: string,
131+ relativePath: string,
132+ stats: ReturnType<typeof lstatSync>,
133+ ) => boolean,
134+) => {
135+ const stats = lstatSync(source);
136+ const rel = relative(root, source);
137+ if (shouldSkip(source, rel, stats)) return;
138+
139+ if (stats.isSymbolicLink()) {
140+ mkdirSync(dirname(target), { recursive: true });
141+ symlinkSync(readlinkSync(source), target);
142+ return;
143+ }
144+
145+ if (stats.isDirectory()) {
146+ mkdirSync(target, { recursive: true });
147+ for (const entry of readdirSync(source)) {
148+ copyRecursive(join(source, entry), join(target, entry), shouldSkip);
149+ }
150+ return;
151+ }
152+
153+ if (stats.isFile()) {
154+ mkdirSync(dirname(target), { recursive: true });
155+ copyFileSync(source, target);
156+ }
157+};
158+
159+const discoverWorkspacePackages = (): Map<string, WorkspacePackage> => {
160+ const packages = new Map<string, WorkspacePackage>();
161+ for (const rootName of packageRoots) {
162+ const rootDir = join(root, rootName);
163+ if (!existsSync(rootDir)) continue;
164+ for (const first of readdirSync(rootDir)) {
165+ const firstDir = join(rootDir, first);
166+ if (!lstatSync(firstDir).isDirectory()) continue;
167+ const candidates = [firstDir, ...readdirSync(firstDir).map((name) => join(firstDir, name))];
168+ for (const candidate of candidates) {
169+ const packageJsonPath = join(candidate, "package.json");
170+ if (!existsSync(packageJsonPath)) continue;
171+ const packageJson = readJson<PackageJson>(packageJsonPath);
172+ if (packageJson.name) {
173+ packages.set(packageJson.name, { name: packageJson.name, dir: candidate, packageJson });
174+ }
175+ }
176+ }
177+ }
178+ return packages;
179+};
180+
181+const workspacePackages = discoverWorkspacePackages();
182+const target = workspacePackages.get(appName);
183+if (!target) throw new Error(`Could not find ${appName}`);
184+
185+const reachable = new Map<string, WorkspacePackage>();
186+const queue = [target];
187+for (const current of queue) {
188+ if (reachable.has(current.name)) continue;
189+ reachable.set(current.name, current);
190+ const dependencies = {
191+ ...current.packageJson.dependencies,
192+ ...current.packageJson.optionalDependencies,
193+ };
194+ for (const dependencyName of Object.keys(dependencies)) {
195+ const workspaceDependency = workspacePackages.get(dependencyName);
196+ if (workspaceDependency) queue.push(workspaceDependency);
197+ }
198+}
199+
200+rmSync(out, { recursive: true, force: true });
201+mkdirSync(out, { recursive: true });
202+
203+copyRecursive(join(root, "package.json"), join(out, "package.json"), () => false);
204+copyRecursive(join(root, "bun.lock"), join(out, "bun.lock"), () => false);
205+
206+copyRecursive(join(root, "node_modules"), join(out, "node_modules"), (source, rel, stats) => {
207+ const base = source.split("/node_modules/.bun/")[1]?.split("/")[0];
208+ if (base?.includes("linux-x64-musl")) return true;
209+ if (base && nodeModulesPackagePrunes.some((pattern) => pattern.test(base))) return true;
210+ const name = source.split("/").at(-1) ?? "";
211+ if (stats.isDirectory() && source !== join(root, "node_modules") && prunedDirNames.has(name))
212+ return true;
213+ if (stats.isFile() && nodeModulePruneExtensions.some((extension) => source.endsWith(extension)))
214+ return true;
215+ return false;
216+});
217+
218+for (const pkg of reachable.values()) {
219+ const destination = join(out, relative(root, pkg.dir));
220+ copyRecursive(pkg.dir, destination, (source, rel, stats) => {
221+ const name = source.split("/").at(-1) ?? "";
222+ if (stats.isDirectory() && prunedDirNames.has(name)) return true;
223+ if (stats.isFile() && packageConfigFiles.has(name)) return true;
224+ if (stats.isFile() && sourcePruneExtensions.some((extension) => source.endsWith(extension)))
225+ return true;
226+ if (stats.isFile() && packagePruneExtensions.some((extension) => source.endsWith(extension)))
227+ return true;
228+ if (rel === "apps/host-selfhost/Dockerfile" || rel === "apps/host-selfhost/docker-compose.yml")
229+ return true;
230+ if (rel.startsWith("apps/host-selfhost/web/")) return true;
231+ if (rel.startsWith("apps/host-selfhost/scripts/")) return true;
232+ return false;
233+ });
234+}
235+
236+console.log(`Packaged ${reachable.size} workspace packages into ${relative(root, out)}`);