Skip to content

Commit

Permalink
feat: ability to include pattern base directory to the result
Browse files Browse the repository at this point in the history
  • Loading branch information
mrmlnc committed Jun 6, 2023
1 parent c8f0a60 commit 89ca562
Show file tree
Hide file tree
Showing 9 changed files with 311 additions and 20 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ This package provides methods for traversing the file system and returning pathn
* [onlyFiles](#onlyfiles)
* [stats](#stats)
* [unique](#unique)
* [includePatternBaseDirectory](#includepatternbasedirectory)
* [Matching control](#matching-control)
* [braceExpansion](#braceexpansion)
* [caseSensitiveMatch](#casesensitivematch)
Expand Down Expand Up @@ -556,6 +557,22 @@ fg.sync(['*.json', 'package.json'], { unique: true }); // ['package.json']

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

#### includePatternBaseDirectory

* Type: `boolean`
* Default: `false`

Include the base directory of the pattern in the results.

> :book: If the base directory of the pattern is `.`, it will not be included in the results.
>
> :book: If the [`onlyFiles`](#onlyfiles) is enabled, then this option is automatically `false`.
```js
fg.sync(['fixtures/**'], { includePatternBaseDirectory: false }); // Entries from directory
fg.sync(['fixtures/**'], { includePatternBaseDirectory: true }); // `fixtures` + entries from directory
```

### Matching control

#### braceExpansion
Expand Down
53 changes: 53 additions & 0 deletions src/providers/async.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,58 @@ describe('Providers → ProviderAsync', () => {
assert.strictEqual((error as ErrnoException).code, 'ENOENT');
}
});

describe('includePatternBaseDirectory', () => {
it('should return base pattern directory', async () => {
const provider = getProvider({
onlyFiles: false,
includePatternBaseDirectory: true
});
const task = tests.task.builder().base('root').positive('*').build();
const baseEntry = tests.entry.builder().path('root').directory().build();
const fileEntry = tests.entry.builder().path('root/file.txt').file().build();

provider.reader.static.resolves([baseEntry]);
provider.reader.dynamic.resolves([fileEntry]);

const expected = ['root', 'root/file.txt'];

const actual = await provider.read(task);

assert.strictEqual(provider.reader.static.callCount, 1);
assert.strictEqual(provider.reader.dynamic.callCount, 1);
assert.deepStrictEqual(actual, expected);
});

it('should do not read base directory for static task', async () => {
const provider = getProvider({
onlyFiles: false,
includePatternBaseDirectory: true
});

const task = tests.task.builder().base('root').positive('file.txt').static().build();

provider.reader.static.resolves([]);

await provider.read(task);

assert.strictEqual(provider.reader.static.callCount, 1);
});

it('should do not read base directory when it is a dot', async () => {
const provider = getProvider({
onlyFiles: false,
includePatternBaseDirectory: true
});
const task = tests.task.builder().base('.').positive('*').build();

provider.reader.static.resolves([]);
provider.reader.dynamic.resolves([]);

await provider.read(task);

assert.strictEqual(provider.reader.static.callCount, 0);
});
});
});
});
22 changes: 19 additions & 3 deletions src/providers/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,28 @@ export default class ProviderAsync extends Provider<Promise<EntryItem[]>> {
const root = this._getRootDirectory(task);
const options = this._getReaderOptions(task);

const entries = await this.api(root, task, options);
return ([] as Entry[])
.concat(await this._readBasePatternDirectory(task, options))
.concat(await this._readTask(root, task, options))
.map((entry) => options.transform(entry));
}

private async _readBasePatternDirectory(task: Task, options: ReaderOptions): Promise<Entry[]> {
/**
* Currently, the micromatch package cannot match the input string `.` when the '**' pattern is used.
*/
if (task.base === '.') {
return [];
}

if (task.dynamic && this._settings.includePatternBaseDirectory) {
return this._reader.static([task.base], options);
}

return entries.map((entry) => options.transform(entry));
return [];
}

public api(root: string, task: Task, options: ReaderOptions): Promise<Entry[]> {
private _readTask(root: string, task: Task, options: ReaderOptions): Promise<Entry[]> {
if (task.dynamic) {
return this._reader.dynamic(root, options);
}
Expand Down
97 changes: 91 additions & 6 deletions src/providers/stream.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as assert from 'assert';
import { PassThrough } from 'stream';
import { PassThrough, Readable } from 'stream';

import * as sinon from 'sinon';

Expand Down Expand Up @@ -27,6 +27,7 @@ function getProvider(options?: Options): TestProvider {
}

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

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

const stream = provider.read(task);

return waitStreamEnd(stream);
}

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

const api = provider.read(task);

api.on('data', (item: EntryItem) => items.push(item));
api.once('error', reject);
api.once('end', () => resolve(items));
stream.on('data', (item: EntryItem) => items.push(item));
stream.once('error', reject);
stream.once('end', () => resolve(items));
});
}

Expand Down Expand Up @@ -119,4 +124,84 @@ describe('Providers → ProviderStream', () => {
actual.emit('close');
});
});

