@pyreon/vue-compat 0.22.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 +71 -143
- package/lib/_chunks/jsx-runtime-CG6mH_9E.js +113 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +17 -16
- package/lib/jsx-runtime.js +2 -106
- package/package.json +5 -5
- package/src/index.ts +33 -6
- package/src/tests/provide-stack-leak-repro.test.ts +81 -0
- package/lib/analysis/jsx-runtime.js.html +0 -5406
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPRODUCTION + REGRESSION — vue-compat's `provide()` and `createApp().provide()`
|
|
3
|
+
* both used position-based `popContext()` (or no pop at all) to release the
|
|
4
|
+
* context-stack frames they pushed. Same bug class as #725 (`@pyreon/core`'s
|
|
5
|
+
* provide) and #729 (ErrorBoundary stack) — see those for the underlying
|
|
6
|
+
* theory.
|
|
7
|
+
*
|
|
8
|
+
* Bug shapes:
|
|
9
|
+
*
|
|
10
|
+
* 1) Component-level `provide(key, value)` — pushed a Map and registered
|
|
11
|
+
* `unmountCallbacks.push(() => popContext())`. `popContext` pops `stack.pop()`
|
|
12
|
+
* (the last frame). When sibling components unmount out-of-order (renderer-
|
|
13
|
+
* driven `<For>` removal, `<Show>` flipping a non-last sibling), the first
|
|
14
|
+
* sibling's onUnmount popped the LAST sibling's frame. Context stack
|
|
15
|
+
* corruption + orphaned frames at the top forever.
|
|
16
|
+
*
|
|
17
|
+
* 2) `createApp(C).provide('K', v).mount(el)` pushed app-level provisions
|
|
18
|
+
* BEFORE the mount but the returned unmount only ran `pyreonMount`'s
|
|
19
|
+
* cleanup — leaving the app-level frames on the global context stack
|
|
20
|
+
* forever, one per provision per mount cycle.
|
|
21
|
+
*
|
|
22
|
+
* Fix: identity-based removal via the newly-exported `removeContextFrame` from
|
|
23
|
+
* `@pyreon/core`. `provide` captures the frame at push, removes by reference.
|
|
24
|
+
* `createApp.mount` tracks every pushed app-level frame and removes each on
|
|
25
|
+
* unmount.
|
|
26
|
+
*/
|
|
27
|
+
import { captureContextStack, h as pyreonH } from '@pyreon/core'
|
|
28
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
29
|
+
import { createApp, inject } from '../index'
|
|
30
|
+
|
|
31
|
+
describe('vue-compat — provide/popContext is identity-safe (#725/#729 class)', () => {
|
|
32
|
+
let container: HTMLElement
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
container = document.createElement('div')
|
|
36
|
+
document.body.appendChild(container)
|
|
37
|
+
})
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
container.remove()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('REGRESSION: createApp().provide(k, v).mount(el); unmount() — context stack returns to baseline', () => {
|
|
43
|
+
const baseLen = captureContextStack().length
|
|
44
|
+
|
|
45
|
+
function Root() {
|
|
46
|
+
const v = inject<string>('K')
|
|
47
|
+
return pyreonH('span', { 'data-testid': 'val' }, v ?? 'missing')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const unmount = createApp(Root).provide('K', 'hello').mount(container)
|
|
51
|
+
|
|
52
|
+
// Sanity: the value was actually injected (proves the push reached the
|
|
53
|
+
// child) — prevents the regression test from passing vacuously.
|
|
54
|
+
expect(container.querySelector('[data-testid="val"]')?.textContent).toBe('hello')
|
|
55
|
+
|
|
56
|
+
unmount()
|
|
57
|
+
|
|
58
|
+
// The critical assertion: app-level provision frames are removed on
|
|
59
|
+
// unmount. Pre-fix the stack accumulates one frame per provision per
|
|
60
|
+
// mount cycle, forever.
|
|
61
|
+
expect(captureContextStack().length).toBe(baseLen)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('REGRESSION: 100 mount/unmount cycles do NOT accumulate context-stack frames', () => {
|
|
65
|
+
const baseLen = captureContextStack().length
|
|
66
|
+
|
|
67
|
+
function Root() {
|
|
68
|
+
return pyreonH('span', null, 'x')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < 100; i++) {
|
|
72
|
+
const unmount = createApp(Root).provide('A', i).provide('B', i + 1000).mount(container)
|
|
73
|
+
unmount()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Pre-fix: each mount pushes 2 frames, unmount removes 0 → 200 orphan
|
|
77
|
+
// frames after 100 cycles. Post-fix: stack returns to baseline.
|
|
78
|
+
const delta = captureContextStack().length - baseLen
|
|
79
|
+
expect(delta).toBeLessThan(5) // allow tiny noise from other unrelated test infra
|
|
80
|
+
})
|
|
81
|
+
})
|