@pyreon/core 0.1.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/LICENSE +21 -0
- package/README.md +79 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +475 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +381 -0
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +53 -0
- package/src/component.ts +66 -0
- package/src/context.ts +79 -0
- package/src/dynamic.ts +12 -0
- package/src/error-boundary.ts +58 -0
- package/src/for.ts +33 -0
- package/src/h.ts +49 -0
- package/src/index.ts +42 -0
- package/src/jsx-dev-runtime.ts +2 -0
- package/src/jsx-runtime.ts +576 -0
- package/src/lazy.ts +25 -0
- package/src/lifecycle.ts +52 -0
- package/src/map-array.ts +42 -0
- package/src/portal.ts +39 -0
- package/src/ref.ts +19 -0
- package/src/show.ts +108 -0
- package/src/suspense.ts +41 -0
- package/src/telemetry.ts +55 -0
- package/src/tests/core.test.ts +1226 -0
- package/src/types.ts +61 -0
package/src/lazy.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { signal } from "@pyreon/reactivity"
|
|
2
|
+
import { h } from "./h"
|
|
3
|
+
import type { LazyComponent } from "./suspense"
|
|
4
|
+
import type { ComponentFn, Props } from "./types"
|
|
5
|
+
|
|
6
|
+
export function lazy<P extends Props>(
|
|
7
|
+
load: () => Promise<{ default: ComponentFn<P> }>,
|
|
8
|
+
): LazyComponent<P> {
|
|
9
|
+
const loaded = signal<ComponentFn<P> | null>(null)
|
|
10
|
+
const error = signal<Error | null>(null)
|
|
11
|
+
|
|
12
|
+
load()
|
|
13
|
+
.then((m) => loaded.set(m.default))
|
|
14
|
+
.catch((e) => error.set(e instanceof Error ? e : new Error(String(e))))
|
|
15
|
+
|
|
16
|
+
const wrapper = ((props: P) => {
|
|
17
|
+
const err = error()
|
|
18
|
+
if (err) throw err
|
|
19
|
+
const comp = loaded()
|
|
20
|
+
return comp ? h(comp as ComponentFn, props as Props) : null
|
|
21
|
+
}) as LazyComponent<P>
|
|
22
|
+
|
|
23
|
+
wrapper.__loading = () => loaded() === null && error() === null
|
|
24
|
+
return wrapper
|
|
25
|
+
}
|
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { CleanupFn, LifecycleHooks } from "./types"
|
|
2
|
+
|
|
3
|
+
// The currently-executing component's hook storage, set by the renderer
|
|
4
|
+
// before calling the component function, cleared immediately after.
|
|
5
|
+
let _current: LifecycleHooks | null = null
|
|
6
|
+
|
|
7
|
+
export function setCurrentHooks(hooks: LifecycleHooks | null) {
|
|
8
|
+
_current = hooks
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getCurrentHooks(): LifecycleHooks | null {
|
|
12
|
+
return _current
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register a callback to run after the component is mounted to the DOM.
|
|
17
|
+
* Optionally return a cleanup function — it will run on unmount.
|
|
18
|
+
*/
|
|
19
|
+
export function onMount(fn: () => CleanupFn | undefined) {
|
|
20
|
+
_current?.mount.push(fn)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Register a callback to run when the component is removed from the DOM.
|
|
25
|
+
*/
|
|
26
|
+
export function onUnmount(fn: () => void) {
|
|
27
|
+
_current?.unmount.push(fn)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register a callback to run after each reactive update.
|
|
32
|
+
*/
|
|
33
|
+
export function onUpdate(fn: () => void) {
|
|
34
|
+
_current?.update.push(fn)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Register an error handler for this component subtree.
|
|
39
|
+
*
|
|
40
|
+
* When an error is thrown during rendering or in a child component,
|
|
41
|
+
* the nearest `onErrorCaptured` handler is called with the error.
|
|
42
|
+
* Return `true` to mark the error as handled and stop propagation.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* onErrorCaptured((err) => {
|
|
46
|
+
* setError(String(err))
|
|
47
|
+
* return true // handled — don't propagate
|
|
48
|
+
* })
|
|
49
|
+
*/
|
|
50
|
+
export function onErrorCaptured(fn: (err: unknown) => boolean | undefined) {
|
|
51
|
+
_current?.error.push(fn)
|
|
52
|
+
}
|
package/src/map-array.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mapArray — keyed reactive list mapping.
|
|
3
|
+
*
|
|
4
|
+
* Creates each mapped item exactly once per key, then reuses it across
|
|
5
|
+
* updates. When the source array is reordered or partially changed, only
|
|
6
|
+
* new keys invoke `map()`; existing entries return the cached result.
|
|
7
|
+
*
|
|
8
|
+
* This makes structural list operations (swap, sort, filter) O(k) in
|
|
9
|
+
* allocations where k is the number of new/removed keys, not O(n).
|
|
10
|
+
*
|
|
11
|
+
* The returned accessor reads `source()` reactively, so it can be passed
|
|
12
|
+
* directly to the keyed-list reconciler.
|
|
13
|
+
*/
|
|
14
|
+
export function mapArray<T, U>(
|
|
15
|
+
source: () => T[],
|
|
16
|
+
getKey: (item: T) => string | number,
|
|
17
|
+
map: (item: T) => U,
|
|
18
|
+
): () => U[] {
|
|
19
|
+
const cache = new Map<string | number, U>()
|
|
20
|
+
|
|
21
|
+
return () => {
|
|
22
|
+
const items = source()
|
|
23
|
+
const result: U[] = []
|
|
24
|
+
const newKeys = new Set<string | number>()
|
|
25
|
+
|
|
26
|
+
for (const item of items) {
|
|
27
|
+
const key = getKey(item)
|
|
28
|
+
newKeys.add(key)
|
|
29
|
+
if (!cache.has(key)) {
|
|
30
|
+
cache.set(key, map(item))
|
|
31
|
+
}
|
|
32
|
+
result.push(cache.get(key) as U)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Evict entries whose keys are no longer present
|
|
36
|
+
for (const key of cache.keys()) {
|
|
37
|
+
if (!newKeys.has(key)) cache.delete(key)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return result
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/portal.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Props, VNode, VNodeChild } from "./types"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Symbol used as the VNode type for a Portal — runtime-dom mounts the
|
|
5
|
+
* children into `target` instead of the normal parent.
|
|
6
|
+
*/
|
|
7
|
+
export const PortalSymbol: unique symbol = Symbol("pyreon.Portal")
|
|
8
|
+
|
|
9
|
+
export interface PortalProps {
|
|
10
|
+
/** DOM element to render children into (e.g. document.body). */
|
|
11
|
+
target: Element
|
|
12
|
+
children: VNodeChild
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Portal — renders `children` into a different DOM node than the
|
|
17
|
+
* current parent tree.
|
|
18
|
+
*
|
|
19
|
+
* Useful for modals, tooltips, dropdowns, and any overlay that needs to
|
|
20
|
+
* escape CSS overflow/stacking context restrictions.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Render a modal at document.body level regardless of where in the
|
|
24
|
+
* // component tree <Modal> is used:
|
|
25
|
+
* Portal({ target: document.body, children: h(Modal, { onClose }) })
|
|
26
|
+
*
|
|
27
|
+
* // JSX:
|
|
28
|
+
* <Portal target={document.body}>
|
|
29
|
+
* <Modal onClose={close} />
|
|
30
|
+
* </Portal>
|
|
31
|
+
*/
|
|
32
|
+
export function Portal(props: PortalProps): VNode {
|
|
33
|
+
return {
|
|
34
|
+
type: PortalSymbol as unknown as string,
|
|
35
|
+
props: props as unknown as Props,
|
|
36
|
+
children: [],
|
|
37
|
+
key: null,
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/ref.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
export function createRef<T = unknown>(): Ref<T> {
|
|
18
|
+
return { current: null }
|
|
19
|
+
}
|
package/src/show.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Props, VNode, VNodeChild, VNodeChildAtom } from "./types"
|
|
2
|
+
|
|
3
|
+
// ─── Show ─────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface ShowProps extends Props {
|
|
6
|
+
/** Accessor — children render when truthy, fallback when falsy. */
|
|
7
|
+
when: () => unknown
|
|
8
|
+
fallback?: VNodeChild
|
|
9
|
+
children?: VNodeChild
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Conditionally render children based on a reactive condition.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* h(Show, { when: () => isLoggedIn() },
|
|
17
|
+
* h(Dashboard, null)
|
|
18
|
+
* )
|
|
19
|
+
*
|
|
20
|
+
* // With fallback:
|
|
21
|
+
* h(Show, { when: () => user(), fallback: h(Login, null) },
|
|
22
|
+
* h(Dashboard, null)
|
|
23
|
+
* )
|
|
24
|
+
*/
|
|
25
|
+
export function Show(props: ShowProps): VNode | null {
|
|
26
|
+
// Returns a reactive accessor; the renderer unwraps it at mount time.
|
|
27
|
+
return ((): VNodeChildAtom =>
|
|
28
|
+
(props.when()
|
|
29
|
+
? (props.children ?? null)
|
|
30
|
+
: (props.fallback ?? null)) as VNodeChildAtom) as unknown as VNode
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Switch / Match ───────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export interface MatchProps extends Props {
|
|
36
|
+
/** Accessor — this branch renders when truthy. */
|
|
37
|
+
when: () => unknown
|
|
38
|
+
children?: VNodeChild
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A branch inside `<Switch>`. Renders when `when()` is truthy.
|
|
43
|
+
* Must be used as a direct child of `Switch`.
|
|
44
|
+
*
|
|
45
|
+
* `Match` acts as a pure type/identity marker — Switch identifies it by checking
|
|
46
|
+
* `vnode.type === Match` rather than by the runtime return value.
|
|
47
|
+
*/
|
|
48
|
+
export function Match(_props: MatchProps): VNode | null {
|
|
49
|
+
// Match is never mounted directly — Switch inspects Match VNodes by type identity.
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SwitchProps extends Props {
|
|
54
|
+
/** Rendered when no Match branch is truthy. */
|
|
55
|
+
fallback?: VNodeChild
|
|
56
|
+
children?: VNodeChild | VNodeChild[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Multi-branch conditional rendering. Evaluates each `Match` child in order,
|
|
61
|
+
* renders the first whose `when()` is truthy, or `fallback` if none match.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* h(Switch, { fallback: h("p", null, "404") },
|
|
65
|
+
* h(Match, { when: () => route() === "/" }, h(Home, null)),
|
|
66
|
+
* h(Match, { when: () => route() === "/about" }, h(About, null)),
|
|
67
|
+
* )
|
|
68
|
+
*/
|
|
69
|
+
function isMatchVNode(branch: VNodeChild): branch is VNode {
|
|
70
|
+
return (
|
|
71
|
+
branch !== null &&
|
|
72
|
+
typeof branch === "object" &&
|
|
73
|
+
!Array.isArray(branch) &&
|
|
74
|
+
(branch as VNode).type === Match
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveMatchChildren(matchVNode: VNode): VNodeChildAtom {
|
|
79
|
+
if (matchVNode.children.length === 0) {
|
|
80
|
+
return ((matchVNode.props as unknown as MatchProps).children ?? null) as VNodeChildAtom
|
|
81
|
+
}
|
|
82
|
+
if (matchVNode.children.length === 1) return matchVNode.children[0] as VNodeChildAtom
|
|
83
|
+
return matchVNode.children as unknown as VNodeChildAtom
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeBranches(children: SwitchProps["children"]): VNodeChild[] {
|
|
87
|
+
if (Array.isArray(children)) return children
|
|
88
|
+
if (children != null) return [children]
|
|
89
|
+
return []
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function Switch(props: SwitchProps): VNode | null {
|
|
93
|
+
// Returns a reactive accessor; the renderer unwraps it at mount time.
|
|
94
|
+
return ((): VNodeChildAtom => {
|
|
95
|
+
const branches = normalizeBranches(props.children)
|
|
96
|
+
|
|
97
|
+
for (const branch of branches) {
|
|
98
|
+
if (!isMatchVNode(branch)) continue
|
|
99
|
+
const matchProps = branch.props as unknown as MatchProps
|
|
100
|
+
if (matchProps.when()) return resolveMatchChildren(branch)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (props.fallback ?? null) as VNodeChildAtom
|
|
104
|
+
}) as unknown as VNode
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Keep MatchSymbol export for any code that was using it
|
|
108
|
+
export const MatchSymbol: unique symbol = Symbol("pyreon.Match")
|
package/src/suspense.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Fragment, h } from "./h"
|
|
2
|
+
import type { Props, VNode, VNodeChild } from "./types"
|
|
3
|
+
|
|
4
|
+
/** Internal marker attached to lazy()-wrapped components */
|
|
5
|
+
export type LazyComponent<P extends Props = Props> = ((props: P) => VNode | null) & {
|
|
6
|
+
__loading: () => boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Suspense — shows `fallback` while a lazy child component is still loading.
|
|
11
|
+
*
|
|
12
|
+
* Works in tandem with `lazy()` from `@pyreon/react-compat` (or `@pyreon/core/lazy`).
|
|
13
|
+
* The child VNode's `.type.__loading()` signal drives the switch.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* const Page = lazy(() => import("./Page"))
|
|
17
|
+
*
|
|
18
|
+
* h(Suspense, { fallback: h(Spinner, null) }, h(Page, null))
|
|
19
|
+
* // or with JSX:
|
|
20
|
+
* <Suspense fallback={<Spinner />}><Page /></Suspense>
|
|
21
|
+
*/
|
|
22
|
+
export function Suspense(props: { fallback: VNodeChild; children?: VNodeChild }): VNode {
|
|
23
|
+
return h(Fragment, null, () => {
|
|
24
|
+
const ch = props.children
|
|
25
|
+
const childNode = typeof ch === "function" ? ch() : ch
|
|
26
|
+
|
|
27
|
+
// Check if the child is a VNode whose type is a lazy component still loading
|
|
28
|
+
const isLoading =
|
|
29
|
+
childNode != null &&
|
|
30
|
+
typeof childNode === "object" &&
|
|
31
|
+
!Array.isArray(childNode) &&
|
|
32
|
+
typeof (childNode as VNode).type === "function" &&
|
|
33
|
+
((childNode as VNode).type as unknown as LazyComponent).__loading?.()
|
|
34
|
+
|
|
35
|
+
if (isLoading) {
|
|
36
|
+
const fb = props.fallback
|
|
37
|
+
return typeof fb === "function" ? fb() : fb
|
|
38
|
+
}
|
|
39
|
+
return childNode
|
|
40
|
+
})
|
|
41
|
+
}
|
package/src/telemetry.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error telemetry — hook into Pyreon's error reporting for Sentry, Datadog, etc.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* import { registerErrorHandler } from "@pyreon/core"
|
|
6
|
+
* import * as Sentry from "@sentry/browser"
|
|
7
|
+
*
|
|
8
|
+
* registerErrorHandler(ctx => {
|
|
9
|
+
* Sentry.captureException(ctx.error, {
|
|
10
|
+
* extra: { component: ctx.component, phase: ctx.phase },
|
|
11
|
+
* })
|
|
12
|
+
* })
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface ErrorContext {
|
|
16
|
+
/** Component function name, or "Anonymous" */
|
|
17
|
+
component: string
|
|
18
|
+
/** Lifecycle phase where the error occurred */
|
|
19
|
+
phase: "setup" | "render" | "mount" | "unmount" | "effect"
|
|
20
|
+
/** The thrown value */
|
|
21
|
+
error: unknown
|
|
22
|
+
/** Unix timestamp (ms) */
|
|
23
|
+
timestamp: number
|
|
24
|
+
/** Component props at the time of the error */
|
|
25
|
+
props?: Record<string, unknown>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ErrorHandler = (ctx: ErrorContext) => void
|
|
29
|
+
|
|
30
|
+
let _handlers: ErrorHandler[] = []
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a global error handler. Called whenever a component throws in any
|
|
34
|
+
* lifecycle phase. Returns an unregister function.
|
|
35
|
+
*/
|
|
36
|
+
export function registerErrorHandler(handler: ErrorHandler): () => void {
|
|
37
|
+
_handlers.push(handler)
|
|
38
|
+
return () => {
|
|
39
|
+
_handlers = _handlers.filter((h) => h !== handler)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Internal — called by the runtime whenever a component error is caught.
|
|
45
|
+
* Existing console.error calls are preserved; this is additive.
|
|
46
|
+
*/
|
|
47
|
+
export function reportError(ctx: ErrorContext): void {
|
|
48
|
+
for (const h of _handlers) {
|
|
49
|
+
try {
|
|
50
|
+
h(ctx)
|
|
51
|
+
} catch {
|
|
52
|
+
// handler errors must never propagate back into the framework
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|