├── bin └── vue-classify ├── renovate.json ├── .coveralls.yml ├── src ├── type.ts ├── output.ts ├── collectors │ ├── vue-data.ts │ ├── vue-computed.ts │ └── vue-watch.ts ├── utils.ts ├── cli.ts ├── collect-state.ts ├── vue-props.ts ├── index.ts └── tsvue-ast-helpers.ts ├── .gitignore ├── .prettierrc ├── examples ├── hooks │ └── LifeCycle.js ├── todo-app │ ├── TodoListItem.vue │ └── TodoList.vue ├── computeds │ └── SimpleComputed.js ├── watch │ └── WatchExample.js ├── props │ └── Prop.js └── todomvc │ └── TodoMVC.js ├── .npmignore ├── .editorconfig ├── .travis.yml ├── tslint.json ├── tsconfig.json ├── __tests__ ├── examples.spec.ts └── __snapshots__ │ └── examples.spec.ts.snap ├── LICENSE ├── .snyk ├── README.md ├── package.json └── CHANGELOG.md /bin/vue-classify: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | require('../lib/cli') 3 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: fzzxAToMSap8GYIiZrws8bwr9uEWWFFRc -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | export type DictOf = { [key: string]: T } 2 | 3 | export type OrNull = T | null 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | .nyc_output 7 | lib 8 | lib_test 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "semi": false, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /examples/hooks/LifeCycle.js: -------------------------------------------------------------------------------- 1 | export default { 2 | beforeRouteEnter() { 3 | console.log('mounted') 4 | }, 5 | mounted() { 6 | console.log('mounted') 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # gitignore 2 | 3 | coverage/ 4 | node_modules/ 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | .nyc_output 9 | lib 10 | lib_test 11 | 12 | # npmignore 13 | 14 | src/ 15 | __tests__/ 16 | .vscode/ -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' # use latest stable nodejs version 4 | cache: npm 5 | script: 6 | - npm run coverage # jest test with coverage flag does coverage too 7 | after_script: 8 | - 'cat coverage/lcov.info | ./node_modules/.bin/coveralls' # sends the coverage report to coveralls 9 | -------------------------------------------------------------------------------- /examples/todo-app/TodoListItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 25 | -------------------------------------------------------------------------------- /examples/computeds/SimpleComputed.js: -------------------------------------------------------------------------------- 1 | import { mapState } from 'vuex' 2 | 3 | export default { 4 | computed: { 5 | current: () => { 6 | return 1 7 | }, 8 | next() { 9 | return this.now + 1 10 | }, 11 | value: { 12 | get() { 13 | return this.current 14 | }, 15 | set(v) { 16 | this.current = v 17 | }, 18 | }, 19 | user: mapGetter('user'), 20 | listA: mapState(state => state.listA), 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest", 4 | "tslint-config-prettier" 5 | ], 6 | "linterOptions": { 7 | "exclude": ["node_modules"] 8 | }, 9 | "rules": { 10 | "member-access": false, 11 | "max-classes-per-file": false, 12 | "no-implicit-dependencies": false, 13 | "interface-over-type-literal": false, 14 | "ordered-imports": false, 15 | "no-console": false, 16 | "prefer-conditional-expression": false, 17 | "prefer-for-of": false, 18 | "object-literal-sort-keys": false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/watch/WatchExample.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'WatchExample', 3 | props: { 4 | value: Number, 5 | }, 6 | data() { 7 | return { 8 | currentValue: this.value, 9 | complex: { 10 | real: 1, 11 | imaginary: 2, 12 | }, 13 | } 14 | }, 15 | watch: { 16 | currentValue(val) { 17 | this.$emit('input', val) 18 | }, 19 | value(val) { 20 | this.currentValue = val 21 | }, 22 | 'complex.real'(val) { 23 | console.log('real part changed') 24 | }, 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "lib": [ 6 | "dom", 7 | "scripthost", 8 | "esnext" 9 | ], 10 | "target": "es2015", 11 | "strict": false, 12 | "strictPropertyInitialization": false, 13 | "outDir": "./lib", 14 | "preserveConstEnums": true, 15 | "removeComments": false, 16 | "inlineSourceMap": false, 17 | "declaration": true, 18 | "typeRoots": [ 19 | "./node_modules/@types" 20 | ] 21 | }, 22 | "include": [ 23 | "src/**/*" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "**/*-spec.ts" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/output.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from 'prettier' 2 | 3 | const PRETTIER_CONFIG = { 4 | parser: 'babel', 5 | printWidth: 120, 6 | tabWidth: 2, 7 | singleQuote: true, 8 | semi: false, 9 | trailingComma: 'all', 10 | } 11 | 12 | export function formatScriptCode(code: string) { 13 | return prettier.format(code, PRETTIER_CONFIG) 14 | } 15 | 16 | function output(opts: { scriptCode: string; templateCode: string; isSFC: boolean }) { 17 | const { scriptCode, templateCode, isSFC } = opts 18 | const formattedCode = formatScriptCode(scriptCode) 19 | 20 | let code: string 21 | if (isSFC) { 22 | code = ['', '', ''].join('\n') 23 | } else { 24 | code = formattedCode 25 | } 26 | return code 27 | } 28 | 29 | export default output 30 | -------------------------------------------------------------------------------- /__tests__/examples.spec.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import { join, extname } from 'path' 3 | import transform from '../src' 4 | 5 | describe('examples', () => { 6 | const EXAMPLE_FILE_LIST = [ 7 | 'props/Prop.js', 8 | 'watch/WatchExample.js', 9 | 'computeds/SimpleComputed.js', 10 | 'todo-app/TodoList.vue', 11 | 'todo-app/TodoListItem.vue', 12 | 'todomvc/TodoMVC.js', 13 | 'hooks/LifeCycle.js', 14 | ] 15 | 16 | EXAMPLE_FILE_LIST.forEach(rPath => { 17 | it(`examples/${rPath}`, () => { 18 | const src = join(__dirname, '../examples', rPath) 19 | // console.log('src', src) 20 | const source = readFileSync(src).toString() 21 | const isSFC = extname(src) === '.vue' 22 | 23 | const result = transform(source, isSFC) 24 | expect(result).toMatchSnapshot() 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /examples/props/Prop.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: { 3 | // Basic type check (`null` matches any type) 4 | propA: Number, 5 | // Multiple possible types 6 | propB: [String, Number], 7 | // Required string 8 | propC: { 9 | type: [String, Number], 10 | required: true 11 | }, 12 | // Number with a default value 13 | propD: { 14 | type: Number, 15 | default: 100 16 | }, 17 | // Object with a default value 18 | propE: { 19 | type: Object, 20 | // Object or array defaults must be returned from 21 | // a factory function 22 | default: function () { 23 | return { message: 'hello' } 24 | } 25 | }, 26 | // Custom validator function 27 | propF: { 28 | validator: (value) => { 29 | // The value must match one of these strings 30 | return ['success', 'warning', 'danger'].indexOf(value) !== -1 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/collectors/vue-data.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import babelTraverse from '@babel/traverse' 3 | import { CollectState } from '../index' 4 | import { visitTopLevelDecalration } from '../utils' 5 | 6 | export default function collectVueData(ast: t.File, state: CollectState) { 7 | const collectDataNodes = propNodes => { 8 | propNodes.forEach(propNode => { 9 | state.data[propNode.key.name] = propNode.value 10 | }) 11 | } 12 | 13 | const collectData = (dataNode: t.Node) => { 14 | if (t.isObjectProperty(dataNode)) { 15 | if (t.isObjectExpression(dataNode.value)) { 16 | collectDataNodes(dataNode.value.properties) 17 | } 18 | } 19 | if (t.isFunctionDeclaration(dataNode) || t.isObjectMethod(dataNode)) { 20 | let propNodes = [] 21 | dataNode.body.body.forEach(node => { 22 | if (t.isReturnStatement(node)) { 23 | if (t.isObjectExpression(node.argument)) { 24 | propNodes = node.argument.properties 25 | } 26 | collectDataNodes(propNodes) 27 | } 28 | }) 29 | } 30 | } 31 | 32 | visitTopLevelDecalration(ast, (path, dec) => { 33 | dec.properties.forEach((propNode: t.ObjectProperty) => { 34 | const keyName = propNode.key.name 35 | if (keyName === 'data') { 36 | collectData(propNode) 37 | } 38 | }) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /examples/todo-app/TodoList.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 70 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-450202: 7 | - '@babel/generator > lodash': 8 | patched: '2019-07-03T21:15:25.548Z' 9 | - '@babel/traverse > @babel/generator > lodash': 10 | patched: '2019-07-03T21:15:25.548Z' 11 | - '@babel/traverse > @babel/helper-function-name > @babel/types > lodash': 12 | patched: '2019-07-03T21:15:25.548Z' 13 | - '@babel/traverse > @babel/helper-function-name > @babel/template > @babel/types > lodash': 14 | patched: '2019-07-03T21:15:25.548Z' 15 | - snyk > inquirer > lodash: 16 | patched: '2020-03-15T06:28:30.549Z' 17 | - snyk > snyk-config > lodash: 18 | patched: '2020-03-15T06:28:30.549Z' 19 | - snyk > @snyk/dep-graph > graphlib > lodash: 20 | patched: '2020-03-15T06:28:30.549Z' 21 | - snyk > snyk-go-plugin > graphlib > lodash: 22 | patched: '2020-03-15T06:28:30.549Z' 23 | - snyk > snyk-nodejs-lockfile-parser > graphlib > lodash: 24 | patched: '2020-03-15T06:28:30.549Z' 25 | - snyk > snyk-nuget-plugin > dotnet-deps-parser > lodash: 26 | patched: '2020-03-15T06:28:30.549Z' 27 | - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > graphlib > lodash: 28 | patched: '2020-03-15T06:28:30.549Z' 29 | - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash: 30 | patched: '2020-03-15T06:28:30.549Z' 31 | SNYK-JS-TREEKILL-536781: 32 | - snyk > snyk-sbt-plugin > tree-kill: 33 | patched: '2019-12-11T21:15:26.602Z' 34 | -------------------------------------------------------------------------------- /src/collectors/vue-computed.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import { NodePath } from '@babel/traverse' 3 | import { CollectState } from '../index' 4 | import { log, convertToObjectMethod } from '../utils' 5 | 6 | export default function collectVueComputed(path: NodePath, state: CollectState) { 7 | const childs: t.Node[] = path.node.value.properties 8 | 9 | if (childs.length) { 10 | childs.forEach(childNode => { 11 | if (t.isObjectProperty(childNode)) { 12 | const key = childNode.key.name 13 | const propValue = childNode.value 14 | if (t.isCallExpression(propValue)) { 15 | const callee = propValue.callee 16 | const calleeName = t.isIdentifier(callee) ? callee.name : null 17 | if (calleeName === 'mapState') { 18 | state.computedStates[key] = childNode 19 | } else if (calleeName === 'mapGetter') { 20 | state.computedGetters[key] = childNode 21 | } else { 22 | log(`Computed with '${calleeName}' is not supported`, 'error') 23 | } 24 | } else if (t.isObjectExpression(propValue)) { 25 | state.computeds[key] = propValue 26 | } else { 27 | const maybeObjectMethod = convertToObjectMethod(key, childNode) 28 | if (maybeObjectMethod) { 29 | state.computeds[key] = maybeObjectMethod 30 | } 31 | } 32 | } else if (t.isObjectMethod(childNode)) { 33 | const key = childNode.key.name 34 | state.computeds[key] = childNode 35 | } else if (t.isSpreadElement(childNode)) { 36 | // TODO: spread mapState and mapGetter 37 | log(`Spread syntax in computed is not supported`, 'error') 38 | } 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/collectors/vue-watch.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import { NodePath } from '@babel/traverse' 3 | import { CollectState } from '../index' 4 | import { log, convertToObjectMethod } from '../utils' 5 | 6 | export default function collectVueWatch(path: NodePath, state: CollectState) { 7 | const childs = path.node.value.properties 8 | 9 | const processNode = (key: string, propNode: any, options: any) => { 10 | state.watches[key] = { 11 | node: propNode, 12 | options, 13 | } 14 | } 15 | 16 | if (childs.length) { 17 | childs.forEach(propNode => { 18 | const key = t.isStringLiteral(propNode.key) ? propNode.key.value : propNode.key.name 19 | const maybeObjectMethod = convertToObjectMethod(key, propNode) 20 | if (maybeObjectMethod) { 21 | processNode(key, propNode, {}) 22 | } else if (t.isObjectProperty(propNode)) { 23 | const watchOptionNode = propNode.value 24 | if (t.isObjectExpression(watchOptionNode)) { 25 | let handler 26 | const options = {} 27 | watchOptionNode.properties.forEach(optPropNode => { 28 | if (t.isSpreadElement(optPropNode)) { 29 | return 30 | } 31 | const optKey = (optPropNode as any).key.name 32 | if (optKey === 'handler') { 33 | handler = convertToObjectMethod(optKey, optPropNode) 34 | } else if (['deep', 'immediate'].includes(optKey)) { 35 | if (t.isObjectProperty(optPropNode) && t.isBooleanLiteral(optPropNode.value)) { 36 | options[optKey] = optPropNode.value.value 37 | } else { 38 | log(`Do not support watch.${optKey}`, 'error') 39 | } 40 | } 41 | }) 42 | if (handler) { 43 | processNode(key, handler, options) 44 | } 45 | } 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-classify 2 | 3 | [![Build Status](https://travis-ci.org/hikerpig/vue-classify.svg?branch=master)](https://travis-ci.org/hikerpig/vue-classify) 4 | 5 | [![Coverage Status](https://coveralls.io/repos/github/hikerpig/vue-classify/badge.svg?branch=master)](https://coveralls.io/github/hikerpig/vue-classify?branch=master) [![Greenkeeper badge](https://badges.greenkeeper.io/hikerpig/vue-classify.svg)](https://greenkeeper.io/) 6 | 7 | Convert option-object style vue component to [vue-class-component](https://github.com/vuejs/vue-class-component) decorated class. 8 | 9 | Inspired by [vue-to-react](https://github.com/dwqs/vue-to-react). 10 | 11 | Here is an [online demo](https://vue-classify-demo.surge.sh) 12 | 13 | # Install 14 | 15 | ```bash 16 | npm i -g vue-classify # or yarn global add vue-classify 17 | ``` 18 | 19 | # Usage 20 | 21 | ``` 22 | Usage: vue-classify [options] 23 | 24 | Options: 25 | -V, --version output the version number 26 | -i, --input the input path for vue component 27 | -o, --output the output path for new component, which default value is process.cwd() 28 | -n, --name the output file name, which default value is "classified.ts" 29 | -h, --help output usage information 30 | 31 | Examples: 32 | 33 | # transform a vue option-object style component to class component. 34 | 35 | $ vue-classify ./components/option-object.js ./components/Component.ts 36 | $ vue-classify -i ./components/option-object.js -o ./components/ -n Component 37 | ``` 38 | 39 | # Preview Screenshots 40 | 41 | ## Convert props 42 | ![demo-1](http://vue-classify-demo.surge.sh/demo-1.png) 43 | 44 | ## SFC 45 | 46 | ![demo-2](http://vue-classify-demo.surge.sh/demo-2.png) 47 | 48 | # Features 49 | 50 | - props/watch -> vue-property-decorator decorated class properties 51 | - computed -> class getter and setter 52 | - lifecycle hooks -> class methods 53 | - methods -> class methods 54 | - other options will be passed to @Component decorator -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-classify", 3 | "version": "0.2.4", 4 | "description": "Convert option-object style vue component to typescript class component", 5 | "license": "MIT", 6 | "repository": "https://github.com/hikerpig/vue-classify", 7 | "author": { 8 | "name": "hikerpig", 9 | "email": "hikerpigwinnie@gmail.com", 10 | "url": "https://github.com/hikerpig" 11 | }, 12 | "keywords": [ 13 | "vue", 14 | "typescript", 15 | "ast", 16 | "transformation" 17 | ], 18 | "files": [ 19 | "bin", 20 | "lib" 21 | ], 22 | "main": "lib/index.js", 23 | "typings": "lib/index.d.ts", 24 | "bin": { 25 | "vue-classify": "bin/vue-classify" 26 | }, 27 | "scripts": { 28 | "clean": "rimraf lib && rimraf coverage", 29 | "format": "prettier --write \"{src,__tests__}/**/*.ts\" --single-quote --trailing-comma es5", 30 | "lint": "tslint --force --format verbose \"src/**/*.ts\"", 31 | "prepublishOnly": "npm run build", 32 | "prebuild": "npm run clean && npm run format && npm run lint && echo Using TypeScript && tsc --version", 33 | "build": "tsc --pretty", 34 | "test": "jest", 35 | "coverage": "jest --coverage", 36 | "dev": "npm run build -- --watch --sourcemap", 37 | "watch:test": "jest --watch", 38 | "snyk-protect": "snyk protect", 39 | "prepublish": "npm run snyk-protect" 40 | }, 41 | "dependencies": { 42 | "@babel/generator": "^7.5.0", 43 | "@babel/parser": "^7.5.0", 44 | "@babel/traverse": "^7.5.0", 45 | "@babel/types": "^7.5.0", 46 | "chalk": "^2.4.2", 47 | "commander": "^3.0.2", 48 | "prettier": "^1.16.1", 49 | "snyk": "^1.298.1", 50 | "vue-template-compiler": "^2.5.21" 51 | }, 52 | "devDependencies": { 53 | "@types/jest": "24.0.14", 54 | "@types/node": "12.0.3", 55 | "coveralls": "3.0.3", 56 | "jest": "24.8.0", 57 | "rimraf": "2.6.3", 58 | "ts-jest": "24.0.2", 59 | "ts-node": "8.1.1", 60 | "tslint": "5.12.0", 61 | "tslint-config-prettier": "1.17.0", 62 | "typescript": "3.2.2" 63 | }, 64 | "engines": { 65 | "node": ">=6.0.0" 66 | }, 67 | "jest": { 68 | "globals": { 69 | "ts-jest": { 70 | "diagnostics": false 71 | } 72 | }, 73 | "transform": { 74 | ".(ts)": "/node_modules/ts-jest/preprocessor.js" 75 | }, 76 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|js)$", 77 | "moduleFileExtensions": [ 78 | "ts", 79 | "json", 80 | "js" 81 | ], 82 | "testEnvironment": "node" 83 | }, 84 | "snyk": true 85 | } 86 | -------------------------------------------------------------------------------- /examples/todomvc/TodoMVC.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // app initial state 3 | data: { 4 | todos: todoStorage.fetch(), 5 | newTodo: '', 6 | editedTodo: null, 7 | visibility: 'all' 8 | }, 9 | 10 | // watch todos change for localStorage persistence 11 | watch: { 12 | todos: { 13 | handler: function (todos) { 14 | todoStorage.save(todos) 15 | }, 16 | deep: true 17 | } 18 | }, 19 | 20 | // computed properties 21 | // http://vuejs.org/guide/computed.html 22 | computed: { 23 | filteredTodos: function () { 24 | return filters[this.visibility](this.todos) 25 | }, 26 | remaining: function () { 27 | return filters.active(this.todos).length 28 | }, 29 | allDone: { 30 | get: function () { 31 | return this.remaining === 0 32 | }, 33 | set: function (value) { 34 | this.todos.forEach(function (todo) { 35 | todo.completed = value 36 | }) 37 | } 38 | } 39 | }, 40 | 41 | filters: { 42 | pluralize: function (n) { 43 | return n === 1 ? 'item' : 'items' 44 | } 45 | }, 46 | 47 | // methods that implement data logic. 48 | // note there's no DOM manipulation here at all. 49 | methods: { 50 | addTodo: function () { 51 | var value = this.newTodo && this.newTodo.trim() 52 | if (!value) { 53 | return 54 | } 55 | this.todos.push({ 56 | id: todoStorage.uid++, 57 | title: value, 58 | completed: false 59 | }) 60 | this.newTodo = '' 61 | }, 62 | 63 | removeTodo: function (todo) { 64 | this.todos.splice(this.todos.indexOf(todo), 1) 65 | }, 66 | 67 | editTodo: function (todo) { 68 | this.beforeEditCache = todo.title 69 | this.editedTodo = todo 70 | }, 71 | 72 | doneEdit: function (todo) { 73 | if (!this.editedTodo) { 74 | return 75 | } 76 | this.editedTodo = null 77 | todo.title = todo.title.trim() 78 | if (!todo.title) { 79 | this.removeTodo(todo) 80 | } 81 | }, 82 | 83 | cancelEdit: function (todo) { 84 | this.editedTodo = null 85 | todo.title = this.beforeEditCache 86 | }, 87 | 88 | removeCompleted: function () { 89 | this.todos = filters.active(this.todos) 90 | } 91 | }, 92 | 93 | // a custom directive to wait for the DOM to be updated 94 | // before focusing on the input field. 95 | // http://vuejs.org/guide/custom-directive.html 96 | directives: { 97 | 'todo-focus': function (el, binding) { 98 | if (binding.value) { 99 | el.focus() 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import chalk from 'chalk' 3 | import babelTraverse, { NodePath } from '@babel/traverse' 4 | 5 | export function parseName(name: string | void) { 6 | name = name || 'my-vue-compoennt' 7 | const segs = name.split('-') 8 | let str = '' 9 | if (segs.length > 1) { 10 | segs.forEach(v => { 11 | v = v[0].toUpperCase() + v.substr(1).toLowerCase() 12 | str += v 13 | }) 14 | } else { 15 | str = name 16 | } 17 | return str 18 | } 19 | 20 | export function log(msg, type = 'error') { 21 | if (type === 'error') { 22 | return console.log(chalk.red(`[vue-classify]: ${msg}`)) 23 | } 24 | console.log(chalk.green(msg)) 25 | } 26 | 27 | export function convertToObjectMethod(key: string, node: t.ObjectProperty | t.ObjectMethod) { 28 | if (t.isObjectMethod(node)) { 29 | return node 30 | } 31 | const propValue = node.value 32 | let methodBody 33 | let params = [] 34 | /* istanbul ignore next */ 35 | if (t.isArrowFunctionExpression(propValue) || t.isFunctionExpression(propValue)) { 36 | methodBody = propValue.body 37 | params = propValue.params 38 | } 39 | /* istanbul ignore next */ 40 | if (methodBody) { 41 | const id = t.identifier(key) 42 | return t.objectMethod('method', id, params, methodBody) 43 | } 44 | } 45 | 46 | export function visitTopLevelDecalration( 47 | ast: t.File, 48 | cb: (path: NodePath, dec: t.ObjectExpression) => any 49 | ) { 50 | babelTraverse(ast as any, { 51 | ExportDefaultDeclaration(path) { 52 | const dec = path.node.declaration 53 | if (t.isObjectExpression(dec)) { 54 | cb(path, dec) 55 | } 56 | }, 57 | }) 58 | } 59 | 60 | /** 61 | * Convert fuction expression to object method, make succeeding process easier 62 | */ 63 | export function preprocessObjectMethod(ast: t.File) { 64 | babelTraverse(ast as any, { 65 | ObjectProperty(path: NodePath) { 66 | const { node } = path 67 | const nodeValue = node.value 68 | let methodNode: t.ObjectMethod 69 | if (t.isArrowFunctionExpression(nodeValue)) { 70 | if (t.isExpression(nodeValue.body)) { 71 | const statement = t.returnStatement(nodeValue.body) 72 | methodNode = t.objectMethod('method', node.key, [], t.blockStatement([statement])) 73 | } else if (t.isBlockStatement(nodeValue.body)) { 74 | methodNode = t.objectMethod('method', node.key, [], nodeValue.body) 75 | } 76 | } else if (t.isFunctionExpression(nodeValue)) { 77 | methodNode = t.objectMethod('method', node.key, nodeValue.params, nodeValue.body) 78 | } 79 | if (methodNode) { 80 | path.replaceWith(methodNode as any) 81 | } 82 | }, 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import * as program from 'commander' 2 | import chalk from 'chalk' 3 | import * as path from 'path' 4 | import * as fs from 'fs' 5 | import transform from './index' 6 | import { log } from './utils' 7 | 8 | let version: string 9 | try { 10 | const pkgPath = path.join(__dirname, '..', 'package.json') 11 | const pkg = JSON.parse(fs.readFileSync(pkgPath).toString()) 12 | version = pkg.version 13 | } catch (e) { 14 | // 15 | } 16 | 17 | program 18 | .version(version) 19 | .usage('[options]') 20 | .option('-i, --input', 'the input path for vue component') 21 | .option('-o, --output', 'the output path for new component, which default value is process.cwd()') 22 | .option('-n, --name', 'the output file name, which default value is "classified.ts"') 23 | 24 | program.on('--help', () => { 25 | console.log() 26 | console.log(' Examples:') 27 | console.log() 28 | console.log(chalk.gray(' # transform a vue option-object style component to class component.')) 29 | console.log() 30 | console.log(' $ vue-classify -i ./components/option-object.js -o ./components/ -n Component') 31 | console.log(' $ vue-classify ./components/option-object.js ./components/Component.ts') 32 | console.log() 33 | }) 34 | 35 | program.parse(process.argv) 36 | 37 | let useStdin = false 38 | if (program.args.length < 1) { 39 | useStdin = true 40 | } 41 | 42 | function doProcessFile() { 43 | let src = program.args[0] 44 | let name = program.args[1] ? program.args[1] : 'classified' 45 | let dist = program.output ? program.output : process.cwd() 46 | 47 | src = path.resolve(process.cwd(), src) 48 | dist = path.resolve(process.cwd(), dist) 49 | 50 | if (!/(\.js|\.vue)$/.test(src)) { 51 | log(`Not support the file format: ${src}`) 52 | process.exit() 53 | } 54 | 55 | if (!fs.existsSync(src)) { 56 | log(`The source file dose not exist: ${src}`) 57 | process.exit() 58 | } 59 | 60 | if (!fs.statSync(src).isFile()) { 61 | log(`The source file is not a file: ${src}`) 62 | process.exit() 63 | } 64 | 65 | if (!fs.existsSync(dist)) { 66 | log(`The dist directory path dose not exist: ${dist}`) 67 | process.exit() 68 | } 69 | 70 | const inputExt = path.extname(name) 71 | const isSFC = /\.vue$/.test(src) 72 | 73 | if (!inputExt) { 74 | if (isSFC) { 75 | if (inputExt !== '.vue') { 76 | name += '.vue' 77 | } 78 | } else if (!/\.js$/.test(name)) { 79 | name += '.ts' 80 | } 81 | } 82 | 83 | const targetPath = path.resolve(process.cwd(), path.join(dist, name)) 84 | 85 | const source = fs.readFileSync(src) 86 | const resultCode = transform(source, isSFC) 87 | fs.writeFileSync(targetPath, resultCode) 88 | 89 | log('Trasform success', 'success') 90 | } 91 | 92 | if (useStdin) { 93 | process.stdin.on('data', buffer => { 94 | const content = buffer.toString() 95 | const resultCode = transform(content, false) 96 | process.stdout.write(resultCode) 97 | }) 98 | } else { 99 | doProcessFile() 100 | } 101 | -------------------------------------------------------------------------------- /src/collect-state.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Collect vue component state(data, props, computed, watch) 3 | */ 4 | import babelTraverse from '@babel/traverse' 5 | import * as t from '@babel/types' 6 | import { log } from './utils' 7 | import collectVueProps from './vue-props' 8 | import collectVueData from './collectors/vue-data' 9 | import collectVueComputed from './collectors/vue-computed' 10 | import collectVueWatch from './collectors/vue-watch' 11 | import { CollectState } from './index' 12 | 13 | export function initProps(ast, state) { 14 | babelTraverse(ast, { 15 | Program(path) { 16 | const nodeLists = path.node.body 17 | let count = 0 18 | 19 | for (let i = 0; i < nodeLists.length; i++) { 20 | const node = nodeLists[i] 21 | // const childPath = path.get(`body.${i}`); 22 | if (t.isExportDefaultDeclaration(node)) { 23 | count++ 24 | } 25 | } 26 | 27 | if (count > 1 || !count) { 28 | const msg = !count ? 'Must hava one' : 'Only one' 29 | log(`${msg} export default declaration in youe vue component file`) 30 | process.exit() 31 | } 32 | }, 33 | 34 | ObjectProperty(path) { 35 | const parent = path.parentPath.parent 36 | const name = path.node.key.name 37 | if (parent && t.isExportDefaultDeclaration(parent)) { 38 | if (name === 'name') { 39 | if (t.isStringLiteral(path.node.value)) { 40 | state.name = path.node.value.value 41 | } else { 42 | log(`The value of name prop should be a string literal.`) 43 | } 44 | } else if (name === 'props') { 45 | collectVueProps(path, state) 46 | path.stop() 47 | } 48 | } 49 | }, 50 | }) 51 | } 52 | 53 | export function initData(ast, state: CollectState) { 54 | collectVueData(ast, state) 55 | } 56 | 57 | export function initComputed(ast, state) { 58 | babelTraverse(ast, { 59 | ObjectProperty(path) { 60 | const parent = path.parentPath.parent 61 | const name = path.node.key.name 62 | if (parent && t.isExportDefaultDeclaration(parent)) { 63 | if (name === 'computed') { 64 | collectVueComputed(path as any, state) 65 | path.stop() 66 | } 67 | } 68 | }, 69 | }) 70 | } 71 | 72 | export function initWatch(ast, state: CollectState) { 73 | babelTraverse(ast, { 74 | ObjectProperty(path) { 75 | const parent = path.parentPath.parent 76 | const name = path.node.key.name 77 | if (parent && t.isExportDefaultDeclaration(parent)) { 78 | if (name === 'watch') { 79 | collectVueWatch(path, state) 80 | path.stop() 81 | } 82 | } 83 | }, 84 | }) 85 | } 86 | 87 | export function initComponents(ast, state) { 88 | babelTraverse(ast, { 89 | ObjectProperty(path) { 90 | const parent = path.parentPath.parent 91 | const name = path.node.key.name 92 | if (parent && t.isExportDefaultDeclaration(parent)) { 93 | const node = path.node 94 | if (name === 'components' && t.isObjectExpression(node.value)) { 95 | const props = node.value.properties 96 | props.forEach((prop: any) => { 97 | state.components[prop.key.name] = prop.value.name 98 | }) 99 | path.stop() 100 | } 101 | } 102 | }, 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /src/vue-props.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import { log } from './utils' 3 | import { NodePath } from '@babel/traverse' 4 | import { CollectState } from './index' 5 | 6 | const nestedPropsVisitor = { 7 | ObjectProperty(path) { 8 | const parentKey = path.parentPath.parent.key 9 | if (parentKey && parentKey.name === this.childKey) { 10 | const key = path.node.key 11 | const node = path.node.value 12 | // console.log('key node', key, node) 13 | const stateProp = this.state.props[this.childKey] 14 | 15 | if (key.name === 'type') { 16 | if (t.isIdentifier(node)) { 17 | this.state.props[this.childKey].type = node.name.toLowerCase() 18 | } else if (t.isArrayExpression(node)) { 19 | const elements = [] 20 | node.elements.forEach(n => { 21 | if ('name' in n) { 22 | elements.push(n.name.toLowerCase()) 23 | } 24 | }) 25 | if (!elements.length) { 26 | log(`Providing a type for the ${this.childKey} prop is a good practice.`) 27 | } 28 | /** 29 | * supports following syntax: 30 | * propKey: { type: [Number, String], default: 0} 31 | */ 32 | this.state.props[this.childKey].type = 33 | elements.length > 1 ? 'typesOfArray' : elements[0] ? elements[0].toLowerCase() : elements 34 | this.state.props[this.childKey].value = elements.length > 1 ? elements : elements[0] ? elements[0] : elements 35 | } else { 36 | log(`The type in ${this.childKey} prop only supports identifier or array expression, eg: Boolean, [String]`) 37 | } 38 | } 39 | 40 | if (t.isLiteral(node)) { 41 | if (key.name === 'default') { 42 | stateProp.defaultValue = node 43 | } 44 | 45 | if (key.name === 'required') { 46 | stateProp.required = node 47 | } 48 | } 49 | } 50 | }, 51 | 52 | ObjectMethod(path) { 53 | const nodeKeyName = path.node.key.name 54 | for (const k of ['default', 'validator']) { 55 | if (k === nodeKeyName) { 56 | const stateProp = this.state.props[this.childKey] 57 | if (stateProp && !stateProp[k]) { 58 | stateProp[k] = path.node 59 | } 60 | } 61 | } 62 | }, 63 | } 64 | 65 | export default function collectVueProps(path, state: CollectState) { 66 | const childs = path.node.value.properties 67 | const parentKey = path.node.key.name // props; 68 | 69 | if (childs.length) { 70 | path.traverse({ 71 | ObjectProperty(propPath: NodePath) { 72 | const parentNode = propPath.parentPath.parent 73 | if (t.isObjectProperty(parentNode) && parentNode.key && parentNode.key.name === parentKey) { 74 | const childNode = propPath.node 75 | const childKey = childNode.key.name 76 | const childVal = childNode.value 77 | 78 | if (!state.props[childKey]) { 79 | if (t.isArrayExpression(childVal)) { 80 | const elements = [] 81 | childVal.elements.forEach(node => { 82 | if (t.isIdentifier(node)) { 83 | elements.push(node.name.toLowerCase()) 84 | } 85 | }) 86 | state.props[childKey] = { 87 | type: elements.length > 1 ? 'typesOfArray' : elements[0] ? elements[0].toLowerCase() : elements, 88 | value: elements.length > 1 ? elements : elements[0] ? elements[0] : elements, 89 | required: null, 90 | validator: null, 91 | } 92 | } else if (t.isObjectExpression(childVal)) { 93 | state.props[childKey] = { 94 | type: '', 95 | value: undefined, 96 | required: null, 97 | validator: null, 98 | } 99 | propPath.traverse(nestedPropsVisitor, { state, childKey }) 100 | } else if (t.isIdentifier(childVal)) { 101 | // supports propKey: type 102 | state.props[childKey] = { 103 | type: childVal.name.toLowerCase(), 104 | value: undefined, 105 | required: null, 106 | validator: null, 107 | } 108 | } else { 109 | /* istanbul ignore next */ 110 | log(`Not supports expression for the ${this.childKey} prop in props.`) 111 | } 112 | } 113 | } 114 | }, 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## [0.2.4](https://github.com/hikerpig/vue-classify/compare/v0.2.3...v0.2.4) (2019-07-08) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * .snyk & package.json to reduce vulnerabilities ([c3aef16](https://github.com/hikerpig/vue-classify/commit/c3aef16)) 12 | 13 | 14 | ### Features 15 | 16 | * support StrintLiteral as in 'watch' options ([3639d24](https://github.com/hikerpig/vue-classify/commit/3639d24)) 17 | 18 | 19 | 20 | 21 | ## [0.2.3](https://github.com/hikerpig/vue-classify/compare/v0.2.2...v0.2.3) (2019-03-13) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * missing program top level decalrations ([8d5aae8](https://github.com/hikerpig/vue-classify/commit/8d5aae8)) 27 | * props type missing 'symbol' support ([9fb32b7](https://github.com/hikerpig/vue-classify/commit/9fb32b7)) 28 | 29 | 30 | 31 | 32 | ## [0.2.2](https://github.com/hikerpig/vue-classify/compare/v0.2.1...v0.2.2) (2019-01-23) 33 | 34 | 35 | 36 | 37 | ## [0.2.1](https://github.com/hikerpig/vue-classify/compare/v0.2.0...v0.2.1) (2019-01-21) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * preprocessObjectMethod misfuntion ([2699267](https://github.com/hikerpig/vue-classify/commit/2699267)) 43 | 44 | 45 | 46 | 47 | # [0.2.0](https://github.com/hikerpig/vue-classify/compare/v0.1.1...v0.2.0) (2019-01-21) 48 | 49 | 50 | ### Features 51 | 52 | * copy other customBlocks in SFC to output code ([ec7a7f1](https://github.com/hikerpig/vue-classify/commit/ec7a7f1)) 53 | 54 | 55 | 56 | 57 | ## [0.1.1](https://github.com/hikerpig/vue-classify/compare/v0.1.0...v0.1.1) (2019-01-21) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * sfc parse and format error ([c08ec65](https://github.com/hikerpig/vue-classify/commit/c08ec65)) 63 | 64 | 65 | 66 | 67 | # [0.1.0](https://github.com/hikerpig/vue-classify/compare/v0.0.6...v0.1.0) (2019-01-17) 68 | 69 | 70 | ### Features 71 | 72 | * support get/set option in computed :sparkles: :white_check_mark: ([459ab26](https://github.com/hikerpig/vue-classify/commit/459ab26)) 73 | * **formatting:** change babel generator and prettier format config ([f5be03a](https://github.com/hikerpig/vue-classify/commit/f5be03a)) 74 | 75 | 76 | 77 | 78 | ## [0.0.6](https://github.com/hikerpig/vue-classify/compare/v0.0.5...v0.0.6) (2019-01-16) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * handleCycleMethods call param error ([c9e8eb2](https://github.com/hikerpig/vue-classify/commit/c9e8eb2)) 84 | 85 | 86 | 87 | 88 | ## [0.0.5](https://gitlab.bestminr.com/fe/vue-classify/compare/v0.0.4...v0.0.5) (2019-01-16) 89 | 90 | 91 | ### Features 92 | 93 | * add unhandled options to @Component param properties ([050f74d](https://gitlab.bestminr.com/fe/vue-classify/commit/050f74d)) 94 | * collectVueComputed should support t.FunctionExpression ([93a540a](https://gitlab.bestminr.com/fe/vue-classify/commit/93a540a)) 95 | * Convert fuction expression to object method, make succeeding process easier ([3e9673a](https://gitlab.bestminr.com/fe/vue-classify/commit/3e9673a)) 96 | * optimize methods collecting ([324ef6e](https://gitlab.bestminr.com/fe/vue-classify/commit/324ef6e)) 97 | * will not camelize component name if it's not kebab case ([81d636f](https://gitlab.bestminr.com/fe/vue-classify/commit/81d636f)) 98 | 99 | 100 | 101 | 102 | ## [0.0.4](https://gitlab.bestminr.com/fe/vue-classify/compare/v0.0.3...v0.0.4) (2019-01-15) 103 | 104 | 105 | ### Bug Fixes 106 | 107 | * handleGeneralMethods error during collecting methods ([d5a9402](https://gitlab.bestminr.com/fe/vue-classify/commit/d5a9402)) 108 | 109 | 110 | ### Features 111 | 112 | * support simple computed methods, [#3](https://gitlab.bestminr.com/fe/vue-classify/issues/3) ([0380221](https://gitlab.bestminr.com/fe/vue-classify/commit/0380221)) 113 | 114 | 115 | 116 | 117 | ## [0.0.3](https://gitlab.bestminr.com/fe/vue-classify/compare/v0.0.2...v0.0.3) (2019-01-14) 118 | 119 | 120 | 121 | 122 | ## [0.0.2](https://gitlab.bestminr.com/fe/vue-classify/compare/v0.0.1...v0.0.2) (2019-01-14) 123 | 124 | 125 | ### Features 126 | 127 | * generate @Component decorator for class, :sparkles: :heavy_plus_sign: ([2951ab2](https://gitlab.bestminr.com/fe/vue-classify/commit/2951ab2)) 128 | * props default/validator, related [#2](https://gitlab.bestminr.com/fe/vue-classify/issues/2) ([0acaf67](https://gitlab.bestminr.com/fe/vue-classify/commit/0acaf67)) 129 | 130 | 131 | 132 | 133 | ## 0.0.1 (2019-01-13) 134 | 135 | 136 | ### Features 137 | 138 | * cli now can read from stdin ([d957f2d](https://gitlab.bestminr.com/fe/vue-classify/commit/d957f2d)) 139 | * support collecting and gen 'watch', [#1](https://gitlab.bestminr.com/fe/vue-classify/issues/1) ([0c21f19](https://gitlab.bestminr.com/fe/vue-classify/commit/0c21f19)) 140 | * support output sfc; and by default write '.ts' file ([77ebe22](https://gitlab.bestminr.com/fe/vue-classify/commit/77ebe22)) 141 | * vue and vue-router hooks ([23a8154](https://gitlab.bestminr.com/fe/vue-classify/commit/23a8154)) 142 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/examples.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`examples examples/computeds/SimpleComputed.js 1`] = ` 4 | "import Vue from 'vue' 5 | import Component from 'vue-class-component' 6 | import { mapState } from 'vuex' 7 | @Component({}) 8 | export default class MyVueCompoennt extends Vue { 9 | get current() { 10 | return 1 11 | } 12 | 13 | get next() { 14 | return this.now + 1 15 | } 16 | 17 | get value() { 18 | return this.current 19 | } 20 | 21 | set value(v) { 22 | this.current = v 23 | } 24 | 25 | @State(state => state.listA) 26 | listA: any 27 | @Getter('user') 28 | user: any 29 | } 30 | " 31 | `; 32 | 33 | exports[`examples examples/hooks/LifeCycle.js 1`] = ` 34 | "import Vue from 'vue' 35 | import Component from 'vue-class-component' 36 | @Component({}) 37 | export default class MyVueCompoennt extends Vue { 38 | beforeRouteEnter() { 39 | console.log('mounted') 40 | } 41 | 42 | mounted() { 43 | console.log('mounted') 44 | } 45 | } 46 | " 47 | `; 48 | 49 | exports[`examples examples/props/Prop.js 1`] = ` 50 | "import Vue from 'vue' 51 | import Component from 'vue-class-component' 52 | import { Prop } from 'vue-property-decorator' 53 | @Component({}) 54 | export default class MyVueCompoennt extends Vue { 55 | @Prop() 56 | propA: number 57 | @Prop() 58 | propB: Array 59 | @Prop({ 60 | required: true, 61 | }) 62 | propC: Array 63 | @Prop({ 64 | default: 100, 65 | }) 66 | propD: number 67 | @Prop({ 68 | // Object or array defaults must be returned from 69 | // a factory function 70 | default() { 71 | return { 72 | message: 'hello', 73 | } 74 | }, 75 | }) 76 | propE: any 77 | @Prop({ 78 | validator() { 79 | // The value must match one of these strings 80 | return ['success', 'warning', 'danger'].indexOf(value) !== -1 81 | }, 82 | }) 83 | propF: any 84 | } 85 | " 86 | `; 87 | 88 | exports[`examples examples/todo-app/TodoList.vue 1`] = ` 89 | " 111 | 112 | " 161 | `; 162 | 163 | exports[`examples examples/todo-app/TodoListItem.vue 1`] = ` 164 | " 174 | 175 | 188 | 189 | " 193 | `; 194 | 195 | exports[`examples examples/todomvc/TodoMVC.js 1`] = ` 196 | "import Vue from 'vue' 197 | import Component from 'vue-class-component' 198 | import { Watch } from 'vue-property-decorator' 199 | @Component({ 200 | filters: { 201 | pluralize(n) { 202 | return n === 1 ? 'item' : 'items' 203 | }, 204 | }, 205 | // a custom directive to wait for the DOM to be updated 206 | // before focusing on the input field. 207 | // http://vuejs.org/guide/custom-directive.html 208 | directives: { 209 | 'todo-focus'(el, binding) { 210 | if (binding.value) { 211 | el.focus() 212 | } 213 | }, 214 | }, 215 | }) 216 | export default class MyVueCompoennt extends Vue { 217 | todos = todoStorage.fetch() 218 | newTodo = '' 219 | editedTodo = null 220 | visibility = 'all' 221 | 222 | get filteredTodos() { 223 | return filters[this.visibility](this.todos) 224 | } 225 | 226 | get remaining() { 227 | return filters.active(this.todos).length 228 | } 229 | 230 | get allDone() { 231 | return this.remaining === 0 232 | } 233 | 234 | set allDone(value) { 235 | this.todos.forEach(function(todo) { 236 | todo.completed = value 237 | }) 238 | } 239 | 240 | @Watch('todos', { 241 | deep: true, 242 | }) 243 | onTodosChange(todos) { 244 | todoStorage.save(todos) 245 | } 246 | 247 | addTodo() { 248 | var value = this.newTodo && this.newTodo.trim() 249 | 250 | if (!value) { 251 | return 252 | } 253 | 254 | this.todos.push({ 255 | id: todoStorage.uid++, 256 | title: value, 257 | completed: false, 258 | }) 259 | this.newTodo = '' 260 | } 261 | 262 | removeTodo(todo) { 263 | this.todos.splice(this.todos.indexOf(todo), 1) 264 | } 265 | 266 | editTodo(todo) { 267 | this.beforeEditCache = todo.title 268 | this.editedTodo = todo 269 | } 270 | 271 | doneEdit(todo) { 272 | if (!this.editedTodo) { 273 | return 274 | } 275 | 276 | this.editedTodo = null 277 | todo.title = todo.title.trim() 278 | 279 | if (!todo.title) { 280 | this.removeTodo(todo) 281 | } 282 | } 283 | 284 | cancelEdit(todo) { 285 | this.editedTodo = null 286 | todo.title = this.beforeEditCache 287 | } 288 | 289 | removeCompleted() { 290 | this.todos = filters.active(this.todos) 291 | } 292 | } 293 | " 294 | `; 295 | 296 | exports[`examples examples/watch/WatchExample.js 1`] = ` 297 | "import Vue from 'vue' 298 | import Component from 'vue-class-component' 299 | import { Prop, Watch } from 'vue-property-decorator' 300 | @Component({}) 301 | export default class WatchExample extends Vue { 302 | @Prop() 303 | value: number 304 | currentValue = this.value 305 | complex = { 306 | real: 1, 307 | imaginary: 2, 308 | } 309 | 310 | @Watch('currentValue') 311 | onCurrentValueChange(val) { 312 | this.$emit('input', val) 313 | } 314 | 315 | @Watch('value') 316 | onValueChange(val) { 317 | this.currentValue = val 318 | } 319 | 320 | @Watch('complex.real') 321 | onComplexRealChange(val) { 322 | console.log('real part changed') 323 | } 324 | } 325 | " 326 | `; 327 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import generate from '@babel/generator' 2 | import babelTraverse, { NodePath } from '@babel/traverse' 3 | import * as t from '@babel/types' 4 | import * as babelParser from '@babel/parser' 5 | import { parseComponent } from 'vue-template-compiler' 6 | import { initComponents, initComputed, initData, initProps, initWatch } from './collect-state' 7 | import { parseName, visitTopLevelDecalration, preprocessObjectMethod } from './utils' 8 | 9 | import { 10 | genClassMethods, 11 | genImports, 12 | genComponentDecorator, 13 | genProps, 14 | genComputeds, 15 | genDatas, 16 | genWatches, 17 | handleCycleMethods, 18 | handleGeneralMethods, 19 | } from './tsvue-ast-helpers' 20 | 21 | import output from './output' 22 | import { DictOf, OrNull } from './type' 23 | 24 | export type CollectStateDatas = { 25 | [key: string]: NodePath[] 26 | } 27 | 28 | type CollectPropObjectMethod = NodePath 29 | 30 | export type CollectProps = DictOf<{ 31 | type: string 32 | value: any 33 | validator?: OrNull 34 | default?: OrNull 35 | defaultValue?: OrNull 36 | required?: OrNull 37 | }> 38 | 39 | export type CollectComputeds = { 40 | [key: string]: t.ObjectMethod | t.ObjectProperty | t.Expression 41 | } 42 | 43 | export type CollectVuexMap = { 44 | [key: string]: t.ObjectMethod | t.ObjectProperty | t.Expression 45 | } 46 | 47 | export type CollectExtraOption = t.ObjectMethod | t.ObjectProperty 48 | 49 | export enum WatchOptionType { 50 | Get, 51 | Option, 52 | } 53 | 54 | export type CollectWatches = { 55 | [key: string]: { 56 | node: t.ObjectMethod | t.ObjectProperty | t.ObjectExpression 57 | options: { 58 | deep?: boolean 59 | immediate?: boolean 60 | } 61 | } 62 | } 63 | 64 | export type CollectState = { 65 | name: string | void 66 | data: CollectStateDatas 67 | dataStatements: t.Statement[] 68 | props: CollectProps 69 | computeds: CollectComputeds 70 | computedStates: CollectVuexMap 71 | computedGetters: CollectVuexMap 72 | watches: CollectWatches 73 | components: any 74 | componentOptions: DictOf 75 | } 76 | 77 | type TopLevelNodeInfo = { 78 | isBeforeComponent: boolean 79 | node: t.Node 80 | } 81 | 82 | type ProgramCollect = { 83 | imports: t.Node[] 84 | classMethods: DictOf 85 | topLevelNodes: TopLevelNodeInfo[] 86 | } 87 | 88 | const LIFECYCLE_HOOKS = [ 89 | 'beforeCreate', 90 | 'created', 91 | 'beforeMount', 92 | 'mounted', 93 | 'beforeUpdate', 94 | 'updated', 95 | 'beforeDestroy', 96 | 'destroyed', 97 | 'activated', 98 | 'deactivated', 99 | 'errorCaptured', 100 | 'ssrPrefetch', 101 | ] 102 | 103 | const VUE_ROUTER_HOOKS = ['beforeRouteEnter', 'beforeRouteLeave', 'beforeRouteUpdate'] 104 | 105 | const VUE_ECO_HOOKS = LIFECYCLE_HOOKS.concat(VUE_ROUTER_HOOKS) 106 | 107 | const HANDLED_OPTION_KEYS = ['name', 'components', 'props', 'data', 'computed', 'watch'] 108 | 109 | type SFCParsedResult = { 110 | template: SFCComponentBlock 111 | script: SFCComponentBlock 112 | styles: SFCComponentBlock[] 113 | customBlocks: SFCComponentBlock[] 114 | } 115 | 116 | type SFCComponentBlock = { 117 | type: string 118 | attrs: DictOf 119 | content: string 120 | start: number 121 | end: number 122 | } 123 | 124 | type ComponentFormattedResult = { 125 | template: string 126 | js: string 127 | blocks?: SFCComponentBlock[] 128 | } 129 | 130 | function formatContent(source: string, isSFC: boolean): ComponentFormattedResult { 131 | if (isSFC) { 132 | const res: SFCParsedResult = parseComponent(source, { pad: 'line' }) 133 | return { 134 | template: res.template.content, 135 | js: res.script.content.replace(/\/\/\n/g, ''), 136 | blocks: [].concat(res.styles).concat(res.customBlocks), 137 | } 138 | } else { 139 | return { 140 | template: null, 141 | js: source, 142 | } 143 | } 144 | } 145 | 146 | export default function transform(buffer: Buffer | string, isSFC: boolean) { 147 | const source = buffer.toString() 148 | const state: CollectState = { 149 | name: undefined, 150 | data: {}, 151 | dataStatements: [], 152 | props: {}, 153 | computeds: {}, 154 | computedStates: {}, 155 | computedGetters: {}, 156 | watches: {}, 157 | components: {}, 158 | componentOptions: {}, 159 | } 160 | 161 | const collect: ProgramCollect = { 162 | imports: [], 163 | classMethods: {}, 164 | topLevelNodes: [], 165 | } 166 | 167 | const component = formatContent(source, isSFC) 168 | 169 | const vast = babelParser.parse(component.js, { 170 | sourceType: 'module', 171 | plugins: isSFC ? [] : ['jsx'], 172 | }) 173 | 174 | preprocessObjectMethod(vast) 175 | 176 | let exportDefaultDeclaration: t.ExportDeclaration 177 | 178 | vast.program.body.forEach((node: t.Node | t.Statement) => { 179 | if (t.isImportDeclaration(node)) { 180 | collect.imports.push(node) 181 | } else if (t.isExportDefaultDeclaration(node)) { 182 | exportDefaultDeclaration = node 183 | } else { 184 | const isBeforeComponent = !exportDefaultDeclaration 185 | collect.topLevelNodes.push({ 186 | isBeforeComponent, 187 | node, 188 | }) 189 | } 190 | }) 191 | 192 | initProps(vast, state) 193 | initData(vast, state) 194 | initComputed(vast, state) 195 | initWatch(vast, state) 196 | initComponents(vast, state) // SFC 197 | 198 | visitTopLevelDecalration(vast, (path, dec) => { 199 | dec.properties.forEach(propNode => { 200 | if (t.isSpreadElement(propNode)) { 201 | return 202 | } 203 | const key = propNode.key.name 204 | if (key === 'methods') { 205 | if (t.isObjectProperty(propNode)) { 206 | if (t.isObjectExpression(propNode.value)) { 207 | propNode.value.properties.forEach(methodNode => { 208 | if (!t.isSpreadElement(methodNode)) { 209 | const name = methodNode.key.name 210 | handleGeneralMethods(methodNode, collect, state, name) 211 | } 212 | }) 213 | } 214 | } 215 | } else if (VUE_ECO_HOOKS.includes(key)) { 216 | handleCycleMethods(propNode, collect, state, key, isSFC) 217 | } else if (HANDLED_OPTION_KEYS.includes(key)) { 218 | // will collect in somewhere else 219 | } else { 220 | state.componentOptions[key] = propNode 221 | } 222 | }) 223 | path.stop() 224 | }) 225 | 226 | // AST for new component 227 | // const scriptTpl = `export default class ${parseName(state.name)} extends Vue {}` 228 | const scriptTpl = `` 229 | const scriptAst: any = babelParser.parse(scriptTpl, { 230 | sourceType: 'module', 231 | plugins: isSFC ? [] : ['jsx'], 232 | }) 233 | 234 | babelTraverse(scriptAst, { 235 | Program(path) { 236 | genImports(path, collect, state) 237 | 238 | const addTopLevelNodes = (isBeforeComponent: boolean) => { 239 | collect.topLevelNodes 240 | .filter(o => o.isBeforeComponent === isBeforeComponent) 241 | .reverse() 242 | .forEach(o => { 243 | ;(path.node.body as any).push(o.node) 244 | }) 245 | } 246 | 247 | addTopLevelNodes(true) 248 | 249 | const classBody = t.classBody([]) 250 | const classBodyList = classBody.body 251 | const classNode = t.classDeclaration(t.identifier(parseName(state.name)), t.identifier('Vue'), classBody) 252 | genProps(classBodyList, state) 253 | genDatas(classBodyList, state) 254 | genComputeds(classBodyList, state) 255 | genWatches(classBodyList, state) 256 | genClassMethods(classBodyList, collect) 257 | const classDecorator = genComponentDecorator(classNode, state) 258 | if (classDecorator) { 259 | path.node.body.push(classDecorator) 260 | } 261 | 262 | path.node.body.push(t.exportDefaultDeclaration(classNode) as any) 263 | 264 | addTopLevelNodes(false) 265 | }, 266 | }) 267 | 268 | const r = generate(scriptAst, { 269 | quotes: 'single', 270 | retainLines: false, 271 | }) 272 | const scriptCode = r.code 273 | 274 | let code = output({ 275 | scriptCode, 276 | isSFC, 277 | templateCode: component.template, 278 | }) 279 | 280 | // extra blocks 281 | if (component.blocks) { 282 | const blockContentList: string[] = component.blocks.reduce( 283 | (list, block) => { 284 | const blockTypeId = t.jsxIdentifier(block.type) 285 | const blockAttrNodes = Object.keys(block.attrs).map(k => { 286 | const attr = block.attrs[k] 287 | return t.jsxAttribute(t.jsxIdentifier(k), t.stringLiteral(attr)) 288 | }) 289 | const blockSource: string = source.slice(block.start, block.end) 290 | const blockAst = t.jsxElement( 291 | t.jsxOpeningElement(blockTypeId, blockAttrNodes), 292 | t.jsxClosingElement(blockTypeId), 293 | [t.jsxText(blockSource)], 294 | false 295 | ) 296 | list.push(generate(blockAst as any).code) 297 | return list 298 | }, 299 | [''] 300 | ) 301 | code += blockContentList.join('\n\n') 302 | } 303 | return code 304 | } 305 | -------------------------------------------------------------------------------- /src/tsvue-ast-helpers.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | import { CollectState, CollectComputeds, CollectStateDatas, CollectProps } from './index' 3 | import { log, convertToObjectMethod } from './utils' 4 | import { isArray } from 'util' 5 | 6 | const TYPE_KEYWORD_CTOR_MAP = { 7 | boolean: t.tsBooleanKeyword, 8 | number: t.tsNumberKeyword, 9 | string: t.tsStringKeyword, 10 | symbol: t.tsSymbolKeyword, 11 | } 12 | 13 | function genTypeKeyword(typeStr: string) { 14 | const ctor = TYPE_KEYWORD_CTOR_MAP[typeStr] || t.tsAnyKeyword 15 | return ctor() 16 | } 17 | 18 | function genPropDecorators(props: CollectProps) { 19 | const keys = Object.keys(props) 20 | const nodes = [] 21 | 22 | for (let i = 0, l = keys.length; i < l; i++) { 23 | const key = keys[i] 24 | const obj = props[key] 25 | // console.log('key', key, obj) 26 | 27 | const properties: Array = [] 28 | if (obj.required) { 29 | properties.push(t.objectProperty(t.identifier('required'), obj.required as t.BooleanLiteral)) 30 | } 31 | if (obj.validator) { 32 | const { validator } = obj 33 | if (validator) { 34 | if (t.isFunctionExpression(validator)) { 35 | properties.push(t.objectMethod('method', t.identifier('validator'), validator.params, validator.body)) 36 | } else if (t.isObjectMethod(obj.validator)) { 37 | properties.push(obj.validator) 38 | } 39 | } 40 | } 41 | if (obj.defaultValue) { 42 | properties.push(t.objectProperty(t.identifier('default'), obj.defaultValue as any)) 43 | } else if (obj.default) { 44 | if (t.isObjectMethod(obj.default)) { 45 | properties.push(obj.default) 46 | } 47 | } 48 | const decoratorParam = properties.length ? t.objectExpression(properties) : null 49 | 50 | const decorator = t.decorator(t.callExpression(t.identifier('Prop'), decoratorParam ? [decoratorParam] : [])) 51 | 52 | let typeAnnotation: t.TSTypeAnnotation 53 | 54 | if (obj.type === 'typesOfArray') { 55 | if (isArray(obj.value)) { 56 | const typeKeywords: t.TSType[] = obj.value.map((typeStr: string) => { 57 | return genTypeKeyword(typeStr) 58 | }) 59 | const typeRef = t.tsTypeReference( 60 | t.identifier('Array'), 61 | t.tsTypeParameterInstantiation([t.tsUnionType(typeKeywords)]) 62 | ) 63 | typeAnnotation = t.tsTypeAnnotation(typeRef) 64 | } 65 | } else if (TYPE_KEYWORD_CTOR_MAP[obj.type as any]) { 66 | typeAnnotation = t.tsTypeAnnotation(genTypeKeyword(obj.type as any)) 67 | } else { 68 | typeAnnotation = t.tsTypeAnnotation(t.tsAnyKeyword()) 69 | } 70 | 71 | if (typeAnnotation && decorator) { 72 | const property = t.classProperty(t.identifier(key), null, typeAnnotation, [decorator]) 73 | nodes.push(property) 74 | } 75 | } 76 | 77 | return nodes 78 | } 79 | 80 | function processVuexComputeds(state: CollectState) { 81 | const nodes = [] 82 | const processCollects = type => { 83 | let obj 84 | let decoratorName 85 | if (type === 'state') { 86 | obj = state.computedStates 87 | decoratorName = 'State' 88 | } else { 89 | obj = state.computedGetters 90 | decoratorName = 'Getter' 91 | } 92 | 93 | for (const key of Object.keys(obj)) { 94 | const node = obj[key] 95 | let decorator: t.Decorator 96 | const id = t.identifier(key) 97 | if (t.isObjectProperty(node)) { 98 | const propValue = node.value 99 | if (t.isCallExpression(propValue)) { 100 | const decCalleeId = t.identifier(decoratorName) 101 | decorator = t.decorator(t.callExpression(decCalleeId, propValue.arguments)) 102 | } 103 | } 104 | 105 | if (decorator) { 106 | const resultNode = t.classProperty(id, null, t.tsTypeAnnotation(t.tsAnyKeyword()), [decorator]) 107 | nodes.push(resultNode) 108 | } 109 | } 110 | } 111 | processCollects('state') 112 | processCollects('geter') 113 | return nodes 114 | } 115 | 116 | function processComputeds(computeds: CollectComputeds) { 117 | const nodes = [] 118 | 119 | Object.keys(computeds).forEach(key => { 120 | const node = computeds[key] 121 | const id = t.identifier(key) 122 | let methodBody 123 | if (t.isObjectMethod(node)) { 124 | methodBody = node.body 125 | } else if (t.isObjectExpression(node)) { 126 | node.properties.forEach(p => { 127 | if (t.isObjectMethod(p)) { 128 | const propK = p.key.name 129 | if (['get', 'set'].includes(propK)) { 130 | nodes.push(t.classMethod(propK, id, p.params, p.body)) 131 | } 132 | } 133 | }) 134 | } else if (t.isObjectProperty(node)) { 135 | const propValue = node.value 136 | if (t.isArrowFunctionExpression(propValue)) { 137 | methodBody = propValue.body 138 | } 139 | } 140 | let resultNode 141 | if (methodBody) { 142 | resultNode = t.classMethod('get', id, [], methodBody) 143 | } 144 | if (resultNode) { 145 | nodes.push(resultNode) 146 | } 147 | }) 148 | return nodes 149 | } 150 | 151 | export function genImports(path, collect, state: CollectState) { 152 | const nodeLists = path.node.body 153 | const importVue = t.importDeclaration([t.importDefaultSpecifier(t.identifier('Vue'))], t.stringLiteral('vue')) 154 | const importVueClassComponent = t.importDeclaration( 155 | [t.importDefaultSpecifier(t.identifier('Component'))], 156 | t.stringLiteral('vue-class-component') 157 | ) 158 | const propertyDecoratorSpecifiers = [] 159 | if (Object.keys(state.props).length) { 160 | propertyDecoratorSpecifiers.push(t.importSpecifier(t.identifier('Prop'), t.identifier('Prop'))) 161 | } 162 | if (Object.keys(state.watches).length) { 163 | propertyDecoratorSpecifiers.push(t.importSpecifier(t.identifier('Watch'), t.identifier('Watch'))) 164 | } 165 | if (propertyDecoratorSpecifiers.length) { 166 | const importD = t.importDeclaration(propertyDecoratorSpecifiers, t.stringLiteral('vue-property-decorator')) 167 | collect.imports.push(importD) 168 | } 169 | 170 | collect.imports.push(importVueClassComponent) 171 | collect.imports.push(importVue) 172 | collect.imports.forEach(node => nodeLists.unshift(node)) 173 | } 174 | 175 | export function genComponentDecorator(node: t.ClassDeclaration, state: CollectState) { 176 | let decorator 177 | if (t.isIdentifier(node.superClass) && node.superClass.name === 'Vue') { 178 | const properties: Array = [] 179 | // const parentPath = path.parentPath 180 | const componentKeys = Object.keys(state.components) 181 | if (componentKeys.length) { 182 | const componentProps = [] 183 | for (const k of componentKeys) { 184 | componentProps.push(t.objectProperty(t.identifier(k), t.identifier(state.components[k]))) 185 | } 186 | properties.push(t.objectProperty(t.identifier('components'), t.objectExpression(componentProps))) 187 | } 188 | for (const k of Object.keys(state.componentOptions)) { 189 | properties.push(state.componentOptions[k]) 190 | } 191 | 192 | const decoratorParam = t.objectExpression(properties) 193 | decorator = t.decorator(t.callExpression(t.identifier('Component'), [decoratorParam])) 194 | } 195 | return decorator 196 | } 197 | 198 | export const genProps = (body, state: CollectState) => { 199 | const props = state.props 200 | const nodeLists = body 201 | if (Object.keys(props).length) { 202 | const propNodes = genPropDecorators(props) 203 | propNodes.forEach(node => { 204 | nodeLists.push(node) 205 | }) 206 | } 207 | } 208 | 209 | export function genClassMethods(body, collect) { 210 | const nodeLists = body 211 | const methods = collect.classMethods 212 | if (Object.keys(methods).length) { 213 | Object.keys(methods).forEach(key => { 214 | nodeLists.push(methods[key]) 215 | }) 216 | } 217 | } 218 | 219 | export function genComputeds(body, state: CollectState) { 220 | const nodeLists = body 221 | const { computeds } = state 222 | const computedNodes = processComputeds(computeds) 223 | const vuexComputedNodes = processVuexComputeds(state) 224 | computedNodes.forEach(node => { 225 | nodeLists.push(node) 226 | }) 227 | vuexComputedNodes.forEach(node => { 228 | nodeLists.push(node) 229 | }) 230 | } 231 | 232 | export function genDatas(body, state: CollectState) { 233 | const nodeLists = body 234 | const { data } = state 235 | Object.keys(data).forEach(key => { 236 | if (key === '_statements') { 237 | return 238 | } 239 | const dataNodePath = data[key] 240 | let property: t.ClassProperty 241 | const id = t.identifier(key) 242 | property = t.classProperty(id, dataNodePath as any) 243 | 244 | if (property) { 245 | nodeLists.push(property) 246 | } 247 | }) 248 | } 249 | 250 | export function genWatches(body: t.Node[], state: CollectState) { 251 | const nodeLists = body 252 | const { watches } = state 253 | Object.keys(watches).forEach(key => { 254 | const { node, options } = watches[key] 255 | let cMethod: t.ClassMethod 256 | let funcNode: t.ObjectMethod | t.FunctionExpression 257 | if (t.isObjectMethod(node)) { 258 | funcNode = node 259 | } else if (t.isObjectProperty(node)) { 260 | if (t.isFunctionExpression(node.value)) { 261 | funcNode = node.value 262 | } 263 | } 264 | if (funcNode) { 265 | const safeKey = `${key[0].toUpperCase()}${key.slice(1)}`.replace(/\.(\w)/g, (m, g1) => { 266 | return `${g1.toUpperCase()}` 267 | }) 268 | const methodName = `on${safeKey}Change` 269 | const watchOptionProps: t.ObjectProperty[] = [] 270 | if (options) { 271 | for (const k of Object.keys(options)) { 272 | watchOptionProps.push(t.objectProperty(t.identifier(k), t.booleanLiteral(options[k]))) 273 | } 274 | } 275 | const watchOptionNode = watchOptionProps.length ? t.objectExpression(watchOptionProps) : null 276 | const watchDecParams: [t.StringLiteral, t.ObjectExpression?] = [t.stringLiteral(key)] 277 | if (watchOptionNode) { 278 | watchDecParams.push(watchOptionNode) 279 | } 280 | const decorator = t.decorator(t.callExpression(t.identifier('Watch'), watchDecParams)) 281 | const paramList = funcNode.params 282 | const blockStatement = funcNode.body 283 | cMethod = t.classMethod('method', t.identifier(methodName), paramList, blockStatement) 284 | cMethod.decorators = [decorator] 285 | 286 | nodeLists.push(cMethod) 287 | } 288 | }) 289 | } 290 | 291 | function createClassMethod(node, state: CollectState, name: string) { 292 | const maybeObjectMethod = convertToObjectMethod(name, node) 293 | if (maybeObjectMethod) { 294 | return t.classMethod('method', t.identifier(name), maybeObjectMethod.params, maybeObjectMethod.body) 295 | } 296 | } 297 | 298 | export function handleCycleMethods(node: t.Node, collect, state, name, isSFC) { 299 | if (name === 'render') { 300 | if (isSFC) { 301 | return 302 | } 303 | collect.classMethods[name] = node 304 | } else { 305 | collect.classMethods[name] = createClassMethod(node, state, name) 306 | } 307 | } 308 | 309 | export function handleGeneralMethods(node, collect, state, name) { 310 | collect.classMethods[name] = createClassMethod(node, state, name) 311 | } 312 | --------------------------------------------------------------------------------