UNPKG

@vuedoc/parser

Version:

Generate a JSON documentation for a Vue file

253 lines 9.3 kB
import { dirname, isAbsolute, join, parse } from 'node:path'; import { ScriptParser } from './ScriptParser.js'; import { MarkupTemplateParser } from './MarkupTemplateParser.js'; import { CompositionParser } from './CompositionParser.js'; import { NameEntry } from '../entity/NameEntry.js'; import { Composition } from '../lib/Composition.js'; import { Feature, Features, DEFAULT_IGNORED_VISIBILITIES, TypedocTag, JSDocTag, FeatureEvent } from '../lib/Enum.js'; import { clear, merge } from '@b613/utils/lib/object.js'; import { ScriptRegisterParser } from './ScriptRegisterParser.js'; import { FS, ParsingError } from '../lib/FS.js'; import { Loader, MissingLoaderError } from '../lib/Loader.js'; import { VueLoader } from '../loaders/vue.js'; import { HtmlLoader } from '../loaders/html.js'; import { JavaScriptLoader } from '../loaders/javascript.js'; import { TypeScriptLoader } from '../loaders/typescript.js'; const IGNORED_KEYWORDS = [ TypedocTag.hidden, JSDocTag.ignore, ]; const DEFAULT_LOADERS = [ Loader.extend('js', JavaScriptLoader), Loader.extend('ts', TypeScriptLoader), Loader.extend('html', HtmlLoader), Loader.extend('vue', VueLoader), ]; function parseLoaders(options) { if (options.loaders) { options.loaders = [...options.loaders]; // immutability } else { options.loaders = []; } for (const loader of DEFAULT_LOADERS) { if (!options.loaders.includes(loader)) { options.loaders.push(loader); } } } export class EntryEvent extends Event { constructor(entry) { super(entry.kind, { cancelable: true }); this.entry = entry; } } export class MessageEvent extends Event { constructor(type, message) { super(type, { cancelable: true }); this.message = message; } } export class EndEvent extends Event { constructor() { super('end', { cancelable: false }); } } export class FatalErrorEvent extends Event { constructor(err) { super('fatal', { cancelable: false }); this.error = err; } } export class VuedocParser extends EventTarget { constructor(options) { super(); this.options = { ...options }; this.features = options.features || Features; this.scope = {}; this.ignoredVisibilities = options.ignoredVisibilities || DEFAULT_IGNORED_VISIBILITIES; this.asyncOperations = []; this.plugins = []; this.composition = new Composition(options.composition); this.scriptOptions = { jsx: this.options.jsx || false, composition: this.options.composition || {}, resolver: this.options.resolver, encoding: this.options.encoding || 'utf8', loaders: this.options.loaders, }; this.createRegister = (source = this.file.script, file = this.file) => { return new ScriptRegisterParser(this, source, file, this.scriptOptions, this.createRegister); }; parseLoaders(options); this.parsePlugins(); this.parseOptions(); this.fs = new FS({ jsx: options.jsx || false, loaders: options.loaders, encoding: options.encoding || 'utf8', resolver: options.resolver, }); } static validateOptions(options) { if (!options.filename && !('filecontent' in options)) { throw new Error('options.filename or options.filecontent is required'); } if (options.features) { if (!Array.isArray(options.features)) { throw new TypeError('options.features must be an array'); } for (const feature of options.features) { if (!Features.includes(feature)) { throw new Error(`Unknow '${feature}' feature. Supported features: ${JSON.stringify(Features)}`); } } } if (options.plugins) { for (const plugin of options.plugins) { if (typeof plugin !== 'function') { throw new TypeError('options.plugins must be a list of functions'); } } } } parseOptions() { if (this.options.filename && isAbsolute(this.options.filename) && !this.options.resolver?.basedir) { if (!this.options.resolver) { this.options.resolver = {}; } this.options.resolver.basedir = dirname(this.options.filename); } } parsePlugins() { if (this.options.plugins) { for (const plugin of this.options.plugins) { if (typeof plugin === 'function') { const def = plugin(this); if (def) { this.plugins.push(def); if (def.resolver) { merge(this.options.resolver, def.resolver); } if (def.composition) { this.composition.unshift(def.composition); } } } } } } preloadPlugins() { for (const plugin of this.plugins) { if (plugin.preload instanceof Array) { for (const filename of plugin.preload) { const file = this.fs.loadFile(filename); if (file.script) { const register = this.createRegister(file.script, file); register.parseAst(file.script.ast.program); Object.assign(this.scope, register.exposedScope); } } } } } reset() { clear(this.scope); this.asyncOperations.splice(0); } emitEntry(entry) { if ('visibility' in entry && this.isIgnoredVisibility(entry.visibility)) { return; } if ('keywords' in entry) { const contentsAnIgnoredKeyword = entry.keywords.some(({ name }) => IGNORED_KEYWORDS.includes(name)); if (contentsAnIgnoredKeyword) { return; } } this.dispatchEvent(new EntryEvent(entry)); } emitWarning(message) { this.dispatchEvent(new MessageEvent('warning', message)); } emitError(message) { this.dispatchEvent(new MessageEvent('error', message)); } emitEnd() { this.dispatchEvent(new EndEvent()); } isIgnoredVisibility(visibility) { return this.ignoredVisibilities.includes(visibility); } execAsync(fn) { this.asyncOperations.push(fn()); } createScriptParser(source, file) { if (!source.attrs.setup) { source.attrs.setup = CompositionParser.isCompositionScript(source.ast.program, this.composition); } return source.attrs.setup ? new CompositionParser(this, source, file, this.scriptOptions, this.createRegister) : new ScriptParser(this, source, file, this.scriptOptions, this.createRegister); } walk() { VuedocParser.validateOptions(this.options); this.preloadPlugins(); try { if ('filecontent' in this.options) { this.file = this.fs.loadContent('vue', this.options.filecontent); } else if (this.options.filename) { const filename = isAbsolute(this.options.filename) ? this.options.filename : join(process.cwd(), this.options.filename); this.file = this.fs.loadFile(filename); } } catch (err) { if (err instanceof MissingLoaderError || err instanceof ParsingError) { this.emitError(err.message); this.emitEnd(); } else { this.dispatchEvent(new FatalErrorEvent(err)); } return; } if (!this.file) { throw new Error('Unable to load main component file. Make sure to define options.filename or options.filecontent'); } let hasNameEntry = false; if (this.features.includes(Feature.name)) { this.addEventListener(FeatureEvent.name, () => { hasNameEntry = true; }, { once: true }); } if (this.file.script) { try { this.createScriptParser(this.file.script, this.file).parse(); } catch (err) { this.emitError(err.message); } } if (this.file.template?.content) { new MarkupTemplateParser(this, this.file.template).parse(); } setTimeout(async () => { await Promise.all(this.asyncOperations); if (!hasNameEntry && this.features.includes(Feature.name)) { this.parseComponentName(); } this.emitEnd(); this.reset(); }, 0); } parseComponentName() { if (this.options.filename) { const name = parse(this.options.filename).name; const entry = new NameEntry(name); this.emitEntry(entry); } } } VuedocParser.SUPPORTED_FEATURES = Features; //# sourceMappingURL=VuedocParser.js.map