cj-tauri 2.0.0 → 2.1.1

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 (157) hide show
  1. package/README.md +206 -139
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +63 -52
  4. package/cli/cjpm.toml +22 -22
  5. package/cli/src/doctor.cj +308 -306
  6. package/cli/src/main.cj +129 -129
  7. package/cli/src/manifest.cj +643 -558
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -770
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +369 -369
  12. package/cli/src/sync.cj +638 -526
  13. package/cli/src/util.cj +168 -168
  14. package/cli/templates/app/README.md +51 -48
  15. package/cli/templates/app/assets/capabilities/default.json +7 -7
  16. package/cli/templates/app/assets/ui/index.html +120 -118
  17. package/cli/templates/app/cj-tauri.toml +32 -22
  18. package/cli/templates/app/cjpm.toml +34 -34
  19. package/cli/templates/app/src/main.cj +101 -90
  20. package/cli/templates/app-react/README.md +56 -56
  21. package/cli/templates/app-react/assets/capabilities/default.json +7 -7
  22. package/cli/templates/app-react/assets/ui/index.html +25 -25
  23. package/cli/templates/app-react/assets/ui/package.json +19 -19
  24. package/cli/templates/app-react/assets/ui/src/App.css +67 -67
  25. package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
  26. package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
  27. package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
  28. package/cli/templates/app-react/cj-tauri.toml +33 -23
  29. package/cli/templates/app-react/cjpm.toml +34 -34
  30. package/cli/templates/app-react/src/main.cj +91 -91
  31. package/cli/templates/app-vue/README.md +56 -56
  32. package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
  33. package/cli/templates/app-vue/assets/ui/index.html +25 -25
  34. package/cli/templates/app-vue/assets/ui/package.json +18 -18
  35. package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
  36. package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
  37. package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
  38. package/cli/templates/app-vue/cj-tauri.toml +33 -23
  39. package/cli/templates/app-vue/cjpm.toml +34 -34
  40. package/cli/templates/app-vue/src/main.cj +91 -91
  41. package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
  42. package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
  43. package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
  44. package/cli/templates/ohos-skeleton/README.md +84 -79
  45. package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
  46. package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
  47. package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
  48. package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
  49. package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
  50. package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
  51. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
  52. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
  53. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  54. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
  55. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
  56. package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
  57. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
  58. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
  59. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
  60. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
  61. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
  62. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
  63. package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
  64. package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
  65. package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
  66. package/native/build_win.bat +62 -62
  67. package/native/core/bridge_core.c +1681 -1626
  68. package/native/core/bridge_core.h +675 -618
  69. package/native/core/bridge_js.h +108 -108
  70. package/native/platforms/harmonyos/README.md +115 -6
  71. package/native/platforms/linux/bridge_linux.c +2787 -2485
  72. package/native/platforms/windows/bridge_win.c +3745 -3248
  73. package/native/tests/gtk_threading_probe.c +143 -143
  74. package/native/tests/test_bridge_core.c +1339 -1228
  75. package/package.json +53 -53
  76. package/src/api_system.cj +67 -67
  77. package/src/app.cj +335 -969
  78. package/src/app_events.cj +166 -0
  79. package/src/app_host_ops.cj +287 -0
  80. package/src/app_run.cj +283 -0
  81. package/src/app_spec.cj +76 -76
  82. package/src/arkweb_menu.cj +216 -0
  83. package/src/arkweb_script.cj +142 -142
  84. package/src/capability.cj +409 -409
  85. package/src/clipboard_bridge.cj +178 -0
  86. package/src/dialog_spec.cj +79 -0
  87. package/src/dragdrop.cj +26 -26
  88. package/src/embedded_assets.cj +98 -98
  89. package/src/host.cj +752 -607
  90. package/src/host_arkweb.cj +924 -778
  91. package/src/host_webkit.cj +663 -570
  92. package/src/host_webview2.cj +663 -570
  93. package/src/hotkey.cj +66 -66
  94. package/src/ipc_hub.cj +401 -337
  95. package/src/ipc_message.cj +286 -286
  96. package/src/launch.cj +62 -62
  97. package/src/logging.cj +127 -127
  98. package/src/menu.cj +197 -197
  99. package/src/platform/platform_app_context.cj +77 -77
  100. package/src/platform/platform_assets.cj +33 -33
  101. package/src/platform/platform_discover.cj +176 -176
  102. package/src/platform/platform_env.cj +29 -29
  103. package/src/platform/platform_feature.cj +31 -31
  104. package/src/platform/platform_page_source.cj +87 -87
  105. package/src/platform/platform_target_info.cj +32 -32
  106. package/src/platform_host_provider.cj +87 -87
  107. package/src/plugin.cj +333 -333
  108. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  109. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  110. package/src/plugins/fs/plugin_fs.cj +105 -1
  111. package/src/plugins/http/http_backend.cj +131 -0
  112. package/src/plugins/http/http_desktop.cj +195 -0
  113. package/src/plugins/http/http_ohos.cj +204 -0
  114. package/src/plugins/http/plugin_http.cj +310 -459
  115. package/src/plugins/log/plugin_log.cj +105 -105
  116. package/src/plugins/menu/plugin_menu.cj +158 -158
  117. package/src/plugins/notification/plugin_notification.cj +154 -154
  118. package/src/plugins/serial/plugin_serial.cj +458 -454
  119. package/src/plugins/serial/serial_bridge.cj +194 -194
  120. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  121. package/src/plugins/serial/serial_ohos.cj +529 -0
  122. package/src/plugins/shell/plugin_shell.cj +258 -258
  123. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  124. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  125. package/src/plugins/ws/plugin_ws.cj +488 -470
  126. package/src/process_hidden.cj +174 -174
  127. package/src/scope.cj +391 -391
  128. package/src/shell_event.cj +69 -69
  129. package/src/tests/app_spec_test.cj +130 -127
  130. package/src/tests/capability_test.cj +243 -243
  131. package/src/tests/dialog_spec_test.cj +73 -0
  132. package/src/tests/fake_host.cj +201 -201
  133. package/src/tests/ipc_hub_test.cj +563 -487
  134. package/src/tests/logging_test.cj +62 -62
  135. package/src/tests/platform_contract_test.cj +264 -251
  136. package/src/tests/plugin_clipboard_test.cj +222 -222
  137. package/src/tests/plugin_dialog_test.cj +237 -237
  138. package/src/tests/plugin_fs_test.cj +207 -0
  139. package/src/tests/plugin_http_test.cj +124 -124
  140. package/src/tests/plugin_notification_test.cj +205 -205
  141. package/src/tests/plugin_serial_test.cj +48 -0
  142. package/src/tests/plugin_test.cj +605 -585
  143. package/src/tests/plugin_ws_test.cj +40 -40
  144. package/src/tests/process_hidden_test.cj +129 -129
  145. package/src/tests/scope_test.cj +350 -350
  146. package/src/tests/system_host_test.cj +99 -81
  147. package/src/tests/urlutil_test.cj +53 -53
  148. package/src/tests/version_test.cj +75 -75
  149. package/src/tests/window_registry_test.cj +123 -123
  150. package/src/tray.cj +90 -90
  151. package/src/urlutil.cj +159 -159
  152. package/src/version.cj +44 -44
  153. package/src/window.cj +51 -51
  154. package/src/window_registry.cj +116 -116
  155. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  156. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  157. package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
