cj-tauri 1.1.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +137 -77
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +13 -2
  4. package/cli/cjpm.toml +22 -13
  5. package/cli/src/doctor.cj +308 -304
  6. package/cli/src/main.cj +129 -121
  7. package/cli/src/manifest.cj +632 -0
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -754
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +46 -38
  12. package/cli/src/sync.cj +526 -0
  13. package/cli/src/util.cj +9 -0
  14. package/cli/templates/app/README.md +30 -59
  15. package/cli/templates/app/{ui → assets/ui}/index.html +2 -0
  16. package/cli/templates/app/cj-tauri.toml +32 -0
  17. package/cli/templates/app/cjpm.toml +21 -4
  18. package/cli/templates/app/src/main.cj +29 -16
  19. package/cli/templates/app-react/README.md +34 -45
  20. package/cli/templates/app-react/{ui → assets/ui}/index.html +25 -25
  21. package/cli/templates/app-react/{ui → assets/ui}/src/App.css +67 -67
  22. package/cli/templates/app-react/{ui → assets/ui}/src/App.jsx +124 -124
  23. package/cli/templates/app-react/cj-tauri.toml +33 -0
  24. package/cli/templates/app-react/cjpm.toml +21 -4
  25. package/cli/templates/app-react/src/main.cj +18 -29
  26. package/cli/templates/app-vue/README.md +34 -38
  27. package/cli/templates/app-vue/{ui → assets/ui}/index.html +25 -25
  28. package/cli/templates/app-vue/{ui → assets/ui}/src/App.vue +176 -176
  29. package/cli/templates/app-vue/cj-tauri.toml +33 -0
  30. package/cli/templates/app-vue/cjpm.toml +21 -4
  31. package/cli/templates/app-vue/src/main.cj +18 -29
  32. package/cli/templates/{ohos-pc → ohos-skeleton}/README.md +5 -0
  33. package/cli/templates/{ohos-pc → ohos-skeleton}/build-hap.sh +15 -0
  34. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/cjpm.toml +8 -0
  35. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  36. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -0
  37. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/module.json5 +8 -0
  38. package/native/build_win.bat +62 -62
  39. package/native/core/bridge_core.c +1681 -1626
  40. package/native/core/bridge_core.h +675 -618
  41. package/native/core/bridge_js.h +108 -108
  42. package/native/platforms/harmonyos/README.md +115 -6
  43. package/native/platforms/linux/bridge_linux.c +2787 -2485
  44. package/native/platforms/windows/bridge_win.c +3740 -3248
  45. package/native/tests/gtk_threading_probe.c +143 -143
  46. package/native/tests/test_bridge_core.c +1339 -1228
  47. package/package.json +53 -51
  48. package/prebuilt/linux-x64/cj-tauri +0 -0
  49. package/src/api_system.cj +67 -67
  50. package/src/app.cj +325 -940
  51. package/src/app_events.cj +166 -0
  52. package/src/app_host_ops.cj +287 -0
  53. package/src/app_run.cj +283 -0
  54. package/src/app_spec.cj +76 -0
  55. package/src/arkweb_menu.cj +216 -0
  56. package/src/arkweb_script.cj +0 -12
  57. package/src/capability.cj +409 -409
  58. package/src/clipboard_bridge.cj +178 -0
  59. package/src/dialog_spec.cj +79 -0
  60. package/src/dragdrop.cj +26 -26
  61. package/src/embedded_assets.cj +98 -98
  62. package/src/host.cj +752 -604
  63. package/src/host_arkweb.cj +201 -17
  64. package/src/host_webkit.cj +663 -548
  65. package/src/host_webview2.cj +663 -548
  66. package/src/hotkey.cj +66 -66
  67. package/src/ipc_hub.cj +337 -337
  68. package/src/ipc_message.cj +286 -286
  69. package/src/launch.cj +62 -0
  70. package/src/logging.cj +127 -127
  71. package/src/menu.cj +197 -197
  72. package/src/platform/platform_app_context.cj +77 -0
  73. package/src/platform/platform_assets.cj +33 -0
  74. package/src/platform/platform_discover.cj +176 -0
  75. package/src/platform/platform_env.cj +29 -0
  76. package/src/platform/platform_feature.cj +31 -0
  77. package/src/platform/platform_page_source.cj +87 -0
  78. package/src/platform/platform_target_info.cj +32 -0
  79. package/src/platform_host_provider.cj +87 -0
  80. package/src/plugin.cj +333 -299
  81. package/src/plugins/clipboard/plugin_clipboard.cj +152 -144
  82. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  83. package/src/plugins/fs/plugin_fs.cj +105 -1
  84. package/src/plugins/http/http_backend.cj +131 -0
  85. package/src/plugins/http/http_desktop.cj +195 -0
  86. package/src/plugins/http/http_ohos.cj +204 -0
  87. package/src/plugins/http/plugin_http.cj +310 -459
  88. package/src/plugins/log/plugin_log.cj +105 -105
  89. package/src/plugins/menu/plugin_menu.cj +8 -0
  90. package/src/plugins/notification/plugin_notification.cj +154 -146
  91. package/src/plugins/serial/plugin_serial.cj +17 -1
  92. package/src/plugins/serial/serial_bridge.cj +9 -1
  93. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  94. package/src/plugins/serial/serial_ohos.cj +529 -0
  95. package/src/plugins/shell/plugin_shell.cj +29 -1
  96. package/src/plugins/sqlite/plugin_sqlite.cj +8 -0
  97. package/src/plugins/ws/plugin_ws.cj +488 -470
  98. package/src/process_hidden.cj +174 -174
  99. package/src/scope.cj +391 -391
  100. package/src/shell_event.cj +69 -69
  101. package/src/tests/app_spec_test.cj +126 -0
  102. package/src/tests/capability_test.cj +243 -243
  103. package/src/tests/dialog_spec_test.cj +73 -0
  104. package/src/tests/fake_host.cj +201 -186
  105. package/src/tests/ipc_hub_test.cj +487 -487
  106. package/src/tests/logging_test.cj +62 -62
  107. package/src/tests/platform_contract_test.cj +251 -0
  108. package/src/tests/plugin_clipboard_test.cj +222 -222
  109. package/src/tests/plugin_dialog_test.cj +237 -237
  110. package/src/tests/plugin_fs_test.cj +207 -0
  111. package/src/tests/plugin_http_test.cj +124 -124
  112. package/src/tests/plugin_notification_test.cj +205 -205
  113. package/src/tests/plugin_serial_test.cj +48 -0
  114. package/src/tests/plugin_test.cj +66 -3
  115. package/src/tests/plugin_ws_test.cj +40 -40
  116. package/src/tests/process_hidden_test.cj +129 -129
  117. package/src/tests/scope_test.cj +350 -350
  118. package/src/tests/system_host_test.cj +99 -81
  119. package/src/tests/urlutil_test.cj +53 -53
  120. package/src/tests/version_test.cj +75 -75
  121. package/src/tests/window_registry_test.cj +123 -123
  122. package/src/tray.cj +90 -90
  123. package/src/urlutil.cj +159 -159
  124. package/src/version.cj +44 -44
  125. package/src/window.cj +51 -51
  126. package/src/window_registry.cj +116 -116
  127. package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +0 -167
  128. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  129. package/src/host_factory.cj +0 -48
  130. /package/cli/templates/app/{capabilities → assets/capabilities}/default.json +0 -0
  131. /package/cli/templates/app-react/{capabilities → assets/capabilities}/default.json +0 -0
  132. /package/cli/templates/app-react/{ui → assets/ui}/package.json +0 -0
  133. /package/cli/templates/app-react/{ui → assets/ui}/src/main.jsx +0 -0
  134. /package/cli/templates/app-react/{ui → assets/ui}/vite.config.js +0 -0
  135. /package/cli/templates/app-vue/{capabilities → assets/capabilities}/default.json +0 -0
  136. /package/cli/templates/app-vue/{ui → assets/ui}/package.json +0 -0
  137. /package/cli/templates/app-vue/{ui → assets/ui}/src/main.js +0 -0
  138. /package/cli/templates/app-vue/{ui → assets/ui}/vite.config.js +0 -0
  139. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/app.json5 +0 -0
  140. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/element/string.json +0 -0
  141. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/background.png +0 -0
  142. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/foreground.png +0 -0
  143. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/layered_image.json +0 -0
  144. /package/cli/templates/{ohos-pc → ohos-skeleton}/build-profile.json5 +0 -0
  145. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/build-profile.json5 +0 -0
  146. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/hvigorfile.ts +0 -0
  147. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/oh-package.json5 +0 -0
  148. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/ability_stage.cj +0 -0
  149. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/app_host.cj +0 -0
  150. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/main_ability.cj +0 -0
  151. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/color.json +0 -0
  152. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/string.json +0 -0
  153. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/background.png +0 -0
  154. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/foreground.png +0 -0
  155. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/layered_image.json +0 -0
  156. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/startIcon.png +0 -0
  157. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/profile/main_pages.json +0 -0
  158. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/capabilities.json +0 -0
  159. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/index.html +0 -0
  160. /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigor/hvigor-config.json5 +0 -0
  161. /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigorfile.ts +0 -0
  162. /package/cli/templates/{ohos-pc → ohos-skeleton}/oh-package.json5 +0 -0
