@yufengtadian/freedom-cli 1.1.11 → 1.12.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.
package/README.md CHANGED
@@ -1,11 +1,46 @@
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
+
1
14
  # freedom-cli
2
15
 
3
- Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.1.11)。
16
+ Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.12.0)。
4
17
 
5
18
  基于自研 Freedom WebView 壳层(对标 Wails / Tauri):前端完全自由、后端可任意语言、渲染复用系统 WebView(Windows WebView2 / macOS WKWebView / Linux WebKitGTK),产物为单个可执行文件 + resources 目录,前端页面内存加载,不占本地端口。
6
19
 
20
+ **v1.12.0 核心升级**:标题栏默认改为完全无边框(`frameless`)——标题栏不存在,关闭 / 最大化 / 最小化按钮由前端自绘融入 UI(模板已内置自绘标题栏示例),不再依赖 Windows 原生按钮。`freedom titlebar` 仍可随时切回 `native` / `hidden`。
21
+
22
+ **v1.12.x 修复与加固**:
23
+ - 修复 close 按钮缺陷:窗口关闭改走 `PostMessage(WM_CLOSE)`(原 `CloseWindow` 语义为最小化,导致点关闭只最小化);
24
+ - 壳二进制平台校验:构建前校验壳文件头(PE / Mach-O / ELF),杜绝"Windows 壳冒充 mac/linux 壳"的假壳被静默分发;
25
+ - `freedom shell download` 默认版本改为动态跟随包版本(`releaseTag()` 不再硬编码 v1.1.10),并新增 `.github/workflows/build-shell.yml` 三平台 CI 构建与 Release 资产上传;
26
+ - 壳进程启用 DPI 感知,高 DPI 屏幕下窗口坐标 / 渲染 / 鼠标自动化定位一致,内容更清晰;
27
+ - `freedom config set` 兼容双引号配置写法;依赖变更检测改用 `package-lock.json` 作基准。
28
+
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 扩展实现),默认不再占用窗口顶部空间。
30
+
31
+ **v1.10.12 核心升级**:macOS 产物升级为标准 .app 应用包,直接产出 `.app.zip`(mac 用户解压即得 `.app`,拖入 /Applications 即可使用);在 macOS 上执行 `freedom dmg` 可再用系统 hdiutil 生成 `.dmg`。全程仍不依赖 Go 工具链。
32
+
7
33
  **v1.1.11 核心升级**:不再依赖 Go 工具链。壳层以预编译二进制随 npm 包分发(`shell/<platform>/`),`freedom build` 直接复制壳 + 写入前端与配置即可出包,Windows / macOS / Linux 三平台一次全出,任意前后端语言自由组合。
8
34
 
35
+ **v1.11.0 新特性**:
36
+ - 自定义应用图标:`icon` 配置 + `freedom icon <path>` 命令,Windows 构建时用 rcedit 注入 `.ico` 到 exe 资源(支持多尺寸,资源管理器 / 任务栏 / 快捷方式统一显示),macOS 构建时把 `.icns` 放入 `.app` 并写入 Info.plist;
37
+ - 标题栏默认改为 `hidden`:默认隐藏标题栏视觉,仅保留 Windows 原生最小化 / 最大化 / 关闭按钮;仍可用 `freedom titlebar` 随时切回 `native` / `frameless`。
38
+
39
+ **v1.10.12 新特性**:
40
+ - macOS 产物打包为标准 `.app` bundle + `.app.zip`:壳复制进 `Contents/MacOS/`,resources 与前端/后端随包,解压即用,无需任何语言运行时;
41
+ - `freedom dmg`:在 macOS 上用系统 hdiutil 把 `.app` 打成 `.dmg`(Windows / Linux 上运行会给出指引);
42
+ - TUI 壳管理去 Go 依赖感:「下载壳」标注无需 Go,「本地编译壳」明确为可选项(壳已预编译随包分发)。
43
+
9
44
  **v1.1.11 新特性**:
10
45
  - `freedom tui` 交互式终端界面:菜单化完成打包 / 新建项目 / 修改配置 / 壳管理 / 教程,零依赖、方向键 + 回车导航、支持搜索过滤与多选;
11
46
  - 前端构建缓存:源码无改动时跳过重复 `vite build`(秒级复用,典型加速 10 倍+),`--no-cache` 强制全量重建;
@@ -30,10 +65,13 @@ npm install
30
65
  # 2. 交互式终端界面(方向键 + 回车选择,q 返回)
31
66
  freedom tui
32
67
 
33
- # 3. 调整标题栏(可选,随时可改)
34
- freedom titlebar native # 保留系统原生标题栏(默认)
68
+ # 3. 调整标题栏(可选,随时可改;默认 frameless:标题栏不存在,关闭/最大化/最小化按钮由前端自绘融入 UI)
69
+ freedom titlebar native # 保留系统原生标题栏
35
70
  freedom titlebar hidden # 隐藏标题栏视觉,仅保留 Windows 原生最小化/最大化/关闭按钮
36
- freedom titlebar frameless # 完全无边框,按钮由前端自绘(模板已内置示例)
71
+ freedom titlebar frameless # 完全无边框,标题栏不存在,三按钮由前端自绘(默认,模板已内置示例)
72
+
73
+ # 3.5 设置应用图标(可选;Windows 用 .ico,macOS 用 .icns)
74
+ freedom icon icon.ico
37
75
 
38
76
  # 4. 打包
39
77
  freedom build # 默认当前平台
@@ -46,10 +84,28 @@ freedom build --no-cache # 忽略前端构建缓存,强制重建
46
84
 
47
85
  构建产物默认输出到 `dist/`。`dist/<应用名>.exe`(或对应平台可执行文件)为壳层程序,运行时从 `dist/resources/` 加载前端页面与配置,不弹 cmd 黑窗。
48
86
 
87
+ **macOS 产物(v1.10.12)**:`freedom build --platform mac` 除裸可执行文件外,额外生成标准 `.app` bundle 与 `<应用名>-<平台>.app.zip`:
88
+
89
+ ```
90
+ dist/<应用名>.app/
91
+ └── Contents/
92
+ ├── Info.plist
93
+ ├── MacOS/
94
+ │ ├── <应用名> # 壳可执行文件
95
+ │ └── resources/ # 前端页面 + config.json + backend/
96
+ └── Resources/
97
+ ```
98
+
99
+ mac 用户解压 `.app.zip` 即得 `.app`,拖入 `/Applications` 即可直接运行,无需安装任何语言运行时。如需 `.dmg` 安装映像,把产物拷到 macOS 上执行 `freedom dmg`(用系统 hdiutil 生成),或仅分发包内 `.app.zip`。
100
+
49
101
  **构建缓存**:CLI 会在 `.freedom/vite-dist/` 记录前端源码指纹,源码无改动时复用上次构建产物直接进入分发阶段(秒级完成)。修改了 `src/` / `index.html` / `vite.config.js` 等任一处会触发自动重建;需要强制全量时可加 `--no-cache`。
