├── .github └── workflows │ ├── release.yml │ ├── stale.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh-CN.md ├── index.js ├── package.json ├── pnpm-lock.yaml ├── rollup.config.js ├── scripts └── update-readme.js ├── src ├── apis │ ├── computed.ts │ ├── createApp.ts │ ├── createElement.ts │ ├── effectScope.ts │ ├── index.ts │ ├── inject.ts │ ├── lifecycle.ts │ ├── nextTick.ts │ ├── setupHelpers.ts │ ├── useCssModule.ts │ ├── warn.ts │ └── watch.ts ├── component │ ├── common.ts │ ├── componentOptions.ts │ ├── componentProps.ts │ ├── componentProxy.ts │ ├── defineAsyncComponent.ts │ ├── defineComponent.ts │ ├── directives.ts │ └── index.ts ├── env.d.ts ├── global.d.ts ├── index.ts ├── install.ts ├── mixin.ts ├── reactivity │ ├── del.ts │ ├── force.ts │ ├── index.ts │ ├── reactive.ts │ ├── readonly.ts │ ├── ref.ts │ └── set.ts ├── runtimeContext.ts ├── types │ └── basic.ts └── utils │ ├── helper.ts │ ├── index.ts │ ├── instance.ts │ ├── sets.ts │ ├── symbols.ts │ ├── typeutils.ts │ ├── utils.ts │ └── vmStateManager.ts ├── test-dts ├── defineAsyncComponent.test-d.tsx ├── defineComponent-vue2.d.tsx ├── defineComponent.test-d.tsx ├── index.d.ts ├── readonly.test-d.tsx ├── ref.test-d.tsx ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.vue3.json └── watch.test-d.tsx ├── test ├── apis │ ├── computed.spec.js │ ├── inject.spec.js │ ├── lifecycle.spec.js │ ├── state.spec.js │ ├── useCssModule.spec.js │ ├── warn.spec.js │ └── watch.spec.js ├── createApp.spec.ts ├── globals.d.ts ├── helpers │ ├── create-local-vue.ts │ ├── index.ts │ ├── mockWarn.ts │ ├── utils.ts │ └── wait-for-update.ts ├── misc.spec.ts ├── setup.spec.js ├── setupContext.spec.ts ├── ssr │ ├── serverPrefetch.spec.js │ └── ssrReactive.spec.ts ├── templateRefs.spec.js ├── types │ └── defineComponent.spec.ts ├── use.spec.ts ├── v3 │ ├── reactivity │ │ ├── computed.spec.ts │ │ ├── del.spec.ts │ │ ├── effectScope.spec.ts │ │ ├── reactive.spec.ts │ │ ├── readonly.spec.ts │ │ ├── ref.spec.ts │ │ └── set.spec.ts │ └── runtime-core │ │ ├── apiAsyncComponent.spec.ts │ │ ├── apiInject.spec.ts │ │ ├── apiLifecycle.spec.ts │ │ ├── apiSetupHelpers.spec.ts │ │ ├── apiWatch.spec.ts │ │ ├── componentProxy.spec.ts │ │ ├── h.spec.ts │ │ └── useCssModule.spec.ts ├── vitest.setup.js └── vue.ts ├── tsconfig.json └── vitest.config.ts /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Github Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - uses: actions/setup-node@v3 17 | - name: Create Release 18 | run: npx conventional-github-releaser -p angular 19 | env: 20 | CI: true 21 | CONVENTIONAL_GITHUB_RELEASER_TOKEN: ${{secrets.GITHUB_TOKEN}} 22 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '39 23 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v3 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | stale-issue-message: 'Stale issue message' 25 | stale-pr-message: 'Stale pull request message' 26 | stale-issue-label: 'no-issue-activity' 27 | stale-pr-label: 'no-pr-activity' 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14.x, 16.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v2 18 | 19 | - name: Set node version to ${{ matrix.node_version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node_version }} 23 | cache: "pnpm" 24 | 25 | - name: Install 26 | run: pnpm i 27 | 28 | - name: Unit Tests 29 | run: pnpm run test 30 | env: 31 | CI: true 32 | 33 | - name: Typing Declaration Tests 34 | run: pnpm run test:dts 35 | env: 36 | CI: true 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | package-lock.json 4 | *.log 5 | dist 6 | lib 7 | .vscode 8 | .idea 9 | .rpt2_cache 10 | TODO.md 11 | typings -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present, liximomo(X.L) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | module.exports = require('./dist/vue-composition-api.common.prod.js') 5 | } else { 6 | module.exports = require('./dist/vue-composition-api.common.js') 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue/composition-api", 3 | "version": "1.7.2", 4 | "packageManager": "pnpm@8.6.12", 5 | "description": "Provide logic composition capabilities for Vue.", 6 | "keywords": [ 7 | "vue", 8 | "composition-api" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/vuejs/composition-api.git" 13 | }, 14 | "main": "./index.js", 15 | "module": "./dist/vue-composition-api.mjs", 16 | "unpkg": "./dist/vue-composition-api.prod.js", 17 | "jsdelivr": "./dist/vue-composition-api.prod.js", 18 | "types": "./dist/vue-composition-api.d.ts", 19 | "exports": { 20 | ".": { 21 | "import": "./dist/vue-composition-api.mjs", 22 | "types": "./dist/vue-composition-api.d.ts", 23 | "require": "./index.js" 24 | }, 25 | "./*": "./*" 26 | }, 27 | "author": { 28 | "name": "liximomo", 29 | "email": "liximomo@gmail.com" 30 | }, 31 | "license": "MIT", 32 | "sideEffects": false, 33 | "files": [ 34 | "dist", 35 | "index.js" 36 | ], 37 | "scripts": { 38 | "start": "rollup -c -w", 39 | "build": "rimraf dist && rollup -c", 40 | "lint": "prettier --write --parser typescript \"{src,test,test-dts}/**/*.ts?(x)\" && prettier --write \"{src,test}/**/*.js\"", 41 | "test": "vitest", 42 | "test:all": "pnpm run test && pnpm run test:dts", 43 | "test:dts": "tsc -p ./test-dts/tsconfig.json && tsc -p ./test-dts/tsconfig.vue3.json && pnpm run build && tsc -p ./test-dts/tsconfig.build.json", 44 | "update-readme": "node ./scripts/update-readme.js", 45 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s && pnpm run update-readme && git add CHANGELOG.md README.*", 46 | "release": "bumpp -x \"npm run changelog\" --all && npm publish", 47 | "release-gh": "conventional-github-releaser -p angular", 48 | "prepublishOnly": "npm run build" 49 | }, 50 | "bugs": { 51 | "url": "https://github.com/vuejs/composition-api/issues" 52 | }, 53 | "homepage": "https://github.com/vuejs/composition-api#readme", 54 | "peerDependencies": { 55 | "vue": ">= 2.5 < 2.7" 56 | }, 57 | "devDependencies": { 58 | "@rollup/plugin-node-resolve": "^13.3.0", 59 | "@rollup/plugin-replace": "^4.0.0", 60 | "@types/node": "^17.0.31", 61 | "bumpp": "^9.1.1", 62 | "conventional-changelog-cli": "^2.2.2", 63 | "conventional-github-releaser": "^3.1.5", 64 | "jsdom": "^20.0.0", 65 | "lint-staged": "^14.0.0", 66 | "prettier": "^3.0.1", 67 | "rimraf": "^5.0.1", 68 | "rollup": "^2.72.0", 69 | "rollup-plugin-dts": "^4.2.1", 70 | "rollup-plugin-terser": "^7.0.2", 71 | "rollup-plugin-typescript2": "^0.31.2", 72 | "simple-git-hooks": "^2.9.0", 73 | "tslib": "^2.4.0", 74 | "typescript": "^4.6.4", 75 | "vitest": "^0.34.1", 76 | "vue": "^2.6.14", 77 | "vue3": "npm:vue@3.2.21", 78 | "vue-router": "^3.5.3", 79 | "vue-server-renderer": "^2.6.14" 80 | }, 81 | "simple-git-hooks": { 82 | "pre-commit": "lint-staged" 83 | }, 84 | "lint-staged": { 85 | "*.js": [ 86 | "prettier --write" 87 | ], 88 | "*.ts?(x)": [ 89 | "prettier --parser=typescript --write" 90 | ] 91 | }, 92 | "prettier": { 93 | "semi": false, 94 | "singleQuote": true, 95 | "printWidth": 80 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import typescript from 'rollup-plugin-typescript2' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | import { terser } from 'rollup-plugin-terser' 5 | import replace from '@rollup/plugin-replace' 6 | import dts from 'rollup-plugin-dts' 7 | import { version } from './package.json' 8 | 9 | const builds = { 10 | 'cjs-dev': { 11 | outFile: 'vue-composition-api.common.js', 12 | format: 'cjs', 13 | mode: 'development', 14 | }, 15 | 'cjs-prod': { 16 | outFile: 'vue-composition-api.common.prod.js', 17 | format: 'cjs', 18 | mode: 'production', 19 | }, 20 | 'umd-dev': { 21 | outFile: 'vue-composition-api.js', 22 | format: 'umd', 23 | mode: 'development', 24 | }, 25 | 'umd-prod': { 26 | outFile: 'vue-composition-api.prod.js', 27 | format: 'umd', 28 | mode: 'production', 29 | }, 30 | esm: { 31 | outFile: 'vue-composition-api.esm.js', 32 | format: 'es', 33 | mode: 'development', 34 | }, 35 | mjs: { 36 | outFile: 'vue-composition-api.mjs', 37 | format: 'es', 38 | mode: 'development', 39 | }, 40 | } 41 | 42 | function onwarn(msg, warn) { 43 | if (!/Circular/.test(msg)) { 44 | warn(msg) 45 | } 46 | } 47 | 48 | function getAllBuilds() { 49 | return Object.keys(builds).map((key) => genConfig(builds[key])) 50 | } 51 | 52 | function genConfig({ outFile, format, mode }) { 53 | const isProd = mode === 'production' 54 | return { 55 | input: './src/index.ts', 56 | output: { 57 | file: path.join('./dist', outFile), 58 | format: format, 59 | globals: { 60 | vue: 'Vue', 61 | }, 62 | exports: 'named', 63 | name: format === 'umd' ? 'VueCompositionAPI' : undefined, 64 | }, 65 | external: ['vue'], 66 | onwarn, 67 | plugins: [ 68 | typescript({ 69 | tsconfigOverride: { 70 | declaration: false, 71 | declarationDir: null, 72 | emitDeclarationOnly: false, 73 | }, 74 | useTsconfigDeclarationDir: true, 75 | }), 76 | resolve(), 77 | replace({ 78 | preventAssignment: true, 79 | 'process.env.NODE_ENV': 80 | format === 'es' 81 | ? // preserve to be handled by bundlers 82 | 'process.env.NODE_ENV' 83 | : // hard coded dev/prod builds 84 | JSON.stringify(isProd ? 'production' : 'development'), 85 | __DEV__: 86 | format === 'es' 87 | ? // preserve to be handled by bundlers 88 | `(process.env.NODE_ENV !== 'production')` 89 | : // hard coded dev/prod builds 90 | !isProd, 91 | __VERSION__: JSON.stringify(version), 92 | }), 93 | isProd && terser(), 94 | ].filter(Boolean), 95 | } 96 | } 97 | 98 | let buildConfig 99 | 100 | if (process.env.TARGET) { 101 | buildConfig = [genConfig(builds[process.env.TARGET])] 102 | } else { 103 | buildConfig = getAllBuilds() 104 | } 105 | 106 | // bundle typings 107 | buildConfig.push({ 108 | input: 'src/index.ts', 109 | output: { 110 | file: 'dist/vue-composition-api.d.ts', 111 | format: 'es', 112 | }, 113 | onwarn, 114 | plugins: [dts()], 115 | }) 116 | 117 | export default buildConfig 118 | -------------------------------------------------------------------------------- /scripts/update-readme.js: -------------------------------------------------------------------------------- 1 | const { promises: fs } = require('fs') 2 | const path = require('path') 3 | const { version } = require('../package.json') 4 | 5 | const files = ['../README.md', '../README.zh-CN.md'] 6 | 7 | const MakeLinks = (version, vueVersion = '2.6') => 8 | ` 9 | \`\`\`html 10 | 11 | 12 | \`\`\` 13 | ` 14 | 15 | ;(async () => { 16 | const links = MakeLinks(version) 17 | 18 | for (const file of files) { 19 | const filepath = path.resolve(__dirname, file) 20 | const raw = await fs.readFile(filepath, 'utf-8') 21 | 22 | const updated = raw.replace( 23 | /([\s\S]*)/g, 24 | `${links}` 25 | ) 26 | 27 | await fs.writeFile(filepath, updated, 'utf-8') 28 | } 29 | })() 30 | -------------------------------------------------------------------------------- /src/apis/computed.ts: -------------------------------------------------------------------------------- 1 | import { getVueConstructor } from '../runtimeContext' 2 | import { createRef, ComputedRef, WritableComputedRef } from '../reactivity' 3 | import { 4 | warn, 5 | noopFn, 6 | defineComponentInstance, 7 | getVueInternalClasses, 8 | isFunction, 9 | } from '../utils' 10 | import { getCurrentScopeVM } from './effectScope' 11 | 12 | export type ComputedGetter = (ctx?: any) => T 13 | export type ComputedSetter = (v: T) => void 14 | 15 | export interface WritableComputedOptions { 16 | get: ComputedGetter 17 | set: ComputedSetter 18 | } 19 | 20 | // read-only 21 | export function computed(getter: ComputedGetter): ComputedRef 22 | // writable 23 | export function computed( 24 | options: WritableComputedOptions 25 | ): WritableComputedRef 26 | // implement 27 | export function computed( 28 | getterOrOptions: ComputedGetter | WritableComputedOptions 29 | ): ComputedRef | WritableComputedRef { 30 | const vm = getCurrentScopeVM() 31 | let getter: ComputedGetter 32 | let setter: ComputedSetter | undefined 33 | 34 | if (isFunction(getterOrOptions)) { 35 | getter = getterOrOptions 36 | } else { 37 | getter = getterOrOptions.get 38 | setter = getterOrOptions.set 39 | } 40 | 41 | let computedSetter 42 | let computedGetter 43 | 44 | if (vm && !vm.$isServer) { 45 | const { Watcher, Dep } = getVueInternalClasses() 46 | let watcher: any 47 | computedGetter = () => { 48 | if (!watcher) { 49 | watcher = new Watcher(vm, getter, noopFn, { lazy: true }) 50 | } 51 | if (watcher.dirty) { 52 | watcher.evaluate() 53 | } 54 | if (Dep.target) { 55 | watcher.depend() 56 | } 57 | return watcher.value 58 | } 59 | 60 | computedSetter = (v: T) => { 61 | if (__DEV__ && !setter) { 62 | warn('Write operation failed: computed value is readonly.', vm!) 63 | return 64 | } 65 | 66 | if (setter) { 67 | setter(v) 68 | } 69 | } 70 | } else { 71 | // fallback 72 | const computedHost = defineComponentInstance(getVueConstructor(), { 73 | computed: { 74 | $$state: { 75 | get: getter, 76 | set: setter, 77 | }, 78 | }, 79 | }) 80 | 81 | vm && vm.$on('hook:destroyed', () => computedHost.$destroy()) 82 | 83 | computedGetter = () => (computedHost as any).$$state 84 | computedSetter = (v: T) => { 85 | if (__DEV__ && !setter) { 86 | warn('Write operation failed: computed value is readonly.', vm!) 87 | return 88 | } 89 | 90 | ;(computedHost as any).$$state = v 91 | } 92 | } 93 | 94 | return createRef( 95 | { 96 | get: computedGetter, 97 | set: computedSetter, 98 | }, 99 | !setter, 100 | true 101 | ) as WritableComputedRef | ComputedRef 102 | } 103 | -------------------------------------------------------------------------------- /src/apis/createApp.ts: -------------------------------------------------------------------------------- 1 | import type Vue from 'vue' 2 | import { VueConstructor } from 'vue' 3 | import { Directive } from '../component/directives' 4 | import { getVueConstructor } from '../runtimeContext' 5 | import { warn } from '../utils' 6 | import { InjectionKey } from './inject' 7 | 8 | // Has a generic to match Vue 3 API and be type compatible 9 | export interface App { 10 | config: VueConstructor['config'] 11 | use: VueConstructor['use'] 12 | mixin: VueConstructor['mixin'] 13 | component: VueConstructor['component'] 14 | directive(name: string): Directive | undefined 15 | directive(name: string, directive: Directive): this 16 | provide(key: InjectionKey | symbol | string, value: T): this 17 | mount: Vue['$mount'] 18 | unmount: Vue['$destroy'] 19 | } 20 | 21 | export function createApp(rootComponent: any, rootProps: any = undefined): App { 22 | const V = getVueConstructor()! 23 | 24 | let mountedVM: Vue | undefined = undefined 25 | 26 | let provide: Record = {} 27 | 28 | const app: App = { 29 | config: V.config, 30 | use: V.use.bind(V), 31 | mixin: V.mixin.bind(V), 32 | component: V.component.bind(V), 33 | provide(key: InjectionKey | symbol | string, value: T) { 34 | provide[key as any] = value 35 | return this 36 | }, 37 | directive(name: string, dir?: Directive | undefined): any { 38 | if (dir) { 39 | V.directive(name, dir as any) 40 | return app 41 | } else { 42 | return V.directive(name) 43 | } 44 | }, 45 | mount: (el, hydrating) => { 46 | if (!mountedVM) { 47 | mountedVM = new V({ 48 | propsData: rootProps, 49 | ...rootComponent, 50 | provide: { ...provide, ...rootComponent.provide }, 51 | }) 52 | mountedVM.$mount(el, hydrating) 53 | return mountedVM 54 | } else { 55 | if (__DEV__) { 56 | warn( 57 | `App has already been mounted.\n` + 58 | `If you want to remount the same app, move your app creation logic ` + 59 | `into a factory function and create fresh app instances for each ` + 60 | `mount - e.g. \`const createMyApp = () => createApp(App)\`` 61 | ) 62 | } 63 | return mountedVM 64 | } 65 | }, 66 | unmount: () => { 67 | if (mountedVM) { 68 | mountedVM.$destroy() 69 | mountedVM = undefined 70 | } else if (__DEV__) { 71 | warn(`Cannot unmount an app that is not mounted.`) 72 | } 73 | }, 74 | } 75 | return app 76 | } 77 | -------------------------------------------------------------------------------- /src/apis/createElement.ts: -------------------------------------------------------------------------------- 1 | import type { CreateElement } from 'vue' 2 | import { 3 | getVueConstructor, 4 | getCurrentInstance, 5 | ComponentInternalInstance, 6 | } from '../runtimeContext' 7 | import { defineComponentInstance } from '../utils/helper' 8 | import { warn } from '../utils' 9 | import { AsyncComponent, Component } from 'vue/types/options' 10 | import { VNode, VNodeChildren, VNodeData } from 'vue/types/vnode' 11 | 12 | export interface H extends CreateElement { 13 | ( 14 | this: ComponentInternalInstance | null | undefined, 15 | tag?: 16 | | string 17 | | Component 18 | | AsyncComponent 19 | | (() => Component), 20 | children?: VNodeChildren 21 | ): VNode 22 | ( 23 | this: ComponentInternalInstance | null | undefined, 24 | tag?: 25 | | string 26 | | Component 27 | | AsyncComponent 28 | | (() => Component), 29 | data?: VNodeData, 30 | children?: VNodeChildren 31 | ): VNode 32 | } 33 | 34 | let fallbackCreateElement: CreateElement 35 | 36 | export const createElement: H = function createElement(this, ...args: any) { 37 | const instance = this?.proxy || getCurrentInstance()?.proxy 38 | if (!instance) { 39 | __DEV__ && 40 | warn('`createElement()` has been called outside of render function.') 41 | if (!fallbackCreateElement) { 42 | fallbackCreateElement = defineComponentInstance( 43 | getVueConstructor() 44 | ).$createElement 45 | } 46 | 47 | return fallbackCreateElement.apply(fallbackCreateElement, args) 48 | } 49 | 50 | return instance.$createElement.apply(instance, args) 51 | } 52 | -------------------------------------------------------------------------------- /src/apis/effectScope.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComponentInternalInstance, 3 | getCurrentInstance, 4 | getVueConstructor, 5 | withCurrentInstanceTrackingDisabled, 6 | } from '../runtimeContext' 7 | import { defineComponentInstance } from '../utils' 8 | import { warn } from './warn' 9 | 10 | let activeEffectScope: EffectScope | undefined 11 | const effectScopeStack: EffectScope[] = [] 12 | 13 | class EffectScopeImpl { 14 | active = true 15 | effects: EffectScope[] = [] 16 | cleanups: (() => void)[] = [] 17 | 18 | /** 19 | * @internal 20 | **/ 21 | vm: Vue 22 | 23 | constructor(vm: Vue) { 24 | this.vm = vm 25 | } 26 | 27 | run(fn: () => T): T | undefined { 28 | if (this.active) { 29 | try { 30 | this.on() 31 | return fn() 32 | } finally { 33 | this.off() 34 | } 35 | } else if (__DEV__) { 36 | warn(`cannot run an inactive effect scope.`) 37 | } 38 | return 39 | } 40 | 41 | on() { 42 | if (this.active) { 43 | effectScopeStack.push(this) 44 | activeEffectScope = this 45 | } 46 | } 47 | 48 | off() { 49 | if (this.active) { 50 | effectScopeStack.pop() 51 | activeEffectScope = effectScopeStack[effectScopeStack.length - 1] 52 | } 53 | } 54 | 55 | stop() { 56 | if (this.active) { 57 | this.vm.$destroy() 58 | this.effects.forEach((e) => e.stop()) 59 | this.cleanups.forEach((cleanup) => cleanup()) 60 | this.active = false 61 | } 62 | } 63 | } 64 | 65 | export class EffectScope extends EffectScopeImpl { 66 | constructor(detached = false) { 67 | let vm: Vue = undefined! 68 | withCurrentInstanceTrackingDisabled(() => { 69 | vm = defineComponentInstance(getVueConstructor()) 70 | }) 71 | super(vm) 72 | if (!detached) { 73 | recordEffectScope(this) 74 | } 75 | } 76 | } 77 | 78 | export function recordEffectScope( 79 | effect: EffectScope, 80 | scope?: EffectScope | null 81 | ) { 82 | scope = scope || activeEffectScope 83 | if (scope && scope.active) { 84 | scope.effects.push(effect) 85 | return 86 | } 87 | // destroy on parent component unmounted 88 | const vm = getCurrentInstance()?.proxy 89 | vm && vm.$on('hook:destroyed', () => effect.stop()) 90 | } 91 | 92 | export function effectScope(detached?: boolean) { 93 | return new EffectScope(detached) 94 | } 95 | 96 | export function getCurrentScope() { 97 | return activeEffectScope 98 | } 99 | 100 | export function onScopeDispose(fn: () => void) { 101 | if (activeEffectScope) { 102 | activeEffectScope.cleanups.push(fn) 103 | } else if (__DEV__) { 104 | warn( 105 | `onScopeDispose() is called when there is no active effect scope` + 106 | ` to be associated with.` 107 | ) 108 | } 109 | } 110 | 111 | /** 112 | * @internal 113 | **/ 114 | export function getCurrentScopeVM() { 115 | return getCurrentScope()?.vm || getCurrentInstance()?.proxy 116 | } 117 | 118 | /** 119 | * @internal 120 | **/ 121 | export function bindCurrentScopeToVM( 122 | vm: ComponentInternalInstance 123 | ): EffectScope { 124 | if (!vm.scope) { 125 | const scope = new EffectScopeImpl(vm.proxy) as EffectScope 126 | vm.scope = scope 127 | vm.proxy.$on('hook:destroyed', () => scope.stop()) 128 | } 129 | return vm.scope 130 | } 131 | -------------------------------------------------------------------------------- /src/apis/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../reactivity' 2 | export { 3 | onBeforeMount, 4 | onMounted, 5 | onBeforeUpdate, 6 | onUpdated, 7 | onBeforeUnmount, 8 | onUnmounted, 9 | onErrorCaptured, 10 | onActivated, 11 | onDeactivated, 12 | onServerPrefetch, 13 | } from './lifecycle' 14 | export * from './watch' 15 | export * from './computed' 16 | export * from './inject' 17 | export { useCssModule, useCSSModule } from './useCssModule' 18 | export { App, createApp } from './createApp' 19 | export { nextTick } from './nextTick' 20 | export { createElement as h } from './createElement' 21 | export { warn } from './warn' 22 | export { 23 | effectScope, 24 | EffectScope, 25 | getCurrentScope, 26 | onScopeDispose, 27 | } from './effectScope' 28 | export { useAttrs, useSlots } from './setupHelpers' 29 | -------------------------------------------------------------------------------- /src/apis/inject.ts: -------------------------------------------------------------------------------- 1 | import { ComponentInstance } from '../component' 2 | import { 3 | hasOwn, 4 | warn, 5 | getCurrentInstanceForFn, 6 | isFunction, 7 | proxy, 8 | } from '../utils' 9 | import { getCurrentInstance } from '../runtimeContext' 10 | 11 | const NOT_FOUND = {} 12 | export interface InjectionKey extends Symbol {} 13 | 14 | function resolveInject( 15 | provideKey: InjectionKey | string, 16 | vm: ComponentInstance 17 | ): any { 18 | let source = vm 19 | while (source) { 20 | if (source._provided && hasOwn(source._provided, provideKey as PropertyKey)) { 21 | return source._provided[provideKey as PropertyKey] 22 | } 23 | source = source.$parent 24 | } 25 | 26 | return NOT_FOUND 27 | } 28 | 29 | export function provide(key: InjectionKey | string, value: T): void { 30 | const vm = getCurrentInstanceForFn('provide')?.proxy 31 | if (!vm) return 32 | 33 | if (!vm._provided) { 34 | const provideCache = {} 35 | proxy(vm, '_provided', { 36 | get: () => provideCache, 37 | set: (v: any) => Object.assign(provideCache, v), 38 | }) 39 | } 40 | 41 | vm._provided[key as string] = value 42 | } 43 | 44 | export function inject(key: InjectionKey | string): T | undefined 45 | export function inject( 46 | key: InjectionKey | string, 47 | defaultValue: T, 48 | treatDefaultAsFactory?: false 49 | ): T 50 | export function inject( 51 | key: InjectionKey | string, 52 | defaultValue: T | (() => T), 53 | treatDefaultAsFactory?: true 54 | ): T 55 | export function inject( 56 | key: InjectionKey | string, 57 | defaultValue?: unknown, 58 | treatDefaultAsFactory = false 59 | ) { 60 | const vm = getCurrentInstance()?.proxy 61 | if (!vm) { 62 | __DEV__ && 63 | warn(`inject() can only be used inside setup() or functional components.`) 64 | return 65 | } 66 | 67 | if (!key) { 68 | __DEV__ && warn(`injection "${String(key)}" not found.`, vm) 69 | return defaultValue 70 | } 71 | 72 | const val = resolveInject(key, vm) 73 | if (val !== NOT_FOUND) { 74 | return val 75 | } else if (arguments.length > 1) { 76 | return treatDefaultAsFactory && isFunction(defaultValue) 77 | ? defaultValue() 78 | : defaultValue 79 | } else if (__DEV__) { 80 | warn(`Injection "${String(key)}" not found.`, vm) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/apis/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import { VueConstructor } from 'vue' 2 | import { 3 | getVueConstructor, 4 | setCurrentInstance, 5 | getCurrentInstance, 6 | ComponentInternalInstance, 7 | } from '../runtimeContext' 8 | import { getCurrentInstanceForFn } from '../utils/helper' 9 | 10 | const genName = (name: string) => `on${name[0].toUpperCase() + name.slice(1)}` 11 | function createLifeCycle(lifeCyclehook: string) { 12 | return (callback: Function, target?: ComponentInternalInstance | null) => { 13 | const instance = getCurrentInstanceForFn(genName(lifeCyclehook), target) 14 | return ( 15 | instance && 16 | injectHookOption(getVueConstructor(), instance, lifeCyclehook, callback) 17 | ) 18 | } 19 | } 20 | 21 | function injectHookOption( 22 | Vue: VueConstructor, 23 | instance: ComponentInternalInstance, 24 | hook: string, 25 | val: Function 26 | ) { 27 | const options = instance.proxy.$options as Record 28 | const mergeFn = Vue.config.optionMergeStrategies[hook] 29 | const wrappedHook = wrapHookCall(instance, val) 30 | options[hook] = mergeFn(options[hook], wrappedHook) 31 | return wrappedHook 32 | } 33 | 34 | function wrapHookCall( 35 | instance: ComponentInternalInstance, 36 | fn: Function 37 | ): Function { 38 | return (...args: any) => { 39 | let prev = getCurrentInstance() 40 | setCurrentInstance(instance) 41 | try { 42 | return fn(...args) 43 | } finally { 44 | setCurrentInstance(prev) 45 | } 46 | } 47 | } 48 | 49 | export const onBeforeMount = createLifeCycle('beforeMount') 50 | export const onMounted = createLifeCycle('mounted') 51 | export const onBeforeUpdate = createLifeCycle('beforeUpdate') 52 | export const onUpdated = createLifeCycle('updated') 53 | export const onBeforeUnmount = createLifeCycle('beforeDestroy') 54 | export const onUnmounted = createLifeCycle('destroyed') 55 | export const onErrorCaptured = createLifeCycle('errorCaptured') 56 | export const onActivated = createLifeCycle('activated') 57 | export const onDeactivated = createLifeCycle('deactivated') 58 | export const onServerPrefetch = createLifeCycle('serverPrefetch') 59 | -------------------------------------------------------------------------------- /src/apis/nextTick.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { getVueConstructor } from '../runtimeContext' 3 | 4 | type NextTick = Vue['$nextTick'] 5 | 6 | export const nextTick: NextTick = function nextTick( 7 | this: ThisType, 8 | ...args: Parameters 9 | ) { 10 | return getVueConstructor()?.nextTick.apply(this, args) 11 | } 12 | -------------------------------------------------------------------------------- /src/apis/setupHelpers.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance, SetupContext } from '../runtimeContext' 2 | import { warn } from '../utils' 3 | 4 | export function useSlots(): SetupContext['slots'] { 5 | return getContext().slots 6 | } 7 | 8 | export function useAttrs(): SetupContext['attrs'] { 9 | return getContext().attrs 10 | } 11 | 12 | function getContext(): SetupContext { 13 | const i = getCurrentInstance()! 14 | if (__DEV__ && !i) { 15 | warn(`useContext() called without active instance.`) 16 | } 17 | return i.setupContext! 18 | } 19 | -------------------------------------------------------------------------------- /src/apis/useCssModule.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from '../runtimeContext' 2 | import { warn } from '../utils' 3 | 4 | const EMPTY_OBJ: { readonly [key: string]: string } = __DEV__ 5 | ? Object.freeze({}) 6 | : {} 7 | 8 | export const useCssModule = (name = '$style'): Record => { 9 | const instance = getCurrentInstance() 10 | if (!instance) { 11 | __DEV__ && warn(`useCssModule must be called inside setup()`) 12 | return EMPTY_OBJ 13 | } 14 | 15 | const mod = (instance.proxy as any)?.[name] 16 | if (!mod) { 17 | __DEV__ && 18 | warn(`Current instance does not have CSS module named "${name}".`) 19 | return EMPTY_OBJ 20 | } 21 | 22 | return mod as Record 23 | } 24 | 25 | /** 26 | * @deprecated use `useCssModule` instead. 27 | */ 28 | export const useCSSModule = useCssModule 29 | -------------------------------------------------------------------------------- /src/apis/warn.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from '../runtimeContext' 2 | import { warn as vueWarn } from '../utils' 3 | 4 | /** 5 | * Displays a warning message (using console.error) with a stack trace if the 6 | * function is called inside of active component. 7 | * 8 | * @param message warning message to be displayed 9 | */ 10 | export function warn(message: string) { 11 | vueWarn(message, getCurrentInstance()?.proxy) 12 | } 13 | -------------------------------------------------------------------------------- /src/component/common.ts: -------------------------------------------------------------------------------- 1 | export type Data = { [key: string]: unknown } 2 | -------------------------------------------------------------------------------- /src/component/componentOptions.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode, ComponentOptions as Vue2ComponentOptions } from 'vue' 2 | import { EmitsOptions, SetupContext } from '../runtimeContext' 3 | import { Data } from './common' 4 | import { ComponentPropsOptions, ExtractPropTypes } from './componentProps' 5 | import { ComponentRenderProxy } from './componentProxy' 6 | export { ComponentPropsOptions } from './componentProps' 7 | 8 | export type ComputedGetter = (ctx?: any) => T 9 | export type ComputedSetter = (v: T) => void 10 | 11 | export interface WritableComputedOptions { 12 | get: ComputedGetter 13 | set: ComputedSetter 14 | } 15 | 16 | export type ComputedOptions = Record< 17 | string, 18 | ComputedGetter | WritableComputedOptions 19 | > 20 | 21 | export interface MethodOptions { 22 | [key: string]: Function 23 | } 24 | 25 | export type SetupFunction< 26 | Props, 27 | RawBindings = {}, 28 | Emits extends EmitsOptions = {} 29 | > = ( 30 | this: void, 31 | props: Readonly, 32 | ctx: SetupContext 33 | ) => RawBindings | (() => VNode | null) | void 34 | 35 | interface ComponentOptionsBase< 36 | Props, 37 | D = Data, 38 | C extends ComputedOptions = {}, 39 | M extends MethodOptions = {}, 40 | Mixin = {}, 41 | Extends = {}, 42 | Emits extends EmitsOptions = {} 43 | > extends Omit< 44 | Vue2ComponentOptions, 45 | 'data' | 'computed' | 'method' | 'setup' | 'props' 46 | > { 47 | // allow any custom options 48 | [key: string]: any 49 | 50 | // rewrite options api types 51 | data?: (this: Props & Vue, vm: Props) => D 52 | computed?: C 53 | methods?: M 54 | } 55 | 56 | export type ExtractComputedReturns = { 57 | [key in keyof T]: T[key] extends { get: (...args: any[]) => infer TReturn } 58 | ? TReturn 59 | : T[key] extends (...args: any[]) => infer TReturn 60 | ? TReturn 61 | : never 62 | } 63 | 64 | export type ComponentOptionsWithProps< 65 | PropsOptions = ComponentPropsOptions, 66 | RawBindings = Data, 67 | D = Data, 68 | C extends ComputedOptions = {}, 69 | M extends MethodOptions = {}, 70 | Mixin = {}, 71 | Extends = {}, 72 | Emits extends EmitsOptions = {}, 73 | Props = ExtractPropTypes 74 | > = ComponentOptionsBase & { 75 | props?: PropsOptions 76 | emits?: Emits & ThisType 77 | setup?: SetupFunction 78 | } & ThisType< 79 | ComponentRenderProxy 80 | > 81 | 82 | export type ComponentOptionsWithArrayProps< 83 | PropNames extends string = string, 84 | RawBindings = Data, 85 | D = Data, 86 | C extends ComputedOptions = {}, 87 | M extends MethodOptions = {}, 88 | Mixin = {}, 89 | Extends = {}, 90 | Emits extends EmitsOptions = {}, 91 | Props = Readonly<{ [key in PropNames]?: any }> 92 | > = ComponentOptionsBase & { 93 | props?: PropNames[] 94 | emits?: Emits & ThisType 95 | setup?: SetupFunction 96 | } & ThisType< 97 | ComponentRenderProxy 98 | > 99 | 100 | export type ComponentOptionsWithoutProps< 101 | Props = {}, 102 | RawBindings = Data, 103 | D = Data, 104 | C extends ComputedOptions = {}, 105 | M extends MethodOptions = {}, 106 | Mixin = {}, 107 | Extends = {}, 108 | Emits extends EmitsOptions = {} 109 | > = ComponentOptionsBase & { 110 | props?: undefined 111 | emits?: Emits & ThisType 112 | setup?: SetupFunction 113 | } & ThisType< 114 | ComponentRenderProxy 115 | > 116 | 117 | export type WithLegacyAPI = T & 118 | Omit, keyof T> 119 | -------------------------------------------------------------------------------- /src/component/componentProps.ts: -------------------------------------------------------------------------------- 1 | import { Data } from './common' 2 | 3 | export type ComponentPropsOptions

= 4 | | ComponentObjectPropsOptions

5 | | string[] 6 | 7 | export type ComponentObjectPropsOptions

= { 8 | [K in keyof P]: Prop | null 9 | } 10 | 11 | export type Prop = PropOptions | PropType 12 | 13 | type DefaultFactory = () => T | null | undefined 14 | 15 | export interface PropOptions { 16 | type?: PropType | true | null 17 | required?: boolean 18 | default?: D | DefaultFactory | null | undefined | object 19 | validator?(value: unknown): boolean 20 | } 21 | 22 | export type PropType = PropConstructor | PropConstructor[] 23 | 24 | type PropConstructor = 25 | | { new (...args: any[]): T & object } 26 | | { (): T } 27 | | { new (...args: string[]): Function } 28 | 29 | type RequiredKeys = { 30 | [K in keyof T]: T[K] extends 31 | | { required: true } 32 | | { default: any } 33 | | BooleanConstructor 34 | | { type: BooleanConstructor } 35 | ? K 36 | : never 37 | }[keyof T] 38 | 39 | type OptionalKeys = Exclude> 40 | 41 | type ExtractFunctionPropType< 42 | T extends Function, 43 | TArgs extends Array = any[], 44 | TResult = any 45 | > = T extends (...args: TArgs) => TResult ? T : never 46 | 47 | type ExtractCorrectPropType = T extends Function 48 | ? ExtractFunctionPropType 49 | : Exclude 50 | 51 | // prettier-ignore 52 | type InferPropType = T extends null 53 | ? any // null & true would fail to infer 54 | : T extends { type: null | true } 55 | ? any // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean` 56 | : T extends ObjectConstructor | { type: ObjectConstructor } 57 | ? Record 58 | : T extends BooleanConstructor | { type: BooleanConstructor } 59 | ? boolean 60 | : T extends DateConstructor | { type: DateConstructor} 61 | ? Date 62 | : T extends FunctionConstructor | { type: FunctionConstructor } 63 | ? Function 64 | : T extends Prop 65 | ? unknown extends V 66 | ? D extends null | undefined 67 | ? V 68 | : D 69 | : ExtractCorrectPropType 70 | : T 71 | 72 | export type ExtractPropTypes = { 73 | // use `keyof Pick>` instead of `RequiredKeys` to support IDE features 74 | [K in keyof Pick>]: InferPropType 75 | } & { 76 | // use `keyof Pick>` instead of `OptionalKeys` to support IDE features 77 | [K in keyof Pick>]?: InferPropType 78 | } 79 | 80 | type DefaultKeys = { 81 | [K in keyof T]: T[K] extends 82 | | { 83 | default: any 84 | } 85 | | BooleanConstructor 86 | | { type: BooleanConstructor } 87 | ? T[K] extends { 88 | type: BooleanConstructor 89 | required: true 90 | } 91 | ? never 92 | : K 93 | : never 94 | }[keyof T] 95 | 96 | // extract props which defined with default from prop options 97 | export type ExtractDefaultPropTypes = O extends object 98 | ? // use `keyof Pick>` instead of `DefaultKeys` to support IDE features 99 | { [K in keyof Pick>]: InferPropType } 100 | : {} 101 | -------------------------------------------------------------------------------- /src/component/componentProxy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractDefaultPropTypes, ExtractPropTypes } from './componentProps' 2 | import { 3 | nextTick, 4 | ShallowUnwrapRef, 5 | UnwrapNestedRefs, 6 | WatchOptions, 7 | WatchStopHandle, 8 | } from '..' 9 | import { Data } from './common' 10 | 11 | import Vue, { 12 | VueConstructor, 13 | ComponentOptions as Vue2ComponentOptions, 14 | } from 'vue' 15 | import { 16 | ComputedOptions, 17 | MethodOptions, 18 | ExtractComputedReturns, 19 | } from './componentOptions' 20 | import { 21 | ComponentInternalInstance, 22 | ComponentRenderEmitFn, 23 | EmitFn, 24 | EmitsOptions, 25 | ObjectEmitsOptions, 26 | Slots, 27 | } from '../runtimeContext' 28 | 29 | type EmitsToProps = T extends string[] 30 | ? { 31 | [K in string & `on${Capitalize}`]?: (...args: any[]) => any 32 | } 33 | : T extends ObjectEmitsOptions 34 | ? { 35 | [K in string & 36 | `on${Capitalize}`]?: K extends `on${infer C}` 37 | ? T[Uncapitalize] extends null 38 | ? (...args: any[]) => any 39 | : ( 40 | ...args: T[Uncapitalize] extends (...args: infer P) => any 41 | ? P 42 | : never 43 | ) => any 44 | : never 45 | } 46 | : {} 47 | 48 | export type ComponentInstance = InstanceType 49 | 50 | // public properties exposed on the proxy, which is used as the render context 51 | // in templates (as `this` in the render option) 52 | export type ComponentRenderProxy< 53 | P = {}, // props type extracted from props option 54 | B = {}, // raw bindings returned from setup() 55 | D = {}, // return from data() 56 | C extends ComputedOptions = {}, 57 | M extends MethodOptions = {}, 58 | Mixin = {}, 59 | Extends = {}, 60 | Emits extends EmitsOptions = {}, 61 | PublicProps = P, 62 | Defaults = {}, 63 | MakeDefaultsOptional extends boolean = false 64 | > = { 65 | $data: D 66 | $props: Readonly< 67 | MakeDefaultsOptional extends true 68 | ? Partial & Omit

69 | : P & PublicProps 70 | > 71 | $attrs: Record 72 | $emit: ComponentRenderEmitFn< 73 | Emits, 74 | keyof Emits, 75 | ComponentRenderProxy< 76 | P, 77 | B, 78 | D, 79 | C, 80 | M, 81 | Mixin, 82 | Extends, 83 | Emits, 84 | PublicProps, 85 | Defaults, 86 | MakeDefaultsOptional 87 | > 88 | > 89 | } & Readonly

