@yufengtadian/freedom-cli 1.12.0 → 1.12.12

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 (57) hide show
  1. package/README.md +25 -29
  2. package/lib/build.js +1 -1
  3. package/lib/cli.js +10 -10
  4. package/lib/config.js +115 -31
  5. package/lib/tui.js +1 -1
  6. package/lib/utils.js +7 -3
  7. package/package.json +1 -1
  8. package/shell/win-x64/freedom-shell.exe +0 -0
  9. package/templates/go/build_out.txt +0 -0
  10. package/templates/go/err.txt +2 -0
  11. package/templates/go/err10.txt +0 -0
  12. package/templates/go/err11.txt +0 -0
  13. package/templates/go/err2.txt +0 -0
  14. package/templates/go/err3.txt +0 -0
  15. package/templates/go/err4.txt +1 -0
  16. package/templates/go/err5.txt +0 -0
  17. package/templates/go/err6.txt +0 -0
  18. package/templates/go/err7.txt +0 -0
  19. package/templates/go/err8.txt +0 -0
  20. package/templates/go/err9.txt +0 -0
  21. package/templates/go/go.mod +6 -0
  22. package/templates/go/go.mod.bak_gbk +11 -0
  23. package/templates/go/go.sum +2 -2
  24. package/templates/go/pkg/freedom/assets/freedom.js +6 -0
  25. package/templates/go/pkg/freedom/assets/index.html +1 -1
  26. package/templates/go/pkg/freedom/configfile.go +0 -2
  27. package/templates/go/pkg/freedom/freedom.go +16 -10
  28. package/templates/go/pkg/freedom/window_other.go +60 -12
  29. package/templates/go/pkg/freedom/window_windows.go +244 -29
  30. package/templates/go/tidy.txt +0 -0
  31. package/templates/go/vet_out.txt +0 -0
  32. package/templates/go/webview_go/.github/workflows/ci.yaml +62 -0
  33. package/templates/go/webview_go/CHANGELOG.md +15 -0
  34. package/templates/go/webview_go/LICENSE +22 -0
  35. package/templates/go/webview_go/README.md +50 -0
  36. package/templates/go/webview_go/examples/basic/main.go +12 -0
  37. package/templates/go/webview_go/examples/bind/main.go +38 -0
  38. package/templates/go/webview_go/glue.c +36 -0
  39. package/templates/go/webview_go/go.mod +3 -0
  40. package/templates/go/webview_go/go.sum +0 -0
  41. package/templates/go/webview_go/libs/mswebview2/LICENSE +27 -0
  42. package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
  43. package/templates/go/webview_go/libs/mswebview2/include/vendor.go +2 -0
  44. package/templates/go/webview_go/libs/mswebview2/vendor.go +2 -0
  45. package/templates/go/webview_go/libs/mswebview2/version.txt +1 -0
  46. package/templates/go/webview_go/libs/webview/LICENSE +22 -0
  47. package/templates/go/webview_go/libs/webview/include/vendor.go +2 -0
  48. package/templates/go/webview_go/libs/webview/include/webview.h +3840 -0
  49. package/templates/go/webview_go/libs/webview/vendor.go +2 -0
  50. package/templates/go/webview_go/libs/webview/version.txt +1 -0
  51. package/templates/go/webview_go/webview.cc +1 -0
  52. package/templates/go/webview_go/webview.go +371 -0
  53. package/templates/go/webview_go/webview_test.go +50 -0
  54. package/templates/project/freedom.config.js +5 -5
  55. package/templates/project/index.html +45 -5
  56. package/templates/project/src/main.js +140 -6
  57. package/tutorial/tutorial.html +4 -5
package/README.md CHANGED
@@ -1,23 +1,22 @@
1
- ---
2
- AIGC:
3
- Label: "1"
4
- ContentProducer: 001191440300708461136T1XGW3
5
- ProduceID: 84e692d233baf4c4ebb909c06f044dd3_2d74bd4d9c4911f184de525400f8a581
6
- ReservedCode1: xo2+Pdj5CsKT7YjGd4Rry/qknv3mVecOBHAw6HqFk7EuALvKYsLlY/JCDoqw3oHEfX+sRjzs+lLZfypsJxbkr73mtQrkuHrqQIZ8f4A7vIr7hTO5V+aMiCz29hrSmbNXeI0q5SXfE/orzUoL63JrdZc1HpZGj82RLwzxXZA/d59gHWK/oYb4EXzirS8=
7
- ContentPropagator: 001191440300708461136T1XGW3
8
- PropagateID: 84e692d233baf4c4ebb909c06f044dd3_2d74bd4d9c4911f184de525400f8a581
9
- ReservedCode2: xo2+Pdj5CsKT7YjGd4Rry/qknv3mVecOBHAw6HqFk7EuALvKYsLlY/JCDoqw3oHEfX+sRjzs+lLZfypsJxbkr73mtQrkuHrqQIZ8f4A7vIr7hTO5V+aMiCz29hrSmbNXeI0q5SXfE/orzUoL63JrdZc1HpZGj82RLwzxXZA/d59gHWK/oYb4EXzirS8=
10
- ---
11
-
12
-
13
-
14
1
  # freedom-cli
