UNPKG

@stackbit/utils

Version:
216 lines (201 loc) 8.17 kB
import path from 'path'; import fs from 'fs'; import fse from 'fs-extra'; import yaml from 'js-yaml'; import toml from '@iarna/toml'; import _ from 'lodash'; import { findPromise, reducePromise } from './promise-utils'; export interface ReadDirRecursivelyOptionsBase { filter?: (filePath: string, stats: fs.Stats) => boolean; absoluteFilePaths?: boolean; includeDirs?: boolean; includeStats?: false; } export interface ReadDirRecursivelyOptionsWithStats extends Omit<ReadDirRecursivelyOptionsBase, 'includeStats'> { includeStats: true; } export type ReadDirRecursivelyOptions = ReadDirRecursivelyOptionsWithStats | ReadDirRecursivelyOptionsBase; export interface ReadDirResultWithStats { filePath: string; stats: fs.Stats; } export async function readDirRecursively(dir: string, options?: ReadDirRecursivelyOptionsBase): Promise<string[]>; export async function readDirRecursively(dir: string, options?: ReadDirRecursivelyOptionsWithStats): Promise<ReadDirResultWithStats[]>; export async function readDirRecursively(dir: string, options?: ReadDirRecursivelyOptions): Promise<string[] | ReadDirResultWithStats[]> { const rootDir: string = _.get(options, 'rootDir', dir); const includeDirs = options?.includeDirs; const includeStats = options?.includeStats; const files = await fse.readdir(dir); return reducePromise( files, async (result: any[], file: string) => { const absFilePath = path.join(dir, file); const relFilePath = options?.absoluteFilePaths ? absFilePath : path.relative(rootDir, absFilePath); const stats = await fse.stat(absFilePath); if (options?.filter && !options.filter(relFilePath, stats)) { return result; } const resultItem = includeStats ? { filePath: relFilePath, stats: stats } : relFilePath; if (stats.isDirectory()) { const childOptions = { ...options, rootDir: rootDir }; const childFiles = await readDirRecursively(absFilePath, childOptions as any); return includeDirs ? result.concat(resultItem, childFiles) : result.concat(childFiles); } else if (stats.isFile()) { return result.concat(resultItem); } else { return result; } }, [] ); } export async function readDirRecursivelyWithExtensions(dirPath: string, allowedExtensions: string[]) { return readDirRecursively(dirPath, { filter: (filePath, stats) => { // return true for all directories to read them recursively if (!stats.isFile()) { return true; } const extension = path.extname(filePath).substring(1).toLowerCase(); return allowedExtensions.includes(extension); } }); } export async function parseFirstExistingFile(fileNames: string[], inputDir: string): Promise<any> { const filePath = await getFirstExistingFile(fileNames, inputDir); if (filePath) { return parseFile(filePath); } else { return null; } } export function getFirstExistingFile(fileNames: string[], inputDir: string): Promise<string | undefined> { const filePaths = _.map(fileNames, (fileName) => path.resolve(inputDir, fileName)); return findPromise(filePaths, (filePath) => fse.pathExists(filePath)); } export async function parseFile(filePath: string): Promise<any> { const data = await fse.readFile(filePath, 'utf8'); return parseDataByFilePath(data, filePath); } export function parseDataByFilePath(string: string, filePath: string) { const extension = path.extname(filePath).substring(1); let data: any; switch (extension) { case 'yml': case 'yaml': string = string.replace(/\n---\s*$/, ''); data = yaml.load(string, { schema: yaml.JSON_SCHEMA }); break; case 'json': data = JSON.parse(string); break; case 'toml': data = toml.parse(string); break; case 'md': case 'mdx': case 'markdown': data = parseMarkdownWithFrontMatter(string); break; case 'js': case 'jsx': data = string; break; default: throw new Error(`parseDataByFilePath error, extension '${extension}' of file ${filePath} is not supported`); } return data; } export function parseMarkdownWithFrontMatter(string: string): { frontmatter: any; markdown: string } { string = string.replace('\r\n', '\n'); let frontmatter: any = null; let markdown = string; const frontMatterTypes = [ { type: 'yaml', startDelimiter: '---\n', endDelimiter: '\n---', parse: (string: string) => yaml.load(string, { schema: yaml.JSON_SCHEMA }) }, { type: 'toml', startDelimiter: '+++\n', endDelimiter: '\n+++', parse: (string: string) => toml.parse(string) }, { type: 'jsonmd', startDelimiter: '---json\n', endDelimiter: '\n---', parse: (string: string) => JSON.parse(string) }, { type: 'json', startDelimiter: '{\n', endDelimiter: '\n}', parse: (string: string) => JSON.parse(`{${string}}`) } ]; _.forEach(frontMatterTypes, (fmType) => { if (string.startsWith(fmType.startDelimiter)) { const index = string.indexOf(fmType.endDelimiter); if (index !== -1) { // The end delimiter must be followed by EOF or by a new line (possibly preceded with spaces) // For example ("." used for spaces): // |--- // |title: Title // |---... // | // |Markdown Content // | // "index" points to the beginning of the second "---" // "endDelimEndIndex" points to the end of the second "---" // "afterEndDelim" is everything after the second "---" // "afterEndDelimMatch" is the matched "...\n" after the second "---" // frontmatter will be: {title: "Title"} // markdown will be "\nMarkdown Content\n" (the first \n after end delimiter is discarded) const endDelimEndIndex = index + fmType.endDelimiter.length; const afterEndDelim = string.substring(endDelimEndIndex); const afterEndDelimMatch = afterEndDelim.match(/^\s*?(\n|$)/); if (afterEndDelimMatch) { const data = string.substring(fmType.startDelimiter.length, index); const afterEndDelimString = afterEndDelimMatch[0] as string; frontmatter = fmType.parse(data); markdown = afterEndDelim.substring(afterEndDelimString.length); } } } }); return { frontmatter: frontmatter, markdown: markdown }; } export function outputData(filePath: string, data: any) { const res = stringifyDataByFilePath(data, filePath); return fse.outputFile(filePath, res); } export function stringifyDataByFilePath(data: any, filePath: string) { const extension = path.extname(filePath).substring(1); let result; switch (extension) { case 'yml': case 'yaml': result = yaml.dump(data, { noRefs: true }); break; case 'json': result = JSON.stringify(data, null, 4); break; case 'toml': result = toml.stringify(data); break; case 'md': case 'mdx': case 'markdown': result = '---\n' + yaml.dump(data.frontmatter, { noRefs: true }) + '---\n' + data.markdown; break; default: throw new Error(`stringifyDataByFilePath error, extension '${extension}' of file ${filePath} is not supported`); } return result; }