@pyreon/core 0.24.5 → 0.24.6

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.
Files changed (56) hide show
  1. package/lib/analysis/index.js.html +1 -1
  2. package/lib/index.js +53 -31
  3. package/package.json +2 -6
  4. package/src/compat-marker.ts +0 -79
  5. package/src/compat-shared.ts +0 -80
  6. package/src/component.ts +0 -98
  7. package/src/context.ts +0 -349
  8. package/src/defer.ts +0 -279
  9. package/src/dynamic.ts +0 -32
  10. package/src/env.d.ts +0 -6
  11. package/src/error-boundary.ts +0 -90
  12. package/src/for.ts +0 -51
  13. package/src/h.ts +0 -80
  14. package/src/index.ts +0 -80
  15. package/src/jsx-dev-runtime.ts +0 -2
  16. package/src/jsx-runtime.ts +0 -747
  17. package/src/lazy.ts +0 -25
  18. package/src/lifecycle.ts +0 -152
  19. package/src/manifest.ts +0 -579
  20. package/src/map-array.ts +0 -42
  21. package/src/portal.ts +0 -39
  22. package/src/props.ts +0 -269
  23. package/src/ref.ts +0 -32
  24. package/src/show.ts +0 -121
  25. package/src/style.ts +0 -102
  26. package/src/suspense.ts +0 -52
  27. package/src/telemetry.ts +0 -120
  28. package/src/tests/compat-marker.test.ts +0 -96
  29. package/src/tests/compat-shared.test.ts +0 -99
  30. package/src/tests/component.test.ts +0 -281
  31. package/src/tests/context.test.ts +0 -629
  32. package/src/tests/core.test.ts +0 -1290
  33. package/src/tests/cx.test.ts +0 -70
  34. package/src/tests/defer.test.ts +0 -359
  35. package/src/tests/dynamic.test.ts +0 -87
  36. package/src/tests/error-boundary.test.ts +0 -181
  37. package/src/tests/extract-props-overloads.types.test.ts +0 -135
  38. package/src/tests/for.test.ts +0 -117
  39. package/src/tests/h.test.ts +0 -221
  40. package/src/tests/jsx-compat.test.tsx +0 -86
  41. package/src/tests/lazy.test.ts +0 -100
  42. package/src/tests/lifecycle.test.ts +0 -350
  43. package/src/tests/manifest-snapshot.test.ts +0 -100
  44. package/src/tests/map-array.test.ts +0 -313
  45. package/src/tests/native-marker-error-boundary.test.ts +0 -12
  46. package/src/tests/portal.test.ts +0 -48
  47. package/src/tests/props-extended.test.ts +0 -157
  48. package/src/tests/props.test.ts +0 -250
  49. package/src/tests/reactive-context.test.ts +0 -69
  50. package/src/tests/reactive-props.test.ts +0 -157
  51. package/src/tests/ref.test.ts +0 -70
  52. package/src/tests/show.test.ts +0 -314
  53. package/src/tests/style.test.ts +0 -157
  54. package/src/tests/suspense.test.ts +0 -139
  55. package/src/tests/telemetry.test.ts +0 -297
  56. package/src/types.ts +0 -116
