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
@@ -1,361 +1,369 @@
1
- /**
2
- * cj-tauri CLI:项目脚手架(create)
3
- *
4
- * 模板位于 <框架根>/cli/templates/<模板目录>,占位符形如 {{PROJECT_NAME}};
5
- * 递归拷贝 + 文本渲染,生成一个可直接 `cj-tauri dev`(桌面)或 `bash build-hap.sh`(鸿蒙)的应用。
6
- */
7
- package cli
8
-
9
- import std.fs.*
10
- import std.collection.*
11
-
12
- /** 模板占位符取值 */
13
- public class TemplateVars {
14
- let projectName: String
15
- let packageName: String
16
- let frameworkRoot: String
17
- let nativeDir: String
18
- let stdxDir: String
19
- let targetSection: String
20
- /** 鸿蒙应用的安装 ID(bundleName),桌面模板不用 */
21
- let bundleName: String
22
- /** 应用显示名(桌面模板用项目名,鸿蒙写进 string.json) */
23
- let appLabel: String
24
-
25
- public init(projectName: String, packageName: String, frameworkRoot: String, stdxDir: String,
26
- bundleName: String, appLabel: String) {
27
- this.projectName = projectName
28
- this.packageName = packageName
29
- this.frameworkRoot = frameworkRoot
30
- this.nativeDir = joinPath(frameworkRoot, "native")
31
- this.stdxDir = stdxDir
32
- this.targetSection = buildTargetSection(joinPath(frameworkRoot, "native"), stdxDir)
33
- this.bundleName = bundleName
34
- this.appLabel = appLabel
35
- }
36
- }
37
-
38
- /** 生成 cjpm.toml 的 [target.<triple>] 段落(含桥链接参数与 stdx 路径) */
39
- func buildTargetSection(nativeDir: String, stdxDir: String): String {
40
- let triple = hostTriple()
41
- var linkOpt = "-L" + escapeTomlString(nativeDir) + " -lcjtbridge"
42
- if (hostIsWindows()) {
43
- // GUI 子系统:不写这个开关,产物是 console 程序,双击时会多出一个控制台黑窗口(AGENTS §4)。
44
- // 取值只能用链接器那套——cjc 把 --link-options 直通 ld.lld,gcc 的 -mwindows 会被拒。
45
- // 注意别在这儿写「行内 `#` 注释」:Cangjie 里 `#` 是 raw string 前缀、不是注释符,
46
- // 生成出来的 toml 那行也就只放开关、不放尾注(保证与文档里的行数表格一致)。
47
- linkOpt = linkOpt + " --subsystem=windows"
48
- } else {
49
- linkOpt = linkOpt + " -lwebkit2gtk-4.1 -lgtk-3 -lgdk-3 -lglib-2.0 -lgio-2.0 -lsoup-3.0" +
50
- " -ljavascriptcoregtk-4.1 -lgobject-2.0"
51
- }
52
- var s = "[target." + triple + "]\n"
53
- s = s + " link-option = \"" + linkOpt + "\"\n"
54
- s = s + "\n[target." + triple + ".bin-dependencies]\n"
55
- s = s + " path-option = [\"" + escapeTomlString(stdxDir) + "\"]\n"
56
- return s
57
- }
58
-
59
- /** 渲染模板文本 */
60
- func renderTemplate(content: String, v: TemplateVars): String {
61
- var out = content
62
- out = out.replace("{{PROJECT_NAME}}", v.projectName)
63
- out = out.replace("{{PACKAGE_NAME}}", v.packageName)
64
- out = out.replace("{{CJC_VERSION}}", minCjcVersion())
65
- out = out.replace("{{FRAMEWORK_ROOT_TOML}}", escapeTomlString(v.frameworkRoot))
66
- out = out.replace("{{NATIVE_DIR_TOML}}", escapeTomlString(v.nativeDir))
67
- out = out.replace("{{STDX_DIR_TOML}}", escapeTomlString(v.stdxDir))
68
- out = out.replace("{{TARGET_SECTION}}", v.targetSection)
69
- out = out.replace("{{FRAMEWORK_ROOT}}", v.frameworkRoot)
70
- out = out.replace("{{NATIVE_DIR}}", v.nativeDir)
71
- out = out.replace("{{STDX_DIR}}", v.stdxDir)
72
- out = out.replace("{{BUNDLE_NAME}}", v.bundleName)
73
- out = out.replace("{{APP_LABEL}}", v.appLabel)
74
- return out
75
- }
76
-
77
- /**
78
- * 模板里哪些文件必须**按字节原样拷**
79
- *
80
- * 模板要带图标(`resources/base/media/` 下的 png),而 `readText` 走 UTF-8——二进制过一遍
81
- * 文本渲染会当场损坏(就算侥幸没崩,png 也变得打不开,要到装到设备上才看得见白方块)。
82
- * 判据只看扩展名:模板里不需要「文本渲染的二进制」,真需要再另说。
83
- */
84
- func isBinaryTemplateFile(name: String): Bool {
85
- let b = name.toArray()
86
- let out = ArrayList<UInt8>()
87
- for (i in 0..b.size) {
88
- out.add(toLowerByte(b[i]))
89
- }
90
- let lower = String.fromUtf8(out.toArray())
91
- let exts = ["png", "jpg", "jpeg", "gif", "webp", "ico", "ttf", "otf", "woff", "woff2",
92
- "so", "dll", "a", "lib", "hap", "zip", "gz", "pdf", "wav", "mp3", "mp4"]
93
- for (ext in exts) {
94
- if (lower.endsWith("." + ext)) {
95
- return true
96
- }
97
- }
98
- return false
99
- }
100
-
101
- /** 项目名 → 仓颉包名(lowerCamelCase):my-app → myApp */
102
- func packageNameFrom(name: String): Option<String> {
103
- let b = name.toArray()
104
- if (b.size == 0) {
105
- return None
106
- }
107
- let out = ArrayList<UInt8>()
108
- var upperNext = false
109
- var first = true
110
- for (i in 0..b.size) {
111
- let c = b[i]
112
- if (c == 0x2D || c == 0x5F || c == 0x2E) { // - _ .
113
- upperNext = true
114
- continue
115
- }
116
- if (!isAsciiAlpha(c) && !isAsciiDigit(c)) {
117
- return None
118
- }
119
- var ch = c
120
- if (upperNext) {
121
- ch = toUpperByte(c)
122
- upperNext = false
123
- }
124
- if (first) {
125
- if (!isAsciiAlpha(ch)) {
126
- return None
127
- }
128
- ch = toLowerByte(ch)
129
- first = false
130
- }
131
- out.add(ch)
132
- }
133
- if (out.size == 0 || first) {
134
- return None
135
- }
136
- return Some(String.fromUtf8(out.toArray()))
137
- }
138
-
139
- /**
140
- * 按字节覆盖写
141
- *
142
- * 模板里的图片不能走 `writeText`:`readText` 是 UTF-8 解码,读 png 当场抛
143
- * `IllegalArgumentException: Invalid unicode scalar value`(create 直接崩、目录留半成品)。
144
- */
145
- func writeBytes(p: String, data: Array<UInt8>): Unit {
146
- if (exists(p)) {
147
- remove(p)
148
- }
149
- let f = File.create(p)
150
- f.write(data)
151
- f.close()
152
- }
153
-
154
- /** 递归拷贝模板目录:目录建目录、文本渲染后写出、二进制原样拷 */
155
- func copyTree(src: String, dst: String, v: TemplateVars): Int64 {
156
- var count = 0
157
- for (info in listDir(src)) {
158
- let nm = baseName(info.path.toString())
159
- if (nm.size == 0) {
160
- continue
161
- }
162
- let s = joinPath(src, nm)
163
- let d = joinPath(dst, nm)
164
- if (info.isDirectory()) {
165
- ensureDir(d)
166
- count = count + copyTree(s, d, v)
167
- } else if (isBinaryTemplateFile(nm)) {
168
- writeBytes(d, File.readFrom(s))
169
- count = count + 1
170
- } else {
171
- writeText(d, renderTemplate(readText(s), v))
172
- count = count + 1
173
- }
174
- }
175
- return count
176
- }
177
-
178
- /** 模板别名 → 仓库里的模板目录名(缺省 app,0.3.x 的老用法不受影响) */
179
- func templateDirName(tpl: String): Option<String> {
180
- if (tpl == "app" || tpl == "vanilla") {
181
- return Some("app") // 内联 HTML 单文件,零 Node 依赖
182
- }
183
- if (tpl == "vue") {
184
- return Some("app-vue") // Vue 3 + Vite:dev 走 HMR,发布产单文件
185
- }
186
- if (tpl == "react") {
187
- return Some("app-react") // React 18 + Vite
188
- }
189
- if (tpl == "ohos" || tpl == "ohos-pc" || tpl == "harmonyos" || tpl == "harmonyos-pc") {
190
- return Some("ohos-pc") // 鸿蒙 PC(2in1):ArkWeb 宿主 + build-hap.sh
191
- }
192
- return None
193
- }
194
-
195
- /** 该项目是不是鸿蒙脚手架(模板目录名即判据,别名在 templateDirName 里已归一到 ohos-pc) */
196
- func isOhosScaffoldTemplate(tplDir: String): Bool {
197
- return baseName(tplDir) == "ohos-pc"
198
- }
199
-
200
- /**
201
- * 项目名 → 缺省 bundleName(`com.example.<小写下划线名>`)
202
- *
203
- * 鸿蒙要求 bundleName 每一段以字母开头、只含字母数字下划线,且至少三段;
204
- * 项目名里的 `-` `.` 之类一律折成 `_`,数字开头补 `app` 前缀。用户可以用 `--bundle` 覆盖。
205
- */
206
- func bundleNameFrom(projectName: String): String {
207
- let b = projectName.toArray()
208
- let out = ArrayList<UInt8>()
209
- for (i in 0..b.size) {
210
- let c = toLowerByte(b[i])
211
- if (isAsciiAlpha(c) || isAsciiDigit(c)) {
212
- out.add(c)
213
- } else {
214
- out.add(0x5F) // '_'
215
- }
216
- }
217
- var s = String.fromUtf8(out.toArray())
218
- if (s.size > 0 && isAsciiDigit(s.toArray()[0])) {
219
- s = "app" + s
220
- }
221
- if (s.size == 0) {
222
- s = "app"
223
- }
224
- return "com.example." + s
225
- }
226
-
227
- /** 解析模板目录:先查别名表,查不到就把参数当模板目录名直接用(方便自己往仓库里加模板) */
228
- func resolveTemplateDir(fwRoot: String, tpl: String): Option<String> {
229
- let base = joinPath(joinPath(fwRoot, "cli"), "templates")
230
- var name = tpl
231
- if (let Some(n) <- templateDirName(tpl)) {
232
- name = n
233
- }
234
- let dir = joinPath(base, name)
235
- if (exists(dir)) {
236
- return Some(dir)
237
- }
238
- return None
239
- }
240
-
241
- /** 列出仓库里实际存在的模板目录(拼错误提示用) */
242
- func availableTemplates(fwRoot: String): String {
243
- let base = joinPath(joinPath(fwRoot, "cli"), "templates")
244
- var out = ""
245
- if (exists(base)) {
246
- for (info in listDir(base)) {
247
- if (!info.isDirectory()) {
248
- continue
249
- }
250
- let nm = baseName(info.path.toString())
251
- if (nm.size == 0) {
252
- continue
253
- }
254
- if (out.size > 0) {
255
- out = out + " / "
256
- }
257
- out = out + nm
258
- }
259
- }
260
- return out
261
- }
262
-
263
- func cmdCreate(nameArg: String, tpl: String, bundleArg: String, labelArg: String): Int64 {
264
- let targetDir = stripTrailingSep(nameArg)
265
- if (targetDir.size == 0) {
266
- eprintln("用法: cj-tauri create <项目名> [--template app|vue|react|ohos]")
267
- return 1
268
- }
269
- let projectName = baseName(targetDir)
270
- var pkg = ""
271
- if (let Some(p) <- packageNameFrom(projectName)) {
272
- pkg = p
273
- } else {
274
- eprintln("[cj-tauri] 错误: 项目名 '${projectName}' 推导不出合法仓颉包名")
275
- eprintln(" 要求以字母开头,只含字母/数字/-/_/.(例如 my-app)")
276
- return 1
277
- }
278
- if (exists(targetDir)) {
279
- eprintln("[cj-tauri] 错误: 目录已存在: ${targetDir}")
280
- return 1
281
- }
282
- var fwRoot = ""
283
- if (let Some(r) <- resolveFrameworkRoot()) {
284
- fwRoot = r
285
- } else {
286
- eprintln("[cj-tauri] 错误: 找不到 cj-tauri 框架根目录")
287
- eprintln(" 请设置环境变量 CJ_TAURI_ROOT 指向 cj-tauri 仓库根目录")
288
- return 1
289
- }
290
- // 模板先解析:鸿蒙模板**不吃桌面 stdx**(编 hap 时由 build-hap.sh 用 CANGJIE_STDX_OHOS),
291
- // 所以它不能因为本机没配 CANGJIE_STDX 就被拦下。
292
- var tplDir = ""
293
- if (let Some(d) <- resolveTemplateDir(fwRoot, tpl)) {
294
- tplDir = d
295
- } else {
296
- eprintln("[cj-tauri] 错误: 未知模板 '${tpl}'")
297
- eprintln(" 可用模板: ${availableTemplates(fwRoot)}")
298
- eprintln(" 别名: app(内联单文件,缺省)/ vue / react / ohos")
299
- return 1
300
- }
301
- let ohos = isOhosScaffoldTemplate(tplDir)
302
- var stdx = ""
303
- if (let Some(s) <- resolveStdx(fwRoot, None)) {
304
- stdx = s
305
- } else if (!ohos) {
306
- eprintln("[cj-tauri] 错误: 找不到 stdx 目录")
307
- eprintln(" 请设置环境变量 CANGJIE_STDX 指向 stdx 动态库目录(含 stdx.encoding.json)")
308
- return 1
309
- }
310
- warnIfSpacey("框架", fwRoot)
311
-
312
- var bundle = bundleArg
313
- if (bundle.size == 0) {
314
- bundle = bundleNameFrom(projectName)
315
- }
316
- var label = labelArg
317
- if (label.size == 0) {
318
- label = projectName
319
- }
320
-
321
- let v = TemplateVars(projectName, pkg, fwRoot, stdx, bundle, label)
322
- ensureDir(targetDir)
323
- let count = copyTree(tplDir, targetDir, v)
324
-
325
- println("[cj-tauri] 已创建项目 ${targetDir}/(${count} 个文件)")
326
- println("")
327
- println(" 项目名 : ${projectName}")
328
- println(" 模板 : ${baseName(tplDir)}")
329
- if (ohos) {
330
- println(" 应用名 : ${label}")
331
- println(" bundle : ${bundle}")
332
- println(" 框架依赖 : ${fwRoot}")
333
- println("")
334
- println("下一步:")
335
- println(" cd ${targetDir}")
336
- println(" export CANGJIE_STDX_OHOS=<鸿蒙版 stdx>/linux_ohos_aarch64_cjnative/dynamic/stdx")
337
- println(" bash build-hap.sh # 编 hap")
338
- println(" bash build-hap.sh --install # 编完装到设备并拉起")
339
- println("")
340
- println("提示: 模板不带签名证书——首次在 DevEco 里 File → Project Structure → Signing Configs")
341
- println(" 勾「Automatically generate signature」(要登录华为账号);不签名只能编出")
342
- println(" *-unsigned.hap,装不进设备。")
343
- return 0
344
- }
345
- println(" 仓颉包名 : ${pkg}")
346
- println(" 框架依赖 : ${fwRoot}")
347
- println(" stdx : ${stdx}")
348
- println("")
349
- println("下一步:")
350
- println(" cd ${targetDir}")
351
- println(" cj-tauri dev # 构建 C 桥 + 编译 + 启动窗口")
352
- println(" cj-tauri build # 仅构建")
353
- println("")
354
- println("提示: cjpm.toml 里的路径是本机绝对路径;换机器需重新执行 create 或手改这两处。")
355
- if (exists(joinPath(joinPath(targetDir, "ui"), "package.json"))) {
356
- println("")
357
- println("提示: 该模板前端用 Vite;首次 cj-tauri dev 会自动 npm install 并起 dev server")
358
- println(" (页面走 CJ_TAURI_DEV_URL,改 ui/src 下的前端源码不重启应用就能看到效果)。")
359
- }
360
- return 0
361
- }
1
+ /**
2
+ * cj-tauri CLI:项目脚手架(create)
3
+ *
4
+ * 模板位于 <框架根>/cli/templates/<模板目录>,占位符形如 {{PROJECT_NAME}};
5
+ * 递归拷贝 + 文本渲染,生成「新形态」工程(cj-tauri.toml 清单 + 业务 static 包 + assets/),
6
+ * 骨架由 cj-tauri sync 派生(RFC-002 §7):桌面 cj-tauri dev,鸿蒙 targets/ohos 的 build-hap.sh。
7
+ * ohos-skeleton 目录是 cj-tauri sync ohos 的 DevEco 骨架源,不是 create 模板。
8
+ */
9
+ package cli
10
+
11
+ import std.fs.*
12
+ import std.collection.*
13
+
14
+ /** 模板占位符取值 */
15
+ public class TemplateVars {
16
+ let projectName: String
17
+ let packageName: String
18
+ let frameworkRoot: String
19
+ let nativeDir: String
20
+ let stdxDir: String
21
+ let targetSection: String
22
+ /** 鸿蒙应用的安装 ID(bundleName),桌面模板不用 */
23
+ let bundleName: String
24
+ /** 应用显示名(桌面模板用项目名,鸿蒙写进 string.json) */
25
+ let appLabel: String
26
+
27
+ public init(projectName: String, packageName: String, frameworkRoot: String, stdxDir: String,
28
+ bundleName: String, appLabel: String) {
29
+ this.projectName = projectName
30
+ this.packageName = packageName
31
+ this.frameworkRoot = frameworkRoot
32
+ this.nativeDir = joinPath(frameworkRoot, "native")
33
+ this.stdxDir = stdxDir
34
+ this.targetSection = buildTargetSection(joinPath(frameworkRoot, "native"), stdxDir)
35
+ this.bundleName = bundleName
36
+ this.appLabel = appLabel
37
+ }
38
+ }
39
+
40
+ /** 生成 cjpm.toml 的 [target.<triple>] 段落(含桥链接参数与 stdx 路径) */
41
+ func buildTargetSection(nativeDir: String, stdxDir: String): String {
42
+ let triple = hostTriple()
43
+ var linkOpt = "-L" + escapeTomlString(nativeDir) + " -lcjtbridge"
44
+ if (hostIsWindows()) {
45
+ // GUI 子系统:不写这个开关,产物是 console 程序,双击时会多出一个控制台黑窗口(AGENTS §4)。
46
+ // 取值只能用链接器那套——cjc 把 --link-options 直通 ld.lld,gcc 的 -mwindows 会被拒。
47
+ // 注意别在这儿写「行内 `#` 注释」:Cangjie 里 `#` 是 raw string 前缀、不是注释符,
48
+ // 生成出来的 toml 那行也就只放开关、不放尾注(保证与文档里的行数表格一致)。
49
+ linkOpt = linkOpt + " --subsystem=windows"
50
+ } else {
51
+ linkOpt = linkOpt + " -lwebkit2gtk-4.1 -lgtk-3 -lgdk-3 -lglib-2.0 -lgio-2.0 -lsoup-3.0" +
52
+ " -ljavascriptcoregtk-4.1 -lgobject-2.0"
53
+ }
54
+ var s = "[target." + triple + "]\n"
55
+ s = s + " link-option = \"" + linkOpt + "\"\n"
56
+ s = s + "\n[target." + triple + ".bin-dependencies]\n"
57
+ s = s + " path-option = [\"" + escapeTomlString(stdxDir) + "\"]\n"
58
+ return s
59
+ }
60
+
61
+ /** 渲染模板文本 */
62
+ func renderTemplate(content: String, v: TemplateVars): String {
63
+ var out = content
64
+ out = out.replace("{{PROJECT_NAME}}", v.projectName)
65
+ out = out.replace("{{PACKAGE_NAME}}", v.packageName)
66
+ out = out.replace("{{CJC_VERSION}}", minCjcVersion())
67
+ out = out.replace("{{FRAMEWORK_ROOT_TOML}}", escapeTomlString(v.frameworkRoot))
68
+ out = out.replace("{{NATIVE_DIR_TOML}}", escapeTomlString(v.nativeDir))
69
+ out = out.replace("{{STDX_DIR_TOML}}", escapeTomlString(v.stdxDir))
70
+ out = out.replace("{{TARGET_SECTION}}", v.targetSection)
71
+ out = out.replace("{{FRAMEWORK_ROOT}}", v.frameworkRoot)
72
+ out = out.replace("{{NATIVE_DIR}}", v.nativeDir)
73
+ out = out.replace("{{STDX_DIR}}", v.stdxDir)
74
+ out = out.replace("{{BUNDLE_NAME}}", v.bundleName)
75
+ out = out.replace("{{APP_LABEL}}", v.appLabel)
76
+ return out
77
+ }
78
+
79
+ /**
80
+ * 模板里哪些文件必须**按字节原样拷**
81
+ *
82
+ * 模板要带图标(`resources/base/media/` 下的 png),而 `readText` 走 UTF-8——二进制过一遍
83
+ * 文本渲染会当场损坏(就算侥幸没崩,png 也变得打不开,要到装到设备上才看得见白方块)。
84
+ * 判据只看扩展名:模板里不需要「文本渲染的二进制」,真需要再另说。
85
+ */
86
+ func isBinaryTemplateFile(name: String): Bool {
87
+ let b = name.toArray()
88
+ let out = ArrayList<UInt8>()
89
+ for (i in 0..b.size) {
90
+ out.add(toLowerByte(b[i]))
91
+ }
92
+ let lower = String.fromUtf8(out.toArray())
93
+ let exts = ["png", "jpg", "jpeg", "gif", "webp", "ico", "ttf", "otf", "woff", "woff2",
94
+ "so", "dll", "a", "lib", "hap", "zip", "gz", "pdf", "wav", "mp3", "mp4"]
95
+ for (ext in exts) {
96
+ if (lower.endsWith("." + ext)) {
97
+ return true
98
+ }
99
+ }
100
+ return false
101
+ }
102
+
103
+ /** 项目名 → 仓颉包名(lowerCamelCase):my-app → myApp */
104
+ func packageNameFrom(name: String): Option<String> {
105
+ let b = name.toArray()
106
+ if (b.size == 0) {
107
+ return None
108
+ }
109
+ let out = ArrayList<UInt8>()
110
+ var upperNext = false
111
+ var first = true
112
+ for (i in 0..b.size) {
113
+ let c = b[i]
114
+ if (c == 0x2D || c == 0x5F || c == 0x2E) { // - _ .
115
+ upperNext = true
116
+ continue
117
+ }
118
+ if (!isAsciiAlpha(c) && !isAsciiDigit(c)) {
119
+ return None
120
+ }
121
+ var ch = c
122
+ if (upperNext) {
123
+ ch = toUpperByte(c)
124
+ upperNext = false
125
+ }
126
+ if (first) {
127
+ if (!isAsciiAlpha(ch)) {
128
+ return None
129
+ }
130
+ ch = toLowerByte(ch)
131
+ first = false
132
+ }
133
+ out.add(ch)
134
+ }
135
+ if (out.size == 0 || first) {
136
+ return None
137
+ }
138
+ return Some(String.fromUtf8(out.toArray()))
139
+ }
140
+
141
+ /**
142
+ * 按字节覆盖写
143
+ *
144
+ * 模板里的图片不能走 `writeText`:`readText` 是 UTF-8 解码,读 png 当场抛
145
+ * `IllegalArgumentException: Invalid unicode scalar value`(create 直接崩、目录留半成品)。
146
+ */
147
+ func writeBytes(p: String, data: Array<UInt8>): Unit {
148
+ if (exists(p)) {
149
+ remove(p)
150
+ }
151
+ let f = File.create(p)
152
+ f.write(data)
153
+ f.close()
154
+ }
155
+
156
+ /** 递归拷贝模板目录:目录建目录、文本渲染后写出、二进制原样拷 */
157
+ func copyTree(src: String, dst: String, v: TemplateVars): Int64 {
158
+ var count = 0
159
+ for (info in listDir(src)) {
160
+ let nm = baseName(info.path.toString())
161
+ if (nm.size == 0) {
162
+ continue
163
+ }
164
+ let s = joinPath(src, nm)
165
+ let d = joinPath(dst, nm)
166
+ if (info.isDirectory()) {
167
+ ensureDir(d)
168
+ count = count + copyTree(s, d, v)
169
+ } else if (isBinaryTemplateFile(nm)) {
170
+ writeBytes(d, File.readFrom(s))
171
+ count = count + 1
172
+ } else {
173
+ writeText(d, renderTemplate(readText(s), v))
174
+ count = count + 1
175
+ }
176
+ }
177
+ return count
178
+ }
179
+
180
+ /** 模板别名 → 仓库里的模板目录名(缺省 app,0.3.x 的老用法不受影响) */
181
+ func templateDirName(tpl: String): Option<String> {
182
+ if (tpl == "app" || tpl == "vanilla") {
183
+ return Some("app") // 内联 HTML 单文件,零 Node 依赖
184
+ }
185
+ if (tpl == "vue") {
186
+ return Some("app-vue") // Vue 3 + Vite:dev 走 HMR,发布产单文件
187
+ }
188
+ if (tpl == "react") {
189
+ return Some("app-react") // React 18 + Vite
190
+ }
191
+ // 鸿蒙生态(PC / 手机 / 平板):新形态是统一工程,模板内容同 app——
192
+ // 清单决定目标端,骨架由 cj-tauri sync ohos 派生;旧 ohos-pc 命名保留兼容
193
+ if (tpl == "ohos" || tpl == "ohos-pc" || tpl == "harmonyos" || tpl == "harmonyos-pc") {
194
+ return Some("app")
195
+ }
196
+ // 内部用:cj-tauri sync ohos 的骨架源(DevEco 工程,不是 create 模板)
197
+ if (tpl == "ohos-skeleton") {
198
+ return Some("ohos-skeleton")
199
+ }
200
+ return None
201
+ }
202
+
203
+ /** create --template 参数是否鸿蒙生态(下一步提示用;新形态下不能按目录名判) */
204
+ func isOhosTemplate(tpl: String): Bool {
205
+ return tpl == "ohos" || tpl == "ohos-pc" || tpl == "harmonyos" || tpl == "harmonyos-pc"
206
+ }
207
+
208
+ /**
209
+ * 项目名 → 缺省 bundleName(`com.example.<小写下划线名>`)
210
+ *
211
+ * 鸿蒙要求 bundleName 每一段以字母开头、只含字母数字下划线,且至少三段;
212
+ * 项目名里的 `-` `.` 之类一律折成 `_`,数字开头补 `app` 前缀。用户可以用 `--bundle` 覆盖。
213
+ */
214
+ func bundleNameFrom(projectName: String): String {
215
+ let b = projectName.toArray()
216
+ let out = ArrayList<UInt8>()
217
+ for (i in 0..b.size) {
218
+ let c = toLowerByte(b[i])
219
+ if (isAsciiAlpha(c) || isAsciiDigit(c)) {
220
+ out.add(c)
221
+ } else {
222
+ out.add(0x5F) // '_'
223
+ }
224
+ }
225
+ var s = String.fromUtf8(out.toArray())
226
+ if (s.size > 0 && isAsciiDigit(s.toArray()[0])) {
227
+ s = "app" + s
228
+ }
229
+ if (s.size == 0) {
230
+ s = "app"
231
+ }
232
+ return "com.example." + s
233
+ }
234
+
235
+ /** 解析模板目录:先查别名表,查不到就把参数当模板目录名直接用(方便自己往仓库里加模板) */
236
+ func resolveTemplateDir(fwRoot: String, tpl: String): Option<String> {
237
+ let base = joinPath(joinPath(fwRoot, "cli"), "templates")
238
+ var name = tpl
239
+ if (let Some(n) <- templateDirName(tpl)) {
240
+ name = n
241
+ }
242
+ let dir = joinPath(base, name)
243
+ if (exists(dir)) {
244
+ return Some(dir)
245
+ }
246
+ return None
247
+ }
248
+
249
+ /** 列出仓库里实际存在的模板目录(拼错误提示用) */
250
+ func availableTemplates(fwRoot: String): String {
251
+ let base = joinPath(joinPath(fwRoot, "cli"), "templates")
252
+ var out = ""
253
+ if (exists(base)) {
254
+ for (info in listDir(base)) {
255
+ if (!info.isDirectory()) {
256
+ continue
257
+ }
258
+ let nm = baseName(info.path.toString())
259
+ if (nm.size == 0) {
260
+ continue
261
+ }
262
+ if (out.size > 0) {
263
+ out = out + " / "
264
+ }
265
+ out = out + nm
266
+ }
267
+ }
268
+ return out
269
+ }
270
+
271
+ func cmdCreate(nameArg: String, tpl: String, bundleArg: String, labelArg: String): Int64 {
272
+ let targetDir = stripTrailingSep(nameArg)
273
+ if (targetDir.size == 0) {
274
+ eprintln("用法: cj-tauri create <项目名> [--template app|vue|react|ohos]")
275
+ return 1
276
+ }
277
+ let projectName = baseName(targetDir)
278
+ var pkg = ""
279
+ if (let Some(p) <- packageNameFrom(projectName)) {
280
+ pkg = p
281
+ } else {
282
+ eprintln("[cj-tauri] 错误: 项目名 '${projectName}' 推导不出合法仓颉包名")
283
+ eprintln(" 要求以字母开头,只含字母/数字/-/_/.(例如 my-app)")
284
+ return 1
285
+ }
286
+ if (exists(targetDir)) {
287
+ eprintln("[cj-tauri] 错误: 目录已存在: ${targetDir}")
288
+ return 1
289
+ }
290
+ var fwRoot = ""
291
+ if (let Some(r) <- resolveFrameworkRoot()) {
292
+ fwRoot = r
293
+ } else {
294
+ eprintln("[cj-tauri] 错误: 找不到 cj-tauri 框架根目录")
295
+ eprintln(" 请设置环境变量 CJ_TAURI_ROOT 指向 cj-tauri 仓库根目录")
296
+ return 1
297
+ }
298
+ // 模板先解析。新形态业务 toml 的 stdx 走 ${CANGJIE_STDX} 环境变量
299
+ // (cjpm 构建期替换),create 不再需要解析 stdx
300
+ var tplDir = ""
301
+ var dirName = ""
302
+ if (let Some(d) <- resolveTemplateDir(fwRoot, tpl)) {
303
+ tplDir = d
304
+ dirName = baseName(d)
305
+ } else {
306
+ eprintln("[cj-tauri] 错误: 未知模板 '${tpl}'")
307
+ eprintln(" 可用模板: ${availableTemplates(fwRoot)}")
308
+ eprintln(" 别名: app(内联单文件,缺省)/ vue / react / ohos")
309
+ return 1
310
+ }
311
+ if (dirName == "ohos-skeleton") {
312
+ eprintln("[cj-tauri] 错误: ohos-skeleton 是 cj-tauri sync ohos 的骨架源,不能用于 create")
313
+ eprintln(" 创建鸿蒙工程请用: cj-tauri create ${nameArg} --template ohos")
314
+ return 1
315
+ }
316
+ let ohos = isOhosTemplate(tpl)
317
+ warnIfSpacey("框架", fwRoot)
318
+
319
+ var bundle = bundleArg
320
+ if (bundle.size == 0) {
321
+ bundle = bundleNameFrom(projectName)
322
+ }
323
+ var label = labelArg
324
+ if (label.size == 0) {
325
+ label = projectName
326
+ }
327
+
328
+ let v = TemplateVars(projectName, pkg, fwRoot, "", bundle, label)
329
+ ensureDir(targetDir)
330
+ let count = copyTree(tplDir, targetDir, v)
331
+
332
+ println("[cj-tauri] 已创建项目 ${targetDir}/(${count} 个文件)")
333
+ println("")
334
+ println(" 项目名 : ${projectName}")
335
+ println(" 模板 : ${tpl}")
336
+ println(" 仓颉包名 : ${pkg}")
337
+ println(" 框架依赖 : ${fwRoot}")
338
+ if (ohos) {
339
+ println(" 应用名 : ${label}")
340
+ println(" bundle : ${bundle}")
341
+ }
342
+ println("")
343
+ println("下一步:")
344
+ println(" cd ${targetDir}")
345
+ println(" cj-tauri sync # 按清单派生 targets/ 骨架(desktop + ohos)")
346
+ if (dirName == "app-vue" || dirName == "app-react") {
347
+ println(" cd assets/ui && npm install && cd ../.. # 前端依赖(需 Node.js)")
348
+ println(" cj-tauri dev # 起 Vite dev server(热更新)+ 编译 + 启动窗口")
349
+ } else {
350
+ println(" cj-tauri dev # 构建 C 桥 + 编译 + 启动窗口")
351
+ }
352
+ if (ohos) {
353
+ println(" 鸿蒙端: export CANGJIE_STDX_OHOS=<鸿蒙版 stdx>/linux_ohos_aarch64_cjnative/dynamic/stdx")
354
+ println(" cd targets/ohos && bash build-hap.sh [--install]")
355
+ println("")
356
+ println(" 提示: 模板不带签名证书——首次在 DevEco 里 File → Project Structure → Signing Configs")
357
+ println(" 勾「Automatically generate signature」(要登录华为账号);不签名只能编出")
358
+ println(" *-unsigned.hap,装不进设备。")
359
+ } else {
360
+ println("")
361
+ println(" 提示: cjpm.toml 里的框架路径是本机绝对路径;换机器需重新执行 create 或手改这一处。")
362
+ }
363
+ if (exists(joinPath(joinPath(joinPath(targetDir, "assets"), "ui"), "package.json"))) {
364
+ println("")
365
+ println(" 提示: 该模板前端用 Vite;首次 cj-tauri dev 会自动 npm install 并起 dev server")
366
+ println(" (页面走 CJ_TAURI_DEV_URL,改 assets/ui/src 下的前端源码不重启应用就能看到效果)。")
367
+ }
368
+ return 0
369
+ }