@pyreon/vue-compat 0.21.0 → 0.23.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 CHANGED
@@ -1,6 +1,8 @@
1
1
  # @pyreon/vue-compat
2
2
 
3
- Vue 3 Composition API shim that runs on Pyreon's signal-based reactive engine. Migrate Vue code by swapping the import path.
3
+ Vue 3 Composition API shim — write Vue-style code that runs on Pyreon's reactive engine.
4
+
5
+ `@pyreon/vue-compat` provides the Vue 3 Composition API surface (`ref`, `shallowRef`, `computed`, `reactive`, `shallowReactive`, `readonly`, `shallowReadonly`, `toRef`, `toRefs`, `toRaw`, `toValue`, `unref`, `isRef`, `isReactive`, `isReadonly`, `isProxy`, `markRaw`, `triggerRef`, `watch`, `watchEffect`, lifecycle hooks `onMounted` / `onUnmounted` / `onUpdated` / `onBeforeMount` / `onBeforeUnmount`, `nextTick`, `provide` / `inject`, `defineComponent`, `defineAsyncComponent`, `createApp`, `effectScope` / `getCurrentScope` / `onScopeDispose`, error/render-track hooks, `<Teleport>`, `<KeepAlive>`, `<Transition>`) all running on Pyreon's reactive engine. **This is a runtime shim, not Vue** — it covers what code imports, NOT the Single-File Component compiler. `.vue` files require a separate SFC compiler.
4
6
 
5
7
  ## Install
6
8
 
@@ -8,178 +10,104 @@ Vue 3 Composition API shim that runs on Pyreon's signal-based reactive engine. M
8
10
  bun add @pyreon/vue-compat
9
11
  ```
10
12
 
11
- ## Quick Start
13
+ ## Quick start
12
14
 
13
15
  ```tsx
14
- // Replace:
15
- // import { ref, computed, watch } from "vue"
16
- // With:
17
- import { ref, computed, watch } from '@pyreon/vue-compat'
16
+ import { ref, computed, watch, onMounted } from '@pyreon/vue-compat'
18
17
 
