@pyreon/vue-compat 0.13.1 → 0.15.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 +20 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/analysis/jsx-runtime.js.html +1 -1
- package/lib/index.js +410 -28
- package/lib/jsx-runtime.js +18 -5
- package/lib/types/index.d.ts +168 -8
- package/package.json +8 -4
- package/src/index.ts +622 -21
- package/src/jsx-runtime.ts +26 -2
- package/src/tests/jsx-runtime-wrapper.test.ts +158 -0
- package/src/tests/new-apis.test.ts +1303 -0
- package/src/vue-compat.browser.test.ts +31 -0
- package/lib/index.js.map +0 -1
- package/lib/jsx-runtime.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/jsx-runtime.d.ts.map +0 -1
package/src/jsx-runtime.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { ComponentFn, Props, VNode, VNodeChild } from '@pyreon/core'
|
|
18
|
-
import { Fragment, h, onUnmount } from '@pyreon/core'
|
|
18
|
+
import { Fragment, h, isNativeCompat, onUnmount } from '@pyreon/core'
|
|
19
19
|
import { runUntracked, signal } from '@pyreon/reactivity'
|
|
20
20
|
|
|
21
21
|
export { Fragment }
|
|
@@ -159,12 +159,36 @@ export function jsx(
|
|
|
159
159
|
const propsWithKey = (key != null ? { ...rest, key } : rest) as Props
|
|
160
160
|
|
|
161
161
|
if (typeof type === 'function') {
|
|
162
|
+
const componentProps = children !== undefined ? { ...propsWithKey, children } : propsWithKey
|
|
163
|
+
// Native Pyreon framework components (context Provider, RouterView,
|
|
164
|
+
// QueryClientProvider, etc.) skip compat wrapping — they manage their
|
|
165
|
+
// own reactivity via signals + Pyreon's lifecycle, and wrapping them
|
|
166
|
+
// would run their setup body inside the compat layer's render context
|
|
167
|
+
// instead of Pyreon's, breaking `provide()` / `onMount()` / `onUnmount()`.
|
|
168
|
+
// See `@pyreon/core`'s `nativeCompat()` for the contract.
|
|
169
|
+
if (isNativeCompat(type)) {
|
|
170
|
+
return h(type as ComponentFn, componentProps)
|
|
171
|
+
}
|
|
162
172
|
// Wrap Vue-style component for re-render support
|
|
163
173
|
const wrapped = wrapCompatComponent(type)
|
|
164
|
-
const componentProps = children !== undefined ? { ...propsWithKey, children } : propsWithKey
|
|
165
174
|
return h(wrapped, componentProps)
|
|
166
175
|
}
|
|
167
176
|
|
|
177
|
+
// DOM element: convert Vue ref ({ value }) to callback ref for Pyreon's runtime-dom
|
|
178
|
+
if (typeof type === 'string' && propsWithKey.ref != null) {
|
|
179
|
+
const r = propsWithKey.ref
|
|
180
|
+
if (
|
|
181
|
+
typeof r === 'object' &&
|
|
182
|
+
r !== null &&
|
|
183
|
+
(r as Record<symbol, unknown>)[Symbol.for('__v_isRef')] === true
|
|
184
|
+
) {
|
|
185
|
+
const vueRef = r as { value: unknown }
|
|
186
|
+
propsWithKey.ref = (el: Element | null) => {
|
|
187
|
+
vueRef.value = el
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
168
192
|
// DOM element or symbol (Fragment): children go in vnode.children
|
|
169
193
|
const childArray = children === undefined ? [] : Array.isArray(children) ? children : [children]
|
|
170
194
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { ComponentFn } from '@pyreon/core'
|
|
2
|
+
import { createContext, h, nativeCompat, provide, useContext } from '@pyreon/core'
|
|
3
|
+
import { mount } from '@pyreon/runtime-dom'
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { onMounted, onUnmounted } from '../index'
|
|
6
|
+
import { jsx } from '../jsx-runtime'
|
|
7
|
+
|
|
8
|
+
// Coverage gap closed in PR #323. Exercises the Vue-compat wrapper
|
|
9
|
+
// (`wrapCompatComponent`) end-to-end: JSX → mount → lifecycle hooks
|
|
10
|
+
// → unmount cleanup. Pins the wrapper's setup-and-teardown contract;
|
|
11
|
+
// fine-grained reactivity behavior is covered by the broader
|
|
12
|
+
// vue-compat.test.ts integration suite.
|
|
13
|
+
|
|
14
|
+
function container(): HTMLElement {
|
|
15
|
+
const el = document.createElement('div')
|
|
16
|
+
document.body.appendChild(el)
|
|
17
|
+
return el
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const tick = () => new Promise<void>((r) => queueMicrotask(() => r()))
|
|
21
|
+
|
|
22
|
+
describe('vue-compat — wrapCompatComponent (jsx-runtime)', () => {
|
|
23
|
+
it('caches the wrapper per source-fn identity (same wrapper on repeat jsx calls)', () => {
|
|
24
|
+
const Comp = () => jsx('div', { children: 'hi' })
|
|
25
|
+
const a = jsx(Comp, {})
|
|
26
|
+
const b = jsx(Comp, {})
|
|
27
|
+
// Both vnodes should reference the same wrapped component (the
|
|
28
|
+
// _wrapperCache WeakMap reuses by source-fn identity)
|
|
29
|
+
expect(a.type).toBe(b.type)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('produces distinct wrappers for distinct source functions', () => {
|
|
33
|
+
const A = () => jsx('div', { children: 'a' })
|
|
34
|
+
const B = () => jsx('div', { children: 'b' })
|
|
35
|
+
expect(jsx(A, {}).type).not.toBe(jsx(B, {}).type)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('mounts a Vue-style component and renders into the container', () => {
|
|
39
|
+
const Comp = () => jsx('div', { children: 'mounted-via-vue-compat' })
|
|
40
|
+
const c = container()
|
|
41
|
+
mount(jsx(Comp, {}), c)
|
|
42
|
+
expect(c.textContent).toContain('mounted-via-vue-compat')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('runs onMounted callback after first render', async () => {
|
|
46
|
+
const mountedSpy = vi.fn()
|
|
47
|
+
const Comp = () => {
|
|
48
|
+
onMounted(mountedSpy)
|
|
49
|
+
return jsx('div', { children: 'hi' })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const c = container()
|
|
53
|
+
mount(jsx(Comp, {}), c)
|
|
54
|
+
await tick()
|
|
55
|
+
await tick()
|
|
56
|
+
expect(mountedSpy).toHaveBeenCalledTimes(1)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('runs onUnmounted callback after disposal', async () => {
|
|
60
|
+
const unmountedSpy = vi.fn()
|
|
61
|
+
const Comp = () => {
|
|
62
|
+
onUnmounted(unmountedSpy)
|
|
63
|
+
return jsx('div', { children: 'hi' })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const c = container()
|
|
67
|
+
const dispose = mount(jsx(Comp, {}), c)
|
|
68
|
+
await tick()
|
|
69
|
+
expect(unmountedSpy).not.toHaveBeenCalled()
|
|
70
|
+
|
|
71
|
+
dispose()
|
|
72
|
+
expect(unmountedSpy).toHaveBeenCalledTimes(1)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('handles components with children prop (passes children through to wrapped fn)', () => {
|
|
76
|
+
const Wrapper = (props: { children?: string }) =>
|
|
77
|
+
jsx('section', { children: props.children ?? '' })
|
|
78
|
+
const c = container()
|
|
79
|
+
mount(jsx(Wrapper, { children: 'inner' }), c)
|
|
80
|
+
expect(c.textContent).toContain('inner')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('handles components with no props', () => {
|
|
84
|
+
const Comp = () => jsx('div', { children: 'noprops' })
|
|
85
|
+
const c = container()
|
|
86
|
+
mount(jsx(Comp, {}), c)
|
|
87
|
+
expect(c.textContent).toContain('noprops')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('skips wrapping for components marked via nativeCompat() (Vue-side parity with React/Preact/Solid compat)', () => {
|
|
91
|
+
// Pre-PR-2 vue-compat's jsx() had no NATIVE marker check — even marked
|
|
92
|
+
// components got wrapCompatComponent applied, which broke Pyreon framework
|
|
93
|
+
// components composed inside Vue-style code (their `provide()` /
|
|
94
|
+
// `onMount()` calls fired outside Pyreon's setup frame). PR 2 added the
|
|
95
|
+
// missing check at the same site as react/preact/solid compat.
|
|
96
|
+
//
|
|
97
|
+
// Bisect-verified: removing the `if (isNativeCompat(type))` branch from
|
|
98
|
+
// jsx-runtime.ts puts vue-compat back in the broken state — this test
|
|
99
|
+
// fails because the wrapped component's identity no longer matches the
|
|
100
|
+
// native source fn.
|
|
101
|
+
const Native = () => jsx('div', { children: 'native' })
|
|
102
|
+
nativeCompat(Native)
|
|
103
|
+
const vnode = jsx(Native, {})
|
|
104
|
+
// Marker hit: jsx() routes through h() directly with the SOURCE fn,
|
|
105
|
+
// never through wrapCompatComponent. So vnode.type === Native.
|
|
106
|
+
expect(vnode.type).toBe(Native)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('jsx() wraps UNMARKED components (control — bypass is selective)', () => {
|
|
110
|
+
// Sibling assertion to "skips wrapping for marked components": proves the
|
|
111
|
+
// marker check is selective (only marked components bypass), not blanket.
|
|
112
|
+
// Without this, a regression that accidentally wraps EVERYTHING would
|
|
113
|
+
// still pass the "marked component" test as a coincidence.
|
|
114
|
+
const Unmarked = () => jsx('div', { children: 'wrapped' })
|
|
115
|
+
const vnode = jsx(Unmarked, {})
|
|
116
|
+
// Wrapper: vnode.type is the cached wrapCompatComponent, NOT the source fn.
|
|
117
|
+
expect(vnode.type).not.toBe(Unmarked)
|
|
118
|
+
expect(typeof vnode.type).toBe('function')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('marked Provider mounts cleanly through compat-mode jsx() — provide() reaches descendants', () => {
|
|
122
|
+
// PR 5 mount-integration smoke (parity with react/preact/solid-compat
|
|
123
|
+
// suites). Sanity check that a marked Provider mounts cleanly through
|
|
124
|
+
// compat-mode jsx() and its `provide()` reaches the descendant Consumer.
|
|
125
|
+
//
|
|
126
|
+
// Note: this assertion is NOT bisect-load-bearing for the marker check
|
|
127
|
+
// itself. Synchronous mount preserves provide() context even WITH the
|
|
128
|
+
// wrapper (provide() pushes onto the global context stack regardless),
|
|
129
|
+
// so removing `nativeCompat(Provider)` here won't fail this test. The
|
|
130
|
+
// genuine multi-render-cycle bug PR #425 prevents (signal change re-fires
|
|
131
|
+
// the wrapper's accessor → provide() in re-run lands in stale stack) is
|
|
132
|
+
// covered by PR #427's e2e gate against real router state.
|
|
133
|
+
//
|
|
134
|
+
// The bisect-load-bearing assertion is the bypass-identity test above
|
|
135
|
+
// (`vnode.type === Native`) — that one fails when the marker check is
|
|
136
|
+
// removed from `jsx-runtime.ts`.
|
|
137
|
+
const Ctx = createContext<string>('default')
|
|
138
|
+
|
|
139
|
+
const Provider: ComponentFn = (props) => {
|
|
140
|
+
provide(Ctx, props.value as string)
|
|
141
|
+
return props.children as never
|
|
142
|
+
}
|
|
143
|
+
nativeCompat(Provider)
|
|
144
|
+
|
|
145
|
+
const Consumer: ComponentFn = () => {
|
|
146
|
+
const value = useContext(Ctx)
|
|
147
|
+
return h('span', { 'data-value': value }, value)
|
|
148
|
+
}
|
|
149
|
+
nativeCompat(Consumer)
|
|
150
|
+
|
|
151
|
+
const el = container()
|
|
152
|
+
mount(jsx(Provider, { value: 'native', children: jsx(Consumer, {}) }), el)
|
|
153
|
+
|
|
154
|
+
const span = el.querySelector('span')
|
|
155
|
+
expect(span?.getAttribute('data-value')).toBe('native')
|
|
156
|
+
expect(span?.textContent).toBe('native')
|
|
157
|
+
})
|
|
158
|
+
})
|