Skip to content

Commit 2f36404

Browse files
add support for leading ../ in patterns (#18)
* add support for leading `../` in patterns * add test for leading `./`
1 parent 1b51f7b commit 2f36404

File tree

2 files changed

+64
-17
lines changed

2 files changed

+64
-17
lines changed

src/index.ts

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import path from 'node:path';
1+
import path, { posix } from 'node:path';
22
import { type Options as FdirOptions, fdir } from 'fdir';
33
import picomatch from 'picomatch';
44

@@ -14,18 +14,34 @@ export interface GlobOptions {
1414
onlyFiles?: boolean;
1515
}
1616

17+
let root: string;
18+
1719
function normalizePattern(pattern: string, expandDirectories: boolean, cwd: string) {
1820
let result: string = pattern;
1921
if (pattern.endsWith('/')) {
2022
result = pattern.slice(0, -1);
2123
}
2224
// using a directory as entry should match all files inside it
23-
if (!pattern.endsWith('*') && expandDirectories) {
25+
if (!result.endsWith('*') && expandDirectories) {
2426
result += '/**';
2527
}
26-
if (result.startsWith(cwd.replace(/\\/g, '/'))) {
27-
result = path.relative(cwd, result).replace(/\\/g, '/');
28+
29+
if (result.startsWith(cwd)) {
30+
return posix.relative(cwd, result);
31+
}
32+
33+
if (result.startsWith('./')) {
34+
result = result.slice(2);
35+
}
36+
37+
const parentDirectoryMatch = /^(\/?\.\.)+/.exec(result);
38+
if (parentDirectoryMatch?.[0]) {
39+
const potentialRoot = posix.join(cwd, parentDirectoryMatch[0]);
40+
if (root.length > potentialRoot.length) {
41+
root = potentialRoot;
42+
}
2843
}
44+
2945
return result;
3046
}
3147

@@ -49,13 +65,24 @@ function processPatterns({ patterns, ignore = [], expandDirectories = true }: Gl
4965
return { match: matchPatterns, ignore: ignorePatterns };
5066
}
5167

52-
function processPath(path: string, cwd: string, absolute?: boolean) {
53-
const pathWithoutTrailingSlash = path.endsWith('/') ? path.slice(0, -1) : path;
68+
// TODO: this is slow, find a better way to do this
69+
function getRelativePath(path: string, cwd: string) {
70+
return posix.relative(cwd, `${root}/${path}`);
71+
}
72+
73+
function processPath(path: string, cwd: string, isDirectory: boolean, absolute?: boolean) {
74+
const relativePath = absolute ? path.slice(root.length + 1) : path;
75+
if (root === cwd) {
76+
return isDirectory ? relativePath.slice(0, -1) : relativePath;
77+
}
5478

55-
return absolute ? pathWithoutTrailingSlash.slice(cwd.length + 1) : pathWithoutTrailingSlash;
79+
return getRelativePath(relativePath, cwd);
5680
}
5781

58-
function getFdirBuilder(options: GlobOptions, cwd: string) {
82+
function crawl(options: GlobOptions, cwd: string, sync: false): Promise<string[]>;
83+
function crawl(options: GlobOptions, cwd: string, sync: true): string[];
84+
function crawl(options: GlobOptions, cwd: string, sync: boolean) {
85+
root = cwd;
5986
const processed = processPatterns(options, cwd);
6087

6188
const matcher = picomatch(processed.match, {
@@ -69,8 +96,8 @@ function getFdirBuilder(options: GlobOptions, cwd: string) {
6996

7097
const fdirOptions: Partial<FdirOptions> = {
7198
// use relative paths in the matcher
72-
filters: [p => matcher(processPath(p, cwd, options.absolute))],
73-
exclude: (_, p) => exclude(p.slice(cwd.length + 1).slice(0, -1)),
99+
filters: [(p, isDirectory) => matcher(processPath(p, cwd, isDirectory, options.absolute))],
100+
exclude: (_, p) => exclude(processPath(p, cwd, true, true)),
74101
pathSeparator: '/',
75102
relativePaths: true
76103
};
@@ -92,7 +119,15 @@ function getFdirBuilder(options: GlobOptions, cwd: string) {
92119
fdirOptions.includeDirs = true;
93120
}
94121

95-
return new fdir(fdirOptions);
122+
const api = new fdir(fdirOptions).crawl(root);
123+
124+
if (cwd === root || options.absolute) {
125+
return sync ? api.sync() : api.withPromise();
126+
}
127+
128+
return sync
129+
? api.sync().map(p => getRelativePath(p, cwd))
130+
: api.withPromise().then(paths => paths.map(p => getRelativePath(p, cwd)));
96131
}
97132

98133
export function glob(patterns: string[], options?: Omit<GlobOptions, 'patterns'>): Promise<string[]>;
@@ -103,9 +138,9 @@ export async function glob(patternsOrOptions: string[] | GlobOptions, options?:
103138
}
104139

105140
const opts = Array.isArray(patternsOrOptions) ? { ...options, patterns: patternsOrOptions } : patternsOrOptions;
106-
const cwd = opts.cwd ? path.resolve(opts.cwd) : process.cwd();
141+
const cwd = opts.cwd ? path.resolve(opts.cwd).replace(/\\/g, '/') : process.cwd().replace(/\\/g, '/');
107142

108-
return getFdirBuilder(opts, cwd).crawl(cwd).withPromise();
143+
return crawl(opts, cwd, false);
109144
}
110145

111146
export function globSync(patterns: string[], options?: Omit<GlobOptions, 'patterns'>): string[];
@@ -116,7 +151,7 @@ export function globSync(patternsOrOptions: string[] | GlobOptions, options?: Gl
116151
}
117152

118153
const opts = Array.isArray(patternsOrOptions) ? { ...options, patterns: patternsOrOptions } : patternsOrOptions;
119-
const cwd = opts.cwd ? path.resolve(opts.cwd) : process.cwd();
154+
const cwd = opts.cwd ? path.resolve(opts.cwd).replace(/\\/g, '/') : process.cwd().replace(/\\/g, '/');
120155

121-
return getFdirBuilder(opts, cwd).crawl(cwd).sync();
156+
return crawl(opts, cwd, true);
122157
}

test/index.test.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ test('handle absolute patterns to some extent', async () => {
7373
assert.deepEqual(files.sort(), ['a/a.ts']);
7474
});
7575

76+
test('leading ../', async () => {
77+
const files = await glob({ patterns: ['../b/*.ts'], cwd: path.join(cwd, 'a') });
78+
assert.deepEqual(files.sort(), ['../b/a.ts', '../b/b.ts']);
79+
});
80+
81+
test('leading ../ with absolute on', async () => {
82+
const files = await glob({ patterns: ['../b/*.ts'], absolute: true, cwd: path.join(cwd, 'a') });
83+
assert.deepEqual(files.sort(), [`${cwd.replaceAll('\\', '/')}/b/a.ts`, `${cwd.replaceAll('\\', '/')}/b/b.ts`]);
84+
});
85+
7686
test('bracket expanding', async () => {
7787
const files = await glob({ patterns: ['a/{a,b}.ts'], cwd });
7888
assert.deepEqual(files.sort(), ['a/a.ts', 'a/b.ts']);
@@ -130,9 +140,11 @@ test('using extglob patterns', async () => {
130140
});
131141

132142
test('pattern normalization', async () => {
133-
const files1 = await glob({ patterns: ['a/'], cwd });
134-
const files2 = await glob({ patterns: ['a'], cwd });
143+
const files1 = await glob({ patterns: ['a'], cwd });
144+
const files2 = await glob({ patterns: ['a/'], cwd });
145+
const files3 = await glob({ patterns: ['./a'], cwd });
135146
assert.deepEqual(files1, files2);
147+
assert.deepEqual(files1, files3);
136148
});
137149

138150
test('negative patterns in options', async () => {

0 commit comments

Comments
 (0)