19
18
  function Counter() {
20
19
  const count = ref(0)
21
20
  const doubled = computed(() => count.value * 2)
22
21
 
23
- watch(count, (newVal, oldVal) => {
24
- console.log(`count: ${oldVal} -> ${newVal}`)
22
+ watch(count, (next, prev) => {
23
+ console.log(`count: ${prev} ${next}`)
25
24
  })
26
25
 
27
- return (
28
- <div>
29
- <span>{doubled.value}</span>
30
- <button onClick={() => count.value++}>Count: {count.value}</button>
31
- </div>
32
- )
33
- }
34
- ```
35
-
36
- ### Reactive Objects
37
-
38
- ```tsx
39
- import { reactive, watchEffect } from '@pyreon/vue-compat'
40
-
41
- function UserForm() {
42
- const form = reactive({ name: '', email: '' })
43
-
44
- watchEffect(() => {
45
- console.log('form changed:', form.name, form.email)
26
+ onMounted(() => {
27
+ console.log('mounted')
46
28
  })
47
29
 
48
30
  return (
49
31
  <div>
50
- <input
51
- value={form.name}
52
- onInput={(e) => (form.name = e.currentTarget.value)}
53
- placeholder="Name"
54
- />
55
- <input
56
- value={form.email}
57
- onInput={(e) => (form.email = e.currentTarget.value)}
58
- placeholder="Email"
59
- />
60
- <p>
61
- Hello, {form.name} ({form.email})
62
- </p>
32
+ <p>Count: {count.value}, doubled: {doubled.value}</p>
33
+ <button onClick={() => count.value++}>+1</button>
63
34
  </div>
64
35
  )
65
36
  }
66
37
  ```
67
38
 
68
- ### Provide / Inject
69
-
70
- ```tsx
71
- import { ref, provide, inject, defineComponent } from '@pyreon/vue-compat'
72
-
73
- const ThemeKey = Symbol('theme')
74
-
75
- function ThemeProvider(props: { children: any }) {
76
- const theme = ref('light')
77
- provide(ThemeKey, theme)
78
- return (
79
- <div>
80
- <button onClick={() => (theme.value = theme.value === 'light' ? 'dark' : 'light')}>
81
- Toggle theme
82
- </button>
83
- {props.children}
84
- </div>
85
- )
86
- }
87
-
88
- function ThemedBox() {
89
- const theme = inject(ThemeKey, ref('light'))
90
- return <div class={`box-${theme.value}`}>Theme: {theme.value}</div>
91
- }
39
+ ## Subpath exports
40
+
41
+ | Subpath | Surface |
42
+ | ------------------------------------ | --------------------------------------------------------------------------------------------- |
43
+ | `@pyreon/vue-compat` | Full Composition API surface — see API table below |
44
+ | `@pyreon/vue-compat/jsx-runtime` | JSX automatic runtime (`jsx`, `jsxs`, `Fragment`) |
45
+ | `@pyreon/vue-compat/jsx-dev-runtime` | Dev variant — same runtime |
46
+
47
+ ## API surface
48
+
49
+ | Category | Exports |
50
+ | ---------------- | --------------------------------------------------------------------------------------------- |
51
+ | Refs | `ref`, `shallowRef`, `triggerRef`, `isRef`, `unref`, `toValue`, `toRef`, `toRefs` |
52
+ | Computed | `computed` (getter form + writable `{ get, set }` form) |
53
+ | Reactive | `reactive`, `shallowReactive`, `readonly`, `shallowReadonly`, `toRaw`, `markRaw`, `isReactive`, `isReadonly`, `isProxy` |
54
+ | Watchers | `watch`, `watchEffect`, `WatchOptions` |
55
+ | Lifecycle | `onMounted`, `onUnmounted`, `onUpdated`, `onBeforeMount`, `onBeforeUnmount`, `onErrorCaptured`, `onRenderTracked`, `onRenderTriggered` |
56
+ | Scheduling | `nextTick` |
57
+ | DI | `provide`, `inject` |
58
+ | Components | `defineComponent`, `defineAsyncComponent`, `createApp(App, props?)` |
59
+ | Scope | `effectScope`, `getCurrentScope`, `onScopeDispose` |
60
+ | Built-ins | `Teleport`, `KeepAlive`, `Transition` |
61
+ | JSX | `h`, `Fragment` |
62
+
63
+ ## Drop-in compat mode
64
+
65
+ `@pyreon/vite-plugin` can alias every `vue` import to this package:
66
+
67
+ ```ts
68
+ // vite.config.ts
69
+ import pyreon from '@pyreon/vite-plugin'
70
+ export default { plugins: [pyreon({ compat: 'vue' })] }
92
71
  ```
93
72
 
94
- ### createApp
73
+ `tsconfig.json`:
95
74
 
96
- ```tsx
97
- import { createApp, ref } from '@pyreon/vue-compat'
98
-
99
- function App() {
100
- const message = ref('Hello from Pyreon')
101
- return <h1>{message.value}</h1>
75
+ ```jsonc
76
+ {
77
+ "compilerOptions": {
78
+ "jsx": "react-jsx",
79
+ "jsxImportSource": "@pyreon/vue-compat"
80
+ }
102
81
  }
103
-
104
- const app = createApp(App)
105
- app.mount('#app')
106
82
  ```
107
83
 
108
- ## Key Differences from Vue
109
-
110
- - **No virtual DOM.** Pyreon uses fine-grained reactivity -- no diffing, no re-renders.
111
- - **Components run once** (setup phase only).
112
- - **`reactive()` uses Pyreon's store proxy** with deep signal wrapping.
113
- - **`readonly()` is strict** -- setting any property (including symbols) throws an error.
114
- - **`provide` / `inject` uses Pyreon's context system** -- fully isolated per component tree.
115
-
116
- ## API
117
-
118
- ### Reactivity: Refs
119
-
120
- - **`ref(value)`** -- returns `{ value }` with reactive `.value`.
121
- - **`shallowRef(value)`** -- shallow reactive ref (no deep tracking).
122
- - **`triggerRef(ref)`** -- force subscribers to re-run.
123
- - **`isRef(val)`** -- type guard.
124
- - **`unref(val)`** -- unwrap a ref or return value as-is.
125
- - **`toRef(obj, key)`** -- create a ref bound to an object property.
126
- - **`toRefs(obj)`** -- convert all properties to refs.
127
-
128
- ### Reactivity: Computed
129
-
130
- - **`computed(fn)`** -- read-only computed ref.
131
- - **`computed({ get, set })`** -- writable computed ref.
132
-
133
- ### Reactivity: Objects
134
-
135
- - **`reactive(obj)`** -- deep reactive proxy (Pyreon store).
136
- - **`shallowReactive(obj)`** -- shallow reactive proxy.
137
- - **`readonly(obj)`** -- read-only proxy that throws on writes.
138
- - **`toRaw(proxy)`** -- unwrap to the original object.
84
+ ## Scope
139
85
 
140
- ### Watchers
86
+ This is a **runtime** shim. It covers what code imports at runtime — the same boundary `@pyreon/solid-compat` draws around Solid's compiler.
141
87
 
142
- - **`watch(source, callback, options?)`** -- watch a ref, getter, or reactive object. Supports `immediate` and `deep`.
143
- - **`watchEffect(fn)`** -- auto-tracking effect, returns stop handle.
88
+ - Composition API `ref`, `computed`, `reactive`, `watch`, lifecycle, `provide` / `inject`, `effectScope`
89
+ - ✅ `defineComponent` (typed) + `defineAsyncComponent`
90
+ - ✅ `createApp(component, props)` — mount via `.mount(selector)`
91
+ - ✅ Built-in components — `Teleport`, `KeepAlive`, `Transition`
92
+ - ❌ `.vue` Single-File Component compiler
93
+ - ❌ `<script setup>` syntax (compiler construct)
94
+ - ❌ Options API class-component lifecycle (`data`, `methods`, `computed` block, `created`, `beforeDestroy`, …)
95
+ - ❌ Template directives (`v-if`, `v-for`, `v-model`) — use JSX equivalents
144
96
 
145
- ### Lifecycle Hooks
97
+ Components are plain functions returning JSX that run on Pyreon's reactive engine.
146
98
 
147
- - **`onMounted(fn)`** / **`onBeforeMount(fn)`**
148
- - **`onUnmounted(fn)`** / **`onBeforeUnmount(fn)`**
149
- - **`onUpdated(fn)`**
99
+ ## Gotchas
150
100
 
151
- ### Dependency Injection
101
+ - **`.value` reads/writes work** because Pyreon signals wrap a value getter/setter to match Vue's ref shape.
102
+ - **`reactive(obj)` returns a Proxy** that delegates to Pyreon signals — mutating `obj.x = 5` triggers subscribers as Vue does.
103
+ - **`watch` deps are tracked automatically** for function sources. For an array of sources, the watcher fires when any one changes.
104
+ - **`createApp(App).mount(selector)`** maps to Pyreon's `mount()` — returns an app instance with `.unmount()`.
105
+ - **Lifecycle hooks fire ONCE** per component instance (run-once model). Vue's render-driven re-fires (`onUpdated`) translate to per-subscription effects under the hood — the visible effect is the same for most use cases.
152
106
 
153
- - **`provide(key, value)`** -- provide a value to descendants.
154
- - **`inject(key, defaultValue?)`** -- inject a value from an ancestor.
107
+ ## Documentation
155
108
 
156
- ### Application
109
+ Full docs: [docs.pyreon.dev/docs/vue-compat](https://docs.pyreon.dev/docs/vue-compat) (or `docs/docs/vue-compat.md` in this repo).
157
110
 
158
- - **`createApp(component, props?)`** -- create an app instance with `.mount(el)` and `.unmount()`.
159
- - **`defineComponent(setup)`** -- wrapper for type inference (returns setup as-is).
160
- - **`nextTick()`** -- wait for the next microtask.
161
-
162
- ### Utilities
163
-
164
- - **`h` / `Fragment`** -- JSX runtime.
165
- - **`batch(fn)`** -- coalesce multiple signal writes.
166
-
167
- ## Composing Pyreon framework components inside vue-compat
168
-
169
- Pyreon's framework components (`RouterView`, `PyreonUI`, `FormProvider`, `QueryClientProvider`, …) ship marked with `nativeCompat()` from `@pyreon/core` — vue-compat's JSX runtime detects the marker and routes them through Pyreon's setup frame instead of the compat wrapper. **You don't need to do anything** for the 24 components shipped marked.
170
-
171
- If you write your **own** Pyreon-flavored helper that uses `provide()` / `onMount()` / `onUnmount()` / `effect()` at component-body scope and use it in a vue-compat app, mark it explicitly:
172
-
173
- ```tsx
174
- import { nativeCompat, provide, createContext } from '@pyreon/core'
175
-
176
- const MyCtx = createContext<string>('default')
177
-
178
- function MyProvider(props: { value: string; children?: unknown }) {
179
- provide(MyCtx, props.value)
180
- return props.children as never
181
- }
182
- nativeCompat(MyProvider) // ← required for compat-mode apps
183
- ```
111
+ ## License
184
112
 
185
- Without the marker, the wrapper relocates the body's render context and `provide()` lands in a torn-down context stack — descendants read the default. See [`packages/core/core/src/compat-marker.ts`](../../core/core/src/compat-marker.ts) for details.
113
+ MIT
@@ -0,0 +1,113 @@
1
+ import { Fragment, 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 _wrapperCache = /* @__PURE__ */ new WeakMap();
42
+ function wrapCompatComponent(vueComponent) {
43
+ let wrapped = _wrapperCache.get(vueComponent);
44
+ if (wrapped) return wrapped;
45
+ wrapped = ((props) => {
46
+ const ctx = {
47
+ hooks: [],
48
+ scheduleRerender: () => {},
49
+ pendingEffects: [],
50
+ pendingLayoutEffects: [],
51
+ unmounted: false,
52
+ unmountCallbacks: [],
53
+ _props: props
54
+ };
55
+ const version = signal(0);
56
+ let updateScheduled = false;
57
+ ctx.scheduleRerender = () => {
58
+ if (ctx.unmounted || updateScheduled) return;
59
+ updateScheduled = true;
60
+ queueMicrotask(() => {
61
+ updateScheduled = false;
62
+ if (!ctx.unmounted) version.set(version.peek() + 1);
63
+ });
64
+ };
65
+ onUnmount(() => {
66
+ ctx.unmounted = true;
67
+ for (const cb of ctx.unmountCallbacks) cb();
68
+ });
69
+ return () => {
70
+ version();
71
+ beginRender(ctx);
72
+ const result = runUntracked(() => vueComponent(props));
73
+ const layoutEffects = ctx.pendingLayoutEffects;
74
+ const effects = ctx.pendingEffects;
75
+ endRender();
76
+ runLayoutEffects(layoutEffects);
77
+ scheduleEffects(ctx, effects);
78
+ return result;
79
+ };
80
+ });
81
+ _wrapperCache.set(vueComponent, wrapped);
82
+ return wrapped;
83
+ }
84
+ function jsx(type, props, key) {
85
+ const { children, ...rest } = props;
86
+ const propsWithKey = key != null ? {
87
+ ...rest,
88
+ key
89
+ } : rest;
90
+ if (typeof type === "function") {
91
+ const componentProps = children !== void 0 ? {
92
+ ...propsWithKey,
93
+ children
94
+ } : propsWithKey;
95
+ if (isNativeCompat(type)) return h(type, componentProps);
96
+ return h(wrapCompatComponent(type), componentProps);
97
+ }
98
+ if (typeof type === "string" && propsWithKey.ref != null) {
99
+ const r = propsWithKey.ref;
100
+ if (typeof r === "object" && r !== null && r[Symbol.for("__v_isRef")] === true) {
101
+ const vueRef = r;
102
+ propsWithKey.ref = (el) => {
103
+ vueRef.value = el;
104
+ };
105
+ }
106
+ }
107
+ return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
108
+ }
109
+ const jsxs = jsx;
110
+
111
+ //#endregion
112
+ export { jsxs as a, jsx as i, getCurrentCtx as n, getHookIndex as r, Fragment as t };
113
+ //# sourceMappingURL=jsx-runtime-CG6mH_9E.js.map
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"5d7b64f5-1","name":"jsx-runtime.ts"},{"uid":"5d7b64f5-3","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"5d7b64f5-1":{"renderedLength":186,"gzipLength":138,"brotliLength":0,"metaUid":"5d7b64f5-0"},"5d7b64f5-3":{"renderedLength":33794,"gzipLength":8644,"brotliLength":0,"metaUid":"5d7b64f5-2"}},"nodeMetas":{"5d7b64f5-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"index.js":"5d7b64f5-1"},"imported":[{"uid":"5d7b64f5-4"},{"uid":"5d7b64f5-5"}],"importedBy":[{"uid":"5d7b64f5-2"}]},"5d7b64f5-2":{"id":"/src/index.ts","moduleParts":{"index.js":"5d7b64f5-3"},"imported":[{"uid":"5d7b64f5-4"},{"uid":"5d7b64f5-5"},{"uid":"5d7b64f5-6"},{"uid":"5d7b64f5-0"}],"importedBy":[],"isEntry":true},"5d7b64f5-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"5d7b64f5-2"},{"uid":"5d7b64f5-0"}]},"5d7b64f5-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"5d7b64f5-2"},{"uid":"5d7b64f5-0"}]},"5d7b64f5-6":{"id":"@pyreon/runtime-dom","moduleParts":{},"imported":[],"importedBy":[{"uid":"5d7b64f5-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src/index.ts","uid":"3631272c-1"}]},{"name":"jsx-runtime.js","children":[{"name":"src/jsx-dev-runtime.ts","uid":"3631272c-3"}]},{"name":"_chunks/jsx-runtime-CG6mH_9E.js","children":[{"name":"src/jsx-runtime.ts","uid":"3631272c-5"}]}],"isRoot":true},"nodeParts":{"3631272c-1":{"renderedLength":34103,"gzipLength":8741,"brotliLength":0,"metaUid":"3631272c-0"},"3631272c-3":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"3631272c-2"},"3631272c-5":{"renderedLength":2833,"gzipLength":1012,"brotliLength":0,"metaUid":"3631272c-4"}},"nodeMetas":{"3631272c-0":{"id":"/src/index.ts","moduleParts":{"index.js":"3631272c-1"},"imported":[{"uid":"3631272c-6"},{"uid":"3631272c-7"},{"uid":"3631272c-8"},{"uid":"3631272c-4"}],"importedBy":[],"isEntry":true},"3631272c-2":{"id":"/src/jsx-dev-runtime.ts","moduleParts":{"jsx-runtime.js":"3631272c-3"},"imported":[{"uid":"3631272c-4"}],"importedBy":[],"isEntry":true},"3631272c-4":{"id":"/src/jsx-runtime.ts","moduleParts":{"_chunks/jsx-runtime-CG6mH_9E.js":"3631272c-5"},"imported":[{"uid":"3631272c-6"},{"uid":"3631272c-7"}],"importedBy":[{"uid":"3631272c-0"},{"uid":"3631272c-2"}]},"3631272c-6":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"3631272c-0"},{"uid":"3631272c-4"}]},"3631272c-7":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"3631272c-0"},{"uid":"3631272c-4"}]},"3631272c-8":{"id":"@pyreon/runtime-dom","moduleParts":{},"imported":[],"importedBy":[{"uid":"3631272c-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -1,18 +1,8 @@
1
- import { Fragment, Portal, Suspense as Suspense$1, createContext, h as pyreonH, onMount, onUnmount, onUpdate, popContext, pushContext, useContext } from "@pyreon/core";
1
+ import { n as getCurrentCtx, r as getHookIndex } from "./_chunks/jsx-runtime-CG6mH_9E.js";
2
+ import { Fragment, Portal, Suspense as Suspense$1, createContext, h as pyreonH, onMount, onUnmount, onUpdate, pushContext, removeContextFrame, useContext } from "@pyreon/core";
2
3
  import { batch, computed as computed$1, createStore, effect, nextTick as nextTick$1, signal } from "@pyreon/reactivity";
3
4
  import { KeepAlive as KeepAlive$1, Transition as Transition$1, TransitionGroup as TransitionGroup$1, mount } from "@pyreon/runtime-dom";
4
5
 
5
- //#region src/jsx-runtime.ts
6
- let _currentCtx = null;
7
- let _hookIndex = 0;
8
- function getCurrentCtx() {
9
- return _currentCtx;
10
- }
11
- function getHookIndex() {
12
- return _hookIndex++;
13
- }
14
-
15
- //#endregion
16
6
  //#region src/index.ts
17
7
  const V_IS_REF = Symbol.for("__v_isRef");
18
8
  const V_IS_READONLY = Symbol("__v_isReadonly");
@@ -624,8 +614,9 @@ function provide(key, value) {
624
614
  if (idx < ctx.hooks.length) return;
625
615
  ctx.hooks[idx] = true;
626
616
  const vueCtx = getOrCreateContext(key);
627
- pushContext(new Map([[vueCtx.id, value]]));
628
- ctx.unmountCallbacks.push(() => popContext());
617
+ const frame = new Map([[vueCtx.id, value]]);
618
+ pushContext(frame);
619
+ ctx.unmountCallbacks.push(() => removeContextFrame(frame));
629
620
  return;
630
621
  }
631
622
  const vueCtx = getOrCreateContext(key);
@@ -725,11 +716,21 @@ function createApp(component, props) {
725
716
  mount(el) {
726
717
  const container = typeof el === "string" ? document.querySelector(el) : el;
727
718
  if (!container) throw new Error(`Cannot find mount target: ${el}`);
719
+ const pushedFrames = [];
728
720
  for (const { key, value } of provisions) {
729
721
  const ctx = getOrCreateContext(key);
730
- pushContext(new Map([[ctx.id, value]]));
722
+ const frame = new Map([[ctx.id, value]]);
723
+ pushContext(frame);
724
+ pushedFrames.push(frame);
731
725
  }
732
- return mount(pyreonH(component, props ?? null), container);
726
+ const unmount = mount(pyreonH(component, props ?? null), container);
727
+ return () => {
728
+ unmount();
729
+ for (let i = pushedFrames.length - 1; i >= 0; i--) {
730
+ const frame = pushedFrames[i];
731
+ if (frame) removeContextFrame(frame);
732
+ }
733
+ };
733
734
  },
734
735
  use(plugin) {
735
736
  plugin.install(app);
@@ -1,107 +1,3 @@
1
- import { Fragment, h, isNativeCompat, onUnmount } from "@pyreon/core";
2
- import { runUntracked, signal } from "@pyreon/reactivity";
1
+ import { a as jsxs, i as jsx, t as Fragment } from "./_chunks/jsx-runtime-CG6mH_9E.js";
3
2
 
4
- //#region src/jsx-runtime.ts
5
- let _currentCtx = null;
6
- let _hookIndex = 0;
7
- function beginRender(ctx) {
8
- _currentCtx = ctx;
9
- _hookIndex = 0;
10
- ctx.pendingEffects = [];
11
- ctx.pendingLayoutEffects = [];
12
- }
13
- function endRender() {
14
- _currentCtx = null;
15
- _hookIndex = 0;
16
- }
17
- function runLayoutEffects(entries) {
18
- for (const entry of entries) {
19
- if (entry.cleanup) entry.cleanup();
20
- const cleanup = entry.fn();
21
- entry.cleanup = typeof cleanup === "function" ? cleanup : void 0;
22
- }
23
- }
24
- function scheduleEffects(ctx, entries) {
25
- if (entries.length === 0) return;
26
- queueMicrotask(() => {
27
- for (const entry of entries) {
28
- if (ctx.unmounted) return;
29
- if (entry.cleanup) entry.cleanup();
30
- const cleanup = entry.fn();
31
- entry.cleanup = typeof cleanup === "function" ? cleanup : void 0;
32
- }
33
- });
34
- }
35
- const _wrapperCache = /* @__PURE__ */ new WeakMap();
36
- function wrapCompatComponent(vueComponent) {
37
- let wrapped = _wrapperCache.get(vueComponent);
38
- if (wrapped) return wrapped;
39
- wrapped = ((props) => {
40
- const ctx = {
41
- hooks: [],
42
- scheduleRerender: () => {},
43
- pendingEffects: [],
44
- pendingLayoutEffects: [],
45
- unmounted: false,
46
- unmountCallbacks: [],
47
- _props: props
48
- };
49
- const version = signal(0);
50
- let updateScheduled = false;
51
- ctx.scheduleRerender = () => {
52
- if (ctx.unmounted || updateScheduled) return;
53
- updateScheduled = true;
54
- queueMicrotask(() => {
55
- updateScheduled = false;
56
- if (!ctx.unmounted) version.set(version.peek() + 1);
57
- });
58
- };
59
- onUnmount(() => {
60
- ctx.unmounted = true;
61
- for (const cb of ctx.unmountCallbacks) cb();
62
- });
63
- return () => {
64
- version();
65
- beginRender(ctx);
66
- const result = runUntracked(() => vueComponent(props));
67
- const layoutEffects = ctx.pendingLayoutEffects;
68
- const effects = ctx.pendingEffects;
69
- endRender();
70
- runLayoutEffects(layoutEffects);
71
- scheduleEffects(ctx, effects);
72
- return result;
73
- };
74
- });
75
- _wrapperCache.set(vueComponent, wrapped);
76
- return wrapped;
77
- }
78
- function jsx(type, props, key) {
79
- const { children, ...rest } = props;
80
- const propsWithKey = key != null ? {
81
- ...rest,
82
- key
83
- } : rest;
84
- if (typeof type === "function") {
85
- const componentProps = children !== void 0 ? {
86
- ...propsWithKey,
87
- children
88
- } : propsWithKey;
89
- if (isNativeCompat(type)) return h(type, componentProps);
90
- return h(wrapCompatComponent(type), componentProps);
91
- }
92
- if (typeof type === "string" && propsWithKey.ref != null) {
93
- const r = propsWithKey.ref;
94
- if (typeof r === "object" && r !== null && r[Symbol.for("__v_isRef")] === true) {
95
- const vueRef = r;
96
- propsWithKey.ref = (el) => {
97
- vueRef.value = el;
98
- };
99
- }
100
- }
101
- return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
102
- }
103
- const jsxs = jsx;
104
-
105
- //#endregion
106
- export { Fragment, jsx, jsxs };
107
- //# sourceMappingURL=jsx-runtime.js.map
3
+ export { Fragment, jsx, jsxs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/vue-compat",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "Vue 3-compatible Composition API shim for Pyreon — write Vue-style code that runs on Pyreon's reactive engine",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/vue-compat#readme",
6
6
  "bugs": {
@@ -54,13 +54,13 @@
54
54
  "prepublishOnly": "bun run build"
55
55
  },
56
56
  "dependencies": {
57
- "@pyreon/core": "^0.21.0",
58
- "@pyreon/reactivity": "^0.21.0",
59
- "@pyreon/runtime-dom": "^0.21.0"
57
+ "@pyreon/core": "^0.23.0",
58
+ "@pyreon/reactivity": "^0.23.0",
59
+ "@pyreon/runtime-dom": "^0.23.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@happy-dom/global-registrator": "^20.8.9",
63
- "@pyreon/test-utils": "^0.13.8",
63
+ "@pyreon/test-utils": "^0.13.10",
64
64
  "@vitest/browser-playwright": "^4.1.4",
65
65
  "happy-dom": "^20.8.3"
66
66
  }
package/src/index.ts CHANGED
@@ -28,11 +28,11 @@ import {
28
28
  onMount,
29
29
  onUnmount,
30
30
  onUpdate,
31
- popContext,
32
31
  Portal,
33
32
  pushContext,
34
33
  h as pyreonH,
35
34
  Suspense as PyreonSuspense,
35
+ removeContextFrame,
36
36
  useContext,
37
37
  } from '@pyreon/core'
38
38
  import {
@@ -885,8 +885,16 @@ export function provide<T>(key: string | symbol, value: T): void {
885
885
  if (idx < ctx.hooks.length) return // Already provided
886
886
  ctx.hooks[idx] = true
887
887
  const vueCtx = getOrCreateContext<T>(key)
888
- pushContext(new Map([[vueCtx.id, value]]))
889
- ctx.unmountCallbacks.push(() => popContext())
888
+ // Identity-based push/pop pair — capture the frame reference at push
889
+ // time, remove it by identity (not position) on unmount. Position-based
890
+ // `popContext()` here would pop the WRONG frame whenever sibling
891
+ // components unmount out-of-order (renderer-driven `<For>` removal,
892
+ // `<Show>` flipping a non-last sibling, route nav unmounting an outer
893
+ // of nested provider chains). Same root cause + same fix shape as
894
+ // `@pyreon/core` #725's `provide()` and #729's `_errorBoundaryStack`.
895
+ const frame = new Map([[vueCtx.id, value]])
896
+ pushContext(frame)
897
+ ctx.unmountCallbacks.push(() => removeContextFrame(frame))
890
898
  return
891
899
  }
892
900
  // Outside component — use Pyreon's provide directly
@@ -1070,13 +1078,32 @@ export function createApp(component: ComponentFn, props?: Props): App {
1070
1078
  if (!container) {
1071
1079
  throw new Error(`Cannot find mount target: ${el}`)
1072
1080
  }
1073
- // Push app-level provisions before mounting
1081
+ // Push app-level provisions before mounting AND track each pushed
1082
+ // frame so the returned unmount callback can remove them by IDENTITY.
1083
+ // Pre-fix the pushes were unmatched — every `createApp(...).provide(k,v).mount(el)`
1084
+ // call leaked one Map reference per provision onto the global context
1085
+ // stack permanently. Mount/unmount cycles compound this.
1086
+ const pushedFrames: Map<symbol, unknown>[] = []
1074
1087
  for (const { key, value } of provisions) {
1075
1088
  const ctx = getOrCreateContext(key)
1076
- pushContext(new Map([[ctx.id, value]]))
1089
+ const frame = new Map([[ctx.id, value]])
1090
+ pushContext(frame)
1091
+ pushedFrames.push(frame)
1077
1092
  }
1078
1093
  const vnode = pyreonH(component, props ?? null)
1079
- return pyreonMount(vnode, container)
1094
+ const unmount = pyreonMount(vnode, container)
1095
+ return () => {
1096
+ unmount()
1097
+ // Remove app-level provisions by identity (reverse order to match
1098
+ // LIFO push order if the same frame ref appears multiple times,
1099
+ // though that's structurally impossible here — each frame is a
1100
+ // fresh Map). Identity-based so other mounts pushing in between
1101
+ // can't accidentally remove our frames or have theirs removed.
1102
+ for (let i = pushedFrames.length - 1; i >= 0; i--) {
1103
+ const frame = pushedFrames[i]
1104
+ if (frame) removeContextFrame(frame)
1105
+ }
1106
+ }
1080
1107
  },
1081
1108
  use(plugin: { install: (app: App) => void }): App {
1082
1109
  plugin.install(app)