UNPKG

kompendium

Version:

Documentation generator for Stencil components

303 lines (240 loc) 8.22 kB
import { JsonDocs, Config, Logger } from '@stencil/core/internal'; import { defaultConfig } from './config'; import { addSources } from './source'; import lnk from 'lnk'; import { createMenu } from './menu'; import { copyFile, mkdir, readFile, stat, writeFile } from 'fs/promises'; import { exists } from './filesystem'; import { createWatcher } from './watch'; import { findGuides } from './guides'; import { KompendiumConfig, KompendiumData, TypeDescription } from '../types'; import { parseFile } from './typedoc'; import { createSchemas } from './schema'; import { createIndex } from './search'; export const kompendium = (config: Partial<KompendiumConfig> = {}) => { if (!generateDocs()) { return () => null; } return kompendiumGenerator(config); }; let logger: Logger; export function kompendiumGenerator( config: Partial<KompendiumConfig>, ): (docs: JsonDocs, stencilConfig: Config) => Promise<void> { config = { ...defaultConfig, ...config, }; initialize(config); return async (docs: JsonDocs, stencilConfig: Config) => { logger = stencilConfig.logger; const timeSpan = logger.createTimeSpan('kompendium started'); const [jsonDocs, title, readme, guides, types] = await Promise.all([ addSources(docs), getProjectTitle(config), getReadme(), findGuides(config), getTypes(config, stencilConfig.tsconfig), ]); const data: KompendiumData = { docs: jsonDocs, title: title, logo: config.logo, menu: createMenu(docs, guides, types), readme: readme, guides: guides, types: types, schemas: createSchemas(docs.components, types), index: null, }; data.index = createIndex(data); await writeData(config, data); timeSpan.finish('kompendium finished'); }; } async function initialize(config: Partial<KompendiumConfig>) { const path = `${config.publicPath}/kompendium.json`; if (isWatcher()) { createWatcher(path, 'unlink', onUnlink(config)); } await createOutputDirs(config); } const onUnlink = (config: Partial<KompendiumConfig>) => () => { createSymlink(config); }; async function createSymlink(config: Partial<KompendiumConfig>) { const source = `${config.path}/kompendium.json`; const target = `${config.publicPath}/kompendium.json`; if (!(await exists(source))) { return; } if (await exists(target)) { return; } lnk([source], config.publicPath); } async function getProjectTitle( config: Partial<KompendiumConfig>, ): Promise<string> { if (config.title) { return config.title; } const json = await readFile('./package.json', 'utf8'); const data = JSON.parse(json); return data.name .replace(/^@[^/]+?\//, '') .split('-') .join(' '); } async function writeData( config: Partial<KompendiumConfig>, data: KompendiumData, ) { // Always write to the kompendium config folder (typically `.kompendium` in // the root of the project) to avoid Stencil deleting the file during build. const filePath = `${config.path}/kompendium.json`; await writeFile(filePath, JSON.stringify(data), 'utf8'); if (isProd()) { // In production, we used to write the kompendium.json file to the // public path. We now copy the file to the public path for backwards // compatibility with projects that do not have problems with Stencil // deleting the file during build. For projects that do have this // problem, they can always copy the file from the config folder. const publicFilePath = `${config.publicPath}/kompendium.json`; if (!(await exists(config.publicPath))) { await mkdir(config.publicPath, { recursive: true }); } await copyFile(filePath, publicFilePath); } if (isWatcher()) { createSymlink(config); } } async function createOutputDirs(config: Partial<KompendiumConfig>) { let path = config.path; if (!(await exists(path))) { mkdir(path, { recursive: true }); } path = config.publicPath; if (!(await exists(path))) { mkdir(path, { recursive: true }); } } async function getReadme(): Promise<string> { const files = ['readme.md', 'README.md', 'README', 'readme']; let data = null; for (const file of files) { if (data) { continue; } if (!(await exists(file))) { continue; } data = await readFile(file, 'utf8'); } if (!data) { logger.warn('README did not exist'); } return data; } function generateDocs(): boolean { return !!process.argv.includes('--docs'); } function isWatcher(): boolean { return !!process.argv.includes('--watch'); } function isProd(): boolean { return !( process.argv.includes('--dev') || process.argv.includes('test') || process.argv.find((arg) => arg.includes('jest-worker')) ); } async function getTypes( config: Partial<KompendiumConfig>, tsconfig?: string, ): Promise<TypeDescription[]> { logger.debug('Getting type information...'); let types = await readTypes(config); const cache = await readCache(config); if (types.length === 0 || (await isModified(types, cache))) { logger.debug('Parsing types...'); const data = parseFile(config.typeRoot, tsconfig); await saveData(config, data); types = data; } return types; } async function isModified(types: any[], cache: Record<string, number>) { if (Object.keys(cache).length === 0) { return true; } const filenames = getUniqueSourceFilenames(types); const stats = await Promise.all(filenames.map(tryStatFile)); return stats.some((fileStat, index) => hasFileChangedSinceCached(filenames[index], fileStat, cache), ); } function getUniqueSourceFilenames(types: any[]): string[] { const filenames = types.map((t) => t.sources).flat(); return [...new Set(filenames)]; } function tryStatFile(filename: string) { return stat(filename).catch(() => null); } function hasFileChangedSinceCached( filename: string, fileStat: Awaited<ReturnType<typeof stat>> | null, cache: Record<string, number>, ): boolean { if (!fileStat) { logger.debug(`${filename} cannot be accessed, marking as modified`); return true; } const result = cache[filename] !== fileStat.mtimeMs; logger.debug(`${filename} was ${result ? '' : 'not'} modified!`); return result; } async function saveData( config: Partial<KompendiumConfig>, types: TypeDescription[], ) { const filenames = getUniqueSourceFilenames(types); const stats = await Promise.all(filenames.map(tryStatFile)); const cache = buildCacheFromFileStats(filenames, stats); await Promise.all([writeCache(config, cache), writeTypes(config, types)]); } function buildCacheFromFileStats( filenames: string[], stats: Array<Awaited<ReturnType<typeof stat>> | null>, ): Record<string, number> { const cache: Record<string, number> = {}; stats.forEach((fileStat, index) => { if (fileStat) { cache[filenames[index]] = fileStat.mtimeMs; } }); return cache; } async function readCache(config: Partial<KompendiumConfig>) { try { const data = await readFile(`${config.path}/cache.json`, 'utf8'); return JSON.parse(data); } catch { return {}; } } async function writeCache(config: Partial<KompendiumConfig>, data: any) { await writeFile(`${config.path}/cache.json`, JSON.stringify(data), 'utf8'); } async function readTypes(config: Partial<KompendiumConfig>) { try { const data = await readFile(`${config.path}/types.json`, 'utf8'); return JSON.parse(data); } catch { return []; } } async function writeTypes(config: Partial<KompendiumConfig>, data: any) { await writeFile(`${config.path}/types.json`, JSON.stringify(data), 'utf8'); }