embedded-react 0.2.3 → 0.4.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/aot/compile.mjs +2407 -697
- package/aot/screenshot-smoke.mjs +34 -17
- package/aot/style-map.mjs +156 -80
- package/assets/bake-font.mjs +45 -21
- package/assets/bake-image.mjs +7 -5
- package/assets/bake-svg.mjs +563 -0
- package/assets/build-builtin-font.mjs +25 -12
- package/assets/emit-c.mjs +52 -20
- package/assets/emit-container.mjs +5 -3
- package/assets/emit-pack.mjs +8 -2
- package/assets/index.mjs +25 -16
- package/assets/rasterize.mjs +45 -11
- package/assets/svg-loader.mjs +81 -0
- package/build.mjs +43 -20
- package/cli.mjs +258 -20
- package/pack-container.mjs +84 -35
- package/package.json +9 -3
- package/persist-transform.mjs +23 -9
- package/qjsc-wasm.mjs +83 -0
- package/sim/embedded-react.cjs +2 -0
- package/sim/embedded-react.js +1 -1
- package/sim/embedded-react.wasm +0 -0
- package/sim-server.mjs +160 -48
- package/src/embedded-react/Animated.js +51 -36
- package/src/embedded-react/AppRegistry.js +4 -4
- package/src/embedded-react/Easing.js +1 -1
- package/src/embedded-react/LayoutAnimation.js +13 -6
- package/src/embedded-react/StyleSheet.js +1 -1
- package/src/embedded-react/imperative.js +19 -7
- package/src/embedded-react/index.js +8 -8
- package/src/embedded-react/layout-anim-config.js +13 -9
- package/src/embedded-react/split-style.js +6 -6
- package/src/embedded-react/svg-ops.js +369 -41
- package/src/embedded-react/usePersistentState.js +3 -3
- package/src/host-config.js +137 -18
- package/src/native-ui.js +3 -1
- package/src/props.js +22 -10
- package/src/renderer.js +3 -3
package/src/host-config.js
CHANGED
|
@@ -18,11 +18,21 @@
|
|
|
18
18
|
//
|
|
19
19
|
// Instances ARE the integer node handles returned by NativeUI.createNode(). We keep no JS-side
|
|
20
20
|
// wrapper objects — the engine owns the scene graph, the handle is the identity.
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
import {DefaultEventPriority} from 'react-reconciler/constants';
|
|
22
|
+
import {NativeUI} from './native-ui.js';
|
|
23
|
+
import {
|
|
24
|
+
buildProps,
|
|
25
|
+
buildTextSpans,
|
|
26
|
+
isEventProp,
|
|
27
|
+
isTextContent,
|
|
28
|
+
} from './props.js';
|
|
29
|
+
import {
|
|
30
|
+
flattenSvg,
|
|
31
|
+
warnVectorCaps,
|
|
32
|
+
scaleVectorArtifact,
|
|
33
|
+
encodeVectorGradients,
|
|
34
|
+
} from './embedded-react/svg-ops.js';
|
|
35
|
+
import {splitAnimatedStyle} from './embedded-react/split-style.js';
|
|
26
36
|
|
|
27
37
|
/**
|
|
28
38
|
* Applies a node's resolved props, binding any Animated.Value found in its `style` to the matching
|
|
@@ -32,8 +42,11 @@ import { splitAnimatedStyle } from './embedded-react/split-style.js';
|
|
|
32
42
|
* already stripped its bindings into a ref, so splitAnimatedStyle finds none here (no double bind).
|
|
33
43
|
*/
|
|
34
44
|
function applyProps(type, handle, props) {
|
|
35
|
-
const {
|
|
36
|
-
NativeUI.setProps(
|
|
45
|
+
const {staticStyle, bindings} = splitAnimatedStyle(props.style);
|
|
46
|
+
NativeUI.setProps(
|
|
47
|
+
handle,
|
|
48
|
+
buildProps(type, bindings.length ? {...props, style: staticStyle} : props),
|
|
49
|
+
);
|
|
37
50
|
for (const b of bindings) b.value.__bind(handle, b.prop);
|
|
38
51
|
}
|
|
39
52
|
|
|
@@ -46,15 +59,100 @@ function applyTextSpans(type, handle, props) {
|
|
|
46
59
|
if (type === 'Text') NativeUI.setTextSpans(handle, buildTextSpans(props));
|
|
47
60
|
}
|
|
48
61
|
|
|
62
|
+
/** Resolves an <Svg>'s render-box dimension from style/props, falling back to the source's intrinsic size. */
|
|
63
|
+
function svgBoxSize(props, dim, intrinsic) {
|
|
64
|
+
const s =
|
|
65
|
+
props.style && typeof props.style[dim] === 'number'
|
|
66
|
+
? props.style[dim]
|
|
67
|
+
: undefined;
|
|
68
|
+
const p = typeof props[dim] === 'number' ? props[dim] : undefined;
|
|
69
|
+
return s ?? p ?? intrinsic;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A `<Svg source={imported}>` whose imported .svg fell back to a RASTER image at build time (the SVG used
|
|
74
|
+
* features the vector baker can't represent). Returns the artifact, or null for a vector/declarative
|
|
75
|
+
* Svg. Such a Svg is rendered as an IMAGE node, not a vector node — `props.source.kind` is the discriminator
|
|
76
|
+
* (an imported artifact's kind is fixed at build time, so it never flips for a given element).
|
|
77
|
+
*/
|
|
78
|
+
function rasterSvgArtifact(type, props) {
|
|
79
|
+
const src = props && props.source;
|
|
80
|
+
return type === 'Svg' && src && src.kind === 'raster' ? src : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Maps a raster `<Svg source>` to equivalent `<Image>` props: the baked asset name + the resolved box size. */
|
|
84
|
+
function rasterImageProps(props, art) {
|
|
85
|
+
const width = svgBoxSize(props, 'width', art.width);
|
|
86
|
+
const height = svgBoxSize(props, 'height', art.height);
|
|
87
|
+
return {
|
|
88
|
+
...props,
|
|
89
|
+
source: art.name,
|
|
90
|
+
style: {...(props.style || {}), width, height},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
49
94
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
95
|
+
* Sets an <Svg>'s vector op-tape. Two sources, source prop wins:
|
|
96
|
+
* <Svg source={imported}> — an imported .svg's baked artifact ({kind:'vector', ops, paints, width,
|
|
97
|
+
* height}); we scale its op-tape from intrinsic px to the node's box.
|
|
98
|
+
* <Svg><Path/>...</Svg> — declarative children flattened here (the Svg owns its subtree; React does
|
|
99
|
+
* not mount the shape children, so we compile them on create + every update).
|
|
53
100
|
*/
|
|
54
101
|
function applyVectorOps(type, handle, props) {
|
|
55
102
|
if (type !== 'Svg') return;
|
|
56
|
-
|
|
57
|
-
|
|
103
|
+
let ops;
|
|
104
|
+
let paints;
|
|
105
|
+
let gradients;
|
|
106
|
+
const src = props.source;
|
|
107
|
+
if (src && src.kind === 'vector' && Array.isArray(src.ops)) {
|
|
108
|
+
({ops, paints, gradients} = scaleVectorArtifact(
|
|
109
|
+
src,
|
|
110
|
+
svgBoxSize(props, 'width', src.width),
|
|
111
|
+
svgBoxSize(props, 'height', src.height),
|
|
112
|
+
));
|
|
113
|
+
} else {
|
|
114
|
+
({ops, paints} = flattenSvg(props));
|
|
115
|
+
}
|
|
116
|
+
warnVectorCaps(
|
|
117
|
+
ops.length,
|
|
118
|
+
paints.length,
|
|
119
|
+
NativeUI.maxVectorOps,
|
|
120
|
+
NativeUI.maxVectorPaints,
|
|
121
|
+
gradients ? gradients.length : 0,
|
|
122
|
+
NativeUI.maxVectorGrads,
|
|
123
|
+
);
|
|
124
|
+
NativeUI.setVectorOps(
|
|
125
|
+
handle,
|
|
126
|
+
ops,
|
|
127
|
+
paints,
|
|
128
|
+
gradients && gradients.length
|
|
129
|
+
? encodeVectorGradients(gradients)
|
|
130
|
+
: undefined,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Decides whether a committed <Svg> update actually needs its op-tape re-uploaded.
|
|
136
|
+
*
|
|
137
|
+
* Re-marshaling a baked vector op-tape across the JS->C bridge every frame is expensive (it dominates an
|
|
138
|
+
* interactive drag on PSRAM-QuickJS), and it's pure waste when only the node's POSITION changed: a
|
|
139
|
+
* `<Svg source>` whose imported artifact and resolved box are unchanged renders identical geometry, and its
|
|
140
|
+
* on-screen movement is already handled by the layout props (left/top/width/height) via applyProps. So we
|
|
141
|
+
* re-upload only when the source artifact reference changes or the resolved box size changes. Declarative
|
|
142
|
+
* `<Svg><Path/></Svg>` has no `source` — its shapes live in props/children, which we can't cheaply diff
|
|
143
|
+
* here, so it always re-flattens (unchanged behavior).
|
|
144
|
+
*/
|
|
145
|
+
function vectorNeedsUpload(type, prevProps, nextProps) {
|
|
146
|
+
if (type !== 'Svg') return false;
|
|
147
|
+
if (!nextProps.source) return true; // declarative children: re-flatten as before
|
|
148
|
+
if (!prevProps || prevProps.source !== nextProps.source) return true;
|
|
149
|
+
const src = nextProps.source;
|
|
150
|
+
const iw = src && src.kind === 'vector' ? src.width : undefined;
|
|
151
|
+
const ih = src && src.kind === 'vector' ? src.height : undefined;
|
|
152
|
+
return (
|
|
153
|
+
svgBoxSize(prevProps, 'width', iw) !== svgBoxSize(nextProps, 'width', iw) ||
|
|
154
|
+
svgBoxSize(prevProps, 'height', ih) !== svgBoxSize(nextProps, 'height', ih)
|
|
155
|
+
);
|
|
58
156
|
}
|
|
59
157
|
|
|
60
158
|
/**
|
|
@@ -63,7 +161,10 @@ function applyVectorOps(type, handle, props) {
|
|
|
63
161
|
function applyEvents(handle, prevProps, nextProps) {
|
|
64
162
|
if (prevProps) {
|
|
65
163
|
for (const key in prevProps) {
|
|
66
|
-
if (
|
|
164
|
+
if (
|
|
165
|
+
isEventProp(key, prevProps[key]) &&
|
|
166
|
+
!(nextProps && isEventProp(key, nextProps[key]))
|
|
167
|
+
) {
|
|
67
168
|
NativeUI.setEvent(handle, key, null);
|
|
68
169
|
}
|
|
69
170
|
}
|
|
@@ -104,6 +205,14 @@ export const hostConfig = {
|
|
|
104
205
|
|
|
105
206
|
// --- Creation ---
|
|
106
207
|
createInstance(type, props) {
|
|
208
|
+
// A raster-fallback <Svg source> becomes a real Image node (the SVG was rasterized at build time).
|
|
209
|
+
const raster = rasterSvgArtifact(type, props);
|
|
210
|
+
if (raster) {
|
|
211
|
+
const handle = NativeUI.createNode('Image');
|
|
212
|
+
applyProps('Image', handle, rasterImageProps(props, raster));
|
|
213
|
+
applyEvents(handle, null, props);
|
|
214
|
+
return handle;
|
|
215
|
+
}
|
|
107
216
|
const handle = NativeUI.createNode(type);
|
|
108
217
|
applyProps(type, handle, props);
|
|
109
218
|
applyTextSpans(type, handle, props);
|
|
@@ -115,7 +224,7 @@ export const hostConfig = {
|
|
|
115
224
|
// Raw text is only legal inside <Text> (handled via shouldSetTextContent). This fallback
|
|
116
225
|
// wraps stray text in a Text node so it still renders rather than crashing.
|
|
117
226
|
const handle = NativeUI.createNode('Text');
|
|
118
|
-
NativeUI.setProps(handle, {
|
|
227
|
+
NativeUI.setProps(handle, {text: String(text)});
|
|
119
228
|
return handle;
|
|
120
229
|
},
|
|
121
230
|
appendInitialChild(parent, child) {
|
|
@@ -162,13 +271,21 @@ export const hostConfig = {
|
|
|
162
271
|
return true;
|
|
163
272
|
},
|
|
164
273
|
commitUpdate(instance, _payload, type, prevProps, nextProps) {
|
|
274
|
+
const raster = rasterSvgArtifact(type, nextProps);
|
|
275
|
+
if (raster) {
|
|
276
|
+
// The Svg instance is an Image node (raster fallback); re-apply as image props, never vector ops.
|
|
277
|
+
applyProps('Image', instance, rasterImageProps(nextProps, raster));
|
|
278
|
+
applyEvents(instance, prevProps, nextProps);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
165
281
|
applyProps(type, instance, nextProps);
|
|
166
282
|
applyTextSpans(type, instance, nextProps);
|
|
167
|
-
|
|
283
|
+
if (vectorNeedsUpload(type, prevProps, nextProps))
|
|
284
|
+
applyVectorOps(type, instance, nextProps);
|
|
168
285
|
applyEvents(instance, prevProps, nextProps);
|
|
169
286
|
},
|
|
170
287
|
commitTextUpdate(textInstance, _oldText, newText) {
|
|
171
|
-
NativeUI.setProps(textInstance, {
|
|
288
|
+
NativeUI.setProps(textInstance, {text: String(newText)});
|
|
172
289
|
},
|
|
173
290
|
|
|
174
291
|
// --- Misc required hooks (no-ops for our renderer) ---
|
|
@@ -188,9 +305,11 @@ export const hostConfig = {
|
|
|
188
305
|
|
|
189
306
|
// --- Scheduling ---
|
|
190
307
|
scheduleTimeout: (fn, delay) => setTimeout(fn, delay),
|
|
191
|
-
cancelTimeout:
|
|
308
|
+
cancelTimeout: id => clearTimeout(id),
|
|
192
309
|
supportsMicrotasks: true,
|
|
193
310
|
scheduleMicrotask:
|
|
194
|
-
typeof queueMicrotask === 'function'
|
|
311
|
+
typeof queueMicrotask === 'function'
|
|
312
|
+
? queueMicrotask
|
|
313
|
+
: fn => Promise.resolve().then(fn),
|
|
195
314
|
now: () => NativeUI.now(),
|
|
196
315
|
};
|
package/src/native-ui.js
CHANGED
|
@@ -20,5 +20,7 @@
|
|
|
20
20
|
export const NativeUI = globalThis.NativeUI;
|
|
21
21
|
|
|
22
22
|
if (!NativeUI) {
|
|
23
|
-
throw new Error(
|
|
23
|
+
throw new Error(
|
|
24
|
+
'NativeUI global is missing — the QuickJS bridge must be installed before the bundle runs.',
|
|
25
|
+
);
|
|
24
26
|
}
|
package/src/props.js
CHANGED
|
@@ -53,7 +53,14 @@ export function flattenStyleObj(style) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// Style fields the engine honors per inline text span (everything else inherits from the node).
|
|
56
|
-
const SPAN_STYLE_KEYS = [
|
|
56
|
+
const SPAN_STYLE_KEYS = [
|
|
57
|
+
'color',
|
|
58
|
+
'fontSize',
|
|
59
|
+
'fontWeight',
|
|
60
|
+
'fontStyle',
|
|
61
|
+
'textDecorationLine',
|
|
62
|
+
'letterSpacing',
|
|
63
|
+
];
|
|
57
64
|
|
|
58
65
|
/**
|
|
59
66
|
* True when a <Text>'s children can be flattened inline — i.e. every leaf is a string/number or a
|
|
@@ -81,7 +88,7 @@ export function flattenTextChildren(children, baseStyle) {
|
|
|
81
88
|
if (text === '') return;
|
|
82
89
|
const last = segments[segments.length - 1];
|
|
83
90
|
if (last && last.style === style) last.text += text;
|
|
84
|
-
else segments.push({
|
|
91
|
+
else segments.push({text, style});
|
|
85
92
|
};
|
|
86
93
|
const walk = (node, style) => {
|
|
87
94
|
if (node == null || node === false || node === true) return;
|
|
@@ -96,7 +103,9 @@ export function flattenTextChildren(children, baseStyle) {
|
|
|
96
103
|
if (node && node.props !== undefined) {
|
|
97
104
|
// Nested <Text>: merge its style over the inherited one (new object only when it has style,
|
|
98
105
|
// so an unstyled nested <Text> keeps the same reference and still coalesces with siblings).
|
|
99
|
-
const merged = node.props.style
|
|
106
|
+
const merged = node.props.style
|
|
107
|
+
? Object.assign({}, style, flattenStyleObj(node.props.style))
|
|
108
|
+
: style;
|
|
100
109
|
walk(node.props.children, merged);
|
|
101
110
|
}
|
|
102
111
|
};
|
|
@@ -113,9 +122,9 @@ export function buildTextSpans(props) {
|
|
|
113
122
|
if (!isTextContent(props.children)) return [];
|
|
114
123
|
const baseStyle = flattenStyleObj(props.style);
|
|
115
124
|
const segments = flattenTextChildren(props.children, baseStyle);
|
|
116
|
-
if (!segments.some(
|
|
117
|
-
return segments.map(
|
|
118
|
-
const span = {
|
|
125
|
+
if (!segments.some(s => s.style !== baseStyle)) return [];
|
|
126
|
+
return segments.map(s => {
|
|
127
|
+
const span = {text: s.text};
|
|
119
128
|
for (const k of SPAN_STYLE_KEYS) {
|
|
120
129
|
if (s.style && s.style[k] !== undefined) span[k] = s.style[k];
|
|
121
130
|
}
|
|
@@ -134,7 +143,8 @@ export function buildTextSpans(props) {
|
|
|
134
143
|
*/
|
|
135
144
|
export function resolveImageSource(source) {
|
|
136
145
|
if (typeof source === 'string') return source;
|
|
137
|
-
if (source && typeof source === 'object' && typeof source.uri === 'string')
|
|
146
|
+
if (source && typeof source === 'object' && typeof source.uri === 'string')
|
|
147
|
+
return source.uri;
|
|
138
148
|
return null;
|
|
139
149
|
}
|
|
140
150
|
|
|
@@ -148,8 +158,10 @@ export function buildProps(type, props) {
|
|
|
148
158
|
// vector node from them. Fold them into the resolved style (an explicit style width/height still wins)
|
|
149
159
|
// so Flow A matches the Flow B AOT, which reads svg.props.width/height directly (compile.mjs emitSvgBox).
|
|
150
160
|
if (type === 'Svg') {
|
|
151
|
-
if (flat.width === undefined && props.width !== undefined)
|
|
152
|
-
|
|
161
|
+
if (flat.width === undefined && props.width !== undefined)
|
|
162
|
+
flat.width = props.width;
|
|
163
|
+
if (flat.height === undefined && props.height !== undefined)
|
|
164
|
+
flat.height = props.height;
|
|
153
165
|
}
|
|
154
166
|
// <Image source={...}> resolves to the engine asset name unless imageName was set explicitly.
|
|
155
167
|
if (flat.imageName === undefined && props.source != null) {
|
|
@@ -162,7 +174,7 @@ export function buildProps(type, props) {
|
|
|
162
174
|
if (type === 'Text' && isTextContent(props.children)) {
|
|
163
175
|
const base = flattenStyleObj(props.style);
|
|
164
176
|
flat.text = flattenTextChildren(props.children, base)
|
|
165
|
-
.map(
|
|
177
|
+
.map(s => s.text)
|
|
166
178
|
.join('');
|
|
167
179
|
}
|
|
168
180
|
return flat;
|
package/src/renderer.js
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
// We use LegacyRoot (synchronous) mode so the initial render flushes without depending on the
|
|
21
21
|
// async scheduler/timers — the host's frame loop then paints whatever the tree became.
|
|
22
22
|
import Reconciler from 'react-reconciler';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
23
|
+
import {hostConfig} from './host-config.js';
|
|
24
|
+
import {NativeUI} from './native-ui.js';
|
|
25
25
|
|
|
26
26
|
const reconciler = Reconciler(hostConfig);
|
|
27
27
|
|
|
@@ -45,7 +45,7 @@ export function createRoot(containerProps) {
|
|
|
45
45
|
false, // isStrictMode
|
|
46
46
|
null, // concurrentUpdatesByDefaultOverride
|
|
47
47
|
'', // identifierPrefix
|
|
48
|
-
|
|
48
|
+
error => console.error('react recoverable error:', error),
|
|
49
49
|
null, // transitionCallbacks
|
|
50
50
|
);
|
|
51
51
|
|