@vuedoc/parser
Version:
Generate a JSON documentation for a Vue file
253 lines • 9.3 kB
JavaScript
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