Skip to content

Commit

Permalink
fix: add support for resolving symlinks with real paths & relative pa…
Browse files Browse the repository at this point in the history
…ths on
  • Loading branch information
thecodrr committed Sep 26, 2024
1 parent d91cda2 commit a42701c
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 73 deletions.
60 changes: 36 additions & 24 deletions __tests__/fdir.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,31 +343,41 @@ for (const type of apiTypes) {
t.expect(
files.indexOf(path.join("dirSymlink", "file-excluded-1")) > -1
).toBeTruthy();
t.expect(
files.indexOf("fileSymlink") > -1
).toBeTruthy();
t.expect(files.indexOf("fileSymlink") > -1).toBeTruthy();
mock.restore();
});

// fdir doesn't support this usecase
test(`[${type}] crawl all files and include resolved symlinks with real paths with relative paths on`, async (t) => {
mock(mockFsWithSymlinks);

mock({
"../../sym/linked": {
"file-1": "file contents",
"file-excluded-1": "file contents",
},
"../../other/dir": {
"file-2": "file contents2",
},
"some/dir": {
fileSymlink: mock.symlink({
path: "../../../../other/dir/file-2",
}),
fileSymlink2: mock.symlink({
path: "../../../../other/dir/file-3",
}),
dirSymlink: mock.symlink({
path: "../../../../sym/linked",
}),
},
});
const api = new fdir()
.withSymlinks()
.withRelativePaths()
.crawl("/some/dir");
.crawl("./some/dir");
const files = await api[type]();
t.expect(files).toHaveLength(3);
t.expect(
files.indexOf(path.join("d", "file-1")) > -1
).toBeTruthy();
t.expect(
files.indexOf(path.join("d", "file-excluded-1")) > -1
).toBeTruthy();
t.expect(
files.indexOf(`${path.sep}file-2`) > -1
).toBeTruthy();
t.expect(files).toStrictEqual([
path.join("..", "..", "..", "..", "sym", "linked", "file-1"),
path.join("..", "..", "..", "..", "sym", "linked", "file-excluded-1"),
path.join("..", "..", "..", "..", "other", "dir", "file-2"),
]);
mock.restore();
});

Expand Down Expand Up @@ -468,7 +478,7 @@ for (const type of apiTypes) {
const globFunction = vi.fn((glob: string | string[]) => {
return (test: string): boolean => test.endsWith(".js");
});
const api = new fdir({globFunction})
const api = new fdir({ globFunction })
.withBasePath()
.glob("**/*.js")
.crawl("node_modules");
Expand All @@ -478,12 +488,14 @@ for (const type of apiTypes) {
});

