@synstack/fs
Version:
File system operations made easy
1 lines • 74.7 kB
Source Map (JSON)
{"version":3,"sources":["../src/fs.index.ts","../src/dir.lib.ts","../src/dirs-array.lib.ts","../src/file.lib.ts","../src/files-array.lib.ts"],"sourcesContent":["export { dir, FsDir, fsDir } from \"./dir.lib.ts\";\nexport { file, FsFile, fsFile } from \"./file.lib.ts\";\nexport {\n files,\n filesFromDir,\n fsFiles,\n fsFilesFromDir,\n type FsFileArray,\n} from \"./files-array.lib.ts\";\n","import { git } from \"@synstack/git\";\nimport { glob } from \"@synstack/glob\";\nimport { type AnyPath, path } from \"@synstack/path\";\nimport { Pipeable } from \"@synstack/pipe\";\nimport type { TemplateExpression } from \"execa\";\nimport * as fsSync from \"fs\";\nimport * as fs from \"fs/promises\";\nimport { dirs } from \"./dirs-array.lib.ts\";\nimport { FsFile } from \"./file.lib.ts\";\nimport { fsFiles, fsFilesFromDir } from \"./files-array.lib.ts\";\n\nexport class FsDir extends Pipeable<FsDir> {\n private readonly _path: AnyPath;\n\n protected constructor(path: AnyPath) {\n super();\n this._path = path;\n }\n\n /**\n * Get the string representation of the directory path.\n *\n * @returns The absolute path of the directory as a string\n *\n * ```typescript\n * const srcDir = fsDir(\"./src\");\n * console.log(srcDir.toString()); // \"/absolute/path/to/src\"\n * ```\n */\n public toString(): AnyPath {\n return this._path;\n }\n\n /**\n * Get the primitive value of the directory path.\n *\n * @returns The absolute path of the directory\n */\n public valueOf(): AnyPath {\n return this._path;\n }\n\n /**\n * Get the current instance of the directory.\n * Used for type compatibility with Pipeable.\n *\n * @returns The current FsDir instance\n */\n public instanceOf(): FsDir {\n return this;\n }\n\n /**\n * Get the absolute path of the directory.\n *\n * @returns The absolute path as a string\n *\n * ```typescript\n * const srcDir = fsDir(\"./src\");\n * console.log(srcDir.path); // \"/absolute/path/to/src\"\n * ```\n */\n public get path(): AnyPath {\n return this._path;\n }\n\n /**\n * Get the relative path from this directory to another file/directory\n *\n * @param dirOrfile - The other directory\n * @returns The relative path as a string\n */\n public relativePathTo(dirOrfile: FsDir | FsFile) {\n return path.relative(this.path, dirOrfile.path);\n }\n\n /**\n * Get the relative path from another file/directory to this directory\n *\n * @param dirOrFile - The other directory\n * @returns The relative path as a string\n */\n public relativePathFrom(dirOrFile: FsDir | FsFile): string {\n if (dirOrFile instanceof FsFile)\n return this.relativePathFrom(dirOrFile.dir());\n return path.relative(dirOrFile.path, this.path);\n }\n\n /**\n * Create a new directory instance with the provided path(s).\n * Resolves relative paths to absolute paths.\n *\n * @param arg - A path or an existing FsDir instance\n * @returns A new FsDir instance with the resolved path\n *\n * ```typescript\n * // Create from absolute path\n * const rootDir = fsDir(\"/path/to/project\");\n *\n * // Create from relative path\n * const srcDir = fsDir(\"./src\");\n *\n * // Create from existing directory\n * const existingDir = fsDir(fsDir(\"/path/to/existing\"));\n * ```\n */\n public static cwd(this: void, arg: FsDir | AnyPath): FsDir;\n public static cwd(this: void, arg: FsDir | AnyPath) {\n if (arg instanceof FsDir) return arg;\n return new FsDir(path.resolve(arg));\n }\n\n /**\n * Create a new directory instance with a path relative to this directory.\n *\n * @alias {@link to}\n * @param relativePath - The relative path to append to the current directory\n * @returns A new FsDir instance representing the combined path\n *\n * ```typescript\n * const projectDir = fsDir(\"/path/to/project\");\n *\n * // Navigate to subdirectories\n * const srcDir = projectDir.toDir(\"src\");\n * const testDir = projectDir.toDir(\"tests\");\n *\n * // Navigate up and down\n * const siblingDir = srcDir.toDir(\"../other\");\n * ```\n */\n public toDir(relativePath: string) {\n const newPath = path.join(this._path, relativePath);\n return new FsDir(newPath);\n }\n\n /**\n * Create a new directory instance with a path relative to this directory.\n *\n * @alias {@link toDir}\n * @param relativePath - The relative path to append to the current directory\n * @returns A new FsDir instance representing the combined path\n *\n * ```typescript\n * const projectDir = fsDir(\"/path/to/project\");\n *\n * // Navigate to subdirectories\n * const srcDir = projectDir.to(\"src\");\n * const testDir = projectDir.to(\"tests\");\n *\n * // Navigate up and down\n * const siblingDir = srcDir.to(\"../other\");\n * ```\n */\n public to(relativePath: string) {\n return this.toDir(relativePath);\n }\n\n /**\n * Create a new file instance with a path relative to this directory.\n *\n * @alias {@link file}\n * @param relativePath - The relative path to the file from this directory\n * @returns A new FsFile instance for the specified path\n * @throws If an absolute path is provided\n *\n * ```typescript\n * const srcDir = fsDir(\"./src\");\n *\n * // Access files in the directory\n * const configFile = srcDir.toFile(\"config.json\");\n * const deepFile = srcDir.toFile(\"components/Button.tsx\");\n *\n * // Error: Cannot use absolute paths\n * srcDir.toFile(\"/absolute/path\"); // throws Error\n * ```\n */\n public toFile(relativePath: string) {\n if (path.isAbsolute(relativePath))\n throw new Error(`\nTrying to access a dir file from an absolute paths:\n - Folder path: ${this._path}\n - File path: ${relativePath}\n`);\n return FsFile.from(path.resolve(this._path, relativePath));\n }\n\n /**\n * Create a new file instance with a path relative to this directory.\n *\n * @alias {@link toFile}\n * @param relativePath - The relative path to the file from this directory\n * @returns A new FsFile instance for the specified path\n * @throws If an absolute path is provided\n *\n * ```typescript\n * const srcDir = fsDir(\"./src\");\n *\n * // Access files in the directory\n * const configFile = srcDir.file(\"config.json\");\n * const deepFile = srcDir.file(\"components/Button.tsx\");\n *\n * // Error: Cannot use absolute paths\n * srcDir.file(\"/absolute/path\"); // throws Error\n * ```\n */\n public file(relativePath: string) {\n return this.toFile(relativePath);\n }\n\n /**\n * Get the name of the directory (last segment of the path).\n *\n * @returns The directory name without the full path\n *\n * ```typescript\n * const srcDir = fsDir(\"/path/to/project/src\");\n * console.log(srcDir.name()); // \"src\"\n * ```\n */\n public name(): string {\n return path.filename(this._path);\n }\n\n /**\n * Check if the directory exists in the file system.\n *\n * @returns A promise that resolves to true if the directory exists, false otherwise\n *\n * ```typescript\n * const configDir = fsDir(\"./config\");\n *\n * if (await configDir.exists()) {\n * // Directory exists, safe to use\n * const files = await configDir.glob(\"*.json\");\n * } else {\n * // Create the directory first\n * await configDir.make();\n * }\n * ```\n */\n public async exists(): Promise<boolean> {\n return fs\n .access(this._path, fsSync.constants.F_OK)\n .then(() => true)\n .catch(() => false);\n }\n\n /**\n * Check if the directory exists in the file system synchronously.\n *\n * @synchronous\n * @returns True if the directory exists, false otherwise\n *\n * ```typescript\n * const configDir = fsDir(\"./config\");\n *\n * if (configDir.existsSync()) {\n * // Directory exists, safe to use\n * const files = configDir.globSync(\"*.json\");\n * } else {\n * // Create the directory first\n * configDir.makeSync();\n * }\n * ```\n */\n public existsSync(): boolean {\n try {\n fsSync.accessSync(this._path, fsSync.constants.F_OK);\n return true;\n } catch (error: any) {\n if (error.code === \"ENOENT\") return false;\n throw error;\n }\n }\n\n /**\n * Create the directory and any necessary parent directories.\n *\n * @returns A promise that resolves when the directory is created\n *\n * ```typescript\n * const assetsDir = fsDir(\"./dist/assets/images\");\n *\n * // Creates all necessary parent directories\n * await assetsDir.make();\n * ```\n */\n public async make(): Promise<void> {\n await fs.mkdir(this._path, { recursive: true });\n }\n\n /**\n * Create the directory and any necessary parent directories synchronously.\n *\n * @synchronous\n *\n * ```typescript\n * const assetsDir = fsDir(\"./dist/assets/images\");\n *\n * // Creates all necessary parent directories immediately\n * assetsDir.makeSync();\n * ```\n */\n public makeSync(): void {\n fsSync.mkdirSync(this._path, { recursive: true });\n }\n\n /**\n * Move the directory to a new location.\n *\n * @param newPath - The new path for the directory\n * @returns A promise that resolves the new directory\n */\n public async move(newPath: FsDir | AnyPath): Promise<FsDir> {\n const newDir = FsDir.cwd(newPath);\n await fs.rename(this._path, newDir.path);\n return newDir;\n }\n\n /**\n * Move the directory to a new location synchronously.\n *\n * @param newPath - The new path for the directory\n * @returns The new directory\n */\n public moveSync(newPath: FsDir | AnyPath): FsDir {\n const newDir = FsDir.cwd(newPath);\n fsSync.renameSync(this._path, newDir.path);\n return newDir;\n }\n\n /**\n * Remove the directory and all its contents recursively.\n * If the directory doesn't exist, the operation is silently ignored.\n *\n * @returns A promise that resolves when the directory is removed\n *\n * ```typescript\n * const tempDir = fsDir(\"./temp\");\n *\n * // Remove directory and all contents\n * await tempDir.rm();\n * ```\n */\n public async rm(): Promise<void> {\n await fs.rm(this._path, { recursive: true }).catch((e) => {\n if (e.code === \"ENOENT\") return;\n throw e;\n });\n }\n\n /**\n * Remove the directory and all its contents recursively synchronously.\n * If the directory doesn't exist, the operation is silently ignored.\n *\n * @synchronous\n *\n * ```typescript\n * const tempDir = fsDir(\"./temp\");\n *\n * // Remove directory and all contents immediately\n * tempDir.rmSync();\n * ```\n */\n public rmSync(): void {\n try {\n fsSync.rmSync(this._path, { recursive: true });\n } catch (error: any) {\n if (error.code === \"ENOENT\") return;\n throw error;\n }\n }\n\n /**\n * Find files in the directory that match the specified glob patterns.\n *\n * @param patterns - One or more glob patterns to match against\n * @returns A promise that resolves to an FsFileArray containing the matching files\n *\n * ```typescript\n * const srcDir = fsDir(\"./src\");\n *\n * // Find all TypeScript files\n * const tsFiles = await srcDir.glob(\"**\\/*.ts\");\n *\n * // Find multiple file types\n * const assets = await srcDir.glob(\"**\\/*.{png,jpg,svg}\");\n *\n * // Use array of patterns\n * const configs = await srcDir.glob([\"*.json\", \"*.yaml\"]);\n * ```\n */\n public glob(...patterns: Array<string> | [Array<string>]) {\n return glob\n .cwd(this._path)\n .options({ absolute: true })\n .find(...patterns)\n .then(fsFiles);\n }\n\n /**\n * Find files in the directory that match the specified glob patterns synchronously.\n *\n * @param patterns - One or more glob patterns to match against\n * @returns An FsFileArray containing the matching files\n * @synchronous\n *\n * ```typescript\n * const srcDir = fsDir(\"./src\");\n *\n * // Find all TypeScript files synchronously\n * const tsFiles = srcDir.globSync(\"**\\/*.ts\");\n *\n * // Find multiple file types\n * const assets = srcDir.globSync(\"**\\/*.{png,jpg,svg}\");\n * ```\n */\n public globSync(...patterns: Array<string> | [Array<string>]) {\n return fsFiles(\n glob\n .cwd(this._path)\n .options({ absolute: true })\n .findSync(...patterns),\n );\n }\n\n /**\n * Find folders in the directory that match the specified glob patterns.\n *\n * @param patterns - One or more glob patterns to match against\n * @returns A promise that resolves to an FsDirArray containing the matching folders\n */\n public globDirs(...patterns: Array<string> | [Array<string>]) {\n const patternsWithTrailingSlash = patterns\n .flat()\n .map((p) => glob.ensureDirTrailingSlash(p));\n\n return glob\n .cwd(this._path)\n .options({ nodir: false, absolute: true })\n .find(patternsWithTrailingSlash)\n .then(dirs);\n }\n\n /**\n * Find folders in the directory that match the specified glob patterns synchronously.\n *\n * @param patterns - One or more glob patterns to match against\n * @returns An FsDirArray containing the matching folders\n * @synchronous\n */\n public globDirsSync(...patterns: Array<string> | [Array<string>]) {\n const patternsWithTrailingSlash = patterns\n .flat()\n .map((p) => glob.ensureDirTrailingSlash(p));\n\n return dirs(\n glob\n .cwd(this._path)\n .options({ nodir: false, absolute: true })\n .findSync(patternsWithTrailingSlash),\n );\n }\n\n /**\n * Find files tracked by git in the directory.\n *\n * @returns A promise that resolves to an FsFileArray containing the git-tracked files\n *\n * ```typescript\n * const projectDir = fsDir(\"./project\");\n *\n * // Get all git-tracked files in the directory\n * const trackedFiles = await projectDir.gitLs();\n * ```\n */\n public async gitLs() {\n return git.ls(this._path).then(fsFilesFromDir(this));\n }\n\n /**\n * Execute a command in the directory.\n */\n public async exec(\n template: TemplateStringsArray,\n ...args: Array<TemplateExpression>\n ) {\n const { execa } = await import(\"execa\").catch(() => {\n throw new Error(\n \"The `execa` package is not installed. Please install it first.\",\n );\n });\n return execa({ cwd: this.path })(template, ...args);\n }\n}\n\n/**\n * Create a new directory instance with the provided path.\n * Resolves relative paths to absolute paths.\n *\n * @param arg - A path or an existing FsDir instance\n * @returns A new FsDir instance with the resolved path\n *\n * ```typescript\n * // Create from absolute path\n * const rootDir = fsDir(\"/path/to/project\");\n *\n * // Create from relative path\n * const srcDir = fsDir(\"./src\");\n *\n * // Create from existing directory\n * const existingDir = fsDir(fsDir(\"/path/to/existing\"));\n * ```\n */\nexport const fsDir = FsDir.cwd;\n\n/**\n * @deprecated Changed to avoid namespacing conflicts. Use {@link fsDir} instead\n */\nexport const dir = FsDir.cwd;\n","import { enhance, type Enhanced } from \"@synstack/enhance\";\nimport { type AnyPath } from \"@synstack/path\";\nimport { fsDir, FsDir } from \"./dir.lib.ts\";\nimport type { FsFile } from \"./file.lib.ts\";\n\n/**\n * Interface defining chainable methods for arrays of FsFile instances.\n * @internal\n */\nexport interface FsDirArrayMethods {\n filter(this: FsDirArray, fn: (dir: FsDir) => boolean): FsDirArray;\n toPaths(this: FsDirArray): Array<string>;\n relativePathsFrom(this: FsDirArray, dir: FsDir | FsFile): Array<string>;\n}\n\nconst dirsArrayMethods: FsDirArrayMethods = {\n filter(fn) {\n return fsDirs(this.filter(fn));\n },\n\n toPaths() {\n return this.map((dir) => dir.path);\n },\n\n relativePathsFrom(dirOrFileOrPath) {\n return this.map((dir) => dir.relativePathFrom(dirOrFileOrPath));\n },\n};\n\nexport type FsDirArray = Enhanced<\n \"dirs_array\",\n Array<FsDir>,\n FsDirArrayMethods\n>;\n\n/**\n * Create a new FsDirArray instance with the provided directories.\n * @param dirs - An array of FsDir or AnyPath instances\n * @returns A new FsDirArray instance\n */\nexport const fsDirs = (dirs: Array<FsDir | AnyPath>): FsDirArray =>\n enhance(\"dirs_array\", dirs.map(fsDir), dirsArrayMethods);\n\n/**\n * @deprecated Changed to avoid namespacing conflicts. Use {@link fsDirs} instead\n */\nexport const dirs = fsDirs;\n","import { glob } from \"@synstack/glob\";\nimport { json } from \"@synstack/json\";\nimport { MdDoc } from \"@synstack/markdown\";\nimport { type AnyPath, path } from \"@synstack/path\";\nimport { Pipeable } from \"@synstack/pipe\";\nimport { str } from \"@synstack/str\";\nimport { type Xml, xml } from \"@synstack/xml\";\nimport { yaml } from \"@synstack/yaml\";\nimport * as fsSync from \"fs\";\nimport * as fs from \"fs/promises\";\nimport type { ZodTypeDef as ZodTypeDefV3, ZodType as ZodTypeV3 } from \"zod/v3\";\nimport type { ZodType as ZodTypeV4 } from \"zod/v4\";\nimport { type Stringable } from \"../../shared/src/ts.utils.ts\";\nimport { FsDir } from \"./dir.lib.ts\";\n\n// Union type to support both Zod v3 and v4 schemas\ntype ZodSchema<OUT = any, IN = any> =\n | ZodTypeV3<OUT, ZodTypeDefV3, IN>\n | ZodTypeV4<OUT, IN>;\n\ntype TextEncoding = Exclude<BufferEncoding, \"buffer\">;\ntype WriteMode = \"preserve\" | \"overwrite\";\n\nexport interface Base64Data {\n type: \"base64\";\n data: string;\n mimeType: string;\n}\n\n/**\n * A strongly-typed, chainable API for file system operations.\n * Provides methods for reading, writing, and manipulating files with support for multiple formats.\n *\n * @typeParam TEncoding - The text encoding to use for file operations (default: 'utf-8')\n * @typeParam TSchema - Optional Zod schema for validating JSON/YAML data\n *\n * ```typescript\n * import { fsFile } from \"@synstack/fs\";\n *\n * // Create a file instance\n * const configFile = fsFile(\"./config.json\")\n * .schema(ConfigSchema)\n * .read.json();\n *\n * // Write text with different encodings\n * const logFile = fsFile(\"./log.txt\")\n * .write.text(\"Hello World\");\n * ```\n */\nexport class FsFile<\n TEncoding extends TextEncoding = \"utf-8\",\n TSchema extends ZodSchema | undefined = undefined,\n> extends Pipeable<FsFile<TEncoding, TSchema>, AnyPath> {\n private readonly _path: AnyPath;\n private readonly _encoding: TEncoding;\n private readonly _schema?: TSchema;\n\n /**\n * Create a new FsFile instance from a path, a list of paths to be resolved, or an existing FsFile instance.\n * The resulting path will be an absolute path.\n *\n * @param paths - A path or an existing FsFile instance\n * @returns A new FsFile instance with UTF-8 encoding\n *\n * ```typescript\n * import { fsFile } from \"@synstack/fs\";\n *\n * const relativeFile = fsFile(\"./relative/path.txt\");\n * const existingFile = fsFile(fsFile(\"/path/to/existing.txt\"));\n * ```\n */\n public static from(this: void, arg: FsFile | AnyPath) {\n if (arg instanceof FsFile) return arg;\n return new FsFile<\"utf-8\", undefined>(path.resolve(arg), \"utf-8\");\n }\n\n protected constructor(path: AnyPath, encoding?: TEncoding, schema?: TSchema) {\n super();\n this._path = path;\n this._encoding = encoding ?? (\"utf-8\" as TEncoding);\n this._schema = schema ?? undefined;\n }\n\n /**\n * Provide a validation schema for the file content. To be used with:\n * - `.read.json`\n * - `.write.json`\n * - `.read.yaml`\n * - `.write.yaml`\n */\n /**\n * Provide a validation schema for JSON/YAML operations.\n * The schema will be used to validate data when reading or writing JSON/YAML files.\n *\n * @typeParam NewSchema - The Zod schema type for validation\n * @param schema - A Zod schema to validate JSON/YAML data\n * @returns A new FsFile instance with the schema attached\n *\n * ```typescript\n * import { fsFile } from \"@synstack/fs\";\n * import { z } from \"zod\";\n *\n * const ConfigSchema = z.object({\n * port: z.number(),\n * host: z.string()\n * });\n *\n * const config = await fsFile(\"config.json\")\n * .schema(ConfigSchema)\n * .read.json();\n * // config is typed as { port: number, host: string }\n * ```\n */\n public schema<NewSchema extends ZodSchema>(schema: NewSchema) {\n return new FsFile<TEncoding, NewSchema>(this._path, this._encoding, schema);\n }\n\n /**\n * Get the path of the file.\n *\n * @returns The absolute path of the file\n */\n public valueOf(): AnyPath {\n return this._path;\n }\n\n /**\n * Get the current instance of the file.\n * Used for type compatibility with Pipeable.\n *\n * @returns The current FsFile instance\n */\n public instanceOf(): FsFile<TEncoding, TSchema> {\n return this;\n }\n\n // #region sub actions\n\n /**\n * Access the read operations for the file.\n * Provides methods for reading file contents in various formats.\n *\n * @returns An FsFileRead instance with methods for reading the file\n *\n * ```typescript\n * const content = await fsFile(\"data.txt\").read.text();\n * const json = await fsFile(\"config.json\").read.json();\n * const yaml = await fsFile(\"config.yml\").read.yaml();\n * ```\n */\n public get read() {\n return new FsFileRead<TEncoding, TSchema>(\n this._path,\n this._encoding,\n this._schema,\n );\n }\n\n /**\n * Access the write operations for the file.\n * Provides methods for writing content to the file in various formats.\n *\n * @returns An FsFileWrite instance with methods for writing to the file\n *\n * ```typescript\n * await fsFile(\"data.txt\").write.text(\"Hello\");\n * await fsFile(\"config.json\").write.json({ hello: \"world\" });\n * await fsFile(\"config.yml\").write.yaml({ config: true });\n * ```\n */\n public get write() {\n return new FsFileWrite<TEncoding, TSchema>(\n this._path,\n this._encoding,\n \"overwrite\",\n this._schema,\n );\n }\n\n // #endregion\n\n // #region sync\n\n /**\n * Get the absolute path of the file.\n *\n * @returns The absolute path as a string\n */\n public get path() {\n return this._path;\n }\n\n /**\n * Get the absolute path of the directory containing the file.\n *\n * @returns The directory path as a string\n */\n public dirPath() {\n return path.dirname(this._path);\n }\n\n /**\n * Get an FsDir instance representing the directory containing the file.\n *\n * @returns An FsDir instance for the parent directory\n *\n * ```typescript\n * const file = fsFile(\"/path/to/file.txt\");\n * const parentDir = file.dir(); // FsDir for \"/path/to\"\n * ```\n */\n public dir() {\n return FsDir.cwd(this.dirPath());\n }\n\n /**\n * Get the name of the file including its extension.\n *\n * @returns The file name with extension\n *\n * ```typescript\n * const file = fsFile(\"/path/to/document.txt\");\n * console.log(file.fileName()); // \"document.txt\"\n * ```\n */\n public fileName() {\n return path.filename(this._path);\n }\n\n /**\n * Get the extension of the file.\n *\n * @returns The file extension including the dot (e.g., \".txt\")\n *\n * ```typescript\n * const file = fsFile(\"/path/to/document.txt\");\n * console.log(file.fileExtension()); // \".txt\"\n * ```\n */\n public fileExtension() {\n return path.fileExtension(this._path);\n }\n\n /**\n * Get the name of the file without its extension.\n *\n * @returns The file name without extension\n *\n * ```typescript\n * const file = fsFile(\"/path/to/document.txt\");\n * console.log(file.fileNameWithoutExtension()); // \"document\"\n * ```\n */\n public fileNameWithoutExtension() {\n return path.filenameWithoutExtension(this._path);\n }\n\n /**\n * Get the MIME type of the file based on its extension.\n *\n * @returns The MIME type string or null if it cannot be determined\n *\n * ```typescript\n * const file = fsFile(\"/path/to/image.png\");\n * console.log(file.mimeType()); // \"image/png\"\n * ```\n */\n public mimeType() {\n return path.mimeType(this._path);\n }\n\n /**\n * Create a new FsFile instance with a path relative to this file's directory.\n *\n * @param relativePath - The relative path from this file's directory\n * @returns A new FsFile instance for the target path\n *\n * ```typescript\n * import { fsFile } from \"@synstack/fs\";\n *\n * const sourceFile = fsFile(\"/path/to/source.txt\");\n * const targetFile = sourceFile.toFile(\"../output/target.txt\");\n * // targetFile.path === \"/path/output/target.txt\"\n * ```\n */\n public toFile(relativePath: string) {\n const newPath = path.resolve(this.dirPath(), relativePath);\n return new FsFile(newPath);\n }\n\n /**\n * Create a new FsDir instance with a path relative to this file's directory.\n *\n * @param relativePath - The relative path from this file's directory\n * @returns A new FsDir instance for the target directory\n *\n * ```typescript\n * import { fsFile } from \"@synstack/fs\";\n *\n * const sourceFile = fsFile(\"/path/to/source.txt\");\n * const outputDir = sourceFile.toDir(\"../output\");\n * // outputDir.path === \"/path/output\"\n * ```\n */\n public toDir(relativePath: string) {\n const newPath = path.join(this.dirPath(), relativePath);\n return FsDir.cwd(newPath);\n }\n\n /**\n * Get the relative path from another file to this file\n *\n * ```ts\n * import { fsFile } from \"@synstack/fs\";\n *\n * const file1 = fsFile(\"/path/to/file1.txt\");\n * const file2 = fsFile(\"/path/to-other/file2.txt\");\n *\n * console.log(file1.relativePathFrom(file2)); // ../to/file1.txt\n * ```\n */\n public relativePathFrom(dirOrFile: FsDir | FsFile): string {\n if (dirOrFile instanceof FsFile)\n return this.relativePathFrom(dirOrFile.dir());\n return path.relative(dirOrFile.path, this.path);\n }\n\n /**\n * Get the relative path to go from this file to another\n *\n * ```ts\n * import { fsFile } from \"@synstack/fs\";\n *\n * const file1 = fsFile(\"/path/to/file1.txt\");\n * const file2 = fsFile(\"/path/to-other/file2.txt\");\n *\n * console.log(file1.relativePathTo(file2)); // ../to-other/file2.txt\n * ```\n */\n public relativePathTo(dirOrFileOrPath: FsDir | FsFile): string {\n return path.relative(this.dirPath(), dirOrFileOrPath.path);\n }\n\n /**\n * Check if the file is located within the specified directory.\n *\n * @param dirOrPath - The directory or path to check against\n * @returns True if the file is in the directory, false otherwise\n *\n * ```typescript\n * import { fsFile, fsDir } from \"@synstack/fs\";\n *\n * const sourceFile = fsFile(\"/path/to/file.txt\");\n * console.log(sourceFile.isInDir(fsDir(\"/path\"))); // true\n * console.log(sourceFile.isInDir(fsDir(\"/other\"))); // false\n * ```\n */\n public isInDir(dirOrPath: AnyPath | FsDir) {\n return path.isInPath(dirOrPath.valueOf(), this._path.valueOf());\n }\n\n /**\n * Delete the file from the file system.\n * If the file doesn't exist, the operation is silently ignored.\n *\n * @returns A promise that resolves when the file is deleted\n *\n * ```typescript\n * import { fsFile } from \"@synstack/fs\";\n *\n * const tempFile = fsFile(\"./temp.txt\");\n * await tempFile.write.text(\"temporary content\");\n * await tempFile.remove(); // File is deleted\n * ```\n */\n public async remove(): Promise<void> {\n await fs.rm(this._path, { recursive: true }).catch((e) => {\n if (e.code === \"ENOENT\") return;\n throw e;\n });\n }\n\n /**\n * @deprecated Use {@link remove} instead.\n */\n public rm(): Promise<void> {\n return this.remove();\n }\n\n /**\n * Delete the file from the file system synchronously.\n * If the file doesn't exist, the operation is silently ignored.\n *\n * @synchronous\n *\n * ```typescript\n * import { fsFile } from \"@synstack/fs\";\n *\n * const tempFile = fsFile(\"./temp.txt\");\n * tempFile.write.textSync(\"temporary content\");\n * tempFile.removeSync(); // File is deleted immediately\n * ```\n */\n public removeSync(): void {\n try {\n fsSync.rmSync(this._path, { recursive: false });\n } catch (error: any) {\n if (error.code === \"ENOENT\") return;\n throw error;\n }\n }\n\n /**\n * @deprecated Use {@link removeSync} instead.\n */\n public rmSync(): void {\n this.removeSync();\n }\n\n /**\n * Move the file to a new location.\n *\n * @param newPath - The new path for the file or an existing FsFile instance\n * @returns A promise that resolves the new file\n */\n public async move(newPath: FsFile | AnyPath): Promise<FsFile> {\n const newFile = FsFile.from(newPath);\n await fs.rename(this._path, newFile.path);\n return newFile;\n }\n\n /**\n * Move the file to a new location synchronously.\n *\n * @param newPath - The new path for the file or an existing FsFile instance\n * @returns The new file\n */\n public moveSync(newPath: FsFile | AnyPath): FsFile {\n const newFile = FsFile.from(newPath);\n fsSync.renameSync(this._path, newFile.path);\n return newFile;\n }\n\n /**\n * Check if the file exists in the file system.\n *\n * @returns A promise that resolves to true if the file exists, false otherwise\n *\n * ```typescript\n * import { fsFile } from \"@synstack/fs\";\n *\n * const configFile = fsFile(\"./config.json\");\n * if (await configFile.exists()) {\n * const config = await configFile.read.json();\n * }\n * ```\n */\n public async exists(): Promise<boolean> {\n return fs\n .access(this._path, fs.constants.F_OK)\n .then(() => true)\n .catch(() => false);\n }\n\n /**\n * Check if the file exists in the file system synchronously.\n *\n * @synchronous\n * @returns True if the file exists, false otherwise\n *\n * ```typescript\n * import { fsFile } from \"@synstack/fs\";\n *\n * const configFile = fsFile(\"./config.json\");\n * if (configFile.existsSync()) {\n * const config = configFile.read.jsonSync();\n * }\n * ```\n */\n public existsSync(): boolean {\n try {\n fsSync.accessSync(this._path);\n return true;\n } catch (error: any) {\n if (error.code === \"ENOENT\") return false;\n throw error;\n }\n }\n\n /**\n * Get the creation date of the file.\n *\n * @returns A promise that resolves to the file's creation date\n * @throws If the file doesn't exist or cannot be accessed\n *\n * ```typescript\n * import { fsFile } from \"@synstack/fs\";\n *\n * const sourceFile = fsFile(\"./source.txt\");\n * const created = await sourceFile.creationDate();\n * console.log(`File created on: ${created.toISOString()}`);\n * ```\n */\n public async creationDate(): Promise<Date> {\n const fileStats = await fs.stat(this._path);\n return fileStats.birthtime;\n }\n\n /**\n * Get the creation date of the file synchronously.\n *\n * @synchronous\n * @returns The file's creation date\n * @throws If the file doesn't exist or cannot be accessed\n *\n * ```typescript\n * import { fsFile } from \"@synstack/fs\";\n *\n * const sourceFile = fsFile(\"./source.txt\");\n * const created = sourceFile.creationDateSync();\n * console.log(`File created on: ${created.toISOString()}`);\n * ```\n */\n public creationDateSync(): Date {\n const stats = fsSync.statSync(this._path);\n return stats.birthtime;\n }\n\n /**\n * Check if the file path matches any of the provided glob patterns.\n *\n * @param globs - One or more glob patterns to match against, either as separate arguments or an array\n * @returns True if the file matches any pattern, false otherwise\n *\n * ```typescript\n * import { fsFile } from \"@synstack/fs\";\n *\n * const sourceFile = fsFile(\"./src/components/Button.tsx\");\n * console.log(sourceFile.matchesGlobs(\"**\\/*.tsx\")); // true\n * console.log(sourceFile.matchesGlobs([\"*.css\", \"*.html\"])); // false\n * console.log(sourceFile.matchesGlobs(\"**\\/*.ts\", \"**\\/*.tsx\")); // true\n * ```\n */\n public matchesGlobs(...globs: Array<string> | [Array<string>]) {\n return glob.matches(this._path, ...globs);\n }\n\n /**\n * Capture parts of the file path using a glob pattern\n *\n * ```ts\n * import { fsFile } from \"@synstack/fs\";\n *\n * const myFile = fsFile(\"/my-domain/my-sub-domain/features/feature-name.controller.ts\");\n * const res = myFile.globCapture(\"/(*)/(*)/features/(*).controller.ts\");\n * if (!res) throw new Error(\"File doesn't match glob pattern\");\n * console.log(res[1]); // my-domain\n * console.log(res[2]); // my-sub-domain\n * console.log(res[3]); // feature-name.controller.ts\n * ```\n */\n public globCapture(globPattern: string) {\n return glob.capture(globPattern, this._path);\n }\n}\n\nclass FsFileRead<\n ENCODING extends TextEncoding = \"utf-8\",\n SCHEMA extends ZodSchema | undefined = undefined,\n> {\n private readonly _path: AnyPath;\n private readonly _encoding: ENCODING;\n private readonly _schema?: SCHEMA;\n\n public constructor(path: AnyPath, encoding: ENCODING, schema?: SCHEMA) {\n this._path = path;\n this._encoding = encoding;\n this._schema = schema;\n }\n\n public get path() {\n return this._path;\n }\n\n // #region sync\n\n /**\n * Read the file contents as a string.\n *\n * @returns A promise that resolves to the file contents as a string\n * @throws If the file doesn't exist or cannot be read\n *\n * ```typescript\n * const content = await fsFile(\"data.txt\").read.text();\n * console.log(content); // \"Hello, World!\"\n * ```\n */\n public async text() {\n return fs.readFile(this._path, this._encoding);\n }\n\n /**\n * Read the file contents as a string synchronously.\n *\n * @synchronous\n * @returns The file contents as a string\n * @throws If the file doesn't exist or cannot be read\n *\n * ```typescript\n * const content = fsFile(\"data.txt\").read.textSync();\n * console.log(content); // \"Hello, World!\"\n * ```\n */\n public textSync() {\n return fsSync.readFileSync(this._path, this._encoding);\n }\n\n /**\n * Read the file contents and return a chainable string instance.\n * Used for further manipulation of the content using @synstack/str methods.\n *\n * @returns A promise that resolves to a chainable string instance\n * @throws If the file doesn't exist or cannot be read\n *\n * ```typescript\n * const content = await fsFile(\"data.txt\").read.str();\n * const lines = content\n * .split(\"\\n\")\n * .filter((line) => line.trim().length > 0);\n * ```\n */\n public async str() {\n return this.text().then(str);\n }\n\n /**\n * Read the file contents and return a chainable string instance synchronously.\n * Used for further manipulation of the content using @synstack/str methods.\n *\n * @synchronous\n * @returns A chainable string instance\n * @throws If the file doesn't exist or cannot be read\n *\n * ```typescript\n * const content = fsFile(\"data.txt\").read.strSync();\n * const lines = content\n * .split(\"\\n\")\n * .filter((line) => line.trim().length > 0);\n * ```\n */\n public strSync() {\n return str(this.textSync());\n }\n\n /**\n * Read and parse the file contents as JSON.\n * If a schema is provided, the parsed data will be validated against it.\n *\n * @returns A promise that resolves to the parsed JSON data\n * @throws If the file doesn't exist, cannot be read, or contains invalid JSON\n * @throws If schema validation fails when a schema is provided\n *\n * ```typescript\n * interface Config {\n * port: number;\n * host: string;\n * }\n *\n * const config = await fsFile(\"config.json\")\n * .schema(ConfigSchema)\n * .read.json();\n * // config is automatically typed as the schema's output type\n * ```\n */\n public json<\n OUT = SCHEMA extends ZodSchema<infer O> ? O : unknown,\n >(): Promise<OUT> {\n return this.text().then((t) =>\n json.deserialize(t, { schema: this._schema }),\n );\n }\n\n /**\n * Read and parse the file contents as JSON synchronously.\n * If a schema is provided, the parsed data will be validated against it.\n *\n * @synchronous\n * @returns The parsed JSON data\n * @throws If the file doesn't exist, cannot be read, or contains invalid JSON\n * @throws If schema validation fails when a schema is provided\n *\n * ```typescript\n * const config = fsFile(\"config.json\")\n * .schema(ConfigSchema)\n * .read.jsonSync();\n * // config is automatically typed as the schema's output type\n * ```\n */\n public jsonSync<\n OUT = SCHEMA extends ZodSchema<infer O> ? O : unknown,\n >(): OUT {\n return json.deserialize(this.textSync(), {\n schema: this._schema,\n });\n }\n\n /**\n * Read and parse the file contents as YAML.\n * If a schema is provided, the parsed data will be validated against it.\n *\n * @typeParam T - The type of the parsed YAML data\n * @returns A promise that resolves to the parsed YAML data\n * @throws If the file doesn't exist, cannot be read, or contains invalid YAML\n * @throws If schema validation fails when a schema is provided\n *\n * ```typescript\n * interface Config {\n * environment: string;\n * settings: Record<string, unknown>;\n * }\n *\n * const config = await fsFile(\"config.yml\")\n * .schema(ConfigSchema)\n * .read.yaml();\n * // config is automatically typed as the schema's output type\n * ```\n */\n public yaml<\n OUT = SCHEMA extends ZodSchema<infer O> ? O : unknown,\n >(): Promise<OUT> {\n return this.text().then((t) =>\n yaml.deserialize(t, { schema: this._schema }),\n );\n }\n\n /**\n * Read and parse the file contents as YAML synchronously.\n * If a schema is provided, the parsed data will be validated against it.\n *\n * @typeParam T - The type of the parsed YAML data\n * @synchronous\n * @returns The parsed YAML data\n * @throws If the file doesn't exist, cannot be read, or contains invalid YAML\n * @throws If schema validation fails when a schema is provided\n *\n * ```typescript\n * const config = fsFile(\"config.yml\")\n * .schema(ConfigSchema)\n * .read.yamlSync();\n * // config is automatically typed as the schema's output type\n * ```\n */\n public yamlSync<\n OUT = SCHEMA extends ZodSchema<infer O> ? O : unknown,\n >(): OUT {\n return yaml.deserialize(this.textSync(), {\n schema: this._schema,\n });\n }\n\n /**\n * Read and parse the file contents as XML using @synstack/xml.\n * This parser is specifically designed for LLM-related XML processing.\n *\n * @typeParam T - The type of the parsed XML nodes array, must extend Array<Xml.Node>\n * @returns A promise that resolves to the parsed XML nodes\n * @throws If the file doesn't exist, cannot be read, or contains invalid XML\n * @see {@link https://github.com/pAIrprogio/synscript/tree/main/packages/xml|@synstack/xml documentation}\n *\n * ```typescript\n * interface XmlNode {\n * tag: string;\n * attributes: Record<string, string>;\n * children: Array<XmlNode>;\n * }\n *\n * const nodes = await fsFile(\"data.xml\").read.xml<XmlNode[]>();\n * console.log(nodes[0].tag); // \"root\"\n * console.log(nodes[0].attributes.id); // \"main\"\n * ```\n *\n * @remarks\n * - Uses a non-spec-compliant XML parser tailored for LLM use cases\n * - Optimized for simple XML structures commonly used in LLM responses\n * - Does not support all XML features (see documentation for details)\n */\n public async xml<T extends Array<Xml.Node>>(): Promise<T> {\n return this.text().then((content) => xml.parse<T>(content));\n }\n\n /**\n * Read and parse the file contents as XML synchronously using @synstack/xml.\n * This parser is specifically designed for LLM-related XML processing.\n *\n * @typeParam T - The type of the parsed XML nodes array, must extend Array<Xml.Node>\n * @synchronous\n * @returns The parsed XML nodes\n * @throws If the file doesn't exist, cannot be read, or contains invalid XML\n * @see {@link https://github.com/pAIrprogio/synscript/tree/main/packages/xml|@synstack/xml documentation}\n *\n * ```typescript\n * const nodes = fsFile(\"data.xml\").read.xmlSync<XmlNode[]>();\n * console.log(nodes[0].tag); // \"root\"\n * console.log(nodes[0].attributes.id); // \"main\"\n * ```\n *\n * @remarks\n * - Uses a non-spec-compliant XML parser tailored for LLM use cases\n * - Optimized for simple XML structures commonly used in LLM responses\n * - Does not support all XML features (see documentation for details)\n */\n public xmlSync<T extends Array<Xml.Node>>(): T {\n return xml.parse<T>(this.textSync());\n }\n\n /**\n * Read the file contents and encode them as a base64 string.\n * Useful for handling binary data or preparing content for data URLs.\n *\n * @returns A promise that resolves to the file contents as a base64-encoded string\n * @throws If the file doesn't exist or cannot be read\n *\n * ```typescript\n * // Read and encode an image file\n * const imageBase64 = await fsFile(\"image.png\").read.base64();\n * console.log(imageBase64); // \"iVBORw0KGgoAAAANSUhEUgAA...\"\n *\n * // Create a data URL for use in HTML/CSS\n * const dataUrl = `data:image/png;base64,${imageBase64}`;\n * ```\n */\n public async base64(): Promise<string> {\n return fs.readFile(this._path, \"base64\");\n }\n\n /**\n * Read the file contents and encode them as a base64 string synchronously.\n * Useful for handling binary data or preparing content for data URLs.\n *\n * @synchronous\n * @returns The file contents as a base64-encoded string\n * @throws If the file doesn't exist or cannot be read\n *\n * ```typescript\n * const imageBase64 = fsFile(\"image.png\").read.base64Sync();\n * const dataUrl = `data:image/png;base64,${imageBase64}`;\n * ```\n */\n public base64Sync(): string {\n return fsSync.readFileSync(this._path, \"base64\");\n }\n\n /**\n * Read the file contents and create a synstack-compatible Base64Data object.\n * This format includes MIME type information along with the base64-encoded data.\n *\n * @param defaultMimeType - The MIME type to use if it cannot be determined from the file extension\n * @returns A promise that resolves to a Base64Data object containing the encoded content and MIME type\n * @throws If the file doesn't exist or cannot be read\n *\n * ```typescript\n * // Read an image with automatic MIME type detection\n * const imageData = await fsFile(\"image.png\").read.base64Data();\n * console.log(imageData);\n * // {\n * // type: \"base64\",\n * // data: \"iVBORw0KGgoAAAANSUhEUgAA...\",\n * // mimeType: \"image/png\"\n * // }\n *\n * // Specify a custom MIME type for a binary file\n * const data = await fsFile(\"custom.bin\")\n * .read.base64Data(\"application/custom\");\n * ```\n */\n public async base64Data(\n defaultMimeType: string = \"application/octet-stream\",\n ): Promise<Base64Data> {\n return {\n type: \"base64\",\n data: await this.base64(),\n mimeType: defaultMimeType,\n } satisfies Base64Data;\n }\n\n /**\n * Read the file contents and create a synstack-compatible Base64Data object synchronously.\n * This format includes MIME type information along with the base64-encoded data.\n *\n * @param defaultMimeType - The MIME type to use if it cannot be determined from the file extension\n * @synchronous\n * @returns A Base64Data object containing the encoded content and MIME type\n * @throws If the file doesn't exist or cannot be read\n *\n * ```typescript\n * const imageData = fsFile(\"image.png\").read.base64DataSync();\n * console.log(imageData);\n * // {\n * // type: \"base64\",\n * // data: \"iVBORw0KGgoAAAANSUhEUgAA...\",\n * // mimeType: \"image/png\"\n * // }\n * ```\n */\n public base64DataSync(\n defaultMimeType: string = \"application/octet-stream\",\n ): Base64Data {\n return {\n type: \"base64\",\n data: this.base64Sync(),\n mimeType: defaultMimeType,\n } satisfies Base64Data;\n }\n\n /**\n * Read the file contents and parse them as a markdown document.\n * If a schema is provided, the header data will be validated before returning.\n *\n * @returns A promise that resolves to the markdown document\n * @throws If the file doesn't exist, cannot be read, or contains invalid markdown\n * @throws If schema validation fails when a schema is provided\n */\n public md<DATA_SHAPE = SCHEMA extends ZodSchema<infer O> ? O : unknown>() {\n return this.text().then((t) =>\n MdDoc.withOptions<DATA_SHAPE>({\n schema: this._schema,\n }).fromString(t),\n );\n }\n\n /**\n * Read the file contents and parse them as a markdown document synchronously.\n * If a schema is provided, the header data will be validated before returning.\n *\n * @synchronous\n * @returns The markdown document\n * @throws If the file doesn't exist, cannot be read, or contains invalid markdown\n * @throws If schema validation fails when a schema is provided\n */\n public mdSync<\n DATA_SHAPE = SCHEMA extends ZodSchema<infer O> ? O : unknown,\n >() {\n return MdDoc.withOptions<DATA_SHAPE>({\n schema: this._schema,\n }).fromString(this.textSync());\n }\n}\n\n// Todo: Passing absolute paths will break the cache, find a way to fix this\nclass FsFileWrite<\n TEncoding extends TextEncoding,\n TSchema extends ZodSchema | undefined = undefined,\n> {\n private readonly _path: AnyPath;\n private readonly _encoding: TEncoding;\n private readonly _schema?: TSchema;\n private readonly _mode: WriteMode;\n\n public constructor(\n path: AnyPath,\n encoding: TEncoding,\n mode: WriteMode = \"overwrite\",\n schema?: TSchema,\n ) {\n this._path = path;\n this._encoding = encoding;\n this._schema = schema;\n this._mode = mode;\n }\n\n /**\n * Set the write mode of the file\n * @argument preserve: If the file already exists, it will be left untouched\n * @argument overwrite: If the file already exists, it will be overwritten\n */\n public mode<NewWriteMode extends WriteMode>(writeMode: NewWriteMode) {\n return new FsFileWrite(this._path, this._encoding, writeMode, this._schema);\n }\n\n /**\n * Write text content to a file asynchronously.\n * Creates parent directories if they don't exist.\n * Respects the write mode (overwrite/preserve) setting.\n *\n * @param content - The content to write, will be converted to string using toString()\n * @returns A promise that resolves when the write operation is complete\n * @throws If the write operation fails or if parent directory creation fails\n */\n public async text(content: Stringable): Promise<void> {\n if (this._mode === \"preserve\" && (await FsFile.from(this._path).exists()))\n return;\n const dirname = path.dirname(this._path);\n await fs.mkdir(dirname, { recursive: true });\n await fs.writeFile(this._path, content.toString(), this._encoding);\n }\n\n /**\n * Write text content to a file synchronously.\n * Creates parent directories if they don't exist.\n * Respects the write mode (overwrite/preserve) setting.\n *\n * @param content - The content to write, will be converted to string using toString()\n * @synchronous\n * @throws If the write operation fails or if parent directory creation fails\n */\n public textSync(content: Stringable): void {\n if (this._mode === \"preserve\" && FsFile.from(this._path).existsSync())\n return;\n const dirname = path.dirname(this._path);\n fsSync.mkdirSync(dirname, { recursive: true });\n fsSync.writeFileSync(this._path, content.toString(), this._encoding);\n }\n\n /**\n * Write data as JSON to a file asynchronously.\n * The data will be serialized using JSON.stringify.\n * If a schema is provided, the data will be validated before writing.\n *\n * @typeParam T - The type of data being written\n * @param data - The data to write, must be JSON-serializable\n * @returns A promise that resolves when the write operation is complete\n * @throws If schema validation fails or if the write operation fails\n */\n public async json<T>(\n data: TSchema extends ZodSchema<infer O> ? O : T,\n ): Promise<void> {\n return this.text(json.serialize(data, { schema: this._schema }));\n }\n\n // Todo: add mergeJson\n\n /**\n * Write data as formatted JSON to a file asynchronously.\n * The data will be serialized using JSON.stringify with pretty printing.\n * If a schema is provided, the data will be validated before writing.\n *\n * @typeParam T - The type of data being written\n * @param data - The data to write, must be JSON-serializable\n * @returns A promise that resolves when the write operation is complete\n * @throws If schema validation fails or if the write operation fails\n */\n public async prettyJson<T>(\n data: TSchema extends ZodSchema<infer O> ? O : T,\n ): Promise<void> {\n return this.text(\n json.serialize(data, { sc