@flun/desktop-builder 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -10
- package/README.md +21 -0
- package/electron-main.js +33 -31
- package/lib/build.js +5 -24
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
# 变更日志
|
|
2
|
+
## [1.0.5] - 2026-07-06 16:27
|
|
3
|
+
### 新增
|
|
4
|
+
- 在 README 中新增“配置”章节,指导用户通过 `allowScripts` 显式放行本包的安装脚本,避免因 `ignore-scripts` 全局配置导致钩子被跳过。
|
|
5
|
+
## [1.0.4] - 2026-07-05 17:17
|
|
6
|
+
### 紧急修复
|
|
7
|
+
- **修复依赖安装失败问题**:因 `node` 包安装脚本报错(`Cannot find module 'node-win-x64/package.json'`)导致安装中断,现通过 `--ignore-scripts` 和 `npm_config_ignore_scripts=true` 强制跳过脚本执行,确保依赖安装顺利完成。
|
|
2
8
|
|
|
3
|
-
## [1.0.3] - 2026-07-04 21:32
|
|
4
|
-
### 修复
|
|
5
|
-
- 修复 Windows 环境下 `npx desktop-builder build` 命令无任何输出的问题(入口判断逻辑改用 `pathToFileURL` 统一路径格式)。
|
|
6
|
-
|
|
7
|
-
## [1.0.2] - 2026-07-03 10:43
|
|
8
9
|
### 优化
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
## [1.0.1] - 2026-07-03 10:38
|
|
12
|
-
### 首发
|
|
13
|
-
- 将任意 Node.js 网站一键打包为当前桌面应用('win', 'mac', 'linux')(基于 Electron),支持高度自定义配置;
|
|
10
|
+
- **优化依赖清理逻辑**:因 Electron 已内置 Node.js 运行时,无需在应用内额外保留 `node` 包及其平台特定二进制目录(如 `node-win-x64`、`node-darwin-x64` 等)。现主动清理这些冗余目录,减小应用体积,同时避免与 Electron 内置运行时产生潜在冲突。注意:此优化不影响 `bcrypt` 等原生模块正常工作,因为所需依赖(如 `node-gyp-build`、`node-addon-api` 等)仍被保留。
|
package/README.md
CHANGED
|
@@ -34,6 +34,27 @@
|
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
+
## 配置
|
|
38
|
+
|
|
39
|
+
### 允许安装脚本执行
|
|
40
|
+
|
|
41
|
+
本包在安装时可能触发某些依赖包的自动脚本(如 `postinstall` 等);如果你的 npm 全局配置或项目配置禁止了脚本执行(例如设置了 `ignore-scripts=true`),可能会导致安装不完整或运行时异常;
|
|
42
|
+
|
|
43
|
+
推荐在项目根目录的 `package.json` 中添加 `allowScripts` 字段,显式放行本包及其依赖的脚本:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"allowScripts": {
|
|
48
|
+
"@flun/desktop-builder": true
|
|
49
|
+
// 如果依赖的其它包(如 bcrypt、electron-winstaller 等)也有脚本,请按需添加,格式相同
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> 如果你信任所有安装包,也可以直接在项目 `.npmrc` 中设置 `allow-scripts = false`(表示关闭脚本拦截,所有脚本均允许执行),或删除 `ignore-script`字段;
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
37
58
|
## 📦 安装
|
|
38
59
|
|
|
39
60
|
在你的项目目录下安装为开发依赖:
|
package/electron-main.js
CHANGED
|
@@ -13,7 +13,6 @@ import { createRequire } from 'module';
|
|
|
13
13
|
let mainWindow = null, serverProcess = null, windowCreationPromise = null, loadRetryCount = 0;
|
|
14
14
|
const require = createRequire(import.meta.url),
|
|
15
15
|
__dirname = path.dirname(fileURLToPath(import.meta.url)), execPromise = promisify(exec),
|
|
16
|
-
// 配置注入
|
|
17
16
|
CONFIG = {
|
|
18
17
|
APP_URL: JSON.parse('__APP_URL__'),
|
|
19
18
|
WINDOW_CONFIG: JSON.parse('__WINDOW_CONFIG__'),
|
|
@@ -23,15 +22,10 @@ const require = createRequire(import.meta.url),
|
|
|
23
22
|
LOGGING_ENABLED: JSON.parse('__LOGGING_ENABLED__'),
|
|
24
23
|
MENU_TEMPLATE: __MENU_TEMPLATE__,
|
|
25
24
|
}, TARGET_URL = CONFIG.APP_URL,
|
|
26
|
-
// 命令行开关
|
|
27
25
|
switches = ['no-sandbox', 'ignore-certificate-errors', 'allow-insecure-localhost'],
|
|
28
|
-
|
|
29
|
-
// --------------------- 函数声明 ---------------------
|
|
30
|
-
// 日志(根据配置决定是否写入)
|
|
26
|
+
// --------------------- 工具函数 ---------------------
|
|
31
27
|
writeLog = (filePath, msg) => {
|
|
32
|
-
try {
|
|
33
|
-
fs.appendFileSync(filePath, new Date().toISOString() + ' ' + msg + '\n');
|
|
34
|
-
} catch (_) { }
|
|
28
|
+
try { fs.appendFileSync(filePath, new Date().toISOString() + ' ' + msg + '\n'); } catch (_) { }
|
|
35
29
|
},
|
|
36
30
|
log = msg => {
|
|
37
31
|
if (!CONFIG.LOGGING_ENABLED) return;
|
|
@@ -41,7 +35,6 @@ const require = createRequire(import.meta.url),
|
|
|
41
35
|
if (!CONFIG.LOGGING_ENABLED) return;
|
|
42
36
|
writeLog(path.join(process.cwd(), 'myapp_emergency.log'), msg);
|
|
43
37
|
},
|
|
44
|
-
// 服务器管理
|
|
45
38
|
killServerProcess = () => {
|
|
46
39
|
if (!serverProcess) return;
|
|
47
40
|
try {
|
|
@@ -50,7 +43,6 @@ const require = createRequire(import.meta.url),
|
|
|
50
43
|
} catch (_) { }
|
|
51
44
|
serverProcess = null;
|
|
52
45
|
},
|
|
53
|
-
|
|
54
46
|
focusMainWindow = () => {
|
|
55
47
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
56
48
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
@@ -59,7 +51,6 @@ const require = createRequire(import.meta.url),
|
|
|
59
51
|
return false;
|
|
60
52
|
},
|
|
61
53
|
sleep = ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
62
|
-
// 依赖安装
|
|
63
54
|
ensureDependencies = async () => {
|
|
64
55
|
const nodeModulesPath = path.join(__dirname, 'node_modules');
|
|
65
56
|
try {
|
|
@@ -94,20 +85,40 @@ const require = createRequire(import.meta.url),
|
|
|
94
85
|
return log('写入 package.json 失败: ' + err.message), false;
|
|
95
86
|
}
|
|
96
87
|
try {
|
|
97
|
-
let cmd = 'npm install';
|
|
88
|
+
let cmd = 'npm install --ignore-scripts --no-optional --force --no-audit --no-fund';
|
|
98
89
|
const lockPath = path.join(__dirname, 'package-lock.json');
|
|
99
90
|
try {
|
|
100
|
-
await fs.promises.access(lockPath, fs.constants.F_OK);
|
|
101
|
-
|
|
91
|
+
await fs.promises.access(lockPath, fs.constants.F_OK), log('找到 package-lock.json,使用 npm ci');
|
|
92
|
+
cmd = 'npm ci --ignore-scripts --no-optional --force --no-audit --no-fund';
|
|
102
93
|
} catch {
|
|
103
94
|
log('未找到 package-lock.json,使用 npm install');
|
|
104
95
|
}
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
|
|
97
|
+
const env = { ...process.env, npm_config_ignore_scripts: 'true', npm_config_optional: 'false' },
|
|
98
|
+
{ stdout, stderr } = await execPromise(cmd, { cwd: __dirname, env: env, timeout: 120000, });
|
|
99
|
+
log('依赖安装完成');
|
|
100
|
+
|
|
101
|
+
// 删除 node 和平台特定 node-* 目录
|
|
102
|
+
try {
|
|
103
|
+
const entries = await fs.promises.readdir(nodeModulesPath, { withFileTypes: true }), toDelete = [];
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (!entry.isDirectory()) continue;
|
|
106
|
+
const name = entry.name;
|
|
107
|
+
if (name === 'node') toDelete.push(path.join(nodeModulesPath, name));
|
|
108
|
+
if (/^node-(win|darwin|linux|freebsd|sunos|aix)-/.test(name)) toDelete.push(path.join(nodeModulesPath, name));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const dir of toDelete) {
|
|
112
|
+
await fs.promises.rm(dir, { recursive: true, force: true }), log(`已删除问题目录: ${dir}`);
|
|
113
|
+
}
|
|
114
|
+
} catch (cleanErr) {
|
|
115
|
+
log('清理目录时出错(可忽略): ' + cleanErr.message);
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
try {
|
|
108
119
|
return await fs.promises.access(nodeModulesPath, fs.constants.F_OK), true;
|
|
109
120
|
} catch {
|
|
110
|
-
return log('
|
|
121
|
+
return log('安装后 node_modules 仍然不存在'), false;
|
|
111
122
|
}
|
|
112
123
|
} catch (error) {
|
|
113
124
|
return log('依赖安装失败: ' + error.message), emergencyLog('依赖安装失败堆栈: ' + error.stack), false;
|
|
@@ -115,7 +126,6 @@ const require = createRequire(import.meta.url),
|
|
|
115
126
|
}
|
|
116
127
|
},
|
|
117
128
|
|
|
118
|
-
// 端口检查
|
|
119
129
|
ensurePortFree = port => {
|
|
120
130
|
return new Promise((resolve) => {
|
|
121
131
|
const server = net.createServer();
|
|
@@ -137,7 +147,6 @@ const require = createRequire(import.meta.url),
|
|
|
137
147
|
});
|
|
138
148
|
},
|
|
139
149
|
|
|
140
|
-
// 等待服务器就绪
|
|
141
150
|
waitForServer = (port, timeout = 30000) => {
|
|
142
151
|
return new Promise((resolve) => {
|
|
143
152
|
const start = Date.now(), protocol = new URL(CONFIG.APP_URL).protocol,
|
|
@@ -170,7 +179,6 @@ const require = createRequire(import.meta.url),
|
|
|
170
179
|
}
|
|
171
180
|
},
|
|
172
181
|
|
|
173
|
-
// 创建窗口
|
|
174
182
|
createWindow = async () => {
|
|
175
183
|
if (mainWindow && !mainWindow.isDestroyed()) return focusMainWindow(), mainWindow;
|
|
176
184
|
if (windowCreationPromise) {
|
|
@@ -183,13 +191,9 @@ const require = createRequire(import.meta.url),
|
|
|
183
191
|
...CONFIG.WINDOW_CONFIG,
|
|
184
192
|
webPreferences: {
|
|
185
193
|
...CONFIG.WINDOW_CONFIG.webPreferences,
|
|
186
|
-
webSecurity: true,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
plugins: true,
|
|
190
|
-
nodeIntegration: true,
|
|
191
|
-
sandbox: false,
|
|
192
|
-
contextIsolation: false,
|
|
194
|
+
webSecurity: true, allowRunningInsecureContent: true, enableWebAuthn: true,
|
|
195
|
+
plugins: true, nodeIntegration: true,
|
|
196
|
+
sandbox: false, contextIsolation: false,
|
|
193
197
|
}
|
|
194
198
|
};
|
|
195
199
|
mainWindow = new BrowserWindow(winConfig);
|
|
@@ -223,7 +227,6 @@ const require = createRequire(import.meta.url),
|
|
|
223
227
|
return windowCreationPromise;
|
|
224
228
|
},
|
|
225
229
|
|
|
226
|
-
// 启动服务器
|
|
227
230
|
startServer = async () => {
|
|
228
231
|
if (!CONFIG.AUTO_START_SERVER) return;
|
|
229
232
|
const serverPath = path.join(__dirname, CONFIG.SERVER_PATH);
|
|
@@ -250,7 +253,6 @@ const require = createRequire(import.meta.url),
|
|
|
250
253
|
log('服务器启动失败,尝试重新加载窗口');
|
|
251
254
|
},
|
|
252
255
|
|
|
253
|
-
// 菜单设置(处理特殊标记)
|
|
254
256
|
setupMenu = () => {
|
|
255
257
|
const template = CONFIG.MENU_TEMPLATE;
|
|
256
258
|
if (!template || template.length === 0) return;
|
|
@@ -285,7 +287,7 @@ try {
|
|
|
285
287
|
app.commandLine.appendSwitch('vmodule', 'webauthn*=3');
|
|
286
288
|
log(`已映射 ${hostname} 到 127.0.0.1`);
|
|
287
289
|
} catch (_) { }
|
|
288
|
-
|
|
290
|
+
|
|
289
291
|
if (!app.requestSingleInstanceLock()) { log('已有另一个实例在运行,退出'); app.quit(); }
|
|
290
292
|
else app.on('second-instance', () => focusMainWindow());
|
|
291
293
|
app.on('certificate-error', (e, wc, url, err, cert, cb) => { e.preventDefault(), cb(true); });
|
|
@@ -294,7 +296,7 @@ app.whenReady().then(async () => {
|
|
|
294
296
|
setupMenu();
|
|
295
297
|
|
|
296
298
|
const depsOk = await ensureDependencies();
|
|
297
|
-
if (!depsOk) log('
|
|
299
|
+
if (!depsOk) log('依赖安装失败,再次尝试启动服务器(可能失败)');
|
|
298
300
|
await startServer(), await createWindow();
|
|
299
301
|
});
|
|
300
302
|
|
package/lib/build.js
CHANGED
|
@@ -11,11 +11,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)), require = create
|
|
|
11
11
|
CACHE_DIR = path.join(os.homedir(), '.electron-builder-cache'), DEFAULT_ICON = path.join(__dirname, 'app.png'),
|
|
12
12
|
DEFAULT_TARGETS = { win: ['nsis'], mac: ['dmg'], linux: ['AppImage'] },
|
|
13
13
|
DEFAULT_INSTALLER_ICON = path.join(__dirname, 'setup.ico'), DEFAULT_UNINSTALLER_ICON = path.join(__dirname, 'un.ico');
|
|
14
|
-
|
|
15
|
-
* 构建桌面应用程序
|
|
16
|
-
* >查看定义:@see {@link build}
|
|
17
|
-
* @returns {Promise<void>}
|
|
18
|
-
*/
|
|
14
|
+
|
|
19
15
|
const build = async () => {
|
|
20
16
|
const configPath = path.join(process.cwd(), 'desktopAppConfig.js');
|
|
21
17
|
if (!(await fs.pathExists(configPath)))
|
|
@@ -37,7 +33,6 @@ const build = async () => {
|
|
|
37
33
|
const projectRoot = path.resolve(process.cwd());
|
|
38
34
|
console.log(chalk.blue('[信息] 正在复制项目文件从 ' + projectRoot + ' 到 ' + tempDir));
|
|
39
35
|
|
|
40
|
-
// 复制文件,应用 excludeFiles
|
|
41
36
|
await fs.copy(projectRoot, tempDir, {
|
|
42
37
|
filter: (src) => {
|
|
43
38
|
const relative = path.relative(projectRoot, src);
|
|
@@ -65,7 +60,6 @@ const build = async () => {
|
|
|
65
60
|
dereference: true,
|
|
66
61
|
});
|
|
67
62
|
|
|
68
|
-
// 生成 main.mjs
|
|
69
63
|
const mainTemplatePath = path.join(__dirname, '..', 'electron-main.js'),
|
|
70
64
|
mainTemplate = await fs.readFile(mainTemplatePath, 'utf-8');
|
|
71
65
|
|
|
@@ -76,7 +70,7 @@ const build = async () => {
|
|
|
76
70
|
return value;
|
|
77
71
|
});
|
|
78
72
|
}
|
|
79
|
-
else console.log(chalk.yellow('[警告]
|
|
73
|
+
else console.log(chalk.yellow('[警告] 未配置菜单,将使用默认英文菜单;'));
|
|
80
74
|
|
|
81
75
|
const windowConfig = {
|
|
82
76
|
width: 1200, height: 800, minWidth: 800, minHeight: 600,
|
|
@@ -96,9 +90,8 @@ const build = async () => {
|
|
|
96
90
|
.replace('__AUTO_KILL_SERVER__', JSON.stringify(advanced.autoKillServer))
|
|
97
91
|
.replace('__MENU_TEMPLATE__', menuCode)
|
|
98
92
|
.replace('__LOGGING_ENABLED__', JSON.stringify(enableLogging));
|
|
99
|
-
|
|
100
93
|
await fs.writeFile(path.join(tempDir, 'main.mjs'), mainJs);
|
|
101
|
-
|
|
94
|
+
|
|
102
95
|
const origPkgPath = path.join(projectRoot, 'package.json');
|
|
103
96
|
let allDeps = {}, pkgJson = {};
|
|
104
97
|
if (await fs.pathExists(origPkgPath)) {
|
|
@@ -116,7 +109,6 @@ const build = async () => {
|
|
|
116
109
|
await fs.writeJson(path.join(tempDir, 'deps.json'), allDeps, { spaces: 2 });
|
|
117
110
|
console.log(chalk.blue('[信息] 成功写入 ' + Object.keys(allDeps).length + ' 个依赖包到 deps.json'));
|
|
118
111
|
|
|
119
|
-
// 图标处理
|
|
120
112
|
const handleIcon = async (userPath, defaultPath, img, label) => {
|
|
121
113
|
const resolved = userPath ? path.resolve(process.cwd(), userPath) : null;
|
|
122
114
|
let src = (resolved && (await fs.pathExists(resolved))) ? resolved : defaultPath;
|
|
@@ -129,7 +121,6 @@ const build = async () => {
|
|
|
129
121
|
await handleIcon(config.branding?.installerIcon, DEFAULT_INSTALLER_ICON, 'installer.ico', '安装');
|
|
130
122
|
await handleIcon(config.branding?.uninstallerIcon, DEFAULT_UNINSTALLER_ICON, 'uninstaller.ico', '卸载');
|
|
131
123
|
|
|
132
|
-
// Electron 版本
|
|
133
124
|
let electronVersion;
|
|
134
125
|
try {
|
|
135
126
|
const electronPkgPath = require.resolve('electron/package.json'), electronPkg = await fs.readJson(electronPkgPath);
|
|
@@ -138,7 +129,6 @@ const build = async () => {
|
|
|
138
129
|
console.error(chalk.red('[错误] 未找到 electron 包,请先安装;')), process.exit(1);
|
|
139
130
|
}
|
|
140
131
|
|
|
141
|
-
// 平台映射
|
|
142
132
|
const platformInfo = {
|
|
143
133
|
win32: { platform: 'win', arg: '--win' },
|
|
144
134
|
darwin: { platform: 'mac', arg: '--mac' },
|
|
@@ -146,8 +136,6 @@ const build = async () => {
|
|
|
146
136
|
},
|
|
147
137
|
info = platformInfo[process.platform] || platformInfo.win32, currentPlatform = info.platform,
|
|
148
138
|
platformArg = info.arg, buildConfig = config.build || {},
|
|
149
|
-
// ----- 构建 electron-builder 配置对象 -----
|
|
150
|
-
// 基础默认配置
|
|
151
139
|
defaultNsis = {
|
|
152
140
|
oneClick: false,
|
|
153
141
|
perMachine: true,
|
|
@@ -178,8 +166,7 @@ const build = async () => {
|
|
|
178
166
|
delete userBuild.nsis, delete userBuild.dmg, delete userBuild.outputDir, delete userBuild.appId;
|
|
179
167
|
Object.assign(configObj, userBuild);
|
|
180
168
|
|
|
181
|
-
|
|
182
|
-
const platformKey = currentPlatform; // 'win', 'mac', 'linux'
|
|
169
|
+
const platformKey = currentPlatform;
|
|
183
170
|
if (!configObj[platformKey]) configObj[platformKey] = { target: DEFAULT_TARGETS[platformKey] };
|
|
184
171
|
else if (!configObj[platformKey].target) configObj[platformKey].target = DEFAULT_TARGETS[platformKey];
|
|
185
172
|
|
|
@@ -218,7 +205,7 @@ const build = async () => {
|
|
|
218
205
|
}
|
|
219
206
|
|
|
220
207
|
if (!success) console.error(chalk.red('[错误] 构建失败,已尝试 3 次:'), lastError), process.exit(1);
|
|
221
|
-
|
|
208
|
+
|
|
222
209
|
const tempDistDir = path.join(tempDir, 'dist'), userOutputDir = buildConfig.outputDir || './dist',
|
|
223
210
|
targetDir = path.resolve(process.cwd(), userOutputDir);
|
|
224
211
|
|
|
@@ -247,12 +234,6 @@ const build = async () => {
|
|
|
247
234
|
else console.warn(chalk.yellow('[警告] 未找到 dist 目录;'));
|
|
248
235
|
};
|
|
249
236
|
|
|
250
|
-
/**
|
|
251
|
-
* 命令行入口
|
|
252
|
-
* 解析 process.argv 并执行对应的构建命令
|
|
253
|
-
* >查看定义:@see {@link runCLI}
|
|
254
|
-
* @returns {Promise<void>}
|
|
255
|
-
*/
|
|
256
237
|
const runCLI = async () => {
|
|
257
238
|
const args = process.argv.slice(2), command = args[0];
|
|
258
239
|
|