From 38f061850705546a27a9fc135488388782bb6291 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Sat, 16 Aug 2025 18:06:48 +0500 Subject: [PATCH 1/3] refactor: move join directory path to its own file joinDirectoryPath is now responsible for building directory path instead of building the path in pushDirectory. --- src/api/functions/join-directory-path.ts | 51 ++++++++++++++ src/api/functions/join-path.ts | 10 +-- src/api/functions/push-directory.ts | 34 ++-------- src/api/functions/walk-directory.ts | 31 +++++---- src/api/walker.ts | 85 +++++++++++++++--------- 5 files changed, 129 insertions(+), 82 deletions(-) create mode 100644 src/api/functions/join-directory-path.ts diff --git a/src/api/functions/join-directory-path.ts b/src/api/functions/join-directory-path.ts new file mode 100644 index 00000000..affac11e --- /dev/null +++ b/src/api/functions/join-directory-path.ts @@ -0,0 +1,51 @@ +import { relative } from "path"; +import { Options, PathSeparator } from "../../types"; +import { convertSlashes } from "../../utils"; + +function joinDirectoryPathWithRelativePath(root: string, options: Options) { + return function ( + dirname: string, + parentPath: string, + separator: PathSeparator + ) { + const sameRoot = parentPath.startsWith(root); + if (sameRoot) + return joinDirectoryPath( + dirname, + parentPath.slice(root.length), + separator + ); + else + return joinDirectoryPath( + dirname, + convertSlashes(relative(root, parentPath), separator) + separator, + separator + ); + }; +} + +export function joinDirectoryPath( + dirname: string, + parentPath: string, + separator: PathSeparator +) { + if (!dirname) return parentPath || "."; + return parentPath + dirname + separator; +} + +export type JoinDirectoryPathFunction = ( + dirname: string, + parentPath: string, + separator: PathSeparator +) => string; + +export function build( + root: string, + options: Options +): JoinDirectoryPathFunction { + const { relativePaths } = options; + + return relativePaths && root + ? joinDirectoryPathWithRelativePath(root, options) + : joinDirectoryPath; +} diff --git a/src/api/functions/join-path.ts b/src/api/functions/join-path.ts index d103cb2c..2becbfe7 100644 --- a/src/api/functions/join-path.ts +++ b/src/api/functions/join-path.ts @@ -1,5 +1,5 @@ import { relative } from "path"; -import { Options, PathSeparator } from "../../types"; +import { Options } from "../../types"; import { convertSlashes } from "../../utils"; export function joinPathWithBasePath(filename: string, directoryPath: string) { @@ -23,14 +23,6 @@ function joinPath(filename: string) { return filename; } -export function joinDirectoryPath( - filename: string, - directoryPath: string, - separator: PathSeparator -) { - return directoryPath + filename + separator; -} - export type JoinPathFunction = ( filename: string, directoryPath: string diff --git a/src/api/functions/push-directory.ts b/src/api/functions/push-directory.ts index 2de954b8..c24b41c5 100644 --- a/src/api/functions/push-directory.ts +++ b/src/api/functions/push-directory.ts @@ -6,25 +6,8 @@ export type PushDirectoryFunction = ( filters?: FilterPredicate[] ) => void; -function pushDirectoryWithRelativePath(root: string): PushDirectoryFunction { - return function (directoryPath, paths) { - paths.push(directoryPath.substring(root.length) || "."); - }; -} - -function pushDirectoryFilterWithRelativePath( - root: string -): PushDirectoryFunction { - return function (directoryPath, paths, filters) { - const relativePath = directoryPath.substring(root.length) || "."; - if (filters!.every((filter) => filter(relativePath, true))) { - paths.push(relativePath); - } - }; -} - const pushDirectory: PushDirectoryFunction = (directoryPath, paths) => { - paths.push(directoryPath || "."); + paths.push(directoryPath); }; const pushDirectoryFilter: PushDirectoryFunction = ( @@ -32,21 +15,14 @@ const pushDirectoryFilter: PushDirectoryFunction = ( paths, filters ) => { - const path = directoryPath || "."; - if (filters!.every((filter) => filter(path, true))) { - paths.push(path); - } + if (filters!.every((filter) => filter(directoryPath, true))) + paths.push(directoryPath); }; const empty: PushDirectoryFunction = () => {}; -export function build(root: string, options: Options): PushDirectoryFunction { - const { includeDirs, filters, relativePaths } = options; +export function build(options: Options): PushDirectoryFunction { + const { includeDirs, filters } = options; if (!includeDirs) return empty; - - if (relativePaths) - return filters && filters.length - ? pushDirectoryFilterWithRelativePath(root) - : pushDirectoryWithRelativePath(root); return filters && filters.length ? pushDirectoryFilter : pushDirectory; } diff --git a/src/api/functions/walk-directory.ts b/src/api/functions/walk-directory.ts index f2b9d368..5b56b4dd 100644 --- a/src/api/functions/walk-directory.ts +++ b/src/api/functions/walk-directory.ts @@ -3,18 +3,23 @@ import type { Dirent } from "fs"; export type WalkDirectoryFunction = ( state: WalkerState, - crawlPath: string, - directoryPath: string, + fullPath: string, + relativePath: string, depth: number, - callback: (entries: Dirent[], directoryPath: string, depth: number) => void + callback: ( + entries: Dirent[], + fullPath: string, + relativePath: string, + depth: number + ) => void ) => void; const readdirOpts = { withFileTypes: true } as const; const walkAsync: WalkDirectoryFunction = ( state, - crawlPath, - directoryPath, + fullPath, + relativePath, currentDepth, callback ) => { @@ -24,13 +29,13 @@ const walkAsync: WalkDirectoryFunction = ( const { fs } = state; - state.visited.push(crawlPath); + state.visited.push(fullPath); state.counts.directories++; // Perf: Node >= 10 introduced withFileTypes that helps us // skip an extra fs.stat call. - fs.readdir(crawlPath || ".", readdirOpts, (error, entries = []) => { - callback(entries, directoryPath, currentDepth); + fs.readdir(fullPath || ".", readdirOpts, (error, entries = []) => { + callback(entries, fullPath, relativePath, currentDepth); state.queue.dequeue(state.options.suppressErrors ? null : error, state); }); @@ -38,23 +43,23 @@ const walkAsync: WalkDirectoryFunction = ( const walkSync: WalkDirectoryFunction = ( state, - crawlPath, - directoryPath, + fullPath, + relativePath, currentDepth, callback ) => { const { fs } = state; if (currentDepth < 0) return; - state.visited.push(crawlPath); + state.visited.push(fullPath); state.counts.directories++; let entries: Dirent[] = []; try { - entries = fs.readdirSync(crawlPath || ".", readdirOpts); + entries = fs.readdirSync(fullPath || ".", readdirOpts); } catch (e) { if (!state.options.suppressErrors) throw e; } - callback(entries, directoryPath, currentDepth); + callback(entries, fullPath, relativePath, currentDepth); }; export function build(isSynchronous: boolean): WalkDirectoryFunction { diff --git a/src/api/walker.ts b/src/api/walker.ts index e6edc0dc..a6d54b6e 100644 --- a/src/api/walker.ts +++ b/src/api/walker.ts @@ -1,7 +1,8 @@ -import { basename, dirname } from "path"; -import { isRootDirectory, normalizePath } from "../utils"; +import { basename, dirname, sep } from "path"; +import { convertSlashes, isRootDirectory, normalizePath } from "../utils"; import { ResultCallback, WalkerState, Options } from "../types"; import * as joinPath from "./functions/join-path"; +import * as joinDirectoryPath from "./functions/join-directory-path"; import * as pushDirectory from "./functions/push-directory"; import * as pushFile from "./functions/push-file"; import * as getArray from "./functions/get-array"; @@ -20,6 +21,7 @@ export class Walker { private readonly root: string; private readonly isSynchronous: boolean; private readonly state: WalkerState; + private readonly joinDirectoryPath: joinDirectoryPath.JoinDirectoryPathFunction; private readonly joinPath: joinPath.JoinPathFunction; private readonly pushDirectory: pushDirectory.PushDirectoryFunction; private readonly pushFile: pushFile.PushFileFunction; @@ -59,8 +61,9 @@ export class Walker { * performance boost. Since these functions are so small, they are automatically inlined * by the javascript engine so there's no function call overhead (in most cases). */ + this.joinDirectoryPath = joinDirectoryPath.build(this.root, options); this.joinPath = joinPath.build(this.root, options); - this.pushDirectory = pushDirectory.build(this.root, options); + this.pushDirectory = pushDirectory.build(options); this.pushFile = pushFile.build(options); this.getArray = getArray.build(options); this.groupFiles = groupFiles.build(options); @@ -69,18 +72,32 @@ export class Walker { } start(): TOutput | null { - this.pushDirectory(this.root, this.state.paths, this.state.options.filters); + const relativePath = this.joinDirectoryPath( + "", + this.root, + this.state.options.pathSeparator + ); + this.pushDirectory( + relativePath, + this.state.paths, + this.state.options.filters + ); this.walkDirectory( this.state, - this.root, - this.root, + convertSlashes(this.root, sep), + relativePath, this.state.options.maxDepth, this.walk ); return this.isSynchronous ? this.callbackInvoker(this.state, null) : null; } - private walk = (entries: Dirent[], directoryPath: string, depth: number) => { + private walk = ( + entries: Dirent[], + fullPath: string, + relativePath: string, + depth: number + ) => { const { paths, options: { @@ -111,52 +128,58 @@ export class Walker { entry.isFile() || (entry.isSymbolicLink() && !resolveSymlinks && !excludeSymlinks) ) { - const filename = this.joinPath(entry.name, directoryPath); + const filename = this.joinPath(entry.name, fullPath); this.pushFile(filename, files, this.state.counts, filters); } else if (entry.isDirectory()) { - let path = joinPath.joinDirectoryPath( + const relativePath = this.joinDirectoryPath( entry.name, - directoryPath, + fullPath, this.state.options.pathSeparator ); - if (exclude && exclude(entry.name, path)) continue; - this.pushDirectory(path, paths, filters); - this.walkDirectory(this.state, path, path, depth - 1, this.walk); + if (exclude && exclude(entry.name, relativePath)) continue; + this.pushDirectory(relativePath, paths, filters); + this.walkDirectory( + this.state, + joinDirectoryPath.joinDirectoryPath(entry.name, fullPath, sep), + relativePath, + depth - 1, + this.walk + ); } else if (this.resolveSymlink && entry.isSymbolicLink()) { - let path = joinPath.joinPathWithBasePath(entry.name, directoryPath); - this.resolveSymlink(path, this.state, (stat, resolvedPath) => { + const symlinkPath = joinPath.joinPathWithBasePath(entry.name, fullPath); + this.resolveSymlink(symlinkPath, this.state, (stat, resolvedPath) => { if (stat.isDirectory()) { resolvedPath = normalizePath(resolvedPath, this.state.options); - if ( - exclude && - exclude( - entry.name, - useRealPaths ? resolvedPath : path + pathSeparator - ) - ) - return; + const directoryPath = useRealPaths + ? resolvedPath + : symlinkPath + pathSeparator; + if (exclude && exclude(entry.name, directoryPath)) return; this.walkDirectory( this.state, - resolvedPath, - useRealPaths ? resolvedPath : path + pathSeparator, + directoryPath, + directoryPath, depth - 1, this.walk ); - } else { - resolvedPath = useRealPaths ? resolvedPath : path; - const filename = basename(resolvedPath); + } else if (useRealPaths) { const directoryPath = normalizePath( dirname(resolvedPath), this.state.options ); - resolvedPath = this.joinPath(filename, directoryPath); - this.pushFile(resolvedPath, files, this.state.counts, filters); + const filename = this.joinPath( + basename(resolvedPath), + directoryPath + ); + this.pushFile(filename, files, this.state.counts, filters); + } else { + const filename = this.joinPath(entry.name, fullPath); + this.pushFile(filename, files, this.state.counts, filters); } }); } } - this.groupFiles(this.state.groups, directoryPath, files); + this.groupFiles(this.state.groups, relativePath, files); }; } From db90f1fffc75262cef2edea61a7a8f2e78036a3b Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Sat, 16 Aug 2025 18:08:00 +0500 Subject: [PATCH 2/3] chore: move away from mock-fs this is in preparation for the iterator api which will use opendir. opendir is not supported by mock-fs so we have to move away from it in order to test it properly. --- __tests__/fdir.test.ts | 57 ++++++++--------- __tests__/mock-fs.ts | 102 ++++++++++++++++++++++++++++++ __tests__/symlinks.test.ts | 123 +++++++++++++++++++++---------------- 3 files changed, 199 insertions(+), 83 deletions(-) create mode 100644 __tests__/mock-fs.ts diff --git a/__tests__/fdir.test.ts b/__tests__/fdir.test.ts index ae63510c..44c25ebf 100644 --- a/__tests__/fdir.test.ts +++ b/__tests__/fdir.test.ts @@ -1,15 +1,11 @@ import { FSLike, fdir } from "../src/index"; import fs from "fs"; -import mock from "mock-fs"; -import { test, beforeEach, TestContext, vi } from "vitest"; +import { test, TestContext, vi } from "vitest"; import path, { sep } from "path"; -import { convertSlashes } from "../src/utils"; +import { convertSlashes, normalizePath } from "../src/utils"; import picomatch from "picomatch"; import { apiTypes, APITypes, cwd, restricted, root } from "./utils"; - -beforeEach(() => { - mock.restore(); -}); +import { MockFS } from "./mock-fs"; test(`crawl single depth directory with callback`, (t) => { const api = new fdir().withErrors().crawl("__tests__"); @@ -45,6 +41,7 @@ for (const type of apiTypes) { includeBasePath: true, suppressErrors: false, }).crawl("__tests__"); + const files = await api[type](); t.expect(files.every((file) => file.startsWith("__tests__"))).toBeTruthy(); }); @@ -130,7 +127,6 @@ for (const type of apiTypes) { test(`[${type}] crawl but exclude node_modules dir`, async (t) => { const api = new fdir() - .withErrors() .withBasePath() .exclude((dir) => dir.includes("node_modules")) .crawl(cwd()); @@ -142,7 +138,6 @@ for (const type of apiTypes) { test(`[${type}] crawl all files with filter`, async (t) => { const api = new fdir() - .withErrors() .withBasePath() .filter((file) => file.includes(".git")) .crawl(cwd()); @@ -152,7 +147,6 @@ for (const type of apiTypes) { test(`[${type}] crawl all files with multifilter`, async (t) => { const api = new fdir() - .withErrors() .withBasePath() .filter((file) => file.includes(".git")) .filter((file) => file.includes(".js")) @@ -175,7 +169,7 @@ for (const type of apiTypes) { }); test(`[${type}] get all files in a directory and output full paths (withFullPaths)`, async (t) => { - const api = new fdir().withErrors().withFullPaths().crawl(cwd()); + const api = new fdir().withFullPaths().crawl(cwd()); const files = await api[type](); t.expect(files.every((file) => file.startsWith(root()))).toBeTruthy(); }); @@ -192,15 +186,9 @@ for (const type of apiTypes) { }); test(`[${type}] recurse root (files should not contain multiple /)`, async (t) => { - mock({ - "/etc": { - hosts: "dooone", - }, - }); - const api = new fdir().withBasePath().normalize().crawl("/"); + const api = new fdir().withBasePath().normalize().crawl("/tmp"); const files = await api[type](); t.expect(files.every((file) => !file.includes("//"))).toBeTruthy(); - mock.restore(); }); test(`[${type}] crawl all files with only counts`, async (t) => { @@ -237,7 +225,6 @@ for (const type of apiTypes) { test(`[${type}] crawl and filter all files and get only counts`, async (t) => { const api = new fdir() - .withErrors() .withBasePath() .filter((file) => file.includes("node_modules")) .onlyCounts() @@ -283,7 +270,7 @@ for (const type of apiTypes) { }); test(`[${type}] crawl and return relative paths with only dirs`, async (t) => { - mock({ + const mockedFs = new MockFS({ "/some/dir/dir1": { file: "some file", }, @@ -294,21 +281,26 @@ for (const type of apiTypes) { file: "some file", }, }); + await mockedFs.init(); + t.onTestFailed(async () => await mockedFs.cleanup()); - const api = new fdir({ excludeFiles: true, excludeSymlinks: true }) - .withDirs() + const api = new fdir() + .onlyDirs() .withRelativePaths() - .crawl("/some"); + .crawl(mockedFs.resolve("/some")); const paths = await api[type](); - - t.expect(paths.length).toBe(5); - t.expect(paths.filter((p) => p === ".").length).toBe(1); - t.expect(paths.filter((p) => p === "").length).toBe(0); - mock.restore(); + t.expect(paths.sort()).toStrictEqual([ + ".", + "dir/", + "dir/dir1/", + "dir/dir2/", + "dir/dir2/dir3/", + ]); + await mockedFs.cleanup(); }); test(`[${type}] crawl and return relative paths with filters and only dirs`, async (t) => { - mock({ + const mockedFs = new MockFS({ "/some/dir/dir1": { file: "some file", }, @@ -319,19 +311,22 @@ for (const type of apiTypes) { file: "some file", }, }); + await mockedFs.init(); + t.onTestFailed(async () => await mockedFs.cleanup()); const api = new fdir({ excludeFiles: true, excludeSymlinks: true }) .withDirs() .withRelativePaths() .filter((p) => p !== path.join("dir", "dir1/")) - .crawl("/some"); + .crawl(mockedFs.resolve("/some")); const paths = await api[type](); t.expect(paths.length).toBe(4); t.expect(paths.includes(path.join("dir", "dir1/"))).toBe(false); t.expect(paths.filter((p) => p === ".").length).toBe(1); t.expect(paths.filter((p) => p === "").length).toBe(0); - mock.restore(); + + await mockedFs.cleanup(); }); test(`[${type}] crawl and return relative paths that end with /`, async (t) => { diff --git a/__tests__/mock-fs.ts b/__tests__/mock-fs.ts new file mode 100644 index 00000000..a7ec900f --- /dev/null +++ b/__tests__/mock-fs.ts @@ -0,0 +1,102 @@ +import Directory from "mock-fs/lib/directory"; +import FileSystem from "mock-fs/lib/filesystem"; +import SymbolicLink from "mock-fs/lib/symlink"; +import { resolve, sep } from "path"; +import { root } from "./utils"; +import { mkdir, mkdtemp, rm, stat, symlink, writeFile } from "fs/promises"; + +export class MockFS { + root: string; + constructor(private readonly filesystem: FileSystem.DirectoryItems) {} + + async cleanup() { + await rm(this.root, { recursive: true, force: true }); + } + + normalize(paths: string[]) { + return paths.map((p) => resolvePath(this.root, this.root, p)); + } + + resolve(path: string) { + return resolvePath(this.root, this.root, path); + } + + async init() { + this.root = await mkdtemp("fdir"); + const { symlinks } = await this.createFilesystem( + this.root, + this.filesystem + ); + for (const { path, targetPath } of symlinks) { + if (!targetPath.includes(this.root)) + throw new Error( + "Cannot recurse above the temp directory: " + targetPath + ); + + const isDirectory = await (async () => { + try { + return (await stat(targetPath)).isDirectory(); + } catch { + return false; + } + })(); + await symlink(targetPath, path, isDirectory ? "dir" : "file"); + } + } + + private async createFilesystem( + root: string, + filesystem: FileSystem.DirectoryItems + ) { + await mkdir(root, { recursive: true }); + let symlinks: { path: string; targetPath: string }[] = []; + for (const name in filesystem) { + const item = filesystem[name]; + const parentPath = resolvePath(this.root, root, name); + if (!parentPath.includes(this.root)) + throw new Error( + "Cannot recurse above the temp directory: " + parentPath + ); + if (typeof item === "function") { + const unknownItem = item(); + if (unknownItem instanceof File) { + await writeFile( + parentPath, + new Uint8Array(await unknownItem.arrayBuffer()) + ); + } else if (unknownItem instanceof SymbolicLink) { + const targetPath = resolvePath( + this.root, + root, + unknownItem.getPath() + ); + symlinks.push({ + path: parentPath, + targetPath, + }); + } else if (unknownItem instanceof Directory) { + throw new Error("Not implemented."); + } + } else if (typeof item === "string" || Buffer.isBuffer(item)) { + await writeFile(parentPath, item); + } else { + symlinks = [ + ...symlinks, + ...(await this.createFilesystem(parentPath, item)).symlinks, + ]; + } + } + return { symlinks }; + } +} + +function resolvePath(rootPath: string, relativeRoot: string, path: string) { + const startsWithRoot = path.startsWith(root()); + const endsWithPathSeparator = path.endsWith(sep); + if (startsWithRoot) + return ( + resolve(rootPath, path.replace(root(), "")) + + (endsWithPathSeparator ? sep : "") + ); + return resolve(relativeRoot, path) + (endsWithPathSeparator ? sep : ""); +} diff --git a/__tests__/symlinks.test.ts b/__tests__/symlinks.test.ts index 00795e7b..357a22e9 100644 --- a/__tests__/symlinks.test.ts +++ b/__tests__/symlinks.test.ts @@ -1,26 +1,28 @@ -import { afterAll, beforeAll, beforeEach, describe, test } from "vitest"; +import { afterAll, beforeAll, describe, test } from "vitest"; import { apiTypes, normalize, root } from "./utils"; import mock from "mock-fs"; import { fdir, Options } from "../src"; import path from "path"; +import { MockFS } from "./mock-fs"; const fsWithRelativeSymlinks = { - "../../sym-relative/linked": { + "sym-relative/linked": { "file-1": "file contents", "file-excluded-1": "file contents", }, - "../../other-relative/dir": { + "other-relative/dir": { "file-2": "file contents2", + "file-3": "file contents3", }, "relative/dir": { fileSymlink: mock.symlink({ - path: "../../../../other-relative/dir/file-2", + path: "../../other-relative/dir/file-2", }), fileSymlink2: mock.symlink({ - path: "../../../../other-relative/dir/file-3", + path: "../../other-relative/dir/file-3", }), dirSymlink: mock.symlink({ - path: "../../../../sym-relative/linked", + path: "../../sym-relative/linked", }), }, }; @@ -67,42 +69,42 @@ const fsWithRecursiveSymlinks = { }; const fsWithRecursiveRelativeSymlinks = { - "double/recursive": { + "ddouble/recursive": { "another-file": "hello", "recursive-4": mock.symlink({ - path: "../../recursive", + path: "../../drecursive", }), }, - "just/some": { + "djust/some": { "another-file": "hello", "another-file2": "hello", "symlink-to-earth": mock.symlink({ - path: "../../random/other", + path: "../../drandom/other", }), }, - "random/other": { + "drandom/other": { "another-file": "hello", "another-file2": "hello", }, - recursive: { + drecursive: { "random-file": "somecontent", }, - "recursive/dir": { + "drecursive/dir": { "some-file": "somecontent2", "recursive-1": mock.symlink({ - path: "../../recursive/dir", + path: "../../drecursive/dir", }), "recursive-2": mock.symlink({ path: "./recursive-1", }), "recursive-3": mock.symlink({ - path: "../../recursive", + path: "../../drecursive", }), "recursive-5": mock.symlink({ - path: "../../double/recursive", + path: "../../ddouble/recursive", }), "not-recursive": mock.symlink({ - path: "../../just/some", + path: "../../djust/some", }), }, }; @@ -128,36 +130,42 @@ const mockFs = { }, "/other/dir": { "file-2": "file contents2", + "file-3": "file contents3", }, "/some/dir": { fileSymlink: mock.symlink({ - path: "/other/dir/file-2", + path: resolveSymlinkRoot("/other/dir/file-2"), }), fileSymlink2: mock.symlink({ - path: "/other/dir/file-3", + path: resolveSymlinkRoot("/other/dir/file-3"), }), dirSymlink: mock.symlink({ - path: "/sym/linked", + path: resolveSymlinkRoot("/sym/linked"), }), }, }; +const mockedFs = new MockFS(mockFs); for (const type of apiTypes) { describe.concurrent(type, () => { - beforeAll(() => { - mock(mockFs); + beforeAll(async () => { + await mockedFs.init(); }); - afterAll(() => { - mock.restore(); + afterAll(async () => { + await mockedFs.cleanup(); }); test(`resolve symlinks`, async (t) => { - const api = new fdir().withErrors().withSymlinks().crawl("/some/dir"); + const api = new fdir() + .withErrors() + .withSymlinks() + .crawl(mockedFs.resolve("/some/dir")); const files = await api[type](); t.expect(files.sort()).toStrictEqual( - normalize([ + mockedFs.normalize([ "/other/dir/file-2", + "/other/dir/file-3", "/sym/linked/file-1", "/sym/linked/file-excluded-1", ]) @@ -165,10 +173,13 @@ for (const type of apiTypes) { }); test(`resolve recursive symlinks`, async (t) => { - const api = new fdir().withErrors().withSymlinks().crawl("/recursive"); + const api = new fdir() + .withErrors() + .withSymlinks() + .crawl(mockedFs.resolve("/recursive")); const files = await api[type](); t.expect(files.sort()).toStrictEqual( - normalize([ + mockedFs.normalize([ "/double/recursive/another-file", "/just/some/another-file", "/just/some/another-file2", @@ -184,10 +195,10 @@ for (const type of apiTypes) { const api = new fdir() .withErrors() .withSymlinks({ resolvePaths: false }) - .crawl("/recursive"); + .crawl(mockedFs.resolve("/recursive")); const files = await api[type](); t.expect(files.sort()).toStrictEqual( - normalize([ + mockedFs.normalize([ "/recursive/dir/not-recursive/another-file", "/recursive/dir/not-recursive/another-file2", "/recursive/dir/not-recursive/symlink-to-earth/another-file", @@ -235,7 +246,7 @@ for (const type of apiTypes) { .withSymlinks({ resolvePaths: false }) .withRelativePaths() .withErrors() - .crawl("./recursive"); + .crawl(mockedFs.resolve("./recursive")); const files = await api[type](); t.expect(files.sort()).toStrictEqual( normalize([ @@ -286,11 +297,11 @@ for (const type of apiTypes) { .withSymlinks() .withRelativePaths() .withErrors() - .crawl("./recursive"); + .crawl(mockedFs.resolve("./recursive")); const files = await api[type](); t.expect(files.sort()).toStrictEqual( normalize([ - "..//double/recursive/another-file", + "../double/recursive/another-file", "../just/some/another-file", "../just/some/another-file2", "../random/other/another-file", @@ -305,13 +316,14 @@ for (const type of apiTypes) { const api = new fdir() .withErrors() .withSymlinks({ resolvePaths: false }) - .crawl("/some/dir"); + .crawl(mockedFs.resolve("/some/dir")); const files = await api[type](); t.expect(files.sort()).toStrictEqual( - normalize([ + mockedFs.normalize([ "/some/dir/dirSymlink/file-1", "/some/dir/dirSymlink/file-excluded-1", "/some/dir/fileSymlink", + "/some/dir/fileSymlink2", ]) ); }); @@ -321,13 +333,14 @@ for (const type of apiTypes) { .withErrors() .withSymlinks({ resolvePaths: false }) .withRelativePaths() - .crawl("/some/dir"); + .crawl(mockedFs.resolve("/some/dir")); const files = await api[type](); t.expect(files.sort()).toStrictEqual( normalize([ "dirSymlink/file-1", "dirSymlink/file-excluded-1", "fileSymlink", + "fileSymlink2", ]) ); }); @@ -337,13 +350,14 @@ for (const type of apiTypes) { .withErrors() .withSymlinks() .withRelativePaths() - .crawl("./relative/dir"); + .crawl(mockedFs.resolve("./relative/dir")); const files = await api[type](); t.expect(files.sort()).toStrictEqual( normalize([ - "../../../../other-relative/dir/file-2", - "../../../../sym-relative/linked/file-1", - "../../../../sym-relative/linked/file-excluded-1", + "../../other-relative/dir/file-2", + "../../other-relative/dir/file-3", + "../../sym-relative/linked/file-1", + "../../sym-relative/linked/file-excluded-1", ]) ); }); @@ -352,28 +366,30 @@ for (const type of apiTypes) { const api = new fdir() .withErrors() .withSymlinks() - .exclude((_name, path) => path === resolveSymlinkRoot("/sym/linked/")) - .crawl("/some/dir"); + .exclude((_name, path) => path === mockedFs.resolve("/sym/linked/")) + .crawl(mockedFs.resolve("/some/dir")); const files = await api[type](); - t.expect(files.sort()).toStrictEqual(normalize(["/other/dir/file-2"])); + t.expect(files.sort()).toStrictEqual( + mockedFs.normalize(["/other/dir/file-2", "/other/dir/file-3"]) + ); }); test("resolve symlinks (exclude /some/dir/dirSymlink/, real paths: false)", async (t) => { const api = new fdir() .withErrors() .withSymlinks({ resolvePaths: false }) - .exclude( - (_name, path) => path === resolveSymlinkRoot("/some/dir/dirSymlink/") - ) - .crawl("/some/dir"); + .exclude((_name, path) => { + return path === mockedFs.resolve("/some/dir/dirSymlink/"); + }) + .crawl(mockedFs.resolve("/some/dir")); const files = await api[type](); t.expect(files.sort()).toStrictEqual( - normalize(["/some/dir/fileSymlink"]) + mockedFs.normalize(["/some/dir/fileSymlink", "/some/dir/fileSymlink2"]) ); }); test(`do not resolve symlinks`, async (t) => { - const api = new fdir().withErrors().crawl("/some/dir"); + const api = new fdir().withErrors().crawl(mockedFs.resolve("/some/dir")); const files = await api[type](); t.expect(files.sort()).toStrictEqual( normalize(["dirSymlink", "fileSymlink", "fileSymlink2"]) @@ -384,7 +400,7 @@ for (const type of apiTypes) { const api = new fdir({ excludeSymlinks: true, suppressErrors: false, - }).crawl("/some/dir"); + }).crawl(mockedFs.resolve("/some/dir")); const files = await api[type](); t.expect(files).toHaveLength(0); }); @@ -395,9 +411,12 @@ for (const type of apiTypes) { const api = new fdir() .withErrors() .withSymlinks({ resolvePaths: false }) - .crawl("/"); + .crawl(mockedFs.resolve("/")); const files = await api[type](); - const expectedFiles = normalize(["/lib/file-1", "/usr/lib/file-1"]); + const expectedFiles = mockedFs.normalize([ + "/lib/file-1", + "/usr/lib/file-1", + ]); for (const expectedFile of expectedFiles) { t.expect(files).toContain(expectedFile); } From e1d1c790701eba5c9a4394332c6c81379956a091 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Sat, 16 Aug 2025 18:15:32 +0500 Subject: [PATCH 3/3] fix: `File is not defined` --- __tests__/mock-fs.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/__tests__/mock-fs.ts b/__tests__/mock-fs.ts index a7ec900f..881c2577 100644 --- a/__tests__/mock-fs.ts +++ b/__tests__/mock-fs.ts @@ -1,6 +1,7 @@ import Directory from "mock-fs/lib/directory"; import FileSystem from "mock-fs/lib/filesystem"; import SymbolicLink from "mock-fs/lib/symlink"; +import File from "mock-fs/lib/file"; import { resolve, sep } from "path"; import { root } from "./utils"; import { mkdir, mkdtemp, rm, stat, symlink, writeFile } from "fs/promises"; @@ -60,10 +61,7 @@ export class MockFS { if (typeof item === "function") { const unknownItem = item(); if (unknownItem instanceof File) { - await writeFile( - parentPath, - new Uint8Array(await unknownItem.arrayBuffer()) - ); + await writeFile(parentPath, unknownItem.getContent()); } else if (unknownItem instanceof SymbolicLink) { const targetPath = resolvePath( this.root,