cj-tauri 2.1.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.
- package/README.md +168 -115
- package/cjpm.toml +1 -1
- package/cli/cjpm.toml +1 -1
- package/cli/src/manifest.cj +13 -2
- package/cli/src/project.cj +1 -1
- package/cli/src/sync.cj +116 -4
- package/native/platforms/windows/bridge_win.c +7 -2
- package/package.json +4 -4
- package/src/app.cj +10 -0
- package/src/ipc_hub.cj +66 -2
- package/src/tests/app_spec_test.cj +5 -1
- package/src/tests/ipc_hub_test.cj +76 -0
- package/src/tests/platform_contract_test.cj +15 -2
- package/src/version.cj +1 -1
- package/prebuilt/linux-x64/cj-tauri +0 -0
package/README.md
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
# cj-tauri
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
A scaffold CLI for building desktop apps in the [Cangjie language](https://cangjie-lang.cn) — it brings
|
|
4
|
+
Tauri's trio to Cangjie: a Cangjie backend (statically compiled) plus a system WebView frontend
|
|
5
|
+
(HTML/CSS/JS), with the same three pieces Tauri has — a WebView host, a bidirectional IPC bridge
|
|
6
|
+
(`invoke` / `resolve` / `event`), and a capability-based security model (capability allowlists).
|
|
7
|
+
|
|
8
|
+
- Repository and full documentation: <https://github.com/yangyongzhen/cj-tauri>
|
|
9
|
+
- Current version: **2.1.1** (fixed in this release: sync now keeps the launcher on the framework path
|
|
10
|
+
the business package declares (no more cjpm module conflicts after editing the path), the desktop
|
|
11
|
+
launcher locates assets/ two levels up so it runs straight from targets/, and new **serial command
|
|
12
|
+
groups** — `TauriApp.registerSerial` — execute same-group commands strictly in arrival order, which
|
|
13
|
+
ai-chat's web-tab commands now use; plus the hello-world example and starter guide)
|
|
14
|
+
- Current status: **Windows (WebView2), Linux (WebKitGTK) and HarmonyOS PC (ArkWeb) all run on real
|
|
15
|
+
hardware**; on the HarmonyOS side the menu capability bit and `fs` / `mqtt` / `redis` / `sqlite` /
|
|
16
|
+
`store` / `http` have been verified on a device
|
|
17
|
+
|
|
18
|
+
## Prerequisite: the Cangjie SDK
|
|
19
|
+
|
|
20
|
+
**This package does not replace the Cangjie SDK.** Your app backend is Cangjie code, and
|
|
21
|
+
`cj-tauri dev` / `build` invoke the SDK's `cjc` / `cjpm` to compile it. So install the SDK first
|
|
22
|
+
(verified with version **1.2.0**, including **stdx**) and make it discoverable:
|
|
17
23
|
|
|
18
24
|
```bash
|
|
19
25
|
# Linux
|
|
@@ -23,108 +29,117 @@ export CANGJIE_HOME=/opt/cangjie/cangjie
|
|
|
23
29
|
set CANGJIE_HOME=D:\Program Files (x86)\Cangjie
|
|
24
30
|
```
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
Linux `/opt/cangjie/cangjie
|
|
32
|
+
When `CANGJIE_HOME` is unset, the package probes the usual install locations (Windows
|
|
33
|
+
`D:\Program Files (x86)\Cangjie`, Linux `/opt/cangjie/cangjie`). Three more variables are needed
|
|
34
|
+
in some situations:
|
|
28
35
|
|
|
29
|
-
|
|
|
36
|
+
| Variable | When you need it |
|
|
30
37
|
|---|---|
|
|
31
|
-
| `CJ_TAURI_ROOT` |
|
|
32
|
-
| `CANGJIE_STDX` |
|
|
33
|
-
| `CANGJIE_STDX_OHOS` |
|
|
38
|
+
| `CJ_TAURI_ROOT` | Running outside the cj-tauri repo when the framework root can't be located (`create` writes the framework path into your project) |
|
|
39
|
+
| `CANGJIE_STDX` | Your app's `cjpm.toml` expands the stdx path via `${CANGJIE_STDX}`; the package probes the framework/project toml for it and falls back to this variable |
|
|
40
|
+
| `CANGJIE_STDX_OHOS` | Only when building a HarmonyOS project (the HarmonyOS build of stdx; `build-hap.sh` locates its dynamic libraries through it) |
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
Self-check once it's installed:
|
|
36
43
|
|
|
37
44
|
```bash
|
|
38
|
-
npx cj-tauri info #
|
|
45
|
+
npx cj-tauri info # prints the resolved framework / project / stdx / SDK / cjpm / C bridge
|
|
39
46
|
```
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
Without the SDK the package prints a clear hint and install instructions instead of throwing
|
|
49
|
+
`cjpm: command not found`. Note that `npx cj-tauri --version` works without the SDK.
|
|
43
50
|
|
|
44
|
-
##
|
|
51
|
+
## Usage
|
|
45
52
|
|
|
46
53
|
```bash
|
|
47
|
-
npx cj-tauri create myapp --template app #
|
|
54
|
+
npx cj-tauri create myapp --template app # inline HTML, zero Node (the default template)
|
|
48
55
|
cd myapp
|
|
49
|
-
npx cj-tauri sync #
|
|
50
|
-
npx cj-tauri dev #
|
|
56
|
+
npx cj-tauri sync # derive the targets/ skeletons from the manifest (first run; idempotent)
|
|
57
|
+
npx cj-tauri dev # build the C bridge + cjpm build + open the window
|
|
51
58
|
|
|
52
|
-
npx cj-tauri create myapp-vue --template vue # Vue 3 + Vite
|
|
59
|
+
npx cj-tauri create myapp-vue --template vue # Vue 3 + Vite (frontend project under assets/ui/)
|
|
53
60
|
npx cj-tauri create myapp-react --template react # React 18 + Vite
|
|
54
61
|
|
|
55
|
-
npx cj-tauri create myohos --template ohos #
|
|
56
|
-
npx cj-tauri create myohos --template ohos --bundle com.example.myapp --label "
|
|
62
|
+
npx cj-tauri create myohos --template ohos # HarmonyOS PC: same business code + targets/ohos
|
|
63
|
+
npx cj-tauri create myohos --template ohos --bundle com.example.myapp --label "My App"
|
|
57
64
|
|
|
58
|
-
npm i -g cj-tauri #
|
|
65
|
+
npm i -g cj-tauri # or install globally and call cj-tauri <subcommand> directly
|
|
59
66
|
```
|
|
60
67
|
|
|
61
|
-
|
|
68
|
+
**One body of business code, skeletons derived per target** — `create` only generates the manifest and
|
|
69
|
+
the business side; the platform skeletons are what `sync` produces:
|
|
62
70
|
|
|
63
71
|
```
|
|
64
72
|
myapp/
|
|
65
|
-
├── cj-tauri.toml #
|
|
66
|
-
├── cjpm.toml #
|
|
67
|
-
├── src/main.cj #
|
|
73
|
+
├── cj-tauri.toml # app manifest — the single source of truth (id/version/window/assets/capability dir)
|
|
74
|
+
├── cjpm.toml # business package (a static library; skeletons reference it by path)
|
|
75
|
+
├── src/main.cj # business assembly point build(): AppSpec (zero platform knowledge)
|
|
68
76
|
├── assets/
|
|
69
|
-
│ ├── ui/index.html #
|
|
70
|
-
│ └── capabilities/default.json #
|
|
71
|
-
└── targets/ # cj-tauri sync
|
|
72
|
-
├── desktop/ #
|
|
73
|
-
└── ohos/ #
|
|
77
|
+
│ ├── ui/index.html # frontend page (any web stack)
|
|
78
|
+
│ └── capabilities/default.json # capability allowlist (commands / events / permission sets)
|
|
79
|
+
└── targets/ # generated by cj-tauri sync (don't hand-edit; check reports drift)
|
|
80
|
+
├── desktop/ # desktop executable (the launcher calls build())
|
|
81
|
+
└── ohos/ # HarmonyOS hap project (DevEco + build-hap.sh)
|
|
74
82
|
```
|
|
75
83
|
|
|
76
|
-
|
|
|
84
|
+
| Subcommand | What it does |
|
|
77
85
|
|---|---|
|
|
78
|
-
| `create
|
|
79
|
-
| `sync [desktop\|ohos]` |
|
|
80
|
-
| `dev` |
|
|
81
|
-
| `build` |
|
|
82
|
-
| `run` |
|
|
83
|
-
| `check` |
|
|
84
|
-
| `info [--json]` |
|
|
85
|
-
| `doctor` |
|
|
86
|
-
| `help` |
|
|
87
|
-
|
|
88
|
-
##
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
| `create <name> [--template app\|vue\|react\|ohos]` | Generate the unified project (manifest + `src/` + `assets/`); `--bundle` / `--label` only matter for the `ohos` template, otherwise they're derived from the project name |
|
|
87
|
+
| `sync [desktop\|ohos]` | Derive the `targets/` skeletons from the manifest (both ends by default; idempotent) |
|
|
88
|
+
| `dev` | Build the C bridge + `cjpm build` + launch the app; Vite templates take over their dev server (HMR) |
|
|
89
|
+
| `build` | Build the C bridge + `cjpm build`; on HarmonyOS run `bash build-hap.sh` inside `targets/ohos` |
|
|
90
|
+
| `run` | Run the built artifact; on HarmonyOS `bash build-hap.sh --install` (build + install + launch) |
|
|
91
|
+
| `check` | Validate `cj-tauri.toml` (id / window / assets present) + `targets/` skeleton drift + three-way plugin comparison |
|
|
92
|
+
| `info [--json]` | Environment self-check (run this first when troubleshooting); `--json` prints only the plugin manifest, machine-readable |
|
|
93
|
+
| `doctor` | Environment self-check with a verdict per line (each line says what to do if it's missing) |
|
|
94
|
+
| `help` | Help |
|
|
95
|
+
|
|
96
|
+
## HarmonyOS PC (`--template ohos`)
|
|
97
|
+
|
|
98
|
+
The same framework with a different host: desktop uses WebView2 / WebKitGTK, HarmonyOS PC uses **ArkWeb**,
|
|
99
|
+
while the page layer, the command layer and capability validation stay untouched. `--template ohos` still
|
|
100
|
+
generates that **unified project** (`cj-tauri.toml` + `src/` + `assets/`), only the manifest carries the
|
|
101
|
+
HarmonyOS side (`[app] id` is the `bundleName`); the DevEco project is a product of `sync`, landing in
|
|
102
|
+
`targets/ohos/`:
|
|
93
103
|
|
|
94
104
|
```bash
|
|
95
|
-
npx cj-tauri create myohos --template ohos --bundle com.example.myapp --label "
|
|
105
|
+
npx cj-tauri create myohos --template ohos --bundle com.example.myapp --label "My App"
|
|
96
106
|
cd myohos
|
|
97
|
-
npx cj-tauri sync ohos #
|
|
107
|
+
npx cj-tauri sync ohos # derive targets/ohos (DevEco skeleton + the page and capability manifest in rawfile)
|
|
98
108
|
cd targets/ohos
|
|
99
|
-
bash build-hap.sh --install #
|
|
109
|
+
bash build-hap.sh --install # build the hap + install via hdc + aa start
|
|
100
110
|
```
|
|
101
111
|
|
|
102
|
-
`targets/ohos/`
|
|
103
|
-
|
|
112
|
+
`targets/ohos/` is a standard DevEco project (`entry/` + `AppScope/` + `build-profile.json5` +
|
|
113
|
+
`hvigorfile.ts`), and the HarmonyOS-specific layer is thin:
|
|
104
114
|
|
|
105
|
-
|
|
|
115
|
+
| Path | Purpose |
|
|
106
116
|
|---|---|
|
|
107
|
-
| `entry/src/main/cangjie/main_ability.cj` | UIAbility
|
|
108
|
-
| `entry/src/main/cangjie/app_host.cj` | `@Component`
|
|
109
|
-
| `entry/src/main/resources/rawfile/` | `index.html`
|
|
110
|
-
| `build-hap.sh` |
|
|
117
|
+
| `entry/src/main/cangjie/main_ability.cj` | UIAbility: `ArkWebRuntime.configure(filesDir)` → `spawn { run() }` off the UI thread → wait for the host to register, then `loadContent` |
|
|
118
|
+
| `entry/src/main/cangjie/app_host.cj` | the `@Component` ArkWeb host + bidirectional bridge (**must live in the app module**; a framework package referencing ArkUI crashes at startup); calls the business `build()` in your `assets/` |
|
|
119
|
+
| `entry/src/main/resources/rawfile/` | `index.html` and the capability manifest (real files copied in from `assets/`) |
|
|
120
|
+
| `build-hap.sh` | the single build entry: probes DevEco / stdx, runs hvigor with `--no-daemon`, optional `--install` |
|
|
111
121
|
|
|
112
|
-
|
|
122
|
+
Two prerequisites:
|
|
113
123
|
|
|
114
|
-
- **DevEco Studio 6.1
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
- **DevEco Studio 6.1** (with the Cangjie plugin) and signing already configured. `build-hap.sh` finds the
|
|
125
|
+
install root via `DEVECO_WIN` (default `D:\oh\DevEcoStudio`) and the HarmonyOS build of stdx's dynamic
|
|
126
|
+
libraries via `CANGJIE_STDX_OHOS`; when either is missing the script says exactly which one.
|
|
127
|
+
- A device with **developer mode + USB debugging** on, so `hdc` lists it; `aa start` needs the **screen unlocked**.
|
|
117
128
|
|
|
118
|
-
|
|
119
|
-
`cangjieOptions.checkDeviceTypes = false
|
|
120
|
-
|
|
129
|
+
The project defaults to desktop form factor: `deviceTypes` in `module.json5` is `2in1`, and
|
|
130
|
+
`cangjieOptions.checkDeviceTypes = false` in `entry/build-profile.json5` (the plugin only allows
|
|
131
|
+
`phone` / `tablet` by default, and writing `2in1` fails with `01103012`). The window **does not touch the
|
|
132
|
+
size** and keeps the system default rectangle (measured at `2090×1394`, about two thirds of the screen
|
|
133
|
+
width and centered).
|
|
121
134
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
135
|
+
Known limitations: signing is configured in DevEco (the command line does not auto-sign, and changing the
|
|
136
|
+
`bundleName` requires re-signing); two projects under the same debug signature evict each other's install;
|
|
137
|
+
single window with no native dialogs yet (those two capability bits honestly return 0); there is no usable
|
|
138
|
+
C bridge, so host capabilities like `launchHidden` / serial / tray are not available on HarmonyOS. After
|
|
139
|
+
editing pages or the capability manifest under `assets/`, re-run `cj-tauri sync ohos` (`targets/` is
|
|
140
|
+
generated, and `check` reports it as drift).
|
|
126
141
|
|
|
127
|
-
|
|
142
|
+
How to prove it works on a device (hilog, `CjTauri` tag):
|
|
128
143
|
|
|
129
144
|
```
|
|
130
145
|
host registered after N polls label=main
|
|
@@ -133,42 +148,80 @@ page begin label=main
|
|
|
133
148
|
main window keeps system default 2090x1394 px at density 190% on screen 3120x2080
|
|
134
149
|
```
|
|
135
150
|
|
|
136
|
-
##
|
|
137
|
-
|
|
138
|
-
-
|
|
139
|
-
|
|
140
|
-
- **
|
|
141
|
-
|
|
142
|
-
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
151
|
+
## What this wrapper layer does
|
|
152
|
+
|
|
153
|
+
- **One cross-platform entry point**: Windows / Linux / Git Bash all use the same `cj-tauri` command, with
|
|
154
|
+
no more separate `.bat` and `.sh`;
|
|
155
|
+
- **Environment self-check**: locating `cjpm` and the SDK runtime libraries, and assembling `PATH` /
|
|
156
|
+
`LD_LIBRARY_PATH`, all happen here;
|
|
157
|
+
- **Obtaining the CLI itself**: the packaged prebuilt binary is preferred (`prebuilt/<platform-arch>/`);
|
|
158
|
+
otherwise the CLI sources are copied to `~/.cache/cj-tauri/<version>/`, built once with the local
|
|
159
|
+
`cjpm build` and cached — nothing is written into the install directory, so a global install can stay
|
|
160
|
+
read-only;
|
|
161
|
+
- **Data directories**: `$XDG_CACHE_HOME/cj-tauri` on Linux (default `~/.cache/cj-tauri`),
|
|
162
|
+
`%LOCALAPPDATA%\cj-tauri` on Windows.
|
|
163
|
+
|
|
164
|
+
Known limitation: the first `dev` / `build` builds the C bridge into the **framework directory's `native/`**
|
|
165
|
+
(that's where the app's `cjpm.toml` link options point), so the package install directory must be writable;
|
|
166
|
+
if you installed under a read-only path, run from inside the repository instead.
|
|
167
|
+
|
|
168
|
+
## Changes in 2.1.1
|
|
169
|
+
|
|
170
|
+
- **Serial command groups** (issue #6-4): `IpcHub.registerSerial(cmd, handler, group)` /
|
|
171
|
+
`TauriApp.registerSerial` — commands in the same group execute strictly in **arrival order**,
|
|
172
|
+
while other groups and plain commands keep the concurrent semantics. Guaranteed by structure:
|
|
173
|
+
dispatch happens on the bridge's message-callback thread, so enqueue order is arrival order,
|
|
174
|
+
and each group has a single worker draining the queue under one lock. ai-chat's five web-tab
|
|
175
|
+
commands are enrolled in the `webtab` group (the front-end queue stays — page emission order
|
|
176
|
+
and host execution order are two halves of the same guarantee).
|
|
177
|
+
- **sync keeps the launcher on the business package's framework path** (issue #6-1): the launcher's
|
|
178
|
+
`cjTauri` dependency now reads the path the business package declares in its own `cjpm.toml`
|
|
179
|
+
(relative paths absolutized against the project root), so editing that path no longer causes
|
|
180
|
+
a cjpm module conflict between two framework copies.
|
|
181
|
+
- **The desktop launcher locates its assets** (issue #6-2/3): `assets/` and `capabilities/` are
|
|
182
|
+
found by walking up two levels, so running the binary straight from `targets/desktop/` no longer
|
|
183
|
+
dies with "asset not found"; sync's next-steps output names the runtime library path
|
|
184
|
+
(`<framework>/native` for `libcjtbridge.so`).
|
|
185
|
+
- New `examples/hello-world` (smallest runnable app, generated by this CLI) and
|
|
186
|
+
`docs/hello-world-入门教程.md` — a starter guide written from an actual npx-create-to-window run.
|
|
187
|
+
|
|
188
|
+
For the full list see `[2.1.1] - 2026-10-26` in the repository's `CHANGELOG.md`.
|
|
189
|
+
|
|
190
|
+
## Changes in 2.1.0
|
|
191
|
+
|
|
192
|
+
- **New**: in-window multi-WebView views (`webview:*` — several page views inside one window with an LRU
|
|
193
|
+
cap of 4, which is what drives ai-chat's nine web-version tabs); the four commands `fs:mkdir` /
|
|
194
|
+
`fs:rename` / `fs:copy` / `fs:stat` (with overwrite and two-path scope validation); the `http` plugin's
|
|
195
|
+
HarmonyOS transport layer (over the `ohos.net.http` system stack, which brings https back); `sqlite` /
|
|
196
|
+
`serial` landing on HarmonyOS; a `notification` capability bit in `system:host`; a pointer to the SDK
|
|
197
|
+
download in the CLI's `doctor`.
|
|
198
|
+
- **Fixed**: the Linux C bridge `duplicate member 'h'` compile error (the embedded view's geometry field
|
|
199
|
+
collided with the host pointer), ai-chat's embedded view not following the widening after the sidebar
|
|
200
|
+
collapses, and the documented state of the HarmonyOS host capability bits (the menu bit is verified on a
|
|
201
|
+
device, so it's no longer all zeros).
|
|
202
|
+
- Verified on a HarmonyOS device: `fs` / `mqtt` / `redis` / `sqlite` / `store` / `http` and the menu
|
|
203
|
+
capability bit.
|
|
204
|
+
|
|
205
|
+
For the full list see `[2.1.0] - 2026-10-25` in the repository's `CHANGELOG.md`.
|
|
206
|
+
|
|
207
|
+
## Changes in 2.0.0
|
|
208
|
+
|
|
209
|
+
- **Breaking**: removed the dead public APIs `setHostFactory` / `requireInjectedHost` (the framework's
|
|
210
|
+
single-slot host injection). Apps still calling them will fail to compile — and those calls never took
|
|
211
|
+
effect anyway (`requireInjectedHost` had no callers at all). Multi-host injection goes through
|
|
212
|
+
`registerHost(HostProvider)` (with priorities); the HarmonyOS host is built in.
|
|
213
|
+
- **Fixed**: projects generated by `sync` wouldn't compile on Windows (TOML paths keeping backslashes
|
|
214
|
+
triggered `errLexEscape`, the HarmonyOS entry's `compile-option` had unescaped inner quotes, and
|
|
215
|
+
`url=\"…\"` in the generated sources was one escape level short); `cj-tauri check`'s three-way
|
|
216
|
+
comparison was always skipped on Windows because of the `main.exe` artifact name; the `modbus-poll`
|
|
217
|
+
example was missing the serial plugin import.
|
|
218
|
+
- **Gates**: version validation grew from five sites to six (adding the website's `website/index.html`),
|
|
219
|
+
and the images / diagrams that share a source with `docs/` now get a byte-for-byte comparison — the
|
|
220
|
+
website is an independent copy that previously sat outside the gate, which is exactly why it went stale.
|
|
221
|
+
|
|
222
|
+
For the full list see `[2.0.0] - 2026-10-23` in the repository's `CHANGELOG.md`.
|
|
171
223
|
|
|
172
224
|
## License
|
|
173
225
|
|
|
174
|
-
[MIT](https://
|
|
226
|
+
[MIT](https://github.com/yangyongzhen/cj-tauri/blob/main/LICENSE) © 2026 yangyongzhen
|
|
227
|
+
|
package/cjpm.toml
CHANGED
package/cli/cjpm.toml
CHANGED
package/cli/src/manifest.cj
CHANGED
|
@@ -366,13 +366,24 @@ public class Manifest {
|
|
|
366
366
|
"import cjTauri.*\n" +
|
|
367
367
|
"import cjTauri.platform.*\n" +
|
|
368
368
|
"import " + this.pkg + ".*\n" +
|
|
369
|
+
"import std.fs.*\n" +
|
|
370
|
+
"\n" +
|
|
371
|
+
"// assets/ 与 capabilities/ 按启动时工作目录相对读取;从 targets/<端>/ 直接启动会找不到\n" +
|
|
372
|
+
"//(issue #6-②,实测「资产未找到」秒退)——向上探两级回到项目根;都找不到就原样返回,\n" +
|
|
373
|
+
"// 让框架按自己的口径报错。\n" +
|
|
374
|
+
"func findDir(rel: String): String {\n" +
|
|
375
|
+
" if (exists(rel)) { return rel }\n" +
|
|
376
|
+
" if (exists(\"../\" + rel)) { return \"../\" + rel }\n" +
|
|
377
|
+
" if (exists(\"../../\" + rel)) { return \"../../\" + rel }\n" +
|
|
378
|
+
" return rel\n" +
|
|
379
|
+
"}\n" +
|
|
369
380
|
"\n" +
|
|
370
381
|
"main(): Int64 {\n" +
|
|
371
382
|
" let spec = build()\n" +
|
|
372
383
|
" .page(PageSource.Asset(\"" + this.page + "\"))\n" +
|
|
373
384
|
" .window(WindowConfig(\"${w.title}\", ${w.width}, ${w.height}))\n" +
|
|
374
|
-
" .assets(DirectoryAssetResolver(\"" + this.assetsRoot + "\"))\n" +
|
|
375
|
-
" .capabilities(\"" + this.capabilitiesDir + "\")\n" +
|
|
385
|
+
" .assets(DirectoryAssetResolver(findDir(\"" + this.assetsRoot + "\")))\n" +
|
|
386
|
+
" .capabilities(findDir(\"" + this.capabilitiesDir + "\"))\n" +
|
|
376
387
|
" launch(spec)\n" +
|
|
377
388
|
" return 0\n" +
|
|
378
389
|
"}\n"
|
package/cli/src/project.cj
CHANGED
package/cli/src/sync.cj
CHANGED
|
@@ -346,11 +346,19 @@ func cmdSync(args: Array<String>): Int64 {
|
|
|
346
346
|
match (fwOverride) {
|
|
347
347
|
case Some(f) => fwRoot = f
|
|
348
348
|
case None =>
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
// 业务包 cjpm.toml 里若已声明 cjTauri 路径(create 写入、用户可能手改),launcher
|
|
350
|
+
// 必须用**同一份**——否则两个依赖指向两个框架副本,cjpm 报模块冲突(issue #6-①)
|
|
351
|
+
match (readBizFrameworkPath()) {
|
|
352
|
+
case Some(p) =>
|
|
353
|
+
fwRoot = p
|
|
354
|
+
eprintln("[cj-tauri] 框架路径取自业务包声明:${p}")
|
|
351
355
|
case None =>
|
|
352
|
-
|
|
353
|
-
|
|
356
|
+
match (resolveFrameworkRoot()) {
|
|
357
|
+
case Some(f) => fwRoot = f
|
|
358
|
+
case None =>
|
|
359
|
+
eprintln("[cj-tauri] 未定位到框架根:请 export CJ_TAURI_ROOT=<框架仓库根>,或 --framework <路径>")
|
|
360
|
+
return 1
|
|
361
|
+
}
|
|
354
362
|
}
|
|
355
363
|
}
|
|
356
364
|
var code = 0
|
|
@@ -360,6 +368,8 @@ func cmdSync(args: Array<String>): Int64 {
|
|
|
360
368
|
writeText("targets/desktop/src/main.cj", m.renderDesktopMain(cliVersion()))
|
|
361
369
|
println("[cj-tauri] 已生成 targets/desktop/cjpm.toml 与 targets/desktop/src/main.cj(launcher 调 ${m.pkg}.build())")
|
|
362
370
|
println("[cj-tauri] 下一步:cd targets/desktop && cjpm build(需 CANGJIE_STDX 指向 stdx dynamic/stdx)")
|
|
371
|
+
// issue #6-③:直接跑产物找不到 libcjtbridge.so 时,这是唯一能指路的提示
|
|
372
|
+
println("[cj-tauri] 运行产物需 LD_LIBRARY_PATH 含 ${toTomlPath(fwRoot)}/native(libcjtbridge.so 所在),或改用 cj-tauri run / dev")
|
|
363
373
|
}
|
|
364
374
|
if (target == "" || target == "ohos") {
|
|
365
375
|
code = syncOhos(m, fwRoot)
|
|
@@ -367,6 +377,108 @@ func cmdSync(args: Array<String>): Int64 {
|
|
|
367
377
|
return code
|
|
368
378
|
}
|
|
369
379
|
|
|
380
|
+
/** 读业务包 cjpm.toml 里业务自己声明的 cjTauri 框架路径(issue #6-①)。
|
|
381
|
+
* create 把框架路径写进业务包;sync 生成 launcher 必须用**同一份**,否则两个依赖指向
|
|
382
|
+
* 两个框架副本,cjpm 报模块冲突(实测:launcher 用 resolveFrameworkRoot() 的结果、
|
|
383
|
+
* 业务包被改成仓库相对路径后,两者不再一致)。
|
|
384
|
+
* 相对路径以当前目录(业务包根)绝对化——launcher 的相对基准是 targets/desktop,不能直接抄。
|
|
385
|
+
* 读不到(无声明 / 解析失败)回 None:回落 resolveFrameworkRoot(),行为与旧版一致。
|
|
386
|
+
* 解析只用已验证的最小 API(readText + 字节迭代 + String.fromUtf8)——本仓没验证过
|
|
387
|
+
* String.substring / split / startsWith,不猜。 */
|
|
388
|
+
func readBizFrameworkPath(): Option<String> {
|
|
389
|
+
var raw = ""
|
|
390
|
+
try {
|
|
391
|
+
raw = readText("cjpm.toml")
|
|
392
|
+
} catch (e: Exception) {
|
|
393
|
+
return None
|
|
394
|
+
}
|
|
395
|
+
var line = ArrayList<Byte>()
|
|
396
|
+
for (b in raw) {
|
|
397
|
+
if (b == 10) { // '\n':行结束,处理这一行
|
|
398
|
+
let hit = pathFromCjTauriLine(line)
|
|
399
|
+
if (let Some(p) <- hit) {
|
|
400
|
+
return Some(absolutizeBizPath(p))
|
|
401
|
+
}
|
|
402
|
+
line.clear()
|
|
403
|
+
} else if (b != 13) { // 跳过 '\r'
|
|
404
|
+
line.add(b)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// 文件无结尾换行时的最后一行
|
|
408
|
+
let last = pathFromCjTauriLine(line)
|
|
409
|
+
if (let Some(p) <- last) {
|
|
410
|
+
return Some(absolutizeBizPath(p))
|
|
411
|
+
}
|
|
412
|
+
return None
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** 行里含 "cjTauri" 时,取首个引号对之间的内容(模板生成的行即 ` cjTauri = { path = "..." }`,
|
|
416
|
+
第一个引号就是路径值的开头);不含或内容为空回 None */
|
|
417
|
+
func pathFromCjTauriLine(line: ArrayList<Byte>): Option<String> {
|
|
418
|
+
let key: Array<Byte> = "cjTauri".toArray()
|
|
419
|
+
if (bytesFind(line, key, 0) < 0) {
|
|
420
|
+
return None
|
|
421
|
+
}
|
|
422
|
+
var first = -1
|
|
423
|
+
for (i in 0..line.size) {
|
|
424
|
+
if (line.get(i).getOrThrow() == 34) { // '"'
|
|
425
|
+
if (first < 0) {
|
|
426
|
+
first = i
|
|
427
|
+
} else {
|
|
428
|
+
if (i <= first + 1) {
|
|
429
|
+
return None // 空引号对:当没解析到
|
|
430
|
+
}
|
|
431
|
+
var out = ArrayList<Byte>()
|
|
432
|
+
for (k in first + 1..i) {
|
|
433
|
+
out.add(line.get(k).getOrThrow())
|
|
434
|
+
}
|
|
435
|
+
let s = String.fromUtf8(out.toArray())
|
|
436
|
+
if (s.size > 0) {
|
|
437
|
+
return Some(s)
|
|
438
|
+
}
|
|
439
|
+
return None
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return None
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** 朴素子串搜索(字节级);找不到回 -1 */
|
|
447
|
+
func bytesFind(data: ArrayList<Byte>, pat: Array<Byte>, from: Int64): Int64 {
|
|
448
|
+
if (pat.size == 0 || data.size < pat.size) {
|
|
449
|
+
return -1
|
|
450
|
+
}
|
|
451
|
+
var i = from
|
|
452
|
+
while (i <= data.size - pat.size) {
|
|
453
|
+
var ok = true
|
|
454
|
+
for (k in 0..pat.size) {
|
|
455
|
+
if (data.get(i + k).getOrThrow() != pat[k]) {
|
|
456
|
+
ok = false
|
|
457
|
+
break
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (ok) {
|
|
461
|
+
return i
|
|
462
|
+
}
|
|
463
|
+
i += 1
|
|
464
|
+
}
|
|
465
|
+
return -1
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** 业务包 toml 里的相对路径以业务包根(= sync 的 cwd)为基准绝对化;
|
|
469
|
+
launcher 在 targets/desktop 下,相对基准不同,不能直接抄 */
|
|
470
|
+
func absolutizeBizPath(p: String): String {
|
|
471
|
+
var first: UInt8 = 0
|
|
472
|
+
for (b in p) {
|
|
473
|
+
first = b
|
|
474
|
+
break
|
|
475
|
+
}
|
|
476
|
+
if (first == 47) { // '/' 开头已是绝对路径
|
|
477
|
+
return p
|
|
478
|
+
}
|
|
479
|
+
return currentDir() + "/" + p
|
|
480
|
+
}
|
|
481
|
+
|
|
370
482
|
// ---------- ohos 骨架(批次 3-PR3,RFC §7.3) ----------
|
|
371
483
|
|
|
372
484
|
/** 能力清单文件名单:[capabilities] 目录下 .json 的 basename(sync 生成与 check 漂移共用) */
|
|
@@ -1549,6 +1549,13 @@ static LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
|
1549
1549
|
return 0;
|
|
1550
1550
|
}
|
|
1551
1551
|
case WM_DESTROY:
|
|
1552
|
+
/* 先置 should_quit + 触发 onDestroy(每个宿主恰好一次)+ 作废进行中的对话框。
|
|
1553
|
+
必须排在下面的 view_close 清理**之前**:view_close 是同步 COM 调用,某个内嵌视图
|
|
1554
|
+
的网页无响应(Edge 渲染进程挂起)时它会卡在宿主线程上——窗口已经没了(视觉上
|
|
1555
|
+
「关闭了」),但 should_quit 没置位,仓颉侧 waitForExit 的轮询就永远等下去,
|
|
1556
|
+
表象是「界面关了进程还在」(2026-10-26 偶发实测)。提前置位后,清理慢就慢,
|
|
1557
|
+
仓颉 main 返回时进程退出会终止全部线程,不再永久残留。 */
|
|
1558
|
+
cj_core_window_destroyed(h);
|
|
1552
1559
|
if (p) {
|
|
1553
1560
|
/* 托盘图标先摘:不摘的话进程退出后系统托盘里会留一个点不动的幽灵图标 */
|
|
1554
1561
|
tray_remove(p);
|
|
@@ -1569,8 +1576,6 @@ static LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
|
1569
1576
|
}
|
|
1570
1577
|
p->hwnd = NULL; /* 窗口没了:别再往它 PostMessage */
|
|
1571
1578
|
}
|
|
1572
|
-
/* 置 should_quit + 触发 onDestroy(每个宿主恰好一次)+ 作废进行中的对话框 */
|
|
1573
|
-
cj_core_window_destroyed(h);
|
|
1574
1579
|
PostQuitMessage(0);
|
|
1575
1580
|
return 0;
|
|
1576
1581
|
default:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cj-tauri",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"description": "Tauri for the Cangjie language: a scaffold CLI for hybrid desktop apps — Cangjie backend (statically compiled) plus a system WebView frontend (HTML/CSS/JS), with a bidirectional IPC bridge and a capability-based security model. Windows, Linux and HarmonyOS PC.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cangjie",
|
|
7
7
|
"cangjie-lang",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"type": "git",
|
|
44
44
|
"url": "git+https://github.com/yangyongzhen/cj-tauri.git"
|
|
45
45
|
},
|
|
46
|
-
"homepage": "https://
|
|
46
|
+
"homepage": "https://github.com/yangyongzhen/cj-tauri#readme",
|
|
47
47
|
"bugs": {
|
|
48
|
-
"url": "https://
|
|
48
|
+
"url": "https://github.com/yangyongzhen/cj-tauri/issues"
|
|
49
49
|
},
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|
package/src/app.cj
CHANGED
|
@@ -238,6 +238,16 @@ public class TauriApp <: AppHost {
|
|
|
238
238
|
return this
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
/**
|
|
242
|
+
* 注册进串行组(issue #6-④):同 group 的命令严格按到达序执行,不同组 / 普通命令
|
|
243
|
+
* 的并发语义不变。适用于共享一份可变宿主状态的命令族——webtab 的 show/hide/close/bounds
|
|
244
|
+
* 这类「页面快速连点时乱序会让原生状态与 DOM 错位」的形状。链式返回 this,与 register 一致。
|
|
245
|
+
*/
|
|
246
|
+
public func registerSerial(cmd: String, handler: CommandHandler, group: String): TauriApp {
|
|
247
|
+
this.hub.registerSerial(cmd, handler, group)
|
|
248
|
+
return this
|
|
249
|
+
}
|
|
250
|
+
|
|
241
251
|
/**
|
|
242
252
|
* 装配插件(对标 Tauri v2 的 .plugin(...))
|
|
243
253
|
*
|
package/src/ipc_hub.cj
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package cjTauri
|
|
2
2
|
|
|
3
3
|
import std.collection.*
|
|
4
|
+
import std.sync.*
|
|
4
5
|
import stdx.encoding.json.*
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -114,6 +115,10 @@ public class IpcHub {
|
|
|
114
115
|
// 只在前端 emit 时被调用;后端自己 emit 给前端的事件**不**回流到这里(否则「后端广播 → 监听器再广播」会自激)。
|
|
115
116
|
private var eventListeners: HashMap<String, ArrayList<(JsonValue) -> Unit>> =
|
|
116
117
|
HashMap<String, ArrayList<(JsonValue) -> Unit>>()
|
|
118
|
+
// 串行命令组(issue #6-④):组名 → 组状态;命令名 → 组名。
|
|
119
|
+
// 同组命令严格按到达序执行(见 registerSerial 与 handleInvoke 的分流注释)。
|
|
120
|
+
private var serialGroups: HashMap<String, SerialGroup> = HashMap<String, SerialGroup>()
|
|
121
|
+
private var serialCmdToGroup: HashMap<String, String> = HashMap<String, String>()
|
|
117
122
|
|
|
118
123
|
public init() {}
|
|
119
124
|
|
|
@@ -139,6 +144,21 @@ public class IpcHub {
|
|
|
139
144
|
this.commands.add(cmd, handler)
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
/**
|
|
148
|
+
* 注册进串行组(issue #6-④):同 group 的命令严格按**到达序**执行,
|
|
149
|
+
* 不同组 / 普通命令的并发语义不变。适用于共享一份可变宿主状态的命令族——
|
|
150
|
+
* webtab 的 show/hide/close/bounds 就是「页面快速连点」时乱序会让原生视图
|
|
151
|
+
* 与 DOM 状态错位的形状(ai-chat 实测,前端只能用 promise 队列自救)。
|
|
152
|
+
* 覆盖语义与 register 一致:同命令重复注册 = 替换。
|
|
153
|
+
*/
|
|
154
|
+
public func registerSerial(cmd: String, handler: CommandHandler, group: String): Unit {
|
|
155
|
+
this.commands.add(cmd, handler)
|
|
156
|
+
if (!this.serialGroups.contains(group)) {
|
|
157
|
+
this.serialGroups.add(group, SerialGroup())
|
|
158
|
+
}
|
|
159
|
+
this.serialCmdToGroup.add(cmd, group)
|
|
160
|
+
}
|
|
161
|
+
|
|
142
162
|
public func hasCommand(cmd: String): Bool {
|
|
143
163
|
return this.commands.contains(cmd)
|
|
144
164
|
}
|
|
@@ -232,8 +252,41 @@ public class IpcHub {
|
|
|
232
252
|
this.reject(req.id, "command not registered: ${req.cmd}", req.window)
|
|
233
253
|
return
|
|
234
254
|
}
|
|
235
|
-
// 3. 交给 worker 线程执行:调用线程立刻返回,UI
|
|
236
|
-
|
|
255
|
+
// 3. 交给 worker 线程执行:调用线程立刻返回,UI 不被命令阻塞。
|
|
256
|
+
// 串行组的命令走入队(handleInvoke 跑在桥的消息回调线程上,入队顺序即到达顺序),
|
|
257
|
+
// 组 worker 单条消费——执行序严格等于到达序(issue #6-④)。
|
|
258
|
+
match (this.serialCmdToGroup.get(req.cmd)) {
|
|
259
|
+
case Some(group) => this.enqueueSerial(group, req)
|
|
260
|
+
case None => let _ = spawn { => this.runCommand(req) }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** 串行组入队:锁内置 busy 检查——组闲则置忙并拉起组 worker,忙则只排队 */
|
|
265
|
+
private func enqueueSerial(group: String, req: InvokeRequest): Unit {
|
|
266
|
+
let g = this.serialGroups.get(group).getOrThrow()
|
|
267
|
+
g.lock.lock()
|
|
268
|
+
g.queue.add(req)
|
|
269
|
+
let needWorker = !g.busy
|
|
270
|
+
g.busy = true
|
|
271
|
+
g.lock.unlock()
|
|
272
|
+
if (needWorker) {
|
|
273
|
+
let _ = spawn { => this.drainSerial(g) }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 组 worker:取空为止;取与清 busy 在同一临界区,保证「空队」与「忙」不同时被观测到 */
|
|
278
|
+
private func drainSerial(g: SerialGroup): Unit {
|
|
279
|
+
while (true) {
|
|
280
|
+
g.lock.lock()
|
|
281
|
+
if (g.queue.size == 0) {
|
|
282
|
+
g.busy = false
|
|
283
|
+
g.lock.unlock()
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
let req = g.queue.remove(at: 0)
|
|
287
|
+
g.lock.unlock()
|
|
288
|
+
this.runCommand(req)
|
|
289
|
+
}
|
|
237
290
|
}
|
|
238
291
|
|
|
239
292
|
/** worker 线程上的实际执行:跑完(或抛错)就把 resolve / reject 回投给前端 */
|
|
@@ -335,3 +388,14 @@ public class IpcHub {
|
|
|
335
388
|
this.jsSink(IpcMessage.event(event, payload, window), window)
|
|
336
389
|
}
|
|
337
390
|
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 串行命令组的状态(issue #6-④):队列 + busy 标志 + 一把锁。
|
|
394
|
+
* 入队只在桥的消息回调线程上发生(单线程),顺序即到达顺序;
|
|
395
|
+
* busy 与取队在锁内闭环——「空队」与「忙」不会同时被观测到,worker 不会漏拉也不会重复拉。
|
|
396
|
+
*/
|
|
397
|
+
internal class SerialGroup {
|
|
398
|
+
let lock: Mutex = Mutex()
|
|
399
|
+
var queue: ArrayList<InvokeRequest> = ArrayList<InvokeRequest>()
|
|
400
|
+
var busy: Bool = false
|
|
401
|
+
}
|
|
@@ -40,10 +40,12 @@ class AppSpecProbeProvider <: HostProvider {
|
|
|
40
40
|
public var last: FakeHost = FakeHost("seed")
|
|
41
41
|
|
|
42
42
|
public func priority(target: TargetInfo): Int64 {
|
|
43
|
+
let _ = target
|
|
43
44
|
return 100
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
public func create(label: String, target: TargetInfo): WebViewHost {
|
|
48
|
+
let _ = target
|
|
47
49
|
this.last = FakeHost(label)
|
|
48
50
|
return this.last
|
|
49
51
|
}
|
|
@@ -58,7 +60,7 @@ func appSpecAssembleWiresApp(): Unit {
|
|
|
58
60
|
.page(PageSource.Inline("<p/>"))
|
|
59
61
|
.window(WindowConfig("spec窗", 640, 480))
|
|
60
62
|
.plugin(StorePlugin())
|
|
61
|
-
.wire({
|
|
63
|
+
.wire({ _ =>
|
|
62
64
|
box.on = true
|
|
63
65
|
})
|
|
64
66
|
let app = assemble(spec)
|
|
@@ -88,10 +90,12 @@ class RunProbeProvider <: HostProvider {
|
|
|
88
90
|
public var last: FakeHost = FakeHost("seed")
|
|
89
91
|
|
|
90
92
|
public func priority(target: TargetInfo): Int64 {
|
|
93
|
+
let _ = target
|
|
91
94
|
return 200
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
public func create(label: String, target: TargetInfo): WebViewHost {
|
|
98
|
+
let _ = target
|
|
95
99
|
this.last = FakeHost(label)
|
|
96
100
|
return this.last
|
|
97
101
|
}
|
|
@@ -485,3 +485,79 @@ func testPlainEmitHasNoCoalesceField(): Unit {
|
|
|
485
485
|
@Expect(JsonValue.fromStr(out[1]).asObject().get("coalesce").isNone(), true)
|
|
486
486
|
@Expect(strField(out[1], "window"), "main")
|
|
487
487
|
}
|
|
488
|
+
|
|
489
|
+
/* ---- 串行组(issue #6-④):同组命令严格按到达序执行 ---- */
|
|
490
|
+
|
|
491
|
+
/** 慢处理器:先睡 delayMs 再回 "slow"——用它把「到达序」与「完成序」拉开 */
|
|
492
|
+
class SleepyHandler <: CommandHandler {
|
|
493
|
+
let delayMs: Int64
|
|
494
|
+
public init(delayMs: Int64) {
|
|
495
|
+
this.delayMs = delayMs
|
|
496
|
+
}
|
|
497
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
498
|
+
let _ = cmd
|
|
499
|
+
let _ = args
|
|
500
|
+
let _ = ipc
|
|
501
|
+
sleep(Duration.millisecond * this.delayMs)
|
|
502
|
+
return JsonString("slow")
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
class InstantHandler <: CommandHandler {
|
|
507
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
508
|
+
let _ = cmd
|
|
509
|
+
let _ = args
|
|
510
|
+
let _ = ipc
|
|
511
|
+
return JsonString("instant")
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** sink 里 data="slow" / data="instant" 两条 resolve 的下标(找不到回 -1) */
|
|
516
|
+
func dataPos(sink: ArrayList<String>, value: String): Int64 {
|
|
517
|
+
var pos = -1
|
|
518
|
+
synchronized(TEST_SINK_LOCK) {
|
|
519
|
+
for (i in 0..sink.size) {
|
|
520
|
+
if (strField(sink.get(i).getOrThrow(), "data") == value) {
|
|
521
|
+
pos = i
|
|
522
|
+
break
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return pos
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
@Test
|
|
530
|
+
func testSerialGroupPreservesArrivalOrder(): Unit {
|
|
531
|
+
let sink = ArrayList<String>()
|
|
532
|
+
let hub = hubWith(["slowA", "fastA"], [], sink)
|
|
533
|
+
// 同组:慢的先到、快的后到。串行组必须让完成序 == 到达序——
|
|
534
|
+
// 若实现退回并发分发,fast(立即完成)几乎必然排在 slow(睡 200ms)前面
|
|
535
|
+
hub.registerSerial("slowA", SleepyHandler(200), "grp")
|
|
536
|
+
hub.registerSerial("fastA", InstantHandler(), "grp")
|
|
537
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":1,\"cmd\":\"slowA\"}")
|
|
538
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":2,\"cmd\":\"fastA\"}")
|
|
539
|
+
waitSink(sink, 2)
|
|
540
|
+
let slowPos = dataPos(sink, "slow")
|
|
541
|
+
let fastPos = dataPos(sink, "instant")
|
|
542
|
+
@Expect(slowPos >= 0, true)
|
|
543
|
+
@Expect(fastPos >= 0, true)
|
|
544
|
+
@Expect(slowPos < fastPos, true)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
@Test
|
|
548
|
+
func testNonSerialCommandsFinishOutOfOrder(): Unit {
|
|
549
|
+
let sink = ArrayList<String>()
|
|
550
|
+
let hub = hubWith(["slowB", "fastB"], [], sink)
|
|
551
|
+
// 反证(锚定「串行组确实改变了语义」):同样的两条,普通 register 并发分发,
|
|
552
|
+
// 快的先完成。sleep 的 200ms 余量在 CI 上足够宽,不怕调度抖动造成假红。
|
|
553
|
+
hub.register("slowB", SleepyHandler(200))
|
|
554
|
+
hub.register("fastB", InstantHandler())
|
|
555
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":1,\"cmd\":\"slowB\"}")
|
|
556
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":2,\"cmd\":\"fastB\"}")
|
|
557
|
+
waitSink(sink, 2)
|
|
558
|
+
let slowPos = dataPos(sink, "slow")
|
|
559
|
+
let fastPos = dataPos(sink, "instant")
|
|
560
|
+
@Expect(slowPos >= 0, true)
|
|
561
|
+
@Expect(fastPos >= 0, true)
|
|
562
|
+
@Expect(fastPos < slowPos, true)
|
|
563
|
+
}
|
|
@@ -32,12 +32,17 @@ func appContextFeatureProbe(): Unit {
|
|
|
32
32
|
@Expect(ctx.dev, false)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** 经函数返回:调用点看到的是 PageSource 全类型,match 的穷尽性检查才不会与流分析打架 */
|
|
36
|
+
func assetPageSource(name: String): PageSource {
|
|
37
|
+
return PageSource.Asset(name)
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
@Test
|
|
36
41
|
func pageSourceMatch(): Unit {
|
|
37
|
-
let src =
|
|
42
|
+
let src = assetPageSource("ui/index.html")
|
|
38
43
|
match (src) {
|
|
39
44
|
case Asset(name) => @Expect(name, "ui/index.html")
|
|
40
|
-
case _ => @Expect(false, true) //
|
|
45
|
+
case _ => @Expect(false, true) // 不应到达:工厂只造 Asset
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
|
|
@@ -61,27 +66,33 @@ func mobileDiscoverSandbox(): Unit {
|
|
|
61
66
|
/** 优先级最高者胜;create 返回假宿主(pickHost 会真实调用它) */
|
|
62
67
|
class HighPrioProvider <: HostProvider {
|
|
63
68
|
public func priority(target: TargetInfo): Int64 {
|
|
69
|
+
let _ = target
|
|
64
70
|
return 10
|
|
65
71
|
}
|
|
66
72
|
public func create(label: String, target: TargetInfo): WebViewHost {
|
|
73
|
+
let _ = target
|
|
67
74
|
return FakeHost(label)
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
class LowPrioProvider <: HostProvider {
|
|
72
79
|
public func priority(target: TargetInfo): Int64 {
|
|
80
|
+
let _ = target
|
|
73
81
|
return 1
|
|
74
82
|
}
|
|
75
83
|
public func create(label: String, target: TargetInfo): WebViewHost {
|
|
84
|
+
let _ = target
|
|
76
85
|
return FakeHost(label)
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
class NullProvider <: HostProvider {
|
|
81
90
|
public func priority(target: TargetInfo): Int64 {
|
|
91
|
+
let _ = target
|
|
82
92
|
return 0
|
|
83
93
|
}
|
|
84
94
|
public func create(label: String, target: TargetInfo): WebViewHost {
|
|
95
|
+
let _ = target
|
|
85
96
|
return FakeHost(label)
|
|
86
97
|
}
|
|
87
98
|
}
|
|
@@ -236,9 +247,11 @@ func expectedDesktopOs(): String {
|
|
|
236
247
|
/** 注册表有匹配时,resolveHost 必须取注册表而不是内置宿主 */
|
|
237
248
|
class WinProvider <: HostProvider {
|
|
238
249
|
public func priority(target: TargetInfo): Int64 {
|
|
250
|
+
let _ = target
|
|
239
251
|
return 5
|
|
240
252
|
}
|
|
241
253
|
public func create(label: String, target: TargetInfo): WebViewHost {
|
|
254
|
+
let _ = target
|
|
242
255
|
return FakeHost(label)
|
|
243
256
|
}
|
|
244
257
|
}
|
package/src/version.cj
CHANGED
|
Binary file
|