UNPKG

@vuese/parser

Version:

Vue file parser for automatic document generation

1,250 lines (1,235 loc) 50.8 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var build = require('vue-template-compiler/build'); var parser = require('@babel/parser'); var path = require('path'); var fs = require('fs'); var traverse = require('@babel/traverse'); var traverse__default = _interopDefault(traverse); var bt = require('@babel/types'); var generate = _interopDefault(require('@babel/generator')); // Use vue-template-compiler/build to avoid detection of vue versions function sfcToAST(source, babelParserPlugins, basedir, jsFile) { const plugins = getBabelParserPlugins(babelParserPlugins); const sfc = build.parseComponent(source); const res = { jsSource: '', templateSource: '' }; if (sfc.script || jsFile) { if (sfc.script && (!sfc.script.content && sfc.script.src)) { // Src Imports if (basedir) { try { sfc.script.content = fs.readFileSync(path.resolve(basedir, sfc.script.src), 'utf-8'); } catch (e) { console.error(e); sfc.script.content = ''; } } } res.jsSource = jsFile ? '' : sfc.script.content || ''; res.sourceType = jsFile ? 'js' : sfc.script.lang; res.jsAst = parser.parse(jsFile ? source : sfc.script.content, { sourceType: 'module', plugins }); } if (sfc.template) { if (!sfc.template.content && sfc.template.src) { // Src Imports if (basedir) { try { sfc.template.content = fs.readFileSync(path.resolve(basedir, sfc.template.src), 'utf-8'); } catch (e) { console.error(e); sfc.template.content = ''; } } } res.templateSource = sfc.template.content || ''; res.templateAst = build.compile(sfc.template.content, { comments: true }).ast; } return res; } function getBabelParserPlugins(plugins) { const defaultBabelParserPlugins = { objectRestSpread: true, dynamicImport: true, 'decorators-legacy': true, classProperties: true, typescript: true, jsx: true }; const finallyBabelParserPlugins = Object.assign(defaultBabelParserPlugins, plugins || {}); return Object.keys(finallyBabelParserPlugins).filter((k) => finallyBabelParserPlugins[k]); } const commentRE = /\s*\*\s{1}/g; const leadRE = /^@(\w+)\b/; /** * @param cnode {bt.Node} a node with comments * @param trailing {boolean} Whether to process the tailing comment */ function getComments(cnode, trailing) { const res = { default: [] }; const commentNodes = trailing ? cnode.trailingComments || [] : cnode.leadingComments || []; if (!commentNodes || !commentNodes.length) return res; let comments = '', matchs, codeBlockStarted; commentNodes.forEach((node) => { if (isCommentLine(node)) { if (isCodeBlockDeclaration(node.value) && codeBlockStarted) codeBlockStarted = false; comments = codeBlockStarted ? node.value.replace(/^\s/, '') : node.value.trim(); if (isCodeBlockDeclaration(node.value) && typeof codeBlockStarted === 'undefined') codeBlockStarted = true; matchs = comments.match(leadRE); if (matchs) { const key = matchs[1]; res[key] = res[key] || []; res[key].push(comments.replace(leadRE, '').trim()); } else { res.default.push(comments); } } else if (isCommentBlock(node)) { comments = node.value .replace(commentRE, '\n') .replace(/^\*/, '') .split('\n'); comments = filterBlockComments(comments); let currentKey = 'default'; comments.forEach(c => { if ((matchs = c.match(leadRE))) { currentKey = matchs[1]; res[currentKey] = res[currentKey] || []; res[currentKey].push(c.replace(leadRE, '').trim()); } else { res.default.push(c); } }); } }); Object.keys(res).forEach(k => { res[k] = res[k].filter(comment => !comment.includes('eslint-disable')); }); return res; } /** * Extract the leading comments of the default export statement * 1、If the default export is a class with a decorator, * we should find the trailing comments of the last decorator node. * 2、In other cases, directly use the leading commets of the default export statement. */ function getComponentDescribe(node) { let res = { default: [] }; if (bt.isClassDeclaration(node.declaration)) { const decorators = node.declaration.decorators; if (decorators && decorators.length) { res = getComments(decorators[decorators.length - 1], true /* trailing */); } } else { res = getComments(node); } return res; } function isCommentLine(node) { return node.type === 'CommentLine'; } function isCommentBlock(node) { return node.type === 'CommentBlock'; } function isCodeBlockDeclaration(value) { return value.includes('```'); } function filterBlockComments(comments) { let codeBlockStarted; return comments .map(t => { if (isCodeBlockDeclaration(t) && codeBlockStarted) codeBlockStarted = false; const res = codeBlockStarted ? t : t.trim(); if (isCodeBlockDeclaration(t) && typeof codeBlockStarted === 'undefined') codeBlockStarted = true; return res; }) .filter(t => t); } /** * If a node satisfies the following conditions, then we will use this node as a Vue component. * 1. It is a default export * 2. others... */ function isVueComponent(path$$1, componentLevel) { const node = path$$1.node; return (bt.isExportDefaultDeclaration(node) || bt.isVariableDeclarator(node) || // this branch for determine if the component is Vue.extend CallExpression (bt.isCallExpression(node) && bt.isMemberExpression(node.callee) && bt.isIdentifier(node.callee.object) && bt.isIdentifier(node.callee.property) && node.callee.object.name === 'Vue' && node.callee.property.name === 'extend') || (bt.isReturnStatement(node) && componentLevel === 1)); } function isValidObjectProperty(node) { return bt.isObjectProperty(node) || bt.isObjectMethod(node); } function isVueOption(path$$1, optionsName, componentLevel) { if (isValidObjectProperty(path$$1.node) && path$$1.parentPath && path$$1.parentPath.parentPath && isVueComponent(path$$1.parentPath.parentPath, componentLevel)) { // General component options return path$$1.node.key.name === optionsName; } else if (isValidObjectProperty(path$$1.node) && path$$1.parentPath && path$$1.parentPath.parentPath && bt.isCallExpression(path$$1.parentPath.parentPath.node) && path$$1.parentPath.parentPath.node.callee.name === 'Component' && path$$1.parentPath.parentPath.parentPath && bt.isDecorator(path$$1.parentPath.parentPath.parentPath.node)) { // options in ts @Component({...}) return path$$1.node.key.name === optionsName; } return false; } function runFunction(fnCode) { const { code: genCode } = generate(fnCode); const code = `return (${genCode})()`; try { const fn = new Function(code); if (typeof fn() === 'object') { return JSON.stringify(fn()); } return fn(); } catch (e) { return; } } function getValueFromGenerate(node) { let code = 'return'; const { code: genCode } = generate(node); code += genCode; const fn = new Function(code); try { return fn(); } catch (e) { console.error(e); } } function computesFromStore(node) { if (node === undefined) { return false; } let fromStore = false; if (bt.isObjectMethod(node) || bt.isArrowFunctionExpression(node)) { fromStore = computesFromStore(node.body); } else if (bt.isObjectProperty(node)) { fromStore = computesFromStore(node.value); } else if (bt.isBlockStatement(node)) { fromStore = computesFromStore(node.body[node.body.length - 1]); } else if (bt.isCallExpression(traverse.NodePath)) { fromStore = computesFromStore(node.callee); } else if (bt.isMemberExpression(node)) { if (bt.isThisExpression(node.object)) { fromStore = node.property.name.toLowerCase().includes('store'); } else { fromStore = computesFromStore(node.object); } } else if (bt.isReturnStatement(node) || node.type.includes('Expression')) { fromStore = computesFromStore(node.argument); } return fromStore; } function getLiteralValue(node) { let data = ''; if (bt.isStringLiteral(node) || bt.isBooleanLiteral(node) || bt.isNumericLiteral(node)) { data = node.value.toString(); } return data; } function processPropValue(propValueNode, result, source) { if (isAllowPropsType(propValueNode)) { result.type = getTypeByTypeNode(propValueNode); } else if (bt.isObjectExpression(propValueNode)) { if (!propValueNode.properties.length) return; const allPropNodes = propValueNode.properties; const typeNode = allPropNodes.filter((node) => { if (node.key.name === 'type') { return true; } return false; }); const otherNodes = allPropNodes.filter((node) => { if (node.key.name !== 'type') { return true; } return false; }); // Prioritize `type` before processing `default`. // Because the difference in `type` will affect the way `default` is handled. if (typeNode.length > 0) { result.type = getTypeByTypeNode(typeNode[0].value); // Get descriptions of the type const typeDesc = getComments(typeNode[0]).default; if (typeDesc.length > 0) { result.typeDesc = typeDesc; } } // Processing props's default value otherNodes.forEach(node => { if (bt.isSpreadElement(node)) { return; } const n = node.key.name; if (n === 'default') { if (!hasFunctionTypeDef(result.type)) { if (bt.isObjectMethod(node)) { // Using functionExpression instead of ObjectMethod const params = node.params || []; let body = node.body; if (!bt.isBlockStatement(body)) { body = bt.blockStatement(body); } const r = bt.functionExpression(null, params, body, false, false); result.default = runFunction(r); } else if (bt.isFunction(node.value)) { result.default = runFunction(node.value); } else { let start = node.value.start || 0; let end = node.value.end || 0; // if node.value is string literal , e.g: "string literal" need to exclude quote if (bt.isStringLiteral(node.value)) { start++; end--; } // type sucks, fix it use any... result.default = source.slice(start, end) || undefined; } } else { if (bt.isObjectMethod(node)) { result.default = generate(node).code; } else if (bt.isFunction(node.value)) { result.default = generate(node.value).code; } } // Get descriptions of the default value const defaultDesc = getComments(node).default; if (defaultDesc.length > 0) { result.defaultDesc = defaultDesc; } } else if (n === 'required') { if (bt.isObjectProperty(node) && bt.isBooleanLiteral(node.value)) { result.required = node.value.value; } } else if (n === 'validator') { if (bt.isObjectMethod(node)) { result.validator = generate(node).code; } else { result.validator = generate(node.value).code; } // Get descriptions of the validator const validatorDesc = getComments(node).default; if (validatorDesc.length > 0) { result.validatorDesc = validatorDesc; } } }); } } function normalizeProps(props) { return props.map(prop => ({ type: null, name: prop })); } function getPropDecorator(classPropertyNode) { const decorators = classPropertyNode.decorators; if (!decorators) return; return decorators.find(deco => // @Prop() (bt.isCallExpression(deco.expression) && bt.isIdentifier(deco.expression.callee) && ['Prop', 'PropSync'].includes(deco.expression.callee.name)) || // @Prop (bt.isIdentifier(deco.expression) && ['Prop', 'PropSync'].includes(deco.expression.name))); } function getArgumentFromPropDecorator(decorator) { const expression = decorator.expression; if (bt.isCallExpression(expression) && bt.isIdentifier(expression.callee)) { const calleeName = expression.callee.name; if (calleeName === 'Prop') { return { propName: '', options: expression.arguments[0] }; } else if (calleeName === 'PropSync') { return { propName: expression.arguments[0].value, options: expression.arguments[1] }; } } return { propName: '', options: null }; } function getTypeByTypeNode(typeNode) { if (bt.isIdentifier(typeNode)) return typeNode.name; if (bt.isArrayExpression(typeNode)) { if (!typeNode.elements.length) return null; return typeNode.elements .filter(node => node && bt.isIdentifier(node)) .map(node => node.name); } return null; } // The `type` of a prop should be an array of constructors or constructors // eg. String or [String, Number] function isAllowPropsType(typeNode) { return bt.isIdentifier(typeNode) || bt.isArrayExpression(typeNode); } function hasFunctionTypeDef(type) { if (typeof type === 'string') { return type.toLowerCase() === 'function'; } else if (Array.isArray(type)) { return type.map(a => a.toLowerCase()).some(b => b === 'function'); } return false; } function processDataValue(dataNode, result) { result.type = getTypeByDataNode(dataNode); result.default = getValueByDataNode(dataNode.value); } function getTypeByDataNode(node) { if (bt.isObjectMethod(node) || bt.isArrowFunctionExpression(node.value)) return 'Function'; const dataNode = node.value; if (bt.isIdentifier(dataNode)) return dataNode.name; if (bt.isAssignmentExpression(dataNode) || bt.isAssignmentPattern(dataNode)) { if (bt.isIdentifier(dataNode.left)) { return dataNode.left.name; } } if (bt.isLiteral(dataNode) || (bt.isExpression(dataNode) && !bt.isBinaryExpression(dataNode))) { return literalToType(dataNode.type); } return ''; } function getValueByDataNode(dataNode) { if (bt.isArrayExpression(dataNode)) { if (!dataNode.elements.length) return ''; return ('[' + dataNode.elements .filter(node => node && bt.isLiteral(node)) .map(node => getLiteralValue(node)) .toString() + ']'); } if (bt.isLiteral(dataNode)) { return getLiteralValue(dataNode); } if (bt.isAssignmentExpression(dataNode) || bt.isAssignmentPattern(dataNode)) { if (bt.isLiteral(dataNode.right)) { return getLiteralValue(dataNode.right); } } return ''; } function literalToType(literal) { const type = literal .replace('Literal', '') .replace('Expression', '') .replace('Numeric', 'Number'); return type; } /** * * @param eventName {string} The event name * @param cnode {bt.Node} Node with comments * @param result {EventResult} */ function processEventName(eventName, cnodePath, result) { const cnode = cnodePath.node; const syncRE = /^update:(.+)/; const eventNameMatchs = eventName.match(syncRE); // Mark as .sync if (eventNameMatchs) { result.isSync = true; result.syncProp = eventNameMatchs[1]; } let allComments = getComments(cnode); const prevPathKey = Number(cnodePath.key) - 1; if (!allComments.default.length && prevPathKey >= 0) { // Use the trailing comments of the prev node allComments = getComments(cnodePath.getSibling(prevPathKey).node, true); result.describe = allComments.default; result.argumentsDesc = allComments.arg; } else { result.describe = allComments.default; result.argumentsDesc = allComments.arg; } } function getEmitDecorator(decorators) { if (!decorators || !decorators.length) return null; for (let i = 0; i < decorators.length; i++) { const exp = decorators[i].expression; if (bt.isCallExpression(exp) && bt.isIdentifier(exp.callee) && exp.callee.name === 'Emit') { return decorators[i]; } } return null; } /** * Used to identify ctx.children in the render function and use it as the default slot * @param functionPath The node path of the render function * @param onSlot */ function determineChildren(functionPath, onSlot) { if (!bt.isFunction(functionPath.node)) return; // Get the last argument of the render function and use it as the render context const lastParamNode = functionPath.node.params[functionPath.node.params.length - 1]; if (!lastParamNode || !bt.isIdentifier(lastParamNode)) return; // Get the binding of the context within the scope of the render function let contextBinding = null; const bindingKeys = Object.keys(functionPath.scope.bindings); for (let i = 0; i < bindingKeys.length; i++) { if (bindingKeys[i] === lastParamNode.name) { contextBinding = functionPath.scope.bindings[lastParamNode.name]; } } if (!contextBinding) return; // Determine ctx.childer contextBinding.referencePaths.forEach(refPath => { if (bt.isIdentifier(refPath.node) && refPath.parentPath && bt.isMemberExpression(refPath.parentPath.node) && bt.isIdentifier(refPath.parentPath.node.property) && refPath.parentPath.node.property.name === 'children') { const slotRes = { name: 'default', describe: '', backerDesc: '', scoped: false, bindings: {}, target: 'script' }; const commentsRes = bt.isExpressionStatement(refPath.parentPath.parentPath) ? getComments(refPath.parentPath.parentPath.node) : getComments(refPath.parentPath.node); slotRes.describe = commentsRes.default.join(''); slotRes.backerDesc = commentsRes.content ? commentsRes.content.join('') : ''; if (onSlot) onSlot(slotRes); } }); } class Seen { constructor() { this.seenSet = new Set(); } seen(label) { const yes = this.seenSet.has(label); if (!yes) this.seenSet.add(label); return yes; } } // const vueComponentVisitor = function parseJavascript(ast, seenEvent, options, source = '') { // backward compatibility const seenSlot = new Seen(); let exportDefaultReferencePath = null; let componentLevel = 0; const vueComponentVisitor = { Decorator(path$$1) { if (componentLevel === 0 && bt.isCallExpression(path$$1.node.expression) && bt.isIdentifier(path$$1.node.expression.callee, { name: 'Component' }) && path$$1.node.expression.arguments.length && bt.isObjectExpression(path$$1.node.expression.arguments[0])) { path$$1.traverse(vueComponentVisitor); } }, ObjectProperty(path$$1) { const { onProp, onMethod, onComputed, onName, onSlot, onMixIn, onData, onWatch } = options; // Processing name if (isVueOption(path$$1, 'name', componentLevel)) { const componentName = path$$1.node.value.value; if (onName) onName(componentName); } // Processing props if (onProp && isVueOption(path$$1, 'props', componentLevel)) { const valuePath = path$$1.get('value'); if (bt.isArrayExpression(valuePath.node)) { // An array of strings const propsValue = getValueFromGenerate(valuePath.node); const propsRes = normalizeProps(propsValue); propsRes.forEach(prop => { if (onProp) onProp(prop); }); } else if (bt.isObjectExpression(valuePath.node)) { // An object valuePath.traverse({ ObjectProperty(propPath) { // Guarantee that this is the prop definition if (propPath.parentPath === valuePath) { const name = bt.isIdentifier(propPath.node.key) ? propPath.node.key.name : propPath.node.key.value; const propValueNode = propPath.node.value; const result = { name, type: null, describe: getComments(propPath.node).default }; processPropValue(propValueNode, result, source); onProp(result); } } }); } } // Processing mixins if (onMixIn && isVueOption(path$$1, 'mixins', componentLevel)) { const properties = path$$1.node.value.elements; properties.forEach(mixIn => { const result = { mixIn: mixIn.name }; onMixIn(result); }); } // Processing computed if (onComputed && isVueOption(path$$1, 'computed', componentLevel) && bt.isObjectExpression(path$$1.node.value)) { const properties = path$$1.node .value.properties.filter(n => bt.isObjectMethod(n) || bt.isObjectProperty(n)); properties.forEach(node => { const commentsRes = getComments(node); const isFromStore = computesFromStore(node); // Collect only computed that have @vuese annotations if (commentsRes.vuese) { const result = { name: node.key.name, type: commentsRes.type, describe: commentsRes.default, isFromStore: isFromStore }; onComputed(result); } }); } if (onData && isVueOption(path$$1, 'data', componentLevel) && (bt.isObjectExpression(path$$1.node.value) || bt.isArrowFunctionExpression(path$$1.node.value))) { let value = bt.isArrowFunctionExpression(path$$1.node.value) ? path$$1.node.value.body : path$$1.node.value; /** * data: () => { * return {} * } * if data property is something like above, should process its return statement * argument */ if (bt.isBlockStatement(value)) { const returnStatement = value.body.filter(n => bt.isReturnStatement(n))[0]; if (returnStatement && returnStatement.argument && bt.isObjectExpression(returnStatement.argument)) { value = returnStatement.argument; } } if (bt.isObjectExpression(value)) { const properties = value.properties.filter(n => bt.isObjectProperty(n)); properties.forEach(node => { if (bt.isSpreadElement(node)) { return; } const commentsRes = getComments(node); // Collect only data that have @vuese annotations if (commentsRes.vuese && bt.isObjectProperty(node)) { const result = { name: node.key.name, type: '', describe: commentsRes.default, default: '' }; processDataValue(node, result); onData(result); } }); } } // Processing methods if (onMethod && isVueOption(path$$1, 'methods', componentLevel)) { const properties = path$$1.node .value.properties.filter(n => bt.isObjectMethod(n) || bt.isObjectProperty(n)); properties.forEach(node => { const commentsRes = getComments(node); // Collect only methods that have @vuese annotations if (commentsRes.vuese) { const result = { name: node.key.name, describe: commentsRes.default, argumentsDesc: commentsRes.arg }; onMethod(result); } }); } // Processing watch if (onWatch && isVueOption(path$$1, 'watch', componentLevel) && bt.isObjectExpression(path$$1.node.value)) { const properties = path$$1.node .value.properties.filter(n => bt.isObjectMethod(n) || bt.isObjectProperty(n)); properties.forEach(node => { const commentsRes = getComments(node); // Collect only data that have @vuese annotations if (commentsRes.vuese) { const result = { name: node.key.name, describe: commentsRes.default, argumentsDesc: commentsRes.arg }; onWatch(result); } }); } // functional component - `ctx.children` in the render function if (onSlot && isVueOption(path$$1, 'render', componentLevel) && !seenSlot.seen('default')) { const functionPath = path$$1.get('value'); determineChildren(functionPath, onSlot); } }, ObjectMethod(path$$1) { const { onData } = options; // @Component: functional component - `ctx.children` in the render function if (options.onSlot && isVueOption(path$$1, 'render', componentLevel) && !seenSlot.seen('default')) { determineChildren(path$$1, options.onSlot); } // Data can be represented as a component or a method if (onData && isVueOption(path$$1, 'data', componentLevel)) { path$$1.node.body.body.forEach(body => { if (bt.isReturnStatement(body)) { const properties = body.argument.properties.filter(n => bt.isObjectMethod(n) || bt.isObjectProperty(n)); properties.forEach(node => { const commentsRes = getComments(node); // Collect only data that have @vuese annotations for backward compability if (commentsRes.vuese) { const result = { name: node.key.name, type: '', describe: commentsRes.default, default: '' }; processDataValue(node, result); onData(result); } }); } }); } }, CallExpression(path$$1) { const node = path$$1.node; // $emit() if (bt.isMemberExpression(node.callee) && bt.isIdentifier(node.callee.property) && node.callee.property.name === '$emit') { // for performance issue only check when it is like a `$emit` CallExpression const parentExpressionStatementNode = path$$1.findParent(path$$1 => bt.isExpressionStatement(path$$1)); if (bt.isExpressionStatement(parentExpressionStatementNode)) { processEmitCallExpression(path$$1, seenEvent, options, parentExpressionStatementNode); } } else if (options.onSlot && bt.isMemberExpression(node.callee) && bt.isMemberExpression(node.callee.object) && bt.isIdentifier(node.callee.object.property) && node.callee.object.property.name === '$scopedSlots') { // scopedSlots let slotsComments; if (bt.isExpressionStatement(path$$1.parentPath)) { slotsComments = getComments(path$$1.parentPath.node); } else { slotsComments = getComments(node); } const scopedSlots = { name: node.callee.property.name, describe: slotsComments.default.join(''), backerDesc: slotsComments.content ? slotsComments.content.join('') : '', bindings: {}, scoped: true, target: 'script' }; options.onSlot(scopedSlots); } }, // Class style component ClassProperty(path$$1) { const propDecorator = getPropDecorator(path$$1.node); if (propDecorator) { let typeAnnotationStart = 0; let typeAnnotationEnd = 0; /** * if ClassProperty like this *` b: number | string` * if classProperty has typeAnnotation just use it as its type, unless it has decorator */ if (path$$1.node.typeAnnotation && bt.isTSTypeAnnotation(path$$1.node.typeAnnotation)) { const { start, end } = path$$1.node.typeAnnotation.typeAnnotation; typeAnnotationStart = start || 0; typeAnnotationEnd = end || 0; } const { propName, options: propDecoratorArg } = getArgumentFromPropDecorator(propDecorator); const result = { name: propName ? propName : path$$1.node.key.name, //null for backward compatibility, type: source.slice(typeAnnotationStart, typeAnnotationEnd) || null, describe: getComments(path$$1.node).default }; if (propDecoratorArg) { processPropValue(propDecoratorArg, result, source); } if (options.onProp) options.onProp(result); } }, ClassMethod(path$$1) { const node = path$$1.node; const commentsRes = getComments(node); // Collect only methods that have @vuese annotations if (commentsRes.vuese) { const result = { name: node.key.name, describe: commentsRes.default, argumentsDesc: commentsRes.arg }; if (options.onMethod) options.onMethod(result); } // Ctx.children in the render function of the Class style component if (options.onSlot && bt.isIdentifier(node.key) && node.key.name === 'render' && !seenSlot.seen('default')) { determineChildren(path$$1, options.onSlot); } // @Emit const emitDecorator = getEmitDecorator(node.decorators); if (emitDecorator) { const result = { name: '', isSync: false, syncProp: '' }; const args = emitDecorator.expression.arguments; if (args && args.length && bt.isStringLiteral(args[0])) { result.name = args[0].value; } else { if (bt.isIdentifier(node.key)) { result.name = node.key.name.replace(/([A-Z])/g, '-$1').toLowerCase(); } } if (!result.name || seenEvent.seen(result.name)) return; processEventName(result.name, path$$1, result); // trigger onEvent if options has an onEvent callback function and // if excludeSyncEvent, should `result.isSync` be true, otherwise just call the callback if (options.onEvent && (!!options.includeSyncEvent || !result.isSync)) { options.onEvent(result); } } }, MemberExpression(path$$1) { const node = path$$1.node; const parentNode = path$$1.parentPath.node; const grandPath = path$$1.parentPath.parentPath; if (options.onSlot && bt.isIdentifier(node.property) && node.property.name === '$slots' && grandPath) { let slotName = ''; let slotsComments = { default: [] }; if (bt.isMemberExpression(parentNode) && bt.isIdentifier(parentNode.property)) { // (this || vm).$slots.xxx slotName = parentNode.property.name; slotsComments = bt.isExpressionStatement(grandPath.node) ? getComments(grandPath.node) : getComments(parentNode); } else if (bt.isCallExpression(parentNode) && bt.isMemberExpression(grandPath.node) && bt.isIdentifier(grandPath.node.property)) { // ctx.$slots().xxx slotName = grandPath.node.property.name; const superNode = grandPath.parentPath.node; slotsComments = bt.isExpressionStatement(superNode) ? getComments(superNode) : getComments(grandPath.node); } // Avoid collecting the same slot multiple times if (!slotName || seenSlot.seen(slotName)) return; const slotRes = { name: slotName, describe: slotsComments.default.join(''), backerDesc: slotsComments.content ? slotsComments.content.join('') : '', bindings: {}, scoped: false, target: 'script' }; options.onSlot(slotRes); } } }; traverse__default(ast, { Program(path$$1) { exportDefaultReferencePath = getExportDefaultReferencePath(path$$1); }, ExportDefaultDeclaration(rootPath) { // Get a description of the component // if it is let traversePath = rootPath; if (isObject(exportDefaultReferencePath) && (bt.isVariableDeclarator(exportDefaultReferencePath) || bt.isReturnStatement(exportDefaultReferencePath))) { traversePath = exportDefaultReferencePath; } if (bt.isExportDefaultDeclaration(traversePath) && options.onDesc) options.onDesc(getComponentDescribe(rootPath.node)); traversePath.traverse({ ObjectExpression: { enter(path$$1) { componentLevel++; if (componentLevel === 1) { if (bt.isVariableDeclarator(traversePath) && options.onDesc) { const comments = getComments(traversePath.parentPath.node); options.onDesc(comments); } else if (bt.isReturnStatement(traversePath) && options.onDesc) { const comments = getComments(traversePath.node); options.onDesc(comments); } path$$1.traverse(vueComponentVisitor); } }, exit() { componentLevel--; } }, ClassBody: { enter(path$$1) { componentLevel++; if (componentLevel === 1) { path$$1.traverse(vueComponentVisitor); } }, exit() { componentLevel--; } } }); } }); } function processEmitCallExpression(path$$1, seenEvent, options, parentExpressionStatementNodePath) { const node = path$$1.node; const { onEvent, includeSyncEvent } = options; const args = node.arguments; const result = { name: '', isSync: false, syncProp: '' }; const firstArg = args[0]; if (firstArg) { if (bt.isStringLiteral(firstArg)) { result.name = firstArg.value; } else { if (bt.isIdentifier(firstArg)) { result.name = '`' + firstArg.name + '`'; } } } if (!result.name || seenEvent.seen(result.name)) return; processEventName(result.name, parentExpressionStatementNodePath, result); if (onEvent && (!!includeSyncEvent || !result.isSync)) { onEvent(result); } } /** * return export default referencePath for uncommon component export * * @param {NodePath<bt.Program>} programPath * @returns {(NodePath<bt.Node> | null)} */ function getExportDefaultReferencePath(programPath) { const bindings = programPath.scope.bindings; let exportDefaultReferencePath = null; Object.keys(bindings).forEach(key => { bindings[key].referencePaths.forEach(path$$1 => { if (bt.isExportDefaultDeclaration(path$$1.parent) || (bt.isCallExpression(path$$1.parentPath) && bt.isExportDefaultDeclaration(path$$1.parentPath.parentPath))) { exportDefaultReferencePath = bindings[key].path; // return ReturnStatement instead of FunctionDeclaration just keep consistency for a component, especially when extract // its comments if (bt.isFunctionDeclaration(exportDefaultReferencePath)) { exportDefaultReferencePath.traverse({ ReturnStatement(path$$1) { exportDefaultReferencePath = path$$1; path$$1.skip(); } }); } } }); }); return exportDefaultReferencePath; } function isObject(obj) { return obj !== null && typeof obj === 'object'; } function parseTemplate$$1(templateAst, seenEvent, options) { const parent = templateAst.parent; // parse event in template if (templateAst.attrsMap) { for (const [attr, value] of Object.entries(templateAst.attrsMap)) { if ((attr.startsWith('v-on:') || attr.startsWith('@')) && /\$emit\(.*?\)/.test(value)) { try { const astFile = parser.parse(value); if (astFile && astFile.type === 'File') { parseExpression(astFile, seenEvent, options); } } catch (err) { console.error(err); } } } } if (templateAst.type === 1) { if (templateAst.tag === 'slot') { const slot = { name: 'default', describe: '', backerDesc: '', bindings: {}, scoped: false, target: 'template' }; slot.bindings = extractAndFilterAttr(templateAst.attrsMap); if (slot.bindings.name) { slot.name = slot.bindings.name; delete slot.bindings.name; } // scoped slot if (Object.keys(slot.bindings).length) slot.scoped = true; if (parent) { const list = parent.children; let currentSlotIndex = 0; for (let i = 0; i < list.length; i++) { const el = list[i]; if (el === templateAst) { currentSlotIndex = i; break; } } // Find the first leading comment node as a description of the slot const copies = list.slice(0, currentSlotIndex).reverse(); for (let i = 0; i < copies.length; i++) { const el = copies[i]; if (el.type !== 3 || (!el.isComment && el.text.trim())) break; if (el.isComment && !(parent.tag === 'slot' && parent.children[0] === el)) { slot.describe = el.text.trim(); break; } } // Find the first child comment node as a description of the default slot content if (templateAst.children.length) { for (let i = 0; i < templateAst.children.length; i++) { const el = templateAst.children[i]; if (el.type !== 3 || (!el.isComment && el.text.trim())) break; if (el.isComment) { slot.backerDesc = el.text.trim(); break; } } } } if (options.onSlot) options.onSlot(slot); } if (templateAst.scopedSlots) { Object.values(templateAst.scopedSlots).forEach(scopedSlot => { parseTemplate$$1(scopedSlot, seenEvent, options); }); } const parseChildren = (templateAst) => { for (let i = 0; i < templateAst.children.length; i++) { parseTemplate$$1(templateAst.children[i], seenEvent, options); } }; if (templateAst.if && templateAst.ifConditions) { // for if statement iterate through the branches templateAst.ifConditions.forEach((c) => { parseChildren(c.block); }); } else { parseChildren(templateAst); } } } const dirRE = /^(v-|:|@)/; const allowRE = /^(v-bind|:)/; function extractAndFilterAttr(attrsMap) { const res = {}; const keys = Object.keys(attrsMap); for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (!dirRE.test(key) || allowRE.test(key)) { res[key.replace(allowRE, '')] = attrsMap[key]; } } return res; } function parseExpression(astFile, seenEvent, options) { traverse__default(astFile, { CallExpression(path$$1) { const node = path$$1.node; // $emit() if (bt.isIdentifier(node.callee) && node.callee.name === '$emit') { const parentExpressionStatementNodePath = path$$1.findParent(path$$1 => bt.isExpressionStatement(path$$1)); if (bt.isExpressionStatement(parentExpressionStatementNodePath)) { processEmitCallExpression(path$$1, seenEvent, options, parentExpressionStatementNodePath); } } } }); } function parser$1(source, options = {}) { const astRes = sfcToAST(source, options.babelParserPlugins, options.basedir, options.jsFile); const res = {}; const defaultOptions = { onName(name) { res.name = name; }, onDesc(desc) { res.componentDesc = desc; }, onProp(propsRes) { (res.props || (res.props = [])).push(propsRes); }, onEvent(eventsRes) { (res.events || (res.events = [])).push(eventsRes); }, onSlot(slotRes) { (res.slots || (res.slots = [])).push(slotRes); }, onMixIn(mixInRes) { (res.mixIns || (res.mixIns = [])).push(mixInRes); }, onMethod(methodRes) { (res.methods || (res.methods = [])).push(methodRes); }, onComputed(computedRes) { (res.computed || (res.computed = [])).push(computedRes); }, onData(dataRes) { (res.data || (res.data = [])).push(dataRes); }, onWatch(watchRes) { (res.watch || (res.watch = [])).push(watchRes); } }; const finallyOptions = { ...defaultOptions, ...options }; const seenEvent = new Seen(); if (astRes.jsAst) { parseJavascript(astRes.jsAst, seenEvent, finallyOptions, astRes.jsSource); } if (astRes.templateAst) { parseTemplate$$1(astRes.templateAst, seenEvent, finallyOptions);