Open History to see the last commit
1/* oxlint-disable t3code/no-global-process-runtime -- Runs in the packaged engine before an Effect runtime exists. */
2// @effect-diagnostics nodeBuiltinImport:off
3import * as NodeFS from "node:fs";
4import * as NodePath from "node:path";
5import * as NodeModule from "node:module";
6import * as NodeV8 from "node:v8";
7import * as NodeZlib from "node:zlib";
8import * as NodeURL from "node:url";
9
10export const runtimeIdentity = () =>
11 [
12 process.versions.electron,
13 process.version,
14 process.platform,
15 process.arch,
16 NodeV8.cachedDataVersionTag(),
17 ].join("-");
18
19// Node 24's portable key is CRC32 of the one-byte ESM type followed by the relative filename.
20// Generation verifies this against files produced by the packaged engine before shipping them.
21export const cacheKey = (active: string, filename: string) =>
22 NodeZlib.crc32(NodePath.relative(active, filename), NodeZlib.crc32(Buffer.from([1])))
23 .toString(16)
24 .padStart(8, "0");
25
26export function seedPreparedCache(prepared: string, active: string, runtime: string): boolean {
27 try {
28 const manifest: unknown = JSON.parse(
29 NodeFS.readFileSync(NodePath.join(prepared, "manifest.json"), "utf8"),
30 );
31 if (
32 typeof manifest !== "object" ||
33 manifest === null ||
34 !("format" in manifest) ||
35 manifest.format !== 1 ||
36 !("runtime" in manifest) ||
37 manifest.runtime !== runtime ||
38 !("entries" in manifest) ||
39 !Array.isArray(manifest.entries) ||
40 manifest.entries.length === 0
41 )
42 return false;
43 for (const entry of manifest.entries) {
44 if (
45 typeof entry?.module !== "string" ||
46 typeof entry?.file !== "string" ||
47 !/^[a-f0-9]{8}$/.test(entry.file) ||
48 !/^(app|server)\.asar\/apps\/server\/dist\/[^/\\]+\.mjs$/.test(entry.module)
49 )
50 return false;
51 const source = NodePath.join(prepared, entry.file);
52 const destination = NodePath.join(
53 active,
54 cacheKey(active, NodePath.join(NodePath.dirname(prepared), entry.module)),
55 );
56 try {
57 // Node replaces cache files atomically. A directory link would instead allow writes into signed resources.
58 if (process.platform === "win32") NodeFS.linkSync(source, destination);
59 else NodeFS.symlinkSync(source, destination);
60 } catch {}
61 if (!NodeFS.existsSync(destination)) return false;
62 }
63 return true;
64 } catch {
65 return false;
66 }
67}
68
69export function enablePreparedServerCache(entry: string, cacheRoot: string): boolean {
70 try {
71 const archive = entry.match(/^(.*[\\/](?:app|server)\.asar)[\\/]/)?.[1];
72 if (!archive) return false;
73 const resources = NodeFS.realpathSync(NodePath.dirname(archive));
74 // Keep different installations and builds separate without reading or copying their source files.
75 const prepared = NodePath.join(resources, "server-compile-cache");
76 if (process.env.APPIMAGE && !NodeFS.existsSync(NodePath.join(prepared, "manifest.json")))
77 return false;
78 const identity = NodeZlib.crc32(resources).toString(16);
79 NodeFS.mkdirSync(NodePath.join(cacheRoot, "server"), { recursive: true });
80 if (process.env.APPIMAGE) {
81 try {
82 for (const candidate of NodeFS.readdirSync(NodePath.join(cacheRoot, "server"), {
83 withFileTypes: true,
84 })) {
85 if (!candidate.isDirectory() || !candidate.name.startsWith("appimage-")) continue;
86 const abandoned = NodePath.join(cacheRoot, "server", candidate.name);
87 let hasStaleLink = false;
88 // Node nests files under an engine/UID directory and can replace links with regular files.
89 const removable = NodeFS.readdirSync(abandoned, { withFileTypes: true }).every(
90 (engine) =>
91 engine.isDirectory() &&
92 NodeFS.readdirSync(NodePath.join(abandoned, engine.name), {
93 withFileTypes: true,
94 }).every((file) => {
95 if (!/^[a-f0-9]{8}$/.test(file.name)) return false;
96 if (!file.isSymbolicLink()) return file.isFile();
97 const target = NodeFS.readlinkSync(
98 NodePath.join(abandoned, engine.name, file.name),
99 );
100 const mount = target.match(/^(.*[\\/]\.mount_[^/\\]+)[\\/]/)?.[1];
101 if (
102 !mount ||
103 !NodePath.isAbsolute(target) ||
104 NodePath.basename(NodePath.dirname(target)) !== "server-compile-cache" ||
105 !/^[a-f0-9]{8}$/.test(NodePath.basename(target))
106 )
107 return false;
108 if (NodeFS.lstatSync(mount, { throwIfNoEntry: false })) return false;
109 hasStaleLink = true;
110 return true;
111 }),
112 );
113 if (removable && hasStaleLink) NodeFS.rmSync(abandoned, { recursive: true, force: true });
114 }
115 } catch {
116 // Concurrent removal or restricted permissions must not prevent enabling this launch's cache.
117 }
118 }
119 const directory = process.env.APPIMAGE
120 ? NodeFS.mkdtempSync(NodePath.join(cacheRoot, "server", "appimage-"))
121 : NodePath.join(cacheRoot, "server", identity);
122 if (process.env.APPIMAGE) {
123 // Each mount has different module paths; discard its links rather than accumulating caches.
124 process.once("exit", () => {
125 try {
126 NodeFS.rmSync(directory, { recursive: true, force: true });
127 } catch {}
128 });
129 }
130 const result = NodeModule.enableCompileCache({
131 directory,
132 portable: true,
133 });
134 const active = NodeModule.getCompileCacheDir();
135 if (result.status !== NodeModule.constants.compileCacheStatus.ENABLED || !active) return false;
136 const seeded = seedPreparedCache(prepared, active, runtimeIdentity());
137 if (seeded) Reflect.set(globalThis, Symbol.for("t3code.preparedServerCache"), true);
138 return true;
139 } catch {
140 return false;
141 }
142}
143
144export async function generatePreparedServerCache(resourcesInput: string): Promise<void> {
145 const resources = NodeFS.realpathSync(resourcesInput);
146 const prepared = NodePath.join(resources, "server-compile-cache");
147 const archive = NodeFS.existsSync(NodePath.join(resources, "server.asar"))
148 ? "server.asar"
149 : "app.asar";
150 const dist = NodePath.join(resources, archive, "apps/server/dist");
151 NodeFS.mkdirSync(prepared, { recursive: true });
152 const result = NodeModule.enableCompileCache({ directory: prepared, portable: true });
153 if (result.status !== NodeModule.constants.compileCacheStatus.ENABLED)
154 throw new Error("Cannot enable release compile cache");
155 const files = NodeFS.readdirSync(dist);
156 const cli = files.find((file) => /^binCli-.*\.mjs$/.test(file));
157 if (!cli) throw new Error("Missing bundled server CLI");
158 const servers = files.filter((file) => /^server-.*\.mjs$/.test(file));
159 if (servers.length > 1) throw new Error("Ambiguous bundled server entry");
160 const server = servers[0];
161 if (!server) throw new Error("Missing bundled server entry");
162 await import(NodeURL.pathToFileURL(NodePath.join(dist, cli)).href);
163 // The CLI loads the server dynamically; importing its declarative layers does not launch them.
164 await import(NodeURL.pathToFileURL(NodePath.join(dist, server)).href);
165 NodeModule.flushCompileCache();
166 const active = NodeModule.getCompileCacheDir()!;
167 const entries = files
168 .filter((file) => file.endsWith(".mjs"))
169 .flatMap((file) => {
170 const key = cacheKey(active, NodePath.join(dist, file));
171 if (!NodeFS.existsSync(NodePath.join(active, key))) return [];
172 NodeFS.copyFileSync(NodePath.join(active, key), NodePath.join(prepared, key));
173 return [{ module: `${archive}/apps/server/dist/${file}`, file: key }];
174 });
175 if (![cli, server].every((file) => entries.some((entry) => entry.module.endsWith(`/${file}`))))
176 throw new Error("Packaged engine cache key format is unsupported");
177 NodeFS.writeFileSync(
178 NodePath.join(prepared, "manifest.json"),
179 JSON.stringify({ format: 1, runtime: runtimeIdentity(), entries }),
180 );
181 NodeFS.rmSync(active, { recursive: true });
182}