1 1
import * as bt from '@babel/types'
2
import { NodePath } from 'ast-types/lib/node-path'
3 1
import { visit, print } from 'recast'
4
import Documentation, {
5
	BlockTag,
6
	DocBlockTags,
7
	PropDescriptor,
8
	ParamTag,
9
	UnnamedParam
10
} from '../Documentation'
11 1
import getDocblock from '../utils/getDocblock'
12 1
import getDoclets from '../utils/getDoclets'
13 1
import transformTagsIntoObject from '../utils/transformTagsIntoObject'
14 1
import getMemberFilter from '../utils/getPropsFilter'
15 1
import getTemplateExpressionAST from '../utils/getTemplateExpressionAST'
16 1
import parseValidatorForValues from './utils/parseValidator'
17

18
type ValueLitteral = bt.StringLiteral | bt.BooleanLiteral | bt.NumericLiteral
19

20
function getRawValueParsedFromFunctionsBlockStatementNode(
21 1
	blockStatementNode: bt.BlockStatement
22
): string | null {
23 1
	const { body } = blockStatementNode
24
	// if there is more than a return statement in the body,
25
	// we cannot resolve the new object, we let the function display as a function
26 1
	if (body.length !== 1 || !bt.isReturnStatement(body[0])) {
27 1
		return null
28
	}
29 1
	const [ret] = body
30 1
	return ret.argument ? print(ret.argument).code : null
31
}
32

33
/**
34
 * Extract props information form an object-style VueJs component
35
 * @param documentation
36
 * @param path
37
 */
38 1
export default function propHandler(documentation: Documentation, path: NodePath): Promise<void> {
39 1
	if (bt.isObjectExpression(path.node)) {
40 1
		const propsPath = path
41
			.get('properties')
42 1
			.filter((p: NodePath) => bt.isObjectProperty(p.node) && getMemberFilter('props')(p))
43

44
		// if no prop return
45 1
		if (!propsPath.length) {
46 1
			return Promise.resolve()
47
		}
48

49 1
		const modelPropertyName = getModelPropName(path)
50

51 1
		const propsValuePath = propsPath[0].get('value')
52

53 1
		if (bt.isObjectExpression(propsValuePath.node)) {
54 1
			const objProp = propsValuePath.get('properties')
55

56
			// filter non object properties
57 1
			const objPropFiltered = objProp.filter((p: NodePath) => bt.isProperty(p.node)) as NodePath<
58
				bt.Property
59
			>[]
60 1
			objPropFiltered.forEach(prop => {
61 1
				const propNode = prop.node
62

63
				// description
64 1
				const docBlock = getDocblock(prop)
65 1
				const jsDoc: DocBlockTags = docBlock ? getDoclets(docBlock) : { description: '', tags: [] }
66 1
				const jsDocTags: BlockTag[] = jsDoc.tags ? jsDoc.tags : []
67

68
				// if it's the v-model describe it only as such
69 1
				const propertyName = bt.isIdentifier(propNode.key)
70 1
					? propNode.key.name
71 1
					: bt.isStringLiteral(propNode.key)
72 1
					? propNode.key.value
73 0
					: null
74

75 1
				if (!propertyName) {
76 0
					return
77
				}
78
				const isPropertyModel =
79 1
					jsDocTags.some(t => t.title === 'model') || propertyName === modelPropertyName
80 1
				const propName = isPropertyModel ? 'v-model' : propertyName
81

82 1
				const propDescriptor = documentation.getPropDescriptor(propName)
83

84 1
				const propValuePath = prop.get('value')
85

86 1
				if (jsDoc.description) {
87 1
					propDescriptor.description = jsDoc.description
88
				}
89

90 1
				if (jsDocTags.length) {
91 1
					propDescriptor.tags = transformTagsIntoObject(jsDocTags)
92
				}
93

94 1
				extractValuesFromTags(propDescriptor)
95

96 1
				if (bt.isArrayExpression(propValuePath.node) || bt.isIdentifier(propValuePath.node)) {
97
					// if it's an immediately typed property, resolve its type immediately
98 1
					propDescriptor.type = getTypeFromTypePath(propValuePath)
99 1
				} else if (bt.isObjectExpression(propValuePath.node)) {
100
					// standard default + type + required
101 1
					const propPropertiesPath = propValuePath
102
						.get('properties')
103
						.filter(
104 1
							(p: NodePath) => bt.isObjectProperty(p.node) || bt.isObjectMethod(p.node)
105
						) as NodePath<bt.ObjectProperty | bt.ObjectMethod>[]
106

107
					// type
108 1
					const litteralType = describeType(propPropertiesPath, propDescriptor)
109

110
					// required
111 1
					describeRequired(propPropertiesPath, propDescriptor)
112

113
					// default
114 1
					describeDefault(propPropertiesPath, propDescriptor, litteralType || '')
115

116
					// validator => values
117 1
					describeValues(propPropertiesPath, propDescriptor)
118 1
				} else if (bt.isTSAsExpression(propValuePath.node)) {
119
					// standard default + type + required with TS as annotation
120 1
					const propPropertiesPath = propValuePath
121
						.get('expression', 'properties')
122 1
						.filter((p: NodePath) => bt.isObjectProperty(p.node)) as NodePath<bt.ObjectProperty>[]
123

124
					// type and values
125 1
					describeTypeAndValuesFromPath(propValuePath, propDescriptor)
126

127
					// required
128 1
					describeRequired(propPropertiesPath, propDescriptor)
129

130
					// default
131 1
					describeDefault(
132
						propPropertiesPath,
133
						propDescriptor,
134 1
						(propDescriptor.type && propDescriptor.type.name) || ''
135
					)
136
				} else {
137
					// in any other case, just display the code for the typing
138 1
					propDescriptor.type = {
139
						name: print(prop.get('value')).code,
140
						func: true
141
					}
142
				}
143
			})
144 1
		} else if (bt.isArrayExpression(propsValuePath.node)) {
145 1
			propsValuePath
146
				.get('elements')
147 1
				.filter((e: NodePath) => bt.isStringLiteral(e.node))
148 1
				.forEach((e: NodePath<bt.StringLiteral>) => {
149 1
					const propDescriptor = documentation.getPropDescriptor(e.node.value)
150 1
					propDescriptor.type = { name: 'undefined' }
151
				})
152
		}
153
	}
154 1
	return Promise.resolve()
155
}
156