describe('includePatternBaseDirectory', () => {
it('should return base pattern directory', async () => {
const provider = getProvider({
onlyFiles: false,
includePatternBaseDirectory: true
});
const task = tests.task.builder().base('root').positive('*').build();
const baseEntry = tests.entry.builder().path('root').directory().build();
const fileEntry = tests.entry.builder().path('root/file.txt').file().build();

// Replace by PassThrough.from after when targeting Node.js 12+.
const staticReaderStream = new PassThrough({ objectMode: true });
const dynamicReaderStream = new PassThrough({ objectMode: true });

provider.reader.static.returns(staticReaderStream);
provider.reader.dynamic.returns(dynamicReaderStream);

staticReaderStream.push(baseEntry);
staticReaderStream.push(null);
dynamicReaderStream.push(fileEntry);
dynamicReaderStream.push(null);

const expected = ['root', 'root/file.txt'];

const actual = await waitStreamEnd(provider.read(task));

assert.strictEqual(provider.reader.static.callCount, 1);
assert.strictEqual(provider.reader.dynamic.callCount, 1);
assert.deepStrictEqual(actual, expected);
});

it('should do not read base directory for static task', async () => {
const provider = getProvider({
onlyFiles: false,
includePatternBaseDirectory: true
});
const task = tests.task.builder().base('root').positive('file.txt').static().build();
const baseEntry = tests.entry.builder().path('root/file.txt').directory().build();

// Replace by PassThrough.from after when targeting Node.js 12+.
const staticReaderStream = new PassThrough({ objectMode: true });
const dynamicReaderStream = new PassThrough({ objectMode: true });

provider.reader.static.returns(staticReaderStream);
provider.reader.dynamic.returns(dynamicReaderStream);

staticReaderStream.push(baseEntry);
staticReaderStream.push(null);
dynamicReaderStream.push(null);

await waitStreamEnd(provider.read(task));

assert.strictEqual(provider.reader.static.callCount, 1);
});

it('should do not read base directory when it is a dot', async () => {
const provider = getProvider({
onlyFiles: false,
includePatternBaseDirectory: true
});
const task = tests.task.builder().base('.').positive('*').build();
const baseEntry = tests.entry.builder().path('.').directory().build();

// Replace by PassThrough.from after when targeting Node.js 12+.
const staticReaderStream = new PassThrough({ objectMode: true });
const dynamicReaderStream = new PassThrough({ objectMode: true });

provider.reader.static.returns(staticReaderStream);
provider.reader.dynamic.returns(dynamicReaderStream);

staticReaderStream.push(baseEntry);
staticReaderStream.push(null);
dynamicReaderStream.push(null);

await waitStreamEnd(provider.read(task));

assert.strictEqual(provider.reader.static.callCount, 0);
});
});
});
38 changes: 33 additions & 5 deletions src/providers/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,49 @@ export default class ProviderStream extends Provider<Readable> {
const root = this._getRootDirectory(task);
const options = this._getReaderOptions(task);

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

source
if (baseDirectoryStream !== null) {
// Do not terminate the destination stream because stream with tasks will emit entries.
baseDirectoryStream
.once('error', (error: ErrnoException) => destination.emit('error', error))
.on('data', (entry: Entry) => destination.emit('data', options.transform(entry)));
}

taskStream
.once('error', (error: ErrnoException) => destination.emit('error', error))
.on('data', (entry: Entry) => destination.emit('data', options.transform(entry)))
.once('end', () => destination.emit('end'));

destination
.once('close', () => source.destroy());
destination.once('close', () => {
if (baseDirectoryStream !== null) {
baseDirectoryStream.destroy();
}

taskStream.destroy();
});

return destination;
}

