import fs from "fs"; import path from "path"; import { type FastifyRequest } from "fastify"; import { type AbsolutePath } from "./path-utils"; export function findAppRootPath(): AbsolutePath { const apiRootPath = findApiRootPath(); return path.dirname(apiRootPath) as AbsolutePath; } export function findApiRootPath(): AbsolutePath { // NOTE: for support npm / yarn / pnpm workspaces // 하지만 workspace 쓰면 process.cwd() 하면 되는데... 이건 나중에 협의 후 수정하는걸로 const workspacePath = process.env.PNPM_SCRIPT_SRC_DIR ?? process.env.INIT_CWD; if (nonNullable(workspacePath)) { return workspacePath as AbsolutePath; } if (nonNullable(process.env.PNPM_PACKAGE_NAME)) { return process.cwd().split(path.sep).join(path.sep) as AbsolutePath; } const cwdPackagePath = path.join(process.cwd(), "package.json"); if (fs.existsSync(cwdPackagePath)) { return process.cwd().split(path.sep).join(path.sep) as AbsolutePath; } const basePath = import.meta.filename; let dir = path.dirname(basePath); if (dir.includes("/.yarn/")) { dir = dir.split("/.yarn/")[0]; } do { if (fs.existsSync(path.join(dir, "/package.json"))) { return dir.split(path.sep).join(path.sep) as AbsolutePath; } dir = dir.split(path.sep).slice(0, -1).join(path.sep); } while (dir.split(path.sep).length > 1); throw new Error("Cannot find AppRoot using Sonamu -2"); } export function nonNullable(value: T): value is NonNullable { return value !== null && value !== undefined; } export function exhaustive(_param: never) { throw new Error(`exhaustive`); } // 일반 버전 export function assertExists(value: T | null | undefined, message?: string): T { if (value === null || value === undefined) { throw new Error(message ?? "Value must exist"); } return value; } // null만 체크 export function assertNotNull(value: T | null, message?: string): T { if (value === null) { throw new Error(message ?? "Value must not be null"); } return value; } // undefined만 체크 export function assertDefined(value: T | undefined, message?: string): T { if (value === undefined) { throw new Error(message ?? "Value must be defined"); } return value; } // lodash intersectionBy 대체 export function intersectionBy( arr1: readonly T[], arr2: readonly T[], iteratee: (item: T) => K, ): T[] { const arr2Keys = new Set(arr2.map(iteratee)); return arr1.filter((item) => arr2Keys.has(iteratee(item))); } // lodash differenceWith 대체 export function differenceWith( arr1: readonly T[], arr2: readonly T[], comparator: (a: T, b: T) => boolean, ): T[] { return arr1.filter((itemA) => !arr2.some((itemB) => comparator(itemA, itemB))); } // biome-ignore lint/suspicious/noExplicitAny: dynamic property access export function merge>(defaultObj: T, userObj: T): T { // 원본 보존을 위해 defaultObj 복사 const result = { ...defaultObj }; // userObj의 각 속성을 순회 for (const key in userObj) { // userObj의 own property만 처리 (프로토타입 체인 제외) if (Object.hasOwn(userObj, key)) { const userValue = userObj[key]; const defaultValue = result[key]; // 두 값이 모두 객체이고, 배열이 아닌 경우 재귀적으로 병합 if (isPlainObject(userValue) && isPlainObject(defaultValue)) { result[key] = merge(defaultValue, userValue); } else { // 그 외의 경우 userObj의 값으로 덮어쓰기 result[key] = userValue; } } } return result; } // plain object 판별 헬퍼 함수 // (배열, null, Date 등을 제외한 순수 객체만 true) export function isPlainObject(value: unknown): value is Record { return ( value !== null && typeof value === "object" && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]" ); } // Convert Fastify headers to standard Headers object export function convertFastifyHeadersToStandard(headers: FastifyRequest["headers"]): Headers { const headersObj = new Headers(); Object.entries(headers).forEach(([key, value]) => { if (value) headersObj.append(key, value.toString()); }); return headersObj; }