@pyreon/core 0.15.0 → 0.18.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 +198 -16
- package/lib/jsx-dev-runtime.js +45 -11
- package/lib/jsx-runtime.js +45 -11
- package/lib/types/index.d.ts +176 -6
- package/lib/types/jsx-dev-runtime.d.ts +16 -2
- package/lib/types/jsx-runtime.d.ts +16 -2
- package/package.json +2 -2
- package/src/defer.ts +241 -0
- package/src/for.ts +13 -1
- package/src/h.ts +16 -2
- package/src/index.ts +11 -1
- package/src/jsx-runtime.ts +46 -8
- package/src/manifest.ts +12 -4
- package/src/props.ts +59 -0
- package/src/tests/core.test.ts +1 -1
- package/src/tests/defer.test.ts +359 -0
- 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/reactive-props.test.ts +71 -1
- package/src/types.ts +43 -2
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
|
/**
|
|
@@ -282,7 +315,19 @@ declare function ErrorBoundary(props: {
|
|
|
282
315
|
*/
|
|
283
316
|
declare const ForSymbol: unique symbol;
|
|
284
317
|
interface ForProps<T> {
|
|
285
|
-
|
|
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[]);
|
|
286
331
|
/** Keying function — use `by` not `key` (JSX extracts `key` for VNode reconciliation). */
|
|
287
332
|
by: (item: T) => string | number;
|
|
288
333
|
children: (item: T) => VNode | NativeItem;
|
|
@@ -306,8 +351,22 @@ interface ForProps<T> {
|
|
|
306
351
|
declare function For<T>(props: ForProps<T>): VNode;
|
|
307
352
|
//#endregion
|
|
308
353
|
//#region src/h.d.ts
|
|
309
|
-
/**
|
|
310
|
-
|
|
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;
|
|
311
370
|
/**
|
|
312
371
|
* Hyperscript function — the compiled output of JSX.
|
|
313
372
|
* `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
|
|
@@ -952,6 +1011,89 @@ declare global {
|
|
|
952
1011
|
}
|
|
953
1012
|
} //# sourceMappingURL=jsx-runtime.d.ts.map
|
|
954
1013
|
//#endregion
|
|
1014
|
+
//#region src/defer.d.ts
|
|
1015
|
+
/**
|
|
1016
|
+
* Module shape `<Defer>` accepts from `chunk()`. Mirrors `lazy()`'s
|
|
1017
|
+
* contract — either an ES module with `default` export, OR a raw
|
|
1018
|
+
* `ComponentFn` returned directly (rare; covers re-export patterns).
|
|
1019
|
+
*/
|
|
1020
|
+
type ChunkResult<P extends Props> = {
|
|
1021
|
+
default: ComponentFn<P>;
|
|
1022
|
+
} | ComponentFn<P>;
|
|
1023
|
+
/**
|
|
1024
|
+
* Trigger discriminant. Exactly ONE shape is provided:
|
|
1025
|
+
* - `when={() => signal()}` — load when the accessor becomes truthy
|
|
1026
|
+
* - `on="visible"` — load when the wrapper enters the viewport
|
|
1027
|
+
* - `on="idle"` — load during browser idle time
|
|
1028
|
+
*/
|
|
1029
|
+
type DeferTrigger = {
|
|
1030
|
+
when: () => boolean;
|
|
1031
|
+
} | {
|
|
1032
|
+
on: 'visible' | 'idle';
|
|
1033
|
+
};
|
|
1034
|
+
/**
|
|
1035
|
+
* Set up the `on="idle"` trigger. Returns a teardown function the
|
|
1036
|
+
* caller must invoke on unmount. Browser-API access is gated by
|
|
1037
|
+
* `typeof` checks so SSR / jsdom environments fall back to a
|
|
1038
|
+
* `setTimeout(1)` shim. Extracted as a standalone helper so it's
|
|
1039
|
+
* directly testable without going through `onMount` (core tests
|
|
1040
|
+
* don't run in happy-dom; runtime-dom is where the lifecycle hooks
|
|
1041
|
+
* live).
|
|
1042
|
+
*
|
|
1043
|
+
* @internal Exported for tests; not part of the stable public API.
|
|
1044
|
+
*/
|
|
1045
|
+
type DeferProps<P extends Props> = DeferTrigger & {
|
|
1046
|
+
/**
|
|
1047
|
+
* Dynamic import to lazy-load. The literal `import('./X')` is what
|
|
1048
|
+
* Rolldown / Vite see when emitting chunks — using a variable here
|
|
1049
|
+
* defeats code splitting.
|
|
1050
|
+
*/
|
|
1051
|
+
chunk: () => Promise<ChunkResult<P>>;
|
|
1052
|
+
/**
|
|
1053
|
+
* Render-prop for the loaded component. Receives the resolved component
|
|
1054
|
+
* and returns its JSX with whatever props the parent needs to pass.
|
|
1055
|
+
* Optional — omitting it renders `<Comp />` with no props.
|
|
1056
|
+
*/
|
|
1057
|
+
children?: (Component: ComponentFn<P>) => VNodeChild; /** Shown while the chunk is loading. Default: `null`. */
|
|
1058
|
+
fallback?: VNodeChild;
|
|
1059
|
+
/**
|
|
1060
|
+
* IntersectionObserver `rootMargin` for `on="visible"` mode. Default
|
|
1061
|
+
* `'200px'` — start loading the chunk before the wrapper is fully in
|
|
1062
|
+
* view so it's typically ready by the time the user scrolls to it.
|
|
1063
|
+
*/
|
|
1064
|
+
rootMargin?: string;
|
|
1065
|
+
};
|
|
1066
|
+
/**
|
|
1067
|
+
* Lazy-load a chunk when a trigger condition is met.
|
|
1068
|
+
*
|
|
1069
|
+
* Three trigger modes:
|
|
1070
|
+
* - `when={() => signal()}` — load when condition flips truthy (modal pattern)
|
|
1071
|
+
* - `on="visible"` — load when the wrapper scrolls into view
|
|
1072
|
+
* - `on="idle"` — load during browser idle time
|
|
1073
|
+
*
|
|
1074
|
+
* The chunk fetch is fired exactly once per `Defer` instance — repeated
|
|
1075
|
+
* trigger firings after the chunk loads are no-ops.
|
|
1076
|
+
*
|
|
1077
|
+
* @example
|
|
1078
|
+
* // Signal-driven (modal):
|
|
1079
|
+
* <Defer chunk={() => import('./ConfirmDeleteModal')} when={open}>
|
|
1080
|
+
* {Modal => <Modal onClose={() => setOpen(false)} />}
|
|
1081
|
+
* </Defer>
|
|
1082
|
+
*
|
|
1083
|
+
* @example
|
|
1084
|
+
* // Viewport-driven (below-fold):
|
|
1085
|
+
* <Defer chunk={() => import('./Comments')} on="visible">
|
|
1086
|
+
* {Comments => <Comments postId={id} />}
|
|
1087
|
+
* </Defer>
|
|
1088
|
+
*
|
|
1089
|
+
* @example
|
|
1090
|
+
* // Idle-driven (non-critical):
|
|
1091
|
+
* <Defer chunk={() => import('./Analytics')} on="idle">
|
|
1092
|
+
* {Dashboard => <Dashboard />}
|
|
1093
|
+
* </Defer>
|
|
1094
|
+
*/
|
|
1095
|
+
declare function Defer<P extends Props>(props: DeferProps<P>): VNode;
|
|
1096
|
+
//#endregion
|
|
955
1097
|
//#region src/suspense.d.ts
|
|
956
1098
|
/** Internal marker attached to lazy()-wrapped components */
|
|
957
1099
|
type LazyComponent<P extends Props = Props> = ((props: P) => VNodeChild) & {
|
|
@@ -1085,6 +1227,34 @@ declare const REACTIVE_PROP: unique symbol;
|
|
|
1085
1227
|
* Called by the compiler for component prop expressions containing signal reads.
|
|
1086
1228
|
*/
|
|
1087
1229
|
declare function _rp<T>(fn: () => T): () => T;
|
|
1230
|
+
/**
|
|
1231
|
+
* Wrap a JSX spread source so its getter-shaped reactive props survive
|
|
1232
|
+
* the JS-level object spread that esbuild's automatic JSX runtime emits
|
|
1233
|
+
* for `<Comp {...source}>`.
|
|
1234
|
+
*
|
|
1235
|
+
* Without this wrapper, esbuild compiles `<Comp {...source}>` to
|
|
1236
|
+
* `jsx(Comp, { ...source })` — and JS spread fires every getter on
|
|
1237
|
+
* `source`, storing the resolved values as plain data properties. Any
|
|
1238
|
+
* compiler-emitted reactive prop (`_rp(() => signal())` converted to a
|
|
1239
|
+
* getter by `makeReactiveProps`) on `source` is collapsed to its
|
|
1240
|
+
* initial value before the receiving component ever sees it.
|
|
1241
|
+
*
|
|
1242
|
+
* `_wrapSpread(source)` walks `source`'s own keys via `Reflect.ownKeys`
|
|
1243
|
+
* (no getter firing) and returns a new object whose values are
|
|
1244
|
+
* `_rp`-branded thunks `() => source[key]`. When `{ ..._wrapSpread(s) }`
|
|
1245
|
+
* is spread by esbuild, the thunks are stored as plain data property
|
|
1246
|
+
* values (no getters to fire), then `makeReactiveProps` in `mount.ts`
|
|
1247
|
+
* converts the brands back into getters that lazily read from the
|
|
1248
|
+
* original `source` — preserving the reactive subscription end-to-end.
|
|
1249
|
+
*
|
|
1250
|
+
* Fast path: when `source` has no getter descriptors, return the
|
|
1251
|
+
* source object unchanged. JS spread will work correctly in that case
|
|
1252
|
+
* because there's nothing reactive to preserve. Saves N thunk
|
|
1253
|
+
* allocations per component render in the 99% case.
|
|
1254
|
+
*
|
|
1255
|
+
* Emitted by the compiler — not generally meant for hand-written code.
|
|
1256
|
+
*/
|
|
1257
|
+
declare function _wrapSpread(source: Record<string, unknown> | null | undefined): Record<string, unknown> | null | undefined;
|
|
1088
1258
|
/**
|
|
1089
1259
|
* Convert compiler-emitted `_rp(() => expr)` prop values into getter properties.
|
|
1090
1260
|
*
|
|
@@ -1204,5 +1374,5 @@ declare function registerErrorHandler(handler: ErrorHandler): () => void;
|
|
|
1204
1374
|
*/
|
|
1205
1375
|
declare function reportError(ctx: ErrorContext): void;
|
|
1206
1376
|
//#endregion
|
|
1207
|
-
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 };
|
|
1377
|
+
export { type AnchorAttributes, type ButtonAttributes, type CSSProperties, CSS_UNITLESS, type ClassValue, type CleanupFn, type ComponentFn, type ComponentInstance, type Context, type ContextSnapshot, Defer, type DeferProps, 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, _wrapSpread, 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 };
|
|
1208
1378
|
//# 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
|
/**
|
|
@@ -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
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"prepublishOnly": "bun run build"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@pyreon/reactivity": "^0.
|
|
56
|
+
"@pyreon/reactivity": "^0.18.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@pyreon/manifest": "0.13.1"
|
package/src/defer.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { effect, signal } from '@pyreon/reactivity'
|
|
2
|
+
import { Fragment, h } from './h'
|
|
3
|
+
import { onMount } from './lifecycle'
|
|
4
|
+
import { createRef } from './ref'
|
|
5
|
+
import type { ComponentFn, Props, VNode, VNodeChild, VNodeChildAccessor } from './types'
|
|
6
|
+
|
|
7
|
+
// Dev-mode gate (bundler-agnostic, see pyreon/no-process-dev-gate).
|
|
8
|
+
const __DEV__ = process.env.NODE_ENV !== 'production'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Module shape `<Defer>` accepts from `chunk()`. Mirrors `lazy()`'s
|
|
12
|
+
* contract — either an ES module with `default` export, OR a raw
|
|
13
|
+
* `ComponentFn` returned directly (rare; covers re-export patterns).
|
|
14
|
+
*/
|
|
15
|
+
type ChunkResult<P extends Props> = { default: ComponentFn<P> } | ComponentFn<P>
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Trigger discriminant. Exactly ONE shape is provided:
|
|
19
|
+
* - `when={() => signal()}` — load when the accessor becomes truthy
|
|
20
|
+
* - `on="visible"` — load when the wrapper enters the viewport
|
|
21
|
+
* - `on="idle"` — load during browser idle time
|
|
22
|
+
*/
|
|
23
|
+
type DeferTrigger = { when: () => boolean } | { on: 'visible' | 'idle' }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set up the `on="idle"` trigger. Returns a teardown function the
|
|
27
|
+
* caller must invoke on unmount. Browser-API access is gated by
|
|
28
|
+
* `typeof` checks so SSR / jsdom environments fall back to a
|
|
29
|
+
* `setTimeout(1)` shim. Extracted as a standalone helper so it's
|
|
30
|
+
* directly testable without going through `onMount` (core tests
|
|
31
|
+
* don't run in happy-dom; runtime-dom is where the lifecycle hooks
|
|
32
|
+
* live).
|
|
33
|
+
*
|
|
34
|
+
* @internal Exported for tests; not part of the stable public API.
|
|
35
|
+
*/
|
|
36
|
+
export function _setupIdleTrigger(startLoad: () => void): () => void {
|
|
37
|
+
const ric = (
|
|
38
|
+
globalThis as { requestIdleCallback?: (cb: () => void) => number }
|
|
39
|
+
).requestIdleCallback
|
|
40
|
+
const cic = (
|
|
41
|
+
globalThis as { cancelIdleCallback?: (id: number) => void }
|
|
42
|
+
).cancelIdleCallback
|
|
43
|
+
if (typeof ric === 'function') {
|
|
44
|
+
const id = ric(startLoad)
|
|
45
|
+
return () => cic?.(id)
|
|
46
|
+
}
|
|
47
|
+
const t = setTimeout(startLoad, 1)
|
|
48
|
+
return () => clearTimeout(t)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Set up the `on="visible"` trigger. Observes `el` via an
|
|
53
|
+
* `IntersectionObserver` and fires `startLoad` once on the first
|
|
54
|
+
* intersection. If `IntersectionObserver` is unavailable (jsdom)
|
|
55
|
+
* or `el` is null (SSR), falls back to loading immediately.
|
|
56
|
+
*
|
|
57
|
+
* Returns a teardown function — call to disconnect the observer.
|
|
58
|
+
*
|
|
59
|
+
* @internal Exported for tests; not part of the stable public API.
|
|
60
|
+
*/
|
|
61
|
+
export function _setupVisibleTrigger(
|
|
62
|
+
el: HTMLElement | null,
|
|
63
|
+
startLoad: () => void,
|
|
64
|
+
rootMargin: string,
|
|
65
|
+
): () => void {
|
|
66
|
+
if (!el || typeof IntersectionObserver === 'undefined') {
|
|
67
|
+
// Observer unavailable or no DOM target — load eagerly so the
|
|
68
|
+
// user still sees the component in environments where the
|
|
69
|
+
// viewport-detection mechanism can't run.
|
|
70
|
+
startLoad()
|
|
71
|
+
return () => {}
|
|
72
|
+
}
|
|
73
|
+
const obs = new IntersectionObserver(
|
|
74
|
+
(entries) => {
|
|
75
|
+
if (entries.some((e) => e.isIntersecting)) {
|
|
76
|
+
startLoad()
|
|
77
|
+
obs.disconnect()
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{ rootMargin },
|
|
81
|
+
)
|
|
82
|
+
obs.observe(el)
|
|
83
|
+
return () => obs.disconnect()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type DeferProps<P extends Props> = DeferTrigger & {
|
|
87
|
+
/**
|
|
88
|
+
* Dynamic import to lazy-load. The literal `import('./X')` is what
|
|
89
|
+
* Rolldown / Vite see when emitting chunks — using a variable here
|
|
90
|
+
* defeats code splitting.
|
|
91
|
+
*/
|
|
92
|
+
chunk: () => Promise<ChunkResult<P>>
|
|
93
|
+
/**
|
|
94
|
+
* Render-prop for the loaded component. Receives the resolved component
|
|
95
|
+
* and returns its JSX with whatever props the parent needs to pass.
|
|
96
|
+
* Optional — omitting it renders `<Comp />` with no props.
|
|
97
|
+
*/
|
|
98
|
+
children?: (Component: ComponentFn<P>) => VNodeChild
|
|
99
|
+
/** Shown while the chunk is loading. Default: `null`. */
|
|
100
|
+
fallback?: VNodeChild
|
|
101
|
+
/**
|
|
102
|
+
* IntersectionObserver `rootMargin` for `on="visible"` mode. Default
|
|
103
|
+
* `'200px'` — start loading the chunk before the wrapper is fully in
|
|
104
|
+
* view so it's typically ready by the time the user scrolls to it.
|
|
105
|
+
*/
|
|
106
|
+
rootMargin?: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Lazy-load a chunk when a trigger condition is met.
|
|
111
|
+
*
|
|
112
|
+
* Three trigger modes:
|
|
113
|
+
* - `when={() => signal()}` — load when condition flips truthy (modal pattern)
|
|
114
|
+
* - `on="visible"` — load when the wrapper scrolls into view
|
|
115
|
+
* - `on="idle"` — load during browser idle time
|
|
116
|
+
*
|
|
117
|
+
* The chunk fetch is fired exactly once per `Defer` instance — repeated
|
|
118
|
+
* trigger firings after the chunk loads are no-ops.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // Signal-driven (modal):
|
|
122
|
+
* <Defer chunk={() => import('./ConfirmDeleteModal')} when={open}>
|
|
123
|
+
* {Modal => <Modal onClose={() => setOpen(false)} />}
|
|
124
|
+
* </Defer>
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* // Viewport-driven (below-fold):
|
|
128
|
+
* <Defer chunk={() => import('./Comments')} on="visible">
|
|
129
|
+
* {Comments => <Comments postId={id} />}
|
|
130
|
+
* </Defer>
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* // Idle-driven (non-critical):
|
|
134
|
+
* <Defer chunk={() => import('./Analytics')} on="idle">
|
|
135
|
+
* {Dashboard => <Dashboard />}
|
|
136
|
+
* </Defer>
|
|
137
|
+
*/
|
|
138
|
+
export function Defer<P extends Props>(props: DeferProps<P>): VNode {
|
|
139
|
+
const Loaded = signal<ComponentFn<P> | null>(null)
|
|
140
|
+
const Failed = signal<Error | null>(null)
|
|
141
|
+
// Module-scope flag prevents repeat fetches when the trigger condition
|
|
142
|
+
// oscillates (e.g. modal opens / closes / opens again). The chunk only
|
|
143
|
+
// loads once per Defer mount.
|
|
144
|
+
let loadStarted = false
|
|
145
|
+
|
|
146
|
+
const startLoad = (): void => {
|
|
147
|
+
if (loadStarted) return
|
|
148
|
+
loadStarted = true
|
|
149
|
+
props
|
|
150
|
+
.chunk()
|
|
151
|
+
.then((mod) => {
|
|
152
|
+
// Accept both ES-module-default and bare ComponentFn shapes.
|
|
153
|
+
const Comp =
|
|
154
|
+
typeof mod === 'function'
|
|
155
|
+
? mod
|
|
156
|
+
: (mod as { default: ComponentFn<P> }).default
|
|
157
|
+
if (__DEV__ && typeof Comp !== 'function') {
|
|
158
|
+
// oxlint-disable-next-line no-console
|
|
159
|
+
console.warn(
|
|
160
|
+
'[Pyreon] <Defer> chunk() resolved without a default-exported component. Make sure your module exports default.',
|
|
161
|
+
)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
Loaded.set(Comp)
|
|
165
|
+
})
|
|
166
|
+
.catch((err) => {
|
|
167
|
+
const wrapped = err instanceof Error ? err : new Error(String(err))
|
|
168
|
+
if (__DEV__) {
|
|
169
|
+
// oxlint-disable-next-line no-console
|
|
170
|
+
console.error('[Pyreon] <Defer> chunk() rejected:', wrapped)
|
|
171
|
+
}
|
|
172
|
+
Failed.set(wrapped)
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Trigger wiring — exactly one branch fires per instance.
|
|
177
|
+
if ('when' in props) {
|
|
178
|
+
// Signal-driven. Subscribe to the accessor; load when it transitions
|
|
179
|
+
// to truthy. Repeat truthy emissions are no-ops via `loadStarted`.
|
|
180
|
+
effect(() => {
|
|
181
|
+
if (props.when() && !loadStarted) startLoad()
|
|
182
|
+
})
|
|
183
|
+
} else if (props.on === 'idle') {
|
|
184
|
+
// Idle-driven. Delegated to `_setupIdleTrigger` so the browser-API
|
|
185
|
+
// branching is testable as a pure function. Wrapped in onMount so
|
|
186
|
+
// SSR / non-browser environments don't fire the callback at all.
|
|
187
|
+
onMount(() => _setupIdleTrigger(startLoad))
|
|
188
|
+
}
|
|
189
|
+
// Note: `on === 'visible'` is wired below alongside the wrapper element
|
|
190
|
+
// because it needs a DOM target to observe.
|
|
191
|
+
|
|
192
|
+
// Inline accessor — type annotation deliberately omitted so the
|
|
193
|
+
// inferred return type narrows to `VNodeChildAtom | VNodeChildAtom[]`
|
|
194
|
+
// (what `h()`'s rest-args expect). Annotating as `VNodeChild` widens
|
|
195
|
+
// to include `VNodeChildAccessor`, which can't be returned from another
|
|
196
|
+
// accessor.
|
|
197
|
+
const renderContent = () => {
|
|
198
|
+
const err = Failed()
|
|
199
|
+
if (err) throw err
|
|
200
|
+
const Comp = Loaded()
|
|
201
|
+
if (!Comp) return props.fallback ?? null
|
|
202
|
+
return props.children ? props.children(Comp) : h(Comp as ComponentFn, {})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if ('on' in props && props.on === 'visible') {
|
|
206
|
+
// Visible-mode needs a DOM target for IntersectionObserver. A
|
|
207
|
+
// wrapper `<div data-pyreon-defer="visible">` carries the ref and
|
|
208
|
+
// styles `display: contents` so it's transparent to layout (the
|
|
209
|
+
// fallback / loaded component render as direct children of Defer's
|
|
210
|
+
// parent).
|
|
211
|
+
const containerRef = createRef<HTMLElement>()
|
|
212
|
+
// Visible-mode trigger is wired via `_setupVisibleTrigger` so the
|
|
213
|
+
// observer-construction + intersection-detection logic is
|
|
214
|
+
// independently testable. onMount keeps the browser-API access
|
|
215
|
+
// out of the SSR path.
|
|
216
|
+
onMount(() =>
|
|
217
|
+
_setupVisibleTrigger(
|
|
218
|
+
containerRef.current,
|
|
219
|
+
startLoad,
|
|
220
|
+
props.rootMargin ?? '200px',
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
// Cast renderContent to VNodeChildAccessor — its inferred return type
|
|
224
|
+
// is `VNodeChild` (broader than the accessor's `atom | atom[]`) because
|
|
225
|
+
// `props.children` itself may return any VNodeChild. The runtime
|
|
226
|
+
// unwraps nested accessors via the same mountChild path that handles
|
|
227
|
+
// <Show>'s thunk shape; the type system doesn't model the unwrap so
|
|
228
|
+
// the cast bridges. See <Show>'s `as unknown as VNode` for prior art.
|
|
229
|
+
return h(
|
|
230
|
+
'div',
|
|
231
|
+
{
|
|
232
|
+
'data-pyreon-defer': 'visible',
|
|
233
|
+
ref: containerRef,
|
|
234
|
+
style: 'display: contents',
|
|
235
|
+
},
|
|
236
|
+
renderContent as VNodeChildAccessor,
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return h(Fragment, null, renderContent as VNodeChildAccessor)
|
|
241
|
+
}
|
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
|
package/src/h.ts
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import type { ComponentFn, Props, VNode, VNodeChild } from './types'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Marker for fragment nodes — renders children without a wrapper element.
|
|
5
|
+
*
|
|
6
|
+
* MUST use `Symbol.for(...)` (global registry, keyed by string), NOT
|
|
7
|
+
* `Symbol(...)` (fresh per evaluation). `h.ts` is inlined into BOTH the
|
|
8
|
+
* main `lib/index.js` and the `lib/jsx-runtime.js` published bundles —
|
|
9
|
+
* each bundle's evaluation of a bare `Symbol(...)` would produce a
|
|
10
|
+
* DISTINCT Symbol identity. JSX `<>` compiles to `jsx(Fragment, ...)` and
|
|
11
|
+
* resolves to jsx-runtime's identity; `runtime-server` checks
|
|
12
|
+
* `vnode.type === Fragment` against the main-entry identity. Mismatch
|
|
13
|
+
* fell through to `renderElement` and crashed SSG with
|
|
14
|
+
* `TypeError: Cannot convert a Symbol value to a string`.
|
|
15
|
+
* `Symbol.for()` keys by string in a global registry shared across all
|
|
16
|
+
* bundle evaluations — same identity everywhere.
|
|
17
|
+
*/
|
|
18
|
+
export const Fragment: symbol = Symbol.for('Pyreon.Fragment')
|
|
5
19
|
|
|
6
20
|
/**
|
|
7
21
|
* Hyperscript function — the compiled output of JSX.
|
package/src/index.ts
CHANGED
|
@@ -35,12 +35,22 @@ export type {
|
|
|
35
35
|
TargetedEvent,
|
|
36
36
|
TextareaAttributes,
|
|
37
37
|
} from './jsx-runtime'
|
|
38
|
+
export type { DeferProps } from './defer'
|
|
39
|
+
export { Defer } from './defer'
|
|
38
40
|
export { lazy } from './lazy'
|
|
39
41
|
export { onErrorCaptured, onMount, onUnmount, onUpdate } from './lifecycle'
|
|
40
42
|
export { mapArray } from './map-array'
|
|
41
43
|
export type { PortalProps } from './portal'
|
|
42
44
|
export { Portal, PortalSymbol } from './portal'
|
|
43
|
-
export {
|
|
45
|
+
export {
|
|
46
|
+
_rp,
|
|
47
|
+
_wrapSpread,
|
|
48
|
+
createUniqueId,
|
|
49
|
+
makeReactiveProps,
|
|
50
|
+
mergeProps,
|
|
51
|
+
REACTIVE_PROP,
|
|
52
|
+
splitProps,
|
|
53
|
+
} from './props'
|
|
44
54
|
export type { Ref, RefCallback, RefProp } from './ref'
|
|
45
55
|
export { createRef } from './ref'
|
|
46
56
|
export type { MatchProps, ShowProps, SwitchProps } from './show'
|
package/src/jsx-runtime.ts
CHANGED
|
@@ -25,19 +25,57 @@ export function jsx(
|
|
|
25
25
|
props: Props & { children?: VNodeChild | VNodeChild[] },
|
|
26
26
|
key?: string | number | null,
|
|
27
27
|
): VNode {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Build the destructured props object by copying own property
|
|
29
|
+
// DESCRIPTORS, not values. Compiler-emitted reactive props (`_rp(() =>
|
|
30
|
+
// signal())` wrappers converted to getter properties by
|
|
31
|
+
// `makeReactiveProps` in mount.ts) MUST survive the destructure with
|
|
32
|
+
// their getters intact. A plain `{ children, ...rest } = props`
|
|
33
|
+
// destructure fires every getter on `props` and stores the resolved
|
|
34
|
+
// value, breaking signal-driven reactivity for any downstream
|
|
35
|
+
// consumer that reads `props.x` in a tracking scope.
|
|
36
|
+
//
|
|
37
|
+
// Fast path: if `props` has no own property descriptors with `get`
|
|
38
|
+
// accessors, we can use the original value-copy shape (cheap object
|
|
39
|
+
// literal allocation). This is the 99% case — only framework wrappers
|
|
40
|
+
// (rocketstyle attrs HOC, Wrapper, styled) and direct signal props
|
|
41
|
+
// produce getter-shaped descriptors.
|
|
42
|
+
const descriptors = Object.getOwnPropertyDescriptors(props)
|
|
43
|
+
let hasGetter = false
|
|
44
|
+
for (const k in descriptors) {
|
|
45
|
+
if (descriptors[k]!.get) {
|
|
46
|
+
hasGetter = true
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const children = props.children
|
|
51
|
+
|
|
52
|
+
if (!hasGetter) {
|
|
53
|
+
const { children: _ignored, ...rest } = props
|
|
54
|
+
const propsWithKey = (key != null ? { ...rest, key } : rest) as Props
|
|
55
|
+
if (typeof type === 'function') {
|
|
56
|
+
const componentProps = children !== undefined ? { ...propsWithKey, children } : propsWithKey
|
|
57
|
+
return h(type, componentProps)
|
|
58
|
+
}
|
|
59
|
+
const childArray = children === undefined ? [] : Array.isArray(children) ? children : [children]
|
|
60
|
+
return h(type, propsWithKey, ...(childArray as VNodeChild[]))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Slow path: at least one getter descriptor present — preserve
|
|
64
|
+
// descriptors during the destructure.
|
|
65
|
+
const propsWithKey: Record<string, unknown> = {}
|
|
66
|
+
for (const k in descriptors) {
|
|
67
|
+
if (k === 'children') continue
|
|
68
|
+
Object.defineProperty(propsWithKey, k, descriptors[k]!)
|
|
69
|
+
}
|
|
70
|
+
if (key != null) propsWithKey.key = key as unknown
|
|
30
71
|
|
|
31
72
|
if (typeof type === 'function') {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const componentProps = children !== undefined ? { ...propsWithKey, children } : propsWithKey
|
|
35
|
-
return h(type, componentProps)
|
|
73
|
+
if (children !== undefined) propsWithKey.children = children
|
|
74
|
+
return h(type, propsWithKey as Props)
|
|
36
75
|
}
|
|
37
76
|
|
|
38
|
-
// DOM element or symbol (Fragment, ForSymbol): children go in vnode.children
|
|
39
77
|
const childArray = children === undefined ? [] : Array.isArray(children) ? children : [children]
|
|
40
|
-
return h(type, propsWithKey, ...(childArray as VNodeChild[]))
|
|
78
|
+
return h(type, propsWithKey as Props, ...(childArray as VNodeChild[]))
|
|
41
79
|
}
|
|
42
80
|
|
|
43
81
|
// jsxs is called when there are multiple static children — same signature
|