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/cli.mjs CHANGED
@@ -23,13 +23,26 @@
23
23
  // embedded-react.wasm host page, and hot-reloads on save (useState preserved). No native toolchain — the
24
24
  // .wasm ships prebuilt in this package. See tools/web-sim/README.md.
25
25
 
26
- import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
27
- import { fileURLToPath } from 'node:url';
28
- import { dirname, relative, resolve } from 'node:path';
29
- import { tmpdir } from 'node:os';
30
- import { buildApp, runDevServer } from './sim-server.mjs';
26
+ import {
27
+ copyFileSync,
28
+ existsSync,
29
+ mkdirSync,
30
+ readFileSync,
31
+ writeFileSync,
32
+ } from 'node:fs';
33
+ import {createRequire} from 'node:module';
34
+ import {fileURLToPath, pathToFileURL} from 'node:url';
35
+ import {basename, dirname, relative, resolve} from 'node:path';
36
+ import {tmpdir} from 'node:os';
37
+ import {buildApp, runDevServer} from './sim-server.mjs';
31
38
 
32
39
  const PKG_ROOT = dirname(fileURLToPath(import.meta.url));
40
+ const require = createRequire(import.meta.url);
41
+
42
+ // QuickJS release the bytecode container targets — MUST match bridges/quickjs/CMakeLists.txt and
43
+ // ER_QUICKJS_TAG in er_runtime.c (the device loader rejects a mismatch). The wasm that compiles the
44
+ // bytecode embeds this same QuickJS.
45
+ const QJS_TAG = 'v0.15.0';
33
46
 
34
47
  function usage() {
35
48
  console.log(`embedded-react — React Native for embedded MCUs
@@ -37,8 +50,14 @@ function usage() {
37
50
  Usage:
38
51
  embedded-react dev [entry] [--port <n>] Run the WASM simulator with hot reload
39
52
  embedded-react export [entry] [--out <dir>] Build a self-contained static playground (no server)
53
+ embedded-react build [entry] [--out <dir>] Build the device artifact:
54
+ default → app.erpkg (Flow A: QuickJS bytecode + assets,
55
+ upload to the device's config region)
56
+ --aot → app.gen.c/.h + assets.generated.c (Flow B:
57
+ no QuickJS, compiled into firmware)
40
58
 
41
- entry App entry file. Defaults to ./index.jsx, ./src/index.jsx, or package.json "main".
59
+ entry dev/export/build use ./index.jsx, ./src/index.jsx, or package.json "main";
60
+ build --aot uses ./App.jsx or ./src/App.jsx. Pass one to override.
42
61
  `);
43
62
  }
44
63
 
@@ -46,15 +65,22 @@ Usage:
46
65
  function simDirOrExit() {
47
66
  const simDir = resolve(PKG_ROOT, 'sim');
48
67
  if (!existsSync(resolve(simDir, 'embedded-react.wasm'))) {
49
- console.error('The prebuilt simulator (sim/embedded-react.wasm) is missing from this install.');
50
- console.error('A published embedded-react package ships it; building from source needs the Emscripten SDK.');
68
+ console.error(
69
+ 'The prebuilt simulator (sim/embedded-react.wasm) is missing from this install.',
70
+ );
71
+ console.error(
72
+ 'A published embedded-react package ships it; building from source needs the Emscripten SDK.',
73
+ );
51
74
  process.exit(1);
52
75
  }
53
76
  return simDir;
54
77
  }
55
78
 
56
79
  const libSrc = () => resolve(PKG_ROOT, 'src/embedded-react/index.js');
57
- const nodePaths = (cwd) => [resolve(PKG_ROOT, 'node_modules'), resolve(cwd, 'node_modules')];
80
+ const nodePaths = cwd => [
81
+ resolve(PKG_ROOT, 'node_modules'),
82
+ resolve(cwd, 'node_modules'),
83
+ ];
58
84
 
59
85
  /** Resolve the app entry: an explicit arg, else common conventions, else package.json "main"/"source". */
60
86
  function resolveEntry(cwd, explicit) {
@@ -66,7 +92,14 @@ function resolveEntry(cwd, explicit) {
66
92
  }
67
93
  return p;
68
94
  }
