@pyreon/core 0.14.0 → 0.16.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/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 +144 -17
- package/lib/jsx-dev-runtime.js +23 -2
- package/lib/jsx-runtime.js +23 -2
- package/lib/types/index.d.ts +169 -15
- package/lib/types/jsx-dev-runtime.d.ts +19 -4
- package/lib/types/jsx-runtime.d.ts +19 -4
- package/package.json +3 -2
- package/src/compat-marker.ts +79 -0
- package/src/context.ts +38 -7
- package/src/dynamic.ts +16 -5
- package/src/error-boundary.ts +15 -2
- package/src/for.ts +13 -1
- package/src/h.ts +16 -2
- package/src/index.ts +1 -0
- package/src/jsx-runtime.ts +20 -2
- package/src/lifecycle.ts +1 -2
- package/src/manifest.ts +55 -7
- package/src/show.ts +19 -6
- package/src/suspense.ts +1 -2
- package/src/telemetry.ts +30 -2
- package/src/tests/compat-marker.test.ts +96 -0
- package/src/tests/core.test.ts +1 -1
- package/src/tests/dynamic.test.ts +33 -1
- package/src/tests/extract-props-overloads.types.test.ts +135 -0
- package/src/tests/for.test.ts +23 -0
- package/src/tests/h.test.ts +21 -0
- package/src/tests/manifest-snapshot.test.ts +6 -1
- package/src/tests/native-marker-error-boundary.test.ts +12 -0
- package/src/tests/show.test.ts +76 -0
- package/src/tests/telemetry.test.ts +61 -0
- package/src/types.ts +45 -2
- package/lib/index.js.map +0 -1
- package/lib/jsx-dev-runtime.js.map +0 -1
- package/lib/jsx-runtime.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/jsx-dev-runtime.d.ts.map +0 -1
- package/lib/types/jsx-runtime.d.ts.map +0 -1
package/lib/types/index.d.ts
CHANGED
|
@@ -16,8 +16,41 @@ type Props = Record<string, unknown>;
|
|
|
16
16
|
* It returns any renderable content and may call lifecycle hooks during setup.
|
|
17
17
|
*/
|
|
18
18
|
type ComponentFn<P extends Props = Props> = (props: P) => VNodeChild;
|
|
19
|
-
/**
|
|
20
|
-
type
|
|
19
|
+
/**
|
|
20
|
+
* Extract the props type from a component function, or pass through if already
|
|
21
|
+
* a props type. **Multi-overload aware** — matches up to 4 call signatures and
|
|
22
|
+
* produces the UNION of their first-argument types. A single-overload function
|
|
23
|
+
* still works (the union of 4 copies of the same props type dedupes back to
|
|
24
|
+
* the single shape).
|
|
25
|
+
*
|
|
26
|
+
* **Why this shape**. `T extends (props: infer P) => any ? P : never` only
|
|
27
|
+
* captures the LAST overload of a multi-overload function — TS's overload-
|
|
28
|
+
* resolution-against-conditional-types semantics. Multi-overload primitives
|
|
29
|
+
* (Iterator, List, Element, etc.) need the union of every overload's props
|
|
30
|
+
* to survive HOC wrapping (`rocketstyle()`, `attrs()`) without silently
|
|
31
|
+
* downgrading the public prop surface to the loosest overload. Mirrors
|
|
32
|
+
* vitus-labs PR #222.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* function Iterator<T extends SimpleValue>(p: { data: T[]; valueName?: string }): VNodeChild
|
|
36
|
+
* function Iterator<T extends ObjectValue>(p: { data: T[]; component: ComponentFn<T> }): VNodeChild
|
|
37
|
+
* type Props = ExtractProps<typeof Iterator>
|
|
38
|
+
* // → { data: SimpleValue[]; valueName?: string }
|
|
39
|
+
* // | { data: ObjectValue[]; component: ComponentFn<ObjectValue> }
|
|
40
|
+
*/
|
|
41
|
+
type ExtractProps<T> = T extends {
|
|
42
|
+
(props: infer P1, ...args: any): any;
|
|
43
|
+
(props: infer P2, ...args: any): any;
|
|
44
|
+
(props: infer P3, ...args: any): any;
|
|
45
|
+
(props: infer P4, ...args: any): any;
|
|
46
|
+
} ? P1 | P2 | P3 | P4 : T extends {
|
|
47
|
+
(props: infer P1, ...args: any): any;
|
|
48
|
+
(props: infer P2, ...args: any): any;
|
|
49
|
+
(props: infer P3, ...args: any): any;
|
|
50
|
+
} ? P1 | P2 | P3 : T extends {
|
|
51
|
+
(props: infer P1, ...args: any): any;
|
|
52
|
+
(props: infer P2, ...args: any): any;
|
|
53
|
+
} ? P1 | P2 : T extends ComponentFn<infer P> ? P : T;
|
|
21
54
|
/** A higher-order component that wraps a component, optionally transforming its props. */
|
|
22
55
|
type HigherOrderComponent<HOP extends Props, P extends Props | undefined = undefined> = (Component: ComponentFn<HOP>) => ComponentFn<P extends undefined ? HOP : P>;
|
|
23
56
|
/**
|
|
@@ -75,6 +108,74 @@ declare function propagateError(err: unknown, hooks: LifecycleHooks): boolean;
|
|
|
75
108
|
*/
|
|
76
109
|
declare function dispatchToErrorBoundary(err: unknown): boolean;
|
|
77
110
|
//#endregion
|
|
111
|
+
//#region src/compat-marker.d.ts
|
|
112
|
+
/**
|
|
113
|
+
* Compat-mode native-component marker.
|
|
114
|
+
*
|
|
115
|
+
* Pyreon ships compat layers (`@pyreon/{react,preact,vue,solid}-compat`) that
|
|
116
|
+
* wrap every JSX-called component function to emulate that source framework's
|
|
117
|
+
* render-on-state-change semantics. That wrapping is correct for user code
|
|
118
|
+
* (the whole point of compat mode) but corrupts Pyreon framework components
|
|
119
|
+
* — those manage their own reactivity via `provide()` / signals / lifecycle
|
|
120
|
+
* hooks, and wrapping them runs their setup body inside the compat layer's
|
|
121
|
+
* render context instead of Pyreon's, breaking `provide()` and
|
|
122
|
+
* `onMount()` / `onUnmount()` calls.
|
|
123
|
+
*
|
|
124
|
+
* Framework components opt out of compat wrapping by setting a well-known
|
|
125
|
+
* registry symbol (`Symbol.for('pyreon:native-compat')`) on the function.
|
|
126
|
+
* The compat layer reads that symbol and routes marked components straight
|
|
127
|
+
* through Pyreon's `h()` mount path. The symbol is registry-shared, so no
|
|
128
|
+
* import direction between framework and compat is implied — both sides
|
|
129
|
+
* reference the same global symbol via the helpers exported here.
|
|
130
|
+
*
|
|
131
|
+
* Audience: framework-package authors writing JSX components in `@pyreon/*`
|
|
132
|
+
* packages whose setup body uses `provide()` / lifecycle hooks / signal
|
|
133
|
+
* subscriptions. Wrap exported components with `nativeCompat()`. One line
|
|
134
|
+
* per export site; zero runtime cost beyond a single property write at
|
|
135
|
+
* module load.
|
|
136
|
+
*/
|
|
137
|
+
/**
|
|
138
|
+
* The well-known registry symbol that marks a component as a Pyreon native
|
|
139
|
+
* framework component. Compat layers check this symbol to decide whether to
|
|
140
|
+
* skip their `wrapCompatComponent` call.
|
|
141
|
+
*
|
|
142
|
+
* Exported for advanced cases where a caller needs to test the marker
|
|
143
|
+
* directly (most callers should use `isNativeCompat()`).
|
|
144
|
+
*/
|
|
145
|
+
declare const NATIVE_COMPAT_MARKER: symbol;
|
|
146
|
+
/**
|
|
147
|
+
* Mark a Pyreon framework component as "self-managing" — compat layers will
|
|
148
|
+
* skip their React/Vue/Solid/Preact-style wrapping and route the component
|
|
149
|
+
* directly through Pyreon's mount path. Use on every `@pyreon/*` JSX
|
|
150
|
+
* component whose setup body uses `provide()`, lifecycle hooks
|
|
151
|
+
* (`onMount` / `onUnmount`), signal-driven reactivity, or any other Pyreon
|
|
152
|
+
* native pattern that depends on the active component-setup frame.
|
|
153
|
+
*
|
|
154
|
+
* Idempotent: re-applying the marker is a no-op. Non-function inputs pass
|
|
155
|
+
* through unchanged so callers don't have to typecheck before wrapping.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* import { nativeCompat, provide } from '@pyreon/core'
|
|
159
|
+
*
|
|
160
|
+
* export const RouterView = nativeCompat(function RouterView(props) {
|
|
161
|
+
* provide(RouterContext, ...)
|
|
162
|
+
* return <div data-pyreon-router-view>{children}</div>
|
|
163
|
+
* })
|
|
164
|
+
*/
|
|
165
|
+
declare function nativeCompat<T>(fn: T): T;
|
|
166
|
+
/**
|
|
167
|
+
* Read whether a component has been marked as a Pyreon native framework
|
|
168
|
+
* component. Compat-layer code calls this from its `jsx()` to decide whether
|
|
169
|
+
* to wrap or pass through.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* import { isNativeCompat } from '@pyreon/core'
|
|
173
|
+
*
|
|
174
|
+
* if (isNativeCompat(type)) return h(type, props)
|
|
175
|
+
* return wrapCompatComponent(type)(props)
|
|
176
|
+
*/
|
|
177
|
+
declare function isNativeCompat(fn: unknown): boolean;
|
|
178
|
+
//#endregion
|
|
78
179
|
//#region src/context.d.ts
|
|
79
180
|
/**
|
|
80
181
|
* Provide / inject — like React context or Vue provide/inject.
|
|
@@ -155,7 +256,16 @@ type ContextSnapshot = Map<symbol, unknown>[];
|
|
|
155
256
|
declare function captureContextStack(): ContextSnapshot;
|
|
156
257
|
/**
|
|
157
258
|
* Execute `fn()` with a previously captured context stack active.
|
|
158
|
-
*
|
|
259
|
+
*
|
|
260
|
+
* After `fn()` returns, removes ONLY the snapshot frames this call pushed
|
|
261
|
+
* — anything `fn()` itself pushed (typically provider frames from
|
|
262
|
+
* `provide()` calls during component mount) stays on the stack so
|
|
263
|
+
* subsequent reactive re-runs (e.g. `_bind` text bindings,
|
|
264
|
+
* `renderEffect` callbacks) can still find ancestor providers via
|
|
265
|
+
* `useContext`. Pre-fix this method was `stack.length = savedLength`,
|
|
266
|
+
* which destructively truncated provider frames pushed during mount —
|
|
267
|
+
* silently breaking `useMode()` / `useTheme()` / `useRouter()` etc. on
|
|
268
|
+
* every signal-driven update under a `mountReactive` boundary.
|
|
159
269
|
*/
|
|
160
270
|
declare function restoreContextStack<T>(snapshot: ContextSnapshot, fn: () => T): T;
|
|
161
271
|
//#endregion
|
|
@@ -205,7 +315,19 @@ declare function ErrorBoundary(props: {
|
|
|
205
315
|
*/
|
|
206
316
|
declare const ForSymbol: unique symbol;
|
|
207
317
|
interface ForProps<T> {
|
|
208
|
-
|
|
318
|
+
/**
|
|
319
|
+
* The list to iterate. Accepts EITHER a function returning the array
|
|
320
|
+
* (preferred — keeps reactivity intact when the array comes from a
|
|
321
|
+
* signal accessor) OR the array directly (convenient for static lists
|
|
322
|
+
* or already-resolved arrays). The runtime in `runtime-dom/src/mount.ts`
|
|
323
|
+
* normalizes both shapes; this type matches the runtime so users aren't
|
|
324
|
+
* forced to write `each={() => items}` for a plain array.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* <For each={items}>{r => <li>{r.label}</li>}</For> // static
|
|
328
|
+
* <For each={() => store.items()}>{r => <li>...</li>}</For> // reactive
|
|
329
|
+
*/
|
|
330
|
+
each: T[] | (() => T[]);
|
|
209
331
|
/** Keying function — use `by` not `key` (JSX extracts `key` for VNode reconciliation). */
|
|
210
332
|
by: (item: T) => string | number;
|
|
211
333
|
children: (item: T) => VNode | NativeItem;
|
|
@@ -229,8 +351,22 @@ interface ForProps<T> {
|
|
|
229
351
|
declare function For<T>(props: ForProps<T>): VNode;
|
|
230
352
|
//#endregion
|
|
231
353
|
//#region src/h.d.ts
|
|
232
|
-
/**
|
|
233
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Marker for fragment nodes — renders children without a wrapper element.
|
|
356
|
+
*
|
|
357
|
+
* MUST use `Symbol.for(...)` (global registry, keyed by string), NOT
|
|
358
|
+
* `Symbol(...)` (fresh per evaluation). `h.ts` is inlined into BOTH the
|
|
359
|
+
* main `lib/index.js` and the `lib/jsx-runtime.js` published bundles —
|
|
360
|
+
* each bundle's evaluation of a bare `Symbol(...)` would produce a
|
|
361
|
+
* DISTINCT Symbol identity. JSX `<>` compiles to `jsx(Fragment, ...)` and
|
|
362
|
+
* resolves to jsx-runtime's identity; `runtime-server` checks
|
|
363
|
+
* `vnode.type === Fragment` against the main-entry identity. Mismatch
|
|
364
|
+
* fell through to `renderElement` and crashed SSG with
|
|
365
|
+
* `TypeError: Cannot convert a Symbol value to a string`.
|
|
366
|
+
* `Symbol.for()` keys by string in a global registry shared across all
|
|
367
|
+
* bundle evaluations — same identity everywhere.
|
|
368
|
+
*/
|
|
369
|
+
declare const Fragment: symbol;
|
|
234
370
|
/**
|
|
235
371
|
* Hyperscript function — the compiled output of JSX.
|
|
236
372
|
* `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
|
|
@@ -365,6 +501,7 @@ interface PyreonHTMLAttributes<E extends Element = HTMLElement> {
|
|
|
365
501
|
} | undefined;
|
|
366
502
|
onClick?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
367
503
|
onDblClick?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
504
|
+
onDoubleClick?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
368
505
|
onMouseDown?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
369
506
|
onMouseUp?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
370
507
|
onMouseEnter?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
@@ -428,7 +565,7 @@ interface InputAttributes extends PyreonHTMLAttributes<HTMLInputElement> {
|
|
|
428
565
|
defaultChecked?: boolean | undefined;
|
|
429
566
|
placeholder?: string | (() => string) | undefined;
|
|
430
567
|
disabled?: boolean | (() => boolean) | undefined;
|
|
431
|
-
readOnly?: boolean | undefined;
|
|
568
|
+
readOnly?: boolean | (() => boolean) | undefined;
|
|
432
569
|
required?: boolean | (() => boolean) | undefined;
|
|
433
570
|
min?: string | number | undefined;
|
|
434
571
|
max?: string | number | undefined;
|
|
@@ -477,7 +614,7 @@ interface TextareaAttributes extends PyreonHTMLAttributes<HTMLTextAreaElement> {
|
|
|
477
614
|
defaultValue?: string | undefined;
|
|
478
615
|
placeholder?: string | (() => string) | undefined;
|
|
479
616
|
disabled?: boolean | (() => boolean) | undefined;
|
|
480
|
-
readOnly?: boolean | undefined;
|
|
617
|
+
readOnly?: boolean | (() => boolean) | undefined;
|
|
481
618
|
required?: boolean | (() => boolean) | undefined;
|
|
482
619
|
rows?: number | undefined;
|
|
483
620
|
cols?: number | undefined;
|
|
@@ -1031,8 +1168,15 @@ declare function createUniqueId(): string;
|
|
|
1031
1168
|
//#endregion
|
|
1032
1169
|
//#region src/show.d.ts
|
|
1033
1170
|
interface ShowProps extends Props {
|
|
1034
|
-
/**
|
|
1035
|
-
|
|
1171
|
+
/**
|
|
1172
|
+
* Truthy condition. Accepts a value or an accessor.
|
|
1173
|
+
*
|
|
1174
|
+
* Use an accessor (`() => signal()`) for reactive conditions.
|
|
1175
|
+
* Bare values are accepted for static cases and as a defensive normalization
|
|
1176
|
+
* for cases where the compiler's signal auto-call has already invoked
|
|
1177
|
+
* a signal at the prop site (e.g. `when={mySignal}` becomes `when={mySignal()}`).
|
|
1178
|
+
*/
|
|
1179
|
+
when: unknown | (() => unknown);
|
|
1036
1180
|
fallback?: VNodeChild;
|
|
1037
1181
|
children?: VNodeChild;
|
|
1038
1182
|
}
|
|
@@ -1051,8 +1195,8 @@ interface ShowProps extends Props {
|
|
|
1051
1195
|
*/
|
|
1052
1196
|
declare function Show(props: ShowProps): VNode | null;
|
|
1053
1197
|
interface MatchProps extends Props {
|
|
1054
|
-
/**
|
|
1055
|
-
when: () => unknown;
|
|
1198
|
+
/** Truthy condition. Accepts a value or an accessor — see {@link ShowProps.when}. */
|
|
1199
|
+
when: unknown | (() => unknown);
|
|
1056
1200
|
children?: VNodeChild;
|
|
1057
1201
|
}
|
|
1058
1202
|
/**
|
|
@@ -1075,6 +1219,10 @@ declare const MatchSymbol: unique symbol;
|
|
|
1075
1219
|
/**
|
|
1076
1220
|
* Error telemetry — hook into Pyreon's error reporting for Sentry, Datadog, etc.
|
|
1077
1221
|
*
|
|
1222
|
+
* Captures errors from ALL lifecycle phases including reactive effects.
|
|
1223
|
+
* `effect()` errors thrown by `@pyreon/reactivity` are bridged through a
|
|
1224
|
+
* globalThis sink (no upward import — reactivity doesn't depend on core).
|
|
1225
|
+
*
|
|
1078
1226
|
* @example
|
|
1079
1227
|
* import { registerErrorHandler } from "@pyreon/core"
|
|
1080
1228
|
* import * as Sentry from "@sentry/browser"
|
|
@@ -1086,7 +1234,7 @@ declare const MatchSymbol: unique symbol;
|
|
|
1086
1234
|
* })
|
|
1087
1235
|
*/
|
|
1088
1236
|
interface ErrorContext {
|
|
1089
|
-
/** Component function name, or "
|
|
1237
|
+
/** Component function name, "Anonymous", or "Effect" for reactive effects */
|
|
1090
1238
|
component: string;
|
|
1091
1239
|
/** Lifecycle phase where the error occurred */
|
|
1092
1240
|
phase: 'setup' | 'render' | 'mount' | 'unmount' | 'effect';
|
|
@@ -1100,7 +1248,13 @@ interface ErrorContext {
|
|
|
1100
1248
|
type ErrorHandler = (ctx: ErrorContext) => void;
|
|
1101
1249
|
/**
|
|
1102
1250
|
* Register a global error handler. Called whenever a component throws in any
|
|
1103
|
-
* lifecycle phase
|
|
1251
|
+
* lifecycle phase, OR an effect throws in `@pyreon/reactivity`. Returns an
|
|
1252
|
+
* unregister function.
|
|
1253
|
+
*
|
|
1254
|
+
* Also installs a `globalThis.__pyreon_report_error__` bridge so the
|
|
1255
|
+
* reactivity package (which can't depend on core) can forward effect errors
|
|
1256
|
+
* into the same telemetry pipeline. Pre-fix the two surfaces were
|
|
1257
|
+
* disconnected — Sentry/Datadog wiring missed effect-thrown errors.
|
|
1104
1258
|
*/
|
|
1105
1259
|
declare function registerErrorHandler(handler: ErrorHandler): () => void;
|
|
1106
1260
|
/**
|
|
@@ -1109,5 +1263,5 @@ declare function registerErrorHandler(handler: ErrorHandler): () => void;
|
|
|
1109
1263
|
*/
|
|
1110
1264
|
declare function reportError(ctx: ErrorContext): void;
|
|
1111
1265
|
//#endregion
|
|
1112
|
-
export { type AnchorAttributes, type ButtonAttributes, type CSSProperties, CSS_UNITLESS, type ClassValue, type CleanupFn, type ComponentFn, type ComponentInstance, type Context, type ContextSnapshot, 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, 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, captureContextStack, createContext, createReactiveContext, createRef, createUniqueId, cx, defineComponent, dispatchToErrorBoundary, h, lazy, makeReactiveProps, mapArray, mergeProps, normalizeStyleValue, onErrorCaptured, onMount, onUnmount, onUpdate, popContext, propagateError, provide, pushContext, registerErrorHandler, reportError, restoreContextStack, runWithHooks, setContextStackProvider, splitProps, toKebabCase, useContext, withContext };
|
|
1266
|
+
export { type AnchorAttributes, type ButtonAttributes, type CSSProperties, CSS_UNITLESS, type ClassValue, type CleanupFn, type ComponentFn, type ComponentInstance, type Context, type ContextSnapshot, 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, 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 };
|
|
1113
1267
|
//# sourceMappingURL=index2.d.ts.map
|
|
@@ -18,8 +18,22 @@ type Props = Record<string, unknown>;
|
|
|
18
18
|
type ComponentFn<P extends Props = Props> = (props: P) => VNodeChild;
|
|
19
19
|
//#endregion
|
|
20
20
|
//#region src/h.d.ts
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Marker for fragment nodes — renders children without a wrapper element.
|
|
23
|
+
*
|
|
24
|
+
* MUST use `Symbol.for(...)` (global registry, keyed by string), NOT
|
|
25
|
+
* `Symbol(...)` (fresh per evaluation). `h.ts` is inlined into BOTH the
|
|
26
|
+
* main `lib/index.js` and the `lib/jsx-runtime.js` published bundles —
|
|
27
|
+
* each bundle's evaluation of a bare `Symbol(...)` would produce a
|
|
28
|
+
* DISTINCT Symbol identity. JSX `<>` compiles to `jsx(Fragment, ...)` and
|
|
29
|
+
* resolves to jsx-runtime's identity; `runtime-server` checks
|
|
30
|
+
* `vnode.type === Fragment` against the main-entry identity. Mismatch
|
|
31
|
+
* fell through to `renderElement` and crashed SSG with
|
|
32
|
+
* `TypeError: Cannot convert a Symbol value to a string`.
|
|
33
|
+
* `Symbol.for()` keys by string in a global registry shared across all
|
|
34
|
+
* bundle evaluations — same identity everywhere.
|
|
35
|
+
*/
|
|
36
|
+
declare const Fragment: symbol;
|
|
23
37
|
//#endregion
|
|
24
38
|
//#region src/ref.d.ts
|
|
25
39
|
/**
|
|
@@ -135,6 +149,7 @@ interface PyreonHTMLAttributes<E extends Element = HTMLElement> {
|
|
|
135
149
|
} | undefined;
|
|
136
150
|
onClick?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
137
151
|
onDblClick?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
152
|
+
onDoubleClick?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
138
153
|
onMouseDown?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
139
154
|
onMouseUp?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
140
155
|
onMouseEnter?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
@@ -198,7 +213,7 @@ interface InputAttributes extends PyreonHTMLAttributes<HTMLInputElement> {
|
|
|
198
213
|
defaultChecked?: boolean | undefined;
|
|
199
214
|
placeholder?: string | (() => string) | undefined;
|
|
200
215
|
disabled?: boolean | (() => boolean) | undefined;
|
|
201
|
-
readOnly?: boolean | undefined;
|
|
216
|
+
readOnly?: boolean | (() => boolean) | undefined;
|
|
202
217
|
required?: boolean | (() => boolean) | undefined;
|
|
203
218
|
min?: string | number | undefined;
|
|
204
219
|
max?: string | number | undefined;
|
|
@@ -247,7 +262,7 @@ interface TextareaAttributes extends PyreonHTMLAttributes<HTMLTextAreaElement> {
|
|
|
247
262
|
defaultValue?: string | undefined;
|
|
248
263
|
placeholder?: string | (() => string) | undefined;
|
|
249
264
|
disabled?: boolean | (() => boolean) | undefined;
|
|
250
|
-
readOnly?: boolean | undefined;
|
|
265
|
+
readOnly?: boolean | (() => boolean) | undefined;
|
|
251
266
|
required?: boolean | (() => boolean) | undefined;
|
|
252
267
|
rows?: number | undefined;
|
|
253
268
|
cols?: number | undefined;
|
|
@@ -18,8 +18,22 @@ type Props = Record<string, unknown>;
|
|
|
18
18
|
type ComponentFn<P extends Props = Props> = (props: P) => VNodeChild;
|
|
19
19
|
//#endregion
|
|
20
20
|
//#region src/h.d.ts
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Marker for fragment nodes — renders children without a wrapper element.
|
|
23
|
+
*
|
|
24
|
+
* MUST use `Symbol.for(...)` (global registry, keyed by string), NOT
|
|
25
|
+
* `Symbol(...)` (fresh per evaluation). `h.ts` is inlined into BOTH the
|
|
26
|
+
* main `lib/index.js` and the `lib/jsx-runtime.js` published bundles —
|
|
27
|
+
* each bundle's evaluation of a bare `Symbol(...)` would produce a
|
|
28
|
+
* DISTINCT Symbol identity. JSX `<>` compiles to `jsx(Fragment, ...)` and
|
|
29
|
+
* resolves to jsx-runtime's identity; `runtime-server` checks
|
|
30
|
+
* `vnode.type === Fragment` against the main-entry identity. Mismatch
|
|
31
|
+
* fell through to `renderElement` and crashed SSG with
|
|
32
|
+
* `TypeError: Cannot convert a Symbol value to a string`.
|
|
33
|
+
* `Symbol.for()` keys by string in a global registry shared across all
|
|
34
|
+
* bundle evaluations — same identity everywhere.
|
|
35
|
+
*/
|
|
36
|
+
declare const Fragment: symbol;
|
|
23
37
|
//#endregion
|
|
24
38
|
//#region src/ref.d.ts
|
|
25
39
|
/**
|
|
@@ -135,6 +149,7 @@ interface PyreonHTMLAttributes<E extends Element = HTMLElement> {
|
|
|
135
149
|
} | undefined;
|
|
136
150
|
onClick?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
137
151
|
onDblClick?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
152
|
+
onDoubleClick?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
138
153
|
onMouseDown?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
139
154
|
onMouseUp?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
140
155
|
onMouseEnter?: ((e: TargetedEvent<E, MouseEvent>) => void) | undefined;
|
|
@@ -198,7 +213,7 @@ interface InputAttributes extends PyreonHTMLAttributes<HTMLInputElement> {
|
|
|
198
213
|
defaultChecked?: boolean | undefined;
|
|
199
214
|
placeholder?: string | (() => string) | undefined;
|
|
200
215
|
disabled?: boolean | (() => boolean) | undefined;
|
|
201
|
-
readOnly?: boolean | undefined;
|
|
216
|
+
readOnly?: boolean | (() => boolean) | undefined;
|
|
202
217
|
required?: boolean | (() => boolean) | undefined;
|
|
203
218
|
min?: string | number | undefined;
|
|
204
219
|
max?: string | number | undefined;
|
|
@@ -247,7 +262,7 @@ interface TextareaAttributes extends PyreonHTMLAttributes<HTMLTextAreaElement> {
|
|
|
247
262
|
defaultValue?: string | undefined;
|
|
248
263
|
placeholder?: string | (() => string) | undefined;
|
|
249
264
|
disabled?: boolean | (() => boolean) | undefined;
|
|
250
|
-
readOnly?: boolean | undefined;
|
|
265
|
+
readOnly?: boolean | (() => boolean) | undefined;
|
|
251
266
|
required?: boolean | (() => boolean) | undefined;
|
|
252
267
|
rows?: number | undefined;
|
|
253
268
|
cols?: number | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"lib",
|
|
17
|
+
"!lib/**/*.map",
|
|
17
18
|
"src",
|
|
18
19
|
"README.md",
|
|
19
20
|
"LICENSE"
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
"prepublishOnly": "bun run build"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@pyreon/reactivity": "^0.
|
|
56
|
+
"@pyreon/reactivity": "^0.16.0"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@pyreon/manifest": "0.13.1"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compat-mode native-component marker.
|
|
3
|
+
*
|
|
4
|
+
* Pyreon ships compat layers (`@pyreon/{react,preact,vue,solid}-compat`) that
|
|
5
|
+
* wrap every JSX-called component function to emulate that source framework's
|
|
6
|
+
* render-on-state-change semantics. That wrapping is correct for user code
|
|
7
|
+
* (the whole point of compat mode) but corrupts Pyreon framework components
|
|
8
|
+
* — those manage their own reactivity via `provide()` / signals / lifecycle
|
|
9
|
+
* hooks, and wrapping them runs their setup body inside the compat layer's
|
|
10
|
+
* render context instead of Pyreon's, breaking `provide()` and
|
|
11
|
+
* `onMount()` / `onUnmount()` calls.
|
|
12
|
+
*
|
|
13
|
+
* Framework components opt out of compat wrapping by setting a well-known
|
|
14
|
+
* registry symbol (`Symbol.for('pyreon:native-compat')`) on the function.
|
|
15
|
+
* The compat layer reads that symbol and routes marked components straight
|
|
16
|
+
* through Pyreon's `h()` mount path. The symbol is registry-shared, so no
|
|
17
|
+
* import direction between framework and compat is implied — both sides
|
|
18
|
+
* reference the same global symbol via the helpers exported here.
|
|
19
|
+
*
|
|
20
|
+
* Audience: framework-package authors writing JSX components in `@pyreon/*`
|
|
21
|
+
* packages whose setup body uses `provide()` / lifecycle hooks / signal
|
|
22
|
+
* subscriptions. Wrap exported components with `nativeCompat()`. One line
|
|
23
|
+
* per export site; zero runtime cost beyond a single property write at
|
|
24
|
+
* module load.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The well-known registry symbol that marks a component as a Pyreon native
|
|
29
|
+
* framework component. Compat layers check this symbol to decide whether to
|
|
30
|
+
* skip their `wrapCompatComponent` call.
|
|
31
|
+
*
|
|
32
|
+
* Exported for advanced cases where a caller needs to test the marker
|
|
33
|
+
* directly (most callers should use `isNativeCompat()`).
|
|
34
|
+
*/
|
|
35
|
+
export const NATIVE_COMPAT_MARKER: symbol = Symbol.for('pyreon:native-compat')
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Mark a Pyreon framework component as "self-managing" — compat layers will
|
|
39
|
+
* skip their React/Vue/Solid/Preact-style wrapping and route the component
|
|
40
|
+
* directly through Pyreon's mount path. Use on every `@pyreon/*` JSX
|
|
41
|
+
* component whose setup body uses `provide()`, lifecycle hooks
|
|
42
|
+
* (`onMount` / `onUnmount`), signal-driven reactivity, or any other Pyreon
|
|
43
|
+
* native pattern that depends on the active component-setup frame.
|
|
44
|
+
*
|
|
45
|
+
* Idempotent: re-applying the marker is a no-op. Non-function inputs pass
|
|
46
|
+
* through unchanged so callers don't have to typecheck before wrapping.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* import { nativeCompat, provide } from '@pyreon/core'
|
|
50
|
+
*
|
|
51
|
+
* export const RouterView = nativeCompat(function RouterView(props) {
|
|
52
|
+
* provide(RouterContext, ...)
|
|
53
|
+
* return <div data-pyreon-router-view>{children}</div>
|
|
54
|
+
* })
|
|
55
|
+
*/
|
|
56
|
+
export function nativeCompat<T>(fn: T): T {
|
|
57
|
+
if (typeof fn === 'function') {
|
|
58
|
+
;(fn as unknown as Record<symbol, boolean>)[NATIVE_COMPAT_MARKER] = true
|
|
59
|
+
}
|
|
60
|
+
return fn
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read whether a component has been marked as a Pyreon native framework
|
|
65
|
+
* component. Compat-layer code calls this from its `jsx()` to decide whether
|
|
66
|
+
* to wrap or pass through.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* import { isNativeCompat } from '@pyreon/core'
|
|
70
|
+
*
|
|
71
|
+
* if (isNativeCompat(type)) return h(type, props)
|
|
72
|
+
* return wrapCompatComponent(type)(props)
|
|
73
|
+
*/
|
|
74
|
+
export function isNativeCompat(fn: unknown): boolean {
|
|
75
|
+
return (
|
|
76
|
+
typeof fn === 'function' &&
|
|
77
|
+
(fn as unknown as Record<symbol, boolean>)[NATIVE_COMPAT_MARKER] === true
|
|
78
|
+
)
|
|
79
|
+
}
|
package/src/context.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* The renderer maintains the context stack as it walks the VNode tree.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { setSnapshotCapture } from '@pyreon/reactivity'
|
|
8
9
|
import { onUnmount } from './lifecycle'
|
|
9
10
|
|
|
10
11
|
export interface Context<T> {
|
|
@@ -66,8 +67,7 @@ function getStack(): Map<symbol, unknown>[] {
|
|
|
66
67
|
|
|
67
68
|
// Dev-mode gate: see `pyreon/no-process-dev-gate` lint rule for why this
|
|
68
69
|
// uses `import.meta.env.DEV` instead of `typeof process !== 'undefined'`.
|
|
69
|
-
|
|
70
|
-
const __DEV__ = import.meta.env?.DEV === true
|
|
70
|
+
const __DEV__ = process.env.NODE_ENV !== 'production'
|
|
71
71
|
|
|
72
72
|
export function pushContext(values: Map<symbol, unknown>) {
|
|
73
73
|
getStack().push(values)
|
|
@@ -149,13 +149,22 @@ export function captureContextStack(): ContextSnapshot {
|
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
151
|
* Execute `fn()` with a previously captured context stack active.
|
|
152
|
-
*
|
|
152
|
+
*
|
|
153
|
+
* After `fn()` returns, removes ONLY the snapshot frames this call pushed
|
|
154
|
+
* — anything `fn()` itself pushed (typically provider frames from
|
|
155
|
+
* `provide()` calls during component mount) stays on the stack so
|
|
156
|
+
* subsequent reactive re-runs (e.g. `_bind` text bindings,
|
|
157
|
+
* `renderEffect` callbacks) can still find ancestor providers via
|
|
158
|
+
* `useContext`. Pre-fix this method was `stack.length = savedLength`,
|
|
159
|
+
* which destructively truncated provider frames pushed during mount —
|
|
160
|
+
* silently breaking `useMode()` / `useTheme()` / `useRouter()` etc. on
|
|
161
|
+
* every signal-driven update under a `mountReactive` boundary.
|
|
153
162
|
*/
|
|
154
163
|
export function restoreContextStack<T>(snapshot: ContextSnapshot, fn: () => T): T {
|
|
155
164
|
const stack = getStack()
|
|
156
|
-
const
|
|
165
|
+
const insertIndex = stack.length
|
|
157
166
|
|
|
158
|
-
// Push
|
|
167
|
+
// Push captured snapshot frames at the END of the current stack.
|
|
159
168
|
for (const frame of snapshot) {
|
|
160
169
|
stack.push(frame)
|
|
161
170
|
}
|
|
@@ -163,7 +172,29 @@ export function restoreContextStack<T>(snapshot: ContextSnapshot, fn: () => T):
|
|
|
163
172
|
try {
|
|
164
173
|
return fn()
|
|
165
174
|
} finally {
|
|
166
|
-
//
|
|
167
|
-
|
|
175
|
+
// Splice out exactly the snapshot frames we pushed (they sit at
|
|
176
|
+
// [insertIndex, insertIndex + snapshot.length)). Any frames `fn()`
|
|
177
|
+
// pushed AFTER our snapshot (provider frames) get shifted down by
|
|
178
|
+
// `snapshot.length` positions but remain on the stack. Their owning
|
|
179
|
+
// components' `onUnmount(popContext)` handlers will pop them in
|
|
180
|
+
// LIFO order on subtree teardown — splice preserves that ordering
|
|
181
|
+
// because it doesn't touch frames at indices >= insertIndex +
|
|
182
|
+
// snapshot.length until the splice operation itself.
|
|
183
|
+
stack.splice(insertIndex, snapshot.length)
|
|
168
184
|
}
|
|
169
185
|
}
|
|
186
|
+
|
|
187
|
+
// ─── Reactivity-layer DI: install context capture/restore for effects ────────
|
|
188
|
+
//
|
|
189
|
+
// `_bind` / `renderEffect` / `effect` (in `@pyreon/reactivity`) capture this
|
|
190
|
+
// snapshot at setup and restore it on every subsequent re-run. Without this,
|
|
191
|
+
// signal-driven re-runs after the synchronous mount see whatever the GLOBAL
|
|
192
|
+
// context stack looks like at that moment — which may be missing provider
|
|
193
|
+
// frames for any number of reasons (sibling subtree mounts/unmounts mutating
|
|
194
|
+
// the stack, async re-render cycles, etc.). Defense-in-depth alongside the
|
|
195
|
+
// `restoreContextStack` splice fix above.
|
|
196
|
+
setSnapshotCapture({
|
|
197
|
+
capture: () => captureContextStack(),
|
|
198
|
+
restore: <T>(snap: unknown, fn: () => T): T =>
|
|
199
|
+
restoreContextStack(snap as ContextSnapshot, fn),
|
|
200
|
+
})
|
package/src/dynamic.ts
CHANGED
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import { h } from './h'
|
|
2
|
-
import type { ComponentFn, Props, VNode } from './types'
|
|
2
|
+
import type { ComponentFn, Props, VNode, VNodeChild } from './types'
|
|
3
3
|
|
|
4
4
|
// Dev-mode gate: see `pyreon/no-process-dev-gate` lint rule for why this
|
|
5
5
|
// uses `import.meta.env.DEV` instead of `typeof process !== 'undefined'`.
|
|
6
|
-
|
|
7
|
-
const __DEV__ = import.meta.env?.DEV === true
|
|
6
|
+
const __DEV__ = process.env.NODE_ENV !== 'production'
|
|
8
7
|
|
|
9
8
|
export interface DynamicProps extends Props {
|
|
10
9
|
component: ComponentFn | string
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
export function Dynamic(props: DynamicProps): VNode | null {
|
|
14
|
-
const { component, ...rest } = props
|
|
13
|
+
const { component, children, ...rest } = props as DynamicProps & { children?: unknown }
|
|
15
14
|
if (__DEV__ && !component) {
|
|
16
15
|
// oxlint-disable-next-line no-console
|
|
17
16
|
console.warn('[Pyreon] <Dynamic> received a falsy `component` prop. Nothing will be rendered.')
|
|
18
17
|
}
|
|
19
18
|
if (!component) return null
|
|
20
|
-
|
|
19
|
+
// Children must NOT remain in props. When `component` is a string tag
|
|
20
|
+
// (e.g. <Dynamic component="h3">x</Dynamic>), runtime-dom's prop applier
|
|
21
|
+
// forwards every prop key to setAttribute, so a leaked `children` prop
|
|
22
|
+
// crashes with `setAttribute('children', ...)`. Re-emit them as h() rest
|
|
23
|
+
// args so they land in vnode.children, which is where both string-tag
|
|
24
|
+
// mounts and component-merge expect them.
|
|
25
|
+
if (children === undefined) {
|
|
26
|
+
return h(component as string | ComponentFn, rest as Props)
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(children)) {
|
|
29
|
+
return h(component as string | ComponentFn, rest as Props, ...(children as VNodeChild[]))
|
|
30
|
+
}
|
|
31
|
+
return h(component as string | ComponentFn, rest as Props, children as VNodeChild)
|
|
21
32
|
}
|
package/src/error-boundary.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { signal } from '@pyreon/reactivity'
|
|
2
|
+
import { nativeCompat } from './compat-marker'
|
|
2
3
|
import { popErrorBoundary, pushErrorBoundary } from './component'
|
|
3
4
|
import { onUnmount } from './lifecycle'
|
|
4
5
|
import { reportError } from './telemetry'
|
|
@@ -6,8 +7,7 @@ import type { VNodeChild, VNodeChildAtom } from './types'
|
|
|
6
7
|
|
|
7
8
|
// Dev-mode gate: see `pyreon/no-process-dev-gate` lint rule for why this
|
|
8
9
|
// uses `import.meta.env.DEV` instead of `typeof process !== 'undefined'`.
|
|
9
|
-
|
|
10
|
-
const __DEV__ = import.meta.env?.DEV === true
|
|
10
|
+
const __DEV__ = process.env.NODE_ENV !== 'production'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* ErrorBoundary — catches errors thrown by child components and renders a
|
|
@@ -53,6 +53,14 @@ export function ErrorBoundary(props: {
|
|
|
53
53
|
|
|
54
54
|
const handler = (err: unknown): boolean => {
|
|
55
55
|
if (error.peek() !== null) return false // already in error state — let outer boundary catch it
|
|
56
|
+
// Synchronous signal write. The handler fires from inside mountComponent's
|
|
57
|
+
// catch, which is itself inside the boundary's own mountReactive effect
|
|
58
|
+
// run (the run that mounted the throwing child). The batch system's
|
|
59
|
+
// two-tier flush handles this correctly: this `error.set(err)` enqueues
|
|
60
|
+
// the boundary's run into the effects queue's nextPass (since the run is
|
|
61
|
+
// currently being visited), and the next pass fires it to swap to the
|
|
62
|
+
// fallback subtree. See packages/core/reactivity/src/batch.ts for the
|
|
63
|
+
// multi-pass effect drain contract.
|
|
56
64
|
error.set(err)
|
|
57
65
|
reportError({ component: 'ErrorBoundary', phase: 'render', error: err, timestamp: Date.now() })
|
|
58
66
|
return true
|
|
@@ -69,3 +77,8 @@ export function ErrorBoundary(props: {
|
|
|
69
77
|
return (typeof ch === 'function' ? ch() : ch) as VNodeChildAtom
|
|
70
78
|
}
|
|
71
79
|
}
|
|
80
|
+
|
|
81
|
+
// Mark as native so compat-mode jsx() runtimes (react/preact/vue/solid-compat)
|
|
82
|
+
// skip wrapCompatComponent — ErrorBoundary uses pushErrorBoundary/onUnmount,
|
|
83
|
+
// which need Pyreon's setup frame (compat wrapping breaks dispatchToErrorBoundary).
|
|
84
|
+
nativeCompat(ErrorBoundary)
|
package/src/for.ts
CHANGED
|
@@ -7,7 +7,19 @@ import type { NativeItem, Props, VNode } from './types'
|
|
|
7
7
|
export const ForSymbol: unique symbol = Symbol('pyreon.For')
|
|
8
8
|
|
|
9
9
|
export interface ForProps<T> {
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The list to iterate. Accepts EITHER a function returning the array
|
|
12
|
+
* (preferred — keeps reactivity intact when the array comes from a
|
|
13
|
+
* signal accessor) OR the array directly (convenient for static lists
|
|
14
|
+
* or already-resolved arrays). The runtime in `runtime-dom/src/mount.ts`
|
|
15
|
+
* normalizes both shapes; this type matches the runtime so users aren't
|
|
16
|
+
* forced to write `each={() => items}` for a plain array.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* <For each={items}>{r => <li>{r.label}</li>}</For> // static
|
|
20
|
+
* <For each={() => store.items()}>{r => <li>...</li>}</For> // reactive
|
|
21
|
+
*/
|
|
22
|
+
each: T[] | (() => T[])
|
|
11
23
|
/** Keying function — use `by` not `key` (JSX extracts `key` for VNode reconciliation). */
|
|
12
24
|
by: (item: T) => string | number
|
|
13
25
|
children: (item: T) => VNode | NativeItem
|