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({...@@ -48,6 +48,23 @@ export default defineConfig({
48 },48 },
49 },49 },
50 pack: [50 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 },
51 {68 {
52 format: "cjs",69 format: "cjs",
53 outDir: "dist-electron",70 outDir: "dist-electron",
...@@ -56,19 +73,17 @@ export default defineConfig({...@@ -56,19 +73,17 @@ export default defineConfig({
56 outExtensions: () => ({ js: ".cjs" }),73 outExtensions: () => ({ js: ".cjs" }),
57 define: publicConfigDefine,74 define: publicConfigDefine,
58 entry: [75 entry: [
59 "src/main.ts",
60 "src/electron/WindowsForegroundFocusWorker.ts",76 "src/electron/WindowsForegroundFocusWorker.ts",
61 "src/snapShot/GlobalShiftShortcutWorker.ts",77 "src/snapShot/GlobalShiftShortcutWorker.ts",
62 "src/snapShot/RegionSnapShotWorker.ts",78 "src/snapShot/RegionSnapShotWorker.ts",
63 "src/snapShot/SnapShotAccessibilityWorker.ts",79 "src/snapShot/SnapShotAccessibilityWorker.ts",
64 ],80 ],
65 clean: true,81 clean: false,
66 deps: {82 deps: {
67 alwaysBundle: (id) => !id.startsWith("node:") && !isMainProcessExternal(id),83 alwaysBundle: (id) => !id.startsWith("node:") && !isMainProcessExternal(id),
68 neverBundle: isMainProcessExternal,84 neverBundle: isMainProcessExternal,
69 onlyBundle: false,85 onlyBundle: false,
70 },86 },
71 ...(shouldLaunchElectronAfterPack ? { onSuccess: "node scripts/dev-electron.mjs" } : {}),
72 },87 },
73 {88 {
74 format: "cjs",89 format: "cjs",