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/aot/compile.mjs +2407 -697
- package/aot/screenshot-smoke.mjs +34 -17
- package/aot/style-map.mjs +156 -80
- package/assets/bake-font.mjs +45 -21
- package/assets/bake-image.mjs +7 -5
- package/assets/bake-svg.mjs +563 -0
- package/assets/build-builtin-font.mjs +25 -12
- package/assets/emit-c.mjs +52 -20
- package/assets/emit-container.mjs +5 -3
- package/assets/emit-pack.mjs +8 -2
- package/assets/index.mjs +25 -16
- package/assets/rasterize.mjs +45 -11
- package/assets/svg-loader.mjs +81 -0
- package/build.mjs +43 -20
- package/cli.mjs +134 -52
- package/pack-container.mjs +84 -35
- package/package.json +8 -3
- package/persist-transform.mjs +23 -9
- package/qjsc-wasm.mjs +19 -8
- package/sim/embedded-react.wasm +0 -0
- package/sim-server.mjs +160 -48
- package/src/embedded-react/Animated.js +51 -36
- package/src/embedded-react/AppRegistry.js +4 -4
- package/src/embedded-react/Easing.js +1 -1
- package/src/embedded-react/LayoutAnimation.js +13 -6
- package/src/embedded-react/StyleSheet.js +1 -1
- package/src/embedded-react/imperative.js +19 -7
- package/src/embedded-react/index.js +8 -8
- package/src/embedded-react/layout-anim-config.js +13 -9
- package/src/embedded-react/split-style.js +6 -6
- package/src/embedded-react/svg-ops.js +369 -41
- package/src/embedded-react/usePersistentState.js +3 -3
- package/src/host-config.js +137 -18
- package/src/native-ui.js +3 -1
- package/src/props.js +22 -10
- package/src/renderer.js +3 -3
package/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 {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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(
|
|
63
|
-
|
|
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 =
|
|
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 [
|
|
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(
|
|
101
|
-
|
|
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(
|
|
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, {
|
|
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(
|
|
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, {
|
|
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({
|
|
142
|
-
|
|
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(
|
|
146
|
-
|
|
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(
|
|
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 {
|
|
170
|
-
const {
|
|
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
|
-
|
|
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
|
|
183
|
-
|
|
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({
|
|
191
|
-
console.log(
|
|
192
|
-
|
|
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 {
|
|
199
|
-
const {
|
|
200
|
-
const {
|
|
201
|
-
const {
|
|
202
|
-
const {
|
|
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({
|
|
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 {
|
|
268
|
+
return {
|
|
269
|
+
contents: `module.exports = ${JSON.stringify(n)};`,
|
|
270
|
+
loader: 'js',
|
|
271
|
+
};
|
|
217
272
|
});
|
|
218
|
-
b.onLoad({
|
|
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 {
|
|
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, {
|
|
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: {
|
|
297
|
+
alias: {'embedded-react': libSrc()},
|
|
239
298
|
nodePaths: nodePaths(cwd),
|
|
240
299
|
plugins: [assetPlugin],
|
|
241
|
-
define: {
|
|
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(
|
|
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))
|
|
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 {
|
|
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]) => ({
|
|
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 =
|
|
339
|
+
const assetPack =
|
|
340
|
+
bakedImages.length || bakedFonts.length
|
|
341
|
+
? emitAssetPack({images: bakedImages, fonts: bakedFonts})
|
|
342
|
+
: null;
|
|
266
343
|
|
|
267
|
-
const container = emitContainer({
|
|
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 =
|
|
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(
|
|
285
|
-
|
|
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
|
}
|
package/pack-container.mjs
CHANGED
|
@@ -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 {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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, {
|
|
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(
|
|
70
|
-
|
|
71
|
-
|
|
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(
|
|
75
|
-
|
|
76
|
-
|
|
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({
|
|
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 {
|
|
117
|
+
return {
|
|
118
|
+
contents: `module.exports = ${JSON.stringify(name)};`,
|
|
119
|
+
loader: 'js',
|
|
120
|
+
};
|
|
91
121
|
});
|
|
92
|
-
b.onLoad({
|
|
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 {
|
|
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, {
|
|
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: {
|
|
144
|
+
alias: {'embedded-react': libEntry},
|
|
111
145
|
nodePaths: [nodeModules],
|
|
112
146
|
plugins: [assetPlugin],
|
|
113
|
-
define: {
|
|
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], {
|
|
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(
|
|
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))
|
|
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 =
|
|
142
|
-
|
|
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]) => ({
|
|
188
|
+
const imageJobs = [...images.entries()].map(([name, path]) => ({path, name}));
|
|
145
189
|
|
|
146
|
-
const bakedImages = imageJobs.map(
|
|
147
|
-
const bakedFonts = fontJobs.map(
|
|
148
|
-
const assetPack =
|
|
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({
|
|
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 =
|
|
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
|
|
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
|
+
"version": "0.4.1",
|
|
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": {
|
package/persist-transform.mjs
CHANGED
|
@@ -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 {
|
|
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)
|
|
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 (
|
|
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({
|
|
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
|
-
[
|
|
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, {
|
|
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'), [
|
|
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: {
|
|
120
|
-
plugins: [syntaxJsx, [persistPlugin, {
|
|
133
|
+
parserOpts: {plugins: ['jsx']},
|
|
134
|
+
plugins: [syntaxJsx, [persistPlugin, {moduleId}]],
|
|
121
135
|
});
|
|
122
136
|
return out.code;
|
|
123
137
|
}
|