& 90 | ShallowUnwrapRef & 91 | D & 92 | M & 93 | ExtractComputedReturns & 94 | Omit 95 | 96 | // for Vetur and TSX support 97 | type VueConstructorProxy< 98 | PropsOptions, 99 | RawBindings, 100 | Data, 101 | Computed extends ComputedOptions, 102 | Methods extends MethodOptions, 103 | Mixin = {}, 104 | Extends = {}, 105 | Emits extends EmitsOptions = {}, 106 | Props = ExtractPropTypes & 107 | ({} extends Emits ? {} : EmitsToProps) 108 | > = Omit & { 109 | new (...args: any[]): ComponentRenderProxy< 110 | Props, 111 | ShallowUnwrapRef, 112 | Data, 113 | Computed, 114 | Methods, 115 | Mixin, 116 | Extends, 117 | Emits, 118 | Props, 119 | ExtractDefaultPropTypes, 120 | true 121 | > 122 | } 123 | 124 | type DefaultData = object | ((this: V) => object) 125 | type DefaultMethods = { [key: string]: (this: V, ...args: any[]) => any } 126 | type DefaultComputed = { [key: string]: any } 127 | 128 | export type VueProxy< 129 | PropsOptions, 130 | RawBindings, 131 | Data = DefaultData, 132 | Computed extends ComputedOptions = DefaultComputed, 133 | Methods extends MethodOptions = DefaultMethods, 134 | Mixin = {}, 135 | Extends = {}, 136 | Emits extends EmitsOptions = {} 137 | > = Vue2ComponentOptions< 138 | Vue, 139 | ShallowUnwrapRef & Data, 140 | Methods, 141 | Computed, 142 | PropsOptions, 143 | ExtractPropTypes 144 | > & 145 | VueConstructorProxy< 146 | PropsOptions, 147 | RawBindings, 148 | Data, 149 | Computed, 150 | Methods, 151 | Mixin, 152 | Extends, 153 | Emits 154 | > 155 | 156 | // public properties exposed on the proxy, which is used as the render context 157 | // in templates (as `this` in the render option) 158 | export type ComponentPublicInstance< 159 | P = {}, // props type extracted from props option 160 | B = {}, // raw bindings returned from setup() 161 | D = {}, // return from data() 162 | C extends ComputedOptions = {}, 163 | M extends MethodOptions = {}, 164 | E extends EmitsOptions = {}, 165 | PublicProps = P, 166 | Defaults = {}, 167 | MakeDefaultsOptional extends boolean = false 168 | > = { 169 | $: ComponentInternalInstance 170 | $data: D 171 | $props: MakeDefaultsOptional extends true 172 | ? Partial & Omit

