Skip to content

Commit 89ca562

Browse files
committed
feat: ability to include pattern base directory to the result
1 parent c8f0a60 commit 89ca562

File tree

9 files changed

+311
-20
lines changed

9 files changed

+311
-20
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ This package provides methods for traversing the file system and returning pathn
4545
* [onlyFiles](#onlyfiles)
4646
* [stats](#stats)
4747
* [unique](#unique)
48+
* [includePatternBaseDirectory](#includepatternbasedirectory)
4849
* [Matching control](#matching-control)
4950
* [braceExpansion](#braceexpansion)
5051
* [caseSensitiveMatch](#casesensitivematch)
@@ -556,6 +557,22 @@ fg.sync(['*.json', 'package.json'], { unique: true }); // ['package.json']
556557

557558
If `true` and similar entries are found, the result is the first found.
558559

560+
#### includePatternBaseDirectory
561+
562+
* Type: `boolean`
563+
* Default: `false`
564+
565+
Include the base directory of the pattern in the results.
566+
567+
> :book: If the base directory of the pattern is `.`, it will not be included in the results.
568+
>
569+
> :book: If the [`onlyFiles`](#onlyfiles) is enabled, then this option is automatically `false`.
570+
571+
```js
572+
fg.sync(['fixtures/**'], { includePatternBaseDirectory: false }); // Entries from directory
573+
fg.sync(['fixtures/**'], { includePatternBaseDirectory: true }); // `fixtures` + entries from directory
574+
```
575+
559576
### Matching control
560577

561578
#### braceExpansion

src/providers/async.spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,58 @@ describe('Providers → ProviderAsync', () => {
8383
assert.strictEqual((error as ErrnoException).code, 'ENOENT');
8484
}
8585
});
86+
87+
describe('includePatternBaseDirectory', () => {
88+
it('should return base pattern directory', async () => {
89+
const provider = getProvider({
90+
onlyFiles: false,
91+
includePatternBaseDirectory: true
92+
});
93+
const task = tests.task.builder().base('root').positive('*').build();
94+
const baseEntry = tests.entry.builder().path('root').directory().build();
95+
const fileEntry = tests.entry.builder().path('root/file.txt').file().build();
96+
97+
provider.reader.static.resolves([baseEntry]);
98+
provider.reader.dynamic.resolves([fileEntry]);
99+
100+
const expected = ['root', 'root/file.txt'];
101+
102+
const actual = await provider.read(task);
103+
104+
assert.strictEqual(provider.reader.static.callCount, 1);
105+
assert.strictEqual(provider.reader.dynamic.callCount, 1);
106+
assert.deepStrictEqual(actual, expected);
107+
});
108+
109+
it('should do not read base directory for static task', async () => {
110+
const provider = getProvider({
111+
onlyFiles: false,
112+
includePatternBaseDirectory: true
113+
});
114+
115+
const task = tests.task.builder().base('root').positive('file.txt').static().build();
116+
117+
provider.reader.static.resolves([]);
118+
119+
await provider.read(task);
120+
121+
assert.strictEqual(provider.reader.static.callCount, 1);
122+
});
123+
124+
it('should do not read base directory when it is a dot', async () => {
125+
const provider = getProvider({
126+
onlyFiles: false,
127+
includePatternBaseDirectory: true
128+
});
129+
const task = tests.task.builder().base('.').positive('*').build();
130+
131+
provider.reader.static.resolves([]);
132+
provider.reader.dynamic.resolves([]);
133+
134+
await provider.read(task);
135+
136+
assert.strictEqual(provider.reader.static.callCount, 0);
137+
});
138+
});
86139
});
87140
});

src/providers/async.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,28 @@ export default class ProviderAsync extends Provider<Promise<EntryItem[]>> {
1010
const root = this._getRootDirectory(task);
1111
const options = this._getReaderOptions(task);
1212

13-
const entries = await this.api(root, task, options);
13+
return ([] as Entry[])
14+
.concat(await this._readBasePatternDirectory(task, options))
15+
.concat(await this._readTask(root, task, options))
16+
.map((entry) => options.transform(entry));
17+
}
18+
19+
private async _readBasePatternDirectory(task: Task, options: ReaderOptions): Promise<Entry[]> {
20+
/**
21+
* Currently, the micromatch package cannot match the input string `.` when the '**' pattern is used.
22+
*/
23+
if (task.base === '.') {
24+
return [];
25+
}
26+
27+
if (task.dynamic && this._settings.includePatternBaseDirectory) {
28+
return this._reader.static([task.base], options);
29+
}
1430

15-
return entries.map((entry) => options.transform(entry));
31+
return [];
1632
}
1733

18-
public api(root: string, task: Task, options: ReaderOptions): Promise<Entry[]> {
34+
private _readTask(root: string, task: Task, options: ReaderOptions): Promise<Entry[]> {
1935
if (task.dynamic) {
2036
return this._reader.dynamic(root, options);
2137
}

src/providers/stream.spec.ts

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as assert from 'assert';
2-
import { PassThrough } from 'stream';
2+
import { PassThrough, Readable } from 'stream';
33

44
import * as sinon from 'sinon';
55

@@ -27,6 +27,7 @@ function getProvider(options?: Options): TestProvider {
2727
}
2828

2929
function getEntries(provider: TestProvider, task: Task, entry: Entry): Promise<EntryItem[]> {
30+
// Replace by PassThrough.from after when targeting Node.js 12+.
3031
const reader = new PassThrough({ objectMode: true });
3132

3233
provider.reader.dynamic.returns(reader);
@@ -35,14 +36,18 @@ function getEntries(provider: TestProvider, task: Task, entry: Entry): Promise<E
3536
reader.push(entry);
3637
reader.push(null);
3738

39+
const stream = provider.read(task);
40+
41+
return waitStreamEnd(stream);
42+
}
43+
44+
function waitStreamEnd(stream: Readable): Promise<EntryItem[]> {
3845
return new Promise((resolve, reject) => {
3946
const items: EntryItem[] = [];
4047

41-
const api = provider.read(task);
42-
43-
api.on('data', (item: EntryItem) => items.push(item));
44-
api.once('error', reject);
45-
api.once('end', () => resolve(items));
48+
stream.on('data', (item: EntryItem) => items.push(item));
49+
stream.once('error', reject);
50+
stream.once('end', () => resolve(items));
4651
});
4752
}
4853

@@ -119,4 +124,84 @@ describe('Providers → ProviderStream', () => {
119124
actual.emit('close');
120125
});
121126
});
127+
128+
describe('includePatternBaseDirectory', () => {
129+
it('should return base pattern directory', async () => {
130+
const provider = getProvider({
131+
onlyFiles: false,
132+
includePatternBaseDirectory: true
133+
});
134+
const task = tests.task.builder().base('root').positive('*').build();
135+
const baseEntry = tests.entry.builder().path('root').directory().build();
136+
const fileEntry = tests.entry.builder().path('root/file.txt').file().build();
137+
138+
// Replace by PassThrough.from after when targeting Node.js 12+.
139+
const staticReaderStream = new PassThrough({ objectMode: true });
140+
const dynamicReaderStream = new PassThrough({ objectMode: true });
141+
142+
provider.reader.static.returns(staticReaderStream);
143+
provider.reader.dynamic.returns(dynamicReaderStream);
144+
145+
staticReaderStream.push(baseEntry);
146+
staticReaderStream.push(null);
147+
dynamicReaderStream.push(fileEntry);
148+
dynamicReaderStream.push(null);
149+
150+
const expected = ['root', 'root/file.txt'];
151+
152+
const actual = await waitStreamEnd(provider.read(task));
153+
154+
assert.strictEqual(provider.reader.static.callCount, 1);
155+
assert.strictEqual(provider.reader.dynamic.callCount, 1);
156+
assert.deepStrictEqual(actual, expected);
157+
});
158+
159+
it('should do not read base directory for static task', async () => {
160+
const provider = getProvider({
161+
onlyFiles: false,
162+
includePatternBaseDirectory: true
163+
});
164+
const task = tests.task.builder().base('root').positive('file.txt').static().build();
165+
const baseEntry = tests.entry.builder().path('root/file.txt').directory().build();
166+
167+
// Replace by PassThrough.from after when targeting Node.js 12+.
168+
const staticReaderStream = new PassThrough({ objectMode: true });
169+
const dynamicReaderStream = new PassThrough({ objectMode: true });
170+
171+
provider.reader.static.returns(staticReaderStream);
172+
provider.reader.dynamic.returns(dynamicReaderStream);
173+
174+
staticReaderStream.push(baseEntry);
175+
staticReaderStream.push(null);
176+
dynamicReaderStream.push(null);
177+
178+
await waitStreamEnd(provider.read(task));
179+
180+
assert.strictEqual(provider.reader.static.callCount, 1);
181+
});
182+
183+
it('should do not read base directory when it is a dot', async () => {
184+
const provider = getProvider({
185+
onlyFiles: false,
186+
includePatternBaseDirectory: true
187+
});
188+
const task = tests.task.builder().base('.').positive('*').build();
189+
const baseEntry = tests.entry.builder().path('.').directory().build();
190+
191+
// Replace by PassThrough.from after when targeting Node.js 12+.
192+
const staticReaderStream = new PassThrough({ objectMode: true });
193+
const dynamicReaderStream = new PassThrough({ objectMode: true });
194+
195+
provider.reader.static.returns(staticReaderStream);
196+
provider.reader.dynamic.returns(dynamicReaderStream);
197+
198+
staticReaderStream.push(baseEntry);
199+
staticReaderStream.push(null);
200+
dynamicReaderStream.push(null);
201+
202+
await waitStreamEnd(provider.read(task));
203+
204+
assert.strictEqual(provider.reader.static.callCount, 0);
205+
});
206+
});
122207
});

src/providers/stream.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,49 @@ export default class ProviderStream extends Provider<Readable> {
1212
const root = this._getRootDirectory(task);
1313
const options = this._getReaderOptions(task);
1414

15-
const source = this.api(root, task, options);
15+
const baseDirectoryStream = this._getBasePatternDirectoryStream(task, options);
16+
const taskStream = this._getTaskStream(root, task, options);
1617
const destination = new Readable({ objectMode: true, read: () => { /* noop */ } });
1718

18-
source
19+
if (baseDirectoryStream !== null) {
20+
// Do not terminate the destination stream because stream with tasks will emit entries.
21+
baseDirectoryStream
22+
.once('error', (error: ErrnoException) => destination.emit('error', error))
23+
.on('data', (entry: Entry) => destination.emit('data', options.transform(entry)));
24+
}
25+
26+
taskStream
1927
.once('error', (error: ErrnoException) => destination.emit('error', error))
2028
.on('data', (entry: Entry) => destination.emit('data', options.transform(entry)))
2129
.once('end', () => destination.emit('end'));
2230

23-
destination
24-
.once('close', () => source.destroy());
31+
destination.once('close', () => {
32+
if (baseDirectoryStream !== null) {
33+
baseDirectoryStream.destroy();
34+
}
35+
36+
taskStream.destroy();
37+
});
2538

2639
return destination;
2740
}
2841

29-
public api(root: string, task: Task, options: ReaderOptions): Readable {
42+
private _getBasePatternDirectoryStream(task: Task, options: ReaderOptions): Readable | null {
43+
/**
44+
* Currently, the micromatch package cannot match the input string `.` when the '**' pattern is used.
45+
*/
46+
if (task.base === '.') {
47+
return null;
48+
}
49+
50+
if (task.dynamic && this._settings.includePatternBaseDirectory) {
51+
return this._reader.static([task.base], options);
52+
}
53+
54+
return null;
55+
}
56+
57+
private _getTaskStream(root: string, task: Task, options: ReaderOptions): Readable {
3058
if (task.dynamic) {
3159
return this._reader.dynamic(root, options);
3260
}

src/providers/sync.spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,58 @@ describe('Providers → ProviderSync', () => {
6262
assert.strictEqual(provider.reader.static.callCount, 1);
6363
assert.deepStrictEqual(actual, expected);
6464
});
65+
66+
describe('includePatternBaseDirectory', () => {
67+
it('should return base pattern directory', () => {
68+
const provider = getProvider({
69+
onlyFiles: false,
70+
includePatternBaseDirectory: true
71+
});
72+
const task = tests.task.builder().base('root').positive('*').build();
73+
const baseEntry = tests.entry.builder().path('root').directory().build();
74+
const fileEntry = tests.entry.builder().path('root/file.txt').file().build();
75+
76+
provider.reader.static.returns([baseEntry]);
77+
provider.reader.dynamic.returns([fileEntry]);
78+
79+
const expected = ['root', 'root/file.txt'];
80+
81+
const actual = provider.read(task);
82+
83+
assert.strictEqual(provider.reader.static.callCount, 1);
84+
assert.strictEqual(provider.reader.dynamic.callCount, 1);
85+
assert.deepStrictEqual(actual, expected);
86+
});
87+
88+
it('should do not read base directory for static task', () => {
89+
const provider = getProvider({
90+
onlyFiles: false,
91+
includePatternBaseDirectory: true
92+
});
93+
94+
const task = tests.task.builder().base('root').positive('file.txt').static().build();
95+
96+
provider.reader.static.returns([]);
97+
98+
provider.read(task);
99+
100+
assert.strictEqual(provider.reader.static.callCount, 1);
101+
});
102+
103+
it('should do not read base directory when it is a dot', () => {
104+
const provider = getProvider({
105+
onlyFiles: false,
106+
includePatternBaseDirectory: true
107+
});
108+
const task = tests.task.builder().base('.').positive('*').build();
109+
110+
provider.reader.static.returns([]);
111+
provider.reader.dynamic.returns([]);
112+
113+
provider.read(task);
114+
115+
assert.strictEqual(provider.reader.static.callCount, 0);
116+
});
117+
});
65118
});
66119
});

src/providers/sync.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,28 @@ export default class ProviderSync extends Provider<EntryItem[]> {
1010
const root = this._getRootDirectory(task);
1111
const options = this._getReaderOptions(task);
1212

13-
const entries = this.api(root, task, options);
13+
return ([] as Entry[])
14+
.concat(this._readBasePatternDirectory(task, options))
15+
.concat(this._readTask(root, task, options))
16+
.map(options.transform);
17+
}
18+
19+
private _readBasePatternDirectory(task: Task, options: ReaderOptions): Entry[] {
20+
/**
21+
* Currently, the micromatch package cannot match the input string `.` when the '**' pattern is used.
22+
*/
23+
if (task.base === '.') {
24+
return [];
25+
}
26+
27+
if (task.dynamic && this._settings.includePatternBaseDirectory) {
28+
return this._reader.static([task.base], options);
29+
}
1430

15-
return entries.map(options.transform);
31+
return [];
1632
}
1733

18-
public api(root: string, task: Task, options: ReaderOptions): Entry[] {
34+
private _readTask(root: string, task: Task, options: ReaderOptions): Entry[] {
1935
if (task.dynamic) {
2036
return this._reader.dynamic(root, options);
2137
}

0 commit comments

Comments
 (0)