@pyreon/core 0.24.4 → 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
@@ -1,297 +0,0 @@
1
- import { clearReactiveTrace, signal } from '@pyreon/reactivity'
2
- import type { ErrorContext } from '../telemetry'
3
- import { registerErrorHandler, reportError } from '../telemetry'
4
-
5
- describe('registerErrorHandler', () => {
6
- test('registers handler that receives error context', () => {
7
- const contexts: ErrorContext[] = []
8
- const unsub = registerErrorHandler((ctx) => {
9
- contexts.push(ctx)
10
- })
11
-
12
- const ctx: ErrorContext = {
13
- component: 'TestComp',
14
- phase: 'render',
15
- error: new Error('test'),
16
- timestamp: 1234567890,
17
- }
18
- reportError(ctx)
19
- expect(contexts).toHaveLength(1)
20
- expect(contexts[0]).toBe(ctx)
21
-
22
- unsub()
23
- })
24
-
25
- test('returns unregister function', () => {
26
- let count = 0
27
- const unsub = registerErrorHandler(() => {
28
- count++
29
- })
30
-
31
- reportError({ component: 'A', phase: 'setup', error: 'e1', timestamp: 0 })
32
- expect(count).toBe(1)
33
-
34
- unsub()
35
-
36
- reportError({ component: 'B', phase: 'render', error: 'e2', timestamp: 0 })
37
- expect(count).toBe(1) // not called after unregister
38
- })
39
-
40
- test('multiple handlers are all called', () => {
41
- let count = 0
42
- const unsub1 = registerErrorHandler(() => count++)
43
- const unsub2 = registerErrorHandler(() => count++)
44
- const unsub3 = registerErrorHandler(() => count++)
45
-
46
- reportError({ component: 'X', phase: 'mount', error: 'err', timestamp: 0 })
47
- expect(count).toBe(3)
48
-
49
- unsub1()
50
- unsub2()
51
- unsub3()
52
- })
53
-
54
- test('handler errors are swallowed — subsequent handlers still called', () => {
55
- let secondCalled = false
56
- let thirdCalled = false
57
-
58
- const unsub1 = registerErrorHandler(() => {
59
- throw new Error('handler crash')
60
- })
61
- const unsub2 = registerErrorHandler(() => {
62
- secondCalled = true
63
- })
64
- const unsub3 = registerErrorHandler(() => {
65
- thirdCalled = true
66
- })
67
-
68
- // Should not throw
69
- expect(() =>
70
- reportError({ component: 'Y', phase: 'unmount', error: 'err', timestamp: 0 }),
71
- ).not.toThrow()
72
- expect(secondCalled).toBe(true)
73
- expect(thirdCalled).toBe(true)
74
-
75
- unsub1()
76
- unsub2()
77
- unsub3()
78
- })
79
-
80
- test('unregistering one handler does not affect others', () => {
81
- const calls: string[] = []
82
- const unsub1 = registerErrorHandler(() => calls.push('a'))
83
- const unsub2 = registerErrorHandler(() => calls.push('b'))
84
- const unsub3 = registerErrorHandler(() => calls.push('c'))
85
-
86
- unsub2() // remove middle handler
87
-
88
- reportError({ component: 'Z', phase: 'effect', error: 'e', timestamp: 0 })
89
- expect(calls).toEqual(['a', 'c'])
90
-
91
- unsub1()
92
- unsub3()
93
- })
94
- })
95
-
96
- describe('reportError', () => {
97
- test('no-op when no handlers registered', () => {
98
- // Should not throw
99
- expect(() =>
100
- reportError({ component: 'None', phase: 'setup', error: 'err', timestamp: 0 }),
101
- ).not.toThrow()
102
- })
103
-
104
- test('passes full ErrorContext to handler', () => {
105
- let received: ErrorContext | null = null
106
- const unsub = registerErrorHandler((ctx) => {
107
- received = ctx
108
- })
109
-
110
- const ctx: ErrorContext = {
111
- component: 'MyComp',
112
- phase: 'render',
113
- error: new Error('detail'),
114
- timestamp: 999,
115
- props: { a: 1, b: 'two' },
116
- }
117
- reportError(ctx)
118
-
119
- expect(received).not.toBeNull()
120
- expect(received!.component).toBe('MyComp')
121
- expect(received!.phase).toBe('render')
122
- expect(received!.error).toBeInstanceOf(Error)
123
- expect(received!.timestamp).toBe(999)
124
- expect(received!.props).toEqual({ a: 1, b: 'two' })
125
-
126
- unsub()
127
- })
128
-
129
- test('handles all phase types', () => {
130
- const phases: ErrorContext['phase'][] = ['setup', 'render', 'mount', 'unmount', 'effect']
131
- const seen: string[] = []
132
- const unsub = registerErrorHandler((ctx) => {
133
- seen.push(ctx.phase)
134
- })
135
-
136
- for (const phase of phases) {
137
- reportError({ component: 'X', phase, error: 'e', timestamp: 0 })
138
- }
139
- expect(seen).toEqual(phases)
140
-
141
- unsub()
142
- })
143
- })
144
-
145
- // ─── Regression: reactivity effect errors reach core's registerErrorHandler ──
146
- //
147
- // Pre-fix: `@pyreon/reactivity` had its own `setErrorHandler` API that drove
148
- // effect errors to `console.error`. `@pyreon/core`'s `registerErrorHandler`
149
- // captured component / mount / render / unmount errors only — effect errors
150
- // never reached it. Sentry/Datadog wiring missed the entire reactive surface.
151
- //
152
- // Post-fix: `registerErrorHandler` installs a `globalThis.__pyreon_report_error__`
153
- // bridge. Reactivity's effect-error path forwards through that bridge, so
154
- // effect errors flow into the same `reportError` pipeline as component
155
- // errors with phase='effect'.
156
- describe('registerErrorHandler — reactivity bridge (regression)', () => {
157
- test('effect() errors forward into registered telemetry handler', async () => {
158
- // Use a clean global state. The bridge install is idempotent within one
159
- // global; re-importing reactivity here re-uses the same module instance.
160
- const { effect, signal } = await import('@pyreon/reactivity')
161
-
162
- const captured: ErrorContext[] = []
163
- const unsub = registerErrorHandler((ctx) => captured.push(ctx))
164
-
165
- const trigger = signal(0)
166
- effect(() => {
167
- if (trigger() > 0) throw new Error('boom in effect')
168
- })
169
-
170
- // Trigger the throw
171
- trigger.set(1)
172
- await new Promise<void>((r) => queueMicrotask(() => r()))
173
-
174
- // Captured exactly once — through the bridge.
175
- expect(captured).toHaveLength(1)
176
- expect(captured[0]?.phase).toBe('effect')
177
- expect(captured[0]?.component).toBe('Effect')
178
- expect((captured[0]?.error as Error).message).toBe('boom in effect')
179
-
180
- unsub()
181
- })
182
-
183
- test('multiple handlers all receive forwarded effect errors', async () => {
184
- const { effect, signal } = await import('@pyreon/reactivity')
185
-
186
- let count1 = 0
187
- let count2 = 0
188
- const unsub1 = registerErrorHandler(() => count1++)
189
- const unsub2 = registerErrorHandler(() => count2++)
190
-
191
- const trigger = signal(0)
192
- effect(() => {
193
- if (trigger() > 0) throw new Error('boom')
194
- })
195
- trigger.set(1)
196
- await new Promise<void>((r) => queueMicrotask(() => r()))
197
-
198
- expect(count1).toBe(1)
199
- expect(count2).toBe(1)
200
-
201
- unsub1()
202
- unsub2()
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
- })
package/src/types.ts DELETED
@@ -1,116 +0,0 @@
1
- /// <reference lib="dom" />
2
-
3
- // ─── VNode ────────────────────────────────────────────────────────────────────
4
-
5
- // Reactive getter returning a child — wraps dynamic expressions in `() =>`
6
- export type VNodeChildAtom = VNode | string | number | boolean | null | undefined
7
- /** Reactive accessor — TS checks this arm FIRST so `{() => cond && <X />}` resolves correctly */
8
- export type VNodeChildAccessor = () => VNodeChildAtom | VNodeChildAtom[]
9
- export type VNodeChild = VNodeChildAccessor | VNodeChildAtom | VNodeChildAtom[]
10
-
11
- export interface VNode {
12
- /** Tag name, component function, or special symbol (Fragment) */
13
- type: string | ComponentFn | symbol
14
- props: Props
15
- children: VNodeChild[]
16
- key: string | number | null
17
- }
18
-
19
- // ─── Props ────────────────────────────────────────────────────────────────────
20
-
21
- export type Props = Record<string, unknown>
22
-
23
- // ─── Component ────────────────────────────────────────────────────────────────
24
-
25
- /**
26
- * A component is a plain function that runs ONCE.
27
- * It returns any renderable content and may call lifecycle hooks during setup.
28
- */
29
- export type ComponentFn<P extends Props = Props> = (props: P) => VNodeChild
30
-
31
- // ─── Utility types ───────────────────────────────────────────────────────────
32
-
33
- /**
34
- * Extract the props type from a component function, or pass through if already
35
- * a props type. **Multi-overload aware** — matches up to 4 call signatures and
36
- * produces the UNION of their first-argument types. A single-overload function
37
- * still works (the union of 4 copies of the same props type dedupes back to
38
- * the single shape).
39
- *
40
- * **Why this shape**. `T extends (props: infer P) => any ? P : never` only
41
- * captures the LAST overload of a multi-overload function — TS's overload-
42
- * resolution-against-conditional-types semantics. Multi-overload primitives
43
- * (Iterator, List, Element, etc.) need the union of every overload's props
44
- * to survive HOC wrapping (`rocketstyle()`, `attrs()`) without silently
45
- * downgrading the public prop surface to the loosest overload. Mirrors
46
- * vitus-labs PR #222.
47
- *
48
- * @example
49
- * function Iterator<T extends SimpleValue>(p: { data: T[]; valueName?: string }): VNodeChild
50
- * function Iterator<T extends ObjectValue>(p: { data: T[]; component: ComponentFn<T> }): VNodeChild
51
- * type Props = ExtractProps<typeof Iterator>
52
- * // → { data: SimpleValue[]; valueName?: string }
53
- * // | { data: ObjectValue[]; component: ComponentFn<ObjectValue> }
54
- */
55
- export type ExtractProps<T> = T extends {
56
- (props: infer P1, ...args: any): any
57
- (props: infer P2, ...args: any): any
58
- (props: infer P3, ...args: any): any
59
- (props: infer P4, ...args: any): any
60
- }
61
- ? P1 | P2 | P3 | P4
62
- : T extends {
63
- (props: infer P1, ...args: any): any
64
- (props: infer P2, ...args: any): any
65
- (props: infer P3, ...args: any): any
66
- }
67
- ? P1 | P2 | P3
68
- : T extends {
69
- (props: infer P1, ...args: any): any
70
- (props: infer P2, ...args: any): any
71
- }
72
- ? P1 | P2
73
- : T extends ComponentFn<infer P>
74
- ? P
75
- : T
76
-
77
- /** A higher-order component that wraps a component, optionally transforming its props. */
78
- export type HigherOrderComponent<HOP extends Props, P extends Props | undefined = undefined> = (
79
- Component: ComponentFn<HOP>,
80
- ) => ComponentFn<P extends undefined ? HOP : P>
81
-
82
- /**
83
- * Internal runtime handle created by the renderer for each mounted component.
84
- */
85
- export interface ComponentInstance {
86
- vnode: VNode | null
87
- /** Trigger a re-check / patch cycle (called by the renderer) */
88
- update(): void
89
- unmount(): void
90
- }
91
-
92
- // ─── Lifecycle hooks storage (attached per-instance by the renderer) ──────────
93
-
94
- // Cleanup function optionally returned by onMount hooks
95
- export type CleanupFn = () => void
96
-
97
- // ─── NativeItem ───────────────────────────────────────────────────────────────
98
-
99
- /**
100
- * Result of createTemplate() — a pre-cloned DOM element with its cleanup.
101
- * Handled directly by mountFor without going through the VNode reconciler,
102
- * saving 2 allocations per row vs the VNode wrapper path.
103
- */
104
- export interface NativeItem {
105
- readonly __isNative: true
106
- el: HTMLElement
107
- cleanup: (() => void) | null
108
- }
109
-
110
- export interface LifecycleHooks {
111
- mount: (() => CleanupFn | void | undefined)[] | null
112
- unmount: (() => void)[] | null
113
- update: (() => void)[] | null
114
- /** Error handlers — return true to mark the error as handled (stops propagation). */
115
- error: ((err: unknown) => boolean | undefined)[] | null
116
- }