UNPKG

@vuedoc/parser

Version:

Generate a JSON documentation for a Vue file

701 lines (575 loc) 20.8 kB
import { Options, ScriptParser } from './ScriptParser.js'; import { RegisterFactory } from './ScriptRegisterParser.js'; import { Syntax, Properties, Feature, Type, Visibility, CompositionProperties, CompositionHooks, CompositionFeature } from '../lib/Enum.js'; import { CompositionValueOptions, generateName } from './AbstractParser.js'; import { CommentParser } from './CommentParser.js'; import { Composition } from '../lib/Composition.js'; import { Value } from '../entity/Value.js'; import { Parser } from '../../types/Parser.js'; import { Entry } from '../../types/Entry.js'; import { File } from '../../types/FileSystem.js'; import * as Babel from '@babel/types'; export type ParseDataValueOptions = { name: string; value: Value; nodeTyping: any; nodeComment: any; }; const generateVarName = generateName('var'); const fallbackComposition: Parser.CompiledComposition = Object.freeze({ fname: 'undefined', feature: 'data', }); const SPECIFIC_COMPOSITION_PROPERTIES = Object.values(CompositionProperties); const DEFINE_COMPONENT_CALL_EXPRESSIONS = [ 'defineComponent', 'defineCustomElement', 'defineAsyncComponent', ]; const EXPLICIT_EXPOSE_CALL_EXPRESSION = [ 'defineExpose', 'expose', ]; function hasDefineExposeStatement(bodyNode): boolean { return bodyNode.body.some((item: Babel.Node) => { if (item.type === Syntax.ExpressionStatement) { if (item.expression.type === Syntax.CallExpression && 'name' in item.expression.callee) { return EXPLICIT_EXPOSE_CALL_EXPRESSION.includes(item.expression.callee.name); } } return item.type === Syntax.ReturnStatement; }); } function hasCompositionProperties(node) { return node.properties.some(({ key: { name } }) => SPECIFIC_COMPOSITION_PROPERTIES.includes(name)); } function checkExpliciExpose(bodyNode, composition?: Composition) { return hasDefineExposeStatement(bodyNode) || CompositionParser.isCompositionScript(bodyNode, composition); } export class CompositionParser extends ScriptParser { shouldEmitOnDeclarations: boolean; publicProperties: null | string[]; effectScopes: string[]; constructor( root: Parser.Interface, source: Required<Parser.Script>, file: Readonly<File>, options: Options, createRegister: RegisterFactory ) { super(root, source, file, options, createRegister); this.shouldEmitOnDeclarations = !checkExpliciExpose(source.ast.program); this.publicProperties = null; this.effectScopes = []; this.defaultModelPropName = 'modelValue'; this.enableNestedEventsParsing = false; } static isComponentCallExpression(node) { return DEFINE_COMPONENT_CALL_EXPRESSIONS.includes(node.callee.name); } static isCompositionScript(bodyNode, composition?: Composition): boolean { for (const node of bodyNode.body) { switch (node.type) { case Syntax.ImportDeclaration: if (composition && node.source.value === 'vue') { const hasCompositionImport = node.specifiers.some((item) => item.imported && composition.exists(item.imported.name)); if (hasCompositionImport) { return true; } } break; case Syntax.ExportDefaultDeclaration: switch (node.declaration.type) { case Syntax.ObjectExpression: return hasCompositionProperties(node.declaration); case Syntax.CallExpression: if (CompositionParser.isComponentCallExpression(node.declaration) && node.declaration.arguments.length) { if (node.declaration.arguments[0].type === Syntax.ObjectExpression) { return hasCompositionProperties(node.declaration.arguments[0]); } } break; } break; } } return false; } emit(entry: Entry.Type) { this.parseExposedEntry(entry); super.emit(entry); } emitScopeEntry(feature: Parser.CompositionFeature, ref: Parser.ScopeEntry<any, any, any>) { switch (feature) { case Feature.methods: ref.node.value.id = { name: ref.key }; this.parsers.methods.sync().parseMethodProperty(ref.node.value, ref.node.value, ref.node.value, { parseEvents: false, hooks: CompositionHooks, }); break; default: super.emitScopeEntry(feature, ref); break; } } parse() { this.transverse(this.source.ast.program.body); this.parseComponentComment(); super.parse(); } parseComponentComment() { if (this.shouldEmitOnDeclarations && this.source.ast.comments?.length && typeof this.source.ast.comments[0].end === 'number' && typeof this.source.ast.program.body[0]?.start === 'number') { const topLevelCommentBlock = this.source.ast.comments[0].end < this.source.ast.program.body[0]?.start ? this.source.ast.comments[0] : null; if (topLevelCommentBlock?.value) { const { description, keywords } = CommentParser.parse(topLevelCommentBlock.value); this.parseComment(description, keywords); } } } parseAst(node: Babel.Node) { if ('body' in node) { if (node.body instanceof Array) { for (const item of node.body) { this.parseAstStatement(item); } return; } } this.parseAstStatement(node); } parseAstStatement(item: Babel.Node) { switch (item.type) { case Syntax.ReturnStatement: this.parseReturnStatement(item); break; default: super.parseAstStatement(item); break; } } parseReturnStatement(node: Babel.ReturnStatement) { switch (node.argument?.type) { case Syntax.ObjectExpression: { this.parseReturnObjectExpression(node.argument); break; } } } parseObjectProperty(node: Babel.ObjectProperty) { if (node.value.type === Syntax.Identifier) { this.parseIdentifier(node.value); } } parseReturnObjectExpression(node: Babel.ObjectExpression) { for (const property of this.parseElements(node.properties)) { if (property.type === Syntax.ObjectProperty) { this.parseObjectProperty(property); } if ('value' in property && this.features.length) { let callExpressionNode: Babel.CallExpression; let propertyKey: string; switch (property.value.type) { case Syntax.Identifier: { const ref = this.getScopeValue(property.value.name); if (ref?.composition) { this.parseCompositionFeatureEntry(ref.key, ref.composition, { id: property, }); continue; } else if (ref?.node.value.type === Syntax.CallExpression) { callExpressionNode = ref.node.value; propertyKey = ref.key; } break; } case Syntax.CallExpression: callExpressionNode = property.value; if ('name' in property.key) { propertyKey = property.key.name; } break; } if (callExpressionNode?.type === Syntax.CallExpression && propertyKey) { const result = this.parseCompositionCallExpression(propertyKey, { id: property as any, init: callExpressionNode, }); if (result) { continue; } } } this.parseData(property); } } parseCompositionCallExpression(key: string, options: Required<Pick<CompositionValueOptions, 'id' | 'init'>>) { const decoratorValue = this.getDeclaratorValue(options); if (decoratorValue.ref && decoratorValue.composition) { if (typeof decoratorValue.composition.parseEntryNode === 'function') { decoratorValue.composition.parseEntryNode(options.init as Babel.CallExpression, this); } else if (!decoratorValue.composition.ignoreVariableIdentifier) { this.setScopeValue(key, decoratorValue.argument || options.id, decoratorValue.ref, { nodeComment: options.id, }); this.parseCompositionFeatureEntry(key, decoratorValue.composition, options); } return true; } return false; } parseSetupScope(node: Babel.BlockStatement | Babel.ObjectExpression) { if (node.type === Syntax.BlockStatement) { const shouldEmitOnDeclarationsBackup = this.shouldEmitOnDeclarations; this.shouldEmitOnDeclarations = !checkExpliciExpose(node, this.composition); this.transverse(node.body); node.body.forEach((item) => this.parseAstStatement(item)); this.shouldEmitOnDeclarations = shouldEmitOnDeclarationsBackup; } else if (node.type === Syntax.ObjectExpression) { this.parseReturnObjectExpression(node); } } parseCallExpression(node) { if (node.callee.type === Syntax.MemberExpression) { if (this.effectScopes.includes(node.callee.object.name) && node.callee.property.name === 'run' && node.arguments[0]) { this.parseSetupScope(node.arguments[0].body); } } else { switch (node.callee.name) { case 'withDefaults': if (this.features.includes(Feature.props)) { this.parsers.props.sync().parseWithDefaultsCall(node); } break; case 'expose': case 'defineExpose': if (node.arguments.length) { this.parseExposeCallExpression(node.arguments[0]); } break; case 'createApp': if (node.arguments.length) { this.parseExportDefaultDeclaration(node.arguments[0]); } break; case 'defineProps': if (this.features.includes(Feature.props)) { this.parsers.props.sync().parse(node); } break; default: if (CompositionParser.isComponentCallExpression(node)) { this.parseExportDefaultDeclaration(node.arguments[0] || node); } else if (this.hasLeftSidePart(node)) { this.parseCompositionFeature(node.callee.name, node); } else { const key = generateVarName.next().value; this.parseCompositionCallExpression(key, { id: node, init: node, }); } break; } } } parseCompositionFeature(fname: string, node, nameToHandle?: string) { const { declarator = node.extra.$declarator || node } = this.getLeftSidePart(node, {} as any); if (declarator.init?.type === Syntax.CallExpression && !(fname in this.scope)) { this.parseImportedDeclarator(declarator); } const composition = this.composition.getDeclarations(fname) .find((composition) => this.features.includes(composition.feature)); if (composition) { const elements = []; switch (declarator.id?.type) { case Syntax.ArrayPattern: elements.push(...declarator.id.elements); break; case Syntax.ObjectPattern: elements.push(...declarator.id.properties); break; } if (elements.length) { if (nameToHandle) { const ref = this.getScopeValue(nameToHandle); const source = ref?.source || nameToHandle; const element = elements.find((element) => element.key.name === source); if (element) { this.parseCompositionFeatureNode( composition, declarator, nameToHandle ); return; } } for (const element of elements) { this.parseCompositionFeatureNode( composition, declarator, element.key.name ); } } else { if (!declarator.id) { declarator.id = { name: generateVarName.next().value, extra: { file: node.extra.file, }, }; } this.parseCompositionFeatureNode( composition, declarator, nameToHandle ); } return; } if (declarator.id) { if (nameToHandle) { const ref = this.getScopeValue(nameToHandle); if (ref) { this.parseImportedDeclarator(declarator); const scopeNodeValue = ref.node.value; if (scopeNodeValue.type === Syntax.CallExpression && scopeNodeValue !== node) { this.parseCompositionFeature(fname, scopeNodeValue, nameToHandle); return; } } } this.parseCompositionFeatureNode( fallbackComposition, declarator, nameToHandle ); } } parseCompositionFeatureEntry( key: string, composition: Readonly<Parser.CompiledComposition>, options: Pick<CompositionValueOptions, 'id'> ) { if (options.id.type === Syntax.Identifier && composition.ignoreVariableIdentifier) { return; } const ref = this.getScopeValue(key); const feature = ref && (ref.function || CompositionParser.isTsFunction(ref.node.value)) ? CompositionFeature.methods : ref.composition?.feature || composition.feature; this.emitScopeEntry(feature, ref); } parseCompositionFeatureNode( composition: Readonly<Parser.CompiledComposition>, declarator, nameToHandle?: string ) { if (declarator.id.type === Syntax.Identifier && composition.ignoreVariableIdentifier) { return; } const name = this.parseKey(declarator); const ref = this.getScopeValue(nameToHandle) || this.getScopeValue(name); if (ref) { const feature = ref.function || CompositionParser.isTsFunction(ref.node.value) ? CompositionFeature.methods : ref.composition?.feature || composition.feature; this.emitScopeEntry(feature, ref); } } parseExposeCallExpression(node) { node.properties.forEach((property) => this.parseData(property)); } parseData(property) { const name = this.parseKey(property); let nodeTyping = property?.value || property; let nodeComment = property; const ref = this.getScopeValue(name); if (ref) { nodeTyping = ref.node.type; nodeComment = ref.node.comment; if (ref.node.value.type === Syntax.CallExpression) { this.parseCompositionFeature(ref.node.value.callee.name, ref.node.value, name); return; } if (ref.composition) { this.parseCompositionFeature(ref.composition.fname, ref.node.value, name); return; } if (ref.node.value.extra?.$declarator?.init?.type === Syntax.CallExpression) { const fname = ref.node.value.extra.$declarator.init.callee.name; this.parseIdentifierName(fname); if (ref.composition) { this.parseCompositionFeature(ref.composition.fname, ref.node.value, name); } else { this.parseCompositionFeature(fname, ref.node.value, name); } return; } if (CompositionParser.isFunction(ref.node.value) || CompositionParser.isTsFunction(ref.node.value)) { this.parseDataValueMethod({ name, value: ref.value, nodeTyping: ref.node.type, nodeComment: ref.node.comment }); return; } this.parseDataValue({ name, value: ref.value, nodeTyping, nodeComment }); return; } if (property.value?.type === Syntax.CallExpression) { this.setLeftSidePart(property.value, property, { ...property, id: property.key || { name, extra: { file: property.extra.file, }, }, }); this.parseCompositionFeature(property.value.callee.name, property.value, name); return; } const propertyValue = property?.value || property; const value = this.getValue(propertyValue); this.parseDataValue({ name, value, nodeTyping, nodeComment }); } parseDataValue({ name, value, nodeTyping, nodeComment }: ParseDataValueOptions) { if (value.type === Type.function) { this.parseDataValueMethod({ name, value, nodeTyping, nodeComment }); } else { this.parsers.data.sync().parseDataValueRaw({ name, value, nodeTyping, nodeComment }); } } parseDataValueMethod({ name, nodeTyping, nodeComment }: ParseDataValueOptions) { if (this.features.includes(Feature.methods)) { const ref = this.getScopeValue(name); if (ref) { const { value: node = nodeTyping, comment: nComment = nodeComment } = ref.node; if (!node.key) { node.key = node.id || nodeComment.key || nodeTyping.id || { name }; } this.parsers.methods.sync().parseMethodProperty(node, node, nComment, { parseEvents: false, hooks: CompositionHooks, }); } else if (CompositionParser.isFunction(nodeTyping)) { if (!nodeTyping.key) { nodeTyping.key = nodeTyping.id || nodeComment.key || nodeTyping.id || { name }; } this.parsers.methods.sync().parseMethodProperty(nodeTyping, nodeTyping, nodeComment, { parseEvents: false, hooks: CompositionHooks, }); } } } parseExpressionStatement(node) { switch (node.expression.type) { case Syntax.CallExpression: this.parseCallExpression(node.expression); break; default: super.parseExpressionStatement(node); break; } } parseFunctionDeclaration(node) { if (this.shouldEmitOnDeclarations) { if (this.features.includes(Feature.methods)) { this.parsers.methods.sync().parseMethodProperty(node, node, node, { parseEvents: false, hooks: CompositionHooks, }); } } else { this.registerFunctionDeclaration(node); } } parseVariableDeclaration(node) { super.parseVariableDeclaration(node); node.declarations.forEach((declaration) => { const name = declaration.id.name; const ref = this.getScopeValue(name); if (ref?.value.$kind === 'effectScope') { this.effectScopes.push(name); } }); if (this.shouldEmitOnDeclarations) { node.declarations.forEach((declaration) => { if (declaration.init.type === Syntax.CallExpression) { this.parseCallExpression(declaration.init); } else { const name = declaration.id.name; const ref = this.getScopeValue(name); if (ref) { const nodeTyping = declaration.init?.typeParameters || declaration.id?.typeAnnotation || declaration; const nodeComment = node.declarations.length > 1 ? declaration : node; this.parseVariableDeclarationValue(declaration, name, ref.value, nodeTyping, nodeComment); } } }); } } parseVariableDeclarationValue(declaration, name, value, nodeTyping, nodeComment) { if (value.type === Type.function) { if (this.features.includes(Feature.methods)) { this.parsers.methods.sync().parseMethodProperty(declaration, declaration.init, nodeComment, { parseEvents: false, hooks: CompositionHooks, }); } } else { this.parsers.data.sync() .parseDataValue({ name, value, nodeTyping, nodeComment }); } } parseExposedEntry(entry) { if (this.publicProperties) { if (this.publicProperties.includes(entry.name)) { entry.visibility = Visibility.public; } else { entry.visibility = Visibility.private; } } } parseExposeProperty(node) { const property = node.properties.find((property) => property.key.name === Properties.expose); if (property?.value) { const properties = this.getValue(property.value); if (properties.type === Type.array) { this.publicProperties = properties.value; } } } parseObjectExpression(node) { this.parseExposeProperty(node); super.parseObjectExpression(node); } parseFeature(property: Babel.ObjectMethod | Babel.ObjectProperty) { if ('name' in property.key) { switch (property.key.name) { case Properties.data: case Properties.setup: if (property.type === Syntax.ObjectMethod) { this.parseSetupScope(property.body); } break; case Properties.model: case Properties.watch: case Properties.extends: // ignore this with Composition API break; case Properties.emits: if (this.features.includes(Feature.events)) { if (property.type === Syntax.ObjectProperty) { this.parsers.events.sync().parse(property.value); } } break; default: super.parseFeature(property); break; } } } }