15
2
 
16
- Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.12.0)。
3
+ Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.13.0)。
17
4
 
18
5
  基于自研 Freedom WebView 壳层(对标 Wails / Tauri):前端完全自由、后端可任意语言、渲染复用系统 WebView(Windows WebView2 / macOS WKWebView / Linux WebKitGTK),产物为单个可执行文件 + resources 目录,前端页面内存加载,不占本地端口。
19
6
 
20
- **v1.12.0 核心升级**:标题栏默认改为完全无边框(`frameless`)——标题栏不存在,关闭 / 最大化 / 最小化按钮由前端自绘融入 UI(模板已内置自绘标题栏示例),不再依赖 Windows 原生按钮。`freedom titlebar` 仍可随时切回 `native` / `hidden`。
7
+ **v1.12.12 三平台 frameless 彻底修复**:macOS / Linux 无边框窗口不再残留原生标题栏——
8
+ - 壳层向 webview_go 新增 `set_decorated` / `window_control` 原生窗口控制 API(GTK 走 `gtk_window_set_decorated` / `gtk_window_*`,Cocoa 走隐藏标题栏 + `performMiniaturize:` / `zoom:` / `performClose:` / `isZoomed`),mac/linux 的 frameless 从"空实现回退原生标题栏"修复为真无边框,UI 上方不再残留未清理的原生标题栏;
9
+ - 自绘三按钮(最小化 / 最大化 / 关闭)在 macOS / Linux 上接入原生窗口控制,双击最大化 / 还原、`isMaximized` 状态查询真实可用(此前 mac/linux 窗口控制为静默空转、`isMaximized` 恒 false);
10
+ - 最大化语义统一为"铺满工作区"的正常窗口最大化:Windows 经 `WM_GETMINMAXINFO` 限定到监视器工作区(rcWork),macOS 走 `zoom:`、Linux 走 `gtk_window_maximize`,均非 F11 式整屏全屏;
11
+ - 打包流程全程仍不依赖 Go 工具链(`freedom build` 直接复制预编译壳,详见 v1.1.11 说明)。
12
+
13
+ **v1.12.11 标题栏优化**:自绘标题栏(frameless)增强——
14
+ - 标题栏左侧显示应用图标,图标直接从 exe 内嵌图标(PE 资源 ID=1)提取为 PNG data URL,**不再依赖 resources 资源文件夹**里的任何图标文件,跨平台无 resources 依赖;
15
+ - 双击标题栏空白区最大化 / 还原;
16
+ - 右键标题栏弹出系统菜单(还原 / 最小化 / 最大化 / 关闭),并按窗口状态自动置灰;
17
+ - 标题栏拖动改由 `window.freedom.window.startDrag()` 原生发起,保留双击与右键事件(`-webkit-app-region: drag` 会吞掉页面事件)。
18
+
19
+ **v1.12.0 核心升级**:标题栏默认改为完全无边框(`frameless`)——标题栏不存在,关闭 / 最大化 / 最小化按钮由前端自绘融入 UI(模板已内置自绘标题栏示例),不再依赖 Windows 原生按钮。`freedom titlebar` 仍可随时切回 `native`。
21
20
 
22
21
  **v1.12.x 修复与加固**:
23
22
  - 修复 close 按钮缺陷:窗口关闭改走 `PostMessage(WM_CLOSE)`(原 `CloseWindow` 语义为最小化,导致点关闭只最小化);
@@ -26,7 +25,7 @@ Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌
26
25
  - 壳进程启用 DPI 感知,高 DPI 屏幕下窗口坐标 / 渲染 / 鼠标自动化定位一致,内容更清晰;
27
26
  - `freedom config set` 兼容双引号配置写法;依赖变更检测改用 `package-lock.json` 作基准。
28
27
 
29
- **v1.11.0 核心升级**:支持自定义应用图标 + 默认隐藏 Windows 原生标题栏。配置 `freedom.config.js` 的 `icon`(Windows 用 `.ico`,macOS 用 `.icns`),`freedom build` 时自动把图标注入到 exe 的 PE 资源(RT_ICON / RT_GROUP_ICON)或 `.app/Contents/Resources`,无需任何资源编译器;`freedom icon <path>` 可一键设置。标题栏默认值由 `native` 调整为 `hidden`——保留 Windows 原生最小化 / 最大化 / 关闭按钮,隐藏标题栏视觉(DWM 扩展实现),默认不再占用窗口顶部空间。
28
+ **v1.11.0 核心升级**:支持自定义应用图标 + 无边框标题栏。配置 `freedom.config.js` 的 `icon`(Windows 用 `.ico`,macOS 用 `.icns`),`freedom build` 时自动把图标注入到 exe 的 PE 资源(RT_ICON / RT_GROUP_ICON)或 `.app/Contents/Resources`,无需任何资源编译器;`freedom icon <path>` 可一键设置。标题栏默认使用无边框(`frameless`),窗口控制按钮由前端自绘,不再占用窗口顶部空间。
30
29
 
