gen/vue-docgen-web-types/src/build.ts (162 lines of code) (raw):
import {WebTypesBuilderConfig} from "../types/config";
import * as path from "path";
import glob from 'globby';
import * as chokidar from 'chokidar';
import {FSWatcher} from 'chokidar';
import {parse} from 'vue-docgen-api'
import {HtmlAttribute, HtmlTag, HtmlVueFilter, JSONSchemaForWebTypes} from "../types/web-types";
import * as fs from "fs";
import mkdirp from 'mkdirp'
import _ from 'lodash'
interface FileContents {
tags?: HtmlTag[];
attributes?: HtmlAttribute[];
"vue-filters"?: HtmlVueFilter[];
}
export default async function build(config: WebTypesBuilderConfig) {
config.componentsRoot = path.resolve(config.cwd, config.componentsRoot)
config.outFile = path.resolve(config.cwd, config.outFile)
// then create the watcher if necessary
const {watcher, componentFiles} = await getSources(
config.components,
config.componentsRoot
)
console.log("Building web-types to " + config.outFile)
const cache: { [filepath: string]: FileContents } = {}
const buildWebTypesBound = rebuild.bind(null, config, componentFiles, cache, watcher)
try {
await buildWebTypesBound()
} catch (e) {
console.error("Error building web-types: " + e.message)
await watcher.close()
return
}
if (config.watch) {
watcher.on('add', buildWebTypesBound)
.on('change', buildWebTypesBound)
.on('unlink', async (filePath) => {
console.log("Rebuilding on file removal " + filePath)
delete cache[filePath]
await writeDownWebTypesFile(config, Object.values(cache), config.outFile)
})
}
else {
await watcher.close()
}
}
async function getSources(
components: string | string[],
cwd: string,
): Promise<{
watcher: chokidar.FSWatcher
componentFiles: string[]
}> {
const watcher = chokidar.watch(components, {cwd})
const allComponentFiles = await glob(components, {cwd})
return {watcher, componentFiles: allComponentFiles}
}
async function rebuild(
config: WebTypesBuilderConfig,
files: string[],
cachedContent: { [filepath: string]: FileContents },
watcher: FSWatcher,
changedFilePath?: string) {
const cacheWebTypesContent = async (filePath: string) => {
cachedContent[filePath.replace(/\\/g, '/')] = await extractInformation(
path.join(config.componentsRoot, filePath),
config
)
return true
}
if (changedFilePath) {
console.log("Rebuilding on update file " + changedFilePath)
try {
// if in chokidar mode (watch), the path of the file that was just changed
// is passed as an argument. We only affect the changed file and avoid re-parsing the rest
await cacheWebTypesContent(changedFilePath)
} catch (e) {
throw new Error(
`Error building file ${config.outFile} when file ${changedFilePath} has changed: ${e.message}`
)
}
} else {
try {
// if we are initializing the current file, parse all components
await Promise.all(files.map(cacheWebTypesContent))
} catch (e) {
throw new Error(`Error building file ${config.outFile}: ${e.message}`)
}
}
// and finally save all concatenated values to the markdown file
await writeDownWebTypesFile(config, Object.values(cachedContent), config.outFile)
}
async function writeDownWebTypesFile(config: WebTypesBuilderConfig, definitions: FileContents[], destFilePath: string) {
const destFolder = path.dirname(destFilePath)
await mkdirp(destFolder)
let writeStream = fs.createWriteStream(destFilePath)
const contents: JSONSchemaForWebTypes = {
framework: "vue",
name: config.packageName,
version: config.packageVersion,
contributions: {
html: {
"description-markup": config.descriptionMarkup,
"types-syntax": config.typesSyntax,
tags: _(definitions).flatMap(d => d.tags || []).orderBy("name","asc").value(),
attributes: _(definitions).flatMap(d => d.attributes || []).orderBy("name","asc").value(),
"vue-filters": _(definitions).flatMap(d => d["vue-filters"] || []).orderBy("name","asc").value(),
}
}
}
const html = contents.contributions.html!
if (html.tags?.length == 0) html.tags = undefined
if (html.attributes?.length == 0) html.attributes = undefined
if (html["vue-filters"]?.length == 0) html["vue-filters"] = undefined
writeStream.write(JSON.stringify(contents, null, 2))
// close the stream
writeStream.close()
}
function ensureRelative(path: string) {
// The .replace() is a fix for paths that end up like "./src\\components\\General\\VerticalButton.vue" on windows machines.
return (path.startsWith("./") || path.startsWith("../") ? path : "./" + path).replace(/\\/g, '/');
}
async function extractInformation(
absolutePath: string,
config: WebTypesBuilderConfig,
): Promise<FileContents> {
const doc = await parse(absolutePath, config.apiOptions)
let description = doc.description?.trim() ?? ""
doc.docsBlocks?.forEach(block => {
if (description.length > 0) {
if (config.descriptionMarkup === "html") {
description += "<br/><br/>"
} else {
description += "\n\n"
}
}
description += block
})
return {
tags: [
{
name: doc.displayName,
description,
attributes: doc.props?.map(prop => ({
name: prop.name,
required: prop.required,
description: prop.description,
value: {
kind: "expression",
type: prop.type?.name ?? "any"
},
default: prop.defaultValue?.value,
})),
events: doc.events?.map(event => ({
name: event.name,
description: event.description
})),
slots: doc.slots?.map(slot => ({
name: slot.name,
description: slot.description
})),
source: {
module: ensureRelative(path.relative(config.cwd, absolutePath)),
symbol: doc.exportName
}
}
]
}
}