├── .coveralls.yml ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs └── compare.png ├── examples ├── vite-vue2 │ ├── .gitignore │ ├── .vscode │ │ └── extensions.json │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ └── vue.svg │ │ ├── components │ │ │ └── HelloWorld.vue │ │ ├── main.ts │ │ ├── style.css │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── vite-vue3 │ ├── .gitignore │ ├── .vscode │ └── extensions.json │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── App.vue │ ├── assets │ │ └── vue.svg │ ├── components │ │ ├── class-setup.vue │ │ └── native.vue │ ├── main.ts │ ├── style.css │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── index.html ├── package.json ├── script ├── build-docs.ts ├── release.sh └── test-all.sh ├── src ├── computed.ts ├── config.ts ├── context.ts ├── define.ts ├── index.ts ├── options.ts ├── pass-on-to.ts ├── property-descriptors.ts ├── setup-reference.ts ├── setup.ts ├── types.ts ├── vue.ts └── watch.ts ├── tests ├── base-component-child.vue ├── base-component.spec.ts ├── base-component.vue ├── base.spec.ts ├── base.vue ├── boolean-props.spec.ts ├── boolean-props.vue ├── context.spec.ts ├── demo.spec.ts ├── demo.vue ├── extend-options.spec.ts ├── extend-options.vue ├── extend.spec.ts ├── extend.vue ├── kebab-case-props-child.vue ├── kebab-case-props-parent.vue ├── kebab-case-props.spec.ts ├── make-up.spec.ts ├── make-up.vue ├── multiple-pass-on-to.spec.ts ├── multiple-pass-on-to.vue ├── options.spec.ts ├── props-bind-child.vue ├── props-bind-parent.vue ├── props-bind.spec.ts ├── props-child.vue ├── props-parent.vue ├── props.spec.ts ├── quick-start.spec.ts ├── quick-start.vue ├── register-hook.spec.ts ├── set-value.spec.ts ├── set-value.vue ├── use.spec.ts ├── use.vue ├── vue.spec.ts ├── watch-computed.spec.ts ├── watch-computed.vue ├── watch-effect.spec.ts ├── watch-effect.vue ├── watch.spec.ts └── watch.vue ├── tsconfig.json └── vite.config.ts /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: zaQVFPoDNH8C69eCILMzPzi9qhZEm1Dws 3 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: ['main'] 9 | pull_request: 10 | branches: ['main'] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [16.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | # with: 26 | # node-version: ${{ matrix.node-version }} 27 | # cache: 'npm' 28 | - run: yarn test:all 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | node_modules_back/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | yarn.lock 107 | .DS_Store 108 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | node_modules_back -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 4, 4 | semi: true, 5 | singleQuote: true, 6 | }; 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.4.4 2 | 3 | - fix: multiple inheritance unbinding object errors 4 | 5 | ## 1.4.3 6 | 7 | - fix: memory leak caused by unused `@Setup` decorator 8 | 9 | ## 1.4.2 10 | 11 | - fix: Type failure 12 | 13 | ## 1.4.1 14 | 15 | - fix: Type failure 16 | 17 | ## 1.4.0 18 | 19 | - fix: Type failure 20 | 21 | ## 1.3.9 22 | 23 | - fix: Type failure 24 | 25 | ## 1.3.8 26 | 27 | - types: fix `DeepReadonly` to `Readonly` 28 | 29 | ## 1.3.7 30 | 31 | - types: fix `VueInstance` type error 32 | 33 | ## 1.3.6 34 | 35 | - types: fix `VueInstance` type error 36 | 37 | ## 1.3.5 38 | 39 | - docs: modify README.md 40 | 41 | ## 1.3.4 42 | 43 | - docs: modify README.md 44 | 45 | ## 1.3.3 46 | 47 | - fix: Vue3 SSR default error 48 | 49 | ## 1.3.0 50 | 51 | - Same as `1.2.9` 52 | 53 | ## 1.2.9 54 | 55 | - feat: `Context` Support props and emit generics 56 | 57 | ## 1.2.8 58 | 59 | - fix: `$vm` return Vue instance on Vue2 60 | 61 | ## 1.2.7 62 | 63 | - chore: Update keywords 64 | 65 | ## 1.2.6 66 | 67 | - fix: Vue3 optimization set default props 68 | 69 | ## 1.2.5 70 | 71 | - fix: Default Boolean value error 72 | 73 | ## 1.2.4 74 | 75 | - feat: `DefaultProps` and `DefaultEmit` exports 76 | 77 | ## 1.2.3 78 | 79 | - fix: props default value error on Vue3 80 | 81 | ## 1.2.2 82 | 83 | - fix: `@PassOnTo(onServerPrefetch)` type error on Vue2 84 | 85 | ## 1.2.1 86 | 87 | - feat: App.use 88 | 89 | ## 1.2.0 90 | 91 | - fix: Set default props error on Vue2 92 | 93 | ## 1.1.9 94 | 95 | - fix: props name style is kebab case type error 96 | - fix: not found: Error: Can't resolve 'vue-class-setup' 97 | 98 | ## 1.1.8 99 | 100 | - fix: Set props default error 101 | 102 | ## 1.1.7 103 | 104 | - docs: Optimize document picture capitalization 105 | 106 | ## 1.1.6 107 | 108 | - fix: Improve unit testing and boundary treatment 109 | 110 | ## 1.1.5 111 | 112 | - feat: Inject to defineComponent and bind this 113 | 114 | ## 1.1.3 115 | 116 | - fix: Vite ssr build error 117 | 118 | ## 1.1.2 119 | 120 | - fix: Vite ssr build error 121 | 122 | ## 1.1.1 123 | 124 | - feat: Watch decorator 125 | 126 | ## 1.1.0 127 | 128 | - Fix: Assemble class cannot be used 129 | 130 | ## 1.0.8 131 | 132 | - feat: Set default props on class 133 | 134 | ## 1.0.7 135 | 136 | - feat: isVue2, isVue3, getCurrentInstance, VueInstance 137 | 138 | ## 1.0.6 139 | 140 | - fix: Define Generic (optional) 141 | 142 | ## 1.0.5 143 | 144 | - feat: $props 145 | - Remove: Remove parameter passing in 146 | 147 | ## 1.0.4 148 | 149 | - feat: Remove execution 150 | 151 | ## 1.0.3 152 | 153 | - feat: Define basic class 154 | 155 | ## 1.0.2 156 | 157 | - feat: packages.json "exports" 158 | 159 | ## 1.0.1 160 | 161 | - fix: Delete unnecessary judgment 162 | 163 | ## 1.0.0 164 | 165 | - feat: Remove `Hook` and add `PassOnTo` 166 | - feat: `getCurrentHookContext` method 167 | 168 | ## 0.1.4 169 | 170 | - fix: Export path error 171 | 172 | ## 0.1.3 173 | 174 | - fix: Initialization hook sequence error 175 | 176 | ## 0.0.9 177 | 178 | - feat: `registerHook` api 179 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Followme Frontend Team 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-class-setup 2 | 3 | Use class style to write setup and support vue2 and vue3 4 | 5 | [![Build Status](https://github.com/fmfe/vue-class-setup/workflows/CI/badge.svg)](https://github.com/fmfe/vue-class-setup/actions) 6 | Coverage Status 7 | [![npm](https://img.shields.io/npm/v/vue-class-setup.svg)](https://www.npmjs.com/package/vue-class-setup) 8 | [![npm](https://img.shields.io/npm/dm/vue-class-setup.svg)](https://www.npmjs.com/package/vue-class-setup) 9 | [![npm](https://img.shields.io/npm/dt/vue-class-setup.svg)](https://www.npmjs.com/package/vue-class-setup) 10 | 11 | ## Why? 12 | 13 | Using class can help you avoid `ref`, `reactive` , `computed` and `withDefaults`, and significantly reduce your mental burden and better organize your code. It supports vue2 and vue3 at the same time. After gzip compression, it is only 1KB 14 | 15 | ## vue-class-component vs vue-class-setup 16 | 17 | **We should deprecate [vue-class-component](https://github.com/vuejs/vue-class-component/issues/569), and use class in setup** 18 | | List | vue-class-component | vue-class-setup | 19 | | --- | --- | ----------- | 20 | | Vue2 | ✅ | ✅ | 21 | | Vue3 | ❌ | ✅ | 22 | | Method bind this | ✅ | ✅ | 23 | | Props type check | ❌ | ✅ | 24 | | Emit type check | ❌ | ✅ | 25 | | Watch type check | ❌ | ✅ | 26 | | Multiple class instances | ❌ | ✅ | 27 | | Class attribute sets the default value of the prop | ❌ | ✅ | 28 | 29 | ## Install 30 | 31 | ```bash 32 | npm install vue-class-setup 33 | # or 34 | yarn add vue-class-setup 35 | ``` 36 | 37 | ## Quick start 38 | 39 | 40 | 41 | ```vue 42 | 65 | 71 | ``` 72 | 73 | 74 | 75 | `Setup` and `Context` collect dependency information together, and convert it into a Vue program after executing the subclass constructor 76 | 77 | ## Setup 78 | 79 | If the component defines `props`, writing the `class` in the `setup` will cause the `setup` function to create a `class` every time as it executes, which will add costs. So we should avoid writing `class` in `setup` and use `Define` basic class to receive `props` and `emit`. 80 | 81 | ### Context and Define 82 | 83 | `Context` automatic injection `$vm`, `Define` extend from `Context`, and `Define` will automatically inject `$props` and `$emit`, when encapsulating public classes, you may not want to inject props and emit 84 | 85 | ### Best practices 86 | 87 | 88 | 89 | ```vue 90 | 122 | 148 | 155 | ``` 156 | 157 | 158 | 159 | ### Multiple class instances 160 | 161 | When the business is complex, multiple instances should be split 162 | 163 | 164 | 165 | ```vue 166 | 211 | 215 | 219 | ``` 220 | 221 | 222 | 223 | ### PassOnTo 224 | 225 | This `callback` will be called back after the `Test class` instantiation is completed, and the decorated function will be passed in, and the TS can check whether the type is correct 226 | 227 | ```ts 228 | @Setup 229 | class App extends Define { 230 | @PassOnTo(myFunc) 231 | public init(name: string) {} 232 | } 233 | 234 | function myFunc(callback: (name: string) => void) { 235 | // ... 236 | } 237 | ``` 238 | 239 | If `PassOnTo` does not pass in a handler, it is called after `reactive` and `computed` execution are completed, You should avoid watching in the `constructor` because it may not have `reactive` 240 | 241 | ```ts 242 | import { Watch } from 'vue'; 243 | 244 | @Setup 245 | class App extends Define { 246 | public value = 0; 247 | @PassOnTo() 248 | private setup() { 249 | // You can safely watch, but it is recommended to use the Watch decorator 250 | watch( 251 | () => this.value, 252 | (value) => { 253 | // ... 254 | } 255 | ); 256 | } 257 | } 258 | ``` 259 | 260 | ### Watch 261 | 262 | It can correctly identify the type 263 | 264 | 265 | 266 | ```vue 267 | 293 | 296 | 301 | ``` 302 | 303 | 304 | 305 | ## Get the injection object in setup 306 | 307 | Useful when `defineExpose` 308 | 309 | 310 | 311 | ```vue 312 | 333 | 340 | 347 | ``` 348 | 349 | 350 | 351 | ## Vue compatible 352 | 353 | - `getCurrentInstance` returns the proxy object by default 354 | - `VueInstance` It is not easy to get a Vue instance object type compatible with vue2 and vue3. We make it easy 355 | 356 | ```ts 357 | import { 358 | isVue2, 359 | isVue3, 360 | getCurrentInstance, 361 | type VueInstance, 362 | } from 'vue-class-setup'; 363 | 364 | // isVue2 -> boolean 365 | // isVue3 -> boolean 366 | // getCurrentInstance -> VueInstance 367 | ``` 368 | -------------------------------------------------------------------------------- /docs/compare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dp-os/vue-class-setup/ba5c91bc270304c71336a54adf261687f293cdbd/docs/compare.png -------------------------------------------------------------------------------- /examples/vite-vue2/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/vite-vue2/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/vite-vue2/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vite-vue2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-vue2", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc --noEmit && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "vue": "^2.7.8" 13 | }, 14 | "devDependencies": { 15 | "vue-class-setup": "file:../../", 16 | "@vitejs/plugin-vue2": "^1.1.2", 17 | "typescript": "^4.6.4", 18 | "vite": "^3.0.7", 19 | "vue-tsc": "^0.39.5" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/vite-vue2/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-vue2/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 34 | -------------------------------------------------------------------------------- /examples/vite-vue2/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-vue2/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 25 | 28 | 29 | 61 | 62 | 67 | -------------------------------------------------------------------------------- /examples/vite-vue2/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import './style.css'; 3 | import App from './App.vue'; 4 | 5 | const app = new Vue({ 6 | render(h) { 7 | return h(App); 8 | }, 9 | }); 10 | app.$mount('#app'); 11 | -------------------------------------------------------------------------------- /examples/vite-vue2/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | .card { 60 | padding: 2em; 61 | } 62 | 63 | #app { 64 | max-width: 1280px; 65 | margin: 0 auto; 66 | padding: 2rem; 67 | text-align: center; 68 | } 69 | 70 | @media (prefers-color-scheme: light) { 71 | :root { 72 | color: #213547; 73 | background-color: #ffffff; 74 | } 75 | a:hover { 76 | color: #747bff; 77 | } 78 | button { 79 | background-color: #f9f9f9; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /examples/vite-vue2/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue'; 5 | const component: DefineComponent<{}, {}, any>; 6 | export default component; 7 | } 8 | -------------------------------------------------------------------------------- /examples/vite-vue2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "experimentalDecorators": true, 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "esModuleInterop": true, 14 | "lib": ["ESNext", "DOM"], 15 | "skipLibCheck": true 16 | }, 17 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 18 | "references": [{ "path": "./tsconfig.node.json" }] 19 | } 20 | -------------------------------------------------------------------------------- /examples/vite-vue2/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/vite-vue2/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue2'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | }); 8 | -------------------------------------------------------------------------------- /examples/vite-vue3/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/vite-vue3/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/vite-vue3/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vite-vue3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-vue3", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc --noEmit && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "vue": "^3.2.37" 13 | }, 14 | "devDependencies": { 15 | "vue-class-setup": "file:../../", 16 | "@vitejs/plugin-vue": "^3.0.3", 17 | "typescript": "^4.6.4", 18 | "vite": "^3.0.7", 19 | "vue-tsc": "^0.39.5" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/vite-vue3/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-vue3/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 15 | -------------------------------------------------------------------------------- /examples/vite-vue3/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-vue3/src/components/class-setup.vue: -------------------------------------------------------------------------------- 1 | 36 | 39 | 56 | 68 | -------------------------------------------------------------------------------- /examples/vite-vue3/src/components/native.vue: -------------------------------------------------------------------------------- 1 | 26 | 43 | 55 | -------------------------------------------------------------------------------- /examples/vite-vue3/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import './style.css'; 3 | import App from './App.vue'; 4 | 5 | createApp(App).mount('#app'); 6 | -------------------------------------------------------------------------------- /examples/vite-vue3/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | } 30 | 31 | h1 { 32 | font-size: 3.2em; 33 | line-height: 1.1; 34 | } 35 | @media (prefers-color-scheme: light) { 36 | :root { 37 | color: #213547; 38 | background-color: #ffffff; 39 | } 40 | a:hover { 41 | color: #747bff; 42 | } 43 | button { 44 | background-color: #f9f9f9; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/vite-vue3/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue'; 5 | const component: DefineComponent<{}, {}, any>; 6 | export default component; 7 | } 8 | -------------------------------------------------------------------------------- /examples/vite-vue3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "esModuleInterop": true, 14 | "lib": ["ESNext", "DOM"], 15 | "skipLibCheck": true 16 | }, 17 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 18 | "references": [{ "path": "./tsconfig.node.json" }] 19 | } 20 | -------------------------------------------------------------------------------- /examples/vite-vue3/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/vite-vue3/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | }); 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vue-class-setup 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-class-setup", 3 | "version": "1.4.4", 4 | "main": "dist/index.cjs.js", 5 | "module": "dist/index.es.js", 6 | "types": "dist/index.d.ts", 7 | "repository": "git@github.com:fmfe/vue-class-setup.git", 8 | "author": "<1340641314@qq.com>", 9 | "license": "MIT", 10 | "description": "Use class style to write setup and support vue2 and vue3", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/index.d.ts", 14 | "import": { 15 | "node": "./dist/index.mjs", 16 | "default": "./dist/index.es.js" 17 | }, 18 | "require": "./dist/index.cjs.js" 19 | }, 20 | "./package.json": "./package.json" 21 | }, 22 | "sideEffects": false, 23 | "files": [ 24 | "src", 25 | "dist", 26 | "tests" 27 | ], 28 | "keywords": [ 29 | "vue-class", 30 | "vue-class-component", 31 | "vue-property-decorator", 32 | "vue-class-composition", 33 | "vue-class-composition-api" 34 | ], 35 | "scripts": { 36 | "lint": "prettier --write .", 37 | "dev": "vite", 38 | "build": "vue-tsc --noEmit && vite build && cp dist/index.es.js ./dist/index.mjs", 39 | "preview": "vite preview", 40 | "test": "vitest", 41 | "test:all": "./script/test-all.sh", 42 | "coverage": "vitest run --coverage", 43 | "coveralls": "coveralls < coverage/lcov.info", 44 | "release": "yarn test:all && ./script/release.sh" 45 | }, 46 | "peerDependencies": { 47 | "vue": ">=2.7.8 || >=3.0.0" 48 | }, 49 | "devDependencies": { 50 | "@vitejs/plugin-vue": "^3.0.1", 51 | "@vue/test-utils": "^2.0.2", 52 | "c8": "^7.12.0", 53 | "coveralls": "^3.1.1", 54 | "happy-dom": "^6.0.4", 55 | "prettier": "^2.7.1", 56 | "typescript": "^4.7.4", 57 | "vite": "^3.0.3", 58 | "vite-plugin-dts": "^1.4.0", 59 | "vitest": "^0.20.2", 60 | "vue": "^3.2.37", 61 | "vue-tsc": "^0.40.1" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /script/build-docs.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | export function buildDocs() { 4 | const filename = path.resolve('README.md'); 5 | let list = fs.readFileSync(filename, 'utf-8').split('\n'); 6 | const startRe = //g; 7 | const endRe = //g; 8 | let exist = false; 9 | const newList: string[] = []; 10 | list.forEach((text) => { 11 | if (exist) { 12 | if (endRe.test(text)) { 13 | exist = false; 14 | const file = newList[newList.length - 1].match(/file:(.*?) /); 15 | if (file) { 16 | const filename = path.resolve(file[1]); 17 | let fileText = fs.readFileSync(filename, 'utf-8'); 18 | const ext = filename.match(/\.([A-z]+)$/); 19 | if (ext) { 20 | fileText = 21 | '\n```' + 22 | (ext[1] || '') + 23 | '\n' + 24 | fileText + 25 | '```\n'; 26 | } else { 27 | fileText = '\n```\n' + fileText + '```\n'; 28 | } 29 | newList.push(fileText); 30 | } 31 | newList.push(text); 32 | return; 33 | } 34 | return; 35 | } 36 | if (startRe.test(text)) { 37 | exist = true; 38 | } 39 | newList.push(text); 40 | }); 41 | const text = newList.join('\n'); 42 | fs.writeFileSync(filename, text, 'utf-8'); 43 | } 44 | -------------------------------------------------------------------------------- /script/release.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -e 3 | version=`node -e "console.log(require('./package.json').version)"` 4 | 5 | npm publish --registry=https://registry.npmjs.org 6 | 7 | git add . 8 | git commit -m "release: vue-class-setup@$version" 9 | git push 10 | git tag v$version 11 | git push origin v$version -------------------------------------------------------------------------------- /script/test-all.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -e 3 | 4 | rm -rf node_modules 5 | rm -rf node_modules_back 6 | yarn install 7 | yarn lint 8 | yarn run build 9 | yarn run coverage 10 | 11 | # mv node_modules node_modules_back 12 | 13 | # cd examples/vite-vue2 14 | # rm -rf node_modules 15 | # yarn install 16 | # yarn run build 17 | # cd ../../ 18 | 19 | # cd examples/vite-vue3 20 | # rm -rf node_modules 21 | # yarn install 22 | # yarn run build 23 | # cd ../../ 24 | 25 | # mv node_modules_back node_modules 26 | yarn run coveralls 27 | -------------------------------------------------------------------------------- /src/computed.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue'; 2 | import { createDefineProperty } from './property-descriptors'; 3 | 4 | export function initComputed( 5 | target: object, 6 | descriptor: Map 7 | ) { 8 | const defineProperty = createDefineProperty(target); 9 | descriptor.forEach((value, key) => { 10 | let get = value.get; 11 | if (get) { 12 | get = get.bind(target); 13 | const compute = computed(get); 14 | defineProperty(key, { 15 | ...value, 16 | get() { 17 | return compute.value; 18 | }, 19 | }); 20 | } 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const SETUP_OPTIONS_NAME = 'setupOptions'; 2 | export const SETUP_NAME = 'setup'; 3 | export const SETUP_PROPERTY_DESCRIPTOR = 'setupPropertyDescriptor'; 4 | export const SETUP_SETUP_DEFINE = '__setupDefine'; 5 | export const SETUP_USE = '__setupUse__'; 6 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance, type VueInstance, isVue3 } from './vue'; 2 | import { bindTarget } from './setup-reference'; 3 | import { TargetName, Target } from './types'; 4 | import { 5 | SETUP_NAME, 6 | SETUP_OPTIONS_NAME, 7 | SETUP_PROPERTY_DESCRIPTOR, 8 | } from './config'; 9 | import { createDefineProperty } from './property-descriptors'; 10 | import { DefineConstructor } from './define'; 11 | import { SETUP_SETUP_DEFINE, SETUP_USE } from './config'; 12 | 13 | let currentTarget: Target | null = null; 14 | let currentName: TargetName | null = null; 15 | 16 | export function getCurrentHookContext(): { target: object; name: TargetName } { 17 | if (currentTarget === null || currentName === null) { 18 | throw new Error('Can only be obtained in hook functions'); 19 | } 20 | return { target: currentTarget, name: currentName }; 21 | } 22 | 23 | export function setCurrentHookTarget(target: Target | null) { 24 | currentTarget = target; 25 | } 26 | export function setCurrentHookName(name: TargetName | null) { 27 | currentName = name; 28 | } 29 | 30 | const WHITE_LIST: string[] = [SETUP_SETUP_DEFINE, '$vm', '$emit', '$props']; 31 | 32 | function use(vm: VueInstance, _This: any) { 33 | let use: Map>; 34 | if (vm[SETUP_USE]) { 35 | use = vm[SETUP_USE]; 36 | } else { 37 | use = new Map(); 38 | Object.defineProperty(vm, SETUP_USE, { 39 | get() { 40 | return use; 41 | }, 42 | }); 43 | } 44 | let app = use.get(_This)!; 45 | if (app) { 46 | return app; 47 | } 48 | app = new _This() as InstanceType; 49 | 50 | use.set(_This, app); 51 | return app; 52 | } 53 | 54 | function proxyVue3Props(app: InstanceType, vm: VueInstance) { 55 | interface Item { 56 | ssrRender?: Function; 57 | render?: Function; 58 | } 59 | if (!vm.$) { 60 | return; 61 | } 62 | const item = vm.$ as Item; 63 | const render = item.ssrRender || item.render; 64 | if (app[SETUP_SETUP_DEFINE] && render) { 65 | const keys = Object.keys(app.$defaultProps); 66 | if (!keys.length) return; 67 | const proxyRender = (...args: any[]) => { 68 | const props = vm.$props; 69 | const arr: { key: string; pd: PropertyDescriptor }[] = []; 70 | const dpp = createDefineProperty(props); 71 | // Set default Props 72 | keys.forEach((key) => { 73 | const value = app[key]; 74 | if (props[key] !== value) { 75 | const pd = Object.getOwnPropertyDescriptor(props, key); 76 | if (!pd) return; 77 | dpp(key, { ...pd, value }); 78 | arr.push({ 79 | key, 80 | pd, 81 | }); 82 | } 83 | }); 84 | const res = render.apply(vm, args); 85 | arr.forEach((item) => { 86 | dpp(item.key, item.pd); 87 | }); 88 | // Resume default props 89 | return res; 90 | }; 91 | 92 | if (item.ssrRender) { 93 | item.ssrRender = proxyRender; 94 | } else if (item.render) { 95 | item.render = proxyRender; 96 | } 97 | } 98 | } 99 | 100 | function initInject(app: InstanceType, vm: VueInstance) { 101 | if (isVue3) { 102 | proxyVue3Props(app, vm); 103 | } 104 | 105 | const names = Object.getOwnPropertyNames(app); 106 | const defineProperty = createDefineProperty(vm); 107 | const propertyDescriptor = app.constructor[ 108 | SETUP_PROPERTY_DESCRIPTOR 109 | ] as Map; 110 | names.forEach((name) => { 111 | if (propertyDescriptor.has(name) || WHITE_LIST.includes(name)) return; 112 | defineProperty(name, { 113 | get() { 114 | return app[name]; 115 | }, 116 | set(val) { 117 | app[name] = val; 118 | }, 119 | }); 120 | }); 121 | propertyDescriptor.forEach((value, name) => { 122 | if (WHITE_LIST.includes(name)) return; 123 | defineProperty(name, { 124 | get() { 125 | return app[name]; 126 | }, 127 | set(val) { 128 | app[name] = val; 129 | }, 130 | }); 131 | }); 132 | } 133 | 134 | export type DefaultProps = Record; 135 | export type DefaultEmit = (...args: any[]) => void; 136 | 137 | export class Context { 138 | public static [SETUP_NAME] = false; 139 | public static [SETUP_OPTIONS_NAME] = new Map(); 140 | public static [SETUP_PROPERTY_DESCRIPTOR] = new Map< 141 | string, 142 | PropertyDescriptor 143 | >(); 144 | public static use any>(this: T) { 145 | const vm = getCurrentInstance(); 146 | if (!vm) { 147 | throw Error('Please run in the setup function'); 148 | } 149 | return use(vm, this) as InstanceType; 150 | } 151 | public static inject any>(this: T) { 152 | const _This = this; 153 | 154 | const options: any = { 155 | created() { 156 | const vm = this as any as VueInstance; 157 | const app = use(vm, _This); 158 | initInject(app, vm); 159 | }, 160 | }; 161 | 162 | return options as { 163 | data: () => Omit, `$${string}`>; 164 | created(): void; 165 | }; 166 | } 167 | public $vm: VueInstance; 168 | public $emit: E; 169 | public constructor() { 170 | const vm = getCurrentInstance(); 171 | this.$vm = vm ?? ({ $props: {}, $emit: emit } as any); 172 | this.$emit = this.$vm.$emit.bind(this.$vm) as E; 173 | bindTarget(this); 174 | } 175 | public get $props() { 176 | return (this.$vm.$props ?? {}) as T; 177 | } 178 | } 179 | function emit() {} 180 | -------------------------------------------------------------------------------- /src/define.ts: -------------------------------------------------------------------------------- 1 | import { type VueInstance, isVue2 } from './vue'; 2 | import { Context, DefaultProps, DefaultEmit } from './context'; 3 | import { createDefineProperty } from './property-descriptors'; 4 | import { SETUP_SETUP_DEFINE } from './config'; 5 | 6 | interface DefineInstance { 7 | readonly $props: T; 8 | readonly $emit: E; 9 | readonly $vm: VueInstance; 10 | readonly $defaultProps: Readonly>; 11 | } 12 | 13 | type DefineInstanceType< 14 | T extends DefaultProps, 15 | E extends DefaultEmit = DefaultEmit 16 | > = Readonly & DefineInstance; 17 | 18 | export interface DefineConstructor { 19 | inject: (typeof Context)['inject']; 20 | use: (typeof Context)['use']; 21 | setup: (typeof Context)['setup']; 22 | setupOptions: (typeof Context)['setupOptions']; 23 | setupDefine: boolean; 24 | setupPropertyDescriptor: Map; 25 | new ( 26 | ...args: any[] 27 | ): DefineInstanceType; 28 | } 29 | function GET_TRUE() { 30 | return true; 31 | } 32 | 33 | export const Define: DefineConstructor = class Define extends Context { 34 | public static setupDefine = true; 35 | public $defaultProps: Record = {}; 36 | public constructor() { 37 | super(); 38 | const defineProperty = createDefineProperty(this); 39 | defineProperty('$defaultProps', { 40 | enumerable: false, 41 | writable: false, 42 | }); 43 | defineProperty(SETUP_SETUP_DEFINE, { 44 | get: GET_TRUE, 45 | }); 46 | } 47 | } as any; 48 | 49 | export function initDefine(target: InstanceType) { 50 | const definePropertyTarget = createDefineProperty(target); 51 | const props = target.$props; 52 | 53 | Object.keys(props).forEach((k) => { 54 | if (k in target) { 55 | // @ts-ignore 56 | target.$defaultProps[k] = target[k]; 57 | const defaultProps = target.$defaultProps; 58 | definePropertyTarget(k, { 59 | get() { 60 | let value = props[k]; 61 | if (typeof value === 'boolean') { 62 | if (!hasDefaultValue(target.$vm, k)) { 63 | value = defaultProps[k]; 64 | } 65 | } else if (isNull(value)) { 66 | value = defaultProps[k]; 67 | } 68 | return value; 69 | }, 70 | }); 71 | } else { 72 | definePropertyTarget(k, { 73 | get() { 74 | return props[k]; 75 | }, 76 | }); 77 | } 78 | }); 79 | } 80 | 81 | function hasDefaultValue(vm: VueInstance, key: string): boolean { 82 | let props: Record | null = null; 83 | if (isVue2) { 84 | props = vm.$options && vm.$options['propsData']; 85 | } else { 86 | props = vm.$ && vm.$.vnode && vm.$.vnode.props; 87 | } 88 | if (props) { 89 | const value = props[key]; 90 | return !isNull(isNull(value) ? props[kebabCase(key)] : value); 91 | } 92 | return false; 93 | } 94 | 95 | function isNull(value: unknown) { 96 | return typeof value === 'undefined' || value === null; 97 | } 98 | 99 | const KEBAB_REGEX = /[A-Z]/g; 100 | 101 | function kebabCase(str: string) { 102 | return str 103 | .replace(KEBAB_REGEX, (match) => { 104 | return '-' + match.toLowerCase(); 105 | }) 106 | .replace(/^-/, ''); 107 | } 108 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Setup } from './setup'; 2 | export { PassOnTo } from './pass-on-to'; 3 | export { 4 | getCurrentHookContext, 5 | Context, 6 | type DefaultEmit, 7 | type DefaultProps, 8 | } from './context'; 9 | export { Define } from './define'; 10 | export { isVue2, isVue3, getCurrentInstance, type VueInstance } from './vue'; 11 | export { Watch } from './watch'; 12 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TargetConstructorOptions, 3 | PassOnToCallback, 4 | TargetName, 5 | } from './types'; 6 | import { SETUP_OPTIONS_NAME } from './config'; 7 | import { Context } from './context'; 8 | 9 | let currentOptions: TargetConstructorOptions = new Map(); 10 | let currentTarget: typeof Context | null = null; 11 | 12 | function getCurrentOptions() { 13 | return currentOptions; 14 | } 15 | 16 | function resetCurrentOptions() { 17 | currentOptions = new Map(); 18 | } 19 | 20 | export function getOptions(Target: typeof Context): TargetConstructorOptions { 21 | return Target[SETUP_OPTIONS_NAME]; 22 | } 23 | 24 | export function setOptions( 25 | Target: typeof Context, 26 | hook: PassOnToCallback, 27 | name: TargetName 28 | ) { 29 | if (!currentTarget) { 30 | currentTarget = Target; 31 | } else if (Target !== currentTarget) { 32 | console.error('@Setup is not set', currentTarget); 33 | throw new TypeError(`@Setup is not set `); 34 | } 35 | const currentOptions = getCurrentOptions(); 36 | const arr = currentOptions.get(hook); 37 | if (!arr) { 38 | currentOptions.set(hook, [name]); 39 | } else if (!arr.includes(name)) { 40 | arr.push(name); 41 | } 42 | } 43 | 44 | export function getSetupOptions(Target: typeof Context) { 45 | const child = getCurrentOptions(); 46 | const parent = getOptions(Target); 47 | parent.forEach((names, hook) => { 48 | const parentNames = [...names]; 49 | const childNames = child.get(hook); 50 | if (childNames) { 51 | childNames.forEach((name) => { 52 | if (!parentNames.includes(name)) { 53 | parentNames.push(name); 54 | } 55 | }); 56 | } 57 | child.set(hook, parentNames); 58 | }); 59 | resetCurrentOptions(); 60 | currentTarget = null; 61 | 62 | return child; 63 | } 64 | -------------------------------------------------------------------------------- /src/pass-on-to.ts: -------------------------------------------------------------------------------- 1 | import { TargetName, PassOnToCallback } from './types'; 2 | import { setOptions } from './options'; 3 | 4 | function onSetup(cb: () => void) { 5 | cb(); 6 | } 7 | 8 | export type TargetConstructor = new (...arg: any[]) => any; 9 | 10 | function PassOnTo any>( 11 | cb: PassOnToCallback = onSetup 12 | ) { 13 | return function PassOnTo( 14 | Target: object, 15 | name: TargetName, 16 | descriptor: TypedPropertyDescriptor< 17 | ( 18 | ...args: Parameters 19 | ) => ReturnType extends void ? any : ReturnType 20 | > 21 | ) { 22 | setOptions(Target as any, cb as any, name); 23 | }; 24 | } 25 | 26 | export { PassOnTo }; 27 | -------------------------------------------------------------------------------- /src/property-descriptors.ts: -------------------------------------------------------------------------------- 1 | const whitelist: string[] = ['constructor', '$props', '$emit']; 2 | 3 | export function getPropertyDescriptors(Target: new (...args: any) => any) { 4 | const list: PropertyDescriptor[] = []; 5 | const map = new Map(); 6 | while (Target && Target.prototype) { 7 | list.unshift(Object.getOwnPropertyDescriptors(Target.prototype)); 8 | Target = Object.getPrototypeOf(Target); 9 | } 10 | 11 | list.forEach((item) => { 12 | Object.keys(item).forEach((key) => { 13 | if (whitelist.includes(key)) { 14 | delete item[key]; 15 | return; 16 | } 17 | map.set(key, item[key]); 18 | }); 19 | }); 20 | 21 | return map; 22 | } 23 | 24 | export function createDefineProperty(target: object) { 25 | return (key: PropertyKey, value: PropertyDescriptor) => { 26 | Object.defineProperty(target, key, value); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/setup-reference.ts: -------------------------------------------------------------------------------- 1 | let count = 0; 2 | let isBind = false; 3 | 4 | export function addCount() { 5 | // 如果还是处于绑定状态,说明上一次解绑的过程中程序执行报错了,需要重置 6 | if (isBind) { 7 | isBind = false; 8 | count = 1; 9 | } else { 10 | count++; 11 | } 12 | } 13 | 14 | const weakMap = new WeakMap(); 15 | 16 | export function unBindTarget(target: object): boolean { 17 | let count = weakMap.get(target); 18 | if (typeof count === 'number') { 19 | count--; 20 | if (count) { 21 | weakMap.set(target, count); 22 | return false; 23 | } else { 24 | weakMap.delete(target); 25 | isBind = false; 26 | return true; 27 | } 28 | } 29 | return false; 30 | } 31 | 32 | export function bindTarget(target: object) { 33 | if (count > 0) { 34 | weakMap.set(target, count); 35 | count = 0; 36 | isBind = true; 37 | } else { 38 | console.warn(`The instance did not use the '@Setup' decorator`); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/setup.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue'; 2 | import { TargetName, PassOnToCallback } from './types'; 3 | import { setCurrentHookName, setCurrentHookTarget, Context } from './context'; 4 | import { 5 | SETUP_OPTIONS_NAME, 6 | SETUP_NAME, 7 | SETUP_PROPERTY_DESCRIPTOR, 8 | SETUP_SETUP_DEFINE, 9 | } from './config'; 10 | import { initComputed } from './computed'; 11 | import { getOptions, getSetupOptions } from './options'; 12 | import { initDefine } from './define'; 13 | import { addCount, unBindTarget } from './setup-reference'; 14 | import { getPropertyDescriptors } from './property-descriptors'; 15 | 16 | export type TargetConstructor = { 17 | use: (typeof Context)['use']; 18 | inject: (typeof Context)['inject']; 19 | setup: (typeof Context)['setup']; 20 | setupOptions: (typeof Context)['setupOptions']; 21 | setupPropertyDescriptor: Map; 22 | new (...args: any[]): any; 23 | }; 24 | 25 | function initHook(target: T) { 26 | setCurrentHookTarget(target); 27 | const Target: TargetConstructor = target.constructor as any; 28 | const options = getOptions(Target); 29 | const propertyDescriptor = Target.setupPropertyDescriptor; 30 | 31 | // bind this 32 | propertyDescriptor.forEach(({ value, writable }, key) => { 33 | if (typeof value === 'function' && writable) { 34 | target[key] = value.bind(target); 35 | } 36 | }); 37 | 38 | // init props 39 | if (target[SETUP_SETUP_DEFINE]) { 40 | initDefine(target as any); 41 | } 42 | 43 | // init computed 44 | initComputed(target, propertyDescriptor); 45 | 46 | // init PassOnTo 47 | options.forEach((names, hook) => { 48 | return names.forEach((name) => { 49 | if (name[0] !== '_') { 50 | initName(name, hook); 51 | } 52 | }); 53 | }); 54 | setCurrentHookTarget(null); 55 | 56 | function initName(name: TargetName, hook: PassOnToCallback) { 57 | setCurrentHookName(name); 58 | hook(target[name]); 59 | setCurrentHookName(null); 60 | } 61 | return target; 62 | } 63 | 64 | function Setup(Target: T) { 65 | class Setup extends Target { 66 | public static [SETUP_OPTIONS_NAME] = getSetupOptions(Target); 67 | public static [SETUP_NAME] = true; 68 | public static [SETUP_PROPERTY_DESCRIPTOR] = 69 | getPropertyDescriptors(Target); 70 | public constructor(...args: any[]) { 71 | addCount(); 72 | super(...args); 73 | if (unBindTarget(this)) { 74 | // Vue3 needs to return, vue2 does not need to return 75 | return initHook(reactive(this)); 76 | } 77 | } 78 | } 79 | return Setup; 80 | } 81 | 82 | export { Setup }; 83 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type TargetConstructorOptions = Map; 2 | export type Target = object; 3 | export type TargetName = string | symbol; 4 | export type PassOnToCallback any = () => void> = ( 5 | cb: T 6 | ) => void; 7 | -------------------------------------------------------------------------------- /src/vue.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance as get, version } from 'vue'; 2 | 3 | export const isVue2 = /^2\./.test(version); 4 | export const isVue3 = /^3\./.test(version); 5 | 6 | export function getCurrentInstance(): VueInstance | null { 7 | const vm = get(); 8 | if (vm && vm.proxy) { 9 | return vm.proxy as VueInstance; 10 | } 11 | return null; 12 | } 13 | 14 | type Instance = NonNullable>['proxy']>; 15 | 16 | type VueModule = typeof import('vue'); 17 | type Vue = VueModule extends { default: infer V } ? V : never; 18 | 19 | export type VueInstance = Vue extends never ? Instance : InstanceType; 20 | -------------------------------------------------------------------------------- /src/watch.ts: -------------------------------------------------------------------------------- 1 | import { watch, WatchOptions } from 'vue'; 2 | 3 | import { TargetName } from './types'; 4 | import { PassOnTo } from './pass-on-to'; 5 | import { getCurrentHookContext } from './context'; 6 | 7 | type OldValue = WatchOptions extends { immediate: true } 8 | ? V | undefined 9 | : V; 10 | 11 | export function Watch( 12 | watchName: Key, 13 | options?: Opt 14 | ) { 15 | return function Watch>( 16 | target: T, 17 | name: TargetName, 18 | descriptor: TypedPropertyDescriptor< 19 | (value: T[Key], oldValue: OldValue) => any 20 | > 21 | ) { 22 | PassOnTo(() => { 23 | const { target, name } = getCurrentHookContext(); 24 | watch( 25 | () => { 26 | return (target as any)[watchName]; 27 | }, 28 | target[name], 29 | options 30 | ); 31 | })(target, name, descriptor); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /tests/base-component-child.vue: -------------------------------------------------------------------------------- 1 | 33 | 59 | 66 | -------------------------------------------------------------------------------- /tests/base-component.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import BaseComponent from './base-component.vue'; 5 | 6 | test('Base', async () => { 7 | const wrapper = mount(BaseComponent); 8 | 9 | assert.equal(wrapper.find('.text').text(), '0'); 10 | assert.equal(wrapper.find('.props-value').text(), '0'); 11 | assert.equal(wrapper.find('.props-dest').text(), '--'); 12 | 13 | await wrapper.find('.btn').trigger('click'); 14 | 15 | assert.equal(wrapper.find('.text').text(), '1'); 16 | assert.equal(wrapper.find('.props-value').text(), '1'); 17 | assert.equal(wrapper.find('.props-dest').text(), 'clicked'); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/base-component.vue: -------------------------------------------------------------------------------- 1 | 14 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /tests/base.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import Base from './base.vue'; 5 | 6 | test('Base', async () => { 7 | const wrapper = mount(Base); 8 | assert.equal(wrapper.find('.value').text(), '0'); 9 | assert.equal(wrapper.find('.text').text(), 'value:0'); 10 | assert.equal(wrapper.find('.ready').text(), 'false'); 11 | const time = wrapper.find('.time').text(); 12 | 13 | await wrapper.find('button').trigger('click'); 14 | 15 | assert.equal(wrapper.find('p').text(), '1'); 16 | assert.equal(wrapper.find('.text').text(), 'value:1'); 17 | assert.equal(wrapper.find('.ready').text(), 'true'); 18 | assert.equal(wrapper.find('.time').text(), time); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/base.vue: -------------------------------------------------------------------------------- 1 | 31 | 34 | 43 | -------------------------------------------------------------------------------- /tests/boolean-props.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import BooleanProps from './boolean-props.vue'; 5 | 6 | test('Base', async () => { 7 | const wrapper = mount(BooleanProps, { 8 | props: { 9 | boolean2: false, 10 | boolean4: true, 11 | age2: 100, 12 | }, 13 | }); 14 | 15 | assert.equal(wrapper.find('.boolean1').text(), 'true'); 16 | assert.equal(wrapper.find('.boolean2').text(), 'false'); 17 | assert.equal(wrapper.find('.boolean3').text(), 'false'); 18 | assert.equal(wrapper.find('.boolean4').text(), 'true'); 19 | assert.equal(wrapper.find('.boolean5').text(), 'false'); 20 | assert.equal(wrapper.find('.boolean6').text(), 'false'); 21 | assert.equal(wrapper.find('.show-icon').text(), 'false'); 22 | assert.equal(wrapper.find('.age1').text(), '10'); 23 | assert.equal(wrapper.find('.age2').text(), '100'); 24 | 25 | wrapper.setProps({ 26 | boolean1: false, 27 | boolean3: true, 28 | // @ts-ignore 29 | boolean5: '', 30 | // @ts-ignore 31 | boolean6: '', 32 | // @ts-ignore 33 | showIcon: '', 34 | age1: 100, 35 | age2: 10, 36 | }); 37 | 38 | await wrapper.vm.$nextTick(); 39 | 40 | assert.equal(wrapper.find('.boolean1').text(), 'false'); 41 | assert.equal(wrapper.find('.boolean2').text(), 'false'); 42 | assert.equal(wrapper.find('.boolean3').text(), 'true'); 43 | assert.equal(wrapper.find('.boolean4').text(), 'true'); 44 | assert.equal(wrapper.find('.boolean5').text(), 'true'); 45 | assert.equal(wrapper.find('.boolean6').text(), 'true'); 46 | assert.equal(wrapper.find('.show-icon').text(), 'true'); 47 | assert.equal(wrapper.find('.age1').text(), '100'); 48 | assert.equal(wrapper.find('.age2').text(), '10'); 49 | 50 | wrapper.setProps({ 51 | boolean1: true, 52 | boolean4: false, 53 | // @ts-ignore 54 | age1: null, 55 | }); 56 | await wrapper.vm.$nextTick(); 57 | 58 | assert.equal(wrapper.find('.boolean1').text(), 'true'); 59 | assert.equal(wrapper.find('.boolean2').text(), 'false'); 60 | assert.equal(wrapper.find('.boolean3').text(), 'true'); 61 | assert.equal(wrapper.find('.boolean4').text(), 'false'); 62 | assert.equal(wrapper.find('.boolean5').text(), 'true'); 63 | assert.equal(wrapper.find('.boolean6').text(), 'true'); 64 | assert.equal(wrapper.find('.show-icon').text(), 'true'); 65 | assert.equal(wrapper.find('.age1').text(), '10'); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/boolean-props.vue: -------------------------------------------------------------------------------- 1 | 18 | 32 | 43 | -------------------------------------------------------------------------------- /tests/context.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, assert } from 'vitest'; 2 | import { Setup, Context } from 'vue-class-setup'; 3 | 4 | test('Context', () => { 5 | @Setup 6 | class Base extends Context { 7 | public age = 100; 8 | public get ageText() { 9 | return String(this.age); 10 | } 11 | public addAge() { 12 | this.age++; 13 | } 14 | } 15 | 16 | @Setup 17 | class Base2 extends Base { 18 | public ok = true; 19 | public num = 100; 20 | public setNum(num: number) { 21 | this.num = num; 22 | } 23 | } 24 | const base2 = {} as Base2; 25 | Base2.inject().created!.call(base2); 26 | assert.equal(base2.age, 100); 27 | assert.equal(base2.ageText, '100'); 28 | assert.isTrue(base2.ok); 29 | assert.equal(base2.num, 100); 30 | 31 | assert.isFunction(base2.addAge); 32 | assert.isFunction(base2.setNum); 33 | 34 | base2.addAge(); 35 | assert.equal(base2.age, 101); 36 | 37 | base2.setNum(200); 38 | assert.equal(base2.num, 200); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/demo.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import Demo from './demo.vue'; 5 | 6 | test('Base', async () => { 7 | const wrapper = mount(Demo); 8 | assert.equal(wrapper.find('p').text(), '0'); 9 | 10 | await wrapper.find('button').trigger('click'); 11 | 12 | assert.equal(wrapper.find('p').text(), '1'); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/demo.vue: -------------------------------------------------------------------------------- 1 | 24 | 30 | -------------------------------------------------------------------------------- /tests/extend-options.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import ExtendOptions from './extend-options.vue'; 5 | 6 | test('Base', async () => { 7 | const wrapper = mount(ExtendOptions); 8 | assert.equal(wrapper.find('p').text(), '0'); 9 | await wrapper.vm.$nextTick(); 10 | 11 | assert.equal(wrapper.find('p').text(), '3'); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/extend-options.vue: -------------------------------------------------------------------------------- 1 | 30 | 33 | -------------------------------------------------------------------------------- /tests/extend.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import Extend from './extend.vue'; 5 | 6 | test('Extend', async () => { 7 | const wrapper = mount(Extend); 8 | assert.equal(wrapper.find('.left').text(), 'value:2'); 9 | assert.equal(wrapper.find('.right').text(), '2'); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/extend.vue: -------------------------------------------------------------------------------- 1 | 46 | 50 | 54 | -------------------------------------------------------------------------------- /tests/kebab-case-props-child.vue: -------------------------------------------------------------------------------- 1 | 8 | 24 | 29 | -------------------------------------------------------------------------------- /tests/kebab-case-props-parent.vue: -------------------------------------------------------------------------------- 1 | 4 | 11 | -------------------------------------------------------------------------------- /tests/kebab-case-props.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import KebabCasePropsParent from './kebab-case-props-parent.vue'; 5 | 6 | test('Base', async () => { 7 | const wrapper = mount(KebabCasePropsParent); 8 | assert.equal(wrapper.find('.name').text(), 'vue-class-setup'); 9 | assert.equal(wrapper.find('.name-and-age').text(), '100'); 10 | assert.equal(wrapper.find('.ssss').text(), '50'); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/make-up.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import MakeUp from './make-up.vue'; 5 | 6 | test('Make up', async () => { 7 | const wrapper = mount(MakeUp); 8 | assert.equal(wrapper.find('.user-count').text(), '0'); 9 | assert.equal(wrapper.find('.blog-count').text(), '0'); 10 | assert.equal(wrapper.find('.total').text(), '0'); 11 | assert.equal(wrapper.find('.user-text').text(), '0'); 12 | assert.equal(wrapper.find('.blog-text').text(), '0'); 13 | assert.equal(wrapper.find('.user-text2').text(), '0user'); 14 | assert.equal(wrapper.find('.blog-text2').text(), '0blog'); 15 | 16 | await wrapper.find('.blog-add').trigger('click'); 17 | 18 | assert.equal(wrapper.find('.user-count').text(), '0'); 19 | assert.equal(wrapper.find('.blog-count').text(), '1'); 20 | assert.equal(wrapper.find('.total').text(), '1'); 21 | assert.equal(wrapper.find('.user-text').text(), '0'); 22 | assert.equal(wrapper.find('.blog-text').text(), '1'); 23 | assert.equal(wrapper.find('.user-text2').text(), '0user'); 24 | assert.equal(wrapper.find('.blog-text2').text(), '1blog'); 25 | 26 | await wrapper.find('.user-add').trigger('click'); 27 | 28 | assert.equal(wrapper.find('.user-count').text(), '1'); 29 | assert.equal(wrapper.find('.blog-count').text(), '1'); 30 | assert.equal(wrapper.find('.total').text(), '2'); 31 | assert.equal(wrapper.find('.user-text').text(), '1'); 32 | assert.equal(wrapper.find('.blog-text').text(), '1'); 33 | assert.equal(wrapper.find('.user-text2').text(), '1user'); 34 | assert.equal(wrapper.find('.blog-text2').text(), '1blog'); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/make-up.vue: -------------------------------------------------------------------------------- 1 | 38 | 41 | 54 | -------------------------------------------------------------------------------- /tests/multiple-pass-on-to.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import Demo from './multiple-pass-on-to.vue'; 5 | 6 | test('Multiple PassOnTo', async () => { 7 | const wrapper = mount(Demo); 8 | assert.equal(wrapper.find('.text').text(), '1'); 9 | assert.equal(wrapper.find('.btn').text(), 'false'); 10 | await wrapper.vm.$nextTick(); 11 | assert.equal(wrapper.find('.btn').text(), 'true'); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/multiple-pass-on-to.vue: -------------------------------------------------------------------------------- 1 | 23 | 26 | 30 | -------------------------------------------------------------------------------- /tests/options.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { Setup, Context, PassOnTo } from 'vue-class-setup'; 3 | 4 | test('options', () => { 5 | class Test extends Context { 6 | @PassOnTo() 7 | public init() {} 8 | } 9 | assert.Throw(() => { 10 | @Setup 11 | class Test2 extends Context { 12 | @PassOnTo() 13 | public init() {} 14 | } 15 | }, '@Setup is not set'); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/props-bind-child.vue: -------------------------------------------------------------------------------- 1 | 19 | 35 | 43 | -------------------------------------------------------------------------------- /tests/props-bind-parent.vue: -------------------------------------------------------------------------------- 1 | 38 | 41 | 46 | -------------------------------------------------------------------------------- /tests/props-bind.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import PropsBindParent from './props-bind-parent.vue'; 5 | 6 | test('Props-bind', async () => { 7 | const wrapper = mount(PropsBindParent); 8 | assert.equal(wrapper.find('.code').text(), '0'); 9 | assert.equal(wrapper.find('.name').text(), 'vue-class-setup'); 10 | assert.equal(wrapper.find('.age').text(), '1'); 11 | assert.equal(wrapper.find('.ok1').text(), 'true'); 12 | assert.equal(wrapper.find('.ok2').text(), 'false'); 13 | 14 | // code = 1 15 | await wrapper.find('button').trigger('click'); 16 | 17 | assert.equal(wrapper.find('.code').text(), '1'); 18 | assert.equal(wrapper.find('.name').text(), 'parent'); 19 | assert.equal(wrapper.find('.age').text(), '2'); 20 | assert.equal(wrapper.find('.ok1').text(), 'false'); 21 | assert.equal(wrapper.find('.ok2').text(), 'true'); 22 | 23 | // code = 2 24 | await wrapper.find('button').trigger('click'); 25 | 26 | assert.equal(wrapper.find('.code').text(), '2'); 27 | assert.equal(wrapper.find('.name').text(), 'vue-class-setup'); 28 | assert.equal(wrapper.find('.age').text(), '1'); 29 | assert.equal(wrapper.find('.ok1').text(), 'true'); 30 | assert.equal(wrapper.find('.ok2').text(), 'false'); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/props-child.vue: -------------------------------------------------------------------------------- 1 | 25 | 39 | 46 | -------------------------------------------------------------------------------- /tests/props-parent.vue: -------------------------------------------------------------------------------- 1 | 16 | 19 | 26 | -------------------------------------------------------------------------------- /tests/props.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import PropsParent from './props-parent.vue'; 5 | 6 | test('Props parent', async () => { 7 | const wrapper = mount(PropsParent); 8 | assert.equal(wrapper.find('.parent-text').text(), '0'); 9 | assert.equal(wrapper.find('.root-text').text(), 'root:0'); 10 | assert.equal(wrapper.find('.child-text').text(), 'child:0'); 11 | assert.equal(wrapper.find('.name').text(), 'vue-class-setup'); 12 | 13 | await wrapper.find('.child-btn').trigger('click'); 14 | assert.equal(wrapper.find('.parent-text').text(), '1'); 15 | assert.equal(wrapper.find('.root-text').text(), 'root:1'); 16 | assert.equal(wrapper.find('.child-text').text(), 'child:1'); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/quick-start.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import QuickStart from './quick-start.vue'; 5 | 6 | test('Base', async () => { 7 | const wrapper = mount(QuickStart); 8 | assert.equal(wrapper.find('p').text(), '0'); 9 | assert.equal(wrapper.find('input').element.value, '0'); 10 | await wrapper.vm.$nextTick(); 11 | assert.equal(wrapper.find('p').text(), '1'); 12 | assert.equal(wrapper.find('input').element.value, '1'); 13 | 14 | await wrapper.find('input').setValue('100'); 15 | assert.equal(wrapper.find('p').text(), '100'); 16 | assert.equal(wrapper.find('input').element.value, '100'); 17 | 18 | await wrapper.find('input').setValue(''); 19 | assert.equal(wrapper.find('p').text(), '0'); 20 | assert.equal(wrapper.find('input').element.value, '0'); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/quick-start.vue: -------------------------------------------------------------------------------- 1 | 20 | 24 | 28 | -------------------------------------------------------------------------------- /tests/register-hook.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { 3 | Setup, 4 | Define, 5 | PassOnTo, 6 | getCurrentHookContext, 7 | } from 'vue-class-setup'; 8 | 9 | function myFunc() { 10 | const { target, name } = getCurrentHookContext(); 11 | target[name](); 12 | } 13 | 14 | test('Register hook', () => { 15 | @Setup 16 | class Count extends Define { 17 | public value = 100; 18 | @PassOnTo(myFunc) 19 | public add() { 20 | this.value++; 21 | } 22 | } 23 | 24 | const count = new Count(); 25 | const { add } = count; 26 | add(); 27 | assert.equal(count.value, 102); 28 | }); 29 | 30 | test('error', () => { 31 | assert.Throw(() => { 32 | return getCurrentHookContext(); 33 | }, 'Can only be obtained in hook functions'); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/set-value.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import SetValue from './set-value.vue'; 5 | 6 | test('Base', async () => { 7 | const wrapper = mount(SetValue); 8 | assert.equal(wrapper.find('.value').text(), '0'); 9 | assert.equal(wrapper.find('.count').text(), '0'); 10 | 11 | await wrapper.find('.value-btn').trigger('click'); 12 | 13 | assert.equal(wrapper.find('.value').text(), '1'); 14 | assert.equal(wrapper.find('.count').text(), '0'); 15 | 16 | await wrapper.find('.count-btn').trigger('click'); 17 | assert.equal(wrapper.find('.value').text(), '1'); 18 | assert.equal(wrapper.find('.count').text(), '100'); 19 | 20 | await wrapper.find('.count-btn2').trigger('click'); 21 | assert.equal(wrapper.find('.value').text(), '1'); 22 | assert.equal(wrapper.find('.count').text(), '49'); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/set-value.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 42 | -------------------------------------------------------------------------------- /tests/use.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import Use from './use.vue'; 5 | 6 | test('Use', async () => { 7 | const wrapper = mount(Use); 8 | assert.equal(wrapper.find('.text').text(), '0'); 9 | assert.equal(wrapper.find('.text-eq').text(), 'true'); 10 | 11 | await wrapper.find('button').trigger('click'); 12 | 13 | assert.equal(wrapper.find('.text').text(), '1'); 14 | assert.equal(wrapper.find('.text-eq').text(), 'true'); 15 | 16 | wrapper.vm.addValue(); 17 | await wrapper.vm.$nextTick(); 18 | 19 | assert.equal(wrapper.find('.text').text(), '2'); 20 | assert.equal(wrapper.find('.text-eq').text(), 'true'); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/use.vue: -------------------------------------------------------------------------------- 1 | 22 | 29 | 36 | -------------------------------------------------------------------------------- /tests/vue.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { isVue3, isVue2 } from 'vue-class-setup'; 3 | 4 | test('vue', () => { 5 | assert.isFalse(isVue2); 6 | assert.isTrue(isVue3); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/watch-computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import WatchComputed from './watch-computed.vue'; 5 | 6 | test('Watch', async () => { 7 | const wrapper = mount(WatchComputed); 8 | assert.equal(wrapper.find('.value').text(), '0'); 9 | 10 | await wrapper.find('button').trigger('click'); 11 | 12 | assert.equal(wrapper.find('.value').text(), '1'); 13 | 14 | wrapper.vm.count.setValue(99); 15 | await wrapper.vm.$nextTick(); 16 | assert.equal(wrapper.find('.value').text(), '99'); 17 | 18 | wrapper.vm.count.setValue(110); 19 | await wrapper.vm.$nextTick(); 20 | assert.equal(wrapper.find('.value').text(), '100'); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/watch-computed.vue: -------------------------------------------------------------------------------- 1 | 30 | 34 | 38 | -------------------------------------------------------------------------------- /tests/watch-effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import WatchEffect from './watch-effect.vue'; 5 | 6 | test('Watch effect', async () => { 7 | const wrapper = mount(WatchEffect); 8 | 9 | assert.equal(wrapper.find('.value').text(), '0'); 10 | assert.deepEqual(wrapper.vm.count.hooks, [ 11 | 'watchPreEffect', 12 | 'watchSyncEffect', 13 | 'beforeMount', 14 | 'watchPostEffect', 15 | ]); 16 | 17 | await wrapper.find('button.add').trigger('click'); 18 | 19 | assert.equal(wrapper.find('.value').text(), '1'); 20 | assert.deepEqual(wrapper.vm.count.hooks, [ 21 | 'watchSyncEffect', 22 | 'watchPreEffect', 23 | 'beforeUpdate', 24 | 'watchPostEffect', 25 | 'updated', 26 | ]); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/watch-effect.vue: -------------------------------------------------------------------------------- 1 | 48 | 52 | 58 | -------------------------------------------------------------------------------- /tests/watch.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import Watch from './watch.vue'; 5 | 6 | test('Watch', async () => { 7 | const wrapper = mount(Watch); 8 | assert.equal(wrapper.find('.value').text(), '0'); 9 | assert.equal(wrapper.find('.immediate-value').text(), '10'); 10 | 11 | await wrapper.find('button').trigger('click'); 12 | 13 | assert.equal(wrapper.find('.value').text(), '1'); 14 | assert.equal(wrapper.find('.immediate-value').text(), '11'); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/watch.vue: -------------------------------------------------------------------------------- 1 | 27 | 30 | 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "noImplicitAny": false, 5 | "target": "esnext", 6 | "useDefineForClassFields": true, 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "strict": true, 10 | "jsx": "preserve", 11 | "sourceMap": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "skipLibCheck": true, 16 | "paths": { 17 | "vue-class-setup": ["./src/index.ts"] 18 | } 19 | }, 20 | "exclude": ["coverage", "node_modules", "examples", "node_modules_back"] 21 | } 22 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { resolve } from 'path'; 3 | import { defineConfig } from 'vite'; 4 | import dts from 'vite-plugin-dts'; 5 | import vue2 from '@vitejs/plugin-vue'; 6 | import { buildDocs } from './script/build-docs'; 7 | 8 | buildDocs(); 9 | 10 | export default defineConfig({ 11 | // @ts-ignore 12 | test: { 13 | globals: true, 14 | environment: 'happy-dom', 15 | coverage: { 16 | reporter: ['lcov', 'html'], 17 | }, 18 | }, 19 | plugins: [ 20 | vue2(), 21 | dts({ 22 | afterDiagnostic(list) { 23 | if (list.length) { 24 | process.exit(1); 25 | } 26 | }, 27 | }), 28 | { 29 | name: 'build-docs', 30 | buildEnd: buildDocs, 31 | }, 32 | ], 33 | build: { 34 | lib: { 35 | formats: ['cjs', 'es'], 36 | entry: resolve(__dirname, 'src/index'), 37 | fileName(format) { 38 | return `index.${format}.js`; 39 | }, 40 | }, 41 | minify: false, 42 | rollupOptions: { 43 | external: ['vue'], 44 | }, 45 | }, 46 | resolve: { 47 | alias: { 48 | 'vue-class-setup': resolve('./src/index'), 49 | }, 50 | }, 51 | }); 52 | --------------------------------------------------------------------------------