157
/**
158
 * Deal with the description of the type
159
 * @param propPropertiesPath
160
 * @param propDescriptor
161
 * @returns the unaltered type member of the prop object
162
 */
163 1
export function describeType(
164
	propPropertiesPath: NodePath<bt.ObjectProperty | bt.ObjectMethod>[],
165 1
	propDescriptor: PropDescriptor
166
): string | undefined {
167 1
	const typeArray = propPropertiesPath.filter(getMemberFilter('type'))
168

169 1
	if (propDescriptor.tags && propDescriptor.tags.type) {
170 1
		const [{ type: typeDesc }] = propDescriptor.tags.type as UnnamedParam[]
171 1
		if (typeDesc) {
172 1
			const typedAST = getTemplateExpressionAST(`const a:${typeDesc.name}`)
173
			let typeValues: string[] | undefined
174 1
			visit(typedAST.program, {
175 1
				visitVariableDeclaration(path) {
176 1
					const { typeAnnotation } = path.get('declarations', 0, 'id', 'typeAnnotation').value
177 1
					if (
178 1
						bt.isTSUnionType(typeAnnotation) &&
179 1
						typeAnnotation.types.every(t => bt.isTSLiteralType(t))
180
					) {
181 1
						typeValues = typeAnnotation.types.map((t: bt.TSLiteralType) =>
182 1
							t.literal.value.toString()
183
						)
184
					}
185 1
					return false
186
				}
187
			})
188 1
			if (typeValues) {
189 1
				propDescriptor.values = typeValues
190
			} else {
191 1
				propDescriptor.type = typeDesc
192 1
				return getTypeFromTypePath(typeArray[0].get('value')).name
193
			}
194
		}
195
	}
196

197 1
	if (typeArray.length) {
198 1
		return describeTypeAndValuesFromPath(typeArray[0].get('value'), propDescriptor)
199
	} else {
200
		// deduce the type from default expression
201 1
		const defaultArray = propPropertiesPath.filter(getMemberFilter('default'))
202 1
		if (defaultArray.length) {
203 1
			const typeNode = defaultArray[0].node
204 1
			if (bt.isObjectProperty(typeNode)) {
205
				const func =
206 1
					bt.isArrowFunctionExpression(typeNode.value) || bt.isFunctionExpression(typeNode.value)
207 1
				const typeValueNode = defaultArray[0].get('value').node as ValueLitteral
208 1
				const typeName = typeof typeValueNode.value
209 1
				propDescriptor.type = { name: func ? 'func' : typeName }
210
			}
211
		}
212
	}
213 1
	return undefined
214
}
215

