embedded-react 0.3.0 → 0.4.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/assets/emit-c.mjs CHANGED
@@ -52,7 +52,7 @@ function glyphInit(g) {
52
52
  * @param {Array<object>} [opts.fonts] Results from bakeFont().
53
53
  * @returns {{c:string, h:string}}
54
54
  */
55
- export function emitAssetsC({ headerName, images = [], fonts = [] }) {
55
+ export function emitAssetsC({headerName, images = [], fonts = []}) {
56
56
  const guard = 'ER_' + cIdent(headerName).toUpperCase();
57
57
  const h =
58
58
  `/* Generated by bridges/quickjs/js/assets — DO NOT EDIT. */\n` +
@@ -66,10 +66,14 @@ export function emitAssetsC({ headerName, images = [], fonts = [] }) {
66
66
 
67
67
  const lines = [];
68
68
  lines.push(`/* Generated by bridges/quickjs/js/assets — DO NOT EDIT. */`);
69
- lines.push(`/* Images: premultiplied ARGB8888, referenced by pointer. Fonts: baked BitmapFont per size. */`);
69
+ lines.push(
70
+ `/* Images: premultiplied ARGB8888, referenced by pointer. Fonts: baked BitmapFont per size. */`,
71
+ );
70
72
  lines.push(``);
71
73
  lines.push(`#include "${headerName}"`);
72
- lines.push(`#include "er_scene.h" /* er_image_load, er_font_register, BitmapFont */`);
74
+ lines.push(
75
+ `#include "er_scene.h" /* er_image_load, er_font_register, BitmapFont */`,
76
+ );
73
77
  lines.push(`#include <stddef.h>`);
74
78
  lines.push(`#include <stdint.h>`);
75
79
  lines.push(``);
@@ -77,7 +81,10 @@ export function emitAssetsC({ headerName, images = [], fonts = [] }) {
77
81
  // --- Images ---------------------------------------------------------------------------------
78
82
  for (const img of images) {
79
83
  const id = cIdent(img.name);
80
- const hex = Array.from(img.pixels, (p) => '0x' + p.toString(16).toUpperCase().padStart(8, '0') + 'u');
84
+ const hex = Array.from(
85
+ img.pixels,
86
+ p => '0x' + p.toString(16).toUpperCase().padStart(8, '0') + 'u',
87
+ );
81
88
  lines.push(`/* image "${img.name}" — ${img.width}x${img.height} */`);
82
89
  lines.push(`static const uint32_t ${id}_px[] = {`);
83
90
  lines.push(rows(hex, 8));
@@ -89,10 +96,14 @@ export function emitAssetsC({ headerName, images = [], fonts = [] }) {
89
96
  const fontRegistrations = [];
90
97
  for (const font of fonts) {
91
98
  const famId = cIdent(font.family);
92
- lines.push(`/* font "${font.family}" — ${font.sizes.map((s) => `${s.pixelSize}px`).join(', ')} */`);
99
+ lines.push(
100
+ `/* font "${font.family}" — ${font.sizes.map(s => `${s.pixelSize}px`).join(', ')} */`,
101
+ );
93
102
  emitFontSizes(lines, font, famId, 'static const '); // file-local: only er_register_assets refers to these
94
103
  for (const sz of font.sizes) {
95
- fontRegistrations.push(` er_font_register(${JSON.stringify(font.family)}, &g_font_${famId}_${sz.pixelSize});`);
104
+ fontRegistrations.push(
105
+ ` er_font_register(${JSON.stringify(font.family)}, &g_font_${famId}_${sz.pixelSize});`,
106
+ );
96
107
  }
97
108
  }
98
109
 
@@ -100,7 +111,9 @@ export function emitAssetsC({ headerName, images = [], fonts = [] }) {
100
111
  lines.push(`void er_register_assets(void)`);
101
112
  lines.push(`{`);
102
113
  for (const img of images) {
103
- lines.push(` er_image_load(${JSON.stringify(img.name)}, ${cIdent(img.name)}_px, ${img.width}, ${img.height});`);
114
+ lines.push(
115
+ ` er_image_load(${JSON.stringify(img.name)}, ${cIdent(img.name)}_px, ${img.width}, ${img.height});`,
116
+ );
104
117
  }
105
118
  for (const reg of fontRegistrations) lines.push(reg);
106
119
  if (images.length === 0 && fontRegistrations.length === 0) {
@@ -109,28 +122,38 @@ export function emitAssetsC({ headerName, images = [], fonts = [] }) {
109
122
  lines.push(`}`);
110
123
  lines.push(``);
111
124
 
112
- return { c: lines.join('\n'), h };
125
+ return {c: lines.join('\n'), h};
113
126
  }
114
127
 
115
128
  /** Emits the per-size BitmapFont blocks (bitmap + glyphs + extras + struct) for one font. */
116
129
  function emitFontSizes(lines, font, symbol, fontStorage) {
117
130
  for (const sz of font.sizes) {
118
131
  const prefix = `s_${symbol}_${sz.pixelSize}`;
119
- const hex = sz.bitmap.map((b) => '0x' + b.toString(16).toUpperCase().padStart(2, '0'));
120
- lines.push(`static const uint8_t ${prefix}_bitmap[${sz.bitmap.length || 1}] = {`);
132
+ const hex = sz.bitmap.map(
133
+ b => '0x' + b.toString(16).toUpperCase().padStart(2, '0'),
134
+ );
135
+ lines.push(
136
+ `static const uint8_t ${prefix}_bitmap[${sz.bitmap.length || 1}] = {`,
137
+ );
121
138
  lines.push(sz.bitmap.length ? rows(hex, 16) : ' 0x00,');
122
139
  lines.push(`};`);
123
140
  lines.push(``);
124
141
 
125
- lines.push(`static const GlyphInfo ${prefix}_glyphs[${sz.dense.length}] = {`);
142
+ lines.push(
143
+ `static const GlyphInfo ${prefix}_glyphs[${sz.dense.length}] = {`,
144
+ );
126
145
  for (const g of sz.dense) lines.push(` ${glyphInit(g)},`);
127
146
  lines.push(`};`);
128
147
  lines.push(``);
129
148
 
130
149
  if (sz.extras.length) {
131
- lines.push(`static const ExtraGlyph ${prefix}_extras[${sz.extras.length}] = {`);
150
+ lines.push(
151
+ `static const ExtraGlyph ${prefix}_extras[${sz.extras.length}] = {`,
152
+ );
132
153
  for (const e of sz.extras) {
133
- lines.push(` { .codepoint = 0x${e.codepoint.toString(16).toUpperCase()}, .info = ${glyphInit(e.info)} },`);
154
+ lines.push(
155
+ ` { .codepoint = 0x${e.codepoint.toString(16).toUpperCase()}, .info = ${glyphInit(e.info)} },`,
156
+ );
134
157
  }
135
158
  lines.push(`};`);
136
159
  lines.push(``);
@@ -139,7 +162,9 @@ function emitFontSizes(lines, font, symbol, fontStorage) {
139
162
  lines.push(`${fontStorage}BitmapFont g_font_${symbol}_${sz.pixelSize} = {`);
140
163
  lines.push(` .bitmap = ${prefix}_bitmap,`);
141
164
  lines.push(` .glyphs = ${prefix}_glyphs,`);
142
- lines.push(` .extras = ${sz.extras.length ? `${prefix}_extras` : 'NULL'},`);
165
+ lines.push(
166
+ ` .extras = ${sz.extras.length ? `${prefix}_extras` : 'NULL'},`,
167
+ );
143
168
  lines.push(` .extras_count = ${sz.extras.length},`);
144
169
  lines.push(` .first = 0x${sz.first.toString(16).toUpperCase()},`);
145
170
  lines.push(` .last = 0x${sz.last.toString(16).toUpperCase()},`);
@@ -163,14 +188,18 @@ function emitFontSizes(lines, font, symbol, fontStorage) {
163
188
  * @param {string} [opts.sourceName] Source font filename, for the header comment.
164
189
  * @returns {string} The .c source text.
165
190
  */
166
- export function emitBuiltinFont({ font, symbol = 'inter', sourceName = '' }) {
167
- const sizesStr = font.sizes.map((s) => s.pixelSize).join(',');
191
+ export function emitBuiltinFont({font, symbol = 'inter', sourceName = ''}) {
192
+ const sizesStr = font.sizes.map(s => s.pixelSize).join(',');
168
193
  const bpp = font.sizes[0] ? font.sizes[0].bpp : 4;
169
194
  const lines = [];
170
- lines.push(`/* AUTO-GENERATED by bridges/quickjs/js/assets/build-builtin-font.mjs — do not edit by hand.`);
195
+ lines.push(
196
+ `/* AUTO-GENERATED by bridges/quickjs/js/assets/build-builtin-font.mjs — do not edit by hand.`,
197
+ );
171
198
  lines.push(` * Source : ${sourceName}`);
172
199
  lines.push(` * Sizes : ${sizesStr} (bpp ${bpp}, family "${font.family}")`);
173
- lines.push(` * Regenerate: cd bridges/quickjs/js && npm run build:builtin-font`);
200
+ lines.push(
201
+ ` * Regenerate: cd bridges/quickjs/js && npm run build:builtin-font`,
202
+ );
174
203
  lines.push(` */`);
175
204
  lines.push(`#include "font_bitmap.h"`);
176
205
  lines.push(`#include <stddef.h>`);
@@ -179,9 +208,12 @@ export function emitBuiltinFont({ font, symbol = 'inter', sourceName = '' }) {
179
208
  emitFontSizes(lines, font, symbol, 'const '); // file-scope const: referenced by g_<symbol>_sizes
180
209
 
181
210
  lines.push(`const BitmapFont *const g_${symbol}_sizes[] = {`);
182
- for (const sz of font.sizes) lines.push(` &g_font_${symbol}_${sz.pixelSize},`);
211
+ for (const sz of font.sizes)
212
+ lines.push(` &g_font_${symbol}_${sz.pixelSize},`);
183
213
  lines.push(`};`);
184
- lines.push(`const size_t g_${symbol}_sizes_count = sizeof(g_${symbol}_sizes) / sizeof(g_${symbol}_sizes[0]);`);
214
+ lines.push(
215
+ `const size_t g_${symbol}_sizes_count = sizeof(g_${symbol}_sizes) / sizeof(g_${symbol}_sizes[0]);`,
216
+ );
185
217
  lines.push(``);
186
218
  return lines.join('\n');
187
219
  }
@@ -95,15 +95,17 @@ class Writer {
95
95
  * @param {string} opts.qjsTag QuickJS release tag the bytecode targets (e.g. "v0.15.0").
96
96
  * @returns {Buffer} The container bytes.
97
97
  */
98
- export function emitContainer({ bytecode, assetPack, qjsTag }) {
99
- if (!bytecode || !bytecode.length) throw new Error('emitContainer: bytecode is required');
98
+ export function emitContainer({bytecode, assetPack, qjsTag}) {
99
+ if (!bytecode || !bytecode.length)
100
+ throw new Error('emitContainer: bytecode is required');
100
101
  if (!qjsTag) throw new Error('emitContainer: qjsTag is required');
101
102
 
102
103
  // Build the body (everything after the crc32 field) first, then prepend magic+version+crc.
103
104
  const body = new Writer();
104
105
  body.str(qjsTag);
105
106
  const sections = [];
106
- if (assetPack && assetPack.length) sections.push([SECTION_ASSET_PACK, assetPack]);
107
+ if (assetPack && assetPack.length)
108
+ sections.push([SECTION_ASSET_PACK, assetPack]);
107
109
  sections.push([SECTION_BYTECODE, bytecode]);
108
110
  body.u32(sections.length);
109
111
  for (const [type, data] of sections) {
@@ -87,7 +87,7 @@ function writeGlyph(w, g) {
87
87
  * @param {Array<object>} [opts.fonts] Results of bakeFont().
88
88
  * @returns {Buffer} The pack bytes.
89
89
  */
90
- export function emitAssetPack({ images = [], fonts = [] }) {
90
+ export function emitAssetPack({images = [], fonts = []}) {
91
91
  const w = new Writer();
92
92
  w.bytes(Buffer.from('ERPK', 'ascii'));
93
93
  w.u32(VERSION);
@@ -100,7 +100,13 @@ export function emitAssetPack({ images = [], fonts = [] }) {
100
100
  w.u32(img.width);
101
101
  w.u32(img.height);
102
102
  // pixels is a Uint32Array (premultiplied ARGB words); copy its LE bytes verbatim.
103
- w.bytes(Buffer.from(img.pixels.buffer, img.pixels.byteOffset, img.pixels.byteLength));
103
+ w.bytes(
104
+ Buffer.from(
105
+ img.pixels.buffer,
106
+ img.pixels.byteOffset,
107
+ img.pixels.byteLength,
108
+ ),
109
+ );
104
110
  }
105
111
 
106
112
  for (const font of fonts) {
package/assets/index.mjs CHANGED
@@ -19,10 +19,10 @@
19
19
  // after bundling; the example firmware compiles the .c and calls er_register_assets() at boot.
20
20
  import fs from 'node:fs';
21
21
  import path from 'node:path';
22
- import { bakeImage } from './bake-image.mjs';
23
- import { bakeFont } from './bake-font.mjs';
24
- import { emitAssetsC } from './emit-c.mjs';
25
- import { emitAssetPack } from './emit-pack.mjs';
22
+ import {bakeImage} from './bake-image.mjs';
23
+ import {bakeFont} from './bake-font.mjs';
24
+ import {emitAssetsC} from './emit-c.mjs';
25
+ import {emitAssetPack} from './emit-pack.mjs';
26
26
 
27
27
  /**
28
28
  * Bakes the given assets and writes assets.generated.{c,h} into outDir.
@@ -34,21 +34,25 @@ import { emitAssetPack } from './emit-pack.mjs';
34
34
  * @param {string} opts.outDir Directory to write the generated files into.
35
35
  * @returns {{cPath:string, hPath:string, images:number, fonts:number}}
36
36
  */
37
- export function bakeAssets({ images = [], fonts = [], outDir }) {
38
- const bakedImages = images.map((i) => bakeImage(i));
39
- const bakedFonts = fonts.map((f) => bakeFont(f));
37
+ export function bakeAssets({images = [], fonts = [], outDir}) {
38
+ const bakedImages = images.map(i => bakeImage(i));
39
+ const bakedFonts = fonts.map(f => bakeFont(f));
40
40
 
41
41
  const headerName = 'assets.generated.h';
42
- const { c, h } = emitAssetsC({ headerName, images: bakedImages, fonts: bakedFonts });
42
+ const {c, h} = emitAssetsC({
43
+ headerName,
44
+ images: bakedImages,
45
+ fonts: bakedFonts,
46
+ });
43
47
 
44
- fs.mkdirSync(outDir, { recursive: true });
48
+ fs.mkdirSync(outDir, {recursive: true});
45
49
  const cPath = path.join(outDir, 'assets.generated.c');
46
50
  const hPath = path.join(outDir, headerName);
47
51
  fs.writeFileSync(cPath, c);
48
52
  fs.writeFileSync(hPath, h);
49
53
 
50
54
  const fontSizes = bakedFonts.reduce((n, f) => n + f.sizes.length, 0);
51
- return { cPath, hPath, images: bakedImages.length, fonts: fontSizes };
55
+ return {cPath, hPath, images: bakedImages.length, fonts: fontSizes};
52
56
  }
53
57
 
54
58
  /**
@@ -61,12 +65,17 @@ export function bakeAssets({ images = [], fonts = [], outDir }) {
61
65
  * @param {string} opts.outPath Path to write the .pack file.
62
66
  * @returns {{path:string, bytes:number, images:number, fonts:number}}
63
67
  */
64
- export function bakeAssetPack({ images = [], fonts = [], outPath }) {
65
- const bakedImages = images.map((i) => bakeImage(i));
66
- const bakedFonts = fonts.map((f) => bakeFont(f));
67
- const pack = emitAssetPack({ images: bakedImages, fonts: bakedFonts });
68
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
68
+ export function bakeAssetPack({images = [], fonts = [], outPath}) {
69
+ const bakedImages = images.map(i => bakeImage(i));
70
+ const bakedFonts = fonts.map(f => bakeFont(f));
71
+ const pack = emitAssetPack({images: bakedImages, fonts: bakedFonts});
72
+ fs.mkdirSync(path.dirname(outPath), {recursive: true});
69
73
  fs.writeFileSync(outPath, pack);
70
74
  const fontSizes = bakedFonts.reduce((n, f) => n + f.sizes.length, 0);
71
- return { path: outPath, bytes: pack.length, images: bakedImages.length, fonts: fontSizes };
75
+ return {
76
+ path: outPath,
77
+ bytes: pack.length,
78
+ images: bakedImages.length,
79
+ fonts: fontSizes,
80
+ };
72
81
  }
@@ -40,11 +40,11 @@ function flattenCommands(commands, steps) {
40
40
  for (const c of commands) {
41
41
  if (c.type === 'M') {
42
42
  if (cur && cur.length > 1) contours.push(cur);
43
- cur = [{ x: c.x, y: c.y }];
43
+ cur = [{x: c.x, y: c.y}];
44
44
  cx = sx = c.x;
45
45
  cy = sy = c.y;
46
46
  } else if (c.type === 'L') {
47
- cur.push({ x: c.x, y: c.y });
47
+ cur.push({x: c.x, y: c.y});
48
48
  cx = c.x;
49
49
  cy = c.y;
50
50
  } else if (c.type === 'Q') {
@@ -63,15 +63,23 @@ function flattenCommands(commands, steps) {
63
63
  const t = i / steps;
64
64
  const mt = 1 - t;
65
65
  cur.push({
66
- x: mt * mt * mt * cx + 3 * mt * mt * t * c.x1 + 3 * mt * t * t * c.x2 + t * t * t * c.x,
67
- y: mt * mt * mt * cy + 3 * mt * mt * t * c.y1 + 3 * mt * t * t * c.y2 + t * t * t * c.y,
66
+ x:
67
+ mt * mt * mt * cx +
68
+ 3 * mt * mt * t * c.x1 +
69
+ 3 * mt * t * t * c.x2 +
70
+ t * t * t * c.x,
71
+ y:
72
+ mt * mt * mt * cy +
73
+ 3 * mt * mt * t * c.y1 +
74
+ 3 * mt * t * t * c.y2 +
75
+ t * t * t * c.y,
68
76
  });
69
77
  }
70
78
  cx = c.x;
71
79
  cy = c.y;
72
80
  } else if (c.type === 'Z') {
73
81
  if (cur) {
74
- cur.push({ x: sx, y: sy }); // close the contour
82
+ cur.push({x: sx, y: sy}); // close the contour
75
83
  if (cur.length > 1) contours.push(cur);
76
84
  cur = null;
77
85
  }
@@ -109,7 +117,13 @@ export function rasterize(path, opts = {}) {
109
117
  }
110
118
  }
111
119
  if (!Number.isFinite(minX)) {
112
- return { width: 0, height: 0, xOffset: 0, yOffset: 0, coverage: new Uint8Array(0) };
120
+ return {
121
+ width: 0,
122
+ height: 0,
123
+ xOffset: 0,
124
+ yOffset: 0,
125
+ coverage: new Uint8Array(0),
126
+ };
113
127
  }
114
128
 
115
129
  const x0 = Math.floor(minX);
@@ -117,7 +131,13 @@ export function rasterize(path, opts = {}) {
117
131
  const width = Math.ceil(maxX) - x0;
118
132
  const height = Math.ceil(maxY) - y0;
119
133
  if (width <= 0 || height <= 0) {
120
- return { width: 0, height: 0, xOffset: 0, yOffset: 0, coverage: new Uint8Array(0) };
134
+ return {
135
+ width: 0,
136
+ height: 0,
137
+ xOffset: 0,
138
+ yOffset: 0,
139
+ coverage: new Uint8Array(0),
140
+ };
121
141
  }
122
142
 
123
143
  // Non-horizontal edges, normalized so ylo < yhi; dir is the winding contribution.
@@ -127,8 +147,22 @@ export function rasterize(path, opts = {}) {
127
147
  const a = ct[i];
128
148
  const b = ct[i + 1];
129
149
  if (a.y === b.y) continue;
130
- if (a.y < b.y) edges.push({ ylo: a.y, yhi: b.y, x: a.x, dxdy: (b.x - a.x) / (b.y - a.y), dir: 1 });
131
- else edges.push({ ylo: b.y, yhi: a.y, x: b.x, dxdy: (a.x - b.x) / (a.y - b.y), dir: -1 });
150
+ if (a.y < b.y)
151
+ edges.push({
152
+ ylo: a.y,
153
+ yhi: b.y,
154
+ x: a.x,
155
+ dxdy: (b.x - a.x) / (b.y - a.y),
156
+ dir: 1,
157
+ });
158
+ else
159
+ edges.push({
160
+ ylo: b.y,
161
+ yhi: a.y,
162
+ x: b.x,
163
+ dxdy: (a.x - b.x) / (a.y - b.y),
164
+ dir: -1,
165
+ });
132
166
  }
133
167
  }
134
168
 
@@ -139,7 +173,7 @@ export function rasterize(path, opts = {}) {
139
173
  const xs = [];
140
174
  for (const e of edges) {
141
175
  if (sampleY >= e.ylo && sampleY < e.yhi) {
142
- xs.push({ x: e.x + (sampleY - e.ylo) * e.dxdy, dir: e.dir });
176
+ xs.push({x: e.x + (sampleY - e.ylo) * e.dxdy, dir: e.dir});
143
177
  }
144
178
  }
145
179
  if (xs.length < 2) continue;
@@ -165,5 +199,5 @@ export function rasterize(path, opts = {}) {
165
199
  for (let i = 0; i < coverage.length; i++) {
166
200
  coverage[i] = Math.round((counts[i] * 255) / maxCov);
167
201
  }
168
- return { width, height, xOffset: x0, yOffset: y0, coverage };
202
+ return {width, height, xOffset: x0, yOffset: y0, coverage};
169
203
  }
@@ -0,0 +1,81 @@
1
+ /*
2
+ * Copyright 2026 Cory Lamming
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ // esbuild loader for `import logo from './logo.svg'`. Two outcomes, decided at bake time:
18
+ // - VECTOR: the SVG is fully representable as the engine's op-tape — bake it and INLINE a
19
+ // {kind:'vector', ops, paints, gradients, width, height} artifact (small numeric data) into the bundle.
20
+ // - RASTER: the SVG uses features the vector baker can't represent (text, mask, filter, use,
21
+ // pattern, …). We warn, rasterize the whole SVG via resvg to a PNG, register that PNG through the
22
+ // bundler's normal image pipeline (a small `addRasterAsset(name, pngPath)` callback), and inline a
23
+ // {kind:'raster', name, width, height} artifact. A <Svg source> renders kind:'vector' as a vector node
24
+ // and kind:'raster' as an image node (host-config), so the fallback is transparent to the app.
25
+ // Shared by every Flow A bundler so the .svg handling lives in one place.
26
+ import {readFileSync} from 'node:fs';
27
+ import {basename} from 'node:path';
28
+ import {svgToVector, svgToRaster, writeRasterPng} from './bake-svg.mjs';
29
+
30
+ const assetName = p => basename(p).replace(/\.[^.]+$/, '');
31
+
32
+ /**
33
+ * Registers a `.svg` onLoad on an esbuild build.
34
+ *
35
+ * @param {import('esbuild').PluginBuild} build The esbuild build passed to a plugin's setup().
36
+ * @param {(name:string, pngPath:string)=>void} [addRasterAsset] Registers a rasterized-SVG PNG into the
37
+ * bundler's image collection (same mechanism as `import x from './x.png'`). When omitted, an SVG that
38
+ * needs raster fallback is baked as a (lossy) vector and a warning notes the dropped content.
39
+ */
40
+ export function registerSvgVectorLoader(build, addRasterAsset) {
41
+ build.onLoad({filter: /\.svg$/i}, async args => {
42
+ try {
43
+ const svg = readFileSync(args.path, 'utf8');
44
+ const {dropped = [], ...vec} = await svgToVector(svg);
45
+ if (dropped.length) {
46
+ const feats = dropped.join(', ');
47
+ if (addRasterAsset) {
48
+ console.warn(
49
+ `embedded-react: ${basename(args.path)} uses unsupported SVG feature(s) [${feats}] — rasterizing ` +
50
+ `it as a fallback image. (Raster loses scalability and costs RAM; simplify the SVG to keep it a ` +
51
+ `live vector.)`,
52
+ );
53
+ const {width, height, png} = await svgToRaster(svg);
54
+ const name = assetName(args.path);
55
+ addRasterAsset(name, writeRasterPng(name, png));
56
+ return {
57
+ contents: `module.exports = ${JSON.stringify({kind: 'raster', name, width, height})};`,
58
+ loader: 'js',
59
+ };
60
+ }
61
+ console.warn(
62
+ `embedded-react: ${basename(args.path)} uses unsupported SVG feature(s) [${feats}] and this build has ` +
63
+ `no raster fallback — that content will NOT render. Simplify the SVG, or import it via a bundler that ` +
64
+ `supports the image pipeline.`,
65
+ );
66
+ }
67
+ return {
68
+ contents: `module.exports = ${JSON.stringify({kind: 'vector', ...vec})};`,
69
+ loader: 'js',
70
+ };
71
+ } catch (e) {
72
+ return {
73
+ errors: [
74
+ {
75
+ text: `embedded-react: failed to bake SVG ${args.path}: ${e.message}`,
76
+ },
77
+ ],
78
+ };
79
+ }
80
+ });
81
+ }
package/build.mjs CHANGED
@@ -26,11 +26,12 @@
26
26
  // npm run build -- marine-dash # a specific demo by folder name
27
27
  // Outputs are always dist/app.bundle.js + dist/assets.generated.{c,h} — the single "active" app the
28
28
  // example hosts pick up.
29
- import { build } from 'esbuild';
30
- import { fileURLToPath, pathToFileURL } from 'node:url';
31
- import { dirname, resolve, basename } from 'node:path';
32
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
33
- import { bakeAssets } from './assets/index.mjs';
29
+ import {build} from 'esbuild';
30
+ import {fileURLToPath, pathToFileURL} from 'node:url';
31
+ import {dirname, resolve, basename} from 'node:path';
32
+ import {existsSync, readdirSync, readFileSync} from 'node:fs';
33
+ import {bakeAssets} from './assets/index.mjs';
34
+ import {registerSvgVectorLoader} from './assets/svg-loader.mjs';
34
35
 
35
36
  const here = dirname(fileURLToPath(import.meta.url)); // bridges/quickjs/js
36
37
  const repoRoot = resolve(here, '../../..');
@@ -46,9 +47,9 @@ const entry = resolve(demoDir, 'index.jsx');
46
47
 
47
48
  if (!existsSync(entry)) {
48
49
  const available = existsSync(demosDir)
49
- ? readdirSync(demosDir, { withFileTypes: true })
50
- .filter((d) => d.isDirectory())
51
- .map((d) => d.name)
50
+ ? readdirSync(demosDir, {withFileTypes: true})
51
+ .filter(d => d.isDirectory())
52
+ .map(d => d.name)
52
53
  : [];
53
54
  console.error(`Demo "${demo}" not found (expected ${entry}).`);
54
55
  console.error(`Available demos: ${available.join(', ') || '(none)'}`);
@@ -64,16 +65,23 @@ const fonts = new Map(); // family -> path
64
65
  const assetPlugin = {
65
66
  name: 'embedded-react-assets',
66
67
  setup(build) {
67
- build.onLoad({ filter: /\.(png|jpe?g|webp|gif|bmp)$/i }, (args) => {
68
+ build.onLoad({filter: /\.(png|jpe?g|webp|gif|bmp)$/i}, args => {
68
69
  const name = basename(args.path).replace(/\.[^.]+$/, '');
69
70
  images.set(name, args.path);
70
- return { contents: `module.exports = ${JSON.stringify(name)};`, loader: 'js' };
71
+ return {
72
+ contents: `module.exports = ${JSON.stringify(name)};`,
73
+ loader: 'js',
74
+ };
71
75
  });
72
- build.onLoad({ filter: /\.(ttf|otf)$/i }, (args) => {
76
+ build.onLoad({filter: /\.(ttf|otf)$/i}, args => {
73
77
  const family = basename(args.path).replace(/\.[^.]+$/, '');
74
78
  fonts.set(family, args.path);
75
- return { contents: `module.exports = ${JSON.stringify(family)};`, loader: 'js' };
79
+ return {
80
+ contents: `module.exports = ${JSON.stringify(family)};`,
81
+ loader: 'js',
82
+ };
76
83
  });
84
+ registerSvgVectorLoader(build, (name, p) => images.set(name, p));
77
85
  },
78
86
  };
79
87
 
@@ -90,11 +98,11 @@ await build({
90
98
  // them: map the bare `embedded-react` import to the library source and let the demo's bare deps
91
99
  // (react, react-reconciler) resolve from this package's node_modules. (The library's own internal
92
100
  // imports still resolve relatively / from node_modules as before.)
93
- alias: { 'embedded-react': libEntry },
101
+ alias: {'embedded-react': libEntry},
94
102
  nodePaths: [nodeModules],
95
103
  plugins: [assetPlugin],
96
104
  // Production React: smaller and avoids dev-only warning machinery that needs more shims.
97
- define: { 'process.env.NODE_ENV': '"production"' },
105
+ define: {'process.env.NODE_ENV': '"production"'},
98
106
  legalComments: 'none',
99
107
  logLevel: 'info',
100
108
  });
@@ -107,7 +115,11 @@ console.log(`Bundled demo "${demo}" -> dist/app.bundle.js`);
107
115
  // and will snap to the nearest baked size at runtime; pin them via assets.config.js if needed.
108
116
  const bundleSrc = readFileSync(bundlePath, 'utf8');
109
117
  const discoveredSizes = [
110
- ...new Set([...bundleSrc.matchAll(/\bfontSize\s*:\s*(\d+(?:\.\d+)?)/g)].map((m) => Math.round(Number(m[1])))),
118
+ ...new Set(
119
+ [...bundleSrc.matchAll(/\bfontSize\s*:\s*(\d+(?:\.\d+)?)/g)].map(m =>
120
+ Math.round(Number(m[1])),
121
+ ),
122
+ ),
111
123
  ].sort((a, b) => a - b);
112
124
 
113
125
  // Optional per-demo overrides: demos/<demo>/assets.config.js
@@ -121,14 +133,25 @@ const fontConfig = config.fonts || {};
121
133
 
122
134
  const fontJobs = [...fonts.entries()].map(([family, path]) => {
123
135
  const fc = fontConfig[family] || {};
124
- const sizes = fc.sizes && fc.sizes.length ? fc.sizes : discoveredSizes.length ? discoveredSizes : [16];
125
- return { path, family, sizes, bpp: fc.bpp ?? 4, glyphs: fc.glyphs ?? 'ascii' };
136
+ const sizes =
137
+ fc.sizes && fc.sizes.length
138
+ ? fc.sizes
139
+ : discoveredSizes.length
140
+ ? discoveredSizes
141
+ : [16];
142
+ return {path, family, sizes, bpp: fc.bpp ?? 4, glyphs: fc.glyphs ?? 'ascii'};
126
143
  });
127
- const imageJobs = [...images.entries()].map(([name, path]) => ({ path, name }));
144
+ const imageJobs = [...images.entries()].map(([name, path]) => ({path, name}));
128
145
 
129
- const summary = bakeAssets({ images: imageJobs, fonts: fontJobs, outDir: distDir });
146
+ const summary = bakeAssets({
147
+ images: imageJobs,
148
+ fonts: fontJobs,
149
+ outDir: distDir,
150
+ });
130
151
  const fontDesc = fontJobs.length
131
- ? fontJobs.map((f) => `${f.family}@[${f.sizes.join(',')}]x${f.bpp}bpp`).join(', ')
152
+ ? fontJobs
153
+ .map(f => `${f.family}@[${f.sizes.join(',')}]x${f.bpp}bpp`)
154
+ .join(', ')
132
155
  : 'none';
133
156
  console.log(
134
157
  `Baked ${summary.images} image(s), ${summary.fonts} font size(s) -> dist/assets.generated.c\n` +