Skip to content

File and directory validation combinators #52

Description

@aryasaatvik

Problem

Users who need file/directory path validation must write custom z.string().refine() logic for existence checks and extension filtering. This is boilerplate that every CLI with file inputs repeats.

Solution

Provide composable file() and directory() wrappers that add filesystem validation to any Standard Schema:

import { defineOption, file, directory } from '@bunli/core'

config: defineOption(
  file(z.string(), {
    mustExist: true,
    extensions: ['.json', '.yaml']
  })
)

outDir: defineOption(
  directory(z.string(), { mustExist: false })
)

Design

Create packages/core/src/fs-validators.ts:

interface FileOptions {
  mustExist?: boolean       // default: false
  extensions?: string[]     // e.g. ['.ts', '.js']
}

interface DirectoryOptions {
  mustExist?: boolean       // default: false
}

function file<S extends StandardSchemaV1>(schema: S, options?: FileOptions): StandardSchemaV1
function directory<S extends StandardSchemaV1>(schema: S, options?: DirectoryOptions): StandardSchemaV1

Implementation wraps the inner schema by creating a Standard Schema V1-conforming object:

  1. Validate raw value with inner schema → get string path
  2. If mustExist: check fs.statSync(path) — verify it's a file (not directory) or vice versa
  3. If extensions: check path.extname() against allowed list (case-insensitive)
  4. Return validated path or issues

The wrapper implements { '~standard': { version: 1, vendor: 'bunli', validate(value) { ... } } }.

Edge cases

  • Relative paths: resolve relative to process.cwd()
  • Symlinks: fs.statSync follows symlinks (correct default behavior)
  • Extension case: compare lowercase (macOS/Windows are case-insensitive)
  • mustExist: false: only structural validation, no filesystem access
  • file() + directory path: stat.isFile() must be true → error if directory
  • directory() + file path: stat.isDirectory() must be true → error if file
  • Async compatibility: validate function must work with Standard Schema V1's sync/async contract

Files

  • New: packages/core/src/fs-validators.ts
  • packages/core/src/index.ts — export file and directory

Testing

  • Temp directories with real files
  • Valid file passes, nonexistent with mustExist fails
  • Wrong extension fails
  • Directory passed to file() fails (and vice versa)
  • Composability: file(z.string().min(1), { mustExist: true, extensions: ['.json'] })

Independent

No dependencies on other issues in this milestone.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions