@pyreon/core 0.24.5 → 0.24.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/lib/analysis/index.js.html +1 -1
  2. package/lib/index.js +53 -31
  3. package/package.json +2 -6
  4. package/src/compat-marker.ts +0 -79
  5. package/src/compat-shared.ts +0 -80
  6. package/src/component.ts +0 -98
  7. package/src/context.ts +0 -349
  8. package/src/defer.ts +0 -279
  9. package/src/dynamic.ts +0 -32
  10. package/src/env.d.ts +0 -6
  11. package/src/error-boundary.ts +0 -90
  12. package/src/for.ts +0 -51
  13. package/src/h.ts +0 -80
  14. package/src/index.ts +0 -80
  15. package/src/jsx-dev-runtime.ts +0 -2
  16. package/src/jsx-runtime.ts +0 -747
  17. package/src/lazy.ts +0 -25
  18. package/src/lifecycle.ts +0 -152
  19. package/src/manifest.ts +0 -579
  20. package/src/map-array.ts +0 -42
  21. package/src/portal.ts +0 -39
  22. package/src/props.ts +0 -269
  23. package/src/ref.ts +0 -32
  24. package/src/show.ts +0 -121
  25. package/src/style.ts +0 -102
  26. package/src/suspense.ts +0 -52
  27. package/src/telemetry.ts +0 -120
  28. package/src/tests/compat-marker.test.ts +0 -96
  29. package/src/tests/compat-shared.test.ts +0 -99
  30. package/src/tests/component.test.ts +0 -281
  31. package/src/tests/context.test.ts +0 -629
  32. package/src/tests/core.test.ts +0 -1290
  33. package/src/tests/cx.test.ts +0 -70
  34. package/src/tests/defer.test.ts +0 -359
  35. package/src/tests/dynamic.test.ts +0 -87
  36. package/src/tests/error-boundary.test.ts +0 -181
  37. package/src/tests/extract-props-overloads.types.test.ts +0 -135
  38. package/src/tests/for.test.ts +0 -117
  39. package/src/tests/h.test.ts +0 -221
  40. package/src/tests/jsx-compat.test.tsx +0 -86
  41. package/src/tests/lazy.test.ts +0 -100
  42. package/src/tests/lifecycle.test.ts +0 -350
  43. package/src/tests/manifest-snapshot.test.ts +0 -100
  44. package/src/tests/map-array.test.ts +0 -313
  45. package/src/tests/native-marker-error-boundary.test.ts +0 -12
  46. package/src/tests/portal.test.ts +0 -48
  47. package/src/tests/props-extended.test.ts +0 -157
  48. package/src/tests/props.test.ts +0 -250
  49. package/src/tests/reactive-context.test.ts +0 -69
  50. package/src/tests/reactive-props.test.ts +0 -157
  51. package/src/tests/ref.test.ts +0 -70
  52. package/src/tests/show.test.ts +0 -314
  53. package/src/tests/style.test.ts +0 -157
  54. package/src/tests/suspense.test.ts +0 -139
  55. package/src/tests/telemetry.test.ts +0 -297
  56. package/src/types.ts +0 -116
