🐛 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
This commit is contained in:
Innei
2026-01-26 02:10:35 +08:00
committed by GitHub
parent 6499365542
commit c3fd2dc785
4 changed files with 135 additions and 21 deletions

View File

@@ -4,7 +4,11 @@ import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getAsarUnpackPatterns, getFilesPatterns } from './native-deps.config.mjs';
import {
copyNativeModules,
getAsarUnpackPatterns,
getFilesPatterns,
} from './native-deps.config.mjs';
dotenv.config();
@@ -86,30 +90,46 @@ const getIconFileName = () => {
*/
const config = {
/**
* AfterPack hook to copy pre-generated Liquid Glass Assets.car for macOS 26+
* AfterPack hook for post-processing:
* 1. Copy native modules to asar.unpacked (resolving pnpm symlinks)
* 2. Copy Liquid Glass Assets.car for macOS 26+
* 3. Remove unused Electron Framework localizations
*
* @see https://github.com/electron-userland/electron-builder/issues/9254
* @see https://github.com/MultiboxLabs/flow-browser/pull/159
* @see https://github.com/electron/packager/pull/1806
*/
afterPack: async (context) => {
// Only process macOS builds
if (!['darwin', 'mas'].includes(context.electronPlatformName)) {
const isMac = ['darwin', 'mas'].includes(context.electronPlatformName);
// Determine resources path based on platform
let resourcesPath;
if (isMac) {
resourcesPath = path.join(
context.appOutDir,
`${context.packager.appInfo.productFilename}.app`,
'Contents',
'Resources',
);
} else {
// Windows and Linux: resources is directly in appOutDir
resourcesPath = path.join(context.appOutDir, 'resources');
}
// Copy native modules to asar.unpacked, resolving pnpm symlinks
const unpackedNodeModules = path.join(resourcesPath, 'app.asar.unpacked', 'node_modules');
await copyNativeModules(unpackedNodeModules);
// macOS-specific post-processing
if (!isMac) {
return;
}
const iconFileName = getIconFileName();
const assetsCarSource = path.join(__dirname, 'build', `${iconFileName}.Assets.car`);
const resourcesPath = path.join(
context.appOutDir,
`${context.packager.appInfo.productFilename}.app`,
'Contents',
'Resources',
);
const assetsCarDest = path.join(resourcesPath, 'Assets.car');
// Remove unused Electron Framework localizations to reduce app size
// Equivalent to:
// ../../Frameworks/Electron Framework.framework/Versions/A/Resources/*.lproj
const frameworkResourcePath = path.join(
context.appOutDir,
`${context.packager.appInfo.productFilename}.app`,
@@ -155,7 +175,7 @@ const config = {
appImage: {
artifactName: '${productName}-${version}.${ext}',
},
asar: true,
// Native modules must be unpacked from asar to work correctly
asarUnpack: getAsarUnpackPatterns(),

View File

@@ -18,6 +18,14 @@ export default defineConfig({
rollupOptions: {
// Native modules must be externalized to work correctly
external: getExternalDependencies(),
output: {
// Prevent debug package from being bundled into index.js to avoid side-effect pollution
manualChunks(id) {
if (id.includes('node_modules/debug')) {
return 'vendor-debug';
}
},
},
},
sourcemap: isDev ? 'inline' : false,
},
@@ -25,7 +33,6 @@ export default defineConfig({
'process.env.UPDATE_CHANNEL': JSON.stringify(process.env.UPDATE_CHANNEL),
'process.env.UPDATE_SERVER_URL': JSON.stringify(process.env.UPDATE_SERVER_URL),
},
resolve: {
alias: {
'@': resolve(__dirname, 'src/main'),

View File

@@ -24,6 +24,7 @@ function getTargetPlatform() {
return process.env.npm_config_platform || os.platform();
}
const isDarwin = getTargetPlatform() === 'darwin';
/**
* List of native modules that need special handling
* Only add the top-level native modules here - dependencies are resolved automatically
@@ -33,8 +34,8 @@ const isDarwin = getTargetPlatform() === 'darwin';
export const nativeModules = [
// macOS-only native modules
...(isDarwin ? ['node-mac-permissions'] : []),
'@napi-rs/canvas',
// Add more native modules here as needed
// e.g., 'better-sqlite3', 'sharp', etc.
];
/**
@@ -53,22 +54,32 @@ function resolveDependencies(
return visited;
}
// Always add the module name first (important for workspace dependencies
// that may not be in local node_modules but are declared in nativeModules)
visited.add(moduleName);
const packageJsonPath = path.join(nodeModulesPath, moduleName, 'package.json');
// Check if module exists
// If module doesn't exist locally, still keep it in visited but skip dependency resolution
if (!fs.existsSync(packageJsonPath)) {
return visited;
}
visited.add(moduleName);
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const dependencies = packageJson.dependencies || {};
const optionalDependencies = packageJson.optionalDependencies || {};
// Resolve regular dependencies
for (const dep of Object.keys(dependencies)) {
resolveDependencies(dep, visited, nodeModulesPath);
}
// Also resolve optional dependencies (important for native modules like @napi-rs/canvas
// which have platform-specific binaries in optional deps)
for (const dep of Object.keys(optionalDependencies)) {
resolveDependencies(dep, visited, nodeModulesPath);
}
} catch {
// Ignore errors reading package.json
}
@@ -116,3 +127,79 @@ export function getAsarUnpackPatterns() {
export function getExternalDependencies() {
return getAllDependencies();
}
/**
* Copy native modules to destination, resolving symlinks
* This is used in afterPack hook to handle pnpm symlinks correctly
* @param {string} destNodeModules - Destination node_modules path
*/
export async function copyNativeModules(destNodeModules) {
const fsPromises = await import('node:fs/promises');
const deps = getAllDependencies();
const sourceNodeModules = path.join(__dirname, 'node_modules');
console.log(`📦 Copying ${deps.length} native modules to unpacked directory...`);
for (const dep of deps) {
const sourcePath = path.join(sourceNodeModules, dep);
const destPath = path.join(destNodeModules, dep);
try {
// Check if source exists (might be a symlink)
const stat = await fsPromises.lstat(sourcePath);
if (stat.isSymbolicLink()) {
// Resolve the symlink to get the real path
const realPath = await fsPromises.realpath(sourcePath);
console.log(` 📎 ${dep} (symlink -> ${path.relative(sourceNodeModules, realPath)})`);
// Create destination directory
await fsPromises.mkdir(path.dirname(destPath), { recursive: true });
// Copy the actual directory content (not the symlink)
await copyDir(realPath, destPath);
} else if (stat.isDirectory()) {
console.log(` 📁 ${dep}`);
await fsPromises.mkdir(path.dirname(destPath), { recursive: true });
await copyDir(sourcePath, destPath);
}
} catch (err) {
// Module might not exist (optional dependency for different platform)
console.log(` ⏭️ ${dep} (skipped: ${err.code || err.message})`);
}
}
console.log(`✅ Native modules copied successfully`);
}
/**
* Recursively copy a directory
* @param {string} src - Source directory
* @param {string} dest - Destination directory
*/
async function copyDir(src, dest) {
const fsPromises = await import('node:fs/promises');
await fsPromises.mkdir(dest, { recursive: true });
const entries = await fsPromises.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
await copyDir(srcPath, destPath);
} else if (entry.isSymbolicLink()) {
// For symlinks within the module, resolve and copy the actual file
const realPath = await fsPromises.realpath(srcPath);
const realStat = await fsPromises.stat(realPath);
if (realStat.isDirectory()) {
await copyDir(realPath, destPath);
} else {
await fsPromises.copyFile(realPath, destPath);
}
} else {
await fsPromises.copyFile(srcPath, destPath);
}
}
}

View File

@@ -33,10 +33,10 @@
"pdfjs-dist": "5.4.530",
"word-extractor": "^1.0.4",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"yauzl": "^3.2.0"
"yauzl": "^3.2.0",
"@napi-rs/canvas": "^0.1.70"
},
"devDependencies": {
"@napi-rs/canvas": "^0.1.70",
"@types/concat-stream": "^2.0.3",
"@types/yauzl": "^2.10.3",
"typescript": "^5.9.3"
@@ -44,4 +44,4 @@
"peerDependencies": {
"typescript": ">=5"
}
}
}