embedded-react 0.1.1 → 0.2.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 CHANGED
@@ -1,238 +1,268 @@
1
- # embedded-react
2
-
3
- **React Native for embedded MCUs** — write JSX, run it on a microcontroller. This npm package is the
4
- **JavaScript layer**: the React-Native-style component API you import, the
5
- [`react-reconciler`](https://www.npmjs.com/package/react-reconciler) host config that drives the C engine
6
- at runtime (**Flow A**), and the JSX→C ahead-of-time compiler (**Flow B**, `aot/`).
7
-
8
- ```
9
- React → react-reconciler → host-config.js → NativeUI.* → er_scene.h (engine)
10
- ```
11
-
12
- > ### Part of a monorepo
13
- > This package is just the `bridges/quickjs/js` folder of the **embedded-react** project. The C rendering
14
- > engine, the hardware backends, the runnable examples, the demo apps, and the simulator all live in the
15
- > main repo — **https://github.com/TheMasterCoder007/embedded-react**. The engine itself is distributed
16
- > separately as C source (CMake `FetchContent`, the ESP-IDF Component Registry, and PlatformIO) — see the
17
- > repos **Install** section. Everything ships at one lockstep version (this package's version == the engine's).
18
- >
19
- > The end-to-end app workflow below (bundling, packing, the simulator, running on a board) assumes a **clone
20
- > of the repo** the CLIs operate on the repo's `demos/` and `examples/`. A standalone project scaffolder
21
- > (`create-embedded-react`) that works in your own directory is still to come (see Status). Until then,
22
- > `npm install embedded-react` gives you the importable component API + the AOT compiler module.
23
-
24
- ## What an app imports
25
-
26
- The package is the React Native analog — same idiom (hooks from `react`, everything else here):
27
-
28
- ```jsx
29
- import { useState } from 'react';
30
- import { View, Text, Pressable, StyleSheet, AppRegistry } from 'embedded-react';
31
-
32
- function App() { /* ... */ }
33
- AppRegistry.registerComponent('demo', () => App);
34
- ```
35
-
36
- `embedded-react` resolves as a Node **package self-reference** (`package.json` `name` + `exports`),
37
- so esbuild and Vitest find it with no aliases.
38
-
39
- ## Layout
40
-
41
- ```
42
- src/
43
- embedded-react/ the public package surface (what apps import)
44
- index.js barrel: components, StyleSheet, Platform, AppRegistry, Animated, Easing
45
- components.js host component tags (View, Text, … → ERNodeType)
46
- StyleSheet.js create() / flatten()
47
- Platform.js { OS: 'embedded', select }
48
- AppRegistry.js registerComponent(...) mounts into a screen-sized root
49
- Animated.js Value / timing / spring / decay / View|Text|Image / interpolate
50
- Easing.js easing tokens (+ bezier) → engine curves
51
- split-style.js pure: split style into static props + animated bindings
52
- __tests__/ co-located UNIT tests for the pure surface
53
- host-config.js reconciler host config NativeUI.* (internal runtime)
54
- renderer.js createRoot(props).render(...); LegacyRoot (sync) (internal)
55
- props.js pure prop helpers (flattenStyle / buildProps / isEventProp)
56
- native-ui.js re-exports globalThis.NativeUI (installed by the C bridge)
57
- __tests__/ co-located UNIT tests (Vitest, *.unit.test.js, no engine)
58
- test/runtime/ e2e tests that need the real engine host
59
- *.runtime.test.jsx run inside QuickJS + engine via the headless harness
60
- harness.js check()/report() — records failures for the C runner
61
- run.mjs bundles each runtime test + runs er-bridge-quickjs-runtest
62
- assets/ build-time asset bakers (pure JS, no native deps) see "Assets" below
63
- rasterize.mjs glyph path → coverage bitmap (supersampled, nonzero winding)
64
- bake-font.mjs TTF/OTF → engine BitmapFont glyph data (opentype.js)
65
- bake-image.mjs PNG → premultiplied ARGB8888 (pngjs)
66
- emit-c.mjs assemble assets.generated.c + the built-in font_data.c
67
- build-builtin-font.mjs regenerate the engine's default Inter font (npm run build:builtin-font)
68
- build.mjs esbuild a demo's index.jsx → dist/app.bundle.js + bake its imported assets
69
- pack-container.mjs bundle + bytecode-compile + bake → dist/app.erpkg config container (npm run pack)
70
- assets/emit-container.mjs ERCF container writer (sections + QuickJS version stamp + CRC32)
71
- vitest.config.js unit test config
72
- ```
73
-
74
- The demo apps themselves live in the repo's top-level **`demos/`** folder (one folder per demo), *not*
75
- in this package this package is the library + reconciler + AOT compiler + tests. `build.mjs` bundles a
76
- selected demo and resolves its `'embedded-react'` import to `src/embedded-react/index.js`. See
77
- [demos/ in the repo](https://github.com/TheMasterCoder007/embedded-react/tree/master/demos).
78
-
79
- The host config flattens RN `style` (+ nested arrays) into the flat prop bag, routes `on*`
80
- handlers to `setEvent`, and uses `shouldSetTextContent` so a flattenable `<Text>` subtree (a string,
81
- interpolation like `Hi {name}`, or nested `<Text>` runs) becomes the node's `text` + inline spans.
82
- `Animated`, `Easing`, and the web timer globals (`setTimeout` / `setInterval`) are all available;
83
- `useEffect` flushes via the host pump (BRIDGE.md §1.2, §1.4, §2).
84
-
85
- ## Build
86
-
87
- ```
88
- npm install
89
- npm run pack # default demo → dist/app.erpkg (deployable config: bytecode + assets + CRC)
90
- npm run pack -- marine-dash # pack a specific demo (demos/<name>) instead
91
- npm run build # lower-level: just bundle dist/app.bundle.js (+ bake assets.generated.c)
92
- npm run create -- my-app # scaffold a new app at demos/my-app (App.jsx + scripts); then `cd` + npm run sim
93
- ```
94
-
95
- `npm run pack` is the deployable artifact the desktop demo and the ESP32 both load — see **Config
96
- container** below. `npm run build` is the lower-level bundle step (used by the bytecode/asset tooling
97
- and by firmware that prefers to compile assets in); both bake the demo's imported images and fonts
98
- (see **Assets**).
99
-
100
- ## Assets (images and fonts)
101
-
102
- Asset handling is **import-driven** and fully build-time there is no runtime decoder or font
103
- rasterizer on the device, and no Python toolchain. An app just imports a file and uses the name it
104
- returns:
105
-
106
- ```jsx
107
- import logo from './assets/logo.png'; // the baked image NAME ("logo")
108
- import Inter from './assets/Inter.ttf'; // the baked font FAMILY ("Inter")
109
-
110
- <Image source={logo} style={{ width: 64, height: 64 }} />
111
- <Text style={{ fontFamily: Inter, fontSize: 18 }}>Hi</Text>
112
- ```
113
-
114
- The baker produces the assets two ways, from the same bytes: `npm run pack` packs them **into the
115
- config container** (`app.erpkg`), registered at load time this is what the desktop demo and ESP32
116
- use. `npm run build` also emits `dist/assets.generated.c` exposing **`er_register_assets()`**, for
117
- firmware that prefers to **compile assets into the image** and call it once at boot (`er_image_load` /
118
- `er_font_register`, both flash-resident, zero runtime RAM). Either way the asset name/family is the
119
- file's basename.
120
-
121
- - **Images:** PNG premultiplied ARGB8888 (`bake-image.mjs`, via `pngjs`).
122
- - **Fonts:** TTF/OTF pre-rasterized `BitmapFont` glyphs (`bake-font.mjs`, via `opentype.js` + a
123
- pure-JS rasterizer). The engine has no runtime rasterizer, so the baker rasterizes **exactly the
124
- literal `fontSize` values the bundle uses**. Computed/dynamic sizes snap to the nearest baked size
125
- at runtime — pin them in `assets.config.js` if needed.
126
-
127
- Optional per-demo overrides live in `demos/<demo>/assets.config.js`:
128
-
129
- ```js
130
- export default {
131
- fonts: {
132
- Inter: { sizes: [14, 18, 24], bpp: 4, glyphs: 'common' }, // bpp 1|2|4|8 (4 default); glyphs: 'ascii'|'common'|'minimal'|'greek'|[codepoints]
133
- },
134
- };
135
- ```
136
-
137
- The engine's **built-in default font** (`engine/font/font_data.c`, the Inter fallback used by any
138
- text without a custom `fontFamily`) is generated by the same baker — regenerate it with
139
- `npm run build:builtin-font` (then re-run the engine text tests, as glyph metrics shift slightly).
140
-
141
- ## Config container
142
-
143
- `npm run pack` wraps the app into one deployable file **`dist/app.erpkg`** (format `ERCF`):
144
-
145
- ```
146
- magic "ERCF" | format_version | crc32 | qjs_tag | sections[ bytecode, asset pack ]
147
- ```
148
-
149
- It bundles the demo, precompiles it to **QuickJS bytecode** (no parser/source shipped), bakes the
150
- imported assets into an ERPK pack, and wraps both with a **QuickJS version stamp** and an **integrity
151
- CRC32**. That one `.erpkg` is "the config": loaded by `er_runtime_load_container()` on the desktop, or
152
- flashed to a device's config partition. The loader verifies CRC + version (a config built for a
153
- different QuickJS is rejected, not run as garbage) and registers the assets before the app mounts. This
154
- is the firmware-vs-config split: the firmware (desktop exe / ESP32 image) ships once; the `.erpkg`
155
- ships and updates independently.
156
-
157
- > Two CRCs are different things: the container's internal CRC32 is embedded-react's own integrity
158
- > check (universal). A bootloader's transfer/flash CRC is a separate,
159
- > project-specific step layered on the `.erpkg` by your upload toolchain.
160
-
161
- The precompiler tool must be built once (`pack` looks for it in the usual build dirs, or set
162
- `ER_COMPILE_BIN`): `cmake -S bridges/quickjs -B bridges/quickjs/build && cmake --build
163
- bridges/quickjs/build --target er-bridge-quickjs-compile`.
164
-
165
- ## Run (desktop)
166
-
167
- After `npm run pack`, **rebuild the `embedded-react-desktop` target** — its build copies
168
- `dist/app.erpkg` into the "config slot" next to the executable, and the host loads it **by default**
169
- (no argument), exactly as the ESP32 loads its config from flash:
170
-
171
- ```
172
- examples/linux/build/embedded-react-desktop # runs the config in the slot
173
- examples/linux/build/embedded-react-desktop other.erpkg # or an explicit container / .qbc / .js path
174
- ```
175
-
176
- The firmware ships no app and no baked assets everything rides in the container. No config / a
177
- corrupt one shows an on-screen panel (no built-in fallback). The C host injects the globals the app
178
- expects: `NativeUI` (the bridge), `screen` (`{ width, height, scale }`), and `console`.
179
-
180
- > Iteration loop: edit `src/*` (library) or `demos/<name>/*` (app) `npm run pack` → rebuild
181
- > `embedded-react-desktop` (re-copies the container into the slot) → run. Or, for an instant
182
- > edit-save-see loop, use the **simulator** (`npm run sim`).
183
-
184
- ## Tests
185
-
186
- Two tiers, by what they need:
187
-
188
- ```
189
- npm test # unit: Vitest over src/**/__tests__/*.unit.test.js (pure JS, no engine)
190
- npm run test:runtime # e2e: bundles test/runtime/*.runtime.test.jsx, runs each in the headless
191
- # QuickJS+engine harness (no window) and checks the result
192
- npm run test:bytecode # same suite, but each bundle is precompiled to a .qbc bytecode blob and run
193
- # via the bytecode path (JS_ReadObject) proves the MCU load path
194
- ```
195
-
196
- The runtime tiers need the harness exe built once (no SDL); `test:bytecode` also needs the compiler:
197
-
198
- ```
199
- cmake --build bridges/quickjs/build --target er-bridge-quickjs-runtest er-bridge-quickjs-compile
200
- ```
201
-
202
- Pick the tier by what the code touches: pure marshalling/logic a co-located `*.unit.test.js`;
203
- anything that exercises the reconciler engine pipeline a `test/runtime/*.runtime.test.jsx`.
204
-
205
- ## Status & known gaps
206
-
207
- - **Render, state, keyed-list reorder, and Animated all work end-to-end.** `<App/>` mounts, taps
208
- re-render via `setState`, keyed reorder moves nodes (`insertBefore`/`appendChild`), and
209
- `Animated.View` runs **native-driver** animations in the engine (no per-frame JS). Covered by
210
- `test/runtime/reorder.runtime.test.jsx` and `animated.runtime.test.jsx`.
211
- - ✅ **Timers, Promises, and `useEffect` work.** `setTimeout`/`setInterval`/`clearTimeout`/
212
- `clearInterval` and the Promise job queue are serviced each frame by the host pump
213
- (`er_bridge_pump`, off the engine clock). React passive effects (`useEffect`) flush on the pump.
214
- Covered by `timers.runtime.test.js` and `effects.runtime.test.jsx`.
215
- - **Animated composition + completion.** `sequence`/`parallel`/`stagger`/`delay`/`loop` and
216
- `.start(({ finished }) => …)` all work — composition is pure JS over each child's start/stop, with
217
- completion wired through the engine's `on_complete`. Covered by `anim-compose.runtime.test.js`.
218
- - **Multi-child `<Text>` + nested spans.** Interpolation (`Hi {name}`) and nested styled
219
- `<Text>` runs both worka flattenable `<Text>` owns its subtree and renders as the node's text
220
- plus, when runs differ in style, an inline span array (`NativeUI.setTextSpans`, max 4). Covered by
221
- `text-spans` unit + runtime tests.
222
- - **LayoutAnimation.** `LayoutAnimation.configureNext(...)` before a layout-changing state update
223
- tweens every node whose computed rect moved on the next commit (in C — no per-frame JS). `Presets`
224
- / `create` / `Types` / `Properties` and the `easeInEaseOut`/`linear`/`spring` shorthands. Covered
225
- by `layout-animation` unit + `layout-anim.runtime.test.jsx`.
226
- - ✅ **Interpolate `extrapolate`.** `interpolate({ inputRange, outputRange, extrapolate })` supports
227
- `'extend'` (default) / `'clamp'` / `'identity'`, with per-end `extrapolateLeft`/`extrapolateRight`
228
- overrides. Math is engine-tested (`test_interpolate`); the bridge path by
229
- `interpolate-extrapolate.runtime.test.js`.
230
- - ✅ **Bytecode + assets + `useAnimatedValue`.** The build compiles the bundle to a `.qbc` bytecode
231
- blob (the MCU load path) and bakes imported images/fonts to flash-resident C; `useAnimatedValue` is
232
- exported. Runs end-to-end on the desktop host and on ESP32-S3 hardware.
233
- - ✅ **State survives hot reload** in the simulator, plain `useState` transparently persists across
234
- saves (a sim-only build transform rewrites it to a persisting helper; press R to reset). On a device
235
- it's just `useState`, so the same app code runs everywhere. `usePersistentState` is the underlying
236
- helper, also exported for explicit use. See
237
- [SIMULATOR.md in the repo](https://github.com/TheMasterCoder007/embedded-react/blob/master/SIMULATOR.md).
238
- - **`create-embedded-react` scaffold** the project-init CLI is still to come (§4).
1
+ # embedded-react
2
+
3
+ **React Native for embedded MCUs** — write JSX, run it on a microcontroller. This npm package is the
4
+ **JavaScript layer**: the React-Native-style component API you import, the
5
+ [`react-reconciler`](https://www.npmjs.com/package/react-reconciler) host config that drives the C engine
6
+ at runtime (**Flow A**), and the JSX→C ahead-of-time compiler (**Flow B**, `aot/`).
7
+
8
+ ```
9
+ React → react-reconciler → host-config.js → NativeUI.* → er_scene.h (engine)
10
+ ```
11
+
12
+ > ### Part of a monorepo
13
+ > This package is just the `bridges/quickjs/js` folder of the **embedded-react** project. The C rendering
14
+ > engine, the hardware backends, the runnable examples, the demo apps, and the simulator all live in the
15
+ > main repo — **https://github.com/TheMasterCoder007/embedded-react**. The engine itself is distributed
16
+ > separately as C source (CMake `FetchContent`, the ESP-IDF Component Registry, and PlatformIO) — see the
17
+ > repos **Install** section. Everything ships at one lockstep version (this package's version == the engine's).
18
+ >
19
+ > **`npx embedded-react dev` works in your own project** — it runs the WASM simulator on your app with hot
20
+ > reload, no clone, and no native toolchain (the simulator `.wasm` ships prebuilt in this package). The other
21
+ > end-to-end CLIs below (`npm run pack`/`build`/AOT, running on a board) still operate on the repo's `demos/`
22
+ > and `examples/`. To start a **fresh standalone project** in your own directory, use the scaffolder:
23
+ > `npm create embedded-react@latest my-app`.
24
+
25
+ ## What an app imports
26
+
27
+ The package is the React Native analog — same idiom (hooks from `react`, everything else here):
28
+
29
+ ```jsx
30
+ import { useState } from 'react';
31
+ import { View, Text, Pressable, StyleSheet, AppRegistry } from 'embedded-react';
32
+
33
+ function App() { /* ... */ }
34
+ AppRegistry.registerComponent('demo', () => App);
35
+ ```
36
+
37
+ `embedded-react` resolves as a Node **package self-reference** (`package.json` `name` + `exports`),
38
+ so esbuild and Vitest find it with no aliases.
39
+
40
+ ## Simulate — `npx embedded-react dev`
41
+
42
+ Run your app in a browser with hot reload — no native toolchain, no repo clone. The engine is compiled to
43
+ WebAssembly and ships **prebuilt** in this package; the CLI bundles your JSX, serves it, and re-loads on save
44
+ (your `useState` survives the reload).
45
+
46
+ ```bash
47
+ npx embedded-react dev # finds ./index.jsx, ./src/index.jsx, or package.json "main"
48
+ npx embedded-react dev app.jsx # or pass the entry explicitly
49
+ npx embedded-react dev --port 4000
50
+ ```
51
+
52
+ Open the printed URL. The canvas fills the viewport, so the browser's device toolbar drives the board size
53
+ (e.g., 240×320) — pixel-accurate to a hardware ARGB panel. Imported images/fonts are baked and hot-reload too.
54
+ A floating gear chip locks to a specific panel size and can wrap the screen in a **device frame** (bezel) for a
55
+ true-to-hardware preview. This is the same simulator as the repo's `tools/web-sim` (see
56
+ [tools/web-sim](https://github.com/TheMasterCoder007/embedded-react/blob/master/tools/web-sim/README.md)).
57
+
58
+ To **share** a UI, export a self-contained static playground — `index.html` + the prebuilt `.wasm` + your
59
+ bundled app that runs in any browser with no server, ready for GitHub Pages / Netlify / a docs' iframe:
60
+
61
+ ```bash
62
+ npx embedded-react export --out sim-export # then: npx serve sim-export (or deploy the folder anywhere)
63
+ ```
64
+
65
+ ## Layout
66
+
67
+ ```
68
+ src/
69
+ embedded-react/ the public package surface (what apps import)
70
+ index.js barrel: components, StyleSheet, Platform, AppRegistry, Animated, Easing
71
+ components.js host component tags (View, Text, … → ERNodeType)
72
+ StyleSheet.js create() / flatten()
73
+ Platform.js { OS: 'embedded', select }
74
+ AppRegistry.js registerComponent(...) mounts into a screen-sized root
75
+ Animated.js Value / timing / spring / decay / View|Text|Image / interpolate
76
+ Easing.js easing tokens (+ bezier) engine curves
77
+ split-style.js pure: split style into static props + animated bindings
78
+ __tests__/ co-located UNIT tests for the pure surface
79
+ host-config.js reconciler host config NativeUI.* (internal runtime)
80
+ renderer.js createRoot(props).render(...); LegacyRoot (sync) (internal)
81
+ props.js pure prop helpers (flattenStyle / buildProps / isEventProp)
82
+ native-ui.js re-exports globalThis.NativeUI (installed by the C bridge)
83
+ __tests__/ co-located UNIT tests (Vitest, *.unit.test.js, no engine)
84
+ test/runtime/ e2e tests that need the real engine host
85
+ *.runtime.test.jsx run inside QuickJS + engine via the headless harness
86
+ harness.js check()/report() — records failures for the C runner
87
+ run.mjs bundles each runtime test + runs er-bridge-quickjs-runtest
88
+ assets/ build-time asset bakers (pure JS, no native deps) — see "Assets" below
89
+ rasterize.mjs glyph path coverage bitmap (supersampled, nonzero winding)
90
+ bake-font.mjs TTF/OTF engine BitmapFont glyph data (opentype.js)
91
+ bake-image.mjs PNG premultiplied ARGB8888 (pngjs)
92
+ emit-c.mjs assemble assets.generated.c + the built-in font_data.c
93
+ build-builtin-font.mjs regenerate the engine's default Inter font (npm run build:builtin-font)
94
+ build.mjs esbuild a demo's index.jsx → dist/app.bundle.js + bake its imported assets
95
+ pack-container.mjs bundle + bytecode-compile + bake dist/app.erpkg config container (npm run pack)
96
+ assets/emit-container.mjs ERCF container writer (sections + QuickJS version stamp + CRC32)
97
+ vitest.config.js unit test config
98
+ ```
99
+
100
+ The demo apps themselves live in the repo's top-level **`demos/`** folder (one folder per demo), *not*
101
+ in this package — this package is the library + reconciler + AOT compiler + tests. `build.mjs` bundles a
102
+ selected demo and resolves its `'embedded-react'` import to `src/embedded-react/index.js`. See
103
+ [demos/ in the repo](https://github.com/TheMasterCoder007/embedded-react/tree/master/demos).
104
+
105
+ The host config flattens RN `style` (+ nested arrays) into the flat prop bag, routes `on*`
106
+ handlers to `setEvent`, and uses `shouldSetTextContent` so a flattenable `<Text>` subtree (a string,
107
+ interpolation like `Hi {name}`, or nested `<Text>` runs) becomes the node's `text` + inline spans.
108
+ `Animated`, `Easing`, and the web timer globals (`setTimeout` / `setInterval`) are all available;
109
+ `useEffect` flushes via the host pump.
110
+
111
+ ## Build
112
+
113
+ ```
114
+ npm install
115
+ npm run pack # default demo dist/app.erpkg (deployable config: bytecode + assets + CRC)
116
+ npm run pack -- marine-dash # pack a specific demo (demos/<name>) instead
117
+ npm run build # lower-level: just bundle dist/app.bundle.js (+ bake assets.generated.c)
118
+ npm run create -- my-app # scaffold a new app at demos/my-app (App.jsx + scripts); then `cd` + npm run sim
119
+ ```
120
+
121
+ `npm run pack` is the deployable artifact the desktop demo and the ESP32 both load — see **Config
122
+ container** below. `npm run build` is the lower-level bundle step (used by the bytecode/asset tooling
123
+ and by firmware that prefers to compile assets in); both bake the demo's imported images and fonts
124
+ (see **Assets**).
125
+
126
+ ## Assets (images and fonts)
127
+
128
+ Asset handling is **import-driven** and fully build-time — there is no runtime decoder or font
129
+ rasterizer on the device, and no Python toolchain. An app just imports a file and uses the name it
130
+ returns:
131
+
132
+ ```jsx
133
+ import logo from './assets/logo.png'; // → the baked image NAME ("logo")
134
+ import Inter from './assets/Inter.ttf'; // → the baked font FAMILY ("Inter")
135
+
136
+ <Image source={logo} style={{ width: 64, height: 64 }} />
137
+ <Text style={{ fontFamily: Inter, fontSize: 18 }}>Hi</Text>
138
+ ```
139
+
140
+ The baker produces the assets two ways, from the same bytes: `npm run pack` packs them **into the
141
+ config container** (`app.erpkg`), registered at load time — this is what the desktop demo and ESP32
142
+ use. `npm run build` also emits `dist/assets.generated.c` exposing **`er_register_assets()`**, for
143
+ firmware that prefers to **compile assets into the image** and call it once at boot (`er_image_load` /
144
+ `er_font_register`, both flash-resident, zero runtime RAM). Either way the asset name/family is the
145
+ file's basename.
146
+
147
+ - **Images:** PNG → premultiplied ARGB8888 (`bake-image.mjs`, via `pngjs`).
148
+ - **Fonts:** TTF/OTF → pre-rasterized `BitmapFont` glyphs (`bake-font.mjs`, via `opentype.js` + a
149
+ pure-JS rasterizer). The engine has no runtime rasterizer, so the baker rasterizes **exactly the
150
+ literal `fontSize` values the bundle uses**. Computed/dynamic sizes snap to the nearest baked size
151
+ at runtime pin them in `assets.config.js` if needed.
152
+
153
+ Optional per-demo overrides live in `demos/<demo>/assets.config.js`:
154
+
155
+ ```js
156
+ export default {
157
+ fonts: {
158
+ Inter: { sizes: [14, 18, 24], bpp: 4, glyphs: 'common' }, // bpp 1|2|4|8 (4 default); glyphs: 'ascii'|'common'|'minimal'|'greek'|[codepoints]
159
+ },
160
+ };
161
+ ```
162
+
163
+ The engine's **built-in default font** (`engine/font/font_data.c`, the Inter fallback used by any
164
+ text without a custom `fontFamily`) is generated by the same baker — regenerate it with
165
+ `npm run build:builtin-font` (then re-run the engine text tests, as glyph metrics shift slightly).
166
+
167
+ ## Config container
168
+
169
+ `npm run pack` wraps the app into one deployable file — **`dist/app.erpkg`** (format `ERCF`):
170
+
171
+ ```
172
+ magic "ERCF" | format_version | crc32 | qjs_tag | sections[ bytecode, asset pack ]
173
+ ```
174
+
175
+ It bundles the demo, precompiles it to **QuickJS bytecode** (no parser/source shipped), bakes the
176
+ imported assets into an ERPK pack, and wraps both with a **QuickJS version stamp** and an **integrity
177
+ CRC32**. That one `.erpkg` is "the config": loaded by `er_runtime_load_container()` on the desktop, or
178
+ flashed to a device's config partition. The loader verifies CRC + version (a config built for a
179
+ different QuickJS is rejected, not run as garbage) and registers the assets before the app mounts. This
180
+ is the firmware-vs-config split: the firmware (desktop exe / ESP32 image) ships once; the `.erpkg`
181
+ ships and updates independently.
182
+
183
+ > Two CRCs are different things: the container's internal CRC32 is embedded-react's own integrity
184
+ > check (universal). A bootloader's transfer/flash CRC is a separate,
185
+ > project-specific step layered on the `.erpkg` by your upload toolchain.
186
+
187
+ The precompiler tool must be built once (`pack` looks for it in the usual build dirs, or set
188
+ `ER_COMPILE_BIN`): `cmake -S bridges/quickjs -B bridges/quickjs/build && cmake --build
189
+ bridges/quickjs/build --target er-bridge-quickjs-compile`.
190
+
191
+ ## Run (desktop)
192
+
193
+ After `npm run pack`, **rebuild the `embedded-react-desktop` target** its build copies
194
+ `dist/app.erpkg` into the "config slot" next to the executable, and the host loads it **by default**
195
+ (no argument), exactly as the ESP32 loads its config from flash:
196
+
197
+ ```
198
+ examples/linux/build/embedded-react-desktop # runs the config in the slot
199
+ examples/linux/build/embedded-react-desktop other.erpkg # or an explicit container / .qbc / .js path
200
+ ```
201
+
202
+ The firmware ships no app and no baked assets everything rides in the container. No config / a
203
+ corrupt one shows an on-screen panel (no built-in fallback). The C host injects the globals the app
204
+ expects: `NativeUI` (the bridge), `screen` (`{ width, height, scale }`), and `console`.
205
+
206
+ > Iteration loop: edit `src/*` (library) or `demos/<name>/*` (app) → `npm run pack` → rebuild
207
+ > `embedded-react-desktop` (re-copies the container into the slot) run. Or, for an instant
208
+ > edit-save-see loop, use the **simulator** (`npm run sim`).
209
+
210
+ ## Tests
211
+
212
+ Two tiers, by what they need:
213
+
214
+ ```
215
+ npm test # unit: Vitest over src/**/__tests__/*.unit.test.js (pure JS, no engine)
216
+ npm run test:runtime # e2e: bundles test/runtime/*.runtime.test.jsx, runs each in the headless
217
+ # QuickJS+engine harness (no window) and checks the result
218
+ npm run test:bytecode # same suite, but each bundle is precompiled to a .qbc bytecode blob and run
219
+ # via the bytecode path (JS_ReadObject) proves the MCU load path
220
+ ```
221
+
222
+ The runtime tiers need the harness exe built once (no SDL); `test:bytecode` also needs the compiler:
223
+
224
+ ```
225
+ cmake --build bridges/quickjs/build --target er-bridge-quickjs-runtest er-bridge-quickjs-compile
226
+ ```
227
+
228
+ Pick the tier by what the code touches: pure marshalling/logic → a co-located `*.unit.test.js`;
229
+ anything that exercises the reconciler → engine pipeline → a `test/runtime/*.runtime.test.jsx`.
230
+
231
+ ## Status & known gaps
232
+
233
+ - ✅ **Render, state, keyed-list reorder, and Animated all work end-to-end.** `<App/>` mounts, taps
234
+ re-render via `setState`, keyed reorder moves nodes (`insertBefore`/`appendChild`), and
235
+ `Animated.View` runs **native-driver** animations in the engine (no per-frame JS). Covered by
236
+ `test/runtime/reorder.runtime.test.jsx` and `animated.runtime.test.jsx`.
237
+ - **Timers, Promises, and `useEffect` work.** `setTimeout`/`setInterval`/`clearTimeout`/
238
+ `clearInterval` and the Promise job queue are serviced each frame by the host pump
239
+ (`er_bridge_pump`, off the engine clock). React passive effects (`useEffect`) flush on the pump.
240
+ Covered by `timers.runtime.test.js` and `effects.runtime.test.jsx`.
241
+ - ✅ **Animated composition + completion.** `sequence`/`parallel`/`stagger`/`delay`/`loop` and
242
+ `.start(({ finished }) => …)` all work — composition is pure JS over each child's start/stop, with
243
+ completion wired through the engine's `on_complete`. Covered by `anim-compose.runtime.test.js`.
244
+ - ✅ **Multi-child `<Text>` + nested spans.** Interpolation (`Hi {name}`) and nested styled
245
+ `<Text>` runs both work — a flattenable `<Text>` owns its subtree and renders as the node's text
246
+ plus, when runs differ in style, an inline span array (`NativeUI.setTextSpans`, max 4). Covered by
247
+ `text-spans` unit + runtime tests.
248
+ - ✅ **LayoutAnimation.** `LayoutAnimation.configureNext(...)` before a layout-changing state update
249
+ tweens every node whose computed rect moved on the next commit (in C — no per-frame JS). `Presets`
250
+ / `create` / `Types` / `Properties` and the `easeInEaseOut`/`linear`/`spring` shorthands. Covered
251
+ by `layout-animation` unit + `layout-anim.runtime.test.jsx`.
252
+ - ✅ **Interpolate `extrapolate`.** `interpolate({ inputRange, outputRange, extrapolate })` supports
253
+ `'extend'` (default) / `'clamp'` / `'identity'`, with per-end `extrapolateLeft`/`extrapolateRight`
254
+ overrides. Math is engine-tested (`test_interpolate`); the bridge path by
255
+ `interpolate-extrapolate.runtime.test.js`.
256
+ - ✅ **Bytecode + assets + `useAnimatedValue`.** The build compiles the bundle to a `.qbc` bytecode
257
+ blob (the MCU load path) and bakes imported images/fonts to flash-resident C; `useAnimatedValue` is
258
+ exported. Runs end-to-end on the desktop host and on ESP32-S3 hardware.
259
+ - ✅ **State survives hot reload** — in the simulator, plain `useState` transparently persists across
260
+ saves (a sim-only build transform rewrites it to a persisting helper; press R to reset). On a device
261
+ it's just `useState`, so the same app code runs everywhere. `usePersistentState` is the underlying
262
+ helper, also exported for explicit use. See
263
+ [tools/simulator in the repo](https://github.com/TheMasterCoder007/embedded-react/blob/master/tools/simulator/README.md).
264
+ - ✅ **`npx embedded-react dev`** — the WASM simulator runs your app in a browser with hot reload, from your
265
+ own project directory, with the engine `.wasm` shipped prebuilt (no Emscripten for consumers). See above.
266
+ - ✅ **`npm create embedded-react@latest my-app`** — scaffolds a fresh standalone project (a styled card with a
267
+ pulsing logo + a `count is N` button) wired for `npm run dev` (simulator) and `npm run export` (static
268
+ playground). Published as the `create-embedded-react` package.
package/aot/compile.mjs CHANGED
@@ -29,7 +29,7 @@
29
29
  // +-*/% , comparisons, ?:)
30
30
  //
31
31
  // The compiler tracks which nodes depend on which state, so a state change re-sets ONLY the dependent
32
- // nodes (er_node_set_props) — no diffing, no reconciler. See /PLAN.md Flow B.
32
+ // nodes (er_node_set_props) — no diffing, no reconciler. See the root README (Flow B).
33
33
  //
34
34
  // npm run aot # default demo (thermostat) — but use a minimal demo for the slice
35
35
  // npm run aot -- music-player # a specific demo by folder name
@@ -1591,7 +1591,7 @@ function compileHandler(fnNode, env, state, out) {
1591
1591
  // ---------------------------------------------------------------------------------------------------
1592
1592
  // Emit — control flow (components / conditionals / lists) all unroll at COMPILE TIME into fixed nodes.
1593
1593
  // Runtime-dynamic conditionals/lists (where the node COUNT changes with state) are not yet supported
1594
- // and throw a clear "AOT: ..." — see /PLAN.md Flow B.
1594
+ // and throw a clear "AOT: ..." — see the root README (Flow B).
1595
1595
  // ---------------------------------------------------------------------------------------------------
1596
1596
 
1597
1597
  /** Reads a component instance's props (attributes) as static values; dynamic props throw (for now). */