├── .babelrc ├── .eslintrc.js ├── .gitignore ├── webpack.config.js ├── package.json ├── tsconfig.json ├── README.md └── src └── main.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "useBuiltIns": "entry", // or "usage" 5 | "corejs": 3 6 | }] 7 | ] 8 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | node: true 9 | }, 10 | plugins: [], 11 | extends: [ 12 | 'eslint:recommended' 13 | ], 14 | rules: {} 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /lib 4 | package-lock.json 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | *.zip 25 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | module.exports = { 5 | devtool: false, 6 | entry: './src/main.ts', 7 | output: { 8 | path: path.resolve(__dirname, './lib'), 9 | publicPath: '/lib/', 10 | filename: 'index.js', 11 | libraryTarget: 'umd', 12 | umdNamedDefine: true 13 | }, 14 | module: { 15 | rules: [{ 16 | test: /\.(ts|tsx)$/, 17 | exclude: /node_modules/, 18 | use: [{ 19 | loader: 'ts-loader', /* https://github.com/TypeStrong/ts-loader */ 20 | options: { 21 | configFile: path.resolve(__dirname, './tsconfig.json'), 22 | appendTsSuffixTo: [/\.vue$/], 23 | transpileOnly: true, 24 | } 25 | }] 26 | }] 27 | }, 28 | plugins: [ 29 | new webpack.DefinePlugin({ 30 | 'process.env': { 31 | NODE_ENV: JSON.stringify('production') 32 | } 33 | }) 34 | ], 35 | target: ['web', 'es5'] 36 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue2-to-composition-api", 3 | "version": "2.0.0", 4 | "description": "Vue2 Opitons api to Vue 3 Composition api", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib", 8 | "src" 9 | ], 10 | "keywords": [ 11 | "vue2 opitons api to vue3 composition api", 12 | "vue2 opitons api", 13 | "vue3 composition api", 14 | "vue api conversion", 15 | "to vue3" 16 | ], 17 | "homepage": "http://wd3322.gitee.io/to-vue3/", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/wd3322/vue2-to-composition-api" 21 | }, 22 | "author": "wd", 23 | "license": "MIT", 24 | "scripts": { 25 | "clean": "npx rimraf node_modules", 26 | "build": "webpack --config webpack.config.js" 27 | }, 28 | "dependencies": { 29 | "core-js": "^3.6.5", 30 | "js-beautify": "^1.14.4" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.15.0", 34 | "@babel/preset-env": "^7.15.0", 35 | "babel-loader": "^8.2.2", 36 | "ts-loader": "^9.3.1", 37 | "typescript": "^4.7.4", 38 | "webpack": "^5.73.0", 39 | "webpack-cli": "^5.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ 7 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 8 | "lib": [ 9 | "es6", 10 | "dom", 11 | "es2017" 12 | ], /* Specify library files to be included in the compilation. */ 13 | // "allowJs": true, /* Allow javascript files to be compiled. */ 14 | "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | // "outDir": "./", /* Redirect output structure to the directory. */ 17 | "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | /* Strict Type-Checking Options */ 19 | "strict": true, /* Enable all strict type-checking options. */ 20 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 21 | /* Advanced Options */ 22 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 23 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 24 | }, 25 | "include": [ 26 | "src/**/*.ts", 27 | "src/**/*.tsx", 28 | "src/**/*.vue", 29 | ], 30 | "exclude": [ 31 | "node_modules", 32 | "dist" 33 | ] 34 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue2 Opitons api to Vue 3 Composition api 2 | 3 | ## 网站 4 | 5 | [Gitee: vue2-to-composition-api](https://wd3322.gitee.io/to-vue3/) 6 | 7 | [Github: vue2-to-composition-api](https://wd3322.github.io/to-vue3/) 8 | 9 | **Git地址** 10 | 11 | [Gitee](https://gitee.com/wd3322/vue2-to-composition-api) 12 | 13 | [Github](https://github.com/wd3322/vue2-to-composition-api) 14 | 15 | ## 在线使用 16 | ![home](https://wd3322.github.io/to-vue3/img/home.png) 17 | 18 | ## Props / Data 数据转换 19 | ![props_data](https://wd3322.github.io/to-vue3/img/props_data.png) 20 | 21 | ## Computed 计算器属性转换 22 | ![computed](https://wd3322.github.io/to-vue3/img/computed.png) 23 | 24 | ## Watch 侦听器转换 25 | ![watch](https://wd3322.github.io/to-vue3/img/watch.png) 26 | 27 | ## Life cycle 生命周期转换 28 | ![life_cycle](https://wd3322.github.io/to-vue3/img/life_cycle.png) 29 | 30 | ## Methods 方法转换 31 | ![methods](https://wd3322.github.io/to-vue3/img/methods.png) 32 | 33 | --- 34 | 35 | ## Template中的Data变更 36 | 37 | 转换后需为 `Template` 中的 `Data` 数据需加上 `.data` 前缀,其原因是许多开发者在Options API语法中做了改变引用类型数据地址的行为(如下),`Data` 将会被转换为一个完整的对象以兼容此类操作,此方式额外产生的迭代成本更小 38 | 39 | **Options API:** 40 | 41 | ```html 42 | 45 | ``` 46 | 47 | ```javascript 48 | export default { 49 | name: 'Sample', 50 | data() { 51 | return { 52 | userInfo: {} 53 | } 54 | }, 55 | created() { 56 | this.userInfo = { name: 'Casey Adams', age: 80 } 57 | } 58 | } 59 | ``` 60 | 61 | **Composition API:** 62 | 63 | ```html 64 | 67 | ``` 68 | 69 | ```javascript 70 | import { reactive } from 'vue' 71 | 72 | const data = reactive({ 73 | userInfo: {} 74 | }) 75 | 76 | data.userInfo = { name: 'Casey Adams', age: 80 } 77 | ``` 78 | 79 | --- 80 | 81 | ## Template中的Filter变更 82 | 83 | `Filter` 已经被废弃,它将会被转换为外部的 `Function` 内容,需要在 `Template` 中改变 `Filter` 的调用方式 84 | 85 | **Options API** 86 | 87 | ```html 88 | 91 | ``` 92 | 93 | ```javascript 94 | export default { 95 | name: 'Sample', 96 | filters: { 97 | toMarried(value) { 98 | return value ? 'Yes' : 'No' 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | **Composition API:** 105 | 106 | ```html 107 | 110 | ``` 111 | 112 | ```javascript 113 | function toMarried(value) { 114 | return value ? 'Yes' : 'No' 115 | } 116 | ``` 117 | 118 | ## Vue2.7中延用Router3.x、Vuex3.x 119 | 120 | 如若不想在 `Vue2.7` 项目中更新 `Router4, Vuex4` ,可以从 `Vue` 实例中获取 `Router, Route, Store` 121 | 122 | ```javascript 123 | import { getCurrentInstance } from 'vue' 124 | 125 | const $vm = getCurrentInstance() 126 | const router = $vm.proxy.$router 127 | const route = $vm.proxy.$route 128 | const store = $vm.proxy.$store 129 | ``` 130 | 131 | --- 132 | 133 | ## 无法解析的内容 134 | 135 | 动态变量或者拼接的内容无法被解析或解析错误 136 | 137 | ```javascript 138 | export default { 139 | methods: { 140 | onSubmit(propName) { 141 | this[propName] = '123' 142 | this.$emit(propName + '-change') 143 | } 144 | } 145 | } 146 | ``` 147 | 148 | --- 149 | 150 | Package: vue2-to-composition-api 151 | 152 | E-mail: diquick@qq.com 153 | 154 | Author: wd3322 155 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * package: vue2-to-composition-api 3 | * e-mail: diquick@qq.com 4 | * author: wd3322 5 | */ 6 | 7 | declare global { 8 | interface Window { 9 | Vue2ToCompositionApiVmBody: any, 10 | require: any 11 | } 12 | } 13 | 14 | interface VmContent { 15 | props: any, 16 | data: Function, 17 | dataOptions: any, 18 | computed: any, 19 | watch: any, 20 | methods: any, 21 | filters: any, 22 | hooks: any, 23 | emits: string[], 24 | refs: string[], 25 | use: any, 26 | import: any 27 | } 28 | 29 | interface VmKeys { 30 | props: string[], 31 | data: string[], 32 | computed: string[], 33 | watch: string[], 34 | methods: string[], 35 | filters: string[], 36 | hooks: string[], 37 | use: Function, 38 | import: Function 39 | } 40 | 41 | interface VmOutput { 42 | import: string, 43 | use: string, 44 | props: string, 45 | emits: string, 46 | refs: string, 47 | data: string, 48 | computed: string, 49 | watch: string, 50 | hooks: string, 51 | methods: string, 52 | filters: string 53 | } 54 | 55 | interface VmSetContentMethods { 56 | props: Function, 57 | data: Function, 58 | computed: Function, 59 | watch: Function, 60 | hooks: Function, 61 | methods: Function, 62 | filters: Function, 63 | emits: Function, 64 | refs: Function, 65 | use: Function, 66 | import: Function, 67 | output: Function 68 | } 69 | 70 | interface UtilsMethods { 71 | getContentStr: Function, 72 | replaceKey: Function, 73 | replaceValue: Function, 74 | addImport: Function, 75 | addUse: Function 76 | } 77 | 78 | function getPrototype(value: any): string { 79 | return Object.prototype.toString.call(value).replace(/^\[object (\S+)\]$/, '$1').toLowerCase() 80 | } 81 | 82 | function Vue2ToCompositionApi( 83 | entryScriptContent: string = '', 84 | options: { 85 | isDebug?: boolean 86 | } = { 87 | isDebug: false 88 | } 89 | ): string | undefined { 90 | if (getPrototype(entryScriptContent) !== 'string') { 91 | throw new Error(`Vue2ToCompositionApi ${entryScriptContent} is not a string`) 92 | } 93 | if (getPrototype(options) !== 'object') { 94 | throw new Error(`Vue2ToCompositionApi ${options} is not a object`) 95 | } 96 | try { 97 | // output script content init 98 | let outputScriptContent: string = '' 99 | 100 | // js-beautify init 101 | const jsBeautify: any = require('js-beautify') 102 | const jsBeautifyOptions: any = { 103 | indent_size: 4, 104 | indent_char: '', 105 | indent_with_tabs: true, 106 | eol: '\n', 107 | brace_style: 'collapse-preserve-inline' 108 | } 109 | 110 | // reg-exp init 111 | const braceRegExp: RegExp = /\{[\s\S]*\}/g 112 | const parenthesisRegExp: RegExp = /\((.*)\)/g 113 | 114 | // vm body init 115 | window.Vue2ToCompositionApiVmBody = {} 116 | window.require = function() {} 117 | const beautifyScriptContent: string = jsBeautify(entryScriptContent, jsBeautifyOptions) 118 | const modelScriptContent: string | undefined = (function() { 119 | const componentsRegExp: RegExp = /components: ((\{\})|(\{[\s\S]+?\}))[\,\n]/ 120 | const mixinsRegExp: RegExp = /mixins: ((\[\])|(\[([\s\S]+?)\]))[\,\n]/ 121 | return beautifyScriptContent 122 | .match(braceRegExp)?.[0] 123 | .replace(componentsRegExp, '') 124 | .replace(mixinsRegExp, '') 125 | })() 126 | if (modelScriptContent) { 127 | eval(`window.Vue2ToCompositionApiVmBody = ${modelScriptContent}`) 128 | } else { 129 | throw new Error(`Vue2ToCompositionApi entry script content not a valid content`) 130 | } 131 | const vmBody: any = window.Vue2ToCompositionApiVmBody 132 | 133 | // vm content init 134 | const vmContent: VmContent = { 135 | props: getPrototype(vmBody.props) === 'object' ? vmBody.props : {}, 136 | data: getPrototype(vmBody.data).indexOf('function') !== -1 ? vmBody.data : () => ({}), 137 | dataOptions: getPrototype(vmBody.data).indexOf('function') !== -1 ? vmBody.data() : {}, 138 | computed: getPrototype(vmBody.computed) === 'object' ? vmBody.computed : {}, 139 | watch: getPrototype(vmBody.watch) === 'object' ? vmBody.watch : {}, 140 | methods: getPrototype(vmBody.methods) === 'object' ? vmBody.methods : {}, 141 | filters: getPrototype(vmBody.filters) === 'object' ? vmBody.filters : {}, 142 | hooks: {}, 143 | emits: [], 144 | refs: [], 145 | use: {}, 146 | import: { vue: [], 'vue-router': [], vuex: [] } 147 | } 148 | 149 | // vm hooks content init 150 | for (const prop in vmBody) { 151 | if ( 152 | ['beforeCreate', 'created', 'beforeMount', 'mounted', 153 | 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 154 | 'activated', 'deactivated', 'errorCaptured'].includes(prop) && 155 | getPrototype(vmBody[prop]).indexOf('function') !== -1 156 | ) { 157 | vmContent.hooks[prop] = vmBody[prop] 158 | } 159 | } 160 | 161 | // vm keys init 162 | const vmKeys: VmKeys = { 163 | props: Object.keys(vmContent.props), 164 | data: Object.keys(vmContent.dataOptions), 165 | computed: Object.keys(vmContent.computed), 166 | watch: Object.keys(vmContent.watch), 167 | methods: Object.keys(vmContent.methods), 168 | filters: Object.keys(vmContent.filters), 169 | hooks: Object.keys(vmContent.hooks), 170 | use: (): string[] => Object.keys(vmContent.use), 171 | import: (): string[] => Object.keys(vmContent.import) 172 | } 173 | 174 | // vm output init 175 | const vmOutput: VmOutput = { 176 | import: '', 177 | use: '', 178 | props: '', 179 | emits: '', 180 | refs: '', 181 | data: '', 182 | computed: '', 183 | watch: '', 184 | hooks: '', 185 | methods: '', 186 | filters: '' 187 | } 188 | 189 | // vm set content methods init 190 | const vmSetContentMethods: VmSetContentMethods = { 191 | props(): void { 192 | if (vmKeys.props.length > 0) { 193 | const propsContentStr: string = utilMethods.getContentStr(vmContent.props, null, { 194 | function: (params: any) => { 195 | const { type, content } = params 196 | if (type === 'custom') { 197 | return content 198 | } 199 | } 200 | }) 201 | if (propsContentStr) { 202 | vmOutput.props = `const props = defineProps(${propsContentStr})` 203 | } 204 | } 205 | }, 206 | data(): void { 207 | if (vmKeys.data.length > 0) { 208 | const dataFunctionStr: string = utilMethods.getContentStr(vmContent.data, true, { 209 | function: (params: any) => { 210 | const { type, body } = params 211 | if (type === 'custom') { 212 | return body 213 | } 214 | } 215 | }) 216 | if (dataFunctionStr) { 217 | const dataContentRegExp: RegExp = /return ([\s\S]*)\}/ 218 | const dataContentStr: string = dataFunctionStr.match(dataContentRegExp)?.[1] || '{}' 219 | vmOutput.data = `const data = reactive(${dataContentStr})` 220 | utilMethods.addImport('vue', 'reactive') 221 | } 222 | } 223 | }, 224 | computed(): void { 225 | if (vmKeys.computed.length > 0) { 226 | const computedValues: string[] = [] 227 | for (const prop in vmContent.computed) { 228 | const computedContent: any = vmContent.computed[prop] 229 | if ( 230 | computedContent && 231 | ['object', 'function', 'asyncfunction'].includes(getPrototype(computedContent)) 232 | ) { 233 | const computedName: string = getPrototype(computedContent).indexOf('function') !== -1 ? computedContent.name : prop 234 | const computedFunctionStr: string = utilMethods.getContentStr(computedContent) 235 | if (computedName && computedFunctionStr) { 236 | computedValues.push(`const ${computedName} = computed(${computedFunctionStr})`) 237 | } 238 | } 239 | } 240 | if (computedValues.length > 0) { 241 | vmOutput.computed = computedValues.join('\n\n') 242 | utilMethods.addImport('vue', 'computed') 243 | } 244 | } 245 | }, 246 | watch(): void { 247 | if (vmKeys.watch.length > 0) { 248 | const watchValues: string[] = [] 249 | for (const prop in vmContent.watch) { 250 | const watchContent: any = vmContent.watch[prop] 251 | if (getPrototype(watchContent).indexOf('function') !== -1) { 252 | const watchName: string = utilMethods.replaceKey(watchContent.name) 253 | const watchFunctionStr: string = utilMethods.getContentStr(watchContent) 254 | if (watchName && watchFunctionStr) { 255 | watchValues.push(`watch(() => ${watchName}, ${watchFunctionStr})`) 256 | } 257 | } else if ( 258 | watchContent && 259 | getPrototype(watchContent) === 'object' && 260 | getPrototype(watchContent.handler).indexOf('function') !== -1 261 | ) { 262 | const watchName: string = utilMethods.replaceKey(prop) 263 | const watchFunctionStr: string = utilMethods.getContentStr(watchContent.handler) 264 | const watchOptionsStr: string = utilMethods.getContentStr(watchContent, null, { 265 | object: (params: any) => { 266 | const { value, values } = params 267 | if (value.handler) { 268 | const index = values.findIndex((item: string) => /^handler\:/.test(item)) 269 | values.splice(index, 1) 270 | } 271 | return values.length > 0 ? `{\n${values.join(',\n')}\n}` : '{}' 272 | } 273 | }) 274 | if (watchName && watchFunctionStr && watchOptionsStr) { 275 | watchValues.push( 276 | watchOptionsStr !== '{}' 277 | ? `watch(() => ${watchName}, ${watchFunctionStr}, ${watchOptionsStr})` 278 | : `watch(() => ${watchName}, ${watchFunctionStr})` 279 | ) 280 | } 281 | } 282 | } 283 | if (watchValues.length > 0) { 284 | vmOutput.watch = watchValues.join('\n\n') 285 | utilMethods.addImport('vue', 'watch') 286 | } 287 | } 288 | }, 289 | hooks(): void { 290 | if (vmKeys.hooks.length > 0) { 291 | const hookValues: string[] = [] 292 | for (const prop in vmContent.hooks) { 293 | const hookContent: any = vmContent.hooks[prop] 294 | if (getPrototype(hookContent).indexOf('function') !== -1) { 295 | if (['beforeCreate', 'created'].includes(hookContent.name)) { 296 | const hookName: string = `on${hookContent.name.substring(0, 1).toUpperCase()}${hookContent.name.substring(1)}` 297 | const hookFunctionStr: string = utilMethods.getContentStr(hookContent, null, { 298 | function: (params: any) => { 299 | const { type, value, arg, body } = params 300 | if (type === 'custom') { 301 | return value.constructor.name === 'AsyncFunction' 302 | ? `async function ${hookName} ${arg} ${body}\n${hookName}()` 303 | : `function ${hookName} ${arg} ${body}\n${hookName}()` 304 | } 305 | } 306 | }) 307 | if (hookName && hookFunctionStr) { 308 | hookValues.push(hookFunctionStr) 309 | } 310 | } else if ( 311 | ['beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 312 | 'activated', 'deactivated', 'errorCaptured'].includes(hookContent.name) 313 | ) { 314 | const v3HooksNameDist: any = { 315 | beforeMount: 'onBeforeMount', 316 | mounted: 'onMounted', 317 | beforeUpdate: 'onBeforeUpdate', 318 | updated: 'onUpdated', 319 | beforeDestroy: 'onBeforeUnmount', 320 | destroyed: 'onUnmounted', 321 | activated: 'onActivated', 322 | deactivated: 'onDeactivated', 323 | errorCaptured: 'onErrorCaptured' 324 | } 325 | const hookName: string = v3HooksNameDist[hookContent.name as string] 326 | const hookFunctionStr: string = utilMethods.getContentStr(hookContent, null, { 327 | function: (params: any) => { 328 | const { type, value, arg, body } = params 329 | if (type === 'custom') { 330 | return value.constructor.name === 'AsyncFunction' 331 | ? `${hookName} (async ${arg} => ${body})` 332 | : `${hookName} (${arg} => ${body})` 333 | } 334 | } 335 | }) 336 | if (hookName && hookFunctionStr) { 337 | hookValues.push(hookFunctionStr) 338 | utilMethods.addImport('vue', hookName) 339 | } 340 | } 341 | } 342 | } 343 | if (hookValues.length > 0) { 344 | vmOutput.hooks = hookValues.join('\n\n') 345 | } 346 | } 347 | }, 348 | methods(): void { 349 | if (vmKeys.methods.length > 0) { 350 | const methodValues: string[] = [] 351 | for (const prop in vmContent.methods) { 352 | const methodContent: any = vmContent.methods[prop] 353 | if (getPrototype(methodContent).indexOf('function') !== -1) { 354 | const methodName: string = methodContent.name 355 | const methodFunctionStr: string = utilMethods.getContentStr(methodContent, null, { 356 | function: (params: any) => { 357 | const { type, value, arg, body } = params 358 | if (type === 'custom') { 359 | return value.constructor.name === 'AsyncFunction' 360 | ? `async function ${methodName} ${arg} ${body}` 361 | : `function ${methodName} ${arg} ${body}` 362 | } 363 | } 364 | }) 365 | if (methodName && methodFunctionStr) { 366 | methodValues.push(methodFunctionStr) 367 | } 368 | } 369 | } 370 | if (methodValues.length > 0) { 371 | vmOutput.methods = methodValues.join('\n\n') 372 | } 373 | } 374 | }, 375 | filters(): void { 376 | if (vmKeys.filters.length > 0) { 377 | const filterValues: string[] = [] 378 | for (const prop in vmContent.filters) { 379 | const filterContent: any = vmContent.filters[prop] 380 | if (getPrototype(filterContent).indexOf('function') !== -1) { 381 | const filterName: string = filterContent.name 382 | const filterFunctionStr: string = utilMethods.getContentStr(filterContent, null, { 383 | function: (params: any) => { 384 | const { type, value, arg, body } = params 385 | if (type === 'custom') { 386 | return value.constructor.name === 'AsyncFunction' 387 | ? `async function ${filterName} ${arg} ${body}` 388 | : `function ${filterName} ${arg} ${body}` 389 | } 390 | } 391 | }) 392 | if (filterName && filterFunctionStr) { 393 | filterValues.push(filterFunctionStr) 394 | } 395 | } 396 | } 397 | if (filterValues.length > 0) { 398 | vmOutput.filters = filterValues.join('\n\n') 399 | } 400 | } 401 | }, 402 | emits(): void { 403 | if (vmContent.emits.length > 0) { 404 | const emitValues: string[] = [] 405 | for (const emit of vmContent.emits) { 406 | if (emit) { 407 | emitValues.push(`\'${emit}\'`) 408 | } 409 | } 410 | if (emitValues.length > 0) { 411 | vmOutput.emits = `const emit = defineEmits([${emitValues.join(', ')}])` 412 | } 413 | } 414 | }, 415 | refs(): void { 416 | if (vmContent.refs.length > 0) { 417 | const refValues: string[] = [] 418 | for (const ref of vmContent.refs) { 419 | if (ref) { 420 | refValues.push(`const ${ref} = ref(null)`) 421 | } 422 | } 423 | if (refValues.length > 0) { 424 | vmOutput.refs = refValues.join('\n') 425 | utilMethods.addImport('vue', 'ref') 426 | } 427 | } 428 | }, 429 | use(): void { 430 | if (vmKeys.use().length > 0) { 431 | const useValues: string[] = [] 432 | for (const prop in vmContent.use) { 433 | const useContent: string = vmContent.use[prop] 434 | if (useContent) { 435 | useValues.push(useContent) 436 | } 437 | } 438 | if (useValues.length > 0) { 439 | vmOutput.use = useValues.sort().join('\n') 440 | } 441 | } 442 | }, 443 | import(): void { 444 | if (vmKeys.import().length > 0) { 445 | const importValues: string[] = [] 446 | for (const prop in vmContent.import) { 447 | const importContent: string[] = vmContent.import[prop] 448 | if (importContent.length > 0) { 449 | importValues.push(`import { ${importContent.sort().join(', ')} } from \'${prop}\'`) 450 | } 451 | } 452 | if (importValues.length > 0) { 453 | vmOutput.import = importValues.join('\n') 454 | } 455 | } 456 | }, 457 | output(): void { 458 | const outputValues: string[] = [] 459 | for (const prop in vmOutput) { 460 | const outputContent: string = vmOutput[prop as keyof VmOutput] 461 | if (outputContent) { 462 | outputValues.push(outputContent) 463 | } 464 | } 465 | if (outputValues.length > 0) { 466 | outputScriptContent = outputValues.join('\n\n') 467 | } 468 | } 469 | } 470 | 471 | // util methods init 472 | const utilMethods: UtilsMethods = { 473 | getContentStr( 474 | value: any, 475 | replaceDataKeyToUseData: boolean = false, 476 | resultCallbackContent: { 477 | string?: Function, 478 | object?: Function, 479 | array?: Function, 480 | function?: Function, 481 | other?: Function, 482 | } = { 483 | string: undefined, 484 | object: undefined, 485 | array: undefined, 486 | function: undefined, 487 | other: undefined 488 | } 489 | ): string | undefined { 490 | let result: string = '' 491 | // string prototype 492 | if (getPrototype(value) === 'string') { 493 | result = `\'${value}\'` 494 | if (resultCallbackContent.string) { 495 | result = resultCallbackContent.string({ value, result }) 496 | } 497 | } 498 | // object prototype 499 | else if (getPrototype(value) === 'object') { 500 | const values: string[] = [] 501 | for (const prop in value) { 502 | const content: string = utilMethods.getContentStr(value[prop], replaceDataKeyToUseData, resultCallbackContent) 503 | values.push(`${prop}: ${content}`) 504 | } 505 | result = values.length > 0 ? `{\n${values.join(',\n')}\n}` : '{}' 506 | if (resultCallbackContent.object) { 507 | result = resultCallbackContent.object({ value, values, result }) || result 508 | } 509 | } 510 | // array prototype 511 | else if (getPrototype(value) === 'array') { 512 | const values: string[] = [] 513 | for (const item of value) { 514 | const content: string = utilMethods.getContentStr(item, replaceDataKeyToUseData, resultCallbackContent) 515 | values.push(content) 516 | } 517 | result = values.length > 0 ? `[${values.join(', ')}]` : '[]' 518 | if (resultCallbackContent.array) { 519 | result = resultCallbackContent.array({ value, values, result }) || result 520 | } 521 | } 522 | // function prototype 523 | else if (getPrototype(value).indexOf('function') !== -1) { 524 | let content: string = value.toString() 525 | // native code 526 | if ( 527 | ['String', 'Number', 'Boolean', 'Array', 'Object', 'Date', 'Function', 'Symbol'].includes(value.name) && 528 | content.match(braceRegExp)?.[0] === '{ [native code] }' 529 | ) { 530 | result = `${value.name}` 531 | if (resultCallbackContent.function) { 532 | result = resultCallbackContent.function({ type: 'native', value, content, result }) || result 533 | } 534 | } 535 | // custom code 536 | else { 537 | content = utilMethods.replaceValue(content, replaceDataKeyToUseData) 538 | const arg: string = content.match(parenthesisRegExp)?.[0] || '()' 539 | const body: string = content.substring(content.indexOf(arg) + arg.length).match(braceRegExp)?.[0] || '{}' 540 | result = value.constructor.name === 'AsyncFunction' 541 | ? `async ${arg} => ${body}` 542 | : `${arg} => ${body}` 543 | if (resultCallbackContent.function) { 544 | result = resultCallbackContent.function({ type: 'custom', value, content, arg, body, result }) || result 545 | } 546 | } 547 | } 548 | // other prototype 549 | else { 550 | result = `${value}` 551 | if (resultCallbackContent.other) { 552 | result = resultCallbackContent.other({ value, result }) || result 553 | } 554 | } 555 | return result 556 | }, 557 | replaceKey(key: string, dataKeyToUseData: boolean = false): string | void { 558 | let result: string = '' 559 | // props key 560 | if (vmKeys.props.includes(key)) { 561 | result = 'props.' + key 562 | } 563 | // computed key 564 | else if (vmKeys.computed.includes(key)) { 565 | result = key + '.value' 566 | } 567 | // methods key 568 | else if (vmKeys.methods.includes(key)) { 569 | result = key 570 | } 571 | // data key 572 | else if (vmKeys.data.includes(key) && !dataKeyToUseData) { 573 | result = 'data.' + key 574 | } 575 | // useData key 576 | else if (vmKeys.data.includes(key) && dataKeyToUseData) { 577 | utilMethods.addUse('data') 578 | result = 'useData().' + key 579 | } 580 | // unknown key 581 | else if (key) { 582 | utilMethods.addImport('vue', 'getCurrentInstance') 583 | utilMethods.addUse('vm') 584 | result = `/* Warn: Unknown source: ${key} */ $vm.${key}` 585 | } 586 | return result 587 | }, 588 | replaceValue(value: string, dataKeyToUseData: boolean = false): string { 589 | let result: string = '' 590 | const thisKeyRegExp: RegExp = /this(\.{1})([$a-zA-Z0-9_]+)/g 591 | const refKeyRegExp: RegExp = /\$REFS_KEY(\.|\?\.)([$a-zA-Z0-9_]+)/g 592 | result = value 593 | .replace(thisKeyRegExp, function( 594 | str: string, 595 | separator: string, 596 | key: string, 597 | index: number, 598 | content: string 599 | ): string { 600 | // props key 601 | if (vmKeys.props.includes(key)) { 602 | return 'props.' + key 603 | } 604 | // computed key 605 | else if (vmKeys.computed.includes(key)) { 606 | return key + '.value' 607 | } 608 | // methods key 609 | else if (vmKeys.methods.includes(key)) { 610 | return key 611 | } 612 | // data key 613 | else if (vmKeys.data.includes(key) && !dataKeyToUseData) { 614 | return 'data.' + key 615 | } 616 | // useData key 617 | else if (vmKeys.data.includes(key) && dataKeyToUseData) { 618 | utilMethods.addUse('data') 619 | return 'useData().' + key 620 | } 621 | // attrs key 622 | else if (key === '$attrs') { 623 | utilMethods.addImport('vue', 'useAttrs') 624 | utilMethods.addUse('attrs') 625 | return key.substring(1) 626 | } 627 | // slots key 628 | else if (key === '$slots') { 629 | utilMethods.addImport('vue', 'useSlots') 630 | utilMethods.addUse('slots') 631 | return key.substring(1) 632 | } 633 | // router key 634 | else if (key === '$router') { 635 | utilMethods.addImport('vue-router', 'useRouter') 636 | utilMethods.addUse('router') 637 | return key.substring(1) 638 | } 639 | // route key 640 | else if (key === '$route') { 641 | utilMethods.addImport('vue-router', 'useRoute') 642 | utilMethods.addUse('route') 643 | return key.substring(1) 644 | } 645 | // store key 646 | else if (key === '$store') { 647 | utilMethods.addImport('vuex', 'useStore') 648 | utilMethods.addUse('store') 649 | return key.substring(1) 650 | } 651 | // nextTick key 652 | else if (key === '$nextTick') { 653 | utilMethods.addImport('vue', 'nextTick') 654 | return key.substring(1) 655 | } 656 | // set key 657 | else if (key === '$set') { 658 | utilMethods.addImport('vue', 'set') 659 | return key.substring(1) 660 | } 661 | // delete key 662 | else if (key === '$delete') { 663 | utilMethods.addImport('vue', 'del') 664 | return key.substring(1) 665 | } 666 | // emit key 667 | else if (key === '$emit') { 668 | const nameRegExp: RegExp = /^\([\'\"\`](update:){0,1}([$a-zA-Z0-9_-]+)[\'\"\`]/ 669 | const name: string = content.substring(index + str.length).match(nameRegExp)?.[2] || '' 670 | if (name) { 671 | !vmContent.emits.includes(name) && vmContent.emits.push(name) 672 | } else { 673 | utilMethods.addImport('vue', 'getCurrentInstance') 674 | utilMethods.addUse('vm') 675 | } 676 | return name 677 | ? key.substring(1) 678 | : `/* Warn: Cannot find emit name */ $vm.$emit` 679 | } 680 | // refs key 681 | else if (key === '$refs') { 682 | const nameRegExp: RegExp = /(^\.|^\?\.)([$a-zA-Z0-9_]+)/ 683 | const name: string = content.substring(index + str.length).match(nameRegExp)?.[2] || '' 684 | if (name) { 685 | !vmContent.refs.includes(name) && vmContent.refs.push(name) 686 | } else { 687 | utilMethods.addImport('vue', 'getCurrentInstance') 688 | utilMethods.addUse('vm') 689 | } 690 | return name 691 | ? '$REFS_KEY' 692 | : `/* Warn: Cannot find refs name */ $vm.$refs` 693 | } 694 | // other key 695 | else if ( 696 | ['$data', '$props', '$el', '$options', '$parent', '$root', '$children', '$isServer', 697 | '$listeners', '$watch', '$on', '$once', '$off', '$mount', '$forceUpdate', '$destroy'].includes(key) 698 | ) { 699 | utilMethods.addImport('vue', 'getCurrentInstance') 700 | utilMethods.addUse('vm') 701 | return '$vm.' + key 702 | } 703 | // unknown key 704 | else if (key) { 705 | utilMethods.addImport('vue', 'getCurrentInstance') 706 | utilMethods.addUse('vm') 707 | return `/* Warn: Unknown source: ${key} */ $vm.${key}` 708 | } 709 | // nonexistent key 710 | else { 711 | utilMethods.addImport('vue', 'getCurrentInstance') 712 | utilMethods.addUse('vm') 713 | return `/* Warn: Cannot find key */ $vm${separator}` 714 | } 715 | }) 716 | .replace(refKeyRegExp, function( 717 | str: string, 718 | separator: string, 719 | name: string 720 | ): string { 721 | // reset refs key 722 | return name + '.value' 723 | }) 724 | return result 725 | }, 726 | addImport(type: string, value: string): void { 727 | if (['vue', 'vue-router', 'vuex'].includes(type)) { 728 | const importContent: string[] = vmContent.import[type] 729 | if (!importContent?.includes(value)) { 730 | importContent.push(value) 731 | } 732 | } 733 | }, 734 | addUse(type: string): void { 735 | if (['data', 'vm', 'attrs', 'slots', 'router', 'route', 'store'].includes(type)) { 736 | const contentDist: any = { 737 | vm: 'const { proxy: $vm } = getCurrentInstance()', 738 | data: 'const useData = () => data', 739 | attrs: 'const attrs = useAttrs()', 740 | slots: 'const slots = useSlots()', 741 | router: 'const router = useRouter()', 742 | route: 'const route = useRoute()', 743 | store: 'const store = useStore()' 744 | } 745 | const useContent: string = contentDist[type] 746 | if (useContent) { 747 | vmContent.use[type] = useContent 748 | } 749 | } 750 | } 751 | } 752 | 753 | // vm set content methods runing 754 | for (const prop in vmSetContentMethods) { 755 | const vmSetContentMethod: Function = vmSetContentMethods[prop as keyof VmSetContentMethods] 756 | if (getPrototype(vmSetContentMethod).indexOf('function') !== -1) { 757 | vmSetContentMethod() 758 | } 759 | } 760 | 761 | // output script content beautify 762 | outputScriptContent = jsBeautify(outputScriptContent, jsBeautifyOptions) 763 | 764 | // debug console log 765 | if (options.isDebug) { 766 | console.log('Vue2ToCompositionApi', { 767 | entryScriptContent, 768 | outputScriptContent, 769 | vmBody, 770 | vmContent, 771 | vmOutput, 772 | vmKeys 773 | }) 774 | } 775 | 776 | // done 777 | return outputScriptContent 778 | } catch (err: any) { 779 | throw new Error(err) 780 | } 781 | } 782 | 783 | export default Vue2ToCompositionApi 784 | --------------------------------------------------------------------------------