31
30
  **v1.10.12 核心升级**:macOS 产物升级为标准 .app 应用包,直接产出 `.app.zip`(mac 用户解压即得 `.app`,拖入 /Applications 即可使用);在 macOS 上执行 `freedom dmg` 可再用系统 hdiutil 生成 `.dmg`。全程仍不依赖 Go 工具链。
32
31
 
@@ -34,7 +33,7 @@ Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌
34
33
 
35
34
  **v1.11.0 新特性**:
36
35
  - 自定义应用图标:`icon` 配置 + `freedom icon <path>` 命令,Windows 构建时用 rcedit 注入 `.ico` 到 exe 资源(支持多尺寸,资源管理器 / 任务栏 / 快捷方式统一显示),macOS 构建时把 `.icns` 放入 `.app` 并写入 Info.plist;
37
- - 标题栏默认改为 `hidden`:默认隐藏标题栏视觉,仅保留 Windows 原生最小化 / 最大化 / 关闭按钮;仍可用 `freedom titlebar` 随时切回 `native` / `frameless`。
36
+ - 标题栏默认无边框(`frameless`):标题栏与 Windows 原生最小化 / 最大化 / 关闭按钮均不存在,由前端自绘;仍可用 `freedom titlebar` 随时切回 `native`。
38
37
 
39
38
  **v1.10.12 新特性**:
40
39
  - macOS 产物打包为标准 `.app` bundle + `.app.zip`:壳复制进 `Contents/MacOS/`,resources 与前端/后端随包,解压即用,无需任何语言运行时;
@@ -67,7 +66,6 @@ freedom tui
67
66
 
68
67
  # 3. 调整标题栏(可选,随时可改;默认 frameless:标题栏不存在,关闭/最大化/最小化按钮由前端自绘融入 UI)
69
68
  freedom titlebar native # 保留系统原生标题栏
70
- freedom titlebar hidden # 隐藏标题栏视觉,仅保留 Windows 原生最小化/最大化/关闭按钮
71
69
  freedom titlebar frameless # 完全无边框,标题栏不存在,三按钮由前端自绘(默认,模板已内置示例)
72
70
 
73
71
  # 3.5 设置应用图标(可选;Windows 用 .ico,macOS 用 .icns)
@@ -77,7 +75,7 @@ freedom icon icon.ico
77
75
  freedom build # 默认当前平台
78
76
  freedom build --platform all # 三平台全量(win + mac + linux)
79
77
  freedom build --platform win-x64 # 仅 Windows
80
- freedom build --platform mac-arm64 # 仅 macOS Apple Silicon
78
+ freedom build --platform darwin-arm64 # 仅 macOS Apple Silicon
81
79
  freedom build --platform linux-x64 # 仅 Linux
82
80
  freedom build --no-cache # 忽略前端构建缓存,强制重建
