├── .gitignore ├── LICENSE ├── README.md ├── assets └── xiaoguo.jpeg ├── index.js ├── package.json ├── parse.js ├── test ├── component │ ├── login.vue │ └── test.vue └── parse.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pid 3 | *.seed 4 | 5 | .DS_Store 6 | 7 | .idea 8 | .tags* 9 | node_modules 10 | _book 11 | 12 | npm-debug.log* 13 | #yarn.lock 14 | package-lock.json 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present, Season Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsdoc-vue-component 2 | 3 | > A simple plugin for jsdoc (`pase vue SFC info to description by AST analysis`). 4 | 5 | Maybe you will try [jsdoc-vuedoc](https://github.com/ccqgithub/jsdoc-vuedoc), and you have a better experience。 6 | 7 | ## Installation 8 | 9 | ```sh 10 | npm i jsdoc-vue-component -D 11 | ``` 12 | 13 | ## Related 14 | 15 | - [espree](https://github.com/eslint/espree): parse code to ast. 16 | - [escodegen](https://github.com/estools/escodegen): generate code from ast. 17 | - [estraverse](https://github.com/estools/estraverse): traverse the AST tree. 18 | - [JsDoc3](https://github.com/jsdoc3/jsdoc). 19 | - [docstrap](https://github.com/docstrap/docstrap): a theme for jsdoc3. 20 | - [jsdoc-vuedoc](https://github.com/ccqgithub/jsdoc-vuedoc): A jsdoc3 plugin use `@vuedoc/md`. 21 | 22 | ## Use: 23 | 24 | > This plugin just extract the component's info into `markdown` format, and instert it into the `@vuedoc`'s position. 25 | 26 | > Not affect other jsdoc features of the code. 27 | 28 | 1. add `@vuedoc` tag to comment. 29 | 2. add `@exports componentName` tag to comment. 30 | 31 | just add `@vuedoc` tag, `@exports` tag, to the to document in you vue SFC. 32 | 33 | ```js 34 | /** 35 | * sidebar component description 36 | * @vuedoc 37 | * @exports component/SideBar 38 | */ 39 | export default {} 40 | ``` 41 | 42 | ## 如何使用jsdoc? 43 | 44 | - 安装jsdoc: `npm i jsdoc -D` 45 | - 安装模板:`npm i sherry-docstrap -D`, 原来的[docstrap](https://github.com/docstrap/docstrap)有点小bug还未修复,所以自己暂时发布一个。 46 | - 在项目目录下建了配置文件:下面有示例,适当修改。 47 | - 在pacakge.json 里添加一个script: `"jsdoc": "rm -rf public/jsdoc && node_modules/.bin/jsdoc -c jsdoc.json"`, `public/jsdoc`为发布位置,适当修改 48 | - 生成文档: `npm run jsdoc` 49 | 50 | ## Options 51 | 52 | - `log`: true, 53 | - `tag`: 'vuedoc' 54 | 55 | ## jsdoc.json 56 | 57 | ```json 58 | { 59 | "plugins": [ 60 | "node_modules/jsdoc-vue-component", 61 | "plugins/markdown", 62 | "plugins/summarize" 63 | ], 64 | "jsdoc-vue-component": { 65 | "log": true 66 | }, 67 | "markdown": { 68 | "tags": ["author", "classdesc", "description", "param", "property", "returns", "see", "throws", "vue"] 69 | }, 70 | "recurseDepth": 10, 71 | "source": { 72 | "include": ["fe/src"], 73 | "includePattern": ".+\\.(js|vue)$", 74 | "excludePattern": "(^|\\/|\\\\)_" 75 | }, 76 | "sourceType": "module", 77 | "tags": { 78 | "allowUnknownTags": true, 79 | "dictionaries": ["jsdoc", "closure"] 80 | }, 81 | "templates": { 82 | "logoFile": "", 83 | "cleverLinks": false, 84 | "monospaceLinks": false, 85 | "dateFormat": "ddd MMM Do YYYY", 86 | "outputSourceFiles": true, 87 | "outputSourcePath": true, 88 | "systemName": "DocStrap", 89 | "footer": "", 90 | "copyright": "DocStrap Copyright © 2012-2015 The contributors to the JSDoc3 and DocStrap projects.", 91 | "navType": "vertical", 92 | "theme": "cosmo", 93 | "linenums": true, 94 | "collapseSymbols": false, 95 | "inverseNav": true, 96 | "protocol": "html://", 97 | "methodHeadingReturns": false 98 | }, 99 | "markdown": { 100 | "parser": "gfm", 101 | "hardwrap": true 102 | }, 103 | "opts": { 104 | "template": "node_modules/sherry-docstrap/template", 105 | "encoding": "utf8", 106 | "destination": "./public/jsdoc/", 107 | "recurse": true, 108 | "readme": "README.md", 109 | "tutorials": "./docs/" 110 | } 111 | } 112 | ``` 113 | 114 | ## 效果 115 | 116 | ![效果](assets/xiaoguo.jpeg). 117 | -------------------------------------------------------------------------------- /assets/xiaoguo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccqgithub/jsdoc-vue-component/553cd6f93f5f36048da21e47229a325e58728d50/assets/xiaoguo.jpeg -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const compiler = require('vue-template-compiler'); 4 | const stripIndent = require('strip-indent'); 5 | const indentString = require('indent-string'); 6 | const parse = require('./parse'); 7 | const log = require('./util').log; 8 | const config = require('./util').config; 9 | 10 | // get markdown comment 11 | function getMarkDown(obj) { 12 | let md = ''; 13 | 14 | // name 15 | let name = obj.name; 16 | if (name) { 17 | md += stripIndent(` 18 | ## Name 19 | > ${name}; 20 | `); 21 | } 22 | 23 | // props 24 | md += stripIndent(` 25 | ## Props 26 | | Name | Type | Required | Default | validator | 27 | | ----- | ---- | ------- | ------- | ------- | 28 | `); 29 | let props = obj.props || []; 30 | props.forEach(prop => { 31 | md += stripIndent(`| ${prop.name} | ${prop.type} | ${prop.required} | ${prop.default} | ${prop.validator} |` + '\n'); 32 | }); 33 | if (!props.length) { 34 | md + '| | | | | |'; 35 | } 36 | 37 | // events 38 | md += stripIndent(` 39 | ## Events 40 | | Name | Data | Code | 41 | | ----- | ----- | ----- | 42 | `); 43 | let events = obj.events || []; 44 | events.forEach(item => { 45 | md += stripIndent(`| ${item.name} | ${item.data} | ${JSON.stringify(item.code)} |` + '\n'); 46 | }); 47 | if (!events.length) { 48 | md += stripIndent(`| | |`); 49 | } 50 | 51 | // methods 52 | md += stripIndent(` 53 | ## Methods 54 | | Name | Code | 55 | | ----- | ----- | 56 | `); 57 | let methods = obj.methods || []; 58 | methods.forEach(item => { 59 | md += stripIndent(`| ${item.name} | ${item.code} |` + '\n'); 60 | }); 61 | if (!methods.length) { 62 | md += '| | |'; 63 | } 64 | 65 | // Components 66 | md += stripIndent(` 67 | ## Components 68 | `); 69 | let components = obj.components || []; 70 | components.forEach(key => { 71 | md += stripIndent(` 72 | - ${key} 73 | `); 74 | }); 75 | 76 | // options 77 | md += stripIndent(` 78 | ## Options 79 | `); 80 | let options = obj.options || []; 81 | options.forEach(key => { 82 | md += stripIndent(` 83 | - ${key} 84 | `); 85 | }); 86 | 87 | return md; 88 | } 89 | 90 | // cache parsed md 91 | const markdownCodes = {}; 92 | 93 | // handlers 94 | exports.handlers = { 95 | beforeParse (e) { 96 | if (/\.vue$/.test(e.filename)) { 97 | log(`parse file begin: ${e.filename}`); 98 | 99 | const parsedComponent = compiler.parseComponent(e.source); 100 | const code = parsedComponent.script ? parsedComponent.script.content : ''; 101 | const parsed = parse(code); 102 | const md = getMarkDown(parsed); 103 | 104 | markdownCodes[e.filename] = md; 105 | 106 | e.source = code; 107 | } 108 | }, 109 | jsdocCommentFound(e) { 110 | const tag = '@' + config.tag; 111 | 112 | if ( 113 | /\.vue$/.test(e.filename) 114 | && e.comment.indexOf(tag) != -1 115 | ) { 116 | let md = markdownCodes[e.filename]; 117 | e.comment = e.comment.replace(tag, md); 118 | } 119 | } 120 | } 121 | 122 | // defineTags 123 | exports.defineTags = function (dictionary) { 124 | const tag = config.tag; 125 | 126 | dictionary.defineTag(tag, { 127 | mustHaveValue: false, 128 | onTagged (doclet, tag) { 129 | const componentName = doclet.meta.filename.split('.').slice(0, -1).join('.'); 130 | 131 | doclet.scope = 'vue'; 132 | doclet.kind = 'module'; 133 | doclet.alias = 'vue-' + componentName; 134 | } 135 | }); 136 | } 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsdoc-vue-component", 3 | "version": "2.2.4", 4 | "description": "A simple plugin for jsdoc (`pase vue SFC info to description`)", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/ccqgithub/jsdoc-vue-component.git" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/ccqgithub/jsdoc-vue-component/issues" 14 | }, 15 | "homepage": "https://github.com/ccqgithub/jsdoc-vue-component#readme", 16 | "dependencies": { 17 | "escodegen": "^1.9.0", 18 | "espree": "^3.5.2", 19 | "indent-string": "^3.2.0", 20 | "strip-indent": "^2.0.0", 21 | "vue-template-compiler": "^2.5.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /parse.js: -------------------------------------------------------------------------------- 1 | const espree = require('espree'); 2 | const estraverse = require('estraverse'); 3 | const escodegen = require('escodegen'); 4 | const log = require('./util').log; 5 | const skips = ['ExperimentalSpreadProperty']; 6 | 7 | function parseProps(item) { 8 | log('parse props begin ...'); 9 | 10 | let props = []; 11 | 12 | // props: [], array 13 | if (item.value.type == 'ArrayExpression') { 14 | item.value.elements.forEach(elem => { 15 | // props: ['a'] 16 | if (elem.type == 'Literal') { 17 | props.push({ 18 | name: elem.value 19 | }); 20 | } 21 | // props: [var] 22 | if (elem.type == 'Identifier') { 23 | props.push({ 24 | name: elem.name + '(var)' 25 | }); 26 | } 27 | }); 28 | } 29 | 30 | // props: {}, object 31 | if (item.value.type == 'ObjectExpression') { 32 | item.value.properties.forEach(prop => { 33 | // test: Type 34 | if (prop.value.type == 'Identifier') { 35 | props.push({ 36 | name: prop.key.name, 37 | type: prop.value.name 38 | }); 39 | } 40 | // test: {type: Type} 41 | if (prop.value.type == 'ObjectExpression') { 42 | let obj = { 43 | name: prop.key.name 44 | }; 45 | // console.log(prop.value.properties) 46 | let type; 47 | let required; 48 | let def; 49 | let validator; 50 | 51 | prop.value.properties.forEach(item => { 52 | if (item.key.name == 'type') { 53 | type = item; 54 | } 55 | if (item.key.name == 'required') { 56 | required = item; 57 | } 58 | if (item.key.name == 'default') { 59 | def = item; 60 | } 61 | if (item.key.name == 'validator') { 62 | validator = item; 63 | } 64 | }); 65 | 66 | // prop.type 67 | if (type) { 68 | // type: function 69 | if (type.value.type == 'FunctionExpression') { 70 | let args = type.value.params.map(arg => { 71 | return escodegen.generate(arg); 72 | }); 73 | obj.type = `Function(${args.join(',')})`; 74 | } 75 | // type: Number ... 76 | if (type.value.type == 'Identifier') { 77 | obj.type = type.value.name; 78 | } 79 | } 80 | 81 | // prop.required 82 | if (required) { 83 | if (required.value.type == 'Identifier') { 84 | obj.required = required.value.name + '(var)'; 85 | } 86 | if (required.value.type == 'Literal') { 87 | obj.required = required.value.value; 88 | } 89 | } 90 | 91 | // prop: default 92 | if (def) { 93 | // default: function() {} 94 | if (def.value.type == 'FunctionExpression') { 95 | let args = def.value.params.map(arg => { 96 | return escodegen.generate(arg); 97 | }); 98 | obj.default = `Function(${args.join(',')})`; 99 | } 100 | // default: a 101 | if (def.value.type == 'Identifier') { 102 | obj.default = def.value.name + '(var)'; 103 | } 104 | // default: 2 105 | if (def.value.type == 'Literal') { 106 | obj.default = def.value.value; 107 | } 108 | } 109 | 110 | // prop: validator 111 | if (validator) { 112 | // default: function() {} 113 | if (validator.value.type == 'FunctionExpression') { 114 | let args = validator.value.params.map(arg => { 115 | return escodegen.generate(arg); 116 | }); 117 | obj.validator = `Function(${args.join(',')})`; 118 | } 119 | // default: Identifier 120 | if (validator.value.type == 'Identifier') { 121 | obj.validator = validator.value.name + '(var)'; 122 | } 123 | } 124 | 125 | props.push(obj); 126 | } 127 | }); 128 | } 129 | 130 | return props; 131 | } 132 | 133 | function parseMethods(item) { 134 | log('parse methods begin ...'); 135 | 136 | let methods = []; 137 | 138 | // error methods format 139 | if (item.value.type != 'ObjectExpression') return methods; 140 | 141 | item.value.properties.forEach(prop => { 142 | if (!prop.value) return; 143 | 144 | if (prop.value.type == 'FunctionExpression') { 145 | let params = prop.value.params.map(p => { 146 | return escodegen.generate(p); 147 | }); 148 | methods.push({ 149 | name: prop.key.name, 150 | code: `Function(${params.join(',')})` 151 | }); 152 | } 153 | 154 | if (prop.value.type == 'Identifier') { 155 | methods.push({ 156 | name: prop.key.name, 157 | code: prop.value.name + '(var)' 158 | }); 159 | } 160 | }); 161 | 162 | return methods; 163 | } 164 | 165 | function parseComponents(item) { 166 | log('parse components begin ...'); 167 | 168 | let components = []; 169 | 170 | // error format 171 | if (item.value.type != 'ObjectExpression') components; 172 | 173 | item.value.properties.forEach(prop => { 174 | if (prop.value.type == 'Identifier') { 175 | components.push(prop.value.name + '(var)'); 176 | } 177 | if (prop.value.type == 'Literal') { 178 | components.push(prop.value.value); 179 | } 180 | }); 181 | 182 | return components; 183 | } 184 | 185 | function parseEvents(ast) { 186 | // find emits 187 | let events = []; 188 | let emitList = []; 189 | 190 | estraverse.traverse(ast, { 191 | enter: function (node, parent) { 192 | if (skips.indexOf(node.type) != -1) return estraverse.VisitorOption.Skip; 193 | 194 | if (node.type == 'CallExpression') { 195 | if ( 196 | node.callee 197 | && node.callee.property 198 | && node.callee.property.name == '$emit' 199 | ) { 200 | emitList.push(node); 201 | } 202 | } 203 | }, 204 | leave: function (node, parent) { 205 | // 206 | } 207 | }); 208 | 209 | emitList.forEach(emit => { 210 | let obj = { 211 | code: escodegen.generate(emit) 212 | }; 213 | 214 | if (!emit.arguments.length) return; 215 | 216 | if (emit.arguments[0].type == 'Literal') { 217 | obj.name = emit.arguments[0].value; 218 | } 219 | 220 | if (emit.arguments[1]) { 221 | obj.data = escodegen.generate(emit.arguments[1]); 222 | } 223 | 224 | events.push(obj); 225 | }); 226 | 227 | return events; 228 | } 229 | 230 | function parseCode(code) { 231 | log('parse code begin ...'); 232 | 233 | const ast = espree.parse(code, { 234 | ecmaVersion: 9, 235 | sourceType: 'module', 236 | ecmaFeatures: { 237 | experimentalObjectRestSpread: true 238 | } 239 | }); 240 | 241 | const parsed = { 242 | name: '', 243 | options: [], 244 | props: [], 245 | events: [], 246 | components: [], 247 | computeds: [], 248 | }; 249 | 250 | // find exports 251 | let exportObj = null; 252 | estraverse.traverse(ast, { 253 | enter: function (node, parent) { 254 | if (skips.indexOf(node.type) != -1) return estraverse.VisitorOption.Skip; 255 | 256 | // export default 257 | if (node.type == 'ExportDefaultDeclaration') { 258 | exportObj = node.declaration; 259 | this.break(); 260 | } 261 | 262 | // module.exports 263 | if ( 264 | node.type == 'AssignmentExpression' 265 | && node.left.type == 'MemberExpression' 266 | && node.left.object.name == 'module' 267 | && node.left.property.name == 'exports' 268 | ) { 269 | exportObj = node.right; 270 | this.break(); 271 | } 272 | } 273 | }); 274 | 275 | if (!exportObj) return parsed; 276 | 277 | let propertyList = exportObj.properties || []; 278 | 279 | // events 280 | parsed.events = parseEvents(ast); 281 | 282 | // other 283 | propertyList.forEach(item => { 284 | // options 285 | parsed.options.push(item.key.name); 286 | 287 | switch(item.key.name) { 288 | case 'name': 289 | parsed.name = item.value.value; 290 | break; 291 | case 'props': 292 | parsed.props = parseProps(item); 293 | break; 294 | case 'methods': 295 | parsed.methods = parseMethods(item); 296 | break; 297 | case 'components': 298 | parsed.components = parseComponents(item); 299 | break; 300 | case 'computed': 301 | parsed.computeds = parseMethods(item); 302 | break; 303 | } 304 | }); 305 | 306 | // console.log(parsed) 307 | return parsed; 308 | } 309 | module.exports = parseCode; 310 | -------------------------------------------------------------------------------- /test/component/login.vue: -------------------------------------------------------------------------------- 1 | import api from '../lib/api'; 2 | import {getUrl} from '../lib/site'; 3 | 4 | /** 5 | * @vue 6 | * @exports component/login 7 | */ 8 | export default { 9 | name: 'Login', 10 | data: () => { 11 | return { 12 | username: '', 13 | password: '', 14 | }; 15 | }, 16 | methods: { 17 | submit() { 18 | if (!this.username.trim() || !this.password.trim()) { 19 | alert('请填写用户名密码!'); 20 | return; 21 | } 22 | 23 | api.postForm('api/auth/login', { 24 | username: this.username.trim(), 25 | password: this.password.trim() 26 | }).then(data => { 27 | alert ('登录成功'); 28 | location.href = getUrl('/app/'); 29 | }).catch(error => { 30 | alert(error.message); 31 | }); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/component/test.vue: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { mapGetters, mapActions, mapMutations } from 'vuex'; 3 | import userTypes from '../../vuex/types/user'; 4 | 5 | /** 6 | * @vue 7 | * @exports component/page/home 8 | */ 9 | export default { 10 | props: { 11 | testA: Number, 12 | testB: { 13 | type: String, 14 | required: false, 15 | default: function() { 16 | return {}; 17 | }, 18 | validator: function(value) { 19 | 20 | } 21 | }, 22 | testC: { 23 | type: function(value) { 24 | return value instanceof Number; 25 | }, 26 | default: 'test' 27 | } 28 | }, 29 | name: 'Home', 30 | components: { 31 | App, 32 | }, 33 | data() { 34 | return { 35 | filter: '', 36 | isLoading: false, 37 | } 38 | }, 39 | computed: { 40 | filterUsers() { 41 | if (this.filter.trim() == '') return this.userList; 42 | return this.userList.filter(user => { 43 | return user.name.indexOf(this.filter.trim()) != -1; 44 | }) 45 | }, 46 | ...mapGetters({ 47 | userList: userTypes.USER_LIST 48 | }) 49 | }, 50 | created() { 51 | this.$emit('update:foo', this.isFoled); 52 | }, 53 | methods: { 54 | ...mapActions({ 55 | userAdd: userTypes.USER_ADD, 56 | userDelete: userTypes.USER_DELETE 57 | }), 58 | // 跳过action,直接使用mutation 59 | ...mapMutations({ 60 | userShuffle:userTypes.USER_SHUFFLE 61 | }), 62 | addNewUser() { 63 | let str = 'abcdefghijklmnopqrstuvwxyz012345678ABCDEFGHIJKLMNOPQRSTUVWXYZ' 64 | let id = Math.round(Math.random() * 1000000000) 65 | let name = new Array(8).fill(1).map((item, index) => { 66 | return str[Math.round(Math.random() * (str.length - 1))]; 67 | }).join(''); 68 | 69 | this.isLoading = true 70 | this.userAdd({id, name}) 71 | .then(data => { 72 | this.isLoading = false 73 | }) 74 | }, 75 | }, 76 | mounted() { 77 | console.log('home ...') 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/parse.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const parse = require('../parse'); 4 | 5 | const code = fs.readFileSync(path.join(__dirname, './component/test.vue')); 6 | console.log('=========begin code=======') 7 | console.log(parse(code)); 8 | console.log('=========end code=======') 9 | 10 | const code2 = fs.readFileSync(path.join(__dirname, './component/login.vue')); 11 | console.log('=========begin code2=======') 12 | console.log(parse(code2)); 13 | console.log('=========end code2=======') 14 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | let config = { 2 | log: true, 3 | tag: 'vuedoc' 4 | } 5 | 6 | try { 7 | const env = require('jsdoc/env') 8 | config = Object.assign({}, config, env.conf['jsdoc-vue-component']); 9 | } catch (e) { 10 | // console.log(e); 11 | } 12 | 13 | function log(message) { 14 | if (!config.log) return; 15 | console.log('jsdoc-vue-component: '); 16 | console.log(message); 17 | } 18 | 19 | exports.config = config; 20 | exports.log = log; 21 | --------------------------------------------------------------------------------