50
102
 
51
103
  **三平台打包**:壳层二进制随包分发,无需本机安装 Go 或交叉编译工具链。首次构建某平台时若本地缺少对应壳二进制,CLI 自动从 GitHub Releases 下载(`freedom shell download <platform>` 可手动补齐);仅当你想用本机 Go 工具链亲自编译壳时才需要 `freedom shell build`(只产出本机平台)。多平台分发为并行执行,全平台打包耗时大幅缩短。
52
104
 
105
+ **壳二进制平台校验**:`freedom build` 在分发前会读取壳二进制文件头,校验其确为目标平台的真实格式(Windows PE / macOS Mach-O / Linux ELF)。若某平台壳缺失或格式不匹配(例如误用其它平台的二进制顶替),会**明确报错并给出修复指引**,绝不静默产出无法运行的假产物。
106
+
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 自动触发)即自动补齐各平台壳资产。
108
+
53
109
  产物目录可在 `freedom.config.js` 的 `outDir` 中调整:默认 `'dist'`,设为 `'.'` 则直接输出到项目根目录(dist 的上级),设为任意相对 / 绝对路径亦可。输出到项目根目录时会自动跳过 `index.html` 副本,避免覆盖项目源文件。
54
110
 
55
111
  ```bash
@@ -61,9 +117,9 @@ freedom config set outDir dist # 恢复默认 dist/
61
117
 
62
118
  | 模式 | 说明 |
63
119
  | --- | --- |
64
- | `native` | 保留系统原生标题栏(默认) |
120
+ | `native` | 保留系统原生标题栏 |
65
121
  | `hidden` | 隐藏标题栏视觉,仅保留 Windows 原生最小化 / 最大化 / 关闭按钮(DWM 扩展实现) |
66
- | `frameless` | 完全无边框,客户区铺满窗口,最小化 / 最大化 / 关闭按钮由前端自绘 |
122
+ | `frameless` | 完全无边框,标题栏不存在,客户区铺满窗口,关闭 / 最大化 / 最小化按钮由前端自绘(默认,模板已内置示例) |
67
123
 
68
124
  `hidden` / `frameless` 模式下,前端可通过注入的 `window.freedom.window` API 控制窗口(`minimize` / `maximize` / `toggleMaximize` / `close` / `isMaximized` / `isFrameless`),模板已内置自绘标题栏示例。
69
125
 
@@ -78,12 +134,18 @@ export default {
78
134
  minHeight: 300,
79
135
  center: true, // 启动居中
80
136
  debug: false, // 开发者工具
81
- titlebar: 'native', // native | hidden | frameless
137
+ titlebar: 'frameless', // frameless(默认,标题栏不存在,三按钮由前端自绘)| native | hidden
138
+ icon: undefined, // 应用图标:Windows 用 .ico(推荐多尺寸),macOS 用 .icns
82
139
  outDir: 'dist', // 产物目录:'dist'(默认)| '.'(项目根目录)| 任意路径
83
140
  // backend: { command: 'node', args: ['backend/main.mjs'] }, // 任意语言后端进程
84
141
  };
85
142
  ```
86
143
 
144
+ **图标说明**:
145
+ - Windows:`.ico` 在构建时由 rcedit 注入 exe 资源(无需 VC 资源编译器),资源管理器 / 任务栏 / 快捷方式统一显示自定义图标;推荐含 16 / 32 / 48 / 256 多尺寸;
146
+ - macOS:`.icns` 在构建时放入 `.app/Contents/Resources` 并写入 Info.plist 的 `CFBundleIconFile`;
147
+ - 未配置时使用壳默认图标。`freedom icon <path>` 可直接写入该配置。
148
+
87
149
  ## 前端
88
150
 
89
151
  前端是标准 Vite 项目,`vite build` 时通过 `vite-plugin-singlefile` 内联为单个 HTML。壳层注入全局对象 `window.freedom`:
@@ -99,6 +161,7 @@ window.freedom.window.minimize(); // 窗口控制
99
161
  - Node.js >= 18
100
162
  - 不需要 Go 工具链(壳层为随包分发的预编译二进制)
101
163
  - 可选:`freedom shell build` 需本机 Go(含 CGO,Windows 需 MSVC 编译环境),仅用于亲自编译壳
164
+ - `.dmg` 打包需在 macOS 上执行(依赖系统 hdiutil);`.app.zip` 在任意平台均可直接产出
102
165
 
103
166
  ## 命令
104
167
 
@@ -107,8 +170,10 @@ freedom tui # 交互式终端界面
107
170
  freedom init <目录> [--force]
108
171
  freedom build [--platform win-x64|mac-arm64|linux-x64|all] [--no-cache]
109
172
  freedom titlebar <native|hidden|frameless>
173
+ freedom icon <path> # 设置应用图标(Windows 用 .ico,macOS 用 .icns)
110
174
  freedom config [get|set]
111
175
  freedom shell list|download <platform>|build <platform>
176
+ freedom dmg [--platform <plat>] # 在 macOS 上把 .app 打包为 .dmg
112
177
  freedom tutorial
113
178
  freedom help
