embedded-react 0.3.0 → 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,12 +23,18 @@
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, writeFileSync } from 'node:fs';
27
- import { createRequire } from 'node:module';
28
- import { fileURLToPath, pathToFileURL } from 'node:url';
29
- import { basename, dirname, relative, resolve } from 'node:path';
30
- import { tmpdir } from 'node:os';
31
- 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';
32
38
 
33
39
  const PKG_ROOT = dirname(fileURLToPath(import.meta.url));
34
40
  const require = createRequire(import.meta.url);
@@ -59,15 +65,22 @@ Usage:
59
65
  function simDirOrExit() {
60
66
  const simDir = resolve(PKG_ROOT, 'sim');
61
67
  if (!existsSync(resolve(simDir, 'embedded-react.wasm'))) {
62
- console.error('The prebuilt simulator (sim/embedded-react.wasm) is missing from this install.');
63
- 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
+ );
64
74
  process.exit(1);
65
75
  }
66
76
  return simDir;
67
77
  }
68
78
 
69
79
  const libSrc = () => resolve(PKG_ROOT, 'src/embedded-react/index.js');
70
- 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
+ ];
71
84
 
72
85
  /** Resolve the app entry: an explicit arg, else common conventions, else package.json "main"/"source". */
73
86
  function resolveEntry(cwd, explicit) {
@@ -79,7 +92,14 @@ function resolveEntry(cwd, explicit) {
79
92
  }
80
93
  return p;
81
94
  }
82
- 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
+ ]) {
83
103
  const p = resolve(cwd, rel);
84
104
  if (existsSync(p)) return p;
85
105
  }
@@ -97,8 +117,12 @@ function resolveEntry(cwd, explicit) {
97
117
  /* ignore */
98
118
  }
99
119
  }
100
- console.error('No app entry found. Pass one explicitly: embedded-react dev <entry.jsx>');
101
- 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
+ );
102
126
  process.exit(1);
103
127
  }
104
128
 
@@ -106,12 +130,14 @@ async function dev(args) {
106
130
  const cwd = process.cwd();
107
131
  const portIdx = args.indexOf('--port');
108
132
  const port = portIdx >= 0 ? parseInt(args[portIdx + 1], 10) : 3333;
109
- 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
+ );
110
136
 
111
137
  const simDir = simDirOrExit();
112
138
  const entry = resolveEntry(cwd, explicit);
113
139
  const outDir = resolve(tmpdir(), 'embedded-react-sim');
114
- mkdirSync(outDir, { recursive: true });
140
+ mkdirSync(outDir, {recursive: true});
115
141
 
116
142
  await runDevServer({
117
143
  entry,
@@ -130,20 +156,33 @@ async function exportApp(args) {
130
156
  const cwd = process.cwd();
131
157
  const outIdx = args.indexOf('--out');
132
158
  const outDir = resolve(cwd, outIdx >= 0 ? args[outIdx + 1] : 'sim-export');
133
- 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
+ );
134
162
 
135
163
  const simDir = simDirOrExit();
136
164
  const entry = resolveEntry(cwd, explicit);
137
165
  const pub = resolve(outDir, 'public');
138
- mkdirSync(pub, { recursive: true });
166
+ mkdirSync(pub, {recursive: true});
139
167
 
140
168
  // Bundle the app + bake assets into public/, then drop the prebuilt module + host page alongside.
141
- await buildApp({ entry, projectRoot: cwd, libSrc: libSrc(), nodePaths: nodePaths(cwd), outDir: pub });
142
- 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));
143
178
  copyFileSync(resolve(simDir, 'index.html'), resolve(outDir, 'index.html'));
144
179
 
145
- console.log(`✓ exported a static playground → ${relative(cwd, outDir) || '.'}/`);
146
- 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
+ );
147
186
  }
148
187
 
149
188
  /** Resolve the root component file for the AOT compiler (App.jsx), distinct from the registry entry. */
@@ -160,18 +199,24 @@ function resolveAppComponent(cwd, explicit) {
160
199
  const p = resolve(cwd, rel);
161
200
  if (existsSync(p)) return p;
162
201
  }
163
- console.error('No App component found. Pass one explicitly: embedded-react build --aot <App.jsx>');
202
+ console.error(
203
+ 'No App component found. Pass one explicitly: embedded-react build --aot <App.jsx>',
204
+ );
164
205
  process.exit(1);
165
206
  }
166
207
 
167
208
  /** Flow B (--aot): compile the App component to C (app.gen.{c,h}) + bake assets, to compile into firmware. */
