@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.
Files changed (37) hide show
  1. package/README.md +95 -0
  2. package/bin/install.mjs +247 -0
  3. package/package.json +49 -0
  4. package/scripts/sync.mjs +69 -0
  5. package/skill/SKILL.md +334 -0
  6. package/skill/app/apple-sdk.md +312 -0
  7. package/skill/app/applinks.md +289 -0
  8. package/skill/app/project.md +760 -0
  9. package/skill/cli/lxdev.md +195 -0
  10. package/skill/cli/reference.md +481 -0
  11. package/skill/examples/hello-host-js/README.md +25 -0
  12. package/skill/examples/hello-host-js/home/lxapp.json +12 -0
  13. package/skill/examples/hello-host-js/home/pages/home/index.json +4 -0
  14. package/skill/examples/hello-host-js/home/pages/home/index.ts +14 -0
  15. package/skill/examples/hello-host-js/home/pages/home/index.tsx +15 -0
  16. package/skill/examples/hello-host-js/lingxia.yaml +39 -0
  17. package/skill/examples/hello-host-rust/Cargo.toml +15 -0
  18. package/skill/examples/hello-host-rust/README.md +44 -0
  19. package/skill/examples/hello-host-rust/home/lxapp.json +13 -0
  20. package/skill/examples/hello-host-rust/home/pages/home/index.html +46 -0
  21. package/skill/examples/hello-host-rust/home/pages/home/index.json +4 -0
  22. package/skill/examples/hello-host-rust/lingxia.yaml +32 -0
  23. package/skill/examples/hello-host-rust/src/lib.rs +58 -0
  24. package/skill/examples/hello-lxapp/README.md +29 -0
  25. package/skill/examples/hello-lxapp/lxapp.config.ts +8 -0
  26. package/skill/examples/hello-lxapp/lxapp.json +14 -0
  27. package/skill/examples/hello-lxapp/package.json +14 -0
  28. package/skill/examples/hello-lxapp/pages/home/index.json +4 -0
  29. package/skill/examples/hello-lxapp/pages/home/index.ts +35 -0
  30. package/skill/examples/hello-lxapp/pages/home/index.tsx +34 -0
  31. package/skill/lxapp/bridge.md +654 -0
  32. package/skill/lxapp/components.md +375 -0
  33. package/skill/lxapp/guide.md +675 -0
  34. package/skill/lxapp/lx-api.md +481 -0
  35. package/skill/native/development.md +414 -0
  36. package/skill/reference/file-lifecycle.md +325 -0
  37. 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.