216 1
const VALID_VUE_TYPES = [
217
	'string',
218
	'number',
219
	'boolean',
220
	'array',
221
	'object',
222
	'date',
223
	'function',
224
	'symbol'
225
]
226

227 1
function resolveParenthesis(typeAnnotation: bt.TSType): bt.TSType {
228 1
	let finalAnno = typeAnnotation
229 1
	while (bt.isTSParenthesizedType(finalAnno)) {
230 1
		finalAnno = finalAnno.typeAnnotation
231
	}
232 1
	return finalAnno
233
}
234

235
function describeTypeAndValuesFromPath(
236
	propPropertiesPath: NodePath<bt.TSAsExpression>,
237 1
	propDescriptor: PropDescriptor
238
): string {
239
	// values
240 1
	const values = getValuesFromTypePath(propPropertiesPath.node.typeAnnotation)
241

242
	// if it has an "as" annotation defining values
243 1
	if (values) {
244 1
		propDescriptor.values = values
245 1
		propDescriptor.type = { name: 'string' }
246
	} else {
247
		// Get natural type from its identifier
248
		// (classic way)
249
		// type: Object
250 1
		propDescriptor.type = getTypeFromTypePath(propPropertiesPath)
251
	}
252 1
	return propDescriptor.type.name
253
}
254

255
function getTypeFromTypePath(
256 1
	typePath: NodePath<bt.TSAsExpression | bt.Identifier>
257
): { name: string; func?: boolean } {
258 1
	const typeNode = typePath.node
259 1
	const { typeAnnotation } = typeNode
260

261
	const typeName =
262 1
		bt.isTSTypeReference(typeAnnotation) && typeAnnotation.typeParameters
263 1
			? print(resolveParenthesis(typeAnnotation.typeParameters.params[0])).code
264 1
			: bt.isArrayExpression(typeNode)
265 1
			? typePath
266
					.get('elements')
267 1
					.map((t: NodePath) => getTypeFromTypePath(t).name)
268
					.join('|')
269 1
			: typeNode &&
270 1
			  bt.isIdentifier(typeNode) &&
271 1
			  VALID_VUE_TYPES.indexOf(typeNode.name.toLowerCase()) > -1
272 1
			? typeNode.name.toLowerCase()
273 1
			: print(typeNode).code
274 1
	return {
275 1
		name: typeName === 'function' ? 'func' : typeName
276
	}
277
}
278

279
/**
280
 * When a prop is type annotated with the "as" keyword,
281
 * It means that its possible values can be extracted from it
282
 * this extracts the values from the as
283
 * @param typeAnnotation the as annotation
284
 */
285 1
function getValuesFromTypePath(typeAnnotation: bt.TSType): string[] | undefined {
286 1
	if (bt.isTSTypeReference(typeAnnotation) && typeAnnotation.typeParameters) {
287 1
		const type = resolveParenthesis(typeAnnotation.typeParameters.params[0])
288 1
		return getValuesFromTypeAnnotation(type)
289
	}
290 1
	return undefined
291
}
292

293 1
export function getValuesFromTypeAnnotation(type: bt.TSType): string[] | undefined {
294 1
	if (bt.isTSUnionType(type) && type.types.every(t => bt.isTSLiteralType(t))) {
295 1
		return type.types.map(t => (bt.isTSLiteralType(t) ? t.literal.value.toString() : ''))
296
	}
297 1
	return undefined
298
}
299

