Files
lobehub/apps/desktop/native-deps.config.mjs
Innei 4994d19a9c 🐛 fix(desktop): remove electron-liquid-glass to fix click event blocking (#13070)
* 🐛 fix(desktop): remove electron-liquid-glass to fix click event blocking

The electron-liquid-glass native addon was blocking all click events in the
Electron desktop app window. Remove the dependency and restore vibrancy-based
transparency with semi-transparent body background via `.desktop` CSS class.

* 🔨 chore(desktop): remove electron-liquid-glass from native modules config
2026-03-17 22:56:29 +08:00

261 lines
8.5 KiB
JavaScript

/* eslint-disable no-console */
/**
* Native dependencies configuration for Electron build
*
* Native modules (containing .node bindings) require special handling:
* 1. Must be externalized in Vite/Rollup to prevent bundling
* 2. Must be included in electron-builder files
* 3. Must be unpacked from asar archive
*
* This module automatically resolves the full dependency tree.
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* Get the current target platform
* During build, electron-builder sets npm_config_platform
* Falls back to os.platform() for development
*/
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
*
* Platform-specific modules are only included when building for their target platform
*/
export const nativeModules = [
// macOS-only native modules
...(isDarwin ? ['node-mac-permissions'] : []),
'@napi-rs/canvas',
// Add more native modules here as needed
];
/**
* Recursively resolve all dependencies of a module
* @param {string} moduleName - The module to resolve
* @param {Set<string>} visited - Set of already visited modules (to avoid cycles)
* @param {string} nodeModulesPath - Path to node_modules directory
* @returns {Set<string>} Set of all dependencies
*/
function resolveDependencies(
moduleName,
visited = new Set(),
nodeModulesPath = path.join(__dirname, 'node_modules'),
) {
if (visited.has(moduleName)) {
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');
// If module doesn't exist locally, still keep it in visited but skip dependency resolution
if (!fs.existsSync(packageJsonPath)) {
return visited;
}
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
}
return visited;
}
/**
* Get all dependencies for all native modules (including transitive dependencies)
* @returns {string[]} Array of all dependency names
*/
export function getAllDependencies() {
const allDeps = new Set();
for (const nativeModule of nativeModules) {
const deps = resolveDependencies(nativeModule);
for (const dep of deps) {
allDeps.add(dep);
}
}
return [...allDeps];
}
/**
* Generate glob patterns for electron-builder files config
* @returns {string[]} Array of glob patterns
*/
export function getFilesPatterns() {
return getAllDependencies().map((dep) => `node_modules/${dep}/**/*`);
}
/**
* Generate files config objects for electron-builder to explicitly copy native modules.
* This uses object form to ensure scoped packages with pnpm symlinks are properly copied.
* @returns {Array<{from: string, to: string, filter: string[]}>}
*/
export function getNativeModulesFilesConfig() {
return getAllDependencies().map((dep) => ({
filter: ['**/*'],
from: `node_modules/${dep}`,
to: `node_modules/${dep}`,
}));
}
/**
* Generate glob patterns for electron-builder asarUnpack config
* @returns {string[]} Array of glob patterns
*/
export function getAsarUnpackPatterns() {
return getAllDependencies().map((dep) => `node_modules/${dep}/**/*`);
}
/**
* Get the list of native dependencies for Vite external config
* @returns {string[]} Array of dependency names
*/
export function getExternalDependencies() {
return getAllDependencies();
}
/**
* Copy native modules to source node_modules, resolving pnpm symlinks.
* This is used in beforePack hook to ensure native modules are properly
* included in the asar archive (electron-builder glob doesn't follow symlinks).
*/
export async function copyNativeModulesToSource() {
const fsPromises = await import('node:fs/promises');
const deps = getAllDependencies();
const sourceNodeModules = path.join(__dirname, 'node_modules');
console.log(`📦 Resolving ${deps.length} native module symlinks for packaging...`);
for (const dep of deps) {
const modulePath = path.join(sourceNodeModules, dep);
try {
const stat = await fsPromises.lstat(modulePath);
if (stat.isSymbolicLink()) {
// Resolve the symlink to get the real path
const realPath = await fsPromises.realpath(modulePath);
console.log(` 📎 ${dep} (resolving symlink)`);
// Remove the symlink
await fsPromises.rm(modulePath, { force: true, recursive: true });
// Create parent directory if needed (for scoped packages like @napi-rs)
await fsPromises.mkdir(path.dirname(modulePath), { recursive: true });
// Copy the actual directory content in place of the symlink
await copyDir(realPath, modulePath);
}
} catch (err) {
// Module might not exist (optional dependency for different platform)
console.log(` ⏭️ ${dep} (skipped: ${err.code || err.message})`);
}
}
console.log(`✅ Native module symlinks resolved`);
}
/**
* 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);
}
}
}