@yufengtadian/freedom-cli 1.13.1 → 1.13.3
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/LICENSE +40 -0
- package/README.md +119 -15
- package/lib/agents.js +448 -0
- package/lib/build.js +189 -45
- package/lib/cli.js +258 -9
- package/lib/config.js +20 -1
- package/lib/desktop.js +133 -0
- package/lib/dev.js +149 -0
- package/lib/mcp.js +217 -0
- package/lib/release.js +90 -0
- package/lib/security.js +113 -54
- package/lib/shell.js +170 -63
- package/lib/toolchain.js +339 -0
- package/lib/tui.js +37 -1
- package/lib/utils.js +51 -0
- package/lib/verify.js +48 -12
- package/lib/webkit.js +45 -0
- package/package.json +3 -2
- package/shell/darwin-arm64/freedom-shell +0 -0
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/skill/freedom/SKILL.md +159 -0
- package/templates/desktop/app.html +559 -0
- package/templates/desktop/backend/desktop.mjs +327 -0
- package/templates/desktop/freedom.config.js +32 -0
- package/templates/desktop/icon.ico +0 -0
- package/templates/go/go.mod +3 -0
- package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
- package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +238 -11
- package/templates/go/pkg/freedom/assets/freedom.js +3 -0
- package/templates/go/pkg/freedom/assets_embed.go +1 -0
- package/templates/go/pkg/freedom/backend_proc.go +5 -5
- package/templates/go/pkg/freedom/events_windows.go +5 -5
- package/templates/go/pkg/freedom/freedom.go +64 -10
- package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
- package/templates/go/pkg/freedom/resources.go +119 -14
- package/templates/go/pkg/freedom/securetemp.go +61 -0
- package/templates/go/pkg/freedom/securetemp_other.go +20 -0
- package/templates/go/pkg/freedom/securetemp_windows.go +25 -0
- package/templates/go/pkg/freedom/security.go +293 -64
- package/templates/go/pkg/freedom/shutdown.go +55 -0
- package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
- package/templates/go/pkg/freedom/store.go +11 -3
- package/templates/go/pkg/freedom/syscap_windows.go +33 -33
- package/templates/go/pkg/freedom/sysint_windows.go +2 -2
- package/templates/go/pkg/freedom/tray_windows.go +18 -18
- package/templates/go/pkg/freedom/updater.go +3 -3
- package/templates/go/pkg/freedom/window_other.go +7 -1
- package/templates/go/pkg/freedom/window_windows.go +185 -17
- package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
- package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
- package/templates/go/third_party/webview_go/LICENSE +22 -0
- package/templates/go/third_party/webview_go/README.md +50 -0
- package/templates/go/third_party/webview_go/glue.c +36 -0
- package/templates/go/third_party/webview_go/go.mod +3 -0
- package/templates/go/third_party/webview_go/go.sum +0 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
- package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
- package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
- package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
- package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
- package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
- package/templates/go/third_party/webview_go/webview.cc +1 -0
- package/templates/go/third_party/webview_go/webview.go +332 -0
- package/templates/installer/app.nsi +43 -0
- package/templates/project/freedom.config.js +14 -0
- package/templates/project/freedom.d.ts +77 -0
- package/templates/project-minimal/freedom.config.js +14 -0
- package/templates/project-minimal/freedom.d.ts +77 -0
package/lib/build.js
CHANGED
|
@@ -32,6 +32,7 @@ const {
|
|
|
32
32
|
isMacPlat,
|
|
33
33
|
platformExeName,
|
|
34
34
|
nativePlatform,
|
|
35
|
+
normalizePlatform,
|
|
35
36
|
localShellPath,
|
|
36
37
|
} = require('./utils');
|
|
37
38
|
const { hasShell, downloadShell, validateLocalShell } = require('./shell');
|
|
@@ -60,12 +61,12 @@ function run(cmd, args, opts = {}) {
|
|
|
60
61
|
return res;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
// --platform 解析:支持逗号 / 中英文逗号 / 空白分隔多平台;win|mac|linux|all 或平台 key
|
|
64
|
+
// --platform 解析:支持逗号 / 中英文逗号 / 空白分隔多平台;win|mac|linux|all 或平台 key
|
|
65
|
+
// (win-x64 等,别名口径与 freedom shell 子命令统一走 utils.normalizePlatform)。
|
|
64
66
|
// 多平台去重保留顺序。all 仅取可分发平台(DIST_PLATFORMS):linux-arm64 无 CI 资产,
|
|
65
67
|
// 若列入 all 会在 build 时 404 拖垮整个全量构建(历史 bug B41)。
|
|
66
68
|
function parsePlatforms(raw) {
|
|
67
69
|
if (!raw) return [nativePlatform()];
|
|
68
|
-
const keyMap = { win: 'win-x64', mac: 'darwin-arm64', linux: 'linux-x64' };
|
|
69
70
|
const seen = [];
|
|
70
71
|
const push = (p) => {
|
|
71
72
|
if (!ALL_PLATFORMS.includes(p)) {
|
|
@@ -81,7 +82,7 @@ function parsePlatforms(raw) {
|
|
|
81
82
|
if (v === 'all') {
|
|
82
83
|
for (const p of DIST_PLATFORMS) push(p);
|
|
83
84
|
} else {
|
|
84
|
-
push(
|
|
85
|
+
push(normalizePlatform(v) || v);
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
if (seen.length === 0) {
|
|
@@ -107,28 +108,40 @@ async function build(projectDir, opts = {}) {
|
|
|
107
108
|
const autoDownload = opts.autoDownload !== false;
|
|
108
109
|
|
|
109
110
|
// 1) 前端打包(带缓存:源码未变更时复用上次 vite 产物,跳过 npm run build)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
// cfg.staticHtml:静态单文件页面直通——不跑 npm install / vite,用于 freedom 自己的
|
|
112
|
+
// Desktop 界面自举打包(以及纯静态页项目),零网络、零依赖。
|
|
113
|
+
let html;
|
|
114
|
+
if (cfg.staticHtml) {
|
|
115
|
+
const staticPath = path.resolve(dir, String(cfg.staticHtml));
|
|
116
|
+
if (!fs.existsSync(staticPath)) {
|
|
117
|
+
throw new Error(`staticHtml 指向的文件不存在:${staticPath}(freedom.config.js 配置项)`);
|
|
118
|
+
}
|
|
119
|
+
html = fs.readFileSync(staticPath, 'utf8');
|
|
120
|
+
warnIfNotSingleFile(html, staticPath);
|
|
114
121
|
} else {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
ensureNodeModules(dir);
|
|
123
|
+
const useCache = opts.noCache !== true;
|
|
124
|
+
if (useCache && !viteNeedsRebuild(dir)) {
|
|
125
|
+
process.stdout.write('[freedom] 前端源码无改动,复用构建缓存...\n');
|
|
126
|
+
} else {
|
|
127
|
+
process.stdout.write('[freedom] 前端打包中(npm run build)...\n');
|
|
128
|
+
// 全量构建时 npm/vite 输出必须实时透传(stdio: inherit),
|
|
129
|
+
// 此前 pipe 缓冲会吞掉构建全程输出,Node 主线程被 spawnSync 同步阻塞,
|
|
130
|
+
// 大前端全量构建时终端长时间零输出,表现如"未响应/卡死"。
|
|
131
|
+
const vite = run('npm', ['run', 'build'], { cwd: dir, stdio: 'inherit' });
|
|
132
|
+
if (vite.status !== 0) {
|
|
133
|
+
throw new Error(`前端打包失败(退出码 ${vite.status}),详见上方构建输出。`);
|
|
134
|
+
}
|
|
135
|
+
writeViteCacheMarker(dir);
|
|
122
136
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
const distHtml = path.join(dir, '.freedom', 'vite-dist', 'index.html');
|
|
138
|
+
if (!fs.existsSync(distHtml)) {
|
|
139
|
+
throw new Error(`前端打包完成但未找到 ${distHtml},请检查 vite 配置(vite-plugin-singlefile)。`);
|
|
140
|
+
}
|
|
141
|
+
html = fs.readFileSync(distHtml, 'utf8');
|
|
142
|
+
warnIfNotSingleFile(html, distHtml);
|
|
128
143
|
}
|
|
129
|
-
const
|
|
130
|
-
warnIfNotSingleFile(html, distHtml);
|
|
131
|
-
const configJSON = renderConfigJSON(cfg, name);
|
|
144
|
+
const configJSON = renderConfigJSON(cfg, name, version);
|
|
132
145
|
// 安全模式:--security 优先于 freedom.config.js 的 security 字段,默认 none。
|
|
133
146
|
// none = 明文资源(默认,兼容历史产物)
|
|
134
147
|
// basic = 明文资源 + 构建期安全提示(剥符号 / 混淆,后续可用高模式加密)
|
|
@@ -149,8 +162,8 @@ async function build(projectDir, opts = {}) {
|
|
|
149
162
|
const results = await Promise.all(
|
|
150
163
|
platforms.map(async (plat) => {
|
|
151
164
|
const targetDir = multi ? path.join(outDirPath, plat) : outDirPath;
|
|
152
|
-
const
|
|
153
|
-
return { plat,
|
|
165
|
+
const emitted = await emitPlatform({ plat, name, version, targetDir, html, configJSON, backendDir, hasBackend, autoDownload, icon, security, installer: opts.installer === true });
|
|
166
|
+
return { plat, ...emitted };
|
|
154
167
|
})
|
|
155
168
|
);
|
|
156
169
|
|
|
@@ -184,7 +197,7 @@ async function build(projectDir, opts = {}) {
|
|
|
184
197
|
return { results };
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
async function emitPlatform({ plat, name, version, targetDir, html, configJSON, backendDir, hasBackend, autoDownload, icon, security }) {
|
|
200
|
+
async function emitPlatform({ plat, name, version, targetDir, html, configJSON, backendDir, hasBackend, autoDownload, icon, security, installer }) {
|
|
188
201
|
// 取预编译壳二进制
|
|
189
202
|
const shell = localShellPath(plat);
|
|
190
203
|
if (!fs.existsSync(shell)) {
|
|
@@ -233,14 +246,24 @@ async function emitPlatform({ plat, name, version, targetDir, html, configJSON,
|
|
|
233
246
|
const p = path.join(resDir, legacy);
|
|
234
247
|
if (fs.existsSync(p)) await fsp.rm(p, { force: true });
|
|
235
248
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
249
|
+
// 后端源码同样进容器:high 模式磁盘上不留 resources/backend 明文目录,
|
|
250
|
+
// 壳启动时把容器内 backend/* 解密到一次性临时目录(退出即删)。
|
|
251
|
+
const legacyBackend = path.join(resDir, 'backend');
|
|
252
|
+
if (fs.existsSync(legacyBackend)) await fsp.rm(legacyBackend, { recursive: true, force: true });
|
|
253
|
+
const backendFiles = {};
|
|
239
254
|
if (hasBackend && fs.existsSync(backendDir)) {
|
|
240
|
-
collectDirFiles(backendDir, '',
|
|
255
|
+
for (const [rel, buf] of Object.entries(collectDirFiles(backendDir, '', {}))) {
|
|
256
|
+
backendFiles['backend/' + rel] = { data: buf, mode: fileMode(path.join(backendDir, rel)) };
|
|
257
|
+
}
|
|
241
258
|
}
|
|
242
|
-
|
|
243
|
-
|
|
259
|
+
// 加密前先抹掉 source map 引用:内联 sourceMappingURL 里往往直接嵌着前端原始源码,
|
|
260
|
+
// 容器解密即还原,等于给 high 模式留了个明文后门。
|
|
261
|
+
const { html: secureHtml, stripped } = stripSourceMapRefs(html);
|
|
262
|
+
if (stripped > 0) {
|
|
263
|
+
process.stdout.write(`[freedom] high 模式:已抹去前端页内 ${stripped} 处 source map 引用(可能含原始源码)。\n`);
|
|
264
|
+
}
|
|
265
|
+
const appBin = encryptApp(name, secureHtml, configJSON, backendFiles); await fsp.writeFile(path.join(resDir, 'app.bin'), appBin);
|
|
266
|
+
await fsp.writeFile(path.join(resDir, '.integrity'), renderIntegrity(buildIntegrity(name, appBin)), 'utf8');
|
|
244
267
|
} else {
|
|
245
268
|
for (const legacy of ['app.bin', '.integrity']) {
|
|
246
269
|
const p = path.join(resDir, legacy);
|
|
@@ -256,10 +279,14 @@ async function emitPlatform({ plat, name, version, targetDir, html, configJSON,
|
|
|
256
279
|
);
|
|
257
280
|
}
|
|
258
281
|
}
|
|
259
|
-
if (hasBackend) {
|
|
282
|
+
if (hasBackend && security !== 'high') {
|
|
260
283
|
copyDir(backendDir, path.join(resDir, 'backend'));
|
|
261
284
|
}
|
|
262
285
|
|
|
286
|
+
// --installer:便携 zip + Windows NSIS 安装器。先于 mac 的 .app 生成,
|
|
287
|
+
// 使 zip 内容只含「exe + resources」,不把 .app 与 .app.zip 二次打包进去。
|
|
288
|
+
const installers = installer ? await buildInstaller({ plat, name, version, targetDir, exeName }) : [];
|
|
289
|
+
|
|
263
290
|
// macOS:额外生成 .app bundle + .app.zip(供 mac 用户解压即用)
|
|
264
291
|
// 壳加载 exe 同目录 resources/,故把 resources 放进 Contents/MacOS/ 即可运行,无需改壳。
|
|
265
292
|
if (isMacPlat(plat)) {
|
|
@@ -269,7 +296,7 @@ async function emitPlatform({ plat, name, version, targetDir, html, configJSON,
|
|
|
269
296
|
);
|
|
270
297
|
}
|
|
271
298
|
|
|
272
|
-
return outFile;
|
|
299
|
+
return { outFile, installers };
|
|
273
300
|
}
|
|
274
301
|
|
|
275
302
|
// 把 .ico 图标注入到 Windows exe 的 PE 资源(RT_ICON + RT_GROUP_ICON),
|
|
@@ -290,7 +317,16 @@ async function applyWindowsIcon(exePath, iconPath, name, version) {
|
|
|
290
317
|
);
|
|
291
318
|
return;
|
|
292
319
|
}
|
|
293
|
-
|
|
320
|
+
let rcedit;
|
|
321
|
+
try {
|
|
322
|
+
rcedit = require('rcedit'); // 惰性加载,避免未配置 icon 时增加启动开销
|
|
323
|
+
} catch (e) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
'exe 图标注入需要依赖 rcedit,但未能加载(npm 安装不完整)。'
|
|
326
|
+
+ '请在 freedom-cli 安装目录执行 npm install 后重试;'
|
|
327
|
+
+ '或临时移除 freedom.config.js 的 icon 配置以跳过注入。'
|
|
328
|
+
);
|
|
329
|
+
}
|
|
294
330
|
const info = { icon: iconPath };
|
|
295
331
|
// rcedit 4.x 参数结构:FileDescription/ProductName 等字符串必须走
|
|
296
332
|
// version-string(--set-version-string 键值对),file-version/product-version
|
|
@@ -402,21 +438,25 @@ ${iconEntry} <key>CFBundleExecutable</key>
|
|
|
402
438
|
}
|
|
403
439
|
|
|
404
440
|
// 把目录压成 .zip:跨平台零额外依赖(历史 bug B27)
|
|
405
|
-
// - Windows
|
|
406
|
-
//
|
|
407
|
-
//
|
|
441
|
+
// - Windows:bsdtar 的 `-a` 按扩展名自动选 zip 压缩器,可用;但必须绝对路径锁定系统
|
|
442
|
+
// tar.exe —— Git Bash / MSYS 会把 GNU tar 塞进 PATH 前面,GNU tar 不认 `-a` 且把
|
|
443
|
+
// `D:\...` 当远程主机(`Cannot connect to D: resolve failed`)。
|
|
444
|
+
// - Linux/macOS:GNU tar 的 `-a` 不支持 .zip,改用系统 zip 命令(缺失时给出安装提示)。
|
|
408
445
|
function zipDir(zipPath, dir) {
|
|
409
446
|
const parent = path.dirname(dir);
|
|
410
447
|
const base = path.basename(dir);
|
|
411
448
|
if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
|
|
412
|
-
let
|
|
449
|
+
let cmd = 'zip';
|
|
450
|
+
let args = ['-r', '-q', zipPath, base];
|
|
413
451
|
if (process.platform === 'win32') {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
452
|
+
const sysRoot = process.env.SystemRoot || 'C:\\Windows';
|
|
453
|
+
const bsdtar = path.join(sysRoot, 'System32', 'tar.exe');
|
|
454
|
+
cmd = fs.existsSync(bsdtar) ? bsdtar : 'tar';
|
|
455
|
+
args = ['-a', '-c', '-f', zipPath, base];
|
|
417
456
|
}
|
|
457
|
+
const res = spawnSync(cmd, args, { cwd: parent, encoding: 'utf8', windowsHide: true });
|
|
418
458
|
if (res.error || res.status !== 0) {
|
|
419
|
-
const detail = (res.stderr || res.stdout ||
|
|
459
|
+
const detail = (res.stderr || res.stdout || (res.error && res.error.message) || '').trim();
|
|
420
460
|
const hint = process.platform !== 'win32'
|
|
421
461
|
? '\n(Linux/macOS 打包 zip 需要 zip 命令:Ubuntu: sudo apt install zip / macOS: brew install zip)'
|
|
422
462
|
: '';
|
|
@@ -424,6 +464,65 @@ function zipDir(zipPath, dir) {
|
|
|
424
464
|
}
|
|
425
465
|
}
|
|
426
466
|
|
|
467
|
+
// --installer 产物:便携 zip 恒产出;Windows 额外填充 NSIS 模板,本机有 makensis
|
|
468
|
+
// 时直接编译 setup.exe,无则产出已填充的 .nsi(缺 makensis 属环境能力而非代码缺陷)。
|
|
469
|
+
// 顺序即排除策略:先压 zip 再写 .nsi/setup.exe,免维护"排除安装器中间物"清单。
|
|
470
|
+
async function buildInstaller({ plat, name, version, targetDir, exeName }) {
|
|
471
|
+
const artifacts = [];
|
|
472
|
+
|
|
473
|
+
const zipPath = path.join(targetDir, `${name}-${plat}-portable.zip`);
|
|
474
|
+
// 中间 zip 必须带平台后缀:多平台打包是并发的(emitPlatform 跑在 Promise.all 里),
|
|
475
|
+
// 而各 plat 的 targetDir 同级,只按 pid 命名会让 A 平台的 zipDir 覆写 B 平台正在 rename 的
|
|
476
|
+
// 临时文件——结果是 A 的 portable.zip 里装着 B 的二进制(静默错平台产物)或 ENOENT 崩在成功提示之后。
|
|
477
|
+
const tmpPath = path.join(path.dirname(targetDir), `.freedom-portable-${plat}-${process.pid}.zip`);
|
|
478
|
+
zipDir(tmpPath, targetDir);
|
|
479
|
+
await fsp.rename(tmpPath, zipPath);
|
|
480
|
+
artifacts.push(zipPath);
|
|
481
|
+
|
|
482
|
+
if (!isWinPlat(plat)) return artifacts;
|
|
483
|
+
|
|
484
|
+
const tplPath = path.join(__dirname, '..', 'templates', 'installer', 'app.nsi');
|
|
485
|
+
if (!fs.existsSync(tplPath)) {
|
|
486
|
+
process.stdout.write(`[freedom] 提示:缺少安装包模板 ${tplPath},已跳过 NSIS。\n`);
|
|
487
|
+
return artifacts;
|
|
488
|
+
}
|
|
489
|
+
const fill = (s) =>
|
|
490
|
+
s.replace(/@NAME@/g, name)
|
|
491
|
+
.replace(/@VERSION@/g, version)
|
|
492
|
+
.replace(/@SRC@/g, targetDir)
|
|
493
|
+
.replace(/@OUT@/g, targetDir)
|
|
494
|
+
.replace(/@EXE@/g, exeName);
|
|
495
|
+
// NSIS 3 对无 BOM 的 UTF-8 脚本按代码页解释,模板含中文注释与 SimpChinese 文案,
|
|
496
|
+
// 故补 BOM(与 build.ps1 的 [Text.Encoding]::UTF8 写出行为一致)。
|
|
497
|
+
const body = fill(fs.readFileSync(tplPath, 'utf8'));
|
|
498
|
+
const nsiPath = path.join(targetDir, `${name}-setup-${version}.nsi`);
|
|
499
|
+
await fsp.writeFile(nsiPath, Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(body, 'utf8')]));
|
|
500
|
+
artifacts.push(nsiPath);
|
|
501
|
+
|
|
502
|
+
if (!hasTool('makensis')) {
|
|
503
|
+
process.stdout.write(
|
|
504
|
+
'[freedom] 未检测到 makensis(NSIS):已产出填充好的 .nsi,在装有 NSIS 的机器上执行\n' +
|
|
505
|
+
` makensis "${nsiPath}" 即可编译安装器。\n`
|
|
506
|
+
);
|
|
507
|
+
return artifacts;
|
|
508
|
+
}
|
|
509
|
+
const res = spawnSync('makensis', ['-V2', nsiPath], { encoding: 'utf8' });
|
|
510
|
+
if (res.status !== 0) {
|
|
511
|
+
const detail = (res.stderr || res.stdout || '').trim().slice(0, 600);
|
|
512
|
+
throw new Error(`makensis 编译失败(退出码 ${res.status}):${detail}`);
|
|
513
|
+
}
|
|
514
|
+
const setupPath = path.join(targetDir, `${name}-setup-${version}.exe`);
|
|
515
|
+
process.stdout.write(`[freedom] NSIS 安装器已生成:${setupPath}\n`);
|
|
516
|
+
artifacts.push(setupPath);
|
|
517
|
+
return artifacts;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 外部命令可用性探测(不影响主流程,仅决定是否尝试调用)。
|
|
521
|
+
function hasTool(cmd) {
|
|
522
|
+
const res = spawnSync(cmd, ['-VERSION'], { encoding: 'utf8', windowsHide: true });
|
|
523
|
+
return !res.error;
|
|
524
|
+
}
|
|
525
|
+
|
|
427
526
|
// 从项目 package.json 取版本号(cfg.version 优先)
|
|
428
527
|
function appVersionFromPkg(dir) {
|
|
429
528
|
try {
|
|
@@ -435,9 +534,12 @@ function appVersionFromPkg(dir) {
|
|
|
435
534
|
}
|
|
436
535
|
|
|
437
536
|
// 渲染 resources/config.json(与壳 resources.go 的 runtimeConfigFile 字段对齐)
|
|
438
|
-
function renderConfigJSON(cfg, name) {
|
|
537
|
+
function renderConfigJSON(cfg, name, version) {
|
|
439
538
|
const obj = {
|
|
440
539
|
name,
|
|
540
|
+
// 应用自己的版本必须随产物落盘:通用壳编译期注入的 freedom.Version 是「壳」的版本域,
|
|
541
|
+
// 自更新拿它跟应用清单比,结果是「永远 uncomparable」或「下完更新还是同一个壳」。
|
|
542
|
+
version: version || undefined,
|
|
441
543
|
title: cfg.title || cfg.name || name,
|
|
442
544
|
titlebar: cfg.titlebar || 'frameless',
|
|
443
545
|
width: intVal(cfg.width, 1024),
|
|
@@ -456,6 +558,25 @@ function renderConfigJSON(cfg, name) {
|
|
|
456
558
|
args: Array.isArray(cfg.backend.args) ? cfg.backend.args : [],
|
|
457
559
|
};
|
|
458
560
|
}
|
|
561
|
+
// 能力透传(与壳 resources.go 对齐):URL 直载 / 单实例 / 自动更新。
|
|
562
|
+
if (typeof cfg.url === 'string' && cfg.url) obj.url = cfg.url;
|
|
563
|
+
if (cfg.singleInstance === true) obj.singleInstance = true;
|
|
564
|
+
if (cfg.updater && typeof cfg.updater.manifestURL === 'string' && typeof cfg.updater.publicKey === 'string'
|
|
565
|
+
&& cfg.updater.manifestURL && cfg.updater.publicKey) {
|
|
566
|
+
obj.updater = {
|
|
567
|
+
manifestURL: cfg.updater.manifestURL,
|
|
568
|
+
publicKey: cfg.updater.publicKey,
|
|
569
|
+
requireSignature: cfg.updater.requireSignature === true ? true : undefined,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
// 能力收口透传:壳在 Config.Capabilities 未编译设置时才采用这里的白/黑名单
|
|
573
|
+
// (capability.go 在 sys/tray/window 三桥派发前判定,拒绝零副作用)。
|
|
574
|
+
const caps = cfg.capabilities;
|
|
575
|
+
if (caps && (Array.isArray(caps.allow) || Array.isArray(caps.deny))) {
|
|
576
|
+
const allow = (caps.allow || []).filter((s) => typeof s === 'string' && s);
|
|
577
|
+
const deny = (caps.deny || []).filter((s) => typeof s === 'string' && s);
|
|
578
|
+
if (allow.length || deny.length) obj.capabilities = { allow, deny };
|
|
579
|
+
}
|
|
459
580
|
return JSON.stringify(obj, null, 2);
|
|
460
581
|
}
|
|
461
582
|
|
|
@@ -579,7 +700,7 @@ function warnIfNotSingleFile(html, distHtml) {
|
|
|
579
700
|
}
|
|
580
701
|
}
|
|
581
702
|
|
|
582
|
-
// 递归收集目录内所有文件:relPath(POSIX 相对路径)-> Buffer 内容,供
|
|
703
|
+
// 递归收集目录内所有文件:relPath(POSIX 相对路径)-> Buffer 内容,供 high 模式入容器。
|
|
583
704
|
function collectDirFiles(dir, prefix, out) {
|
|
584
705
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
585
706
|
const abs = path.join(dir, entry.name);
|
|
@@ -592,4 +713,27 @@ function collectDirFiles(dir, prefix, out) {
|
|
|
592
713
|
return out;
|
|
593
714
|
}
|
|
594
715
|
|
|
595
|
-
|
|
716
|
+
// 文件权限位(POSIX mode 低 12 位):进容器后由壳在临时目录还原(执行位决定后端能否直接跑)。
|
|
717
|
+
function fileMode(abs) {
|
|
718
|
+
try {
|
|
719
|
+
return fs.statSync(abs).mode & 0o7777;
|
|
720
|
+
} catch (e) {
|
|
721
|
+
return 0o644;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// 抹掉前端页内的 source map 引用(JS 的 //# sourceMappingURL=…、CSS 的 /*# … */)。
|
|
726
|
+
// 返回 { html, stripped }。
|
|
727
|
+
function stripSourceMapRefs(html) {
|
|
728
|
+
let stripped = 0;
|
|
729
|
+
const out = String(html).replace(
|
|
730
|
+
/\/\/[#@]\s*sourceMappingURL=[^\r\n]*|\/\*#\s*sourceMappingURL=[\s\S]*?\*\//g,
|
|
731
|
+
() => {
|
|
732
|
+
stripped += 1;
|
|
733
|
+
return '';
|
|
734
|
+
},
|
|
735
|
+
);
|
|
736
|
+
return { html: out, stripped };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
module.exports = { build, parsePlatforms, renderConfigJSON };
|