168
209
  async function buildAot(cwd, explicit, outDir) {
169
- const { compileSource } = await import('./aot/compile.mjs');
170
- const { bakeAssets } = await import('./assets/index.mjs');
210
+ const {compileSource, bakeSvgArtifacts} = await import('./aot/compile.mjs');
211
+ const {bakeAssets} = await import('./assets/index.mjs');
171
212
  const appPath = resolveAppComponent(cwd, explicit);
213
+ const appDir = dirname(appPath);
214
+ const src = readFileSync(appPath, 'utf8');
172
215
  let result;
173
216
  try {
174
- result = compileSource(readFileSync(appPath, 'utf8'), 'app', { filename: appPath });
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});
175
220
  } catch (e) {
176
221
  console.error(e && e.aotLoc ? e.message : e?.message || String(e));
177
222
  process.exit(1);
@@ -179,27 +224,34 @@ async function buildAot(cwd, explicit, outDir) {
179
224
  writeFileSync(resolve(outDir, 'app.gen.c'), result.c);
180
225
  writeFileSync(resolve(outDir, 'app.gen.h'), result.h);
181
226
 
182
- const appDir = dirname(appPath);
183
- const imageJobs = result.images.map((im) => ({ name: im.name, path: resolve(appDir, im.importPath) }));
227
+ const imageJobs = result.images.map(im => ({
228
+ name: im.name,
229
+ path: resolve(appDir, im.importPath),
230
+ }));
184
231
  for (const j of imageJobs) {
185
232
  if (!existsSync(j.path)) {
186
233
  console.error(`<Image> asset "${j.name}" not found at ${j.path}`);
187
234
  process.exit(1);
188
235
  }
189
236
  }
190
- const baked = bakeAssets({ images: imageJobs, fonts: [], outDir });
191
- console.log(`✓ Flow B (AOT) → ${relative(cwd, outDir) || '.'}/app.gen.c (+ app.gen.h, assets.generated.c — ${baked.images} image(s))`);
192
- console.log(' No QuickJS on the device: compile these into your firmware against the engine (er_scene.h).');
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
+ );
193
244
  }
194
245
 
195
246
  /** Flow A (default): bundle → QuickJS bytecode (via the prebuilt wasm) + baked assets → app.erpkg. */
