@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.
@@ -0,0 +1,108 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Freedom</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ html, body { height: 100%; }
10
+ body {
11
+ font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
12
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
13
+ color: #e2e8f0;
14
+ min-height: 100vh;
15
+ display: flex;
16
+ flex-direction: column;
17
+ user-select: none;
18
+ overflow: hidden;
19
+ }
20
+ .main {
21
+ flex: 1;
22
+ display: flex;
23
+ flex-direction: column;
24
+ align-items: center;
25
+ justify-content: center;
26
+ gap: 18px;
27
+ }
28
+ .logo {
29
+ width: 84px; height: 84px;
30
+ border-radius: 22px;
31
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
32
+ display: flex; align-items: center; justify-content: center;
33
+ font-size: 40px; font-weight: 800; color: #fff;
34
+ box-shadow: 0 18px 40px rgba(99,102,241,.35);
35
+ }
36
+ h1 { font-size: 26px; font-weight: 700; letter-spacing: 1px; }
37
+ .sub { color: #94a3b8; font-size: 14px; }
38
+ .status {
39
+ margin-top: 8px;
40
+ padding: 8px 16px;
41
+ border-radius: 999px;
42
+ background: rgba(99,102,241,.15);
43
+ border: 1px solid rgba(99,102,241,.4);
44
+ color: #a5b4fc;
45
+ font-size: 13px;
46
+ }
47
+
48
+ /* 无边框模式:前端自绘标题栏(hidden / frameless 时显示) */
49
+ .titlebar {
50
+ display: none;
51
+ height: 36px;
52
+ flex: 0 0 36px;
53
+ background: rgba(15, 23, 42, .92);
54
+ border-bottom: 1px solid rgba(148,163,184,.12);
55
+ align-items: center;
56
+ justify-content: space-between;
57
+ padding-left: 12px;
58
+ -webkit-app-region: drag; /* WebView2 支持:整条标题栏可拖拽窗口 */
59
+ }
60
+ .titlebar .tb-title { font-size: 13px; color: #cbd5e1; letter-spacing: .5px; }
61
+ .tb-btns { display: flex; height: 100%; -webkit-app-region: no-drag; }
62
+ .tb-btn {
63
+ width: 46px; height: 100%;
64
+ display: flex; align-items: center; justify-content: center;
65
+ border: none; background: transparent;
66
+ color: #cbd5e1; font-size: 12px; cursor: default;
67
+ font-family: "Segoe MDL2 Assets", "Segoe UI Symbol", sans-serif;
68
+ }
69
+ .tb-btn:hover { background: rgba(148,163,184,.18); color: #fff; }
70
+ .tb-btn.close:hover { background: #e81123; color: #fff; }
71
+ body.freedom-frameless .titlebar { display: flex; }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <div class="titlebar" id="titlebar">
76
+ <div class="tb-title" id="tbTitle">Freedom</div>
77
+ <div class="tb-btns">
78
+ <button class="tb-btn" id="minBtn" title="最小化">&#xE921;</button>
79
+ <button class="tb-btn" id="maxBtn" title="最大化">&#xE922;</button>
80
+ <button class="tb-btn close" id="closeBtn" title="关闭">&#xE8BB;</button>
81
+ </div>
82
+ </div>
83
+ <div class="main">
84
+ <div class="logo">F</div>
85
+ <h1>Freedom</h1>
86
+ <div class="sub">WebView 桌面壳子框架 · 默认占位页</div>
87
+ <div class="status" id="status">初始化中...</div>
88
+ </div>
89
+ <script>
90
+ // 无边框模式:显示自绘标题栏并绑定窗口控制
91
+ if (window.freedom && window.freedom.window) {
92
+ window.freedom.window.isFrameless().then(function (frameless) {
93
+ if (frameless) {
94
+ document.body.classList.add('freedom-frameless');
95
+ window.freedom.window.bindButtons({ min: '#minBtn', max: '#maxBtn', close: '#closeBtn' });
96
+ }
97
+ }).catch(function () { /* 非桌面环境忽略 */ });
98
+ }
99
+
100
+ // 验证 IPC 双向桥接链路(框架内置 __freedom__ping)
101
+ window.freedom.call('__freedom__ping').then(function (r) {
102
+ document.getElementById('status').textContent = '桥接正常 · ' + r;
103
+ }).catch(function (e) {
104
+ document.getElementById('status').textContent = '桥接异常:' + e;
105
+ });
106
+ </script>
107
+ </body>
108
+ </html>
@@ -8,8 +8,11 @@ import _ "embed"
8
8
  //go:embed assets/freedom.js
9
9
  var jsSDK string
10
10
 
11
- // indexHTML 是前端打包产物(Vite 单文件 HTML),build 阶段写入 assets/index.html,
12
- // 运行时通过 SetHtml 从内存加载。
11
+ // indexHTML 是内置的默认占位页。
12
+ //
13
+ // 预编译通用壳不嵌入应用专属页面:应用 HTML 来自 exe 同目录 resources/index.html
14
+ //(freedom CLI build 时写入,见 resolveHTML 的加载优先级)。仅当外部页面缺失时
15
+ // 回退到本占位页,保证"直接运行壳"也有可见 UI。
13
16
  //
14
17
  //go:embed assets/index.html
15
18
  var indexHTML string
@@ -70,6 +70,16 @@ func NewProcBackend(command ...string) *ProcBackend {
70
70
  }
71
71
  }
72
72
 
73
+ // SetDir 设置后端进程的工作目录。
74
+ // 相对路径参数(如 "backend/main.mjs")将相对该目录解析。
75
+ // 通用壳在加载 resources/config.json 时自动设为 resources 目录。
76
+ func (p *ProcBackend) SetDir(dir string) *ProcBackend {
77
+ if dir != "" {
78
+ p.cmd.Dir = dir
79
+ }
80
+ return p
81
+ }
82
+
73
83
  // SetTimeout 设置单次调用的最大等待时间(默认 60s)。<=0 表示不超时。
74
84
  func (p *ProcBackend) SetTimeout(d time.Duration) *ProcBackend {
75
85
  p.timeout = d
@@ -0,0 +1,121 @@
1
+ package freedom
2
+
3
+ // 运行时外部资源(resources/)加载逻辑。
4
+ //
5
+ // 通用壳二进制(预编译)本身不嵌入任何应用专属的前端与配置;
6
+ // 应用内容来自 exe 同目录的 resources/ 目录:
7
+ // resources/index.html 前端页面(CLI build 写入)
8
+ // resources/config.json 窗口与后端配置(CLI build 写入)
9
+ //
10
+ // 外部资源缺失时静默回退:配置使用编译期/默认值,页面使用内置占位页。
11
+ // 因此同一份壳二进制可以复用于任意应用,且打包时无需任何语言工具链。
12
+
13
+ import (
14
+ "encoding/json"
15
+ "os"
16
+ "path/filepath"
17
+ )
18
+
19
+ // runtimeBackend 描述 config.json 中的后端进程配置(任意语言,经 stdio NDJSON 桥接)。
20
+ type runtimeBackend struct {
21
+ Command string `json:"command"`
22
+ Args []string `json:"args"`
23
+ }
24
+
25
+ // runtimeConfigFile 描述 resources/config.json 的磁盘结构。
26
+ // 由 freedom CLI 在 build 阶段根据 freedom.config.js 生成。
27
+ type runtimeConfigFile struct {
28
+ Name string `json:"name"`
29
+ Title string `json:"title"`
30
+ TitleBar string `json:"titlebar"`
31
+ Width int `json:"width"`
32
+ Height int `json:"height"`
33
+ MinWidth int `json:"minWidth"`
34
+ MinHeight int `json:"minHeight"`
35
+ Center *bool `json:"center"`
36
+ Debug *bool `json:"debug"`
37
+ Backend *runtimeBackend `json:"backend"`
38
+ }
39
+
40
+ // resourcesDir 返回 exe 同目录的 resources 目录绝对路径。
41
+ func resourcesDir() (string, error) {
42
+ exe, err := os.Executable()
43
+ if err != nil {
44
+ return "", err
45
+ }
46
+ return filepath.Join(filepath.Dir(exe), "resources"), nil
47
+ }
48
+
49
+ // loadRuntimeConfig 读取 exe 同目录 resources/config.json,将命中的字段覆盖到应用配置。
50
+ // 文件不存在或内容非法时静默回退(返回 nil),保持编译期/默认配置不变。
51
+ func (a *App) loadRuntimeConfig() error {
52
+ dir, err := resourcesDir()
53
+ if err != nil {
54
+ return nil
55
+ }
56
+ data, err := os.ReadFile(filepath.Join(dir, "config.json"))
57
+ if err != nil {
58
+ return nil
59
+ }
60
+ var rc runtimeConfigFile
61
+ if err := json.Unmarshal(data, &rc); err != nil {
62
+ return nil
63
+ }
64
+ if rc.Title != "" {
65
+ a.cfg.Title = rc.Title
66
+ } else if rc.Name != "" {
67
+ a.cfg.Title = rc.Name
68
+ }
69
+ switch rc.TitleBar {
70
+ case "native":
71
+ a.cfg.TitleBar = TitleBarNative
72
+ case "hidden":
73
+ a.cfg.TitleBar = TitleBarHidden
74
+ case "frameless":
75
+ a.cfg.TitleBar = TitleBarFrameless
76
+ }
77
+ if rc.Width > 0 {
78
+ a.cfg.Width = rc.Width
79
+ }
80
+ if rc.Height > 0 {
81
+ a.cfg.Height = rc.Height
82
+ }
83
+ if rc.MinWidth > 0 {
84
+ a.cfg.MinWidth = rc.MinWidth
85
+ }
86
+ if rc.MinHeight > 0 {
87
+ a.cfg.MinHeight = rc.MinHeight
88
+ }
89
+ if rc.Center != nil {
90
+ a.cfg.Center = *rc.Center
91
+ }
92
+ if rc.Debug != nil {
93
+ a.cfg.Debug = *rc.Debug
94
+ }
95
+ // 外部后端进程配置:仅在壳未显式绑定后端时生效。
96
+ // 工作目录设为 resources 目录,使相对路径参数(backend/main.mjs)按
97
+ // resources/ 解析(CLI 会把项目 backend/ 目录分发到 resources/backend/)。
98
+ if rc.Backend != nil && rc.Backend.Command != "" && !a.backendExplicit {
99
+ args := append([]string{rc.Backend.Command}, rc.Backend.Args...)
100
+ pb := NewProcBackend(args...)
101
+ if dir, err := resourcesDir(); err == nil && dir != "" {
102
+ pb.SetDir(dir)
103
+ }
104
+ a.cfg.Backend = pb
105
+ a.backend = pb
106
+ }
107
+ return nil
108
+ }
109
+
110
+ // loadRuntimeHTML 读取 exe 同目录 resources/index.html;文件不存在时返回空串。
111
+ func loadRuntimeHTML() (string, error) {
112
+ dir, err := resourcesDir()
113
+ if err != nil {
114
+ return "", err
115
+ }
116
+ data, err := os.ReadFile(filepath.Join(dir, "index.html"))
117
+ if err != nil {
118
+ return "", err
119
+ }
120
+ return string(data), nil
121
+ }
@@ -65,10 +65,11 @@ type Config struct {
65
65
 
66
66
  // App 是 Freedom 应用实例。
67
67
  type App struct {
68
- cfg Config
69
- view webview.WebView
70
- backend Backend
71
- onReady func(a *App)
68
+ cfg Config
69
+ view webview.WebView
70
+ backend Backend
71
+ backendExplicit bool // 调用方是否显式指定了后端(外部 config.json 不应覆盖显式绑定)
72
+ onReady func(a *App)
72
73
  }
73
74
 
74
75
  // New 创建并初始化一个 Freedom 应用。调用 Run() 之前不会显示窗口。
@@ -85,10 +86,11 @@ func New(cfg Config) *App {
85
86
  if cfg.Height <= 0 {
86
87
  cfg.Height = 768
87
88
  }
88
- if cfg.Backend == nil {
89
+ explicit := cfg.Backend != nil
90
+ if !explicit {
89
91
  cfg.Backend = NewEmbedBackend()
90
92
  }
91
- return &App{cfg: cfg, backend: cfg.Backend}
93
+ return &App{cfg: cfg, backend: cfg.Backend, backendExplicit: explicit}
92
94
  }
93
95
 
94
96
  // OnReady 注册一个回调,在窗口与桥接层就绪、页面加载前执行。
@@ -121,6 +123,10 @@ func (a *App) Unbind(name string) {
121
123
 
122
124
  // Run 启动窗口并进入主事件循环,阻塞直到窗口被关闭。
123
125
  func (a *App) Run() {
126
+ // 通用壳:先加载 exe 同目录 resources/config.json 覆盖窗口与后端配置
127
+ //(CLI build 时写入;缺失则使用编译期/默认配置)。
128
+ _ = a.loadRuntimeConfig()
129
+
124
130
  html, err := a.resolveHTML()
125
131
  if err != nil {
126
132
  fmt.Printf("freedom: failed to resolve HTML: %v\n", err)
@@ -249,7 +255,11 @@ func (a *App) windowControl(action string) (interface{}, error) {
249
255
  }
250
256
 
251
257
  // resolveHTML 依据配置返回页面内容。
258
+ // 优先级:exe 同目录 resources/index.html(预编译通用壳,CLI build 写入)> cfg.HTML > 内置占位页。
252
259
  func (a *App) resolveHTML() (string, error) {
260
+ if html, err := loadRuntimeHTML(); err == nil && html != "" {
261
+ return html, nil
262
+ }
253
263
  if a.cfg.HTML != nil {
254
264
  return a.cfg.HTML()
255
265
  }
@@ -55,8 +55,13 @@ npm install</code></pre>
55
55
  <p>也可以直接编辑 <code>freedom.config.js</code> 里的 <code>titlebar</code> 字段。</p>
56
56
 
57
57
  <h2>3. 打包</h2>
58
- <pre><code>freedom build</code></pre>
59
- <p>前端会先经 Vite 打成单文件 HTML,再嵌入 Go 壳层编译出可执行文件。产物默认在 <code>dist/</code> 目录,且以 GUI 子系统编译,运行时不会弹出 cmd 黑窗。</p>
58
+ <pre><code>freedom build
59
+ freedom build --platform all # 三平台全量(win + mac + linux)
60
+ freedom build --platform win-x64 # 仅 Windows
61
+ freedom build --platform mac-arm64 # 仅 macOS Apple Silicon
62
+ freedom build --platform linux-x64 # 仅 Linux</code></pre>
63
+ <p>前端经 Vite 打成单文件 HTML 后写入 <code>dist/resources/</code>,与随包分发的预编译壳二进制合并出可执行文件。不再依赖本机 Go 工具链,任一平台都能一键产出三平台产物。产物以 GUI 子系统运行,不会弹出 cmd 黑窗。</p>
64
+ <p><code>resources/</code> 内含壳运行时加载的前端页面 <code>index.html</code> 与配置 <code>config.json</code>;声明了 <code>backend</code> 时 <code>backend/</code> 目录会被原样分发到 <code>resources/backend/</code>,壳启动后自动以 <code>resources/</code> 为工作目录拉起后端进程(黑窗同样隐藏)。</p>
60
65
 
61
66
  <h2>3.5 调整产物目录(终端一键切换)</h2>
62
67
  <pre><code>freedom config set outDir . # 输出到项目根目录(dist 的上级)