300 1
export function describeRequired(
301
	propPropertiesPath: NodePath<bt.ObjectProperty | bt.ObjectMethod>[],
302 1
	propDescriptor: PropDescriptor
303
) {
304 1
	const requiredArray = propPropertiesPath.filter(getMemberFilter('required'))
305 1
	const requiredNode = requiredArray.length ? requiredArray[0].get('value').node : undefined
306
	const required =
307 1
		requiredNode && bt.isBooleanLiteral(requiredNode) ? requiredNode.value : undefined
308 1
	if (required !== undefined) {
309 1
		propDescriptor.required = required
310
	}
311
}
312

313 1
export function describeDefault(
314
	propPropertiesPath: NodePath<bt.ObjectProperty | bt.ObjectMethod>[],
315
	propDescriptor: PropDescriptor,
316 1
	propType: string
317
): void {
318 1
	const defaultArray = propPropertiesPath.filter(getMemberFilter('default'))
319 1
	if (defaultArray.length) {
320
		/**
321
		 * This means the default value is formatted like so: `default: any`
322
		 */
323 1
		const defaultValueIsProp = bt.isObjectProperty(defaultArray[0].value)
324
		/**
325
		 * This means the default value is formatted like so: `default () { return {} }`
326
		 */
327 1
		const defaultValueIsObjectMethod = bt.isObjectMethod(defaultArray[0].value)
328
		// objects and arrays should try to extract the body from functions
329 1
		if (propType === 'object' || propType === 'array') {
330 1
			if (defaultValueIsProp) {
331
				/* todo: add correct type info here ↓ */
332 1
				const defaultFunction = defaultArray[0].get('value')
333 1
				const isArrowFunction = bt.isArrowFunctionExpression(defaultFunction.node)
334 1
				const isOldSchoolFunction = bt.isFunctionExpression(defaultFunction.node)
335

336
				// if default is undefined or null, litterals are allowed
337 1
				if (
338 1
					bt.isNullLiteral(defaultFunction.node) ||
339 1
					(bt.isIdentifier(defaultFunction.node) && defaultFunction.node.name === 'undefined')
340
				) {
341 1
					propDescriptor.defaultValue = {
342
						func: false,
343
						value: print(defaultFunction.node).code
344
					}
345 1
					return
346
				}
347

348
				// check if the prop value is a function
349 1
				if (!isArrowFunction && !isOldSchoolFunction) {
350 0
					throw new Error(
351
						'A default value needs to be a function when your type is an object or array'
352
					)
353
				}
354
				// retrieve the function "body" from the arrow function
355 1
				if (isArrowFunction) {
356 1
					const arrowFunctionBody = defaultFunction.get('body')
357
					// arrow function looks like `() => { return {} }`
358 1
					if (bt.isBlockStatement(arrowFunctionBody.node)) {
359 1
						const rawValueParsed = getRawValueParsedFromFunctionsBlockStatementNode(
360
							arrowFunctionBody.node
361
						)
362 1
						if (rawValueParsed) {
363 1
							propDescriptor.defaultValue = {
364
								func: false,
365
								value: rawValueParsed
366
							}
367 1
							return
368
						}
369
					}
370

371 1
					if (
372 1
						bt.isArrayExpression(arrowFunctionBody.node) ||
373 1
						bt.isObjectExpression(arrowFunctionBody.node)
374
					) {
375 1
						propDescriptor.defaultValue = {
376
							func: false,
377
							value: print(arrowFunctionBody.node).code
378
						}
379 1
						return
380
					}
381

382
					// arrow function looks like `() => ({})`
383 1
					propDescriptor.defaultValue = {
384
						func: true,
385
						value: print(defaultFunction).code
386
					}
387 1
					return
388
				}
389
			}
390
			// defaultValue was either an ObjectMethod or an oldSchoolFunction
391
			// in either case we need to retrieve the blockStatement and work with that
392
			/* todo: add correct type info here ↓ */
393 1
			const defaultBlockStatement = defaultValueIsObjectMethod
394 1
				? defaultArray[0].get('body')
395 1
				: defaultArray[0].get('value').get('body')
396 1
			const defaultBlockStatementNode: bt.BlockStatement = defaultBlockStatement.node
397 1
			const rawValueParsed = getRawValueParsedFromFunctionsBlockStatementNode(
398
				defaultBlockStatementNode
399
			)
400 1
			if (rawValueParsed) {
401 1
				propDescriptor.defaultValue = {
402
					func: false,
403
					value: rawValueParsed
404
				}
405 1
				return
406
			}
407
		}
408

409
		// otherwise the rest should return whatever there is
410 1
		if (defaultValueIsProp) {
411
			// in this case, just return the rawValue
412 1
			const defaultPath = defaultArray[0].get('value')
413 1
			const rawValue = print(defaultPath).code
414 1
			propDescriptor.defaultValue = {
415
				func: bt.isFunction(defaultPath.node),
416
				value: rawValue
417
			}
418 1
			return
419
		}
420

421 1
		if (defaultValueIsObjectMethod) {
422
			// in this case, just the function needs to be reconstructed a bit
423 1
			const defaultObjectMethod = defaultArray[0].get('value')
424 1
			const paramNodeArray = defaultObjectMethod.node.params
425 1
			const params = paramNodeArray.map((p: any) => p.name).join(', ')
426

427 1
			const defaultBlockStatement = defaultArray[0].get('body')
428 1
			const rawValue = print(defaultBlockStatement).code
429
			// the function should be reconstructed as "old-school" function, because they have the same handling of "this", whereas arrow functions do not.
430 1
			const rawValueParsed = `function(${params}) ${rawValue.trim()}`
431 1
			propDescriptor.defaultValue = {
432
				func: true,
433
				value: rawValueParsed
434
			}
435 1
			return
436
		}
437 0
		throw new Error('Your default value was formatted incorrectly')
438
	}
439
}
440

