embedded-react 0.2.1 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "embedded-react",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",
@@ -86,6 +86,23 @@ function persistPlugin({ types: t }) {
86
86
  };
87
87
  }
88
88
 
89
+ /**
90
+ * Whether `absPath` is an app source file that should receive the persist transform: under the project
91
+ * root, but NOT a dependency. The library itself lives under the project root in a consumer install
92
+ * (`<project>/node_modules/embedded-react`), and transforming it would rewrite the `useState` *inside*
93
+ * `usePersistentState` into a call to `usePersistentState` — infinite self-recursion → stack overflow.
94
+ * Excluding `node_modules` is what keeps the helper (and react) untransformed in a published install,
95
+ * not just in the monorepo (where the library happens to sit outside the demo's project root).
96
+ *
97
+ * @param {string} absPath Absolute path of the module esbuild is loading.
98
+ * @param {string} projectRootNorm Project root, forward-slash normalized.
99
+ * @returns {boolean}
100
+ */
101
+ export function shouldPersist(absPath, projectRootNorm) {
102
+ const p = absPath.replace(/\\/g, '/');
103
+ return p.startsWith(projectRootNorm) && !p.includes('/node_modules/');
104
+ }
105
+
89
106
  /**
90
107
  * Applies the persist transform to a module's source.
91
108
  *
package/sim-server.mjs CHANGED
@@ -34,7 +34,7 @@ const HERE = dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/,
34
34
  const require = createRequire(import.meta.url);
35
35
  const esbuild = require('esbuild');
36
36
  const { bakeAssetPack } = await import(pathToFileURL(resolve(HERE, 'assets/index.mjs')).href);
37
- const { transformPersist } = await import(pathToFileURL(resolve(HERE, 'persist-transform.mjs')).href);
37
+ const { transformPersist, shouldPersist } = await import(pathToFileURL(resolve(HERE, 'persist-transform.mjs')).href);
38
38
 
39
39
  const MIME = {
40
40
  '.html': 'text/html; charset=utf-8',
@@ -127,7 +127,9 @@ function createBundle({ entry, projectRoot, libSrc, nodePaths, outDir, persist =
127
127
  fonts.clear();
128
128
  });
129
129
  b.onLoad({ filter: /\.(jsx?|tsx?)$/ }, (a) => {
130
- if (!persist || !a.path.replace(/\\/g, '/').startsWith(projNorm)) return undefined;
130
+ // Transform ONLY the app's own source — never dependencies (see shouldPersist: excluding
131
+ // node_modules is what stops the library's usePersistentState being rewritten to call itself).
132
+ if (!persist || !shouldPersist(a.path, projNorm)) return undefined;
131
133
  try {
132
134
  return { contents: transformPersist(readFileSync(a.path, 'utf8'), relative(projectRoot, a.path).replace(/\\/g, '/')), loader: 'jsx' };
133
135
  } catch (e) {
@@ -280,8 +280,8 @@ export function loop(animation, config) {
280
280
  const done = once(onComplete);
281
281
  const startValue = resetBeforeIteration && animation._value ? animation._value.__getValue() : null;
282
282
  let count = 0;
283
- const run = (result) => {
284
- if (stopped || !result || result.finished === false) {
283
+ const startIteration = () => {
284
+ if (stopped) {
285
285
  done({ finished: false });
286
286
  return;
287
287
  }
@@ -293,9 +293,22 @@ export function loop(animation, config) {
293
293
  if (resetBeforeIteration && animation._value && startValue != null && count > 1) {
294
294
  animation._value.setValue(startValue);
295
295
  }
296
- animation.start(run);
296
+ animation.start(onIterationDone);
297
+ };
298
+ const onIterationDone = (result) => {
299
+ if (stopped || !result || result.finished === false) {
300
+ done({ finished: false });
301
+ return;
302
+ }
303
+ // Defer the next iteration to a fresh task instead of starting it inline. A child animation can
304
+ // complete *synchronously* (a long-duration timing finishing inside one large catch-up frame in
305
+ // the simulator), and starting the next iteration from within, that completion callback would
306
+ // recurse — loop → child → completion → loop → … — until the stack overflows. setTimeout breaks
307
+ // the chain: the host pump runs it on the next frame, by which point real time has advanced so
308
+ // the animation runs its full duration again. (Negligible cost in the normal async case.)
309
+ setTimeout(startIteration, 0);
297
310
  };
298
- run({ finished: true });
311
+ startIteration();
299
312
  },
300
313
  stop() {
301
314
  stopped = true;