@pyreon/vue-compat 0.16.0 → 0.19.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/lib/analysis/index.js.html +1 -1
- package/lib/analysis/jsx-runtime.js.html +1 -1
- package/lib/index.js +304 -6
- package/lib/jsx-runtime.js +2 -1
- package/lib/types/index.d.ts +310 -2
- package/package.json +5 -5
- package/src/index.ts +430 -5
- package/src/jsx-runtime.ts +14 -0
- package/src/tests/new-apis.test.ts +359 -6
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
defineAsyncComponent,
|
|
7
7
|
defineComponent,
|
|
8
8
|
effectScope,
|
|
9
|
+
getCurrentInstance,
|
|
9
10
|
getCurrentScope,
|
|
10
11
|
h,
|
|
11
12
|
inject,
|
|
@@ -24,8 +25,13 @@ import {
|
|
|
24
25
|
readonly,
|
|
25
26
|
ref,
|
|
26
27
|
shallowReadonly,
|
|
28
|
+
Suspense,
|
|
27
29
|
Teleport,
|
|
28
30
|
toValue,
|
|
31
|
+
Transition,
|
|
32
|
+
TransitionGroup,
|
|
33
|
+
useAttrs,
|
|
34
|
+
useSlots,
|
|
29
35
|
version,
|
|
30
36
|
watch,
|
|
31
37
|
watchEffect,
|
|
@@ -35,6 +41,7 @@ import {
|
|
|
35
41
|
import {
|
|
36
42
|
beginRender,
|
|
37
43
|
endRender,
|
|
44
|
+
jsx,
|
|
38
45
|
type RenderContext,
|
|
39
46
|
} from '../jsx-runtime'
|
|
40
47
|
|
|
@@ -548,14 +555,35 @@ describe('Teleport', () => {
|
|
|
548
555
|
})
|
|
549
556
|
|
|
550
557
|
describe('KeepAlive', () => {
|
|
551
|
-
it('
|
|
552
|
-
const
|
|
553
|
-
|
|
558
|
+
it('mounts children (real runtime-dom KeepAlive wrapper)', () => {
|
|
559
|
+
const el = container()
|
|
560
|
+
const vnode = h(KeepAlive as ComponentFn, null, h('span', null, 'hello'))
|
|
561
|
+
const unmount = mount(vnode, el)
|
|
562
|
+
expect(el.textContent).toBe('hello')
|
|
563
|
+
unmount()
|
|
554
564
|
})
|
|
555
565
|
|
|
556
|
-
it('
|
|
557
|
-
const
|
|
558
|
-
|
|
566
|
+
it('keeps children mounted but hidden when active() is false', () => {
|
|
567
|
+
const el = container()
|
|
568
|
+
const vnode = h(
|
|
569
|
+
KeepAlive as ComponentFn,
|
|
570
|
+
{ active: () => false } as Record<string, unknown>,
|
|
571
|
+
h('span', { id: 'ka-hidden' }, 'kept'),
|
|
572
|
+
)
|
|
573
|
+
const unmount = mount(vnode, el)
|
|
574
|
+
// Children are still in the DOM (state preserved), just CSS-hidden
|
|
575
|
+
const span = el.querySelector('#ka-hidden')
|
|
576
|
+
expect(span).not.toBeNull()
|
|
577
|
+
expect(span!.textContent).toBe('kept')
|
|
578
|
+
unmount()
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('renders a transparent wrapper VNode (not raw children)', () => {
|
|
582
|
+
const vnode = KeepAlive({ children: 'x' })
|
|
583
|
+
// Real KeepAlive returns a <div style="display: contents"> VNode,
|
|
584
|
+
// not the bare children string the old stub returned.
|
|
585
|
+
expect(typeof vnode).toBe('object')
|
|
586
|
+
expect(vnode).not.toBeNull()
|
|
559
587
|
})
|
|
560
588
|
})
|
|
561
589
|
|
|
@@ -1301,3 +1329,328 @@ describe('defineComponent slots', () => {
|
|
|
1301
1329
|
unmount()
|
|
1302
1330
|
})
|
|
1303
1331
|
})
|
|
1332
|
+
|
|
1333
|
+
// ─── Transition ─────────────────────────────────────────────────────────────
|
|
1334
|
+
|
|
1335
|
+
describe('Transition', () => {
|
|
1336
|
+
it('renders its child when show() is true', () => {
|
|
1337
|
+
const el = container()
|
|
1338
|
+
const vnode = h(
|
|
1339
|
+
Transition as ComponentFn,
|
|
1340
|
+
{ name: 'fade', show: () => true } as Record<string, unknown>,
|
|
1341
|
+
h('div', { class: 'modal' }, 'content'),
|
|
1342
|
+
)
|
|
1343
|
+
const unmount = mount(vnode, el)
|
|
1344
|
+
expect(el.textContent).toBe('content')
|
|
1345
|
+
expect(el.querySelector('.modal')).not.toBeNull()
|
|
1346
|
+
unmount()
|
|
1347
|
+
})
|
|
1348
|
+
|
|
1349
|
+
it('defaults show() to always-true when omitted', () => {
|
|
1350
|
+
const el = container()
|
|
1351
|
+
const vnode = h(
|
|
1352
|
+
Transition as ComponentFn,
|
|
1353
|
+
{ name: 'fade' } as Record<string, unknown>,
|
|
1354
|
+
h('div', null, 'always'),
|
|
1355
|
+
)
|
|
1356
|
+
const unmount = mount(vnode, el)
|
|
1357
|
+
expect(el.textContent).toBe('always')
|
|
1358
|
+
unmount()
|
|
1359
|
+
})
|
|
1360
|
+
|
|
1361
|
+
it('maps Vue enterFromClass → Pyreon enterFrom (renders the child)', () => {
|
|
1362
|
+
// Pyreon's Transition injects a ref into the child element to drive
|
|
1363
|
+
// class application; the Vue class-name props are forwarded as the
|
|
1364
|
+
// Pyreon-named overrides. Mount end-to-end to prove the mapping holds.
|
|
1365
|
+
const el = container()
|
|
1366
|
+
const vnode = h(
|
|
1367
|
+
Transition as ComponentFn,
|
|
1368
|
+
{
|
|
1369
|
+
show: () => true,
|
|
1370
|
+
enterFromClass: 'my-enter-from',
|
|
1371
|
+
enterActiveClass: 'my-enter-active',
|
|
1372
|
+
} as Record<string, unknown>,
|
|
1373
|
+
h('div', { class: 'mapped' }, 'x'),
|
|
1374
|
+
)
|
|
1375
|
+
const unmount = mount(vnode, el)
|
|
1376
|
+
expect(el.querySelector('.mapped')).not.toBeNull()
|
|
1377
|
+
expect(el.textContent).toBe('x')
|
|
1378
|
+
unmount()
|
|
1379
|
+
})
|
|
1380
|
+
|
|
1381
|
+
it('passes lifecycle callbacks through', () => {
|
|
1382
|
+
// Smoke: building the VNode with hooks must not throw.
|
|
1383
|
+
const result = Transition({
|
|
1384
|
+
show: () => true,
|
|
1385
|
+
onBeforeEnter: () => {},
|
|
1386
|
+
onAfterEnter: () => {},
|
|
1387
|
+
onBeforeLeave: () => {},
|
|
1388
|
+
onAfterLeave: () => {},
|
|
1389
|
+
children: h('div', null, 'y'),
|
|
1390
|
+
})
|
|
1391
|
+
expect(result).not.toBeNull()
|
|
1392
|
+
})
|
|
1393
|
+
})
|
|
1394
|
+
|
|
1395
|
+
// ─── TransitionGroup ────────────────────────────────────────────────────────
|
|
1396
|
+
|
|
1397
|
+
describe('TransitionGroup', () => {
|
|
1398
|
+
it('renders a keyed list', () => {
|
|
1399
|
+
const el = container()
|
|
1400
|
+
const items = [{ id: 1 }, { id: 2 }, { id: 3 }]
|
|
1401
|
+
const vnode = h(TransitionGroup as ComponentFn, {
|
|
1402
|
+
tag: 'ul',
|
|
1403
|
+
name: 'list',
|
|
1404
|
+
items: () => items,
|
|
1405
|
+
keyFn: (it: { id: number }) => it.id,
|
|
1406
|
+
render: (it: { id: number }) => h('li', { class: 'item' }, String(it.id)),
|
|
1407
|
+
} as Record<string, unknown>)
|
|
1408
|
+
const unmount = mount(vnode, el)
|
|
1409
|
+
const lis = el.querySelectorAll('li.item')
|
|
1410
|
+
expect(lis.length).toBe(3)
|
|
1411
|
+
expect(el.textContent).toBe('123')
|
|
1412
|
+
expect(el.querySelector('ul')).not.toBeNull()
|
|
1413
|
+
unmount()
|
|
1414
|
+
})
|
|
1415
|
+
|
|
1416
|
+
it('maps Vue moveClass → Pyreon moveClass (returns a VNode)', () => {
|
|
1417
|
+
const result = TransitionGroup({
|
|
1418
|
+
items: () => [{ id: 1 }],
|
|
1419
|
+
keyFn: (it: { id: number }) => it.id,
|
|
1420
|
+
render: (it: { id: number }) => h('div', null, String(it.id)),
|
|
1421
|
+
moveClass: 'my-move',
|
|
1422
|
+
enterFromClass: 'ef',
|
|
1423
|
+
})
|
|
1424
|
+
expect(result).not.toBeNull()
|
|
1425
|
+
expect(typeof result).toBe('object')
|
|
1426
|
+
})
|
|
1427
|
+
})
|
|
1428
|
+
|
|
1429
|
+
// ─── Suspense ───────────────────────────────────────────────────────────────
|
|
1430
|
+
|
|
1431
|
+
describe('Suspense', () => {
|
|
1432
|
+
it('renders children synchronously when no async child', () => {
|
|
1433
|
+
const el = container()
|
|
1434
|
+
const vnode = h(
|
|
1435
|
+
Suspense as ComponentFn,
|
|
1436
|
+
{ fallback: h('div', null, 'loading') } as Record<string, unknown>,
|
|
1437
|
+
h('div', { class: 'page' }, 'ready'),
|
|
1438
|
+
)
|
|
1439
|
+
const unmount = mount(vnode, el)
|
|
1440
|
+
expect(el.textContent).toBe('ready')
|
|
1441
|
+
unmount()
|
|
1442
|
+
})
|
|
1443
|
+
|
|
1444
|
+
it('shows fallback while a defineAsyncComponent child loads', async () => {
|
|
1445
|
+
let resolveLoader: (m: { default: ComponentFn }) => void = () => {}
|
|
1446
|
+
const Async = defineAsyncComponent(
|
|
1447
|
+
() =>
|
|
1448
|
+
new Promise<{ default: ComponentFn }>((res) => {
|
|
1449
|
+
resolveLoader = res
|
|
1450
|
+
}),
|
|
1451
|
+
)
|
|
1452
|
+
const el = container()
|
|
1453
|
+
const vnode = h(
|
|
1454
|
+
Suspense as ComponentFn,
|
|
1455
|
+
{ fallback: h('div', { id: 'fb' }, 'loading…') } as Record<string, unknown>,
|
|
1456
|
+
h(Async as ComponentFn, null),
|
|
1457
|
+
)
|
|
1458
|
+
const unmount = mount(vnode, el)
|
|
1459
|
+
// Initially: fallback visible
|
|
1460
|
+
expect(el.textContent).toContain('loading…')
|
|
1461
|
+
|
|
1462
|
+
resolveLoader({ default: (() => h('div', { id: 'loaded' }, 'done')) as ComponentFn })
|
|
1463
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1464
|
+
|
|
1465
|
+
expect(el.querySelector('#loaded')).not.toBeNull()
|
|
1466
|
+
expect(el.textContent).toContain('done')
|
|
1467
|
+
unmount()
|
|
1468
|
+
})
|
|
1469
|
+
|
|
1470
|
+
it('defaults fallback to null when omitted', () => {
|
|
1471
|
+
const result = Suspense({ children: h('div', null, 'x') })
|
|
1472
|
+
expect(result).not.toBeNull()
|
|
1473
|
+
})
|
|
1474
|
+
})
|
|
1475
|
+
|
|
1476
|
+
// ─── getCurrentInstance ─────────────────────────────────────────────────────
|
|
1477
|
+
|
|
1478
|
+
describe('getCurrentInstance()', () => {
|
|
1479
|
+
it('returns null outside a component', () => {
|
|
1480
|
+
expect(getCurrentInstance()).toBeNull()
|
|
1481
|
+
})
|
|
1482
|
+
|
|
1483
|
+
it('returns a stable instance with uid/proxy/slots/attrs inside a component', async () => {
|
|
1484
|
+
let inst1: ReturnType<typeof getCurrentInstance> = null
|
|
1485
|
+
let inst2: ReturnType<typeof getCurrentInstance> = null
|
|
1486
|
+
let renderCount = 0
|
|
1487
|
+
const r = ref(0)
|
|
1488
|
+
|
|
1489
|
+
// Use jsx() so the component is wrapped → hook context + _props active
|
|
1490
|
+
// (this is how real vue-compat apps run via the vite plugin).
|
|
1491
|
+
const Comp = () => {
|
|
1492
|
+
const i = getCurrentInstance()
|
|
1493
|
+
if (renderCount === 0) inst1 = i
|
|
1494
|
+
else inst2 = i
|
|
1495
|
+
renderCount++
|
|
1496
|
+
void r.value // track → forces a re-render
|
|
1497
|
+
return jsx('div', { children: 'c' })
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const el = container()
|
|
1501
|
+
const unmount = mount(jsx(Comp, { children: 'kid' }), el)
|
|
1502
|
+
|
|
1503
|
+
expect(inst1).not.toBeNull()
|
|
1504
|
+
expect(typeof inst1!.uid).toBe('number')
|
|
1505
|
+
expect(typeof inst1!.proxy).toBe('object')
|
|
1506
|
+
expect(inst1!.isMounted).toBe(true)
|
|
1507
|
+
// attrs mirrors props (children present)
|
|
1508
|
+
expect(typeof inst1!.attrs).toBe('object')
|
|
1509
|
+
// slots.default derived from children
|
|
1510
|
+
expect(typeof inst1!.slots.default).toBe('function')
|
|
1511
|
+
expect(inst1!.slots.default!()).toBe('kid')
|
|
1512
|
+
|
|
1513
|
+
// Stable uid across re-render (hook-indexed)
|
|
1514
|
+
r.value = 1
|
|
1515
|
+
await new Promise((res) => setTimeout(res, 30))
|
|
1516
|
+
expect(inst2).not.toBeNull()
|
|
1517
|
+
expect(inst2!.uid).toBe(inst1!.uid)
|
|
1518
|
+
unmount()
|
|
1519
|
+
})
|
|
1520
|
+
|
|
1521
|
+
it('instance.emit(event, ...args) invokes the matching on{Event} prop handler', () => {
|
|
1522
|
+
const calls: unknown[][] = []
|
|
1523
|
+
const Comp = () => {
|
|
1524
|
+
const inst = getCurrentInstance()
|
|
1525
|
+
inst!.emit('save', 42, 'x')
|
|
1526
|
+
return jsx('div', { children: 'c' })
|
|
1527
|
+
}
|
|
1528
|
+
const el = container()
|
|
1529
|
+
const unmount = mount(
|
|
1530
|
+
jsx(Comp, { onSave: (...a: unknown[]) => calls.push(a) }),
|
|
1531
|
+
el,
|
|
1532
|
+
)
|
|
1533
|
+
// emit is called from the render body, which the compat wrapper may run
|
|
1534
|
+
// more than once — assert the handler was invoked with the right args
|
|
1535
|
+
// (not an exact call count, which is render-cadence coupled).
|
|
1536
|
+
expect(calls.length).toBeGreaterThanOrEqual(1)
|
|
1537
|
+
expect(calls[0]).toEqual([42, 'x'])
|
|
1538
|
+
unmount()
|
|
1539
|
+
})
|
|
1540
|
+
|
|
1541
|
+
it('instance.attrs excludes declared props under defineComponent({ props })', () => {
|
|
1542
|
+
let attrs: Record<string, unknown> = {}
|
|
1543
|
+
const Comp = defineComponent({
|
|
1544
|
+
props: { id: {} },
|
|
1545
|
+
setup() {
|
|
1546
|
+
attrs = getCurrentInstance()!.attrs
|
|
1547
|
+
return () => jsx('div', { children: 'a' })
|
|
1548
|
+
},
|
|
1549
|
+
})
|
|
1550
|
+
const el = container()
|
|
1551
|
+
const unmount = mount(
|
|
1552
|
+
jsx(Comp as never, { id: 'declared', role: 'dialog' }),
|
|
1553
|
+
el,
|
|
1554
|
+
)
|
|
1555
|
+
expect('id' in attrs).toBe(false)
|
|
1556
|
+
expect(attrs.role).toBe('dialog')
|
|
1557
|
+
unmount()
|
|
1558
|
+
})
|
|
1559
|
+
})
|
|
1560
|
+
|
|
1561
|
+
// ─── useSlots / useAttrs ────────────────────────────────────────────────────
|
|
1562
|
+
|
|
1563
|
+
describe('useSlots()', () => {
|
|
1564
|
+
it('returns {} outside a component', () => {
|
|
1565
|
+
expect(useSlots()).toEqual({})
|
|
1566
|
+
})
|
|
1567
|
+
|
|
1568
|
+
it('exposes default slot derived from children', () => {
|
|
1569
|
+
let slots: Record<string, (() => unknown) | undefined> = {}
|
|
1570
|
+
const Comp = () => {
|
|
1571
|
+
slots = useSlots()
|
|
1572
|
+
return jsx('div', { children: 'wrap' })
|
|
1573
|
+
}
|
|
1574
|
+
const el = container()
|
|
1575
|
+
const unmount = mount(jsx(Comp, { children: 'slotted' }), el)
|
|
1576
|
+
expect(typeof slots.default).toBe('function')
|
|
1577
|
+
expect(slots.default!()).toBe('slotted')
|
|
1578
|
+
unmount()
|
|
1579
|
+
})
|
|
1580
|
+
|
|
1581
|
+
it('returns {} when component has no children', () => {
|
|
1582
|
+
let slots: Record<string, (() => unknown) | undefined> = { default: () => 'x' }
|
|
1583
|
+
const Comp = () => {
|
|
1584
|
+
slots = useSlots()
|
|
1585
|
+
return jsx('div', { children: 'no-kids' })
|
|
1586
|
+
}
|
|
1587
|
+
const el = container()
|
|
1588
|
+
const unmount = mount(jsx(Comp, {}), el)
|
|
1589
|
+
expect(slots).toEqual({})
|
|
1590
|
+
unmount()
|
|
1591
|
+
})
|
|
1592
|
+
})
|
|
1593
|
+
|
|
1594
|
+
describe('useAttrs()', () => {
|
|
1595
|
+
it('returns {} outside a component', () => {
|
|
1596
|
+
expect(useAttrs()).toEqual({})
|
|
1597
|
+
})
|
|
1598
|
+
|
|
1599
|
+
it('returns the full props object (no declared/fallthrough split)', () => {
|
|
1600
|
+
let attrs: Record<string, unknown> = {}
|
|
1601
|
+
const Comp = () => {
|
|
1602
|
+
attrs = useAttrs()
|
|
1603
|
+
return jsx('div', { children: 'a' })
|
|
1604
|
+
}
|
|
1605
|
+
const el = container()
|
|
1606
|
+
const unmount = mount(jsx(Comp, { 'data-x': 'y', title: 'hi' }), el)
|
|
1607
|
+
expect(attrs['data-x']).toBe('y')
|
|
1608
|
+
expect(attrs.title).toBe('hi')
|
|
1609
|
+
unmount()
|
|
1610
|
+
})
|
|
1611
|
+
|
|
1612
|
+
it('excludes declared props (Vue fallthrough split) when defineComponent({ props })', () => {
|
|
1613
|
+
let attrs: Record<string, unknown> = {}
|
|
1614
|
+
const Comp = defineComponent({
|
|
1615
|
+
props: { msg: {}, count: {} },
|
|
1616
|
+
setup() {
|
|
1617
|
+
attrs = useAttrs()
|
|
1618
|
+
return () => jsx('div', { children: 'a' })
|
|
1619
|
+
},
|
|
1620
|
+
})
|
|
1621
|
+
const el = container()
|
|
1622
|
+
const unmount = mount(
|
|
1623
|
+
jsx(Comp as never, {
|
|
1624
|
+
msg: 'declared',
|
|
1625
|
+
count: 1,
|
|
1626
|
+
'data-x': 'y',
|
|
1627
|
+
onClick: () => {},
|
|
1628
|
+
children: 'kid',
|
|
1629
|
+
}),
|
|
1630
|
+
el,
|
|
1631
|
+
)
|
|
1632
|
+
// declared props + the internal children payload are NOT in attrs
|
|
1633
|
+
expect('msg' in attrs).toBe(false)
|
|
1634
|
+
expect('count' in attrs).toBe(false)
|
|
1635
|
+
expect('children' in attrs).toBe(false)
|
|
1636
|
+
// genuine fallthrough attrs ARE present
|
|
1637
|
+
expect(attrs['data-x']).toBe('y')
|
|
1638
|
+
expect(typeof attrs.onClick).toBe('function')
|
|
1639
|
+
unmount()
|
|
1640
|
+
})
|
|
1641
|
+
})
|
|
1642
|
+
|
|
1643
|
+
// ─── new exports importable ─────────────────────────────────────────────────
|
|
1644
|
+
|
|
1645
|
+
describe('new API exports', () => {
|
|
1646
|
+
it('are all defined on the module', async () => {
|
|
1647
|
+
const mod = await import('../index')
|
|
1648
|
+
expect(mod.Transition).toBeDefined()
|
|
1649
|
+
expect(mod.TransitionGroup).toBeDefined()
|
|
1650
|
+
expect(mod.Suspense).toBeDefined()
|
|
1651
|
+
expect(mod.getCurrentInstance).toBeDefined()
|
|
1652
|
+
expect(mod.useSlots).toBeDefined()
|
|
1653
|
+
expect(mod.useAttrs).toBeDefined()
|
|
1654
|
+
expect(mod.KeepAlive).toBeDefined()
|
|
1655
|
+
})
|
|
1656
|
+
})
|