1import * as NodeChildProcess from "node:child_process";
2import * as NodeFSP from "node:fs/promises";
3import * as NodeOS from "node:os";
4import * as NodePath from "node:path";
5import * as NodeVM from "node:vm";
6import { build } from "vite-plus/pack";
7import { assert, it } from "vite-plus/test";
8
9import desktopConfig from "../vite.config.ts";
10
11it("keeps lazy Linux imports and worker bundles from executing desktop startup twice", async () => {
12 const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-desktop-bundle-"));
13 try {
14 const workerEntries = [
15 "src/electron/WindowsForegroundFocusWorker.ts",
16 "src/snapShot/GlobalShiftShortcutWorker.ts",
17 "src/snapShot/RegionSnapShotWorker.ts",
18 "src/snapShot/SnapShotAccessibilityWorker.ts",
19 ];
20 await Promise.all([
21 NodeFSP.mkdir(NodePath.join(directory, "src/electron"), { recursive: true }),
22 NodeFSP.mkdir(NodePath.join(directory, "src/snapShot"), { recursive: true }),
23 ]);
24 await Promise.all([
25 NodeFSP.writeFile(
26 NodePath.join(directory, "src/main.ts"),
27 `import { shared } from "./shared.ts";
28process.emit("startup", shared.value);
29void import("./linux.ts").then(({ result }) => process.emit("ready", result));`,
30 ),
31 NodeFSP.writeFile(
32 NodePath.join(directory, "src/shared.ts"),
33 "export const shared = { value: 42 };",
34 ),
35 NodeFSP.writeFile(
36 NodePath.join(directory, "src/linux.ts"),
37 'import { shared } from "./shared.ts"; export const result = shared.value + 1;',
38 ),
39 ...workerEntries.map((entry) =>
40 NodeFSP.writeFile(
41 NodePath.join(directory, entry),
42 'import { shared } from "../shared.ts"; process.emit("worker", shared.value);',
43 ),
44 ),
45 ]);
46 assert.ok(Array.isArray(desktopConfig.pack));
47 const fixtureEntries = new Set(["src/main.ts", ...workerEntries]);
48 for (const packConfig of desktopConfig.pack) {
49 if (!Array.isArray(packConfig.entry)) continue;
50 if (!packConfig.entry.some((entry) => fixtureEntries.has(entry))) continue;
51 await build({
52 ...packConfig,
53 config: false,
54 cwd: directory,
55 tsconfig: false,
56 sourcemap: false,
57 onSuccess: undefined,
58 logLevel: "silent",
59 });
60 }
61
62 const outputDirectory = NodePath.join(directory, "dist-electron");
63 const filenames = (await NodeFSP.readdir(outputDirectory, { recursive: true })).filter(
64 (filename) => filename.endsWith(".cjs"),
65 );
66 const sources = new Map(
67 await Promise.all(
68 filenames.map(async (filename) => {
69 const path = NodePath.join(outputDirectory, filename);
70 return [path, await NodeFSP.readFile(path, "utf8")];
71 }),
72 ),
73 );
74 const modules = new Map();
75 const startups = [];
76 const workers = [];
77 const ready = Promise.withResolvers();
78 const load = (filename, cacheModule = true) => {
79 const cached = modules.get(filename);
80 if (cached) return cached.exports;
81 const module = { exports: {} };
82 if (cacheModule) modules.set(filename, module);
83 const source = sources.get(filename);
84 assert.ok(source, `Missing bundle: ${filename}`);
85 NodeVM.runInNewContext(source, {
86 exports: module.exports,
87 module,
88 require: (specifier) => load(NodePath.resolve(NodePath.dirname(filename), specifier)),
89 process: {
90 emit: (event, value) => {
91 if (event === "startup") startups.push(value);
92 if (event === "worker") workers.push(value);
93 if (event === "ready") ready.resolve(value);
94 },
95 },
96 });
97 return module.exports;
98 };
99
100 load(NodePath.join(outputDirectory, "main.cjs"), false);
101 assert.equal(await ready.promise, 43);
102 assert.deepEqual(startups, [42]);
103 for (const entry of workerEntries) {
104 load(NodePath.join(outputDirectory, entry.replace(/^src\//, "").replace(/\.ts$/, ".cjs")));
105 }
106 assert.deepEqual(workers, [42, 42, 42, 42]);
107 assert.deepEqual(startups, [42]);
108 } finally {
109 await NodeFSP.rm(directory, { recursive: true, force: true });
110 }
111});
112
113it("loads the emitted packaged boot entry and backend cache preload", async () => {
114 const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-desktop-boot-"));
115 try {
116 const entries = new Set(["src/boot.ts", "src/compileCache.ts"]);
117 const outputDirectory = NodePath.join(directory, "bundle/apps/desktop/dist-electron");
118 assert.ok(Array.isArray(desktopConfig.pack));
119 for (const packConfig of desktopConfig.pack) {
120 if (!Array.isArray(packConfig.entry)) continue;
121 if (!packConfig.entry.some((entry) => entries.has(entry))) continue;
122 await build({
123 ...packConfig,
124 config: false,
125 cwd: NodePath.resolve(import.meta.dirname, ".."),
126 outDir: outputDirectory,
127 tsconfig: false,
128 sourcemap: false,
129 onSuccess: undefined,
130 logLevel: "silent",
131 });
132 }
133 const fixture = `console.log(require('node:module').getCompileCacheDir() ? 'cached' : 'uncached');`;
134 await NodeFSP.writeFile(NodePath.join(outputDirectory, "main.cjs"), fixture);
135 await NodeFSP.writeFile(
136 NodePath.join(outputDirectory, "backend.mjs"),
137 `import { getCompileCacheDir } from 'node:module'; console.log(getCompileCacheDir() ? 'cached' : 'uncached');`,
138 );
139 for (const disabled of [false, true]) {
140 for (const args of [
141 [NodePath.join(outputDirectory, "boot.cjs")],
142 [
143 "--require",
144 NodePath.join(outputDirectory, "compileCache.cjs"),
145 NodePath.join(outputDirectory, "backend.mjs"),
146 ],
147 ]) {
148 const child = NodeChildProcess.spawnSync(process.execPath, args, {
149 encoding: "utf8",
150 env: {
151 ...process.env,
152 APPIMAGE: "",
153 NODE_COMPILE_CACHE: undefined,
154 NODE_DISABLE_COMPILE_CACHE: disabled ? "1" : undefined,
155 XDG_CACHE_HOME: directory,
156 TMPDIR: directory,
157 TEMP: directory,
158 TMP: directory,
159 },
160 });
161 assert.equal(child.status, 0, child.stderr);
162 assert.equal(child.stdout.trim(), disabled ? "uncached" : "cached");
163 }
164 }
165 const shell = "a".repeat(64);
166 const payload = NodePath.join(directory, "data/userdata/fork-releases/release-test");
167 for (const name of ["apps/desktop/dist-electron", "apps/server/dist/client"]) {
168 await NodeFSP.mkdir(NodePath.join(payload, name), { recursive: true });
169 }
170 await NodeFSP.cp(outputDirectory, NodePath.join(payload, "apps/desktop/dist-electron"), {
171 recursive: true,
172 });
173 await NodeFSP.writeFile(
174 NodePath.join(payload, "apps/desktop/dist-electron/main.cjs"),
175 "console.log('payload:' + JSON.parse(process.env.T3CODE_DESKTOP_PAYLOAD).version);",
176 );
177 await NodeFSP.writeFile(NodePath.join(payload, "apps/desktop/dist-electron/preload.cjs"), "");
178 await NodeFSP.writeFile(NodePath.join(payload, "apps/server/dist/bin.mjs"), "");
179 await NodeFSP.writeFile(
180 NodePath.join(payload, "apps/server/dist/client/index.html"),
181 "fixture",
182 );
183 await NodeFSP.writeFile(
184 NodePath.join(directory, "bundle/knife-runtime.json"),
185 JSON.stringify({ shell }),
186 );
187 await NodeFSP.writeFile(
188 NodePath.join(payload, "../current.json"),
189 JSON.stringify({
190 directory: "release-test",
191 shell,
192 runtime: "b".repeat(64),
193 version: "0.0.2000000000",
194 }),
195 );
196 const electronStub = NodePath.join(directory, "electron.cjs");
197 await NodeFSP.writeFile(
198 electronStub,
199 `
200 Object.defineProperty(process.versions, 'electron', { value: '44.4.5' });
201 const Module = require('node:module');
202 const load = Module._load;
203 Module._load = function(name, ...args) {
204 return name === 'electron' ? { app: { isPackaged: true } } : load.call(this, name, ...args);
205 };
206 `,
207 );
208 const selected = NodeChildProcess.spawnSync(
209 process.execPath,
210 ["--require", electronStub, NodePath.join(outputDirectory, "boot.cjs")],
211 {
212 encoding: "utf8",
213 env: { ...process.env, T3CODE_HOME: NodePath.join(directory, "data") },
214 },
215 );
216 assert.equal(selected.status, 0, selected.stderr);
217 assert.equal(selected.stdout.trim(), "payload:0.0.2000000000");
218 } finally {
219 await NodeFSP.rm(directory, { recursive: true, force: true });
220 }
221});