83
81
  ```
@@ -104,7 +102,7 @@ mac 用户解压 `.app.zip` 即得 `.app`,拖入 `/Applications` 即可直接
104
102
 
105
103
  **壳二进制平台校验**:`freedom build` 在分发前会读取壳二进制文件头,校验其确为目标平台的真实格式(Windows PE / macOS Mach-O / Linux ELF)。若某平台壳缺失或格式不匹配(例如误用其它平台的二进制顶替),会**明确报错并给出修复指引**,绝不静默产出无法运行的假产物。
106
104
 
107
- **预编译壳的来源与 CI**:`webview_go` 依赖各系统自带 WebView 框架,**无法交叉编译**,三平台壳必须在对应平台本机编译。仓库已提供 `.github/workflows/build-shell.yml`:在 win / mac(Intel / Apple Silicon)/ linux runner 上分别编译真实壳并上传到 GitHub Release,供 `freedom shell download` 拉取。发布新版本时:先 `npm publish`,再创建同名 GitHub Release,手动触发 `build-shell` workflow(或直接发 Release 自动触发)即自动补齐各平台壳资产。
105
+ **预编译壳的来源与 CI**:`webview_go` 依赖各系统自带 WebView 框架,**无法交叉编译**,三平台壳必须在对应平台本机编译。仓库已提供 `.github/workflows/build-shell.yml`:在 win / mac(Apple Silicon)/ linux runner 上分别编译真实壳并上传到 GitHub Release,供 `freedom shell download` 拉取(Intel Mac 已不支持,见 `nativePlatform()` 的明确报错)。发布新版本时:先 `npm publish`,再创建同名 GitHub Release,手动触发 `build-shell` workflow(或直接发 Release 自动触发)即自动补齐各平台壳资产。
108
106
 
109
107
  产物目录可在 `freedom.config.js` 的 `outDir` 中调整:默认 `'dist'`,设为 `'.'` 则直接输出到项目根目录(dist 的上级),设为任意相对 / 绝对路径亦可。输出到项目根目录时会自动跳过 `index.html` 副本,避免覆盖项目源文件。
110
108
 
@@ -117,11 +115,10 @@ freedom config set outDir dist # 恢复默认 dist/
117
115
 
118
116
  | 模式 | 说明 |
119
117
  | --- | --- |
120
- | `native` | 保留系统原生标题栏 |
121
- | `hidden` | 隐藏标题栏视觉,仅保留 Windows 原生最小化 / 最大化 / 关闭按钮(DWM 扩展实现) |
122
- | `frameless` | 完全无边框,标题栏不存在,客户区铺满窗口,关闭 / 最大化 / 最小化按钮由前端自绘(默认,模板已内置示例) |
118
+ | `native` | 保留系统原生标题栏,标题栏图标与 exe 图标一致 |
119
+ | `frameless` | 完全无边框,标题栏不存在,客户区铺满窗口,关闭 / 最大化 / 最小化按钮由前端自绘(默认,模板已内置示例)。当前仅 Windows 完整实现;macOS / Linux 由系统窗口管理器托管,回退为原生标题栏且窗口控制动作返回明确错误 |
123
120
 
124
- `hidden` / `frameless` 模式下,前端可通过注入的 `window.freedom.window` API 控制窗口(`minimize` / `maximize` / `toggleMaximize` / `close` / `isMaximized` / `isFrameless`),模板已内置自绘标题栏示例。
121
+ `frameless` 模式下(Windows),前端可通过注入的 `window.freedom.window` API 控制窗口(`minimize` / `maximize` / `toggleMaximize` / `close` / `isMaximized` / `isFrameless`),模板已内置自绘标题栏示例;macOS / Linux 上这些动作会 reject,前端应捕获并提示。
125
122
 
126
123
  ## 配置(freedom.config.js)
127
124
 
@@ -134,7 +131,7 @@ export default {
134
131
  minHeight: 300,
135
132
  center: true, // 启动居中
136
133
  debug: false, // 开发者工具
137
- titlebar: 'frameless', // frameless(默认,标题栏不存在,三按钮由前端自绘)| native | hidden
134
+ titlebar: 'frameless', // frameless(默认,标题栏不存在,三按钮由前端自绘)| native
138
135
  icon: undefined, // 应用图标:Windows 用 .ico(推荐多尺寸),macOS 用 .icns
139
136
  outDir: 'dist', // 产物目录:'dist'(默认)| '.'(项目根目录)| 任意路径
140
137
  // backend: { command: 'node', args: ['backend/main.mjs'] }, // 任意语言后端进程
@@ -168,8 +165,8 @@ window.freedom.window.minimize(); // 窗口控制
168
165
  ```
169
166
  freedom tui # 交互式终端界面
170
167
  freedom init <目录> [--force]
171
- freedom build [--platform win-x64|mac-arm64|linux-x64|all] [--no-cache]
172
- freedom titlebar <native|hidden|frameless>
168
+ freedom build [--platform win-x64|darwin-arm64|linux-x64|all] [--no-cache]
169
+ freedom titlebar <native|frameless>
173
170
  freedom icon <path> # 设置应用图标(Windows 用 .ico,macOS 用 .icns)
174
171
  freedom config [get|set]
175
172
  freedom shell list|download <platform>|build <platform>