package/src/plugin.cj CHANGED
@@ -1,333 +1,333 @@
1
- package cjTauri
2
-
3
- import std.collection.*
4
- import std.sort.*
5
- import stdx.encoding.json.*
6
-
7
- /**
8
- * 插件(对标 Tauri v2 的 plugin,设计见 docs/RFC-插件体系.md)
9
- *
10
- * 一个插件 = 一组命令(全名 `<插件名>:<短名>`)+ 可选事件 + 可选前端 JS 片段。
11
- * 权限仍由应用的 capabilities 目录里的 json 决定:插件只声明「我提供什么」,**不自动放行**,
12
- * 默认最小权限不被破坏(RFC §5.3 的取舍 B)。
13
- *
14
- * 装配(必须在 run() / runUrl() 之前——页面加载前命令表要定稿):
15
- * TauriApp().plugin(FsPlugin())
16
- *
17
- * 命令全名与内置的 system:* 同形,所以 IpcHub 分发与 Capability 校验零改动。
18
- * 接口带默认实现,插件作者通常只写 name() 与 commands()。
19
- */
20
- public interface Plugin {
21
- /** 插件名:作为命令/事件的命名空间(如 "fs"),不得为空、不得含 ':' */
22
- func name(): String
23
-
24
- /** 命令短名 → 处理器;框架注册为 "<插件名>:<短名>" */
25
- func commands(): HashMap<String, CommandHandler>
26
-
27
- /** 可能推送的事件短名;框架登记为 "<插件名>:<短名>",用于缺权限提示。默认无事件 */
28
- func events(): ArrayList<String> {
29
- return ArrayList<String>()
30
- }
31
-
32
- /** 前端 JS 片段:定义 window.__CJ_TAURI__.<插件名>;没有就返回空串 */
33
- func jsShim(): String {
34
- return ""
35
- }
36
-
37
- /** 初始化:记录 app 引用、做一次性准备。默认空实现 */
38
- func setup(app: TauriApp): Unit { let _ = app }
39
-
40
- /**
41
- * 命名权限集(v1.1):短集名 → 成员(写短名会自动补 "<插件名>:",写全名则原样保留)。
42
- * 应用在 capabilities 的 permissions 里按 "<插件名>:<短集名>" 引用,一次放行一组。
43
- * 默认无。**集声明不等于放行**——清单不引用它,一条也不会生效。
44
- */
45
- func permissions(): HashMap<String, ArrayList<String>> {
46
- return HashMap<String, ArrayList<String>>()
47
- }
48
-
49
- /**
50
- * 本插件是否吃「路径 scope」这层校验(RFC-003 第 4 期,默认 false)。
51
- *
52
- * 声明 true 意味着:命令里带路径的参数一律经 `IpcContext.resolveScopedPath(插件名, 路径)` 取值
53
- * (越界即拒),且清单没为它写 scopes 时框架会在启动时提示「按旧行为全量放行」。
54
- * 为什么不由框架猜「哪个参数是路径」:只有插件自己知道参数语义,猜出来的白名单等于没有白名单。
55
- */
56
- func scopeAware(): Bool {
57
- return false
58
- }
59
-
60
- /**
61
- * 自描述(批次 3,RFC §4.6.7):供 sync/check 三方比对的静态数据。
62
- * 默认实现 = 全宿主、无能力依赖(多数插件无需覆盖);
63
- * 有平台约束的插件必须覆盖(如 SqlitePlugin 只支持 webkitgtk)。
64
- *
65
- * v1 曾结论「不加接口方法」(数据可从 commands()/events()/permissions() 推出);
66
- * 批次 3 改自描述双通道——supportedHosts / requiredCapabilities 是命令表推不出来的
67
- * 语义信息,只能由插件自报。
68
- */
69
- func describe(): PluginManifest {
70
- return PluginManifest(ArrayList<String>(), ArrayList<String>())
71
- }
72
- }
73
-
74
- /**
75
- * 插件自描述(批次 3,RFC §4.6.7):供 sync/check 三方比对的静态数据。
76
- *
77
- * - supportedHosts:插件实际支持的 hostKind 列表;空 = 全宿主。
78
- * 不在列表的宿主上,插件必须以桩如实失败「端不支持」(P-2),不得静默缺失;
79
- * - requiredCapabilities:插件依赖的能力清单 identifier;空 = 无依赖。
80
- * check 拿它与 capabilities 目录的 identifier 交叉比对(缺声明 = 失败)。
81
- */
82
- public class PluginManifest {
83
- public let supportedHosts: ArrayList<String>
84
- public let requiredCapabilities: ArrayList<String>
85
-
86
- public init(supportedHosts: ArrayList<String>, requiredCapabilities: ArrayList<String>) {
87
- this.supportedHosts = supportedHosts
88
- this.requiredCapabilities = requiredCapabilities
89
- }
90
- }
91
-
92
- /**
93
- * 插件名是否合法:非空且不含全/半角冒号。
94
- * 冒号是命名空间分隔符,混进来会让 "<插件名>:<短名>" 无法解析、权限也写不清。
95
- */
96
- public func isValidPluginName(name: String): Bool {
97
- if (name.size == 0) {
98
- return false
99
- }
100
- return !name.contains(":") && !name.contains(":")
101
- }
102
-
103
- /** 某插件的命令全名列表(`<插件名>:<短名>`) */
104
- public func pluginCommandNames(p: Plugin): ArrayList<String> {
105
- let names = ArrayList<String>()
106
- for ((short, _) in p.commands()) {
107
- names.add("${p.name()}:${short}")
108
- }
109
- return names
110
- }
111
-
112
- /** 某插件的事件全名列表(`<插件名>:<短名>`) */
113
- public func pluginEventNames(p: Plugin): ArrayList<String> {
114
- let names = ArrayList<String>()
115
- for (short in p.events()) {
116
- names.add("${p.name()}:${short}")
117
- }
118
- return names
119
- }
120
-
121
- /** 把插件的命令按 "<插件名>:<短名>" 注册进 hub(TauriApp.prepare 与测试共用) */
122
- public func registerPluginCommands(hub: IpcHub, p: Plugin): Unit {
123
- for ((short, handler) in p.commands()) {
124
- hub.register("${p.name()}:${short}", handler)
125
- }
126
- }
127
-
128
- /** 某插件的命名权限集:`<插件名>:<短集名>` → 展开后的成员全名列表 */
129
- public func pluginPermissionSets(p: Plugin): HashMap<String, ArrayList<String>> {
130
- let out = HashMap<String, ArrayList<String>>()
131
- for ((shortSet, members) in p.permissions()) {
132
- let expanded = ArrayList<String>()
133
- for (m in members) {
134
- if (m.contains(":")) {
135
- expanded.add(m)
136
- } else {
137
- expanded.add("${p.name()}:${m}")
138
- }
139
- }
140
- out.add("${p.name()}:${shortSet}", expanded)
141
- }
142
- return out
143
- }
144
-
145
- /** 把插件的命名权限集注册进能力注册中心(TauriApp.prepare 与测试共用) */
146
- public func registerPluginPermissionSets(reg: CapabilityRegistry, p: Plugin): Unit {
147
- for ((setName, members) in pluginPermissionSets(p)) {
148
- reg.addPermissionSet(setName, members)
149
- }
150
- }
151
-
152
- /** 人类可读的命名权限集清单(启动日志用);一个集都没有时返回空串 */
153
- public func describePermissionSets(plugins: ArrayList<Plugin>): String {
154
- var text = ""
155
- for (p in plugins) {
156
- for ((setName, members) in pluginPermissionSets(p)) {
157
- var list = ""
158
- for (m in members) {
159
- if (list.size > 0) {
160
- list += ", "
161
- }
162
- list += m
163
- }
164
- text += " ${setName} ${list}\n"
165
- }
166
- }
167
- return text
168
- }
169
-
170
- /**
171
- * 收集各插件的 jsShim,作为「预执行脚本」逐条返回(一个插件一段,顺序 = 装配顺序)
172
- *
173
- * v2 方案(RFC §5.4):不再把 shim 拼成 script 标签塞进 HTML——那段字符串操作对畸形 HTML 不健壮,
174
- * 而且 runUrl 的页面 HTML 根本不在本进程、拼不进去。改为交给宿主的 addInitScript,两平台都以
175
- * document-start 注入,HTML / URL 两种来源一视同仁。
176
- * 一个插件一段(而不是拼成一整段):某个插件的 shim 写坏了不会带崩其它插件的命名空间。
177
- */
178
- public func pluginInitScripts(plugins: ArrayList<Plugin>): ArrayList<String> {
179
- let out = ArrayList<String>()
180
- for (p in plugins) {
181
- let shim = p.jsShim()
182
- if (shim.size > 0) {
183
- out.add(shim)
184
- }
185
- }
186
- return out
187
- }
188
-
189
- /** 人类可读的插件清单(启动日志用,对齐 RFC §5.5 的打印形态) */
190
- public func describePlugins(plugins: ArrayList<Plugin>): String {
191
- var text = ""
192
- for (p in plugins) {
193
- var cmds = ""
194
- for (n in pluginCommandNames(p)) {
195
- if (cmds.size > 0) {
196
- cmds += ", "
197
- }
198
- cmds += n
199
- }
200
- text += " ${p.name()} ${cmds}"
201
- let evts = pluginEventNames(p)
202
- if (evts.size > 0) {
203
- text += " events: "
204
- var first = true
205
- for (n in evts) {
206
- if (!first) {
207
- text += ", "
208
- }
209
- text += n
210
- first = false
211
- }
212
- }
213
- text += "\n"
214
- }
215
- return text
216
- }
217
-
218
- /**
219
- * 机器可读的插件清单(RFC §8 第 7 条的答案 C:给工具/CI 用,**不动 Plugin 接口**)
220
- *
221
- * 形态 `[{name, commands, events, permissions, hasShim}, ...]`:
222
- * - `commands` / `events`:全名数组(`<插件名>:<短名>`,与 IPC 注册名、能力清单里写的名字一致)
223
- * - `permissions`:集全名 → 成员全名数组(**框架展开后**的样子,也就是清单里能引用的名字)
224
- * - `hasShim`:是否带前端 JS 片段(工具据此判断要不要等 `__CJ_TAURI__.<插件名>`)
225
- * 数组与权限键都按字典序排:HashMap 迭代顺序不定,工具的输出 / diff 需要稳定。
226
- *
227
- * 数据来自 describe()(批次 3 自描述双通道):命令/事件/权限集之外,携带 supportedHosts /
228
- * requiredCapabilities 两个命令表推不出来的语义项(RFC §4.6.7)。
229
- */
230
- public func describePluginsJson(plugins: ArrayList<Plugin>): JsonValue {
231
- let arr = ArrayList<JsonValue>()
232
- for (p in plugins) {
233
- var obj = JsonObject()
234
- let d = p.describe()
235
- obj.put("name", JsonString(p.name()))
236
- obj.put("commands", jsonStringArray(pluginCommandNames(p)))
237
- obj.put("events", jsonStringArray(pluginEventNames(p)))
238
- obj.put("permissions", jsonPermissionSets(p))
239
- obj.put("hasShim", JsonBool(p.jsShim().size > 0))
240
- obj.put("supportedHosts", jsonStringArray(d.supportedHosts))
241
- obj.put("requiredCapabilities", jsonStringArray(d.requiredCapabilities))
242
- arr.add(obj)
243
- }
244
- return JsonArray(arr)
245
- }
246
-
247
- /** 字符串数组 → JSON 数组,元素按字典序(HashMap 来的一律先排序,保证输出稳定) */
248
- func jsonStringArray(items: ArrayList<String>): JsonValue {
249
- let sorted = sortedStrings(items)
250
- let arr = ArrayList<JsonValue>()
251
- for (s in sorted) {
252
- arr.add(JsonString(s))
253
- }
254
- return JsonArray(arr)
255
- }
256
-
257
- /** 命名权限集 → JSON 对象(键 = 集全名,值 = 成员全名数组);键也按字典序 */
258
- func jsonPermissionSets(p: Plugin): JsonValue {
259
- let sets = pluginPermissionSets(p)
260
- let keys = ArrayList<String>()
261
- for ((setName, _) in sets) {
262
- keys.add(setName)
263
- }
264
- var obj = JsonObject()
265
- for (setName in sortedStrings(keys)) {
266
- match (sets.get(setName)) {
267
- case Some(members) => obj.put(setName, jsonStringArray(members))
268
- case None => ()
269
- }
270
- }
271
- return obj
272
- }
273
-
274
- /** 复制一份并按字典序排序(不改动入参;元素量级只有每插件的几条命令,开销可忽略) */
275
- func sortedStrings(items: ArrayList<String>): ArrayList<String> {
276
- let out = ArrayList<String>()
277
- for (s in items) {
278
- out.add(s)
279
- }
280
- sort(out)
281
- return out
282
- }
283
-
284
- /**
285
- * 未授权提示:插件声明了、但应用的能力清单没放行时,说清缺哪条、加到哪去。
286
- * 只提示、不改权限——放行与否仍由 capabilities 决定(RFC §5.3 的取舍 B)。
287
- * 调用时机:能力清单加载完成之后、页面加载之前(TauriApp.prepare)。
288
- */
289
- public func warnUnauthorizedPlugins(plugins: ArrayList<Plugin>, caps: CapabilityRegistry): Unit {
290
- for (p in plugins) {
291
- for (name in pluginCommandNames(p)) {
292
- if (!caps.canInvoke(name)) {
293
- eprintln("[cj-tauri] 插件 ${p.name()} 的命令 ${name} 尚未授权,前端调用会被拒绝")
294
- eprintln("[cj-tauri] 请在 capabilities 目录的 json 里把 \"${name}\" 加进 commands")
295
- }
296
- }
297
- for (name in pluginEventNames(p)) {
298
- if (!caps.canEmit(name)) {
299
- eprintln("[cj-tauri] 插件 ${p.name()} 的事件 ${name} 尚未授权,推送会被丢弃")
300
- eprintln("[cj-tauri] 请在 capabilities 目录的 json 里把 \"${name}\" 加进 events")
301
- }
302
- }
303
- }
304
- }
305
-
306
- /**
307
- * 提示「吃 scope 的插件没有被声明 scope」——旧清单缺省行为:全量放行(RFC-003 第 4 期)。
308
- *
309
- * 只在「该插件至少有一条命令被授权」时提示:没用到它的应用不必被这行日志打扰。
310
- * 声明了 scope 的插件(caps.hasScopeForPlugin 为真)不提示。
311
- */
312
- public func warnUnscopedPlugins(plugins: ArrayList<Plugin>, caps: CapabilityRegistry): Unit {
313
- for (p in plugins) {
314
- if (!p.scopeAware()) {
315
- continue
316
- }
317
- var authorized = false
318
- for (name in pluginCommandNames(p)) {
319
- if (caps.canInvoke(name)) {
320
- authorized = true
321
- break
322
- }
323
- }
324
- if (!authorized) {
325
- continue
326
- }
327
- if (caps.hasScopeForPlugin(p.name())) {
328
- continue
329
- }
330
- eprintln("[cj-tauri] 插件 ${p.name()} 的路径参数未声明 scope:按旧行为全量放行")
331
- eprintln("[cj-tauri] 建议在 capabilities 的 json 里加 \"scopes\": { \"${p.name()}\": { \"allow\": [\"<放行路径>\"] } }")
332
- }
333
- }
1
+ package cjTauri
2
+
3
+ import std.collection.*
4
+ import std.sort.*
5
+ import stdx.encoding.json.*
6
+
7
+ /**
8
+ * 插件(对标 Tauri v2 的 plugin,设计见 docs/RFC-插件体系.md)
9
+ *
10
+ * 一个插件 = 一组命令(全名 `<插件名>:<短名>`)+ 可选事件 + 可选前端 JS 片段。
11
+ * 权限仍由应用的 capabilities 目录里的 json 决定:插件只声明「我提供什么」,**不自动放行**,
12
+ * 默认最小权限不被破坏(RFC §5.3 的取舍 B)。
13
+ *
14
+ * 装配(必须在 run() / runUrl() 之前——页面加载前命令表要定稿):
15
+ * TauriApp().plugin(FsPlugin())
16
+ *
17
+ * 命令全名与内置的 system:* 同形,所以 IpcHub 分发与 Capability 校验零改动。
18
+ * 接口带默认实现,插件作者通常只写 name() 与 commands()。
19
+ */
20
+ public interface Plugin {
21
+ /** 插件名:作为命令/事件的命名空间(如 "fs"),不得为空、不得含 ':' */
22
+ func name(): String
23
+
24
+ /** 命令短名 → 处理器;框架注册为 "<插件名>:<短名>" */
25
+ func commands(): HashMap<String, CommandHandler>
26
+
27
+ /** 可能推送的事件短名;框架登记为 "<插件名>:<短名>",用于缺权限提示。默认无事件 */
28
+ func events(): ArrayList<String> {
29
+ return ArrayList<String>()
30
+ }
31
+
32
+ /** 前端 JS 片段:定义 window.__CJ_TAURI__.<插件名>;没有就返回空串 */
33
+ func jsShim(): String {
34
+ return ""
35
+ }
36
+
37
+ /** 初始化:记录 app 引用、做一次性准备。默认空实现 */
38
+ func setup(app: TauriApp): Unit { let _ = app }
39
+
40
+ /**
41
+ * 命名权限集(v1.1):短集名 → 成员(写短名会自动补 "<插件名>:",写全名则原样保留)。
42
+ * 应用在 capabilities 的 permissions 里按 "<插件名>:<短集名>" 引用,一次放行一组。
43
+ * 默认无。**集声明不等于放行**——清单不引用它,一条也不会生效。
44
+ */
45
+ func permissions(): HashMap<String, ArrayList<String>> {
46
+ return HashMap<String, ArrayList<String>>()
47
+ }
48
+
49
+ /**
50
+ * 本插件是否吃「路径 scope」这层校验(RFC-003 第 4 期,默认 false)。
51
+ *
52
+ * 声明 true 意味着:命令里带路径的参数一律经 `IpcContext.resolveScopedPath(插件名, 路径)` 取值
53
+ * (越界即拒),且清单没为它写 scopes 时框架会在启动时提示「按旧行为全量放行」。
54
+ * 为什么不由框架猜「哪个参数是路径」:只有插件自己知道参数语义,猜出来的白名单等于没有白名单。
55
+ */
56
+ func scopeAware(): Bool {
57
+ return false
58
+ }
59
+
60
+ /**
61
+ * 自描述(批次 3,RFC §4.6.7):供 sync/check 三方比对的静态数据。
62
+ * 默认实现 = 全宿主、无能力依赖(多数插件无需覆盖);
63
+ * 有平台约束的插件必须覆盖(如 SqlitePlugin 只支持 webkitgtk)。
64
+ *
65
+ * v1 曾结论「不加接口方法」(数据可从 commands()/events()/permissions() 推出);
66
+ * 批次 3 改自描述双通道——supportedHosts / requiredCapabilities 是命令表推不出来的
67
+ * 语义信息,只能由插件自报。
68
+ */
69
+ func describe(): PluginManifest {
70
+ return PluginManifest(ArrayList<String>(), ArrayList<String>())
71
+ }
72
+ }
73
+
74
+ /**
75
+ * 插件自描述(批次 3,RFC §4.6.7):供 sync/check 三方比对的静态数据。
76
+ *
77
+ * - supportedHosts:插件实际支持的 hostKind 列表;空 = 全宿主。
78
+ * 不在列表的宿主上,插件必须以桩如实失败「端不支持」(P-2),不得静默缺失;
79
+ * - requiredCapabilities:插件依赖的能力清单 identifier;空 = 无依赖。
80
+ * check 拿它与 capabilities 目录的 identifier 交叉比对(缺声明 = 失败)。
81
+ */
82
+ public class PluginManifest {
83
+ public let supportedHosts: ArrayList<String>
84
+ public let requiredCapabilities: ArrayList<String>
85
+
86
+ public init(supportedHosts: ArrayList<String>, requiredCapabilities: ArrayList<String>) {
87
+ this.supportedHosts = supportedHosts
88
+ this.requiredCapabilities = requiredCapabilities
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 插件名是否合法:非空且不含全/半角冒号。
94
+ * 冒号是命名空间分隔符,混进来会让 "<插件名>:<短名>" 无法解析、权限也写不清。
95
+ */
96
+ public func isValidPluginName(name: String): Bool {
97
+ if (name.size == 0) {
98
+ return false
99
+ }
100
+ return !name.contains(":") && !name.contains(":")
101
+ }
102
+
103
+ /** 某插件的命令全名列表(`<插件名>:<短名>`) */
104
+ public func pluginCommandNames(p: Plugin): ArrayList<String> {
105
+ let names = ArrayList<String>()
106
+ for ((short, _) in p.commands()) {
107
+ names.add("${p.name()}:${short}")
108
+ }
109
+ return names
110
+ }
111
+
112
+ /** 某插件的事件全名列表(`<插件名>:<短名>`) */
113
+ public func pluginEventNames(p: Plugin): ArrayList<String> {
114
+ let names = ArrayList<String>()
115
+ for (short in p.events()) {
116
+ names.add("${p.name()}:${short}")
117
+ }
118
+ return names
119
+ }
120
+
121
+ /** 把插件的命令按 "<插件名>:<短名>" 注册进 hub(TauriApp.prepare 与测试共用) */
122
+ public func registerPluginCommands(hub: IpcHub, p: Plugin): Unit {
123
+ for ((short, handler) in p.commands()) {
124
+ hub.register("${p.name()}:${short}", handler)
125
+ }
126
+ }
127
+
128
+ /** 某插件的命名权限集:`<插件名>:<短集名>` → 展开后的成员全名列表 */
129
+ public func pluginPermissionSets(p: Plugin): HashMap<String, ArrayList<String>> {
130
+ let out = HashMap<String, ArrayList<String>>()
131
+ for ((shortSet, members) in p.permissions()) {
132
+ let expanded = ArrayList<String>()
133
+ for (m in members) {
134
+ if (m.contains(":")) {
135
+ expanded.add(m)
136
+ } else {
137
+ expanded.add("${p.name()}:${m}")
138
+ }
139
+ }
140
+ out.add("${p.name()}:${shortSet}", expanded)
141
+ }
142
+ return out
143
+ }
144
+
145
+ /** 把插件的命名权限集注册进能力注册中心(TauriApp.prepare 与测试共用) */
146
+ public func registerPluginPermissionSets(reg: CapabilityRegistry, p: Plugin): Unit {
147
+ for ((setName, members) in pluginPermissionSets(p)) {
148
+ reg.addPermissionSet(setName, members)
149
+ }
150
+ }
151
+
152
+ /** 人类可读的命名权限集清单(启动日志用);一个集都没有时返回空串 */
153
+ public func describePermissionSets(plugins: ArrayList<Plugin>): String {
154
+ var text = ""
155
+ for (p in plugins) {
156
+ for ((setName, members) in pluginPermissionSets(p)) {
157
+ var list = ""
158
+ for (m in members) {
159
+ if (list.size > 0) {
160
+ list += ", "
161
+ }
162
+ list += m
163
+ }
164
+ text += " ${setName} ${list}\n"
165
+ }
166
+ }
167
+ return text
168
+ }
169
+
170
+ /**
171
+ * 收集各插件的 jsShim,作为「预执行脚本」逐条返回(一个插件一段,顺序 = 装配顺序)
172
+ *
173
+ * v2 方案(RFC §5.4):不再把 shim 拼成 script 标签塞进 HTML——那段字符串操作对畸形 HTML 不健壮,
174
+ * 而且 runUrl 的页面 HTML 根本不在本进程、拼不进去。改为交给宿主的 addInitScript,两平台都以
175
+ * document-start 注入,HTML / URL 两种来源一视同仁。
176
+ * 一个插件一段(而不是拼成一整段):某个插件的 shim 写坏了不会带崩其它插件的命名空间。
177
+ */
178
+ public func pluginInitScripts(plugins: ArrayList<Plugin>): ArrayList<String> {
179
+ let out = ArrayList<String>()
180
+ for (p in plugins) {
181
+ let shim = p.jsShim()
182
+ if (shim.size > 0) {
183
+ out.add(shim)
184
+ }
185
+ }
186
+ return out
187
+ }
188
+
189
+ /** 人类可读的插件清单(启动日志用,对齐 RFC §5.5 的打印形态) */
190
+ public func describePlugins(plugins: ArrayList<Plugin>): String {
191
+ var text = ""
192
+ for (p in plugins) {
193
+ var cmds = ""
194
+ for (n in pluginCommandNames(p)) {
195
+ if (cmds.size > 0) {
196
+ cmds += ", "
197
+ }
198
+ cmds += n
199
+ }
200
+ text += " ${p.name()} ${cmds}"
201
+ let evts = pluginEventNames(p)
202
+ if (evts.size > 0) {
203
+ text += " events: "
204
+ var first = true
205
+ for (n in evts) {
206
+ if (!first) {
207
+ text += ", "
208
+ }
209
+ text += n
210
+ first = false
211
+ }
212
+ }
213
+ text += "\n"
214
+ }
215
+ return text
216
+ }
217
+
218
+ /**
219
+ * 机器可读的插件清单(RFC §8 第 7 条的答案 C:给工具/CI 用,**不动 Plugin 接口**)
220
+ *
221
+ * 形态 `[{name, commands, events, permissions, hasShim}, ...]`:
222
+ * - `commands` / `events`:全名数组(`<插件名>:<短名>`,与 IPC 注册名、能力清单里写的名字一致)
223
+ * - `permissions`:集全名 → 成员全名数组(**框架展开后**的样子,也就是清单里能引用的名字)
224
+ * - `hasShim`:是否带前端 JS 片段(工具据此判断要不要等 `__CJ_TAURI__.<插件名>`)
225
+ * 数组与权限键都按字典序排:HashMap 迭代顺序不定,工具的输出 / diff 需要稳定。
226
+ *
227
+ * 数据来自 describe()(批次 3 自描述双通道):命令/事件/权限集之外,携带 supportedHosts /
228
+ * requiredCapabilities 两个命令表推不出来的语义项(RFC §4.6.7)。
229
+ */
230
+ public func describePluginsJson(plugins: ArrayList<Plugin>): JsonValue {
231
+ let arr = ArrayList<JsonValue>()
232
+ for (p in plugins) {
233
+ var obj = JsonObject()
234
+ let d = p.describe()
235
+ obj.put("name", JsonString(p.name()))
236
+ obj.put("commands", jsonStringArray(pluginCommandNames(p)))
237
+ obj.put("events", jsonStringArray(pluginEventNames(p)))
238
+ obj.put("permissions", jsonPermissionSets(p))
239
+ obj.put("hasShim", JsonBool(p.jsShim().size > 0))
240
+ obj.put("supportedHosts", jsonStringArray(d.supportedHosts))
241
+ obj.put("requiredCapabilities", jsonStringArray(d.requiredCapabilities))
242
+ arr.add(obj)
243
+ }
244
+ return JsonArray(arr)
245
+ }
246
+
247
+ /** 字符串数组 → JSON 数组,元素按字典序(HashMap 来的一律先排序,保证输出稳定) */
248
+ func jsonStringArray(items: ArrayList<String>): JsonValue {
249
+ let sorted = sortedStrings(items)
250
+ let arr = ArrayList<JsonValue>()
251
+ for (s in sorted) {
252
+ arr.add(JsonString(s))
253
+ }
254
+ return JsonArray(arr)
255
+ }
256
+
257
+ /** 命名权限集 → JSON 对象(键 = 集全名,值 = 成员全名数组);键也按字典序 */
258
+ func jsonPermissionSets(p: Plugin): JsonValue {
259
+ let sets = pluginPermissionSets(p)
260
+ let keys = ArrayList<String>()
261
+ for ((setName, _) in sets) {
262
+ keys.add(setName)
263
+ }
264
+ var obj = JsonObject()
265
+ for (setName in sortedStrings(keys)) {
266
+ match (sets.get(setName)) {
267
+ case Some(members) => obj.put(setName, jsonStringArray(members))
268
+ case None => ()
269
+ }
270
+ }
271
+ return obj
272
+ }
273
+
274
+ /** 复制一份并按字典序排序(不改动入参;元素量级只有每插件的几条命令,开销可忽略) */
275
+ func sortedStrings(items: ArrayList<String>): ArrayList<String> {
276
+ let out = ArrayList<String>()
277
+ for (s in items) {
278
+ out.add(s)
279
+ }
280
+ sort(out)
281
+ return out
282
+ }
283
+
284
+ /**
285
+ * 未授权提示:插件声明了、但应用的能力清单没放行时,说清缺哪条、加到哪去。
286
+ * 只提示、不改权限——放行与否仍由 capabilities 决定(RFC §5.3 的取舍 B)。
287
+ * 调用时机:能力清单加载完成之后、页面加载之前(TauriApp.prepare)。
288
+ */
289
+ public func warnUnauthorizedPlugins(plugins: ArrayList<Plugin>, caps: CapabilityRegistry): Unit {
290
+ for (p in plugins) {
291
+ for (name in pluginCommandNames(p)) {
292
+ if (!caps.canInvoke(name)) {
293
+ eprintln("[cj-tauri] 插件 ${p.name()} 的命令 ${name} 尚未授权,前端调用会被拒绝")
294
+ eprintln("[cj-tauri] 请在 capabilities 目录的 json 里把 \"${name}\" 加进 commands")
295
+ }
296
+ }
297
+ for (name in pluginEventNames(p)) {
298
+ if (!caps.canEmit(name)) {
299
+ eprintln("[cj-tauri] 插件 ${p.name()} 的事件 ${name} 尚未授权,推送会被丢弃")
300
+ eprintln("[cj-tauri] 请在 capabilities 目录的 json 里把 \"${name}\" 加进 events")
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ /**
307
+ * 提示「吃 scope 的插件没有被声明 scope」——旧清单缺省行为:全量放行(RFC-003 第 4 期)。
308
+ *
309
+ * 只在「该插件至少有一条命令被授权」时提示:没用到它的应用不必被这行日志打扰。
310
+ * 声明了 scope 的插件(caps.hasScopeForPlugin 为真)不提示。
311
+ */
312
+ public func warnUnscopedPlugins(plugins: ArrayList<Plugin>, caps: CapabilityRegistry): Unit {
313
+ for (p in plugins) {
314
+ if (!p.scopeAware()) {
315
+ continue
316
+ }
317
+ var authorized = false
318
+ for (name in pluginCommandNames(p)) {
319
+ if (caps.canInvoke(name)) {
320
+ authorized = true
321
+ break
322
+ }
323
+ }
324
+ if (!authorized) {
325
+ continue
326
+ }
327
+ if (caps.hasScopeForPlugin(p.name())) {
328
+ continue
329
+ }
330
+ eprintln("[cj-tauri] 插件 ${p.name()} 的路径参数未声明 scope:按旧行为全量放行")
331
+ eprintln("[cj-tauri] 建议在 capabilities 的 json 里加 \"scopes\": { \"${p.name()}\": { \"allow\": [\"<放行路径>\"] } }")
332
+ }
333
+ }