tools/core/iterators/file.iterator.ts

tools/core/iterators/file.iterator.ts is a file in Coordination Surface. 79 lines of code and 21 definitions.

import { join, relative, sep } from "node:path";
import { readFileSync, readdirSync, statSync } from "node:fs";
import type { WalkOptions } from "../types/file.types.ts";
import { isFilesystemRefusal } from "../predicates/file.predicate.ts";

type EntryKind = "directory" | "file" | "other";

interface Listed {
    readonly entry: string;
    readonly full: string;
    readonly kind: EntryKind;
}

interface Listing {
    readonly directories: readonly string[];
    readonly files: readonly string[];
}

const endsWithAny = function endsWithAny(name: string, suffixes: readonly string[]): boolean {
    return suffixes.some((suffix) => name.length > suffix.length && name.endsWith(suffix));
};

const kept = function kept(entry: string, options: WalkOptions): boolean {
    const excluded = options.excluded !== undefined && endsWithAny(entry, options.excluded);
    return !excluded && (options.extensions.length === 0 || endsWithAny(entry, options.extensions));
};

const entriesOf = function entriesOf(directory: string): string[] {
    try {
        return readdirSync(directory);
    } catch (error) {
        if (isFilesystemRefusal(error)) {
            return [];
        }
        throw error;
    }
};

const kindOf = function kindOf(full: string): EntryKind {
    try {
        const info = statSync(full);
        if (info.isDirectory()) {
            return "directory";
        }
        return info.isFile() ? "file" : "other";
    } catch (error) {
        if (isFilesystemRefusal(error)) {
            return "other";
        }
        throw error;
    }
};

const listingOf = function listingOf(directory: string, options: WalkOptions): Listing {
    const listed: Listed[] = entriesOf(directory)
        .filter((entry) => !options.ignored.includes(entry))
        .map((entry) => {
            const full = join(directory, entry);
            return { entry, full, kind: kindOf(full) };
        });

    return {
        directories: listed.filter((item) => item.kind === "directory").map((item) => item.full),
        files: listed.filter((item) => item.kind === "file" && kept(item.entry, options)).map((item) => item.full),
    };
};

export const walk = function walk(options: WalkOptions): string[] {
    const found: string[] = [];
    const stack: string[] = [options.root];
    let directory = stack.pop();

    while (directory !== undefined) {
        const listing = listingOf(directory, options);
        found.push(...listing.files);
        stack.push(...listing.directories);
        directory = stack.pop();
    }

    return found.toSorted((left, right) => left.localeCompare(right, "en"));
};

export const readSource = function readSource(path: string): string {
    return readFileSync(path, "utf8");
};

export const forwardSlashed = function forwardSlashed(text: string): string {
    return text.replaceAll("\\", "/");
};

export const toPosix = function toPosix(root: string, path: string): string {
    const rel = relative(root, path);
    return sep === "/" ? rel : forwardSlashed(rel);
};