@yufengtadian/freedom-cli 1.12.17 → 1.13.0

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.
Files changed (81) hide show
  1. package/lib/build.js +74 -9
  2. package/lib/cli.js +61 -3
  3. package/lib/init.js +9 -1
  4. package/lib/security.js +2 -2
  5. package/lib/utils.js +5 -2
  6. package/lib/verify.js +236 -0
  7. package/package.json +43 -39
  8. package/shell/darwin-arm64/freedom-shell +0 -0
  9. package/shell/linux-x64/freedom-shell +0 -0
  10. package/shell/win-x64/freedom-shell.exe +0 -0
  11. package/templates/go/go.mod +1 -7
  12. package/templates/go/go.sum +2 -2
  13. package/templates/go/main.go +22 -8
  14. package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
  15. package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
  16. package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
  17. package/templates/go/pkg/freedom/assets/freedom.js +219 -21
  18. package/templates/go/pkg/freedom/assets_embed.go +11 -13
  19. package/templates/go/pkg/freedom/authenticode_other.go +11 -0
  20. package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
  21. package/templates/go/pkg/freedom/backend_proc.go +295 -51
  22. package/templates/go/pkg/freedom/bridge.go +13 -2
  23. package/templates/go/pkg/freedom/capability.go +77 -0
  24. package/templates/go/pkg/freedom/center_other.go +3 -0
  25. package/templates/go/pkg/freedom/center_windows.go +64 -36
  26. package/templates/go/pkg/freedom/dispatch.go +60 -0
  27. package/templates/go/pkg/freedom/events_windows.go +319 -0
  28. package/templates/go/pkg/freedom/freedom.go +242 -92
  29. package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
  30. package/templates/go/pkg/freedom/osver_other.go +15 -0
  31. package/templates/go/pkg/freedom/osver_windows.go +121 -0
  32. package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
  33. package/templates/go/pkg/freedom/{configfile.go → resources.go} +56 -32
  34. package/templates/go/pkg/freedom/security.go +19 -12
  35. package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
  36. package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
  37. package/templates/go/pkg/freedom/spawn_other.go +11 -0
  38. package/templates/go/pkg/freedom/spawn_windows.go +23 -0
  39. package/templates/go/pkg/freedom/store.go +458 -0
  40. package/templates/go/pkg/freedom/syscap_linux.go +278 -0
  41. package/templates/go/pkg/freedom/syscap_other.go +22 -0
  42. package/templates/go/pkg/freedom/syscap_windows.go +845 -0
  43. package/templates/go/pkg/freedom/sysint_common.go +70 -0
  44. package/templates/go/pkg/freedom/sysint_windows.go +426 -0
  45. package/templates/go/pkg/freedom/tray_common.go +12 -0
  46. package/templates/go/pkg/freedom/tray_linux.go +302 -0
  47. package/templates/go/pkg/freedom/tray_other.go +10 -0
  48. package/templates/go/pkg/freedom/tray_windows.go +536 -0
  49. package/templates/go/pkg/freedom/updater.go +372 -0
  50. package/templates/go/pkg/freedom/window_mgr.go +385 -0
  51. package/templates/go/pkg/freedom/window_other.go +32 -66
  52. package/templates/go/pkg/freedom/window_windows.go +406 -251
  53. package/templates/project-minimal/freedom.config.js +58 -0
  54. package/templates/project-minimal/index.html +41 -0
  55. package/templates/project-minimal/package.json +14 -0
  56. package/templates/project-minimal/src/main.js +19 -0
  57. package/templates/project-minimal/vite.config.js +18 -0
  58. package/templates/go/gen_config.go +0 -22
  59. package/templates/go/pkg/freedom/security_test.go +0 -153
  60. package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
  61. package/templates/go/webview_go/CHANGELOG.md +0 -15
  62. package/templates/go/webview_go/LICENSE +0 -22
  63. package/templates/go/webview_go/README.md +0 -50
  64. package/templates/go/webview_go/examples/basic/main.go +0 -12
  65. package/templates/go/webview_go/examples/bind/main.go +0 -38
  66. package/templates/go/webview_go/glue.c +0 -36
  67. package/templates/go/webview_go/go.mod +0 -3
  68. package/templates/go/webview_go/go.sum +0 -0
  69. package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
  70. package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
  71. package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
  72. package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
  73. package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
  74. package/templates/go/webview_go/libs/webview/LICENSE +0 -22
  75. package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
  76. package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
  77. package/templates/go/webview_go/libs/webview/vendor.go +0 -2
  78. package/templates/go/webview_go/libs/webview/version.txt +0 -1
  79. package/templates/go/webview_go/webview.cc +0 -1
  80. package/templates/go/webview_go/webview.go +0 -481
  81. package/templates/go/webview_go/webview_test.go +0 -50
