@pyreon/svelte-compat 0.17.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 +114 -0
- package/lib/_chunks/jsx-runtime-Cbf3f1f5.js +159 -0
- package/lib/_chunks/jsx-runtime-Cbf3f1f5.js.map +1 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +316 -0
- package/lib/index.js.map +1 -0
- package/lib/jsx-runtime.js +3 -0
- package/lib/store.js +3 -0
- package/lib/types/index.d.ts +106 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/jsx-runtime.d.ts +10 -0
- package/lib/types/jsx-runtime.d.ts.map +1 -0
- package/lib/types/store.d.ts +56 -0
- package/lib/types/store.d.ts.map +1 -0
- package/package.json +72 -0
- package/src/env.d.ts +6 -0
- package/src/index.ts +538 -0
- package/src/jsx-dev-runtime.ts +1 -0
- package/src/jsx-runtime.ts +316 -0
- package/src/store.ts +26 -0
- package/src/svelte-compat.browser.test.ts +67 -0
- package/src/tests/child-instance-leak-repro.test.ts +123 -0
- package/src/tests/lifecycle-cleanup-leak-repro.test.ts +81 -0
- package/src/tests/native-marker-bypass.test.tsx +72 -0
- package/src/tests/setup.ts +3 -0
- package/src/tests/store-entry.test.ts +36 -0
- package/src/tests/svelte-compat.test.ts +547 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Vit Bokisch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @pyreon/svelte-compat
|
|
2
|
+
|
|
3
|
+
Svelte-compatible importable runtime — stores, lifecycle, context — running on Pyreon's reactive engine.
|
|
4
|
+
|
|
5
|
+
`@pyreon/svelte-compat` shims the Svelte APIs your code actually `import`s — `svelte/store` (`writable` / `readable` / `derived` / `get` / `readonly`) and the `svelte` lifecycle / context / dispatch surface (`onMount`, `onDestroy`, `beforeUpdate`, `afterUpdate`, `tick`, `setContext` / `getContext` / `hasContext` / `getAllContexts`, `createEventDispatcher`, `mount` / `unmount` / `flushSync`) — all backed by Pyreon's signal-based reactivity. **This is a runtime shim, not a Svelte compiler.** Single-file components (`.svelte`), Svelte 5 rune *syntax* (`$state` / `$derived` / `$effect` / `$store` auto-subscription), and `<svelte:component>` directives are compiler constructs and are out of scope — only what code imports at runtime is covered (the same boundary `@pyreon/solid-compat` draws around Solid's compiler).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @pyreon/svelte-compat
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { writable, derived, get } from '@pyreon/svelte-compat/store'
|
|
17
|
+
|
|
18
|
+
const count = writable(0)
|
|
19
|
+
const doubled = derived(count, ($c) => $c * 2)
|
|
20
|
+
|
|
21
|
+
const unsub = count.subscribe((c) => console.log(c))
|
|
22
|
+
count.set(5) // logs 5
|
|
23
|
+
count.update((c) => c + 1) // logs 6
|
|
24
|
+
get(doubled) // 12
|
|
25
|
+
unsub()
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { onMount, onDestroy, setContext, getContext, createEventDispatcher } from '@pyreon/svelte-compat'
|
|
30
|
+
|
|
31
|
+
const THEME = Symbol('theme')
|
|
32
|
+
|
|
33
|
+
function ThemeProvider({ children }) {
|
|
34
|
+
setContext(THEME, { mode: 'dark' })
|
|
35
|
+
onMount(() => console.log('mounted'))
|
|
36
|
+
onDestroy(() => console.log('unmounted'))
|
|
37
|
+
return <div>{children}</div>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Child() {
|
|
41
|
+
const theme = getContext(THEME)
|
|
42
|
+
const dispatch = createEventDispatcher<{ click: { x: number } }>()
|
|
43
|
+
return <button onClick={() => dispatch('click', { x: 1 })}>{theme.mode}</button>
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Subpath exports
|
|
48
|
+
|
|
49
|
+
| Subpath | Surface |
|
|
50
|
+
| --------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `@pyreon/svelte-compat` | Lifecycle / context / dispatch: `onMount`, `onDestroy`, `beforeUpdate`, `afterUpdate`, `tick`, `setContext`, `getContext`, `hasContext`, `getAllContexts`, `createEventDispatcher`, `mount`, `unmount`, `flushSync`. Control-flow re-exports: `<For>`, `<Show>`, `<Switch>`, `<Match>`, `<ErrorBoundary>`, `<Suspense>` |
|
|
52
|
+
| `@pyreon/svelte-compat/store` | Stores: `writable`, `readable`, `derived`, `readonly`, `get`, plus type exports (`Subscriber`, `Invalidator`, `Unsubscriber`, `Updater`, `StartStopNotifier`, `Readable`, `Writable`) |
|
|
53
|
+
| `@pyreon/svelte-compat/jsx-runtime` | JSX automatic runtime (`jsx`, `jsxs`, `Fragment`) |
|
|
54
|
+
| `@pyreon/svelte-compat/jsx-dev-runtime` | Dev variant — same runtime |
|
|
55
|
+
|
|
56
|
+
## Stores (`svelte/store` equivalents)
|
|
57
|
+
|
|
58
|
+
| API | Notes |
|
|
59
|
+
| ------------------------------------ | -------------------------------------------------------------------------------------- |
|
|
60
|
+
| `writable<T>(value?, start?)` | Returns `{ set, update, subscribe }`. `start` runs on first subscribe with `set`/`update` and returns a stop fn. |
|
|
61
|
+
| `readable<T>(value?, start?)` | Same as `writable` minus `set` / `update` — only `start` mutates. |
|
|
62
|
+
| `derived(stores, fn, initial?)` | Auto-recomputes when any input store changes. `stores` can be a single store or an array. |
|
|
63
|
+
| `readonly(store)` | Strips `set` / `update` from a writable. |
|
|
64
|
+
| `get(store)` | Synchronous one-shot read. |
|
|
65
|
+
|
|
66
|
+
Stores are **NOT** Pyreon `Signal`s under the hood (load-bearing lesson PR #704 caught) — using `signal()` inside the wrapper would freeze the store after first write under compat-mode component re-renders. Stores are a plain subscriber-set + value-snapshot, matching Svelte's own runtime semantics.
|
|
67
|
+
|
|
68
|
+
## Drop-in compat mode
|
|
69
|
+
|
|
70
|
+
`@pyreon/vite-plugin` can alias every `svelte` / `svelte/store` / `svelte/internal` import to this package:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// vite.config.ts
|
|
74
|
+
import pyreon from '@pyreon/vite-plugin'
|
|
75
|
+
export default { plugins: [pyreon({ compat: 'svelte' })] }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`tsconfig.json`:
|
|
79
|
+
|
|
80
|
+
```jsonc
|
|
81
|
+
{
|
|
82
|
+
"compilerOptions": {
|
|
83
|
+
"jsx": "react-jsx",
|
|
84
|
+
"jsxImportSource": "@pyreon/svelte-compat"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Scope
|
|
90
|
+
|
|
91
|
+
This is a **runtime** shim. It covers what code imports at runtime — the same boundary `@pyreon/solid-compat` draws around Solid's compiler.
|
|
92
|
+
|
|
93
|
+
- ✅ `svelte/store` — `writable`, `readable`, `derived`, `get`, `readonly`
|
|
94
|
+
- ✅ `svelte` — `onMount`, `onDestroy`, `beforeUpdate`, `afterUpdate`, `tick`, context, dispatch, mount/unmount/flushSync
|
|
95
|
+
- ❌ `.svelte` single-file-component compiler
|
|
96
|
+
- ❌ Svelte 5 rune *syntax* (`$state` / `$derived` / `$effect` / `$store` auto-subscription)
|
|
97
|
+
- ❌ Reactive `$:` statements
|
|
98
|
+
- ❌ `<svelte:component>` / `<svelte:element>` directives
|
|
99
|
+
|
|
100
|
+
Components are plain functions returning JSX that run on Pyreon via the shared compat JSX runtime; they re-render the subtree on store change (matching Svelte's runtime semantics under the compat wrapper, not Pyreon's fine-grained model).
|
|
101
|
+
|
|
102
|
+
## Gotchas
|
|
103
|
+
|
|
104
|
+
- **Compat-layer stores must NOT be backed by Pyreon signals.** The compat-mode component wrapper disposes inner effects on the next re-render — a signal-backed store would freeze after first write. This is implemented internally; you only need to know the consequence: store APIs work as documented even under multi-render-cycle compat-mode components.
|
|
105
|
+
- **`createEventDispatcher` returns a callable**, mirroring Svelte. The dispatched event passes through Pyreon's standard handler invocation — the parent component sees `(e: CustomEvent<TDetail>) => …` shapes.
|
|
106
|
+
- **`flushSync` / `tick`** map to Pyreon's batch-flush primitives — they don't drive a full Svelte render cycle.
|
|
107
|
+
|
|
108
|
+
## Documentation
|
|
109
|
+
|
|
110
|
+
Full docs: [docs.pyreon.dev/docs/svelte-compat](https://docs.pyreon.dev/docs/svelte-compat) (or `docs/docs/svelte-compat.md` in this repo).
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { ErrorBoundary, For, Fragment, Match, Show, Suspense, Switch, h, isNativeCompat, onUnmount } from "@pyreon/core";
|
|
2
|
+
import { runUntracked, signal } from "@pyreon/reactivity";
|
|
3
|
+
|
|
4
|
+
//#region src/jsx-runtime.ts
|
|
5
|
+
let _currentCtx = null;
|
|
6
|
+
let _hookIndex = 0;
|
|
7
|
+
function getCurrentCtx() {
|
|
8
|
+
return _currentCtx;
|
|
9
|
+
}
|
|
10
|
+
function getHookIndex() {
|
|
11
|
+
return _hookIndex++;
|
|
12
|
+
}
|
|
13
|
+
function beginRender(ctx) {
|
|
14
|
+
_currentCtx = ctx;
|
|
15
|
+
_hookIndex = 0;
|
|
16
|
+
ctx.pendingEffects = [];
|
|
17
|
+
ctx.pendingLayoutEffects = [];
|
|
18
|
+
}
|
|
19
|
+
function endRender() {
|
|
20
|
+
_currentCtx = null;
|
|
21
|
+
_hookIndex = 0;
|
|
22
|
+
}
|
|
23
|
+
function runLayoutEffects(entries) {
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (entry.cleanup) entry.cleanup();
|
|
26
|
+
const cleanup = entry.fn();
|
|
27
|
+
entry.cleanup = typeof cleanup === "function" ? cleanup : void 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function scheduleEffects(ctx, entries) {
|
|
31
|
+
if (entries.length === 0) return;
|
|
32
|
+
queueMicrotask(() => {
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (ctx.unmounted) return;
|
|
35
|
+
if (entry.cleanup) entry.cleanup();
|
|
36
|
+
const cleanup = entry.fn();
|
|
37
|
+
entry.cleanup = typeof cleanup === "function" ? cleanup : void 0;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const _CHILD_INSTANCE = Symbol.for("pyreon.childInstance");
|
|
42
|
+
const noop = () => {};
|
|
43
|
+
const _wrapperCache = /* @__PURE__ */ new WeakMap();
|
|
44
|
+
const _nativeComponents = new Set([
|
|
45
|
+
Show,
|
|
46
|
+
For,
|
|
47
|
+
Switch,
|
|
48
|
+
Match,
|
|
49
|
+
Suspense,
|
|
50
|
+
ErrorBoundary
|
|
51
|
+
]);
|
|
52
|
+
function wrapCompatComponent(solidComponent) {
|
|
53
|
+
if (_nativeComponents.has(solidComponent)) return solidComponent;
|
|
54
|
+
let wrapped = _wrapperCache.get(solidComponent);
|
|
55
|
+
if (wrapped) return wrapped;
|
|
56
|
+
wrapped = ((props) => {
|
|
57
|
+
const existing = props[_CHILD_INSTANCE];
|
|
58
|
+
const ctx = existing?.ctx ?? {
|
|
59
|
+
hooks: [],
|
|
60
|
+
scheduleRerender: () => {},
|
|
61
|
+
pendingEffects: [],
|
|
62
|
+
pendingLayoutEffects: [],
|
|
63
|
+
unmounted: false,
|
|
64
|
+
unmountCallbacks: []
|
|
65
|
+
};
|
|
66
|
+
if (existing) {
|
|
67
|
+
ctx.unmounted = false;
|
|
68
|
+
ctx.unmountCallbacks = [];
|
|
69
|
+
}
|
|
70
|
+
const version = existing?.version ?? signal(0);
|
|
71
|
+
let updateScheduled = existing?.updateScheduled ?? false;
|
|
72
|
+
ctx.scheduleRerender = () => {
|
|
73
|
+
if (ctx.unmounted || updateScheduled) return;
|
|
74
|
+
updateScheduled = true;
|
|
75
|
+
queueMicrotask(() => {
|
|
76
|
+
updateScheduled = false;
|
|
77
|
+
if (!ctx.unmounted) version.set(version.peek() + 1);
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
onUnmount(() => {
|
|
81
|
+
ctx.unmounted = true;
|
|
82
|
+
for (const cb of ctx.unmountCallbacks) cb();
|
|
83
|
+
});
|
|
84
|
+
const { [_CHILD_INSTANCE]: _stripped, ...cleanProps } = props;
|
|
85
|
+
ctx.props = cleanProps;
|
|
86
|
+
return () => {
|
|
87
|
+
version();
|
|
88
|
+
beginRender(ctx);
|
|
89
|
+
const result = runUntracked(() => solidComponent(cleanProps));
|
|
90
|
+
const layoutEffects = ctx.pendingLayoutEffects;
|
|
91
|
+
const effects = ctx.pendingEffects;
|
|
92
|
+
endRender();
|
|
93
|
+
runLayoutEffects(layoutEffects);
|
|
94
|
+
scheduleEffects(ctx, effects);
|
|
95
|
+
return result;
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
if ("__loading" in solidComponent) wrapped.__loading = solidComponent.__loading;
|
|
99
|
+
_wrapperCache.set(solidComponent, wrapped);
|
|
100
|
+
return wrapped;
|
|
101
|
+
}
|
|
102
|
+
function createChildInstance() {
|
|
103
|
+
return {
|
|
104
|
+
ctx: {
|
|
105
|
+
hooks: [],
|
|
106
|
+
scheduleRerender: noop,
|
|
107
|
+
pendingEffects: [],
|
|
108
|
+
pendingLayoutEffects: [],
|
|
109
|
+
unmounted: false,
|
|
110
|
+
unmountCallbacks: []
|
|
111
|
+
},
|
|
112
|
+
version: signal(0),
|
|
113
|
+
updateScheduled: false
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* During a parent component render, get or create the child instance at the
|
|
118
|
+
* current hook index. Returns undefined when called outside a component render.
|
|
119
|
+
*/
|
|
120
|
+
function resolveChildInstance() {
|
|
121
|
+
const parentCtx = _currentCtx;
|
|
122
|
+
if (!parentCtx) return void 0;
|
|
123
|
+
const idx = _hookIndex++;
|
|
124
|
+
if (idx < parentCtx.hooks.length) return parentCtx.hooks[idx];
|
|
125
|
+
const instance = createChildInstance();
|
|
126
|
+
parentCtx.hooks[idx] = instance;
|
|
127
|
+
return instance;
|
|
128
|
+
}
|
|
129
|
+
function jsx(type, props, key) {
|
|
130
|
+
const { children, ...rest } = props;
|
|
131
|
+
const propsWithKey = key != null ? {
|
|
132
|
+
...rest,
|
|
133
|
+
key
|
|
134
|
+
} : rest;
|
|
135
|
+
if (typeof type === "function") {
|
|
136
|
+
if (_nativeComponents.has(type)) return h(type, children !== void 0 ? {
|
|
137
|
+
...propsWithKey,
|
|
138
|
+
children
|
|
139
|
+
} : propsWithKey);
|
|
140
|
+
if (isNativeCompat(type)) return h(type, children !== void 0 ? {
|
|
141
|
+
...propsWithKey,
|
|
142
|
+
children
|
|
143
|
+
} : propsWithKey);
|
|
144
|
+
const wrapped = wrapCompatComponent(type);
|
|
145
|
+
const componentProps = children !== void 0 ? {
|
|
146
|
+
...propsWithKey,
|
|
147
|
+
children
|
|
148
|
+
} : { ...propsWithKey };
|
|
149
|
+
const childInstance = resolveChildInstance();
|
|
150
|
+
if (childInstance) componentProps[_CHILD_INSTANCE] = childInstance;
|
|
151
|
+
return h(wrapped, componentProps);
|
|
152
|
+
}
|
|
153
|
+
return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
|
|
154
|
+
}
|
|
155
|
+
const jsxs = jsx;
|
|
156
|
+
|
|
157
|
+
//#endregion
|
|
158
|
+
export { jsxs as a, jsx as i, getCurrentCtx as n, getHookIndex as r, Fragment as t };
|
|
159
|
+
//# sourceMappingURL=jsx-runtime-Cbf3f1f5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsx-runtime-Cbf3f1f5.js","names":[],"sources":["../../src/jsx-runtime.ts"],"sourcesContent":["/**\n * Compat JSX runtime for Svelte compatibility mode.\n *\n * When `jsxImportSource` is redirected to `@pyreon/svelte-compat` (via the vite\n * plugin's `compat: \"svelte\"` option), OXC rewrites JSX to import from this file.\n *\n * For component VNodes, we wrap the component function so it returns a reactive\n * accessor — enabling Svelte-store-driven re-renders on state change while Pyreon's\n * existing renderer handles all DOM work.\n *\n * The component body runs inside `runUntracked` to prevent signal reads (from\n * createSignal getters) from being tracked by the reactive accessor. Only the\n * version signal triggers re-renders.\n *\n * ## Child instance preservation\n *\n * When a parent component re-renders, mountReactive does a full teardown+rebuild\n * of the DOM tree. Without preservation, child components get brand new\n * RenderContexts with empty hooks arrays — causing `onMount` and `onCleanup`\n * to fire again, which can trigger infinite re-render loops.\n *\n * To fix this, we store child RenderContexts in the parent's hooks array (indexed\n * by the parent's hook counter). When the child wrapper is called again after a\n * parent re-render, it reuses the existing ctx (preserving hooks state), so\n * hook-indexed guards like `if (idx >= ctx.hooks.length) return` work correctly\n * and lifecycle hooks don't re-fire.\n */\n\nimport type { ComponentFn, Props, VNode, VNodeChild } from '@pyreon/core'\nimport {\n ErrorBoundary,\n For,\n Fragment,\n h,\n isNativeCompat,\n Match,\n onUnmount,\n Show,\n Suspense,\n Switch,\n} from '@pyreon/core'\nimport { runUntracked, signal } from '@pyreon/reactivity'\n\nexport { Fragment }\n\n// ─── Render context (used by hooks) ──────────────────────────────────────────\n\nexport interface RenderContext {\n hooks: unknown[]\n scheduleRerender: () => void\n /** Effect entries pending execution after render */\n pendingEffects: EffectEntry[]\n /** Layout effect entries pending execution after render */\n pendingLayoutEffects: EffectEntry[]\n /** Set to true when the component is unmounted */\n unmounted: boolean\n /** Callbacks to run on unmount (lifecycle + effect cleanups) */\n unmountCallbacks: (() => void)[]\n /** Current component props — read by createEventDispatcher() */\n props?: Record<string, unknown>\n}\n\nexport interface EffectEntry {\n fn: () => (() => void) | void\n deps: unknown[] | undefined\n cleanup: (() => void) | undefined\n}\n\nlet _currentCtx: RenderContext | null = null\nlet _hookIndex = 0\n\nexport function getCurrentCtx(): RenderContext | null {\n return _currentCtx\n}\n\nexport function getHookIndex(): number {\n return _hookIndex++\n}\n\nexport function beginRender(ctx: RenderContext): void {\n _currentCtx = ctx\n _hookIndex = 0\n ctx.pendingEffects = []\n ctx.pendingLayoutEffects = []\n}\n\nexport function endRender(): void {\n _currentCtx = null\n _hookIndex = 0\n}\n\n// ─── Effect runners ──────────────────────────────────────────────────────────\n\nfunction runLayoutEffects(entries: EffectEntry[]): void {\n for (const entry of entries) {\n if (entry.cleanup) entry.cleanup()\n const cleanup = entry.fn()\n entry.cleanup = typeof cleanup === 'function' ? cleanup : undefined\n }\n}\n\nfunction scheduleEffects(ctx: RenderContext, entries: EffectEntry[]): void {\n if (entries.length === 0) return\n queueMicrotask(() => {\n for (const entry of entries) {\n if (ctx.unmounted) return\n if (entry.cleanup) entry.cleanup()\n const cleanup = entry.fn()\n entry.cleanup = typeof cleanup === 'function' ? cleanup : undefined\n }\n })\n}\n\n// ─── Child instance preservation ─────────────────────────────────────────────\n\n/** Stored in the parent's hooks array to preserve child state across re-renders */\ninterface ChildInstance {\n ctx: RenderContext\n version: ReturnType<typeof signal<number>>\n updateScheduled: boolean\n}\n\n// Internal prop keys for passing parent context info to child wrappers\nconst _CHILD_INSTANCE = Symbol.for('pyreon.childInstance')\nconst noop = () => {\n /* noop */\n}\n\n// ─── Component wrapping ──────────────────────────────────────────────────────\n\nconst _wrapperCache = new WeakMap<Function, ComponentFn>()\n\n// Pyreon core components that must NOT be wrapped — they rely on internal reactivity\nconst _nativeComponents: Set<Function> = new Set([\n Show,\n For,\n Switch,\n Match,\n Suspense,\n ErrorBoundary,\n])\n\nfunction wrapCompatComponent(solidComponent: Function): ComponentFn {\n if (_nativeComponents.has(solidComponent)) return solidComponent as ComponentFn\n\n let wrapped = _wrapperCache.get(solidComponent)\n if (wrapped) return wrapped\n\n // The wrapper returns a reactive accessor (() => VNodeChild) which Pyreon's\n // mountChild treats as a reactive expression via mountReactive.\n wrapped = ((props: Props) => {\n // Check for a preserved child instance from the parent's hooks\n const existing = (props as Record<symbol, unknown>)[_CHILD_INSTANCE] as\n | ChildInstance\n | undefined\n\n const ctx: RenderContext = existing?.ctx ?? {\n hooks: [],\n scheduleRerender: () => {\n // Will be replaced below after version signal is created\n },\n pendingEffects: [],\n pendingLayoutEffects: [],\n unmounted: false,\n unmountCallbacks: [],\n }\n\n // When reusing an existing ctx after parent re-render, reset unmounted flag\n // and clear stale unmount callbacks (they belong to the previous mount cycle)\n if (existing) {\n ctx.unmounted = false\n ctx.unmountCallbacks = []\n }\n\n const version = existing?.version ?? signal(0)\n\n // Use a shared updateScheduled flag (preserved across parent re-renders)\n let updateScheduled = existing?.updateScheduled ?? false\n\n ctx.scheduleRerender = () => {\n if (ctx.unmounted || updateScheduled) return\n updateScheduled = true\n queueMicrotask(() => {\n updateScheduled = false\n if (!ctx.unmounted) version.set(version.peek() + 1)\n })\n }\n\n // Register cleanup when component unmounts\n onUnmount(() => {\n ctx.unmounted = true\n for (const cb of ctx.unmountCallbacks) cb()\n })\n\n // Strip the internal prop before passing to the component\n const { [_CHILD_INSTANCE]: _stripped, ...cleanProps } = props as Record<\n string | symbol,\n unknown\n >\n\n // Expose props on the ctx so createEventDispatcher() can forward\n // child events to the parent's on<Type> / on:<type> prop.\n ctx.props = cleanProps as Record<string, unknown>\n\n // Return reactive accessor — Pyreon's mountChild calls mountReactive\n return () => {\n version() // tracked read — triggers re-execution when state changes\n beginRender(ctx)\n // runUntracked prevents signal reads (from createSignal getters) from\n // being tracked by this accessor — only the version signal should trigger re-renders\n const result = runUntracked(() => (solidComponent as ComponentFn)(cleanProps as Props))\n const layoutEffects = ctx.pendingLayoutEffects\n const effects = ctx.pendingEffects\n endRender()\n\n runLayoutEffects(layoutEffects)\n scheduleEffects(ctx, effects)\n\n return result\n }\n }) as unknown as ComponentFn\n\n // Forward __loading from lazy components so Pyreon's Suspense can detect them\n if ('__loading' in solidComponent) {\n ;(wrapped as unknown as Record<string, unknown>).__loading = (\n solidComponent as unknown as Record<string, unknown>\n ).__loading\n }\n\n _wrapperCache.set(solidComponent, wrapped)\n return wrapped\n}\n\n// ─── Child instance lookup ───────────────────────────────────────────────────\n\nfunction createChildInstance(): ChildInstance {\n return {\n ctx: {\n hooks: [],\n scheduleRerender: noop,\n pendingEffects: [],\n pendingLayoutEffects: [],\n unmounted: false,\n unmountCallbacks: [],\n },\n version: signal(0),\n updateScheduled: false,\n }\n}\n\n/**\n * During a parent component render, get or create the child instance at the\n * current hook index. Returns undefined when called outside a component render.\n */\nfunction resolveChildInstance(): ChildInstance | undefined {\n const parentCtx = _currentCtx\n if (!parentCtx) return undefined\n\n const idx = _hookIndex++\n if (idx < parentCtx.hooks.length) {\n return parentCtx.hooks[idx] as ChildInstance\n }\n const instance = createChildInstance()\n parentCtx.hooks[idx] = instance\n return instance\n}\n\n// ─── JSX functions ───────────────────────────────────────────────────────────\n\nexport function jsx(\n type: string | ComponentFn | symbol,\n props: Props & { children?: VNodeChild | VNodeChild[] },\n key?: string | number | null,\n): VNode {\n const { children, ...rest } = props\n const propsWithKey = (key != null ? { ...rest, key } : rest) as Props\n\n if (typeof type === 'function') {\n // Defense-in-depth: hardcoded set of Pyreon core control-flow primitives\n // that are always native (kept even after the marker convergence — these\n // are imported into solid-compat directly, so guarding their identity\n // doesn't cost a property lookup and ensures the marker is never lost\n // through any tree-shaking edge case).\n if (_nativeComponents.has(type)) {\n const componentProps = children !== undefined ? { ...propsWithKey, children } : propsWithKey\n return h(type as ComponentFn, componentProps)\n }\n\n // Native Pyreon framework components (context Providers, RouterView, etc.)\n // skip compat wrapping — see `@pyreon/core`'s `nativeCompat()` for the\n // full contract.\n if (isNativeCompat(type)) {\n const componentProps = children !== undefined ? { ...propsWithKey, children } : propsWithKey\n return h(type as ComponentFn, componentProps)\n }\n\n const wrapped = wrapCompatComponent(type)\n const componentProps =\n children !== undefined ? { ...propsWithKey, children } : { ...propsWithKey }\n\n const childInstance = resolveChildInstance()\n if (childInstance) {\n ;(componentProps as Record<symbol, unknown>)[_CHILD_INSTANCE] = childInstance\n }\n\n return h(wrapped, componentProps)\n }\n\n // DOM element or symbol (Fragment): children go in vnode.children\n const childArray = children === undefined ? [] : Array.isArray(children) ? children : [children]\n\n return h(type, propsWithKey, ...(childArray as VNodeChild[]))\n}\n\nexport const jsxs = jsx\nexport const jsxDEV = jsx\n"],"mappings":"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"}
|