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 @@
1import {
2 copyFileSync,
3 existsSync,
4 lstatSync,
5 mkdirSync,
6 readdirSync,
7 readFileSync,
8 readlinkSync,
9 rmSync,
10 symlinkSync,
11} from "node:fs";
12import { dirname, join, relative } from "node:path";
13
14const root = process.cwd();
15const out = join(root, ".selfhost-runtime");
16const appName = "@executor-js/host-selfhost";
17
18const packageRoots = ["apps", "packages", "examples"] as const;
19const packagePruneExtensions = [".map", ".d.ts", ".d.mts", ".d.cts", ".md", ".markdown"];
20const nodeModulePruneExtensions = [...packagePruneExtensions, ".ts", ".tsx"];
21const sourcePruneExtensions = [".test.ts", ".test.tsx"];
22const prunedDirNames = new Set([
23 ".git",
24 ".turbo",
25 "test",
26 "tests",
27 "__tests__",
28 "fixtures",
29 "fixture",
30 "examples",
31 "__snapshots__",
32]);
33const packageConfigFiles = new Set([
34 "tsconfig.json",
35 "tsup.config.ts",
36 "vite.config.ts",
37 "vitest.config.ts",
38 "CHANGELOG.md",
39]);
40
41const 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
112type PackageJson = {
113 readonly name?: string;
114 readonly dependencies?: Record<string, string>;
115 readonly optionalDependencies?: Record<string, string>;
116};
117
118type WorkspacePackage = {
119 readonly name: string;
120 readonly dir: string;
121 readonly packageJson: PackageJson;
122};
123
124const readJson = <T>(path: string): T => JSON.parse(readFileSync(path, "utf8")) as T;
125
126const 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
159const 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
181const workspacePackages = discoverWorkspacePackages();
182const target = workspacePackages.get(appName);
183if (!target) throw new Error(`Could not find ${appName}`);
184
185const reachable = new Map<string, WorkspacePackage>();
186const queue = [target];
187for (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
200rmSync(out, { recursive: true, force: true });
201mkdirSync(out, { recursive: true });
202
203copyRecursive(join(root, "package.json"), join(out, "package.json"), () => false);
204copyRecursive(join(root, "bun.lock"), join(out, "bun.lock"), () => false);
205
206copyRecursive(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
218for (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
236console.log(`Packaged ${reachable.size} workspace packages into ${relative(root, out)}`);