package/lib/build.js CHANGED
@@ -112,9 +112,13 @@ async function build(projectDir, opts = {}) {
112
112
  if (useCache && !viteNeedsRebuild(dir)) {
113
113
  process.stdout.write('[freedom] 前端源码无改动,复用构建缓存...\n');
114
114
  } else {
115
- const vite = run('npm', ['run', 'build'], { cwd: dir });
115
+ process.stdout.write('[freedom] 前端打包中(npm run build)...\n');
116
+ // 全量构建时 npm/vite 输出必须实时透传(stdio: inherit),
117
+ // 此前 pipe 缓冲会吞掉构建全程输出,Node 主线程被 spawnSync 同步阻塞,
118
+ // 大前端全量构建时终端长时间零输出,表现如"未响应/卡死"。
119
+ const vite = run('npm', ['run', 'build'], { cwd: dir, stdio: 'inherit' });
116
120
  if (vite.status !== 0) {
117
- throw new Error(`前端打包失败:\n${vite.stdout}\n${vite.stderr}`);
121
+ throw new Error(`前端打包失败(退出码 ${vite.status}),详见上方构建输出。`);
118
122
  }
119
123
  writeViteCacheMarker(dir);
120
124
  }
@@ -150,6 +154,33 @@ async function build(projectDir, opts = {}) {
150
154
  })
151
155
  );
152
156
 
157
+ // 5) 产物自检 + 形态树(消除"build 后形态未知 / 需实测"):
158
+ // build 完成后自动校验产物完整性,并打印产物结构;自检发现问题仅告警不阻断,
159
+ // 完整失败信息可用 freedom verify 复核(返回非零退出码,供 CI 使用)。
160
+ const { verifyProduct, renderTree, formatChecks } = require('./verify');
161
+ const v = await verifyProduct(dir);
162
+ if (v.error) {
163
+ process.stdout.write(`[freedom] 产物自检跳过:${v.error}\n`);
164
+ } else if (v.targets.length === 0) {
165
+ process.stdout.write('[freedom] 产物自检跳过:未在产物目录发现可执行文件。\n');
166
+ } else {
167
+ process.stdout.write('\n[freedom] 产物自检:\n');
168
+ for (const t of v.targets) {
169
+ process.stdout.write(` [${t.plat}] ${t.dir}\n`);
170
+ for (const line of formatChecks(t.checks)) {
171
+ process.stdout.write(` ${line}\n`);
172
+ }
173
+ }
174
+ process.stdout.write('\n[freedom] 产物结构:\n');
175
+ for (const line of renderTree(v.targets).split('\n')) {
176
+ process.stdout.write(` ${line}\n`);
177
+ }
178
+ const failed = v.targets.some((t) => t.checks.some((c) => !c.pass));
179
+ if (failed) {
180
+ process.stdout.write(`\n[freedom] 警告:产物自检存在失败项,请运行 ${'freedom verify'} 复核。\n`);
181
+ }
182
+ }
183
+
153
184
  return { results };
154
185
  }
155
186
 