package/src/props.ts DELETED
@@ -1,269 +0,0 @@
1
- // Prop utilities for component authoring.
2
-
3
- /**
4
- * Split props into two groups: keys you want and the rest.
5
- * Unlike destructuring, this preserves reactivity (getters on the original object).
6
- *
7
- * @example
8
- * const [own, html] = splitProps(props, ["label", "icon"])
9
- * return <button {...html}><Icon name={own.icon} /> {own.label}</button>
10
- */
11
- export function splitProps<T extends object, K extends (keyof T)[]>(
12
- props: T,
13
- keys: K,
14
- ): [Pick<T, K[number]>, Omit<T, K[number]>] {
15
- const picked = {} as Pick<T, K[number]>
16
- const rest = {} as Omit<T, K[number]>
17
- const keySet = new Set<string | symbol>(keys as (string | symbol)[])
18
-
19
- // Reflect.ownKeys includes symbol-keyed properties; Object.keys drops them
20
- // silently. Without this, symbol-keyed props (e.g. branded reactive props
21
- // under Symbol.for('pyreon.reactiveProp')) would vanish from both picked
22
- // and rest.
23
- for (const key of Reflect.ownKeys(props)) {
24
- const desc = Object.getOwnPropertyDescriptor(props, key)
25
- if (!desc) continue
26
- // Force configurable: true when copying to a fresh object. Source descriptors
27
- // may be non-configurable (default when created with `Object.defineProperty`
28
- // and the caller omitted `configurable`). If we preserved that, any later
29
- // `Object.defineProperty` on the same key — including subsequent splitProps
30
- // post-processing or test mocks — would throw "Cannot redefine property".
31
- const safe = { ...desc, configurable: true }
32
- if (keySet.has(key)) {
33
- Object.defineProperty(picked, key, safe)
34
- } else {
35
- Object.defineProperty(rest, key, safe)
36
- }
37
- }
38
-
39
- return [picked, rest]
40
- }
41
-
42
- /** Merge a getter-backed source property with an existing getter or value. */
43
- function mergeGetterWithExisting(
44
- result: Record<string, unknown>,
45
- key: string,
46
- desc: PropertyDescriptor,
47
- existing: PropertyDescriptor,
48
- ): void {
49
- const prevGet = existing.get ?? (() => existing.value)
50
- const nextGet = desc.get as () => unknown
51
- Object.defineProperty(result, key, {
52
- get: () => {
53
- const v = nextGet()
54
- return v !== undefined ? v : prevGet()
55
- },
56
- enumerable: true,
57
- configurable: true,
58
- })
59
- }
60
-
61
- /** Merge a static source property when the existing property has a getter. */
62
- function mergeStaticWithGetter(
63
- result: Record<string, unknown>,
64
- key: string,
65
- desc: PropertyDescriptor,
66
- existingGet: () => unknown,
67
- ): void {
68
- if (desc.value !== undefined) {
69
- Object.defineProperty(result, key, { ...desc, configurable: true })
70
- } else {
71
- Object.defineProperty(result, key, {
72
- get: existingGet,
73
- enumerable: true,
74
- configurable: true,
75
- })
76
- }
77
- }
78
-
79
- /** Apply a single source property onto the result object, handling getter/static combos. */
80
- function mergeProperty(
81
- result: Record<string, unknown>,
82
- key: string,
83
- desc: PropertyDescriptor,
84
- ): void {
85
- const existing = Object.getOwnPropertyDescriptor(result, key)
86
- if (desc.get && existing) {
87
- mergeGetterWithExisting(result, key, desc, existing)
88
- } else if (desc.get) {
89
- // Force configurable: true — source getters may have been defined via
90
- // `Object.defineProperty` without an explicit configurable flag (which
91
- // defaults to false). Without this, a later source in the same mergeProps
92
- // call that overrides the same key would crash with TypeError:
93
- // "Cannot redefine property".
94
- Object.defineProperty(result, key, { ...desc, configurable: true })
95
- } else if (existing?.get) {
96
- mergeStaticWithGetter(result, key, desc, existing.get)
97
- } else if (desc.value !== undefined || !existing) {
98
- // Both static — later value wins if defined
99
- Object.defineProperty(result, key, { ...desc, configurable: true })
100
- }
101
- }
102
-
103
- /**
104
- * Merge default values with component props. Defaults are used when
105
- * the prop is `undefined`. Preserves getter reactivity.
106
- *
107
- * @example
108
- * const merged = mergeProps({ size: "md", variant: "primary" }, props)
109
- * // merged.size is reactive — falls back to "md" when props.size is undefined
110
- */
111
- export function mergeProps<T extends Record<string, unknown>>(...sources: T[]): T {
112
- const result = {} as T
113
- for (const source of sources) {
114
- // See splitProps for why this uses Reflect.ownKeys instead of Object.keys.
115
- for (const key of Reflect.ownKeys(source)) {
116
- const desc = Object.getOwnPropertyDescriptor(source, key)
117
- if (!desc) continue
118
- mergeProperty(result, key as string, desc)
119
- }
120
- }
121
- return result
122
- }
123
-
124
- /**
125
- * Brand symbol for compiler-emitted reactive prop wrappers.
126
- * Distinguishes `() => expr` wrappers from user-written accessor props
127
- * (like Show's `when={() => condition()}`).
128
- */
129
- export const REACTIVE_PROP = Symbol.for('pyreon.reactiveProp')
130
-
131
- /** Symbol to access the underlying props signal for updates. */
132
- export const PROPS_SIGNAL = Symbol.for('pyreon.propsSignal')
133
-
134
- /**
135
- * Create a branded reactive prop wrapper.
136
- * Called by the compiler for component prop expressions containing signal reads.
137
- */
138
- export function _rp<T>(fn: () => T): () => T {
139
- ;(fn as any)[REACTIVE_PROP] = true
140
- return fn
141
- }
142
-
143
- /**
144
- * Wrap a JSX spread source so its getter-shaped reactive props survive
145
- * the JS-level object spread that esbuild's automatic JSX runtime emits
146
- * for `<Comp {...source}>`.
147
- *
148
- * Without this wrapper, esbuild compiles `<Comp {...source}>` to
149
- * `jsx(Comp, { ...source })` — and JS spread fires every getter on
150
- * `source`, storing the resolved values as plain data properties. Any
151
- * compiler-emitted reactive prop (`_rp(() => signal())` converted to a
152
- * getter by `makeReactiveProps`) on `source` is collapsed to its
153
- * initial value before the receiving component ever sees it.
154
- *
155
- * `_wrapSpread(source)` walks `source`'s own keys via `Reflect.ownKeys`
156
- * (no getter firing) and returns a new object whose values are
157
- * `_rp`-branded thunks `() => source[key]`. When `{ ..._wrapSpread(s) }`
158
- * is spread by esbuild, the thunks are stored as plain data property
159
- * values (no getters to fire), then `makeReactiveProps` in `mount.ts`
160
- * converts the brands back into getters that lazily read from the
161
- * original `source` — preserving the reactive subscription end-to-end.
162
- *
163
- * Fast path: when `source` has no getter descriptors, return the
164
- * source object unchanged. JS spread will work correctly in that case
165
- * because there's nothing reactive to preserve. Saves N thunk
166
- * allocations per component render in the 99% case.
167
- *
168
- * Emitted by the compiler — not generally meant for hand-written code.
169
- */
170
- export function _wrapSpread(
171
- source: Record<string, unknown> | null | undefined,
172
- ): Record<string, unknown> | null | undefined {
173
- if (!source || typeof source !== 'object') return source
174
- const descriptors = Object.getOwnPropertyDescriptors(source)
175
- let hasGetter = false
176
- for (const k in descriptors) {
177
- if (descriptors[k]!.get) {
178
- hasGetter = true
179
- break
180
- }
181
- }
182
- if (!hasGetter) return source
183
-
184
- const result: Record<string, unknown> = {}
185
- // Reflect.ownKeys covers symbol keys too — REACTIVE_PROP brands and
186
- // other framework symbols must round-trip through the wrap.
187
- for (const key of Reflect.ownKeys(source)) {
188
- const desc = descriptors[key as string]
189
- if (!desc) continue
190
- if (desc.get) {
191
- const fn: () => unknown = () => source[key as string]
192
- ;(fn as unknown as Record<symbol, boolean>)[REACTIVE_PROP] = true
193
- result[key as string] = fn
194
- } else {
195
- // Static data property — copy through as-is.
196
- result[key as string] = desc.value
197
- }
198
- }
199
- return result
200
- }
201
-
202
- /**
203
- * Convert compiler-emitted `_rp(() => expr)` prop values into getter properties.
204
- *
205
- * Only converts functions branded with REACTIVE_PROP — user-written accessor
206
- * props (like Show's when, For's each) are left as-is.
207
- *
208
- * Returns the same object if no reactive props found (fast path).
209
- */
210
- export function makeReactiveProps(
211
- raw: Record<string, unknown>,
212
- ): Record<string, unknown> {
213
- // Fast path: scan for any REACTIVE_PROP-branded function first.
214
- // If none found, return raw immediately — no object allocation, no property copying.
215
- // This saves ~90 object allocations + ~450 property copies per page load
216
- // for components with all-static props (buttons, icons, layout, etc.).
217
- const keys = Object.keys(raw)
218
- let hasAny = false
219
- for (let i = 0; i < keys.length; i++) {
220
- const val = raw[keys[i]!]
221
- if (typeof val === 'function' && (val as any)[REACTIVE_PROP]) {
222
- hasAny = true
223
- break
224
- }
225
- }
226
- if (!hasAny) return raw
227
-
228
- // At least one reactive prop exists — build the getter-backed object.
229
- const result: Record<string, unknown> = {}
230
- for (let i = 0; i < keys.length; i++) {
231
- const key = keys[i]!
232
- const val = raw[key]
233
- if (typeof val === 'function' && (val as any)[REACTIVE_PROP]) {
234
- Object.defineProperty(result, key, {
235
- get: val as () => unknown,
236
- enumerable: true,
237
- configurable: true,
238
- })
239
- } else {
240
- result[key] = val
241
- }
242
- }
243
-
244
- return result
245
- }
246
-
247
- // ─── Unique ID ───────────────────────────────────────────────────────────────
248
-
249
- let _idCounter = 0
250
-
251
- /**
252
- * Generate a unique ID string for accessibility attributes (htmlFor, aria-describedby, etc.).
253
- * SSR-safe: uses a deterministic counter that resets per request context.
254
- *
255
- * @example
256
- * const id = createUniqueId()
257
- * return <>
258
- * <label for={id}>Name</label>
259
- * <input id={id} />
260
- * </>
261
- */
262
- export function createUniqueId(): string {
263
- return `pyreon-${++_idCounter}`
264
- }
265
-
266
- /** Reset the ID counter (called by SSR per-request). */
267
- export function _resetIdCounter(): void {
268
- _idCounter = 0
269
- }
package/src/ref.ts DELETED
@@ -1,32 +0,0 @@
1
- /**
2
- * createRef — mutable container for a DOM element or component value.
3
- *
4
- * Usage:
5
- * const inputRef = createRef<HTMLInputElement>()
6
- * onMount(() => { inputRef.current?.focus() })
7
- * return <input ref={inputRef} />
8
- *
9
- * The runtime sets `ref.current` after the element is inserted into the DOM
10
- * and clears it to `null` when the element is removed.
11
- */
12
-
13
- export interface Ref<T = unknown> {
14
- current: T | null
15
- }
16
-
17
- /** Callback ref — receives the element on mount and null on unmount. */
18
- export type RefCallback<T = unknown> = (el: T | null) => void
19
-
20
- /**
21
- * Union of object ref and callback ref — accepted by the JSX ref prop.
22
- * Callback refs are called with the element on mount and with `null` on
23
- * unmount (matches React/Solid/Vue). Callback refs MUST accept `T | null`
24
- * — the previous `(el: T) => void` mount-only arm was removed in the
25
- * post-#233 cleanup because the runtime always invokes with null on
26
- * unmount and the narrower type silently lied to consumers.
27
- */
28
- export type RefProp<T = unknown> = Ref<T> | RefCallback<T>
29
-
30
- export function createRef<T = unknown>(): Ref<T> {
31
- return { current: null }
32
- }
package/src/show.ts DELETED
@@ -1,121 +0,0 @@
1
- import type { Props, VNode, VNodeChild, VNodeChildAtom } from './types'
2
-
3
- // ─── Show ─────────────────────────────────────────────────────────────────────
4
-
5
- export interface ShowProps extends Props {
6
- /**
7
- * Truthy condition. Accepts a value or an accessor.
8
- *
9
- * Use an accessor (`() => signal()`) for reactive conditions.
10
- * Bare values are accepted for static cases and as a defensive normalization
11
- * for cases where the compiler's signal auto-call has already invoked
12
- * a signal at the prop site (e.g. `when={mySignal}` becomes `when={mySignal()}`).
13
- */
14
- when: unknown | (() => unknown)
15
- fallback?: VNodeChild
16
- children?: VNodeChild
17
- }
18
-
19
- // Normalize a value-or-accessor `when` into a single accessor.
20
- // Same shape used by Match — kept inline (one branch) to stay zero-cost.
21
- function callWhen(when: unknown): unknown {
22
- return typeof when === 'function' ? (when as () => unknown)() : when
23
- }
24
-
25
- /**
26
- * Conditionally render children based on a reactive condition.
27
- *
28
- * @example
29
- * h(Show, { when: () => isLoggedIn() },
30
- * h(Dashboard, null)
31
- * )
32
- *
33
- * // With fallback:
34
- * h(Show, { when: () => user(), fallback: h(Login, null) },
35
- * h(Dashboard, null)
36
- * )
37
- */
38
- export function Show(props: ShowProps): VNode | null {
39
- // Returns a reactive accessor; the renderer unwraps it at mount time.
40
- return ((): VNodeChildAtom =>
41
- (callWhen(props.when)
42
- ? (props.children ?? null)
43
- : (props.fallback ?? null)) as VNodeChildAtom) as unknown as VNode
44
- }
45
-
46
- // ─── Switch / Match ───────────────────────────────────────────────────────────
47
-
48
- export interface MatchProps extends Props {
49
- /** Truthy condition. Accepts a value or an accessor — see {@link ShowProps.when}. */
50
- when: unknown | (() => unknown)
51
- children?: VNodeChild
52
- }
53
-
54
- /**
55
- * A branch inside `<Switch>`. Renders when `when()` is truthy.
56
- * Must be used as a direct child of `Switch`.
57
- *
58
- * `Match` acts as a pure type/identity marker — Switch identifies it by checking
59
- * `vnode.type === Match` rather than by the runtime return value.
60
- */
61
- export function Match(_props: MatchProps): VNode | null {
62
- // Match is never mounted directly — Switch inspects Match VNodes by type identity.
63
- return null
64
- }
65
-
66
- export interface SwitchProps extends Props {
67
- /** Rendered when no Match branch is truthy. */
68
- fallback?: VNodeChild
69
- children?: VNodeChild | VNodeChild[]
70
- }
71
-
72
- /**
73
- * Multi-branch conditional rendering. Evaluates each `Match` child in order,
74
- * renders the first whose `when()` is truthy, or `fallback` if none match.
75
- *
76
- * @example
77
- * h(Switch, { fallback: h("p", null, "404") },
78
- * h(Match, { when: () => route() === "/" }, h(Home, null)),
79
- * h(Match, { when: () => route() === "/about" }, h(About, null)),
80
- * )
81
- */
82
- function isMatchVNode(branch: VNodeChild): branch is VNode {
83
- return (
84
- branch !== null &&
85
- typeof branch === 'object' &&
86
- !Array.isArray(branch) &&
87
- (branch as VNode).type === Match
88
- )
89
- }
90
-
91
- function resolveMatchChildren(matchVNode: VNode): VNodeChildAtom {
92
- if (matchVNode.children.length === 0) {
93
- return ((matchVNode.props as unknown as MatchProps).children ?? null) as VNodeChildAtom
94
- }
95
- if (matchVNode.children.length === 1) return matchVNode.children[0] as VNodeChildAtom
96
- return matchVNode.children as unknown as VNodeChildAtom
97
- }
98
-
99
- function normalizeBranches(children: SwitchProps['children']): VNodeChild[] {
100
- if (Array.isArray(children)) return children
101
- if (children != null) return [children]
102
- return []
103
- }
104
-
105
- export function Switch(props: SwitchProps): VNode | null {
106
- // Returns a reactive accessor; the renderer unwraps it at mount time.
107
- return ((): VNodeChildAtom => {
108
- const branches = normalizeBranches(props.children)
109
-
110
- for (const branch of branches) {
111
- if (!isMatchVNode(branch)) continue
112
- const matchProps = branch.props as unknown as MatchProps
113
- if (callWhen(matchProps.when)) return resolveMatchChildren(branch)
114
- }
115
-
116
- return (props.fallback ?? null) as VNodeChildAtom
117
- }) as unknown as VNode
118
- }
119
-
120
- // Keep MatchSymbol export for any code that was using it
121
- export const MatchSymbol: unique symbol = Symbol('pyreon.Match')
package/src/style.ts DELETED
@@ -1,102 +0,0 @@
1
- // Shared style utilities used by both runtime-dom and runtime-server.
2
-
3
- // CSS properties where numeric values are unitless (e.g. `opacity: 0.5`, `zIndex: 10`).
4
- // All other numeric values get "px" appended automatically (e.g. `height: 100` → `"100px"`).
5
- export const CSS_UNITLESS = new Set([
6
- 'animationIterationCount',
7
- 'aspectRatio',
8
- 'borderImageOutset',
9
- 'borderImageSlice',
10
- 'borderImageWidth',
11
- 'boxFlex',
12
- 'boxFlexGroup',
13
- 'boxOrdinalGroup',
14
- 'columnCount',
15
- 'columns',
16
- 'flex',
17
- 'flexGrow',
18
- 'flexPositive',
19
- 'flexShrink',
20
- 'flexNegative',
21
- 'flexOrder',
22
- 'gridArea',
23
- 'gridRow',
24
- 'gridRowEnd',
25
- 'gridRowSpan',
26
- 'gridRowStart',
27
- 'gridColumn',
28
- 'gridColumnEnd',
29
- 'gridColumnSpan',
30
- 'gridColumnStart',
31
- 'fontWeight',
32
- 'lineClamp',
33
- 'lineHeight',
34
- 'opacity',
35
- 'order',
36
- 'orphans',
37
- 'scale',
38
- 'tabSize',
39
- 'widows',
40
- 'zIndex',
41
- 'zoom',
42
- 'fillOpacity',
43
- 'floodOpacity',
44
- 'stopOpacity',
45
- 'strokeDasharray',
46
- 'strokeDashoffset',
47
- 'strokeMiterlimit',
48
- 'strokeOpacity',
49
- 'strokeWidth',
50
- ])
51
-
52
- // ─── Class utilities ─────────────────────────────────────────────────────────
53
-
54
- /** Value accepted by the `class` prop — string, array, object, or nested mix. */
55
- export type ClassValue =
56
- | string
57
- | number
58
- | boolean
59
- | null
60
- | undefined
61
- | ClassValue[]
62
- | Record<string, boolean | null | undefined | (() => boolean)>
63
-
64
- function cxObject(obj: Record<string, boolean | null | undefined | (() => boolean)>): string {
65
- let result = ''
66
- for (const key in obj) {
67
- const v = obj[key]
68
- const truthy = typeof v === 'function' ? v() : v
69
- if (truthy) result = result ? `${result} ${key}` : key
70
- }
71
- return result
72
- }
73
-
74
- function cxArray(arr: ClassValue[]): string {
75
- let result = ''
76
- for (const item of arr) {
77
- const resolved = cx(item)
78
- if (resolved) result = result ? `${result} ${resolved}` : resolved
79
- }
80
- return result
81
- }
82
-
83
- /** Resolve a ClassValue into a flat class string (like clsx/cx). */
84
- export function cx(value: ClassValue): string {
85
- if (value == null || value === false || value === true) return ''
86
- if (typeof value === 'string') return value
87
- if (typeof value === 'number') return String(value)
88
- if (Array.isArray(value)) return cxArray(value)
89
- return cxObject(value)
90
- }
91
-
92
- // ─── Style utilities ─────────────────────────────────────────────────────────
93
-
94
- /** Convert a camelCase CSS property name to kebab-case. */
95
- export function toKebabCase(str: string): string {
96
- return str.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)
97
- }
98
-
99
- /** Normalize a style value — appends "px" to numbers for non-unitless properties. */
100
- export function normalizeStyleValue(key: string, value: unknown): string {
101
- return typeof value === 'number' && !CSS_UNITLESS.has(key) ? `${value}px` : String(value)
102
- }
package/src/suspense.ts DELETED
@@ -1,52 +0,0 @@
1
- import { Fragment, h } from './h'
2
- import type { Props, VNode, VNodeChild } from './types'
3
-
4
- // Dev-mode gate: see `pyreon/no-process-dev-gate` lint rule for why this
5
- // uses `import.meta.env.DEV` instead of `typeof process !== 'undefined'`.
6
- const __DEV__ = process.env.NODE_ENV !== 'production'
7
-
8
- /** Internal marker attached to lazy()-wrapped components */
9
- export type LazyComponent<P extends Props = Props> = ((props: P) => VNodeChild) & {
10
- __loading: () => boolean
11
- }
12
-
13
- /**
14
- * Suspense — shows `fallback` while a lazy child component is still loading.
15
- *
16
- * Works in tandem with `lazy()` from `@pyreon/react-compat` (or `@pyreon/core/lazy`).
17
- * The child VNode's `.type.__loading()` signal drives the switch.
18
- *
19
- * Usage:
20
- * const Page = lazy(() => import("./Page"))
21
- *
22
- * h(Suspense, { fallback: h(Spinner, null) }, h(Page, null))
23
- * // or with JSX:
24
- * <Suspense fallback={<Spinner />}><Page /></Suspense>
25
- */
26
- export function Suspense(props: { fallback: VNodeChild; children?: VNodeChild }): VNode {
27
- if (__DEV__ && props.fallback === undefined) {
28
- // oxlint-disable-next-line no-console
29
- console.warn(
30
- '[Pyreon] <Suspense> is missing a `fallback` prop. Provide fallback UI to show while loading.',
31
- )
32
- }
33
-
34
- return h(Fragment, null, () => {
35
- const ch = props.children
36
- const childNode = typeof ch === 'function' ? ch() : ch
37
-
38
- // Check if the child is a VNode whose type is a lazy component still loading
39
- const isLoading =
40
- childNode != null &&
41
- typeof childNode === 'object' &&
42
- !Array.isArray(childNode) &&
43
- typeof (childNode as VNode).type === 'function' &&
44
- ((childNode as VNode).type as unknown as LazyComponent).__loading?.()
45
-
46
- if (isLoading) {
47
- const fb = props.fallback
48
- return typeof fb === 'function' ? fb() : fb
49
- }
50
- return childNode
51
- })
52
- }
package/src/telemetry.ts DELETED
@@ -1,120 +0,0 @@
1
- /**
2
- * Error telemetry — hook into Pyreon's error reporting for Sentry, Datadog, etc.
3
- *
4
- * Captures errors from ALL lifecycle phases including reactive effects.
5
- * `effect()` errors thrown by `@pyreon/reactivity` are bridged through a
6
- * globalThis sink (no upward import — reactivity doesn't depend on core).
7
- *
8
- * @example
9
- * import { registerErrorHandler } from "@pyreon/core"
10
- * import * as Sentry from "@sentry/browser"
11
- *
12
- * registerErrorHandler(ctx => {
13
- * Sentry.captureException(ctx.error, {
14
- * extra: { component: ctx.component, phase: ctx.phase },
15
- * })
16
- * })
17
- */
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
-
26
- export interface ErrorContext {
27
- /** Component function name, "Anonymous", or "Effect" for reactive effects */
28
- component: string
29
- /** Lifecycle phase where the error occurred */
30
- phase: 'setup' | 'render' | 'mount' | 'unmount' | 'effect'
31
- /** The thrown value */
32
- error: unknown
33
- /** Unix timestamp (ms) */
34
- timestamp: number
35
- /** Component props at the time of the error */
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[]
53
- }
54
-
55
- export type ErrorHandler = (ctx: ErrorContext) => void
56
-
57
- let _handlers: ErrorHandler[] = []
58
-
59
- /**
60
- * Register a global error handler. Called whenever a component throws in any
61
- * lifecycle phase, OR an effect throws in `@pyreon/reactivity`. Returns an
62
- * unregister function.
63
- *
64
- * Also installs a `globalThis.__pyreon_report_error__` bridge so the
65
- * reactivity package (which can't depend on core) can forward effect errors
66
- * into the same telemetry pipeline. Pre-fix the two surfaces were
67
- * disconnected — Sentry/Datadog wiring missed effect-thrown errors.
68
- */
69
- export function registerErrorHandler(handler: ErrorHandler): () => void {
70
- _handlers.push(handler)
71
- _installReactivityBridge()
72
- return () => {
73
- _handlers = _handlers.filter((h) => h !== handler)
74
- }
75
- }
76
-
77
- /**
78
- * Internal — called by the runtime whenever a component error is caught.
79
- * Existing console.error calls are preserved; this is additive.
80
- */
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
- }
96
- for (const h of _handlers) {
97
- try {
98
- h(ctx)
99
- } catch {
100
- // handler errors must never propagate back into the framework
101
- }
102
- }
103
- }
104
-
105
- // ─── Reactivity bridge ──────────────────────────────────────────────────────
106
- // Installs `globalThis.__pyreon_report_error__` so `@pyreon/reactivity`
107
- // effect-error path can forward into reportError. Idempotent — multiple
108
- // `registerErrorHandler` calls install once.
109
-
110
- interface PyreonErrorBridge {
111
- __pyreon_report_error__?: (err: unknown, phase: 'effect') => void
112
- }
113
- const _bridgeHost = globalThis as PyreonErrorBridge
114
-
115
- function _installReactivityBridge(): void {
116
- if (_bridgeHost.__pyreon_report_error__) return
117
- _bridgeHost.__pyreon_report_error__ = (err, phase) => {
118
- reportError({ component: 'Effect', phase, error: err, timestamp: Date.now() })
119
- }
120
- }