cj-tauri 1.1.2 → 2.0.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 (101) hide show
  1. package/README.md +64 -18
  2. package/cjpm.toml +52 -52
  3. package/cli/cjpm.toml +10 -1
  4. package/cli/src/doctor.cj +4 -2
  5. package/cli/src/main.cj +11 -3
  6. package/cli/src/manifest.cj +558 -0
  7. package/cli/src/project.cj +38 -22
  8. package/cli/src/scaffold.cj +369 -361
  9. package/cli/src/sync.cj +526 -0
  10. package/cli/src/util.cj +168 -159
  11. package/cli/templates/app/README.md +48 -80
  12. package/cli/templates/app/{capabilities → assets/capabilities}/default.json +7 -7
  13. package/cli/templates/app/{ui → assets/ui}/index.html +118 -118
  14. package/cli/templates/app/cj-tauri.toml +22 -0
  15. package/cli/templates/app/cjpm.toml +34 -17
  16. package/cli/templates/app/src/main.cj +90 -88
  17. package/cli/templates/app-react/README.md +56 -67
  18. package/cli/templates/app-react/{capabilities → assets/capabilities}/default.json +7 -7
  19. package/cli/templates/app-react/{ui → assets/ui}/package.json +19 -19
  20. package/cli/templates/app-react/{ui → assets/ui}/src/main.jsx +6 -6
  21. package/cli/templates/app-react/{ui → assets/ui}/vite.config.js +24 -24
  22. package/cli/templates/app-react/cj-tauri.toml +23 -0
  23. package/cli/templates/app-react/cjpm.toml +34 -17
  24. package/cli/templates/app-react/src/main.cj +91 -102
  25. package/cli/templates/app-vue/README.md +56 -60
  26. package/cli/templates/app-vue/{capabilities → assets/capabilities}/default.json +7 -7
  27. package/cli/templates/app-vue/{ui → assets/ui}/package.json +18 -18
  28. package/cli/templates/app-vue/{ui → assets/ui}/src/main.js +4 -4
  29. package/cli/templates/app-vue/{ui → assets/ui}/vite.config.js +24 -24
  30. package/cli/templates/app-vue/cj-tauri.toml +23 -0
  31. package/cli/templates/app-vue/cjpm.toml +34 -17
  32. package/cli/templates/app-vue/src/main.cj +91 -102
  33. package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/app.json5 +13 -13
  34. package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/element/string.json +8 -8
  35. package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/layered_image.json +6 -6
  36. package/cli/templates/{ohos-pc → ohos-skeleton}/README.md +79 -79
  37. package/cli/templates/{ohos-pc → ohos-skeleton}/build-profile.json5 +46 -46
  38. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/build-profile.json5 +26 -26
  39. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/cjpm.toml +43 -43
  40. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/hvigorfile.ts +6 -6
  41. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/oh-package.json5 +9 -9
  42. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/ability_stage.cj +17 -17
  43. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/app_host.cj +94 -94
  44. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/index.cj +167 -167
  45. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/main_ability.cj +139 -139
  46. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/module.json5 +49 -49
  47. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/color.json +8 -8
  48. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/string.json +16 -16
  49. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/layered_image.json +6 -6
  50. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/profile/main_pages.json +2 -2
  51. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/capabilities.json +6 -6
  52. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/index.html +92 -92
  53. package/cli/templates/{ohos-pc → ohos-skeleton}/hvigor/hvigor-config.json5 +5 -5
  54. package/cli/templates/{ohos-pc → ohos-skeleton}/hvigorfile.ts +6 -6
  55. package/cli/templates/{ohos-pc → ohos-skeleton}/oh-package.json5 +10 -10
  56. package/package.json +4 -2
  57. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  58. package/src/app.cj +49 -20
  59. package/src/app_spec.cj +76 -0
  60. package/src/arkweb_script.cj +142 -154
  61. package/src/host.cj +20 -17
  62. package/src/host_arkweb.cj +778 -740
  63. package/src/host_webkit.cj +22 -0
  64. package/src/host_webview2.cj +22 -0
  65. package/src/launch.cj +62 -0
  66. package/src/platform/platform_app_context.cj +77 -0
  67. package/src/platform/platform_assets.cj +33 -0
  68. package/src/platform/platform_discover.cj +176 -0
  69. package/src/platform/platform_env.cj +29 -0
  70. package/src/platform/platform_feature.cj +31 -0
  71. package/src/platform/platform_page_source.cj +87 -0
  72. package/src/platform/platform_target_info.cj +32 -0
  73. package/src/platform_host_provider.cj +87 -0
  74. package/src/plugin.cj +36 -2
  75. package/src/plugins/clipboard/plugin_clipboard.cj +8 -0
  76. package/src/plugins/menu/plugin_menu.cj +158 -150
  77. package/src/plugins/notification/plugin_notification.cj +8 -0
  78. package/src/plugins/serial/plugin_serial.cj +454 -442
  79. package/src/plugins/serial/serial_bridge.cj +194 -186
  80. package/src/plugins/serial/serial_ohos_stub.cj +61 -0
  81. package/src/plugins/shell/plugin_shell.cj +258 -230
  82. package/src/plugins/sqlite/plugin_sqlite.cj +710 -697
  83. package/src/plugins/sqlite/sqlite_ffi.cj +211 -206
  84. package/src/plugins/sqlite/sqlite_ohos_stub.cj +91 -0
  85. package/src/tests/app_spec_test.cj +127 -0
  86. package/src/tests/fake_host.cj +23 -8
  87. package/src/tests/platform_contract_test.cj +251 -0
  88. package/src/tests/plugin_test.cj +585 -542
  89. package/src/version.cj +1 -1
  90. package/src/host_factory.cj +0 -48
  91. /package/cli/templates/app-react/{ui → assets/ui}/index.html +0 -0
  92. /package/cli/templates/app-react/{ui → assets/ui}/src/App.css +0 -0
  93. /package/cli/templates/app-react/{ui → assets/ui}/src/App.jsx +0 -0
  94. /package/cli/templates/app-vue/{ui → assets/ui}/index.html +0 -0
  95. /package/cli/templates/app-vue/{ui → assets/ui}/src/App.vue +0 -0
  96. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/background.png +0 -0
  97. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/foreground.png +0 -0
  98. /package/cli/templates/{ohos-pc → ohos-skeleton}/build-hap.sh +0 -0
  99. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/background.png +0 -0
  100. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/foreground.png +0 -0
  101. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/startIcon.png +0 -0
