@pyreon/core 0.16.0 → 0.19.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.
- package/README.md +1 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/analysis/jsx-dev-runtime.js.html +1 -1
- package/lib/analysis/jsx-runtime.js.html +1 -1
- package/lib/index.js +278 -16
- package/lib/jsx-dev-runtime.js +29 -9
- package/lib/jsx-runtime.js +29 -9
- package/lib/types/index.d.ts +171 -18
- package/package.json +2 -2
- package/src/compat-shared.ts +80 -0
- package/src/defer.ts +279 -0
- package/src/index.ts +13 -2
- package/src/jsx-runtime.ts +46 -8
- package/src/lifecycle.ts +4 -2
- package/src/props.ts +59 -0
- package/src/telemetry.ts +37 -0
- package/src/tests/compat-shared.test.ts +99 -0
- package/src/tests/defer.test.ts +359 -0
- package/src/tests/reactive-props.test.ts +71 -1
- package/src/tests/telemetry.test.ts +94 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { makeReactiveProps, REACTIVE_PROP, _rp } from '../props'
|
|
2
|
+
import { makeReactiveProps, REACTIVE_PROP, _rp, _wrapSpread } from '../props'
|
|
3
3
|
|
|
4
4
|
describe('makeReactiveProps', () => {
|
|
5
5
|
it('returns raw object when no reactive props exist (fast path)', () => {
|
|
@@ -85,3 +85,73 @@ describe('_rp', () => {
|
|
|
85
85
|
expect(branded()).toBe('hello')
|
|
86
86
|
})
|
|
87
87
|
})
|
|
88
|
+
|
|
89
|
+
describe('_wrapSpread', () => {
|
|
90
|
+
it('returns null/undefined unchanged (primitive guard)', () => {
|
|
91
|
+
expect(_wrapSpread(null)).toBe(null)
|
|
92
|
+
expect(_wrapSpread(undefined)).toBe(undefined)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('returns source unchanged when no getter descriptors exist (fast path)', () => {
|
|
96
|
+
const source = { a: 1, b: 'x', c: true }
|
|
97
|
+
expect(_wrapSpread(source)).toBe(source)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('returns source unchanged for empty objects', () => {
|
|
101
|
+
const source = {}
|
|
102
|
+
expect(_wrapSpread(source)).toBe(source)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('wraps getter-shaped reactive props as _rp-branded thunks', () => {
|
|
106
|
+
let liveValue = 'a'
|
|
107
|
+
const source = {} as Record<string, unknown>
|
|
108
|
+
Object.defineProperty(source, 'x', {
|
|
109
|
+
get: () => liveValue,
|
|
110
|
+
enumerable: true,
|
|
111
|
+
configurable: true,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const result = _wrapSpread(source) as Record<string, unknown>
|
|
115
|
+
expect(result).not.toBe(source) // new object allocated
|
|
116
|
+
|
|
117
|
+
const wrappedX = result.x as () => unknown
|
|
118
|
+
expect(typeof wrappedX).toBe('function')
|
|
119
|
+
expect((wrappedX as unknown as Record<symbol, unknown>)[REACTIVE_PROP]).toBe(true)
|
|
120
|
+
|
|
121
|
+
// Lazy read — each call reads the current source[x] getter value
|
|
122
|
+
expect(wrappedX()).toBe('a')
|
|
123
|
+
liveValue = 'b'
|
|
124
|
+
expect(wrappedX()).toBe('b') // live re-read, not captured
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('preserves data properties as-is when mixed with getters', () => {
|
|
128
|
+
const source = { plain: 'data' } as Record<string, unknown>
|
|
129
|
+
Object.defineProperty(source, 'reactive', {
|
|
130
|
+
get: () => 'live',
|
|
131
|
+
enumerable: true,
|
|
132
|
+
configurable: true,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const result = _wrapSpread(source) as Record<string, unknown>
|
|
136
|
+
expect(result.plain).toBe('data') // copied through
|
|
137
|
+
expect(typeof result.reactive).toBe('function') // wrapped as thunk
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('preserves Reflect.ownKeys symbol-keyed properties', () => {
|
|
141
|
+
const sym = Symbol('marker')
|
|
142
|
+
const source = { regular: 'x' } as Record<string | symbol, unknown>
|
|
143
|
+
Object.defineProperty(source, 'reactive', {
|
|
144
|
+
get: () => 'live',
|
|
145
|
+
enumerable: true,
|
|
146
|
+
configurable: true,
|
|
147
|
+
})
|
|
148
|
+
source[sym] = 'symbol-value'
|
|
149
|
+
|
|
150
|
+
const result = _wrapSpread(source) as Record<string | symbol, unknown>
|
|
151
|
+
expect(result.regular).toBe('x')
|
|
152
|
+
// Note: symbol keys go through Reflect.ownKeys; the wrap path indexes
|
|
153
|
+
// via `key as string` for type narrowing but the runtime carries them
|
|
154
|
+
// forward as data properties.
|
|
155
|
+
expect(typeof result.reactive).toBe('function')
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -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
|
+
})
|