From 5e104d8c559a03b1f40af80299b66c1cc0771cef Mon Sep 17 00:00:00 2001 From: Damiano Mazzella Date: Sat, 29 Nov 2025 14:04:48 +0100 Subject: [PATCH 1/4] fix: electron-forge/plugin-auto-unpack-natives does not unpack natives #3934 --- packages/plugin/vite/src/VitePlugin.ts | 61 +++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/packages/plugin/vite/src/VitePlugin.ts b/packages/plugin/vite/src/VitePlugin.ts index 8b72f5f960..9721d88459 100644 --- a/packages/plugin/vite/src/VitePlugin.ts +++ b/packages/plugin/vite/src/VitePlugin.ts @@ -77,6 +77,41 @@ export default class VitePlugin extends PluginBase { )); } + /** + * Scans node_modules to find packages containing native .node files. + * This is used to selectively include only native module packages in the asar, + * enabling AutoUnpackNativesPlugin to work correctly while keeping the asar size minimal. + */ + private findNativePackages(): Set { + const nativePackages = new Set(); + const nodeModulesPath = path.join(this.projectDir, 'node_modules'); + + const scanDir = (dir: string, depth = 0): void => { + // Limit recursion depth to avoid scanning too deep + if (depth > 5) return; + + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory() && !entry.name.startsWith('.')) { + scanDir(fullPath, depth + 1); + } else if (entry.isFile() && entry.name.endsWith('.node')) { + // Found a .node file, extract the package name + const relativePath = path.relative(nodeModulesPath, fullPath); + const pkgName = relativePath.split(path.sep)[0]; + nativePackages.add(pkgName); + } + } + } catch { + // Ignore errors (e.g., permission denied) + } + }; + + scanDir(nodeModulesPath); + return nativePackages; + } + getHooks = (): ForgeMultiHookMap => { return { preStart: [ @@ -175,15 +210,37 @@ Your packaged app may be larger than expected if you dont ignore everything othe return forgeConfig; } + // Find packages containing native modules (.node files) + // These need to be included in the asar for AutoUnpackNativesPlugin to work + const nativePackages = this.findNativePackages(); + d('Found native packages:', Array.from(nativePackages)); + forgeConfig.packagerConfig.ignore = (file: string) => { if (!file) return false; // `file` always starts with `/` // @see - https://github.com/electron/packager/blob/v18.1.3/src/copy-filter.ts#L89-L93 - // Collect the files built by Vite - return !file.startsWith('/.vite'); + // Include files built by Vite + if (file.startsWith('/.vite')) return false; + + // Include node_modules folder itself (required for the ignore function to work) + if (file === '/node_modules') return false; + + // For files inside node_modules, only include packages with native modules + if (file.startsWith('/node_modules/')) { + // Extract the package name (first segment after /node_modules/) + const parts = file.split('/'); + const pkgName = parts[2]; + + // Include if this package contains native modules + if (nativePackages.has(pkgName)) return false; + } + + // Exclude everything else + return true; }; + return forgeConfig; }; From ff23c55ca5cb8fe90b5a016035151cb86133fdc4 Mon Sep 17 00:00:00 2001 From: dmazzella Date: Sat, 6 Dec 2025 13:22:14 +0100 Subject: [PATCH 2/4] test(vite): enhance packageAfterCopy tests and add native module handling --- packages/plugin/vite/spec/VitePlugin.spec.ts | 94 +++++++++++++++++++- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/packages/plugin/vite/spec/VitePlugin.spec.ts b/packages/plugin/vite/spec/VitePlugin.spec.ts index a67d91ea61..585d2cebb3 100644 --- a/packages/plugin/vite/spec/VitePlugin.spec.ts +++ b/packages/plugin/vite/spec/VitePlugin.spec.ts @@ -19,6 +19,10 @@ describe('VitePlugin', async () => { const tmpdir = path.join(tmp, 'electron-forge-test-'); const viteTestDir = await fs.promises.mkdtemp(tmpdir); + afterAll(async () => { + await fs.promises.rm(viteTestDir, { recursive: true }); + }); + describe('packageAfterCopy', () => { const packageJSONPath = path.join(viteTestDir, 'package.json'); const packagedPath = path.join(viteTestDir, 'packaged'); @@ -88,10 +92,6 @@ describe('VitePlugin', async () => { plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath), ).rejects.toThrow(/entry point/); }); - - afterAll(async () => { - await fs.promises.rm(viteTestDir, { recursive: true }); - }); }); describe('resolveForgeConfig', () => { @@ -205,6 +205,92 @@ describe('VitePlugin', async () => { ), ).toEqual(false); }); + + it('includes /node_modules directory', async () => { + plugin = new VitePlugin(baseConfig); + plugin.setDirectories(viteTestDir); + const config = await plugin.resolveForgeConfig( + {} as ResolvedForgeConfig, + ); + const ignore = config.packagerConfig.ignore as IgnoreFunction; + + expect(ignore('/node_modules')).toEqual(false); + }); + + it('excludes packages without native modules from node_modules', async () => { + plugin = new VitePlugin(baseConfig); + plugin.setDirectories(viteTestDir); + const config = await plugin.resolveForgeConfig( + {} as ResolvedForgeConfig, + ); + const ignore = config.packagerConfig.ignore as IgnoreFunction; + + // Non-native packages should be excluded + expect(ignore('/node_modules/lodash')).toEqual(true); + expect(ignore('/node_modules/lodash/index.js')).toEqual(true); + }); + + describe('with native modules', () => { + const nativeModulesTestDir = path.join(viteTestDir, 'native-test'); + + beforeAll(async () => { + // Create a fake node_modules structure with a native module + const nativePackagePath = path.join( + nativeModulesTestDir, + 'node_modules', + 'native-package', + 'build', + 'Release', + ); + await fs.promises.mkdir(nativePackagePath, { recursive: true }); + await fs.promises.writeFile( + path.join(nativePackagePath, 'binding.node'), + 'fake native module', + ); + + // Create a non-native package + const nonNativePackagePath = path.join( + nativeModulesTestDir, + 'node_modules', + 'non-native-package', + ); + await fs.promises.mkdir(nonNativePackagePath, { recursive: true }); + await fs.promises.writeFile( + path.join(nonNativePackagePath, 'index.js'), + 'module.exports = {}', + ); + }); + + it('includes packages containing .node files', async () => { + plugin = new VitePlugin(baseConfig); + plugin.setDirectories(nativeModulesTestDir); + const config = await plugin.resolveForgeConfig( + {} as ResolvedForgeConfig, + ); + const ignore = config.packagerConfig.ignore as IgnoreFunction; + + // Native package should be included (not ignored) + expect(ignore('/node_modules/native-package')).toEqual(false); + expect( + ignore('/node_modules/native-package/build/Release/binding.node'), + ).toEqual(false); + }); + + it('excludes packages without .node files', async () => { + plugin = new VitePlugin(baseConfig); + plugin.setDirectories(nativeModulesTestDir); + const config = await plugin.resolveForgeConfig( + {} as ResolvedForgeConfig, + ); + const ignore = config.packagerConfig.ignore as IgnoreFunction; + + // Non-native package should be excluded (ignored) + expect(ignore('/node_modules/non-native-package')).toEqual(true); + expect(ignore('/node_modules/non-native-package/index.js')).toEqual( + true, + ); + }); + }); }); }); }); From d02cbd2c8763b02ee74ecc0486a7d30df012907c Mon Sep 17 00:00:00 2001 From: dmazzella Date: Sat, 6 Dec 2025 14:08:30 +0100 Subject: [PATCH 3/4] test(vite): set directories for VitePlugin in multiple test cases --- packages/plugin/vite/spec/VitePlugin.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/plugin/vite/spec/VitePlugin.spec.ts b/packages/plugin/vite/spec/VitePlugin.spec.ts index 585d2cebb3..4def339780 100644 --- a/packages/plugin/vite/spec/VitePlugin.spec.ts +++ b/packages/plugin/vite/spec/VitePlugin.spec.ts @@ -99,6 +99,7 @@ describe('VitePlugin', async () => { beforeAll(() => { plugin = new VitePlugin(baseConfig); + plugin.setDirectories(viteTestDir); }); it('sets packagerConfig and packagerConfig.ignore if it does not exist', async () => { @@ -133,6 +134,7 @@ describe('VitePlugin', async () => { it('ignores source map files by default', async () => { const viteConfig = { ...baseConfig }; plugin = new VitePlugin(viteConfig); + plugin.setDirectories(viteTestDir); const config = await plugin.resolveForgeConfig( {} as ResolvedForgeConfig, ); @@ -171,6 +173,7 @@ describe('VitePlugin', async () => { it('includes source map files when specified by config', async () => { const viteConfig = { ...baseConfig, packageSourceMaps: true }; plugin = new VitePlugin(viteConfig); + plugin.setDirectories(viteTestDir); const config = await plugin.resolveForgeConfig( {} as ResolvedForgeConfig, ); From 066c4629da826ce6a1eb55d9738f7bf38525ecd8 Mon Sep 17 00:00:00 2001 From: dmazzella Date: Mon, 8 Dec 2025 11:55:07 +0100 Subject: [PATCH 4/4] feat: add flora-colossus dependency and refactor native package handling in VitePlugin --- packages/plugin/vite/package.json | 1 + packages/plugin/vite/spec/VitePlugin.spec.ts | 75 -------------------- packages/plugin/vite/src/VitePlugin.ts | 46 +++--------- yarn.lock | 1 + 4 files changed, 12 insertions(+), 111 deletions(-) diff --git a/packages/plugin/vite/package.json b/packages/plugin/vite/package.json index 08d56f185a..774595add8 100644 --- a/packages/plugin/vite/package.json +++ b/packages/plugin/vite/package.json @@ -16,6 +16,7 @@ "@electron-forge/shared-types": "7.10.2", "chalk": "^4.0.0", "debug": "^4.3.1", + "flora-colossus": "^2.0.0", "fs-extra": "^10.0.0", "listr2": "^7.0.2" }, diff --git a/packages/plugin/vite/spec/VitePlugin.spec.ts b/packages/plugin/vite/spec/VitePlugin.spec.ts index 4def339780..6a60f8f238 100644 --- a/packages/plugin/vite/spec/VitePlugin.spec.ts +++ b/packages/plugin/vite/spec/VitePlugin.spec.ts @@ -219,81 +219,6 @@ describe('VitePlugin', async () => { expect(ignore('/node_modules')).toEqual(false); }); - - it('excludes packages without native modules from node_modules', async () => { - plugin = new VitePlugin(baseConfig); - plugin.setDirectories(viteTestDir); - const config = await plugin.resolveForgeConfig( - {} as ResolvedForgeConfig, - ); - const ignore = config.packagerConfig.ignore as IgnoreFunction; - - // Non-native packages should be excluded - expect(ignore('/node_modules/lodash')).toEqual(true); - expect(ignore('/node_modules/lodash/index.js')).toEqual(true); - }); - - describe('with native modules', () => { - const nativeModulesTestDir = path.join(viteTestDir, 'native-test'); - - beforeAll(async () => { - // Create a fake node_modules structure with a native module - const nativePackagePath = path.join( - nativeModulesTestDir, - 'node_modules', - 'native-package', - 'build', - 'Release', - ); - await fs.promises.mkdir(nativePackagePath, { recursive: true }); - await fs.promises.writeFile( - path.join(nativePackagePath, 'binding.node'), - 'fake native module', - ); - - // Create a non-native package - const nonNativePackagePath = path.join( - nativeModulesTestDir, - 'node_modules', - 'non-native-package', - ); - await fs.promises.mkdir(nonNativePackagePath, { recursive: true }); - await fs.promises.writeFile( - path.join(nonNativePackagePath, 'index.js'), - 'module.exports = {}', - ); - }); - - it('includes packages containing .node files', async () => { - plugin = new VitePlugin(baseConfig); - plugin.setDirectories(nativeModulesTestDir); - const config = await plugin.resolveForgeConfig( - {} as ResolvedForgeConfig, - ); - const ignore = config.packagerConfig.ignore as IgnoreFunction; - - // Native package should be included (not ignored) - expect(ignore('/node_modules/native-package')).toEqual(false); - expect( - ignore('/node_modules/native-package/build/Release/binding.node'), - ).toEqual(false); - }); - - it('excludes packages without .node files', async () => { - plugin = new VitePlugin(baseConfig); - plugin.setDirectories(nativeModulesTestDir); - const config = await plugin.resolveForgeConfig( - {} as ResolvedForgeConfig, - ); - const ignore = config.packagerConfig.ignore as IgnoreFunction; - - // Non-native package should be excluded (ignored) - expect(ignore('/node_modules/non-native-package')).toEqual(true); - expect(ignore('/node_modules/non-native-package/index.js')).toEqual( - true, - ); - }); - }); }); }); }); diff --git a/packages/plugin/vite/src/VitePlugin.ts b/packages/plugin/vite/src/VitePlugin.ts index 9721d88459..0fc8ad8364 100644 --- a/packages/plugin/vite/src/VitePlugin.ts +++ b/packages/plugin/vite/src/VitePlugin.ts @@ -6,6 +6,7 @@ import path from 'node:path'; import { namedHookWithTaskFn, PluginBase } from '@electron-forge/plugin-base'; import chalk from 'chalk'; import debug from 'debug'; +import { DepType, Module, Walker } from 'flora-colossus'; import fs from 'fs-extra'; import { Listr, PRESET_TIMER } from 'listr2'; import { default as vite } from 'vite'; @@ -78,38 +79,15 @@ export default class VitePlugin extends PluginBase { } /** - * Scans node_modules to find packages containing native .node files. - * This is used to selectively include only native module packages in the asar, - * enabling AutoUnpackNativesPlugin to work correctly while keeping the asar size minimal. + * Scans node_modules to find packages in production dependencies */ - private findNativePackages(): Set { - const nativePackages = new Set(); + private async getFlatDependencies(): Promise { const nodeModulesPath = path.join(this.projectDir, 'node_modules'); - const scanDir = (dir: string, depth = 0): void => { - // Limit recursion depth to avoid scanning too deep - if (depth > 5) return; - - try { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory() && !entry.name.startsWith('.')) { - scanDir(fullPath, depth + 1); - } else if (entry.isFile() && entry.name.endsWith('.node')) { - // Found a .node file, extract the package name - const relativePath = path.relative(nodeModulesPath, fullPath); - const pkgName = relativePath.split(path.sep)[0]; - nativePackages.add(pkgName); - } - } - } catch { - // Ignore errors (e.g., permission denied) - } - }; + const walker = new Walker(nodeModulesPath); + const deps = await walker.walkTree(); - scanDir(nodeModulesPath); - return nativePackages; + return deps.filter((dep) => dep.depType === DepType.PROD); } getHooks = (): ForgeMultiHookMap => { @@ -212,8 +190,7 @@ Your packaged app may be larger than expected if you dont ignore everything othe // Find packages containing native modules (.node files) // These need to be included in the asar for AutoUnpackNativesPlugin to work - const nativePackages = this.findNativePackages(); - d('Found native packages:', Array.from(nativePackages)); + const flatDeps = await this.getFlatDependencies(); forgeConfig.packagerConfig.ignore = (file: string) => { if (!file) return false; @@ -229,12 +206,9 @@ Your packaged app may be larger than expected if you dont ignore everything othe // For files inside node_modules, only include packages with native modules if (file.startsWith('/node_modules/')) { - // Extract the package name (first segment after /node_modules/) - const parts = file.split('/'); - const pkgName = parts[2]; - - // Include if this package contains native modules - if (nativePackages.has(pkgName)) return false; + // Collect dependencies from package.json + const [, , name] = file.split('/'); + return flatDeps.some((dep) => dep.name === name); } // Exclude everything else diff --git a/yarn.lock b/yarn.lock index 608b513f7b..b8b168c1b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1166,6 +1166,7 @@ __metadata: "@types/node": "npm:^18.0.3" chalk: "npm:^4.0.0" debug: "npm:^4.3.1" + flora-colossus: "npm:^2.0.0" fs-extra: "npm:^10.0.0" listr2: "npm:^7.0.2" vite: "npm:^5.0.12"