@lingxia/skill 0.8.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.
- package/README.md +95 -0
- package/bin/install.mjs +247 -0
- package/package.json +49 -0
- package/scripts/sync.mjs +69 -0
- package/skill/SKILL.md +334 -0
- package/skill/app/apple-sdk.md +312 -0
- package/skill/app/applinks.md +289 -0
- package/skill/app/project.md +760 -0
- package/skill/cli/lxdev.md +195 -0
- package/skill/cli/reference.md +481 -0
- package/skill/examples/hello-host-js/README.md +25 -0
- package/skill/examples/hello-host-js/home/lxapp.json +12 -0
- package/skill/examples/hello-host-js/home/pages/home/index.json +4 -0
- package/skill/examples/hello-host-js/home/pages/home/index.ts +14 -0
- package/skill/examples/hello-host-js/home/pages/home/index.tsx +15 -0
- package/skill/examples/hello-host-js/lingxia.yaml +39 -0
- package/skill/examples/hello-host-rust/Cargo.toml +15 -0
- package/skill/examples/hello-host-rust/README.md +44 -0
- package/skill/examples/hello-host-rust/home/lxapp.json +13 -0
- package/skill/examples/hello-host-rust/home/pages/home/index.html +46 -0
- package/skill/examples/hello-host-rust/home/pages/home/index.json +4 -0
- package/skill/examples/hello-host-rust/lingxia.yaml +32 -0
- package/skill/examples/hello-host-rust/src/lib.rs +58 -0
- package/skill/examples/hello-lxapp/README.md +29 -0
- package/skill/examples/hello-lxapp/lxapp.config.ts +8 -0
- package/skill/examples/hello-lxapp/lxapp.json +14 -0
- package/skill/examples/hello-lxapp/package.json +14 -0
- package/skill/examples/hello-lxapp/pages/home/index.json +4 -0
- package/skill/examples/hello-lxapp/pages/home/index.ts +35 -0
- package/skill/examples/hello-lxapp/pages/home/index.tsx +34 -0
- package/skill/lxapp/bridge.md +654 -0
- package/skill/lxapp/components.md +375 -0
- package/skill/lxapp/guide.md +675 -0
- package/skill/lxapp/lx-api.md +481 -0
- package/skill/native/development.md +414 -0
- package/skill/reference/file-lifecycle.md +325 -0
- package/skill/skill-manifest.json +6 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# Native Development Guide
|
|
2
|
+
|
|
3
|
+
This guide covers the Rust native surface for LingXia host apps.
|
|
4
|
+
|
|
5
|
+
Use this guide when you want to:
|
|
6
|
+
|
|
7
|
+
- expose Rust host APIs to pages with `#[lingxia::native]`
|
|
8
|
+
- add optional JS AppService extensions under `lingxia::js`
|
|
9
|
+
- call shared LingXia SDK services from Rust through facade modules such as
|
|
10
|
+
`lingxia::app`, `lingxia::task`, `lingxia::file`, `lingxia::media`, and
|
|
11
|
+
`lingxia::update`
|
|
12
|
+
|
|
13
|
+
For lxapp page development, see [LxApp Development Guide](../lxapp/guide.md).
|
|
14
|
+
For host project configuration, see [App Project](../app/project.md).
|
|
15
|
+
|
|
16
|
+
## Host Addon
|
|
17
|
+
|
|
18
|
+
Every native host library registers a `HostAddon` before runtime initialization.
|
|
19
|
+
The addon is the place to install native routes, optional JS extensions, and
|
|
20
|
+
background services.
|
|
21
|
+
|
|
22
|
+
```rust
|
|
23
|
+
struct AppHostAddon;
|
|
24
|
+
|
|
25
|
+
impl lingxia::HostAddon for AppHostAddon {
|
|
26
|
+
fn install_host_apis(&self) {
|
|
27
|
+
// For each #[lingxia::native] fn, call the macro-generated companion
|
|
28
|
+
// `<fn>_host()` and pass it to register_host_entry. See "The
|
|
29
|
+
// macro-generated <fn>_host() companion" below.
|
|
30
|
+
//
|
|
31
|
+
// lingxia::host::register_host_entry(pick_document_host());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[cfg(feature = "standard")]
|
|
35
|
+
fn install_logic_extensions(&self) {
|
|
36
|
+
lingxia::js::register_logic_extension(Box::new(WorkspaceDocsExtension));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn start_services(&self) {
|
|
40
|
+
#[cfg(feature = "devtools")]
|
|
41
|
+
lingxia_devtool::start_devtool_bridge_from_env();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn register_host_addon() {
|
|
46
|
+
lingxia::register_host_addon(Box::new(AppHostAddon));
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Platform entrypoints call that registration function:
|
|
51
|
+
|
|
52
|
+
```rust
|
|
53
|
+
#[cfg(target_os = "android")]
|
|
54
|
+
#[unsafe(no_mangle)]
|
|
55
|
+
pub extern "system" fn Java_com_example_app_MainActivity_nativeRegisterHostAddon(
|
|
56
|
+
_env: jni::EnvUnowned,
|
|
57
|
+
_class: jni::objects::JClass,
|
|
58
|
+
) {
|
|
59
|
+
register_host_addon();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
|
63
|
+
#[unsafe(no_mangle)]
|
|
64
|
+
pub extern "C" fn lingxia_register_host_addon() {
|
|
65
|
+
register_host_addon();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[cfg(target_env = "ohos")]
|
|
69
|
+
#[napi_derive_ohos::napi]
|
|
70
|
+
pub fn lingxia_register_host_addon() {
|
|
71
|
+
register_host_addon();
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Generated host templates already contain this wiring.
|
|
76
|
+
|
|
77
|
+
## Native Routes
|
|
78
|
+
|
|
79
|
+
Native routes expose Rust functions to the View layer. Define them with
|
|
80
|
+
`#[lingxia::native("namespace.method")]` and return `lingxia::Result<T>`.
|
|
81
|
+
|
|
82
|
+
```rust
|
|
83
|
+
use std::sync::Arc;
|
|
84
|
+
|
|
85
|
+
#[derive(serde::Deserialize)]
|
|
86
|
+
struct PickDocumentInput {
|
|
87
|
+
title: String,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[lingxia::native("editor.pickDocument")]
|
|
91
|
+
async fn pick_document(
|
|
92
|
+
app: Arc<lingxia::LxApp>,
|
|
93
|
+
input: PickDocumentInput,
|
|
94
|
+
) -> lingxia::Result<String> {
|
|
95
|
+
Ok(lingxia::app::state_file_for(&app, &format!("{}.md", input.title))?
|
|
96
|
+
.to_string_lossy()
|
|
97
|
+
.into_owned())
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Supported parameters:
|
|
102
|
+
|
|
103
|
+
- optional first parameter: `Arc<lingxia::LxApp>`
|
|
104
|
+
- optional JSON payload parameter
|
|
105
|
+
- optional last parameter: `lingxia::host::HostCancel`
|
|
106
|
+
|
|
107
|
+
Rules:
|
|
108
|
+
|
|
109
|
+
- `Arc<lingxia::LxApp>` must be first when present.
|
|
110
|
+
- `HostCancel` must be last when present.
|
|
111
|
+
- Only one JSON payload parameter is supported.
|
|
112
|
+
- Payload types must implement `serde::Deserialize`.
|
|
113
|
+
- Return values must implement `serde::Serialize`.
|
|
114
|
+
- Handler errors should use `lingxia::Result`.
|
|
115
|
+
|
|
116
|
+
### The macro-generated `<fn>_host()` companion
|
|
117
|
+
|
|
118
|
+
`#[lingxia::native(...)]` is an attribute macro. In addition to wrapping the
|
|
119
|
+
function body, it generates a sibling `fn <name>_host() -> HostEntry` that
|
|
120
|
+
returns the registration value the host addon hands to
|
|
121
|
+
`lingxia::host::register_host_entry`. You do not write this companion yourself
|
|
122
|
+
and you cannot rename it.
|
|
123
|
+
|
|
124
|
+
For `pick_document` above, the macro generates `pick_document_host()`. Use it
|
|
125
|
+
from `HostAddon::install_host_apis`:
|
|
126
|
+
|
|
127
|
+
```rust
|
|
128
|
+
impl lingxia::HostAddon for AppHostAddon {
|
|
129
|
+
fn install_host_apis(&self) {
|
|
130
|
+
lingxia::host::register_host_entry(pick_document_host());
|
|
131
|
+
lingxia::host::register_host_entry(load_document_host());
|
|
132
|
+
// …one register_host_entry call per #[lingxia::native] fn
|
|
133
|
+
}
|
|
134
|
+
fn start_services(&self) {}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
If you forget to register the companion, the View call returns
|
|
139
|
+
`BRIDGE_METHOD_NOT_FOUND` — the route compiled but never made it into the
|
|
140
|
+
runtime's dispatch table. This is the most common cause of that error.
|
|
141
|
+
|
|
142
|
+
`stream` and `channel` variants of the macro (covered below) also generate
|
|
143
|
+
their respective `<fn>_host()` companion; register them the same way.
|
|
144
|
+
|
|
145
|
+
### Cancellation
|
|
146
|
+
|
|
147
|
+
Use `HostCancel` for async work that should stop when the page cancels the
|
|
148
|
+
request.
|
|
149
|
+
|
|
150
|
+
```rust
|
|
151
|
+
#[lingxia::native("editor.loadDocument")]
|
|
152
|
+
async fn load_document(
|
|
153
|
+
input: PickDocumentInput,
|
|
154
|
+
mut cancel: lingxia::host::HostCancel,
|
|
155
|
+
) -> lingxia::Result<String> {
|
|
156
|
+
let work = async move {
|
|
157
|
+
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
|
|
158
|
+
Ok(format!("# {}", input.title))
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
lingxia::host::await_or_cancel(&mut cancel, work)
|
|
162
|
+
.await
|
|
163
|
+
.map_err(Into::into)
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Streams
|
|
168
|
+
|
|
169
|
+
Use `#[lingxia::native(..., stream)]` for incremental results.
|
|
170
|
+
|
|
171
|
+
```rust
|
|
172
|
+
#[derive(serde::Serialize)]
|
|
173
|
+
struct ExportProgress {
|
|
174
|
+
progress: u32,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[lingxia::native("editor.exportPdf", stream)]
|
|
178
|
+
async fn export_pdf(
|
|
179
|
+
mut stream: lingxia::host::StreamContext<ExportProgress, String>,
|
|
180
|
+
) -> lingxia::Result<()> {
|
|
181
|
+
for progress in [25, 60, 100] {
|
|
182
|
+
tokio::select! {
|
|
183
|
+
_ = stream.canceled() => return Ok(()),
|
|
184
|
+
_ = tokio::time::sleep(std::time::Duration::from_millis(250)) => {}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if progress < 100 {
|
|
188
|
+
stream.send(ExportProgress { progress })?;
|
|
189
|
+
} else {
|
|
190
|
+
stream.end("/exports/report.pdf".to_string())?;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
Ok(())
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Channels
|
|
199
|
+
|
|
200
|
+
Use `#[lingxia::native(..., channel)]` for bidirectional sessions.
|
|
201
|
+
|
|
202
|
+
```rust
|
|
203
|
+
#[derive(serde::Deserialize)]
|
|
204
|
+
struct EditorSessionInput {
|
|
205
|
+
kind: String,
|
|
206
|
+
payload: String,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#[derive(serde::Serialize)]
|
|
210
|
+
struct EditorSessionEvent {
|
|
211
|
+
kind: String,
|
|
212
|
+
payload: String,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[lingxia::native("editor.session", channel)]
|
|
216
|
+
async fn editor_session(
|
|
217
|
+
mut channel: lingxia::host::ChannelContext<EditorSessionInput, EditorSessionEvent>,
|
|
218
|
+
) -> lingxia::Result<()> {
|
|
219
|
+
while let Some(message) = channel.recv().await? {
|
|
220
|
+
match message {
|
|
221
|
+
lingxia::host::ChannelMessage::Data(input) => {
|
|
222
|
+
channel.send(EditorSessionEvent {
|
|
223
|
+
kind: input.kind,
|
|
224
|
+
payload: input.payload,
|
|
225
|
+
})?;
|
|
226
|
+
}
|
|
227
|
+
lingxia::host::ChannelMessage::Close { .. } => break,
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
Ok(())
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Generated Native Client
|
|
236
|
+
|
|
237
|
+
Generate the View client from the native Rust crate's `build.rs` with
|
|
238
|
+
`lingxia-native-codegen`. This keeps native route discovery next to the crate
|
|
239
|
+
that owns `#[lingxia::native]` handlers, and `cargo build` fails before the
|
|
240
|
+
lxapp is packaged if the generated client drifts.
|
|
241
|
+
|
|
242
|
+
Native host templates already include this wiring. For a custom native crate,
|
|
243
|
+
add the build dependency:
|
|
244
|
+
|
|
245
|
+
```toml
|
|
246
|
+
[package]
|
|
247
|
+
build = "build.rs"
|
|
248
|
+
|
|
249
|
+
[build-dependencies]
|
|
250
|
+
lingxia-native-codegen = "0.6.8"
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Then generate to the lxapp's source tree:
|
|
254
|
+
|
|
255
|
+
```rust
|
|
256
|
+
// build.rs
|
|
257
|
+
use std::path::PathBuf;
|
|
258
|
+
|
|
259
|
+
fn main() {
|
|
260
|
+
println!("cargo:rerun-if-changed=build.rs");
|
|
261
|
+
println!("cargo:rerun-if-changed=src");
|
|
262
|
+
println!("cargo:rerun-if-env-changed=LINGXIA_NATIVE_CLIENT_OUT");
|
|
263
|
+
|
|
264
|
+
let Some(out) = std::env::var_os("LINGXIA_NATIVE_CLIENT_OUT") else {
|
|
265
|
+
return;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
|
269
|
+
let rust_dir = manifest_dir.join("src");
|
|
270
|
+
let out = PathBuf::from(out);
|
|
271
|
+
let out = if out.is_absolute() { out } else { manifest_dir.join(out) };
|
|
272
|
+
|
|
273
|
+
lingxia_native_codegen::generate_native_client_from_paths(&rust_dir, &out)
|
|
274
|
+
.expect("generate LingXia native client");
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The generator scans `#[lingxia::native]` handlers and nearby struct DTOs. It
|
|
279
|
+
supports TypeScript module output (`.ts`) and browser-global output (`.js`).
|
|
280
|
+
|
|
281
|
+
The CLI sets `LINGXIA_NATIVE_CLIENT_OUT` to the framework-specific generated
|
|
282
|
+
client path during native cargo builds: React/Vue use `.lingxia/native.ts`;
|
|
283
|
+
HTML uses `.lingxia/native.js`.
|
|
284
|
+
|
|
285
|
+
Use it from View code:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import { native } from "@lingxia/native";
|
|
289
|
+
|
|
290
|
+
const path = await native.editor.pickDocument({ title: "meeting-notes" });
|
|
291
|
+
|
|
292
|
+
const stream = native.editor.exportPdf();
|
|
293
|
+
stream.onEvent((event) => console.log(event.progress));
|
|
294
|
+
const output = await stream.result;
|
|
295
|
+
console.log(output);
|
|
296
|
+
|
|
297
|
+
const channel = await native.editor.session();
|
|
298
|
+
channel.onMessage((event) => console.log(event));
|
|
299
|
+
channel.send({ kind: "cursor", payload: "{}" });
|
|
300
|
+
channel.close();
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
For plain HTML views, browser-global output is available at the fixed path:
|
|
304
|
+
|
|
305
|
+
```html
|
|
306
|
+
<script src="lingxia://lxapp/.lingxia/native.js"></script>
|
|
307
|
+
<script>
|
|
308
|
+
window.native.editor.pickDocument({ title: "meeting-notes" }).then(console.log);
|
|
309
|
+
</script>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Generated clients handle bridge details internally. Module clients use the
|
|
313
|
+
high-level `@lingxia/bridge` helpers. Browser-global clients use
|
|
314
|
+
`LingXiaBridge.raw.*` because they already generate full `host.*` routes and
|
|
315
|
+
wrap stream/channel handles themselves.
|
|
316
|
+
|
|
317
|
+
## LingXia Facade Modules
|
|
318
|
+
|
|
319
|
+
Native route handlers should use facade modules instead of internal crates:
|
|
320
|
+
|
|
321
|
+
```rust
|
|
322
|
+
let state_file = lingxia::app::state_file_for(&app, "editor.json")?;
|
|
323
|
+
let downloaded = lingxia::file::download(&app, "https://example.com/report.pdf").await?;
|
|
324
|
+
let media = lingxia::media::choose_media(&app, request).await?;
|
|
325
|
+
let files = lingxia::file::choose_file(&app, request).await?;
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Use `lingxia::task` for runtime helpers:
|
|
329
|
+
|
|
330
|
+
```rust
|
|
331
|
+
let value = lingxia::task::spawn_blocking(|| expensive_work()).await?;
|
|
332
|
+
lingxia::task::spawn(async move {
|
|
333
|
+
// background work
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Host app update defaults to LingXia's built-in UX. Native apps that need full
|
|
338
|
+
custom UI should opt into custom mode and drive the returned update task:
|
|
339
|
+
|
|
340
|
+
```rust
|
|
341
|
+
lingxia::update::use_custom_host_app_update();
|
|
342
|
+
|
|
343
|
+
if let Some(update) = lingxia::update::check_host_app_update().await? {
|
|
344
|
+
let info = update.info();
|
|
345
|
+
println!(
|
|
346
|
+
"update {} size {:?}",
|
|
347
|
+
info.version(),
|
|
348
|
+
info.package_size_bytes()
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
let mut apply = update.apply();
|
|
352
|
+
while let Some(event) = apply.next().await {
|
|
353
|
+
println!("update event: {event:?}");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
The checked update owns the package metadata. App code should not pass versions,
|
|
359
|
+
package paths, or raw provider results back into the update API.
|
|
360
|
+
|
|
361
|
+
Provider authors should import provider traits through `lingxia::provider`.
|
|
362
|
+
Media stream providers should import stream traits through `lingxia::media`.
|
|
363
|
+
|
|
364
|
+
## JS AppService Extensions
|
|
365
|
+
|
|
366
|
+
JS AppService extensions are optional and are only available with the
|
|
367
|
+
`standard` Cargo feature. They are scoped under `lingxia::js`.
|
|
368
|
+
|
|
369
|
+
```rust
|
|
370
|
+
#[cfg(feature = "standard")]
|
|
371
|
+
use lingxia::js::LxLogicExtension;
|
|
372
|
+
|
|
373
|
+
#[cfg(feature = "standard")]
|
|
374
|
+
struct WorkspaceDocsExtension;
|
|
375
|
+
|
|
376
|
+
#[cfg(feature = "standard")]
|
|
377
|
+
impl LxLogicExtension for WorkspaceDocsExtension {
|
|
378
|
+
fn init(&self, ctx: &rong::JSContext) -> rong::JSResult<()> {
|
|
379
|
+
let lx = ctx.global().get::<_, rong::JSObject>("lx")?;
|
|
380
|
+
let ns = rong::JSObject::new(ctx);
|
|
381
|
+
ns.set("loadDocument", rong::JSFunc::new(ctx, load_document)?)?;
|
|
382
|
+
lx.set("workspaceDocs", ns)?;
|
|
383
|
+
Ok(())
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#[cfg(feature = "standard")]
|
|
388
|
+
fn load_document(_ctx: rong::JSContext, id: String) -> rong::JSResult<String> {
|
|
389
|
+
Ok(format!("# {id}"))
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Register the extension from `HostAddon::install_logic_extensions`:
|
|
394
|
+
|
|
395
|
+
```rust
|
|
396
|
+
#[cfg(feature = "standard")]
|
|
397
|
+
fn install_logic_extensions(&self) {
|
|
398
|
+
lingxia::js::register_logic_extension(Box::new(WorkspaceDocsExtension));
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
When `features.appService: false` in `lingxia.yaml`, the generated host builds
|
|
403
|
+
without `standard`; `lingxia::js` is not public, and logic-enabled lxapps are
|
|
404
|
+
rejected at runtime. Lxapp manifests must use `logic`, not `appService`.
|
|
405
|
+
|
|
406
|
+
## Choosing The Surface
|
|
407
|
+
|
|
408
|
+
| Surface | Runs in | Called from | Use for |
|
|
409
|
+
| --- | --- | --- | --- |
|
|
410
|
+
| `#[lingxia::native]` | Rust host async runtime | View / generated native client | page-scoped native UI, file pickers, browser controls, native streams/channels |
|
|
411
|
+
| `lingxia::js` extension | JS AppService runtime | Logic layer as `lx.*` | business logic helpers, app-owned data APIs, synchronous JS-facing helpers |
|
|
412
|
+
|
|
413
|
+
Keep business state and app logic in AppService. Use native routes for
|
|
414
|
+
page-scoped host capabilities and native-owned workflows.
|