Skip to content

Commit c3fd2dc

Browse files
authored
🐛 fix(desktop): prevent duplicate IPC handler registration from dynamic imports (#11827)
* 🐛 fix(desktop): prevent duplicate IPC handler registration from dynamic imports Fix an issue where dynamic imports in file-loaders package would cause the debug package to be bundled into index.js, leading to side-effect pollution and duplicate electron-log IPC handler registration. - Add manualChunks config to isolate debug package into separate chunk - Add @napi-rs/canvas to native modules for proper externalization * ✨ feat(desktop): enhance afterPack hook and add native module copying
1 parent 6499365 commit c3fd2dc

4 files changed

Lines changed: 135 additions & 21 deletions

File tree

apps/desktop/electron-builder.mjs

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import os from 'node:os';
44
import path from 'node:path';
55
import { fileURLToPath } from 'node:url';
66

7-
import { getAsarUnpackPatterns, getFilesPatterns } from './native-deps.config.mjs';
7+
import {
8+
copyNativeModules,
9+
getAsarUnpackPatterns,
10+
getFilesPatterns,
11+
} from './native-deps.config.mjs';
812

913
dotenv.config();
1014

@@ -86,30 +90,46 @@ const getIconFileName = () => {
8690
*/
8791
const config = {
8892
/**
89-
* AfterPack hook to copy pre-generated Liquid Glass Assets.car for macOS 26+
93+
* AfterPack hook for post-processing:
94+
* 1. Copy native modules to asar.unpacked (resolving pnpm symlinks)
95+
* 2. Copy Liquid Glass Assets.car for macOS 26+
96+
* 3. Remove unused Electron Framework localizations
97+
*
9098
* @see https://github.com/electron-userland/electron-builder/issues/9254
9199
* @see https://github.com/MultiboxLabs/flow-browser/pull/159
92100
* @see https://github.com/electron/packager/pull/1806
93101
*/
94102
afterPack: async (context) => {
95-
// Only process macOS builds
96-
if (!['darwin', 'mas'].includes(context.electronPlatformName)) {
103+
const isMac = ['darwin', 'mas'].includes(context.electronPlatformName);
104+
105+
// Determine resources path based on platform
106+
let resourcesPath;
107+
if (isMac) {
108+
resourcesPath = path.join(
109+
context.appOutDir,
110+
`${context.packager.appInfo.productFilename}.app`,
111+
'Contents',
112+
'Resources',
113+
);
114+
} else {
115+
// Windows and Linux: resources is directly in appOutDir
116+
resourcesPath = path.join(context.appOutDir, 'resources');
117+
}
118+
119+
// Copy native modules to asar.unpacked, resolving pnpm symlinks
120+
const unpackedNodeModules = path.join(resourcesPath, 'app.asar.unpacked', 'node_modules');
121+
await copyNativeModules(unpackedNodeModules);
122+
123+
// macOS-specific post-processing
124+
if (!isMac) {
97125
return;
98126
}
99127

100128
const iconFileName = getIconFileName();
101129
const assetsCarSource = path.join(__dirname, 'build', `${iconFileName}.Assets.car`);
102-
const resourcesPath = path.join(
103-
context.appOutDir,
104-
`${context.packager.appInfo.productFilename}.app`,
105-
'Contents',
106-
'Resources',
107-
);
108130
const assetsCarDest = path.join(resourcesPath, 'Assets.car');
109131

110132
// Remove unused Electron Framework localizations to reduce app size
111-
// Equivalent to:
112-
// ../../Frameworks/Electron Framework.framework/Versions/A/Resources/*.lproj
113133
const frameworkResourcePath = path.join(
114134
context.appOutDir,
115135
`${context.packager.appInfo.productFilename}.app`,
@@ -155,7 +175,7 @@ const config = {
155175
appImage: {
156176
artifactName: '${productName}-${version}.${ext}',
157177
},
158-
asar: true,
178+
159179
// Native modules must be unpacked from asar to work correctly
160180
asarUnpack: getAsarUnpackPatterns(),
161181

apps/desktop/electron.vite.config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,21 @@ export default defineConfig({
1818
rollupOptions: {
1919
// Native modules must be externalized to work correctly
2020
external: getExternalDependencies(),
21+
output: {
22+
// Prevent debug package from being bundled into index.js to avoid side-effect pollution
23+
manualChunks(id) {
24+
if (id.includes('node_modules/debug')) {
25+
return 'vendor-debug';
26+
}
27+
},
28+
},
2129
},
2230
sourcemap: isDev ? 'inline' : false,
2331
},
2432
define: {
2533
'process.env.UPDATE_CHANNEL': JSON.stringify(process.env.UPDATE_CHANNEL),
2634
'process.env.UPDATE_SERVER_URL': JSON.stringify(process.env.UPDATE_SERVER_URL),
2735
},
28-
2936
resolve: {
3037
alias: {
3138
'@': resolve(__dirname, 'src/main'),

apps/desktop/native-deps.config.mjs

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ function getTargetPlatform() {
2424
return process.env.npm_config_platform || os.platform();
2525
}
2626
const isDarwin = getTargetPlatform() === 'darwin';
27+
2728
/**
2829
* List of native modules that need special handling
2930
* Only add the top-level native modules here - dependencies are resolved automatically
@@ -33,8 +34,8 @@ const isDarwin = getTargetPlatform() === 'darwin';
3334
export const nativeModules = [
3435
// macOS-only native modules
3536
...(isDarwin ? ['node-mac-permissions'] : []),
37+
'@napi-rs/canvas',
3638
// Add more native modules here as needed
37-
// e.g., 'better-sqlite3', 'sharp', etc.
3839
];
3940

4041
/**
@@ -53,22 +54,32 @@ function resolveDependencies(
5354
return visited;
5455
}
5556

57+
// Always add the module name first (important for workspace dependencies
58+
// that may not be in local node_modules but are declared in nativeModules)
59+
visited.add(moduleName);
60+
5661
const packageJsonPath = path.join(nodeModulesPath, moduleName, 'package.json');
5762

58-
// Check if module exists
63+
// If module doesn't exist locally, still keep it in visited but skip dependency resolution
5964
if (!fs.existsSync(packageJsonPath)) {
6065
return visited;
6166
}
6267

63-
visited.add(moduleName);
64-
6568
try {
6669
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
6770
const dependencies = packageJson.dependencies || {};
71+
const optionalDependencies = packageJson.optionalDependencies || {};
6872

73+
// Resolve regular dependencies
6974
for (const dep of Object.keys(dependencies)) {
7075
resolveDependencies(dep, visited, nodeModulesPath);
7176
}
77+
78+
// Also resolve optional dependencies (important for native modules like @napi-rs/canvas
79+
// which have platform-specific binaries in optional deps)
80+
for (const dep of Object.keys(optionalDependencies)) {
81+
resolveDependencies(dep, visited, nodeModulesPath);
82+
}
7283
} catch {
7384
// Ignore errors reading package.json
7485
}
@@ -116,3 +127,79 @@ export function getAsarUnpackPatterns() {
116127
export function getExternalDependencies() {
117128
return getAllDependencies();
118129
}
130+
131+
/**
132+
* Copy native modules to destination, resolving symlinks
133+
* This is used in afterPack hook to handle pnpm symlinks correctly
134+
* @param {string} destNodeModules - Destination node_modules path
135+
*/
136+
export async function copyNativeModules(destNodeModules) {
137+
const fsPromises = await import('node:fs/promises');
138+
const deps = getAllDependencies();
139+
const sourceNodeModules = path.join(__dirname, 'node_modules');
140+
141+
console.log(`📦 Copying ${deps.length} native modules to unpacked directory...`);
142+
143+
for (const dep of deps) {
144+
const sourcePath = path.join(sourceNodeModules, dep);
145+
const destPath = path.join(destNodeModules, dep);
146+
147+
try {
148+
// Check if source exists (might be a symlink)
149+
const stat = await fsPromises.lstat(sourcePath);
150+
151+
if (stat.isSymbolicLink()) {
152+
// Resolve the symlink to get the real path
153+
const realPath = await fsPromises.realpath(sourcePath);
154+
console.log(` 📎 ${dep} (symlink -> ${path.relative(sourceNodeModules, realPath)})`);
155+
156+
// Create destination directory
157+
await fsPromises.mkdir(path.dirname(destPath), { recursive: true });
158+
159+
// Copy the actual directory content (not the symlink)
160+
await copyDir(realPath, destPath);
161+
} else if (stat.isDirectory()) {
162+
console.log(` 📁 ${dep}`);
163+
await fsPromises.mkdir(path.dirname(destPath), { recursive: true });
164+
await copyDir(sourcePath, destPath);
165+
}
166+
} catch (err) {
167+
// Module might not exist (optional dependency for different platform)
168+
console.log(` ⏭️ ${dep} (skipped: ${err.code || err.message})`);
169+
}
170+
}
171+
172+
console.log(`✅ Native modules copied successfully`);
173+
}
174+
175+
/**
176+
* Recursively copy a directory
177+
* @param {string} src - Source directory
178+
* @param {string} dest - Destination directory
179+
*/
180+
async function copyDir(src, dest) {
181+
const fsPromises = await import('node:fs/promises');
182+
183+
await fsPromises.mkdir(dest, { recursive: true });
184+
const entries = await fsPromises.readdir(src, { withFileTypes: true });
185+
186+
for (const entry of entries) {
187+
const srcPath = path.join(src, entry.name);
188+
const destPath = path.join(dest, entry.name);
189+
190+
if (entry.isDirectory()) {
191+
await copyDir(srcPath, destPath);
192+
} else if (entry.isSymbolicLink()) {
193+
// For symlinks within the module, resolve and copy the actual file
194+
const realPath = await fsPromises.realpath(srcPath);
195+
const realStat = await fsPromises.stat(realPath);
196+
if (realStat.isDirectory()) {
197+
await copyDir(realPath, destPath);
198+
} else {
199+
await fsPromises.copyFile(realPath, destPath);
200+
}
201+
} else {
202+
await fsPromises.copyFile(srcPath, destPath);
203+
}
204+
}
205+
}

packages/file-loaders/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@
3333
"pdfjs-dist": "5.4.530",
3434
"word-extractor": "^1.0.4",
3535
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
36-
"yauzl": "^3.2.0"
36+
"yauzl": "^3.2.0",
37+
"@napi-rs/canvas": "^0.1.70"
3738
},
3839
"devDependencies": {
39-
"@napi-rs/canvas": "^0.1.70",
4040
"@types/concat-stream": "^2.0.3",
4141
"@types/yauzl": "^2.10.3",
4242
"typescript": "^5.9.3"
4343
},
4444
"peerDependencies": {
4545
"typescript": ">=5"
4646
}
47-
}
47+
}

0 commit comments

Comments
 (0)