/** * File Loader - 단일 파일 로더 * * JSON, YAML, TOML 파일을 로드하고 파싱 * * @example * ```ts * file({ path: 'data/settings.json' }) * file({ path: 'config/app.yaml' }) * ``` */ import type { Loader, FileLoaderOptions } from "./types"; import type { LoaderContext } from "../types"; import { LoaderError, ParseError } from "../types"; import { generateFileDigest } from "../digest"; import { inferParser } from "./types"; import * as fs from "fs"; import * as path from "path"; /** * JSON 파싱 */ function parseJson(content: string, filePath: string): unknown { try { return JSON.parse(content); } catch (error) { throw new ParseError( `Failed to parse JSON: ${error instanceof Error ? error.message : error}`, undefined, filePath ); } } /** * YAML 파싱 (동적 임포트) */ async function parseYaml(content: string, filePath: string): Promise { try { // yaml 패키지 동적 로드 // @ts-ignore dynamic optional import const yaml = await import("yaml").catch(() => null); if (!yaml) { throw new Error("yaml package not installed. Run: pnpm add yaml"); } return yaml.parse(content); } catch (error) { if (error instanceof Error && error.message.includes("not installed")) { throw error; } throw new ParseError( `Failed to parse YAML: ${error instanceof Error ? error.message : error}`, undefined, filePath ); } } /** * TOML 파싱 (동적 임포트) */ async function parseToml(content: string, filePath: string): Promise { try { // @iarna/toml 또는 toml 패키지 동적 로드 // @ts-ignore dynamic optional import const toml = await import("@iarna/toml").catch(() => // @ts-ignore dynamic optional import import("toml").catch(() => null) ); if (!toml) { throw new Error("toml package not installed. Run: pnpm add @iarna/toml"); } return toml.parse(content); } catch (error) { if (error instanceof Error && error.message.includes("not installed")) { throw error; } throw new ParseError( `Failed to parse TOML: ${error instanceof Error ? error.message : error}`, undefined, filePath ); } } /** * File Loader 팩토리 */ export function file(options: FileLoaderOptions): Loader { const { path: filePath, parser: explicitParser } = options; return { name: "file", async load(context: LoaderContext): Promise { const { store, config, logger, parseData, generateDigest: _generateDigest } = context; // 절대 경로 계산 const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(config.root, filePath); // 파일 존재 확인 if (!fs.existsSync(absolutePath)) { throw new LoaderError( `File not found: ${absolutePath}`, context.collection ); } // 파일 읽기 const content = fs.readFileSync(absolutePath, "utf-8"); // 다이제스트 생성 const digest = generateFileDigest(content); // 기존 엔트리와 비교 const existingEntry = store.get("_single"); if (existingEntry?.digest === digest) { logger.debug("File unchanged, skipping parse"); return; } // 파서 결정 const parserType = explicitParser ?? inferParser(absolutePath); if (!parserType || parserType === "markdown") { throw new LoaderError( `Cannot determine parser for file: ${absolutePath}. Use .json, .yaml, .yml, or .toml extension, or specify parser option.`, context.collection ); } // 파싱 let rawData: unknown; switch (parserType) { case "json": rawData = parseJson(content, absolutePath); break; case "yaml": rawData = await parseYaml(content, absolutePath); break; case "toml": rawData = await parseToml(content, absolutePath); break; } // 스키마 검증 const data = await parseData({ id: "_single", data: rawData, filePath: absolutePath, }); // 저장 store.set({ id: "_single", data: data as Record, filePath: absolutePath, digest, }); logger.info(`Loaded file: ${filePath}`); }, }; }