Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISSUE-47: Ability to include pattern base directory to the result #393

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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