@@ -184,8 +215,10 @@ async function emitPlatform({ plat, name, version, targetDir, html, configJSON,
184
215
  }
185
216
 
186
217
  // 自定义 exe 图标(仅 Windows PE 支持嵌入 .ico 资源;mac 用 .icns 走 .app 分支)
218
+ // 修复:此前引用未传入 emitPlatform 作用域的 cfg 导致 ReferenceError(cfg is not defined),
219
+ // 配置 icon 后 Windows 构建必然失败;改用 emitPlatform 已解构的 name/version。
187
220
  if (isWinPlat(plat) && icon) {
188
- await applyWindowsIcon(outFile, icon);
221
+ await applyWindowsIcon(outFile, icon, name, version);
189
222
  }
190
223
 
191
224
  // 写 resources:页面 + 配置 + 后端
@@ -239,9 +272,11 @@ async function emitPlatform({ plat, name, version, targetDir, html, configJSON,
239
272
  return outFile;
240
273
  }
241
274
 
242
- // 把 .ico 图标注入到 Windows exe 的 PE 资源(RT_ICON + RT_GROUP_ICON)。
275
+ // 把 .ico 图标注入到 Windows exe 的 PE 资源(RT_ICON + RT_GROUP_ICON),
276
+ // 并设置版本信息(产品名 / 说明 / 版本),使资源管理器详情可读。
243
277
  // 依赖 rcedit(随 npm 包分发的预编译 rcedit-x64.exe,无需 Go/资源编译器)。
244
- async function applyWindowsIcon(exePath, iconPath) {
278
+ // 产品名取自 cfg.name,版本取自 cfg.version —— 修复"产物无产品名 / 版本无法配置"。
279
+ async function applyWindowsIcon(exePath, iconPath, name, version) {
245
280
  const ext = path.extname(iconPath).toLowerCase();
246
281
  if (ext !== '.ico') {
247
282
  process.stdout.write(
@@ -256,8 +291,24 @@ async function applyWindowsIcon(exePath, iconPath) {
256
291
  return;
257
292
  }
258
293
  const rcedit = require('rcedit'); // 惰性加载,避免未配置 icon 时增加启动开销
259
- await rcedit(exePath, { icon: iconPath });
260
- process.stdout.write(`[freedom] 已注入 exe 图标:${iconPath}\n`);
294
+ const info = { icon: iconPath };
295
+ // rcedit 4.x 参数结构:FileDescription/ProductName 等字符串必须走
296
+ // version-string(--set-version-string 键值对),file-version/product-version
297
+ // 才是独立的 --set-* 单值参数。旧版平铺的 file-description/product-name 会被静默忽略。
298
+ const versionString = {};
299
+ if (name) {
300
+ versionString['FileDescription'] = name;
301
+ versionString['ProductName'] = name;
302
+ }
303
+ if (Object.keys(versionString).length > 0) {
304
+ info['version-string'] = versionString;
305
+ }
306
+ if (version) {
307
+ info['file-version'] = version;
308
+ info['product-version'] = version;
309
+ }
310
+ await rcedit(exePath, info);
311
+ process.stdout.write(`[freedom] 已注入 exe 图标与版本信息:${iconPath} (${name || '?'} ${version || '?'})\n`);
261
312
  }
262
313
 
263
314
  // 解析 cfg.icon:相对项目根或绝对路径 -> 绝对路径;未配置返回 null。
@@ -383,7 +434,7 @@ function appVersionFromPkg(dir) {
383
434
  }
384
435
  }
385
436
 
386
- // 渲染 resources/config.json(与壳 configfile.go 的 runtimeConfigFile 字段对齐)
437
+ // 渲染 resources/config.json(与壳 resources.go 的 runtimeConfigFile 字段对齐)
387
438
  function renderConfigJSON(cfg, name) {
388
439
  const obj = {
389
440
  name,
@@ -497,7 +548,11 @@ function intVal(v, dft) {
497
548
  // 导致页面 JS/CSS 丢失静默空白。检测到此类引用时明确告警,避免用户无感知翻车。
498
549
  function warnIfNotSingleFile(html, distHtml) {
499
550
  const refs = [];
500
- const re = /<(?:script|link)\b[^>]*(?:src|href)\s*=\s*["']([^"']+)["']/gi;
551
+ // M4:此前只检查 script/link,漏掉了 img/iframe/video/audio/source/object/embed/track 等
552
+ // 一切带 URL 引用的标签。壳只 SetHtml 单页内存加载,任何相对路径 / 根路径 / 协议相对的
553
+ // 外部引用(JS、CSS、图片、子页面、音视频、内嵌对象)在无服务器环境下都会失效,
554
+ // 需一并告警,避免"脚本内联了但图片仍空白"这类半翻车。
555
+ const re = /<(?:script|link|img|iframe|video|audio|source|object|embed|track)\b[^>]*(?:src|href|data)\s*=\s*["']([^"']+)["']/gi;
501
556
  let m;
502
557
  while ((m = re.exec(html)) !== null) {
503
558
  const url = m[1];
@@ -505,6 +560,16 @@ function warnIfNotSingleFile(html, distHtml) {
505
560
  if (/^(?:data:|blob:|https?:|file:)/i.test(url)) continue;
506
561
  refs.push(url);
507
562
  }
563
+ // img 的 srcset(逗号分隔多候选 URL)单独扫描
564
+ const srcsetRe = /<img\b[^>]*\bsrcset\s*=\s*["']([^"']+)["']/gi;
565
+ while ((m = srcsetRe.exec(html)) !== null) {
566
+ for (const part of m[1].split(',')) {
567
+ const url = part.trim().split(/\s+/)[0];
568
+ if (!url) continue;
569
+ if (/^(?:data:|blob:|https?:|file:)/i.test(url)) continue;
570
+ refs.push(url);
571
+ }
572
+ }
508
573
  if (refs.length > 0) {
509
574
  console.warn(
510
575
  `[freedom] 警告:${path.basename(distHtml)} 引用了外部资源(${refs.join(', ')})。` +
package/lib/cli.js CHANGED
@@ -22,12 +22,13 @@ function help() {
22
22
  L.push(` ${paint('freedom', C.fg.cyan, C.bold)} ${dim('进入完整 CLI 模式(交互式菜单,新建 / 打包 / 配置 / 壳管理)')}`);
23
23
  L.push(` ${paint('freedom tui', C.fg.cyan, C.bold)} ${dim('同上(兼容写法,显式进入交互式模式)')}`);
24
24
  L.push(section('项目'));
25
- L.push(` ${paint('freedom init [目录] [--force]', C.fg.cyan)} ${dim('在当前 / 指定目录新建项目模板')}`);
25
+ L.push(` ${paint('freedom init [目录] [--force] [--template full|minimal]', C.fg.cyan)} ${dim('新建项目模板(默认 full:含自绘标题栏示例;minimal:极简无自绘标题栏)')}`);
26
26
  L.push(` ${paint('freedom tutorial', C.fg.cyan)} ${dim('再次打开安装教程')}`);
27
27
  L.push(section('打包'));
28
28
  L.push(` ${paint('freedom build [--platform <p>]', C.fg.cyan)} ${dim('前端打包并分发桌面应用(默认当前平台)')}`);
29
29
  L.push(` ${dim('--platform win|mac|linux|all')} ${dim('指定目标平台(all = 三平台全量)')}`);
30
30
  L.push(` ${dim('--no-cache')} ${dim('忽略前端构建缓存,强制重新打包')}`);
31
+ L.push(` ${paint('freedom verify [--platform <p>]', C.fg.cyan)} ${dim('校验已构建产物完整性并打印产物结构(CI 可依赖退出码)')}`);
31
32
  L.push(` ${paint('freedom dmg [--platform <plat>]', C.fg.cyan)} ${dim('将已构建的 .app 打包为 .dmg(需 macOS)')}`);
32
33
  L.push(section('外观'));
33
34
  L.push(` ${paint('freedom titlebar <native|frameless>', C.fg.cyan)} ${dim('一键切换标题栏策略')}`);
@@ -113,9 +114,25 @@ async function run(argv) {
113
114
 
114
115
  case 'init': {
115
116
  const force = rest.includes('--force');
116
- const dirArg = rest.filter((a) => a !== '--force')[0];
117
- const dir = init(dirArg || '.', { force });
117
+ const tmplArg = rest.find((a) => a.startsWith('--template'));
118
+ let template;
119
+ if (tmplArg) {
120
+ template = tmplArg.includes('=') ? tmplArg.split('=')[1] : rest[rest.indexOf(tmplArg) + 1];
121
+ if (!template || template.startsWith('--')) {
122
+ console.error(`${err('--template 缺少取值。')} ${dim('用法:')}${paint('freedom init <目录> --template <full|minimal>', C.fg.cyan)}`);
123
+ return 1;
124
+ }
125
+ }
126
+ const dirArg = rest.filter((a) => a !== '--force' && !a.startsWith('--template'))[0];
127
+ let dir;
128
+ try {
129
+ dir = init(dirArg || '.', { force, template });
130
+ } catch (e) {
131
+ console.error(`${err(e.message)}`);
132
+ return 1;
133
+ }
118
134
  console.log(`${ok('项目已创建:')}${paint(dir, C.fg.cyan, C.bold)}`);
135
+ console.log(` ${dim(`模板:${template === 'minimal' ? 'minimal(极简,无自绘标题栏)' : 'full(完整,含自绘标题栏示例)'}`)}`);
119
136
  console.log(` ${dim('下一步:')}`);
120
137
  console.log(` ${paint(`cd ${dir}`, C.fg.white)}`);
121
138
  console.log(` ${paint('npm install', C.fg.white)}`);
@@ -161,6 +178,47 @@ async function run(argv) {
161
178
  return 0;
162
179
  }
163
180
 
181
+ case 'verify': {
182
+ const platArg = rest.find((a) => a.startsWith('--platform') || a.startsWith('-p'));
183
+ let platform;
184
+ if (platArg) {
185
+ const inline = platArg.includes('=') ? platArg.split('=')[1] : undefined;
186
+ platform = inline || rest[rest.indexOf(platArg) + 1];
187
+ if (!platform || platform.startsWith('--')) {
188
+ console.error(`${err('--platform 缺少取值。')} ${dim('用法:')}${paint('freedom verify [--platform <p>]', C.fg.cyan)}`);
189
+ return 1;
190
+ }
191
+ }
192
+ const { verifyProduct, renderTree, formatChecks } = require('./verify');
193
+ const v = await verifyProduct(process.cwd(), { platform });
194
+ if (v.error) {
195
+ console.error(`${err('校验失败:')}${v.error}`);
196
+ return 1;
197
+ }
198
+ if (v.targets.length === 0) {
199
+ console.error(`${err('未在产物目录发现可执行文件。')} ${dim('请先运行')} ${paint('freedom build', C.fg.cyan)} ${dim('生成产物。')}`);
200
+ return 1;
201
+ }
202
+ let allPass = true;
203
+ for (const t of v.targets) {
204
+ console.log(`\n${section(`[${t.plat}] ${t.dir}`)}`);
205
+ for (const c of t.checks) {
206
+ console.log(` ${c.pass ? ok('通过') : err('失败')} ${c.name}:${c.detail}`);
207
+ }
208
+ allPass = allPass && t.checks.every((c) => c.pass);
209
+ }
210
+ console.log(`\n${dim('产物结构:')}`);
211
+ for (const line of renderTree(v.targets).split('\n')) {
212
+ console.log(` ${line}`);
213
+ }
214
+ if (allPass) {
215
+ console.log(`\n${ok('产物校验全部通过。')}`);
216
+ return 0;
217
+ }
218
+ console.log(`\n${err('产物校验存在失败项,请检查上方输出。')}`);
219
+ return 1;
220
+ }
221
+
164
222
  case 'shell':
165
223
  return await runShell(rest);
166
224
 
package/lib/init.js CHANGED
@@ -12,8 +12,16 @@ function init(targetDir, opts = {}) {
12
12
  throw new Error(`目标目录 ${abs} 非空,请使用空目录,或加 --force 覆盖。`);
13
13
  }
14
14
 
15
+ // 模板选择:'minimal' 极简(无自绘标题栏 / 无演示内容,默认 native 标题栏);默认完整模板。
16
+ // 非法值快速失败,避免静默回退成默认模板让用户误以为指定成功。
17
+ const template = opts.template || 'full';
18
+ const templateName = template === 'minimal' ? 'project-minimal' : 'project';
19
+ if (template !== 'full' && template !== 'minimal') {
20
+ throw new Error(`未知模板:${template}。可选:full(完整模板,含自绘标题栏示例)/ minimal(极简模板,无自绘标题栏)。`);
21
+ }
22
+
15
23
  fs.mkdirSync(abs, { recursive: true });
16
- copyDir(projectTemplateDir(), abs);
24
+ copyDir(projectTemplateDir(templateName), abs);
17
25
 
18
26
  // 项目名替换到 package.json 与 freedom.config.js
19
27
  const name = opts.name || path.basename(abs);
package/lib/security.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // 安全加固模块(v1.13.0)
3
+ // 安全加固模块(v1.13.10)
4
4
  //
5
5
  // 为 freedom build 提供两种安全模式:
6
6
  // - basic(简单模式):资源压缩 + 壳剥离符号建议,阻止"随手打开 resources/
@@ -16,7 +16,7 @@
16
16
  // 提取主密钥并复现派生算法"。真正"每应用唯一密钥"需要每应用独立编译壳
17
17
  // (破坏通用壳 + 零工具链架构),不在本模式范围内。
18
18
  //
19
- // ⚠️ 跨语言同步点:本文件与 templates/go/pkg/freedom/security.go 必须保持
19
+ // ⚠️ 跨语言同步点:本文件与 templates/go/pkg/freedom/security.go(= 仓库根 security.go)必须保持
20
20
  // MASTER_KEY / DERIVE_SALT / PBKDF2_ITER / KEY_LEN / APP_BIN_MAGIC
21
21
  // 以及加密算法(aes-256-ctr + hmac-sha256)完全一致。
22
22
  // 修改任一侧必须同步另一侧,否则 build 出的加密资源壳无法解密。
package/lib/utils.js CHANGED
@@ -13,8 +13,11 @@ function templateDir() {
13
13
  return path.join(PKG_ROOT, 'templates');
14
14
  }
15
15
 
16
- function projectTemplateDir() {
17
- return path.join(templateDir(), 'project');
16
+ // 项目模板目录:name 为 templates/ 下的子目录名。
17
+ // 'project' 完整模板(默认):含自绘标题栏(frameless)示例、右键菜单、桥接演示;
18
+ // 'project-minimal' 极简模板:无自绘标题栏 / 无演示内容,默认 native 标题栏,开箱即用。
19
+ function projectTemplateDir(name = 'project') {
20
+ return path.join(templateDir(), name);
18
21
  }
19
22
 
20
23
  function goTemplateDir() {
package/lib/verify.js ADDED
@@ -0,0 +1,236 @@
1
+ 'use strict';
2
+
3
+ // freedom verify:产物完整性自检 + 产物形态树。
4
+ // 供两个场景复用:
5
+ // 1. freedom build 完成后自动调用(发现问题输出告警,但不阻断正常构建流程);
6
+ // 2. freedom verify 命令独立运行(发现问题返回非零退出码,供 CI / 发布前检查)。
7
+ // 消除"build 后形态未知 / 需手动实测":自检覆盖可执行文件、resources、安全模式资源形态、
8
+ // 运行时配置一致性、后端目录、Windows PE 头与 high 模式 app.bin 容器头。
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { isWinPlat, platformExeName } = require('./utils');
13
+
14
+ // ---- 产物定位 ----
15
+
16
+ // 依据项目配置探测 outDir 下的产物目标。
17
+ // 布局:
18
+ // 单平台:outDir/<app>.exe(win)/ outDir/<app>(unix)
19
+ // 多平台:outDir/<plat>/<app>.exe 或 outDir/<plat>/<app>
20
+ // 返回 [{ plat, dir, outFile }]。
21
+ function findProducts(projectDir, cfg) {
22
+ const name = (cfg.name || 'freedom-app').replace(/[^a-zA-Z0-9_.-]/g, '-');
23
+ const outDir = String(cfg.outDir || 'dist').trim() || 'dist';
24
+ const outDirPath = path.resolve(projectDir, outDir);
25
+ if (!fs.existsSync(outDirPath)) return [];
26
+
27
+ const winExe = `${name}.exe`;
28
+ const targets = [];
29
+
30
+ // 平台子目录(--platform all 布局):目录名即平台 key,目录内含 <app>[.exe] 或 resources/
31
+ const entries = fs.readdirSync(outDirPath, { withFileTypes: true });
32
+ for (const e of entries) {
33
+ if (!e.isDirectory()) continue;
34
+ const dir = path.join(outDirPath, e.name);
35
+ const exePath = path.join(dir, isWinPlat(e.name) ? winExe : name);
36
+ if (fs.existsSync(exePath) || fs.existsSync(path.join(dir, 'resources'))) {
37
+ targets.push({ plat: e.name, dir, outFile: exePath });
38
+ }
39
+ }
40
+ // 单平台布局:outDir 根下的可执行文件
41
+ if (targets.length === 0) {
42
+ for (const p of ['win-x64', 'linux-x64', 'darwin-arm64', 'linux-arm64']) {
43
+ const exePath = path.join(outDirPath, isWinPlat(p) ? winExe : name);
44
+ if (fs.existsSync(exePath)) {
45
+ targets.push({ plat: p, dir: outDirPath, outFile: exePath });
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ return targets;
51
+ }
52
+
53
+ // ---- 单平台自检 ----
54
+
55
+ function safeStat(p) {
56
+ try {
57
+ return fs.statSync(p);
58
+ } catch (e) {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ function fmtSize(n) {
64
+ if (n < 1024) return `${n} B`;
65
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
66
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
67
+ }
68
+
69
+ // 单平台产物校验,返回 [{ name, pass, detail }]。
70
+ function checkPlatformProduct({ targetDir, plat, appName, hasBackend }) {
71
+ const checks = [];
72
+ const fail = (name, detail) => checks.push({ name, pass: false, detail });
73
+ const okc = (name, detail) => checks.push({ name, pass: true, detail });
74
+
75
+ // 1) 可执行文件存在且非空
76
+ const exeName = platformExeName(plat, appName);
77
+ const exePath = path.join(targetDir, exeName);
78
+ const exeStat = safeStat(exePath);
79
+ if (!exeStat) {
80
+ fail('可执行文件', `缺失:${exeName}`);
81
+ } else if (exeStat.size <= 0) {
82
+ fail('可执行文件', `${exeName} 为空文件(0 字节)`);
83
+ } else {
84
+ okc('可执行文件', `${exeName}(${fmtSize(exeStat.size)})`);
85
+ // Windows 产物校验 PE 头(MZ),杜绝"假壳 / 空壳"被静默分发
86
+ if (isWinPlat(plat)) {
87
+ let head = '';
88
+ try {
89
+ head = fs.readFileSync(exePath).subarray(0, 2).toString('latin1');
90
+ } catch (e) { /* 读取失败按不通过处理 */ }
91
+ if (head === 'MZ') okc('PE 格式', 'MZ 头有效');
92
+ else fail('PE 格式', '文件头不是 MZ,非有效 Windows 可执行文件');
93
+ }
94
+ }
95
+
96
+ // 2) resources 目录
97
+ const resDir = path.join(targetDir, 'resources');
98
+ const resStat = safeStat(resDir);
99
+ if (!resStat || !resStat.isDirectory()) {
100
+ fail('resources 目录', '缺失:resources/');
101
+ return checks; // 后续资源检查依赖该目录,缺失则提前返回
102
+ }
103
+ okc('resources 目录', '存在');
104
+
105
+ // 3) 资源形态与安全模式一致(high: app.bin+.integrity;明文: index.html+config.json;互斥)
106
+ const hasBin = fs.existsSync(path.join(resDir, 'app.bin'));
107
+ const hasIntegrity = fs.existsSync(path.join(resDir, '.integrity'));
108
+ const hasHtml = fs.existsSync(path.join(resDir, 'index.html'));
109
+ const hasConfig = fs.existsSync(path.join(resDir, 'config.json'));
110
+ if (hasBin) {
111
+ if (hasHtml || hasConfig) {
112
+ fail('资源形态', 'high 模式残留明文 index.html/config.json(互斥被破坏)');
113
+ } else {
114
+ okc('资源形态', 'high 模式(app.bin + .integrity)');
115
+ }
116
+ if (!hasIntegrity) fail('完整性清单', '缺失 .integrity');
117
+ else okc('完整性清单', '.integrity 存在');
118
+ // app.bin 容器头校验(FRDM1)
119
+ let head = '';
120
+ try {
121
+ head = fs.readFileSync(path.join(resDir, 'app.bin')).subarray(0, 5).toString('latin1');
122
+ } catch (e) { /* 读取失败按不通过处理 */ }
123
+ if (head === 'FRDM1') okc('app.bin 容器', 'FRDM1 头有效');
124
+ else fail('app.bin 容器', '容器头不是 FRDM1,文件损坏或非本工具产物');
125
+ } else {
126
+ if (hasIntegrity) {
127
+ fail('资源形态', '明文模式残留 high 产物(.integrity)');
128
+ } else {
129
+ okc('资源形态', '明文模式(index.html + config.json)');
130
+ }
131
+ const htmlStat = safeStat(path.join(resDir, 'index.html'));
132
+ if (!hasHtml) fail('前端页面', '缺失 resources/index.html');
133
+ else if (htmlStat.size <= 0) fail('前端页面', 'index.html 为空(0 字节)');
134
+ else okc('前端页面', `index.html(${fmtSize(htmlStat.size)})`);
135
+ if (!hasConfig) {
136
+ fail('运行时配置', '缺失 resources/config.json');
137
+ } else {
138
+ try {
139
+ const cfg = JSON.parse(fs.readFileSync(path.join(resDir, 'config.json'), 'utf8'));
140
+ if (cfg.name !== appName) {
141
+ fail('运行时配置', `config.json name=${cfg.name} 与可执行文件名 ${appName} 不一致`);
142
+ } else {
143
+ okc('运行时配置', `config.json 可解析,name=${cfg.name} 一致`);
144
+ }
145
+ } catch (e) {
146
+ fail('运行时配置', `config.json 解析失败:${e.message}`);
147
+ }
148
+ }
149
+ }
150
+
151
+ // 4) 后端目录
152
+ if (hasBackend) {
153
+ if (fs.existsSync(path.join(resDir, 'backend'))) okc('后端进程', 'resources/backend 存在');
154
+ else fail('后端进程', '配置了 backend 但 resources/backend 缺失');
155
+ }
156
+
157
+ return checks;
158
+ }
159
+
160
+ // ---- 对外入口 ----
161
+
162
+ // 校验项目产物。返回 { ok, targets: [{ plat, dir, outFile, checks }] }。
163
+ async function verifyProduct(projectDir, opts = {}) {
164
+ const dir = path.resolve(projectDir || '.');
165
+ const { loadConfig } = require('./utils');
166
+ let cfg;
167
+ try {
168
+ cfg = await loadConfig(dir);
169
+ } catch (e) {
170
+ return { ok: false, targets: [], error: e.message };
171
+ }
172
+ const appName = (cfg.name || 'freedom-app').replace(/[^a-zA-Z0-9_.-]/g, '-');
173
+ const hasBackendDir = !!(cfg.backend && fs.existsSync(path.join(dir, cfg.backendDir || 'backend')));
174
+
175
+ const targets = findProducts(dir, cfg)
176
+ .filter((t) => !opts.platform || t.plat === opts.platform)
177
+ .map((t) => ({ ...t, checks: checkPlatformProduct({ targetDir: t.dir, plat: t.plat, appName, hasBackend: hasBackendDir }) }));
178
+
179
+ const ok = targets.length > 0 && targets.every((t) => t.checks.every((c) => c.pass));
180
+ return { ok, targets, appName };
181
+ }
182
+
183
+ // ---- 产物形态树 ----
184
+
185
+ // 递归收集目录树:[{ rel, size, isDir, depth }],depth 限制避免深层目录(如 backend/node_modules)爆炸。
186
+ function collectTree(dir, depth, out, maxDepth) {
187
+ let entries;
188
+ try {
189
+ entries = fs.readdirSync(dir, { withFileTypes: true });
190
+ } catch (e) {
191
+ return out;
192
+ }
193
+ for (const e of entries) {
194
+ if (e.name === '.integrity') {
195
+ // 完整性清单内容为敏感校验值,仅标注存在与大小,不展开内容
196
+ out.push({ rel: e.name, size: 0, isDir: false, depth, hidden: true });
197
+ continue;
198
+ }
199
+ const abs = path.join(dir, e.name);
200
+ const stat = safeStat(abs);
201
+ if (e.isDirectory()) {
202
+ out.push({ rel: e.name, size: 0, isDir: true, depth });
203
+ if (depth < maxDepth) collectTree(abs, depth + 1, out, maxDepth);
204
+ } else if (stat) {
205
+ out.push({ rel: e.name, size: stat.size, isDir: false, depth });
206
+ }
207
+ }
208
+ return out;
209
+ }
210
+
211
+ function renderTree(targets) {
212
+ const lines = [];
213
+ for (const t of targets) {
214
+ lines.push(`${t.dir}`);
215
+ const maxDepth = 5;
216
+ const tree = collectTree(t.dir, 1, [], maxDepth).sort((a, b) => {
217
+ if (a.depth !== b.depth) return a.depth - b.depth;
218
+ return a.rel.localeCompare(b.rel);
219
+ });
220
+ // 同层排序:目录优先,保证层级清晰
221
+ for (const node of tree) {
222
+ const prefix = ' '.repeat(node.depth) + (node.isDir ? '[dir] ' : ' ');
223
+ const size = node.isDir ? '' : ` (${fmtSize(node.size)})`;
224
+ const hidden = node.hidden ? ' [校验值]' : '';
225
+ lines.push(`${prefix}${node.rel}${size}${hidden}`);
226
+ }
227
+ }
228
+ return lines.join('\n');
229
+ }
230
+
231
+ // 校验项 -> 可打印行(build 自动自检 / verify 命令共用;纯文本,跨平台不依赖彩色终端)
232
+ function formatChecks(checks) {
233
+ return checks.map((c) => `${c.pass ? '[通过]' : '[失败]'} ${c.name}:${c.detail}`);
234
+ }
235
+
236
+ module.exports = { verifyProduct, findProducts, checkPlatformProduct, renderTree, formatChecks };
package/package.json CHANGED
@@ -1,39 +1,43 @@
1
- {
2
- "name": "@yufengtadian/freedom-cli",
3
- "version": "1.12.17",
4
- "description": "Freedom WebView desktop shell packaging tool - no Go toolchain required, one command packs three-platform desktop apps",
5
- "keywords": [
6
- "desktop",
7
- "webview",
8
- "electron-alternative",
9
- "wails",
10
- "tauri",
11
- "frontend",
12
- "cross-platform"
13
- ],
14
- "license": "MIT",
15
- "publishConfig": {
16
- "access": "public"
17
- },
18
- "bin": {
19
- "freedom": "bin/freedom.js"
20
- },
21
- "files": [
22
- "bin",
23
- "lib",
24
- "shell",
25
- "templates",
26
- "tutorial",
27
- "postinstall.js",
28
- "README.md"
29
- ],
30
- "scripts": {
31
- "postinstall": "node postinstall.js"
32
- },
33
- "engines": {
34
- "node": ">=18"
35
- },
36
- "dependencies": {
37
- "rcedit": "^4.0.1"
38
- }
39
- }
1
+ {
2
+ "name": "@yufengtadian/freedom-cli",
3
+ "version": "1.13.0",
4
+ "description": "Freedom WebView desktop shell packaging tool - no Go toolchain required, one command packs three-platform desktop apps",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/YUfeng12TA/freedom.git"
8
+ },
9
+ "keywords": [
10
+ "desktop",
11
+ "webview",
12
+ "electron-alternative",
13
+ "wails",
14
+ "tauri",
15
+ "frontend",
16
+ "cross-platform"
17
+ ],
18
+ "license": "MIT",
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "bin": {
23
+ "freedom": "bin/freedom.js"
24
+ },
25
+ "files": [
26
+ "bin",
27
+ "lib",
28
+ "shell",
29
+ "templates",
30
+ "tutorial",
31
+ "postinstall.js",
32
+ "README.md"
33
+ ],
34
+ "scripts": {
35
+ "postinstall": "node postinstall.js"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "dependencies": {
41
+ "rcedit": "^4.0.1"
42
+ }
43
+ }
Binary file
Binary file
Binary file
@@ -1,11 +1,5 @@
1
1
  module freedom-cli-shell
2
2
 
3
- go 1.22
3
+ go 1.26.5
4
4
 
5
5
  require github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
6
-
7
- require golang.org/x/sys v0.28.0
8
-
9
- // Freedom fork: 使用随包分发的本地 webview_go(标题栏图标与 exe 图标一致、
10
- // 无边框窗口最大化限定工作区、彻底清理非客户区等定制),避免拉取上游被覆盖。
11
- replace github.com/webview/webview_go => ./webview_go
@@ -1,2 +1,2 @@
1
- golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
2
- golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
1
+ github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0=
2
+ github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk=