@pyreon/core 0.18.0 → 0.20.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.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"5310d1da-1","name":"lifecycle.ts"},{"uid":"5310d1da-3","name":"component.ts"},{"uid":"5310d1da-5","name":"compat-marker.ts"},{"uid":"5310d1da-7","name":"context.ts"},{"uid":"5310d1da-9","name":"h.ts"},{"uid":"5310d1da-11","name":"dynamic.ts"},{"uid":"5310d1da-13","name":"telemetry.ts"},{"uid":"5310d1da-15","name":"error-boundary.ts"},{"uid":"5310d1da-17","name":"for.ts"},{"uid":"5310d1da-19","name":"ref.ts"},{"uid":"5310d1da-21","name":"defer.ts"},{"uid":"5310d1da-23","name":"lazy.ts"},{"uid":"5310d1da-25","name":"map-array.ts"},{"uid":"5310d1da-27","name":"portal.ts"},{"uid":"5310d1da-29","name":"props.ts"},{"uid":"5310d1da-31","name":"show.ts"},{"uid":"5310d1da-33","name":"style.ts"},{"uid":"5310d1da-35","name":"suspense.ts"},{"uid":"5310d1da-37","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"5310d1da-1":{"renderedLength":3078,"gzipLength":1313,"brotliLength":0,"metaUid":"5310d1da-0"},"5310d1da-3":{"renderedLength":1471,"gzipLength":693,"brotliLength":0,"metaUid":"5310d1da-2"},"5310d1da-5":{"renderedLength":3173,"gzipLength":1409,"brotliLength":0,"metaUid":"5310d1da-4"},"5310d1da-7":{"renderedLength":3600,"gzipLength":1542,"brotliLength":0,"metaUid":"5310d1da-6"},"5310d1da-9":{"renderedLength":1813,"gzipLength":957,"brotliLength":0,"metaUid":"5310d1da-8"},"5310d1da-11":{"renderedLength":490,"gzipLength":291,"brotliLength":0,"metaUid":"5310d1da-10"},"5310d1da-13":{"renderedLength":1208,"gzipLength":633,"brotliLength":0,"metaUid":"5310d1da-12"},"5310d1da-15":{"renderedLength":1659,"gzipLength":843,"brotliLength":0,"metaUid":"5310d1da-14"},"5310d1da-17":{"renderedLength":700,"gzipLength":478,"brotliLength":0,"metaUid":"5310d1da-16"},"5310d1da-19":{"renderedLength":86,"gzipLength":98,"brotliLength":0,"metaUid":"5310d1da-18"},"5310d1da-21":{"renderedLength":3979,"gzipLength":1715,"brotliLength":0,"metaUid":"5310d1da-20"},"5310d1da-23":{"renderedLength":461,"gzipLength":273,"brotliLength":0,"metaUid":"5310d1da-22"},"5310d1da-25":{"renderedLength":1018,"gzipLength":571,"brotliLength":0,"metaUid":"5310d1da-24"},"5310d1da-27":{"renderedLength":818,"gzipLength":491,"brotliLength":0,"metaUid":"5310d1da-26"},"5310d1da-29":{"renderedLength":6310,"gzipLength":2344,"brotliLength":0,"metaUid":"5310d1da-28"},"5310d1da-31":{"renderedLength":2022,"gzipLength":854,"brotliLength":0,"metaUid":"5310d1da-30"},"5310d1da-33":{"renderedLength":1858,"gzipLength":825,"brotliLength":0,"metaUid":"5310d1da-32"},"5310d1da-35":{"renderedLength":1104,"gzipLength":614,"brotliLength":0,"metaUid":"5310d1da-34"},"5310d1da-37":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"5310d1da-36"}},"nodeMetas":{"5310d1da-0":{"id":"/src/lifecycle.ts","moduleParts":{"index.js":"5310d1da-1"},"imported":[],"importedBy":[{"uid":"5310d1da-36"},{"uid":"5310d1da-2"},{"uid":"5310d1da-6"},{"uid":"5310d1da-14"},{"uid":"5310d1da-20"}]},"5310d1da-2":{"id":"/src/component.ts","moduleParts":{"index.js":"5310d1da-3"},"imported":[{"uid":"5310d1da-0"}],"importedBy":[{"uid":"5310d1da-36"},{"uid":"5310d1da-14"}]},"5310d1da-4":{"id":"/src/compat-marker.ts","moduleParts":{"index.js":"5310d1da-5"},"imported":[],"importedBy":[{"uid":"5310d1da-36"},{"uid":"5310d1da-14"}]},"5310d1da-6":{"id":"/src/context.ts","moduleParts":{"index.js":"5310d1da-7"},"imported":[{"uid":"5310d1da-38"},{"uid":"5310d1da-0"}],"importedBy":[{"uid":"5310d1da-36"}]},"5310d1da-8":{"id":"/src/h.ts","moduleParts":{"index.js":"5310d1da-9"},"imported":[],"importedBy":[{"uid":"5310d1da-36"},{"uid":"5310d1da-10"},{"uid":"5310d1da-20"},{"uid":"5310d1da-22"},{"uid":"5310d1da-34"}]},"5310d1da-10":{"id":"/src/dynamic.ts","moduleParts":{"index.js":"5310d1da-11"},"imported":[{"uid":"5310d1da-8"}],"importedBy":[{"uid":"5310d1da-36"}]},"5310d1da-12":{"id":"/src/telemetry.ts","moduleParts":{"index.js":"5310d1da-13"},"imported":[],"importedBy":[{"uid":"5310d1da-36"},{"uid":"5310d1da-14"}]},"5310d1da-14":{"id":"/src/error-boundary.ts","moduleParts":{"index.js":"5310d1da-15"},"imported":[{"uid":"5310d1da-38"},{"uid":"5310d1da-4"},{"uid":"5310d1da-2"},{"uid":"5310d1da-0"},{"uid":"5310d1da-12"}],"importedBy":[{"uid":"5310d1da-36"}]},"5310d1da-16":{"id":"/src/for.ts","moduleParts":{"index.js":"5310d1da-17"},"imported":[],"importedBy":[{"uid":"5310d1da-36"}]},"5310d1da-18":{"id":"/src/ref.ts","moduleParts":{"index.js":"5310d1da-19"},"imported":[],"importedBy":[{"uid":"5310d1da-36"},{"uid":"5310d1da-20"}]},"5310d1da-20":{"id":"/src/defer.ts","moduleParts":{"index.js":"5310d1da-21"},"imported":[{"uid":"5310d1da-38"},{"uid":"5310d1da-8"},{"uid":"5310d1da-0"},{"uid":"5310d1da-18"}],"importedBy":[{"uid":"5310d1da-36"}]},"5310d1da-22":{"id":"/src/lazy.ts","moduleParts":{"index.js":"5310d1da-23"},"imported":[{"uid":"5310d1da-38"},{"uid":"5310d1da-8"}],"importedBy":[{"uid":"5310d1da-36"}]},"5310d1da-24":{"id":"/src/map-array.ts","moduleParts":{"index.js":"5310d1da-25"},"imported":[],"importedBy":[{"uid":"5310d1da-36"}]},"5310d1da-26":{"id":"/src/portal.ts","moduleParts":{"index.js":"5310d1da-27"},"imported":[],"importedBy":[{"uid":"5310d1da-36"}]},"5310d1da-28":{"id":"/src/props.ts","moduleParts":{"index.js":"5310d1da-29"},"imported":[],"importedBy":[{"uid":"5310d1da-36"}]},"5310d1da-30":{"id":"/src/show.ts","moduleParts":{"index.js":"5310d1da-31"},"imported":[],"importedBy":[{"uid":"5310d1da-36"}]},"5310d1da-32":{"id":"/src/style.ts","moduleParts":{"index.js":"5310d1da-33"},"imported":[],"importedBy":[{"uid":"5310d1da-36"}]},"5310d1da-34":{"id":"/src/suspense.ts","moduleParts":{"index.js":"5310d1da-35"},"imported":[{"uid":"5310d1da-8"}],"importedBy":[{"uid":"5310d1da-36"}]},"5310d1da-36":{"id":"/src/index.ts","moduleParts":{"index.js":"5310d1da-37"},"imported":[{"uid":"5310d1da-2"},{"uid":"5310d1da-4"},{"uid":"5310d1da-6"},{"uid":"5310d1da-10"},{"uid":"5310d1da-14"},{"uid":"5310d1da-16"},{"uid":"5310d1da-8"},{"uid":"5310d1da-20"},{"uid":"5310d1da-22"},{"uid":"5310d1da-0"},{"uid":"5310d1da-24"},{"uid":"5310d1da-26"},{"uid":"5310d1da-28"},{"uid":"5310d1da-18"},{"uid":"5310d1da-30"},{"uid":"5310d1da-32"},{"uid":"5310d1da-34"},{"uid":"5310d1da-12"}],"importedBy":[],"isEntry":true},"5310d1da-38":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"5310d1da-6"},{"uid":"5310d1da-14"},{"uid":"5310d1da-20"},{"uid":"5310d1da-22"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"d1de8643-1","name":"lifecycle.ts"},{"uid":"d1de8643-3","name":"component.ts"},{"uid":"d1de8643-5","name":"compat-marker.ts"},{"uid":"d1de8643-7","name":"compat-shared.ts"},{"uid":"d1de8643-9","name":"context.ts"},{"uid":"d1de8643-11","name":"h.ts"},{"uid":"d1de8643-13","name":"dynamic.ts"},{"uid":"d1de8643-15","name":"telemetry.ts"},{"uid":"d1de8643-17","name":"error-boundary.ts"},{"uid":"d1de8643-19","name":"for.ts"},{"uid":"d1de8643-21","name":"ref.ts"},{"uid":"d1de8643-23","name":"defer.ts"},{"uid":"d1de8643-25","name":"lazy.ts"},{"uid":"d1de8643-27","name":"map-array.ts"},{"uid":"d1de8643-29","name":"portal.ts"},{"uid":"d1de8643-31","name":"props.ts"},{"uid":"d1de8643-33","name":"show.ts"},{"uid":"d1de8643-35","name":"style.ts"},{"uid":"d1de8643-37","name":"suspense.ts"},{"uid":"d1de8643-39","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"d1de8643-1":{"renderedLength":3090,"gzipLength":1316,"brotliLength":0,"metaUid":"d1de8643-0"},"d1de8643-3":{"renderedLength":1471,"gzipLength":693,"brotliLength":0,"metaUid":"d1de8643-2"},"d1de8643-5":{"renderedLength":3173,"gzipLength":1409,"brotliLength":0,"metaUid":"d1de8643-4"},"d1de8643-7":{"renderedLength":2346,"gzipLength":1033,"brotliLength":0,"metaUid":"d1de8643-6"},"d1de8643-9":{"renderedLength":3600,"gzipLength":1542,"brotliLength":0,"metaUid":"d1de8643-8"},"d1de8643-11":{"renderedLength":1813,"gzipLength":957,"brotliLength":0,"metaUid":"d1de8643-10"},"d1de8643-13":{"renderedLength":490,"gzipLength":292,"brotliLength":0,"metaUid":"d1de8643-12"},"d1de8643-15":{"renderedLength":1990,"gzipLength":950,"brotliLength":0,"metaUid":"d1de8643-14"},"d1de8643-17":{"renderedLength":1659,"gzipLength":843,"brotliLength":0,"metaUid":"d1de8643-16"},"d1de8643-19":{"renderedLength":700,"gzipLength":478,"brotliLength":0,"metaUid":"d1de8643-18"},"d1de8643-21":{"renderedLength":86,"gzipLength":98,"brotliLength":0,"metaUid":"d1de8643-20"},"d1de8643-23":{"renderedLength":4387,"gzipLength":1891,"brotliLength":0,"metaUid":"d1de8643-22"},"d1de8643-25":{"renderedLength":461,"gzipLength":273,"brotliLength":0,"metaUid":"d1de8643-24"},"d1de8643-27":{"renderedLength":1018,"gzipLength":571,"brotliLength":0,"metaUid":"d1de8643-26"},"d1de8643-29":{"renderedLength":818,"gzipLength":491,"brotliLength":0,"metaUid":"d1de8643-28"},"d1de8643-31":{"renderedLength":6310,"gzipLength":2344,"brotliLength":0,"metaUid":"d1de8643-30"},"d1de8643-33":{"renderedLength":2022,"gzipLength":854,"brotliLength":0,"metaUid":"d1de8643-32"},"d1de8643-35":{"renderedLength":1858,"gzipLength":825,"brotliLength":0,"metaUid":"d1de8643-34"},"d1de8643-37":{"renderedLength":1104,"gzipLength":614,"brotliLength":0,"metaUid":"d1de8643-36"},"d1de8643-39":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"d1de8643-38"}},"nodeMetas":{"d1de8643-0":{"id":"/src/lifecycle.ts","moduleParts":{"index.js":"d1de8643-1"},"imported":[],"importedBy":[{"uid":"d1de8643-38"},{"uid":"d1de8643-2"},{"uid":"d1de8643-8"},{"uid":"d1de8643-16"},{"uid":"d1de8643-22"}]},"d1de8643-2":{"id":"/src/component.ts","moduleParts":{"index.js":"d1de8643-3"},"imported":[{"uid":"d1de8643-0"}],"importedBy":[{"uid":"d1de8643-38"},{"uid":"d1de8643-16"}]},"d1de8643-4":{"id":"/src/compat-marker.ts","moduleParts":{"index.js":"d1de8643-5"},"imported":[],"importedBy":[{"uid":"d1de8643-38"},{"uid":"d1de8643-16"}]},"d1de8643-6":{"id":"/src/compat-shared.ts","moduleParts":{"index.js":"d1de8643-7"},"imported":[],"importedBy":[{"uid":"d1de8643-38"}]},"d1de8643-8":{"id":"/src/context.ts","moduleParts":{"index.js":"d1de8643-9"},"imported":[{"uid":"d1de8643-40"},{"uid":"d1de8643-0"}],"importedBy":[{"uid":"d1de8643-38"}]},"d1de8643-10":{"id":"/src/h.ts","moduleParts":{"index.js":"d1de8643-11"},"imported":[],"importedBy":[{"uid":"d1de8643-38"},{"uid":"d1de8643-12"},{"uid":"d1de8643-22"},{"uid":"d1de8643-24"},{"uid":"d1de8643-36"}]},"d1de8643-12":{"id":"/src/dynamic.ts","moduleParts":{"index.js":"d1de8643-13"},"imported":[{"uid":"d1de8643-10"}],"importedBy":[{"uid":"d1de8643-38"}]},"d1de8643-14":{"id":"/src/telemetry.ts","moduleParts":{"index.js":"d1de8643-15"},"imported":[{"uid":"d1de8643-40"}],"importedBy":[{"uid":"d1de8643-38"},{"uid":"d1de8643-16"}]},"d1de8643-16":{"id":"/src/error-boundary.ts","moduleParts":{"index.js":"d1de8643-17"},"imported":[{"uid":"d1de8643-40"},{"uid":"d1de8643-4"},{"uid":"d1de8643-2"},{"uid":"d1de8643-0"},{"uid":"d1de8643-14"}],"importedBy":[{"uid":"d1de8643-38"}]},"d1de8643-18":{"id":"/src/for.ts","moduleParts":{"index.js":"d1de8643-19"},"imported":[],"importedBy":[{"uid":"d1de8643-38"}]},"d1de8643-20":{"id":"/src/ref.ts","moduleParts":{"index.js":"d1de8643-21"},"imported":[],"importedBy":[{"uid":"d1de8643-38"},{"uid":"d1de8643-22"}]},"d1de8643-22":{"id":"/src/defer.ts","moduleParts":{"index.js":"d1de8643-23"},"imported":[{"uid":"d1de8643-40"},{"uid":"d1de8643-10"},{"uid":"d1de8643-0"},{"uid":"d1de8643-20"}],"importedBy":[{"uid":"d1de8643-38"}]},"d1de8643-24":{"id":"/src/lazy.ts","moduleParts":{"index.js":"d1de8643-25"},"imported":[{"uid":"d1de8643-40"},{"uid":"d1de8643-10"}],"importedBy":[{"uid":"d1de8643-38"}]},"d1de8643-26":{"id":"/src/map-array.ts","moduleParts":{"index.js":"d1de8643-27"},"imported":[],"importedBy":[{"uid":"d1de8643-38"}]},"d1de8643-28":{"id":"/src/portal.ts","moduleParts":{"index.js":"d1de8643-29"},"imported":[],"importedBy":[{"uid":"d1de8643-38"}]},"d1de8643-30":{"id":"/src/props.ts","moduleParts":{"index.js":"d1de8643-31"},"imported":[],"importedBy":[{"uid":"d1de8643-38"}]},"d1de8643-32":{"id":"/src/show.ts","moduleParts":{"index.js":"d1de8643-33"},"imported":[],"importedBy":[{"uid":"d1de8643-38"}]},"d1de8643-34":{"id":"/src/style.ts","moduleParts":{"index.js":"d1de8643-35"},"imported":[],"importedBy":[{"uid":"d1de8643-38"}]},"d1de8643-36":{"id":"/src/suspense.ts","moduleParts":{"index.js":"d1de8643-37"},"imported":[{"uid":"d1de8643-10"}],"importedBy":[{"uid":"d1de8643-38"}]},"d1de8643-38":{"id":"/src/index.ts","moduleParts":{"index.js":"d1de8643-39"},"imported":[{"uid":"d1de8643-2"},{"uid":"d1de8643-4"},{"uid":"d1de8643-6"},{"uid":"d1de8643-8"},{"uid":"d1de8643-12"},{"uid":"d1de8643-16"},{"uid":"d1de8643-18"},{"uid":"d1de8643-10"},{"uid":"d1de8643-22"},{"uid":"d1de8643-24"},{"uid":"d1de8643-0"},{"uid":"d1de8643-26"},{"uid":"d1de8643-28"},{"uid":"d1de8643-30"},{"uid":"d1de8643-20"},{"uid":"d1de8643-32"},{"uid":"d1de8643-34"},{"uid":"d1de8643-36"},{"uid":"d1de8643-14"}],"importedBy":[],"isEntry":true},"d1de8643-40":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"d1de8643-8"},{"uid":"d1de8643-16"},{"uid":"d1de8643-22"},{"uid":"d1de8643-24"},{"uid":"d1de8643-14"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { effect, setSnapshotCapture, signal } from "@pyreon/reactivity";
1
+ import { effect, getReactiveTrace, setSnapshotCapture, signal } from "@pyreon/reactivity";
2
2
 
3
3
  //#region src/lifecycle.ts
4
- const __DEV__$4 = process.env.NODE_ENV !== "production";
4
+ const __DEV__$5 = process.env.NODE_ENV !== "production";
5
5
  let _current = null;
6
6
  function setCurrentHooks(hooks) {
7
7
  _current = hooks;
@@ -40,10 +40,10 @@ function captureCallSite() {
40
40
  return "";
41
41
  }
42
42
  function warnOutsideSetup(hookName) {
43
- if (__DEV__$4 && !_current) {
43
+ if (__DEV__$5 && !_current) {
44
44
  const callSite = captureCallSite();
45
- const location = callSite ? `\n Called from: ${callSite}` : "";
46
- console.warn(`[Pyreon] ${hookName}() called outside component setup. Lifecycle hooks must be called synchronously during a component's setup function.` + location + (hookName === "onUnmount" ? "\n Hint: `provide()` internally calls onUnmount(). If you use provide(), ensure it runs during synchronous component setup — not inside effects, callbacks, or after awaits." : ""));
45
+ const callSiteSuffix = callSite ? `\n Called from: ${callSite}` : "";
46
+ console.warn(`[Pyreon] ${hookName}() called outside component setup. Lifecycle hooks must be called synchronously during a component's setup function.` + callSiteSuffix + (hookName === "onUnmount" ? "\n Hint: `provide()` internally calls onUnmount(). If you use provide(), ensure it runs during synchronous component setup — not inside effects, callbacks, or after awaits." : ""));
47
47
  }
48
48
  }
49
49
  /**
@@ -231,6 +231,71 @@ function isNativeCompat(fn) {
231
231
  return typeof fn === "function" && fn[NATIVE_COMPAT_MARKER] === true;
232
232
  }
233
233
 
234
+ //#endregion
235
+ //#region src/compat-shared.ts
236
+ /**
237
+ * Code shared by the framework-compat JSX runtimes
238
+ * (`@pyreon/react-compat`, `@pyreon/preact-compat`).
239
+ *
240
+ * These helpers were previously copy-pasted byte-for-byte into both
241
+ * packages. `@pyreon/core` is the correct single home — it's already a
242
+ * dependency of every compat package and already hosts the sibling
243
+ * cross-compat module `compat-marker.ts` (`nativeCompat` / `isNativeCompat`).
244
+ */
245
+ /**
246
+ * Shallow props comparison used by compat `memo()` / `useState` bailout.
247
+ * Same-length key sets with `Object.is`-equal values → equal.
248
+ */
249
+ function shallowEqualProps(a, b) {
250
+ const keysA = Object.keys(a);
251
+ const keysB = Object.keys(b);
252
+ if (keysA.length !== keysB.length) return false;
253
+ for (const k of keysA) if (!Object.is(a[k], b[k])) return false;
254
+ return true;
255
+ }
256
+ /**
257
+ * Map React/Preact-style DOM attributes to standard HTML attributes,
258
+ * mutating `props` in place. No-op when `type` is not a host string
259
+ * (component vnodes keep their props untouched).
260
+ *
261
+ * The React and Preact variants were identical apart from React also
262
+ * stripping `suppressContentEditableWarning`. Both keys are React/Preact
263
+ * authoring-only and never valid DOM attributes, so always stripping
264
+ * both is behavior-preserving for Preact (the key is never set there;
265
+ * `delete` of an absent key is a no-op) and removes the only divergence.
266
+ */
267
+ function mapCompatDomProps(props, type) {
268
+ if (typeof type !== "string") return;
269
+ if (props.className !== void 0) {
270
+ props.class = props.className;
271
+ delete props.className;
272
+ }
273
+ if (props.htmlFor !== void 0) {
274
+ props.for = props.htmlFor;
275
+ delete props.htmlFor;
276
+ }
277
+ if ((type === "input" || type === "textarea" || type === "select") && props.onChange !== void 0) {
278
+ if (props.onInput === void 0) props.onInput = props.onChange;
279
+ delete props.onChange;
280
+ }
281
+ if (props.autoFocus !== void 0) {
282
+ props.autofocus = props.autoFocus;
283
+ delete props.autoFocus;
284
+ }
285
+ if (type === "input" || type === "textarea") {
286
+ if (props.defaultValue !== void 0 && props.value === void 0) {
287
+ props.value = props.defaultValue;
288
+ delete props.defaultValue;
289
+ }
290
+ if (props.defaultChecked !== void 0 && props.checked === void 0) {
291
+ props.checked = props.defaultChecked;
292
+ delete props.defaultChecked;
293
+ }
294
+ }
295
+ delete props.suppressHydrationWarning;
296
+ delete props.suppressContentEditableWarning;
297
+ }
298
+
234
299
  //#endregion
235
300
  //#region src/context.ts
236
301
  /**
@@ -396,10 +461,10 @@ function flattenChildren(children) {
396
461
 
397
462
  //#endregion
398
463
  //#region src/dynamic.ts
399
- const __DEV__$3 = process.env.NODE_ENV !== "production";
464
+ const __DEV__$4 = process.env.NODE_ENV !== "production";
400
465
  function Dynamic(props) {
401
466
  const { component, children, ...rest } = props;
402
- if (__DEV__$3 && !component) console.warn("[Pyreon] <Dynamic> received a falsy `component` prop. Nothing will be rendered.");
467
+ if (__DEV__$4 && !component) console.warn("[Pyreon] <Dynamic> received a falsy `component` prop. Nothing will be rendered.");
403
468
  if (!component) return null;
404
469
  if (children === void 0) return h(component, rest);
405
470
  if (Array.isArray(children)) return h(component, rest, ...children);
@@ -408,6 +473,24 @@ function Dynamic(props) {
408
473
 
409
474
  //#endregion
410
475
  //#region src/telemetry.ts
476
+ /**
477
+ * Error telemetry — hook into Pyreon's error reporting for Sentry, Datadog, etc.
478
+ *
479
+ * Captures errors from ALL lifecycle phases including reactive effects.
480
+ * `effect()` errors thrown by `@pyreon/reactivity` are bridged through a
481
+ * globalThis sink (no upward import — reactivity doesn't depend on core).
482
+ *
483
+ * @example
484
+ * import { registerErrorHandler } from "@pyreon/core"
485
+ * import * as Sentry from "@sentry/browser"
486
+ *
487
+ * registerErrorHandler(ctx => {
488
+ * Sentry.captureException(ctx.error, {
489
+ * extra: { component: ctx.component, phase: ctx.phase },
490
+ * })
491
+ * })
492
+ */
493
+ const __DEV__$3 = process.env.NODE_ENV !== "production";
411
494
  let _handlers = [];
412
495
  /**
413
496
  * Register a global error handler. Called whenever a component throws in any
@@ -431,6 +514,10 @@ function registerErrorHandler(handler) {
431
514
  * Existing console.error calls are preserved; this is additive.
432
515
  */
433
516
  function reportError(ctx) {
517
+ if (__DEV__$3 && ctx.reactiveTrace === void 0) try {
518
+ const trace = getReactiveTrace();
519
+ if (trace.length > 0) ctx.reactiveTrace = trace;
520
+ } catch {}
434
521
  for (const h of _handlers) try {
435
522
  h(ctx);
436
523
  } catch {}
@@ -617,6 +704,11 @@ function Defer(props) {
617
704
  const startLoad = () => {
618
705
  if (loadStarted) return;
619
706
  loadStarted = true;
707
+ if (!props.chunk) {
708
+ const err = /* @__PURE__ */ new Error("[Pyreon] <Defer> has no `chunk` prop. Either pass `chunk={() => import(\"...\")}` (explicit form), or use the inline form `<Defer when={...}><Component /></Defer>` with `@pyreon/vite-plugin` enabled — the compiler rewrites inline JSX to an explicit chunk-prop call.");
709
+ Failed.set(err);
710
+ return;
711
+ }
620
712
  props.chunk().then((mod) => {
621
713
  const Comp = typeof mod === "function" ? mod : mod.default;
622
714
  if (__DEV__$1 && typeof Comp !== "function") {
@@ -639,7 +731,9 @@ function Defer(props) {
639
731
  if (err) throw err;
640
732
  const Comp = Loaded();
641
733
  if (!Comp) return props.fallback ?? null;
642
- return props.children ? props.children(Comp) : h(Comp, {});
734
+ const ch = props.children;
735
+ if (typeof ch === "function") return ch(Comp);
736
+ return h(Comp, {});
643
737
  };
644
738
  if ("on" in props && props.on === "visible") {
645
739
  const containerRef = createRef();
@@ -1104,5 +1198,5 @@ function Suspense(props) {
1104
1198
  }
1105
1199
 
1106
1200
  //#endregion
1107
- export { CSS_UNITLESS, Defer, Dynamic, EMPTY_PROPS, ErrorBoundary, For, ForSymbol, Fragment, Match, MatchSymbol, NATIVE_COMPAT_MARKER, Portal, PortalSymbol, REACTIVE_PROP, Show, Suspense, Switch, _rp, _wrapSpread, captureContextStack, createContext, createReactiveContext, createRef, createUniqueId, cx, defineComponent, dispatchToErrorBoundary, h, isNativeCompat, lazy, makeReactiveProps, mapArray, mergeProps, nativeCompat, normalizeStyleValue, onErrorCaptured, onMount, onUnmount, onUpdate, popContext, propagateError, provide, pushContext, registerErrorHandler, reportError, restoreContextStack, runWithHooks, setContextStackProvider, splitProps, toKebabCase, useContext, withContext };
1201
+ export { CSS_UNITLESS, Defer, Dynamic, EMPTY_PROPS, ErrorBoundary, For, ForSymbol, Fragment, Match, MatchSymbol, NATIVE_COMPAT_MARKER, Portal, PortalSymbol, REACTIVE_PROP, Show, Suspense, Switch, _rp, _wrapSpread, captureContextStack, createContext, createReactiveContext, createRef, createUniqueId, cx, defineComponent, dispatchToErrorBoundary, h, isNativeCompat, lazy, makeReactiveProps, mapArray, mapCompatDomProps, mergeProps, nativeCompat, normalizeStyleValue, onErrorCaptured, onMount, onUnmount, onUpdate, popContext, propagateError, provide, pushContext, registerErrorHandler, reportError, restoreContextStack, runWithHooks, setContextStackProvider, shallowEqualProps, splitProps, toKebabCase, useContext, withContext };
1108
1202
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,5 @@
1
+ import { ReactiveTraceEntry } from "@pyreon/reactivity";
2
+
1
3
  //#region src/types.d.ts
2
4
  type VNodeChildAtom = VNode | string | number | boolean | null | undefined;
3
5
  /** Reactive accessor — TS checks this arm FIRST so `{() => cond && <X />}` resolves correctly */
@@ -176,6 +178,34 @@ declare function nativeCompat<T>(fn: T): T;
176
178
  */
177
179
  declare function isNativeCompat(fn: unknown): boolean;
178
180
  //#endregion
181
+ //#region src/compat-shared.d.ts
182
+ /**
183
+ * Code shared by the framework-compat JSX runtimes
184
+ * (`@pyreon/react-compat`, `@pyreon/preact-compat`).
185
+ *
186
+ * These helpers were previously copy-pasted byte-for-byte into both
187
+ * packages. `@pyreon/core` is the correct single home — it's already a
188
+ * dependency of every compat package and already hosts the sibling
189
+ * cross-compat module `compat-marker.ts` (`nativeCompat` / `isNativeCompat`).
190
+ */
191
+ /**
192
+ * Shallow props comparison used by compat `memo()` / `useState` bailout.
193
+ * Same-length key sets with `Object.is`-equal values → equal.
194
+ */
195
+ declare function shallowEqualProps<P extends Record<string, unknown>>(a: P, b: P): boolean;
196
+ /**
197
+ * Map React/Preact-style DOM attributes to standard HTML attributes,
198
+ * mutating `props` in place. No-op when `type` is not a host string
199
+ * (component vnodes keep their props untouched).
200
+ *
201
+ * The React and Preact variants were identical apart from React also
202
+ * stripping `suppressContentEditableWarning`. Both keys are React/Preact
203
+ * authoring-only and never valid DOM attributes, so always stripping
204
+ * both is behavior-preserving for Preact (the key is never set there;
205
+ * `delete` of an absent key is a no-op) and removes the only divergence.
206
+ */
207
+ declare function mapCompatDomProps(props: Record<string, unknown>, type: unknown): void;
208
+ //#endregion
179
209
  //#region src/context.d.ts
180
210
  /**
181
211
  * Provide / inject — like React context or Vue provide/inject.
@@ -1047,14 +1077,27 @@ type DeferProps<P extends Props> = DeferTrigger & {
1047
1077
  * Dynamic import to lazy-load. The literal `import('./X')` is what
1048
1078
  * Rolldown / Vite see when emitting chunks — using a variable here
1049
1079
  * defeats code splitting.
1080
+ *
1081
+ * Typed as optional ONLY because the compiler-driven inline form
1082
+ * (`<Defer when={x}><Modal /></Defer>`) doesn't include a `chunk`
1083
+ * prop at source level — `@pyreon/compiler`'s `transformDeferInline`
1084
+ * synthesizes it before runtime. Authors using the explicit form
1085
+ * must pass `chunk` — runtime throws a clear dev-mode error when
1086
+ * the trigger fires and `chunk` is missing.
1050
1087
  */
1051
- chunk: () => Promise<ChunkResult<P>>;
1088
+ chunk?: () => Promise<ChunkResult<P>>;
1052
1089
  /**
1053
- * Render-prop for the loaded component. Receives the resolved component
1054
- * and returns its JSX with whatever props the parent needs to pass.
1055
- * Optional omitting it renders `<Comp />` with no props.
1090
+ * Children accept TWO shapes:
1091
+ * 1. Render-prop `(Component) => VNodeChild` the explicit form.
1092
+ * Receives the loaded component, lets the author pass props.
1093
+ * 2. Inline JSX (`<Defer when={x}><Modal /></Defer>`) — the compiler-
1094
+ * driven form. The compiler extracts the subtree into a chunk
1095
+ * and rewrites this to the render-prop form before runtime.
1096
+ *
1097
+ * Type widening is necessary because TypeScript checks the raw source
1098
+ * BEFORE the compiler pass runs — both shapes must typecheck.
1056
1099
  */
1057
- children?: (Component: ComponentFn<P>) => VNodeChild; /** Shown while the chunk is loading. Default: `null`. */
1100
+ children?: ((Component: ComponentFn<P>) => VNodeChild) | VNodeChild; /** Shown while the chunk is loading. Default: `null`. */
1058
1101
  fallback?: VNodeChild;
1059
1102
  /**
1060
1103
  * IntersectionObserver `rootMargin` for `on="visible"` mode. Default
@@ -1327,23 +1370,6 @@ declare function Switch(props: SwitchProps): VNode | null;
1327
1370
  declare const MatchSymbol: unique symbol;
1328
1371
  //#endregion
1329
1372
  //#region src/telemetry.d.ts
1330
- /**
1331
- * Error telemetry — hook into Pyreon's error reporting for Sentry, Datadog, etc.
1332
- *
1333
- * Captures errors from ALL lifecycle phases including reactive effects.
1334
- * `effect()` errors thrown by `@pyreon/reactivity` are bridged through a
1335
- * globalThis sink (no upward import — reactivity doesn't depend on core).
1336
- *
1337
- * @example
1338
- * import { registerErrorHandler } from "@pyreon/core"
1339
- * import * as Sentry from "@sentry/browser"
1340
- *
1341
- * registerErrorHandler(ctx => {
1342
- * Sentry.captureException(ctx.error, {
1343
- * extra: { component: ctx.component, phase: ctx.phase },
1344
- * })
1345
- * })
1346
- */
1347
1373
  interface ErrorContext {
1348
1374
  /** Component function name, "Anonymous", or "Effect" for reactive effects */
1349
1375
  component: string;
@@ -1355,6 +1381,22 @@ interface ErrorContext {
1355
1381
  timestamp: number;
1356
1382
  /** Component props at the time of the error */
1357
1383
  props?: Record<string, unknown>;
1384
+ /**
1385
+ * The last N signal writes (chronological, oldest → newest) leading
1386
+ * up to the error — the causal sequence of reactive state changes,
1387
+ * not a point-in-time snapshot. Each entry is `{ name, prev, next,
1388
+ * timestamp }` with `prev` / `next` as bounded string previews.
1389
+ *
1390
+ * Populated automatically in development from `@pyreon/reactivity`'s
1391
+ * dev-only ring buffer. **`undefined` in production** — the recorder
1392
+ * feeding the buffer tree-shakes out of prod bundles, so the cost is
1393
+ * zero and the field is simply absent.
1394
+ *
1395
+ * For a signal framework this answers the first question a crash
1396
+ * raises — "what reactive state changed in the run-up?" — that the
1397
+ * thrown value + stack alone can't.
1398
+ */
1399
+ reactiveTrace?: ReactiveTraceEntry[];
1358
1400
  }
1359
1401
  type ErrorHandler = (ctx: ErrorContext) => void;
1360
1402
  /**
@@ -1374,5 +1416,5 @@ declare function registerErrorHandler(handler: ErrorHandler): () => void;
1374
1416
  */
1375
1417
  declare function reportError(ctx: ErrorContext): void;
1376
1418
  //#endregion
1377
- export { type AnchorAttributes, type ButtonAttributes, type CSSProperties, CSS_UNITLESS, type ClassValue, type CleanupFn, type ComponentFn, type ComponentInstance, type Context, type ContextSnapshot, Defer, type DeferProps, Dynamic, type DynamicProps, EMPTY_PROPS, ErrorBoundary, type ErrorContext, type ErrorHandler, type ExtractProps, For, type ForProps, ForSymbol, type FormAttributes, Fragment, type HigherOrderComponent, type ImgAttributes, type InputAttributes, type LazyComponent, type LifecycleHooks, Match, type MatchProps, MatchSymbol, NATIVE_COMPAT_MARKER, type NativeItem, Portal, type PortalProps, PortalSymbol, type Props, type PyreonHTMLAttributes, REACTIVE_PROP, type ReactiveContext, type Ref, type RefCallback, type RefProp, type SelectAttributes, Show, type ShowProps, type StyleValue, Suspense, type SvgAttributes, Switch, type SwitchProps, type TargetedEvent, type TextareaAttributes, type VNode, type VNodeChild, type VNodeChildAccessor, type VNodeChildAtom, _rp, _wrapSpread, captureContextStack, createContext, createReactiveContext, createRef, createUniqueId, cx, defineComponent, dispatchToErrorBoundary, h, isNativeCompat, lazy, makeReactiveProps, mapArray, mergeProps, nativeCompat, normalizeStyleValue, onErrorCaptured, onMount, onUnmount, onUpdate, popContext, propagateError, provide, pushContext, registerErrorHandler, reportError, restoreContextStack, runWithHooks, setContextStackProvider, splitProps, toKebabCase, useContext, withContext };
1419
+ export { type AnchorAttributes, type ButtonAttributes, type CSSProperties, CSS_UNITLESS, type ClassValue, type CleanupFn, type ComponentFn, type ComponentInstance, type Context, type ContextSnapshot, Defer, type DeferProps, Dynamic, type DynamicProps, EMPTY_PROPS, ErrorBoundary, type ErrorContext, type ErrorHandler, type ExtractProps, For, type ForProps, ForSymbol, type FormAttributes, Fragment, type HigherOrderComponent, type ImgAttributes, type InputAttributes, type LazyComponent, type LifecycleHooks, Match, type MatchProps, MatchSymbol, NATIVE_COMPAT_MARKER, type NativeItem, Portal, type PortalProps, PortalSymbol, type Props, type PyreonHTMLAttributes, REACTIVE_PROP, type ReactiveContext, type ReactiveTraceEntry, type Ref, type RefCallback, type RefProp, type SelectAttributes, Show, type ShowProps, type StyleValue, Suspense, type SvgAttributes, Switch, type SwitchProps, type TargetedEvent, type TextareaAttributes, type VNode, type VNodeChild, type VNodeChildAccessor, type VNodeChildAtom, _rp, _wrapSpread, captureContextStack, createContext, createReactiveContext, createRef, createUniqueId, cx, defineComponent, dispatchToErrorBoundary, h, isNativeCompat, lazy, makeReactiveProps, mapArray, mapCompatDomProps, mergeProps, nativeCompat, normalizeStyleValue, onErrorCaptured, onMount, onUnmount, onUpdate, popContext, propagateError, provide, pushContext, registerErrorHandler, reportError, restoreContextStack, runWithHooks, setContextStackProvider, shallowEqualProps, splitProps, toKebabCase, useContext, withContext };
1378
1420
  //# sourceMappingURL=index2.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/core",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Core component model and lifecycle for Pyreon",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/core#readme",
6
6
  "bugs": {
@@ -53,7 +53,7 @@
53
53
  "prepublishOnly": "bun run build"
54
54
  },
55
55
  "dependencies": {
56
- "@pyreon/reactivity": "^0.18.0"
56
+ "@pyreon/reactivity": "^0.20.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@pyreon/manifest": "0.13.1"
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Code shared by the framework-compat JSX runtimes
3
+ * (`@pyreon/react-compat`, `@pyreon/preact-compat`).
4
+ *
5
+ * These helpers were previously copy-pasted byte-for-byte into both
6
+ * packages. `@pyreon/core` is the correct single home — it's already a
7
+ * dependency of every compat package and already hosts the sibling
8
+ * cross-compat module `compat-marker.ts` (`nativeCompat` / `isNativeCompat`).
9
+ */
10
+
11
+ /**
12
+ * Shallow props comparison used by compat `memo()` / `useState` bailout.
13
+ * Same-length key sets with `Object.is`-equal values → equal.
14
+ */
15
+ export function shallowEqualProps<P extends Record<string, unknown>>(a: P, b: P): boolean {
16
+ const keysA = Object.keys(a)
17
+ const keysB = Object.keys(b)
18
+ if (keysA.length !== keysB.length) return false
19
+ for (const k of keysA) {
20
+ if (!Object.is(a[k], b[k])) return false
21
+ }
22
+ return true
23
+ }
24
+
25
+ /**
26
+ * Map React/Preact-style DOM attributes to standard HTML attributes,
27
+ * mutating `props` in place. No-op when `type` is not a host string
28
+ * (component vnodes keep their props untouched).
29
+ *
30
+ * The React and Preact variants were identical apart from React also
31
+ * stripping `suppressContentEditableWarning`. Both keys are React/Preact
32
+ * authoring-only and never valid DOM attributes, so always stripping
33
+ * both is behavior-preserving for Preact (the key is never set there;
34
+ * `delete` of an absent key is a no-op) and removes the only divergence.
35
+ */
36
+ export function mapCompatDomProps(props: Record<string, unknown>, type: unknown): void {
37
+ if (typeof type !== 'string') return
38
+
39
+ if (props.className !== undefined) {
40
+ props.class = props.className
41
+ delete props.className
42
+ }
43
+ if (props.htmlFor !== undefined) {
44
+ props.for = props.htmlFor
45
+ delete props.htmlFor
46
+ }
47
+
48
+ // React/Preact onChange fires on every keystroke for form elements (like onInput)
49
+ if (
50
+ (type === 'input' || type === 'textarea' || type === 'select') &&
51
+ props.onChange !== undefined
52
+ ) {
53
+ if (props.onInput === undefined) {
54
+ props.onInput = props.onChange
55
+ }
56
+ delete props.onChange
57
+ }
58
+
59
+ // autoFocus → autofocus
60
+ if (props.autoFocus !== undefined) {
61
+ props.autofocus = props.autoFocus
62
+ delete props.autoFocus
63
+ }
64
+
65
+ // defaultValue / defaultChecked → value / checked when no controlled value
66
+ if (type === 'input' || type === 'textarea') {
67
+ if (props.defaultValue !== undefined && props.value === undefined) {
68
+ props.value = props.defaultValue
69
+ delete props.defaultValue
70
+ }
71
+ if (props.defaultChecked !== undefined && props.checked === undefined) {
72
+ props.checked = props.defaultChecked
73
+ delete props.defaultChecked
74
+ }
75
+ }
76
+
77
+ // Strip authoring-only props that have no DOM equivalent
78
+ delete props.suppressHydrationWarning
79
+ delete props.suppressContentEditableWarning
80
+ }
package/src/defer.ts CHANGED
@@ -88,14 +88,27 @@ export type DeferProps<P extends Props> = DeferTrigger & {
88
88
  * Dynamic import to lazy-load. The literal `import('./X')` is what
89
89
  * Rolldown / Vite see when emitting chunks — using a variable here
90
90
  * defeats code splitting.
91
+ *
92
+ * Typed as optional ONLY because the compiler-driven inline form
93
+ * (`<Defer when={x}><Modal /></Defer>`) doesn't include a `chunk`
94
+ * prop at source level — `@pyreon/compiler`'s `transformDeferInline`
95
+ * synthesizes it before runtime. Authors using the explicit form
96
+ * must pass `chunk` — runtime throws a clear dev-mode error when
97
+ * the trigger fires and `chunk` is missing.
91
98
  */
92
- chunk: () => Promise<ChunkResult<P>>
99
+ chunk?: () => Promise<ChunkResult<P>>
93
100
  /**
94
- * Render-prop for the loaded component. Receives the resolved component
95
- * and returns its JSX with whatever props the parent needs to pass.
96
- * Optional omitting it renders `<Comp />` with no props.
101
+ * Children accept TWO shapes:
102
+ * 1. Render-prop `(Component) => VNodeChild` the explicit form.
103
+ * Receives the loaded component, lets the author pass props.
104
+ * 2. Inline JSX (`<Defer when={x}><Modal /></Defer>`) — the compiler-
105
+ * driven form. The compiler extracts the subtree into a chunk
106
+ * and rewrites this to the render-prop form before runtime.
107
+ *
108
+ * Type widening is necessary because TypeScript checks the raw source
109
+ * BEFORE the compiler pass runs — both shapes must typecheck.
97
110
  */
98
- children?: (Component: ComponentFn<P>) => VNodeChild
111
+ children?: ((Component: ComponentFn<P>) => VNodeChild) | VNodeChild
99
112
  /** Shown while the chunk is loading. Default: `null`. */
100
113
  fallback?: VNodeChild
101
114
  /**
@@ -146,6 +159,20 @@ export function Defer<P extends Props>(props: DeferProps<P>): VNode {
146
159
  const startLoad = (): void => {
147
160
  if (loadStarted) return
148
161
  loadStarted = true
162
+ if (!props.chunk) {
163
+ // Missing chunk = either the user is hand-writing the inline form
164
+ // without the compiler pass running, or they wrote the explicit
165
+ // form and forgot to pass chunk. Either way, error early with an
166
+ // actionable message instead of crashing later inside the `.then`.
167
+ const err = new Error(
168
+ '[Pyreon] <Defer> has no `chunk` prop. Either pass `chunk={() => import("...")}` ' +
169
+ '(explicit form), or use the inline form `<Defer when={...}><Component /></Defer>` ' +
170
+ 'with `@pyreon/vite-plugin` enabled — the compiler rewrites inline JSX to ' +
171
+ 'an explicit chunk-prop call.',
172
+ )
173
+ Failed.set(err)
174
+ return
175
+ }
149
176
  props
150
177
  .chunk()
151
178
  .then((mod) => {
@@ -199,7 +226,18 @@ export function Defer<P extends Props>(props: DeferProps<P>): VNode {
199
226
  if (err) throw err
200
227
  const Comp = Loaded()
201
228
  if (!Comp) return props.fallback ?? null
202
- return props.children ? props.children(Comp) : h(Comp as ComponentFn, {})
229
+ // children is widened to `VNodeChild | render-prop` so the compiler-
230
+ // driven inline form (where author writes `<Defer ...><Modal /></Defer>`)
231
+ // typechecks at source level. At RUNTIME children is always either
232
+ // undefined OR the render-prop — the compiler rewrites the inline
233
+ // form's JSX children to a render-prop before this code runs.
234
+ // A non-function children at runtime means the user is invoking the
235
+ // inline form without the compiler pass (e.g. running tests through
236
+ // a bundler that doesn't include `@pyreon/vite-plugin`) — in that
237
+ // case we render `<Comp />` with no props as a best-effort fallback.
238
+ const ch = props.children
239
+ if (typeof ch === 'function') return ch(Comp)
240
+ return h(Comp as ComponentFn, {})
203
241
  }
204
242
 
205
243
  if ('on' in props && props.on === 'visible') {
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  export { defineComponent, dispatchToErrorBoundary, propagateError, runWithHooks } from './component'
4
4
  export { isNativeCompat, NATIVE_COMPAT_MARKER, nativeCompat } from './compat-marker'
5
+ export { mapCompatDomProps, shallowEqualProps } from './compat-shared'
5
6
  export type { Context, ContextSnapshot, ReactiveContext } from './context'
6
7
  export {
7
8
  captureContextStack,
@@ -59,7 +60,7 @@ export type { ClassValue } from './style'
59
60
  export { CSS_UNITLESS, cx, normalizeStyleValue, toKebabCase } from './style'
60
61
  export type { LazyComponent } from './suspense'
61
62
  export { Suspense } from './suspense'
62
- export type { ErrorContext, ErrorHandler } from './telemetry'
63
+ export type { ErrorContext, ErrorHandler, ReactiveTraceEntry } from './telemetry'
63
64
  export { registerErrorHandler, reportError } from './telemetry'
64
65
  export type {
65
66
  CleanupFn,
package/src/lifecycle.ts CHANGED
@@ -59,12 +59,14 @@ function captureCallSite(): string {
59
59
  function warnOutsideSetup(hookName: string): void {
60
60
  if (__DEV__ && !_current) {
61
61
  const callSite = captureCallSite()
62
- const location = callSite ? `\n Called from: ${callSite}` : ''
62
+ // Local name must NOT shadow the `location` browser global (poor
63
+ // hygiene + trips SSR static analysis into a false positive).
64
+ const callSiteSuffix = callSite ? `\n Called from: ${callSite}` : ''
63
65
  // oxlint-disable-next-line no-console
64
66
  console.warn(
65
67
  `[Pyreon] ${hookName}() called outside component setup. ` +
66
68
  "Lifecycle hooks must be called synchronously during a component's setup function." +
67
- location +
69
+ callSiteSuffix +
68
70
  (hookName === 'onUnmount'
69
71
  ? '\n Hint: `provide()` internally calls onUnmount(). If you use provide(), ensure it runs during synchronous component setup — not inside effects, callbacks, or after awaits.'
70
72
  : ''),
package/src/telemetry.ts CHANGED
@@ -16,6 +16,13 @@
16
16
  * })
17
17
  */
18
18
 
19
+ import { getReactiveTrace, type ReactiveTraceEntry } from '@pyreon/reactivity'
20
+
21
+ // Bundler-agnostic dev gate (see pyreon/no-process-dev-gate).
22
+ const __DEV__ = process.env.NODE_ENV !== 'production'
23
+
24
+ export type { ReactiveTraceEntry }
25
+
19
26
  export interface ErrorContext {
20
27
  /** Component function name, "Anonymous", or "Effect" for reactive effects */
21
28
  component: string
@@ -27,6 +34,22 @@ export interface ErrorContext {
27
34
  timestamp: number
28
35
  /** Component props at the time of the error */
29
36
  props?: Record<string, unknown>
37
+ /**
38
+ * The last N signal writes (chronological, oldest → newest) leading
39
+ * up to the error — the causal sequence of reactive state changes,
40
+ * not a point-in-time snapshot. Each entry is `{ name, prev, next,
41
+ * timestamp }` with `prev` / `next` as bounded string previews.
42
+ *
43
+ * Populated automatically in development from `@pyreon/reactivity`'s
44
+ * dev-only ring buffer. **`undefined` in production** — the recorder
45
+ * feeding the buffer tree-shakes out of prod bundles, so the cost is
46
+ * zero and the field is simply absent.
47
+ *
48
+ * For a signal framework this answers the first question a crash
49
+ * raises — "what reactive state changed in the run-up?" — that the
50
+ * thrown value + stack alone can't.
51
+ */
52
+ reactiveTrace?: ReactiveTraceEntry[]
30
53
  }
31
54
 
32
55
  export type ErrorHandler = (ctx: ErrorContext) => void
@@ -56,6 +79,20 @@ export function registerErrorHandler(handler: ErrorHandler): () => void {
56
79
  * Existing console.error calls are preserved; this is additive.
57
80
  */
58
81
  export function reportError(ctx: ErrorContext): void {
82
+ // Enrich with the recent-signal-write trace so every handler (Sentry,
83
+ // Datadog, console) gets the causal reactive sequence for free. Only
84
+ // when the caller didn't already supply one, and only in dev — the
85
+ // gate lets the `getReactiveTrace` call (and the buffer behind it)
86
+ // tree-shake out of production. A throwing/empty trace must never
87
+ // block error reporting, so it's best-effort.
88
+ if (__DEV__ && ctx.reactiveTrace === undefined) {
89
+ try {
90
+ const trace = getReactiveTrace()
91
+ if (trace.length > 0) ctx.reactiveTrace = trace
92
+ } catch {
93
+ // Trace capture is diagnostic — never let it swallow the real error.
94
+ }
95
+ }
59
96
  for (const h of _handlers) {
60
97
  try {
61
98
  h(ctx)
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mapCompatDomProps, shallowEqualProps } from '../compat-shared'
3
+
4
+ describe('shallowEqualProps', () => {
5
+ it('equal for same-key same-value objects', () => {
6
+ expect(shallowEqualProps({ a: 1, b: 'x' }, { a: 1, b: 'x' })).toBe(true)
7
+ })
8
+
9
+ it('not equal when a value differs', () => {
10
+ expect(shallowEqualProps({ a: 1 }, { a: 2 })).toBe(false)
11
+ })
12
+
13
+ it('not equal when key counts differ', () => {
14
+ expect(shallowEqualProps({ a: 1 }, { a: 1, b: 2 })).toBe(false)
15
+ })
16
+
17
+ it('uses Object.is semantics (NaN equal, ±0 distinct)', () => {
18
+ expect(shallowEqualProps({ n: NaN }, { n: NaN })).toBe(true)
19
+ expect(shallowEqualProps({ z: 0 }, { z: -0 })).toBe(false)
20
+ })
21
+
22
+ it('empty objects are equal', () => {
23
+ expect(shallowEqualProps({}, {})).toBe(true)
24
+ })
25
+ })
26
+
27
+ describe('mapCompatDomProps', () => {
28
+ it('no-op for component (non-string) type', () => {
29
+ const Comp = () => null
30
+ const p: Record<string, unknown> = { className: 'x', htmlFor: 'y' }
31
+ mapCompatDomProps(p, Comp)
32
+ expect(p).toEqual({ className: 'x', htmlFor: 'y' })
33
+ })
34
+
35
+ it('className → class, htmlFor → for', () => {
36
+ const p: Record<string, unknown> = { className: 'btn', htmlFor: 'email' }
37
+ mapCompatDomProps(p, 'label')
38
+ expect(p).toEqual({ class: 'btn', for: 'email' })
39
+ })
40
+
41
+ it('onChange → onInput on input/textarea/select', () => {
42
+ for (const tag of ['input', 'textarea', 'select']) {
43
+ const fn = () => {}
44
+ const p: Record<string, unknown> = { onChange: fn }
45
+ mapCompatDomProps(p, tag)
46
+ expect(p).toEqual({ onInput: fn })
47
+ }
48
+ })
49
+
50
+ it('onChange does not clobber an explicit onInput', () => {
51
+ const onChange = () => {}
52
+ const onInput = () => {}
53
+ const p: Record<string, unknown> = { onChange, onInput }
54
+ mapCompatDomProps(p, 'input')
55
+ expect(p).toEqual({ onInput })
56
+ })
57
+
58
+ it('onChange left alone on non-form elements', () => {
59
+ const onChange = () => {}
60
+ const p: Record<string, unknown> = { onChange }
61
+ mapCompatDomProps(p, 'div')
62
+ expect(p).toEqual({ onChange })
63
+ })
64
+
65
+ it('autoFocus → autofocus', () => {
66
+ const p: Record<string, unknown> = { autoFocus: true }
67
+ mapCompatDomProps(p, 'input')
68
+ expect(p).toEqual({ autofocus: true })
69
+ })
70
+
71
+ it('defaultValue/defaultChecked → value/checked only when uncontrolled', () => {
72
+ const a: Record<string, unknown> = { defaultValue: 'd', defaultChecked: true }
73
+ mapCompatDomProps(a, 'input')
74
+ expect(a).toEqual({ value: 'd', checked: true })
75
+
76
+ const b: Record<string, unknown> = {
77
+ defaultValue: 'd',
78
+ value: 'controlled',
79
+ defaultChecked: true,
80
+ checked: false,
81
+ }
82
+ mapCompatDomProps(b, 'input')
83
+ expect(b).toEqual({
84
+ defaultValue: 'd',
85
+ value: 'controlled',
86
+ defaultChecked: true,
87
+ checked: false,
88
+ })
89
+ })
90
+
91
+ it('strips authoring-only props with no DOM equivalent', () => {
92
+ const p: Record<string, unknown> = {
93
+ suppressHydrationWarning: true,
94
+ suppressContentEditableWarning: true,
95
+ }
96
+ mapCompatDomProps(p, 'div')
97
+ expect(p).toEqual({})
98
+ })
99
+ })
@@ -1,3 +1,4 @@
1
+ import { clearReactiveTrace, signal } from '@pyreon/reactivity'
1
2
  import type { ErrorContext } from '../telemetry'
2
3
  import { registerErrorHandler, reportError } from '../telemetry'
3
4
 
@@ -201,3 +202,96 @@ describe('registerErrorHandler — reactivity bridge (regression)', () => {
201
202
  unsub2()
202
203
  })
203
204
  })
205
+
206
+ describe('reportError — reactiveTrace enrichment', () => {
207
+ beforeEach(() => clearReactiveTrace())
208
+
209
+ test('attaches recent signal writes to the error context (dev)', () => {
210
+ const s = signal(0, { name: 'enrichTest' })
211
+ s.set(1)
212
+ s.set(2)
213
+
214
+ let captured: ErrorContext | undefined
215
+ const unsub = registerErrorHandler((ctx) => {
216
+ captured = ctx
217
+ })
218
+ reportError({
219
+ component: 'C',
220
+ phase: 'render',
221
+ error: new Error('boom'),
222
+ timestamp: Date.now(),
223
+ })
224
+ unsub()
225
+
226
+ expect(captured?.reactiveTrace).toBeDefined()
227
+ expect(captured!.reactiveTrace).toHaveLength(2)
228
+ expect(captured!.reactiveTrace![0]).toMatchObject({
229
+ name: 'enrichTest',
230
+ prev: '0',
231
+ next: '1',
232
+ })
233
+ expect(captured!.reactiveTrace![1]).toMatchObject({ prev: '1', next: '2' })
234
+ })
235
+
236
+ test('does not overwrite a caller-supplied reactiveTrace', () => {
237
+ const s = signal(0, { name: 'x' })
238
+ s.set(99)
239
+
240
+ let captured: ErrorContext | undefined
241
+ const unsub = registerErrorHandler((ctx) => {
242
+ captured = ctx
243
+ })
244
+ const supplied = [{ name: 'manual', prev: 'a', next: 'b', timestamp: 1 }]
245
+ reportError({
246
+ component: 'C',
247
+ phase: 'effect',
248
+ error: new Error('boom'),
249
+ timestamp: Date.now(),
250
+ reactiveTrace: supplied,
251
+ })
252
+ unsub()
253
+
254
+ expect(captured!.reactiveTrace).toBe(supplied)
255
+ })
256
+
257
+ test('no trace field when there were no signal writes', () => {
258
+ let captured: ErrorContext | undefined
259
+ const unsub = registerErrorHandler((ctx) => {
260
+ captured = ctx
261
+ })
262
+ reportError({
263
+ component: 'C',
264
+ phase: 'mount',
265
+ error: new Error('boom'),
266
+ timestamp: Date.now(),
267
+ })
268
+ unsub()
269
+
270
+ // Empty buffer → field stays undefined (don't attach a noisy []).
271
+ expect(captured?.reactiveTrace).toBeUndefined()
272
+ })
273
+
274
+ test('the effect-error bridge path is also enriched', () => {
275
+ const s = signal('idle', { name: 'phase' })
276
+ s.set('running')
277
+
278
+ let captured: ErrorContext | undefined
279
+ const unsub = registerErrorHandler((ctx) => {
280
+ captured = ctx
281
+ })
282
+ // Drive the reactivity → core bridge the same way an effect throw does.
283
+ const bridge = (
284
+ globalThis as { __pyreon_report_error__?: (e: unknown, p: 'effect') => void }
285
+ ).__pyreon_report_error__
286
+ bridge?.(new Error('effect boom'), 'effect')
287
+ unsub()
288
+
289
+ expect(captured?.component).toBe('Effect')
290
+ expect(captured?.reactiveTrace).toBeDefined()
291
+ expect(captured!.reactiveTrace![0]).toMatchObject({
292
+ name: 'phase',
293
+ prev: '"idle"',
294
+ next: '"running"',
295
+ })
296
+ })
297
+ })