173 | : P & PublicProps 174 | $attrs: Data 175 | $refs: Data 176 | $slots: Slots 177 | $root: ComponentPublicInstance | null 178 | $parent: ComponentPublicInstance | null 179 | $emit: EmitFn 180 | $el: any 181 | // $options: Options & MergedComponentOptionsOverride 182 | $forceUpdate: () => void 183 | $nextTick: typeof nextTick 184 | $watch( 185 | source: string | Function, 186 | cb: Function, 187 | options?: WatchOptions 188 | ): WatchStopHandle 189 | } & P & 190 | ShallowUnwrapRef & 191 | UnwrapNestedRefs & 192 | ExtractComputedReturns & 193 | M 194 | -------------------------------------------------------------------------------- /src/component/defineAsyncComponent.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isObject, warn } from '../utils' 2 | import { VueProxy } from './componentProxy' 3 | import { AsyncComponent } from 'vue' 4 | 5 | import { 6 | ComponentOptionsWithoutProps, 7 | ComponentOptionsWithArrayProps, 8 | ComponentOptionsWithProps, 9 | } from './componentOptions' 10 | 11 | type Component = VueProxy 12 | 13 | type ComponentOrComponentOptions = 14 | // Component 15 | | Component 16 | // ComponentOptions 17 | | ComponentOptionsWithoutProps 18 | | ComponentOptionsWithArrayProps 19 | | ComponentOptionsWithProps 20 | 21 | export type AsyncComponentResolveResult = 22 | | T 23 | | { default: T } // es modules 24 | 25 | export type AsyncComponentLoader = () => Promise 26 | 27 | export interface AsyncComponentOptions { 28 | loader: AsyncComponentLoader 29 | loadingComponent?: ComponentOrComponentOptions 30 | errorComponent?: ComponentOrComponentOptions 31 | delay?: number 32 | timeout?: number 33 | suspensible?: boolean 34 | onError?: ( 35 | error: Error, 36 | retry: () => void, 37 | fail: () => void, 38 | attempts: number 39 | ) => any 40 | } 41 | 42 | export function defineAsyncComponent( 43 | source: AsyncComponentLoader | AsyncComponentOptions 44 | ): AsyncComponent { 45 | if (isFunction(source)) { 46 | source = { loader: source } 47 | } 48 | 49 | const { 50 | loader, 51 | loadingComponent, 52 | errorComponent, 53 | delay = 200, 54 | timeout, // undefined = never times out 55 | suspensible = false, // in Vue 3 default is true 56 | onError: userOnError, 57 | } = source 58 | 59 | if (__DEV__ && suspensible) { 60 | warn( 61 | `The suspensiblbe option for async components is not supported in Vue2. It is ignored.` 62 | ) 63 | } 64 | 65 | let pendingRequest: Promise | null = null 66 | 67 | let retries = 0 68 | const retry = () => { 69 | retries++ 70 | pendingRequest = null 71 | return load() 72 | } 73 | 74 | const load = (): Promise => { 75 | let thisRequest: Promise 76 | return ( 77 | pendingRequest || 78 | (thisRequest = pendingRequest = 79 | loader() 80 | .catch((err) => { 81 | err = err instanceof Error ? err : new Error(String(err)) 82 | if (userOnError) { 83 | return new Promise((resolve, reject) => { 84 | const userRetry = () => resolve(retry()) 85 | const userFail = () => reject(err) 86 | userOnError(err, userRetry, userFail, retries + 1) 87 | }) 88 | } else { 89 | throw err 90 | } 91 | }) 92 | .then((comp: any) => { 93 | if (thisRequest !== pendingRequest && pendingRequest) { 94 | return pendingRequest 95 | } 96 | if (__DEV__ && !comp) { 97 | warn( 98 | `Async component loader resolved to undefined. ` + 99 | `If you are using retry(), make sure to return its return value.` 100 | ) 101 | } 102 | // interop module default 103 | if ( 104 | comp && 105 | (comp.__esModule || comp[Symbol.toStringTag] === 'Module') 106 | ) { 107 | comp = comp.default 108 | } 109 | if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) { 110 | throw new Error(`Invalid async component load result: ${comp}`) 111 | } 112 | return comp 113 | })) 114 | ) 115 | } 116 | 117 | return () => { 118 | const component = load() 119 | 120 | return { 121 | component, 122 | delay, 123 | timeout, 124 | error: errorComponent, 125 | loading: loadingComponent, 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/component/defineComponent.ts: -------------------------------------------------------------------------------- 1 | import { ComponentPropsOptions } from './componentProps' 2 | import { 3 | MethodOptions, 4 | ComputedOptions, 5 | ComponentOptionsWithoutProps, 6 | ComponentOptionsWithArrayProps, 7 | ComponentOptionsWithProps, 8 | } from './componentOptions' 9 | import { VueProxy } from './componentProxy' 10 | import { Data } from './common' 11 | import { HasDefined } from '../types/basic' 12 | import { EmitsOptions } from '../runtimeContext' 13 | 14 | /** 15 | * overload 1: object format with no props 16 | */ 17 | export function defineComponent< 18 | RawBindings, 19 | D = Data, 20 | C extends ComputedOptions = {}, 21 | M extends MethodOptions = {}, 22 | Mixin = {}, 23 | Extends = {}, 24 | Emits extends EmitsOptions = {} 25 | >( 26 | options: ComponentOptionsWithoutProps< 27 | {}, 28 | RawBindings, 29 | D, 30 | C, 31 | M, 32 | Mixin, 33 | Extends, 34 | Emits 35 | > 36 | ): VueProxy<{}, RawBindings, D, C, M, Mixin, Extends, Emits> 37 | /** 38 | * overload 2: object format with array props declaration 39 | * props inferred as `{ [key in PropNames]?: any }` 40 | * 41 | * return type is for Vetur and TSX support 42 | */ 43 | export function defineComponent< 44 | PropNames extends string, 45 | RawBindings = Data, 46 | D = Data, 47 | C extends ComputedOptions = {}, 48 | M extends MethodOptions = {}, 49 | Mixin = {}, 50 | Extends = {}, 51 | Emits extends EmitsOptions = {}, 52 | PropsOptions extends ComponentPropsOptions = ComponentPropsOptions 53 | >( 54 | options: ComponentOptionsWithArrayProps< 55 | PropNames, 56 | RawBindings, 57 | D, 58 | C, 59 | M, 60 | Mixin, 61 | Extends, 62 | Emits 63 | > 64 | ): VueProxy< 65 | Readonly<{ [key in PropNames]?: any }>, 66 | RawBindings, 67 | D, 68 | C, 69 | M, 70 | Mixin, 71 | Extends, 72 | Emits 73 | > 74 | 75 | /** 76 | * overload 3: object format with object props declaration 77 | * 78 | * see `ExtractPropTypes` in './componentProps.ts' 79 | */ 80 | export function defineComponent< 81 | Props, 82 | RawBindings = Data, 83 | D = Data, 84 | C extends ComputedOptions = {}, 85 | M extends MethodOptions = {}, 86 | Mixin = {}, 87 | Extends = {}, 88 | Emits extends EmitsOptions = {}, 89 | PropsOptions extends ComponentPropsOptions = ComponentPropsOptions 90 | >( 91 | options: HasDefined extends true 92 | ? ComponentOptionsWithProps< 93 | PropsOptions, 94 | RawBindings, 95 | D, 96 | C, 97 | M, 98 | Mixin, 99 | Extends, 100 | Emits, 101 | Props 102 | > 103 | : ComponentOptionsWithProps< 104 | PropsOptions, 105 | RawBindings, 106 | D, 107 | C, 108 | M, 109 | Mixin, 110 | Extends, 111 | Emits 112 | > 113 | ): VueProxy 114 | 115 | // implementation, close to no-op 116 | export function defineComponent(options: any) { 117 | return options as any 118 | } 119 | -------------------------------------------------------------------------------- /src/component/directives.ts: -------------------------------------------------------------------------------- 1 | import type { VNodeDirective, VNode } from 'vue' 2 | 3 | export type DirectiveModifiers = Record 4 | 5 | export interface DirectiveBinding extends Readonly { 6 | readonly modifiers: DirectiveModifiers 7 | readonly value: V 8 | readonly oldValue: V | null 9 | } 10 | 11 | export type DirectiveHook = ( 12 | el: T, 13 | binding: DirectiveBinding, 14 | vnode: VNode, 15 | prevVNode: Prev 16 | ) => void 17 | 18 | export interface ObjectDirective { 19 | bind?: DirectiveHook 20 | inserted?: DirectiveHook 21 | update?: DirectiveHook 22 | componentUpdated?: DirectiveHook 23 | unbind?: DirectiveHook 24 | } 25 | export type FunctionDirective = DirectiveHook 26 | 27 | export type Directive = 28 | | ObjectDirective 29 | | FunctionDirective 30 | -------------------------------------------------------------------------------- /src/component/index.ts: -------------------------------------------------------------------------------- 1 | export { defineComponent } from './defineComponent' 2 | export { defineAsyncComponent } from './defineAsyncComponent' 3 | export { 4 | SetupFunction, 5 | ComputedOptions, 6 | MethodOptions, 7 | ComponentPropsOptions, 8 | } from './componentOptions' 9 | export { 10 | ComponentInstance, 11 | ComponentPublicInstance, 12 | ComponentRenderProxy, 13 | } from './componentProxy' 14 | export { Data } from './common' 15 | export { 16 | PropType, 17 | PropOptions, 18 | ExtractPropTypes, 19 | ExtractDefaultPropTypes, 20 | } from './componentProps' 21 | 22 | export { 23 | DirectiveModifiers, 24 | DirectiveBinding, 25 | DirectiveHook, 26 | ObjectDirective, 27 | FunctionDirective, 28 | Directive, 29 | } from './directives' 30 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | import { VueConstructor } from 'vue' 2 | import { VfaState } from './utils/vmStateManager' 3 | import { VueWatcher } from './apis/watch' 4 | 5 | declare global { 6 | interface Window { 7 | Vue: VueConstructor 8 | } 9 | } 10 | 11 | declare module 'vue/types/vue' { 12 | interface Vue { 13 | readonly _uid: number 14 | readonly _data: Record 15 | _watchers: VueWatcher[] 16 | _provided: Record 17 | __composition_api_state__?: VfaState 18 | } 19 | 20 | interface VueConstructor { 21 | observable(x: any): T 22 | util: { 23 | warn(msg: string, vm?: Vue | null) 24 | defineReactive( 25 | obj: Object, 26 | key: string, 27 | val: any, 28 | customSetter?: Function, 29 | shallow?: boolean 30 | ) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | // Global compile-time constants 2 | declare const __DEV__: boolean 3 | declare const __VERSION__: string 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type Vue from 'vue' 2 | import { Data, SetupFunction } from './component' 3 | import { Plugin } from './install' 4 | 5 | export const version = __VERSION__ 6 | 7 | export * from './apis' 8 | export * from './component' 9 | export { 10 | getCurrentInstance, 11 | ComponentInternalInstance, 12 | SetupContext, 13 | } from './runtimeContext' 14 | 15 | export default Plugin 16 | 17 | declare module 'vue/types/options' { 18 | interface ComponentOptions { 19 | setup?: SetupFunction 20 | } 21 | } 22 | 23 | // auto install when using CDN 24 | if (typeof window !== 'undefined' && window.Vue) { 25 | window.Vue.use(Plugin) 26 | } 27 | -------------------------------------------------------------------------------- /src/install.ts: -------------------------------------------------------------------------------- 1 | import type { VueConstructor } from 'vue' 2 | import { AnyObject } from './types/basic' 3 | import { isFunction, hasSymbol, hasOwn, isPlainObject, warn } from './utils' 4 | import { isRef } from './reactivity' 5 | import { setVueConstructor, isVueRegistered } from './runtimeContext' 6 | import { mixin } from './mixin' 7 | 8 | /** 9 | * Helper that recursively merges two data objects together. 10 | */ 11 | function mergeData(from: AnyObject, to: AnyObject): Object { 12 | if (!from) return to 13 | if (!to) return from 14 | 15 | let key: any 16 | let toVal: any 17 | let fromVal: any 18 | 19 | const keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from) 20 | 21 | for (let i = 0; i < keys.length; i++) { 22 | key = keys[i] 23 | // in case the object is already observed... 24 | if (key === '__ob__') continue 25 | toVal = to[key] 26 | fromVal = from[key] 27 | if (!hasOwn(to, key)) { 28 | to[key] = fromVal 29 | } else if ( 30 | toVal !== fromVal && 31 | isPlainObject(toVal) && 32 | !isRef(toVal) && 33 | isPlainObject(fromVal) && 34 | !isRef(fromVal) 35 | ) { 36 | mergeData(fromVal, toVal) 37 | } 38 | } 39 | return to 40 | } 41 | 42 | export function install(Vue: VueConstructor) { 43 | if (isVueRegistered(Vue)) { 44 | if (__DEV__) { 45 | warn( 46 | '[vue-composition-api] already installed. Vue.use(VueCompositionAPI) should be called only once.' 47 | ) 48 | } 49 | return 50 | } 51 | 52 | if (__DEV__) { 53 | if (Vue.version) { 54 | if (Vue.version[0] !== '2' || Vue.version[1] !== '.') { 55 | warn( 56 | `[vue-composition-api] only works with Vue 2, v${Vue.version} found.` 57 | ) 58 | } 59 | } else { 60 | warn('[vue-composition-api] no Vue version found') 61 | } 62 | } 63 | 64 | Vue.config.optionMergeStrategies.setup = function ( 65 | parent: Function, 66 | child: Function 67 | ) { 68 | return function mergedSetupFn(props: any, context: any) { 69 | return mergeData( 70 | isFunction(parent) ? parent(props, context) || {} : undefined, 71 | isFunction(child) ? child(props, context) || {} : undefined 72 | ) 73 | } 74 | } 75 | 76 | setVueConstructor(Vue) 77 | mixin(Vue) 78 | } 79 | 80 | export const Plugin = { 81 | install: (Vue: VueConstructor) => install(Vue), 82 | } 83 | -------------------------------------------------------------------------------- /src/mixin.ts: -------------------------------------------------------------------------------- 1 | import type { VueConstructor } from 'vue' 2 | import { ComponentInstance, SetupFunction, Data } from './component' 3 | import { isRef, isReactive, toRefs, isRaw } from './reactivity' 4 | import { 5 | isPlainObject, 6 | assert, 7 | proxy, 8 | warn, 9 | isFunction, 10 | isObject, 11 | def, 12 | isArray, 13 | } from './utils' 14 | import { ref } from './apis' 15 | import vmStateManager from './utils/vmStateManager' 16 | import { 17 | afterRender, 18 | activateCurrentInstance, 19 | resolveScopedSlots, 20 | asVmProperty, 21 | updateVmAttrs, 22 | } from './utils/instance' 23 | import { 24 | getVueConstructor, 25 | SetupContext, 26 | toVue3ComponentInstance, 27 | } from './runtimeContext' 28 | import { createObserver } from './reactivity/reactive' 29 | 30 | export function mixin(Vue: VueConstructor) { 31 | Vue.mixin({ 32 | beforeCreate: functionApiInit, 33 | mounted(this: ComponentInstance) { 34 | afterRender(this) 35 | }, 36 | beforeUpdate() { 37 | updateVmAttrs(this as ComponentInstance) 38 | }, 39 | updated(this: ComponentInstance) { 40 | afterRender(this) 41 | }, 42 | }) 43 | 44 | /** 45 | * Vuex init hook, injected into each instances init hooks list. 46 | */ 47 | 48 | function functionApiInit(this: ComponentInstance) { 49 | const vm = this 50 | const $options = vm.$options 51 | const { setup, render } = $options 52 | 53 | if (render) { 54 | // keep currentInstance accessible for createElement 55 | $options.render = function (...args: any): any { 56 | return activateCurrentInstance(toVue3ComponentInstance(vm), () => 57 | render.apply(this, args) 58 | ) 59 | } 60 | } 61 | 62 | if (!setup) { 63 | return 64 | } 65 | if (!isFunction(setup)) { 66 | if (__DEV__) { 67 | warn( 68 | 'The "setup" option should be a function that returns a object in component definitions.', 69 | vm 70 | ) 71 | } 72 | return 73 | } 74 | 75 | const { data } = $options 76 | // wrapper the data option, so we can invoke setup before data get resolved 77 | $options.data = function wrappedData() { 78 | initSetup(vm, vm.$props) 79 | return isFunction(data) 80 | ? ( 81 | data as (this: ComponentInstance, x: ComponentInstance) => object 82 | ).call(vm, vm) 83 | : data || {} 84 | } 85 | } 86 | 87 | function initSetup(vm: ComponentInstance, props: Record = {}) { 88 | const setup = vm.$options.setup! 89 | const ctx = createSetupContext(vm) 90 | const instance = toVue3ComponentInstance(vm) 91 | instance.setupContext = ctx 92 | 93 | // fake reactive for `toRefs(props)` 94 | def(props, '__ob__', createObserver()) 95 | 96 | // resolve scopedSlots and slots to functions 97 | resolveScopedSlots(vm, ctx.slots) 98 | 99 | let binding: ReturnType> | undefined | null 100 | activateCurrentInstance(instance, () => { 101 | // make props to be fake reactive, this is for `toRefs(props)` 102 | binding = setup(props, ctx) 103 | }) 104 | 105 | if (!binding) return 106 | if (isFunction(binding)) { 107 | // keep typescript happy with the binding type. 108 | const bindingFunc = binding 109 | // keep currentInstance accessible for createElement 110 | vm.$options.render = () => { 111 | resolveScopedSlots(vm, ctx.slots) 112 | return activateCurrentInstance(instance, () => bindingFunc()) 113 | } 114 | return 115 | } else if (isObject(binding)) { 116 | if (isReactive(binding)) { 117 | binding = toRefs(binding) as Data 118 | } 119 | 120 | vmStateManager.set(vm, 'rawBindings', binding) 121 | const bindingObj = binding 122 | 123 | Object.keys(bindingObj).forEach((name) => { 124 | let bindingValue: any = bindingObj[name] 125 | 126 | if (!isRef(bindingValue)) { 127 | if (!isReactive(bindingValue)) { 128 | if (isFunction(bindingValue)) { 129 | const copy = bindingValue 130 | bindingValue = bindingValue.bind(vm) 131 | Object.keys(copy).forEach(function (ele) { 132 | bindingValue[ele] = copy[ele] 133 | }) 134 | } else if (!isObject(bindingValue)) { 135 | bindingValue = ref(bindingValue) 136 | } else if (hasReactiveArrayChild(bindingValue)) { 137 | // creates a custom reactive properties without make the object explicitly reactive 138 | // NOTE we should try to avoid this, better implementation needed 139 | customReactive(bindingValue) 140 | } 141 | } else if (isArray(bindingValue)) { 142 | bindingValue = ref(bindingValue) 143 | } 144 | } 145 | asVmProperty(vm, name, bindingValue) 146 | }) 147 | 148 | return 149 | } 150 | 151 | if (__DEV__) { 152 | assert( 153 | false, 154 | `"setup" must return a "Object" or a "Function", got "${Object.prototype.toString 155 | .call(binding) 156 | .slice(8, -1)}"` 157 | ) 158 | } 159 | } 160 | 161 | function customReactive(target: object, seen = new Set()) { 162 | if (seen.has(target)) return 163 | if ( 164 | !isPlainObject(target) || 165 | isRef(target) || 166 | isReactive(target) || 167 | isRaw(target) 168 | ) 169 | return 170 | const Vue = getVueConstructor() 171 | // @ts-expect-error https://github.com/vuejs/vue/pull/12132 172 | const defineReactive = Vue.util.defineReactive 173 | 174 | Object.keys(target).forEach((k) => { 175 | const val = target[k] 176 | defineReactive(target, k, val) 177 | if (val) { 178 | seen.add(val) 179 | customReactive(val, seen) 180 | } 181 | return 182 | }) 183 | } 184 | 185 | function hasReactiveArrayChild(target: object, visited = new Map()): boolean { 186 | if (visited.has(target)) { 187 | return visited.get(target) 188 | } 189 | visited.set(target, false) 190 | if (isArray(target) && isReactive(target)) { 191 | visited.set(target, true) 192 | return true 193 | } 194 | 195 | if (!isPlainObject(target) || isRaw(target) || isRef(target)) { 196 | return false 197 | } 198 | return Object.keys(target).some((x) => 199 | hasReactiveArrayChild(target[x], visited) 200 | ) 201 | } 202 | 203 | function createSetupContext( 204 | vm: ComponentInstance & { [x: string]: any } 205 | ): SetupContext { 206 | const ctx = { slots: {} } as SetupContext 207 | 208 | const propsPlain = [ 209 | 'root', 210 | 'parent', 211 | 'refs', 212 | 'listeners', 213 | 'isServer', 214 | 'ssrContext', 215 | ] 216 | const methodReturnVoid = ['emit'] 217 | 218 | propsPlain.forEach((key) => { 219 | let srcKey = `$${key}` 220 | proxy(ctx, key, { 221 | get: () => vm[srcKey], 222 | set() { 223 | __DEV__ && 224 | warn( 225 | `Cannot assign to '${key}' because it is a read-only property`, 226 | vm 227 | ) 228 | }, 229 | }) 230 | }) 231 | 232 | updateVmAttrs(vm, ctx) 233 | 234 | methodReturnVoid.forEach((key) => { 235 | const srcKey = `$${key}` 236 | proxy(ctx, key, { 237 | get() { 238 | return (...args: any[]) => { 239 | const fn: Function = vm[srcKey] 240 | fn.apply(vm, args) 241 | } 242 | }, 243 | }) 244 | }) 245 | if (process.env.NODE_ENV === 'test') { 246 | ;(ctx as any)._vm = vm 247 | } 248 | return ctx 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/reactivity/del.ts: -------------------------------------------------------------------------------- 1 | import { AnyObject } from '../types/basic' 2 | import { getVueConstructor } from '../runtimeContext' 3 | import { 4 | hasOwn, 5 | isPrimitive, 6 | isUndef, 7 | isArray, 8 | isValidArrayIndex, 9 | } from '../utils' 10 | 11 | /** 12 | * Delete a property and trigger change if necessary. 13 | */ 14 | export function del(target: AnyObject, key: any) { 15 | const Vue = getVueConstructor() 16 | const { warn } = Vue.util 17 | 18 | if (__DEV__ && (isUndef(target) || isPrimitive(target))) { 19 | warn( 20 | `Cannot delete reactive property on undefined, null, or primitive value: ${target}` 21 | ) 22 | } 23 | if (isArray(target) && isValidArrayIndex(key)) { 24 | target.splice(key, 1) 25 | return 26 | } 27 | const ob = target.__ob__ 28 | if (target._isVue || (ob && ob.vmCount)) { 29 | __DEV__ && 30 | warn( 31 | 'Avoid deleting properties on a Vue instance or its root $data ' + 32 | '- just set it to null.' 33 | ) 34 | return 35 | } 36 | if (!hasOwn(target, key)) { 37 | return 38 | } 39 | delete target[key] 40 | if (!ob) { 41 | return 42 | } 43 | ob.dep.notify() 44 | } 45 | -------------------------------------------------------------------------------- /src/reactivity/force.ts: -------------------------------------------------------------------------------- 1 | let _isForceTrigger = false 2 | 3 | export function isForceTrigger() { 4 | return _isForceTrigger 5 | } 6 | 7 | export function setForceTrigger(v: boolean) { 8 | _isForceTrigger = v 9 | } 10 | -------------------------------------------------------------------------------- /src/reactivity/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | reactive, 3 | isReactive, 4 | markRaw, 5 | shallowReactive, 6 | toRaw, 7 | isRaw, 8 | } from './reactive' 9 | export { 10 | ref, 11 | customRef, 12 | isRef, 13 | createRef, 14 | toRefs, 15 | toRef, 16 | unref, 17 | shallowRef, 18 | triggerRef, 19 | proxyRefs, 20 | } from './ref' 21 | export { readonly, isReadonly, shallowReadonly } from './readonly' 22 | export { set } from './set' 23 | export { del } from './del' 24 | 25 | export type { 26 | Ref, 27 | ComputedRef, 28 | WritableComputedRef, 29 | ToRefs, 30 | UnwrapRef, 31 | UnwrapRefSimple, 32 | ShallowUnwrapRef, 33 | } from './ref' 34 | export type { DeepReadonly, UnwrapNestedRefs } from './readonly' 35 | -------------------------------------------------------------------------------- /src/reactivity/reactive.ts: -------------------------------------------------------------------------------- 1 | import { AnyObject } from '../types/basic' 2 | import { getRegisteredVueOrDefault } from '../runtimeContext' 3 | import { 4 | isPlainObject, 5 | def, 6 | warn, 7 | isArray, 8 | hasOwn, 9 | noopFn, 10 | isObject, 11 | proxy, 12 | } from '../utils' 13 | import { isComponentInstance, defineComponentInstance } from '../utils/helper' 14 | import { RefKey } from '../utils/symbols' 15 | import { isRef, UnwrapRef } from './ref' 16 | import { rawSet, accessModifiedSet } from '../utils/sets' 17 | import { isForceTrigger } from './force' 18 | 19 | const SKIPFLAG = '__v_skip' 20 | 21 | export function isRaw(obj: any): boolean { 22 | return Boolean( 23 | obj && 24 | hasOwn(obj, '__ob__') && 25 | typeof obj.__ob__ === 'object' && 26 | obj.__ob__?.[SKIPFLAG] 27 | ) 28 | } 29 | 30 | export function isReactive(obj: any): boolean { 31 | return Boolean( 32 | obj && 33 | hasOwn(obj, '__ob__') && 34 | typeof obj.__ob__ === 'object' && 35 | !obj.__ob__?.[SKIPFLAG] 36 | ) 37 | } 38 | 39 | /** 40 | * Proxing property access of target. 41 | * We can do unwrapping and other things here. 42 | */ 43 | function setupAccessControl(target: AnyObject): void { 44 | if ( 45 | !isPlainObject(target) || 46 | isRaw(target) || 47 | isArray(target) || 48 | isRef(target) || 49 | isComponentInstance(target) || 50 | accessModifiedSet.has(target) 51 | ) 52 | return 53 | 54 | accessModifiedSet.set(target, true) 55 | 56 | const keys = Object.keys(target) 57 | for (let i = 0; i < keys.length; i++) { 58 | defineAccessControl(target, keys[i]) 59 | } 60 | } 61 | 62 | /** 63 | * Auto unwrapping when access property 64 | */ 65 | export function defineAccessControl(target: AnyObject, key: any, val?: any) { 66 | if (key === '__ob__') return 67 | if (isRaw(target[key])) return 68 | 69 | let getter: (() => any) | undefined 70 | let setter: ((x: any) => void) | undefined 71 | const property = Object.getOwnPropertyDescriptor(target, key) 72 | if (property) { 73 | if (property.configurable === false) { 74 | return 75 | } 76 | getter = property.get 77 | setter = property.set 78 | if ( 79 | (!getter || setter) /* not only have getter */ && 80 | arguments.length === 2 81 | ) { 82 | val = target[key] 83 | } 84 | } 85 | 86 | setupAccessControl(val) 87 | proxy(target, key, { 88 | get: function getterHandler() { 89 | const value = getter ? getter.call(target) : val 90 | // if the key is equal to RefKey, skip the unwrap logic 91 | if (key !== RefKey && isRef(value)) { 92 | return value.value 93 | } else { 94 | return value 95 | } 96 | }, 97 | set: function setterHandler(newVal: any) { 98 | if (getter && !setter) return 99 | 100 | // If the key is equal to RefKey, skip the unwrap logic 101 | // If and only if "value" is ref and "newVal" is not a ref, 102 | // the assignment should be proxied to "value" ref. 103 | if (key !== RefKey && isRef(val) && !isRef(newVal)) { 104 | val.value = newVal 105 | } else if (setter) { 106 | setter.call(target, newVal) 107 | val = newVal 108 | } else { 109 | val = newVal 110 | } 111 | setupAccessControl(newVal) 112 | }, 113 | }) 114 | } 115 | 116 | export function observe(obj: T): T { 117 | const Vue = getRegisteredVueOrDefault() 118 | let observed: T 119 | if (Vue.observable) { 120 | observed = Vue.observable(obj) 121 | } else { 122 | const vm = defineComponentInstance(Vue, { 123 | data: { 124 | $$state: obj, 125 | }, 126 | }) 127 | observed = vm._data.$$state 128 | } 129 | 130 | // in SSR, there is no __ob__. Mock for reactivity check 131 | if (!hasOwn(observed, '__ob__')) { 132 | mockReactivityDeep(observed) 133 | } 134 | 135 | return observed 136 | } 137 | 138 | /** 139 | * Mock __ob__ for object recursively 140 | */ 141 | export function mockReactivityDeep(obj: any, seen = new Set()) { 142 | if (seen.has(obj) || hasOwn(obj, '__ob__') || !Object.isExtensible(obj)) 143 | return 144 | 145 | def(obj, '__ob__', mockObserver(obj)) 146 | seen.add(obj) 147 | 148 | for (const key of Object.keys(obj)) { 149 | const value = obj[key] 150 | if ( 151 | !(isPlainObject(value) || isArray(value)) || 152 | isRaw(value) || 153 | !Object.isExtensible(value) 154 | ) { 155 | continue 156 | } 157 | mockReactivityDeep(value, seen) 158 | } 159 | } 160 | 161 | function mockObserver(value: any = {}): any { 162 | return { 163 | value, 164 | dep: { 165 | notify: noopFn, 166 | depend: noopFn, 167 | addSub: noopFn, 168 | removeSub: noopFn, 169 | }, 170 | } 171 | } 172 | 173 | export function createObserver() { 174 | return observe({}).__ob__ 175 | } 176 | 177 | export function shallowReactive(obj: T): T 178 | export function shallowReactive(obj: any) { 179 | if (!isObject(obj)) { 180 | if (__DEV__) { 181 | warn('"shallowReactive()" must be called on an object.') 182 | } 183 | return obj 184 | } 185 | 186 | if ( 187 | !(isPlainObject(obj) || isArray(obj)) || 188 | isRaw(obj) || 189 | !Object.isExtensible(obj) 190 | ) { 191 | return obj 192 | } 193 | 194 | const observed = observe(isArray(obj) ? [] : {}) 195 | 196 | const ob = (observed as any).__ob__ 197 | 198 | for (const key of Object.keys(obj)) { 199 | let val = obj[key] 200 | let getter: (() => any) | undefined 201 | let setter: ((x: any) => void) | undefined 202 | const property = Object.getOwnPropertyDescriptor(obj, key) 203 | if (property) { 204 | if (property.configurable === false) { 205 | continue 206 | } 207 | getter = property.get 208 | setter = property.set 209 | } 210 | 211 | proxy(observed, key, { 212 | get: function getterHandler() { 213 | ob.dep?.depend() 214 | return val 215 | }, 216 | set: function setterHandler(newVal: any) { 217 | if (getter && !setter) return 218 | 219 | if (!isForceTrigger() && val === newVal) return 220 | if (setter) { 221 | setter.call(obj, newVal) 222 | } else { 223 | val = newVal 224 | } 225 | ob.dep?.notify() 226 | }, 227 | }) 228 | } 229 | return observed 230 | } 231 | 232 | /** 233 | * Make obj reactivity 234 | */ 235 | export function reactive(obj: T): UnwrapRef { 236 | if (!isObject(obj)) { 237 | if (__DEV__) { 238 | warn('"reactive()" must be called on an object.') 239 | } 240 | return obj 241 | } 242 | 243 | if ( 244 | !(isPlainObject(obj) || isArray(obj)) || 245 | isRaw(obj) || 246 | !Object.isExtensible(obj) 247 | ) { 248 | return obj as any 249 | } 250 | 251 | const observed = observe(obj) 252 | setupAccessControl(observed) 253 | return observed as UnwrapRef 254 | } 255 | 256 | /** 257 | * Make sure obj can't be a reactive 258 | */ 259 | export function markRaw(obj: T): T { 260 | if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) { 261 | return obj 262 | } 263 | 264 | // set the vue observable flag at obj 265 | const ob = createObserver() 266 | ob[SKIPFLAG] = true 267 | def(obj, '__ob__', ob) 268 | 269 | // mark as Raw 270 | rawSet.set(obj, true) 271 | 272 | return obj 273 | } 274 | 275 | export function toRaw(observed: T): T { 276 | if (isRaw(observed) || !Object.isExtensible(observed)) { 277 | return observed 278 | } 279 | 280 | return (observed as any)?.__ob__?.value || observed 281 | } 282 | -------------------------------------------------------------------------------- /src/reactivity/readonly.ts: -------------------------------------------------------------------------------- 1 | import { reactive, Ref, UnwrapRefSimple } from '.' 2 | import { isArray, isPlainObject, isObject, warn, proxy } from '../utils' 3 | import { readonlySet } from '../utils/sets' 4 | import { isReactive, observe } from './reactive' 5 | import { isRef, RefImpl } from './ref' 6 | 7 | export function isReadonly(obj: any): boolean { 8 | return readonlySet.has(obj) 9 | } 10 | 11 | type Primitive = string | number | boolean | bigint | symbol | undefined | null 12 | type Builtin = Primitive | Function | Date | Error | RegExp 13 | 14 | // prettier-ignore 15 | export type DeepReadonly = T extends Builtin 16 | ? T 17 | : T extends Map 18 | ? ReadonlyMap, DeepReadonly> 19 | : T extends ReadonlyMap 20 | ? ReadonlyMap, DeepReadonly> 21 | : T extends WeakMap 22 | ? WeakMap, DeepReadonly> 23 | : T extends Set 24 | ? ReadonlySet> 25 | : T extends ReadonlySet 26 | ? ReadonlySet> 27 | : T extends WeakSet 28 | ? WeakSet> 29 | : T extends Promise 30 | ? Promise> 31 | : T extends {} 32 | ? { readonly [K in keyof T]: DeepReadonly } 33 | : Readonly 34 | 35 | // only unwrap nested ref 36 | export type UnwrapNestedRefs = T extends Ref ? T : UnwrapRefSimple 37 | 38 | /** 39 | * **In @vue/composition-api, `reactive` only provides type-level readonly check** 40 | * 41 | * Creates a readonly copy of the original object. Note the returned copy is not 42 | * made reactive, but `readonly` can be called on an already reactive object. 43 | */ 44 | export function readonly( 45 | target: T 46 | ): DeepReadonly> { 47 | if (__DEV__ && !isObject(target)) { 48 | warn(`value cannot be made reactive: ${String(target)}`) 49 | } else { 50 | readonlySet.set(target, true) 51 | } 52 | return target as any 53 | } 54 | 55 | export function shallowReadonly(obj: T): Readonly 56 | export function shallowReadonly(obj: any): any { 57 | if (!isObject(obj)) { 58 | if (__DEV__) { 59 | warn(`value cannot be made reactive: ${String(obj)}`) 60 | } 61 | return obj 62 | } 63 | 64 | if ( 65 | !(isPlainObject(obj) || isArray(obj)) || 66 | (!Object.isExtensible(obj) && !isRef(obj)) 67 | ) { 68 | return obj 69 | } 70 | 71 | const readonlyObj = isRef(obj) 72 | ? new RefImpl({} as any) 73 | : isReactive(obj) 74 | ? observe({}) 75 | : {} 76 | const source = reactive({}) 77 | const ob = (source as any).__ob__ 78 | 79 | for (const key of Object.keys(obj)) { 80 | let val = obj[key] 81 | let getter: (() => any) | undefined 82 | const property = Object.getOwnPropertyDescriptor(obj, key) 83 | if (property) { 84 | if (property.configurable === false && !isRef(obj)) { 85 | continue 86 | } 87 | getter = property.get 88 | } 89 | 90 | proxy(readonlyObj, key, { 91 | get: function getterHandler() { 92 | const value = getter ? getter.call(obj) : val 93 | ob.dep.depend() 94 | return value 95 | }, 96 | set(v: any) { 97 | if (__DEV__) { 98 | warn(`Set operation on key "${key}" failed: target is readonly.`) 99 | } 100 | }, 101 | }) 102 | } 103 | 104 | readonlySet.set(readonlyObj, true) 105 | 106 | return readonlyObj 107 | } 108 | -------------------------------------------------------------------------------- /src/reactivity/ref.ts: -------------------------------------------------------------------------------- 1 | import { RefKey } from '../utils/symbols' 2 | import { proxy, isPlainObject, warn, def } from '../utils' 3 | import { reactive, isReactive, shallowReactive } from './reactive' 4 | import { readonlySet } from '../utils/sets' 5 | import { set } from './set' 6 | import { setForceTrigger } from './force' 7 | 8 | declare const _refBrand: unique symbol 9 | export interface Ref { 10 | readonly [_refBrand]: true 11 | value: T 12 | } 13 | 14 | export interface WritableComputedRef extends Ref { 15 | /** 16 | * `effect` is added to be able to differentiate refs from computed properties. 17 | * **Differently from Vue 3, it's just `true`**. This is because there is no equivalent 18 | * of `ReactiveEffect` in `@vue/composition-api`. 19 | */ 20 | effect: true 21 | } 22 | 23 | export interface ComputedRef extends WritableComputedRef { 24 | readonly value: T 25 | } 26 | 27 | export type ToRefs = { [K in keyof T]: Ref } 28 | 29 | export type CollectionTypes = IterableCollections | WeakCollections 30 | 31 | type IterableCollections = Map | Set 32 | type WeakCollections = WeakMap | WeakSet 33 | 34 | // corner case when use narrows type 35 | // Ex. type RelativePath = string & { __brand: unknown } 36 | // RelativePath extends object -> true 37 | type BaseTypes = string | number | boolean | Node | Window | Date 38 | 39 | export type ShallowUnwrapRef = { 40 | [K in keyof T]: T[K] extends Ref ? V : T[K] 41 | } 42 | 43 | export type UnwrapRef = T extends Ref 44 | ? UnwrapRefSimple 45 | : UnwrapRefSimple 46 | 47 | export type UnwrapRefSimple = T extends 48 | | Function 49 | | CollectionTypes 50 | | BaseTypes 51 | | Ref 52 | ? T 53 | : T extends Array 54 | ? { [K in keyof T]: UnwrapRefSimple } 55 | : T extends object 56 | ? { 57 | [P in keyof T]: P extends symbol ? T[P] : UnwrapRef 58 | } 59 | : T 60 | 61 | interface RefOption { 62 | get(): T 63 | set?(x: T): void 64 | } 65 | export class RefImpl implements Ref { 66 | readonly [_refBrand]!: true 67 | public value!: T 68 | constructor({ get, set }: RefOption) { 69 | proxy(this, 'value', { 70 | get, 71 | set, 72 | }) 73 | } 74 | } 75 | 76 | export function createRef( 77 | options: RefOption, 78 | isReadonly = false, 79 | isComputed = false 80 | ): RefImpl { 81 | const r = new RefImpl(options) 82 | 83 | // add effect to differentiate refs from computed 84 | if (isComputed) (r as ComputedRef).effect = true 85 | 86 | // seal the ref, this could prevent ref from being observed 87 | // It's safe to seal the ref, since we really shouldn't extend it. 88 | // related issues: #79 89 | const sealed = Object.seal(r) 90 | 91 | if (isReadonly) readonlySet.set(sealed, true) 92 | 93 | return sealed 94 | } 95 | 96 | export function ref( 97 | raw: T 98 | ): T extends Ref ? T : Ref> 99 | export function ref(raw: T): Ref> 100 | export function ref(): Ref 101 | export function ref(raw?: unknown) { 102 | if (isRef(raw)) { 103 | return raw 104 | } 105 | 106 | const value = reactive({ [RefKey]: raw }) 107 | return createRef({ 108 | get: () => value[RefKey] as any, 109 | set: (v) => ((value[RefKey] as any) = v), 110 | }) 111 | } 112 | 113 | export function isRef(value: any): value is Ref { 114 | return value instanceof RefImpl 115 | } 116 | 117 | export function unref(ref: T | Ref): T { 118 | return isRef(ref) ? (ref.value as any) : ref 119 | } 120 | 121 | export function toRefs(obj: T): ToRefs { 122 | if (__DEV__ && !isReactive(obj)) { 123 | warn(`toRefs() expects a reactive object but received a plain one.`) 124 | } 125 | if (!isPlainObject(obj)) return obj 126 | 127 | const ret: any = {} 128 | for (const key in obj) { 129 | ret[key] = toRef(obj, key) 130 | } 131 | 132 | return ret 133 | } 134 | 135 | export type CustomRefFactory = ( 136 | track: () => void, 137 | trigger: () => void 138 | ) => { 139 | get: () => T 140 | set: (value: T) => void 141 | } 142 | 143 | export function customRef(factory: CustomRefFactory): Ref { 144 | const version = ref(0) 145 | return createRef( 146 | factory( 147 | () => void version.value, 148 | () => { 149 | ++version.value 150 | } 151 | ) 152 | ) 153 | } 154 | 155 | export function toRef( 156 | object: T, 157 | key: K 158 | ): Ref { 159 | if (!(key in object)) set(object, key, undefined) 160 | const v = object[key] 161 | if (isRef(v)) return v 162 | 163 | return createRef({ 164 | get: () => object[key], 165 | set: (v) => (object[key] = v), 166 | }) 167 | } 168 | 169 | export function shallowRef( 170 | value: T 171 | ): T extends Ref ? T : Ref 172 | export function shallowRef(value: T): Ref 173 | export function shallowRef(): Ref 174 | export function shallowRef(raw?: unknown) { 175 | if (isRef(raw)) { 176 | return raw 177 | } 178 | const value = shallowReactive({ [RefKey]: raw }) 179 | return createRef({ 180 | get: () => value[RefKey] as any, 181 | set: (v) => ((value[RefKey] as any) = v), 182 | }) 183 | } 184 | 185 | export function triggerRef(value: any) { 186 | if (!isRef(value)) return 187 | 188 | setForceTrigger(true) 189 | value.value = value.value 190 | setForceTrigger(false) 191 | } 192 | 193 | export function proxyRefs( 194 | objectWithRefs: T 195 | ): ShallowUnwrapRef { 196 | if (isReactive(objectWithRefs)) { 197 | return objectWithRefs as ShallowUnwrapRef 198 | } 199 | const value: Record = reactive({ [RefKey]: objectWithRefs }) 200 | 201 | def(value, RefKey, value[RefKey], false) 202 | 203 | for (const key of Object.keys(objectWithRefs)) { 204 | proxy(value, key, { 205 | get() { 206 | if (isRef(value[RefKey][key])) { 207 | return value[RefKey][key].value 208 | } 209 | return value[RefKey][key] 210 | }, 211 | set(v: unknown) { 212 | if (isRef(value[RefKey][key])) { 213 | return (value[RefKey][key].value = unref(v)) 214 | } 215 | value[RefKey][key] = unref(v) 216 | }, 217 | }) 218 | } 219 | 220 | return value as ShallowUnwrapRef 221 | } 222 | -------------------------------------------------------------------------------- /src/reactivity/set.ts: -------------------------------------------------------------------------------- 1 | import { AnyObject } from '../types/basic' 2 | import { getVueConstructor } from '../runtimeContext' 3 | import { 4 | isArray, 5 | isPrimitive, 6 | isUndef, 7 | isValidArrayIndex, 8 | isObject, 9 | hasOwn, 10 | } from '../utils' 11 | import { defineAccessControl, mockReactivityDeep } from './reactive' 12 | 13 | /** 14 | * Set a property on an object. Adds the new property, triggers change 15 | * notification and intercept it's subsequent access if the property doesn't 16 | * already exist. 17 | */ 18 | export function set(target: AnyObject, key: any, val: T): T { 19 | const Vue = getVueConstructor() 20 | // @ts-expect-error https://github.com/vuejs/vue/pull/12132 21 | const { warn, defineReactive } = Vue.util 22 | if (__DEV__ && (isUndef(target) || isPrimitive(target))) { 23 | warn( 24 | `Cannot set reactive property on undefined, null, or primitive value: ${target}` 25 | ) 26 | } 27 | 28 | const ob = target.__ob__ 29 | 30 | function ssrMockReactivity() { 31 | // in SSR, there is no __ob__. Mock for reactivity check 32 | if (ob && isObject(val) && !hasOwn(val, '__ob__')) { 33 | mockReactivityDeep(val) 34 | } 35 | } 36 | 37 | if (isArray(target)) { 38 | if (isValidArrayIndex(key)) { 39 | target.length = Math.max(target.length, key) 40 | target.splice(key, 1, val) 41 | ssrMockReactivity() 42 | return val 43 | } else if (key === 'length' && (val as any) !== target.length) { 44 | target.length = val as any 45 | ob?.dep.notify() 46 | return val 47 | } 48 | } 49 | if (key in target && !(key in Object.prototype)) { 50 | target[key] = val 51 | ssrMockReactivity() 52 | return val 53 | } 54 | if (target._isVue || (ob && ob.vmCount)) { 55 | __DEV__ && 56 | warn( 57 | 'Avoid adding reactive properties to a Vue instance or its root $data ' + 58 | 'at runtime - declare it upfront in the data option.' 59 | ) 60 | return val 61 | } 62 | 63 | if (!ob) { 64 | target[key] = val 65 | return val 66 | } 67 | 68 | defineReactive(ob.value, key, val) 69 | // IMPORTANT: define access control before trigger watcher 70 | defineAccessControl(target, key, val) 71 | ssrMockReactivity() 72 | 73 | ob.dep.notify() 74 | return val 75 | } 76 | -------------------------------------------------------------------------------- /src/runtimeContext.ts: -------------------------------------------------------------------------------- 1 | import type { VueConstructor, VNode } from 'vue' 2 | import { bindCurrentScopeToVM, EffectScope } from './apis/effectScope' 3 | import { ComponentInstance, Data } from './component' 4 | import { 5 | assert, 6 | hasOwn, 7 | warn, 8 | proxy, 9 | UnionToIntersection, 10 | isFunction, 11 | } from './utils' 12 | import type Vue$1 from 'vue' 13 | 14 | let vueDependency: VueConstructor | undefined = undefined 15 | 16 | try { 17 | const requiredVue = require('vue') 18 | if (requiredVue && isVue(requiredVue)) { 19 | vueDependency = requiredVue 20 | } else if ( 21 | requiredVue && 22 | 'default' in requiredVue && 23 | isVue(requiredVue.default) 24 | ) { 25 | vueDependency = requiredVue.default 26 | } 27 | } catch { 28 | // not available 29 | } 30 | 31 | let vueConstructor: VueConstructor | null = null 32 | let currentInstance: ComponentInternalInstance | null = null 33 | let currentInstanceTracking = true 34 | 35 | const PluginInstalledFlag = '__composition_api_installed__' 36 | 37 | function isVue(obj: any): obj is VueConstructor { 38 | return obj && isFunction(obj) && obj.name === 'Vue' 39 | } 40 | 41 | export function isPluginInstalled() { 42 | return !!vueConstructor 43 | } 44 | 45 | export function isVueRegistered(Vue: VueConstructor) { 46 | // resolve issue: https://github.com/vuejs/composition-api/issues/876#issue-1087619365 47 | return vueConstructor && hasOwn(Vue, PluginInstalledFlag) 48 | } 49 | 50 | export function getVueConstructor(): VueConstructor { 51 | if (__DEV__) { 52 | assert( 53 | vueConstructor, 54 | `must call Vue.use(VueCompositionAPI) before using any function.` 55 | ) 56 | } 57 | 58 | return vueConstructor! 59 | } 60 | 61 | // returns registered vue or `vue` dependency 62 | export function getRegisteredVueOrDefault(): VueConstructor { 63 | let constructor = vueConstructor || vueDependency 64 | 65 | if (__DEV__) { 66 | assert(constructor, `No vue dependency found.`) 67 | } 68 | 69 | return constructor! 70 | } 71 | 72 | export function setVueConstructor(Vue: VueConstructor) { 73 | // @ts-ignore 74 | if (__DEV__ && vueConstructor && Vue.__proto__ !== vueConstructor.__proto__) { 75 | warn('[vue-composition-api] another instance of Vue installed') 76 | } 77 | vueConstructor = Vue 78 | Object.defineProperty(Vue, PluginInstalledFlag, { 79 | configurable: true, 80 | writable: true, 81 | value: true, 82 | }) 83 | } 84 | 85 | /** 86 | * For `effectScope` to create instance without populate the current instance 87 | * @internal 88 | **/ 89 | export function withCurrentInstanceTrackingDisabled(fn: () => void) { 90 | const prev = currentInstanceTracking 91 | currentInstanceTracking = false 92 | try { 93 | fn() 94 | } finally { 95 | currentInstanceTracking = prev 96 | } 97 | } 98 | 99 | export function setCurrentVue2Instance(vm: ComponentInstance | null) { 100 | if (!currentInstanceTracking) return 101 | setCurrentInstance(vm ? toVue3ComponentInstance(vm) : vm) 102 | } 103 | 104 | export function setCurrentInstance(instance: ComponentInternalInstance | null) { 105 | if (!currentInstanceTracking) return 106 | const prev = currentInstance 107 | prev?.scope.off() 108 | currentInstance = instance 109 | currentInstance?.scope.on() 110 | } 111 | 112 | export type Slot = (...args: any[]) => VNode[] 113 | 114 | export type InternalSlots = { 115 | [name: string]: Slot | undefined 116 | } 117 | 118 | export type ObjectEmitsOptions = Record< 119 | string, 120 | ((...args: any[]) => any) | null 121 | > 122 | export type EmitsOptions = ObjectEmitsOptions | string[] 123 | 124 | export type EmitFn< 125 | Options = ObjectEmitsOptions, 126 | Event extends keyof Options = keyof Options, 127 | ReturnType extends void | Vue$1 = void 128 | > = Options extends Array 129 | ? (event: V, ...args: any[]) => ReturnType 130 | : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function 131 | ? (event: string, ...args: any[]) => ReturnType 132 | : UnionToIntersection< 133 | { 134 | [key in Event]: Options[key] extends (...args: infer Args) => any 135 | ? (event: key, ...args: Args) => ReturnType 136 | : (event: key, ...args: any[]) => ReturnType 137 | }[Event] 138 | > 139 | 140 | export type ComponentRenderEmitFn< 141 | Options = ObjectEmitsOptions, 142 | Event extends keyof Options = keyof Options, 143 | T extends Vue$1 | void = void 144 | > = EmitFn 145 | 146 | export type Slots = Readonly 147 | 148 | export interface SetupContext { 149 | attrs: Data 150 | slots: Slots 151 | emit: EmitFn 152 | /** 153 | * @deprecated not available in Vue 2 154 | */ 155 | expose: (exposed?: Record) => void 156 | 157 | /** 158 | * @deprecated not available in Vue 3 159 | */ 160 | readonly parent: ComponentInstance | null 161 | 162 | /** 163 | * @deprecated not available in Vue 3 164 | */ 165 | readonly root: ComponentInstance 166 | 167 | /** 168 | * @deprecated not available in Vue 3 169 | */ 170 | readonly listeners: { [key in string]?: Function } 171 | 172 | /** 173 | * @deprecated not available in Vue 3 174 | */ 175 | readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] } 176 | } 177 | 178 | export interface ComponentPublicInstance {} 179 | 180 | /** 181 | * We expose a subset of properties on the internal instance as they are 182 | * useful for advanced external libraries and tools. 183 | */ 184 | export declare interface ComponentInternalInstance { 185 | uid: number 186 | type: Record // ConcreteComponent 187 | parent: ComponentInternalInstance | null 188 | root: ComponentInternalInstance 189 | 190 | //appContext: AppContext 191 | 192 | /** 193 | * Vnode representing this component in its parent's vdom tree 194 | */ 195 | vnode: VNode 196 | /** 197 | * Root vnode of this component's own vdom tree 198 | */ 199 | // subTree: VNode // does not exist in Vue 2 200 | 201 | /** 202 | * The reactive effect for rendering and patching the component. Callable. 203 | */ 204 | update: Function 205 | 206 | data: Data 207 | props: Data 208 | attrs: Data 209 | refs: Data 210 | emit: EmitFn 211 | 212 | slots: InternalSlots 213 | emitted: Record | null 214 | 215 | proxy: ComponentInstance 216 | 217 | isMounted: boolean 218 | isUnmounted: boolean 219 | isDeactivated: boolean 220 | 221 | /** 222 | * @internal 223 | */ 224 | scope: EffectScope 225 | 226 | /** 227 | * @internal 228 | */ 229 | setupContext: SetupContext | null 230 | } 231 | 232 | export function getCurrentInstance() { 233 | return currentInstance 234 | } 235 | 236 | const instanceMapCache = new WeakMap< 237 | ComponentInstance, 238 | ComponentInternalInstance 239 | >() 240 | 241 | export function toVue3ComponentInstance( 242 | vm: ComponentInstance 243 | ): ComponentInternalInstance { 244 | if (instanceMapCache.has(vm)) { 245 | return instanceMapCache.get(vm)! 246 | } 247 | 248 | const instance: ComponentInternalInstance = { 249 | proxy: vm, 250 | update: vm.$forceUpdate, 251 | type: vm.$options, 252 | uid: vm._uid, 253 | 254 | // $emit is defined on prototype and it expected to be bound 255 | emit: vm.$emit.bind(vm), 256 | 257 | parent: null, 258 | root: null!, // to be immediately set 259 | } as unknown as ComponentInternalInstance 260 | 261 | bindCurrentScopeToVM(instance) 262 | 263 | // map vm.$props = 264 | const instanceProps = [ 265 | 'data', 266 | 'props', 267 | 'attrs', 268 | 'refs', 269 | 'vnode', 270 | 'slots', 271 | ] as const 272 | 273 | instanceProps.forEach((prop) => { 274 | proxy(instance, prop, { 275 | get() { 276 | return (vm as any)[`$${prop}`] 277 | }, 278 | }) 279 | }) 280 | 281 | proxy(instance, 'isMounted', { 282 | get() { 283 | // @ts-expect-error private api 284 | return vm._isMounted 285 | }, 286 | }) 287 | 288 | proxy(instance, 'isUnmounted', { 289 | get() { 290 | // @ts-expect-error private api 291 | return vm._isDestroyed 292 | }, 293 | }) 294 | 295 | proxy(instance, 'isDeactivated', { 296 | get() { 297 | // @ts-expect-error private api 298 | return vm._inactive 299 | }, 300 | }) 301 | 302 | proxy(instance, 'emitted', { 303 | get() { 304 | // @ts-expect-error private api 305 | return vm._events 306 | }, 307 | }) 308 | 309 | instanceMapCache.set(vm, instance) 310 | 311 | if (vm.$parent) { 312 | instance.parent = toVue3ComponentInstance(vm.$parent) 313 | } 314 | 315 | if (vm.$root) { 316 | instance.root = toVue3ComponentInstance(vm.$root) 317 | } 318 | 319 | return instance 320 | } 321 | -------------------------------------------------------------------------------- /src/types/basic.ts: -------------------------------------------------------------------------------- 1 | export type AnyObject = Record 2 | 3 | // Conditional returns can enforce identical types. 4 | // See here: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650 5 | // prettier-ignore 6 | type Equal = 7 | (() => U extends Left ? 1 : 0) extends (() => U extends Right ? 1 : 0) ? true : false; 8 | 9 | export type HasDefined = Equal extends true ? false : true 10 | -------------------------------------------------------------------------------- /src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode, ComponentOptions, VueConstructor } from 'vue' 2 | import { ComponentInstance } from '../component' 3 | import { 4 | ComponentInternalInstance, 5 | getCurrentInstance, 6 | getVueConstructor, 7 | Slot, 8 | } from '../runtimeContext' 9 | import { warn } from './utils' 10 | 11 | export function getCurrentInstanceForFn( 12 | hook: string, 13 | target?: ComponentInternalInstance | null 14 | ): ComponentInternalInstance | null { 15 | target = target || getCurrentInstance() 16 | if (__DEV__ && !target) { 17 | warn( 18 | `${hook} is called when there is no active component instance to be ` + 19 | `associated with. ` + 20 | `Lifecycle injection APIs can only be used during execution of setup().` 21 | ) 22 | } 23 | return target 24 | } 25 | 26 | export function defineComponentInstance( 27 | Ctor: VueConstructor, 28 | options: ComponentOptions = {} 29 | ) { 30 | const silent = Ctor.config.silent 31 | Ctor.config.silent = true 32 | const vm = new Ctor(options) 33 | Ctor.config.silent = silent 34 | return vm 35 | } 36 | 37 | export function isComponentInstance(obj: any) { 38 | const Vue = getVueConstructor() 39 | return Vue && obj instanceof Vue 40 | } 41 | 42 | export function createSlotProxy(vm: ComponentInstance, slotName: string): Slot { 43 | return ((...args: any) => { 44 | if (!vm.$scopedSlots[slotName]) { 45 | if (__DEV__) 46 | return warn( 47 | `slots.${slotName}() got called outside of the "render()" scope`, 48 | vm 49 | ) 50 | return 51 | } 52 | 53 | return vm.$scopedSlots[slotName]!.apply(vm, args) 54 | }) as Slot 55 | } 56 | 57 | export function resolveSlots( 58 | slots: { [key: string]: Function } | void, 59 | normalSlots: { [key: string]: VNode[] | undefined } 60 | ): { [key: string]: true } { 61 | let res: { [key: string]: true } 62 | if (!slots) { 63 | res = {} 64 | } else if (slots._normalized) { 65 | // fast path 1: child component re-render only, parent did not change 66 | return slots._normalized as any 67 | } else { 68 | res = {} 69 | for (const key in slots) { 70 | if (slots[key] && key[0] !== '$') { 71 | res[key] = true 72 | } 73 | } 74 | } 75 | 76 | // expose normal slots on scopedSlots 77 | for (const key in normalSlots) { 78 | if (!(key in res)) { 79 | res[key] = true 80 | } 81 | } 82 | 83 | return res 84 | } 85 | 86 | let vueInternalClasses: 87 | | { 88 | Watcher: any 89 | Dep: any 90 | } 91 | | undefined 92 | 93 | export const getVueInternalClasses = () => { 94 | if (!vueInternalClasses) { 95 | const vm: any = defineComponentInstance(getVueConstructor(), { 96 | computed: { 97 | value() { 98 | return 0 99 | }, 100 | }, 101 | }) 102 | 103 | // to get Watcher class 104 | const Watcher = vm._computedWatchers.value.constructor 105 | // to get Dep class 106 | const Dep = vm._data.__ob__.dep.constructor 107 | 108 | vueInternalClasses = { 109 | Watcher, 110 | Dep, 111 | } 112 | 113 | vm.$destroy() 114 | } 115 | 116 | return vueInternalClasses 117 | } 118 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils' 2 | export * from './helper' 3 | export * from './typeutils' 4 | -------------------------------------------------------------------------------- /src/utils/instance.ts: -------------------------------------------------------------------------------- 1 | import type { VNode } from 'vue' 2 | import { ComponentInstance } from '../component' 3 | import vmStateManager from './vmStateManager' 4 | import { 5 | setCurrentInstance, 6 | getCurrentInstance, 7 | ComponentInternalInstance, 8 | InternalSlots, 9 | SetupContext, 10 | } from '../runtimeContext' 11 | import { Ref, isRef, isReactive } from '../apis' 12 | import { hasOwn, proxy, warn } from './utils' 13 | import { createSlotProxy, resolveSlots } from './helper' 14 | import { reactive } from '../reactivity/reactive' 15 | 16 | export function asVmProperty( 17 | vm: ComponentInstance, 18 | propName: string, 19 | propValue: Ref 20 | ) { 21 | const props = vm.$options.props 22 | if (!(propName in vm) && !(props && hasOwn(props, propName))) { 23 | if (isRef(propValue)) { 24 | proxy(vm, propName, { 25 | get: () => propValue.value, 26 | set: (val: unknown) => { 27 | propValue.value = val 28 | }, 29 | }) 30 | } else { 31 | proxy(vm, propName, { 32 | get: () => { 33 | if (isReactive(propValue)) { 34 | ;(propValue as any).__ob__.dep.depend() 35 | } 36 | return propValue 37 | }, 38 | set: (val: any) => { 39 | propValue = val 40 | }, 41 | }) 42 | } 43 | 44 | if (__DEV__) { 45 | // expose binding to Vue Devtool as a data property 46 | // delay this until state has been resolved to prevent repeated works 47 | vm.$nextTick(() => { 48 | if (Object.keys(vm._data).indexOf(propName) !== -1) { 49 | return 50 | } 51 | if (isRef(propValue)) { 52 | proxy(vm._data, propName, { 53 | get: () => propValue.value, 54 | set: (val: unknown) => { 55 | propValue.value = val 56 | }, 57 | }) 58 | } else { 59 | proxy(vm._data, propName, { 60 | get: () => propValue, 61 | set: (val: any) => { 62 | propValue = val 63 | }, 64 | }) 65 | } 66 | }) 67 | } 68 | } else if (__DEV__) { 69 | if (props && hasOwn(props, propName)) { 70 | warn( 71 | `The setup binding property "${propName}" is already declared as a prop.`, 72 | vm 73 | ) 74 | } else { 75 | warn(`The setup binding property "${propName}" is already declared.`, vm) 76 | } 77 | } 78 | } 79 | 80 | function updateTemplateRef(vm: ComponentInstance) { 81 | const rawBindings = vmStateManager.get(vm, 'rawBindings') || {} 82 | if (!rawBindings || !Object.keys(rawBindings).length) return 83 | 84 | const refs = vm.$refs 85 | const oldRefKeys = vmStateManager.get(vm, 'refs') || [] 86 | for (let index = 0; index < oldRefKeys.length; index++) { 87 | const key = oldRefKeys[index] 88 | const setupValue = rawBindings[key] 89 | if (!refs[key] && setupValue && isRef(setupValue)) { 90 | setupValue.value = null 91 | } 92 | } 93 | 94 | const newKeys = Object.keys(refs) 95 | const validNewKeys = [] 96 | for (let index = 0; index < newKeys.length; index++) { 97 | const key = newKeys[index] 98 | const setupValue = rawBindings[key] 99 | if (refs[key] && setupValue && isRef(setupValue)) { 100 | setupValue.value = refs[key] 101 | validNewKeys.push(key) 102 | } 103 | } 104 | vmStateManager.set(vm, 'refs', validNewKeys) 105 | } 106 | 107 | export function afterRender(vm: ComponentInstance) { 108 | const stack = [(vm as any)._vnode as VNode] 109 | while (stack.length) { 110 | const vnode = stack.pop() 111 | if (vnode) { 112 | if (vnode.context) updateTemplateRef(vnode.context) 113 | if (vnode.children) { 114 | for (let i = 0; i < vnode.children.length; ++i) { 115 | stack.push(vnode.children[i]) 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | export function updateVmAttrs(vm: ComponentInstance, ctx?: SetupContext) { 123 | if (!vm) { 124 | return 125 | } 126 | let attrBindings = vmStateManager.get(vm, 'attrBindings') 127 | if (!attrBindings && !ctx) { 128 | // fix 840 129 | return 130 | } 131 | if (!attrBindings) { 132 | const observedData = reactive({}) 133 | attrBindings = { ctx: ctx!, data: observedData } 134 | vmStateManager.set(vm, 'attrBindings', attrBindings) 135 | proxy(ctx, 'attrs', { 136 | get: () => { 137 | return attrBindings?.data 138 | }, 139 | set() { 140 | __DEV__ && 141 | warn( 142 | `Cannot assign to '$attrs' because it is a read-only property`, 143 | vm 144 | ) 145 | }, 146 | }) 147 | } 148 | 149 | const source = vm.$attrs 150 | for (const attr of Object.keys(source)) { 151 | if (!hasOwn(attrBindings.data, attr)) { 152 | proxy(attrBindings.data, attr, { 153 | get: () => { 154 | // to ensure it always return the latest value 155 | return vm.$attrs[attr] 156 | }, 157 | }) 158 | } 159 | } 160 | } 161 | 162 | export function resolveScopedSlots( 163 | vm: ComponentInstance, 164 | slotsProxy: InternalSlots 165 | ): void { 166 | const parentVNode = (vm.$options as any)._parentVnode 167 | if (!parentVNode) return 168 | 169 | const prevSlots = vmStateManager.get(vm, 'slots') || [] 170 | const curSlots = resolveSlots(parentVNode.data.scopedSlots, vm.$slots) 171 | // remove staled slots 172 | for (let index = 0; index < prevSlots.length; index++) { 173 | const key = prevSlots[index] 174 | if (!curSlots[key]) { 175 | delete slotsProxy[key] 176 | } 177 | } 178 | 179 | // proxy fresh slots 180 | const slotNames = Object.keys(curSlots) 181 | for (let index = 0; index < slotNames.length; index++) { 182 | const key = slotNames[index] 183 | if (!slotsProxy[key]) { 184 | slotsProxy[key] = createSlotProxy(vm, key) 185 | } 186 | } 187 | vmStateManager.set(vm, 'slots', slotNames) 188 | } 189 | 190 | export function activateCurrentInstance( 191 | instance: ComponentInternalInstance, 192 | fn: (instance: ComponentInternalInstance) => any, 193 | onError?: (err: Error) => void 194 | ) { 195 | let preVm = getCurrentInstance() 196 | setCurrentInstance(instance) 197 | try { 198 | return fn(instance) 199 | } catch ( 200 | // FIXME: remove any 201 | err: any 202 | ) { 203 | if (onError) { 204 | onError(err) 205 | } else { 206 | throw err 207 | } 208 | } finally { 209 | setCurrentInstance(preVm) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/utils/sets.ts: -------------------------------------------------------------------------------- 1 | export const accessModifiedSet = new WeakMap() 2 | export const rawSet = new WeakMap() 3 | export const readonlySet = new WeakMap() 4 | -------------------------------------------------------------------------------- /src/utils/symbols.ts: -------------------------------------------------------------------------------- 1 | import { hasSymbol } from './utils' 2 | 3 | function createSymbol(name: string): string { 4 | return hasSymbol ? (Symbol.for(name) as any) : name 5 | } 6 | 7 | export const WatcherPreFlushQueueKey = createSymbol( 8 | 'composition-api.preFlushQueue' 9 | ) 10 | export const WatcherPostFlushQueueKey = createSymbol( 11 | 'composition-api.postFlushQueue' 12 | ) 13 | 14 | // must be a string, symbol key is ignored in reactive 15 | export const RefKey = 'composition-api.refKey' 16 | -------------------------------------------------------------------------------- /src/utils/typeutils.ts: -------------------------------------------------------------------------------- 1 | export type UnionToIntersection = ( 2 | U extends any ? (k: U) => void : never 3 | ) extends (k: infer I) => void 4 | ? I 5 | : never 6 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { getRegisteredVueOrDefault } from '../runtimeContext' 2 | 3 | const toString = (x: any) => Object.prototype.toString.call(x) 4 | 5 | export function isNative(Ctor: any): boolean { 6 | return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) 7 | } 8 | 9 | export const hasSymbol = 10 | typeof Symbol !== 'undefined' && 11 | isNative(Symbol) && 12 | typeof Reflect !== 'undefined' && 13 | isNative(Reflect.ownKeys) 14 | 15 | export const noopFn: any = (_: any) => _ 16 | 17 | export function proxy( 18 | target: any, 19 | key: string, 20 | { get, set }: { get?: Function; set?: Function } 21 | ) { 22 | Object.defineProperty(target, key, { 23 | enumerable: true, 24 | configurable: true, 25 | get: get || noopFn, 26 | set: set || noopFn, 27 | }) 28 | } 29 | 30 | export function def(obj: Object, key: string, val: any, enumerable?: boolean) { 31 | Object.defineProperty(obj, key, { 32 | value: val, 33 | enumerable: !!enumerable, 34 | writable: true, 35 | configurable: true, 36 | }) 37 | } 38 | 39 | export function hasOwn(obj: Object, key: PropertyKey): boolean { 40 | return Object.hasOwnProperty.call(obj, key) 41 | } 42 | 43 | export function assert(condition: any, msg: string) { 44 | if (!condition) { 45 | throw new Error(`[vue-composition-api] ${msg}`) 46 | } 47 | } 48 | 49 | export function isPrimitive(value: any): boolean { 50 | return ( 51 | typeof value === 'string' || 52 | typeof value === 'number' || 53 | // $flow-disable-line 54 | typeof value === 'symbol' || 55 | typeof value === 'boolean' 56 | ) 57 | } 58 | 59 | export function isArray(x: unknown): x is T[] { 60 | return Array.isArray(x) 61 | } 62 | 63 | export const objectToString = Object.prototype.toString 64 | 65 | export const toTypeString = (value: unknown): string => 66 | objectToString.call(value) 67 | 68 | export const isMap = (val: unknown): val is Map => 69 | toTypeString(val) === '[object Map]' 70 | 71 | export const isSet = (val: unknown): val is Set => 72 | toTypeString(val) === '[object Set]' 73 | 74 | const MAX_VALID_ARRAY_LENGTH = 4294967295 // Math.pow(2, 32) - 1 75 | export function isValidArrayIndex(val: any): boolean { 76 | const n = parseFloat(String(val)) 77 | return ( 78 | n >= 0 && 79 | Math.floor(n) === n && 80 | isFinite(val) && 81 | n <= MAX_VALID_ARRAY_LENGTH 82 | ) 83 | } 84 | 85 | export function isObject(val: unknown): val is Record { 86 | return val !== null && typeof val === 'object' 87 | } 88 | 89 | export function isPlainObject(x: unknown): x is Record { 90 | return toString(x) === '[object Object]' 91 | } 92 | 93 | export function isFunction(x: unknown): x is Function { 94 | return typeof x === 'function' 95 | } 96 | 97 | export function isUndef(v: any): boolean { 98 | return v === undefined || v === null 99 | } 100 | 101 | export function warn(msg: string, vm?: Vue) { 102 | const Vue = getRegisteredVueOrDefault() 103 | if (!Vue || !Vue.util) console.warn(`[vue-composition-api] ${msg}`) 104 | else Vue.util.warn(msg, vm) 105 | } 106 | 107 | export function logError(err: Error, vm: Vue, info: string) { 108 | if (__DEV__) { 109 | warn(`Error in ${info}: "${err.toString()}"`, vm) 110 | } 111 | if (typeof window !== 'undefined' && typeof console !== 'undefined') { 112 | console.error(err) 113 | } else { 114 | throw err 115 | } 116 | } 117 | 118 | /** 119 | * Object.is polyfill 120 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is 121 | * */ 122 | export function isSame(value1: any, value2: any): boolean { 123 | if (value1 === value2) { 124 | return value1 !== 0 || 1 / value1 === 1 / value2 125 | } else { 126 | return value1 !== value1 && value2 !== value2 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/utils/vmStateManager.ts: -------------------------------------------------------------------------------- 1 | import { ComponentInstance, Data } from '../component' 2 | import { SetupContext } from '../runtimeContext' 3 | 4 | export interface VfaState { 5 | refs?: string[] 6 | rawBindings?: Data 7 | attrBindings?: { 8 | ctx: SetupContext 9 | data: Data 10 | } 11 | slots?: string[] 12 | } 13 | 14 | function set( 15 | vm: ComponentInstance, 16 | key: K, 17 | value: VfaState[K] 18 | ): void { 19 | const state = (vm.__composition_api_state__ = 20 | vm.__composition_api_state__ || {}) 21 | state[key] = value 22 | } 23 | 24 | function get( 25 | vm: ComponentInstance, 26 | key: K 27 | ): VfaState[K] | undefined { 28 | return (vm.__composition_api_state__ || {})[key] 29 | } 30 | 31 | export default { 32 | set, 33 | get, 34 | } 35 | -------------------------------------------------------------------------------- /test-dts/defineAsyncComponent.test-d.tsx: -------------------------------------------------------------------------------- 1 | import { defineAsyncComponent, defineComponent, expectType, h } from './index' 2 | 3 | const asyncComponent1 = async () => defineComponent({}) 4 | 5 | const asyncComponent2 = async () => ({ template: 'ASYNC' }) 6 | 7 | const syncComponent1 = defineComponent({ 8 | template: '', 9 | }) 10 | 11 | const syncComponent2 = { 12 | template: '', 13 | } 14 | 15 | defineAsyncComponent(asyncComponent1) 16 | defineAsyncComponent(asyncComponent2) 17 | 18 | defineAsyncComponent({ 19 | loader: asyncComponent1, 20 | delay: 200, 21 | timeout: 3000, 22 | errorComponent: syncComponent1, 23 | loadingComponent: syncComponent1, 24 | }) 25 | 26 | defineAsyncComponent({ 27 | loader: asyncComponent2, 28 | delay: 200, 29 | timeout: 3000, 30 | errorComponent: syncComponent2, 31 | loadingComponent: syncComponent2, 32 | }) 33 | 34 | defineAsyncComponent(async () => syncComponent1) 35 | 36 | defineAsyncComponent(async () => syncComponent2) 37 | 38 | const component = defineAsyncComponent({ 39 | loader: asyncComponent1, 40 | loadingComponent: defineComponent({}), 41 | errorComponent: defineComponent({}), 42 | delay: 200, 43 | timeout: 3000, 44 | suspensible: false, 45 | onError(error, retry, fail, attempts) { 46 | expectType<() => void>(retry) 47 | expectType<() => void>(fail) 48 | expectType(attempts) 49 | expectType(error) 50 | }, 51 | }) 52 | 53 | h(component) 54 | -------------------------------------------------------------------------------- /test-dts/defineComponent-vue2.d.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | defineComponent, 3 | describe, 4 | expectError, 5 | expectType, 6 | Ref, 7 | ref, 8 | } from './index' 9 | import Vue from 'vue' 10 | 11 | describe('emits', () => { 12 | const testComponent = defineComponent({ 13 | emits: { 14 | click: (n: number) => typeof n === 'number', 15 | input: (b: string) => b.length > 1, 16 | }, 17 | setup(props, { emit }) { 18 | emit('click', 1) 19 | emit('input', 'foo') 20 | }, 21 | created() { 22 | this.$emit('click', 1) 23 | this.$emit('click', 1).$emit('click', 1) 24 | this.$emit('input', 'foo') 25 | this.$emit('input', 'foo').$emit('click', 1) 26 | expectType>(this.$attrs) 27 | // @ts-expect-error 28 | expectError(this.$emit('input', 1).$emit('nope')) 29 | }, 30 | }) 31 | 32 | // interface of vue2's $emit has no generics, notice that untyped types will be "event: string, ...args: any[]) => this" when using vue-class-component. 33 | // but we can get correct type when we use correct params 34 | // maybe we need vue 2.7 to fully support emit type 35 | type VueClass = { new (...args: any[]): V & Vue } & typeof Vue 36 | 37 | function useComponentRef>() { 38 | return ref>(undefined!) as Ref> 39 | } 40 | 41 | const foo = useComponentRef() 42 | 43 | foo.value.$emit('click', 1) 44 | foo.value.$emit('input', 'foo') 45 | foo.value.$emit('click', 1).$emit('click', 1) 46 | // @ts-expect-error 47 | expectError(foo.value.$emit('blah').$emit('click', 1)) 48 | // @ts-expect-error 49 | expectError(foo.value.$emit('click').$emit('click', 1)) 50 | // @ts-expect-error 51 | expectError(foo.value.$emit('blah').$emit('click', 1)) 52 | // @ts-expect-error 53 | expectError(foo.value.$emit('blah')) 54 | }) 55 | -------------------------------------------------------------------------------- /test-dts/index.d.ts: -------------------------------------------------------------------------------- 1 | import type {} from '@vue/runtime-dom' 2 | export * from '@vue/composition-api' 3 | // export * from 'vue3' 4 | 5 | export function describe(_name: string, _fn: () => void): void 6 | 7 | export function expectType(value: T): void 8 | export function expectError(value: T): void 9 | export function expectAssignable(value: T2): void 10 | 11 | // https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360 12 | type IfNotAny = 0 extends 1 & T ? never : T 13 | type IfNotUndefined = Exclude extends never ? never : T 14 | export function isNotAnyOrUndefined(value: IfNotAny>): void 15 | 16 | export type IsUnion = ( 17 | T extends any ? (U extends T ? false : true) : never 18 | ) extends false 19 | ? false 20 | : true 21 | -------------------------------------------------------------------------------- /test-dts/readonly.test-d.tsx: -------------------------------------------------------------------------------- 1 | import { expectType, readonly, ref } from './index' 2 | 3 | describe('readonly', () => { 4 | it('nested', () => { 5 | const r = readonly({ 6 | obj: { k: 'v' }, 7 | arr: [1, 2, '3'], 8 | objInArr: [{ foo: 'bar' }], 9 | }) 10 | 11 | // @ts-expect-error 12 | r.obj = {} 13 | // @ts-expect-error 14 | r.obj.k = 'x' 15 | 16 | // @ts-expect-error 17 | r.arr.push(42) 18 | // @ts-expect-error 19 | r.objInArr[0].foo = 'bar2' 20 | }) 21 | 22 | it('with ref', () => { 23 | const r = readonly( 24 | ref({ 25 | obj: { k: 'v' }, 26 | arr: [1, 2, '3'], 27 | objInArr: [{ foo: 'bar' }], 28 | }) 29 | ) 30 | 31 | console.log(r.value) 32 | 33 | expectType(r.value.obj.k) 34 | 35 | // @ts-expect-error 36 | r.value = {} 37 | 38 | // @ts-expect-error 39 | r.value.obj = {} 40 | // @ts-expect-error 41 | r.value.obj.k = 'x' 42 | 43 | // @ts-expect-error 44 | r.value.arr.push(42) 45 | // @ts-expect-error 46 | r.value.objInArr[0].foo = 'bar2' 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test-dts/ref.test-d.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Ref, 3 | ref, 4 | shallowRef, 5 | isRef, 6 | unref, 7 | reactive, 8 | expectType, 9 | } from './index' 10 | 11 | function plainType(arg: number | Ref) { 12 | // ref coercing 13 | const coerced = ref(arg) 14 | expectType>(coerced) 15 | 16 | // isRef as type guard 17 | if (isRef(arg)) { 18 | expectType>(arg) 19 | } 20 | 21 | // ref unwrapping 22 | expectType(unref(arg)) 23 | 24 | // ref inner type should be unwrapped 25 | const nestedRef = ref({ 26 | foo: ref(1), 27 | }) 28 | expectType>(nestedRef) 29 | expectType<{ foo: number }>(nestedRef.value) 30 | 31 | // ref boolean 32 | const falseRef = ref(false) 33 | expectType>(falseRef) 34 | expectType(falseRef.value) 35 | 36 | // ref true 37 | const trueRef = ref(true) 38 | expectType>(trueRef) 39 | expectType(trueRef.value) 40 | 41 | // tuple 42 | expectType<[number, string]>(unref(ref([1, '1']))) 43 | 44 | interface IteratorFoo { 45 | [Symbol.iterator]: any 46 | } 47 | 48 | // with symbol 49 | expectType>( 50 | ref() 51 | ) 52 | } 53 | 54 | plainType(1) 55 | 56 | function bailType(arg: HTMLElement | Ref) { 57 | // ref coercing 58 | const coerced = ref(arg) 59 | expectType>(coerced) 60 | 61 | // isRef as type guard 62 | if (isRef(arg)) { 63 | expectType>(arg) 64 | } 65 | 66 | // ref unwrapping 67 | expectType(unref(arg)) 68 | 69 | // ref inner type should be unwrapped 70 | const nestedRef = ref({ foo: ref(document.createElement('DIV')) }) 71 | 72 | expectType>(nestedRef) 73 | expectType<{ foo: HTMLElement }>(nestedRef.value) 74 | } 75 | const el = document.createElement('DIV') 76 | bailType(el) 77 | 78 | function withSymbol() { 79 | const customSymbol = Symbol() 80 | const obj = { 81 | [Symbol.asyncIterator]: ref(1), 82 | [Symbol.hasInstance]: { a: ref('a') }, 83 | [Symbol.isConcatSpreadable]: { b: ref(true) }, 84 | [Symbol.iterator]: [ref(1)], 85 | [Symbol.match]: new Set>(), 86 | [Symbol.matchAll]: new Map>(), 87 | [Symbol.replace]: { arr: [ref('a')] }, 88 | [Symbol.search]: { set: new Set>() }, 89 | [Symbol.species]: { map: new Map>() }, 90 | [Symbol.split]: new WeakSet>(), 91 | [Symbol.toPrimitive]: new WeakMap, string>(), 92 | [Symbol.toStringTag]: { weakSet: new WeakSet>() }, 93 | [Symbol.unscopables]: { weakMap: new WeakMap, string>() }, 94 | [customSymbol]: { arr: [ref(1)] }, 95 | } 96 | 97 | const objRef = ref(obj) 98 | 99 | expectType>(objRef.value[Symbol.asyncIterator]) 100 | expectType<{ a: Ref }>(objRef.value[Symbol.hasInstance]) 101 | expectType<{ b: Ref }>(objRef.value[Symbol.isConcatSpreadable]) 102 | expectType[]>(objRef.value[Symbol.iterator]) 103 | expectType>>(objRef.value[Symbol.match]) 104 | expectType>>(objRef.value[Symbol.matchAll]) 105 | expectType<{ arr: Ref[] }>(objRef.value[Symbol.replace]) 106 | expectType<{ set: Set> }>(objRef.value[Symbol.search]) 107 | expectType<{ map: Map> }>(objRef.value[Symbol.species]) 108 | expectType>>(objRef.value[Symbol.split]) 109 | expectType, string>>(objRef.value[Symbol.toPrimitive]) 110 | expectType<{ weakSet: WeakSet> }>( 111 | objRef.value[Symbol.toStringTag] 112 | ) 113 | expectType<{ weakMap: WeakMap, string> }>( 114 | objRef.value[Symbol.unscopables] 115 | ) 116 | expectType<{ arr: Ref[] }>(objRef.value[customSymbol]) 117 | } 118 | 119 | withSymbol() 120 | 121 | const state = reactive({ 122 | foo: { 123 | value: 1, 124 | label: 'bar', 125 | }, 126 | }) 127 | 128 | expectType(state.foo.label) 129 | 130 | type Status = 'initial' | 'ready' | 'invalidating' 131 | const shallowStatus = shallowRef('initial') 132 | if (shallowStatus.value === 'initial') { 133 | expectType>(shallowStatus) 134 | expectType(shallowStatus.value) 135 | shallowStatus.value = 'invalidating' 136 | } 137 | 138 | const refStatus = ref('initial') 139 | if (refStatus.value === 'initial') { 140 | expectType>(shallowStatus) 141 | expectType(shallowStatus.value) 142 | refStatus.value = 'invalidating' 143 | } 144 | -------------------------------------------------------------------------------- /test-dts/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "@vue/composition-api": ["../dist/vue-composition-api.d.ts"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test-dts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "declaration": true, 6 | "baseUrl": ".", 7 | "jsx": "preserve", 8 | "paths": { 9 | "@vue/composition-api": ["../src"] 10 | } 11 | }, 12 | "exclude": ["../test"], 13 | "include": ["../src", "./*.ts", "./*.tsx"] 14 | } 15 | -------------------------------------------------------------------------------- /test-dts/tsconfig.vue3.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "@vue/composition-api": ["../node_modules/vue3/dist/vue.d.ts"] 6 | } 7 | }, 8 | "exclude": ["./defineComponent-vue2.d.tsx"] 9 | } 10 | -------------------------------------------------------------------------------- /test-dts/watch.test-d.tsx: -------------------------------------------------------------------------------- 1 | import { ref, computed, watch, expectType } from './index' 2 | 3 | const source = ref('foo') 4 | const source2 = computed(() => source.value) 5 | const source3 = () => 1 6 | 7 | // lazy watcher will have consistent types for oldValue. 8 | watch(source, (value, oldValue) => { 9 | expectType(value) 10 | expectType(oldValue) 11 | }) 12 | 13 | // spread array 14 | watch( 15 | [source, source2, source3], 16 | ([source1, source2, source3], [oldSource1, oldSource2, oldSource3]) => { 17 | expectType(source1) 18 | expectType(source2) 19 | expectType(source3) 20 | expectType(oldSource1) 21 | expectType(oldSource2) 22 | expectType(oldSource3) 23 | } 24 | ) 25 | 26 | // const array 27 | watch([source, source2, source3] as const, (values, oldValues) => { 28 | expectType>(values) 29 | expectType>(oldValues) 30 | expectType(values[0]) 31 | expectType(values[1]) 32 | expectType(values[2]) 33 | expectType(oldValues[0]) 34 | expectType(oldValues[1]) 35 | expectType(oldValues[2]) 36 | }) 37 | 38 | // const spread array 39 | watch( 40 | [source, source2, source3] as const, 41 | ([source1, source2, source3], [oldSource1, oldSource2, oldSource3]) => { 42 | expectType(source1) 43 | expectType(source2) 44 | expectType(source3) 45 | expectType(oldSource1) 46 | expectType(oldSource2) 47 | expectType(oldSource3) 48 | } 49 | ) 50 | 51 | // immediate watcher's oldValue will be undefined on first run. 52 | watch( 53 | source, 54 | (value, oldValue) => { 55 | expectType(value) 56 | expectType(oldValue) 57 | }, 58 | { immediate: true } 59 | ) 60 | 61 | watch( 62 | [source, source2, source3], 63 | (values, oldValues) => { 64 | expectType<(string | number)[]>(values) 65 | expectType<(string | number | undefined)[]>(oldValues) 66 | }, 67 | { immediate: true } 68 | ) 69 | 70 | // const array 71 | watch( 72 | [source, source2, source3] as const, 73 | (values, oldValues) => { 74 | expectType>(values) 75 | expectType< 76 | Readonly<[string | undefined, string | undefined, number | undefined]> 77 | >(oldValues) 78 | }, 79 | { immediate: true } 80 | ) 81 | 82 | // should provide correct ref.value inner type to callbacks 83 | const nestedRefSource = ref({ 84 | foo: ref(1), 85 | }) 86 | 87 | watch(nestedRefSource, (v, ov) => { 88 | expectType<{ foo: number }>(v) 89 | expectType<{ foo: number }>(ov) 90 | }) 91 | -------------------------------------------------------------------------------- /test/apis/computed.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.common.js' 2 | import { ref, computed, isReadonly, reactive, isRef, toRef } from '../../src' 3 | 4 | describe('Hooks computed', () => { 5 | let warn = null 6 | 7 | beforeEach(() => { 8 | warn = vi.spyOn(global.console, 'error').mockImplementation(() => null) 9 | }) 10 | afterEach(() => { 11 | warn.mockRestore() 12 | }) 13 | 14 | it('basic usage', () => 15 | new Promise((done, reject) => { 16 | done.fail = reject 17 | 18 | const vm = new Vue({ 19 | template: '

