@lucent-lang/lucent 0.0.4 → 0.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 +97 -0
- package/app.plugin.js +46 -10
- package/dist/{bench-CR3E7e2e.js → bench-eEez4jhQ.js} +10 -6
- package/dist/{build-CAIrRSYA.js → build-CpUjeHFz.js} +1 -1
- package/dist/{build-check-QozuNJm5.js → build-check-BA1ZEp9s.js} +28 -13
- package/dist/{check-1SCmYlqA.js → check-BZx3FgI6.js} +1 -1
- package/dist/cli.js +62 -13
- package/dist/{codes-CIuwXt1S.js → codes-D-6LxUqD.js} +163 -7
- package/dist/compiler.js +4 -4
- package/dist/{dashboard-B-NzjjaN.js → dashboard-CC-HXhcd.js} +2 -2
- package/dist/{dev-BYWMP7oC.js → dev-DI5yL8zx.js} +62 -23
- package/dist/{diagnostic-BM9sXmwl.js → diagnostic-BvwLZQS1.js} +3 -1
- package/dist/{doctor-C0PrfWT6.js → doctor-CTAX3euc.js} +1 -1
- package/dist/{doctor-DdR4JnVc.js → doctor-CrgOAV5L.js} +1 -1
- package/dist/{explain--vV1oaRl.js → explain-Byit07F-.js} +1 -1
- package/dist/{packages-HyskKIsY.js → packages-CSmKeaAh.js} +2 -33
- package/dist/pipeline-BM_VM2X_.js +700 -0
- package/dist/project-BKNoTEEp.js +342 -0
- package/dist/sdk-coverage-iiv3JqMp.js +113 -0
- package/dist/sdk-diff-ByJ1Gw-F.js +109 -0
- package/dist/sdk-lock-BQn7IyRs.js +38 -0
- package/dist/{sdk-prefetch-mo04JDZV.js → sdk-prefetch-Bv2pNega.js} +2 -2
- package/dist/{sdk-search-Iv4K3-8a.js → sdk-search-CPGJm_Ne.js} +2 -2
- package/dist/{sdk-show-CkrdlqlP.js → sdk-show-BUkLqRnA.js} +5 -4
- package/dist/sdk-usage-0ZFMAYg1.js +95 -0
- package/dist/src-ChnmWLwn.js +40851 -0
- package/dist/trace-Bi4hkmPx.js +139 -0
- package/lib/core.cjs +368 -1
- package/lib/sdk/android.d.ts +54 -1
- package/lib/sdk/compose.d.ts +94 -0
- package/lib/sdk/compose.schemas.json.gz +0 -0
- package/lib/sdk/core.d.ts +201 -0
- package/lib/sdk/ios.d.ts +75 -2
- package/lib/sdk/ui.d.ts +129 -0
- package/metro/transformer.cjs +11 -2
- package/package.json +4 -3
- package/runtime/cpp/lucent/abort.cpp +59 -11
- package/runtime/cpp/lucent/abort.h +22 -6
- package/runtime/cpp/lucent/array.h +12 -2
- package/runtime/cpp/lucent/async.h +101 -21
- package/runtime/cpp/lucent/bigint.cpp +898 -0
- package/runtime/cpp/lucent/bigint.h +302 -0
- package/runtime/cpp/lucent/buffer.cpp +228 -0
- package/runtime/cpp/lucent/buffer.h +328 -0
- package/runtime/cpp/lucent/builtins.cpp +4 -1
- package/runtime/cpp/lucent/bytes.h +21 -2
- package/runtime/cpp/lucent/callback.cpp +29 -0
- package/runtime/cpp/lucent/callback.h +230 -0
- package/runtime/cpp/lucent/compute.cpp +370 -0
- package/runtime/cpp/lucent/compute.h +455 -0
- package/runtime/cpp/lucent/conversions.h +111 -0
- package/runtime/cpp/lucent/core.h +16 -5
- package/runtime/cpp/lucent/equality.h +107 -28
- package/runtime/cpp/lucent/execution.cpp +416 -0
- package/runtime/cpp/lucent/execution.h +270 -0
- package/runtime/cpp/lucent/extension.cpp +97 -0
- package/runtime/cpp/lucent/extension.h +210 -0
- package/runtime/cpp/lucent/helpers.h +12 -5
- package/runtime/cpp/lucent/items.h +52 -0
- package/runtime/cpp/lucent/jserror.cpp +9 -0
- package/runtime/cpp/lucent/jserror.h +11 -0
- package/runtime/cpp/lucent/jsi/convert.cpp +138 -0
- package/runtime/cpp/lucent/jsi/convert.h +128 -59
- package/runtime/cpp/lucent/jsi/host.cpp +213 -35
- package/runtime/cpp/lucent/jsi/host.h +116 -16
- package/runtime/cpp/lucent/json.h +19 -1
- package/runtime/cpp/lucent/lifecycle.cpp +260 -0
- package/runtime/cpp/lucent/lifecycle.h +124 -0
- package/runtime/cpp/lucent/lucent.h +7 -0
- package/runtime/cpp/lucent/map.h +41 -7
- package/runtime/cpp/lucent/native.cpp +35 -47
- package/runtime/cpp/lucent/native.h +116 -26
- package/runtime/cpp/lucent/number.cpp +38 -38
- package/runtime/cpp/lucent/number.h +33 -0
- package/runtime/cpp/lucent/operation.h +65 -0
- package/runtime/cpp/lucent/ops.h +14 -6
- package/runtime/cpp/lucent/platform/android.cpp +223 -10
- package/runtime/cpp/lucent/platform/android.h +227 -4
- package/runtime/cpp/lucent/platform/android_activity.cpp +258 -0
- package/runtime/cpp/lucent/platform/android_requests.h +140 -0
- package/runtime/cpp/lucent/platform/compose.h +256 -0
- package/runtime/cpp/lucent/platform/ios.h +325 -25
- package/runtime/cpp/lucent/platform/ios.mm +1 -14
- package/runtime/cpp/lucent/platform/ios_ui.h +192 -0
- package/runtime/cpp/lucent/platform/ios_ui.mm +454 -0
- package/runtime/cpp/lucent/platform/swiftui.h +190 -0
- package/runtime/cpp/lucent/presentation.cpp +11 -0
- package/runtime/cpp/lucent/presentation.h +109 -0
- package/runtime/cpp/lucent/reactive.cpp +406 -0
- package/runtime/cpp/lucent/reactive.h +566 -0
- package/runtime/cpp/lucent/report.cpp +40 -0
- package/runtime/cpp/lucent/report.h +16 -0
- package/runtime/cpp/lucent/resource.cpp +83 -0
- package/runtime/cpp/lucent/resource.h +75 -0
- package/runtime/cpp/lucent/scheduler.cpp +38 -124
- package/runtime/cpp/lucent/scheduler.h +102 -83
- package/runtime/cpp/lucent/scope.cpp +308 -0
- package/runtime/cpp/lucent/scope.h +452 -0
- package/runtime/cpp/lucent/sizing.h +263 -0
- package/runtime/cpp/lucent/slots.h +231 -0
- package/runtime/cpp/lucent/trace.cpp +539 -0
- package/runtime/cpp/lucent/trace.h +251 -0
- package/runtime/cpp/lucent/transport.cpp +9 -0
- package/runtime/cpp/lucent/transport.h +341 -0
- package/runtime/cpp/lucent/view.cpp +123 -0
- package/runtime/cpp/lucent/view.h +157 -0
- package/runtime/cpp/rn/LucentComponentView.h +121 -0
- package/runtime/cpp/rn/LucentComponentView.mm +479 -0
- package/runtime/cpp/rn/LucentModule.cpp +37 -7
- package/runtime/cpp/rn/LucentModule.h +3 -0
- package/runtime/cpp/rn/LucentViewChildren.h +80 -0
- package/runtime/cpp/rn/LucentViewChildren.mm +211 -0
- package/runtime/cpp/rn/LucentViewRegistry.h +62 -0
- package/runtime/cpp/rn/LucentViewRequests.cpp +91 -0
- package/runtime/cpp/rn/LucentViewRequests.h +104 -0
- package/runtime/cpp/rn/LucentViewSizing.h +345 -0
- package/runtime/cpp/rn/LucentViewSlots.h +242 -0
- package/runtime/cpp/rn/LucentViewValues.h +125 -0
- package/runtime/cpp/rn/LucentViews.h +440 -0
- package/runtime/cpp/rn/LucentViewsAndroid.cpp +439 -0
- package/runtime/cpp/rn/LucentViewsAndroid.h +138 -0
- package/runtime/cpp/third_party/dragonbox/LICENSE-Boost +23 -0
- package/runtime/cpp/third_party/dragonbox/README.md +16 -0
- package/runtime/cpp/third_party/dragonbox/dragonbox.h +4440 -0
- package/runtime/js/index.d.ts +11 -1
- package/runtime/js/index.js +84 -8
- package/runtime/js/views.d.ts +38 -0
- package/runtime/js/views.js +226 -0
- package/runtime/native/android/CMakeLists.txt +4 -1
- package/runtime/native/android/src/compose/java/dev/lucent/compose/CompositionRules.kt +44 -0
- package/runtime/native/android/src/compose/java/dev/lucent/compose/LucentComposition.kt +161 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentActivities.java +292 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentChildren.java +174 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentHostView.java +232 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentInitializer.java +46 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentPackage.java +4 -3
- package/runtime/native/android/src/main/java/dev/lucent/LucentRequestActivity.java +92 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentSlotView.java +102 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentViewManager.java +149 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentViewManagers.java +16 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentViews.java +44 -0
- package/runtime/test/jsi/harness.cpp +9 -0
- package/schemas/build.schema.json +96 -1
- package/schemas/check.schema.json +56 -1
- package/schemas/lucent.schema.json +495 -9
- package/schemas/sdk-diff.schema.json +70 -0
- package/schemas/sdk-lock.schema.json +67 -0
- package/ts-plugin/index.js +85 -4
- package/dist/pipeline-CH7-2cxu.js +0 -232
- package/dist/project-D8tEet0k.js +0 -150
- package/dist/sdk-coverage-BoW_3Efd.js +0 -64
- package/dist/src-BD7f1_aZ.js +0 -11493
package/runtime/js/index.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** What lucent build compiled: the runtime ABI, each target's program and each module's API. */
|
|
2
|
+
export interface BuildIdentity {
|
|
3
|
+
runtimeAbi: number;
|
|
4
|
+
programs: Record<string, string>;
|
|
5
|
+
apis: Record<string, Record<string, string>>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Returns the exports object of a compiled Lucent module, after checking
|
|
9
|
+
* the app's native code against `expected`.
|
|
10
|
+
*/
|
|
2
11
|
export declare function loadModule(
|
|
3
12
|
name: string,
|
|
4
13
|
registry: () => { get(name: string): unknown },
|
|
14
|
+
expected?: BuildIdentity,
|
|
5
15
|
): Record<string, unknown>;
|
|
6
16
|
/** Wraps a native class factory in a constructor usable with `new`. */
|
|
7
17
|
export declare function lucentClass<T>(factory: T): T;
|
package/runtime/js/index.js
CHANGED
|
@@ -9,12 +9,10 @@
|
|
|
9
9
|
let native;
|
|
10
10
|
|
|
11
11
|
function getNative(registry) {
|
|
12
|
+
// Test hosts install modules directly, and may install new ones.
|
|
13
|
+
if (typeof globalThis.__lucentModules === "object" && globalThis.__lucentModules)
|
|
14
|
+
return globalThis.__lucentModules;
|
|
12
15
|
if (native) return native;
|
|
13
|
-
// Test hosts install modules directly.
|
|
14
|
-
if (typeof globalThis.__lucentModules === "object" && globalThis.__lucentModules) {
|
|
15
|
-
native = globalThis.__lucentModules;
|
|
16
|
-
return native;
|
|
17
|
-
}
|
|
18
16
|
native = registry().get("Lucent");
|
|
19
17
|
if (!native) {
|
|
20
18
|
throw new Error(
|
|
@@ -25,13 +23,91 @@ function getNative(registry) {
|
|
|
25
23
|
return native;
|
|
26
24
|
}
|
|
27
25
|
|
|
26
|
+
const REBUILD =
|
|
27
|
+
"Recompile the app's native code: run `lucent build`, then build and install the app again " +
|
|
28
|
+
"(compile-native). A JavaScript update (a reload, or over the air) cannot replace native code.";
|
|
29
|
+
const RELOAD = "Reload JavaScript built for the installed app (reload-js).";
|
|
30
|
+
|
|
31
|
+
/** An error that names what to do: `action` is one of lucent build's pending action kinds. */
|
|
32
|
+
function mismatch(problem, action) {
|
|
33
|
+
const e = new Error(`Lucent: ${problem} ${action === "reload-js" ? RELOAD : REBUILD}`);
|
|
34
|
+
e.code = "LUCENT_NATIVE_MISMATCH";
|
|
35
|
+
e.action = action;
|
|
36
|
+
return e;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What the native code and this JavaScript's build share, once per host and
|
|
41
|
+
* build: undefined, or the error to throw. A different program with the
|
|
42
|
+
* same APIs runs, with a warning: calls are safe, but the implementation is
|
|
43
|
+
* not the one this JavaScript was built with.
|
|
44
|
+
*/
|
|
45
|
+
const programs = new Map();
|
|
46
|
+
|
|
47
|
+
function checkProgram(actual, expected) {
|
|
48
|
+
const key = `${actual.host}\n${expected.runtimeAbi}\n${expected.programs[actual.target]}`;
|
|
49
|
+
if (programs.has(key)) return programs.get(key);
|
|
50
|
+
|
|
51
|
+
let problem;
|
|
52
|
+
if (actual.runtimeAbi !== expected.runtimeAbi) {
|
|
53
|
+
problem = mismatch(
|
|
54
|
+
`the app's native code has Lucent runtime ABI ${actual.runtimeAbi}, and this JavaScript needs ${expected.runtimeAbi}.`,
|
|
55
|
+
actual.runtimeAbi > expected.runtimeAbi ? "reload-js" : "compile-native",
|
|
56
|
+
);
|
|
57
|
+
} else if (!expected.programs[actual.target]) {
|
|
58
|
+
problem = mismatch(
|
|
59
|
+
`this JavaScript was built without the ${actual.target} native code the app runs.`,
|
|
60
|
+
"compile-native",
|
|
61
|
+
);
|
|
62
|
+
} else if (actual.program !== expected.programs[actual.target]) {
|
|
63
|
+
console.warn(
|
|
64
|
+
`Lucent: the app's native code (${actual.target} program ${actual.program}) was not built from ` +
|
|
65
|
+
`the sources this JavaScript was (program ${expected.programs[actual.target]}): its modules have ` +
|
|
66
|
+
`the same APIs, but the installed implementation runs. ${REBUILD} If the app was installed ` +
|
|
67
|
+
`after this JavaScript was built, reload JavaScript instead (reload-js).`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
programs.set(key, problem);
|
|
72
|
+
return problem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Throws unless module `name` of the native code is what this JavaScript was built against. */
|
|
76
|
+
function check(n, name, expected) {
|
|
77
|
+
const actual = n.__lucentIdentity;
|
|
78
|
+
if (!actual)
|
|
79
|
+
throw mismatch(
|
|
80
|
+
"the app's native code was built before Lucent gave it a build identity, so it cannot be checked against this JavaScript.",
|
|
81
|
+
"compile-native",
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const problem = checkProgram(actual, expected);
|
|
85
|
+
if (problem) throw problem;
|
|
86
|
+
|
|
87
|
+
const want = expected.apis[actual.target] && expected.apis[actual.target][name];
|
|
88
|
+
const have = actual.modules[name];
|
|
89
|
+
if (have === undefined)
|
|
90
|
+
throw mismatch(`module "${name}" is not in the app's native code.`, "compile-native");
|
|
91
|
+
if (have !== want)
|
|
92
|
+
throw mismatch(
|
|
93
|
+
`module "${name}" of the app's native code has other exports or signatures (API ${have}) ` +
|
|
94
|
+
`than this JavaScript was built for (API ${want}).`,
|
|
95
|
+
"compile-native",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
28
99
|
/**
|
|
29
|
-
* The exports of a compiled Lucent module
|
|
100
|
+
* The exports of a compiled Lucent module, after checking that the app's
|
|
101
|
+
* native code was built as this JavaScript expects.
|
|
30
102
|
* @param {string} name module name (the file name without .lucent.ts)
|
|
31
103
|
* @param {() => { get(name: string): any }} registry returns React Native's TurboModuleRegistry
|
|
104
|
+
* @param {{ runtimeAbi: number, programs: Record<string, string>, apis: Record<string, Record<string, string>> }} [expected]
|
|
105
|
+
* the build identity lucent build wrote next to the proxies (none from older proxies: nothing is checked)
|
|
32
106
|
*/
|
|
33
|
-
function loadModule(name, registry) {
|
|
34
|
-
const
|
|
107
|
+
function loadModule(name, registry, expected) {
|
|
108
|
+
const n = getNative(registry);
|
|
109
|
+
if (expected) check(n, name, expected);
|
|
110
|
+
const m = n[name];
|
|
35
111
|
if (!m) {
|
|
36
112
|
throw new Error(
|
|
37
113
|
`Lucent: module "${name}" is not in the native build. Run \`lucent build\` and rebuild the app.`,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a value is encoded for its native view: 0, as it is; `n`, a nullable
|
|
3
|
+
* value, boxed; `a`, an array's elements; `o`, some of an object's fields.
|
|
4
|
+
*/
|
|
5
|
+
export type Shape =
|
|
6
|
+
| 0
|
|
7
|
+
| { n: Shape }
|
|
8
|
+
| { a: Shape }
|
|
9
|
+
| { o: readonly { name: string; shape: Shape }[] };
|
|
10
|
+
|
|
11
|
+
/** A component's description, which the compiler generates into its module's proxy. */
|
|
12
|
+
export interface ComponentConfig {
|
|
13
|
+
/** The registration name of its native view. */
|
|
14
|
+
name: string;
|
|
15
|
+
displayName: string;
|
|
16
|
+
/** Whether it takes React children, which its native view mounts in its slot. */
|
|
17
|
+
children?: boolean;
|
|
18
|
+
/** Its props, each under its transport key. */
|
|
19
|
+
props: readonly { name: string; key: string; shape: Shape }[];
|
|
20
|
+
/** Its events, by slot: the handler's key and the event's name. */
|
|
21
|
+
events: readonly { name: string; key: string; event: string }[];
|
|
22
|
+
/** Its ref's commands: whether each answers, and its parameters' shapes. */
|
|
23
|
+
commands: readonly { name: string; request: boolean; params: readonly Shape[] }[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The React component rendering a Lucent component's native view. */
|
|
27
|
+
export declare function lucentComponent(
|
|
28
|
+
config: ComponentConfig,
|
|
29
|
+
react: object,
|
|
30
|
+
reactNative: object,
|
|
31
|
+
): (props: never) => unknown;
|
|
32
|
+
|
|
33
|
+
/** Settles a command's request: rejected with `error` unless it is null, else resolved with `value`. */
|
|
34
|
+
export declare function settleRequest(
|
|
35
|
+
id: number,
|
|
36
|
+
error: string | null | undefined,
|
|
37
|
+
value?: unknown,
|
|
38
|
+
): void;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Lucent components in React. A module's proxy describes each component it
|
|
3
|
+
// exports (see lucentComponent), and this file makes the description a
|
|
4
|
+
// React component rendering the native view React Native registers under
|
|
5
|
+
// the component's registration name. `lucent build` copies it into the
|
|
6
|
+
// native package (js/_lucent/views.js).
|
|
7
|
+
// Like the loader, it requires nothing itself: the proxy passes the app's
|
|
8
|
+
// React and React Native in.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A value as its native view reads it. A nullable value travels boxed,
|
|
12
|
+
* `[value]` (`[null]` for null), because React sends a removed prop as
|
|
13
|
+
* null and turns undefined into null inside objects and argument lists:
|
|
14
|
+
* unboxed, a missing value and null would look the same. `shape` says
|
|
15
|
+
* where a value holds nullable ones: 0 (nowhere), `{ n }` (it is one),
|
|
16
|
+
* `{ a }` (its elements) or `{ o }` (some of its fields).
|
|
17
|
+
*/
|
|
18
|
+
function encode(value, shape) {
|
|
19
|
+
if (value === undefined || shape === 0) return value;
|
|
20
|
+
|
|
21
|
+
if ("n" in shape) return value === null ? [null] : [encode(value, shape.n)];
|
|
22
|
+
|
|
23
|
+
if ("a" in shape) return Array.isArray(value) ? value.map((v) => encode(v, shape.a)) : value;
|
|
24
|
+
|
|
25
|
+
if (value === null || typeof value !== "object") return value;
|
|
26
|
+
|
|
27
|
+
const out = { ...value };
|
|
28
|
+
for (const field of shape.o) {
|
|
29
|
+
if (Object.prototype.hasOwnProperty.call(out, field.name))
|
|
30
|
+
out[field.name] = encode(out[field.name], field.shape);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** React Native's view configuration: the props and handlers under their keys, the events by slot. */
|
|
36
|
+
function viewConfig(config) {
|
|
37
|
+
const validAttributes = {};
|
|
38
|
+
const directEventTypes = {};
|
|
39
|
+
|
|
40
|
+
for (const prop of config.props) validAttributes[prop.key] = true;
|
|
41
|
+
|
|
42
|
+
for (const event of config.events) {
|
|
43
|
+
validAttributes[event.key] = true;
|
|
44
|
+
directEventTypes[event.event] = { registrationName: event.key };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { uiViewClassName: config.name, validAttributes, directEventTypes };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A prop React was given: not what every object inherits (toString, constructor). */
|
|
51
|
+
const given = (props, name) =>
|
|
52
|
+
Object.prototype.hasOwnProperty.call(props, name) ? props[name] : undefined;
|
|
53
|
+
|
|
54
|
+
/** The native view's props for React's `props` (but its ref). */
|
|
55
|
+
function hostProps(config, props) {
|
|
56
|
+
const out = { style: props.style };
|
|
57
|
+
|
|
58
|
+
for (const prop of config.props) {
|
|
59
|
+
const value = given(props, prop.name);
|
|
60
|
+
if (value !== undefined) out[prop.key] = encode(value, prop.shape);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// React calls the handler of the latest commit, which calls that commit's
|
|
64
|
+
// callback with the arguments the native call passed, by position (React
|
|
65
|
+
// Native adds the view's tag to every payload as `target`).
|
|
66
|
+
for (const event of config.events) {
|
|
67
|
+
const callback = given(props, event.name);
|
|
68
|
+
if (typeof callback === "function") out[event.key] = (e) => callback(...e.nativeEvent.args);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- requests: commands that answer ---------------------------------------------------
|
|
75
|
+
//
|
|
76
|
+
// A request's promise settles once: with the command's result, with its
|
|
77
|
+
// error (an Error of the native side's message), or with an AbortError
|
|
78
|
+
// when its view unmounts first (the native side's work is not stopped:
|
|
79
|
+
// its answer is dropped). A command sent while the view is not mounted
|
|
80
|
+
// fails with an InvalidStateError: thrown by a void command, the
|
|
81
|
+
// rejection of a request.
|
|
82
|
+
|
|
83
|
+
/** An Error named `name`, as JavaScript names the state it reports. */
|
|
84
|
+
function named(name, message) {
|
|
85
|
+
const e = new Error(message);
|
|
86
|
+
e.name = name;
|
|
87
|
+
return e;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const pending = new Map();
|
|
91
|
+
let lastRequest = 0;
|
|
92
|
+
let connected = false;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Settles request `id`: rejects it with `error` (a message) unless that is
|
|
96
|
+
* null or undefined, else resolves it with `value`. A request that is no
|
|
97
|
+
* longer pending (its view unmounted) is ignored. The native host calls it
|
|
98
|
+
* on the JavaScript thread.
|
|
99
|
+
*/
|
|
100
|
+
function settleRequest(id, error, value) {
|
|
101
|
+
const request = pending.get(id);
|
|
102
|
+
if (!request) return;
|
|
103
|
+
|
|
104
|
+
pending.delete(id);
|
|
105
|
+
request.mount.delete(id);
|
|
106
|
+
|
|
107
|
+
if (error === null || error === undefined) request.resolve(value);
|
|
108
|
+
else request.reject(new Error(`Lucent: ${error}`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Gives the native host settleRequest, once: requests are answered through its channel. */
|
|
112
|
+
function connect(ReactNative) {
|
|
113
|
+
if (connected) return;
|
|
114
|
+
|
|
115
|
+
const host = ReactNative.TurboModuleRegistry.get("Lucent");
|
|
116
|
+
if (!host || typeof host.__lucentViewRequests !== "function") {
|
|
117
|
+
const e = new Error(
|
|
118
|
+
"Lucent: the app's native code cannot answer view commands. Recompile the app's native " +
|
|
119
|
+
"code: run `lucent build`, then build and install the app again (compile-native).",
|
|
120
|
+
);
|
|
121
|
+
e.code = "LUCENT_NATIVE_MISMATCH";
|
|
122
|
+
e.action = "compile-native";
|
|
123
|
+
throw e;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
host.__lucentViewRequests(settleRequest);
|
|
127
|
+
connected = true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The ref's methods: each sends its command to the mounted native view. */
|
|
131
|
+
function commandsOf(config, host, mount, native, ReactNative) {
|
|
132
|
+
const out = {};
|
|
133
|
+
|
|
134
|
+
for (const command of config.commands) {
|
|
135
|
+
const where = `${config.displayName}.${command.name}()`;
|
|
136
|
+
const encoded = (args) =>
|
|
137
|
+
args.slice(0, command.params.length).map((a, i) => encode(a, command.params[i]));
|
|
138
|
+
|
|
139
|
+
const unmounted = () =>
|
|
140
|
+
named("InvalidStateError", `Lucent: ${where}: the view is not mounted.`);
|
|
141
|
+
|
|
142
|
+
out[command.name] = command.request
|
|
143
|
+
? (...args) =>
|
|
144
|
+
new Promise((resolve, reject) => {
|
|
145
|
+
if (!host.current) throw unmounted();
|
|
146
|
+
|
|
147
|
+
connect(ReactNative);
|
|
148
|
+
|
|
149
|
+
// The answer comes in a later turn of the JavaScript thread, after the id is pending.
|
|
150
|
+
const id = ++lastRequest;
|
|
151
|
+
native[command.name](host.current, id, ...encoded(args));
|
|
152
|
+
|
|
153
|
+
pending.set(id, { resolve, reject, mount });
|
|
154
|
+
mount.add(id);
|
|
155
|
+
})
|
|
156
|
+
: (...args) => {
|
|
157
|
+
if (!host.current) throw unmounted();
|
|
158
|
+
|
|
159
|
+
native[command.name](host.current, ...encoded(args));
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Rejects the requests a mount left unanswered: its unmount cancelled them. */
|
|
167
|
+
function abandon(config, mount) {
|
|
168
|
+
for (const id of mount) {
|
|
169
|
+
const request = pending.get(id);
|
|
170
|
+
pending.delete(id);
|
|
171
|
+
if (request)
|
|
172
|
+
request.reject(
|
|
173
|
+
named("AbortError", `Lucent: ${config.displayName} unmounted before answering.`),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
mount.clear();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The React component a proxy exports for a Lucent component.
|
|
181
|
+
* @param config the component's description, which the compiler generates
|
|
182
|
+
* @param React the app's React
|
|
183
|
+
* @param ReactNative the app's React Native
|
|
184
|
+
*/
|
|
185
|
+
function lucentComponent(config, React, ReactNative) {
|
|
186
|
+
const Host = ReactNative.NativeComponentRegistry.get(config.name, () => viewConfig(config));
|
|
187
|
+
const native = config.commands.length
|
|
188
|
+
? ReactNative.codegenNativeCommands({ supportedCommands: config.commands.map((c) => c.name) })
|
|
189
|
+
: {};
|
|
190
|
+
// A component taking none gets children only from untyped code: its host has no slot for them.
|
|
191
|
+
let refused = false;
|
|
192
|
+
const childrenOf = (props) => {
|
|
193
|
+
if (config.children) return [props.children];
|
|
194
|
+
|
|
195
|
+
if (props.children !== undefined && !refused) {
|
|
196
|
+
refused = true;
|
|
197
|
+
console.error(
|
|
198
|
+
`Lucent: ${config.displayName} takes no children (its props declare no \`children: Children\`): they are not rendered.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return [];
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
function LucentComponent({ ref, ...props }) {
|
|
206
|
+
const host = React.useRef(null);
|
|
207
|
+
// The ids of this mount's unanswered requests.
|
|
208
|
+
const [requests] = React.useState(() => new Set());
|
|
209
|
+
|
|
210
|
+
React.useImperativeHandle(ref, () => commandsOf(config, host, requests, native, ReactNative), [
|
|
211
|
+
requests,
|
|
212
|
+
]);
|
|
213
|
+
React.useEffect(() => () => abandon(config, requests), [requests]);
|
|
214
|
+
|
|
215
|
+
return React.createElement(
|
|
216
|
+
Host,
|
|
217
|
+
{ ...hostProps(config, props), ref: host },
|
|
218
|
+
...childrenOf(props),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
LucentComponent.displayName = config.displayName;
|
|
223
|
+
return LucentComponent;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { lucentComponent, settleRequest };
|
|
@@ -48,4 +48,7 @@ target_compile_options(lucentnative PRIVATE
|
|
|
48
48
|
$<$<COMPILE_LANGUAGE:CXX>:-Wno-comma>
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
-
target_link_libraries(lucentnative jsi reactnative fbjni log lucentregexp)
|
|
51
|
+
target_link_libraries(lucentnative jsi reactnative fbjni log android lucentregexp)
|
|
52
|
+
|
|
53
|
+
# Lucent packages' C and C++ sources, when any (written by lucent build).
|
|
54
|
+
include(${CMAKE_CURRENT_LIST_DIR}/packages.cmake OPTIONAL)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Generated by Lucent. Do not edit.
|
|
2
|
+
// The rules LucentComposition follows, in plain Kotlin: when a component's
|
|
3
|
+
// composition starts and ends, and which owners it takes.
|
|
4
|
+
package dev.lucent.compose
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* When a component's composition starts and ends: at its view's first
|
|
8
|
+
* attach, and with its mount, once. A detach keeps it. A mount that ends
|
|
9
|
+
* before its view is ever attached (a view recycled before it showed)
|
|
10
|
+
* never composes.
|
|
11
|
+
*/
|
|
12
|
+
internal class CompositionTurns {
|
|
13
|
+
private enum class Turn {
|
|
14
|
+
NEW,
|
|
15
|
+
COMPOSED,
|
|
16
|
+
ENDED,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private var turn = Turn.NEW
|
|
20
|
+
|
|
21
|
+
/** The view is attached: whether its composition starts now. */
|
|
22
|
+
fun attached(): Boolean {
|
|
23
|
+
if (turn != Turn.NEW) return false
|
|
24
|
+
|
|
25
|
+
turn = Turn.COMPOSED
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The mount ended: whether a composition started, which ends now. */
|
|
30
|
+
fun ended(): Boolean {
|
|
31
|
+
val composed = turn == Turn.COMPOSED
|
|
32
|
+
|
|
33
|
+
turn = Turn.ENDED
|
|
34
|
+
return composed
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The owner of kind `type` a composition takes: its view tree's, else the
|
|
40
|
+
* first of `contexts` (the context its view was made with, then each one
|
|
41
|
+
* that wraps: the host's Activity among them) that is one; null if none is.
|
|
42
|
+
*/
|
|
43
|
+
internal fun <T : Any> firstOwner(type: Class<T>, viewTree: T?, contexts: Sequence<Any>): T? =
|
|
44
|
+
viewTree ?: contexts.filterIsInstance(type).firstOrNull()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Generated by Lucent. Do not edit.
|
|
2
|
+
package dev.lucent.compose
|
|
3
|
+
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.ContextWrapper
|
|
6
|
+
import android.view.View
|
|
7
|
+
import androidx.compose.foundation.layout.Box
|
|
8
|
+
import androidx.compose.foundation.layout.wrapContentSize
|
|
9
|
+
import androidx.compose.runtime.Composable
|
|
10
|
+
import androidx.compose.runtime.CompositionLocalProvider
|
|
11
|
+
import androidx.compose.runtime.remember
|
|
12
|
+
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
|
|
13
|
+
import androidx.compose.runtime.saveable.SaveableStateRegistry
|
|
14
|
+
import androidx.compose.ui.Alignment
|
|
15
|
+
import androidx.compose.ui.Modifier
|
|
16
|
+
import androidx.compose.ui.layout.onSizeChanged
|
|
17
|
+
import androidx.compose.ui.platform.ComposeView
|
|
18
|
+
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
19
|
+
import androidx.lifecycle.LifecycleOwner
|
|
20
|
+
import androidx.lifecycle.ViewModelStoreOwner
|
|
21
|
+
import androidx.lifecycle.findViewTreeLifecycleOwner
|
|
22
|
+
import androidx.lifecycle.findViewTreeViewModelStoreOwner
|
|
23
|
+
import androidx.lifecycle.setViewTreeLifecycleOwner
|
|
24
|
+
import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
|
25
|
+
import androidx.savedstate.SavedStateRegistryOwner
|
|
26
|
+
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
|
|
27
|
+
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A component's Compose content in the ComposeView its setup returns: the
|
|
31
|
+
* host of every body Lucent generates (`<registration>Host.create` gives
|
|
32
|
+
* it the body's composable). The C++ holder (lucent/platform/compose.h)
|
|
33
|
+
* makes it with the context of the view hosting the mount, the Activity's
|
|
34
|
+
* as React Native themes its views, and ends it with the mount.
|
|
35
|
+
*
|
|
36
|
+
* - The composition starts at the view's first attach (CompositionRules.kt),
|
|
37
|
+
* with the owners of its view tree. A window whose tree lacks one (a
|
|
38
|
+
* plain dialog's, an overlay's) gets the host Activity's, on its root.
|
|
39
|
+
* - A detach keeps it. The mount's end disposes it, and so does the
|
|
40
|
+
* destruction of the view tree's lifecycle, for a host that ends a
|
|
41
|
+
* window without dropping its views.
|
|
42
|
+
* - The content lays out at its own size. When that size changes, the view
|
|
43
|
+
* asks for a layout, and the component's host measures it again.
|
|
44
|
+
* - rememberSaveable keeps a value as long as the mount: nothing is saved
|
|
45
|
+
* for another mount, or restored from one.
|
|
46
|
+
* - Once the mount ends, the view keeps nothing of the body.
|
|
47
|
+
*/
|
|
48
|
+
class LucentComposition(context: Context, content: @Composable () -> Unit) :
|
|
49
|
+
View.OnAttachStateChangeListener {
|
|
50
|
+
private val composeView = ComposeView(context)
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The view showing the content: the value of the setup's `compose()`.
|
|
54
|
+
* A View to JNI, which finds it by a descriptor a shrinker leaves alone.
|
|
55
|
+
*/
|
|
56
|
+
val view: View
|
|
57
|
+
get() = composeView
|
|
58
|
+
|
|
59
|
+
/** The body, until the mount ends (see dispose). */
|
|
60
|
+
private var content: (@Composable () -> Unit)? = content
|
|
61
|
+
|
|
62
|
+
private val turns = CompositionTurns()
|
|
63
|
+
|
|
64
|
+
init {
|
|
65
|
+
composeView.addOnAttachStateChangeListener(this)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override fun onViewAttachedToWindow(v: View) {
|
|
69
|
+
if (!turns.attached()) return
|
|
70
|
+
|
|
71
|
+
composeView.removeOnAttachStateChangeListener(this)
|
|
72
|
+
|
|
73
|
+
// Before composing: the window's recomposer takes the lifecycle it finds from the root.
|
|
74
|
+
val contexts = generateSequence<Any>(composeView.context) { (it as? ContextWrapper)?.baseContext }
|
|
75
|
+
|
|
76
|
+
for (kind in OWNERS) kind.ensure(composeView, contexts)
|
|
77
|
+
|
|
78
|
+
composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
|
79
|
+
composeView.setContent { Hosted() }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override fun onViewDetachedFromWindow(v: View) = Unit
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The mount ended: its composition ends, or never starts, and the view
|
|
86
|
+
* lets go of the body. A disposed composition leaves its layout nodes,
|
|
87
|
+
* whose modifiers hold the body's callbacks, and through them the
|
|
88
|
+
* holder's actions: the setup's functions, whose captures may hold this
|
|
89
|
+
* view by a JNI global reference, a cycle the garbage collector cannot
|
|
90
|
+
* see.
|
|
91
|
+
*/
|
|
92
|
+
fun dispose() {
|
|
93
|
+
composeView.removeOnAttachStateChangeListener(this)
|
|
94
|
+
|
|
95
|
+
if (turns.ended()) composeView.disposeComposition()
|
|
96
|
+
|
|
97
|
+
composeView.removeAllViews()
|
|
98
|
+
content = null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Composable
|
|
102
|
+
private fun Hosted() {
|
|
103
|
+
val saveable = remember { SaveableStateRegistry(null) { true } }
|
|
104
|
+
|
|
105
|
+
CompositionLocalProvider(LocalSaveableStateRegistry provides saveable) {
|
|
106
|
+
Box(
|
|
107
|
+
Modifier.wrapContentSize(Alignment.TopStart, unbounded = true).onSizeChanged {
|
|
108
|
+
composeView.requestLayout()
|
|
109
|
+
}
|
|
110
|
+
) {
|
|
111
|
+
content?.invoke()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** An owner Compose finds in its view tree, and whether it cannot compose without one. */
|
|
118
|
+
private class OwnerKind<T : Any>(
|
|
119
|
+
private val type: Class<T>,
|
|
120
|
+
private val find: (View) -> T?,
|
|
121
|
+
private val give: (View, T) -> Unit,
|
|
122
|
+
private val required: Boolean,
|
|
123
|
+
) {
|
|
124
|
+
/** Gives `view`'s window the first of `contexts` that is one if its tree has none. */
|
|
125
|
+
fun ensure(view: View, contexts: Sequence<Any>) {
|
|
126
|
+
val tree = find(view)
|
|
127
|
+
val owner = firstOwner(type, tree, contexts)
|
|
128
|
+
|
|
129
|
+
if (owner == null) {
|
|
130
|
+
check(!required) {
|
|
131
|
+
"Compose content needs a ${type.simpleName}: neither its window's view tree nor the " +
|
|
132
|
+
"Activity hosting it has one (host React Native in a ComponentActivity)"
|
|
133
|
+
}
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (owner !== tree) give(view.rootView, owner)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private val OWNERS =
|
|
142
|
+
listOf(
|
|
143
|
+
OwnerKind(
|
|
144
|
+
LifecycleOwner::class.java,
|
|
145
|
+
{ it.findViewTreeLifecycleOwner() },
|
|
146
|
+
{ v, o -> v.setViewTreeLifecycleOwner(o) },
|
|
147
|
+
required = true,
|
|
148
|
+
),
|
|
149
|
+
OwnerKind(
|
|
150
|
+
SavedStateRegistryOwner::class.java,
|
|
151
|
+
{ it.findViewTreeSavedStateRegistryOwner() },
|
|
152
|
+
{ v, o -> v.setViewTreeSavedStateRegistryOwner(o) },
|
|
153
|
+
required = true,
|
|
154
|
+
),
|
|
155
|
+
OwnerKind(
|
|
156
|
+
ViewModelStoreOwner::class.java,
|
|
157
|
+
{ it.findViewTreeViewModelStoreOwner() },
|
|
158
|
+
{ v, o -> v.setViewTreeViewModelStoreOwner(o) },
|
|
159
|
+
required = false,
|
|
160
|
+
),
|
|
161
|
+
)
|