@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.
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +53 -31
- package/package.json +2 -6
- package/src/compat-marker.ts +0 -79
- package/src/compat-shared.ts +0 -80
- package/src/component.ts +0 -98
- package/src/context.ts +0 -349
- package/src/defer.ts +0 -279
- package/src/dynamic.ts +0 -32
- package/src/env.d.ts +0 -6
- package/src/error-boundary.ts +0 -90
- package/src/for.ts +0 -51
- package/src/h.ts +0 -80
- package/src/index.ts +0 -80
- package/src/jsx-dev-runtime.ts +0 -2
- package/src/jsx-runtime.ts +0 -747
- package/src/lazy.ts +0 -25
- package/src/lifecycle.ts +0 -152
- package/src/manifest.ts +0 -579
- package/src/map-array.ts +0 -42
- package/src/portal.ts +0 -39
- package/src/props.ts +0 -269
- package/src/ref.ts +0 -32
- package/src/show.ts +0 -121
- package/src/style.ts +0 -102
- package/src/suspense.ts +0 -52
- package/src/telemetry.ts +0 -120
- package/src/tests/compat-marker.test.ts +0 -96
- package/src/tests/compat-shared.test.ts +0 -99
- package/src/tests/component.test.ts +0 -281
- package/src/tests/context.test.ts +0 -629
- package/src/tests/core.test.ts +0 -1290
- package/src/tests/cx.test.ts +0 -70
- package/src/tests/defer.test.ts +0 -359
- package/src/tests/dynamic.test.ts +0 -87
- package/src/tests/error-boundary.test.ts +0 -181
- package/src/tests/extract-props-overloads.types.test.ts +0 -135
- package/src/tests/for.test.ts +0 -117
- package/src/tests/h.test.ts +0 -221
- package/src/tests/jsx-compat.test.tsx +0 -86
- package/src/tests/lazy.test.ts +0 -100
- package/src/tests/lifecycle.test.ts +0 -350
- package/src/tests/manifest-snapshot.test.ts +0 -100
- package/src/tests/map-array.test.ts +0 -313
- package/src/tests/native-marker-error-boundary.test.ts +0 -12
- package/src/tests/portal.test.ts +0 -48
- package/src/tests/props-extended.test.ts +0 -157
- package/src/tests/props.test.ts +0 -250
- package/src/tests/reactive-context.test.ts +0 -69
- package/src/tests/reactive-props.test.ts +0 -157
- package/src/tests/ref.test.ts +0 -70
- package/src/tests/show.test.ts +0 -314
- package/src/tests/style.test.ts +0 -157
- package/src/tests/suspense.test.ts +0 -139
- package/src/tests/telemetry.test.ts +0 -297
- 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
|
-
}
|