{{ b }}
', 20 | setup() { 21 | const a = ref(1) 22 | const b = computed(() => a.value + 1) 23 | return { 24 | a, 25 | b, 26 | } 27 | }, 28 | }).$mount() 29 | expect(vm.b).toBe(2) 30 | expect(vm.$el.textContent).toBe('2') 31 | vm.a = 2 32 | expect(vm.b).toBe(3) 33 | waitForUpdate(() => { 34 | expect(vm.$el.textContent).toBe('3') 35 | }).then(done) 36 | })) 37 | 38 | it('with setter', () => 39 | new Promise((done, reject) => { 40 | done.fail = reject 41 | 42 | const vm = new Vue({ 43 | template: '
{{ b }}
', 44 | setup() { 45 | const a = ref(1) 46 | const b = computed({ 47 | get: () => a.value + 1, 48 | set: (v) => (a.value = v - 1), 49 | }) 50 | return { 51 | a, 52 | b, 53 | } 54 | }, 55 | }).$mount() 56 | expect(vm.b).toBe(2) 57 | expect(vm.$el.textContent).toBe('2') 58 | vm.a = 2 59 | expect(vm.b).toBe(3) 60 | waitForUpdate(() => { 61 | expect(vm.$el.textContent).toBe('3') 62 | vm.b = 1 63 | expect(vm.a).toBe(0) 64 | }) 65 | .then(() => { 66 | expect(vm.$el.textContent).toBe('1') 67 | }) 68 | .then(done) 69 | })) 70 | 71 | it('warn assigning to computed with no setter', () => { 72 | const vm = new Vue({ 73 | setup() { 74 | const b = computed(() => 1) 75 | return { 76 | b, 77 | } 78 | }, 79 | }) 80 | vm.b = 2 81 | expect(warn.mock.calls[0][0]).toMatch( 82 | '[Vue warn]: Write operation failed: computed value is readonly.' 83 | ) 84 | }) 85 | 86 | it('watching computed', () => 87 | new Promise((done, reject) => { 88 | done.fail = reject 89 | 90 | const spy = vi.fn() 91 | const vm = new Vue({ 92 | setup() { 93 | const a = ref(1) 94 | const b = computed(() => a.value + 1) 95 | return { 96 | a, 97 | b, 98 | } 99 | }, 100 | }) 101 | vm.$watch('b', spy) 102 | vm.a = 2 103 | waitForUpdate(() => { 104 | expect(spy).toHaveBeenCalledWith(3, 2) 105 | }).then(done) 106 | })) 107 | 108 | it('caching', () => { 109 | const spy = vi.fn() 110 | const vm = new Vue({ 111 | setup() { 112 | const a = ref(1) 113 | const b = computed(() => { 114 | spy() 115 | return a.value + 1 116 | }) 117 | return { 118 | a, 119 | b, 120 | } 121 | }, 122 | }) 123 | expect(spy.mock.calls.length).toBe(0) 124 | vm.b 125 | expect(spy.mock.calls.length).toBe(1) 126 | vm.b 127 | expect(spy.mock.calls.length).toBe(1) 128 | }) 129 | 130 | it('as component', () => 131 | new Promise((done, reject) => { 132 | done.fail = reject 133 | 134 | const Comp = Vue.extend({ 135 | template: `
{{ b }} {{ c }}
`, 136 | setup() { 137 | const a = ref(1) 138 | const b = computed(() => { 139 | return a.value + 1 140 | }) 141 | return { 142 | a, 143 | b, 144 | } 145 | }, 146 | }) 147 | 148 | const vm = new Comp({ 149 | setup(_, { _vm }) { 150 | const c = computed(() => { 151 | return _vm.b + 1 152 | }) 153 | 154 | return { 155 | c, 156 | } 157 | }, 158 | }).$mount() 159 | expect(vm.b).toBe(2) 160 | expect(vm.c).toBe(3) 161 | expect(vm.$el.textContent).toBe('2 3') 162 | vm.a = 2 163 | expect(vm.b).toBe(3) 164 | expect(vm.c).toBe(4) 165 | waitForUpdate(() => { 166 | expect(vm.$el.textContent).toBe('3 4') 167 | }).then(done) 168 | })) 169 | 170 | it('rethrow computed error', () => { 171 | const vm = new Vue({ 172 | setup() { 173 | const a = computed(() => { 174 | throw new Error('rethrow') 175 | }) 176 | 177 | return { 178 | a, 179 | } 180 | }, 181 | }) 182 | expect(() => vm.a).toThrowError('rethrow') 183 | }) 184 | 185 | it('Mixins should not break computed properties', () => { 186 | const ExampleComponent = Vue.extend({ 187 | props: ['test'], 188 | render: (h) => h('div'), 189 | setup: (props) => ({ example: computed(() => props.test) }), 190 | }) 191 | 192 | Vue.mixin({ 193 | computed: { 194 | foobar() { 195 | return 'test' 196 | }, 197 | }, 198 | }) 199 | 200 | const app = new Vue({ 201 | render: (h) => 202 | h('div', [ 203 | h(ExampleComponent, { props: { test: 'A' } }), 204 | h(ExampleComponent, { props: { test: 'B' } }), 205 | ]), 206 | }).$mount() 207 | 208 | expect(app.$children[0].example).toBe('A') 209 | expect(app.$children[1].example).toBe('B') 210 | }) 211 | 212 | it('should watch a reactive property created via toRef', () => 213 | new Promise((done, reject) => { 214 | done.fail = reject 215 | 216 | const spy = vi.fn() 217 | const vm = new Vue({ 218 | setup() { 219 | const a = reactive({}) 220 | const b = toRef(a, 'b') 221 | 222 | return { 223 | a, 224 | b, 225 | } 226 | }, 227 | }) 228 | vm.$watch('b', spy) 229 | vm.b = 2 230 | waitForUpdate(() => { 231 | expect(spy).toHaveBeenCalledWith(2, undefined) 232 | }).then(done) 233 | })) 234 | 235 | it('should be readonly', () => { 236 | let a = { a: 1 } 237 | const x = computed(() => a) 238 | expect(isReadonly(x)).toBe(true) 239 | expect(isReadonly(x.value)).toBe(false) 240 | expect(isReadonly(x.value.a)).toBe(false) 241 | const z = computed({ 242 | get() { 243 | return a 244 | }, 245 | set(v) { 246 | a = v 247 | }, 248 | }) 249 | expect(isReadonly(z.value)).toBe(false) 250 | expect(isReadonly(z.value.a)).toBe(false) 251 | }) 252 | 253 | it('passes isComputed', () => { 254 | function isComputed(o) { 255 | return !!(o && isRef(o) && o.effect) 256 | } 257 | 258 | expect(isComputed(computed(() => 2))).toBe(true) 259 | expect( 260 | isComputed( 261 | computed({ 262 | get: () => 2, 263 | set: () => {}, 264 | }) 265 | ) 266 | ).toBe(true) 267 | 268 | expect(isComputed(ref({}))).toBe(false) 269 | expect(isComputed(reactive({}))).toBe(false) 270 | expect(isComputed({})).toBe(false) 271 | expect(isComputed(undefined)).toBe(false) 272 | expect(isComputed(null)).toBe(false) 273 | expect(isComputed(true)).toBe(false) 274 | expect(isComputed(20)).toBe(false) 275 | expect(isComputed('hey')).toBe(false) 276 | expect(isComputed('')).toBe(false) 277 | }) 278 | }) 279 | -------------------------------------------------------------------------------- /test/apis/inject.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.common.js' 2 | import { inject, provide, ref, reactive } from '../../src' 3 | 4 | let injected 5 | const injectedComp = { 6 | render() {}, 7 | setup() { 8 | return { 9 | foo: inject('foo'), 10 | bar: inject('bar'), 11 | } 12 | }, 13 | created() { 14 | injected = [this.foo, this.bar] 15 | }, 16 | } 17 | 18 | beforeEach(() => { 19 | injected = null 20 | }) 21 | 22 | describe('Hooks provide/inject', () => { 23 | let warn = null 24 | 25 | beforeEach(() => { 26 | warn = vi.spyOn(global.console, 'error').mockImplementation(() => null) 27 | }) 28 | afterEach(() => { 29 | warn.mockRestore() 30 | }) 31 | 32 | it('should work', () => { 33 | new Vue({ 34 | template: ``, 35 | setup() { 36 | const count = ref(1) 37 | provide('foo', count) 38 | provide('bar', false) 39 | }, 40 | components: { 41 | child: { 42 | template: ``, 43 | components: { 44 | injectedComp, 45 | }, 46 | }, 47 | }, 48 | }).$mount() 49 | 50 | expect(injected).toEqual([1, false]) 51 | }) 52 | 53 | it('should return a default value when inject not found', () => { 54 | let injected 55 | new Vue({ 56 | template: ``, 57 | components: { 58 | child: { 59 | template: `
{{ msg }}
`, 60 | setup() { 61 | injected = inject('not-existed-inject-key', 'foo') 62 | return { 63 | injected, 64 | } 65 | }, 66 | }, 67 | }, 68 | }).$mount() 69 | 70 | expect(injected).toBe('foo') 71 | }) 72 | 73 | it('should work for ref value', () => 74 | new Promise((done, reject) => { 75 | done.fail = reject 76 | 77 | const Msg = Symbol() 78 | const app = new Vue({ 79 | template: ``, 80 | setup() { 81 | provide(Msg, ref('hello')) 82 | }, 83 | components: { 84 | child: { 85 | template: `
{{ msg }}
`, 86 | setup() { 87 | return { 88 | msg: inject(Msg), 89 | } 90 | }, 91 | }, 92 | }, 93 | }).$mount() 94 | 95 | app.$children[0].msg = 'bar' 96 | waitForUpdate(() => { 97 | expect(app.$el.textContent).toBe('bar') 98 | }).then(done) 99 | })) 100 | 101 | it('should work for reactive value', () => 102 | new Promise((done, reject) => { 103 | done.fail = reject 104 | 105 | const State = Symbol() 106 | let obj 107 | const app = new Vue({ 108 | template: ``, 109 | setup() { 110 | provide(State, reactive({ msg: 'foo' })) 111 | }, 112 | components: { 113 | child: { 114 | template: `
{{ state.msg }}
`, 115 | setup() { 116 | obj = inject(State) 117 | return { 118 | state: obj, 119 | } 120 | }, 121 | }, 122 | }, 123 | }).$mount() 124 | expect(obj.msg).toBe('foo') 125 | app.$children[0].state.msg = 'bar' 126 | waitForUpdate(() => { 127 | expect(app.$el.textContent).toBe('bar') 128 | }).then(done) 129 | })) 130 | 131 | it('should work when combined with 2.x provide option', () => { 132 | const State = Symbol() 133 | let obj1 134 | let obj2 135 | new Vue({ 136 | template: ``, 137 | setup() { 138 | provide(State, { msg: 'foo' }) 139 | }, 140 | provide: { 141 | X: { msg: 'bar' }, 142 | }, 143 | components: { 144 | child: { 145 | setup() { 146 | obj1 = inject(State) 147 | obj2 = inject('X') 148 | }, 149 | template: `
`, 150 | }, 151 | }, 152 | }).$mount() 153 | expect(obj1.msg).toBe('foo') 154 | expect(obj2.msg).toBe('bar') 155 | }) 156 | 157 | it('should call default value as factory', () => { 158 | const State = Symbol() 159 | let fn = vi.fn() 160 | new Vue({ 161 | template: ``, 162 | setup() {}, 163 | provide: { 164 | X: { msg: 'bar' }, 165 | }, 166 | components: { 167 | child: { 168 | setup() { 169 | inject(State, fn, true) 170 | }, 171 | template: `
`, 172 | }, 173 | }, 174 | }).$mount() 175 | expect(fn).toHaveBeenCalled() 176 | }) 177 | }) 178 | -------------------------------------------------------------------------------- /test/apis/useCssModule.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.common.js' 2 | import { useCssModule } from '../../src' 3 | 4 | const style = { whateverStyle: 'whateverStyle' } 5 | 6 | function injectStyles() { 7 | Object.defineProperty(this, '$style', { 8 | configurable: true, 9 | get: function () { 10 | return style 11 | }, 12 | }) 13 | } 14 | 15 | describe('api/useCssModule', () => { 16 | it('should get the same object', () => 17 | new Promise((done) => { 18 | const vm = new Vue({ 19 | beforeCreate() { 20 | injectStyles.call(this) 21 | }, 22 | template: '
{{style}}
', 23 | setup() { 24 | const style = useCssModule() 25 | return { style } 26 | }, 27 | }) 28 | vm.$mount() 29 | expect(vm.style).toBe(style) 30 | done() 31 | })) 32 | }) 33 | -------------------------------------------------------------------------------- /test/apis/warn.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.common.js' 2 | import { warn as apiWarn } from '../../src' 3 | 4 | describe('api/warn', () => { 5 | let warn = null 6 | 7 | beforeEach(() => { 8 | warn = vi.spyOn(global.console, 'error').mockImplementation(() => null) 9 | }) 10 | afterEach(() => { 11 | warn.mockRestore() 12 | }) 13 | 14 | it('can be called inside a component', () => { 15 | new Vue({ 16 | setup() { 17 | apiWarn('warned') 18 | }, 19 | template: `
`, 20 | }).$mount() 21 | 22 | expect(warn).toHaveBeenCalledTimes(1) 23 | expect(warn.mock.calls[0][0]).toMatch( 24 | /\[Vue warn\]: warned[\s\S]*\(found in \)/ 25 | ) 26 | }) 27 | 28 | it('can be called outside a component', () => { 29 | apiWarn('warned') 30 | 31 | expect(warn).toHaveBeenCalledTimes(1) 32 | expect(warn).toHaveBeenCalledWith('[Vue warn]: warned') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/createApp.spec.ts: -------------------------------------------------------------------------------- 1 | import { createApp, defineComponent, ref, nextTick } from '../src' 2 | 3 | describe('createApp', () => { 4 | it('should work', async () => { 5 | const app = createApp({ 6 | setup() { 7 | return { 8 | a: ref(1), 9 | } 10 | }, 11 | template: '

{{a}}

', 12 | }) 13 | const vm = app.mount() 14 | 15 | await nextTick() 16 | expect(vm.$el.textContent).toBe('1') 17 | }) 18 | 19 | it('should work with rootProps', async () => { 20 | const app = createApp( 21 | defineComponent({ 22 | props: { 23 | msg: String, 24 | }, 25 | template: '

{{msg}}

', 26 | }), 27 | { 28 | msg: 'foobar', 29 | } 30 | ) 31 | const vm = app.mount() 32 | 33 | await nextTick() 34 | expect(vm.$el.textContent).toBe('foobar') 35 | }) 36 | 37 | it('should work with components', async () => { 38 | const Foo = defineComponent({ 39 | props: { 40 | msg: { 41 | type: String, 42 | required: true, 43 | }, 44 | }, 45 | template: '

{{msg}}

', 46 | }) 47 | 48 | const app = createApp( 49 | defineComponent({ 50 | props: { 51 | msg: String, 52 | }, 53 | template: '', 54 | }), 55 | { 56 | msg: 'foobar', 57 | } 58 | ) 59 | app.component('Foo', Foo) 60 | const vm = app.mount() 61 | 62 | await nextTick() 63 | expect(vm.$el.textContent).toBe('foobar') 64 | }) 65 | 66 | it('should work with provide', async () => { 67 | const Foo = defineComponent({ 68 | inject: ['msg'], 69 | template: '

{{msg}}

', 70 | }) 71 | 72 | const app = createApp( 73 | defineComponent({ 74 | template: '', 75 | components: { Foo }, 76 | }) 77 | ) 78 | app.provide('msg', 'foobar') 79 | const vm = app.mount() 80 | 81 | await nextTick() 82 | expect(vm.$el.textContent).toBe('foobar') 83 | }) 84 | 85 | it("should respect root component's provide", async () => { 86 | const Foo = defineComponent({ 87 | inject: ['msg'], 88 | template: '

{{msg}}

', 89 | }) 90 | 91 | const app = createApp( 92 | defineComponent({ 93 | template: '', 94 | provide: { 95 | msg: 'root component', 96 | }, 97 | components: { Foo }, 98 | }) 99 | ) 100 | app.provide('msg', 'application') 101 | const vm = app.mount() 102 | 103 | await nextTick() 104 | expect(vm.$el.textContent).toBe('root component') 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /test/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare function waitForUpdate(cb: Function): Promise 2 | 3 | declare interface Window { 4 | waitForUpdate(cb: Function): Promise 5 | } 6 | -------------------------------------------------------------------------------- /test/helpers/create-local-vue.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VueConstructor } from 'vue' 2 | 3 | // based on https://github.com/vuejs/vue-test-utils/blob/dev/packages/test-utils/src/create-local-vue.js 4 | 5 | export function createLocalVue(_Vue: VueConstructor = Vue) { 6 | const instance = _Vue.extend() 7 | 8 | Object.keys(_Vue).forEach((key) => { 9 | // @ts-ignore 10 | instance[key] = _Vue[key] 11 | }) 12 | 13 | // @ts-ignore 14 | if (instance._installedPlugins && instance._installedPlugins.length) { 15 | // @ts-ignore 16 | instance._installedPlugins.length = 0 17 | } 18 | 19 | instance.config = _Vue.config 20 | 21 | const use = instance.use 22 | //@ts-ignore 23 | instance.use = (plugin, ...rest) => { 24 | if (plugin.installed === true) { 25 | plugin.installed = false 26 | } 27 | if (plugin.install && plugin.install.installed === true) { 28 | plugin.install.installed = false 29 | } 30 | use.call(instance, plugin, ...rest) 31 | } 32 | return instance 33 | } 34 | -------------------------------------------------------------------------------- /test/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mockWarn' 2 | export * from './utils' 3 | -------------------------------------------------------------------------------- /test/helpers/mockWarn.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace Vi { 3 | interface JestAssertion { 4 | toHaveBeenWarned(): T 5 | toHaveBeenWarnedLast(): T 6 | toHaveBeenWarnedTimes(n: number): T 7 | } 8 | } 9 | } 10 | 11 | import type { SpyInstance } from 'vitest' 12 | 13 | export const mockError = () => mockWarn(true) 14 | 15 | export function mockWarn(asError = false) { 16 | expect.extend({ 17 | toHaveBeenWarned(received: string) { 18 | asserted.add(received) 19 | const passed = warn.mock.calls.some( 20 | (args) => args[0].toString().indexOf(received) > -1 21 | ) 22 | if (passed) { 23 | return { 24 | pass: true, 25 | message: () => `expected "${received}" not to have been warned.`, 26 | } 27 | } else { 28 | const msgs = warn.mock.calls.map((args) => args[0]).join('\n - ') 29 | return { 30 | pass: false, 31 | message: () => 32 | `expected "${received}" to have been warned.\n\nActual messages:\n\n - ${msgs}`, 33 | } 34 | } 35 | }, 36 | 37 | toHaveBeenWarnedLast(received: string) { 38 | asserted.add(received) 39 | const passed = 40 | warn.mock.calls[warn.mock.calls.length - 1][0].indexOf(received) > -1 41 | if (passed) { 42 | return { 43 | pass: true, 44 | message: () => `expected "${received}" not to have been warned last.`, 45 | } 46 | } else { 47 | const msgs = warn.mock.calls.map((args) => args[0]).join('\n - ') 48 | return { 49 | pass: false, 50 | message: () => 51 | `expected "${received}" to have been warned last.\n\nActual messages:\n\n - ${msgs}`, 52 | } 53 | } 54 | }, 55 | 56 | toHaveBeenWarnedTimes(received: string, n: number) { 57 | asserted.add(received) 58 | let found = 0 59 | warn.mock.calls.forEach((args) => { 60 | if (args[0].indexOf(received) > -1) { 61 | found++ 62 | } 63 | }) 64 | 65 | if (found === n) { 66 | return { 67 | pass: true, 68 | message: () => 69 | `expected "${received}" to have been warned ${n} times.`, 70 | } 71 | } else { 72 | return { 73 | pass: false, 74 | message: () => 75 | `expected "${received}" to have been warned ${n} times but got ${found}.`, 76 | } 77 | } 78 | }, 79 | }) 80 | 81 | let warn: SpyInstance 82 | const asserted: Set = new Set() 83 | 84 | beforeEach(() => { 85 | asserted.clear() 86 | warn = vi.spyOn(console, asError ? 'error' : 'warn') 87 | warn.mockImplementation(() => {}) 88 | }) 89 | 90 | afterEach(() => { 91 | const assertedArray = Array.from(asserted) 92 | const nonAssertedWarnings = warn.mock.calls 93 | .map((args) => args[0]) 94 | .filter((received) => { 95 | return !assertedArray.some((assertedMsg) => { 96 | return received.toString().indexOf(assertedMsg) > -1 97 | }) 98 | }) 99 | warn.mockRestore() 100 | if (nonAssertedWarnings.length) { 101 | nonAssertedWarnings.forEach((warning) => { 102 | console.warn(warning) 103 | }) 104 | throw new Error(`test case threw unexpected warnings.`) 105 | } 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /test/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | const Vue = require('vue/dist/vue.common.js') 2 | 3 | export function nextTick(): Promise { 4 | return Vue.nextTick() 5 | } 6 | 7 | export function sleep(ms = 100) { 8 | return new Promise((resolve) => setTimeout(resolve, ms)) 9 | } 10 | -------------------------------------------------------------------------------- /test/helpers/wait-for-update.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | // helper for async assertions. 4 | // Use like this: 5 | // 6 | // vm.a = 123 7 | // waitForUpdate(() => { 8 | // expect(vm.$el.textContent).toBe('123') 9 | // vm.a = 234 10 | // }) 11 | // .then(() => { 12 | // // more assertions... 13 | // }) 14 | // .then(done) 15 | 16 | export const waitForUpdate = (initialCb) => { 17 | let end 18 | const queue = initialCb ? [initialCb] : [] 19 | 20 | function shift() { 21 | const job = queue.shift() 22 | if (queue.length) { 23 | let hasError = false 24 | try { 25 | job.wait ? job(shift) : job() 26 | } catch (e) { 27 | hasError = true 28 | const done = queue[queue.length - 1] 29 | if (done && done.fail) { 30 | done.fail(e) 31 | } 32 | } 33 | if (!hasError && !job.wait) { 34 | if (queue.length) { 35 | Vue.nextTick(shift) 36 | } 37 | } 38 | } else if (job && (job.fail || job === end)) { 39 | job() // done 40 | } 41 | } 42 | 43 | Vue.nextTick(() => { 44 | if (!queue.length || (!end && !queue[queue.length - 1].fail)) { 45 | throw new Error('waitForUpdate chain is missing .then(done)') 46 | } 47 | shift() 48 | }) 49 | 50 | const chainer = { 51 | then: (nextCb) => { 52 | queue.push(nextCb) 53 | return chainer 54 | }, 55 | thenWaitFor: (wait) => { 56 | if (typeof wait === 'number') { 57 | wait = timeout(wait) 58 | } 59 | wait.wait = true 60 | queue.push(wait) 61 | return chainer 62 | }, 63 | end: (endFn) => { 64 | queue.push(endFn) 65 | end = endFn 66 | }, 67 | } 68 | 69 | return chainer 70 | } 71 | 72 | function timeout(n) { 73 | return (next) => setTimeout(next, n) 74 | } 75 | -------------------------------------------------------------------------------- /test/misc.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from './vue' 2 | import { ref, nextTick, isReactive } from '../src' 3 | 4 | describe('nextTick', () => { 5 | it('should works with callbacks', () => { 6 | const vm = new Vue<{ a: number }>({ 7 | template: `
{{a}}
`, 8 | setup() { 9 | return { 10 | a: ref(1), 11 | } 12 | }, 13 | }).$mount() 14 | 15 | expect(vm.$el.textContent).toBe('1') 16 | vm.a = 2 17 | expect(vm.$el.textContent).toBe('1') 18 | 19 | nextTick(() => { 20 | expect(vm.$el.textContent).toBe('2') 21 | vm.a = 3 22 | expect(vm.$el.textContent).toBe('2') 23 | 24 | nextTick(() => { 25 | expect(vm.$el.textContent).toBe('3') 26 | }) 27 | }) 28 | }) 29 | 30 | it('should works with await', async () => { 31 | const vm = new Vue<{ a: number }>({ 32 | template: `
{{a}}
`, 33 | setup() { 34 | return { 35 | a: ref(1), 36 | } 37 | }, 38 | }).$mount() 39 | 40 | expect(vm.$el.textContent).toBe('1') 41 | vm.a = 2 42 | expect(vm.$el.textContent).toBe('1') 43 | 44 | await nextTick() 45 | expect(vm.$el.textContent).toBe('2') 46 | vm.a = 3 47 | expect(vm.$el.textContent).toBe('2') 48 | 49 | await nextTick() 50 | expect(vm.$el.textContent).toBe('3') 51 | }) 52 | }) 53 | 54 | describe('observable', () => { 55 | it('observable should be reactive', () => { 56 | const o: Record = Vue.observable({ 57 | a: 1, 58 | b: [{ a: 1 }], 59 | }) 60 | 61 | expect(isReactive(o)).toBe(true) 62 | 63 | expect(isReactive(o.b)).toBe(true) 64 | expect(isReactive(o.b[0])).toBe(true) 65 | 66 | // TODO new array items should be reactive 67 | // o.b.push({ a: 2 }) 68 | // expect(isReactive(o.b[1])).toBe(true) 69 | }) 70 | 71 | it('nested deps should keep __ob__', () => { 72 | const o: any = Vue.observable({ 73 | a: { b: 1 }, 74 | }) 75 | 76 | expect(o.__ob__).not.toBeUndefined() 77 | expect(o.a.__ob__).not.toBeUndefined() 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /test/setupContext.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | defineComponent, 4 | createApp, 5 | ref, 6 | computed, 7 | nextTick, 8 | SetupContext, 9 | getCurrentInstance, 10 | } from '../src' 11 | import { mockWarn } from './helpers' 12 | 13 | describe('setupContext', () => { 14 | mockWarn(true) 15 | it('should have proper properties', () => { 16 | let context: SetupContext = undefined! 17 | 18 | const vm = createApp( 19 | defineComponent({ 20 | setup(_, ctx) { 21 | context = ctx 22 | }, 23 | template: '
', 24 | }) 25 | ).mount() 26 | 27 | expect(context).toBeDefined() 28 | expect('parent' in context).toBe(true) 29 | expect(context.slots).toBeDefined() 30 | expect(context.attrs).toEqual(vm.$attrs) 31 | 32 | // CAUTION: these will be removed in 3.0 33 | expect(context.root).toBe(vm.$root) 34 | expect(context.parent).toBe(vm.$parent) 35 | expect(context.listeners).toBe(vm.$listeners) 36 | expect(context.refs).toBe(vm.$refs) 37 | expect(typeof context.emit === 'function').toBe(true) 38 | }) 39 | 40 | it('slots should work in render function', () => { 41 | const vm = createApp( 42 | defineComponent({ 43 | template: ` 44 | 45 | 48 | 51 | 52 | `, 53 | components: { 54 | test: defineComponent({ 55 | setup(_, { slots }) { 56 | return () => { 57 | return h('div', [slots.default?.(), slots.item?.()]) 58 | } 59 | }, 60 | }), 61 | }, 62 | }) 63 | ).mount() 64 | expect(vm.$el.innerHTML).toBe('foomeh') 65 | }) 66 | 67 | it('warn for slots calls outside of the render() function', () => { 68 | let warn = vi.spyOn(global.console, 'error').mockImplementation(() => null) 69 | 70 | createApp( 71 | defineComponent({ 72 | template: ` 73 | 74 | 77 | 78 | `, 79 | components: { 80 | test: { 81 | setup(_, { slots }) { 82 | slots.default?.() 83 | }, 84 | }, 85 | }, 86 | }) 87 | ).mount() 88 | expect(warn.mock.calls[0][0]).toMatch( 89 | 'slots.default() got called outside of the "render()" scope' 90 | ) 91 | warn.mockRestore() 92 | }) 93 | 94 | it('staled slots should be removed', () => { 95 | const Child = { 96 | template: '
', 97 | } 98 | const vm = createApp( 99 | defineComponent({ 100 | components: { Child }, 101 | template: ` 102 | 103 | 106 | 107 | `, 108 | }) 109 | ).mount() 110 | expect(vm.$el.textContent).toMatch(`foo foo`) 111 | }) 112 | 113 | it('slots should be synchronized', async () => { 114 | let slotKeys: string[] = [] 115 | 116 | const Foo = defineComponent({ 117 | setup(_, { slots }) { 118 | slotKeys = Object.keys(slots) 119 | return () => { 120 | slotKeys = Object.keys(slots) 121 | return h('div', [ 122 | slots.default && slots.default('from foo default'), 123 | slots.one && slots.one('from foo one'), 124 | slots.two && slots.two('from foo two'), 125 | slots.three && slots.three('from foo three'), 126 | ]) 127 | } 128 | }, 129 | }) 130 | 131 | const vm = createApp( 132 | defineComponent({ 133 | data() { 134 | return { 135 | a: 'one', 136 | b: 'two', 137 | } 138 | }, 139 | template: ` 140 | 141 | 142 | 143 | 144 | `, 145 | components: { Foo }, 146 | }) 147 | ).mount() 148 | 149 | expect(slotKeys).toEqual(['one', 'two']) 150 | expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch( 151 | `a from foo one b from foo two` 152 | ) 153 | 154 | // @ts-expect-error 155 | vm.a = 'two' 156 | // @ts-expect-error 157 | vm.b = 'three' 158 | 159 | await nextTick() 160 | // expect(slotKeys).toEqual(['one', 'three']); 161 | expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch( 162 | `a from foo two b from foo three ` 163 | ) 164 | }) 165 | 166 | // #264 167 | it('attrs should be reactive after destructuring', async () => { 168 | let _attrs: SetupContext['attrs'] = undefined! 169 | const foo = ref('bar') 170 | 171 | const ComponentA = defineComponent({ 172 | setup(_, { attrs }) { 173 | _attrs = attrs 174 | }, 175 | template: `
{{$attrs}}
`, 176 | }) 177 | const Root = defineComponent({ 178 | components: { 179 | ComponentA, 180 | }, 181 | setup() { 182 | return { foo } 183 | }, 184 | template: ` 185 | 186 | `, 187 | }) 188 | 189 | createApp(Root).mount() 190 | 191 | expect(_attrs).toBeDefined() 192 | expect(_attrs.foo).toBe('bar') 193 | 194 | foo.value = 'bar2' 195 | 196 | await nextTick() 197 | 198 | expect(_attrs.foo).toBe('bar2') 199 | }) 200 | 201 | // #563 202 | it('should not RangeError: Maximum call stack size exceeded', async () => { 203 | createApp( 204 | defineComponent({ 205 | template: `
`, 206 | setup() { 207 | // @ts-expect-error 208 | const app = getCurrentInstance().proxy 209 | let mockNT: any = [] 210 | mockNT.__ob__ = {} 211 | const test = { 212 | app, 213 | mockNT, 214 | } 215 | return { 216 | test, 217 | } 218 | }, 219 | }) 220 | ).mount() 221 | 222 | await nextTick() 223 | expect( 224 | `"RangeError: Maximum call stack size exceeded"` 225 | ).not.toHaveBeenWarned() 226 | }) 227 | 228 | // #794 229 | it('should not trigger getter w/ object computed nested', async () => { 230 | const spy = vi.fn() 231 | createApp( 232 | defineComponent({ 233 | template: `
`, 234 | setup() { 235 | const person = { 236 | name: computed(() => { 237 | spy() 238 | return 1 239 | }), 240 | } 241 | return { 242 | person, 243 | } 244 | }, 245 | }) 246 | ).mount() 247 | expect(spy).toHaveBeenCalledTimes(0) 248 | }) 249 | }) 250 | -------------------------------------------------------------------------------- /test/ssr/serverPrefetch.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.common.js' 2 | import { createRenderer } from 'vue-server-renderer' 3 | import { ref, onServerPrefetch, getCurrentInstance } from '../../src' 4 | 5 | function fetch(result) { 6 | return new Promise((resolve) => { 7 | setTimeout(() => { 8 | resolve(result) 9 | }, 10) 10 | }) 11 | } 12 | 13 | describe('serverPrefetch', () => { 14 | it('should prefetch async operations before rendering', async () => { 15 | const app = new Vue({ 16 | setup() { 17 | const count = ref(0) 18 | 19 | onServerPrefetch(async () => { 20 | count.value = await fetch(42) 21 | }) 22 | 23 | return { 24 | count, 25 | } 26 | }, 27 | render(h) { 28 | return h('div', this.count) 29 | }, 30 | }) 31 | 32 | const serverRenderer = createRenderer() 33 | const html = await serverRenderer.renderToString(app) 34 | expect(html).toBe('
42
') 35 | }) 36 | 37 | it('should prefetch many async operations before rendering', async () => { 38 | const app = new Vue({ 39 | setup() { 40 | const count = ref(0) 41 | const label = ref('') 42 | 43 | onServerPrefetch(async () => { 44 | count.value = await fetch(42) 45 | }) 46 | 47 | onServerPrefetch(async () => { 48 | label.value = await fetch('meow') 49 | }) 50 | 51 | return { 52 | count, 53 | label, 54 | } 55 | }, 56 | render(h) { 57 | return h('div', [this.count, this.label]) 58 | }, 59 | }) 60 | 61 | const serverRenderer = createRenderer() 62 | const html = await serverRenderer.renderToString(app) 63 | expect(html).toBe('
42meow
') 64 | }) 65 | 66 | it('should pass ssrContext', async () => { 67 | const child = { 68 | setup(props, { ssrContext }) { 69 | const content = ref() 70 | 71 | expect(ssrContext.foo).toBe('bar') 72 | 73 | onServerPrefetch(async () => { 74 | content.value = await fetch(ssrContext.foo) 75 | }) 76 | 77 | return { 78 | content, 79 | } 80 | }, 81 | render(h) { 82 | return h('div', this.content) 83 | }, 84 | } 85 | 86 | const app = new Vue({ 87 | components: { 88 | child, 89 | }, 90 | render(h) { 91 | return h('child') 92 | }, 93 | }) 94 | 95 | const serverRenderer = createRenderer() 96 | const html = await serverRenderer.renderToString(app, { foo: 'bar' }) 97 | expect(html).toBe('
bar
') 98 | }) 99 | 100 | it('should not share context', async () => { 101 | const instances = [] 102 | function createApp(context) { 103 | return new Vue({ 104 | setup() { 105 | const count = ref(0) 106 | 107 | onServerPrefetch(async () => { 108 | count.value = await fetch(context.result) 109 | }) 110 | 111 | instances.push(getCurrentInstance()) 112 | 113 | return { 114 | count, 115 | } 116 | }, 117 | render(h) { 118 | return h('div', this.count) 119 | }, 120 | }) 121 | } 122 | 123 | const serverRenderer = createRenderer() 124 | const promises = [] 125 | // Parallel requests 126 | for (let i = 1; i < 3; i++) { 127 | promises.push( 128 | new Promise(async (resolve) => { 129 | const app = createApp({ result: i }) 130 | const html = await serverRenderer.renderToString(app) 131 | expect(html).toBe(`
${i}
`) 132 | resolve() 133 | }) 134 | ) 135 | } 136 | await Promise.all(promises) 137 | expect((instances[0] === instances[1]) === instances[2]).toBe(false) 138 | }) 139 | }) 140 | -------------------------------------------------------------------------------- /test/ssr/ssrReactive.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | import Vue from '../vue' 6 | import { 7 | isReactive, 8 | reactive, 9 | ref, 10 | isRaw, 11 | isRef, 12 | set, 13 | shallowRef, 14 | getCurrentInstance, 15 | nextTick, 16 | } from '../../src' 17 | import { createRenderer } from 'vue-server-renderer' 18 | import { mockWarn } from '../helpers' 19 | 20 | describe('SSR Reactive', () => { 21 | mockWarn(true) 22 | beforeEach(() => { 23 | process.env.VUE_ENV = 'server' 24 | }) 25 | 26 | it('should in SSR context', async () => { 27 | expect(typeof window).toBe('undefined') 28 | expect((Vue.observable({}) as any).__ob__).toBeUndefined() 29 | }) 30 | 31 | it('should render', async () => { 32 | const app = new Vue({ 33 | setup() { 34 | return { 35 | count: ref(42), 36 | } 37 | }, 38 | render(this: any, h) { 39 | return h('div', this.count) 40 | }, 41 | }) 42 | 43 | const serverRenderer = createRenderer() 44 | const html = await serverRenderer.renderToString(app) 45 | expect(html).toBe('
42
') 46 | }) 47 | 48 | it('reactive + isReactive', async () => { 49 | const state = reactive({}) 50 | expect(isReactive(state)).toBe(true) 51 | expect(isRaw(state)).toBe(false) 52 | }) 53 | 54 | it('shallowRef + isRef', async () => { 55 | const state = shallowRef({}) 56 | expect(isRef(state)).toBe(true) 57 | expect(isRaw(state)).toBe(false) 58 | }) 59 | 60 | it('should work on objects sets with set()', () => { 61 | const state = ref({}) 62 | 63 | set(state.value, 'a', {}) 64 | expect(isReactive(state.value.a)).toBe(true) 65 | 66 | set(state.value, 'a', {}) 67 | expect(isReactive(state.value.a)).toBe(true) 68 | }) 69 | 70 | it('should work on arrays sets with set()', () => { 71 | const state = ref([]) 72 | 73 | set(state.value, 1, {}) 74 | expect(isReactive(state.value[1])).toBe(true) 75 | 76 | set(state.value, 1, {}) 77 | expect(isReactive(state.value[1])).toBe(true) 78 | }) 79 | 80 | // #550 81 | it('props should work with set', () => 82 | new Promise(async (done) => { 83 | let props: any 84 | 85 | const app = new Vue({ 86 | render(this: any, h) { 87 | return h('child', { attrs: { msg: this.msg } }) 88 | }, 89 | setup() { 90 | return { msg: ref('hello') } 91 | }, 92 | components: { 93 | child: { 94 | render(this: any, h: any) { 95 | return h('span', this.data.msg) 96 | }, 97 | props: ['msg'], 98 | setup(_props) { 99 | props = _props 100 | 101 | return { data: _props } 102 | }, 103 | }, 104 | }, 105 | }) 106 | 107 | const serverRenderer = createRenderer() 108 | const html = await serverRenderer.renderToString(app) 109 | 110 | expect(html).toBe('hello') 111 | 112 | expect(props.bar).toBeUndefined() 113 | set(props, 'bar', 'bar') 114 | expect(props.bar).toBe('bar') 115 | 116 | done() 117 | })) 118 | 119 | // #721 120 | it('should behave correctly', () => { 121 | const state = ref({ old: ref(false) }) 122 | set(state.value, 'new', ref(true)) 123 | // console.log(process.server, 'state.value', JSON.stringify(state.value)) 124 | 125 | expect(state.value).toMatchObject({ 126 | old: false, 127 | new: true, 128 | }) 129 | }) 130 | 131 | // #721 132 | it('should behave correctly for the nested ref in the object', () => { 133 | const state = { old: ref(false) } 134 | set(state, 'new', ref(true)) 135 | expect(JSON.stringify(state)).toBe( 136 | '{"old":{"value":false},"new":{"value":true}}' 137 | ) 138 | }) 139 | 140 | // #721 141 | it('should behave correctly for ref of object', () => { 142 | const state = ref({ old: ref(false) }) 143 | set(state.value, 'new', ref(true)) 144 | expect(JSON.stringify(state.value)).toBe('{"old":false,"new":true}') 145 | }) 146 | 147 | // test the input parameter of mockReactivityDeep 148 | it('ssr should not RangeError: Maximum call stack size exceeded', async () => { 149 | new Vue({ 150 | setup() { 151 | // @ts-expect-error 152 | const app = getCurrentInstance().proxy 153 | let mockNt: any = [] 154 | mockNt.__ob__ = {} 155 | const test = reactive({ 156 | app, 157 | mockNt, 158 | }) 159 | return { 160 | test, 161 | } 162 | }, 163 | }) 164 | await nextTick() 165 | expect( 166 | `"RangeError: Maximum call stack size exceeded"` 167 | ).not.toHaveBeenWarned() 168 | }) 169 | 170 | it('should work on objects sets with set()', () => { 171 | const state = ref({}) 172 | set(state.value, 'a', {}) 173 | 174 | expect(isReactive(state.value.a)).toBe(true) 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /test/templateRefs.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.common.js' 2 | import { ref, watchEffect, nextTick } from '../src' 3 | 4 | describe('ref', () => { 5 | it('should work', () => 6 | new Promise((done) => { 7 | let dummy 8 | const vm = new Vue({ 9 | setup() { 10 | const ref1 = ref(null) 11 | watchEffect(() => { 12 | dummy = ref1.value 13 | }) 14 | 15 | return { 16 | bar: ref1, 17 | } 18 | }, 19 | template: `
20 | 21 |
`, 22 | components: { 23 | test: { 24 | id: 'test', 25 | template: '
test
', 26 | }, 27 | }, 28 | }).$mount() 29 | vm.$nextTick() 30 | .then(() => { 31 | expect(dummy).toBe(vm.$refs.bar) 32 | }) 33 | .then(done) 34 | })) 35 | 36 | it('should dynamically update refs', () => 37 | new Promise((done, reject) => { 38 | done.fail = reject 39 | 40 | let dummy1 = null 41 | let dummy2 = null 42 | 43 | const vm = new Vue({ 44 | setup() { 45 | const ref1 = ref(null) 46 | const ref2 = ref(null) 47 | watchEffect(() => { 48 | dummy1 = ref1.value 49 | dummy2 = ref2.value 50 | }) 51 | 52 | return { 53 | value: 'bar', 54 | bar: ref1, 55 | foo: ref2, 56 | } 57 | }, 58 | template: '
', 59 | }).$mount() 60 | waitForUpdate(() => {}) 61 | .then(() => { 62 | expect(dummy1).toBe(vm.$refs.bar) 63 | expect(dummy2).toBe(null) 64 | vm.value = 'foo' 65 | }) 66 | .then(() => { 67 | // vm updated. ref update occures after updated; 68 | }) 69 | .then(() => { 70 | // no render cycle, empty tick 71 | }) 72 | .then(() => { 73 | expect(dummy1).toBe(null) 74 | expect(dummy2).toBe(vm.$refs.foo) 75 | }) 76 | .then(done) 77 | })) 78 | 79 | // #296 80 | it('should dynamically update refs in context', async () => { 81 | const vm = new Vue({ 82 | setup() { 83 | const barRef = ref(null) 84 | return { 85 | barRef, 86 | } 87 | }, 88 | template: `
89 | 90 |
`, 91 | components: { 92 | bar: { 93 | setup() { 94 | const name = ref('bar') 95 | return { 96 | name, 97 | } 98 | }, 99 | template: '
{{name}}
', 100 | }, 101 | foo: { 102 | setup() { 103 | const showSlot = ref(false) 104 | const setShow = () => { 105 | showSlot.value = true 106 | } 107 | return { 108 | setShow, 109 | showSlot, 110 | } 111 | }, 112 | template: `
`, 113 | }, 114 | }, 115 | }).$mount() 116 | await nextTick() 117 | //@ts-ignore 118 | vm.$children[0].setShow() 119 | await nextTick() 120 | //@ts-ignore 121 | expect(vm.$refs.barRef).toBe(vm.barRef) 122 | }) 123 | 124 | it('should update deeply nested component refs using scoped slots', async () => { 125 | const vm = new Vue({ 126 | setup() { 127 | const divRef = ref(null) 128 | const showDiv = ref(false) 129 | return { 130 | divRef, 131 | showDiv, 132 | } 133 | }, 134 | template: `
Slot:
`, 135 | components: { 136 | foo: { 137 | components: { 138 | bar: { 139 | template: `
`, 140 | }, 141 | }, 142 | template: '
', 143 | }, 144 | }, 145 | }).$mount() 146 | await nextTick() 147 | //@ts-ignore 148 | vm.showDiv = true 149 | await nextTick() 150 | //@ts-ignore 151 | expect(vm.$refs.divRef).toBe(vm.divRef) 152 | }) 153 | // TODO: how ? 154 | // it('work with createElement', () => { 155 | // let root; 156 | // const vm = new Vue({ 157 | // setup() { 158 | // root = ref(null); 159 | // return () => { 160 | // return h('div', { 161 | // ref: root, 162 | // }); 163 | // }; 164 | // }, 165 | // }).$mount(); 166 | // expect(root.value).toBe(vm.$el); 167 | // }); 168 | }) 169 | -------------------------------------------------------------------------------- /test/types/defineComponent.spec.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h, ref, SetupContext, PropType } from '../../src' 2 | import Router from 'vue-router' 3 | 4 | const Vue = require('vue/dist/vue.common.js') 5 | 6 | type Equal = (() => U extends Left ? 1 : 0) extends < 7 | U 8 | >() => U extends Right ? 1 : 0 9 | ? true 10 | : false 11 | 12 | const isTypeEqual = (shouldBeEqual: Equal) => { 13 | void shouldBeEqual 14 | expect(true).toBe(true) 15 | } 16 | const isSubType = ( 17 | shouldBeEqual: SubType extends SuperType ? true : false 18 | ) => { 19 | void shouldBeEqual 20 | expect(true).toBe(true) 21 | } 22 | 23 | describe('defineComponent', () => { 24 | it('should work', () => { 25 | const Child = defineComponent({ 26 | props: { msg: String }, 27 | setup(props) { 28 | return () => h('span', props.msg) 29 | }, 30 | }) 31 | 32 | const App = defineComponent({ 33 | setup() { 34 | const msg = ref('hello') 35 | return () => 36 | h('div', [ 37 | h(Child, { 38 | props: { 39 | msg: msg.value, 40 | }, 41 | }), 42 | ]) 43 | }, 44 | }) 45 | const vm = new Vue(App).$mount() 46 | expect(vm.$el.querySelector('span').textContent).toBe('hello') 47 | }) 48 | 49 | it('should infer props type', () => { 50 | const App = defineComponent({ 51 | props: { 52 | a: { 53 | type: Number, 54 | default: 0, 55 | }, 56 | b: String, 57 | }, 58 | setup(props, ctx) { 59 | type PropsType = typeof props 60 | isTypeEqual(true) 61 | isSubType(true) 62 | isSubType<{ readonly b?: string; readonly a: number }, PropsType>(true) 63 | return () => null 64 | }, 65 | }) 66 | new Vue(App) 67 | expect.assertions(3) 68 | }) 69 | 70 | it('custom props interface', () => { 71 | interface IPropsType { 72 | b: string 73 | } 74 | const App = defineComponent({ 75 | props: { 76 | b: {}, 77 | }, 78 | setup(props, ctx) { 79 | type PropsType = typeof props 80 | isTypeEqual(true) 81 | isSubType(true) 82 | isSubType<{ b: string }, PropsType>(true) 83 | return () => null 84 | }, 85 | }) 86 | new Vue(App) 87 | expect.assertions(3) 88 | }) 89 | 90 | it('custom props type function', () => { 91 | interface IPropsTypeFunction { 92 | fn: (arg: boolean) => void 93 | } 94 | const App = defineComponent({ 95 | props: { 96 | fn: Function as PropType<(arg: boolean) => void>, 97 | }, 98 | setup(props, ctx) { 99 | type PropsType = typeof props 100 | isTypeEqual(true) 101 | isSubType void }>(true) 102 | isSubType<{ fn: (arg: boolean) => void }, PropsType>(true) 103 | return () => null 104 | }, 105 | }) 106 | new Vue(App) 107 | expect.assertions(3) 108 | }) 109 | 110 | it('custom props type inferred from PropType', () => { 111 | interface User { 112 | name: string 113 | } 114 | const App = defineComponent({ 115 | props: { 116 | user: Object as PropType, 117 | func: Function as PropType<() => boolean>, 118 | userFunc: Function as PropType<(u: User) => User>, 119 | }, 120 | setup(props) { 121 | type PropsType = typeof props 122 | isSubType< 123 | { user?: User; func?: () => boolean; userFunc?: (u: User) => User }, 124 | PropsType 125 | >(true) 126 | isSubType< 127 | PropsType, 128 | { user?: User; func?: () => boolean; userFunc?: (u: User) => User } 129 | >(true) 130 | }, 131 | }) 132 | new Vue(App) 133 | expect.assertions(2) 134 | }) 135 | 136 | it('no props', () => { 137 | const App = defineComponent({ 138 | setup(props, ctx) { 139 | isTypeEqual(true) 140 | isTypeEqual<{}, typeof props>(true) 141 | return () => null 142 | }, 143 | }) 144 | new Vue(App) 145 | expect.assertions(2) 146 | }) 147 | 148 | it('should accept tuple props', () => { 149 | const App = defineComponent({ 150 | props: ['p1', 'p2'], 151 | setup(props) { 152 | props.p1 153 | props.p2 154 | type PropsType = typeof props 155 | type Expected = { readonly p1?: any; readonly p2?: any } 156 | isSubType(true) 157 | isSubType(true) 158 | }, 159 | }) 160 | new Vue(App) 161 | expect.assertions(2) 162 | }) 163 | 164 | it('should allow any custom options', () => { 165 | const App = defineComponent({ 166 | foo: 'foo', 167 | bar: 'bar', 168 | }) 169 | new Vue(App) 170 | }) 171 | 172 | it('infer the required prop', () => { 173 | const App = defineComponent({ 174 | props: { 175 | foo: { 176 | type: String, 177 | required: true, 178 | }, 179 | bar: { 180 | type: String, 181 | default: 'default', 182 | }, 183 | zoo: { 184 | type: String, 185 | required: false, 186 | }, 187 | }, 188 | propsData: { 189 | foo: 'foo', 190 | }, 191 | setup(props) { 192 | type PropsType = typeof props 193 | isSubType< 194 | { readonly foo: string; readonly bar: string; readonly zoo?: string }, 195 | PropsType 196 | >(true) 197 | isSubType< 198 | PropsType, 199 | { readonly foo: string; readonly bar: string; readonly zoo?: string } 200 | >(true) 201 | return () => null 202 | }, 203 | }) 204 | new Vue(App) 205 | expect.assertions(2) 206 | }) 207 | 208 | describe('compatible with vue router', () => { 209 | it('RouteConfig.component', () => { 210 | new Router({ 211 | routes: [ 212 | { 213 | path: '/', 214 | name: 'root', 215 | component: defineComponent({}), 216 | }, 217 | ], 218 | }) 219 | }) 220 | }) 221 | }) 222 | -------------------------------------------------------------------------------- /test/use.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import CompositionApi from '../src' 3 | import { createLocalVue } from './helpers/create-local-vue' 4 | import { mockWarn } from './helpers' 5 | 6 | describe('use', () => { 7 | mockWarn(true) 8 | 9 | it('should allow install in multiple vue', () => { 10 | const localVueOne = createLocalVue() 11 | localVueOne.use(CompositionApi) 12 | 13 | const localVueTwo = createLocalVue() 14 | localVueTwo.use(CompositionApi) 15 | 16 | expect( 17 | '[vue-composition-api] another instance of Vue installed' 18 | ).not.toHaveBeenWarned() 19 | }) 20 | 21 | it('should warn install in multiple vue', () => { 22 | try { 23 | const fakeVue = { 24 | version: '2._.x', 25 | config: { 26 | optionMergeStrategies: {}, 27 | }, 28 | mixin: vi.fn(), 29 | } 30 | 31 | // @ts-ignore 32 | CompositionApi.install(fakeVue) 33 | expect( 34 | '[vue-composition-api] another instance of Vue installed' 35 | ).toHaveBeenWarned() 36 | } finally { 37 | Vue.use(CompositionApi) 38 | expect( 39 | '[vue-composition-api] another instance of Vue installed' 40 | ).toHaveBeenWarned() 41 | } 42 | }) 43 | 44 | it('should warn installing multiple times', () => { 45 | const localVueOne = createLocalVue() 46 | localVueOne.use(CompositionApi) 47 | 48 | // vue prevents the same plugin of being installed, this will create a new plugin instance 49 | localVueOne.use({ 50 | install(v) { 51 | CompositionApi.install(v) 52 | }, 53 | }) 54 | 55 | expect( 56 | '[vue-composition-api] already installed. Vue.use(VueCompositionAPI) should be called only once.' 57 | ).toHaveBeenWarned() 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /test/v3/reactivity/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | computed, 3 | reactive, 4 | ref, 5 | watchEffect, 6 | WritableComputedRef, 7 | nextTick, 8 | } from '../../../src' 9 | import { mockWarn } from '../../helpers' 10 | 11 | describe('reactivity/computed', () => { 12 | mockWarn(true) 13 | 14 | it('should return updated value', async () => { 15 | const value = reactive<{ foo?: number }>({ foo: undefined }) 16 | const cValue = computed(() => value.foo) 17 | expect(cValue.value).toBe(undefined) 18 | value.foo = 1 19 | await nextTick() 20 | 21 | expect(cValue.value).toBe(1) 22 | }) 23 | 24 | it('should compute lazily', () => { 25 | const value = reactive<{ foo?: number }>({ foo: undefined }) 26 | const getter = vi.fn(() => value.foo) 27 | const cValue = computed(getter) 28 | 29 | // lazy 30 | expect(getter).not.toHaveBeenCalled() 31 | 32 | expect(cValue.value).toBe(undefined) 33 | expect(getter).toHaveBeenCalledTimes(1) 34 | 35 | // should not compute again 36 | cValue.value 37 | expect(getter).toHaveBeenCalledTimes(1) 38 | 39 | // should not compute until needed 40 | value.foo = 1 41 | expect(getter).toHaveBeenCalledTimes(1) 42 | 43 | // now it should compute 44 | expect(cValue.value).toBe(1) 45 | expect(getter).toHaveBeenCalledTimes(2) 46 | 47 | // should not compute again 48 | cValue.value 49 | expect(getter).toHaveBeenCalledTimes(2) 50 | }) 51 | 52 | it('should trigger effect', () => { 53 | const value = reactive<{ foo?: number }>({ foo: undefined }) 54 | const cValue = computed(() => value.foo) 55 | let dummy 56 | watchEffect( 57 | () => { 58 | dummy = cValue.value 59 | }, 60 | { flush: 'sync' } 61 | ) 62 | expect(dummy).toBe(undefined) 63 | value.foo = 1 64 | expect(dummy).toBe(1) 65 | }) 66 | 67 | it('should work when chained', () => { 68 | const value = reactive({ foo: 0 }) 69 | const c1 = computed(() => value.foo) 70 | const c2 = computed(() => c1.value + 1) 71 | expect(c2.value).toBe(1) 72 | expect(c1.value).toBe(0) 73 | value.foo++ 74 | expect(c2.value).toBe(2) 75 | expect(c1.value).toBe(1) 76 | }) 77 | 78 | it('should trigger effect when chained', () => { 79 | const value = reactive({ foo: 0 }) 80 | const getter1 = vi.fn(() => value.foo) 81 | const getter2 = vi.fn(() => { 82 | return c1.value + 1 83 | }) 84 | const c1 = computed(getter1) 85 | const c2 = computed(getter2) 86 | 87 | let dummy 88 | watchEffect( 89 | () => { 90 | dummy = c2.value 91 | }, 92 | { flush: 'sync' } 93 | ) 94 | expect(dummy).toBe(1) 95 | expect(getter1).toHaveBeenCalledTimes(1) 96 | expect(getter2).toHaveBeenCalledTimes(1) 97 | value.foo++ 98 | expect(dummy).toBe(2) 99 | // should not result in duplicate calls 100 | expect(getter1).toHaveBeenCalledTimes(2) 101 | expect(getter2).toHaveBeenCalledTimes(2) 102 | }) 103 | 104 | it('should trigger effect when chained (mixed invocations)', async () => { 105 | const value = reactive({ foo: 0 }) 106 | const getter1 = vi.fn(() => value.foo) 107 | const getter2 = vi.fn(() => { 108 | return c1.value + 1 109 | }) 110 | const c1 = computed(getter1) 111 | const c2 = computed(getter2) 112 | 113 | let dummy 114 | watchEffect(() => { 115 | dummy = c1.value + c2.value 116 | }) 117 | await nextTick() 118 | expect(dummy).toBe(1) 119 | 120 | expect(getter1).toHaveBeenCalledTimes(1) 121 | expect(getter2).toHaveBeenCalledTimes(1) 122 | value.foo++ 123 | 124 | await nextTick() 125 | 126 | expect(dummy).toBe(3) 127 | // should not result in duplicate calls 128 | expect(getter1).toHaveBeenCalledTimes(2) 129 | expect(getter2).toHaveBeenCalledTimes(2) 130 | }) 131 | 132 | // it('should no longer update when stopped', () => { 133 | // const value = reactive<{ foo?: number }>({}); 134 | // const cValue = computed(() => value.foo); 135 | // let dummy; 136 | // effect(() => { 137 | // dummy = cValue.value; 138 | // }); 139 | // expect(dummy).toBe(undefined); 140 | // value.foo = 1; 141 | // expect(dummy).toBe(1); 142 | // stop(cValue.effect); 143 | // value.foo = 2; 144 | // expect(dummy).toBe(1); 145 | // }); 146 | 147 | it('should support setter', () => { 148 | const n = ref(1) 149 | const plusOne = computed({ 150 | get: () => n.value + 1, 151 | set: (val) => { 152 | n.value = val - 1 153 | }, 154 | }) 155 | 156 | expect(plusOne.value).toBe(2) 157 | n.value++ 158 | expect(plusOne.value).toBe(3) 159 | 160 | plusOne.value = 0 161 | expect(n.value).toBe(-1) 162 | }) 163 | 164 | it('should trigger effect w/ setter', async () => { 165 | const n = ref(1) 166 | const plusOne = computed({ 167 | get: () => n.value + 1, 168 | set: (val) => { 169 | n.value = val - 1 170 | }, 171 | }) 172 | 173 | let dummy 174 | watchEffect(() => { 175 | dummy = n.value 176 | }) 177 | expect(dummy).toBe(1) 178 | 179 | plusOne.value = 0 180 | await nextTick() 181 | expect(dummy).toBe(-1) 182 | }) 183 | 184 | it('should warn if trying to set a readonly computed', async () => { 185 | const n = ref(1) 186 | const plusOne = computed(() => n.value + 1) 187 | ;(plusOne as WritableComputedRef).value++ // Type cast to prevent TS from preventing the error 188 | await nextTick() 189 | 190 | expect( 191 | 'Write operation failed: computed value is readonly' 192 | ).toHaveBeenWarnedLast() 193 | }) 194 | }) 195 | -------------------------------------------------------------------------------- /test/v3/reactivity/del.spec.ts: -------------------------------------------------------------------------------- 1 | import { del, reactive, ref, watch, set, watchEffect } from '../../../src' 2 | 3 | // Vue.delete workaround for triggering view updates on object property/array index deletion 4 | describe('reactivity/del', () => { 5 | it('should not trigger reactivity on native object member deletion', () => { 6 | const obj = reactive<{ a?: object }>({ 7 | a: {}, 8 | }) 9 | const spy = vi.fn() 10 | watch(obj, spy, { deep: true, flush: 'sync' }) 11 | delete obj.a // Vue 2 limitation 12 | expect(spy).not.toHaveBeenCalled() 13 | expect(obj).toStrictEqual({}) 14 | }) 15 | 16 | it('should trigger reactivity when using del on reactive object', () => { 17 | const obj = reactive<{ a?: object }>({ 18 | a: {}, 19 | }) 20 | const spy = vi.fn() 21 | watch(obj, spy, { deep: true, flush: 'sync' }) 22 | del(obj, 'a') 23 | expect(spy).toBeCalledTimes(1) 24 | expect(obj).toStrictEqual({}) 25 | }) 26 | 27 | it('should not remove element on array index and should not trigger reactivity', () => { 28 | const arr = ref([1, 2, 3]) 29 | const spy = vi.fn() 30 | watch(arr, spy, { flush: 'sync' }) 31 | delete arr.value[1] // Vue 2 limitation; workaround with .splice() 32 | expect(spy).not.toHaveBeenCalled() 33 | expect(arr.value).toEqual([1, undefined, 3]) 34 | }) 35 | 36 | it('should trigger reactivity when using del on array', () => { 37 | const arr = ref([1, 2, 3]) 38 | const spy = vi.fn() 39 | watch(arr, spy, { flush: 'sync' }) 40 | del(arr.value, 1) 41 | expect(spy).toBeCalledTimes(1) 42 | expect(arr.value).toEqual([1, 3]) 43 | }) 44 | 45 | it('should trigger reactivity when using del on array to delete index out of valid array length', () => { 46 | const arr = ref([]) 47 | const MAX_VALID_ARRAY_LENGTH = Math.pow(2, 32) - 1 48 | const NON_VALID_INDEX = MAX_VALID_ARRAY_LENGTH + 1 49 | set(arr.value, NON_VALID_INDEX, 0) 50 | const spy = vi.fn() 51 | watchEffect( 52 | () => { 53 | spy(arr.value) 54 | }, 55 | { flush: 'sync' } 56 | ) 57 | expect(spy).toBeCalledTimes(1) 58 | del(arr.value, NON_VALID_INDEX) 59 | expect(spy).toBeCalledTimes(2) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /test/v3/reactivity/effectScope.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | nextTick, 3 | watch, 4 | watchEffect, 5 | reactive, 6 | EffectScope, 7 | onScopeDispose, 8 | computed, 9 | ref, 10 | ComputedRef, 11 | createApp, 12 | getCurrentScope, 13 | } from '../../../src' 14 | import { mockWarn } from '../../helpers' 15 | 16 | describe('reactivity/effect/scope', () => { 17 | mockWarn(true) 18 | 19 | it('should run', () => { 20 | const fnSpy = vi.fn(() => {}) 21 | new EffectScope().run(fnSpy) 22 | expect(fnSpy).toHaveBeenCalledTimes(1) 23 | }) 24 | 25 | it('should accept zero argument', () => { 26 | const scope = new EffectScope() 27 | expect(scope.effects.length).toBe(0) 28 | }) 29 | 30 | it('should return run value', () => { 31 | expect(new EffectScope().run(() => 1)).toBe(1) 32 | }) 33 | 34 | it('should collect the effects', async () => { 35 | const scope = new EffectScope() 36 | let dummy = 0 37 | scope.run(() => { 38 | const counter = reactive({ num: 0 }) 39 | watchEffect(() => (dummy = counter.num)) 40 | 41 | expect(dummy).toBe(0) 42 | counter.num = 7 43 | }) 44 | 45 | await nextTick() 46 | 47 | expect(dummy).toBe(7) 48 | // expect(scope.effects.length).toBe(1) 49 | }) 50 | 51 | it('stop', async () => { 52 | let dummy, doubled 53 | const counter = reactive({ num: 0 }) 54 | 55 | const scope = new EffectScope() 56 | scope.run(() => { 57 | watchEffect(() => (dummy = counter.num)) 58 | watchEffect(() => (doubled = counter.num * 2)) 59 | }) 60 | 61 | // expect(scope.effects.length).toBe(2) 62 | 63 | expect(dummy).toBe(0) 64 | counter.num = 7 65 | await nextTick() 66 | expect(dummy).toBe(7) 67 | expect(doubled).toBe(14) 68 | 69 | scope.stop() 70 | 71 | counter.num = 6 72 | await nextTick() 73 | expect(dummy).toBe(7) 74 | expect(doubled).toBe(14) 75 | }) 76 | 77 | it('should collect nested scope', async () => { 78 | let dummy, doubled 79 | const counter = reactive({ num: 0 }) 80 | 81 | const scope = new EffectScope() 82 | scope.run(() => { 83 | // nested scope 84 | new EffectScope().run(() => { 85 | watchEffect(() => (doubled = counter.num * 2)) 86 | }) 87 | watchEffect(() => (dummy = counter.num)) 88 | }) 89 | 90 | // expect(scope.effects.length).toBe(2) 91 | expect(scope.effects[0]).toBeInstanceOf(EffectScope) 92 | 93 | expect(dummy).toBe(0) 94 | counter.num = 7 95 | await nextTick() 96 | expect(dummy).toBe(7) 97 | expect(doubled).toBe(14) 98 | 99 | // stop the nested scope as well 100 | scope.stop() 101 | 102 | counter.num = 6 103 | await nextTick() 104 | expect(dummy).toBe(7) 105 | expect(doubled).toBe(14) 106 | }) 107 | 108 | it('nested scope can be escaped', async () => { 109 | let dummy, doubled 110 | const counter = reactive({ num: 0 }) 111 | 112 | const scope = new EffectScope() 113 | scope.run(() => { 114 | watchEffect(() => (dummy = counter.num)) 115 | // nested scope 116 | new EffectScope(true).run(() => { 117 | watchEffect(() => (doubled = counter.num * 2)) 118 | }) 119 | }) 120 | 121 | expect(scope.effects.length).toBe(0) 122 | 123 | expect(dummy).toBe(0) 124 | counter.num = 7 125 | await nextTick() 126 | expect(dummy).toBe(7) 127 | expect(doubled).toBe(14) 128 | 129 | scope.stop() 130 | 131 | counter.num = 6 132 | await nextTick() 133 | expect(dummy).toBe(7) 134 | 135 | // nested scope should not be stopped 136 | expect(doubled).toBe(12) 137 | }) 138 | 139 | it('able to run the scope', async () => { 140 | let dummy, doubled 141 | const counter = reactive({ num: 0 }) 142 | 143 | const scope = new EffectScope() 144 | scope.run(() => { 145 | watchEffect(() => (dummy = counter.num)) 146 | }) 147 | 148 | // expect(scope.effects.length).toBe(1) 149 | 150 | scope.run(() => { 151 | watchEffect(() => (doubled = counter.num * 2)) 152 | }) 153 | 154 | // expect(scope.effects.length).toBe(2) 155 | 156 | counter.num = 7 157 | await nextTick() 158 | expect(dummy).toBe(7) 159 | expect(doubled).toBe(14) 160 | 161 | scope.stop() 162 | }) 163 | 164 | it('can not run an inactive scope', async () => { 165 | let dummy, doubled 166 | const counter = reactive({ num: 0 }) 167 | 168 | const scope = new EffectScope() 169 | scope.run(() => { 170 | watchEffect(() => (dummy = counter.num)) 171 | }) 172 | 173 | // expect(scope.effects.length).toBe(1) 174 | 175 | scope.stop() 176 | 177 | scope.run(() => { 178 | watchEffect(() => (doubled = counter.num * 2)) 179 | }) 180 | 181 | expect( 182 | '[Vue warn]: cannot run an inactive effect scope.' 183 | ).toHaveBeenWarned() 184 | 185 | // expect(scope.effects.length).toBe(1) 186 | 187 | counter.num = 7 188 | await nextTick() 189 | expect(dummy).toBe(0) 190 | expect(doubled).toBe(undefined) 191 | }) 192 | 193 | it('should fire onScopeDispose hook', () => { 194 | let dummy = 0 195 | 196 | const scope = new EffectScope() 197 | scope.run(() => { 198 | onScopeDispose(() => (dummy += 1)) 199 | onScopeDispose(() => (dummy += 2)) 200 | }) 201 | 202 | scope.run(() => { 203 | onScopeDispose(() => (dummy += 4)) 204 | }) 205 | 206 | expect(dummy).toBe(0) 207 | 208 | scope.stop() 209 | expect(dummy).toBe(7) 210 | }) 211 | 212 | it('should warn onScopeDispose() is called when there is no active effect scope', () => { 213 | const spy = vi.fn() 214 | const scope = new EffectScope() 215 | scope.run(() => { 216 | onScopeDispose(spy) 217 | }) 218 | 219 | expect(spy).toHaveBeenCalledTimes(0) 220 | 221 | onScopeDispose(spy) 222 | 223 | expect( 224 | '[Vue warn]: onScopeDispose() is called when there is no active effect scope to be associated with.' 225 | ).toHaveBeenWarned() 226 | 227 | scope.stop() 228 | expect(spy).toHaveBeenCalledTimes(1) 229 | }) 230 | 231 | it('test with higher level APIs', async () => { 232 | const r = ref(1) 233 | 234 | const computedSpy = vi.fn() 235 | const watchSpy = vi.fn() 236 | const watchEffectSpy = vi.fn() 237 | 238 | let c: ComputedRef 239 | const scope = new EffectScope() 240 | scope.run(() => { 241 | c = computed(() => { 242 | computedSpy() 243 | return r.value + 1 244 | }) 245 | 246 | watch(r, watchSpy) 247 | watchEffect(() => { 248 | watchEffectSpy() 249 | r.value 250 | }) 251 | }) 252 | 253 | c!.value // computed is lazy so trigger collection 254 | expect(computedSpy).toHaveBeenCalledTimes(1) 255 | expect(watchSpy).toHaveBeenCalledTimes(0) 256 | expect(watchEffectSpy).toHaveBeenCalledTimes(1) 257 | 258 | r.value++ 259 | c!.value 260 | await nextTick() 261 | expect(computedSpy).toHaveBeenCalledTimes(2) 262 | expect(watchSpy).toHaveBeenCalledTimes(1) 263 | expect(watchEffectSpy).toHaveBeenCalledTimes(2) 264 | 265 | scope.stop() 266 | 267 | r.value++ 268 | c!.value 269 | await nextTick() 270 | // should not trigger anymore 271 | expect(computedSpy).toHaveBeenCalledTimes(2) 272 | expect(watchSpy).toHaveBeenCalledTimes(1) 273 | expect(watchEffectSpy).toHaveBeenCalledTimes(2) 274 | }) 275 | 276 | it('should stop along with parent component', async () => { 277 | let dummy, doubled 278 | const counter = reactive({ num: 0 }) 279 | 280 | const root = document.createElement('div') 281 | const vm = createApp({ 282 | setup() { 283 | const scope = new EffectScope() 284 | scope.run(() => { 285 | watchEffect(() => (dummy = counter.num)) 286 | watchEffect(() => (doubled = counter.num * 2)) 287 | }) 288 | }, 289 | }) 290 | vm.mount(root) 291 | 292 | expect(dummy).toBe(0) 293 | counter.num = 7 294 | await nextTick() 295 | expect(dummy).toBe(7) 296 | expect(doubled).toBe(14) 297 | 298 | vm.unmount() 299 | 300 | counter.num = 6 301 | await nextTick() 302 | expect(dummy).toBe(7) 303 | expect(doubled).toBe(14) 304 | }) 305 | 306 | it('component should be a valid scope', async () => { 307 | let dummy = 0 308 | let scope 309 | 310 | const root = document.createElement('div') 311 | const vm = createApp({ 312 | setup() { 313 | scope = getCurrentScope() 314 | onScopeDispose(() => (dummy += 1)) 315 | scope?.cleanups.push(() => (dummy += 1)) 316 | }, 317 | }) 318 | 319 | vm.mount(root) 320 | expect(dummy).toBe(0) 321 | expect(scope).not.toBeFalsy() 322 | 323 | vm.unmount() 324 | await nextTick() 325 | expect(dummy).toBe(2) 326 | }) 327 | }) 328 | -------------------------------------------------------------------------------- /test/v3/reactivity/set.spec.ts: -------------------------------------------------------------------------------- 1 | import { set, reactive, ref, watch, watchEffect } from '../../../src' 2 | 3 | describe('reactivity/set', () => { 4 | it('should not trigger reactivity on native object member assignment', () => { 5 | const obj = reactive<{ a?: number }>({}) 6 | const spy = vi.fn() 7 | watch(obj, spy, { deep: true, flush: 'sync' }) 8 | obj.a = 1 9 | expect(spy).not.toHaveBeenCalled() 10 | expect(obj).toStrictEqual({ a: 1 }) 11 | }) 12 | 13 | it('should trigger reactivity when using set on reactive object', () => { 14 | const obj = reactive<{ a?: number }>({}) 15 | const spy = vi.fn() 16 | watch(obj, spy, { deep: true, flush: 'sync' }) 17 | set(obj, 'a', 1) 18 | expect(spy).toBeCalledTimes(1) 19 | expect(obj).toStrictEqual({ a: 1 }) 20 | }) 21 | 22 | it('should trigger watchEffect when using set to change value of array length', () => { 23 | const arr = ref([1, 2, 3]) 24 | const spy = vi.fn() 25 | watchEffect( 26 | () => { 27 | spy(arr.value) 28 | }, 29 | { flush: 'sync' } 30 | ) 31 | 32 | expect(spy).toHaveBeenCalledTimes(1) 33 | set(arr.value, 'length', 1) 34 | expect(arr.value.length).toBe(1) 35 | expect(spy).toHaveBeenCalledTimes(2) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/v3/runtime-core/apiInject.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | provide, 4 | inject, 5 | InjectionKey, 6 | ref, 7 | nextTick, 8 | Ref, 9 | reactive, 10 | defineComponent, 11 | createApp, 12 | } from '../../../src' 13 | import { mockWarn } from '../../helpers' 14 | 15 | describe('api: provide/inject', () => { 16 | mockWarn(true) 17 | it('string keys', async () => { 18 | const Provider = { 19 | setup() { 20 | provide('foo', 1) 21 | return () => h(Middle) 22 | }, 23 | } 24 | 25 | const Middle = { 26 | setup() { 27 | return () => h(Consumer) 28 | }, 29 | } 30 | 31 | const Consumer = { 32 | setup() { 33 | const foo = inject('foo') 34 | return () => h('div', foo as unknown as string) 35 | }, 36 | } 37 | 38 | const root = document.createElement('div') 39 | const vm = createApp(Provider).mount(root) 40 | expect(vm.$el.outerHTML).toBe(`
1
`) 41 | }) 42 | 43 | it('symbol keys', () => { 44 | // also verifies InjectionKey type sync 45 | const key: InjectionKey = Symbol() 46 | 47 | const Provider = { 48 | setup() { 49 | provide(key, 1) 50 | return () => h(Middle) 51 | }, 52 | } 53 | 54 | const Middle = { 55 | setup() { 56 | return () => h(Consumer) 57 | }, 58 | } 59 | 60 | const Consumer = { 61 | setup() { 62 | const foo = inject(key) || 1 63 | return () => h('div', (foo + 1) as unknown as string) 64 | }, 65 | } 66 | 67 | const root = document.createElement('div') 68 | const vm = createApp(Provider).mount(root) 69 | expect(vm.$el.outerHTML).toBe(`
2
`) 70 | }) 71 | 72 | it('default values', () => { 73 | const Provider = { 74 | setup() { 75 | provide('foo', 'foo') 76 | return () => h(Middle) 77 | }, 78 | } 79 | 80 | const Middle = { 81 | setup() { 82 | return () => h(Consumer) 83 | }, 84 | } 85 | 86 | const Consumer = { 87 | setup() { 88 | // default value should be ignored if value is provided 89 | const foo = inject('foo', 'fooDefault') 90 | // default value should be used if value is not provided 91 | const bar = inject('bar', 'bar') 92 | return () => h('div', (foo + bar) as unknown as string) 93 | }, 94 | } 95 | 96 | const root = document.createElement('div') 97 | const vm = createApp(Provider).mount(root) 98 | expect(vm.$el.outerHTML).toBe(`
foobar
`) 99 | }) 100 | 101 | it('bound to instance', () => { 102 | const Provider = { 103 | setup() { 104 | return () => h(Consumer) 105 | }, 106 | } 107 | 108 | const Consumer = defineComponent({ 109 | name: 'Consumer', 110 | inject: { 111 | foo: { 112 | from: 'foo', 113 | default() { 114 | return this!.$options.name 115 | }, 116 | }, 117 | }, 118 | render() { 119 | return h('div', this.foo as unknown as string) 120 | }, 121 | }) 122 | 123 | const root = document.createElement('div') 124 | const vm = createApp(Provider).mount(root) 125 | expect(vm.$el.outerHTML).toBe(`
Consumer
`) 126 | }) 127 | 128 | it('nested providers', () => { 129 | const ProviderOne = { 130 | setup() { 131 | provide('foo', 'foo') 132 | provide('bar', 'bar') 133 | return () => h(ProviderTwo) 134 | }, 135 | } 136 | 137 | const ProviderTwo = { 138 | setup() { 139 | // override parent value 140 | provide('foo', 'fooOverride') 141 | provide('baz', 'baz') 142 | return () => h(Consumer) 143 | }, 144 | } 145 | 146 | const Consumer = { 147 | setup() { 148 | const foo = inject('foo') 149 | const bar = inject('bar') 150 | const baz = inject('baz') 151 | return () => h('div', [foo, bar, baz].join(',') as unknown as string) 152 | }, 153 | } 154 | 155 | const root = document.createElement('div') 156 | const vm = createApp(ProviderOne).mount(root) 157 | expect(vm.$el.outerHTML).toBe(`
fooOverride,bar,baz
`) 158 | }) 159 | 160 | it('reactivity with refs', async () => { 161 | const count = ref(1) 162 | 163 | const Provider = { 164 | setup() { 165 | provide('count', count) 166 | return () => h(Middle) 167 | }, 168 | } 169 | 170 | const Middle = { 171 | setup() { 172 | return () => h(Consumer) 173 | }, 174 | } 175 | 176 | const Consumer = { 177 | setup() { 178 | const count = inject>('count')! 179 | return () => h('div', count.value as unknown as string) 180 | }, 181 | } 182 | 183 | const root = document.createElement('div') 184 | const vm = createApp(Provider).mount(root) 185 | expect(vm.$el.outerHTML).toBe(`
1
`) 186 | 187 | count.value++ 188 | await nextTick() 189 | expect(vm.$el.outerHTML).toBe(`
2
`) 190 | }) 191 | 192 | it('reactivity with objects', async () => { 193 | const rootState = reactive({ count: 1 }) 194 | 195 | const Provider = { 196 | setup() { 197 | provide('state', rootState) 198 | return () => h(Middle) 199 | }, 200 | } 201 | 202 | const Middle = { 203 | setup() { 204 | return () => h(Consumer) 205 | }, 206 | } 207 | 208 | const Consumer = { 209 | setup() { 210 | const state = inject('state')! 211 | return () => h('div', state.count as unknown as string) 212 | }, 213 | } 214 | 215 | const root = document.createElement('div') 216 | const vm = createApp(Provider).mount(root) 217 | expect(vm.$el.outerHTML).toBe(`
1
`) 218 | 219 | rootState.count++ 220 | await nextTick() 221 | expect(vm.$el.outerHTML).toBe(`
2
`) 222 | }) 223 | 224 | it('should warn unfound', () => { 225 | const Provider = { 226 | setup() { 227 | return () => h(Consumer) 228 | }, 229 | } 230 | 231 | const Consumer = { 232 | setup() { 233 | const foo = inject('foo') 234 | expect(foo).toBeUndefined() 235 | return () => h('div', foo as unknown as string) 236 | }, 237 | } 238 | 239 | const root = document.createElement('div') 240 | const vm = createApp(Provider).mount(root) 241 | expect(vm.$el.outerHTML).toBe(`
`) 242 | expect(`[Vue warn]: Injection "foo" not found.`).toHaveBeenWarned() 243 | }) 244 | 245 | it('should warn unfound w/ injectionKey is undefined', () => { 246 | const Provider = { 247 | setup() { 248 | return () => h(Consumer) 249 | }, 250 | } 251 | 252 | const Consumer = { 253 | setup() { 254 | // The emulation does not use TypeScript 255 | const foo = inject(undefined as unknown as string) 256 | expect(foo).toBeUndefined() 257 | return () => h('div', foo as unknown as string) 258 | }, 259 | } 260 | 261 | const root = document.createElement('div') 262 | const vm = createApp(Provider).mount(root) 263 | expect(vm.$el.outerHTML).toBe(`
`) 264 | expect(`[Vue warn]: injection "undefined" not found.`).toHaveBeenWarned() 265 | }) 266 | 267 | it('should not self-inject', () => { 268 | const Comp = { 269 | setup() { 270 | provide('foo', 'foo') 271 | const injection = inject('foo', null) 272 | return () => h('div', injection as unknown as string) 273 | }, 274 | } 275 | 276 | const root = document.createElement('div') 277 | const vm = createApp(Comp).mount(root) 278 | expect(vm.$el.outerHTML).toBe(`
foo
`) 279 | }) 280 | 281 | it('should not warn when default value is undefined', () => { 282 | const Provider = { 283 | setup() { 284 | provide('foo', undefined) 285 | return () => h(Middle) 286 | }, 287 | } 288 | 289 | const Middle = { 290 | setup() { 291 | return () => h(Consumer) 292 | }, 293 | } 294 | 295 | const Consumer = { 296 | setup() { 297 | const foo = inject('foo') 298 | return () => h('div', foo as unknown as string) 299 | }, 300 | } 301 | 302 | const root = document.createElement('div') 303 | const vm = createApp(Provider).mount(root) 304 | expect(vm.$el.outerHTML).toBe(`
`) 305 | expect(`injection "foo" not found.`).not.toHaveBeenWarned() 306 | }) 307 | }) 308 | -------------------------------------------------------------------------------- /test/v3/runtime-core/apiSetupHelpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createApp, 3 | defineComponent, 4 | SetupContext, 5 | useAttrs, 6 | useSlots, 7 | } from '../../../src' 8 | 9 | describe('SFC