test(`[${type}] crawl files that match using a custom glob with options`, async (t) => {
const globFunction = vi.fn((glob: string | string[], options?: {foo: number}) => {
return (test: string): boolean => test.endsWith(".js");
});
const api = new fdir({globFunction})
const globFunction = vi.fn(
(glob: string | string[], options?: { foo: number }) => {
return (test: string): boolean => test.endsWith(".js");
}
);
const api = new fdir({ globFunction })
.withBasePath()
.globWithOptions(["**/*.js"], {foo: 5})
.globWithOptions(["**/*.js"], { foo: 5 })
.crawl("node_modules");
const files = await api[type]();
t.expect(globFunction).toHaveBeenCalled();
Expand All @@ -492,7 +504,7 @@ for (const type of apiTypes) {

test(`[${type}] crawl files that match using a picomatch`, async (t) => {
const globFunction = picomatch;
const api = new fdir({globFunction})
const api = new fdir({ globFunction })
.withBasePath()
.glob("**/*.js")
.crawl("node_modules");
Expand Down
15 changes: 12 additions & 3 deletions src/api/functions/join-path.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { relative } from "path";
import { Options, PathSeparator } from "../../types";
import { convertSlashes } from "../../utils";

export function joinPathWithBasePath(filename: string, directoryPath: string) {
return directoryPath + filename;
}

function joinPathWithRelativePath(root: string) {
function joinPathWithRelativePath(root: string, options: Options) {
return function (filename: string, directoryPath: string) {
return directoryPath.substring(root.length) + filename;
const sameRoot = directoryPath.startsWith(root);
if (sameRoot) return directoryPath.replace(root, "") + filename;
else
return (
convertSlashes(relative(root, directoryPath), options.pathSeparator) +
options.pathSeparator +
filename
);
};
}

Expand All @@ -31,7 +40,7 @@ export function build(root: string, options: Options): JoinPathFunction {
const { relativePaths, includeBasePath } = options;

return relativePaths && root
? joinPathWithRelativePath(root)
? joinPathWithRelativePath(root, options)
: includeBasePath
? joinPathWithBasePath
: joinPath;
Expand Down
55 changes: 27 additions & 28 deletions src/api/walker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { basename, dirname, resolve as pathResolve } from "path";
import { cleanPath, convertSlashes } from "../utils";
import { basename, dirname } from "path";
import { normalizePath } from "../utils";
import { ResultCallback, WalkerState, Options } from "../types";
import * as joinPath from "./functions/join-path";
import * as pushDirectory from "./functions/push-directory";
Expand Down Expand Up @@ -46,7 +46,7 @@ export class Walker<TOutput extends Output> {
),
};

this.root = this.normalizePath(root);
this.root = normalizePath(root, this.state.options);

/*
* Perf: We conditionally change functions according to options. This gives a slight
Expand All @@ -72,28 +72,17 @@ export class Walker<TOutput extends Output> {
return this.isSynchronous ? this.callbackInvoker(this.state, null) : null;
}

private normalizePath(path: string) {
const { resolvePaths, normalizePath, pathSeparator } = this.state.options;
const pathNeedsCleaning =
(process.platform === "win32" && path.includes("/")) ||
path.startsWith(".");

if (resolvePaths) path = pathResolve(path);
if (normalizePath || pathNeedsCleaning) path = cleanPath(path);

if (path === ".") return "";

const needsSeperator = path[path.length - 1] !== pathSeparator;
return convertSlashes(
needsSeperator ? path + pathSeparator : path,
pathSeparator
);
}

private walk = (entries: Dirent[], directoryPath: string, depth: number) => {
const {
paths,
options: { filters, resolveSymlinks, excludeSymlinks, exclude, maxFiles, signal },
options: {
filters,
resolveSymlinks,
excludeSymlinks,
exclude,
maxFiles,
signal,
},
} = this.state;

if ((signal && signal.aborted) || (maxFiles && paths.length > maxFiles))
Expand All @@ -105,7 +94,10 @@ export class Walker<TOutput extends Output> {
for (let i = 0; i < entries.length; ++i) {
const entry = entries[i];

if (entry.isFile() || (entry.isSymbolicLink() && !resolveSymlinks && !excludeSymlinks)) {
if (
entry.isFile() ||
(entry.isSymbolicLink() && !resolveSymlinks && !excludeSymlinks)
) {
const filename = this.joinPath(entry.name, directoryPath);
this.pushFile(filename, files, this.state.counts, filters);
} else if (entry.isDirectory()) {
Expand All @@ -116,18 +108,25 @@ export class Walker<TOutput extends Output> {
);
if (exclude && exclude(entry.name, path)) continue;
this.walkDirectory(this.state, path, depth - 1, this.walk);
} else if (entry.isSymbolicLink() && resolveSymlinks && !excludeSymlinks) {
} else if (
entry.isSymbolicLink() &&
resolveSymlinks &&
!excludeSymlinks
) {
let path = joinPath.joinPathWithBasePath(entry.name, directoryPath);
this.resolveSymlink!(path, this.state, (stat, resolvedPath) => {
if (stat.isDirectory()) {
resolvedPath = this.normalizePath(resolvedPath);
resolvedPath = normalizePath(resolvedPath, this.state.options);
if (exclude && exclude(entry.name, resolvedPath)) return;

this.walkDirectory(this.state, resolvedPath, depth - 1, this.walk);
} else {
const filename = basename(resolvedPath);
const directoryPath = this.normalizePath(dirname(resolvedPath));
resolvedPath = this.joinPath(filename, directoryPath);
const filename = basename(resolvedPath);
const directoryPath = normalizePath(
dirname(resolvedPath),
this.state.options
);
resolvedPath = this.joinPath(filename, directoryPath);
this.pushFile(resolvedPath, files, this.state.counts, filters);
}
});
Expand Down
61 changes: 43 additions & 18 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
import { sep, normalize } from "path";
import { PathSeparator } from "./types";

export function cleanPath(path: string) {
let normalized = normalize(path);

// we have to remove the last path separator
// to account for / root path
if (normalized.length > 1 && normalized[normalized.length - 1] === sep)
normalized = normalized.substring(0, normalized.length - 1);

return normalized;
}

const SLASHES_REGEX = /[\\/]/g;
export function convertSlashes(path: string, separator: PathSeparator) {
return path.replace(SLASHES_REGEX, separator);
}
import { sep, normalize, resolve } from "path";
import { PathSeparator } from "./types";

export function cleanPath(path: string) {
let normalized = normalize(path);

// we have to remove the last path separator
// to account for / root path
if (normalized.length > 1 && normalized[normalized.length - 1] === sep)
normalized = normalized.substring(0, normalized.length - 1);

return normalized;
}

const SLASHES_REGEX = /[\\/]/g;
export function convertSlashes(path: string, separator: PathSeparator) {
return path.replace(SLASHES_REGEX, separator);
}

export function normalizePath(
path: string,
options: {
resolvePaths?: boolean;
normalizePath?: boolean;
pathSeparator: PathSeparator;
}
) {
const { resolvePaths, normalizePath, pathSeparator } = options;
const pathNeedsCleaning =
(process.platform === "win32" && path.includes("/")) ||
path.startsWith(".");

if (resolvePaths) path = resolve(path);
if (normalizePath || pathNeedsCleaning) path = cleanPath(path);

if (path === ".") return "";

const needsSeperator = path[path.length - 1] !== pathSeparator;
return convertSlashes(
needsSeperator ? path + pathSeparator : path,
pathSeparator
);
}

1 comment on commit a42701c

@SuperchupuDev
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for figuring this out! i think you forgot to update the docs to remove that one sentence that said this usecase wasn't supported

Please sign in to comment.