UNPKG

@toktokhan-dev/node

Version:

A Node.js utility library built by TOKTOKHAN.DEV

753 lines (728 loc) 22.8 kB
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 };