public api(root: string, task: Task, options: ReaderOptions): Readable {
private _getBasePatternDirectoryStream(task: Task, options: ReaderOptions): Readable | null {
/**
* Currently, the micromatch package cannot match the input string `.` when the '**' pattern is used.
*/
if (task.base === '.') {
return null;
}

if (task.dynamic && this._settings.includePatternBaseDirectory) {
return this._reader.static([task.base], options);
}

return null;
}

private _getTaskStream(root: string, task: Task, options: ReaderOptions): Readable {
if (task.dynamic) {
return this._reader.dynamic(root, options);
}
Expand Down
53 changes: 53 additions & 0 deletions src/providers/sync.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,58 @@ describe('Providers → ProviderSync', () => {
assert.strictEqual(provider.reader.static.callCount, 1);
assert.deepStrictEqual(actual, expected);
});

describe('includePatternBaseDirectory', () => {
it('should return base pattern directory', () => {
const provider = getProvider({
onlyFiles: false,
includePatternBaseDirectory: true
});
const task = tests.task.builder().base('root').positive('*').build();
const baseEntry = tests.entry.builder().path('root').directory().build();
const fileEntry = tests.entry.builder().path('root/file.txt').file().build();

provider.reader.static.returns([baseEntry]);
provider.reader.dynamic.returns([fileEntry]);

const expected = ['root', 'root/file.txt'];

const actual = provider.read(task);

assert.strictEqual(provider.reader.static.callCount, 1);
assert.strictEqual(provider.reader.dynamic.callCount, 1);
assert.deepStrictEqual(actual, expected);
});

it('should do not read base directory for static task', () => {
const provider = getProvider({
onlyFiles: false,
includePatternBaseDirectory: true
});

const task = tests.task.builder().base('root').positive('file.txt').static().build();

provider.reader.static.returns([]);

provider.read(task);

assert.strictEqual(provider.reader.static.callCount, 1);
});

it('should do not read base directory when it is a dot', () => {
const provider = getProvider({
onlyFiles: false,
includePatternBaseDirectory: true
});
const task = tests.task.builder().base('.').positive('*').build();

provider.reader.static.returns([]);
provider.reader.dynamic.returns([]);

provider.read(task);

assert.strictEqual(provider.reader.static.callCount, 0);
});
});
});
});
22 changes: 19 additions & 3 deletions src/providers/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,28 @@ export default class ProviderSync extends Provider<EntryItem[]> {
const root = this._getRootDirectory(task);
const options = this._getReaderOptions(task);

const entries = this.api(root, task, options);
return ([] as Entry[])
.concat(this._readBasePatternDirectory(task, options))
.concat(this._readTask(root, task, options))
.map(options.transform);
}

private _readBasePatternDirectory(task: Task, options: ReaderOptions): Entry[] {
/**
* Currently, the micromatch package cannot match the input string `.` when the '**' pattern is used.
*/
if (task.base === '.') {
return [];
}

if (task.dynamic && this._settings.includePatternBaseDirectory) {
return this._reader.static([task.base], options);
}

return entries.map(options.transform);
return [];
}

public api(root: string, task: Task, options: ReaderOptions): Entry[] {
private _readTask(root: string, task: Task, options: ReaderOptions): Entry[] {
if (task.dynamic) {
return this._reader.dynamic(root, options);
}
Expand Down
Loading

0 comments on commit 89ca562

Please sign in to comment.