114
179
  ```
@@ -135,3 +200,5 @@ export default {
135
200
  ## License
136
201
 
137
202
  MIT
203
+ *(内容由AI生成,仅供参考)*
204
+ *(内容由AI生成,仅供参考)*
package/lib/build.js CHANGED
@@ -11,6 +11,11 @@
11
11
  // - 写 resources/config.json(窗口 + 后端配置,壳运行时读取)
12
12
  // - 复制壳为 outDir/<app>[.exe]
13
13
  // - 复制 backend/ 到 resources/backend/(配置了 backend 时)
14
+ // 4. macOS 平台额外生成 .app bundle + .app.zip:
15
+ // outDir/<app>.app/Contents/{Info.plist, MacOS/<app>, MacOS/resources/}
16
+ // 壳加载 exe 同目录 resources/,因此 .app 无需改壳即可运行;
17
+ // mac 用户解压 .app.zip 得 .app,拖入 /Applications 即可使用;
18
+ // 如需 .dmg,在 macOS 上运行 freedom dmg 用系统 hdiutil 生成。
14
19
  //
15
20
  // 产物不嵌入 HTML,页面与配置均在 exe 同目录 resources/ 下,因此壳可复用、
16
21
  // 可跨平台分发、无需在目标机器安装任何语言运行时。
@@ -23,11 +28,12 @@ const { copyDir } = require('./utils');
23
28
  const {
24
29
  ALL_PLATFORMS,
25
30
  isWinPlat,
31
+ isMacPlat,
26
32
  platformExeName,
27
33
  nativePlatform,
28
34
  localShellPath,
29
35
  } = require('./utils');
30
- const { hasShell, downloadShell } = require('./shell');
36
+ const { hasShell, downloadShell, validateLocalShell } = require('./shell');
31
37
 
32
38
  function run(cmd, args, opts = {}) {
33
39
  // Windows 下 npm 是 .cmd 批处理,必须经 shell 执行
@@ -69,6 +75,7 @@ async function build(projectDir, opts = {}) {
69
75
  const cfg = await loadConfig(dir);
70
76
 
71
77
  const name = (cfg.name || 'freedom-app').replace(/[^a-zA-Z0-9_.-]/g, '-');
78
+ const version = String(cfg.version || appVersionFromPkg(dir) || '1.0.0');
72
79
  const outDir = String(cfg.outDir || 'dist').trim() || 'dist';
73
80
  const outDirPath = path.resolve(dir, outDir);
74
81
  const platforms = parsePlatforms(opts.platform);
@@ -97,13 +104,17 @@ async function build(projectDir, opts = {}) {
97
104
  const backendDir = path.join(dir, cfg.backendDir || 'backend');
98
105
  const hasBackend = !!(cfg.backend && fs.existsSync(backendDir));
99
106
 
100
- // 3) 逐平台分发(多平台并行,显著缩短全平台打包耗时)
107
+ // 3) 应用图标:cfg.icon(相对项目根或绝对路径)。Windows 注入 .ico 到 exe 资源,
108
+ // macOS 把 .icns 放入 .app/Contents/Resources 并在 Info.plist 声明。
109
+ const icon = resolveIcon(dir, cfg.icon);
110
+
111
+ // 4) 逐平台分发(多平台并行,显著缩短全平台打包耗时)
101
112
  // 单平台直接输出到 outDir;多平台各自放到 outDir/<plat>/ 子目录,避免互相覆盖
102
113
  const multi = platforms.length > 1;
103
114
  const results = await Promise.all(
104
115
  platforms.map(async (plat) => {
105
116
  const targetDir = multi ? path.join(outDirPath, plat) : outDirPath;
106
- const outFile = await emitPlatform({ plat, name, targetDir, html, configJSON, backendDir, hasBackend, autoDownload });
117
+ const outFile = await emitPlatform({ plat, name, version, targetDir, html, configJSON, backendDir, hasBackend, autoDownload, icon });
107
118
  return { plat, outFile };
108
119
  })
109
120
  );
@@ -111,7 +122,7 @@ async function build(projectDir, opts = {}) {
111
122
  return { results };
112
123
  }
113
124
 
114
- async function emitPlatform({ plat, name, targetDir, html, configJSON, backendDir, hasBackend, autoDownload }) {
125
+ async function emitPlatform({ plat, name, version, targetDir, html, configJSON, backendDir, hasBackend, autoDownload, icon }) {
115
126
  // 取预编译壳二进制
116
127
  const shell = localShellPath(plat);
117
128
  if (!fs.existsSync(shell)) {
@@ -123,6 +134,12 @@ async function emitPlatform({ plat, name, targetDir, html, configJSON, backendDi
123
134
  }
124
135
  process.stdout.write(`[freedom] 本地无 ${plat} 壳,尝试自动下载...\n`);
125
136
  await downloadShell(plat);
137
+ } else {
138
+ // 壳格式校验:防止 mac/linux 平台误用 Windows 假壳被静默分发(历史缺陷:shell/<darwin-*>/<linux-*> 曾误填 Windows PE 副本)。
139
+ const formatIssue = validateLocalShell(plat);
140
+ if (formatIssue) {
141
+ throw new Error(formatIssue);
142
+ }
126
143
  }
127
144
 
128
145
  await fsp.mkdir(targetDir, { recursive: true });
@@ -135,6 +152,11 @@ async function emitPlatform({ plat, name, targetDir, html, configJSON, backendDi
135
152
  await fsp.chmod(outFile, 0o755);
136
153
  }
137
154
 
155
+ // 自定义 exe 图标(仅 Windows PE 支持嵌入 .ico 资源;mac 用 .icns 走 .app 分支)
156
+ if (isWinPlat(plat) && icon) {
157
+ await applyWindowsIcon(outFile, icon);
158
+ }
159
+
138
160
  // 写 resources:页面 + 配置 + 后端
139
161
  const resDir = path.join(targetDir, 'resources');
140
162
  await fsp.mkdir(resDir, { recursive: true });
@@ -144,15 +166,158 @@ async function emitPlatform({ plat, name, targetDir, html, configJSON, backendDi
144
166
  copyDir(backendDir, path.join(resDir, 'backend'));
145
167
  }
146
168
 
169
+ // macOS:额外生成 .app bundle + .app.zip(供 mac 用户解压即用)
170
+ // 壳加载 exe 同目录 resources/,故把 resources 放进 Contents/MacOS/ 即可运行,无需改壳。
171
+ if (isMacPlat(plat)) {
172
+ const appZip = createMacApp({ targetDir, name, plat, exeName, resDir, version, icon });
173
+ process.stdout.write(
174
+ `[freedom] macOS 产物已打包为 .app.zip:${path.relative(process.cwd(), appZip)}\n`
175
+ );
176
+ }
177
+
147
178
  return outFile;
148
179
  }
149
180
 
181
+ // 把 .ico 图标注入到 Windows exe 的 PE 资源(RT_ICON + RT_GROUP_ICON)。
182
+ // 依赖 rcedit(随 npm 包分发的预编译 rcedit-x64.exe,无需 Go/资源编译器)。
183
+ async function applyWindowsIcon(exePath, iconPath) {
184
+ const ext = path.extname(iconPath).toLowerCase();
185
+ if (ext !== '.ico') {
186
+ process.stdout.write(
187
+ `[freedom] 提示:Windows exe 图标需 .ico 格式,已跳过(${iconPath})。\n`
188
+ );
189
+ return;
190
+ }
191
+ if (process.platform !== 'win32') {
192
+ process.stdout.write(
193
+ '[freedom] 提示:exe 图标注入需在 Windows 本机执行,已跳过。\n'
194
+ );
195
+ return;
196
+ }
197
+ const rcedit = require('rcedit'); // 惰性加载,避免未配置 icon 时增加启动开销
198
+ await rcedit(exePath, { icon: iconPath });
199
+ process.stdout.write(`[freedom] 已注入 exe 图标:${iconPath}\n`);
200
+ }
201
+
202
+ // 解析 cfg.icon:相对项目根或绝对路径 -> 绝对路径;未配置返回 null。
203
+ // 不校验格式,格式由各平台注入逻辑决定(win 需 .ico / mac 需 .icns)。
204
+ function resolveIcon(dir, icon) {
205
+ if (!icon) return null;
206
+ const p = path.isAbsolute(icon) ? icon : path.resolve(dir, icon);
207
+ if (!fs.existsSync(p)) {
208
+ throw new Error(`未找到图标文件:${icon}(已尝试 ${p})。请检查 freedom.config.js 的 icon 配置。`);
209
+ }
210
+ return p;
211
+ }
212
+
213
+ // 把已分发的裸产物升级为标准 .app bundle,并压缩为 .app.zip
214
+ function createMacApp({ targetDir, name, plat, exeName, resDir, version, icon }) {
215
+ const appDir = path.join(targetDir, `${name}.app`);
216
+ const contents = path.join(appDir, 'Contents');
217
+ const macos = path.join(contents, 'MacOS');
218
+ const appRes = path.join(macos, 'resources');
219
+ fs.mkdirSync(macos, { recursive: true });
220
+ fs.mkdirSync(path.join(contents, 'Resources'), { recursive: true });
221
+
222
+ // 可执行文件:exe 名与 app 同名,放入 MacOS/
223
+ const exeDst = path.join(macos, name);
224
+ fs.copyFileSync(path.join(targetDir, exeName), exeDst);
225
+ fs.chmodSync(exeDst, 0o755);
226
+
227
+ // resources -> MacOS/resources(壳的运行时目录)
228
+ if (fs.existsSync(resDir)) {
229
+ copyDir(resDir, appRes);
230
+ }
231
+
232
+ // 自定义 .app 图标:icon 为 .icns 时放入 Resources/ 并在 Info.plist 声明
233
+ let icnsName = null;
234
+ if (icon && path.extname(icon).toLowerCase() === '.icns') {
235
+ icnsName = 'icon.icns';
236
+ fs.copyFileSync(icon, path.join(contents, 'Resources', icnsName));
237
+ } else if (icon) {
238
+ process.stdout.write(
239
+ `[freedom] 提示:macOS .app 图标需 .icns 格式,已跳过(${icon})。\n`
240
+ );
241
+ }
242
+
243
+ // Info.plist
244
+ fs.writeFileSync(path.join(contents, 'Info.plist'), renderInfoPlist(name, version, icnsName), 'utf8');
245
+
246
+ // .app.zip:解压即得 .app,拖入 /Applications 即可使用
247
+ const zipPath = path.join(targetDir, `${name}-${plat}.app.zip`);
248
+ zipDir(zipPath, appDir);
249
+ return zipPath;
250
+ }
251
+
252
+ function renderInfoPlist(name, version, icnsName) {
253
+ const safe = String(name).replace(/&/g, '&amp;');
254
+ const bundleId = `com.freedom.app.${String(name).toLowerCase().replace(/[^a-z0-9.-]/g, '-')}`;
255
+ const iconEntry = icnsName
256
+ ? ` <key>CFBundleIconFile</key>\n <string>${icnsName}</string>\n`
257
+ : '';
258
+ return `<?xml version="1.0" encoding="UTF-8"?>
259
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
260
+ <plist version="1.0">
261
+ <dict>
262
+ <key>CFBundleName</key>
263
+ <string>${safe}</string>
264
+ <key>CFBundleDisplayName</key>
265
+ <string>${safe}</string>
266
+ ${iconEntry} <key>CFBundleExecutable</key>
267
+ <string>${safe}</string>
268
+ <key>CFBundleIdentifier</key>
269
+ <string>${bundleId}</string>
270
+ <key>CFBundleVersion</key>
271
+ <string>${version}</string>
272
+ <key>CFBundleShortVersionString</key>
273
+ <string>${version}</string>
274
+ <key>CFBundlePackageType</key>
275
+ <string>APPL</string>
276
+ <key>CFBundleInfoDictionaryVersion</key>
277
+ <string>6.0</string>
278
+ <key>LSMinimumSystemVersion</key>
279
+ <string>10.13</string>
280
+ <key>NSHighResolutionCapable</key>
281
+ <true/>
282
+ <key>NSAppTransportSecurity</key>
283
+ <dict>
284
+ <key>NSAllowsArbitraryLoads</key>
285
+ <true/>
286
+ </dict>
287
+ </dict>
288
+ </plist>
289
+ `;
290
+ }
291
+
292
+ // 用系统 tar(Windows 为 bsdtar)把目录压成 zip:跨平台可用,无需额外依赖
293
+ function zipDir(zipPath, dir) {
294
+ const parent = path.dirname(dir);
295
+ const base = path.basename(dir);
296
+ if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
297
+ const res = spawnSync('tar', ['-a', '-c', '-f', zipPath, base], { cwd: parent, encoding: 'utf8' });
298
+ if (res.error || res.status !== 0) {
299
+ throw new Error(
300
+ `打包 ${base} 为 zip 失败:${(res.stderr || res.stdout || '').trim() || res.error.message}`
301
+ );
302
+ }
303
+ }
304
+
305
+ // 从项目 package.json 取版本号(cfg.version 优先)
306
+ function appVersionFromPkg(dir) {
307
+ try {
308
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
309
+ return typeof pkg.version === 'string' ? pkg.version : null;
310
+ } catch (e) {
311
+ return null;
312
+ }
313
+ }
314
+
150
315
  // 渲染 resources/config.json(与壳 configfile.go 的 runtimeConfigFile 字段对齐)
151
316
  function renderConfigJSON(cfg, name) {
152
317
  const obj = {
153
318
  name,
154
319
  title: cfg.title || cfg.name || name,
155
- titlebar: cfg.titlebar || 'native',
320
+ titlebar: cfg.titlebar || 'frameless',
156
321
  width: intVal(cfg.width, 1024),
157
322
  height: intVal(cfg.height, 720),
158
323
  minWidth: intVal(cfg.minWidth, 400),
@@ -230,18 +395,19 @@ function writeViteCacheMarker(dir) {
230
395
  }
231
396
 
232
397
  function ensureNodeModules(dir) {
233
- // 缺陷修复:node_modules 存在但 package.json 更新过(依赖变更)时自动重装。
398
+ // 依赖变更检测:package.json package-lock.json 新,说明依赖声明有更新,自动重装。
399
+ // 以 package-lock.json(npm install 后必然生成)为基准,比旧版依赖 node_modules/.package-lock.json 更可靠。
234
400
  const pkgFile = path.join(dir, 'package.json');
401
+ const lockFile = path.join(dir, 'package-lock.json');
235
402
  const nmDir = path.join(dir, 'node_modules');
236
- const marker = path.join(nmDir, '.package-lock.json');
237
403
  if (!fs.existsSync(nmDir)) {
238
404
  const res = run('npm', ['install'], { cwd: dir, stdio: 'inherit' });
239
405
  if (res.status !== 0) throw new Error('npm install 失败。');
240
406
  return;
241
407
  }
242
- if (fs.existsSync(pkgFile) && fs.existsSync(marker)) {
408
+ if (fs.existsSync(pkgFile) && fs.existsSync(lockFile)) {
243
409
  const pkgMtime = fs.statSync(pkgFile).mtimeMs;
244
- const lockMtime = fs.statSync(marker).mtimeMs;
410
+ const lockMtime = fs.statSync(lockFile).mtimeMs;
245
411
  if (pkgMtime > lockMtime + 1000) {
246
412
  process.stdout.write('[freedom] package.json 已更新,重新安装依赖...\n');
247
413
  const res = run('npm', ['install'], { cwd: dir, stdio: 'inherit' });
package/lib/cli.js CHANGED
@@ -21,9 +21,11 @@ freedom - Freedom 桌面壳打包工具 v${VERSION}
21
21
  --no-cache 忽略前端构建缓存,强制重新打包
22
22
  freedom titlebar <native|hidden|frameless>
23
23
  一键切换标题栏策略
24
+ freedom icon <path> 设置应用图标(Windows 需 .ico,macOS 需 .icns)
24
25
  freedom shell list 列出本地已就绪的预编译壳
25
- freedom shell download <plat> 从 GitHub Releases 下载预编译壳
26
- freedom shell build <plat> 本地用 Go 编译壳(需要 Go)
26
+ freedom shell download <plat> 从 GitHub Releases 下载预编译壳(无需 Go)
27
+ freedom shell build <plat> 本地用 Go 编译壳(可选,壳已预编译一般无需)
28
+ freedom dmg [--platform <plat>] 将已构建的 .app 打包为 .dmg(需在 macOS 上执行)
27
29
  freedom config 查看当前配置
28
30
  freedom config get <key> 读取单个配置项
29
31
  freedom config set <key> <value> 修改单个配置项
@@ -34,10 +36,19 @@ freedom - Freedom 桌面壳打包工具 v${VERSION}
34
36
  平台(<p>/<plat>):win-x64 / darwin-x64 / darwin-arm64 / linux-x64 / linux-arm64,
35
37
  快捷别名:win / mac / linux / all。
36
38
 
39
+ macOS 产物说明:
40
+ freedom build --platform mac 直接产出 <app>.app.zip(解压即得 .app,
41
+ 拖入 /Applications 即可使用,无需安装任何语言运行时);如需 .dmg,
42
+ 在 macOS 上执行 freedom dmg 用系统 hdiutil 生成。
43
+
37
44
  标题栏策略说明:
38
- native 保留系统原生标题栏(默认)
45
+ native 保留系统原生标题栏
39
46
  hidden 隐藏标题栏视觉,仅保留 Windows 原生最小化/最大化/关闭按钮
40
- frameless 完全无边框,按钮由前端自绘(模板已内置示例)
47
+ frameless 完全无边框,标题栏不存在,关闭/最大化/最小化按钮由前端自绘(默认,模板已内置示例)
48
+
49
+ 图标说明:
50
+ Windows exe 图标:在 freedom.config.js 配置 icon(.ico 路径),构建时自动注入;
51
+ macOS .app 图标:icon 配置 .icns 路径即可;未配置则使用壳默认图标。
41
52
  `.trim();