@@ -1,286 +1,286 @@
1
- package cjTauri
2
-
3
- import stdx.encoding.json.*
4
-
5
- /**
6
- * 窗口 label 的缺省值(对外契约,不能改)
7
- *
8
- * 与桥 JS 的 `windowLabel` 缺省、`emitToWindow(..., "main")` 是同一个约定:老前端与现有示例
9
- * 都写死 `"main"`,默认值一错,事件就静默收不到、回执就静默投不回原窗口(见架构文档 §6.4)。
10
- */
11
- public let DEFAULT_WINDOW_LABEL = "main"
12
-
13
- /**
14
- * IPC 消息模型(对标 Tauri IPC 协议)
15
- *
16
- * JS → 仓颉 (invoke):
17
- * {"type":"invoke","id":1,"cmd":"greet","args":{...},"window":"main"}
18
- * JS → 仓颉 (emit):
19
- * {"type":"emit","id":2,"event":"tick","payload":{...},"window":"main"}
20
- * `window` 是发起窗口的 label(批次 2 起由桥 JS 带上)。**缺省 / 非字符串 / 空串一律按 "main" 处理**:
21
- * 上行方向没有「广播」语义——一条回执必须能回到唯一一个窗口,否则前端那条 promise 永远 pending。
22
- * 仓颉 → JS (resolve/reject):
23
- * {"type":"resolve","id":1,"ok":true,"data":...}
24
- * {"type":"resolve","id":1,"ok":false,"error":"..."}
25
- * 仓颉 → JS (event):
26
- * {"type":"event","event":"tick","payload":{...},"window":"main"}
27
- * window 字段缺省 = 广播(所有窗口);定向推送时给接收窗口的 label,
28
- * 页面里由桥按自己的 label 过滤(多窗口落地前只有一个 "main")。
29
- * `emitLatest` 发出的报文多一个 "coalesce":true —— 页面侧同一帧内同名事件只投最后一次
30
- * payload(中间值丢弃),tick = 一帧。
31
- */
32
-
33
- public class InvokeRequest {
34
- public let id: Int64
35
- public let cmd: String
36
- public let args: JsonObject
37
- /** 发起窗口的 label(批次 2 起随报文来);回执按它回投给同一窗口 */
38
- public let window: String
39
-
40
- public init(id: Int64, cmd: String, args: JsonObject, window!: String = DEFAULT_WINDOW_LABEL) {
41
- this.id = id
42
- this.cmd = cmd
43
- this.args = args
44
- this.window = window
45
- }
46
-
47
- /**
48
- * 从 JS 侧 postMessage 的原始 JSON 解析 invoke 请求。
49
- * 解析失败返回 None(由调用方判定为非法消息)。
50
- */
51
- public static func parse(raw: String): Option<InvokeRequest> {
52
- try {
53
- let envOpt = parseEnvelope(raw)
54
- if (envOpt.isNone()) {
55
- return None
56
- }
57
- let env = envOpt.getOrThrow()
58
- if (env.msgType != "invoke") {
59
- return None
60
- }
61
- let obj = env.obj
62
- let idV = obj.get("id")
63
- let cmdV = obj.get("cmd")
64
- if (idV.isNone() || cmdV.isNone()) {
65
- return None
66
- }
67
- let id = idV.getOrThrow().asInt().getValue()
68
- let cmd = cmdV.getOrThrow().asString().getValue()
69
- var args = JsonObject()
70
- if (let Some(a) <- obj.get("args")) {
71
- match (a.kind()) {
72
- case JsonKind.JsObject => args = a.asObject()
73
- case _ => ()
74
- }
75
- }
76
- return Some(InvokeRequest(id, cmd, args, window: parseWindowLabel(obj)))
77
- } catch (e: Exception) {
78
- return None
79
- }
80
- }
81
- }
82
-
83
- /**
84
- * 前端 emit 事件(对标 Tauri v2 前端 `emit()` → `plugin:event|emit`)
85
- *
86
- * 语义与 Tauri 的**有意差异**(选了更保守的一条,理由与后续演进口径见 docs/IPC-通信机制.md §7):
87
- * 事件只投给**后端监听器**(`IpcHub.listenEvent` 注册的),不回投给任何页面。
88
- * Tauri 的 `emit` 是全局广播(含发起者自己),在单窗口下退化成「自己发自己收」,
89
- * 且多窗口寻址(报文里的 window 字段)还没落地——等窗口有了再把投递侧接到窗口注册表上,
90
- * 前端与报文格式都不用改。
91
- *
92
- * 带 id 是有意的:事件未授权 / 报文非法都要能把失败**回给前端**(reject 同一个 id),
93
- * 而不是只在 stderr 里悄悄丢(对照 Tauri 的 `emit()` 返回 Promise<void>)。
94
- */
95
- public class EmitRequest {
96
- public let id: Int64
97
- public let event: String
98
- public let payload: JsonValue
99
- /** 发起窗口的 label(批次 2 起随报文来);resolve 回执按它回投给同一窗口 */
100
- public let window: String
101
-
102
- public init(id: Int64, event: String, payload: JsonValue, window!: String = DEFAULT_WINDOW_LABEL) {
103
- this.id = id
104
- this.event = event
105
- this.payload = payload
106
- this.window = window
107
- }
108
-
109
- /**
110
- * 解析前端 emit 报文;payload 缺省为空对象,解析失败返回 None。
111
- */
112
- public static func parse(raw: String): Option<EmitRequest> {
113
- try {
114
- let envOpt = parseEnvelope(raw)
115
- if (envOpt.isNone()) {
116
- return None
117
- }
118
- let env = envOpt.getOrThrow()
119
- if (env.msgType != "emit") {
120
- return None
121
- }
122
- let obj = env.obj
123
- let idV = obj.get("id")
124
- let eventV = obj.get("event")
125
- if (idV.isNone() || eventV.isNone()) {
126
- return None
127
- }
128
- let id = idV.getOrThrow().asInt().getValue()
129
- let event = eventV.getOrThrow().asString().getValue()
130
- var payload: JsonValue = JsonObject()
131
- if (let Some(p) <- obj.get("payload")) {
132
- payload = p
133
- }
134
- return Some(EmitRequest(id, event, payload, window: parseWindowLabel(obj)))
135
- } catch (e: Exception) {
136
- return None
137
- }
138
- }
139
- }
140
-
141
- /**
142
- * 报文信封:顶层 JSON 对象 + 字符串 type 字段。
143
- * invoke / emit 两条解析路径共用——两份重复的预检迟早漂移,而「type 必须是字符串」这类
144
- * 判定正是非法报文与正常报文的分界。
145
- */
146
- class IpcEnvelope {
147
- let obj: JsonObject
148
- let msgType: String
149
-
150
- init(obj: JsonObject, msgType: String) {
151
- this.obj = obj
152
- this.msgType = msgType
153
- }
154
- }
155
-
156
- /**
157
- * 从报文里取发起窗口的 label:缺失 / 非字符串 / 空串一律回落到 `DEFAULT_WINDOW_LABEL`
158
- *
159
- * 为什么空串也回落:下行方向的空串是「广播」(谁在听谁收),上行方向没有这个语义——
160
- * 一条回执必须回到唯一一个窗口,否则前端那条 promise 永远 pending。
161
- */
162
- func parseWindowLabel(obj: JsonObject): String {
163
- if (let Some(w) <- obj.get("window")) {
164
- match (w.kind()) {
165
- case JsonKind.JsString =>
166
- let s = w.asString().getValue()
167
- if (s.size > 0) {
168
- return s
169
- }
170
- case _ => ()
171
- }
172
- }
173
- return DEFAULT_WINDOW_LABEL
174
- }
175
-
176
- /** 解析报文信封;非 JSON / 顶层不是对象 / type 缺失或不是字符串,一律 None */
177
- func parseEnvelope(raw: String): Option<IpcEnvelope> {
178
- try {
179
- let root = JsonValue.fromStr(raw)
180
- match (root.kind()) {
181
- case JsonKind.JsObject => ()
182
- case _ => return None
183
- }
184
- let obj = root.asObject()
185
- if (let Some(t) <- obj.get("type")) {
186
- match (t.kind()) {
187
- case JsonKind.JsString => return Some(IpcEnvelope(obj, t.asString().getValue()))
188
- case _ => return None
189
- }
190
- }
191
- return None
192
- } catch (e: Exception) {
193
- return None
194
- }
195
- }
196
-
197
- public class IpcMessage {
198
- /**
199
- * 构造 resolve 响应 JSON(ok=true)
200
- */
201
- public static func resolve(id: Int64, data: JsonValue): String {
202
- var obj = JsonObject()
203
- obj.put("type", JsonString("resolve"))
204
- obj.put("id", JsonInt(id))
205
- obj.put("ok", JsonBool(true))
206
- obj.put("data", data)
207
- return obj.toJsonString()
208
- }
209
-
210
- /**
211
- * 构造 reject 响应 JSON(ok=false)
212
- */
213
- public static func reject(id: Int64, error: String): String {
214
- var obj = JsonObject()
215
- obj.put("type", JsonString("resolve"))
216
- obj.put("id", JsonInt(id))
217
- obj.put("ok", JsonBool(false))
218
- obj.put("error", JsonString(error))
219
- return obj.toJsonString()
220
- }
221
-
222
- /**
223
- * 构造事件推送 JSON
224
- *
225
- * @param window 接收窗口的 label;空串 = 广播(报文里不带 window 字段,桥放行)。
226
- * 多窗口落地前只会是 "main",但字段现在就留在协议里,免得那次改造再动报文。
227
- */
228
- public static func event(event: String, payload: JsonValue, window: String): String {
229
- return eventJson(event, payload, window, false).toJsonString()
230
- }
231
-
232
- /**
233
- * 构造「只保最新」的事件推送 JSON(`IpcHub.emitLatest` 用)
234
- *
235
- * 报文只比 `event` 多一个 `coalesce: true`:页面侧桥把它落进「本帧槽位」,
236
- * 同一帧内同名事件只投最后一次 payload,中间值丢弃。要不要这种语义由**调用方显式选**——
237
- * 默认路径不做合并(静默丢数据比慢更难排查,Tauri v2 核心同样不合并)。
238
- * tick = 一帧,理由与对照见 docs/IPC-通信机制.md §6 第 5 项。
239
- */
240
- public static func eventLatest(event: String, payload: JsonValue, window: String): String {
241
- return eventJson(event, payload, window, true).toJsonString()
242
- }
243
-
244
- /** 事件报文的公共构造:`coalesce` 只在需要时落字段,旧报文形态逐字节不变 */
245
- private static func eventJson(event: String, payload: JsonValue, window: String, coalesce: Bool): JsonObject {
246
- var obj = JsonObject()
247
- obj.put("type", JsonString("event"))
248
- obj.put("protocol", JsonString(CJ_IPC_PROTOCOL))
249
- obj.put("event", JsonString(event))
250
- obj.put("payload", payload)
251
- if (coalesce) {
252
- obj.put("coalesce", JsonBool(true))
253
- }
254
- if (window.size > 0) {
255
- obj.put("window", JsonString(window))
256
- }
257
- return obj
258
- }
259
-
260
- /**
261
- * 尽力从报文里取出 id(只认顶层 int 字段),取下取不到返回 None。
262
- *
263
- * 用途是非法报文的回包:前端 `invoke` / `emit` 都带 id,只要 id 可读就应该回一条 reject,
264
- * 否则前端那条 promise 会永久 pending(比报错更难排查)。取不到 id 的(例如裸字符串、
265
- * 没有 id 的 JSON)没法对应到任何 promise,只能打 stderr。
266
- */
267
- public static func peekId(raw: String): Option<Int64> {
268
- try {
269
- let root = JsonValue.fromStr(raw)
270
- match (root.kind()) {
271
- case JsonKind.JsObject => ()
272
- case _ => return None
273
- }
274
- let idV = root.asObject().get("id")
275
- if (idV.isNone()) {
276
- return None
277
- }
278
- match (idV.getOrThrow().kind()) {
279
- case JsonKind.JsInt => return Some(idV.getOrThrow().asInt().getValue())
280
- case _ => return None
281
- }
282
- } catch (e: Exception) {
283
- return None
284
- }
285
- }
286
- }
1
+ package cjTauri
2
+
3
+ import stdx.encoding.json.*
4
+
5
+ /**
6
+ * 窗口 label 的缺省值(对外契约,不能改)
7
+ *
8
+ * 与桥 JS 的 `windowLabel` 缺省、`emitToWindow(..., "main")` 是同一个约定:老前端与现有示例
9
+ * 都写死 `"main"`,默认值一错,事件就静默收不到、回执就静默投不回原窗口(见架构文档 §6.4)。
10
+ */
11
+ public let DEFAULT_WINDOW_LABEL = "main"
12
+
13
+ /**
14
+ * IPC 消息模型(对标 Tauri IPC 协议)
15
+ *
16
+ * JS → 仓颉 (invoke):
17
+ * {"type":"invoke","id":1,"cmd":"greet","args":{...},"window":"main"}
18
+ * JS → 仓颉 (emit):
19
+ * {"type":"emit","id":2,"event":"tick","payload":{...},"window":"main"}
20
+ * `window` 是发起窗口的 label(批次 2 起由桥 JS 带上)。**缺省 / 非字符串 / 空串一律按 "main" 处理**:
21
+ * 上行方向没有「广播」语义——一条回执必须能回到唯一一个窗口,否则前端那条 promise 永远 pending。
22
+ * 仓颉 → JS (resolve/reject):
23
+ * {"type":"resolve","id":1,"ok":true,"data":...}
24
+ * {"type":"resolve","id":1,"ok":false,"error":"..."}
25
+ * 仓颉 → JS (event):
26
+ * {"type":"event","event":"tick","payload":{...},"window":"main"}
27
+ * window 字段缺省 = 广播(所有窗口);定向推送时给接收窗口的 label,
28
+ * 页面里由桥按自己的 label 过滤(多窗口落地前只有一个 "main")。
29
+ * `emitLatest` 发出的报文多一个 "coalesce":true —— 页面侧同一帧内同名事件只投最后一次
30
+ * payload(中间值丢弃),tick = 一帧。
31
+ */
32
+
33
+ public class InvokeRequest {
34
+ public let id: Int64
35
+ public let cmd: String
36
+ public let args: JsonObject
37
+ /** 发起窗口的 label(批次 2 起随报文来);回执按它回投给同一窗口 */
38
+ public let window: String
39
+
40
+ public init(id: Int64, cmd: String, args: JsonObject, window!: String = DEFAULT_WINDOW_LABEL) {
41
+ this.id = id
42
+ this.cmd = cmd
43
+ this.args = args
44
+ this.window = window
45
+ }
46
+
47
+ /**
48
+ * 从 JS 侧 postMessage 的原始 JSON 解析 invoke 请求。
49
+ * 解析失败返回 None(由调用方判定为非法消息)。
50
+ */
51
+ public static func parse(raw: String): Option<InvokeRequest> {
52
+ try {
53
+ let envOpt = parseEnvelope(raw)
54
+ if (envOpt.isNone()) {
55
+ return None
56
+ }
57
+ let env = envOpt.getOrThrow()
58
+ if (env.msgType != "invoke") {
59
+ return None
60
+ }
61
+ let obj = env.obj
62
+ let idV = obj.get("id")
63
+ let cmdV = obj.get("cmd")
64
+ if (idV.isNone() || cmdV.isNone()) {
65
+ return None
66
+ }
67
+ let id = idV.getOrThrow().asInt().getValue()
68
+ let cmd = cmdV.getOrThrow().asString().getValue()
69
+ var args = JsonObject()
70
+ if (let Some(a) <- obj.get("args")) {
71
+ match (a.kind()) {
72
+ case JsonKind.JsObject => args = a.asObject()
73
+ case _ => ()
74
+ }
75
+ }
76
+ return Some(InvokeRequest(id, cmd, args, window: parseWindowLabel(obj)))
77
+ } catch (e: Exception) {
78
+ return None
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * 前端 emit 事件(对标 Tauri v2 前端 `emit()` → `plugin:event|emit`)
85
+ *
86
+ * 语义与 Tauri 的**有意差异**(选了更保守的一条,理由与后续演进口径见 docs/IPC-通信机制.md §7):
87
+ * 事件只投给**后端监听器**(`IpcHub.listenEvent` 注册的),不回投给任何页面。
88
+ * Tauri 的 `emit` 是全局广播(含发起者自己),在单窗口下退化成「自己发自己收」,
89
+ * 且多窗口寻址(报文里的 window 字段)还没落地——等窗口有了再把投递侧接到窗口注册表上,
90
+ * 前端与报文格式都不用改。
91
+ *
92
+ * 带 id 是有意的:事件未授权 / 报文非法都要能把失败**回给前端**(reject 同一个 id),
93
+ * 而不是只在 stderr 里悄悄丢(对照 Tauri 的 `emit()` 返回 Promise<void>)。
94
+ */
95
+ public class EmitRequest {
96
+ public let id: Int64
97
+ public let event: String
98
+ public let payload: JsonValue
99
+ /** 发起窗口的 label(批次 2 起随报文来);resolve 回执按它回投给同一窗口 */
100
+ public let window: String
101
+
102
+ public init(id: Int64, event: String, payload: JsonValue, window!: String = DEFAULT_WINDOW_LABEL) {
103
+ this.id = id
104
+ this.event = event
105
+ this.payload = payload
106
+ this.window = window
107
+ }
108
+
109
+ /**
110
+ * 解析前端 emit 报文;payload 缺省为空对象,解析失败返回 None。
111
+ */
112
+ public static func parse(raw: String): Option<EmitRequest> {
113
+ try {
114
+ let envOpt = parseEnvelope(raw)
115
+ if (envOpt.isNone()) {
116
+ return None
117
+ }
118
+ let env = envOpt.getOrThrow()
119
+ if (env.msgType != "emit") {
120
+ return None
121
+ }
122
+ let obj = env.obj
123
+ let idV = obj.get("id")
124
+ let eventV = obj.get("event")
125
+ if (idV.isNone() || eventV.isNone()) {
126
+ return None
127
+ }
128
+ let id = idV.getOrThrow().asInt().getValue()
129
+ let event = eventV.getOrThrow().asString().getValue()
130
+ var payload: JsonValue = JsonObject()
131
+ if (let Some(p) <- obj.get("payload")) {
132
+ payload = p
133
+ }
134
+ return Some(EmitRequest(id, event, payload, window: parseWindowLabel(obj)))
135
+ } catch (e: Exception) {
136
+ return None
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * 报文信封:顶层 JSON 对象 + 字符串 type 字段。
143
+ * invoke / emit 两条解析路径共用——两份重复的预检迟早漂移,而「type 必须是字符串」这类
144
+ * 判定正是非法报文与正常报文的分界。
145
+ */
146
+ class IpcEnvelope {
147
+ let obj: JsonObject
148
+ let msgType: String
149
+
150
+ init(obj: JsonObject, msgType: String) {
151
+ this.obj = obj
152
+ this.msgType = msgType
153
+ }
154
+ }
155
+
156
+ /**
157
+ * 从报文里取发起窗口的 label:缺失 / 非字符串 / 空串一律回落到 `DEFAULT_WINDOW_LABEL`
158
+ *
159
+ * 为什么空串也回落:下行方向的空串是「广播」(谁在听谁收),上行方向没有这个语义——
160
+ * 一条回执必须回到唯一一个窗口,否则前端那条 promise 永远 pending。
161
+ */
162
+ func parseWindowLabel(obj: JsonObject): String {
163
+ if (let Some(w) <- obj.get("window")) {
164
+ match (w.kind()) {
165
+ case JsonKind.JsString =>
166
+ let s = w.asString().getValue()
167
+ if (s.size > 0) {
168
+ return s
169
+ }
170
+ case _ => ()
171
+ }
172
+ }
173
+ return DEFAULT_WINDOW_LABEL
174
+ }
175
+
176
+ /** 解析报文信封;非 JSON / 顶层不是对象 / type 缺失或不是字符串,一律 None */
177
+ func parseEnvelope(raw: String): Option<IpcEnvelope> {
178
+ try {
179
+ let root = JsonValue.fromStr(raw)
180
+ match (root.kind()) {
181
+ case JsonKind.JsObject => ()
182
+ case _ => return None
183
+ }
184
+ let obj = root.asObject()
185
+ if (let Some(t) <- obj.get("type")) {
186
+ match (t.kind()) {
187
+ case JsonKind.JsString => return Some(IpcEnvelope(obj, t.asString().getValue()))
188
+ case _ => return None
189
+ }
190
+ }
191
+ return None
192
+ } catch (e: Exception) {
193
+ return None
194
+ }
195
+ }
196
+
197
+ public class IpcMessage {
198
+ /**
199
+ * 构造 resolve 响应 JSON(ok=true)
200
+ */
201
+ public static func resolve(id: Int64, data: JsonValue): String {
202
+ var obj = JsonObject()
203
+ obj.put("type", JsonString("resolve"))
204
+ obj.put("id", JsonInt(id))
205
+ obj.put("ok", JsonBool(true))
206
+ obj.put("data", data)
207
+ return obj.toJsonString()
208
+ }
209
+
210
+ /**
211
+ * 构造 reject 响应 JSON(ok=false)
212
+ */
213
+ public static func reject(id: Int64, error: String): String {
214
+ var obj = JsonObject()
215
+ obj.put("type", JsonString("resolve"))
216
+ obj.put("id", JsonInt(id))
217
+ obj.put("ok", JsonBool(false))
218
+ obj.put("error", JsonString(error))
219
+ return obj.toJsonString()
220
+ }
221
+
222
+ /**
223
+ * 构造事件推送 JSON
224
+ *
225
+ * @param window 接收窗口的 label;空串 = 广播(报文里不带 window 字段,桥放行)。
226
+ * 多窗口落地前只会是 "main",但字段现在就留在协议里,免得那次改造再动报文。
227
+ */
228
+ public static func event(event: String, payload: JsonValue, window: String): String {
229
+ return eventJson(event, payload, window, false).toJsonString()
230
+ }
231
+
232
+ /**
233
+ * 构造「只保最新」的事件推送 JSON(`IpcHub.emitLatest` 用)
234
+ *
235
+ * 报文只比 `event` 多一个 `coalesce: true`:页面侧桥把它落进「本帧槽位」,
236
+ * 同一帧内同名事件只投最后一次 payload,中间值丢弃。要不要这种语义由**调用方显式选**——
237
+ * 默认路径不做合并(静默丢数据比慢更难排查,Tauri v2 核心同样不合并)。
238
+ * tick = 一帧,理由与对照见 docs/IPC-通信机制.md §6 第 5 项。
239
+ */
240
+ public static func eventLatest(event: String, payload: JsonValue, window: String): String {
241
+ return eventJson(event, payload, window, true).toJsonString()
242
+ }
243
+
244
+ /** 事件报文的公共构造:`coalesce` 只在需要时落字段,旧报文形态逐字节不变 */
245
+ private static func eventJson(event: String, payload: JsonValue, window: String, coalesce: Bool): JsonObject {
246
+ var obj = JsonObject()
247
+ obj.put("type", JsonString("event"))
248
+ obj.put("protocol", JsonString(CJ_IPC_PROTOCOL))
249
+ obj.put("event", JsonString(event))
250
+ obj.put("payload", payload)
251
+ if (coalesce) {
252
+ obj.put("coalesce", JsonBool(true))
253
+ }
254
+ if (window.size > 0) {
255
+ obj.put("window", JsonString(window))
256
+ }
257
+ return obj
258
+ }
259
+
260
+ /**
261
+ * 尽力从报文里取出 id(只认顶层 int 字段),取下取不到返回 None。
262
+ *
263
+ * 用途是非法报文的回包:前端 `invoke` / `emit` 都带 id,只要 id 可读就应该回一条 reject,
264
+ * 否则前端那条 promise 会永久 pending(比报错更难排查)。取不到 id 的(例如裸字符串、
265
+ * 没有 id 的 JSON)没法对应到任何 promise,只能打 stderr。
266
+ */
267
+ public static func peekId(raw: String): Option<Int64> {
268
+ try {
269
+ let root = JsonValue.fromStr(raw)
270
+ match (root.kind()) {
271
+ case JsonKind.JsObject => ()
272
+ case _ => return None
273
+ }
274
+ let idV = root.asObject().get("id")
275
+ if (idV.isNone()) {
276
+ return None
277
+ }
278
+ match (idV.getOrThrow().kind()) {
279
+ case JsonKind.JsInt => return Some(idV.getOrThrow().asInt().getValue())
280
+ case _ => return None
281
+ }
282
+ } catch (e: Exception) {
283
+ return None
284
+ }
285
+ }
286
+ }
package/src/launch.cj ADDED
@@ -0,0 +1,62 @@
1
+ package cjTauri
2
+
3
+ import cjTauri.platform.*
4
+ import stdx.encoding.json.JsonValue
5
+
6
+ /**
7
+ * 三段式 API 的第二段(批次 3,RFC-002 §4.2)
8
+ *
9
+ * 业务入口全文就这几行:
10
+ * ```
11
+ * import cjTauri.*
12
+ * main(): Int64 {
13
+ * launch(build())
14
+ * return 0
15
+ * }
16
+ * ```
17
+ *
18
+ * `Platform.discover()` 是唯一「端在哪儿」的知识入口(§5.1);
19
+ * 桌面阻塞主循环,移动端的非 UI 线程 spawn 由宿主负责(§4.2 鸿蒙示例)。
20
+ *
21
+ * 与 RFC 示意写法的偏差(照实记录):两个重载代替默认参数
22
+ * (当前工具链不用默认参数),`launchWith` 供测试注入假上下文、
23
+ * 鸿蒙 Ability 触发时给移动端上下文。
24
+ */
25
+ public func launch(spec: AppSpec): Unit {
26
+ let target = Platform.guessTarget()
27
+ launchWith(spec, Platform.discover(spec.appId, target.arch))
28
+ }
29
+
30
+ /** 注入上下文的变体:测试给假上下文;鸿蒙侧给 Platform.mobile(filesDir) */
31
+ public func launchWith(spec: AppSpec, context: AppContext): Unit {
32
+ eprintln("[cj-tauri] launch:${spec.name}(os=${context.target.os} display=${context.target.display})")
33
+ let app = assemble(spec)
34
+ app.run(spec.pageSource)
35
+ }
36
+
37
+ /**
38
+ * spec → TauriApp(纯装配、不 run):launch 与单测共用
39
+ *
40
+ * 装配顺序即框架契约顺序:窗口配置 → 插件 → 资产解析器 → 能力目录 → 业务命令(wire)。
41
+ */
42
+ public func assemble(spec: AppSpec): TauriApp {
43
+ let app = TauriApp().window(spec.windowConfig)
44
+ for (p in spec.plugins) {
45
+ app.plugin(p)
46
+ }
47
+ if (spec.assetResolver.isSome()) {
48
+ app.assets(spec.assetResolver.getOrThrow())
49
+ }
50
+ if (spec.capabilitiesDir.size > 0) {
51
+ app.loadCapabilities(spec.capabilitiesDir)
52
+ }
53
+ // 能力清单 JSON 走与显式挂载同一入口:鸿蒙胶水把 rawfile 原文传进来(批次 3-PR3)
54
+ for (j in spec.capabilityJsons) {
55
+ app.addCapabilityJson(JsonValue.fromStr(j))
56
+ }
57
+ if (spec.wireFn.isSome()) {
58
+ let f = spec.wireFn.getOrThrow()
59
+ f(app)
60
+ }
61
+ return app
62
+ }