authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-14 22:11:10-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-14 22:11:10-07:00
logb20d29dc438bcabb2c25e85976dc108169bb4c6d
treef31848f877fecf97247d91fe9ae8a55583a5dce5
parent0310cbf9f46ca94e9df0231a3440bca08ac44709
signature Signed by PGP key B5690EEEBB952194

fix(desktop): prevent startup from running twice (#11857)


2 files changed, 128 insertions(+), 3 deletions(-)

apps/desktop/scripts/main-process-bundle.test.mjs created+110
......@@ -0,0 +1,110 @@
1import * as NodeFSP from "node:fs/promises";
2import * as NodeOS from "node:os";
3import * as NodePath from "node:path";
4import * as NodeVM from "node:vm";
5import { build } from "vite-plus/pack";
6import { assert, it } from "vite-plus/test";
7
8import desktopConfig from "../vite.config.ts";
9
10it("keeps lazy Linux imports and worker bundles from executing desktop startup twice", async () => {
11 const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-desktop-bundle-"));
12 try {
13 const workerEntries = [
14 "src/electron/WindowsForegroundFocusWorker.ts",
15 "src/snapShot/GlobalShiftShortcutWorker.ts",
16 "src/snapShot/RegionSnapShotWorker.ts",
17 "src/snapShot/SnapShotAccessibilityWorker.ts",
18 ];
19 await Promise.all([
20 NodeFSP.mkdir(NodePath.join(directory, "src/electron"), { recursive: true }),
21 NodeFSP.mkdir(NodePath.join(directory, "src/snapShot"), { recursive: true }),
22 ]);
23 await Promise.all([
24 NodeFSP.writeFile(
25 NodePath.join(directory, "src/main.ts"),
26 `import { shared } from "./shared.ts";
27process.emit("startup", shared.value);
28void import("./linux.ts").then(({ result }) => process.emit("ready", result));`,
29 ),
30 NodeFSP.writeFile(
31 NodePath.join(directory, "src/shared.ts"),
32 "export const shared = { value: 42 };",
33 ),
34 NodeFSP.writeFile(
35 NodePath.join(directory, "src/linux.ts"),
36 'import { shared } from "./shared.ts"; export const result = shared.value + 1;',
37 ),
38 ...workerEntries.map((entry) =>
39 NodeFSP.writeFile(
40 NodePath.join(directory, entry),
41 'import { shared } from "../shared.ts"; process.emit("worker", shared.value);',
42 ),
43 ),
44 ]);
45 assert.ok(Array.isArray(desktopConfig.pack));
46 const fixtureEntries = new Set(["src/main.ts", ...workerEntries]);
47 for (const packConfig of desktopConfig.pack) {
48 if (!Array.isArray(packConfig.entry)) continue;
49 if (!packConfig.entry.some((entry) => fixtureEntries.has(entry))) continue;
50 await build({
51 ...packConfig,
52 config: false,
53 cwd: directory,
54 tsconfig: false,
55 sourcemap: false,
56 onSuccess: undefined,
57 logLevel: "silent",
58 });
59 }
60
61 const outputDirectory = NodePath.join(directory, "dist-electron");
62 const filenames = (await NodeFSP.readdir(outputDirectory, { recursive: true })).filter(
63 (filename) => filename.endsWith(".cjs"),
64 );
65 const sources = new Map(
66 await Promise.all(
67 filenames.map(async (filename) => {
68 const path = NodePath.join(outputDirectory, filename);
69 return [path, await NodeFSP.readFile(path, "utf8")];
70 }),
71 ),
72 );
73 const modules = new Map();
74 const startups = [];
75 const workers = [];
76 const ready = Promise.withResolvers();
77 const load = (filename, cacheModule = true) => {
78 const cached = modules.get(filename);
79 if (cached) return cached.exports;
80 const module = { exports: {} };
81 if (cacheModule) modules.set(filename, module);
82 const source = sources.get(filename);
83 assert.ok(source, `Missing bundle: ${filename}`);
84 NodeVM.runInNewContext(source, {
85 exports: module.exports,
86 module,
87 require: (specifier) => load(NodePath.resolve(NodePath.dirname(filename), specifier)),
88 process: {
89 emit: (event, value) => {
90 if (event === "startup") startups.push(value);
91 if (event === "worker") workers.push(value);
92 if (event === "ready") ready.resolve(value);
93 },
94 },
95 });
96 return module.exports;
97 };
98
99 load(NodePath.join(outputDirectory, "main.cjs"), false);
100 assert.equal(await ready.promise, 43);
101 assert.deepEqual(startups, [42]);
102 for (const entry of workerEntries) {
103 load(NodePath.join(outputDirectory, entry.replace(/^src\//, "").replace(/\.ts$/, ".cjs")));
104 }
105 assert.deepEqual(workers, [42, 42, 42, 42]);
106 assert.deepEqual(startups, [42]);
107 } finally {
108 await NodeFSP.rm(directory, { recursive: true, force: true });
109 }
110});
apps/desktop/vite.config.ts+18-3
......@@ -48,6 +48,23 @@ export default defineConfig({
4848 },
4949 },
5050 pack: [
51 {
52 format: "cjs",
53 outDir: "dist-electron",
54 dts: false,
55 sourcemap: true,
56 outExtensions: () => ({ js: ".cjs" }),
57 define: publicConfigDefine,
58 outputOptions: { codeSplitting: false },
59 entry: ["src/main.ts"],
60 clean: true,
61 deps: {
62 alwaysBundle: (id) => !id.startsWith("node:") && !isMainProcessExternal(id),
63 neverBundle: isMainProcessExternal,
64 onlyBundle: false,
65 },
66 ...(shouldLaunchElectronAfterPack ? { onSuccess: "node scripts/dev-electron.mjs" } : {}),
67 },
5168 {
5269 format: "cjs",
5370 outDir: "dist-electron",
......@@ -56,19 +73,17 @@ export default defineConfig({
5673 outExtensions: () => ({ js: ".cjs" }),
5774 define: publicConfigDefine,
5875 entry: [
59 "src/main.ts",
6076 "src/electron/WindowsForegroundFocusWorker.ts",
6177 "src/snapShot/GlobalShiftShortcutWorker.ts",
6278 "src/snapShot/RegionSnapShotWorker.ts",
6379 "src/snapShot/SnapShotAccessibilityWorker.ts",
6480 ],
65 clean: true,
81 clean: false,
6682 deps: {
6783 alwaysBundle: (id) => !id.startsWith("node:") && !isMainProcessExternal(id),
6884 neverBundle: isMainProcessExternal,
6985 onlyBundle: false,
7086 },
71 ...(shouldLaunchElectronAfterPack ? { onSuccess: "node scripts/dev-electron.mjs" } : {}),
7287 },
7388 {
7489 format: "cjs",