apiSetupHelpers.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. import {
  2. EMPTY_OBJ,
  3. type LooseRequired,
  4. type Prettify,
  5. type UnionToIntersection,
  6. camelize,
  7. extend,
  8. hasChanged,
  9. isArray,
  10. isFunction,
  11. isPromise,
  12. } from '@vue/shared'
  13. import {
  14. type SetupContext,
  15. createSetupContext,
  16. getCurrentInstance,
  17. setCurrentInstance,
  18. unsetCurrentInstance,
  19. } from './component'
  20. import type { EmitFn, EmitsOptions, ObjectEmitsOptions } from './componentEmits'
  21. import type {
  22. ComponentOptionsMixin,
  23. ComponentOptionsWithoutProps,
  24. ComputedOptions,
  25. MethodOptions,
  26. } from './componentOptions'
  27. import type {
  28. ComponentObjectPropsOptions,
  29. ComponentPropsOptions,
  30. ExtractPropTypes,
  31. NormalizedProps,
  32. PropOptions,
  33. } from './componentProps'
  34. import { warn } from './warning'
  35. import type { SlotsType, StrictUnwrapSlotsType } from './componentSlots'
  36. import { type Ref, customRef, ref } from '@vue/reactivity'
  37. import { watchSyncEffect } from '.'
  38. // dev only
  39. const warnRuntimeUsage = (method: string) =>
  40. warn(
  41. `${method}() is a compiler-hint helper that is only usable inside ` +
  42. `<script setup> of a single file component. Its arguments should be ` +
  43. `compiled away and passing it at runtime has no effect.`,
  44. )
  45. /**
  46. * Vue `<script setup>` compiler macro for declaring component props. The
  47. * expected argument is the same as the component `props` option.
  48. *
  49. * Example runtime declaration:
  50. * ```js
  51. * // using Array syntax
  52. * const props = defineProps(['foo', 'bar'])
  53. * // using Object syntax
  54. * const props = defineProps({
  55. * foo: String,
  56. * bar: {
  57. * type: Number,
  58. * required: true
  59. * }
  60. * })
  61. * ```
  62. *
  63. * Equivalent type-based declaration:
  64. * ```ts
  65. * // will be compiled into equivalent runtime declarations
  66. * const props = defineProps<{
  67. * foo?: string
  68. * bar: number
  69. * }>()
  70. * ```
  71. *
  72. * @see {@link https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits}
  73. *
  74. * This is only usable inside `<script setup>`, is compiled away in the
  75. * output and should **not** be actually called at runtime.
  76. */
  77. // overload 1: runtime props w/ array
  78. export function defineProps<PropNames extends string = string>(
  79. props: PropNames[],
  80. ): Prettify<Readonly<{ [key in PropNames]?: any }>>
  81. // overload 2: runtime props w/ object
  82. export function defineProps<
  83. PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions,
  84. >(props: PP): Prettify<Readonly<ExtractPropTypes<PP>>>
  85. // overload 3: typed-based declaration
  86. export function defineProps<TypeProps>(): DefineProps<
  87. LooseRequired<TypeProps>,
  88. BooleanKey<TypeProps>
  89. >
  90. // implementation
  91. export function defineProps() {
  92. if (__DEV__) {
  93. warnRuntimeUsage(`defineProps`)
  94. }
  95. return null as any
  96. }
  97. export type DefineProps<T, BKeys extends keyof T> = Readonly<T> & {
  98. readonly [K in BKeys]-?: boolean
  99. }
  100. type BooleanKey<T, K extends keyof T = keyof T> = K extends any
  101. ? [T[K]] extends [boolean | undefined]
  102. ? K
  103. : never
  104. : never
  105. /**
  106. * Vue `<script setup>` compiler macro for declaring a component's emitted
  107. * events. The expected argument is the same as the component `emits` option.
  108. *
  109. * Example runtime declaration:
  110. * ```js
  111. * const emit = defineEmits(['change', 'update'])
  112. * ```
  113. *
  114. * Example type-based declaration:
  115. * ```ts
  116. * const emit = defineEmits<{
  117. * // <eventName>: <expected arguments>
  118. * change: []
  119. * update: [value: string] // named tuple syntax
  120. * }>()
  121. *
  122. * emit('change')
  123. * emit('update', 1)
  124. * ```
  125. *
  126. * This is only usable inside `<script setup>`, is compiled away in the
  127. * output and should **not** be actually called at runtime.
  128. *
  129. * @see {@link https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits}
  130. */
  131. // overload 1: runtime emits w/ array
  132. export function defineEmits<EE extends string = string>(
  133. emitOptions: EE[],
  134. ): EmitFn<EE[]>
  135. export function defineEmits<E extends EmitsOptions = EmitsOptions>(
  136. emitOptions: E,
  137. ): EmitFn<E>
  138. export function defineEmits<
  139. T extends ((...args: any[]) => any) | Record<string, any[]>,
  140. >(): T extends (...args: any[]) => any ? T : ShortEmits<T>
  141. // implementation
  142. export function defineEmits() {
  143. if (__DEV__) {
  144. warnRuntimeUsage(`defineEmits`)
  145. }
  146. return null as any
  147. }
  148. type RecordToUnion<T extends Record<string, any>> = T[keyof T]
  149. type ShortEmits<T extends Record<string, any>> = UnionToIntersection<
  150. RecordToUnion<{
  151. [K in keyof T]: (evt: K, ...args: T[K]) => void
  152. }>
  153. >
  154. /**
  155. * Vue `<script setup>` compiler macro for declaring a component's exposed
  156. * instance properties when it is accessed by a parent component via template
  157. * refs.
  158. *
  159. * `<script setup>` components are closed by default - i.e. variables inside
  160. * the `<script setup>` scope is not exposed to parent unless explicitly exposed
  161. * via `defineExpose`.
  162. *
  163. * This is only usable inside `<script setup>`, is compiled away in the
  164. * output and should **not** be actually called at runtime.
  165. *
  166. * @see {@link https://vuejs.org/api/sfc-script-setup.html#defineexpose}
  167. */
  168. export function defineExpose<
  169. Exposed extends Record<string, any> = Record<string, any>,
  170. >(exposed?: Exposed) {
  171. if (__DEV__) {
  172. warnRuntimeUsage(`defineExpose`)
  173. }
  174. }
  175. /**
  176. * Vue `<script setup>` compiler macro for declaring a component's additional
  177. * options. This should be used only for options that cannot be expressed via
  178. * Composition API - e.g. `inheritAttrs`.
  179. *
  180. * @see {@link https://vuejs.org/api/sfc-script-setup.html#defineoptions}
  181. */
  182. export function defineOptions<
  183. RawBindings = {},
  184. D = {},
  185. C extends ComputedOptions = {},
  186. M extends MethodOptions = {},
  187. Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  188. Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  189. >(
  190. options?: ComponentOptionsWithoutProps<
  191. {},
  192. RawBindings,
  193. D,
  194. C,
  195. M,
  196. Mixin,
  197. Extends
  198. > & { emits?: undefined; expose?: undefined; slots?: undefined },
  199. ): void {
  200. if (__DEV__) {
  201. warnRuntimeUsage(`defineOptions`)
  202. }
  203. }
  204. export function defineSlots<
  205. S extends Record<string, any> = Record<string, any>,
  206. >(): StrictUnwrapSlotsType<SlotsType<S>> {
  207. if (__DEV__) {
  208. warnRuntimeUsage(`defineSlots`)
  209. }
  210. return null as any
  211. }
  212. export type ModelRef<T, M extends string | number | symbol = string> = Ref<T> &
  213. [ModelRef<T, M>, Record<M, true | undefined>]
  214. /**
  215. * Vue `<script setup>` compiler macro for declaring a
  216. * two-way binding prop that can be consumed via `v-model` from the parent
  217. * component. This will declare a prop with the same name and a corresponding
  218. * `update:propName` event.
  219. *
  220. * If the first argument is a string, it will be used as the prop name;
  221. * Otherwise the prop name will default to "modelValue". In both cases, you
  222. * can also pass an additional object which will be used as the prop's options.
  223. *
  224. * The the returned ref behaves differently depending on whether the parent
  225. * provided the corresponding v-model props or not:
  226. * - If yes, the returned ref's value will always be in sync with the parent
  227. * prop.
  228. * - If not, the returned ref will behave like a normal local ref.
  229. *
  230. * @example
  231. * ```ts
  232. * // default model (consumed via `v-model`)
  233. * const modelValue = defineModel<string>()
  234. * modelValue.value = "hello"
  235. *
  236. * // default model with options
  237. * const modelValue = defineModel<string>({ required: true })
  238. *
  239. * // with specified name (consumed via `v-model:count`)
  240. * const count = defineModel<number>('count')
  241. * count.value++
  242. *
  243. * // with specified name and default value
  244. * const count = defineModel<number>('count', { default: 0 })
  245. * ```
  246. */
  247. export function defineModel<T, M extends string | number | symbol = string>(
  248. options: { required: true } & PropOptions<T> & UseModelOptions<T>,
  249. ): ModelRef<T, M>
  250. export function defineModel<T, M extends string | number | symbol = string>(
  251. options: { default: any } & PropOptions<T> & UseModelOptions<T>,
  252. ): ModelRef<T, M>
  253. export function defineModel<T, M extends string | number | symbol = string>(
  254. options?: PropOptions<T> & UseModelOptions<T>,
  255. ): ModelRef<T | undefined, M>
  256. export function defineModel<T, M extends string | number | symbol = string>(
  257. name: string,
  258. options: { required: true } & PropOptions<T> & UseModelOptions<T>,
  259. ): ModelRef<T, M>
  260. export function defineModel<T, M extends string | number | symbol = string>(
  261. name: string,
  262. options: { default: any } & PropOptions<T> & UseModelOptions<T>,
  263. ): ModelRef<T, M>
  264. export function defineModel<T, M extends string | number | symbol = string>(
  265. name: string,
  266. options?: PropOptions<T> & UseModelOptions<T>,
  267. ): ModelRef<T | undefined, M>
  268. export function defineModel(): any {
  269. if (__DEV__) {
  270. warnRuntimeUsage('defineModel')
  271. }
  272. }
  273. type NotUndefined<T> = T extends undefined ? never : T
  274. type InferDefaults<T> = {
  275. [K in keyof T]?: InferDefault<T, T[K]>
  276. }
  277. type NativeType = null | number | string | boolean | symbol | Function
  278. type InferDefault<P, T> =
  279. | ((props: P) => T & {})
  280. | (T extends NativeType ? T : never)
  281. type PropsWithDefaults<
  282. T,
  283. Defaults extends InferDefaults<T>,
  284. BKeys extends keyof T,
  285. > = Readonly<Omit<T, keyof Defaults>> & {
  286. readonly [K in keyof Defaults]-?: K extends keyof T
  287. ? Defaults[K] extends undefined
  288. ? T[K]
  289. : NotUndefined<T[K]>
  290. : never
  291. } & {
  292. readonly [K in BKeys]-?: K extends keyof Defaults
  293. ? Defaults[K] extends undefined
  294. ? boolean | undefined
  295. : boolean
  296. : boolean
  297. }
  298. /**
  299. * Vue `<script setup>` compiler macro for providing props default values when
  300. * using type-based `defineProps` declaration.
  301. *
  302. * Example usage:
  303. * ```ts
  304. * withDefaults(defineProps<{
  305. * size?: number
  306. * labels?: string[]
  307. * }>(), {
  308. * size: 3,
  309. * labels: () => ['default label']
  310. * })
  311. * ```
  312. *
  313. * This is only usable inside `<script setup>`, is compiled away in the output
  314. * and should **not** be actually called at runtime.
  315. *
  316. * @see {@link https://vuejs.org/guide/typescript/composition-api.html#typing-component-props}
  317. */
  318. export function withDefaults<
  319. T,
  320. BKeys extends keyof T,
  321. Defaults extends InferDefaults<T>,
  322. >(
  323. props: DefineProps<T, BKeys>,
  324. defaults: Defaults,
  325. ): PropsWithDefaults<T, Defaults, BKeys> {
  326. if (__DEV__) {
  327. warnRuntimeUsage(`withDefaults`)
  328. }
  329. return null as any
  330. }
  331. export function useSlots(): SetupContext['slots'] {
  332. return getContext().slots
  333. }
  334. export function useAttrs(): SetupContext['attrs'] {
  335. return getContext().attrs
  336. }
  337. type UseModelOptions<T = any> = {
  338. get?: (v: T) => any
  339. set?: (v: T) => any
  340. }
  341. export function useModel<
  342. M extends string | number | symbol,
  343. T extends Record<string, any>,
  344. K extends keyof T,
  345. >(props: T, name: K, options?: UseModelOptions<T[K]>): ModelRef<T[K], M>
  346. export function useModel(
  347. props: Record<string, any>,
  348. name: string,
  349. options: UseModelOptions = EMPTY_OBJ,
  350. ): Ref {
  351. const i = getCurrentInstance()!
  352. if (__DEV__ && !i) {
  353. warn(`useModel() called without active instance.`)
  354. return ref() as any
  355. }
  356. if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[name]) {
  357. warn(`useModel() called with prop "${name}" which is not declared.`)
  358. return ref() as any
  359. }
  360. const camelizedName = camelize(name)
  361. const res = customRef((track, trigger) => {
  362. let localValue: any
  363. watchSyncEffect(() => {
  364. const propValue = props[name]
  365. if (hasChanged(localValue, propValue)) {
  366. localValue = propValue
  367. trigger()
  368. }
  369. })
  370. return {
  371. get() {
  372. track()
  373. return options.get ? options.get(localValue) : localValue
  374. },
  375. set(value) {
  376. const rawProps = i.vnode!.props
  377. if (
  378. !(
  379. rawProps &&
  380. // check if parent has passed v-model
  381. (name in rawProps || camelizedName in rawProps) &&
  382. (`onUpdate:${name}` in rawProps ||
  383. `onUpdate:${camelizedName}` in rawProps)
  384. ) &&
  385. hasChanged(value, localValue)
  386. ) {
  387. localValue = value
  388. trigger()
  389. }
  390. i.emit(`update:${name}`, options.set ? options.set(value) : value)
  391. },
  392. }
  393. })
  394. const modifierKey =
  395. name === 'modelValue' ? 'modelModifiers' : `${name}Modifiers`
  396. // @ts-expect-error
  397. res[Symbol.iterator] = () => {
  398. let i = 0
  399. return {
  400. next() {
  401. if (i < 2) {
  402. return { value: i++ ? props[modifierKey] || {} : res, done: false }
  403. } else {
  404. return { done: true }
  405. }
  406. },
  407. }
  408. }
  409. return res
  410. }
  411. function getContext(): SetupContext {
  412. const i = getCurrentInstance()!
  413. if (__DEV__ && !i) {
  414. warn(`useContext() called without active instance.`)
  415. }
  416. return i.setupContext || (i.setupContext = createSetupContext(i))
  417. }
  418. /**
  419. * @internal
  420. */
  421. export function normalizePropsOrEmits(
  422. props: ComponentPropsOptions | EmitsOptions,
  423. ) {
  424. return isArray(props)
  425. ? props.reduce(
  426. (normalized, p) => ((normalized[p] = null), normalized),
  427. {} as ComponentObjectPropsOptions | ObjectEmitsOptions,
  428. )
  429. : props
  430. }
  431. /**
  432. * Runtime helper for merging default declarations. Imported by compiled code
  433. * only.
  434. * @internal
  435. */
  436. export function mergeDefaults(
  437. raw: ComponentPropsOptions,
  438. defaults: Record<string, any>,
  439. ): ComponentObjectPropsOptions {
  440. const props = normalizePropsOrEmits(raw)
  441. for (const key in defaults) {
  442. if (key.startsWith('__skip')) continue
  443. let opt = props[key]
  444. if (opt) {
  445. if (isArray(opt) || isFunction(opt)) {
  446. opt = props[key] = { type: opt, default: defaults[key] }
  447. } else {
  448. opt.default = defaults[key]
  449. }
  450. } else if (opt === null) {
  451. opt = props[key] = { default: defaults[key] }
  452. } else if (__DEV__) {
  453. warn(`props default key "${key}" has no corresponding declaration.`)
  454. }
  455. if (opt && defaults[`__skip_${key}`]) {
  456. opt.skipFactory = true
  457. }
  458. }
  459. return props
  460. }
  461. /**
  462. * Runtime helper for merging model declarations.
  463. * Imported by compiled code only.
  464. * @internal
  465. */
  466. export function mergeModels(
  467. a: ComponentPropsOptions | EmitsOptions,
  468. b: ComponentPropsOptions | EmitsOptions,
  469. ) {
  470. if (!a || !b) return a || b
  471. if (isArray(a) && isArray(b)) return a.concat(b)
  472. return extend({}, normalizePropsOrEmits(a), normalizePropsOrEmits(b))
  473. }
  474. /**
  475. * Used to create a proxy for the rest element when destructuring props with
  476. * defineProps().
  477. * @internal
  478. */
  479. export function createPropsRestProxy(
  480. props: any,
  481. excludedKeys: string[],
  482. ): Record<string, any> {
  483. const ret: Record<string, any> = {}
  484. for (const key in props) {
  485. if (!excludedKeys.includes(key)) {
  486. Object.defineProperty(ret, key, {
  487. enumerable: true,
  488. get: () => props[key],
  489. })
  490. }
  491. }
  492. return ret
  493. }
  494. /**
  495. * `<script setup>` helper for persisting the current instance context over
  496. * async/await flows.
  497. *
  498. * `@vue/compiler-sfc` converts the following:
  499. *
  500. * ```ts
  501. * const x = await foo()
  502. * ```
  503. *
  504. * into:
  505. *
  506. * ```ts
  507. * let __temp, __restore
  508. * const x = (([__temp, __restore] = withAsyncContext(() => foo())),__temp=await __temp,__restore(),__temp)
  509. * ```
  510. * @internal
  511. */
  512. export function withAsyncContext(getAwaitable: () => any) {
  513. const ctx = getCurrentInstance()!
  514. if (__DEV__ && !ctx) {
  515. warn(
  516. `withAsyncContext called without active current instance. ` +
  517. `This is likely a bug.`,
  518. )
  519. }
  520. let awaitable = getAwaitable()
  521. unsetCurrentInstance()
  522. if (isPromise(awaitable)) {
  523. awaitable = awaitable.catch(e => {
  524. setCurrentInstance(ctx)
  525. throw e
  526. })
  527. }
  528. return [awaitable, () => setCurrentInstance(ctx)]
  529. }