@toktokhan-dev/node
Version:
A Node.js utility library built by TOKTOKHAN.DEV
753 lines (728 loc) • 22.8 kB
JavaScript
import { minimatch } from 'minimatch';
import fs, { readFileSync as readFileSync$1, rmSync, mkdirSync } from 'fs';
import path from 'path';
import curry from 'lodash/curry.js';
import boxen from 'boxen';
import chalk from 'chalk';
import prettier from 'prettier';
import { cwd as cwd$1 } from 'process';
import flow from 'lodash/flow.js';
import { parse } from 'yaml';
import { globby } from 'globby';
import { spawn } from 'child_process';
import ora from 'ora';
import { removeEmptyObject } from '@toktokhan-dev/universal';
import camelCase from 'lodash/camelCase.js';
import snakeCase from 'lodash/snakeCase.js';
import startCase from 'lodash/startCase.js';
import { Eta } from 'eta';
// -- Shims --
import cjsUrl from 'node:url';
import cjsPath from 'node:path';
import cjsModule from 'node:module';
const __filename = cjsUrl.fileURLToPath(import.meta.url);
const __dirname = cjsPath.dirname(__filename);
const require = cjsModule.createRequire(import.meta.url);
/**
* 파일 접근 권한을 확인합니다.
*
* @category Utils/Fs
*/
const checkFileAccess = ({ filename, include, ignored, }) => {
const check = (patterns, path) => {
return patterns.some((pattern) => minimatch(path, pattern));
};
if (!include?.length && !ignored?.length)
return true;
if (!include?.length)
return !check(ignored || [], filename);
if (!ignored?.length)
return check(include, filename);
return check(include, filename) && !check(ignored || [], filename);
};
/**
* 성공 메시지를 생성하는 함수입니다.
*
* @category Utils/Logger
*
* @param value - 성공 메시지에 추가할 값
* @returns 성공 메시지 문자열
*
* @example
* ```typescript
* // 성공 메시지를 생성하는 예시
* const message = success('Operation completed successfully.');
* ```
*/
const success = (value) => {
return chalk.green(`${chalk.green.bold('success')}:${value}`);
};
/**
* 오류 메시지를 생성하는 함수입니다.
*
* @category Utils/Logger
*
* @param value - 오류 메시지에 추가할 값
* @returns 오류 메시지 문자열
*
* @example
* ```typescript
* // 오류 메시지를 생성하는 예시
* const message = error('An error occurred.');
* ```
*/
const error = (value) => {
return chalk.red(`${chalk.red.bold('error')}:${value}`);
};
/**
* 정보 메시지를 생성하는 함수입니다.
*
* @category Utils/Logger
*
* @param value - 정보 메시지에 추가할 값
* @returns 정보 메시지 문자열
*
* @example
* ```typescript
* // 정보 메시지를 생성하는 예시
* const message = info('Additional information.');
* ```
*/
const info = (value) => {
return chalk.blue(`${chalk.blue.bold('info')}:${value}`);
};
/**
* 성공 로그를 출력하는 함수입니다.
*
* @category Utils/Logger
*
* @param title - 로그 제목
* @param value - 로그 값
* @returns 입력된 값
*
* @example
* ```typescript
* // 성공 로그를 출력하는 예시
* successLog('Operation', result);
* successLog('Operation')(result);
* ```
*/
const successLog = curry((title, value) => {
console.log(success(title), value);
return value;
});
/**
* 오류 로그를 출력하는 함수입니다.
*
* @category Utils/Logger
*
* @param title - 로그 제목
* @param value - 로그 값
* @returns 입력된 값
*
* @example
* ```typescript
* // 오류 로그를 출력하는 예시
* errorLog('Error', errorMessage);
* errorLog('Error')(errorMessage);
* ```
*/
const errorLog = curry((title, value) => {
console.log(error(title), value);
return value;
});
/**
* 정보 로그를 출력하는 함수입니다.
*
* @category Utils/Logger
*
* @param title - 로그 제목
* @param value - 로그 값
* @returns 입력된 값
*
* @example
* ```typescript
* // 정보 로그를 출력하는 예시
* infoLog('Information', infoMessage);
* infoLog('Information')(infoMessage);
* ```
*/
const infoLog = curry((title, value) => {
console.log(info(title), value);
return value;
});
/**
* 존재 로그를 출력하는 함수입니다.
*
* @category Utils/Logger
*
* @param value - 존재 로그에 추가할 값
* @returns -
*
* @example
* ```typescript
* // 존재 로그를 출력하는 예시
* existLog('File exists.');
* ```
*/
const existLog = (value) => {
console.log(chalk.black.bgYellow('EXIST'), value);
};
/**
* 생성 로그를 출력하는 함수입니다.
*
* @category Utils/Logger
*
* @param value - 생성 로그에 추가할 값
* @returns -
*
* @example
* ```typescript
* // 생성 로그를 출력하는 예시
* generateLog('File generated successfully.');
* ```
*/
const generateLog = (value) => {
console.log(chalk.black.bgGreen('GENERATE'), value);
};
/**
* Prettier 로그를 출력하는 함수입니다.
*
* @category Utils/Logger
*
* @param value - Prettier 로그에 추가할 값
* @returns -
*
* @example
* ```typescript
* // Prettier 로그를 출력하는 예시
* prettierLog('Code formatted successfully.');
* ```
*/
const prettierLog = (value) => {
console.log(chalk.bgBlue('PRETTIER'), value);
};
/**
* box형태의 로그를 출력하는 함수입니다.
*
* @category Utils/Logger
* @param value - box 로그에 추가할 값
*
* @returns -
*
* @example
* ```typescript
* // Box 로그를 출력하는 예시
* boxLog(['box log 1', 'box log 2'], {title: 'Toktokhan'})
* ┌ Toktokhan_Dev ┐
* │ │
* │ box log 1 │
* │ box log 2 │
* │ │
* └───────────────┘
* ```
*/
const boxLog = (value, options) => {
console.log(boxen(value.join('\n'), {
titleAlignment: 'center',
padding: 1,
...options,
title: chalk.green.bold(options.title),
}));
};
/**
* 현재 작업 디렉터리(CWD)의 경로를 계산하여 반환하는 함수입니다.
*
* @category Utils/Path
*
* @param paths - 작업 디렉터리에 추가될 하위 경로들
* @returns 현재 작업 디렉터리(CWD)의 경로
*
* @example
* ```typescript
* // 현재 작업 디렉터리의 경로를 계산하는 예시
* const filePath = cwd('src', 'components', 'Button');
* ```
*/
const cwd = (...paths) => {
return path.resolve(process.cwd(), ...paths);
};
/**
* 주어진 디렉터리에서 파일을 검색하여 해당 파일의 경로를 반환하는 함수입니다.
*
* @category Utils/Path
*
* @param dir - 검색할 디렉터리의 경로
* @param filename - 검색할 파일의 이름
* @returns 해당 파일의 경로, 찾지 못한 경우 null 반환
*
* @example
* ```typescript
* // 주어진 디렉터리에서 파일을 검색하는 예시
* const filePath = findFile('src/components', 'index.js');
* ```
*/
const findFile = (dir, filename) => {
const files = fs.readdirSync(dir);
if (files.includes(filename)) {
return path.resolve(dir, filename);
}
return null;
};
/**
* 주어진 디렉터리부터 상위 디렉터리까지 파일을 검색하여 해당 파일의 경로를 반환하는 함수입니다.
*
* @category Utils/Path
*
* @param dir - 검색을 시작할 디렉터리의 경로
* @param filename - 검색할 파일의 이름
* @returns 해당 파일의 경로, 찾지 못한 경우 null 반환
*
* @example
* ```typescript
* // 주어진 디렉터리부터 상위 디렉터리까지 파일을 검색하는 예시
* const filePath = findFileToTop('src/components', 'index.js');
* ```
*/
const findFileToTop = (dir, filename) => {
const found = findFile(dir, filename);
if (found) {
return found;
}
const parentDir = path.resolve(dir, '..');
if (parentDir === dir) {
return null;
}
return findFileToTop(parentDir, filename);
};
/**
* 주어진 디렉터리부터 상위 디렉터리에 있는 package.json 파일의 경로를 기준으로
* 상대 경로를 사용하여 디렉터리를 생성하는 함수를 반환합니다.
*
* @category Utils/Path
*
* @param dir - 상위 디렉터리에 있는 package.json 파일을 찾을 시작 디렉터리의 경로
* @returns 생성된 디렉터리의 경로를 반환하는 함수
*
* @example
* ```typescript
* // 주어진 디렉터리부터 상위 디렉터리의 package.json 파일을 찾아 상대 경로를 사용하여 디렉터리를 생성하는 함수를 생성하는 예시
* const createRootDir = createPackageRoot(__dirname);
* const myDir = createRootDir('src', 'components', 'Button');
* ```
*/
const createPackageRoot = (dir) => (...paths) => {
const packageJson = findFileToTop(dir, 'package.json');
if (!packageJson)
throw new Error('package.json not found');
const root = path.dirname(packageJson);
return path.resolve(root, ...paths);
};
/**
*
* @category Utils/Path
*
* 현재 모듈의 디렉터리를 기준으로 package.json 파일의 상위 디렉터리에 있는 package.json 파일의 경로를 기준으로
* 상대 경로를 사용하여 디렉터리를 생성하는 함수입니다.
*
* @example
* ```typescript
* // 현재 모듈의 디렉터리를 기준으로 디렉터리를 생성하는 함수를 생성하는 예시
* const myDir = packageRoot('src', 'components', 'Button');
* ```
*/
const packageRoot = createPackageRoot(__dirname);
/**
* 주어진 디렉터리부터 하위 디렉터리까지 파일을 검색하여 해당 파일의 경로를 반환하는 함수입니다.
*
* @category Utils/Path
*
* @param dir - 검색을 시작할 디렉터리의 경로
* @param filename - 검색할 파일의 이름
* @returns 해당 파일의 경로, 찾지 못한 경우 null 반환
*
* @example
* ```typescript
* // 주어진 디렉터리부터 하위 디렉터리까지 파일을 검색하는 예시
* const filePath = findFileToBottom('src', 'index.js');
* ```
*/
const findFileToBottom = (dir, filename) => {
const found = findFile(dir, filename);
if (found) {
return found;
}
const targetFiles = fs.readdirSync(dir, { withFileTypes: true });
for (const file of targetFiles) {
const filePath = path.resolve(dir, file.name);
if (file.isDirectory()) {
const found = findFileToBottom(filePath, filename);
if (found) {
return found;
}
}
}
return null;
};
const _forEachFiles = (param, TPath) => {
const { each, recursive = true, filter = () => true } = param;
const targetFiles = fs.readdirSync(TPath, { withFileTypes: true });
targetFiles.forEach((file) => {
const filePath = path.resolve(TPath, file.name);
if (recursive && file.isDirectory()) {
_forEachFiles({ recursive, filter, each }, filePath);
}
if (!filter(file))
return;
each(file);
});
};
/**
* 주어진 디렉터리 내의 모든 파일 및 디렉터리에 대해 지정된 작업을 수행하는 함수입니다.
*
* @category Utils/Path
*
* @param param - 각 파일 또는 디렉터리에 대해 실행할 작업과 설정
* @param TPath - 작업을 수행할 디렉터리의 경로
*
* @example
* ```typescript
* // 주어진 디렉터리 내의 모든 파일 및 디렉터리에 대해 작업을 수행하는 예시
* forEachFiles({
* each: (file) => console.log(file.name),
* recursive: true,
* filter: (file) => file.isDirectory(),
* }, 'src');
* ```
*/
const forEachFiles = curry(_forEachFiles);
/**
* 주어진 대상 경로를 기준 경로와 결합하여 새 경로를 생성합니다.
*
* @category Utils/Path
*
* @param target - 대상 경로입니다.
* @param base - 기준 경로입니다.
* @returns 대상 경로와 기준 경로를 결합한 새 경로를 반환합니다.
*
* @example
* ```typescript
* // 주어진 대상 경로를 기준 경로와 결합하여 새 경로를 생성하는 예시
* const resolvePath = pathOf('file.txt');
* const result = resolvePath('/home/user'); // '/home/user/file.txt'
* ```
*/
const pathOf = curry((target, base) => path.join(base, target));
/**
* 주어진 기준 경로를 대상 경로와 결합하여 새 경로를 생성합니다.
*
* @category Utils/Path
*
* @param base - 기준 경로입니다.
* @param target - 대상 경로입니다.
* @returns 기준 경로와 대상 경로를 결합한 새 경로를 반환합니다.
*
* @example
* ```typescript
* // 주어진 기준 경로를 대상 경로와 결합하여 새 경로를 생성하는 예시
* const resolvePath = pathOn('/home/user');
* const result = resolvePath('file.txt'); // '/home/user/file.txt'
* ```
*/
const pathOn = curry((base, target) => path.join(base, target));
/**
* 주어진 문자열을 prettier를 사용하여 서식을 맞춥니다.
*
* @category Utils/String
*
* @param string - 서식을 맞출 문자열입니다.
* @param options - prettier의 옵션입니다.
* @returns 서식을 맞춘 결과를 반환합니다.
*/
async function prettierString(string, options) {
const configs = await (async () => {
if (!options?.configPath) {
return {};
}
const configPath = options.configPath === 'auto' ?
findFileToTop(cwd$1(), '.prettierrc.js') || '.prettierrc.js'
: options.configPath;
return prettier.resolveConfig(configPath, {
useCache: false,
});
})();
return prettier.format(string, {
...configs,
parser: 'babel',
...options,
});
}
/**
* 주어진 파일의 내용을 prettier를 사용하여 서식을 맞춥니다.
*
* @category Utils/String
*
* @param outputPath - 서식을 맞출 파일의 경로입니다.
* @param options - prettier의 옵션입니다.
*/
async function prettierFile(outputPath, options) {
const file = fs.readFileSync(outputPath, { encoding: 'utf-8' });
const prettyFile = await prettierString(file, options);
fs.writeFileSync(outputPath, prettyFile);
}
const _generateCodeFile = async (config, code) => {
fs.mkdirSync(path.parse(cwd(config.outputPath)).dir, { recursive: true });
fs.writeFileSync(cwd(config.outputPath), await prettierString(code, config.prettier), 'utf-8');
generateLog(cwd(config.outputPath));
};
/**
* 코드를 파일로 생성하는 함수입니다.
*
* @category Utils/Fs
*
* @param config - 코드 파일 생성에 필요한 설정 객체
* @param config.outputPath - 생성된 코드 파일의 경로
* @param config.prettier - 코드 파일을 포맷팅할 때 사용할 Prettier 옵션 (선택 사항)
* @param code - 생성할 코드 문자열
*
* @example
* ```typescript
* // 코드 파일 생성 예시
* const code = 'const message = "Hello, world!";'
*
* await generateCodeFile({
* outputPath: 'output/example.js',
* }, code)
*
* await generateCodeFile({
* outputPath: 'output/example.js',
* })(code)
*
* const genExample = generateCodeFile({
* outputPath: 'output/example.js',
* prettier: { semi: false, singleQuote: true },
* })
*
* await genExample(code)
* ```
*/
const generateCodeFile = curry(_generateCodeFile);
/**
* 동기적으로 파일을 읽어오는 함수입니다.
*
* @category Utils/Fs
*
* @param encoding - 파일의 인코딩 유형
* @param path - 읽을 파일의 경로
* @returns 파일의 내용을 문자열로 반환합니다.
*
* @example
* ```typescript
* // 파일을 동기적으로 읽어오는 예시
* const content = readFileSync('utf-8', 'example.txt');
* const content = readFileSync('utf-8')('example.txt');
*
* ```
*/
const readFileSync = curry((encoding, path) => readFileSync$1(path, { encoding }));
/**
* 주어진 경로에 해당하는 디렉터리를 재설정하는 함수입니다.
* 주어진 경로의 디렉터리를 먼저 재귀적으로 제거한 후, 새로운 디렉터리를 생성합니다.
*
* @category Utils/Fs
*
* @param path - 디렉터리를 재설정할 경로
*/
const resetDirSync = (path) => {
rmSync(path, { recursive: true, force: true });
mkdirSync(path, { recursive: true });
};
/**
* 주어진 경로의 디렉터리 또는 파일을 재귀적으로 제거하는 함수입니다.
* @category Utils/Fs
*
* @param path - 제거할 디렉터리 또는 파일의 경로
*/
const removeAll = (path) => {
rmSync(path, { recursive: true, force: true });
};
/**
* 주어진 JSON 파일을 읽어 파싱하여 객체로 반환하는 함수입니다.
*
* @category Utils/Fs
*
* @typeParam T - 반환될 객체의 타입
* @param path - 읽을 JSON 파일의 경로
* @returns JSON 파일을 파싱한 객체
*
* @example
* ```typescript
* // JSON 파일을 읽어 객체로 반환하는 예시
* const data = json<{ name: string; age: number }>('data.json');
* ```
*/
const json = flow(readFileSync("utf-8"), JSON.parse);
/**
* YAML 파일을 읽어 파싱하여 객체로 반환하는 함수입니다.
*
* @category Utils/Fs
*
* @typeParam T - 반환될 객체의 타입
* @param path - 읽을 YAML 파일의 경로
* @returns YAML 파일을 파싱한 객체
* @example
* ```typescript
* // YAML 파일을 읽어 객체로 반환하는 예시
* const data = yaml<{ name: string; age: number }>('data.yaml');
* ```
*/
const yaml = flow(readFileSync("utf-8"), parse);
/**
* 주어진 파일 경로의 모든 하위 경로를 반환합니다.
*
* @category Utils/Fs
* @param path - 파일 경로. 이 경로의 모든 하위 경로가 반환됩니다.
*
* @example
* ```typescript
* const paths = await getFilePaths('./src');
* console.log(paths); // ['./src/index.ts', './src/utils.ts', ...]
* ```
*/
const getFilePaths = async (path) => await globby(path);
/**
* execa를 사용하여 주어진 명령어를 실행합니다.
*
* @category Utils/Process
*
* @param cmd - 실행할 명령어입니다.
* @param args - 명령어에 전달할 인수들입니다.
* @param options - execa 옵션입니다.
* @returns execaChildProcess 객체를 반환합니다.
*
* @example
* ```typescript
* // execa를 사용하여 명령어를 실행하는 예시
* const result = $(cmd, args, options);
* ```
*/
function $(cmd, args, options) {
return spawn(cmd, args, { stdio: 'inherit', ...options });
}
/**
* 로딩 상태를 보여주면서 비동기 작업을 실행합니다.
*
* @category Utils/Process
*
* @param title - 로딩 상태 메시지의 제목입니다.
* @param description - 로딩 상태 메시지의 설명입니다.
* @param callback - 비동기 작업을 수행하는 함수입니다. 로딩 상태를 갱신하기 위해 `spinner` 객체를 전달받습니다.
* @param options - 옵션 객체로, 오류 발생 시 처리 방법을 지정합니다.
* @param options.onError - 오류가 발생했을 때 실행할 콜백 함수입니다.
* @returns 비동기 작업의 결과를 반환합니다.
*
* @example
* ```typescript
* // 로딩 상태를 보여주면서 비동기 작업을 실행하는 예시
* const result = await withLoading(
* 'Loading',
* 'Some description',
* async (spinner) => {
* // 비동기 작업 수행
* },
* {
* onError: (err) => {
* // 오류 처리
* },
* }
* );
* ```
*/
async function withLoading(title, description, callback, options) {
const spinner = ora().start(`${title}\n`);
try {
const res = await callback(spinner);
spinner
.succeed(`${success(title)} ${description}`)
.stop()
.clear();
return res;
}
catch (err) {
console.error(err);
options?.onError?.(err);
spinner.fail(`${error(title)} ${description}`).clear();
}
}
const _convertFilePathToObject = (params, targetPath) => {
const { includingPattern = [], ignoredPattern = [], recursive = true, basePath = '', formatKey = toUpperSnakeCase, formatValue, } = params || {};
const result = {};
const setFile = (TPath, obj) => {
fs.readdirSync(TPath, { withFileTypes: true }) //
.forEach((file) => {
const targetFile = path.join(TPath, file.name);
const targetFileInfo = path.parse(targetFile);
const isTargetFile = checkFileAccess({
filename: targetFile,
ignored: ignoredPattern,
include: includingPattern,
});
const key = formatKey(targetFileInfo.name, {
toUpperSnakeCase,
toPascalCase,
});
if (isTargetFile && !file.isDirectory()) {
let resolvedPath = targetFile.replace(targetPath + '/', '');
resolvedPath = path.join(basePath, resolvedPath);
obj[key] =
formatValue?.({
key,
path: resolvedPath,
wholePath: targetFile,
info: targetFileInfo,
}) || resolvedPath;
return;
}
if (recursive && file.isDirectory()) {
obj[key] = {};
setFile(targetFile, obj[key]);
}
});
};
setFile(targetPath, result);
return removeEmptyObject(result);
};
const convertFilePathToObject = curry(_convertFilePathToObject);
function toUpperSnakeCase(str) {
return snakeCase(str).toUpperCase();
}
function toPascalCase(str) {
return startCase(camelCase(str)).replace(/ /g, '');
}
/**
* 지정된 변수 이름과 데이터를 사용하여 내보낼 상수를 렌더링합니다.
*
* @category Utils/Render
*
* @param varName - 상수의 변수 이름입니다.
* @param data - 상수의 데이터입니다.
* @returns 렌더링된 상수를 반환합니다.
* @throws {Error} 대상 템플릿을 찾을 수 없을 때 발생합니다.
*
* @example
* ```typescript
* // 내보낼 상수를 렌더링하는 예시
* const renderedConst = renderExportConst('myConst', 'someData');
* ```
*/
const renderExportConst = (varName, data) => {
const view = new Eta().renderString('export const <%~ it.varName %> = <%~ it.data %>', {
varName,
data,
});
if (!view) {
throw new Error('Not found target template');
}
return view;
};
export { $, boxLog, checkFileAccess, convertFilePathToObject, createPackageRoot, cwd, error, errorLog, existLog, findFile, findFileToBottom, findFileToTop, forEachFiles, generateCodeFile, generateLog, getFilePaths, info, infoLog, json, packageRoot, pathOf, pathOn, prettierFile, prettierLog, prettierString, readFileSync, removeAll, renderExportConst, resetDirSync, success, successLog, withLoading, yaml };