@pyreon/vue-compat 0.18.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
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
Portal,
|
|
33
33
|
pushContext,
|
|
34
34
|
h as pyreonH,
|
|
35
|
+
Suspense as PyreonSuspense,
|
|
35
36
|
useContext,
|
|
36
37
|
} from '@pyreon/core'
|
|
37
38
|
import {
|
|
@@ -42,7 +43,12 @@ import {
|
|
|
42
43
|
type Signal,
|
|
43
44
|
signal,
|
|
44
45
|
} from '@pyreon/reactivity'
|
|
45
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
KeepAlive as PyreonKeepAlive,
|
|
48
|
+
mount as pyreonMount,
|
|
49
|
+
Transition as PyreonTransition,
|
|
50
|
+
TransitionGroup as PyreonTransitionGroup,
|
|
51
|
+
} from '@pyreon/runtime-dom'
|
|
46
52
|
import { getCurrentCtx, getHookIndex } from './jsx-runtime'
|
|
47
53
|
|
|
48
54
|
// ─── Internal symbols ─────────────────────────────────────────────────────────
|
|
@@ -918,6 +924,27 @@ interface ComponentOptions<P extends Props = Props> {
|
|
|
918
924
|
props?: Record<string, unknown>
|
|
919
925
|
}
|
|
920
926
|
|
|
927
|
+
/**
|
|
928
|
+
* Computes Vue's fallthrough `attrs` — every passed prop that is NOT a
|
|
929
|
+
* declared prop (and not the internal `children` slot payload). When the
|
|
930
|
+
* component declared no props (`declared` undefined) the split is unknowable,
|
|
931
|
+
* so the full props object is returned (honest back-compat — matches the
|
|
932
|
+
* pre-split behavior rather than guessing).
|
|
933
|
+
*/
|
|
934
|
+
function splitVueAttrs(
|
|
935
|
+
props: Record<string, unknown>,
|
|
936
|
+
declared: string[] | undefined,
|
|
937
|
+
): Record<string, unknown> {
|
|
938
|
+
if (!declared) return props
|
|
939
|
+
const declaredSet = new Set(declared)
|
|
940
|
+
const attrs: Record<string, unknown> = {}
|
|
941
|
+
for (const key of Object.keys(props)) {
|
|
942
|
+
if (key === 'children' || declaredSet.has(key)) continue
|
|
943
|
+
attrs[key] = props[key]
|
|
944
|
+
}
|
|
945
|
+
return attrs
|
|
946
|
+
}
|
|
947
|
+
|
|
921
948
|
/**
|
|
922
949
|
* Defines a component using Vue 3 Composition API style.
|
|
923
950
|
* Only supports the `setup()` function — Options API is not supported.
|
|
@@ -928,9 +955,15 @@ export function defineComponent<P extends Props = Props>(
|
|
|
928
955
|
if (typeof options === 'function') {
|
|
929
956
|
return options as ComponentFn<P>
|
|
930
957
|
}
|
|
958
|
+
const declaredProps = options.props ? Object.keys(options.props) : undefined
|
|
931
959
|
const comp = (props: P) => {
|
|
932
960
|
// Extract children from props for slots
|
|
933
961
|
const children = (props as Record<string, unknown>).children as VNodeChild | undefined
|
|
962
|
+
// Publish the declared-prop names on the active render context so the
|
|
963
|
+
// standalone useAttrs() / getCurrentInstance() can compute the Vue
|
|
964
|
+
// declared-vs-fallthrough split (Vue's `attrs` excludes declared props).
|
|
965
|
+
const rc = getCurrentCtx()
|
|
966
|
+
if (rc && declaredProps) rc._declaredProps = declaredProps
|
|
934
967
|
// Create a minimal SetupContext
|
|
935
968
|
const setupCtx: SetupContext = {
|
|
936
969
|
emit: (event: string, ...args: unknown[]) => {
|
|
@@ -941,7 +974,7 @@ export function defineComponent<P extends Props = Props>(
|
|
|
941
974
|
slots: {
|
|
942
975
|
default: children !== undefined ? (() => children) : undefined,
|
|
943
976
|
} as Record<string, (() => VNodeChild) | undefined>,
|
|
944
|
-
attrs: props as Record<string, unknown>,
|
|
977
|
+
attrs: splitVueAttrs(props as Record<string, unknown>, declaredProps),
|
|
945
978
|
}
|
|
946
979
|
const result = options.setup(props, setupCtx)
|
|
947
980
|
if (typeof result === 'function') {
|
|
@@ -1216,10 +1249,402 @@ export function Teleport(props: {
|
|
|
1216
1249
|
}
|
|
1217
1250
|
|
|
1218
1251
|
/**
|
|
1219
|
-
* KeepAlive —
|
|
1252
|
+
* KeepAlive — mounts its children once and keeps them alive (state preserved)
|
|
1253
|
+
* even when hidden, instead of destroying/recreating them.
|
|
1254
|
+
*
|
|
1255
|
+
* Wraps `@pyreon/runtime-dom`'s real KeepAlive. Vue's `<KeepAlive>` keeps a
|
|
1256
|
+
* cache of inactive component instances; this maps the common single-slot
|
|
1257
|
+
* usage to Pyreon's `active`-accessor model.
|
|
1258
|
+
*
|
|
1259
|
+
* LIMITATIONS vs Vue 3:
|
|
1260
|
+
* - Vue's `include` / `exclude` / `max` props are NOT supported. Pyreon's
|
|
1261
|
+
* KeepAlive is a single always-mounted slot toggled by an `active`
|
|
1262
|
+
* accessor — there is no per-component LRU cache to filter or bound.
|
|
1263
|
+
* These props are accepted (so existing Vue code typechecks) but ignored.
|
|
1264
|
+
* - Vue toggles activation via the dynamic child (`<component :is>`); here
|
|
1265
|
+
* you pass an `active` accessor (`() => boolean`). When omitted, children
|
|
1266
|
+
* are always mounted and visible (a faithful default — nothing is
|
|
1267
|
+
* destroyed, matching KeepAlive's core guarantee).
|
|
1268
|
+
*
|
|
1269
|
+
* @example
|
|
1270
|
+
* import { KeepAlive, ref } from "@pyreon/vue-compat"
|
|
1271
|
+
*
|
|
1272
|
+
* function App() {
|
|
1273
|
+
* const showA = ref(true)
|
|
1274
|
+
* return (
|
|
1275
|
+
* <KeepAlive active={() => showA.value}>
|
|
1276
|
+
* <ExpensiveTab />
|
|
1277
|
+
* </KeepAlive>
|
|
1278
|
+
* )
|
|
1279
|
+
* }
|
|
1280
|
+
*/
|
|
1281
|
+
export function KeepAlive(props: {
|
|
1282
|
+
active?: () => boolean
|
|
1283
|
+
/** Accepted for Vue compatibility — ignored (no per-instance cache). */
|
|
1284
|
+
include?: string | RegExp | (string | RegExp)[]
|
|
1285
|
+
/** Accepted for Vue compatibility — ignored (no per-instance cache). */
|
|
1286
|
+
exclude?: string | RegExp | (string | RegExp)[]
|
|
1287
|
+
/** Accepted for Vue compatibility — ignored (no per-instance cache). */
|
|
1288
|
+
max?: number
|
|
1289
|
+
children?: VNodeChild
|
|
1290
|
+
}): VNodeChild {
|
|
1291
|
+
return PyreonKeepAlive({
|
|
1292
|
+
...(props.active !== undefined ? { active: props.active } : {}),
|
|
1293
|
+
children: props.children ?? null,
|
|
1294
|
+
})
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// ─── Transition / TransitionGroup ────────────────────────────────────────────
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* Transition — adds CSS enter/leave animation classes to a single child,
|
|
1301
|
+
* controlled by a reactive `show` accessor.
|
|
1302
|
+
*
|
|
1303
|
+
* Wraps `@pyreon/runtime-dom`'s Transition. Vue's class-name conventions
|
|
1304
|
+
* (`enter-from-class`, `enter-active-class`, …) are mapped onto Pyreon's
|
|
1305
|
+
* (`enterFrom`, `enterActive`, …), and Vue's `@before-enter` / `@after-enter`
|
|
1306
|
+
* style hooks are mapped onto Pyreon's `onBeforeEnter` / `onAfterEnter`.
|
|
1307
|
+
*
|
|
1308
|
+
* LIMITATIONS vs Vue 3:
|
|
1309
|
+
* - Vue's `<Transition>` infers visibility from a `v-if` / `v-show` on its
|
|
1310
|
+
* child. Pyreon has no template directives, so you MUST pass an explicit
|
|
1311
|
+
* `show: () => boolean` accessor. Without it the child is shown
|
|
1312
|
+
* unconditionally (no enter/leave is ever triggered).
|
|
1313
|
+
* - `mode` ("out-in" / "in-out"), `css: false`, and JS-only hook-driven
|
|
1314
|
+
* transitions are NOT supported — Pyreon's Transition is CSS-class based.
|
|
1315
|
+
* The props are accepted for typechecking but ignored.
|
|
1316
|
+
* - The Vue `name` convention (`name="fade"` → `fade-enter-from` …) is
|
|
1317
|
+
* preserved 1:1 (Pyreon uses the identical class-name scheme).
|
|
1318
|
+
*
|
|
1319
|
+
* @example
|
|
1320
|
+
* import { Transition, ref } from "@pyreon/vue-compat"
|
|
1321
|
+
*
|
|
1322
|
+
* function App() {
|
|
1323
|
+
* const visible = ref(false)
|
|
1324
|
+
* return (
|
|
1325
|
+
* <Transition name="fade" show={() => visible.value}>
|
|
1326
|
+
* <div class="modal">Hello</div>
|
|
1327
|
+
* </Transition>
|
|
1328
|
+
* )
|
|
1329
|
+
* }
|
|
1330
|
+
* // CSS:
|
|
1331
|
+
* // .fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
1332
|
+
* // .fade-enter-active, .fade-leave-active { transition: opacity 300ms; }
|
|
1333
|
+
*/
|
|
1334
|
+
export function Transition(props: {
|
|
1335
|
+
name?: string
|
|
1336
|
+
show?: () => boolean
|
|
1337
|
+
appear?: boolean
|
|
1338
|
+
/** Vue class-name prop — mapped to Pyreon's `enterFrom`. */
|
|
1339
|
+
enterFromClass?: string
|
|
1340
|
+
/** Vue class-name prop — mapped to Pyreon's `enterActive`. */
|
|
1341
|
+
enterActiveClass?: string
|
|
1342
|
+
/** Vue class-name prop — mapped to Pyreon's `enterTo`. */
|
|
1343
|
+
enterToClass?: string
|
|
1344
|
+
/** Vue class-name prop — mapped to Pyreon's `leaveFrom`. */
|
|
1345
|
+
leaveFromClass?: string
|
|
1346
|
+
/** Vue class-name prop — mapped to Pyreon's `leaveActive`. */
|
|
1347
|
+
leaveActiveClass?: string
|
|
1348
|
+
/** Vue class-name prop — mapped to Pyreon's `leaveTo`. */
|
|
1349
|
+
leaveToClass?: string
|
|
1350
|
+
/** Accepted for Vue compatibility — ignored (CSS-class transitions only). */
|
|
1351
|
+
mode?: 'in-out' | 'out-in' | 'default'
|
|
1352
|
+
/** Accepted for Vue compatibility — ignored (CSS-class transitions only). */
|
|
1353
|
+
css?: boolean
|
|
1354
|
+
onBeforeEnter?: (el: HTMLElement) => void
|
|
1355
|
+
onAfterEnter?: (el: HTMLElement) => void
|
|
1356
|
+
onBeforeLeave?: (el: HTMLElement) => void
|
|
1357
|
+
onAfterLeave?: (el: HTMLElement) => void
|
|
1358
|
+
children?: VNodeChild
|
|
1359
|
+
}): VNodeChild {
|
|
1360
|
+
return PyreonTransition({
|
|
1361
|
+
show: props.show ?? (() => true),
|
|
1362
|
+
...(props.name !== undefined ? { name: props.name } : {}),
|
|
1363
|
+
...(props.appear !== undefined ? { appear: props.appear } : {}),
|
|
1364
|
+
...(props.enterFromClass !== undefined ? { enterFrom: props.enterFromClass } : {}),
|
|
1365
|
+
...(props.enterActiveClass !== undefined ? { enterActive: props.enterActiveClass } : {}),
|
|
1366
|
+
...(props.enterToClass !== undefined ? { enterTo: props.enterToClass } : {}),
|
|
1367
|
+
...(props.leaveFromClass !== undefined ? { leaveFrom: props.leaveFromClass } : {}),
|
|
1368
|
+
...(props.leaveActiveClass !== undefined ? { leaveActive: props.leaveActiveClass } : {}),
|
|
1369
|
+
...(props.leaveToClass !== undefined ? { leaveTo: props.leaveToClass } : {}),
|
|
1370
|
+
...(props.onBeforeEnter !== undefined ? { onBeforeEnter: props.onBeforeEnter } : {}),
|
|
1371
|
+
...(props.onAfterEnter !== undefined ? { onAfterEnter: props.onAfterEnter } : {}),
|
|
1372
|
+
...(props.onBeforeLeave !== undefined ? { onBeforeLeave: props.onBeforeLeave } : {}),
|
|
1373
|
+
...(props.onAfterLeave !== undefined ? { onAfterLeave: props.onAfterLeave } : {}),
|
|
1374
|
+
children: props.children ?? null,
|
|
1375
|
+
})
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* TransitionGroup — animates a keyed reactive list with CSS enter/leave plus
|
|
1380
|
+
* FLIP move animations.
|
|
1381
|
+
*
|
|
1382
|
+
* Wraps `@pyreon/runtime-dom`'s TransitionGroup. Vue's class-name props are
|
|
1383
|
+
* mapped onto Pyreon's, same as {@link Transition}.
|
|
1384
|
+
*
|
|
1385
|
+
* LIMITATIONS vs Vue 3:
|
|
1386
|
+
* - Vue's `<TransitionGroup>` renders its children via slots and reads keys
|
|
1387
|
+
* from the child VNode `key`. Pyreon's API is explicit: pass `items`
|
|
1388
|
+
* (a reactive accessor), `keyFn` (stable key extractor), and `render`
|
|
1389
|
+
* (returns one DOM-element VNode per item). This is the faithful Pyreon
|
|
1390
|
+
* shape — the animation behavior (enter/leave/FLIP-move) is identical.
|
|
1391
|
+
* - `mode` and `css: false` are NOT supported (CSS-class transitions only).
|
|
1392
|
+
*
|
|
1393
|
+
* @example
|
|
1394
|
+
* import { TransitionGroup, ref } from "@pyreon/vue-compat"
|
|
1395
|
+
*
|
|
1396
|
+
* function App() {
|
|
1397
|
+
* const items = ref([{ id: 1 }, { id: 2 }])
|
|
1398
|
+
* return (
|
|
1399
|
+
* <TransitionGroup
|
|
1400
|
+
* tag="ul"
|
|
1401
|
+
* name="list"
|
|
1402
|
+
* items={() => items.value}
|
|
1403
|
+
* keyFn={(it) => it.id}
|
|
1404
|
+
* render={(it) => <li class="item">{it.id}</li>}
|
|
1405
|
+
* />
|
|
1406
|
+
* )
|
|
1407
|
+
* }
|
|
1408
|
+
*/
|
|
1409
|
+
export function TransitionGroup<T = unknown>(props: {
|
|
1410
|
+
tag?: string
|
|
1411
|
+
name?: string
|
|
1412
|
+
appear?: boolean
|
|
1413
|
+
enterFromClass?: string
|
|
1414
|
+
enterActiveClass?: string
|
|
1415
|
+
enterToClass?: string
|
|
1416
|
+
leaveFromClass?: string
|
|
1417
|
+
leaveActiveClass?: string
|
|
1418
|
+
leaveToClass?: string
|
|
1419
|
+
/** Vue class-name prop — mapped to Pyreon's `moveClass`. */
|
|
1420
|
+
moveClass?: string
|
|
1421
|
+
items: () => T[]
|
|
1422
|
+
keyFn: (item: T, index: number) => string | number
|
|
1423
|
+
render: (item: T, index: number) => ReturnType<typeof pyreonH>
|
|
1424
|
+
onBeforeEnter?: (el: HTMLElement) => void
|
|
1425
|
+
onAfterEnter?: (el: HTMLElement) => void
|
|
1426
|
+
onBeforeLeave?: (el: HTMLElement) => void
|
|
1427
|
+
onAfterLeave?: (el: HTMLElement) => void
|
|
1428
|
+
}): VNodeChild {
|
|
1429
|
+
return PyreonTransitionGroup<T>({
|
|
1430
|
+
items: props.items,
|
|
1431
|
+
keyFn: props.keyFn,
|
|
1432
|
+
render: props.render,
|
|
1433
|
+
...(props.tag !== undefined ? { tag: props.tag } : {}),
|
|
1434
|
+
...(props.name !== undefined ? { name: props.name } : {}),
|
|
1435
|
+
...(props.appear !== undefined ? { appear: props.appear } : {}),
|
|
1436
|
+
...(props.enterFromClass !== undefined ? { enterFrom: props.enterFromClass } : {}),
|
|
1437
|
+
...(props.enterActiveClass !== undefined ? { enterActive: props.enterActiveClass } : {}),
|
|
1438
|
+
...(props.enterToClass !== undefined ? { enterTo: props.enterToClass } : {}),
|
|
1439
|
+
...(props.leaveFromClass !== undefined ? { leaveFrom: props.leaveFromClass } : {}),
|
|
1440
|
+
...(props.leaveActiveClass !== undefined ? { leaveActive: props.leaveActiveClass } : {}),
|
|
1441
|
+
...(props.leaveToClass !== undefined ? { leaveTo: props.leaveToClass } : {}),
|
|
1442
|
+
...(props.moveClass !== undefined ? { moveClass: props.moveClass } : {}),
|
|
1443
|
+
...(props.onBeforeEnter !== undefined ? { onBeforeEnter: props.onBeforeEnter } : {}),
|
|
1444
|
+
...(props.onAfterEnter !== undefined ? { onAfterEnter: props.onAfterEnter } : {}),
|
|
1445
|
+
...(props.onBeforeLeave !== undefined ? { onBeforeLeave: props.onBeforeLeave } : {}),
|
|
1446
|
+
...(props.onAfterLeave !== undefined ? { onAfterLeave: props.onAfterLeave } : {}),
|
|
1447
|
+
})
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// ─── Suspense ────────────────────────────────────────────────────────────────
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* Suspense — shows `fallback` content while an async (lazy) child is loading.
|
|
1454
|
+
*
|
|
1455
|
+
* Re-exports `@pyreon/core`'s Suspense. Vue 3's `<Suspense>` uses named
|
|
1456
|
+
* `#default` / `#fallback` slots; this maps the `fallback` slot to Pyreon
|
|
1457
|
+
* Suspense's `fallback` prop and the default slot to `children`.
|
|
1458
|
+
*
|
|
1459
|
+
* LIMITATIONS vs Vue 3:
|
|
1460
|
+
* - Vue resolves `<Suspense>` against any `async setup()` in the subtree and
|
|
1461
|
+
* supports `@resolve` / `@pending` / `@fallback` events plus the `timeout`
|
|
1462
|
+
* prop. Pyreon's Suspense resolves against components carrying a
|
|
1463
|
+
* `__loading` accessor (e.g. {@link defineAsyncComponent} output) and does
|
|
1464
|
+
* not emit those events. The events / `timeout` prop are accepted for
|
|
1465
|
+
* typechecking but ignored.
|
|
1466
|
+
*
|
|
1467
|
+
* @example
|
|
1468
|
+
* import { Suspense, defineAsyncComponent } from "@pyreon/vue-compat"
|
|
1469
|
+
*
|
|
1470
|
+
* const AsyncPage = defineAsyncComponent(() => import("./Page"))
|
|
1471
|
+
*
|
|
1472
|
+
* function App() {
|
|
1473
|
+
* return (
|
|
1474
|
+
* <Suspense fallback={<div>Loading…</div>}>
|
|
1475
|
+
* <AsyncPage />
|
|
1476
|
+
* </Suspense>
|
|
1477
|
+
* )
|
|
1478
|
+
* }
|
|
1220
1479
|
*/
|
|
1221
|
-
export function
|
|
1222
|
-
|
|
1480
|
+
export function Suspense(props: {
|
|
1481
|
+
fallback?: VNodeChild
|
|
1482
|
+
/** Accepted for Vue compatibility — ignored (no timeout phase). */
|
|
1483
|
+
timeout?: number
|
|
1484
|
+
children?: VNodeChild
|
|
1485
|
+
}): VNodeChild {
|
|
1486
|
+
return PyreonSuspense({
|
|
1487
|
+
fallback: props.fallback ?? null,
|
|
1488
|
+
children: props.children ?? null,
|
|
1489
|
+
})
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// ─── getCurrentInstance / useSlots / useAttrs ────────────────────────────────
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* Vue-compatible component-instance handle.
|
|
1496
|
+
*
|
|
1497
|
+
* Backed by the compat hook-context. Only the fields commonly read by
|
|
1498
|
+
* composable libraries are populated. See {@link getCurrentInstance}.
|
|
1499
|
+
*/
|
|
1500
|
+
export interface ComponentInternalInstance {
|
|
1501
|
+
/** Monotonic per-instance id. */
|
|
1502
|
+
uid: number
|
|
1503
|
+
/**
|
|
1504
|
+
* Vue's `proxy` — the component public instance. In this shim it is an
|
|
1505
|
+
* empty object (Pyreon components are plain functions; there is no
|
|
1506
|
+
* `this`-bound options instance). Present so `inst.proxy` access doesn't
|
|
1507
|
+
* throw; do not rely on reading reactive state off it.
|
|
1508
|
+
*/
|
|
1509
|
+
proxy: Record<string, unknown>
|
|
1510
|
+
/** Slots derived from the current component's children. */
|
|
1511
|
+
slots: Record<string, (() => VNodeChild) | undefined>
|
|
1512
|
+
/** Fallthrough attrs (declared props excluded — mirrors `useAttrs()`). */
|
|
1513
|
+
attrs: Record<string, unknown>
|
|
1514
|
+
/**
|
|
1515
|
+
* Emits an event by invoking the matching `on{Event}` prop handler —
|
|
1516
|
+
* same behavior as the `emit` on `defineComponent`'s setup context.
|
|
1517
|
+
* Libraries that call `instance.emit(...)` (vee-validate, etc.) work.
|
|
1518
|
+
*/
|
|
1519
|
+
emit: (event: string, ...args: unknown[]) => void
|
|
1520
|
+
/** `true` — present for libraries that branch on `isMounted`-like flags. */
|
|
1521
|
+
isMounted: boolean
|
|
1522
|
+
/** @internal — the underlying compat render context. */
|
|
1523
|
+
_ctx: ReturnType<typeof getCurrentCtx>
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
let _instanceUid = 0
|
|
1527
|
+
|
|
1528
|
+
/**
|
|
1529
|
+
* Returns a handle to the current component instance, or `null` if called
|
|
1530
|
+
* outside a component setup.
|
|
1531
|
+
*
|
|
1532
|
+
* Vue 3's `getCurrentInstance()` is an internal API many composable libraries
|
|
1533
|
+
* (vee-validate, vue-i18n, pinia plugins, …) read for `uid`, `proxy`, `slots`,
|
|
1534
|
+
* `attrs`. This shim returns a minimal stable object with those fields so such
|
|
1535
|
+
* libraries don't crash.
|
|
1536
|
+
*
|
|
1537
|
+
* LIMITATIONS vs Vue 3:
|
|
1538
|
+
* - `proxy` is an empty object — Pyreon components are plain functions with
|
|
1539
|
+
* no `this`-bound Options instance. Code that reads reactive state off
|
|
1540
|
+
* `instance.proxy.$data` / `.$props` will not work; use `props` directly.
|
|
1541
|
+
* - `instance.emit(event, ...args)` IS provided (invokes the matching
|
|
1542
|
+
* `on{Event}` prop handler). `instance.attrs` is the fallthrough split
|
|
1543
|
+
* (declared props excluded) when the component used `defineComponent({
|
|
1544
|
+
* props })`; otherwise it is the full props object.
|
|
1545
|
+
* - `appContext`, `parent`, `vnode`, `expose`, render internals are NOT
|
|
1546
|
+
* provided. Libraries that walk the parent chain are not supported.
|
|
1547
|
+
* - The same `uid` is stable across re-renders of the same instance
|
|
1548
|
+
* (hook-indexed), matching Vue's per-instance-id guarantee.
|
|
1549
|
+
*
|
|
1550
|
+
* @example
|
|
1551
|
+
* import { getCurrentInstance } from "@pyreon/vue-compat"
|
|
1552
|
+
*
|
|
1553
|
+
* function useUid() {
|
|
1554
|
+
* const inst = getCurrentInstance()
|
|
1555
|
+
* return inst ? inst.uid : -1
|
|
1556
|
+
* }
|
|
1557
|
+
*/
|
|
1558
|
+
export function getCurrentInstance(): ComponentInternalInstance | null {
|
|
1559
|
+
const ctx = getCurrentCtx()
|
|
1560
|
+
if (!ctx) return null
|
|
1561
|
+
|
|
1562
|
+
const idx = getHookIndex()
|
|
1563
|
+
if (idx < ctx.hooks.length) {
|
|
1564
|
+
return ctx.hooks[idx] as ComponentInternalInstance
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
const props = ctx._props ?? {}
|
|
1568
|
+
const children = (props as Record<string, unknown>).children as VNodeChild | undefined
|
|
1569
|
+
const instance: ComponentInternalInstance = {
|
|
1570
|
+
uid: _instanceUid++,
|
|
1571
|
+
proxy: {},
|
|
1572
|
+
slots: {
|
|
1573
|
+
default: children !== undefined ? () => children : undefined,
|
|
1574
|
+
},
|
|
1575
|
+
attrs: splitVueAttrs(props, ctx._declaredProps),
|
|
1576
|
+
emit: (event: string, ...args: unknown[]) => {
|
|
1577
|
+
const handlerKey = `on${event.charAt(0).toUpperCase()}${event.slice(1)}`
|
|
1578
|
+
const handler = (props as Record<string, unknown>)[handlerKey]
|
|
1579
|
+
if (typeof handler === 'function') (handler as (...a: unknown[]) => void)(...args)
|
|
1580
|
+
},
|
|
1581
|
+
isMounted: true,
|
|
1582
|
+
_ctx: ctx,
|
|
1583
|
+
}
|
|
1584
|
+
ctx.hooks[idx] = instance
|
|
1585
|
+
return instance
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* Returns the current component's slots — a map of slot-name → render
|
|
1590
|
+
* function. Only the `default` slot is populated (derived from `children`),
|
|
1591
|
+
* matching how `@pyreon/vue-compat` models slots elsewhere
|
|
1592
|
+
* ({@link defineComponent}'s setup context).
|
|
1593
|
+
*
|
|
1594
|
+
* LIMITATIONS vs Vue 3:
|
|
1595
|
+
* - Vue supports arbitrary named + scoped slots resolved from the parent
|
|
1596
|
+
* template. Pyreon passes a single `children` payload, so only
|
|
1597
|
+
* `slots.default` is available. Named/scoped slots are not modeled.
|
|
1598
|
+
* - Returns an empty object (no `default`) when there are no children or
|
|
1599
|
+
* when called outside a component.
|
|
1600
|
+
*
|
|
1601
|
+
* @example
|
|
1602
|
+
* import { useSlots } from "@pyreon/vue-compat"
|
|
1603
|
+
*
|
|
1604
|
+
* function Wrapper() {
|
|
1605
|
+
* const slots = useSlots()
|
|
1606
|
+
* return <div class="box">{slots.default?.()}</div>
|
|
1607
|
+
* }
|
|
1608
|
+
*/
|
|
1609
|
+
export function useSlots(): Record<string, (() => VNodeChild) | undefined> {
|
|
1610
|
+
const ctx = getCurrentCtx()
|
|
1611
|
+
if (!ctx) return {}
|
|
1612
|
+
const props = ctx._props ?? {}
|
|
1613
|
+
const children = (props as Record<string, unknown>).children as VNodeChild | undefined
|
|
1614
|
+
if (children === undefined) return {}
|
|
1615
|
+
return { default: () => children }
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Returns the current component's non-prop attributes.
|
|
1620
|
+
*
|
|
1621
|
+
* In Vue, `useAttrs()` is the fallthrough attributes NOT declared in `props`.
|
|
1622
|
+
* `@pyreon/vue-compat` does not do declared-prop separation (components are
|
|
1623
|
+
* plain functions receiving one `props` object), so this returns the full
|
|
1624
|
+
* props object — every consumer-supplied attribute is present.
|
|
1625
|
+
*
|
|
1626
|
+
* LIMITATIONS vs Vue 3:
|
|
1627
|
+
* - No declared-vs-fallthrough split: `useAttrs()` here === the full props
|
|
1628
|
+
* object (including any that Vue would have consumed as declared props).
|
|
1629
|
+
* Read the specific keys you need; don't assume the result excludes
|
|
1630
|
+
* declared props.
|
|
1631
|
+
* - Returns an empty object when called outside a component.
|
|
1632
|
+
*
|
|
1633
|
+
* @example
|
|
1634
|
+
* import { useAttrs } from "@pyreon/vue-compat"
|
|
1635
|
+
*
|
|
1636
|
+
* function Passthrough() {
|
|
1637
|
+
* const attrs = useAttrs()
|
|
1638
|
+
* return <input {...attrs} />
|
|
1639
|
+
* }
|
|
1640
|
+
*/
|
|
1641
|
+
export function useAttrs(): Record<string, unknown> {
|
|
1642
|
+
const ctx = getCurrentCtx()
|
|
1643
|
+
if (!ctx) return {}
|
|
1644
|
+
// Vue's `attrs` = fallthrough props (declared props excluded). The split is
|
|
1645
|
+
// known when the component used `defineComponent({ props })`; otherwise the
|
|
1646
|
+
// full props object is returned (honest — the split is unknowable).
|
|
1647
|
+
return splitVueAttrs(ctx._props ?? {}, ctx._declaredProps)
|
|
1223
1648
|
}
|
|
1224
1649
|
|
|
1225
1650
|
// ─── watchPostEffect / watchSyncEffect ───────────────────────────────────────
|
package/src/jsx-runtime.ts
CHANGED
|
@@ -33,6 +33,19 @@ export interface RenderContext {
|
|
|
33
33
|
unmounted: boolean
|
|
34
34
|
/** Callbacks to run on unmount (lifecycle + effect cleanups) */
|
|
35
35
|
unmountCallbacks: (() => void)[]
|
|
36
|
+
/**
|
|
37
|
+
* The props object the wrapped component was invoked with. Read by
|
|
38
|
+
* `getCurrentInstance()` / `useSlots()` / `useAttrs()` to derive slots
|
|
39
|
+
* + attrs. Set by the wrapper before each render.
|
|
40
|
+
*/
|
|
41
|
+
_props?: Record<string, unknown>
|
|
42
|
+
/**
|
|
43
|
+
* Names of props declared via `defineComponent({ props })`. Set by
|
|
44
|
+
* `defineComponent` so `useAttrs()` / `getCurrentInstance().attrs` can
|
|
45
|
+
* compute the Vue declared-vs-fallthrough split. Undefined when the
|
|
46
|
+
* component didn't declare props (then attrs = the full props object).
|
|
47
|
+
*/
|
|
48
|
+
_declaredProps?: string[]
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
export interface EffectEntry {
|
|
@@ -106,6 +119,7 @@ function wrapCompatComponent(vueComponent: Function): ComponentFn {
|
|
|
106
119
|
pendingLayoutEffects: [],
|
|
107
120
|
unmounted: false,
|
|
108
121
|
unmountCallbacks: [],
|
|
122
|
+
_props: props as Record<string, unknown>,
|
|
109
123
|
}
|
|
110
124
|
|
|
111
125
|
const version = signal(0)
|