196
247
  async function buildContainer(cwd, explicit, outDir) {
197
248
  const esbuild = require('esbuild');
198
- const { bakeImage } = await import('./assets/bake-image.mjs');
199
- const { bakeFont } = await import('./assets/bake-font.mjs');
200
- const { emitAssetPack } = await import('./assets/emit-pack.mjs');
201
- const { emitContainer } = await import('./assets/emit-container.mjs');
202
- const { compileToBytecode } = await import('./qjsc-wasm.mjs');
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');
203
255
 
204
256
  const simDir = simDirOrExit();
205
257
  const entry = resolveEntry(cwd, explicit);
@@ -210,22 +262,29 @@ async function buildContainer(cwd, explicit, outDir) {
210
262
  const assetPlugin = {
211
263
  name: 'embedded-react-assets',
212
264
  setup(b) {
213
- b.onLoad({ filter: /\.(png|jpe?g|webp|gif|bmp)$/i }, (a) => {
265
+ b.onLoad({filter: /\.(png|jpe?g|webp|gif|bmp)$/i}, a => {
214
266
  const n = basename(a.path).replace(/\.[^.]+$/, '');
215
267
  images.set(n, a.path);
216
- return { contents: `module.exports = ${JSON.stringify(n)};`, loader: 'js' };
268
+ return {
269
+ contents: `module.exports = ${JSON.stringify(n)};`,
270
+ loader: 'js',
271
+ };
217
272
  });
218
- b.onLoad({ filter: /\.(ttf|otf)$/i }, (a) => {
273
+ b.onLoad({filter: /\.(ttf|otf)$/i}, a => {
219
274
  const f = basename(a.path).replace(/\.[^.]+$/, '');
220
275
  fonts.set(f, a.path);
221
- return { contents: `module.exports = ${JSON.stringify(f)};`, loader: 'js' };
276
+ return {
277
+ contents: `module.exports = ${JSON.stringify(f)};`,
278
+ loader: 'js',
279
+ };
222
280
  });
281
+ registerSvgVectorLoader(b, (name, p) => images.set(name, p)); // raster-fallback SVGs join the image pack
223
282
  },
224
283
  };
225
284
  // The bundle is an intermediate (it becomes bytecode in the .erpkg) — keep it out of the user's outDir
226
285
  // so `dist/` ends up holding only app.erpkg.
227
286
  const tmp = resolve(tmpdir(), 'embedded-react-build');
228
- mkdirSync(tmp, { recursive: true });
287
+ mkdirSync(tmp, {recursive: true});
229
288
  const bundlePath = resolve(tmp, 'app.bundle.js');
230
289
  await esbuild.build({
231
290
  entryPoints: [entry],
@@ -235,10 +294,10 @@ async function buildContainer(cwd, explicit, outDir) {
235
294
  platform: 'neutral',
236
295
  target: 'es2020',
237
296
  jsx: 'automatic',
238
- alias: { 'embedded-react': libSrc() },
297
+ alias: {'embedded-react': libSrc()},
239
298
  nodePaths: nodePaths(cwd),
240
299
  plugins: [assetPlugin],
241
- define: { 'process.env.NODE_ENV': '"production"' },
300
+ define: {'process.env.NODE_ENV': '"production"'},
242
301
  legalComments: 'none',
243
302
  logLevel: 'silent',
244
303
  });
@@ -249,30 +308,51 @@ async function buildContainer(cwd, explicit, outDir) {
249
308
 
250
309
  // Bake imported images/fonts into an ERPK pack (font sizes discovered from the bundle).
251
310
  const discoveredSizes = [
252
- ...new Set([...bundleSrc.matchAll(/\bfontSize\s*:\s*(\d+(?:\.\d+)?)/g)].map((m) => Math.round(Number(m[1])))),
311
+ ...new Set(
312
+ [...bundleSrc.matchAll(/\bfontSize\s*:\s*(\d+(?:\.\d+)?)/g)].map(m =>
313
+ Math.round(Number(m[1])),
314
+ ),
315
+ ),
253
316
  ].sort((a, b) => a - b);
254
317
  let cfg = {};
255
318
  const cp = resolve(cwd, 'assets.config.js');
256
- if (existsSync(cp)) cfg = (await import(pathToFileURL(cp).href)).default || {};
319
+ if (existsSync(cp))
320
+ cfg = (await import(pathToFileURL(cp).href)).default || {};
257
321
  const fontConfig = cfg.fonts || {};
258
322
  const fontJobs = [...fonts.entries()].map(([family, path]) => {
259
323
  const fc = fontConfig[family] || {};
260
- return { path, family, sizes: fc.sizes?.length ? fc.sizes : discoveredSizes.length ? discoveredSizes : [16], bpp: fc.bpp ?? 4, glyphs: fc.glyphs ?? 'ascii' };
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
+ };
261
335
  });
262
- const imageJobs = [...images.entries()].map(([name, path]) => ({ path, name }));
336
+ const imageJobs = [...images.entries()].map(([name, path]) => ({path, name}));
263
337
  const bakedImages = imageJobs.map(bakeImage);
264
338
  const bakedFonts = fontJobs.map(bakeFont);
265
- const assetPack = bakedImages.length || bakedFonts.length ? emitAssetPack({ images: bakedImages, fonts: bakedFonts }) : null;
339
+ const assetPack =
340
+ bakedImages.length || bakedFonts.length
341
+ ? emitAssetPack({images: bakedImages, fonts: bakedFonts})
342
+ : null;
266
343
 
267
- const container = emitContainer({ bytecode, assetPack, qjsTag: QJS_TAG });
344
+ const container = emitContainer({bytecode, assetPack, qjsTag: QJS_TAG});
268
345
  const outPath = resolve(outDir, 'app.erpkg');
269
346
  writeFileSync(outPath, container);
270
- const kb = (n) => `${(n / 1024).toFixed(1)} KB`;
347
+ const kb = n => `${(n / 1024).toFixed(1)} KB`;
271
348
  console.log(
272
349
  `✓ Flow A → ${relative(cwd, outPath) || 'app.erpkg'} (${kb(container.length)}; qjs ${QJS_TAG}, bytecode ${kb(bytecode.length)}` +
273
- (assetPack ? `, assets ${kb(assetPack.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.",
274
355
  );
275
- console.log(" Upload app.erpkg to your device's config region (er_runtime_load_container), or run it on the desktop host.");
276
356
  }
277
357
 
278
358
  /** `embedded-react build [--aot] [entry] [--out dir]` — produce the device artifact. */
@@ -281,8 +361,10 @@ async function build(args) {
281
361
  const aot = args.includes('--aot');
282
362
  const outIdx = args.indexOf('--out');
283
363
  const outDir = resolve(cwd, outIdx >= 0 ? args[outIdx + 1] : 'dist');
284
- const explicit = args.find((a, i) => !a.startsWith('--') && (outIdx < 0 || i !== outIdx + 1));
285
- mkdirSync(outDir, { recursive: true });
364
+ const explicit = args.find(
365
+ (a, i) => !a.startsWith('--') && (outIdx < 0 || i !== outIdx + 1),
366
+ );
367
+ mkdirSync(outDir, {recursive: true});
286
368
  if (aot) await buildAot(cwd, explicit, outDir);
287
369
  else await buildContainer(cwd, explicit, outDir);
288
370
  }
@@ -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.3.0",
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",
@@ -70,18 +70,23 @@
70
70
  "test": "vitest run",
71
71
  "test:watch": "vitest",
72
72
  "test:runtime": "node test/runtime/run.mjs",
73
- "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}\""
74
76
  },
75
77
  "dependencies": {
76
78
  "@babel/core": "^7.29.7",
77
79
  "@babel/plugin-syntax-jsx": "^7.29.7",
80
+ "@resvg/resvg-js": "^2.6.2",
78
81
  "esbuild": "^0.28.1",
79
82
  "opentype.js": "^2.0.0",
80
83
  "pngjs": "^7.0.0",
81
84
  "react": "18.3.1",
82
- "react-reconciler": "0.29.2"
85
+ "react-reconciler": "0.29.2",
86
+ "svgson": "^5.3.1"
83
87
  },
84
88
  "devDependencies": {
89
+ "prettier": "^3.4.2",
85
90
  "vitest": "^3.2.4"
86
91
  },
87
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
  }