@@ -0,0 +1,558 @@
1
+ /**
2
+ * cj-tauri.toml 清单(RFC-002 §3):解析 + 校验 + 桌面骨架派生
3
+ *
4
+ * 批次 2 用受控 TOML 子集并手写解析(stdx 无 toml 模块,实测);子集约定:
5
+ * - 段:[app] / [[windows]] / [assets] / [capabilities],无嵌套表;
6
+ * - 值:带引号字符串、整数、true/false;引号值后不写行内注释(整行 # 注释不受限);
7
+ * - 未知键直接报错:清单由本 CLI 生态生成与消费,拼错键要在 check 期暴露。
8
+ */
9
+ package cli
10
+
11
+ import std.collection.*
12
+ import std.convert.*
13
+
14
+ /** [[windows]] 的一条声明(RFC §3) */
15
+ public class ManifestWindow {
16
+ public var label: String = "main"
17
+ public var title: String = ""
18
+ public var width: Int64 = 1100
19
+ public var height: Int64 = 720
20
+ public var resizable: Bool = true
21
+ }
22
+
23
+ /** 清单内存模型;字段默认值即「最小清单」的缺省(RFC §3 约定值) */
24
+ public class Manifest {
25
+ public var appId: String = ""
26
+ public var name: String = ""
27
+ public var version: String = ""
28
+ public var icon: String = ""
29
+ public var entry: String = "src/main.cj"
30
+ public var pkg: String = "" // 业务 cjpm 包名:sync 生成 launcher 的 import 用
31
+ public var windows: ArrayList<ManifestWindow> = ArrayList<ManifestWindow>()
32
+ public var assetsRoot: String = "assets"
33
+ public var page: String = "ui/index.html"
34
+ public var capabilitiesDir: String = "assets/capabilities"
35
+
36
+ /** 可执行名:appId 最后一段 */
37
+ public func exeName(): String {
38
+ let parts = appId.split(".")
39
+ if (parts.size == 0) {
40
+ return "app"
41
+ }
42
+ return parts[parts.size - 1]
43
+ }
44
+
45
+ /**
46
+ * 骨架模块名(cjpm [package] name):默认 exeName(appId 最后一段);与业务包名
47
+ * (清单 package = 仓颉包名)撞名时加 _app 后缀。cjpm 强制两条约束(批次 3-PR2 实测):
48
+ * ① 每个工程的 toml 模块名 == 其源文件包名(music:toml `music_app` ↔ `package music_app`);
49
+ * ② 依赖树内模块名唯一。业务包名被 ① 钉死为 package,最常见的命名
50
+ * com.example.<pkg> + package = <pkg> 必然撞 ②,撞时骨架模块名为 <pkg>_app。
51
+ * 二进制名与模块名无关:cjpm 可执行工程恒输出 target/release/bin/main。
52
+ */
53
+ public func skeletonName(): String {
54
+ if (this.exeName() == this.pkg) {
55
+ return this.exeName() + "_app"
56
+ }
57
+ return this.exeName()
58
+ }
59
+
60
+ /** 主窗口:无 [[windows]] 时按 name 给默认窗 */
61
+ public func mainWindow(): ManifestWindow {
62
+ if (windows.size == 0) {
63
+ let w = ManifestWindow()
64
+ w.title = name
65
+ return w
66
+ }
67
+ return windows.get(0).getOrThrow()
68
+ }
69
+
70
+ // ---------- 解析 ----------
71
+
72
+ public static func fromToml(text: String, source: String): Manifest {
73
+ let m = Manifest()
74
+ var section = ""
75
+ var lineNo: Int64 = 0
76
+ for (raw in text.split("\n")) {
77
+ lineNo += 1
78
+ let trimmed = raw.trimAscii()
79
+ if (trimmed.size == 0 || trimmed.startsWith("#")) {
80
+ continue
81
+ }
82
+ if (trimmed.startsWith("[")) {
83
+ section = trimmed.replace("[", "").replace("]", "").trimAscii()
84
+ if (section == "windows") {
85
+ m.windows.add(ManifestWindow())
86
+ }
87
+ continue
88
+ }
89
+ let kv = trimmed.split("=")
90
+ if (kv.size != 2) {
91
+ throw IllegalArgumentException("${source}:${lineNo} 无法解析的行(应为 key = value):${trimmed}")
92
+ }
93
+ try {
94
+ m.apply(section, kv[0].trimAscii(), kv[1].trimAscii(), "${source}:${lineNo}")
95
+ } catch (e: Exception) {
96
+ throw IllegalArgumentException("${e.message}")
97
+ }
98
+ }
99
+ return m
100
+ }
101
+
102
+ private func apply(section: String, key: String, value: String, at: String): Unit {
103
+ if (section.size == 0) {
104
+ throw IllegalArgumentException("${at}: 键出现在任何段之前:${key}")
105
+ }
106
+ if (section == "app") {
107
+ match (key) {
108
+ case "id" => this.appId = strValue(value, at)
109
+ case "name" => this.name = strValue(value, at)
110
+ case "version" => this.version = strValue(value, at)
111
+ case "icon" => this.icon = strValue(value, at)
112
+ case "entry" => this.entry = strValue(value, at)
113
+ case "package" => this.pkg = strValue(value, at)
114
+ case _ => throw IllegalArgumentException("${at}: 未知的 [app] 键:${key}")
115
+ }
116
+ return
117
+ }
118
+ if (section == "windows") {
119
+ if (this.windows.size == 0) {
120
+ this.windows.add(ManifestWindow())
121
+ }
122
+ let w = this.windows.get(this.windows.size - 1).getOrThrow()
123
+ match (key) {
124
+ case "label" => w.label = strValue(value, at)
125
+ case "title" => w.title = strValue(value, at)
126
+ case "width" => w.width = intValue(value, at)
127
+ case "height" => w.height = intValue(value, at)
128
+ case "resizable" => w.resizable = boolValue(value, at)
129
+ case _ => throw IllegalArgumentException("${at}: 未知的 [[windows]] 键:${key}")
130
+ }
131
+ return
132
+ }
133
+ if (section == "assets") {
134
+ match (key) {
135
+ case "root" => this.assetsRoot = strValue(value, at)
136
+ case "page" => this.page = strValue(value, at)
137
+ case _ => throw IllegalArgumentException("${at}: 未知的 [assets] 键:${key}")
138
+ }
139
+ return
140
+ }
141
+ if (section == "capabilities") {
142
+ match (key) {
143
+ case "dir" => this.capabilitiesDir = strValue(value, at)
144
+ case _ => throw IllegalArgumentException("${at}: 未知的 [capabilities] 键:${key}")
145
+ }
146
+ return
147
+ }
148
+ throw IllegalArgumentException("${at}: 未知段:[${section}]")
149
+ }
150
+
151
+ /** 引号值:取首个引号内内容(引号后的行内注释一并忽略);裸值原样 */
152
+ private static func strValue(raw: String, at: String): String {
153
+ let v = raw.trimAscii()
154
+ if (v.startsWith("\"")) {
155
+ let parts = v.split("\"")
156
+ if (parts.size < 3) {
157
+ throw IllegalArgumentException("${at}: 引号值缺右引号:${v}")
158
+ }
159
+ return parts[1]
160
+ }
161
+ return v
162
+ }
163
+
164
+ private static func intValue(raw: String, at: String): Int64 {
165
+ let v = raw.trimAscii()
166
+ if (v.startsWith("\"")) {
167
+ throw IllegalArgumentException("${at}: 应为整数,却是字符串:${v}")
168
+ }
169
+ let body = v.split("#")[0].trimAscii()
170
+ try {
171
+ return Int64.parse(body)
172
+ } catch (e: Exception) {
173
+ throw IllegalArgumentException("${at}: 不是整数:${body}")
174
+ }
175
+ }
176
+
177
+ private static func boolValue(raw: String, at: String): Bool {
178
+ let v = raw.trimAscii().split("#")[0].trimAscii()
179
+ if (v == "true") {
180
+ return true
181
+ }
182
+ if (v == "false") {
183
+ return false
184
+ }
185
+ throw IllegalArgumentException("${at}: 应为 true/false:${v}")
186
+ }
187
+
188
+ // ---------- 校验 ----------
189
+
190
+ /** 纯静态校验(不碰文件系统);返回错误列表,空 = 通过。文件系统检查在 check 命令里 */
191
+ public func validate(): ArrayList<String> {
192
+ let errs = ArrayList<String>()
193
+ if (this.appId.size == 0) {
194
+ errs.add("[app] id 不能为空")
195
+ } else {
196
+ let segs = this.appId.split(".")
197
+ if (segs.size < 2) {
198
+ errs.add("[app] id 至少两段反向域名(如 com.example.music):${this.appId}")
199
+ }
200
+ for (seg in segs) {
201
+ if (seg.size == 0) {
202
+ errs.add("[app] id 的段不能为空:${this.appId}")
203
+ continue
204
+ }
205
+ // 段须字母开头、只含小写字母/数字/下划线——同时满足 Tauri identifier 习惯与
206
+ // 鸿蒙 bundleName 规范(bundleNameFrom 会给 my-app 这类名字生成 pr4_app 段,
207
+ // 下划线是鸿蒙侧的合法字符,不能禁)
208
+ var bad = false
209
+ for (i in 0..seg.size) {
210
+ let c = Int64(seg.toArray()[i])
211
+ if (i == 0) {
212
+ if (c < 97 || c > 122) {
213
+ bad = true
214
+ break
215
+ }
216
+ } else if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 122) || c == 95)) {
217
+ bad = true
218
+ break
219
+ }
220
+ }
221
+ if (bad) {
222
+ errs.add("[app] id 的段须字母开头、只含小写字母/数字/下划线(段=${seg})")
223
+ }
224
+ }
225
+ }
226
+ if (this.name.size == 0) {
227
+ errs.add("[app] name 不能为空")
228
+ }
229
+ if (this.version.size == 0) {
230
+ errs.add("[app] version 不能为空")
231
+ }
232
+ if (this.pkg.size == 0) {
233
+ errs.add("[app] package 不能为空(sync 生成 launcher 的 import 需要)")
234
+ }
235
+ let labels = ArrayList<String>()
236
+ for (w in this.windows) {
237
+ if (w.label.size == 0) {
238
+ errs.add("[[windows]] label 不能为空")
239
+ continue
240
+ }
241
+ var dup = false
242
+ for (seen in labels) {
243
+ if (seen == w.label) {
244
+ dup = true
245
+ break
246
+ }
247
+ }
248
+ if (dup) {
249
+ errs.add("[[windows]] label 重复:${w.label}")
250
+ } else {
251
+ labels.add(w.label)
252
+ }
253
+ if (w.width <= 0 || w.height <= 0) {
254
+ errs.add("[[windows]] ${w.label} 尺寸必须为正")
255
+ }
256
+ }
257
+ if (this.page.size == 0) {
258
+ errs.add("[assets] page 不能为空")
259
+ }
260
+ return errs
261
+ }
262
+
263
+ // ---------- 桌面骨架派生(RFC §7.1/§7.2;批次 2 仅 desktop) ----------
264
+
265
+ /** targets/desktop/cjpm.toml;链接段照抄 examples/music 的既有配置(AGENTS:产物口径不变) */
266
+ public func renderDesktopToml(fwRoot: String, cliVer: String): String {
267
+ let native = toTomlPath(fwRoot) + "/native"
268
+ // TOML 注释只认 '#'('//' 是仓颉/C 风格,cjpm 的解析器会当场拒绝),指纹头与 .cj 生成物同款但换字符
269
+ return "# generated by cj-tauri sync (v" + cliVer + ") — 不要手改;改动会被 check 判为漂移\n" +
270
+ "# 派生自 cj-tauri.toml:" + this.appId + "@" + this.version + "\n" +
271
+ "[package]\n" +
272
+ " cjc-version = \"1.0.5\"\n" +
273
+ " name = \"" + this.skeletonName() + "\"\n" +
274
+ " description = \"generated desktop skeleton for " + this.appId + "\"\n" +
275
+ " version = \"" + this.version + "\"\n" +
276
+ " target-dir = \"\"\n" +
277
+ " output-type = \"executable\"\n" +
278
+ " compile-option = \"\"\n" +
279
+ " override-compile-option = \"\"\n" +
280
+ " link-option = \"\"\n" +
281
+ " package-configuration = {}\n" +
282
+ "\n[dependencies]\n" +
283
+ " cjTauri = { path = \"" + toTomlPath(fwRoot) + "\" }\n" +
284
+ " " + this.pkg + " = { path = \"../..\" }\n" +
285
+ "\n[target.x86_64-unknown-linux-gnu]\n" +
286
+ " link-option = \"-L" + native + " -lcjtbridge -lwebkit2gtk-4.1 -lgtk-3 -lgdk-3 -lglib-2.0 -lgio-2.0 -lsoup-3.0 -ljavascriptcoregtk-4.1 -lgobject-2.0\"\n" +
287
+ "\n[target.x86_64-unknown-linux-gnu.bin-dependencies]\n" +
288
+ " path-option = [\"" + "$" + "{CANGJIE_STDX}\"]\n" +
289
+ "\n[target.x86_64-w64-mingw32]\n" +
290
+ " link-option = \"-L" + native + " -lcjtbridge --subsystem=windows\"\n" +
291
+ "\n[target.x86_64-w64-mingw32.bin-dependencies]\n" +
292
+ " path-option = [\"" + "$" + "{CANGJIE_STDX}\"]\n"
293
+ }
294
+
295
+ /** targets/desktop/src/main.cj:launcher 调业务装配点 build()(批次 3,RFC §4.1/§4.2) */
296
+ public func renderDesktopMain(cliVer: String): String {
297
+ let w = this.mainWindow()
298
+ return "// generated by cj-tauri sync (v" + cliVer + ") — 不要手改;改动会被 check 判为漂移\n" +
299
+ "// 桌面 launcher:页面/窗口/资产/能力目录由清单派生;业务装配点 " + this.pkg + ".build()(RFC §4.1/§4.2)\n" +
300
+ "package " + this.skeletonName() + "\n" +
301
+ "\n" +
302
+ "import cjTauri.*\n" +
303
+ "import cjTauri.platform.*\n" +
304
+ "import " + this.pkg + ".*\n" +
305
+ "\n" +
306
+ "main(): Int64 {\n" +
307
+ " let spec = build()\n" +
308
+ " .page(PageSource.Asset(\"" + this.page + "\"))\n" +
309
+ " .window(WindowConfig(\"${w.title}\", ${w.width}, ${w.height}))\n" +
310
+ " .assets(DirectoryAssetResolver(\"" + this.assetsRoot + "\"))\n" +
311
+ " .capabilities(\"" + this.capabilitiesDir + "\")\n" +
312
+ " launch(spec)\n" +
313
+ " return 0\n" +
314
+ "}\n"
315
+ }
316
+
317
+ // ---------- ohos 骨架派生(RFC §7.1/§7.3;批次 3-PR3) ----------
318
+ // 生成策略:ohos-skeleton 模板 copyTree 整体拷渲染(静态物 + 图标),4 个清单派生文件用下列
319
+ // 渲染器覆盖——check 的漂移检测复用同一批渲染器,生成与校验天然一致。
320
+
321
+ /** 生成物里要写字面 `${X}`:仓颉源码中 ${} 是插值,只能拼接出来 */
322
+ private static func interp(name: String): String {
323
+ return "$" + "{" + name + "}"
324
+ }
325
+
326
+ /** targets/ohos/AppScope/app.json5:bundleName=id、vendor=name、versionName=version 全部清单派生 */
327
+ public func renderOhosAppJson(cliVer: String): String {
328
+ return "// generated by cj-tauri sync (v" + cliVer + ") — 不要手改;改动会被 check 判为漂移\n" +
329
+ "{\n" +
330
+ " \"app\": {\n" +
331
+ " /* bundleName 是应用在设备上的唯一 ID:两个应用写同一个 bundleName 会互相覆盖安装。\n" +
332
+ " 改它必须重新签名——DevEco 的「自动签名」会按新包名重新申请 profile。\n" +
333
+ " 取值来自清单 [app] id(sync 保证与清单一致)。 */\n" +
334
+ " \"bundleName\": \"" + this.appId + "\",\n" +
335
+ " \"vendor\": \"" + this.name + "\",\n" +
336
+ " \"versionCode\": 1000000,\n" +
337
+ " \"versionName\": \"" + this.version + "\",\n" +
338
+ " \"buildVersion\": \"1\",\n" +
339
+ " \"icon\": \"$media:layered_image\",\n" +
340
+ " \"label\": \"$string:app_name\"\n" +
341
+ " }\n" +
342
+ "}\n"
343
+ }
344
+
345
+ /**
346
+ * targets/ohos/entry/cjpm.toml:dynamic 入口模块
347
+ *
348
+ * 与模板的差异:① 业务包路径依赖(../../.. = 工程根,业务代码只有一份——RFC §7.3);
349
+ * ② version 随清单;③ 框架路径绝对化(本机口径,与 create 同规矩)。
350
+ * 模块名恒为 ohos_app_cangjie_entry(module.json5 的 srcEntry 钉死,不参与清单命名)。
351
+ */
352
+ public func renderOhosEntryToml(fwRoot: String, cliVer: String): String {
353
+ let dc = Manifest.interp("DEVECO_CANGJIE_HOME")
354
+ let dn = Manifest.interp("DEVECO_OH_NATIVE_HOME")
355
+ let ce = Manifest.interp("COMPILE_CONDITION_ENTRY")
356
+ let a1 = Manifest.interp("AARCH64_LIBS")
357
+ let a2 = Manifest.interp("AARCH64_MACRO_LIBS")
358
+ let a3 = Manifest.interp("AARCH64_KIT_LIBS")
359
+ let x1 = Manifest.interp("X86_64_OHOS_LIBS")
360
+ let x2 = Manifest.interp("X86_64_OHOS_MACRO_LIBS")
361
+ let x3 = Manifest.interp("X86_64_OHOS_KIT_LIBS")
362
+ return "# generated by cj-tauri sync (v" + cliVer + ") — 不要手改;改动会被 check 判为漂移\n" +
363
+ "[package]\n" +
364
+ " cjc-version = \"1.1.3\"\n" +
365
+ " compile-option = \"--dy-std --cfg=\\\"" + ce + "\\\"\"\n" +
366
+ " description = \"" + this.name + " - cj-tauri 鸿蒙入口 module\"\n" +
367
+ " name = \"ohos_app_cangjie_entry\"\n" +
368
+ " output-type = \"dynamic\"\n" +
369
+ " src-dir = \"./src/main/cangjie\"\n" +
370
+ " target-dir = \"\"\n" +
371
+ " version = \"" + this.version + "\"\n" +
372
+ "\n[dependencies]\n" +
373
+ " # 源码依赖:hvigor 的 CompileCangjie 会把框架各包按本模块(dynamic)需要的形态编进来。\n" +
374
+ " # 路径是本机绝对路径;换机器 / 挪仓库后重跑 cj-tauri sync ohos(与 create 同一条规矩)。\n" +
375
+ " cjTauri = { path = \"" + toTomlPath(fwRoot) + "\" }\n" +
376
+ " # 业务包:业务代码的唯一真相在工程根(RFC §7.3——骨架永不复制业务代码、不改包名)\n" +
377
+ " " + this.pkg + " = { path = \"../../..\" }\n" +
378
+ "\n[profile]\n" +
379
+ " [profile.build]\n" +
380
+ " incremental = true\n" +
381
+ " lto = \"\"\n" +
382
+ " [profile.customized-option]\n" +
383
+ " debug = \"-g -Woff all -Won apilevel-check\"\n" +
384
+ " release = \"--fast-math -O2 -s -Woff all -Won apilevel-check\"\n" +
385
+ " [profile.test]\n" +
386
+ "\n[target.aarch64-linux-ohos]\n" +
387
+ " compile-option = \"-B \\\"" + dc + "/build-tools/third_party/llvm/bin\\\" -B \\\"" + dn + "/sysroot/usr/lib/aarch64-linux-ohos\\\" -L \\\"" + dn + "/sysroot/usr/lib/aarch64-linux-ohos\\\" -L \\\"" + dn + "/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos\\\" -L \\\"" + dn + "/llvm/lib/aarch64-linux-ohos\\\" --sysroot \\\"" + dn + "/sysroot\\\"\"\n" +
388
+ "[target.aarch64-linux-ohos.bin-dependencies]\n" +
389
+ " # 鸿蒙版 stdx:相对路径指向骨架根上的 `stdx` 联接(build-hap.sh 用 CANGJIE_STDX_OHOS\n" +
390
+ " # 建,不入库)。hvigor 只替换它自己的模板变量,写别的 " + "$" + "{...}" + " 会变空串、整条\n" +
391
+ " # path-option 失效(模板里的实测教训,原样保留)。\n" +
392
+ " path-option = [\"" + a1 + "\", \"" + a2 + "\", \"" + a3 + "\", \"../stdx\"]\n" +
393
+ " package-option = {}\n" +
394
+ "\n[target.x86_64-linux-ohos]\n" +
395
+ " compile-option = \"-B \\\"" + dc + "/build-tools/third_party/llvm/bin\\\" -B \\\"" + dn + "/sysroot/usr/lib/x86_64-linux-ohos\\\" -L \\\"" + dn + "/sysroot/usr/lib/x86_64-linux-ohos\\\" -L \\\"" + dn + "/llvm/lib/clang/15.0.4/lib/x86_64-linux-ohos\\\" -L \\\"" + dn + "/llvm/lib/x86_64-linux-ohos\\\" --sysroot \\\"" + dn + "/sysroot\\\"\"\n" +
396
+ "[target.x86_64-linux-ohos.bin-dependencies]\n" +
397
+ " path-option = [\"" + x1 + "\", \"" + x2 + "\", \"" + x3 + "\", \"../stdx\"]\n" +
398
+ " package-option = {}\n"
399
+ }
400
+
401
+ /**
402
+ * targets/ohos/entry/src/main/cangjie/app_host.cj:鸿蒙侧框架装配(L0 胶水)
403
+ *
404
+ * 业务装配点 build() 三端共用(零平台知识,RFC §4.1);胶水层职责:
405
+ * ① 沙箱目录(ArkWebRuntime.configure)② rawfile 资产解析器 ③ 能力清单挂载 ④ launchWith(mobile)。
406
+ * 能力文件名单来自清单 [capabilities] 目录(sync 枚举后逐个渲染)。
407
+ */
408
+ public func renderOhosAppHost(cliVer: String, capsFiles: ArrayList<String>): String {
409
+ var capsBlock = ""
410
+ if (capsFiles.size == 0) {
411
+ capsBlock = " Hilog.info(1, \"CjTauri\", \"清单没有 [capabilities] 目录:未挂载能力清单(命令会如实被拒)\")\n"
412
+ } else {
413
+ var k: Int64 = 0
414
+ for (f in capsFiles) {
415
+ let varName = "caps" + k.toString()
416
+ capsBlock = capsBlock +
417
+ " let " + varName + " = readRawText(rm, \"capabilities/" + f + "\")\n" +
418
+ " if (" + varName + ".size > 0) {\n" +
419
+ " spec.capabilityJson(" + varName + ")\n" +
420
+ " } else {\n" +
421
+ " Hilog.info(1, \"CjTauri\", \"rawfile/capabilities/" + f + " 读不到(构建时打进 hap 了吗?):命令会被全部拒绝\")\n" +
422
+ " }\n"
423
+ k += 1
424
+ }
425
+ }
426
+ return "// generated by cj-tauri sync (v" + cliVer + ") — 不要手改;改动会被 check 判为漂移\n" +
427
+ "// 鸿蒙侧框架装配(批次 3-PR3,RFC §4.2/§4.6.5):业务装配点 " + this.pkg + ".build() 三端共用\n" +
428
+ "package ohos_app_cangjie_entry\n" +
429
+ "\n" +
430
+ "import kit.PerformanceAnalysisKit.Hilog\n" +
431
+ "import cjTauri.*\n" +
432
+ "import cjTauri.platform.*\n" +
433
+ "import " + this.pkg + ".*\n" +
434
+ "import ohos.resource_manager.*\n" +
435
+ "\n" +
436
+ "/**\n" +
437
+ " * rawfile 资产解析器(L0,RFC §6.2):hap 里没有工程目录,Asset 只能从 rawfile 解析。\n" +
438
+ " * 与桌面 DirectoryAssetResolver 同形:以「能否读到」为准,失败 = None(host_arkweb 在其上如实报错)。\n" +
439
+ " */\n" +
440
+ "class RawfileAssetResolver <: AssetResolver {\n" +
441
+ " private let rm: ResourceManager\n" +
442
+ "\n" +
443
+ " public init(rm: ResourceManager) {\n" +
444
+ " this.rm = rm\n" +
445
+ " }\n" +
446
+ "\n" +
447
+ " public func readText(name: String): Option<String> {\n" +
448
+ " try {\n" +
449
+ " return Some(String.fromUtf8(this.rm.getRawFileContent(name)))\n" +
450
+ " } catch (e: Exception) {\n" +
451
+ " return None\n" +
452
+ " }\n" +
453
+ " }\n" +
454
+ "\n" +
455
+ " public func exists(name: String): Bool {\n" +
456
+ " return readText(name).isSome()\n" +
457
+ " }\n" +
458
+ "}\n" +
459
+ "\n" +
460
+ "/** 读 hap 里的 rawfile(失败返回空串并打一行 hilog;调用方按「空 = 没有」降级,不让异常冒到生命周期) */\n" +
461
+ "func readRawText(rm: ResourceManager, path: String): String {\n" +
462
+ " try {\n" +
463
+ " return String.fromUtf8(rm.getRawFileContent(path))\n" +
464
+ " } catch (e: Exception) {\n" +
465
+ " Hilog.info(1, \"CjTauri\", \"rawfile " + "$" + "{path} 读取失败: " + "$" + "{e}\")\n" +
466
+ " return \"\"\n" +
467
+ " }\n" +
468
+ "}\n" +
469
+ "\n" +
470
+ "/**\n" +
471
+ " * 装配并运行框架应用(非 UI 线程调用;run() 阻塞到 quit())\n" +
472
+ " *\n" +
473
+ " * 页面取清单 [assets] page(sync 生成时已校验存在);能力清单是构建期拷进 hap 的\n" +
474
+ " * rawfile/capabilities 下的各份 json——文件名单是清单侧的真相,由生成器逐个渲染。\n" +
475
+ " */\n" +
476
+ "public func startApp(filesDir: String, rm: ResourceManager): Unit {\n" +
477
+ " Hilog.info(1, \"CjTauri\", \"startApp begin (ohos skeleton, business = " + this.pkg + ".build())\")\n" +
478
+ " // 沙箱目录要在 load 之前给定:宿主把页面落沙箱再以 file:// 加载(与桌面 load(source) 同一契约)\n" +
479
+ " ArkWebRuntime.configure(filesDir)\n" +
480
+ " let spec = build()\n" +
481
+ " .page(PageSource.Asset(\"" + this.page + "\"))\n" +
482
+ " .assets(RawfileAssetResolver(rm))\n" +
483
+ capsBlock +
484
+ " launchWith(spec, Platform.mobile(filesDir, \"unknown\"))\n" +
485
+ " Hilog.info(1, \"CjTauri\", \"after run\")\n" +
486
+ "}\n"
487
+ }
488
+
489
+ /**
490
+ * targets/ohos/entry/src/main/cangjie/main_ability.cj:UIAbility(只管生命周期,装配在 app_host.cj)
491
+ *
492
+ * registerSelf / waitAndLogHost / 不碰窗口尺寸 这些段落原样取自 ohos-skeleton 模板——模板已真机验证。
493
+ */
494
+ public func renderOhosMainAbility(cliVer: String): String {
495
+ return "// generated by cj-tauri sync (v" + cliVer + ") — 不要手改;改动会被 check 判为漂移\n" +
496
+ "// 鸿蒙 UIAbility(批次 3-PR3,RFC §4.2):本类只管生命周期;装配在非 UI 线程的 startApp(见 app_host.cj)\n" +
497
+ "package ohos_app_cangjie_entry\n" +
498
+ "\n" +
499
+ "import kit.PerformanceAnalysisKit.Hilog\n" +
500
+ "import kit.AbilityKit.Want\n" +
501
+ "import kit.AbilityKit.UIAbility\n" +
502
+ "import kit.AbilityKit.LaunchParam\n" +
503
+ "import kit.ArkUI.WindowStage\n" +
504
+ "import cjTauri.*\n" +
505
+ "\n" +
506
+ "class MainAbility <: UIAbility {\n" +
507
+ " public init() {\n" +
508
+ " super()\n" +
509
+ " registerSelf()\n" +
510
+ " }\n" +
511
+ "\n" +
512
+ " public override func onCreate(want: Want, launchParam: LaunchParam): Unit {\n" +
513
+ " Hilog.info(1, \"CjTauri\", \"MainAbility OnCreated." + "$" + "{want.abilityName}\")\n" +
514
+ " // run() 会阻塞到 quit(),只能在非 UI 线程上跑——UI 线程要留给 ArkUI 建树。\n" +
515
+ " // 沙箱准备、rawfile 材化、框架装配都在 startApp 里完成(L0 胶水,见 app_host.cj)。\n" +
516
+ " let filesDir = \"" + "$" + "{this.context.filesDir}\"\n" +
517
+ " let rm = this.context.resourceManager\n" +
518
+ " let _ = spawn { startApp(filesDir, rm) }\n" +
519
+ " }\n" +
520
+ "\n" +
521
+ " public override func onWindowStageCreate(windowStage: WindowStage): Unit {\n" +
522
+ " Hilog.info(1, \"CjTauri\", \"MainAbility onWindowStageCreate.\")\n" +
523
+ " // 等框架把宿主登记上再建树:组件的 build() 只有在该 label 有宿主、页面地址非空时\n" +
524
+ " // 才渲染 `Web`。等不到也照常 loadContent——页面会自己报桥缺失,比静默白屏好排查。\n" +
525
+ " this.waitAndLogHost(DEFAULT_WINDOW_LABEL)\n" +
526
+ " windowStage.loadContent(\"EntryView\")\n" +
527
+ " }\n" +
528
+ "\n" +
529
+ " /**\n" +
530
+ " * 有界等待(3s)默认窗口的宿主登记完成,并把页面地址 / 预执行脚本条数打进 hilog\n" +
531
+ " *\n" +
532
+ " * 白屏定位用:宿主把页面落盘后的 file:// 地址必须非空,否则组件拿不到 src。\n" +
533
+ " */\n" +
534
+ " private func waitAndLogHost(label: String): Unit {\n" +
535
+ " var polls = 0\n" +
536
+ " while (polls < 300) {\n" +
537
+ " match (ArkWebRuntime.hostFor(label)) {\n" +
538
+ " case Some(_) => break\n" +
539
+ " case None => ()\n" +
540
+ " }\n" +
541
+ " sleep(Duration.millisecond * 10)\n" +
542
+ " polls += 1\n" +
543
+ " }\n" +
544
+ " if (polls >= 300) {\n" +
545
+ " Hilog.info(1, \"CjTauri\", \"host not registered in 3s: label=" + "$" + "{label}\")\n" +
546
+ " return\n" +
547
+ " }\n" +
548
+ " Hilog.info(1, \"CjTauri\", \"host registered after " + "$" + "{polls} polls label=" + "$" + "{label}\")\n" +
549
+ " match (ArkWebRuntime.hostFor(label)) {\n" +
550
+ " case Some(h) =>\n" +
551
+ " Hilog.info(1, \"CjTauri\", \"host pageUrl label=" + "$" + "{label} url=\\\"" + "$" + "{h.currentUrl()}\\\"\")\n" +
552
+ " Hilog.info(1, \"CjTauri\", \"host initScripts label=" + "$" + "{label} count=" + "$" + "{h.initScriptsSnapshot().size}\")\n" +
553
+ " case None => Hilog.info(1, \"CjTauri\", \"host missing at loadContent label=" + "$" + "{label}\")\n" +
554
+ " }\n" +
555
+ " }\n" +
556
+ "}\n"
557
+ }
558
+ }