69
- for (const rel of ['index.jsx', 'src/index.jsx', 'index.tsx', 'src/index.tsx', 'App.jsx', 'src/App.jsx']) {
95
+ for (const rel of [
96
+ 'index.jsx',
97
+ 'src/index.jsx',
98
+ 'index.tsx',
99
+ 'src/index.tsx',
100
+ 'App.jsx',
101
+ 'src/App.jsx',
102
+ ]) {
70
103
  const p = resolve(cwd, rel);
71
104
  if (existsSync(p)) return p;
72
105
  }
@@ -84,8 +117,12 @@ function resolveEntry(cwd, explicit) {
84
117
  /* ignore */
85
118
  }
86
119
  }
87
- console.error('No app entry found. Pass one explicitly: embedded-react dev <entry.jsx>');
88
- console.error('(looked for ./index.jsx, ./src/index.jsx, and package.json "main")');
120
+ console.error(
121
+ 'No app entry found. Pass one explicitly: embedded-react dev <entry.jsx>',
122
+ );
123
+ console.error(
124
+ '(looked for ./index.jsx, ./src/index.jsx, … and package.json "main")',
125
+ );
89
126
  process.exit(1);
90
127
  }
91
128
 
@@ -93,12 +130,14 @@ async function dev(args) {
93
130
  const cwd = process.cwd();
94
131
  const portIdx = args.indexOf('--port');
95
132
  const port = portIdx >= 0 ? parseInt(args[portIdx + 1], 10) : 3333;
96
- const explicit = args.find((a, i) => !a.startsWith('--') && (portIdx < 0 || i !== portIdx + 1));
133
+ const explicit = args.find(
134
+ (a, i) => !a.startsWith('--') && (portIdx < 0 || i !== portIdx + 1),
135
+ );
97
136
 
98
137
  const simDir = simDirOrExit();
99
138
  const entry = resolveEntry(cwd, explicit);
100
139
  const outDir = resolve(tmpdir(), 'embedded-react-sim');
101
- mkdirSync(outDir, { recursive: true });
140
+ mkdirSync(outDir, {recursive: true});
102
141
 
103
142
  await runDevServer({
104
143
  entry,
@@ -117,20 +156,217 @@ async function exportApp(args) {
117
156
  const cwd = process.cwd();
118
157
  const outIdx = args.indexOf('--out');
119
158
  const outDir = resolve(cwd, outIdx >= 0 ? args[outIdx + 1] : 'sim-export');
120
- const explicit = args.find((a, i) => !a.startsWith('--') && (outIdx < 0 || i !== outIdx + 1));
159
+ const explicit = args.find(
160
+ (a, i) => !a.startsWith('--') && (outIdx < 0 || i !== outIdx + 1),
161
+ );
121
162
 
122
163
  const simDir = simDirOrExit();
123
164
  const entry = resolveEntry(cwd, explicit);
124
165
  const pub = resolve(outDir, 'public');
125
- mkdirSync(pub, { recursive: true });
166
+ mkdirSync(pub, {recursive: true});
126
167
 
127
168
  // Bundle the app + bake assets into public/, then drop the prebuilt module + host page alongside.
128
- await buildApp({ entry, projectRoot: cwd, libSrc: libSrc(), nodePaths: nodePaths(cwd), outDir: pub });
129
- for (const f of ['embedded-react.js', 'embedded-react.wasm']) copyFileSync(resolve(simDir, f), resolve(pub, f));
169
+ await buildApp({
170
+ entry,
171
+ projectRoot: cwd,
172
+ libSrc: libSrc(),
173
+ nodePaths: nodePaths(cwd),
174
+ outDir: pub,
175
+ });
176
+ for (const f of ['embedded-react.js', 'embedded-react.wasm'])
177
+ copyFileSync(resolve(simDir, f), resolve(pub, f));
130
178
  copyFileSync(resolve(simDir, 'index.html'), resolve(outDir, 'index.html'));
131
179
 
132
- console.log(`✓ exported a static playground → ${relative(cwd, outDir) || '.'}/`);
133
- console.log(` serve it over http (e.g. \`npx serve ${relative(cwd, outDir) || '.'}\`) or deploy the folder to any static host.`);
180
+ console.log(
181
+ `✓ exported a static playground ${relative(cwd, outDir) || '.'}/`,
182
+ );
183
+ console.log(
184
+ ` serve it over http (e.g. \`npx serve ${relative(cwd, outDir) || '.'}\`) or deploy the folder to any static host.`,
185
+ );
186
+ }
187
+
188
+ /** Resolve the root component file for the AOT compiler (App.jsx), distinct from the registry entry. */
189
+ function resolveAppComponent(cwd, explicit) {
190
+ if (explicit) {
191
+ const p = resolve(cwd, explicit);
192
+ if (!existsSync(p)) {
193
+ console.error(`Entry not found: ${p}`);
194
+ process.exit(1);
195
+ }
196
+ return p;
197
+ }
198
+ for (const rel of ['App.jsx', 'src/App.jsx', 'App.tsx', 'src/App.tsx']) {
199
+ const p = resolve(cwd, rel);
200
+ if (existsSync(p)) return p;
201
+ }
202
+ console.error(
203
+ 'No App component found. Pass one explicitly: embedded-react build --aot <App.jsx>',
204
+ );
205
+ process.exit(1);
206
+ }
207
+
208
+ /** Flow B (--aot): compile the App component to C (app.gen.{c,h}) + bake assets, to compile into firmware. */
209
+ async function buildAot(cwd, explicit, outDir) {
210
+ const {compileSource, bakeSvgArtifacts} = await import('./aot/compile.mjs');
211
+ const {bakeAssets} = await import('./assets/index.mjs');
212
+ const appPath = resolveAppComponent(cwd, explicit);
213
+ const appDir = dirname(appPath);
214
+ const src = readFileSync(appPath, 'utf8');
215
+ let result;
216
+ try {
217
+ // Bake <Svg source> .svg imports → vector artifacts (incl. gradients), then compile with them in hand.
218
+ const svgArtifacts = await bakeSvgArtifacts(src, appDir);
219
+ result = compileSource(src, 'app', {filename: appPath, svgArtifacts});
220
+ } catch (e) {
221
+ console.error(e && e.aotLoc ? e.message : e?.message || String(e));
222
+ process.exit(1);
223
+ }
224
+ writeFileSync(resolve(outDir, 'app.gen.c'), result.c);
225
+ writeFileSync(resolve(outDir, 'app.gen.h'), result.h);
226
+
227
+ const imageJobs = result.images.map(im => ({
228
+ name: im.name,
229
+ path: resolve(appDir, im.importPath),
230
+ }));
231
+ for (const j of imageJobs) {
232
+ if (!existsSync(j.path)) {
233
+ console.error(`<Image> asset "${j.name}" not found at ${j.path}`);
234
+ process.exit(1);
235
+ }
236
+ }
237
+ const baked = bakeAssets({images: imageJobs, fonts: [], outDir});
238
+ console.log(
239
+ `✓ Flow B (AOT) → ${relative(cwd, outDir) || '.'}/app.gen.c (+ app.gen.h, assets.generated.c — ${baked.images} image(s))`,
240
+ );
241
+ console.log(
242
+ ' No QuickJS on the device: compile these into your firmware against the engine (er_scene.h).',
243
+ );
244
+ }
245
+
246
+ /** Flow A (default): bundle → QuickJS bytecode (via the prebuilt wasm) + baked assets → app.erpkg. */
247
+ async function buildContainer(cwd, explicit, outDir) {
248
+ const esbuild = require('esbuild');
249
+ const {bakeImage} = await import('./assets/bake-image.mjs');
250
+ const {bakeFont} = await import('./assets/bake-font.mjs');
251
+ const {emitAssetPack} = await import('./assets/emit-pack.mjs');
252
+ const {emitContainer} = await import('./assets/emit-container.mjs');
253
+ const {registerSvgVectorLoader} = await import('./assets/svg-loader.mjs');
254
+ const {compileToBytecode} = await import('./qjsc-wasm.mjs');
255
+
256
+ const simDir = simDirOrExit();
257
+ const entry = resolveEntry(cwd, explicit);
258
+
259
+ // Bundle the app (IIFE) with import-driven asset discovery (same as the dev/pack paths).
260
+ const images = new Map();
261
+ const fonts = new Map();
262
+ const assetPlugin = {
263
+ name: 'embedded-react-assets',
264
+ setup(b) {
265
+ b.onLoad({filter: /\.(png|jpe?g|webp|gif|bmp)$/i}, a => {
266
+ const n = basename(a.path).replace(/\.[^.]+$/, '');
267
+ images.set(n, a.path);
268
+ return {
269
+ contents: `module.exports = ${JSON.stringify(n)};`,
270
+ loader: 'js',
271
+ };
272
+ });
273
+ b.onLoad({filter: /\.(ttf|otf)$/i}, a => {
274
+ const f = basename(a.path).replace(/\.[^.]+$/, '');
275
+ fonts.set(f, a.path);
276
+ return {
277
+ contents: `module.exports = ${JSON.stringify(f)};`,
278
+ loader: 'js',
279
+ };
280
+ });
281
+ registerSvgVectorLoader(b, (name, p) => images.set(name, p)); // raster-fallback SVGs join the image pack
282
+ },
283
+ };
284
+ // The bundle is an intermediate (it becomes bytecode in the .erpkg) — keep it out of the user's outDir
285
+ // so `dist/` ends up holding only app.erpkg.
286
+ const tmp = resolve(tmpdir(), 'embedded-react-build');
287
+ mkdirSync(tmp, {recursive: true});
288
+ const bundlePath = resolve(tmp, 'app.bundle.js');
289
+ await esbuild.build({
290
+ entryPoints: [entry],
291
+ bundle: true,
292
+ format: 'iife',
293
+ outfile: bundlePath,
294
+ platform: 'neutral',
295
+ target: 'es2020',
296
+ jsx: 'automatic',
297
+ alias: {'embedded-react': libSrc()},
298
+ nodePaths: nodePaths(cwd),
299
+ plugins: [assetPlugin],
300
+ define: {'process.env.NODE_ENV': '"production"'},
301
+ legalComments: 'none',
302
+ logLevel: 'silent',
303
+ });
304
+ const bundleSrc = readFileSync(bundlePath, 'utf8');
305
+
306
+ // Precompile to QuickJS bytecode through the prebuilt sim wasm — no native toolchain.
307
+ const bytecode = await compileToBytecode(bundleSrc, simDir);
308
+
309
+ // Bake imported images/fonts into an ERPK pack (font sizes discovered from the bundle).
310
+ const discoveredSizes = [
311
+ ...new Set(
312
+ [...bundleSrc.matchAll(/\bfontSize\s*:\s*(\d+(?:\.\d+)?)/g)].map(m =>
313
+ Math.round(Number(m[1])),
314
+ ),
315
+ ),
316
+ ].sort((a, b) => a - b);
317
+ let cfg = {};
318
+ const cp = resolve(cwd, 'assets.config.js');
319
+ if (existsSync(cp))
320
+ cfg = (await import(pathToFileURL(cp).href)).default || {};
321
+ const fontConfig = cfg.fonts || {};
322
+ const fontJobs = [...fonts.entries()].map(([family, path]) => {
323
+ const fc = fontConfig[family] || {};
324
+ return {
325
+ path,
326
+ family,
327
+ sizes: fc.sizes?.length
328
+ ? fc.sizes
329
+ : discoveredSizes.length
330
+ ? discoveredSizes
331
+ : [16],
332
+ bpp: fc.bpp ?? 4,
333
+ glyphs: fc.glyphs ?? 'ascii',
334
+ };
335
+ });
336
+ const imageJobs = [...images.entries()].map(([name, path]) => ({path, name}));
337
+ const bakedImages = imageJobs.map(bakeImage);
338
+ const bakedFonts = fontJobs.map(bakeFont);
339
+ const assetPack =
340
+ bakedImages.length || bakedFonts.length
341
+ ? emitAssetPack({images: bakedImages, fonts: bakedFonts})
342
+ : null;
343
+
344
+ const container = emitContainer({bytecode, assetPack, qjsTag: QJS_TAG});
345
+ const outPath = resolve(outDir, 'app.erpkg');
346
+ writeFileSync(outPath, container);
347
+ const kb = n => `${(n / 1024).toFixed(1)} KB`;
348
+ console.log(
349
+ `✓ Flow A → ${relative(cwd, outPath) || 'app.erpkg'} (${kb(container.length)}; qjs ${QJS_TAG}, bytecode ${kb(bytecode.length)}` +
350
+ (assetPack ? `, assets ${kb(assetPack.length)}` : '') +
351
+ ')',
352
+ );
353
+ console.log(
354
+ " Upload app.erpkg to your device's config region (er_runtime_load_container), or run it on the desktop host.",
355
+ );
356
+ }
357
+
358
+ /** `embedded-react build [--aot] [entry] [--out dir]` — produce the device artifact. */
359
+ async function build(args) {
360
+ const cwd = process.cwd();
361
+ const aot = args.includes('--aot');
362
+ const outIdx = args.indexOf('--out');
363
+ const outDir = resolve(cwd, outIdx >= 0 ? args[outIdx + 1] : 'dist');
364
+ const explicit = args.find(
365
+ (a, i) => !a.startsWith('--') && (outIdx < 0 || i !== outIdx + 1),
366
+ );
367
+ mkdirSync(outDir, {recursive: true});
368
+ if (aot) await buildAot(cwd, explicit, outDir);
369
+ else await buildContainer(cwd, explicit, outDir);
134
370
  }
135
371
 
136
372
  const [cmd, ...rest] = process.argv.slice(2);
@@ -138,6 +374,8 @@ if (cmd === 'dev') {
138
374
  await dev(rest);
139
375
  } else if (cmd === 'export') {
140
376
  await exportApp(rest);
377
+ } else if (cmd === 'build') {
378
+ await build(rest);
141
379
  } else if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
142
380
  usage();
143
381
  } else {
@@ -28,15 +28,22 @@
28
28
  //
29
29
  // Prereq: the bytecode precompiler (er-bridge-quickjs-compile) must be built. This script searches the
30
30
  // usual build dirs and tells you how to build it if it's missing (override with ER_COMPILE_BIN).
31
- import { build } from 'esbuild';
32
- import { spawnSync } from 'node:child_process';
33
- import { fileURLToPath, pathToFileURL } from 'node:url';
34
- import { dirname, resolve, basename } from 'node:path';
35
- import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
36
- import { bakeImage } from './assets/bake-image.mjs';
37
- import { bakeFont } from './assets/bake-font.mjs';
38
- import { emitAssetPack } from './assets/emit-pack.mjs';
39
- import { emitContainer } from './assets/emit-container.mjs';
31
+ import {build} from 'esbuild';
32
+ import {spawnSync} from 'node:child_process';
33
+ import {fileURLToPath, pathToFileURL} from 'node:url';
34
+ import {dirname, resolve, basename} from 'node:path';
35
+ import {
36
+ existsSync,
37
+ readdirSync,
38
+ readFileSync,
39
+ writeFileSync,
40
+ mkdirSync,
41
+ } from 'node:fs';
42
+ import {bakeImage} from './assets/bake-image.mjs';
43
+ import {bakeFont} from './assets/bake-font.mjs';
44
+ import {emitAssetPack} from './assets/emit-pack.mjs';
45
+ import {emitContainer} from './assets/emit-container.mjs';
46
+ import {registerSvgVectorLoader} from './assets/svg-loader.mjs';
40
47
 
41
48
  // QuickJS release the bytecode targets — MUST match the FetchContent pin in
42
49
  // bridges/quickjs/CMakeLists.txt and ER_QUICKJS_TAG in er_runtime.c. The loader rejects a mismatch.
@@ -54,7 +61,9 @@ const demoDir = resolve(demosDir, demo);
54
61
  const entry = resolve(demoDir, 'index.jsx');
55
62
  if (!existsSync(entry)) {
56
63
  const available = existsSync(demosDir)
57
- ? readdirSync(demosDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
64
+ ? readdirSync(demosDir, {withFileTypes: true})
65
+ .filter(d => d.isDirectory())
66
+ .map(d => d.name)
58
67
  : [];
59
68
  console.error(`Demo "${demo}" not found (expected ${entry}).`);
60
69
  console.error(`Available demos: ${available.join(', ') || '(none)'}`);
@@ -66,14 +75,32 @@ const exe = process.platform === 'win32' ? '.exe' : '';
66
75
  const compileBin =
67
76
  process.env.ER_COMPILE_BIN ||
68
77
  [
69
- resolve(repoRoot, 'bridges/quickjs/build', `er-bridge-quickjs-compile${exe}`),
70
- resolve(repoRoot, 'examples/linux/build/bridges/quickjs', `er-bridge-quickjs-compile${exe}`),
71
- resolve(repoRoot, 'tools/simulator/build/bridges/quickjs', `er-bridge-quickjs-compile${exe}`),
78
+ resolve(
79
+ repoRoot,
80
+ 'bridges/quickjs/build',
81
+ `er-bridge-quickjs-compile${exe}`,
82
+ ),
83
+ resolve(
84
+ repoRoot,
85
+ 'examples/linux/build/bridges/quickjs',
86
+ `er-bridge-quickjs-compile${exe}`,
87
+ ),
88
+ resolve(
89
+ repoRoot,
90
+ 'tools/simulator/build/bridges/quickjs',
91
+ `er-bridge-quickjs-compile${exe}`,
92
+ ),
72
93
  ].find(existsSync);
73
94
  if (!compileBin) {
74
- console.error('Bytecode precompiler (er-bridge-quickjs-compile) not found. Build it once:');
75
- console.error(' cmake -S bridges/quickjs -B bridges/quickjs/build -G "MinGW Makefiles"');
76
- console.error(' cmake --build bridges/quickjs/build --target er-bridge-quickjs-compile');
95
+ console.error(
96
+ 'Bytecode precompiler (er-bridge-quickjs-compile) not found. Build it once:',
97
+ );
98
+ console.error(
99
+ ' cmake -S bridges/quickjs -B bridges/quickjs/build -G "MinGW Makefiles"',
100
+ );
101
+ console.error(
102
+ ' cmake --build bridges/quickjs/build --target er-bridge-quickjs-compile',
103
+ );
77
104
  console.error('(or set ER_COMPILE_BIN to the binary path)');
78
105
  process.exit(1);
79
106
  }
@@ -84,20 +111,27 @@ const fonts = new Map(); // family -> path
84
111
  const assetPlugin = {
85
112
  name: 'embedded-react-assets',
86
113
  setup(b) {
87
- b.onLoad({ filter: /\.(png|jpe?g|webp|gif|bmp)$/i }, (args) => {
114
+ b.onLoad({filter: /\.(png|jpe?g|webp|gif|bmp)$/i}, args => {
88
115
  const name = basename(args.path).replace(/\.[^.]+$/, '');
89
116
  images.set(name, args.path);
90
- return { contents: `module.exports = ${JSON.stringify(name)};`, loader: 'js' };
117
+ return {
118
+ contents: `module.exports = ${JSON.stringify(name)};`,
119
+ loader: 'js',
120
+ };
91
121
  });
92
- b.onLoad({ filter: /\.(ttf|otf)$/i }, (args) => {
122
+ b.onLoad({filter: /\.(ttf|otf)$/i}, args => {
93
123
  const family = basename(args.path).replace(/\.[^.]+$/, '');
94
124
  fonts.set(family, args.path);
95
- return { contents: `module.exports = ${JSON.stringify(family)};`, loader: 'js' };
125
+ return {
126
+ contents: `module.exports = ${JSON.stringify(family)};`,
127
+ loader: 'js',
128
+ };
96
129
  });
130
+ registerSvgVectorLoader(b, (name, p) => images.set(name, p)); // raster-fallback SVGs join the image pack
97
131
  },
98
132
  };
99
133
 
100
- mkdirSync(distDir, { recursive: true });
134
+ mkdirSync(distDir, {recursive: true});
101
135
  const bundlePath = resolve(distDir, 'app.bundle.js');
102
136
  await build({
103
137
  entryPoints: [entry],
@@ -107,10 +141,10 @@ await build({
107
141
  platform: 'neutral',
108
142
  target: 'es2020',
109
143
  jsx: 'automatic',
110
- alias: { 'embedded-react': libEntry },
144
+ alias: {'embedded-react': libEntry},
111
145
  nodePaths: [nodeModules],
112
146
  plugins: [assetPlugin],
113
- define: { 'process.env.NODE_ENV': '"production"' },
147
+ define: {'process.env.NODE_ENV': '"production"'},
114
148
  legalComments: 'none',
115
149
  logLevel: 'info',
116
150
  });
@@ -118,7 +152,7 @@ console.log(`Bundled demo "${demo}" -> dist/app.bundle.js`);
118
152
 
119
153
  // --- Precompile to QuickJS bytecode -------------------------------------------------------------
120
154
  const qbcPath = resolve(distDir, 'app.bundle.qbc');
121
- const cc = spawnSync(compileBin, [bundlePath, qbcPath], { stdio: 'inherit' });
155
+ const cc = spawnSync(compileBin, [bundlePath, qbcPath], {stdio: 'inherit'});
122
156
  if (cc.status !== 0) {
123
157
  console.error('Bytecode compile failed.');
124
158
  process.exit(1);
@@ -128,34 +162,49 @@ const bytecode = readFileSync(qbcPath);
128
162
  // --- Bake imported assets into an ERPK pack (same sizing rules as build.mjs / sim.mjs) ----------
129
163
  const bundleSrc = readFileSync(bundlePath, 'utf8');
130
164
  const discoveredSizes = [
131
- ...new Set([...bundleSrc.matchAll(/\bfontSize\s*:\s*(\d+(?:\.\d+)?)/g)].map((m) => Math.round(Number(m[1])))),
165
+ ...new Set(
166
+ [...bundleSrc.matchAll(/\bfontSize\s*:\s*(\d+(?:\.\d+)?)/g)].map(m =>
167
+ Math.round(Number(m[1])),
168
+ ),
169
+ ),
132
170
  ].sort((a, b) => a - b);
133
171
 
134
172
  let config = {};
135
173
  const configPath = resolve(demoDir, 'assets.config.js');
136
- if (existsSync(configPath)) config = (await import(pathToFileURL(configPath).href)).default || {};
174
+ if (existsSync(configPath))
175
+ config = (await import(pathToFileURL(configPath).href)).default || {};
137
176
  const fontConfig = config.fonts || {};
138
177
 
139
178
  const fontJobs = [...fonts.entries()].map(([family, path]) => {
140
179
  const fc = fontConfig[family] || {};
141
- const sizes = fc.sizes && fc.sizes.length ? fc.sizes : discoveredSizes.length ? discoveredSizes : [16];
142
- return { path, family, sizes, bpp: fc.bpp ?? 4, glyphs: fc.glyphs ?? 'ascii' };
180
+ const sizes =
181
+ fc.sizes && fc.sizes.length
182
+ ? fc.sizes
183
+ : discoveredSizes.length
184
+ ? discoveredSizes
185
+ : [16];
186
+ return {path, family, sizes, bpp: fc.bpp ?? 4, glyphs: fc.glyphs ?? 'ascii'};
143
187
  });
144
- const imageJobs = [...images.entries()].map(([name, path]) => ({ path, name }));
188
+ const imageJobs = [...images.entries()].map(([name, path]) => ({path, name}));
145
189
 
146
- const bakedImages = imageJobs.map((i) => bakeImage(i));
147
- const bakedFonts = fontJobs.map((f) => bakeFont(f));
148
- const assetPack = bakedImages.length || bakedFonts.length ? emitAssetPack({ images: bakedImages, fonts: bakedFonts }) : null;
190
+ const bakedImages = imageJobs.map(i => bakeImage(i));
191
+ const bakedFonts = fontJobs.map(f => bakeFont(f));
192
+ const assetPack =
193
+ bakedImages.length || bakedFonts.length
194
+ ? emitAssetPack({images: bakedImages, fonts: bakedFonts})
195
+ : null;
149
196
  const fontSizeCount = bakedFonts.reduce((n, f) => n + f.sizes.length, 0);
150
197
 
151
198
  // --- Wrap into the ERCF container ---------------------------------------------------------------
152
- const container = emitContainer({ bytecode, assetPack, qjsTag: QJS_TAG });
199
+ const container = emitContainer({bytecode, assetPack, qjsTag: QJS_TAG});
153
200
  const outPath = resolve(distDir, 'app.erpkg');
154
201
  writeFileSync(outPath, container);
155
202
 
156
- const kb = (n) => `${(n / 1024).toFixed(1)} KB`;
203
+ const kb = n => `${(n / 1024).toFixed(1)} KB`;
157
204
  console.log(
158
205
  `Packed config -> dist/app.erpkg (${kb(container.length)})\n` +
159
206
  ` qjs ${QJS_TAG} · bytecode ${kb(bytecode.length)}` +
160
- (assetPack ? ` · assets ${kb(assetPack.length)} (${bakedImages.length} image(s), ${fontSizeCount} font size(s))` : ' · no assets'),
207
+ (assetPack
208
+ ? ` · assets ${kb(assetPack.length)} (${bakedImages.length} image(s), ${fontSizeCount} font size(s))`
209
+ : ' · no assets'),
161
210
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "embedded-react",
3
- "version": "0.2.3",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "React Native-style component package + reconciler that drives the embedded-react C engine through the QuickJS NativeUI bridge (Flow A).",
6
6
  "license": "Apache-2.0",
@@ -46,6 +46,7 @@
46
46
  "persist-transform.mjs",
47
47
  "cli.mjs",
48
48
  "sim-server.mjs",
49
+ "qjsc-wasm.mjs",
49
50
  "sim/",
50
51
  "README.md",
51
52
  "LICENSE",
@@ -69,18 +70,23 @@
69
70
  "test": "vitest run",
70
71
  "test:watch": "vitest",
71
72
  "test:runtime": "node test/runtime/run.mjs",
72
- "test:bytecode": "node test/runtime/run.mjs --bytecode"
73
+ "test:bytecode": "node test/runtime/run.mjs --bytecode",
74
+ "format": "cd ../../.. && prettier --write \"**/*.{js,jsx,mjs,cjs}\"",
75
+ "format:check": "cd ../../.. && prettier --check \"**/*.{js,jsx,mjs,cjs}\""
73
76
  },
74
77
  "dependencies": {
75
78
  "@babel/core": "^7.29.7",
76
79
  "@babel/plugin-syntax-jsx": "^7.29.7",
80
+ "@resvg/resvg-js": "^2.6.2",
77
81
  "esbuild": "^0.28.1",
78
82
  "opentype.js": "^2.0.0",
79
83
  "pngjs": "^7.0.0",
80
84
  "react": "18.3.1",
81
- "react-reconciler": "0.29.2"
85
+ "react-reconciler": "0.29.2",
86
+ "svgson": "^5.3.1"
82
87
  },
83
88
  "devDependencies": {
89
+ "prettier": "^3.4.2",
84
90
  "vitest": "^3.2.4"
85
91
  },
86
92
  "overrides": {
@@ -24,7 +24,7 @@
24
24
  // stable across the edits you make most often (JSX/logic). Adding/removing a useState in a component,
25
25
  // or renaming it, shifts that component's keys and resets its state — press R in the sim for a clean
26
26
  // reset any time.
27
- import { transformSync } from '@babel/core';
27
+ import {transformSync} from '@babel/core';
28
28
  import syntaxJsx from '@babel/plugin-syntax-jsx';
29
29
 
30
30
  /** Best-effort name of the function/component enclosing a path (for a stable, readable key). */
@@ -34,15 +34,21 @@ function enclosingName(path) {
34
34
  if (fn.node.id && fn.node.id.name) return fn.node.id.name; // function Foo() {}
35
35
  const parent = fn.parentPath && fn.parentPath.node;
36
36
  if (parent) {
37
- if (parent.type === 'VariableDeclarator' && parent.id && parent.id.name) return parent.id.name; // const Foo = () =>
37
+ if (parent.type === 'VariableDeclarator' && parent.id && parent.id.name)
38
+ return parent.id.name; // const Foo = () =>
38
39
  if (parent.key && parent.key.name) return parent.key.name; // method / property
39
- if (parent.type === 'AssignmentExpression' && parent.left && parent.left.name) return parent.left.name;
40
+ if (
41
+ parent.type === 'AssignmentExpression' &&
42
+ parent.left &&
43
+ parent.left.name
44
+ )
45
+ return parent.left.name;
40
46
  }
41
47
  return '_';
42
48
  }
43
49
 
44
50
  /** Babel plugin: useState(init) → __erPersistState("key", init), importing the helper when used. */
45
- function persistPlugin({ types: t }) {
51
+ function persistPlugin({types: t}) {
46
52
  return {
47
53
  name: 'er-persist-usestate',
48
54
  visitor: {
@@ -56,14 +62,19 @@ function persistPlugin({ types: t }) {
56
62
  path.unshiftContainer(
57
63
  'body',
58
64
  t.importDeclaration(
59
- [t.importSpecifier(t.identifier('__erPersistState'), t.identifier('usePersistentState'))],
65
+ [
66
+ t.importSpecifier(
67
+ t.identifier('__erPersistState'),
68
+ t.identifier('usePersistentState'),
69
+ ),
70
+ ],
60
71
  t.stringLiteral('embedded-react'),
61
72
  ),
62
73
  );
63
74
  },
64
75
  },
65
76
  CallExpression(path, state) {
66
- if (!t.isIdentifier(path.node.callee, { name: 'useState' })) return;
77
+ if (!t.isIdentifier(path.node.callee, {name: 'useState'})) return;
67
78
  // Only rewrite the real useState (an imported binding from react / embedded-react).
68
79
  const binding = path.scope.getBinding('useState');
69
80
  if (!binding || binding.kind !== 'module') return;
@@ -77,7 +88,10 @@ function persistPlugin({ types: t }) {
77
88
  const key = `${state.opts.moduleId}::${fnName}#${idx}`;
78
89
 
79
90
  path.replaceWith(
80
- t.callExpression(t.identifier('__erPersistState'), [t.stringLiteral(key), ...path.node.arguments]),
91
+ t.callExpression(t.identifier('__erPersistState'), [
92
+ t.stringLiteral(key),
93
+ ...path.node.arguments,
94
+ ]),
81
95
  );
82
96
  state.erUsed = true;
83
97
  path.skip();
@@ -116,8 +130,8 @@ export function transformPersist(code, moduleId) {
116
130
  babelrc: false,
117
131
  configFile: false,
118
132
  sourceType: 'module',
119
- parserOpts: { plugins: ['jsx'] },
120
- plugins: [syntaxJsx, [persistPlugin, { moduleId }]],
133
+ parserOpts: {plugins: ['jsx']},
134
+ plugins: [syntaxJsx, [persistPlugin, {moduleId}]],
121
135
  });
122
136
  return out.code;
123
137
  }