Skip to content

Commit 3ae5551

Browse files
committed
feat: ability to include pattern base directory to the result
1 parent ba7e86c commit 3ae5551

9 files changed

Lines changed: 292 additions & 23 deletions

File tree

README.md

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

520521
If `true` and similar entries are found, the result is the first found.
521522

523+
#### includePatternBaseDirectory
524+
525+
* Type: `boolean`
526+
* Default: `false`
527+
528+
Include the base directory of the pattern in the results.
529+
530+
> :book: If the base directory of the pattern is `.`, it will not be included in the results.
531+
>
532+
> :book: If the [`onlyFiles`](#onlyfiles) is enabled, then this option is automatically `false`.
533+
534+
```js
535+
fg.sync(['fixtures/**'], { includePatternBaseDirectory: false }); // Entries from directory
536+
fg.sync(['fixtures/**'], { includePatternBaseDirectory: true }); // `fixtures` + entries from directory
537+
```
538+
522539
### Matching control
523540

524541
#### 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: 72 additions & 9 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,22 +27,21 @@ function getProvider(options?: Options): TestProvider {
2727
}
2828

2929
function getEntries(provider: TestProvider, task: Task, entry: Entry): Promise<EntryItem[]> {
30-
const reader = new PassThrough({ objectMode: true });
30+
const reader = PassThrough.from([entry], { objectMode: true });
3131

3232
provider.reader.dynamic.returns(reader);
3333
provider.reader.static.returns(reader);
3434

35-
reader.push(entry);
36-
reader.push(null);
35+
return waitStreamEnd(provider.read(task));
36+
}
3737

38+
function waitStreamEnd(stream: Readable): Promise<EntryItem[]> {
3839
return new Promise((resolve, reject) => {
3940
const items: EntryItem[] = [];
4041

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));
42+
stream.on('data', (item: EntryItem) => items.push(item));
43+
stream.once('error', reject);
44+
stream.once('end', () => resolve(items));
4645
});
4746
}
4847

@@ -118,5 +117,69 @@ describe('Providers → ProviderStream', () => {
118117

119118
actual.emit('close');
120119
});
120+
121+
describe('includePatternBaseDirectory', () => {
122+
it('should return base pattern directory', async () => {
123+
const provider = getProvider({
124+
onlyFiles: false,
125+
includePatternBaseDirectory: true
126+
});
127+
const task = tests.task.builder().base('root').positive('*').build();
128+
const baseEntry = tests.entry.builder().path('root').directory().build();
129+
const fileEntry = tests.entry.builder().path('root/file.txt').file().build();
130+
131+
const staticReaderStream = PassThrough.from([baseEntry], { objectMode: true });
132+
const dynamicReaderStream = PassThrough.from([fileEntry], { objectMode: true });
133+
134+
provider.reader.static.returns(staticReaderStream);
135+
provider.reader.dynamic.returns(dynamicReaderStream);
136+
137+
const expected = ['root', 'root/file.txt'];
138+
139+
const actual = await waitStreamEnd(provider.read(task));
140+
141+
assert.strictEqual(provider.reader.static.callCount, 1);
142+
assert.strictEqual(provider.reader.dynamic.callCount, 1);
143+
assert.deepStrictEqual(actual, expected);
144+
});
145+
146+
it('should do not read base directory for static task', async () => {
147+
const provider = getProvider({
148+
onlyFiles: false,
149+
includePatternBaseDirectory: true
150+
});
151+
const task = tests.task.builder().base('root').positive('file.txt').static().build();
152+
const baseEntry = tests.entry.builder().path('root/file.txt').directory().build();
153+
154+
const staticReaderStream = PassThrough.from([baseEntry], { objectMode: true });
155+
const dynamicReaderStream = PassThrough.from([]);
156+
157+
provider.reader.static.returns(staticReaderStream);
158+
provider.reader.dynamic.returns(dynamicReaderStream);
159+
160+
await waitStreamEnd(provider.read(task));
161+
162+
assert.strictEqual(provider.reader.static.callCount, 1);
163+
});
164+
165+
it('should do not read base directory when it is a dot', async () => {
166+
const provider = getProvider({
167+
onlyFiles: false,
168+
includePatternBaseDirectory: true
169+
});
170+
const task = tests.task.builder().base('.').positive('*').build();
171+
const baseEntry = tests.entry.builder().path('.').directory().build();
172+
173+
const staticReaderStream = PassThrough.from([baseEntry], { objectMode: true });
174+
const dynamicReaderStream = PassThrough.from([], { objectMode: true });
175+
176+
provider.reader.static.returns(staticReaderStream);
177+
provider.reader.dynamic.returns(dynamicReaderStream);
178+
179+
await waitStreamEnd(provider.read(task));
180+
181+
assert.strictEqual(provider.reader.static.callCount, 0);
182+
});
183+
});
121184
});
122185
});

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)