package/src/manifest.ts DELETED
@@ -1,579 +0,0 @@
1
- import { defineManifest } from '@pyreon/manifest'
2
-
3
- export default defineManifest({
4
- name: '@pyreon/core',
5
- title: 'Complete API',
6
- tagline:
7
- 'VNode, h(), Fragment, lifecycle, context, JSX runtime, Suspense, ErrorBoundary, lazy(), Dynamic, cx(), splitProps, mergeProps, createUniqueId',
8
- description:
9
- 'Component model and lifecycle for Pyreon. Provides the VNode type system, `h()` hyperscript function, JSX automatic runtime (`@pyreon/core/jsx-runtime`), lifecycle hooks (`onMount`, `onUnmount`), two-tier context system (`createContext` for static values, `createReactiveContext` for signal-backed values), control-flow components (`Show`, `Switch`/`Match`, `For`, `Suspense`, `ErrorBoundary`), code-splitting via `lazy()`, dynamic rendering via `Dynamic`, and props utilities (`splitProps`, `mergeProps`, `cx`, `createUniqueId`). Components are plain functions (`ComponentFn<P> = (props: P) => VNodeChild`) that run ONCE — reactivity comes from reading signals inside reactive scopes, not from re-running the component.',
10
- category: 'universal',
11
- longExample: `import { h, Fragment, onMount, onUnmount, provide, createContext, createReactiveContext, useContext, Show, Switch, Match, For, Suspense, ErrorBoundary, lazy, Dynamic, cx, splitProps, mergeProps, createUniqueId, untrack } from "@pyreon/core"
12
- import { signal, computed } from "@pyreon/reactivity"
13
-
14
- // Context — static (destructure-safe) vs reactive (must call to read)
15
- const ThemeCtx = createContext<"light" | "dark">("light")
16
- const ModeCtx = createReactiveContext<"light" | "dark">("light")
17
-
18
- const App = (props: { children: any }) => {
19
- const mode = signal<"light" | "dark">("dark")
20
- provide(ThemeCtx, "dark") // static — safe to destructure
21
- provide(ModeCtx, () => mode()) // reactive — consumer must call
22
-
23
- return <>{props.children}</>
24
- }
25
-
26
- // Lifecycle
27
- const Timer = () => {
28
- const count = signal(0)
29
- onMount(() => {
30
- const id = setInterval(() => count.update(n => n + 1), 1000)
31
- return () => clearInterval(id) // cleanup runs on unmount
32
- })
33
- return <div>{() => count()}</div>
34
- }
35
-
36
- // Control flow — reactive conditional rendering
37
- const Page = (props: { items: { id: number; name: string }[]; loggedIn: () => boolean }) => (
38
- <div>
39
- <Show when={props.loggedIn()} fallback={<p>Please log in</p>}>
40
- <For each={props.items} by={item => item.id}>
41
- {item => <li>{item.name}</li>}
42
- </For>
43
- </Show>
44
- </div>
45
- )
46
-
47
- // Props utilities — preserve reactivity
48
- const Button = (props: { class?: string; size?: string; onClick: () => void; children: any }) => {
49
- const [local, rest] = splitProps(props, ["class", "size"])
50
- const merged = mergeProps({ size: "md" }, local)
51
- const id = createUniqueId()
52
- return <button id={id} {...rest} class={cx("btn", \`btn-\${merged.size}\`, local.class)} />
53
- }
54
-
55
- // Code splitting
56
- const HeavyPage = lazy(() => import("./HeavyPage"))
57
- const LazyApp = () => (
58
- <Suspense fallback={<div>Loading...</div>}>
59
- <HeavyPage />
60
- </Suspense>
61
- )`,
62
- features: [
63
- 'h() — hyperscript function producing VNodes, JSX compiles to h() or _tpl()',
64
- 'Fragment — group children without a wrapper DOM element',
65
- 'onMount / onUnmount — lifecycle hooks, onMount supports cleanup return',
66
- 'createContext / createReactiveContext — two-tier context system',
67
- 'provide / useContext — push and read context values',
68
- 'Show / Switch+Match — reactive conditional rendering',
69
- 'For — keyed reactive list rendering with by prop',
70
- 'Suspense / ErrorBoundary — async and error boundaries',
71
- 'lazy() / Dynamic — code splitting and dynamic component rendering',
72
- 'splitProps / mergeProps — reactivity-preserving props utilities',
73
- 'cx() — class value combiner (strings, objects, arrays, nested)',
74
- 'createUniqueId — SSR-safe unique ID generation',
75
- ],
76
- api: [
77
- {
78
- name: 'h',
79
- kind: 'function',
80
- signature:
81
- 'h<P extends Props>(type: ComponentFn<P> | string | symbol, props: P | null, ...children: VNodeChild[]): VNode',
82
- summary:
83
- 'Create a VNode from a component function, HTML tag string, or symbol (Fragment, Portal). Low-level API — prefer JSX which compiles to `h()` calls (or `_tpl()` + `_bind()` for template-optimized paths). Children are stored in `vnode.children`; components must merge them via `props.children = vnode.children.length === 1 ? vnode.children[0] : vnode.children`.',
84
- example: `const vnode = h("div", { class: "container" },
85
- h("h1", null, "Hello"),
86
- h(Counter, { initial: 0 })
87
- )`,
88
- mistakes: [
89
- '`h("div", "text")` — second arg is always props (or null). Text children go in the third+ positions: `h("div", null, "text")`',
90
- '`h(MyComponent, { children: <span /> })` — children go as rest args, not a prop: `h(MyComponent, null, <span />)`',
91
- '`h("input", { className: "x" })` — use `class` not `className` (Pyreon uses standard HTML attributes)',
92
- '`h("input", { onChange: handler })` — use `onInput` for keypress-by-keypress updates (native DOM events)',
93
- ],
94
- seeAlso: ['Fragment', 'Dynamic', 'lazy'],
95
- },
96
- {
97
- name: 'Fragment',
98
- kind: 'constant',
99
- signature: 'Fragment: symbol',
100
- summary:
101
- 'Symbol used as the type for fragment VNodes that group children without producing a wrapper DOM element. In JSX, `<>...</>` compiles to `h(Fragment, null, ...)`. Useful when a component needs to return multiple sibling elements.',
102
- example: `// JSX:
103
- <>
104
- <h1>Title</h1>
105
- <p>Content</p>
106
- </>
107
-
108
- // h() API:
109
- h(Fragment, null, h("h1", null, "Title"), h("p", null, "Content"))`,
110
- seeAlso: ['h'],
111
- },
112
- {
113
- name: 'onMount',
114
- kind: 'function',
115
- signature: 'onMount(fn: () => CleanupFn | void): void',
116
- summary:
117
- 'Register a callback that runs after the component mounts into the DOM. The callback can optionally return a cleanup function that runs on unmount — this is the idiomatic pattern for event listeners, timers, and subscriptions. Must be called during component setup (the synchronous function body), not inside effects or async callbacks.',
118
- example: `const Timer = () => {
119
- const count = signal(0)
120
-
121
- onMount(() => {
122
- const id = setInterval(() => count.update(n => n + 1), 1000)
123
- return () => clearInterval(id) // cleanup on unmount
124
- })
125
-
126
- return <div>{() => count()}</div>
127
- }`,
128
- mistakes: [
129
- 'Forgetting cleanup: `onMount(() => { const id = setInterval(...) })` leaks the interval. Return cleanup: `return () => clearInterval(id)`',
130
- 'Using `onMount` + separate `onUnmount` for paired setup/teardown — prefer returning cleanup from `onMount` instead',
131
- 'Calling `onMount` inside an `effect()` or async callback — it only works during synchronous component setup',
132
- 'Accessing DOM refs before mount — the callback runs AFTER mount, which is the right place for DOM measurements',
133
- ],
134
- seeAlso: ['onUnmount', 'onUpdate'],
135
- },
136
- {
137
- name: 'onUnmount',
138
- kind: 'function',
139
- signature: 'onUnmount(fn: () => void): void',
140
- summary:
141
- 'Register a callback that runs when the component is removed from the DOM. For paired setup/teardown, prefer returning a cleanup function from `onMount` instead — it co-locates the cleanup with the setup. `onUnmount` is useful when cleanup needs to reference state computed separately from the mount callback.',
142
- example: `onUnmount(() => {
143
- console.log("Component removed from DOM")
144
- })`,
145
- seeAlso: ['onMount'],
146
- },
147
- {
148
- name: 'onUpdate',
149
- kind: 'function',
150
- signature: 'onUpdate(fn: () => void): void',
151
- summary:
152
- 'Register a callback that runs after the component updates (reactive dependencies change and DOM patches complete). Rarely needed — most update logic belongs in `effect()` or `computed()`. Useful for imperative DOM measurements that need to run after all reactive updates have flushed.',
153
- example: `onUpdate(() => {
154
- console.log("Component updated, DOM is current")
155
- })`,
156
- seeAlso: ['onMount', 'onUnmount'],
157
- },
158
- {
159
- name: 'onErrorCaptured',
160
- kind: 'function',
161
- signature: 'onErrorCaptured(fn: (error: unknown) => boolean | void): void',
162
- summary:
163
- 'Register an error handler that captures errors thrown by descendant components. Return `false` to prevent the error from propagating further up the tree. Works alongside `ErrorBoundary` for programmatic error handling.',
164
- example: `onErrorCaptured((error) => {
165
- console.error("Caught:", error)
166
- return false // stop propagation
167
- })`,
168
- seeAlso: ['ErrorBoundary'],
169
- },
170
- {
171
- name: 'createContext',
172
- kind: 'function',
173
- signature: 'createContext<T>(defaultValue: T): Context<T>',
174
- summary:
175
- 'Create a static context. `useContext()` returns the value directly (`T`), so it is safe to destructure. Use this for values that do not change after being provided (theme name, locale string, config object). For values that change reactively (mode signal, locale signal), use `createReactiveContext` instead — otherwise consumers capture a stale snapshot at setup time.',
176
- example: `const ThemeCtx = createContext<"light" | "dark">("light")
177
-
178
- // Provide:
179
- const App = () => {
180
- provide(ThemeCtx, "dark")
181
- return <Child />
182
- }
183
-
184
- // Consume:
185
- const Child = () => {
186
- const theme = useContext(ThemeCtx) // "dark" — safe to destructure
187
- return <div class={theme}>...</div>
188
- }`,
189
- mistakes: [
190
- '`provide(ThemeCtx, () => modeSignal())` with a static context — the consumer receives the function itself, not the signal value. Use `createReactiveContext` for dynamic values',
191
- 'Destructuring a reactive context value: `const { mode } = useContext(reactiveCtx)` captures once. Keep the object reference and access lazily',
192
- 'Calling `useContext` outside a component body — it reads from the component context stack, which only exists during setup',
193
- ],
194
- seeAlso: ['createReactiveContext', 'provide', 'useContext'],
195
- },
196
- {
197
- name: 'createReactiveContext',
198
- kind: 'function',
199
- signature: 'createReactiveContext<T>(defaultValue: T): ReactiveContext<T>',
200
- summary:
201
- 'Create a reactive context. `useContext()` returns `() => T` — an accessor that must be called to read the current value. Use this for values that change over time (mode, locale, user). The accessor subscribes to updates when read inside reactive scopes (`effect()`, JSX thunks, `computed()`).',
202
- example: `const ModeCtx = createReactiveContext<"light" | "dark">("light")
203
-
204
- // Provide:
205
- const App = () => {
206
- const mode = signal<"light" | "dark">("dark")
207
- provide(ModeCtx, () => mode())
208
- return <Child />
209
- }
210
-
211
- // Consume:
212
- const Child = () => {
213
- const getMode = useContext(ModeCtx) // () => "dark"
214
- return <div class={getMode()}>...</div>
215
- }`,
216
- seeAlso: ['createContext', 'provide', 'useContext'],
217
- },
218
- {
219
- name: 'provide',
220
- kind: 'function',
221
- signature: 'provide<T>(ctx: Context<T> | ReactiveContext<T>, value: T): void',
222
- summary:
223
- 'Push a context value for all descendant components. Auto-cleans up on unmount. Must be called during component setup (synchronous function body). Preferred over manual `pushContext`/`popContext`. For reactive values, provide a getter function to a `ReactiveContext`: `provide(ModeCtx, () => modeSignal())`.',
224
- example: `const ThemeCtx = createContext<"light" | "dark">("light")
225
-
226
- function App() {
227
- provide(ThemeCtx, "dark")
228
- return <Child />
229
- }`,
230
- mistakes: [
231
- '`provide(ctx, "static")` for a value that changes — use `createReactiveContext` + `provide(ctx, () => signal())`',
232
- 'Calling `provide` inside `onMount` or `effect` — it must run during synchronous component setup',
233
- 'Providing the same context twice in one component — the second `provide` shadows the first for that subtree',
234
- ],
235
- seeAlso: ['createContext', 'createReactiveContext', 'useContext'],
236
- },
237
- {
238
- name: 'useContext',
239
- kind: 'function',
240
- signature: 'useContext<T>(ctx: Context<T>): T',
241
- summary:
242
- 'Read the nearest provided value for a context. For static `Context<T>`, returns `T` directly. For `ReactiveContext<T>`, returns `() => T` — must call the accessor to read. Falls back to the default value if no ancestor provides the context.',
243
- example: `const theme = useContext(ThemeContext) // static: returns T
244
- const getMode = useContext(ModeCtx) // reactive: returns () => T`,
245
- seeAlso: ['provide', 'createContext', 'createReactiveContext'],
246
- },
247
- {
248
- name: 'Show',
249
- kind: 'component',
250
- signature: '<Show when={condition} fallback={alternative}>{children}</Show>',
251
- summary:
252
- 'Reactive conditional rendering. Mounts children when `when` is truthy, unmounts and shows `fallback` when falsy. More efficient than ternary for signal-driven conditions because it avoids re-evaluating the entire branch expression on every signal change — `Show` only transitions between mounted/unmounted when the boolean flips. `when` accepts BOTH a value (`when={true}`, `when={signal()}`) and an accessor (`when={() => signal()}`) — the framework normalizes via `typeof === "function"`. The accessor form is required for true reactivity (the framework re-evaluates it on signal change); a bare `when={signal}` reference works because the compiler\'s signal auto-call rewrites it to `when={signal()}`.',
253
- example: `<Show when={isLoggedIn()} fallback={<LoginForm />}>
254
- <Dashboard />
255
- </Show>`,
256
- mistakes: [
257
- '`{cond() ? <A /> : <B />}` — works but less efficient than `<Show>` for signal-driven conditions',
258
- '`<Show when={items().length}>` — works (truthy check), but be explicit: `<Show when={items().length > 0}>`',
259
- '`<Show when={signal}>` (bare reference) — relies on the compiler\'s signal auto-call to rewrite to `when={signal()}`. Works defensively but use `when={() => signal()}` for explicit accessor semantics across the entire reactive lifecycle.',
260
- ],
261
- seeAlso: ['Switch', 'Match', 'For'],
262
- },
263
- {
264
- name: 'Switch',
265
- kind: 'component',
266
- signature: '<Switch fallback={default}>{Match children}</Switch>',
267
- summary:
268
- 'Multi-branch conditional rendering. Renders the first `<Match>` child whose `when` prop is truthy. If no match, renders the `fallback`. More readable than nested `<Show>` for multi-way conditions.',
269
- example: `<Switch fallback={<p>Unknown status</p>}>
270
- <Match when={status() === "loading"}>
271
- <Spinner />
272
- </Match>
273
- <Match when={status() === "error"}>
274
- <ErrorDisplay />
275
- </Match>
276
- <Match when={status() === "success"}>
277
- <Results />
278
- </Match>
279
- </Switch>`,
280
- seeAlso: ['Match', 'Show'],
281
- },
282
- {
283
- name: 'Match',
284
- kind: 'component',
285
- signature: '<Match when={condition}>{children}</Match>',
286
- summary:
287
- 'A branch inside a `<Switch>`. Renders its children when `when` is truthy and it is the first truthy `<Match>` in the parent `<Switch>`. Must be a direct child of `<Switch>`. `when` accepts both a value and an accessor (same normalization as `<Show>`).',
288
- example: `<Switch>
289
- <Match when={tab() === "home"}><Home /></Match>
290
- <Match when={tab() === "settings"}><Settings /></Match>
291
- </Switch>`,
292
- seeAlso: ['Switch', 'Show'],
293
- },
294
- {
295
- name: 'For',
296
- kind: 'component',
297
- signature: '<For each={items} by={keyFn}>{renderFn}</For>',
298
- summary:
299
- 'Keyed reactive list rendering. Uses the `by` prop (not `key`) for the key function because JSX extracts `key` as a special VNode reconciliation prop. The render function receives each item and its index. Internally uses an LIS-based reconciler for minimal DOM mutations when the list changes.',
300
- example: `const items = signal([
301
- { id: 1, name: "Apple" },
302
- { id: 2, name: "Banana" },
303
- ])
304
-
305
- <For each={items()} by={item => item.id}>
306
- {(item, index) => <li>{item.name}</li>}
307
- </For>`,
308
- mistakes: [
309
- '`<For each={items}>` — must call the signal: `<For each={items()}>`',
310
- '`<For each={items()} key={...}>` — use `by` not `key` (JSX reserves `key` for VNode reconciliation)',
311
- '`{items().map(...)}` — use `<For>` for reactive list rendering; `.map()` re-creates all DOM nodes on every change',
312
- '`<For each={items()} by={index}>` — using array index as key defeats the reconciler; use a stable identity like `item.id`',
313
- ],
314
- seeAlso: ['Show', 'mapArray'],
315
- },
316
- {
317
- name: 'Suspense',
318
- kind: 'component',
319
- signature: '<Suspense fallback={loadingUI}>{children}</Suspense>',
320
- summary:
321
- 'Async boundary that shows `fallback` while any `lazy()` component or async child inside is loading. SSR mode streams the fallback immediately and swaps in the resolved content when ready (30s timeout). Nested Suspense boundaries are independent — an inner boundary resolving does not affect the outer.',
322
- example: `const LazyPage = lazy(() => import("./HeavyPage"))
323
-
324
- <Suspense fallback={<div>Loading...</div>}>
325
- <LazyPage />
326
- </Suspense>`,
327
- seeAlso: ['lazy', 'ErrorBoundary'],
328
- },
329
- {
330
- name: 'ErrorBoundary',
331
- kind: 'component',
332
- signature: '<ErrorBoundary onCatch={handler} fallback={errorUI}>{children}</ErrorBoundary>',
333
- summary:
334
- 'Catches render errors thrown by descendant components. The `fallback` receives the error object for display. `onCatch` fires with the error for logging/telemetry. Without an ErrorBoundary, uncaught errors propagate to the nearest `registerErrorHandler` or crash the app.',
335
- example: `<ErrorBoundary
336
- onCatch={(err) => console.error(err)}
337
- fallback={(err) => <div>Error: {err.message}</div>}
338
- >
339
- <App />
340
- </ErrorBoundary>`,
341
- seeAlso: ['Suspense', 'onErrorCaptured'],
342
- },
343
- {
344
- name: 'lazy',
345
- kind: 'function',
346
- signature:
347
- 'lazy(loader: () => Promise<{ default: ComponentFn }>, options?: LazyOptions): LazyComponent',
348
- summary:
349
- 'Wrap a dynamic import for code splitting. Returns a component that integrates with `Suspense` — the parent Suspense boundary shows its fallback until the import resolves. The loaded component is cached after first resolution.',
350
- example: `const Settings = lazy(() => import("./pages/Settings"))
351
-
352
- // Use in JSX (wrap with Suspense):
353
- <Suspense fallback={<Spinner />}>
354
- <Settings />
355
- </Suspense>`,
356
- seeAlso: ['Suspense', 'Dynamic'],
357
- },
358
- {
359
- name: 'Dynamic',
360
- kind: 'component',
361
- signature: '<Dynamic component={comp} {...props} />',
362
- summary:
363
- 'Renders a component by reference or string tag name. Useful when the component to render is determined at runtime (tab panels, plugin systems, polymorphic containers). When `component` changes, the previous component unmounts and the new one mounts.',
364
- example: `const components = { home: HomePage, about: AboutPage }
365
- const current = signal("home")
366
-
367
- <Dynamic component={components[current()]} />`,
368
- seeAlso: ['lazy', 'h'],
369
- },
370
- {
371
- name: 'cx',
372
- kind: 'function',
373
- signature: 'cx(...values: ClassValue[]): string',
374
- summary:
375
- 'Combine class values into a single string. Accepts strings, booleans (falsy values ignored), objects (`{ active: true }`), and arrays (nested). The `class` prop on JSX elements already accepts `ClassValue` directly, so explicit `cx()` is only needed when building class strings outside JSX or when composing values from multiple sources.',
376
- example: `cx("foo", "bar") // "foo bar"
377
- cx("base", isActive && "active") // conditional
378
- cx({ base: true, active: isActive() }) // object syntax
379
- cx(["a", ["b", { c: true }]]) // nested arrays
380
-
381
- // class prop accepts ClassValue directly:
382
- <div class={["base", cond && "active"]} />
383
- <div class={{ base: true, active: isActive() }} />`,
384
- seeAlso: ['splitProps', 'mergeProps'],
385
- },
386
- {
387
- name: 'splitProps',
388
- kind: 'function',
389
- signature:
390
- 'splitProps<T, K extends keyof T>(props: T, keys: K[]): [Pick<T, K>, Omit<T, K>]',
391
- summary:
392
- 'Split a props object into two parts: the picked keys and the rest. Both halves preserve signal reactivity — reads through either half still track the original reactive prop getters. This is the Pyreon replacement for `const { x, ...rest } = props` destructuring, which captures values once and loses reactivity.',
393
- example: `const Button = (props: { class?: string; onClick: () => void; children: VNodeChild }) => {
394
- const [local, rest] = splitProps(props, ["class"])
395
- return <button {...rest} class={cx("btn", local.class)} />
396
- }`,
397
- mistakes: [
398
- '`const { class: cls, ...rest } = props` — destructuring captures once, loses reactivity. Use `splitProps(props, ["class"])`',
399
- 'Passing a non-props object — `splitProps` relies on reactive getter descriptors that the compiler creates on props objects',
400
- 'Forgetting that symbol-keyed props are preserved — `splitProps` uses `Reflect.ownKeys` so symbols (like `REACTIVE_PROP`) survive',
401
- ],
402
- seeAlso: ['mergeProps', 'cx'],
403
- },
404
- {
405
- name: 'mergeProps',
406
- kind: 'function',
407
- signature: 'mergeProps<T extends object[]>(...sources: T): MergedProps<T>',
408
- summary:
409
- 'Merge multiple props objects with last-source-wins semantics. Reads are lazy — the merged object delegates to the source objects via getters, so signal reactivity is preserved. Commonly used to inject default props: `mergeProps({ size: "md" }, props)`. Forces `configurable: true` on copied descriptors to prevent "Cannot redefine property" errors.',
410
- example: `const Button = (props: { size?: string; variant?: string }) => {
411
- const merged = mergeProps({ size: "md", variant: "primary" }, props)
412
- return <button class={\`btn-\${merged.size} btn-\${merged.variant}\`} />
413
- }`,
414
- mistakes: [
415
- '`Object.assign({}, defaults, props)` — loses reactivity. Use `mergeProps(defaults, props)` instead',
416
- '`mergeProps(props, defaults)` — wrong order. Defaults go FIRST, actual props last (last source wins)',
417
- ],
418
- seeAlso: ['splitProps', 'cx'],
419
- },
420
- {
421
- name: 'createUniqueId',
422
- kind: 'function',
423
- signature: 'createUniqueId(): string',
424
- summary:
425
- 'Generate a unique string ID ("pyreon-1", "pyreon-2", ...) that is consistent between server and client when called in the same order. SSR-safe — the counter resets per request context. Use for `id`/`for`/`aria-*` attribute pairing in components.',
426
- example: `const LabeledInput = (props: { label: string }) => {
427
- const id = createUniqueId()
428
- return (
429
- <>
430
- <label for={id}>{props.label}</label>
431
- <input id={id} />
432
- </>
433
- )
434
- }`,
435
- seeAlso: ['splitProps'],
436
- },
437
- {
438
- name: 'Portal',
439
- kind: 'component',
440
- signature: '<Portal target={element}>{children}</Portal>',
441
- summary:
442
- 'Render children into a DOM element outside the component tree (typically `document.body`). Useful for modals, tooltips, and overlays that need to escape parent overflow/z-index stacking contexts. Context values from the Portal source tree are preserved.',
443
- example: `<Portal target={document.body}>
444
- <div class="modal-overlay">
445
- <div class="modal">Content</div>
446
- </div>
447
- </Portal>`,
448
- seeAlso: ['Dynamic'],
449
- },
450
- {
451
- name: 'mapArray',
452
- kind: 'function',
453
- signature: 'mapArray<T, U>(list: () => T[], mapFn: (item: T, index: () => number) => U): () => U[]',
454
- summary:
455
- 'Low-level reactive array mapping used internally by `<For>`. Maps a reactive array signal through a transform function, caching results per item identity. Prefer `<For>` in JSX — use `mapArray` only when you need a reactive derived array outside of rendering.',
456
- example: `const items = signal([1, 2, 3])
457
- const doubled = mapArray(() => items(), (item) => item * 2)
458
- // doubled() → [2, 4, 6] — updates reactively`,
459
- seeAlso: ['For'],
460
- },
461
- {
462
- name: 'createRef',
463
- kind: 'function',
464
- signature: 'createRef<T>(): Ref<T>',
465
- summary:
466
- 'Create a mutable ref object (`{ current: T | null }`) for holding DOM element references. Pass as the `ref` prop on JSX elements — the runtime sets `.current` after mount and clears it on unmount. Callback refs (`(el: T | null) => void`) are also supported via `RefProp<T>`.',
467
- example: `const inputRef = createRef<HTMLInputElement>()
468
- onMount(() => inputRef.current?.focus())
469
- return <input ref={inputRef} />`,
470
- seeAlso: ['onMount'],
471
- },
472
- {
473
- name: 'untrack',
474
- kind: 'function',
475
- signature: '(fn: () => T) => T',
476
- summary:
477
- 'Execute a function reading signals WITHOUT subscribing to them. Alias for `runUntracked` from `@pyreon/reactivity`. Use inside effects when you need a one-shot snapshot of a signal value without the effect re-running when that signal changes.',
478
- example: `effect(() => {
479
- const current = count() // tracked
480
- const other = untrack(() => otherSignal()) // NOT tracked
481
- })`,
482
- seeAlso: ['@pyreon/reactivity'],
483
- },
484
- {
485
- name: 'nativeCompat',
486
- kind: 'function',
487
- signature: '<T>(fn: T) => T',
488
- summary:
489
- 'Mark a Pyreon framework component as "self-managing" so compat layers (`@pyreon/{react,preact,vue,solid}-compat`) skip their wrapping and route the component through Pyreon\'s mount path. Use on every `@pyreon/*` JSX component whose setup body uses `provide()` / lifecycle hooks / signal subscriptions — wrapping breaks those by running the body inside the compat layer\'s render context instead of Pyreon\'s. Idempotent; non-function inputs pass through unchanged. The marker is a registry symbol (`Symbol.for("pyreon:native-compat")`), so framework and compat sides share it without an import dependency between them.',
490
- example: `// In a framework package:
491
- export const RouterView = nativeCompat(function RouterView(props) {
492
- provide(RouterContext, ...)
493
- return <div>{children}</div>
494
- })`,
495
- seeAlso: ['isNativeCompat', 'NATIVE_COMPAT_MARKER'],
496
- mistakes: [
497
- 'Forgetting to mark a new framework JSX export — under compat mode, the component\'s `provide()` / `onMount()` calls fail with "called outside component setup" warnings and the rendered DOM silently breaks.',
498
- 'Marking user-app components — only `@pyreon/*` framework components that already manage their own reactivity should be marked. User components in compat mode are SUPPOSED to be wrapped (that\'s how they get re-render-on-state-change semantics).',
499
- ],
500
- },
501
- {
502
- name: 'isNativeCompat',
503
- kind: 'function',
504
- signature: '(fn: unknown) => boolean',
505
- summary:
506
- 'Compat-layer-side: read whether a function has been marked as a Pyreon native framework component via `nativeCompat()`. Compat `jsx()` calls this to decide whether to skip the React/Vue/Solid/Preact-style wrapping. Always returns `false` for non-function inputs.',
507
- example: `// In a compat layer's jsx-runtime:
508
- if (isNativeCompat(type)) return h(type, props)
509
- return wrapCompatComponent(type)(props)`,
510
- seeAlso: ['nativeCompat', 'NATIVE_COMPAT_MARKER'],
511
- },
512
- {
513
- name: 'NATIVE_COMPAT_MARKER',
514
- kind: 'constant',
515
- signature: 'symbol',
516
- summary:
517
- 'The well-known registry symbol (`Symbol.for("pyreon:native-compat")`) used to mark a component as a Pyreon native framework component. Most callers should use `nativeCompat()` / `isNativeCompat()` instead of touching the symbol directly; exported for advanced cases (e.g., a compat layer that wants to inspect the property without going through the helper).',
518
- example: `import { NATIVE_COMPAT_MARKER } from '@pyreon/core'
519
-
520
- // Equivalent to nativeCompat(MyComponent):
521
- ;(MyComponent as Record<symbol, boolean>)[NATIVE_COMPAT_MARKER] = true`,
522
- seeAlso: ['nativeCompat', 'isNativeCompat'],
523
- },
524
- {
525
- name: 'ExtractProps',
526
- kind: 'type',
527
- signature:
528
- 'type ExtractProps<T> = /* matches up to 4 overloads, unions the props */ T extends ComponentFn<infer P> ? P : T',
529
- summary:
530
- "Extracts the props type from a `ComponentFn`. Passes through unchanged if `T` is not a `ComponentFn`. **Multi-overload aware** — matches up to 4 call signatures and produces the UNION of their first-argument types. Critical for multi-overload primitives (Iterator, List, Element) whose loosest overload is last; without overload-aware extraction, HOC wrapping (`rocketstyle()`, `attrs()`) silently downgraded their public prop surface. Single-overload functions still work — the union of 4 copies of the same props type dedupes back to the single shape.",
531
- example: `function Iterator<T extends SimpleValue>(p: { data: T[]; valueName?: string }): VNodeChild
532
- function Iterator<T extends ObjectValue>(p: { data: T[]; component: ComponentFn<T> }): VNodeChild
533
- type Props = ExtractProps<typeof Iterator>
534
- // → { data: SimpleValue[]; valueName?: string }
535
- // | { data: ObjectValue[]; component: ComponentFn<ObjectValue> }`,
536
- mistakes: [
537
- 'Assuming `ExtractProps<T>` returns only the LAST overload — pre-fix it did, post-fix it returns the UNION of up to 4 overloads. Functions with more than 4 overloads still drop the extras.',
538
- 'Using `T extends (props: infer P) => any ? P : never` directly in user code — that pattern captures only the LAST overload of a multi-overload function. Use `ExtractProps<T>` to get the full union.',
539
- ],
540
- seeAlso: ['HigherOrderComponent'],
541
- },
542
- {
543
- name: 'HigherOrderComponent',
544
- kind: 'type',
545
- signature: 'type HigherOrderComponent<HOP, P> = ComponentFn<HOP & P>',
546
- summary:
547
- 'Typed HOC pattern where `HOP` is the props the HOC adds and `P` is the wrapped component\'s own props. The resulting component accepts both sets of props.',
548
- example: `function withLogger<P>(Wrapped: ComponentFn<P>): HigherOrderComponent<{ logLevel?: string }, P> {
549
- return (props) => {
550
- console.log(\`[\${props.logLevel ?? "info"}] Rendering\`)
551
- return <Wrapped {...props} />
552
- }
553
- }`,
554
- seeAlso: ['ExtractProps'],
555
- },
556
- ],
557
- gotchas: [
558
- {
559
- label: 'Components run once',
560
- note: 'Pyreon components are plain functions that execute a single time. Reactivity comes from reading signals inside reactive scopes (JSX expression thunks, `effect()`, `computed()`), not from re-running the component function. `if (!cond()) return null` at the top level runs once and is static — use `return (() => { if (!cond()) return null; return <div /> })` for reactive conditional rendering.',
561
- },
562
- {
563
- label: 'Destructuring props kills reactivity',
564
- note: '`const { name } = props` captures the value at setup time — it becomes static. Use `props.name` inside reactive scopes, or `splitProps(props, ["name"])` for rest patterns. The compiler handles `const x = props.y; return <div>{x}</div>` by inlining `props.y` back at the use site, but only for `const` (not `let`/`var`).',
565
- },
566
- {
567
- label: 'Two context types',
568
- note: '`createContext<T>` returns `T` from `useContext()` — safe to destructure. `createReactiveContext<T>` returns `() => T` — must call to read. Using the wrong one is a common source of stale-value bugs (static context for dynamic values) or unnecessary ceremony (reactive context for constants).',
569
- },
570
- {
571
- label: 'For uses by, not key',
572
- note: 'The `<For>` component uses the `by` prop for its key function because JSX extracts `key` as a special VNode reconciliation prop. Writing `<For each={items()} key={fn}>` silently passes the key to the VNode system instead of the list reconciler.',
573
- },
574
- {
575
- label: 'JSX uses standard HTML attributes',
576
- note: 'Use `class` not `className`, `for` not `htmlFor`, `onInput` not `onChange` for per-keystroke updates. Pyreon maps to native DOM events, not the React synthetic event system.',
577
- },
578
- ],
579
- })
package/src/map-array.ts DELETED
@@ -1,42 +0,0 @@
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 DELETED
@@ -1,39 +0,0 @@
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
- }