441
function describeValues(
442
	propPropertiesPath: NodePath<bt.ObjectProperty | bt.ObjectMethod>[],
443 1
	propDescriptor: PropDescriptor
444
) {
445 1
	if (propDescriptor.values) {
446 1
		return
447
	}
448

449 1
	const validatorArray = propPropertiesPath.filter(getMemberFilter('validator'))
450 1
	if (validatorArray.length) {
451 1
		const validatorNode = validatorArray[0].get('value').node
452 1
		const values = parseValidatorForValues(validatorNode)
453 1
		if (values) {
454 1
			propDescriptor.values = values
455
		}
456
	}
457
}
458

459 1
export function extractValuesFromTags(propDescriptor: PropDescriptor) {
460 1
	if (propDescriptor.tags && propDescriptor.tags.values) {
461 1
		const values = propDescriptor.tags.values.map(tag => {
462 1
			const description = ((tag as any) as ParamTag).description
463 1
			const choices = typeof description === 'string' ? description.split(',') : undefined
464 1
			if (choices) {
465 1
				return choices.map((v: string) => v.trim())
466
			}
467 0
			return []
468
		})
469 1
		propDescriptor.values = ([] as string[]).concat(...values)
470

471 1
		delete propDescriptor.tags.values
472
	}
473
}
474

475
/**
476
 * extract the property model.prop from the component object
477
 * @param path component NodePath
478
 * @returns name of the model prop, null if none
479
 */
480 1
function getModelPropName(path: NodePath): string | null {
481 1
	const modelPath = path
482
		.get('properties')
483 1
		.filter((p: NodePath) => bt.isObjectProperty(p.node) && getMemberFilter('model')(p))
484

485 1
	if (!modelPath.length) {
486 1
		return null
487
	}
488

489
	const modelPropertyNamePath =
490 1
		modelPath.length &&
491 1
		modelPath[0]
492
			.get('value')
493
			.get('properties')
494 1
			.filter((p: NodePath) => bt.isObjectProperty(p.node) && getMemberFilter('prop')(p))
495

496 1
	if (!modelPropertyNamePath.length) {
497 1
		return null
498
	}
499

500 1
	const valuePath = modelPropertyNamePath[0].get('value')
501

502 1
	return bt.isStringLiteral(valuePath.node) ? valuePath.node.value : null
503
}

Read our documentation on viewing source code .

Loading