Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 93 additions & 4 deletions packages/plugin/vite/spec/VitePlugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -88,17 +92,14 @@ describe('VitePlugin', async () => {
plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath),
).rejects.toThrow(/entry point/);
});

afterAll(async () => {
await fs.promises.rm(viteTestDir, { recursive: true });
});
});

describe('resolveForgeConfig', () => {
let plugin: VitePlugin;

beforeAll(() => {
plugin = new VitePlugin(baseConfig);
plugin.setDirectories(viteTestDir);
});

it('sets packagerConfig and packagerConfig.ignore if it does not exist', async () => {
Expand Down Expand Up @@ -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,
);
Expand Down Expand Up @@ -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,
);
Expand Down Expand Up @@ -205,6 +208,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,
);
});
});
});
});
});
61 changes: 59 additions & 2 deletions packages/plugin/vite/src/VitePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,41 @@ export default class VitePlugin extends PluginBase<VitePluginConfig> {
));
}

/**
* 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<string> {
const nativePackages = new Set<string>();
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: [
Expand Down Expand Up @@ -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;
};

Expand Down