@@ -200,5 +197,4 @@ export default {
200
197
  ## License
201
198
 
202
199
  MIT
203
- *(内容由AI生成,仅供参考)*
204
- *(内容由AI生成,仅供参考)*
200
+
package/lib/build.js CHANGED
@@ -57,7 +57,7 @@ function parsePlatforms(raw) {
57
57
  if (!raw) return [nativePlatform()];
58
58
  const v = String(raw).toLowerCase();
59
59
  if (v === 'all') return ALL_PLATFORMS;
60
- const keyMap = { win: 'win-x64', mac: 'darwin-x64', linux: 'linux-x64' };
60
+ const keyMap = { win: 'win-x64', mac: 'darwin-arm64', linux: 'linux-x64' };
61
61
  if (keyMap[v]) return [keyMap[v]];
62
62
  if (ALL_PLATFORMS.includes(v)) return [v];
63
63
  throw new Error(
package/lib/cli.js CHANGED
@@ -19,7 +19,7 @@ freedom - Freedom 桌面壳打包工具 v${VERSION}
19
19
  freedom build [--platform <p>] 前端打包并分发桌面应用(默认当前平台)
20
20
  --platform win|mac|linux|all 指定目标平台(all = 三平台全量)
21
21
  --no-cache 忽略前端构建缓存,强制重新打包
22
- freedom titlebar <native|hidden|frameless>
22
+ freedom titlebar <native|frameless>
23
23
  一键切换标题栏策略
24
24
  freedom icon <path> 设置应用图标(Windows 需 .ico,macOS 需 .icns)
25
25
  freedom shell list 列出本地已就绪的预编译壳
@@ -33,7 +33,7 @@ freedom - Freedom 桌面壳打包工具 v${VERSION}
33
33
  freedom help 显示本帮助
34
34
  freedom version 显示版本
35
35
 
36
- 平台(<p>/<plat>):win-x64 / darwin-x64 / darwin-arm64 / linux-x64 / linux-arm64,
36
+ 平台(<p>/<plat>):win-x64 / darwin-arm64 / linux-x64 / linux-arm64,
37
37
  快捷别名:win / mac / linux / all。
38
38
 
39
39
  macOS 产物说明:
@@ -42,9 +42,9 @@ macOS 产物说明:
42
42
  在 macOS 上执行 freedom dmg 用系统 hdiutil 生成。
43
43
 
44
44
  标题栏策略说明:
45
- native 保留系统原生标题栏
46
- hidden 隐藏标题栏视觉,仅保留 Windows 原生最小化/最大化/关闭按钮
47
- frameless 完全无边框,标题栏不存在,关闭/最大化/最小化按钮由前端自绘(默认,模板已内置示例)
45
+ native 保留系统原生标题栏,标题栏图标与 exe 图标一致
46
+ frameless 完全无边框,标题栏与 Windows 原生最小化/最大化/关闭按钮均不存在,
47
+ 关闭/最大化/最小化按钮由前端自绘(默认,模板已内置示例)
48
48
 
49
49
  图标说明:
50
50
  Windows exe 图标:在 freedom.config.js 配置 icon(.ico 路径),构建时自动注入;
@@ -107,8 +107,8 @@ async function run(argv) {
107
107
 
108
108
  case 'titlebar': {
109
109
  const mode = rest[0];
110
- if (!['native', 'hidden', 'frameless'].includes(mode)) {
111
- console.error('[freedom] 用法:freedom titlebar <native|hidden|frameless>');
110
+ if (!['native', 'frameless'].includes(mode)) {
111
+ console.error('[freedom] 用法:freedom titlebar <native|frameless>');
112
112
  return 1;
113
113
  }
114
114
  setConfig(process.cwd(), 'titlebar', mode);
@@ -197,7 +197,7 @@ async function runShell(rest) {
197
197
  case 'download': {
198
198
  const plat = rest[1];
199
199
  if (!plat) {
200
- console.error('[freedom] 用法:freedom shell download <win-x64|darwin-x64|darwin-arm64|linux-x64|linux-arm64>');
200
+ console.error('[freedom] 用法:freedom shell download <win-x64|darwin-arm64|linux-x64|linux-arm64>');
201
201
  return 1;
202
202
  }
203
203
  const dest = await downloadShell(plat);
@@ -207,7 +207,7 @@ async function runShell(rest) {
207
207
  case 'build': {
208
208
  const plat = rest[1];
209
209
  if (!plat) {
210
- console.error('[freedom] 用法:freedom shell build <win-x64|darwin-x64|darwin-arm64|linux-x64|linux-arm64>');
210
+ console.error('[freedom] 用法:freedom shell build <win-x64|darwin-arm64|linux-x64|linux-arm64>');
211
211
  return 1;
212
212
  }
213
213
  const dest = buildShell(plat);
@@ -235,7 +235,7 @@ async function runDmg(rest) {
235
235
  ? (platArg.includes('=') ? platArg.split('=')[1] : rest[rest.indexOf(platArg) + 1])
236
236
  : nativePlatform();
237
237
  if (!plat || !plat.startsWith('darwin')) {
238
- console.error('[freedom] dmg 仅支持 macOS 平台(darwin-x64 / darwin-arm64)。');
238
+ console.error('[freedom] dmg 仅支持 macOS 平台(darwin-arm64)。');
239
239
  return 1;
240
240
  }
241
241
 
package/lib/config.js CHANGED
@@ -4,51 +4,114 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { loadConfig, hasConfig } = require('./utils');
6
6
 
7
- // 配置文件中可安全写入的字段及其默认值(用于渲染或回填)。
8
- const KNOWN_KEYS = {
9
- name: 'freedom-app',
10
- width: 1024,
11
- height: 720,
12
- minWidth: 400,
13
- minHeight: 300,
14
- center: true,
15
- debug: false,
16
- titlebar: 'frameless',
17
- icon: undefined,
18
- outDir: 'dist',
7
+ // 配置文件中可安全写入的字段:类型约束(写入前校验,防止坏值延迟到 build 才爆)。
8
+ const KEY_TYPES = {
9
+ name: 'string',
10
+ width: 'int',
11
+ height: 'int',
12
+ minWidth: 'int',
13
+ minHeight: 'int',
14
+ center: 'bool',
15
+ debug: 'bool',
16
+ titlebar: 'titlebar',
17
+ icon: 'string',
18
+ outDir: 'string',
19
19
  };
20
20
 
21
- // 把配置写回 freedom.config.js(保留注释,只替换已知键的取值)。
21
+ // CLI/TUI 传入的值规范化为配置语义上的目标值;非法即抛错。
22
+ function normalizeValue(key, value) {
23
+ const t = KEY_TYPES[key];
24
+ if (t === 'string') return String(value);
25
+ if (t === 'int') {
26
+ const n = Number(value);
27
+ if (!Number.isInteger(n)) {
28
+ throw new Error(`配置项 ${key} 需要整数值,收到:${JSON.stringify(value)}`);
29
+ }
30
+ return n;
31
+ }
32
+ if (t === 'bool') {
33
+ if (typeof value !== 'boolean') {
34
+ throw new Error(`配置项 ${key} 仅接受 true / false`);
35
+ }
36
+ return value;
37
+ }
38
+ if (t === 'titlebar') {
39
+ const s = String(value);
40
+ if (!['native', 'frameless'].includes(s)) {
41
+ throw new Error('titlebar 取值必须为:native / frameless。');
42
+ }
43
+ return s;
44
+ }
45
+ // 扩展键(如 backend):字符串命令简写转 {command,args} 对象,
46
+ // 与 build.js renderConfigJSON 的契约对齐(纯字符串形态不会被打包为后端进程)。
47
+ if (key === 'backend') {
48
+ if (value === undefined || value === null || value === '') return undefined;
49
+ if (typeof value === 'string') {
50
+ const parts = value.trim().split(/\s+/);
51
+ return { command: parts[0], args: parts.slice(1) };
52
+ }
53
+ return value; // 对象/数组形式原样写入
54
+ }
55
+ return value;
56
+ }
57
+
58
+ // 把值渲染为合法 JS 字面量。统一走 JSON.stringify:
59
+ // 反斜杠 / 引号自动转义,且不存在 $ 替换模式注入面(配合函数式 replace 使用)。
60
+ function renderLiteral(v) {
61
+ if (v === undefined) return 'undefined';
62
+ if (v === null) return 'null';
63
+ return JSON.stringify(v, null, typeof v === 'object' ? 2 : 0);
64
+ }
65
+
66
+ // 统计一行内 { 与 } 的差值,用于把多行对象字面量并入替换范围
67
+ //(启发式实现;配置值内含花括号字符串的场景需手动编辑配置文件)。
68
+ function braceDelta(line) {
69
+ let d = 0;
70
+ for (const ch of line) {
71
+ if (ch === '{') d++;
72
+ else if (ch === '}') d--;
73
+ }
74
+ return d;
75
+ }
76
+
77
+ // 把配置写回 freedom.config.js(保留注释与格式,只替换目标键所在行)。
78
+ // 逐行定位且跳过注释行:模板中被注释的示例键(如 // backend: undefined,)不会被误改。
22
79
  function setConfig(dir, key, value) {
23
80
  const cfgPath = path.join(dir, 'freedom.config.js');
24
81
  if (!fs.existsSync(cfgPath)) {
25
82
  throw new Error('未找到 freedom.config.js。');
26
83
  }
27
84
  const text = fs.readFileSync(cfgPath, 'utf8');
85
+ const norm = normalizeValue(key, value);
86
+ const rendered = renderLiteral(norm);
28
87
 
29
- if (key === 'titlebar') {
30
- const modes = ['native', 'hidden', 'frameless'];
31
- if (!modes.includes(value)) {
32
- throw new Error(`titlebar 取值必须为:${modes.join(' / ')}。`);
88
+ const lines = text.split('\n');
89
+ const keyRe = new RegExp(`^(\\s*)${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:(.*)$`);
90
+ let replaced = false;
91
+ for (let i = 0; i < lines.length; i++) {
92
+ if (lines[i].trim().startsWith('//')) continue; // 注释行不参与匹配与改写
93
+ const m = lines[i].match(keyRe);
94
+ if (!m) continue;
95
+ // 值为多行对象时吞并后续行直到花括号闭合,避免残留孤儿括号
96
+ let j = i;
97
+ let depth = braceDelta(lines[i]);
98
+ while (depth > 0 && j + 1 < lines.length) {
99
+ j++;
100
+ depth += braceDelta(lines[j]);
33
101
  }
102
+ const trailing = /,\s*$/.test(m[2]) ? ',' : '';
103
+ lines.splice(i, j - i + 1, `${m[1]}${key}: ${rendered}${trailing}`);
104
+ replaced = true;
105
+ break;
34
106
  }
35
-
36
- // 同时兼容单引号与双引号字符串写法(模板统一单引号,用户手写可能用双引号)。
37
- const regex = new RegExp(`(${key}\\s*:\\s*)(\"[^\"]*\"|'[^']*'|\\d+|true|false|undefined|null)`, 'g');
38
- if (!regex.test(text)) {
39
- throw new Error(`配置项 ${key} 未在 freedom.config.js 中找到,请手动添加。`);
40
- }
41
-
42
- let rendered = String(value);
43
- if (typeof value === 'string') {
44
- rendered = `'${value.replace(/'/g, "\\'")}'`;
107
+ if (!replaced) {
108
+ throw new Error(`配置项 ${key} 未在 freedom.config.js 中找到(注释行不参与修改),请手动添加。`);
45
109
  }
46
-
47
- const updated = text.replace(regex, `$1${rendered}`);
48
- fs.writeFileSync(cfgPath, updated, 'utf8');
49
- return value;
110
+ fs.writeFileSync(cfgPath, lines.join('\n'), 'utf8');
111
+ return norm;
50
112
  }
51
113
 
114
+
52
115
  async function getConfig(dir) {
53
116
  if (!hasConfig(dir)) {
54
117
  throw new Error('当前目录不是 Freedom 项目(缺少 freedom.config.js)。');
@@ -57,6 +120,27 @@ async function getConfig(dir) {
57
120
  return cfg;
58
121
  }
59
122
 
123
+ // 配置文件中可安全写入的字段及其默认值(用于渲染或回填)。
124
+ const KNOWN_KEYS = {
125
+ name: 'freedom-app',
126
+ width: 1024,
127
+ height: 720,
128
+ minWidth: 400,
129
+ minHeight: 300,
130
+ center: true,
131
+ debug: false,
132
+ titlebar: 'frameless',
133
+ icon: undefined,
134
+ outDir: 'dist',
135
+ };
136
+
137
+ async function getConfig(dir) {
138
+ if (!hasConfig(dir)) {
139
+ throw new Error('当前目录不是 Freedom 项目(缺少 freedom.config.js)。');
140
+ }
141
+ return loadConfig(dir);
142
+ }
143
+
60
144
  async function showConfig(dir) {
61
145
  const cfg = await loadConfig(dir);
62
146
  const lines = Object.keys(KNOWN_KEYS).map((k) => {
package/lib/tui.js CHANGED
@@ -192,7 +192,7 @@ const CONFIG_KEYS = [
192
192
  ['minHeight', '窗口最小高度'],
193
193
  ['center', '启动居中(true/false)'],
194
194
  ['debug', '开发者工具(true/false)'],
195
- ['titlebar', '标题栏:native | hidden | frameless'],
195
+ ['titlebar', '标题栏:native | frameless'],
196
196
  ['icon', '应用图标:.ico(Windows)/ .icns(macOS)路径'],
197
197
  ['outDir', '产物目录:dist(默认)| .(项目根)| 任意路径'],
198
198
  ['backend', '任意语言后端进程(JSON)'],
package/lib/utils.js CHANGED
@@ -28,11 +28,10 @@ function shellDir() {
28
28
  // 平台矩阵:key -> { exe 文件名(壳二进制名) }
29
29
  // 通用壳为三平台预编译二进制,应用内容通过 exe 同目录 resources/ 外部加载,
30
30
  // 因此同一壳可复用于任意应用,打包时无需任何语言工具链。
31
- const ALL_PLATFORMS = ['win-x64', 'darwin-x64', 'darwin-arm64', 'linux-x64', 'linux-arm64'];
31
+ const ALL_PLATFORMS = ['win-x64', 'darwin-arm64', 'linux-x64', 'linux-arm64'];
32
32
 
33
33
  const SHELL_EXE_NAME = {
34
34
  'win-x64': 'freedom-shell.exe',
35
- 'darwin-x64': 'freedom-shell',
36
35
  'darwin-arm64': 'freedom-shell',
37
36
  'linux-x64': 'freedom-shell',
38
37
  'linux-arm64': 'freedom-shell',
@@ -57,7 +56,12 @@ function nativePlatform() {
57
56
  const plat = process.platform;
58
57
  const arch = process.arch;
59
58
  if (plat === 'win32') return 'win-x64';
60
- if (plat === 'darwin') return arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
59
+ if (plat === 'darwin') {
60
+ if (arch !== 'arm64') {
61
+ throw new Error('已不支持 Intel Mac(darwin-x64):请改用 Apple Silicon Mac 构建 darwin-arm64,或直接使用 darwin-arm64 产物。');
62
+ }
63
+ return 'darwin-arm64';
64
+ }
61
65
  if (plat === 'linux') return arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
62
66
  return `${plat}-${arch}`;
63
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yufengtadian/freedom-cli",
3
- "version": "1.12.0",
3
+ "version": "1.12.12",
4
4
  "description": "Freedom WebView desktop shell packaging tool - no Go toolchain required, one command packs three-platform desktop apps",
5
5
  "keywords": [
6
6
  "desktop",
Binary file
File without changes
@@ -0,0 +1,2 @@
1
+ # freedom-cli-shell/pkg/freedom
2
+ pkg\freedom\freedom.go:212:16: cannot use a.backend.Handle(method, params) (value of type interface{}) as json.RawMessage value in assignment: need type assertion
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1 @@
1
+ github.com/webview/webview_go: build constraints exclude all Go files in D:\dev\自研desktop exe打包框架\freedom-cli\templates\go\webview_go
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -3,3 +3,9 @@ module freedom-cli-shell
3
3
  go 1.22
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
@@ -0,0 +1,11 @@
1
+ module freedom-cli-shell
2
+
3
+ go 1.22
4
+
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
- 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=
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=
@@ -52,10 +52,16 @@
52
52
  minimize: function () { return windowAction('minimize'); },
53
53
  maximize: function () { return windowAction('maximize'); },
54
54
  unmaximize: function () { return windowAction('unmaximize'); },
55
+ restore: function () { return windowAction('unmaximize'); },
55
56
  toggleMaximize: function () { return windowAction('toggleMaximize'); },
56
57
  close: function () { return windowAction('close'); },
57
58
  isMaximized: function () { return windowAction('isMaximized'); },
58
59
  isFrameless: function () { return windowAction('isFrameless'); },
60
+ // 应用图标(data URL)。从 exe 内嵌图标提取,供自绘标题栏显示,不依赖 resources 资源文件夹。
61
+ appIcon: function () { return windowAction('appIcon'); },
62
+ // 标题栏 JS 拖动(WM_NCLBUTTONDOWN + HTCAPTION),比 -webkit-app-region: drag
63
+ // 更稳(保留双击最大化 / 右键菜单等页面事件)。非 Windows 平台为 no-op。
64
+ startDrag: function () { return windowAction('startDrag'); },
59
65
  },
60
66
  };
61
67
 
@@ -45,7 +45,7 @@
45
45
  font-size: 13px;
46
46
  }
47
47
 
48
- /* 无边框模式:前端自绘标题栏(hidden / frameless 时显示) */
48
+ /* 无边框模式:前端自绘标题栏(frameless 时显示) */
49
49
  .titlebar {
50
50
  display: none;
51
51
  height: 36px;
@@ -69,8 +69,6 @@ func (a *App) loadRuntimeConfig() error {
69
69
  switch rc.TitleBar {
70
70
  case "native":
71
71
  a.cfg.TitleBar = TitleBarNative
72
- case "hidden":
73
- a.cfg.TitleBar = TitleBarHidden
74
72
  case "frameless":
75
73
  a.cfg.TitleBar = TitleBarFrameless
76
74
  }
@@ -29,14 +29,11 @@ import (
29
29
  type TitleBarMode string
30
30
 
31
31
  const (
32
- // TitleBarNative 保留系统原生标题栏(默认)。
32
+ // TitleBarNative 保留系统原生标题栏(默认),标题栏图标与 exe 图标一致。
33
33
  TitleBarNative TitleBarMode = "native"
34
- // TitleBarHidden 隐藏标题栏视觉,但保留系统原生最小化 / 最大化 / 关闭按钮。
35
- // 当前仅 Windows 生效(基于 DWM 扩展实现);macOS / Linux 上回退为原生标题栏。
36
- TitleBarHidden TitleBarMode = "hidden"
37
34
  // TitleBarFrameless 完全无边框,客户区铺满整个窗口。
38
- // 最小化 / 最大化 / 关闭按钮需由前端自绘(通过 window.freedom.window.* 控制),
39
- // 三平台行为一致。
35
+ // 标题栏与系统原生最小化 / 最大化 / 关闭按钮均不存在,
36
+ // 需由前端自绘(通过 window.freedom.window.* 控制),三平台行为一致。
40
37
  TitleBarFrameless TitleBarMode = "frameless"
41
38
  )
42
39
 
@@ -162,6 +159,7 @@ func (a *App) Run() {
162
159
  }
163
160
  a.applyCenter()
164
161
  a.applyTitleBar()
162
+ a.setWindowIcon()
165
163
 
166
164
  // 注入前端 SDK:window.freedom 全局对象。
167
165
  w.Init(jsSDK)
@@ -195,7 +193,15 @@ func (a *App) Run() {
195
193
  // bridge 是前端调用后端的统一入口(JSON-RPC 风格)。
196
194
  // 前端 SDK 通过 window.__freedom_bridge(method, paramsJson) 调用,
197
195
  // 桥接层把请求转发给当前绑定的后端(内嵌 Go 方法或任意语言进程)。
198
- func (a *App) bridge(method string, paramsJSON string) (json.RawMessage, error) {
196
+ func (a *App) bridge(method string, paramsJSON string) (result json.RawMessage, err error) {
197
+ // 防御后端方法 panic 导致整个窗口崩溃:统一转为错误回传前端。
198
+ defer func() {
199
+ if r := recover(); r != nil {
200
+ result = nil
201
+ err = fmt.Errorf("freedom: method %q panicked: %v", method, r)
202
+ }
203
+ }()
204
+
199
205
  var params []json.RawMessage
200
206
  if len(paramsJSON) > 0 && paramsJSON != "null" {
201
207
  if err := json.Unmarshal([]byte(paramsJSON), &params); err != nil {
@@ -203,11 +209,11 @@ func (a *App) bridge(method string, paramsJSON string) (json.RawMessage, error)
203
209
  }
204
210
  }
205
211
 
206
- result, err := a.backend.Handle(method, params)
212
+ raw, err := a.backend.Handle(method, params)
207
213
  if err != nil {
208
214
  return nil, err
209
215
  }
210
- data, err := json.Marshal(result)
216
+ data, err := json.Marshal(raw)
211
217
  if err != nil {
212
218
  return nil, fmt.Errorf("freedom: method %q: cannot marshal result: %w", method, err)
213
219
  }
@@ -251,7 +257,7 @@ func (a *App) WindowHandle() uintptr {
251
257
  // windowControl 处理前端 window.freedom.window.* 的窗口控制请求。
252
258
  // 具体实现按平台分文件:window_windows.go(Windows)/ window_other.go(macOS、Linux)。
253
259
  func (a *App) windowControl(action string) (interface{}, error) {
254
- return windowControl(a.WindowHandle(), action, a.cfg.TitleBar)
260
+ return windowControl(a, action)
255
261
  }
256
262
 
257
263
  // resolveHTML 依据配置返回页面内容。