@yufengtadian/freedom-cli 1.0.0 → 1.1.11
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 +45 -8
- package/lib/build.js +189 -104
- package/lib/cli.js +68 -4
- package/lib/shell.js +139 -0
- package/lib/tui.js +331 -0
- package/lib/utils.js +47 -0
- package/package.json +5 -4
- package/shell/darwin-arm64/freedom-shell +0 -0
- package/shell/darwin-x64/freedom-shell +0 -0
- package/shell/linux-arm64/freedom-shell +0 -0
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/gen_config.go +6 -3
- package/templates/go/go.sum +2 -0
- package/templates/go/pkg/freedom/assets/index.html +108 -0
- package/templates/go/pkg/freedom/assets_embed.go +5 -2
- package/templates/go/pkg/freedom/backend_proc.go +10 -0
- package/templates/go/pkg/freedom/configfile.go +121 -0
- package/templates/go/pkg/freedom/freedom.go +16 -6
- package/tutorial/tutorial.html +7 -2
package/README.md
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
# freedom-cli
|
|
2
2
|
|
|
3
|
-
Freedom 桌面壳打包工具:把你的 Web
|
|
3
|
+
Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.1.11)。
|
|
4
4
|
|
|
5
|
-
基于自研 Freedom WebView 壳层(对标 Wails / Tauri):前端完全自由、后端可任意语言、渲染复用系统 WebView(Windows WebView2 / macOS WKWebView / Linux WebKitGTK
|
|
5
|
+
基于自研 Freedom WebView 壳层(对标 Wails / Tauri):前端完全自由、后端可任意语言、渲染复用系统 WebView(Windows WebView2 / macOS WKWebView / Linux WebKitGTK),产物为单个可执行文件 + resources 目录,前端页面内存加载,不占本地端口。
|
|
6
|
+
|
|
7
|
+
**v1.1.11 核心升级**:不再依赖 Go 工具链。壳层以预编译二进制随 npm 包分发(`shell/<platform>/`),`freedom build` 直接复制壳 + 写入前端与配置即可出包,Windows / macOS / Linux 三平台一次全出,任意前后端语言自由组合。
|
|
8
|
+
|
|
9
|
+
**v1.1.11 新特性**:
|
|
10
|
+
- `freedom tui` 交互式终端界面:菜单化完成打包 / 新建项目 / 修改配置 / 壳管理 / 教程,零依赖、方向键 + 回车导航、支持搜索过滤与多选;
|
|
11
|
+
- 前端构建缓存:源码无改动时跳过重复 `vite build`(秒级复用,典型加速 10 倍+),`--no-cache` 强制全量重建;
|
|
12
|
+
- 多平台分发并行化:`--platform all` 各平台同时复制分发,全平台打包耗时显著缩短。
|
|
6
13
|
|
|
7
14
|
## 安装
|
|
8
15
|
|
|
@@ -20,16 +27,28 @@ freedom init my-app
|
|
|
20
27
|
cd my-app
|
|
21
28
|
npm install
|
|
22
29
|
|
|
23
|
-
# 2.
|
|
30
|
+
# 2. 交互式终端界面(方向键 + 回车选择,q 返回)
|
|
31
|
+
freedom tui
|
|
32
|
+
|
|
33
|
+
# 3. 调整标题栏(可选,随时可改)
|
|
24
34
|
freedom titlebar native # 保留系统原生标题栏(默认)
|
|
25
35
|
freedom titlebar hidden # 隐藏标题栏视觉,仅保留 Windows 原生最小化/最大化/关闭按钮
|
|
26
36
|
freedom titlebar frameless # 完全无边框,按钮由前端自绘(模板已内置示例)
|
|
27
37
|
|
|
28
|
-
#
|
|
29
|
-
freedom build
|
|
38
|
+
# 4. 打包
|
|
39
|
+
freedom build # 默认当前平台
|
|
40
|
+
freedom build --platform all # 三平台全量(win + mac + linux)
|
|
41
|
+
freedom build --platform win-x64 # 仅 Windows
|
|
42
|
+
freedom build --platform mac-arm64 # 仅 macOS Apple Silicon
|
|
43
|
+
freedom build --platform linux-x64 # 仅 Linux
|
|
44
|
+
freedom build --no-cache # 忽略前端构建缓存,强制重建
|
|
30
45
|
```
|
|
31
46
|
|
|
32
|
-
构建产物默认输出到 `dist
|
|
47
|
+
构建产物默认输出到 `dist/`。`dist/<应用名>.exe`(或对应平台可执行文件)为壳层程序,运行时从 `dist/resources/` 加载前端页面与配置,不弹 cmd 黑窗。
|
|
48
|
+
|
|
49
|
+
**构建缓存**:CLI 会在 `.freedom/vite-dist/` 记录前端源码指纹,源码无改动时复用上次构建产物直接进入分发阶段(秒级完成)。修改了 `src/` / `index.html` / `vite.config.js` 等任一处会触发自动重建;需要强制全量时可加 `--no-cache`。
|
|
50
|
+
|
|
51
|
+
**三平台打包**:壳层二进制随包分发,无需本机安装 Go 或交叉编译工具链。首次构建某平台时若本地缺少对应壳二进制,CLI 自动从 GitHub Releases 下载(`freedom shell download <platform>` 可手动补齐);仅当你想用本机 Go 工具链亲自编译壳时才需要 `freedom shell build`(只产出本机平台)。多平台分发为并行执行,全平台打包耗时大幅缩短。
|
|
33
52
|
|
|
34
53
|
产物目录可在 `freedom.config.js` 的 `outDir` 中调整:默认 `'dist'`,设为 `'.'` 则直接输出到项目根目录(dist 的上级),设为任意相对 / 绝对路径亦可。输出到项目根目录时会自动跳过 `index.html` 副本,避免覆盖项目源文件。
|
|
35
54
|
|
|
@@ -78,19 +97,37 @@ window.freedom.window.minimize(); // 窗口控制
|
|
|
78
97
|
## 构建要求
|
|
79
98
|
|
|
80
99
|
- Node.js >= 18
|
|
81
|
-
- Go
|
|
100
|
+
- 不需要 Go 工具链(壳层为随包分发的预编译二进制)
|
|
101
|
+
- 可选:`freedom shell build` 需本机 Go(含 CGO,Windows 需 MSVC 编译环境),仅用于亲自编译壳
|
|
82
102
|
|
|
83
103
|
## 命令
|
|
84
104
|
|
|
85
105
|
```
|
|
106
|
+
freedom tui # 交互式终端界面
|
|
86
107
|
freedom init <目录> [--force]
|
|
87
|
-
freedom build
|
|
108
|
+
freedom build [--platform win-x64|mac-arm64|linux-x64|all] [--no-cache]
|
|
88
109
|
freedom titlebar <native|hidden|frameless>
|
|
89
110
|
freedom config [get|set]
|
|
111
|
+
freedom shell list|download <platform>|build <platform>
|
|
90
112
|
freedom tutorial
|
|
91
113
|
freedom help
|
|
92
114
|
```
|
|
93
115
|
|
|
116
|
+
## 任意语言后端
|
|
117
|
+
|
|
118
|
+
壳与后端进程通过 stdin/stdout 的 NDJSON/JSON-RPC 桥接(协议语言无关),因此后端可用任意语言实现(Node / Python / Rust / Go / C#…)。
|
|
119
|
+
|
|
120
|
+
在 `freedom.config.js` 中声明后端进程,`freedom build` 会把你的 `backend/` 目录原样分发到 `dist/resources/backend/`,壳启动时自动以 `resources/` 为工作目录拉起后端进程(Windows 下后端黑窗同样隐藏):
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
export default {
|
|
124
|
+
...
|
|
125
|
+
backend: { command: 'node', args: ['backend/main.mjs'] },
|
|
126
|
+
};
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
后端只需读写 stdin/stdout,示例见模板注释与 `tutorial` 教程。
|
|
130
|
+
|
|
94
131
|
## 关于生成产物
|
|
95
132
|
|
|
96
133
|
本工具生成的所有文件(含模板与构建产物)均干净整洁,无任何冗余尾注,可直接作为交付物使用。
|
package/lib/build.js
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// freedom build:前端打包 + 通用壳分发(v1.1.10,零语言工具链)
|
|
4
|
+
//
|
|
5
|
+
// 流程(不再调用 go build / 不依赖 Go):
|
|
6
|
+
// 1. 前端打包:npm install(如缺依赖)+ vite build -> .freedom/vite-dist/index.html
|
|
7
|
+
// 2. 选择目标平台:--platform <win|mac|linux|all>(默认当前平台)
|
|
8
|
+
// 3. 每个平台:
|
|
9
|
+
// - 取预编译通用壳二进制(包内 shell/<plat>/,缺失自动从 GitHub Releases 下载)
|
|
10
|
+
// - 写 resources/index.html(前端单文件页)
|
|
11
|
+
// - 写 resources/config.json(窗口 + 后端配置,壳运行时读取)
|
|
12
|
+
// - 复制壳为 outDir/<app>[.exe]
|
|
13
|
+
// - 复制 backend/ 到 resources/backend/(配置了 backend 时)
|
|
14
|
+
//
|
|
15
|
+
// 产物不嵌入 HTML,页面与配置均在 exe 同目录 resources/ 下,因此壳可复用、
|
|
16
|
+
// 可跨平台分发、无需在目标机器安装任何语言运行时。
|
|
17
|
+
|
|
3
18
|
const fs = require('fs');
|
|
19
|
+
const fsp = fs.promises;
|
|
4
20
|
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
21
|
const { spawnSync } = require('child_process');
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
const { copyDir } = require('./utils');
|
|
23
|
+
const {
|
|
24
|
+
ALL_PLATFORMS,
|
|
25
|
+
isWinPlat,
|
|
26
|
+
platformExeName,
|
|
27
|
+
nativePlatform,
|
|
28
|
+
localShellPath,
|
|
29
|
+
} = require('./utils');
|
|
30
|
+
const { hasShell, downloadShell } = require('./shell');
|
|
14
31
|
|
|
15
32
|
function run(cmd, args, opts = {}) {
|
|
16
33
|
// Windows 下 npm 是 .cmd 批处理,必须经 shell 执行
|
|
@@ -29,12 +46,17 @@ function run(cmd, args, opts = {}) {
|
|
|
29
46
|
return res;
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
// --platform 解析:win|mac|linux|all 或平台 key(win-x64 等)
|
|
50
|
+
function parsePlatforms(raw) {
|
|
51
|
+
if (!raw) return [nativePlatform()];
|
|
52
|
+
const v = String(raw).toLowerCase();
|
|
53
|
+
if (v === 'all') return ALL_PLATFORMS;
|
|
54
|
+
const keyMap = { win: 'win-x64', mac: 'darwin-x64', linux: 'linux-x64' };
|
|
55
|
+
if (keyMap[v]) return [keyMap[v]];
|
|
56
|
+
if (ALL_PLATFORMS.includes(v)) return [v];
|
|
57
|
+
throw new Error(
|
|
58
|
+
`未知平台:${raw}。可选:win / mac / linux / all,或 ${ALL_PLATFORMS.join(' / ')}`
|
|
59
|
+
);
|
|
38
60
|
}
|
|
39
61
|
|
|
40
62
|
async function build(projectDir, opts = {}) {
|
|
@@ -47,118 +69,185 @@ async function build(projectDir, opts = {}) {
|
|
|
47
69
|
const cfg = await loadConfig(dir);
|
|
48
70
|
|
|
49
71
|
const name = (cfg.name || 'freedom-app').replace(/[^a-zA-Z0-9_.-]/g, '-');
|
|
50
|
-
const titlebar = TITLEBAR_MAP[cfg.titlebar] || 'freedom.TitleBarNative';
|
|
51
|
-
|
|
52
|
-
// 产物目录:默认 dist,可配置为 '.'(项目根目录,即 dist 的上级)或任意相对/绝对目录。
|
|
53
72
|
const outDir = String(cfg.outDir || 'dist').trim() || 'dist';
|
|
54
73
|
const outDirPath = path.resolve(dir, outDir);
|
|
55
|
-
const
|
|
56
|
-
const
|
|
74
|
+
const platforms = parsePlatforms(opts.platform);
|
|
75
|
+
const autoDownload = opts.autoDownload !== false;
|
|
57
76
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// 1) 前端打包:npm install(如缺依赖)+ vite build -> .freedom/vite-dist/index.html
|
|
77
|
+
// 1) 前端打包(带缓存:源码未变更时复用上次 vite 产物,跳过 npm run build)
|
|
61
78
|
ensureNodeModules(dir);
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
79
|
+
const useCache = opts.noCache !== true;
|
|
80
|
+
if (useCache && !viteNeedsRebuild(dir)) {
|
|
81
|
+
process.stdout.write('[freedom] 前端源码无改动,复用构建缓存...\n');
|
|
82
|
+
} else {
|
|
83
|
+
const vite = run('npm', ['run', 'build'], { cwd: dir });
|
|
84
|
+
if (vite.status !== 0) {
|
|
85
|
+
throw new Error(`前端打包失败:\n${vite.stdout}\n${vite.stderr}`);
|
|
86
|
+
}
|
|
87
|
+
writeViteCacheMarker(dir);
|
|
65
88
|
}
|
|
66
89
|
const distHtml = path.join(dir, '.freedom', 'vite-dist', 'index.html');
|
|
67
90
|
if (!fs.existsSync(distHtml)) {
|
|
68
91
|
throw new Error(`前端打包完成但未找到 ${distHtml},请检查 vite 配置(vite-plugin-singlefile)。`);
|
|
69
92
|
}
|
|
93
|
+
const html = fs.readFileSync(distHtml, 'utf8');
|
|
94
|
+
const configJSON = renderConfigJSON(cfg, name);
|
|
70
95
|
|
|
71
|
-
// 2)
|
|
72
|
-
const buildDir = path.join(dir, '.freedom', 'build');
|
|
73
|
-
fs.rmSync(buildDir, { recursive: true, force: true });
|
|
74
|
-
fs.mkdirSync(buildDir, { recursive: true });
|
|
75
|
-
copyDir(goTemplateDir(), buildDir);
|
|
76
|
-
|
|
77
|
-
// 3) 写入前端产物与后端
|
|
78
|
-
fs.copyFileSync(distHtml, path.join(buildDir, 'pkg', 'freedom', 'assets', 'index.html'));
|
|
96
|
+
// 2) 后端目录(若配置了 backend 进程)
|
|
79
97
|
const backendDir = path.join(dir, cfg.backendDir || 'backend');
|
|
80
|
-
|
|
81
|
-
|
|
98
|
+
const hasBackend = !!(cfg.backend && fs.existsSync(backendDir));
|
|
99
|
+
|
|
100
|
+
// 3) 逐平台分发(多平台并行,显著缩短全平台打包耗时)
|
|
101
|
+
// 单平台直接输出到 outDir;多平台各自放到 outDir/<plat>/ 子目录,避免互相覆盖
|
|
102
|
+
const multi = platforms.length > 1;
|
|
103
|
+
const results = await Promise.all(
|
|
104
|
+
platforms.map(async (plat) => {
|
|
105
|
+
const targetDir = multi ? path.join(outDirPath, plat) : outDirPath;
|
|
106
|
+
const outFile = await emitPlatform({ plat, name, targetDir, html, configJSON, backendDir, hasBackend, autoDownload });
|
|
107
|
+
return { plat, outFile };
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return { results };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function emitPlatform({ plat, name, targetDir, html, configJSON, backendDir, hasBackend, autoDownload }) {
|
|
115
|
+
// 取预编译壳二进制
|
|
116
|
+
const shell = localShellPath(plat);
|
|
117
|
+
if (!fs.existsSync(shell)) {
|
|
118
|
+
if (!autoDownload) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`缺少平台 ${plat} 的壳二进制:${shell}\n` +
|
|
121
|
+
`可运行 freedom shell download ${plat} 下载,或 freedom shell build ${plat} 本地编译。`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
process.stdout.write(`[freedom] 本地无 ${plat} 壳,尝试自动下载...\n`);
|
|
125
|
+
await downloadShell(plat);
|
|
82
126
|
}
|
|
83
127
|
|
|
84
|
-
|
|
85
|
-
const genConfig = renderGenConfig(cfg, name, titlebar);
|
|
86
|
-
fs.writeFileSync(path.join(buildDir, 'gen_config.go'), genConfig, 'utf8');
|
|
128
|
+
await fsp.mkdir(targetDir, { recursive: true });
|
|
87
129
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
130
|
+
// 壳二进制 -> 应用可执行文件
|
|
131
|
+
const exeName = platformExeName(plat, name);
|
|
132
|
+
const outFile = path.join(targetDir, exeName);
|
|
133
|
+
await fsp.copyFile(shell, outFile);
|
|
134
|
+
if (!isWinPlat(plat)) {
|
|
135
|
+
await fsp.chmod(outFile, 0o755);
|
|
93
136
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
137
|
+
|
|
138
|
+
// 写 resources:页面 + 配置 + 后端
|
|
139
|
+
const resDir = path.join(targetDir, 'resources');
|
|
140
|
+
await fsp.mkdir(resDir, { recursive: true });
|
|
141
|
+
await fsp.writeFile(path.join(resDir, 'index.html'), html, 'utf8');
|
|
142
|
+
await fsp.writeFile(path.join(resDir, 'config.json'), configJSON, 'utf8');
|
|
143
|
+
if (hasBackend) {
|
|
144
|
+
copyDir(backendDir, path.join(resDir, 'backend'));
|
|
97
145
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
146
|
+
|
|
147
|
+
return outFile;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 渲染 resources/config.json(与壳 configfile.go 的 runtimeConfigFile 字段对齐)
|
|
151
|
+
function renderConfigJSON(cfg, name) {
|
|
152
|
+
const obj = {
|
|
153
|
+
name,
|
|
154
|
+
title: cfg.title || cfg.name || name,
|
|
155
|
+
titlebar: cfg.titlebar || 'native',
|
|
156
|
+
width: intVal(cfg.width, 1024),
|
|
157
|
+
height: intVal(cfg.height, 720),
|
|
158
|
+
minWidth: intVal(cfg.minWidth, 400),
|
|
159
|
+
minHeight: intVal(cfg.minHeight, 300),
|
|
160
|
+
center: typeof cfg.center === 'boolean' ? cfg.center : true,
|
|
161
|
+
debug: typeof cfg.debug === 'boolean' ? cfg.debug : false,
|
|
162
|
+
};
|
|
163
|
+
if (cfg.backend && Array.isArray(cfg.backend.command) && cfg.backend.command.length > 0) {
|
|
164
|
+
// 兼容 command 为数组形式:command=[cmd, ...args]
|
|
165
|
+
obj.backend = { command: cfg.backend.command[0], args: cfg.backend.command.slice(1) };
|
|
166
|
+
} else if (cfg.backend && typeof cfg.backend.command === 'string' && cfg.backend.command) {
|
|
167
|
+
obj.backend = {
|
|
168
|
+
command: cfg.backend.command,
|
|
169
|
+
args: Array.isArray(cfg.backend.args) ? cfg.backend.args : [],
|
|
170
|
+
};
|
|
107
171
|
}
|
|
172
|
+
return JSON.stringify(obj, null, 2);
|
|
173
|
+
}
|
|
108
174
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
175
|
+
// ---- 前端构建缓存 ----
|
|
176
|
+
// 追踪影响前端产物的源文件(index.html / vite.config.* / src/**)的最新 mtime,
|
|
177
|
+
// 与上次 vite build 记录值比较:未变更则跳过 npm run build,直接复用 .freedom/vite-dist 产物。
|
|
178
|
+
|
|
179
|
+
function collectViteSources(dir) {
|
|
180
|
+
const files = [];
|
|
181
|
+
const roots = ['index.html', 'vite.config.js', 'vite.config.mjs', 'vite.config.ts', 'vite.config.cjs'];
|
|
182
|
+
for (const r of roots) {
|
|
183
|
+
const p = path.join(dir, r);
|
|
184
|
+
if (fs.existsSync(p)) files.push(p);
|
|
115
185
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
186
|
+
const src = path.join(dir, 'src');
|
|
187
|
+
if (fs.existsSync(src)) {
|
|
188
|
+
const walk = (d) => {
|
|
189
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
190
|
+
const p = path.join(d, entry.name);
|
|
191
|
+
if (entry.isDirectory()) walk(p);
|
|
192
|
+
else files.push(p);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
walk(src);
|
|
119
196
|
}
|
|
197
|
+
return files;
|
|
198
|
+
}
|
|
120
199
|
|
|
121
|
-
|
|
200
|
+
function maxMtime(files) {
|
|
201
|
+
return files.reduce((m, f) => {
|
|
202
|
+
try {
|
|
203
|
+
const t = fs.statSync(f).mtimeMs;
|
|
204
|
+
return t > m ? t : m;
|
|
205
|
+
} catch (e) {
|
|
206
|
+
return m;
|
|
207
|
+
}
|
|
208
|
+
}, 0);
|
|
122
209
|
}
|
|
123
210
|
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
211
|
+
function viteCacheMarker(dir) {
|
|
212
|
+
return path.join(dir, '.freedom', 'vite-dist', '.cache-mtime');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function viteNeedsRebuild(dir) {
|
|
216
|
+
const distHtml = path.join(dir, '.freedom', 'vite-dist', 'index.html');
|
|
217
|
+
const marker = viteCacheMarker(dir);
|
|
218
|
+
if (!fs.existsSync(distHtml) || !fs.existsSync(marker)) return true;
|
|
219
|
+
let recorded = 0;
|
|
220
|
+
try {
|
|
221
|
+
recorded = Number(fs.readFileSync(marker, 'utf8'));
|
|
222
|
+
} catch (e) { return true; }
|
|
223
|
+
// 留 1s 容差避免文件系统时间精度抖动
|
|
224
|
+
return maxMtime(collectViteSources(dir)) > recorded + 1000;
|
|
130
225
|
}
|
|
131
226
|
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
lines.push('package main');
|
|
136
|
-
lines.push('');
|
|
137
|
-
lines.push('import "freedom-cli-shell/pkg/freedom"');
|
|
138
|
-
lines.push('');
|
|
139
|
-
lines.push('// appConfig 返回应用窗口配置。');
|
|
140
|
-
lines.push('// 本文件由 freedom CLI 在 build 阶段根据 freedom.config.js 生成,');
|
|
141
|
-
lines.push('// 不要手动修改(手动修改会在下次 build 时被覆盖)。');
|
|
142
|
-
lines.push('func appConfig() freedom.Config {');
|
|
143
|
-
lines.push(' return freedom.Config{');
|
|
144
|
-
lines.push(` Title: ${goJSONString(cfg.name || name)},`);
|
|
145
|
-
lines.push(` TitleBar: ${titlebar},`);
|
|
146
|
-
lines.push(` Width: ${intVal(cfg.width, 1024)},`);
|
|
147
|
-
lines.push(` Height: ${intVal(cfg.height, 720)},`);
|
|
148
|
-
lines.push(` MinWidth: ${intVal(cfg.minWidth, 0)},`);
|
|
149
|
-
lines.push(` MinHeight: ${intVal(cfg.minHeight, 0)},`);
|
|
150
|
-
lines.push(` Center: ${boolVal(cfg.center, true)},`);
|
|
151
|
-
lines.push(` Debug: ${boolVal(cfg.debug, false)},`);
|
|
152
|
-
lines.push(' }');
|
|
153
|
-
lines.push('}');
|
|
154
|
-
lines.push('');
|
|
155
|
-
return lines.join('\n');
|
|
227
|
+
function writeViteCacheMarker(dir) {
|
|
228
|
+
fs.mkdirSync(path.join(dir, '.freedom', 'vite-dist'), { recursive: true });
|
|
229
|
+
fs.writeFileSync(viteCacheMarker(dir), String(maxMtime(collectViteSources(dir))), 'utf8');
|
|
156
230
|
}
|
|
157
231
|
|
|
158
|
-
function
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
232
|
+
function ensureNodeModules(dir) {
|
|
233
|
+
// 缺陷修复:node_modules 存在但 package.json 更新过(依赖变更)时自动重装。
|
|
234
|
+
const pkgFile = path.join(dir, 'package.json');
|
|
235
|
+
const nmDir = path.join(dir, 'node_modules');
|
|
236
|
+
const marker = path.join(nmDir, '.package-lock.json');
|
|
237
|
+
if (!fs.existsSync(nmDir)) {
|
|
238
|
+
const res = run('npm', ['install'], { cwd: dir, stdio: 'inherit' });
|
|
239
|
+
if (res.status !== 0) throw new Error('npm install 失败。');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (fs.existsSync(pkgFile) && fs.existsSync(marker)) {
|
|
243
|
+
const pkgMtime = fs.statSync(pkgFile).mtimeMs;
|
|
244
|
+
const lockMtime = fs.statSync(marker).mtimeMs;
|
|
245
|
+
if (pkgMtime > lockMtime + 1000) {
|
|
246
|
+
process.stdout.write('[freedom] package.json 已更新,重新安装依赖...\n');
|
|
247
|
+
const res = run('npm', ['install'], { cwd: dir, stdio: 'inherit' });
|
|
248
|
+
if (res.status !== 0) throw new Error('npm install 失败。');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
162
251
|
}
|
|
163
252
|
|
|
164
253
|
function intVal(v, dft) {
|
|
@@ -166,8 +255,4 @@ function intVal(v, dft) {
|
|
|
166
255
|
return Number.isFinite(n) && n >= 0 ? n : dft;
|
|
167
256
|
}
|
|
168
257
|
|
|
169
|
-
|
|
170
|
-
return typeof v === 'boolean' ? v : dft;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
module.exports = { build };
|
|
258
|
+
module.exports = { build, parsePlatforms };
|
package/lib/cli.js
CHANGED
|
@@ -14,10 +14,16 @@ function help() {
|
|
|
14
14
|
freedom - Freedom 桌面壳打包工具 v${VERSION}
|
|
15
15
|
|
|
16
16
|
用法:
|
|
17
|
+
freedom tui 进入交互式 TUI 界面(新建/打包/配置/壳管理)
|
|
17
18
|
freedom init [目录] [--force] 在当前/指定目录新建项目模板
|
|
18
|
-
freedom build
|
|
19
|
+
freedom build [--platform <p>] 前端打包并分发桌面应用(默认当前平台)
|
|
20
|
+
--platform win|mac|linux|all 指定目标平台(all = 三平台全量)
|
|
21
|
+
--no-cache 忽略前端构建缓存,强制重新打包
|
|
19
22
|
freedom titlebar <native|hidden|frameless>
|
|
20
23
|
一键切换标题栏策略
|
|
24
|
+
freedom shell list 列出本地已就绪的预编译壳
|
|
25
|
+
freedom shell download <plat> 从 GitHub Releases 下载预编译壳
|
|
26
|
+
freedom shell build <plat> 本地用 Go 编译壳(需要 Go)
|
|
21
27
|
freedom config 查看当前配置
|
|
22
28
|
freedom config get <key> 读取单个配置项
|
|
23
29
|
freedom config set <key> <value> 修改单个配置项
|
|
@@ -25,6 +31,9 @@ freedom - Freedom 桌面壳打包工具 v${VERSION}
|
|
|
25
31
|
freedom help 显示本帮助
|
|
26
32
|
freedom version 显示版本
|
|
27
33
|
|
|
34
|
+
平台(<p>/<plat>):win-x64 / darwin-x64 / darwin-arm64 / linux-x64 / linux-arm64,
|
|
35
|
+
快捷别名:win / mac / linux / all。
|
|
36
|
+
|
|
28
37
|
标题栏策略说明:
|
|
29
38
|
native 保留系统原生标题栏(默认)
|
|
30
39
|
hidden 隐藏标题栏视觉,仅保留 Windows 原生最小化/最大化/关闭按钮
|
|
@@ -49,6 +58,9 @@ async function run(argv) {
|
|
|
49
58
|
console.log(VERSION);
|
|
50
59
|
return 0;
|
|
51
60
|
|
|
61
|
+
case 'tui':
|
|
62
|
+
return await require('./tui').tui(process.cwd());
|
|
63
|
+
|
|
52
64
|
case 'init': {
|
|
53
65
|
const force = rest.includes('--force');
|
|
54
66
|
const dirArg = rest.filter((a) => a !== '--force')[0];
|
|
@@ -62,13 +74,22 @@ async function run(argv) {
|
|
|
62
74
|
}
|
|
63
75
|
|
|
64
76
|
case 'build': {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
const platArg = rest.find((a) => a.startsWith('--platform') || a.startsWith('-p'));
|
|
78
|
+
let platform;
|
|
79
|
+
if (platArg) {
|
|
80
|
+
platform = platArg.includes('=') ? platArg.split('=')[1] : rest[rest.indexOf(platArg) + 1];
|
|
81
|
+
}
|
|
82
|
+
const { results } = await build(process.cwd(), { platform, noCache: rest.includes('--no-cache') });
|
|
83
|
+
for (const r of results) {
|
|
84
|
+
console.log(`[freedom] [${r.plat}] 构建完成:${r.outFile}`);
|
|
68
85
|
}
|
|
69
86
|
return 0;
|
|
70
87
|
}
|
|
71
88
|
|
|
89
|
+
case 'shell': {
|
|
90
|
+
return await runShell(rest);
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
case 'titlebar': {
|
|
73
94
|
const mode = rest[0];
|
|
74
95
|
if (!['native', 'hidden', 'frameless'].includes(mode)) {
|
|
@@ -127,6 +148,49 @@ function coerce(value) {
|
|
|
127
148
|
return value;
|
|
128
149
|
}
|
|
129
150
|
|
|
151
|
+
async function runShell(rest) {
|
|
152
|
+
const { listLocal, downloadShell, buildShell } = require('./shell');
|
|
153
|
+
const { ALL_PLATFORMS } = require('./utils');
|
|
154
|
+
const sub = rest[0];
|
|
155
|
+
switch (sub) {
|
|
156
|
+
case undefined:
|
|
157
|
+
case 'list': {
|
|
158
|
+
const ready = listLocal();
|
|
159
|
+
if (ready.length === 0) {
|
|
160
|
+
console.log('[freedom] 本地暂无预编译壳。');
|
|
161
|
+
} else {
|
|
162
|
+
console.log('[freedom] 本地已就绪的壳平台:');
|
|
163
|
+
for (const p of ready) console.log(` ${p}`);
|
|
164
|
+
}
|
|
165
|
+
console.log(`[freedom] 可选平台:${ALL_PLATFORMS.join(' / ')}`);
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
case 'download': {
|
|
169
|
+
const plat = rest[1];
|
|
170
|
+
if (!plat) {
|
|
171
|
+
console.error('[freedom] 用法:freedom shell download <win-x64|darwin-x64|darwin-arm64|linux-x64|linux-arm64>');
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
const dest = await downloadShell(plat);
|
|
175
|
+
console.log(`[freedom] 已下载 ${plat} 壳:${dest}`);
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
case 'build': {
|
|
179
|
+
const plat = rest[1];
|
|
180
|
+
if (!plat) {
|
|
181
|
+
console.error('[freedom] 用法:freedom shell build <win-x64|darwin-x64|darwin-arm64|linux-x64|linux-arm64>');
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
const dest = buildShell(plat);
|
|
185
|
+
console.log(`[freedom] 已编译 ${plat} 壳:${dest}`);
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
default:
|
|
189
|
+
console.error(`[freedom] 未知 shell 子命令:${sub}`);
|
|
190
|
+
return 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
130
194
|
function openBrowser(file) {
|
|
131
195
|
const { spawn } = require('child_process');
|
|
132
196
|
const plat = process.platform;
|