@flun/desktop-builder 1.0.1
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 +4 -0
- package/LICENSE +17 -0
- package/README.md +596 -0
- package/copy-files.js +34 -0
- package/desktopAppConfig.js +222 -0
- package/electron-main.js +317 -0
- package/index.d.ts +37 -0
- package/index.js +4 -0
- package/lib/app.png +0 -0
- package/lib/build.js +271 -0
- package/lib/setup.ico +0 -0
- package/lib/un.ico +0 -0
- package/package.json +62 -0
package/lib/build.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { execa } from 'execa';
|
|
8
|
+
import { minimatch } from 'minimatch';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url)), require = createRequire(import.meta.url),
|
|
11
|
+
CACHE_DIR = path.join(os.homedir(), '.electron-builder-cache'), DEFAULT_ICON = path.join(__dirname, 'app.png'),
|
|
12
|
+
DEFAULT_TARGETS = { win: ['nsis'], mac: ['dmg'], linux: ['AppImage'] },
|
|
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
|
+
*/
|
|
19
|
+
const build = async () => {
|
|
20
|
+
const configPath = path.join(process.cwd(), 'desktopAppConfig.js');
|
|
21
|
+
if (!(await fs.pathExists(configPath)))
|
|
22
|
+
console.error(chalk.red('[错误] 未找到 desktopAppConfig.js 配置文件;')), process.exit(1);
|
|
23
|
+
|
|
24
|
+
const configModule = await import(`file://${configPath}?t=${Date.now()}`), config = configModule.default;
|
|
25
|
+
if (!config.serverPath || !config.appUrl || !config.appName)
|
|
26
|
+
console.error(chalk.red('[错误] 配置文件缺少必填字段: serverPath, appUrl, appName')), process.exit(1);
|
|
27
|
+
|
|
28
|
+
const excludeFiles = config.excludeFiles || [], excludeDependencies = config.excludeDependencies || [],
|
|
29
|
+
excludeOutputs = config.excludeOutputs || [], enableLogging = config.enableLogging ?? false;
|
|
30
|
+
|
|
31
|
+
await fs.ensureDir(CACHE_DIR);
|
|
32
|
+
process.env.ELECTRON_BUILDER_CACHE = CACHE_DIR, process.env.ELECTRON_CACHE = CACHE_DIR;
|
|
33
|
+
|
|
34
|
+
const tempDir = path.join(os.tmpdir(), 'desktop-builder-build', path.basename(process.cwd()) + '-' + Date.now());
|
|
35
|
+
await fs.ensureDir(tempDir), await fs.emptyDir(tempDir);
|
|
36
|
+
|
|
37
|
+
const projectRoot = path.resolve(process.cwd());
|
|
38
|
+
console.log(chalk.blue('[信息] 正在复制项目文件从 ' + projectRoot + ' 到 ' + tempDir));
|
|
39
|
+
|
|
40
|
+
// 复制文件,应用 excludeFiles
|
|
41
|
+
await fs.copy(projectRoot, tempDir, {
|
|
42
|
+
filter: (src) => {
|
|
43
|
+
const relative = path.relative(projectRoot, src);
|
|
44
|
+
if (relative === '') return true;
|
|
45
|
+
|
|
46
|
+
const isRootFile = !relative.includes(path.sep);
|
|
47
|
+
for (const pattern of excludeFiles) {
|
|
48
|
+
if (pattern.endsWith('/')) {
|
|
49
|
+
const dirName = pattern.slice(0, -1);
|
|
50
|
+
if (relative === dirName || relative.startsWith(dirName + path.sep)) return false;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (pattern.startsWith('./')) {
|
|
55
|
+
const strippedPattern = pattern.slice(2);
|
|
56
|
+
if (isRootFile && minimatch(relative, strippedPattern, { dot: true, matchBase: false })) return false;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (relative === pattern) return false;
|
|
61
|
+
if (minimatch(relative, pattern, { dot: true, matchBase: true })) return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
},
|
|
65
|
+
dereference: true,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// 生成 main.mjs
|
|
69
|
+
const mainTemplatePath = path.join(__dirname, '..', 'electron-main.js'),
|
|
70
|
+
mainTemplate = await fs.readFile(mainTemplatePath, 'utf-8');
|
|
71
|
+
|
|
72
|
+
let menuCode = 'null';
|
|
73
|
+
if (config.menu && Array.isArray(config.menu) && config.menu.length > 0) {
|
|
74
|
+
menuCode = JSON.stringify(config.menu, (key, value) => {
|
|
75
|
+
if (typeof value === 'function') return value.toString();
|
|
76
|
+
return value;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else console.log(chalk.yellow('[警告] 未配置菜单,将使用默认英文菜单;'));
|
|
80
|
+
|
|
81
|
+
const windowConfig = {
|
|
82
|
+
width: 1200, height: 800, minWidth: 800, minHeight: 600,
|
|
83
|
+
resizable: true, fullscreenable: true, frame: true,
|
|
84
|
+
alwaysOnTop: false, show: false,
|
|
85
|
+
backgroundColor: '#ffffff',
|
|
86
|
+
webPreferences: { nodeIntegration: false, contextIsolation: true },
|
|
87
|
+
...(config.window || {}),
|
|
88
|
+
},
|
|
89
|
+
advanced = { autoStartServer: true, autoKillServer: true, ...(config.advanced || {}) },
|
|
90
|
+
serverRelPath = path.basename(config.serverPath),
|
|
91
|
+
mainJs = mainTemplate
|
|
92
|
+
.replace('__APP_URL__', JSON.stringify(config.appUrl))
|
|
93
|
+
.replace('__WINDOW_CONFIG__', JSON.stringify(windowConfig))
|
|
94
|
+
.replace('__SERVER_PATH__', JSON.stringify(serverRelPath))
|
|
95
|
+
.replace('__AUTO_START_SERVER__', JSON.stringify(advanced.autoStartServer))
|
|
96
|
+
.replace('__AUTO_KILL_SERVER__', JSON.stringify(advanced.autoKillServer))
|
|
97
|
+
.replace('__MENU_TEMPLATE__', menuCode)
|
|
98
|
+
.replace('__LOGGING_ENABLED__', JSON.stringify(enableLogging));
|
|
99
|
+
|
|
100
|
+
await fs.writeFile(path.join(tempDir, 'main.mjs'), mainJs);
|
|
101
|
+
// 处理 package.json
|
|
102
|
+
const origPkgPath = path.join(projectRoot, 'package.json');
|
|
103
|
+
let allDeps = {}, pkgJson = {};
|
|
104
|
+
if (await fs.pathExists(origPkgPath)) {
|
|
105
|
+
const origPkg = await fs.readJson(origPkgPath);
|
|
106
|
+
allDeps = { ...origPkg.dependencies, ...origPkg.devDependencies };
|
|
107
|
+
for (const ex of excludeDependencies) delete allDeps[ex];
|
|
108
|
+
|
|
109
|
+
console.log(chalk.blue('[信息] 排除后依赖包数量: ' + Object.keys(allDeps).length));
|
|
110
|
+
pkgJson = { ...origPkg }, delete pkgJson.dependencies, delete pkgJson.devDependencies;
|
|
111
|
+
pkgJson.main = 'main.mjs', pkgJson.description = config.appName;
|
|
112
|
+
}
|
|
113
|
+
else console.warn(chalk.yellow('[错误] 请配置 package.json 文件')), process.exit(1);
|
|
114
|
+
|
|
115
|
+
await fs.writeJson(path.join(tempDir, 'package.json'), pkgJson, { spaces: 2 });
|
|
116
|
+
await fs.writeJson(path.join(tempDir, 'deps.json'), allDeps, { spaces: 2 });
|
|
117
|
+
console.log(chalk.blue('[信息] 成功写入 ' + Object.keys(allDeps).length + ' 个依赖包到 deps.json'));
|
|
118
|
+
|
|
119
|
+
// 图标处理
|
|
120
|
+
const handleIcon = async (userPath, defaultPath, img, label) => {
|
|
121
|
+
const resolved = userPath ? path.resolve(process.cwd(), userPath) : null;
|
|
122
|
+
let src = (resolved && (await fs.pathExists(resolved))) ? resolved : defaultPath;
|
|
123
|
+
await fs.copy(src, path.join(tempDir, img));
|
|
124
|
+
if (src !== resolved) console.log(chalk.yellow(`[警告] 未找到自定义${label}图标,将使用默认图标;`));
|
|
125
|
+
return src;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
await handleIcon(config.branding?.appIcon, DEFAULT_ICON, 'app.png', '应用');
|
|
129
|
+
await handleIcon(config.branding?.installerIcon, DEFAULT_INSTALLER_ICON, 'installer.ico', '安装');
|
|
130
|
+
await handleIcon(config.branding?.uninstallerIcon, DEFAULT_UNINSTALLER_ICON, 'uninstaller.ico', '卸载');
|
|
131
|
+
|
|
132
|
+
// Electron 版本
|
|
133
|
+
let electronVersion;
|
|
134
|
+
try {
|
|
135
|
+
const electronPkgPath = require.resolve('electron/package.json'), electronPkg = await fs.readJson(electronPkgPath);
|
|
136
|
+
electronVersion = electronPkg.version, console.log(chalk.blue('[信息] Electron 版本: ' + electronVersion));
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(chalk.red('[错误] 未找到 electron 包,请先安装;')), process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 平台映射
|
|
142
|
+
const platformInfo = {
|
|
143
|
+
win32: { platform: 'win', arg: '--win' },
|
|
144
|
+
darwin: { platform: 'mac', arg: '--mac' },
|
|
145
|
+
linux: { platform: 'linux', arg: '--linux' },
|
|
146
|
+
},
|
|
147
|
+
info = platformInfo[process.platform] || platformInfo.win32, currentPlatform = info.platform,
|
|
148
|
+
platformArg = info.arg, buildConfig = config.build || {},
|
|
149
|
+
// ----- 构建 electron-builder 配置对象 -----
|
|
150
|
+
// 基础默认配置
|
|
151
|
+
defaultNsis = {
|
|
152
|
+
oneClick: false,
|
|
153
|
+
perMachine: true,
|
|
154
|
+
allowToChangeInstallationDirectory: true,
|
|
155
|
+
createDesktopShortcut: true,
|
|
156
|
+
createStartMenuShortcut: true,
|
|
157
|
+
shortcutName: config.appName,
|
|
158
|
+
deleteAppDataOnUninstall: false,
|
|
159
|
+
installerIcon: 'installer.ico',
|
|
160
|
+
uninstallerIcon: 'uninstaller.ico',
|
|
161
|
+
},
|
|
162
|
+
defaultDmg = { iconSize: 128, window: { width: 540, height: 380 } },
|
|
163
|
+
configObj = {
|
|
164
|
+
files: [
|
|
165
|
+
'!builder.json',
|
|
166
|
+
'!app.png', '!installer.ico', '!uninstaller.ico'
|
|
167
|
+
],
|
|
168
|
+
appId: buildConfig.appId || 'com.example.app',
|
|
169
|
+
productName: config.appName,
|
|
170
|
+
directories: { output: buildConfig.outputDir || './dist' },
|
|
171
|
+
asar: false,
|
|
172
|
+
electronVersion: electronVersion,
|
|
173
|
+
npmRebuild: false,
|
|
174
|
+
nsis: { ...defaultNsis, ...(buildConfig.nsis || {}) },
|
|
175
|
+
dmg: { ...defaultDmg, ...(buildConfig.dmg || {}) },
|
|
176
|
+
icon: 'app.png',
|
|
177
|
+
}, userBuild = { ...buildConfig };
|
|
178
|
+
delete userBuild.nsis, delete userBuild.dmg, delete userBuild.outputDir, delete userBuild.appId;
|
|
179
|
+
Object.assign(configObj, userBuild);
|
|
180
|
+
|
|
181
|
+
// 平台特定配置:根据当前平台补充默认 target(如果用户未提供该平台配置)
|
|
182
|
+
const platformKey = currentPlatform; // 'win', 'mac', 'linux'
|
|
183
|
+
if (!configObj[platformKey]) configObj[platformKey] = { target: DEFAULT_TARGETS[platformKey] };
|
|
184
|
+
else if (!configObj[platformKey].target) configObj[platformKey].target = DEFAULT_TARGETS[platformKey];
|
|
185
|
+
|
|
186
|
+
const configFile = path.join(tempDir, 'builder.json');
|
|
187
|
+
await fs.writeJson(configFile, configObj, { spaces: 2 });
|
|
188
|
+
|
|
189
|
+
const args = [
|
|
190
|
+
'--no-install', 'electron-builder', '--project',
|
|
191
|
+
tempDir, '--config', configFile, platformArg
|
|
192
|
+
];
|
|
193
|
+
console.log(chalk.blue('[信息] 正在执行构建: npx ' + args.join(' ')));
|
|
194
|
+
let retries = 3, lastError = null, success = false;
|
|
195
|
+
while (retries > 0) {
|
|
196
|
+
try {
|
|
197
|
+
await execa('npx', args, {
|
|
198
|
+
cwd: process.cwd(), stdio: 'inherit',
|
|
199
|
+
env: {
|
|
200
|
+
...process.env,
|
|
201
|
+
ELECTRON_MIRROR: 'https://npmmirror.com/mirrors/electron/',
|
|
202
|
+
NSIS_MIRROR: 'https://npmmirror.com/mirrors/nsis/',
|
|
203
|
+
ELECTRON_BUILDER_BINARIES_MIRROR: 'https://mirrors.huaweicloud.com/electron-builder-binaries/',
|
|
204
|
+
ELECTRON_BUILDER_CACHE: CACHE_DIR,
|
|
205
|
+
CSC_IDENTITY_AUTO_DISCOVERY: 'false',
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
success = true;
|
|
209
|
+
break;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
lastError = error, retries--;
|
|
212
|
+
if (retries > 0) {
|
|
213
|
+
console.warn(chalk.yellow(`[警告] 构建失败,正在重试...(剩余 ${retries} 次尝试)`));
|
|
214
|
+
console.warn(chalk.yellow(' 错误信息: ' + error.message));
|
|
215
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!success) console.error(chalk.red('[错误] 构建失败,已尝试 3 次:'), lastError), process.exit(1);
|
|
221
|
+
// 复制最终安装包,应用 excludeOutputs
|
|
222
|
+
const tempDistDir = path.join(tempDir, 'dist'), userOutputDir = buildConfig.outputDir || './dist',
|
|
223
|
+
targetDir = path.resolve(process.cwd(), userOutputDir);
|
|
224
|
+
|
|
225
|
+
if (await fs.pathExists(tempDistDir)) {
|
|
226
|
+
await fs.ensureDir(targetDir);
|
|
227
|
+
const files = await fs.readdir(tempDistDir);
|
|
228
|
+
let copiedCount = 0;
|
|
229
|
+
for (const file of files) {
|
|
230
|
+
let shouldExclude = false;
|
|
231
|
+
for (const pattern of excludeOutputs) {
|
|
232
|
+
if (minimatch(file, pattern, { dot: true })) {
|
|
233
|
+
shouldExclude = true, console.log(chalk.yellow(`[信息] 排除输出文件: ${file}`));
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (shouldExclude) continue;
|
|
238
|
+
const INSTALLER_EXTENSIONS = ['.exe', '.dmg', '.AppImage', '.deb', '.rpm', '.pkg', '.zip'];
|
|
239
|
+
if (INSTALLER_EXTENSIONS.some(ext => file.endsWith(ext))) {
|
|
240
|
+
const src = path.join(tempDistDir, file), dest = path.join(targetDir, file);
|
|
241
|
+
await fs.copy(src, dest), copiedCount++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (copiedCount > 0) console.log(chalk.green('[成功] 构建完成!安装包位于: ' + targetDir));
|
|
245
|
+
else console.warn(chalk.yellow('[警告] 未找到安装包文件;'));
|
|
246
|
+
}
|
|
247
|
+
else console.warn(chalk.yellow('[警告] 未找到 dist 目录;'));
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 命令行入口
|
|
252
|
+
* 解析 process.argv 并执行对应的构建命令
|
|
253
|
+
* >查看定义:@see {@link runCLI}
|
|
254
|
+
* @returns {Promise<void>}
|
|
255
|
+
*/
|
|
256
|
+
const runCLI = async () => {
|
|
257
|
+
const args = process.argv.slice(2), command = args[0];
|
|
258
|
+
|
|
259
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
260
|
+
console.log(`用法:先配置 desktopAppConfig.js 文件,然后运行-> desktop-builder build 指令构建桌面应用程序`);
|
|
261
|
+
process.exit(0);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (command === 'build') await build();
|
|
265
|
+
else {
|
|
266
|
+
console.error(`未知命令: ${command}`);
|
|
267
|
+
console.log('运行 "desktop-builder" 或 "desktop-builder --help" 查看用法。'), process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export { runCLI, build };
|
package/lib/setup.ico
ADDED
|
Binary file
|
package/lib/un.ico
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flun/desktop-builder",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "将任意 Node.js 网站一键打包为当前桌面应用('win', 'mac', 'linux')(基于 Electron),支持高度自定义配置;",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "index.js",
|
|
11
|
+
"bin": {
|
|
12
|
+
"desktop-builder": "./index.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"postinstall": "node copy-files.js 2>&1"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"electron",
|
|
19
|
+
"desktop",
|
|
20
|
+
"nodejs",
|
|
21
|
+
"packager",
|
|
22
|
+
"installer",
|
|
23
|
+
"nsis",
|
|
24
|
+
"dmg",
|
|
25
|
+
"appimage"
|
|
26
|
+
],
|
|
27
|
+
"author": "flun <open@flun.top>",
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/OpenFlun/desktop-builder.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/OpenFlun/desktop-builder#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/OpenFlun/desktop-builder/issues"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"index.js",
|
|
42
|
+
"index.d.ts",
|
|
43
|
+
"lib/",
|
|
44
|
+
"copy-files.js",
|
|
45
|
+
"desktopAppConfig.js",
|
|
46
|
+
"electron-main.js",
|
|
47
|
+
"LICENSE",
|
|
48
|
+
"CHANGELOG.md",
|
|
49
|
+
"README.md"
|
|
50
|
+
],
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"electron": "^43.0.0",
|
|
53
|
+
"electron-builder": "^26.15.3",
|
|
54
|
+
"execa": "^9.6.1",
|
|
55
|
+
"fs-extra": "^11.3.6",
|
|
56
|
+
"chalk": "^5.6.2"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=22.12.0",
|
|
60
|
+
"npm": ">=10.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|