42
53
  }
43
54
 
@@ -90,6 +101,10 @@ async function run(argv) {
90
101
  return await runShell(rest);
91
102
  }
92
103
 
104
+ case 'dmg': {
105
+ return await runDmg(rest);
106
+ }
107
+
93
108
  case 'titlebar': {
94
109
  const mode = rest[0];
95
110
  if (!['native', 'hidden', 'frameless'].includes(mode)) {
@@ -102,6 +117,20 @@ async function run(argv) {
102
117
  return 0;
103
118
  }
104
119
 
120
+ case 'icon': {
121
+ const iconPath = rest[0];
122
+ if (!iconPath) {
123
+ console.error('[freedom] 用法:freedom icon <path>');
124
+ console.error(' 示例:freedom icon icon.ico (Windows exe 图标,.ico 格式)');
125
+ console.error(' freedom icon icon.icns (macOS .app 图标,.icns 格式)');
126
+ return 1;
127
+ }
128
+ setConfig(process.cwd(), 'icon', iconPath);
129
+ console.log(`[freedom] icon 已设置为:${iconPath}`);
130
+ console.log(' 运行 freedom build 重新打包生效。');
131
+ return 0;
132
+ }
133
+
105
134
  case 'config': {
106
135
  const sub = rest[0];
107
136
  if (sub === 'get') {
@@ -183,6 +212,7 @@ async function runShell(rest) {
183
212
  }
184
213
  const dest = buildShell(plat);
185
214
  console.log(`[freedom] 已编译 ${plat} 壳:${dest}`);
215
+ console.log('[freedom] 提示:壳已预编译随包分发,一般无需本地编译。');
186
216
  return 0;
187
217
  }
188
218
  default:
@@ -191,6 +221,45 @@ async function runShell(rest) {
191
221
  }
192
222
  }
193
223
 
224
+ // freedom dmg:在 macOS 上把已构建的 .app 打包为 .dmg(依赖系统 hdiutil)
225
+ async function runDmg(rest) {
226
+ const { loadConfig, nativePlatform } = require('./utils');
227
+ const { makeDmg } = require('./dmg');
228
+ const dir = process.cwd();
229
+ const cfg = await loadConfig(dir);
230
+ const name = (cfg.name || 'freedom-app').replace(/[^a-zA-Z0-9_.-]/g, '-');
231
+ const outDir = String(cfg.outDir || 'dist').trim() || 'dist';
232
+
233
+ const platArg = rest.find((a) => a.startsWith('--platform'));
234
+ const plat = platArg
235
+ ? (platArg.includes('=') ? platArg.split('=')[1] : rest[rest.indexOf(platArg) + 1])
236
+ : nativePlatform();
237
+ if (!plat || !plat.startsWith('darwin')) {
238
+ console.error('[freedom] dmg 仅支持 macOS 平台(darwin-x64 / darwin-arm64)。');
239
+ return 1;
240
+ }
241
+
242
+ const baseDir = path.resolve(dir, outDir);
243
+ // 兼容 build --platform all 的 outDir/<plat> 布局
244
+ const candidates = [path.join(baseDir, `${name}.app`), path.join(baseDir, plat, `${name}.app`)];
245
+ const appDir = candidates.find((p) => fs.existsSync(p));
246
+ if (!appDir) {
247
+ console.error(`[freedom] 未找到 ${name}.app(已检查 ${candidates.join(' / ')})。`);
248
+ console.error(' 请先在 macOS 上运行 freedom build --platform mac 生成 .app。');
249
+ return 1;
250
+ }
251
+
252
+ const outPath = path.join(path.dirname(appDir), `${name}-${plat}.dmg`);
253
+ try {
254
+ const dmgPath = await makeDmg(appDir, outPath, name);
255
+ console.log(`[freedom] 已生成 dmg:${dmgPath}`);
256
+ return 0;
257
+ } catch (e) {
258
+ console.error(`[freedom] ${e.message}`);
259
+ return 1;
260
+ }
261
+ }
262
+
194
263
  function openBrowser(file) {
195
264
  const { spawn } = require('child_process');
196
265
  const plat = process.platform;
package/lib/config.js CHANGED
@@ -13,7 +13,8 @@ const KNOWN_KEYS = {
13
13
  minHeight: 300,
14
14
  center: true,
15
15
  debug: false,
16
- titlebar: 'native',
16
+ titlebar: 'frameless',
17
+ icon: undefined,
17
18
  outDir: 'dist',
18
19
  };
19
20
 
@@ -32,7 +33,8 @@ function setConfig(dir, key, value) {
32
33
  }
33
34
  }
34
35
 
35
- const regex = new RegExp(`(${key}\\s*:\\s*)('[^']*'|\\d+|true|false|undefined|null)`, 'g');
36
+ // 同时兼容单引号与双引号字符串写法(模板统一单引号,用户手写可能用双引号)。
37
+ const regex = new RegExp(`(${key}\\s*:\\s*)(\"[^\"]*\"|'[^']*'|\\d+|true|false|undefined|null)`, 'g');
36
38
  if (!regex.test(text)) {
37
39
  throw new Error(`配置项 ${key} 未在 freedom.config.js 中找到,请手动添加。`);
38
40
  }
package/lib/dmg.js ADDED
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ // freedom dmg:把已构建的 .app bundle 打包为 .dmg(macOS 分发格式)
4
+ //
5
+ // 说明:.dmg 依赖 macOS 系统自带的 hdiutil,只能在 macOS 上生成。
6
+ // Windows / Linux 上运行 freedom build 已直接产出 .app.zip(解压即得 .app,
7
+ // 拖入 /Applications 即可使用),无需 dmg;如仍需 dmg,把产物拷到 Mac 上执行
8
+ // freedom dmg 即可。
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { spawnSync } = require('child_process');
13
+
14
+ async function makeDmg(appDir, outPath, volName) {
15
+ if (process.platform !== 'darwin') {
16
+ throw new Error(
17
+ '.dmg 依赖 macOS 系统 hdiutil,只能在 macOS 上生成。\n' +
18
+ ' 当前平台可直接分发 .app.zip(解压即得 .app,拖入 /Applications 即可使用)。\n' +
19
+ ' 如需 .dmg,请把 .app 拷到 macOS 上运行:freedom dmg'
20
+ );
21
+ }
22
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
23
+ if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
24
+
25
+ const args = [
26
+ 'create',
27
+ '-volname',
28
+ String(volName || path.basename(appDir, '.app')),
29
+ '-srcfolder',
30
+ appDir,
31
+ '-ov',
32
+ '-format',
33
+ 'UDZO',
34
+ outPath,
35
+ ];
36
+ const res = spawnSync('hdiutil', args, { encoding: 'utf8' });
37
+ if (res.error || res.status !== 0) {
38
+ throw new Error(`hdiutil 生成 dmg 失败:\n${(res.stderr || res.stdout || '').trim()}`);
39
+ }
40
+ return outPath;
41
+ }
42
+
43
+ module.exports = { makeDmg };
package/lib/shell.js CHANGED
@@ -27,15 +27,75 @@ const {
27
27
  // GitHub Releases 下载源(可用环境变量覆盖)。
28
28
  // 资产命名约定:freedom-shell-<plat>(单文件二进制,不压缩)。
29
29
  function releaseRepo() {
30
- return process.env.FREEDOM_SHELL_REPO || 'yufengtadian/freedom';
30
+ return process.env.FREEDOM_SHELL_REPO || 'YUfeng12TA/freedom';
31
+ }
32
+
33
+ // 远程壳下载的默认版本:与当前包版本保持一致。
34
+ // 通过环境变量 FREEDOM_SHELL_TAG 可强制覆盖;默认动态读取 package.json 的 version,
35
+ // 避免与包版本漂移(历史曾硬编码 v1.1.10 导致下载到旧壳)。
36
+ function pkgVersion() {
37
+ try {
38
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
39
+ return typeof pkg.version === 'string' && pkg.version ? pkg.version : '1.1.10';
40
+ } catch (e) {
41
+ return '1.1.10';
42
+ }
31
43
  }
32
44
  function releaseTag() {
33
- return process.env.FREEDOM_SHELL_TAG || 'v1.1.10';
45
+ return process.env.FREEDOM_SHELL_TAG || `v${pkgVersion()}`;
34
46
  }
35
47
  function releaseUrl(plat) {
36
48
  return `https://github.com/${releaseRepo()}/releases/download/${releaseTag()}/freedom-shell-${plat}`;
37
49
  }
38
50
 
51
+ // ---- 壳二进制平台格式校验 ----
52
+ // 读取二进制文件头判断真实平台格式,防止"用 Windows 壳冒充 mac/linux 壳"这类假壳
53
+ // 被静默分发(历史缺陷:shell/<darwin-*>/<linux-*> 曾误填 Windows PE 副本)。
54
+ function detectShellFormat(buf) {
55
+ if (!buf || buf.length < 4) return 'unknown';
56
+ // Windows PE:MZ
57
+ if (buf[0] === 0x4d && buf[1] === 0x5a) return 'win';
58
+ // Mach-O 64 位:CF FA ED FE(32 位为 CE FA ED FE,本项目仅 64 位)
59
+ if (buf[0] === 0xcf && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) return 'mac';
60
+ // ELF:7F 45 4C 46
61
+ if (buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46) return 'linux';
62
+ return 'unknown';
63
+ }
64
+
65
+ // 平台 key -> 期望的二进制格式族
66
+ function expectedFormat(plat) {
67
+ if (plat.startsWith('win')) return 'win';
68
+ if (plat.startsWith('darwin')) return 'mac';
69
+ if (plat.startsWith('linux')) return 'linux';
70
+ return 'unknown';
71
+ }
72
+
73
+ // 校验本地壳二进制是否为目标平台的真实格式;不匹配返回错误说明,不抛错(供调用方决策)。
74
+ function validateLocalShell(plat) {
75
+ const p = localShellPath(plat);
76
+ if (!fs.existsSync(p)) {
77
+ return null; // 壳不存在:不校验,由调用方决定下载/编译
78
+ }
79
+ let buf;
80
+ try {
81
+ buf = fs.readFileSync(p);
82
+ } catch (e) {
83
+ return `无法读取壳二进制 ${p}:${e.message}`;
84
+ }
85
+ const fmt = detectShellFormat(buf);
86
+ const want = expectedFormat(plat);
87
+ if (fmt === 'unknown') {
88
+ return `壳二进制 ${p} 不是可识别的 PE/Mach-O/ELF 格式(detect=${fmt})。`;
89
+ }
90
+ if (fmt !== want) {
91
+ return `壳二进制 ${p} 格式与目标平台 ${plat} 不匹配:期望 ${want},实际 ${fmt}。` +
92
+ `这是假壳(历史缺陷曾把 Windows 壳复制到 mac/linux 平台)。` +
93
+ `请运行 freedom shell build ${plat} 在 ${plat} 本机编译真实壳,` +
94
+ `或 freedom shell download ${plat} 拉取 CI 预编译产物。`;
95
+ }
96
+ return null;
97
+ }
98
+
39
99
  // 列出本地已就绪的壳平台
40
100
  function listLocal() {
41
101
  const dir = shellDir();
@@ -136,4 +196,14 @@ function nativePlatformKey() {
136
196
  return `${plat}-${arch}`;
137
197
  }
138
198
 
139
- module.exports = { listLocal, hasShell, downloadShell, buildShell, releaseRepo, releaseTag };
199
+ module.exports = {
200
+ listLocal,
201
+ hasShell,
202
+ downloadShell,
203
+ buildShell,
204
+ releaseRepo,
205
+ releaseTag,
206
+ detectShellFormat,
207
+ expectedFormat,
208
+ validateLocalShell,
209
+ };
package/lib/tui.js CHANGED
@@ -8,7 +8,9 @@ const readline = require('readline');
8
8
  const { init } = require('./init');
9
9
  const { build } = require('./build');
10
10
  const { setConfig, showConfig } = require('./config');
11
- const { tutorialFile, hasConfig, ALL_PLATFORMS } = require('./utils');
11
+ const { tutorialFile, hasConfig, ALL_PLATFORMS, packageRoot } = require('./utils');
12
+
13
+ const TUI_VERSION = require(path.join(packageRoot(), 'package.json')).version;
12
14
 
13
15
  // ---------------- ANSI ----------------
14
16
  const ESC = '\x1b';
@@ -75,7 +77,7 @@ class TUI {
75
77
  const pad = (s) => ` ${s}`;
76
78
  const lines = [
77
79
  '',
78
- ` ${C.fgCyan}${C.bold}▚ F R E E D O M T U I ${C.fgGray}v1.1.10${C.reset}`,
80
+ ` ${C.fgCyan}${C.bold}▚ F R E E D O M T U I ${C.fgGray}v${TUI_VERSION}${C.reset}`,
79
81
  '',
80
82
  top,
81
83
  pad(titleMid),
@@ -191,6 +193,7 @@ const CONFIG_KEYS = [
191
193
  ['center', '启动居中(true/false)'],
192
194
  ['debug', '开发者工具(true/false)'],
193
195
  ['titlebar', '标题栏:native | hidden | frameless'],
196
+ ['icon', '应用图标:.ico(Windows)/ .icns(macOS)路径'],
194
197
  ['outDir', '产物目录:dist(默认)| .(项目根)| 任意路径'],
195
198
  ['backend', '任意语言后端进程(JSON)'],
196
199
  ];
@@ -259,7 +262,7 @@ async function configFlow(tui, cwd) {
259
262
  async function shellFlow(tui) {
260
263
  const { listLocal, downloadShell, buildShell } = require('./shell');
261
264
  for (;;) {
262
- const items = ['查看已就绪壳', '下载壳', '本地编译壳', '返回'];
265
+ const items = ['查看已就绪壳', '下载壳(无需 Go)', '本地编译壳(可选·需 Go)', '返回'];
263
266
  const idx = await tui.menu('壳管理', items);
264
267
  if (idx === null || idx === items.length - 1) return;
265
268
 
@@ -277,7 +280,7 @@ async function shellFlow(tui) {
277
280
  console.log(`${C.fgGreen}[freedom]${C.reset} 已下载 ${ALL_PLATFORMS[p]} 壳:${dest}`);
278
281
  });
279
282
  } else {
280
- const p = await tui.menu('选择要编译的壳平台(需 Go 且仅本机平台)', ALL_PLATFORMS);
283
+ const p = await tui.menu('选择要编译的壳平台(可选·需 Go,壳已预编译随包分发,一般无需编译)', ALL_PLATFORMS);
281
284
  if (p === null) continue;
282
285
  await tui.runTask(async () => {
283
286
  const dest = buildShell(ALL_PLATFORMS[p]);
package/lib/utils.js CHANGED
@@ -42,6 +42,11 @@ function isWinPlat(plat) {
42
42
  return plat.startsWith('win');
43
43
  }
44
44
 
45
+ // macOS 平台(darwin-*):产物需升级为 .app bundle + .app.zip 供 mac 用户解压即用
46
+ function isMacPlat(plat) {
47
+ return plat.startsWith('darwin');
48
+ }
49
+
45
50
  // 平台 key -> 产物可执行文件名(Windows 带 .exe,其余无扩展名)
46
51
  function platformExeName(plat, appName) {
47
52
  return isWinPlat(plat) ? `${appName}.exe` : appName;
@@ -106,6 +111,7 @@ module.exports = {
106
111
  ALL_PLATFORMS,
107
112
  SHELL_EXE_NAME,
108
113
  isWinPlat,
114
+ isMacPlat,
109
115
  platformExeName,
110
116
  nativePlatform,
111
117
  localShellPath,
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
- {
1
+ {
2
2
  "name": "@yufengtadian/freedom-cli",
3
- "version": "1.1.11",
3
+ "version": "1.12.0",
4
4
  "description": "Freedom WebView desktop shell packaging tool - no Go toolchain required, one command packs three-platform desktop apps",
5
- "keywords": ["desktop", "webview", "electron-alternative", "wails", "tauri", "frontend", "cross-platform"],
5
+ "keywords": [
6
+ "desktop",
7
+ "webview",
8
+ "electron-alternative",
9
+ "wails",
10
+ "tauri",
11
+ "frontend",
12
+ "cross-platform"
13
+ ],
6
14
  "license": "MIT",
7
15
  "publishConfig": {
8
16
  "access": "public"
@@ -24,5 +32,8 @@
24
32
  },
25
33
  "engines": {
26
34
  "node": ">=18"
35
+ },
36
+ "dependencies": {
37
+ "rcedit": "^4.0.1"
27
38
  }
28
39
  }
Binary file
@@ -11,7 +11,7 @@ import "freedom-cli-shell/pkg/freedom"
11
11
  func appConfig() freedom.Config {
12
12
  return freedom.Config{
13
13
  Title: "Freedom App",
14
- TitleBar: freedom.TitleBarNative,
14
+ TitleBar: freedom.TitleBarFrameless,
15
15
  Width: 1024,
16
16
  Height: 720,
17
17
  MinWidth: 400,
@@ -8,6 +8,20 @@ import (
8
8
  "unsafe"
9
9
  )
10
10
 
11
+ // 进程级 DPI 感知:让窗口坐标 / WebView 渲染统一按物理像素工作。
12
+ // 若不设置,高 DPI 系统(如 125%/150% 缩放)下 GetWindowRect 返回逻辑坐标,
13
+ // 与物理像素的鼠标坐标错位(UI 自动化难定位),且 WebView 内容渲染模糊。
14
+ // 必须在窗口创建前生效,故放在 init()。
15
+ func init() {
16
+ shcore := syscall.NewLazyDLL("shcore.dll")
17
+ procSetProcessDpiAwareness := shcore.NewProc("SetProcessDpiAwareness")
18
+ r, _, _ := procSetProcessDpiAwareness.Call(2) // PER_MONITOR_DPI_AWARE
19
+ if r != 0 {
20
+ // 回退到 user32 的旧版 API(Win 8 以下 / 调用失败时)
21
+ user32win.NewProc("SetProcessDPIAware").Call()
22
+ }
23
+ }
24
+
11
25
  // Windows 平台原生窗口操作(user32 / dwmapi)。
12
26
  // 供标题栏策略(applyTitleBar)与前端 window.freedom.window.* 控制使用。
13
27
 
@@ -17,10 +31,10 @@ var (
17
31
  procGetWindowLongPtr = user32win.NewProc("GetWindowLongPtrW")
18
32
  procSetWindowLongPtr = user32win.NewProc("SetWindowLongPtrW")
19
33
  procShowWindow = user32win.NewProc("ShowWindow")
20
- procCloseWindow = user32win.NewProc("CloseWindow")
21
34
  procIsZoomed = user32win.NewProc("IsZoomed")
22
35
  procSetWindowPos = user32win.NewProc("SetWindowPos")
23
36
  procSetWindowText = user32win.NewProc("SetWindowTextW")
37
+ procPostMessage = user32win.NewProc("PostMessageW")
24
38
 
25
39
  dwmapi = syscall.NewLazyDLL("dwmapi.dll")
26
40
  procDwmExtendFrameIntoArea = dwmapi.NewProc("DwmExtendFrameIntoClientArea")
@@ -31,6 +45,7 @@ const (
31
45
  wsSysMenu = 0x00080000
32
46
  wsThickFrame = 0x00040000
33
47
 
48
+ wmClose = 0x0010 // WM_CLOSE:请求窗口正常关闭(触发 GoWnd 的关闭回调)
34
49
  swHide = 0
35
50
  swShow = 5
36
51
  swMinimize = 6
@@ -89,7 +104,9 @@ func windowControl(hwnd uintptr, action string, mode TitleBarMode) (interface{},
89
104
  }
90
105
  return nil, nil
91
106
  case "close":
92
- procCloseWindow.Call(hwnd)
107
+ // 发送 WM_CLOSE 请求正常关闭(触发 GoWnd 关闭回调,释放 WebView 资源)。
108
+ // 注意:不能使用 user32.CloseWindow —— 该 API 语义是最小化窗口。
109
+ procPostMessage.Call(hwnd, wmClose, 0, 0)
93
110
  return nil, nil
94
111
  case "isMaximized":
95
112
  return isZoomed(hwnd), nil
@@ -19,11 +19,18 @@ export default {
19
19
  debug: false,
20
20
 
21
21
  // 标题栏策略,在终端随时可改,三选一:
22
- // 'native' - 保留系统原生标题栏(默认)
22
+ // 'native' - 保留系统原生标题栏
23
23
  // 'hidden' - 隐藏标题栏视觉,仅保留 Windows 原生最小化 / 最大化 / 关闭按钮
24
- // 'frameless' - 完全无边框,按钮由前端自绘(模板已内置自绘标题栏示例)
24
+ // 'frameless' - 完全无边框,标题栏不存在,关闭 / 最大化 / 最小化按钮由前端自绘(模板已内置示例,默认)
25
25
  // 终端命令:freedom titlebar <native|hidden|frameless>
26
- titlebar: 'native',
26
+ titlebar: 'frameless',
27
+
28
+ // 应用图标(可执行文件 / .app 的图标):
29
+ // Windows 需 .ico(推荐含 16/32/48/256 多尺寸),构建时注入 exe 资源;
30
+ // macOS 需 .icns,构建时放入 .app/Contents/Resources 并写入 Info.plist。
31
+ // 值为相对项目根目录的路径或绝对路径,未配置则使用壳默认图标。
32
+ // 终端命令:freedom icon <path>(Windows 主用)
33
+ icon: undefined,
27
34
 
28
35
  // 产物输出目录(相对项目根目录或绝对路径):
29
36
  // 'dist' - 输出到 dist/ 子目录(默认)
Binary file
Binary file
Binary file
Binary file