@vuedoc/parser
Version:
Generate a JSON documentation for a Vue file
269 lines (208 loc) • 6.99 kB
text/typescript
import { readFileSync, createWriteStream } from 'node:fs';
import { dirname, isAbsolute, join, parse } from 'node:path';
import { fileURLToPath } from 'node:url';
import { VuedocParser, parseComponent } from '../main.js';
import { Parser } from '../../types/Parser.js';
import merge from 'deepmerge';
import JsonSchemav from 'jsonschemav';
import ValidationError from 'jsonschemav/lib/error.js';
import ConfigSchema from '../schema/config.js';
export type CliOptions = {
join: boolean;
stream: any;
filenames: string[];
output?: string;
parsing: Partial<Parser.Options>;
};
const jsv = new JsonSchemav();
const validator = jsv.compile(ConfigSchema);
const ARG_IGNORE_PREFIX = '--ignore-';
const usage = 'Usage: vuedoc-json [*.{js,vue} files]...';
export const MISSING_FILENAME_MESSAGE = `Missing filenames. ${usage}\n`;
export async function parseArgs(argv: string[], requireFiles = false): Promise<CliOptions> {
const parsing: Partial<Parser.Options> = {
features: VuedocParser.SUPPORTED_FEATURES as Parser.Feature[],
};
const options: CliOptions = {
join: false,
stream: true as any,
filenames: [],
parsing,
};
const promises = [];
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
switch (arg) {
/* istanbul ignore next */
case '-v':
/* istanbul ignore next */
case '--version': {
const __dirname = dirname(fileURLToPath(import.meta.url));
const packageFilename = join(__dirname, '../../package.json');
const { name, version } = JSON.parse(readFileSync(packageFilename, 'utf-8'));
const output = `${name} v${version}\n`;
process.stdout.write(output);
return null;
}
case '-c':
case '--config': {
let configFile = argv[i + 1] || '';
if (configFile.endsWith('.js')) {
i++;
} else {
configFile = 'vuedoc.config.js';
}
const configPath = isAbsolute(configFile)
? configFile
: join(process.cwd(), configFile);
promises.push((async () => {
const config = await import(configPath);
if (config.default) {
Object.assign(options, config.default);
if (config.default.parsing) {
options.parsing = { ...parsing, ...config.default.parsing };
}
}
})());
break;
}
case '-o':
case '--output':
if (!argv[i + 1]) {
throw new Error('Missing output value. Usage: --output [file or directory]\n');
}
options.output = argv[i + 1];
i++;
break;
case '-j':
case '--join':
options.join = true;
break;
case '-':
break;
default: {
if (arg.startsWith(ARG_IGNORE_PREFIX)) {
const feature = arg.substring(ARG_IGNORE_PREFIX.length);
options.parsing.features = options.parsing.features.filter((item) => item !== feature);
} else {
options.filenames.push(arg);
}
break;
}
}
}
await Promise.all(promises);
if (requireFiles && options.filenames.length === 0) {
throw new Error(MISSING_FILENAME_MESSAGE);
}
return options;
}
function renderFile(filename: string, { ...options }: CliOptions) {
return ({ warnings = [], errors = [], ...component }) => new Promise((resolve, reject) => {
warnings.forEach((message) => process.stderr.write(`Warn: ${message}\n`));
if (errors.length) {
errors.forEach((message) => process.stderr.write(`Err: ${message}\n`));
reject(new Error(component.errors[0]));
return;
}
const output = JSON.stringify(component, null, 2) + '\n';
const stream = typeof options.stream === 'function' && filename
? options.stream(filename)
: options.stream;
stream.write(output);
resolve(output);
});
}
export async function parseComponentOptions({ stream, filename, ...options }) {
if (!options.parsing) {
options.parsing = {};
}
// compatibility with previous versions
if (filename && !options.filenames) {
options.filenames = [filename];
}
try {
const instance = await validator;
const { parsing: parsingOptions, join, filenames, ...restOptions } = await instance.validate(options);
const promises: Promise<any>[] = await new Promise((resolve, reject) => {
const renderOptions = { stream, ...restOptions };
if (filenames.length) {
const parsers = filenames.map((filename) => parseComponent({ ...parsingOptions, filename }));
let promises = [];
if (join) {
promises = [
Promise.all(parsers).then(merge.all).then(renderFile(null, renderOptions)),
];
} else {
promises = parsers.map((promise, index) => {
return promise.then(renderFile(filenames[index], renderOptions));
});
}
resolve(promises);
} else if (parsingOptions.filecontent) {
resolve([
parseComponent(parsingOptions).then(renderFile(null, renderOptions)),
]);
} else {
reject(new Error('Invalid options. Missing options.filenames'));
}
});
const docs = await Promise.all(promises);
if (filenames.length === 1 && docs.length === 1) {
return docs[0];
}
return docs;
} catch (err) {
if (err instanceof ValidationError) {
err.message = 'Invalid options';
}
throw err;
}
}
export async function processRawContent(argv: string[], componentRawContent: string) {
const options = await parseArgs(argv, false);
if (options) {
options.stream = process.stdout;
(options.parsing as Parser.FilecontentOptions).filecontent = componentRawContent;
return parseComponentOptions(options as any);
}
return '';
}
export async function processContentOutput(options) {
if (options.output.endsWith('.json')) {
options.stream = createWriteStream(options.output);
} else {
options.stream = (filename: string) => {
const info = parse(filename);
const jsoname = `${info.name}.json`;
const dest = join(options.output, jsoname);
return createWriteStream(dest);
};
}
return parseComponentOptions(options);
}
export async function processContent(options) {
if (options.output) {
return processContentOutput(options);
}
options.stream = process.stdout;
return parseComponentOptions(options);
}
export async function exec(argv: string[], componentRawContent = '') {
if (componentRawContent) {
await processRawContent(argv, componentRawContent);
} else {
const options = await parseArgs(argv, true);
if (options) {
await processContent(options);
}
}
}
export async function silenceExec(argv: string[], componentRawContent = '') {
try {
await exec(argv, componentRawContent);
} catch (err) {
process.stderr.write(`${err.message}\n`);
}
}