@stackbit/utils
Version:
Stackbit utilities
216 lines (201 loc) • 8.17 kB
text/typescript
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;
}