@lucent-lang/lucent 0.0.4 → 0.1.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 (152) hide show
  1. package/README.md +97 -0
  2. package/app.plugin.js +46 -10
  3. package/dist/{bench-CR3E7e2e.js → bench-B555m0wU.js} +10 -6
  4. package/dist/{build-CAIrRSYA.js → build-UZ5Zm5m6.js} +1 -1
  5. package/dist/{build-check-QozuNJm5.js → build-check-Dmf8_yiw.js} +28 -13
  6. package/dist/{check-1SCmYlqA.js → check-D-H9WLG4.js} +1 -1
  7. package/dist/cli.js +62 -13
  8. package/dist/{codes-CIuwXt1S.js → codes-D-6LxUqD.js} +163 -7
  9. package/dist/compiler.js +4 -4
  10. package/dist/{dashboard-B-NzjjaN.js → dashboard-C8iF5-6T.js} +2 -2
  11. package/dist/{dev-BYWMP7oC.js → dev-B3lSx5CF.js} +62 -23
  12. package/dist/{diagnostic-BM9sXmwl.js → diagnostic-BvwLZQS1.js} +3 -1
  13. package/dist/{doctor-C0PrfWT6.js → doctor-CTAX3euc.js} +1 -1
  14. package/dist/{doctor-DdR4JnVc.js → doctor-CrgOAV5L.js} +1 -1
  15. package/dist/{explain--vV1oaRl.js → explain-Byit07F-.js} +1 -1
  16. package/dist/{packages-HyskKIsY.js → packages-CSmKeaAh.js} +2 -33
  17. package/dist/pipeline-BBY0df_2.js +700 -0
  18. package/dist/project-BLhWwnns.js +342 -0
  19. package/dist/sdk-coverage-DFGiDOzR.js +113 -0
  20. package/dist/sdk-diff-DpPYWzrl.js +109 -0
  21. package/dist/sdk-lock-QOxEFD1o.js +38 -0
  22. package/dist/{sdk-prefetch-mo04JDZV.js → sdk-prefetch-DX6ztlPN.js} +2 -2
  23. package/dist/{sdk-search-Iv4K3-8a.js → sdk-search-BShBL1LL.js} +2 -2
  24. package/dist/{sdk-show-CkrdlqlP.js → sdk-show-B3bI-0M7.js} +5 -4
  25. package/dist/sdk-usage-ljX5bTuZ.js +95 -0
  26. package/dist/src-DlICbK91.js +39682 -0
  27. package/dist/trace-Bi4hkmPx.js +139 -0
  28. package/lib/core.cjs +368 -1
  29. package/lib/sdk/android.d.ts +54 -1
  30. package/lib/sdk/compose.d.ts +94 -0
  31. package/lib/sdk/compose.schemas.json.gz +0 -0
  32. package/lib/sdk/core.d.ts +201 -0
  33. package/lib/sdk/ios.d.ts +75 -2
  34. package/lib/sdk/ui.d.ts +129 -0
  35. package/metro/transformer.cjs +11 -2
  36. package/package.json +2 -1
  37. package/runtime/cpp/lucent/abort.cpp +59 -11
  38. package/runtime/cpp/lucent/abort.h +22 -6
  39. package/runtime/cpp/lucent/array.h +12 -2
  40. package/runtime/cpp/lucent/async.h +101 -21
  41. package/runtime/cpp/lucent/bigint.cpp +898 -0
  42. package/runtime/cpp/lucent/bigint.h +302 -0
  43. package/runtime/cpp/lucent/buffer.cpp +228 -0
  44. package/runtime/cpp/lucent/buffer.h +328 -0
  45. package/runtime/cpp/lucent/builtins.cpp +4 -1
  46. package/runtime/cpp/lucent/bytes.h +21 -2
  47. package/runtime/cpp/lucent/callback.cpp +29 -0
  48. package/runtime/cpp/lucent/callback.h +230 -0
  49. package/runtime/cpp/lucent/compute.cpp +370 -0
  50. package/runtime/cpp/lucent/compute.h +455 -0
  51. package/runtime/cpp/lucent/conversions.h +111 -0
  52. package/runtime/cpp/lucent/core.h +16 -5
  53. package/runtime/cpp/lucent/equality.h +107 -28
  54. package/runtime/cpp/lucent/execution.cpp +416 -0
  55. package/runtime/cpp/lucent/execution.h +270 -0
  56. package/runtime/cpp/lucent/extension.cpp +97 -0
  57. package/runtime/cpp/lucent/extension.h +210 -0
  58. package/runtime/cpp/lucent/helpers.h +12 -5
  59. package/runtime/cpp/lucent/items.h +52 -0
  60. package/runtime/cpp/lucent/jserror.cpp +9 -0
  61. package/runtime/cpp/lucent/jserror.h +11 -0
  62. package/runtime/cpp/lucent/jsi/convert.cpp +138 -0
  63. package/runtime/cpp/lucent/jsi/convert.h +128 -59
  64. package/runtime/cpp/lucent/jsi/host.cpp +213 -35
  65. package/runtime/cpp/lucent/jsi/host.h +116 -16
  66. package/runtime/cpp/lucent/json.h +19 -1
  67. package/runtime/cpp/lucent/lifecycle.cpp +260 -0
  68. package/runtime/cpp/lucent/lifecycle.h +124 -0
  69. package/runtime/cpp/lucent/lucent.h +7 -0
  70. package/runtime/cpp/lucent/map.h +41 -7
  71. package/runtime/cpp/lucent/native.cpp +35 -47
  72. package/runtime/cpp/lucent/native.h +116 -26
  73. package/runtime/cpp/lucent/number.cpp +38 -38
  74. package/runtime/cpp/lucent/number.h +33 -0
  75. package/runtime/cpp/lucent/operation.h +65 -0
  76. package/runtime/cpp/lucent/ops.h +14 -6
  77. package/runtime/cpp/lucent/platform/android.cpp +223 -10
  78. package/runtime/cpp/lucent/platform/android.h +227 -4
  79. package/runtime/cpp/lucent/platform/android_activity.cpp +258 -0
  80. package/runtime/cpp/lucent/platform/android_requests.h +140 -0
  81. package/runtime/cpp/lucent/platform/compose.h +256 -0
  82. package/runtime/cpp/lucent/platform/ios.h +325 -25
  83. package/runtime/cpp/lucent/platform/ios.mm +1 -14
  84. package/runtime/cpp/lucent/platform/ios_ui.h +192 -0
  85. package/runtime/cpp/lucent/platform/ios_ui.mm +454 -0
  86. package/runtime/cpp/lucent/platform/swiftui.h +190 -0
  87. package/runtime/cpp/lucent/presentation.cpp +11 -0
  88. package/runtime/cpp/lucent/presentation.h +109 -0
  89. package/runtime/cpp/lucent/reactive.cpp +406 -0
  90. package/runtime/cpp/lucent/reactive.h +566 -0
  91. package/runtime/cpp/lucent/report.cpp +40 -0
  92. package/runtime/cpp/lucent/report.h +16 -0
  93. package/runtime/cpp/lucent/resource.cpp +83 -0
  94. package/runtime/cpp/lucent/resource.h +75 -0
  95. package/runtime/cpp/lucent/scheduler.cpp +38 -124
  96. package/runtime/cpp/lucent/scheduler.h +102 -83
  97. package/runtime/cpp/lucent/scope.cpp +308 -0
  98. package/runtime/cpp/lucent/scope.h +452 -0
  99. package/runtime/cpp/lucent/sizing.h +263 -0
  100. package/runtime/cpp/lucent/slots.h +231 -0
  101. package/runtime/cpp/lucent/trace.cpp +539 -0
  102. package/runtime/cpp/lucent/trace.h +251 -0
  103. package/runtime/cpp/lucent/transport.cpp +9 -0
  104. package/runtime/cpp/lucent/transport.h +341 -0
  105. package/runtime/cpp/lucent/view.cpp +123 -0
  106. package/runtime/cpp/lucent/view.h +157 -0
  107. package/runtime/cpp/rn/LucentComponentView.h +121 -0
  108. package/runtime/cpp/rn/LucentComponentView.mm +479 -0
  109. package/runtime/cpp/rn/LucentModule.cpp +37 -7
  110. package/runtime/cpp/rn/LucentModule.h +3 -0
  111. package/runtime/cpp/rn/LucentViewChildren.h +80 -0
  112. package/runtime/cpp/rn/LucentViewChildren.mm +211 -0
  113. package/runtime/cpp/rn/LucentViewRegistry.h +62 -0
  114. package/runtime/cpp/rn/LucentViewRequests.cpp +91 -0
  115. package/runtime/cpp/rn/LucentViewRequests.h +104 -0
  116. package/runtime/cpp/rn/LucentViewSizing.h +345 -0
  117. package/runtime/cpp/rn/LucentViewSlots.h +242 -0
  118. package/runtime/cpp/rn/LucentViewValues.h +125 -0
  119. package/runtime/cpp/rn/LucentViews.h +440 -0
  120. package/runtime/cpp/rn/LucentViewsAndroid.cpp +439 -0
  121. package/runtime/cpp/rn/LucentViewsAndroid.h +138 -0
  122. package/runtime/cpp/third_party/dragonbox/LICENSE-Boost +23 -0
  123. package/runtime/cpp/third_party/dragonbox/README.md +16 -0
  124. package/runtime/cpp/third_party/dragonbox/dragonbox.h +4440 -0
  125. package/runtime/js/index.d.ts +11 -1
  126. package/runtime/js/index.js +84 -8
  127. package/runtime/js/views.d.ts +38 -0
  128. package/runtime/js/views.js +226 -0
  129. package/runtime/native/android/CMakeLists.txt +4 -1
  130. package/runtime/native/android/src/compose/java/dev/lucent/compose/CompositionRules.kt +44 -0
  131. package/runtime/native/android/src/compose/java/dev/lucent/compose/LucentComposition.kt +161 -0
  132. package/runtime/native/android/src/main/java/dev/lucent/LucentActivities.java +292 -0
  133. package/runtime/native/android/src/main/java/dev/lucent/LucentChildren.java +174 -0
  134. package/runtime/native/android/src/main/java/dev/lucent/LucentHostView.java +232 -0
  135. package/runtime/native/android/src/main/java/dev/lucent/LucentInitializer.java +46 -0
  136. package/runtime/native/android/src/main/java/dev/lucent/LucentPackage.java +4 -3
  137. package/runtime/native/android/src/main/java/dev/lucent/LucentRequestActivity.java +92 -0
  138. package/runtime/native/android/src/main/java/dev/lucent/LucentSlotView.java +102 -0
  139. package/runtime/native/android/src/main/java/dev/lucent/LucentViewManager.java +149 -0
  140. package/runtime/native/android/src/main/java/dev/lucent/LucentViewManagers.java +16 -0
  141. package/runtime/native/android/src/main/java/dev/lucent/LucentViews.java +44 -0
  142. package/runtime/test/jsi/harness.cpp +9 -0
  143. package/schemas/build.schema.json +96 -1
  144. package/schemas/check.schema.json +56 -1
  145. package/schemas/lucent.schema.json +495 -9
  146. package/schemas/sdk-diff.schema.json +70 -0
  147. package/schemas/sdk-lock.schema.json +67 -0
  148. package/ts-plugin/index.js +77 -4
  149. package/dist/pipeline-CH7-2cxu.js +0 -232
  150. package/dist/project-D8tEet0k.js +0 -150
  151. package/dist/sdk-coverage-BoW_3Efd.js +0 -64
  152. package/dist/src-BD7f1_aZ.js +0 -11493
@@ -1,7 +1,17 @@
1
- /** Returns the exports object of a compiled Lucent module. */
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;
@@ -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 m = getNative(registry)[name];
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
+ )