62 | >
63 | // if there is no type annotation, get it from the decorators arguments
64 | if (!propPath.node.typeAnnotation) {
65 | describeType(propsPath, propDescriptor)
66 | }
67 | describeDefault(propsPath, propDescriptor)
68 | describeRequired(propsPath, propDescriptor)
69 | }
70 | }
71 | })
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/template-handlers/__tests__/propHandler.ts:
--------------------------------------------------------------------------------
1 | import { compile } from 'vue-template-compiler'
2 | import { Documentation } from '../../Documentation'
3 | import { traverse } from '../../parse-template'
4 | import propHandler from '../propHandler'
5 |
6 | describe('slotHandler', () => {
7 | let doc: Documentation
8 | beforeEach(() => {
9 | doc = new Documentation()
10 | })
11 |
12 | it('should match props in attributes expressions', () => {
13 | const ast = compile(
14 | [
15 | '',
16 | '
titleof the template
',
17 | ' ',
18 | ' ',
19 | ' ',
20 | '',
21 | ].join('\n'),
22 | { comments: true },
23 | ).ast
24 | if (ast) {
25 | traverse(ast, doc, [propHandler], { functional: true, rootLeadingComment: '' })
26 | expect(doc.toObject().props).toMatchObject({
27 | size: { type: { name: 'number' }, description: 'width of the button' },
28 | value: { type: { name: 'string' }, description: 'value in the form' },
29 | })
30 | } else {
31 | fail()
32 | }
33 | })
34 |
35 | it('should match props in interpolated text', () => {
36 | const ast = compile(
37 | [
38 | '',
39 | '
titleof the template
',
40 | ' ',
45 | '',
46 | ].join('\n'),
47 | { comments: true },
48 | ).ast
49 | if (ast) {
50 | traverse(ast, doc, [propHandler], { functional: true, rootLeadingComment: '' })
51 | expect(doc.toObject().props).toMatchObject({
52 | name: { type: { name: 'mixed' }, description: 'Your Name' },
53 | adress: { type: { name: 'string' }, description: 'Your Adress' },
54 | })
55 | } else {
56 | fail()
57 | }
58 | })
59 |
60 | it('should not match props if in a string litteral', () => {
61 | const ast = compile(
62 | [
63 | '',
64 | '
titleof the template
',
65 | ' ',
67 | ].join('\n'),
68 | { comments: true },
69 | ).ast
70 | if (ast) {
71 | traverse(ast, doc, [propHandler], { functional: true, rootLeadingComment: '' })
72 | expect(doc.toObject().props).toBeUndefined()
73 | } else {
74 | fail()
75 | }
76 | })
77 |
78 | it('should not match props if in a non evaluated attribute', () => {
79 | const ast = compile(
80 | [
81 | '',
82 | '
titleof the template
',
83 | ' ',
85 | ].join('\n'),
86 | { comments: true },
87 | ).ast
88 | if (ast) {
89 | traverse(ast, doc, [propHandler], { functional: true, rootLeadingComment: '' })
90 | expect(doc.toObject().props).toBeUndefined()
91 | } else {
92 | fail()
93 | }
94 | })
95 | })
96 |
--------------------------------------------------------------------------------
/src/script-handlers/__tests__/extendsHandler.ts:
--------------------------------------------------------------------------------
1 | import * as bt from '@babel/types'
2 | import babylon from '../../babel-parser'
3 | jest.mock('../../utils/resolveRequired')
4 | jest.mock('../../utils/resolvePathFrom')
5 | jest.mock('../../parse')
6 | import { Documentation } from '../../Documentation'
7 | import { parseFile } from '../../parse'
8 | import resolveExportedComponent from '../../utils/resolveExportedComponent'
9 | import resolvePathFrom from '../../utils/resolvePathFrom'
10 | import resolveRequired from '../../utils/resolveRequired'
11 | import extendsHandler from '../extendsHandler'
12 |
13 | describe('extendsHandler', () => {
14 | let resolveRequiredMock: jest.Mock
15 | let mockResolvePathFrom: jest.Mock
16 | let mockParse: jest.Mock
17 | const doc = new Documentation()
18 | beforeEach(() => {
19 | resolveRequiredMock = resolveRequired as jest.Mock<
20 | (ast: bt.File, varNameFilter?: string[]) => { [key: string]: string }
21 | >
22 | resolveRequiredMock.mockReturnValue({
23 | testComponent: { filePath: './componentPath', exportName: 'default' },
24 | })
25 |
26 | mockResolvePathFrom = resolvePathFrom as jest.Mock<(path: string, from: string) => string>
27 | mockResolvePathFrom.mockReturnValue('./component/full/path')
28 |
29 | mockParse = parseFile as jest.Mock
30 | mockParse.mockReturnValue({ component: 'documentation' })
31 | })
32 |
33 | function parseItExtends(src: string) {
34 | const ast = babylon().parse(src)
35 | const path = resolveExportedComponent(ast).get('default')
36 | if (path) {
37 | extendsHandler(doc, path, ast, { filePath: '' })
38 | }
39 | }
40 |
41 | it('should resolve extended modules variables in import default', () => {
42 | const src = [
43 | 'import testComponent from "./testComponent"',
44 | 'export default {',
45 | ' extends:testComponent',
46 | '}',
47 | ].join('\n')
48 | parseItExtends(src)
49 | expect(parseFile).toHaveBeenCalledWith(doc, {
50 | filePath: './component/full/path',
51 | nameFilter: ['default'],
52 | })
53 | })
54 |
55 | it('should resolve extended modules variables in require', () => {
56 | const src = [
57 | 'const testComponent = require("./testComponent");',
58 | 'export default {',
59 | ' extends:testComponent',
60 | '}',
61 | ].join('\n')
62 | parseItExtends(src)
63 | expect(parseFile).toHaveBeenCalledWith(doc, {
64 | filePath: './component/full/path',
65 | nameFilter: ['default'],
66 | })
67 | })
68 |
69 | it('should resolve extended modules variables in import', () => {
70 | const src = [
71 | 'import { test as testComponent, other } from "./testComponent"',
72 | 'export default {',
73 | ' extends:testComponent',
74 | '}',
75 | ].join('\n')
76 | parseItExtends(src)
77 | expect(parseFile).toHaveBeenCalledWith(doc, {
78 | filePath: './component/full/path',
79 | nameFilter: ['default'],
80 | })
81 | })
82 |
83 | it('should resolve extended modules variables in class style components', () => {
84 | const src = [
85 | 'import { testComponent } from "./testComponent";',
86 | '@Component',
87 | 'export default class Bart extends testComponent {',
88 | '}',
89 | ].join('\n')
90 | parseItExtends(src)
91 | expect(parseFile).toHaveBeenCalledWith(doc, {
92 | filePath: './component/full/path',
93 | nameFilter: ['default'],
94 | })
95 | })
96 | })
97 |
--------------------------------------------------------------------------------
/src/template-handlers/propHandler.ts:
--------------------------------------------------------------------------------
1 | import * as bt from '@babel/types'
2 | import recast from 'recast'
3 | import { ASTElement, ASTExpression, ASTNode } from 'vue-template-compiler'
4 | import buildParser from '../babel-parser'
5 | import { Documentation, ParamTag } from '../Documentation'
6 | import { TemplateParserOptions } from '../parse-template'
7 | import extractLeadingComment from '../utils/extractLeadingComment'
8 | import getDoclets from '../utils/getDoclets'
9 |
10 | const parser = buildParser({ plugins: ['typescript'] })
11 |
12 | const allowRE = /^(v-bind|:)/
13 | export default function propTemplateHandler(
14 | documentation: Documentation,
15 | templateAst: ASTElement,
16 | options: TemplateParserOptions,
17 | ) {
18 | if (options.functional) {
19 | propsInAttributes(templateAst, documentation, options)
20 | propsInInterpolation(templateAst, documentation, options)
21 | }
22 | }
23 |
24 | function propsInAttributes(
25 | templateAst: ASTElement,
26 | documentation: Documentation,
27 | options: TemplateParserOptions,
28 | ) {
29 | const bindings = templateAst.attrsMap
30 | const keys = Object.keys(bindings)
31 | for (const key of keys) {
32 | // only look at expressions
33 | if (allowRE.test(key)) {
34 | const expression = bindings[key]
35 | getPropsFromExpression(templateAst.parent, templateAst, expression, documentation, options)
36 | }
37 | }
38 | }
39 |
40 | function propsInInterpolation(
41 | templateAst: ASTElement,
42 | documentation: Documentation,
43 | options: TemplateParserOptions,
44 | ) {
45 | if (templateAst.children) {
46 | templateAst.children
47 | .filter(c => c.type === 2)
48 | .forEach((expr: ASTExpression) => {
49 | getPropsFromExpression(templateAst, expr, expr.expression, documentation, options)
50 | })
51 | }
52 | }
53 |
54 | function getPropsFromExpression(
55 | parentAst: ASTElement | undefined,
56 | item: ASTNode,
57 | expression: string,
58 | documentation: Documentation,
59 | options: TemplateParserOptions,
60 | ) {
61 | const ast = parser.parse(expression)
62 | const propsFound: string[] = []
63 | recast.visit(ast.program, {
64 | visitMemberExpression(path) {
65 | const obj = path.node ? path.node.object : undefined
66 | const propName = path.node ? path.node.property : undefined
67 | if (
68 | obj &&
69 | propName &&
70 | bt.isIdentifier(obj) &&
71 | obj.name === 'props' &&
72 | bt.isIdentifier(propName)
73 | ) {
74 | const pName = propName.name
75 | const p = documentation.getPropDescriptor(pName)
76 | propsFound.push(pName)
77 | p.type = { name: 'undefined' }
78 | }
79 | return false
80 | },
81 | })
82 | if (propsFound.length) {
83 | const comment = extractLeadingComment(parentAst, item, options.rootLeadingComment)
84 | const doclets = getDoclets(comment)
85 | const propTags = doclets.tags && (doclets.tags.filter(d => d.title === 'prop') as ParamTag[])
86 | if (propTags && propTags.length) {
87 | propsFound.forEach(pName => {
88 | const propTag = propTags.filter(pt => pt.name === pName)
89 | if (propTag.length) {
90 | const p = documentation.getPropDescriptor(pName)
91 | p.type = propTag[0].type
92 | if (typeof propTag[0].description === 'string') {
93 | p.description = propTag[0].description
94 | }
95 | }
96 | })
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/utils/resolveRequired.ts:
--------------------------------------------------------------------------------
1 | import * as bt from '@babel/types'
2 | import { NodePath } from 'ast-types'
3 | import recast from 'recast'
4 |
5 | interface ImportedVariable {
6 | filePath: string
7 | exportName: string
8 | }
9 |
10 | export interface ImportedVariableSet {
11 | [key: string]: ImportedVariable
12 | }
13 |
14 | /**
15 | *
16 | * @param ast
17 | * @param varNameFilter
18 | */
19 | export default function resolveRequired(
20 | ast: bt.File,
21 | varNameFilter?: string[],
22 | ): ImportedVariableSet {
23 | const varToFilePath: ImportedVariableSet = {}
24 |
25 | recast.visit(ast.program, {
26 | visitImportDeclaration(astPath: NodePath) {
27 | const specifiers = astPath.get('specifiers')
28 |
29 | // if `import 'module'` without variable name cannot be a mixin
30 | specifiers.each((sp: NodePath) => {
31 | const nodeSpecifier = sp.node
32 | if (bt.isImportDefaultSpecifier(nodeSpecifier) || bt.isImportSpecifier(nodeSpecifier)) {
33 | const localVariableName = nodeSpecifier.local.name
34 |
35 | const exportName = bt.isImportDefaultSpecifier(nodeSpecifier)
36 | ? 'default'
37 | : nodeSpecifier.imported.name
38 |
39 | if (!varNameFilter || varNameFilter.indexOf(localVariableName) > -1) {
40 | const nodeSource = (astPath.get('source') as NodePath).node
41 | if (bt.isStringLiteral(nodeSource)) {
42 | const filePath = nodeSource.value
43 | varToFilePath[localVariableName] = {
44 | filePath,
45 | exportName,
46 | }
47 | }
48 | }
49 | }
50 | })
51 | return false
52 | },
53 |
54 | visitVariableDeclaration(astPath: NodePath) {
55 | // only look at variable declarations
56 | if (!bt.isVariableDeclaration(astPath.node)) {
57 | return false
58 | }
59 | astPath.node.declarations.forEach(nodeDeclaration => {
60 | let sourceNode: bt.Node
61 | let source: string = ''
62 |
63 | const { init, exportName } =
64 | nodeDeclaration.init && bt.isMemberExpression(nodeDeclaration.init)
65 | ? {
66 | init: nodeDeclaration.init.object,
67 | exportName: bt.isIdentifier(nodeDeclaration.init.property)
68 | ? nodeDeclaration.init.property.name
69 | : 'default',
70 | }
71 | : { init: nodeDeclaration.init, exportName: 'default' }
72 | if (!init) {
73 | return
74 | }
75 |
76 | if (bt.isCallExpression(init)) {
77 | if (!bt.isIdentifier(init.callee) || init.callee.name !== 'require') {
78 | return
79 | }
80 | sourceNode = init.arguments[0]
81 | if (!bt.isStringLiteral(sourceNode)) {
82 | return
83 | }
84 | source = sourceNode.value
85 | } else {
86 | return
87 | }
88 |
89 | if (bt.isIdentifier(nodeDeclaration.id)) {
90 | const varName = nodeDeclaration.id.name
91 | varToFilePath[varName] = { filePath: source, exportName }
92 | } else if (bt.isObjectPattern(nodeDeclaration.id)) {
93 | nodeDeclaration.id.properties.forEach((p: bt.ObjectProperty) => {
94 | const varName = p.key.name
95 | varToFilePath[varName] = { filePath: source, exportName }
96 | })
97 | } else {
98 | return
99 | }
100 | })
101 | return false
102 | },
103 | })
104 |
105 | return varToFilePath
106 | }
107 |
--------------------------------------------------------------------------------
/tests/components/button/Button.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
138 |
139 |
151 |
--------------------------------------------------------------------------------
/src/script-handlers/__tests__/slotHandler.ts:
--------------------------------------------------------------------------------
1 | import { NodePath } from 'ast-types'
2 | import babylon from '../../babel-parser'
3 | import { Documentation, SlotDescriptor } from '../../Documentation'
4 | import resolveExportedComponent from '../../utils/resolveExportedComponent'
5 | import slotHandler from '../slotHandler'
6 |
7 | jest.mock('../../Documentation')
8 |
9 | function parse(src: string): NodePath | undefined {
10 | const ast = babylon().parse(src)
11 | return resolveExportedComponent(ast).get('default')
12 | }
13 |
14 | describe('render function slotHandler', () => {
15 | let documentation: Documentation
16 | let mockSlotDescriptor: SlotDescriptor
17 |
18 | beforeEach(() => {
19 | mockSlotDescriptor = { description: '' }
20 | documentation = new Documentation()
21 | const mockGetSlotDescriptor = documentation.getSlotDescriptor as jest.Mock
22 | mockGetSlotDescriptor.mockReturnValue(mockSlotDescriptor)
23 | })
24 |
25 | it('should find slots in render function', () => {
26 | const src = `
27 | export default {
28 | render: function (createElement) {
29 | return createElement('div', this.$slots.mySlot)
30 | }
31 | }
32 | `
33 | const def = parse(src)
34 | if (def) {
35 | slotHandler(documentation, def)
36 | }
37 | expect(documentation.getSlotDescriptor).toHaveBeenCalledWith('mySlot')
38 | })
39 |
40 | it('should find scoped slots in render function', () => {
41 | const src = `
42 | export default {
43 | render: function (createElement) {
44 | return createElement('div', [
45 | this.$scopedSlots.myScopedSlot({
46 | text: this.message
47 | })
48 | ])
49 | }
50 | }
51 | `
52 | const def = parse(src)
53 | if (def) {
54 | slotHandler(documentation, def)
55 | }
56 | expect(documentation.getSlotDescriptor).toHaveBeenCalledWith('myScopedSlot')
57 | })
58 |
59 | it('should find scoped slots in render object method', () => {
60 | const src = `
61 | export default {
62 | render(createElement) {
63 | return createElement('div', [
64 | this.$scopedSlots.myOtherScopedSlot({
65 | text: this.message
66 | })
67 | ])
68 | }
69 | }
70 | `
71 | const def = parse(src)
72 | if (def) {
73 | slotHandler(documentation, def)
74 | }
75 | expect(documentation.getSlotDescriptor).toHaveBeenCalledWith('myOtherScopedSlot')
76 | })
77 |
78 | it('should find slots in jsx render', () => {
79 | const src = `
80 | export default {
81 | render(createElement) {
82 | return (,
83 |
84 |
)
85 | }
86 | }
87 | `
88 | const def = parse(src)
89 | if (def) {
90 | slotHandler(documentation, def)
91 | }
92 | expect(documentation.getSlotDescriptor).toHaveBeenCalledWith('myMain')
93 | })
94 |
95 | it('should find default slots in jsx render', () => {
96 | const src = `
97 | export default {
98 | render(createElement) {
99 | return (,
100 |
101 |
)
102 | }
103 | }
104 | `
105 | const def = parse(src)
106 | if (def) {
107 | slotHandler(documentation, def)
108 | }
109 | expect(documentation.getSlotDescriptor).toHaveBeenCalledWith('default')
110 | })
111 |
112 | it('should allow describing slots in jsx render', () => {
113 | const src = `
114 | export default {
115 | render(createElement) {
116 | return (,
117 | {/** @slot Use this slot header */}
118 |
119 |
)
120 | }
121 | }
122 | `
123 | const def = parse(src)
124 | if (def) {
125 | slotHandler(documentation, def)
126 | }
127 | expect(mockSlotDescriptor.description).toEqual('Use this slot header')
128 | })
129 | })
130 |
--------------------------------------------------------------------------------
/src/script-handlers/__tests__/classPropHandler.ts:
--------------------------------------------------------------------------------
1 | import { NodePath } from 'ast-types'
2 | import Map from 'ts-map'
3 | import babylon from '../../babel-parser'
4 | import { Documentation, PropDescriptor } from '../../Documentation'
5 | import resolveExportedComponent from '../../utils/resolveExportedComponent'
6 | import classPropHandler from '../classPropHandler'
7 |
8 | jest.mock('../../Documentation')
9 |
10 | function parse(src: string): Map {
11 | const ast = babylon({ plugins: ['typescript'] }).parse(src)
12 | return resolveExportedComponent(ast)
13 | }
14 |
15 | describe('propHandler', () => {
16 | let documentation: Documentation
17 | let mockPropDescriptor: PropDescriptor
18 |
19 | beforeEach(() => {
20 | mockPropDescriptor = {
21 | description: '',
22 | required: '',
23 | tags: {},
24 | }
25 | const MockDocumentation = require('../../Documentation').Documentation
26 | documentation = new MockDocumentation()
27 | const mockGetPropDescriptor = documentation.getPropDescriptor as jest.Mock
28 | mockGetPropDescriptor.mockReturnValue(mockPropDescriptor)
29 | })
30 |
31 | function tester(src: string, matchedObj: any) {
32 | const def = parse(src).get('default')
33 | classPropHandler(documentation, def as any)
34 | expect(mockPropDescriptor).toMatchObject(matchedObj)
35 | }
36 |
37 | describe('base', () => {
38 | it('should ignore data that does not have the prop decorator', () => {
39 | const src = `
40 | @Component
41 | export default class MyComp {
42 | someData: boolean;
43 | }`
44 | tester(src, {})
45 | expect(documentation.getPropDescriptor).not.toHaveBeenCalledWith('someData')
46 | })
47 | it('should detect all data that have the prop decorator', () => {
48 | const src = `
49 | @Component
50 | export default class MyComp {
51 | @Prop
52 | test: string;
53 | }`
54 | tester(src, {
55 | type: { name: 'string' },
56 | })
57 | expect(documentation.getPropDescriptor).toHaveBeenCalledWith('test')
58 | })
59 |
60 | it('should get default expression from the prop decorator', () => {
61 | const src = `
62 | @Component
63 | export default class MyTest {
64 | @Prop({default: 'hello'})
65 | testDefault: string;
66 | }`
67 | tester(src, {
68 | type: { name: 'string' },
69 | defaultValue: {
70 | value: `"hello"`,
71 | },
72 | })
73 | expect(documentation.getPropDescriptor).toHaveBeenCalledWith('testDefault')
74 | })
75 |
76 | it('should get required from the prop decorator', () => {
77 | const src = `
78 | @Component
79 | export default class MyTest {
80 | @Prop({required: true})
81 | testRequired: string;
82 | }`
83 | tester(src, {
84 | type: { name: 'string' },
85 | required: true,
86 | })
87 | expect(documentation.getPropDescriptor).toHaveBeenCalledWith('testRequired')
88 | })
89 |
90 | it('should extract descriptions from leading comments', () => {
91 | const src = `
92 | @Component
93 | export default class MyTest {
94 | /**
95 | * A described prop
96 | **/
97 | @Prop
98 | testDescribed: boolean;
99 | }`
100 | tester(src, {
101 | description: 'A described prop',
102 | })
103 | expect(documentation.getPropDescriptor).toHaveBeenCalledWith('testDescribed')
104 | })
105 |
106 | it('should extract type from decorator arguments', () => {
107 | const src = `
108 | @Component
109 | export default class MyTest {
110 | @Prop({type:String})
111 | testTyped;
112 | }`
113 | tester(src, {
114 | type: { name: 'string' },
115 | })
116 | expect(documentation.getPropDescriptor).toHaveBeenCalledWith('testTyped')
117 | })
118 | })
119 | })
120 |
--------------------------------------------------------------------------------
/src/script-handlers/__tests__/eventHandler.ts:
--------------------------------------------------------------------------------
1 | import * as bt from '@babel/types'
2 | import { NodePath } from 'ast-types'
3 | import babylon from '../../babel-parser'
4 | import { Documentation, EventDescriptor } from '../../Documentation'
5 | import resolveExportedComponent from '../../utils/resolveExportedComponent'
6 | import eventHandler from '../eventHandler'
7 |
8 | jest.mock('../../Documentation')
9 |
10 | function parse(src: string): { component: NodePath | undefined; ast: bt.File } {
11 | const ast = babylon().parse(src)
12 | return { component: resolveExportedComponent(ast).get('default'), ast }
13 | }
14 |
15 | describe('eventHandler', () => {
16 | let documentation: Documentation
17 | let mockEventDescriptor: EventDescriptor
18 |
19 | beforeEach(() => {
20 | mockEventDescriptor = { description: '', properties: [] }
21 | documentation = new (require('../../Documentation')).Documentation()
22 | const mockGetEventDescriptor = documentation.getEventDescriptor as jest.Mock
23 | mockGetEventDescriptor.mockReturnValue(mockEventDescriptor)
24 | })
25 |
26 | it('should find events emmitted', () => {
27 | const src = `
28 | export default {
29 | methods: {
30 | testEmit() {
31 | /**
32 | * Describe the event
33 | * @property {number} prop1
34 | * @property {number} prop2
35 | */
36 | this.$emit('success', 1, 2)
37 | }
38 | }
39 | }
40 | `
41 | const def = parse(src)
42 | if (def.component) {
43 | eventHandler(documentation, def.component, def.ast)
44 | }
45 | const eventComp: EventDescriptor = {
46 | description: 'Describe the event',
47 | properties: [
48 | {
49 | name: 'prop1',
50 | type: {
51 | names: ['number'],
52 | },
53 | },
54 | {
55 | name: 'prop2',
56 | type: {
57 | names: ['number'],
58 | },
59 | },
60 | ],
61 | }
62 | expect(documentation.getEventDescriptor).toHaveBeenCalledWith('success')
63 | expect(mockEventDescriptor).toMatchObject(eventComp)
64 | })
65 |
66 | it('should find events undocumented properties', () => {
67 | const src = `
68 | export default {
69 | methods: {
70 | testEmit() {
71 | this.$emit('success', 1, 2)
72 | }
73 | }
74 | }
75 | `
76 | const def = parse(src)
77 | if (def.component) {
78 | eventHandler(documentation, def.component, def.ast)
79 | }
80 | const eventComp: EventDescriptor = {
81 | description: '',
82 | type: {
83 | names: ['undefined'],
84 | },
85 | properties: [
86 | {
87 | name: '',
88 | type: {
89 | names: ['undefined'],
90 | },
91 | },
92 | ],
93 | }
94 | expect(documentation.getEventDescriptor).toHaveBeenCalledWith('success')
95 | expect(mockEventDescriptor).toMatchObject(eventComp)
96 | })
97 |
98 | it('should find events names stored in variables', () => {
99 | const src = `
100 | const successEventName = 'success';
101 | export default {
102 | methods: {
103 | testEmit() {
104 | this.$emit(successEventName, 1, 2)
105 | }
106 | }
107 | }
108 | `
109 | const def = parse(src)
110 | if (def.component) {
111 | eventHandler(documentation, def.component, def.ast)
112 | }
113 | expect(documentation.getEventDescriptor).toHaveBeenCalledWith('success')
114 | })
115 |
116 | it('should find events whose names are only spcified in the JSDoc', () => {
117 | const src = `
118 | export default {
119 | methods: {
120 | testEmit() {
121 | /**
122 | * @event success
123 | */
124 | this.$emit(A.successEventName, 1, 2)
125 | }
126 | }
127 | }
128 | `
129 | const def = parse(src)
130 | if (def.component) {
131 | eventHandler(documentation, def.component, def.ast)
132 | }
133 | expect(documentation.getEventDescriptor).toHaveBeenCalledWith('success')
134 | })
135 | })
136 |
--------------------------------------------------------------------------------
/tests/components/grid-jsx/grid-jsx.test.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path'
2 | import { ComponentDoc, PropDescriptor } from '../../../src/Documentation'
3 | import { parse } from '../../../src/main'
4 |
5 | const grid = path.join(__dirname, './Grid.vue')
6 | let docGrid: ComponentDoc
7 |
8 | describe('tests grid jsx', () => {
9 | beforeAll(done => {
10 | docGrid = parse(grid)
11 | done()
12 | })
13 |
14 | it('should return an object', () => {
15 | expect(typeof docGrid).toBe('object')
16 | })
17 |
18 | it('The component name should be grid', () => {
19 | expect(docGrid.displayName).toEqual('grid')
20 | })
21 |
22 | it('should the component has tags', () => {
23 | expect(typeof docGrid.tags !== 'undefined').toBe(true)
24 | })
25 |
26 | it('should the component has authors', () => {
27 | expect(typeof docGrid.tags.author !== 'undefined').toBe(true)
28 | })
29 |
30 | it('should the component has description', () => {
31 | expect(typeof docGrid.description !== 'undefined').toBe(true)
32 | })
33 |
34 | it('should has methods', () => {
35 | expect(typeof docGrid.methods !== 'undefined').toBe(true)
36 | })
37 |
38 | it('should the component has one method', () => {
39 | expect(Object.keys(docGrid.methods).length).toEqual(1)
40 | })
41 |
42 | it('should has props', () => {
43 | expect(docGrid.props).not.toBeUndefined()
44 | })
45 |
46 | it('should the component has version', () => {
47 | expect(typeof docGrid.tags.version !== 'undefined').toBe(true)
48 | })
49 | describe('props', () => {
50 | let props: { [propName: string]: PropDescriptor }
51 |
52 | beforeAll(() => {
53 | props = docGrid.props ? docGrid.props : {}
54 | })
55 | it('should the component has four props', () => {
56 | expect(Object.keys(props).length).toEqual(6)
57 | })
58 |
59 | it('grid component should have a msg prop as string|number type', () => {
60 | expect(props.msg.type).toMatchObject({ name: 'string|number' })
61 | })
62 |
63 | it('grid component should have a filterKey prop as string type', () => {
64 | expect(props.filterKey.type).toMatchObject({ name: 'string' })
65 | })
66 |
67 | it('grid component should have a propFunc prop as func type', () => {
68 | expect(props.propFunc.type).toMatchObject({ name: 'func' })
69 | })
70 |
71 | it('grid component should have a images prop as Array type', () => {
72 | expect(props.images.type).toMatchObject({ name: 'array' })
73 | })
74 |
75 | it('grid component should have a data prop as Array type', () => {
76 | expect(props.data.type).toMatchObject({ name: 'array' })
77 | })
78 |
79 | it('grid component should have a columns prop as Array type', () => {
80 | expect(props.columns.type).toMatchObject({ name: 'array' })
81 | })
82 |
83 | it('should the prop msg has four tags', () => {
84 | expect(Object.keys(props.msg.tags).length).toEqual(4)
85 | })
86 | })
87 |
88 | it('should the component has two event', () => {
89 | expect(Object.keys(docGrid.events || {}).length).toEqual(2)
90 | })
91 |
92 | it('should the component has event, it called success', () => {
93 | expect(typeof (docGrid.events || {}).success !== 'undefined').toBe(true)
94 | })
95 |
96 | it('should the description of success event is Success event.', () => {
97 | expect((docGrid.events || {}).success.description).toEqual('Success event.')
98 | })
99 |
100 | it('should the component has event, it called error', () => {
101 | expect(typeof (docGrid.events || {}).error !== 'undefined').toBe(true)
102 | })
103 |
104 | it('should the description of error event is Error event.', () => {
105 | expect((docGrid.events || {}).error.description).toEqual('Error event.')
106 | })
107 |
108 | it('should define the return type of the first method', () => {
109 | expect(docGrid.methods[0].returns).toMatchObject({ description: 'Test' })
110 | })
111 |
112 | it('should return slots from the render method', () => {
113 | expect(docGrid.slots.header).toMatchObject({ description: 'Use this slot header' })
114 | })
115 |
116 | it('should match the snapshot', () => {
117 | expect(docGrid).toMatchSnapshot()
118 | })
119 | })
120 |
--------------------------------------------------------------------------------
/src/script-handlers/eventHandler.ts:
--------------------------------------------------------------------------------
1 | import * as bt from '@babel/types'
2 | import { NodePath } from 'ast-types'
3 | import recast from 'recast'
4 | import {
5 | BlockTag,
6 | DocBlockTags,
7 | Documentation,
8 | EventDescriptor,
9 | ParamTag,
10 | ParamType,
11 | Tag,
12 | } from '../Documentation'
13 | import getDocblock from '../utils/getDocblock'
14 | import getDoclets from '../utils/getDoclets'
15 | import resolveIdentifier from '../utils/resolveIdentifier'
16 |
17 | export interface TypedParamTag extends ParamTag {
18 | type: ParamType
19 | }
20 |
21 | export default function eventHandler(
22 | documentation: Documentation,
23 | path: NodePath,
24 | astPath: bt.File,
25 | ) {
26 | recast.visit(path.node, {
27 | visitCallExpression(pathExpression) {
28 | if (
29 | bt.isMemberExpression(pathExpression.node.callee) &&
30 | bt.isThisExpression(pathExpression.node.callee.object) &&
31 | bt.isIdentifier(pathExpression.node.callee.property) &&
32 | pathExpression.node.callee.property.name === '$emit'
33 | ) {
34 | const args = pathExpression.node.arguments
35 | if (!args.length) {
36 | return false
37 | }
38 | // fetch the leading comments on the wrapping expression
39 | const docblock = getDocblock(pathExpression.parentPath)
40 | const doclets = getDoclets(docblock || '')
41 | let eventName: string
42 | const eventTags = doclets.tags ? doclets.tags.filter(d => d.title === 'event') : []
43 |
44 | // if someone wants to document it with anything else, they can force it
45 | if (eventTags.length) {
46 | eventName = (eventTags[0] as Tag).content as string
47 | } else {
48 | let firstArg = pathExpression.get('arguments', 0)
49 | if (bt.isIdentifier(firstArg.node)) {
50 | firstArg = resolveIdentifier(astPath, firstArg)
51 | }
52 |
53 | if (!bt.isStringLiteral(firstArg.node)) {
54 | return false
55 | }
56 | eventName = firstArg.node.value
57 | }
58 |
59 | // if this event is documented somewhere else leave it alone
60 | const evtDescriptor = documentation.getEventDescriptor(eventName)
61 |
62 | setEventDescriptor(evtDescriptor, doclets)
63 |
64 | if (args.length > 1 && !evtDescriptor.type) {
65 | evtDescriptor.type = {
66 | names: ['undefined'],
67 | }
68 | }
69 |
70 | if (args.length > 2 && !evtDescriptor.properties) {
71 | evtDescriptor.properties = []
72 | }
73 | if (evtDescriptor.properties && evtDescriptor.properties.length < args.length - 2) {
74 | let i = args.length - 2 - evtDescriptor.properties.length
75 | while (i--) {
76 | evtDescriptor.properties.push({
77 | type: { names: ['undefined'] },
78 | name: ``,
79 | })
80 | }
81 | }
82 | return false
83 | }
84 | this.traverse(pathExpression)
85 | },
86 | })
87 | }
88 |
89 | export function setEventDescriptor(
90 | eventDescriptor: EventDescriptor,
91 | jsDoc: DocBlockTags,
92 | ): EventDescriptor {
93 | eventDescriptor.description = jsDoc.description || ''
94 |
95 | const nonNullTags: BlockTag[] = jsDoc.tags ? jsDoc.tags : []
96 |
97 | const typeTags = nonNullTags.filter(tg => tg.title === 'type')
98 | eventDescriptor.type = typeTags.length
99 | ? { names: typeTags.map((t: TypedParamTag) => t.type.name) }
100 | : undefined
101 |
102 | const propertyTags = nonNullTags.filter(tg => tg.title === 'property')
103 | if (propertyTags.length) {
104 | eventDescriptor.properties = propertyTags.map((tg: TypedParamTag) => {
105 | return { type: { names: [tg.type.name] }, name: tg.name, description: tg.description }
106 | })
107 | }
108 |
109 | // remove the property an type tags from the tag array
110 | const tags = nonNullTags.filter(
111 | (tg: BlockTag) => tg.title !== 'type' && tg.title !== 'property' && tg.title !== 'event',
112 | )
113 |
114 | if (tags.length) {
115 | eventDescriptor.tags = tags
116 | } else {
117 | delete eventDescriptor.tags
118 | }
119 |
120 | return eventDescriptor
121 | }
122 |
--------------------------------------------------------------------------------
/tests/components/grid-jsx/Grid.vue:
--------------------------------------------------------------------------------
1 |
2 |
157 |
158 |
163 |
--------------------------------------------------------------------------------
/tests/components/grid/Grid.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | |
9 | {{ key | capitalize }}
10 |
11 | |
12 |
13 |
14 |
15 |
16 | | {{entry[key]}} |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
161 |
162 |
167 |
--------------------------------------------------------------------------------
/src/script-handlers/slotHandler.ts:
--------------------------------------------------------------------------------
1 | import * as bt from '@babel/types'
2 | import { NodePath } from 'ast-types'
3 | import recast from 'recast'
4 | import { Documentation, ParamTag, ParamType, Tag } from '../Documentation'
5 | import getDoclets from '../utils/getDoclets'
6 |
7 | export interface TypedParamTag extends ParamTag {
8 | type: ParamType
9 | }
10 |
11 | export default function slotHandler(documentation: Documentation, path: NodePath) {
12 | if (bt.isObjectExpression(path.node)) {
13 | const renderPath = path
14 | .get('properties')
15 | .filter(
16 | (p: NodePath) =>
17 | (bt.isObjectProperty(p.node) || bt.isObjectMethod(p.node)) &&
18 | p.node.key.name === 'render',
19 | )
20 |
21 | // if no prop return
22 | if (!renderPath.length) {
23 | return
24 | }
25 |
26 | const renderValuePath = bt.isObjectProperty(renderPath[0].node)
27 | ? renderPath[0].get('value')
28 | : renderPath[0]
29 | recast.visit(renderValuePath.node, {
30 | visitCallExpression(pathCall) {
31 | if (
32 | bt.isMemberExpression(pathCall.node.callee) &&
33 | bt.isMemberExpression(pathCall.node.callee.object) &&
34 | bt.isThisExpression(pathCall.node.callee.object.object) &&
35 | bt.isIdentifier(pathCall.node.callee.property) &&
36 | pathCall.node.callee.object.property.name === '$scopedSlots'
37 | ) {
38 | documentation.getSlotDescriptor(pathCall.node.callee.property.name)
39 | return false
40 | }
41 | this.traverse(pathCall)
42 | },
43 | visitMemberExpression(pathMember) {
44 | if (
45 | bt.isMemberExpression(pathMember.node.object) &&
46 | bt.isThisExpression(pathMember.node.object.object) &&
47 | bt.isIdentifier(pathMember.node.object.property) &&
48 | pathMember.node.object.property.name === '$slots' &&
49 | bt.isIdentifier(pathMember.node.property)
50 | ) {
51 | documentation.getSlotDescriptor(pathMember.node.property.name)
52 | return false
53 | }
54 | this.traverse(pathMember)
55 | },
56 | visitJSXElement(pathJSX) {
57 | const tagName = pathJSX.node.openingElement.name
58 | const nodeJSX = pathJSX.node
59 | if (!bt.isJSXElement(nodeJSX)) {
60 | this.traverse(pathJSX)
61 | return
62 | }
63 | if (bt.isJSXIdentifier(tagName) && tagName.name === 'slot') {
64 | const doc = documentation.getSlotDescriptor(getName(nodeJSX))
65 | const parentNode = pathJSX.parentPath.node
66 | if (bt.isJSXElement(parentNode)) {
67 | doc.description = getDescription(nodeJSX, parentNode.children)
68 | }
69 | }
70 | this.traverse(pathJSX)
71 | },
72 | })
73 | }
74 | }
75 |
76 | function getName(nodeJSX: bt.JSXElement): string {
77 | const oe = nodeJSX.openingElement
78 | const names = oe.attributes.filter(
79 | (a: bt.JSXAttribute) => bt.isJSXAttribute(a) && a.name.name === 'name',
80 | ) as bt.JSXAttribute[]
81 |
82 | const nameNode = names.length ? names[0].value : null
83 | return nameNode && bt.isStringLiteral(nameNode) ? nameNode.value : 'default'
84 | }
85 |
86 | function getDescription(nodeJSX: bt.JSXElement, siblings: bt.Node[]): string {
87 | if (!siblings) {
88 | return ''
89 | }
90 | const indexInParent = siblings.indexOf(nodeJSX)
91 |
92 | let commentExpression: bt.JSXExpressionContainer | null = null
93 | for (let i = indexInParent - 1; i > -1; i--) {
94 | const currentNode = siblings[i]
95 | if (bt.isJSXExpressionContainer(currentNode)) {
96 | commentExpression = currentNode
97 | break
98 | }
99 | }
100 | if (!commentExpression || !commentExpression.expression.innerComments) {
101 | return ''
102 | }
103 | const cmts = commentExpression.expression.innerComments
104 | const lastComment = cmts[cmts.length - 1]
105 | if (lastComment.type !== 'CommentBlock') {
106 | return ''
107 | }
108 | const docBlock = lastComment.value.replace(/^\*/, '').trim()
109 | const jsDoc = getDoclets(docBlock)
110 | if (!jsDoc.tags) {
111 | return ''
112 | }
113 | const slotTags = jsDoc.tags.filter(t => t.title === 'slot')
114 | if (slotTags.length) {
115 | const tagContent = (slotTags[0] as Tag).content
116 | return typeof tagContent === 'string' ? tagContent : ''
117 | }
118 | return ''
119 | }
120 |
--------------------------------------------------------------------------------
/src/parse.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs'
2 | import * as path from 'path'
3 | import { parseComponent, SFCDescriptor } from 'vue-template-compiler'
4 | import { Documentation } from './Documentation'
5 | import parseScript, { Handler as ScriptHandler } from './parse-script'
6 | import parseTemplate, { Handler as TemplateHandler } from './parse-template'
7 | import scriptHandlers from './script-handlers'
8 | import templateHandlers from './template-handlers'
9 | import cacher from './utils/cacher'
10 |
11 | const ERROR_EMPTY_DOCUMENT = 'The passed source is empty'
12 |
13 | export { ScriptHandler, TemplateHandler }
14 |
15 | export interface ParseOptions extends DocGenOptions {
16 | filePath: string
17 | /**
18 | * In what language is the component written
19 | * @default undefined - let the system decide
20 | */
21 | lang?: 'ts' | 'js'
22 | }
23 |
24 | export interface DocGenOptions {
25 | /**
26 | * Which exported variables should be looked at
27 | * @default undefined - means treat all dependencies
28 | */
29 | nameFilter?: string[]
30 | /**
31 | * What alias should be replaced in requires and imports
32 | */
33 | alias?: { [alias: string]: string }
34 | /**
35 | * What directories should be searched when resolving modules
36 | */
37 | modules?: string[]
38 | /**
39 | * Handlers that will be added at the end of the script analysis
40 | */
41 | addScriptHandlers?: ScriptHandler[]
42 | /**
43 | * Handlers that will be added at the end of the template analysis
44 | */
45 | addTemplateHandlers?: TemplateHandler[]
46 | }
47 |
48 | /**
49 | * parses the source and returns the doc
50 | * @param {string} source code whose documentation is parsed
51 | * @param {string} filePath path of the current file against whom to resolve the mixins
52 | * @returns {object} documentation object
53 | */
54 | export function parseFile(documentation: Documentation, opt: ParseOptions) {
55 | const source = fs.readFileSync(opt.filePath, {
56 | encoding: 'utf-8',
57 | })
58 | return parseSource(documentation, source, opt)
59 | }
60 |
61 | /**
62 | * parses the source and returns the doc
63 | * @param {string} source code whose documentation is parsed
64 | * @param {string} filePath path of the current file against whom to resolve the mixins
65 | * @returns {object} documentation object
66 | */
67 | export function parseSource(documentation: Documentation, source: string, opt: ParseOptions) {
68 | const singleFileComponent = /\.vue$/i.test(path.extname(opt.filePath))
69 | let parts: SFCDescriptor | null = null
70 |
71 | if (source === '') {
72 | throw new Error(ERROR_EMPTY_DOCUMENT)
73 | }
74 |
75 | if (singleFileComponent) {
76 | parts = cacher(() => parseComponent(source), source)
77 | }
78 |
79 | // get slots and props from template
80 | if (parts && parts.template) {
81 | const extTemplSrc: string =
82 | parts && parts.template && parts.template.attrs ? parts.template.attrs.src : ''
83 | const extTemplSource =
84 | extTemplSrc && extTemplSrc.length
85 | ? fs.readFileSync(path.resolve(path.dirname(opt.filePath), extTemplSrc), {
86 | encoding: 'utf-8',
87 | })
88 | : ''
89 | if (extTemplSource.length) {
90 | parts.template.content = extTemplSource
91 | }
92 | const addTemplateHandlers: TemplateHandler[] = opt.addTemplateHandlers || []
93 | parseTemplate(
94 | parts.template,
95 | documentation,
96 | [...templateHandlers, ...addTemplateHandlers],
97 | opt.filePath,
98 | )
99 | }
100 |
101 | const extSrc: string = parts && parts.script && parts.script.attrs ? parts.script.attrs.src : ''
102 | const extSource =
103 | extSrc && extSrc.length
104 | ? fs.readFileSync(path.resolve(path.dirname(opt.filePath), extSrc), {
105 | encoding: 'utf-8',
106 | })
107 | : ''
108 |
109 | const scriptSource = extSource.length
110 | ? extSource
111 | : parts
112 | ? parts.script
113 | ? parts.script.content
114 | : undefined
115 | : source
116 | if (scriptSource) {
117 | opt.lang =
118 | (parts && parts.script && parts.script.attrs && parts.script.attrs.lang === 'ts') ||
119 | /\.tsx?$/i.test(path.extname(opt.filePath)) ||
120 | /\.tsx?$/i.test(extSrc)
121 | ? 'ts'
122 | : 'js'
123 |
124 | const addScriptHandlers: ScriptHandler[] = opt.addScriptHandlers || []
125 | if (scriptSource) {
126 | parseScript(scriptSource, documentation, [...scriptHandlers, ...addScriptHandlers], opt)
127 | }
128 | }
129 |
130 | if (!documentation.get('displayName')) {
131 | // a component should always have a display name
132 | documentation.set('displayName', path.basename(opt.filePath).replace(/\.\w+$/, ''))
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/utils/resolveExportedComponent.ts:
--------------------------------------------------------------------------------
1 | import * as bt from '@babel/types'
2 | import { NodePath } from 'ast-types'
3 | import recast from 'recast'
4 | import Map from 'ts-map'
5 | import isExportedAssignment from './isExportedAssignment'
6 | import resolveExportDeclaration from './resolveExportDeclaration'
7 | import resolveIdentifier from './resolveIdentifier'
8 |
9 | function ignore(): boolean {
10 | return false
11 | }
12 |
13 | function isComponentDefinition(path: NodePath): boolean {
14 | return (
15 | // export default {}
16 | bt.isObjectExpression(path.node) ||
17 | // export const myComp = {}
18 | (bt.isVariableDeclarator(path.node) &&
19 | path.node.init &&
20 | bt.isObjectExpression(path.node.init)) ||
21 | // export default Vue.extends({})
22 | (bt.isCallExpression(path.node) &&
23 | bt.isMemberExpression(path.node.callee) &&
24 | bt.isIdentifier(path.node.callee.object) &&
25 | path.node.callee.object.name === 'Vue' &&
26 | path.node.callee.property.name === 'extend') ||
27 | // @Component
28 | // export default class MyComp extends VueComp
29 | (bt.isClassDeclaration(path.node) &&
30 | (path.node.decorators || []).some(d => {
31 | const exp = bt.isCallExpression(d.expression) ? d.expression.callee : d.expression
32 | return bt.isIdentifier(exp) && exp.name === 'Component'
33 | }))
34 | )
35 | }
36 |
37 | /**
38 | * Given an AST, this function tries to find the exported component definitions.
39 | *
40 | * If a definition is part of the following statements, it is considered to be
41 | * exported:
42 | *
43 | * modules.exports = Definition;
44 | * exports.foo = Definition;
45 | * export default Definition;
46 | * export var Definition = ...;
47 | */
48 | export default function resolveExportedComponent(ast: bt.File): Map {
49 | const components = new Map()
50 |
51 | function setComponent(exportName: string, definition: NodePath) {
52 | if (definition && !components.get(exportName)) {
53 | components.set(exportName, normalizeComponentPath(definition))
54 | }
55 | }
56 |
57 | // function run for every non "assignment" export declaration
58 | // in extenso export default or export myvar
59 | function exportDeclaration(path: NodePath) {
60 | const definitions = resolveExportDeclaration(path)
61 |
62 | definitions.forEach((definition: NodePath, name: string) => {
63 | const realDef = resolveIdentifier(ast, definition)
64 | if (realDef && isComponentDefinition(realDef)) {
65 | setComponent(name, realDef)
66 | }
67 | })
68 | return false
69 | }
70 |
71 | recast.visit(ast.program, {
72 | // to look only at the root we ignore all traversing
73 | visitFunctionDeclaration: ignore,
74 | visitFunctionExpression: ignore,
75 | visitClassDeclaration: ignore,
76 | visitClassExpression: ignore,
77 | visitIfStatement: ignore,
78 | visitWithStatement: ignore,
79 | visitSwitchStatement: ignore,
80 | visitWhileStatement: ignore,
81 | visitDoWhileStatement: ignore,
82 | visitForStatement: ignore,
83 | visitForInStatement: ignore,
84 |
85 | visitDeclareExportDeclaration: exportDeclaration,
86 | visitExportNamedDeclaration: exportDeclaration,
87 | visitExportDefaultDeclaration: exportDeclaration,
88 |
89 | visitAssignmentExpression(path: NodePath) {
90 | // function run on every assignments (with an =)
91 |
92 | // Ignore anything that is not `exports.X = ...;` or
93 | // `module.exports = ...;`
94 | if (!isExportedAssignment(path)) {
95 | return false
96 | }
97 |
98 | // Resolve the value of the right hand side. It should resolve to a call
99 | // expression, something like Vue.extend({})
100 | const pathRight = path.get('right')
101 | const pathLeft = path.get('left')
102 | const realComp = resolveIdentifier(ast, pathRight)
103 | if (!realComp || !isComponentDefinition(realComp)) {
104 | return false
105 | }
106 |
107 | const name =
108 | bt.isMemberExpression(pathLeft.node) &&
109 | bt.isIdentifier(pathLeft.node.property) &&
110 | pathLeft.node.property.name !== 'exports'
111 | ? pathLeft.node.property.name
112 | : 'default'
113 |
114 | setComponent(name, realComp)
115 | return false
116 | },
117 | })
118 |
119 | return components
120 | }
121 |
122 | function normalizeComponentPath(path: NodePath): NodePath {
123 | if (bt.isObjectExpression(path.node)) {
124 | return path
125 | } else if (bt.isCallExpression(path.node)) {
126 | return path.get('arguments', 0)
127 | } else if (bt.isVariableDeclarator(path.node)) {
128 | return path.get('init')
129 | }
130 | return path
131 | }
132 |
--------------------------------------------------------------------------------
/tests/components/grid-typescript/Grid.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | |
9 | {{ key | capitalize }}
10 |
11 | |
12 |
13 |
14 |
15 |
16 | | {{entry[key]}} |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
182 |
183 |
188 |
--------------------------------------------------------------------------------
/tests/components/grid-typescript/grid-ts.test.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path'
2 | import { ComponentDoc, Param, PropDescriptor } from '../../../src/Documentation'
3 | import { parse } from '../../../src/main'
4 |
5 | const typescriptGrid = path.join(__dirname, './Grid.vue')
6 | let docGrid: ComponentDoc
7 | describe('tests typescript grid', () => {
8 | beforeAll(done => {
9 | docGrid = parse(typescriptGrid)
10 | done()
11 | })
12 |
13 | it('should return an object', () => {
14 | expect(typeof docGrid).toBe('object')
15 | })
16 |
17 | it('The component name should be grid', () => {
18 | expect(docGrid.displayName).toEqual('grid')
19 | })
20 |
21 | it('should the component has tags', () => {
22 | expect(docGrid.tags).not.toBeUndefined()
23 | })
24 |
25 | it('should the component has authors', () => {
26 | expect(docGrid.tags.author).not.toBeUndefined()
27 | })
28 |
29 | it('should the component has description', () => {
30 | expect(docGrid.description).not.toBeUndefined()
31 | })
32 |
33 | it('should have methods', () => {
34 | expect(docGrid.methods).not.toBeUndefined()
35 | })
36 |
37 | it('should the component has one method', () => {
38 | expect(Object.keys(docGrid.methods).length).toEqual(2)
39 | })
40 |
41 | it('should has props', () => {
42 | expect(docGrid.props).not.toBeUndefined()
43 | })
44 |
45 | it('should the component has version', () => {
46 | expect(docGrid.tags.version).not.toBeUndefined()
47 | })
48 |
49 | describe('props', () => {
50 | let props: { [propName: string]: PropDescriptor }
51 |
52 | beforeAll(() => {
53 | props = docGrid.props ? docGrid.props : {}
54 | })
55 |
56 | it('should the component has four props', () => {
57 | expect(Object.keys(props).length).toEqual(6)
58 | })
59 |
60 | it('grid component should have a msg prop as string|number type', () => {
61 | expect(props.msg.type).toMatchObject({ name: 'string|number' })
62 | })
63 |
64 | it('grid component should have a filterKey prop as string type', () => {
65 | expect(props.filterKey.type).toMatchObject({ name: 'string' })
66 | })
67 |
68 | it('grid component should have a propFunc prop as func type', () => {
69 | expect(props.propFunc.type).toMatchObject({ name: 'func' })
70 | })
71 |
72 | it('grid component should have a images prop as Array type', () => {
73 | expect(props.images.type).toMatchObject({ name: 'array' })
74 | })
75 |
76 | it('grid component should have a data prop as Array type', () => {
77 | expect(props.data.type).toMatchObject({ name: 'array' })
78 | })
79 |
80 | it('grid component should have a columns prop as Array type', () => {
81 | expect(props.columns.type).toMatchObject({ name: 'array' })
82 | })
83 |
84 | it('should the prop msg has four tags', () => {
85 | expect(Object.keys(props.msg.tags).length).toEqual(4)
86 | })
87 | })
88 |
89 | it('should extract the type of the argument from typescript', () => {
90 | const publicMethod = docGrid.methods.filter(m => m.name === 'publicMethod')[0]
91 | const safePublicMethodParams: Param[] =
92 | publicMethod && publicMethod.params ? publicMethod.params : []
93 |
94 | expect(safePublicMethodParams[0]).toMatchObject({
95 | type: {
96 | name: 'number',
97 | },
98 | })
99 |
100 | expect(safePublicMethodParams[1]).toMatchObject({
101 | type: {
102 | name: 'ForParam',
103 | },
104 | })
105 | })
106 |
107 | it('should the component has two event', () => {
108 | expect(Object.keys(docGrid.events || {}).length).toEqual(2)
109 | })
110 |
111 | it('should the component has event, it called success', () => {
112 | expect(typeof (docGrid.events || {}).success).not.toBeUndefined()
113 | })
114 |
115 | it('should the description of success event is Success event.', () => {
116 | expect((docGrid.events || {}).success.description).toEqual('Success event.')
117 | })
118 |
119 | it('should the component has event, it called error', () => {
120 | expect(typeof (docGrid.events || {}).error).not.toBeUndefined()
121 | })
122 |
123 | it('should the description of error event is Error event.', () => {
124 | expect((docGrid.events || {}).error.description).toEqual('Error event.')
125 | })
126 | it('should have two slots.', () => {
127 | expect(Object.keys(docGrid.slots).length).toEqual(2)
128 | })
129 |
130 | it('should have a slot named header.', () => {
131 | expect(typeof docGrid.slots.header).not.toBeUndefined()
132 | })
133 |
134 | it('the header slot should have "Use this slot header" as description', () => {
135 | expect(docGrid.slots.header.description).toEqual('Use this slot header')
136 | })
137 |
138 | it('should have a slot named footer.', () => {
139 | expect(typeof docGrid.slots.footer).not.toBeUndefined()
140 | })
141 |
142 | it('the footer slot should have "Use this slot footer" as description', () => {
143 | expect(docGrid.slots.footer.description).toEqual('Use this slot footer')
144 | })
145 |
146 | it('should match the snapshot', () => {
147 | expect(docGrid).toMatchSnapshot()
148 | })
149 | })
150 |
--------------------------------------------------------------------------------
/tests/components/grid/grid.test.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path'
2 | import { ComponentDoc, PropDescriptor } from '../../../src/Documentation'
3 | import { parse } from '../../../src/main'
4 | const grid = path.join(__dirname, './Grid.vue')
5 | let docGrid: ComponentDoc
6 |
7 | describe('tests grid', () => {
8 | beforeEach(done => {
9 | docGrid = parse(grid)
10 | done()
11 | })
12 |
13 | it('should return an object', () => {
14 | expect(typeof docGrid).toBe('object')
15 | })
16 |
17 | it('The component name should be grid', () => {
18 | expect(docGrid.displayName).toEqual('grid')
19 | })
20 |
21 | it('should the component has tags', () => {
22 | expect(typeof docGrid.tags !== 'undefined').toBe(true)
23 | })
24 |
25 | it('should the component has authors', () => {
26 | expect(typeof docGrid.tags.author !== 'undefined').toBe(true)
27 | })
28 |
29 | it('should the component has description', () => {
30 | expect(typeof docGrid.description !== 'undefined').toBe(true)
31 | })
32 |
33 | it('should has methods', () => {
34 | expect(typeof docGrid.methods !== 'undefined').toBe(true)
35 | })
36 |
37 | it('should return one method for the component', () => {
38 | expect(docGrid.methods.length).toEqual(1)
39 | })
40 |
41 | it('should return one param for this method', () => {
42 | const params = docGrid.methods[0].params
43 | expect(params !== undefined ? params.length : 0).toEqual(1)
44 | })
45 |
46 | it('should return the correct object for this param', () => {
47 | expect(docGrid.methods[0].params).toMatchObject([
48 | {
49 | name: 'key',
50 | description: 'Key to order',
51 | },
52 | ])
53 | })
54 |
55 | it('should has props', () => {
56 | expect(docGrid.props).not.toBeUndefined()
57 | })
58 |
59 | it('should the component has version', () => {
60 | expect(docGrid.tags.version).not.toBeUndefined()
61 | })
62 |
63 | describe('props', () => {
64 | let props: { [propName: string]: PropDescriptor }
65 |
66 | beforeAll(() => {
67 | props = docGrid.props ? docGrid.props : {}
68 | })
69 | it('should the component has four props', () => {
70 | expect(Object.keys(props).length).toEqual(6)
71 | })
72 |
73 | it('grid component should have a msg prop as string|number type', () => {
74 | expect(props.msg.type).toMatchObject({ name: 'string|number' })
75 | })
76 |
77 | it('grid component should have a filterKey prop as string type', () => {
78 | expect(props.filterKey.type).toMatchObject({ name: 'string' })
79 | })
80 |
81 | it('grid component should have a propFunc prop as func type', () => {
82 | expect(props.propFunc.type).toMatchObject({ name: 'func' })
83 | })
84 |
85 | it('grid component should have a images prop as Array type', () => {
86 | expect(props.images.type).toMatchObject({ name: 'array' })
87 | })
88 |
89 | it('grid component should have a data prop as Array type', () => {
90 | expect(props.data.type).toMatchObject({ name: 'array' })
91 | })
92 |
93 | it('grid component should have a columns prop as Array type', () => {
94 | expect(props.columns.type).toMatchObject({ name: 'array' })
95 | })
96 | it('should the prop msg has four tags', () => {
97 | expect(Object.keys(props.msg.tags).length).toEqual(4)
98 | })
99 | })
100 |
101 | it('should the component has two event', () => {
102 | expect(Object.keys(docGrid.events || {}).length).toEqual(2)
103 | })
104 |
105 | it('should the component has event, it called success', () => {
106 | expect((docGrid.events || {}).success).not.toBeUndefined()
107 | })
108 |
109 | it('should the description of success event is Success event.', () => {
110 | expect((docGrid.events || {}).success.description).toEqual('Success event.')
111 | })
112 |
113 | it('should the component has event, it called error', () => {
114 | expect((docGrid.events || {}).error).not.toBeUndefined()
115 | })
116 |
117 | it('should the description of error event is Error event.', () => {
118 | expect((docGrid.events || {}).error.description).toEqual('Error event.')
119 | })
120 |
121 | it('should make the type of error event an object.', () => {
122 | expect((docGrid.events || {}).error).toMatchObject({ type: { names: ['object'] } })
123 | })
124 |
125 | it('should have two slots.', () => {
126 | expect(Object.keys(docGrid.slots).length).toEqual(2)
127 | })
128 |
129 | it('should have a slot named header.', () => {
130 | expect(typeof docGrid.slots.header !== 'undefined').toBe(true)
131 | })
132 |
133 | it('the header slot should have "Use this slot header" as description', () => {
134 | expect(docGrid.slots.header.description).toEqual('Use this slot header')
135 | })
136 |
137 | it('should have a slot named footer.', () => {
138 | expect(typeof docGrid.slots.footer !== 'undefined').toBe(true)
139 | })
140 |
141 | it('the footer slot should have "Use this slot footer" as description', () => {
142 | expect(docGrid.slots.footer.description).toEqual('Use this slot footer')
143 | })
144 |
145 | it('should match the snapshot', () => {
146 | expect(docGrid).toMatchSnapshot()
147 | })
148 | })
149 |
--------------------------------------------------------------------------------
/src/script-handlers/methodHandler.ts:
--------------------------------------------------------------------------------
1 | import * as bt from '@babel/types'
2 | import { NodePath } from 'ast-types'
3 | import {
4 | BlockTag,
5 | DocBlockTags,
6 | Documentation,
7 | MethodDescriptor,
8 | Param,
9 | ParamTag,
10 | Tag,
11 | } from '../Documentation'
12 | import getDocblock from '../utils/getDocblock'
13 | import getDoclets from '../utils/getDoclets'
14 | import getTypeFromAnnotation from '../utils/getTypeFromAnnotation'
15 | import transformTagsIntoObject from '../utils/transformTagsIntoObject'
16 |
17 | export default function methodHandler(documentation: Documentation, path: NodePath) {
18 | if (bt.isObjectExpression(path.node)) {
19 | const methodsPath = path
20 | .get('properties')
21 | .filter(
22 | (p: NodePath) => bt.isObjectProperty(p.node) && p.node.key.name === 'methods',
23 | ) as Array>
24 |
25 | // if no method return
26 | if (!methodsPath.length) {
27 | return
28 | }
29 |
30 | const methodsObject = methodsPath[0].get('value')
31 | if (bt.isObjectExpression(methodsObject.node)) {
32 | methodsObject.get('properties').each((p: NodePath) => {
33 | let methodName = ''
34 | if (bt.isObjectProperty(p.node)) {
35 | const val = p.get('value')
36 | methodName = p.node.key.name
37 | if (!Array.isArray(val)) {
38 | p = val
39 | }
40 | }
41 | if (bt.isFunction(p.node)) {
42 | methodName = bt.isObjectMethod(p.node) ? p.node.key.name : methodName
43 |
44 | const docBlock = getDocblock(bt.isObjectMethod(p.node) ? p : p.parentPath)
45 |
46 | const jsDoc: DocBlockTags = docBlock
47 | ? getDoclets(docBlock)
48 | : { description: '', tags: [] }
49 | const jsDocTags: BlockTag[] = jsDoc.tags ? jsDoc.tags : []
50 |
51 | // ignore the method if there is no public tag
52 | if (!jsDocTags.some((t: Tag) => t.title === 'access' && t.content === 'public')) {
53 | return
54 | }
55 |
56 | const methodDescriptor = documentation.getMethodDescriptor(methodName)
57 |
58 | if (jsDoc.description) {
59 | methodDescriptor.description = jsDoc.description
60 | }
61 | setMethodDescriptor(methodDescriptor, p as NodePath, jsDocTags)
62 | }
63 | })
64 | }
65 | }
66 | }
67 |
68 | export function setMethodDescriptor(
69 | methodDescriptor: MethodDescriptor,
70 | method: NodePath,
71 | jsDocTags: BlockTag[],
72 | ) {
73 | // params
74 | describeParams(method, methodDescriptor, jsDocTags.filter(tag => tag.title === 'param'))
75 |
76 | // returns
77 | describeReturns(method, methodDescriptor, jsDocTags.filter(t => t.title === 'returns'))
78 |
79 | // tags
80 | methodDescriptor.tags = transformTagsIntoObject(jsDocTags)
81 |
82 | return methodDescriptor
83 | }
84 |
85 | function describeParams(
86 | methodPath: NodePath,
87 | methodDescriptor: MethodDescriptor,
88 | jsDocParamTags: ParamTag[],
89 | ) {
90 | // if there is no parameter no need to parse them
91 | const fExp = methodPath.node
92 | if (!fExp.params.length && !jsDocParamTags.length) {
93 | return
94 | }
95 |
96 | const params: Param[] = []
97 | fExp.params.forEach((par: bt.Identifier, i) => {
98 | const param: Param = { name: par.name }
99 |
100 | const jsDocTags = jsDocParamTags.filter(tag => tag.name === param.name)
101 | let jsDocTag = jsDocTags.length ? jsDocTags[0] : undefined
102 |
103 | // if tag is not namely described try finding it by its order
104 | if (!jsDocTag) {
105 | if (jsDocParamTags[i] && !jsDocParamTags[i].name) {
106 | jsDocTag = jsDocParamTags[i]
107 | }
108 | }
109 |
110 | if (jsDocTag) {
111 | if (jsDocTag.type) {
112 | param.type = jsDocTag.type
113 | }
114 | if (jsDocTag.description) {
115 | param.description = jsDocTag.description
116 | }
117 | }
118 |
119 | if (!param.type && par.typeAnnotation) {
120 | const type = getTypeFromAnnotation(par.typeAnnotation)
121 | if (type) {
122 | param.type = type
123 | }
124 | }
125 |
126 | params.push(param)
127 | })
128 |
129 | // in case the arguments are abstracted (using the arguments keyword)
130 | if (!params.length) {
131 | jsDocParamTags.forEach(doc => {
132 | params.push(doc)
133 | })
134 | }
135 |
136 | if (params.length) {
137 | methodDescriptor.params = params
138 | }
139 | }
140 |
141 | function describeReturns(
142 | methodPath: NodePath,
143 | methodDescriptor: MethodDescriptor,
144 | jsDocReturnTags: ParamTag[],
145 | ) {
146 | if (jsDocReturnTags.length) {
147 | methodDescriptor.returns = jsDocReturnTags[0]
148 | }
149 |
150 | if (!methodDescriptor.returns || !methodDescriptor.returns.type) {
151 | const methodNode = methodPath.node
152 | if (methodNode.returnType) {
153 | const type = getTypeFromAnnotation(methodNode.returnType)
154 | if (type) {
155 | methodDescriptor.returns = methodDescriptor.returns || {}
156 | methodDescriptor.returns.type = type
157 | }
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/Documentation.ts:
--------------------------------------------------------------------------------
1 | import Map from 'ts-map'
2 |
3 | export type BlockTag = ParamTag | Tag
4 |
5 | export interface ParamType {
6 | name: string
7 | elements?: ParamType[]
8 | }
9 |
10 | export interface UnnamedParam {
11 | type?: ParamType
12 | description?: string | boolean
13 | }
14 |
15 | export interface Param extends UnnamedParam {
16 | name?: string
17 | }
18 |
19 | interface RootTag {
20 | title: string
21 | }
22 |
23 | export interface Tag extends RootTag {
24 | content: string | boolean
25 | }
26 |
27 | export interface ParamTag extends RootTag, Param {}
28 |
29 | export interface DocBlockTags {
30 | description: string
31 | tags?: Array
32 | }
33 |
34 | interface EventType {
35 | names: string[]
36 | }
37 |
38 | interface EventProperty {
39 | type: EventType
40 | name?: string
41 | description?: string | boolean
42 | }
43 |
44 | export interface EventDescriptor extends DocBlockTags {
45 | type?: EventType
46 | properties: EventProperty[] | undefined
47 | }
48 |
49 | export interface PropDescriptor {
50 | type?: { name: string; func?: boolean }
51 | description: string
52 | required?: string | boolean
53 | defaultValue?: { value: string; func?: boolean }
54 | tags: { [title: string]: BlockTag[] }
55 | }
56 |
57 | export interface MethodDescriptor {
58 | name: string
59 | description: string
60 | returns?: UnnamedParam
61 | tags?: { [key: string]: BlockTag[] }
62 | params?: Param[]
63 | modifiers: string[]
64 | [key: string]: any
65 | }
66 |
67 | export interface SlotDescriptor {
68 | description?: string
69 | bindings?: Record
70 | scoped?: boolean
71 | }
72 |
73 | export interface ComponentDoc {
74 | displayName: string
75 | props: { [propName: string]: PropDescriptor } | undefined
76 | methods: MethodDescriptor[]
77 | slots: { [name: string]: SlotDescriptor }
78 | events?: { [name: string]: EventDescriptor }
79 | tags: { [key: string]: BlockTag[] }
80 | [key: string]: any
81 | }
82 |
83 | export class Documentation {
84 | private propsMap: Map
85 | private methodsMap: Map
86 | private slotsMap: Map
87 | private eventsMap: Map
88 | private dataMap: Map
89 | constructor() {
90 | this.propsMap = new Map()
91 | this.methodsMap = new Map()
92 | this.slotsMap = new Map()
93 | this.eventsMap = new Map()
94 |
95 | this.dataMap = new Map()
96 | }
97 |
98 | public set(key: string, value: any) {
99 | this.dataMap.set(key, value)
100 | }
101 |
102 | public get(key: string): any {
103 | return this.dataMap.get(key)
104 | }
105 |
106 | public getPropDescriptor(propName: string): PropDescriptor {
107 | return this.getDescriptor(propName, this.propsMap, () => ({
108 | description: '',
109 | tags: {},
110 | }))
111 | }
112 |
113 | public getMethodDescriptor(methodName: string): MethodDescriptor {
114 | return this.getDescriptor(methodName, this.methodsMap, () => ({
115 | name: methodName,
116 | modifiers: [],
117 | description: '',
118 | tags: {},
119 | }))
120 | }
121 |
122 | public getEventDescriptor(eventName: string): EventDescriptor {
123 | return this.getDescriptor(eventName, this.eventsMap, () => ({
124 | properties: undefined,
125 | description: '',
126 | tags: [],
127 | }))
128 | }
129 |
130 | public getSlotDescriptor(slotName: string): SlotDescriptor {
131 | return this.getDescriptor(slotName, this.slotsMap, () => ({
132 | description: '',
133 | }))
134 | }
135 |
136 | public toObject(): ComponentDoc {
137 | const props = this.getPropsObject()
138 | const methods = this.getMethodsObject()
139 | const events = this.getEventsObject() || {}
140 | const slots = this.getSlotsObject()
141 |
142 | const obj: { [key: string]: any } = {}
143 | this.dataMap.forEach((value, key) => {
144 | if (key) {
145 | obj[key] = value
146 | }
147 | })
148 |
149 | return {
150 | ...obj,
151 | // initialize non null params
152 | description: obj.description || '',
153 | tags: obj.tags || {},
154 |
155 | // set all the static properties
156 | displayName: obj.displayName,
157 | props,
158 | events,
159 | methods,
160 | slots,
161 | }
162 | }
163 |
164 | private getDescriptor(name: string, map: Map, init: () => T): T {
165 | let descriptor = map.get(name)
166 | if (!descriptor) {
167 | map.set(name, (descriptor = init()))
168 | }
169 | return descriptor
170 | }
171 |
172 | private getObjectFromDescriptor(map: Map): { [name: string]: T } | undefined {
173 | if (map.size > 0) {
174 | const obj: { [name: string]: T } = {}
175 | map.forEach((descriptor, name) => {
176 | if (name && descriptor) {
177 | obj[name] = descriptor
178 | }
179 | })
180 | return obj
181 | } else {
182 | return undefined
183 | }
184 | }
185 |
186 | private getPropsObject(): { [propName: string]: PropDescriptor } | undefined {
187 | return this.getObjectFromDescriptor(this.propsMap)
188 | }
189 |
190 | private getMethodsObject(): MethodDescriptor[] {
191 | const methods: MethodDescriptor[] = []
192 | this.methodsMap.forEach((descriptor, name) => {
193 | if (name && methods && descriptor) {
194 | methods.push(descriptor)
195 | }
196 | })
197 | return methods
198 | }
199 |
200 | private getEventsObject(): { [eventName: string]: EventDescriptor } | undefined {
201 | return this.getObjectFromDescriptor(this.eventsMap)
202 | }
203 |
204 | private getSlotsObject(): { [slotName: string]: SlotDescriptor } {
205 | return this.getObjectFromDescriptor(this.slotsMap) || {}
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/tests/components/grid-jsx/__snapshots__/grid-jsx.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`tests grid jsx should match the snapshot 1`] = `
4 | Object {
5 | "description": "This is an example of creating a reusable grid component and using it with external data.",
6 | "displayName": "grid",
7 | "events": Object {
8 | "error": Object {
9 | "description": "Error event.",
10 | "properties": undefined,
11 | "type": Object {
12 | "names": Array [
13 | "object",
14 | ],
15 | },
16 | },
17 | "success": Object {
18 | "description": "Success event.",
19 | "properties": undefined,
20 | "type": Object {
21 | "names": Array [
22 | "object",
23 | ],
24 | },
25 | },
26 | },
27 | "methods": Array [
28 | Object {
29 | "description": "Sets the order",
30 | "modifiers": Array [],
31 | "name": "sortBy",
32 | "params": Array [
33 | Object {
34 | "description": "Key to order",
35 | "name": "key",
36 | "type": Object {
37 | "name": "string",
38 | },
39 | },
40 | ],
41 | "returns": Object {
42 | "description": "Test",
43 | "title": "returns",
44 | "type": Object {
45 | "name": "string",
46 | },
47 | },
48 | "tags": Object {
49 | "access": Array [
50 | Object {
51 | "description": "public",
52 | "title": "access",
53 | },
54 | ],
55 | "params": Array [
56 | Object {
57 | "description": "Key to order",
58 | "name": "key",
59 | "title": "param",
60 | "type": Object {
61 | "name": "string",
62 | },
63 | },
64 | ],
65 | "returns": Array [
66 | Object {
67 | "description": "Test",
68 | "title": "returns",
69 | "type": Object {
70 | "name": "string",
71 | },
72 | },
73 | ],
74 | "since": Array [
75 | Object {
76 | "description": "Version 1.0.1",
77 | "title": "since",
78 | },
79 | ],
80 | "version": Array [
81 | Object {
82 | "description": "1.0.5",
83 | "title": "version",
84 | },
85 | ],
86 | },
87 | },
88 | ],
89 | "props": Object {
90 | "columns": Object {
91 | "description": "get columns list",
92 | "tags": Object {},
93 | "type": Object {
94 | "name": "array",
95 | },
96 | },
97 | "data": Object {
98 | "description": "describe data",
99 | "tags": Object {
100 | "version": Array [
101 | Object {
102 | "description": "1.0.5",
103 | "title": "version",
104 | },
105 | ],
106 | },
107 | "type": Object {
108 | "name": "array",
109 | },
110 | },
111 | "filterKey": Object {
112 | "defaultValue": Object {
113 | "func": false,
114 | "value": "'example'",
115 | },
116 | "description": "filter key",
117 | "required": "",
118 | "tags": Object {
119 | "ignore": Array [
120 | Object {
121 | "description": true,
122 | "title": "ignore",
123 | },
124 | ],
125 | },
126 | "type": Object {
127 | "name": "string",
128 | },
129 | },
130 | "images": Object {
131 | "defaultValue": Object {
132 | "func": true,
133 | "value": "function() {
134 | return [{}]
135 | }",
136 | },
137 | "description": "",
138 | "required": "",
139 | "tags": Object {},
140 | "type": Object {
141 | "name": "array",
142 | },
143 | },
144 | "msg": Object {
145 | "defaultValue": Object {
146 | "func": false,
147 | "value": "text",
148 | },
149 | "description": "object/array defaults should be returned from a factory function",
150 | "required": "",
151 | "tags": Object {
152 | "link": Array [
153 | Object {
154 | "description": "See [Wikipedia](https://en.wikipedia.org/wiki/Web_colors#HTML_color_names) for a list of color names",
155 | "title": "link",
156 | },
157 | ],
158 | "see": Array [
159 | Object {
160 | "description": "See [Wikipedia](https://en.wikipedia.org/wiki/Web_colors#HTML_color_names) for a list of color names",
161 | "title": "see",
162 | },
163 | ],
164 | "since": Array [
165 | Object {
166 | "description": "Version 1.0.1",
167 | "title": "since",
168 | },
169 | ],
170 | "version": Array [
171 | Object {
172 | "description": "1.0.5",
173 | "title": "version",
174 | },
175 | ],
176 | },
177 | "type": Object {
178 | "name": "string|number",
179 | },
180 | },
181 | "propFunc": Object {
182 | "defaultValue": Object {
183 | "func": true,
184 | "value": "function() {}",
185 | },
186 | "description": "prop function",
187 | "required": "",
188 | "tags": Object {},
189 | "type": Object {
190 | "name": "func",
191 | },
192 | },
193 | },
194 | "slots": Object {
195 | "header": Object {
196 | "description": "Use this slot header",
197 | },
198 | },
199 | "tags": Object {
200 | "author": Array [
201 | Object {
202 | "description": "[Rafael](https://github.com/rafaesc92)",
203 | "title": "author",
204 | },
205 | ],
206 | "since": Array [
207 | Object {
208 | "description": "Version 1.0.1",
209 | "title": "since",
210 | },
211 | ],
212 | "version": Array [
213 | Object {
214 | "description": "1.0.5",
215 | "title": "version",
216 | },
217 | ],
218 | },
219 | }
220 | `;
221 |
--------------------------------------------------------------------------------