diff --git a/__snapshots__/include-pattern-base-directory.e2e.js b/__snapshots__/include-pattern-base-directory.e2e.js new file mode 100644 index 00000000..6d765d2c --- /dev/null +++ b/__snapshots__/include-pattern-base-directory.e2e.js @@ -0,0 +1,173 @@ +exports['Options IncludePatternBaseDirectory {"pattern":"fixtures/**","options":{"onlyFiles":false,"includePatternBaseDirectory":true}} (sync) 1'] = [ + "fixtures", + "fixtures/file.md", + "fixtures/first", + "fixtures/first/file.md", + "fixtures/first/nested", + "fixtures/first/nested/directory", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second", + "fixtures/second/file.md", + "fixtures/second/nested", + "fixtures/second/nested/directory", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", + "fixtures/third", + "fixtures/third/library", + "fixtures/third/library/a", + "fixtures/third/library/a/book.md", + "fixtures/third/library/b", + "fixtures/third/library/b/book.md" +] + +exports['Options IncludePatternBaseDirectory {"pattern":"fixtures/**","options":{"onlyFiles":false,"includePatternBaseDirectory":true}} (async) 1'] = [ + "fixtures", + "fixtures/file.md", + "fixtures/first", + "fixtures/first/file.md", + "fixtures/first/nested", + "fixtures/first/nested/directory", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second", + "fixtures/second/file.md", + "fixtures/second/nested", + "fixtures/second/nested/directory", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", + "fixtures/third", + "fixtures/third/library", + "fixtures/third/library/a", + "fixtures/third/library/a/book.md", + "fixtures/third/library/b", + "fixtures/third/library/b/book.md" +] + +exports['Options IncludePatternBaseDirectory {"pattern":"fixtures/**","options":{"onlyFiles":false,"includePatternBaseDirectory":true}} (stream) 1'] = [ + "fixtures", + "fixtures/file.md", + "fixtures/first", + "fixtures/first/file.md", + "fixtures/first/nested", + "fixtures/first/nested/directory", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second", + "fixtures/second/file.md", + "fixtures/second/nested", + "fixtures/second/nested/directory", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", + "fixtures/third", + "fixtures/third/library", + "fixtures/third/library/a", + "fixtures/third/library/a/book.md", + "fixtures/third/library/b", + "fixtures/third/library/b/book.md" +] + +exports['Options IncludePatternBaseDirectory {"pattern":"./","options":{"cwd":"fixtures","onlyFiles":false,"includePatternBaseDirectory":true}} (sync) 1'] = [ + "./" +] + +exports['Options IncludePatternBaseDirectory {"pattern":"./","options":{"cwd":"fixtures","onlyFiles":false,"includePatternBaseDirectory":true}} (async) 1'] = [ + "./" +] + +exports['Options IncludePatternBaseDirectory {"pattern":"./","options":{"cwd":"fixtures","onlyFiles":false,"includePatternBaseDirectory":true}} (stream) 1'] = [ + "./" +] + +exports['Options IncludePatternBaseDirectory {"pattern":"fixtures/**","options":{"ignore":["**"],"onlyFiles":false,"includePatternBaseDirectory":true}} (sync) 1'] = [] + +exports['Options IncludePatternBaseDirectory {"pattern":"fixtures/**","options":{"ignore":["**"],"onlyFiles":false,"includePatternBaseDirectory":true}} (async) 1'] = [] + +exports['Options IncludePatternBaseDirectory {"pattern":"fixtures/**","options":{"ignore":["**"],"onlyFiles":false,"includePatternBaseDirectory":true}} (stream) 1'] = [] + +exports['Options IncludePatternBaseDirectory {"pattern":"fixtures/{first,second}/**","options":{"onlyFiles":false,"includePatternBaseDirectory":true}} (sync) 1'] = [ + "fixtures", + "fixtures/first", + "fixtures/first/file.md", + "fixtures/first/nested", + "fixtures/first/nested/directory", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second", + "fixtures/second/file.md", + "fixtures/second/nested", + "fixtures/second/nested/directory", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md" +] + +exports['Options IncludePatternBaseDirectory {"pattern":"fixtures/{first,second}/**","options":{"onlyFiles":false,"includePatternBaseDirectory":true}} (async) 1'] = [ + "fixtures", + "fixtures/first", + "fixtures/first/file.md", + "fixtures/first/nested", + "fixtures/first/nested/directory", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second", + "fixtures/second/file.md", + "fixtures/second/nested", + "fixtures/second/nested/directory", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md" +] + +exports['Options IncludePatternBaseDirectory {"pattern":"fixtures/{first,second}/**","options":{"onlyFiles":false,"includePatternBaseDirectory":true}} (stream) 1'] = [ + "fixtures", + "fixtures/first", + "fixtures/first/file.md", + "fixtures/first/nested", + "fixtures/first/nested/directory", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second", + "fixtures/second/file.md", + "fixtures/second/nested", + "fixtures/second/nested/directory", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md" +] + +exports['Options IncludePatternBaseDirectory {"pattern":"fixtures/{first,}/**","options":{"onlyFiles":false,"includePatternBaseDirectory":true}} (sync) 1'] = [ + "fixtures", + "fixtures/first", + "fixtures/first/file.md", + "fixtures/first/nested", + "fixtures/first/nested/directory", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md" +] + +exports['Options IncludePatternBaseDirectory {"pattern":"fixtures/{first,}/**","options":{"onlyFiles":false,"includePatternBaseDirectory":true}} (async) 1'] = [ + "fixtures", + "fixtures/first", + "fixtures/first/file.md", + "fixtures/first/nested", + "fixtures/first/nested/directory", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md" +] + +exports['Options IncludePatternBaseDirectory {"pattern":"fixtures/{first,}/**","options":{"onlyFiles":false,"includePatternBaseDirectory":true}} (stream) 1'] = [ + "fixtures", + "fixtures/first", + "fixtures/first/file.md", + "fixtures/first/nested", + "fixtures/first/nested/directory", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md" +] diff --git a/src/managers/tasks.spec.ts b/src/managers/tasks.spec.ts index bf54af63..c7fd4f78 100644 --- a/src/managers/tasks.spec.ts +++ b/src/managers/tasks.spec.ts @@ -44,26 +44,61 @@ describe('Managers → Task', () => { assert.deepStrictEqual(actual, expected); }); + + it('should return task for the base directory', () => { + const settings = new Settings({ + onlyFiles: false, + includePatternBaseDirectory: true + }); + + const expected = [ + tests.task.builder().base('a').positive('a').negative('b/*.md').static().build(), + tests.task.builder().base('a').positive('a/*').negative('b/*.md').build() + ]; + + const actual = manager.generate(['a/*', '!b/*.md'], settings); + + assert.deepStrictEqual(actual, expected); + }); + + it('should do not generate the task for the base directory when it is a static patterns', () => { + const settings = new Settings({ + onlyFiles: false, + includePatternBaseDirectory: true + }); + + const expected = [ + tests.task.builder().base('.').positive('a').static().build() + ]; + + const actual = manager.generate(['a'], settings); + + assert.deepStrictEqual(actual, expected); + }); }); describe('.convertPatternsToTasks', () => { it('should return one task when positive patterns have a global pattern', () => { + const settings = new Settings(); + const expected = [ tests.task.builder().base('.').positive('*').negative('*.md').build() ]; - const actual = manager.convertPatternsToTasks(['*'], ['*.md'], /* dynamic */ true); + const actual = manager.convertPatternsToTasks(['*'], ['*.md'], settings, /* dynamic */ true); assert.deepStrictEqual(actual, expected); }); it('should return two tasks when one of patterns contains reference to the parent directory', () => { + const settings = new Settings(); + const expected = [ tests.task.builder().base('..').positive('../*.md').negative('*.md').build(), tests.task.builder().base('.').positive('*').positive('a/*').negative('*.md').build() ]; - const actual = manager.convertPatternsToTasks(['*', 'a/*', '../*.md'], ['*.md'], /* dynamic */ true); + const actual = manager.convertPatternsToTasks(['*', 'a/*', '../*.md'], ['*.md'], settings, /* dynamic */ true); console.dir(actual, { colors: true }); @@ -71,12 +106,14 @@ describe('Managers → Task', () => { }); it('should return two tasks when all patterns refers to the different base directories', () => { + const settings = new Settings(); + const expected = [ tests.task.builder().base('a').positive('a/*').negative('b/*.md').build(), tests.task.builder().base('b').positive('b/*').negative('b/*.md').build() ]; - const actual = manager.convertPatternsToTasks(['a/*', 'b/*'], ['b/*.md'], /* dynamic */ true); + const actual = manager.convertPatternsToTasks(['a/*', 'b/*'], ['b/*.md'], settings, /* dynamic */ true); assert.deepStrictEqual(actual, expected); }); @@ -133,12 +170,14 @@ describe('Managers → Task', () => { describe('.convertPatternGroupsToTasks', () => { it('should return two tasks', () => { + const settings = new Settings(); + const expected = [ tests.task.builder().base('a').positive('a/*').negative('b/*.md').build(), tests.task.builder().base('b').positive('b/*').negative('b/*.md').build() ]; - const actual = manager.convertPatternGroupsToTasks({ a: ['a/*'], b: ['b/*'] }, ['b/*.md'], /* dynamic */ true); + const actual = manager.convertPatternGroupsToTasks({ a: ['a/*'], b: ['b/*'] }, ['b/*.md'], settings, /* dynamic */ true); assert.deepStrictEqual(actual, expected); }); diff --git a/src/managers/tasks.ts b/src/managers/tasks.ts index 3cc4f9dc..c4b783a9 100644 --- a/src/managers/tasks.ts +++ b/src/managers/tasks.ts @@ -17,8 +17,8 @@ export function generate(patterns: Pattern[], settings: Settings): Task[] { const staticPatterns = positivePatterns.filter((pattern) => utils.pattern.isStaticPattern(pattern, settings)); const dynamicPatterns = positivePatterns.filter((pattern) => utils.pattern.isDynamicPattern(pattern, settings)); - const staticTasks = convertPatternsToTasks(staticPatterns, negativePatterns, /* dynamic */ false); - const dynamicTasks = convertPatternsToTasks(dynamicPatterns, negativePatterns, /* dynamic */ true); + const staticTasks = convertPatternsToTasks(staticPatterns, negativePatterns, settings, /* dynamic */ false); + const dynamicTasks = convertPatternsToTasks(dynamicPatterns, negativePatterns, settings, /* dynamic */ true); return staticTasks.concat(dynamicTasks); } @@ -29,7 +29,7 @@ export function generate(patterns: Pattern[], settings: Settings): Task[] { * Patterns that can be found inside (`./`) and outside (`../`) the current directory are handled separately. * This is necessary because directory traversal starts at the base directory and goes deeper. */ -export function convertPatternsToTasks(positive: Pattern[], negative: Pattern[], dynamic: boolean): Task[] { +export function convertPatternsToTasks(positive: Pattern[], negative: Pattern[], settings: Settings, dynamic: boolean): Task[] { const tasks: Task[] = []; const patternsOutsideCurrentDirectory = utils.pattern.getPatternsOutsideCurrentDirectory(positive); @@ -38,16 +38,20 @@ export function convertPatternsToTasks(positive: Pattern[], negative: Pattern[], const outsideCurrentDirectoryGroup = groupPatternsByBaseDirectory(patternsOutsideCurrentDirectory); const insideCurrentDirectoryGroup = groupPatternsByBaseDirectory(patternsInsideCurrentDirectory); - tasks.push(...convertPatternGroupsToTasks(outsideCurrentDirectoryGroup, negative, dynamic)); + tasks.push(...convertPatternGroupsToTasks(outsideCurrentDirectoryGroup, negative, settings, dynamic)); /* * For the sake of reducing future accesses to the file system, we merge all tasks within the current directory * into a global task, if at least one pattern refers to the root (`.`). In this case, the global task covers the rest. */ if ('.' in insideCurrentDirectoryGroup) { + if (settings.includePatternBaseDirectory && dynamic) { + tasks.push(createBaseDirectoryTask('.', negative)); + } + tasks.push(convertPatternGroupToTask('.', patternsInsideCurrentDirectory, negative, dynamic)); } else { - tasks.push(...convertPatternGroupsToTasks(insideCurrentDirectoryGroup, negative, dynamic)); + tasks.push(...convertPatternGroupsToTasks(insideCurrentDirectoryGroup, negative, settings, dynamic)); } return tasks; @@ -80,10 +84,22 @@ export function groupPatternsByBaseDirectory(patterns: Pattern[]): PatternsGroup }, group); } -export function convertPatternGroupsToTasks(positive: PatternsGroup, negative: Pattern[], dynamic: boolean): Task[] { - return Object.keys(positive).map((base) => { - return convertPatternGroupToTask(base, positive[base], negative, dynamic); - }); +export function convertPatternGroupsToTasks(group: PatternsGroup, negative: Pattern[], settings: Settings, dynamic: boolean): Task[] { + const tasks: Task[] = []; + + for (const [base, patterns] of Object.entries(group)) { + if (settings.includePatternBaseDirectory && dynamic) { + tasks.push(createBaseDirectoryTask(base, negative)); + } + + tasks.push(convertPatternGroupToTask(base, patterns, negative, dynamic)); + } + + return tasks; +} + +function createBaseDirectoryTask(base: string, negative: Pattern[]): Task { + return convertPatternGroupToTask(base, [base], negative, /* dynamic */ false); } export function convertPatternGroupToTask(base: string, positive: Pattern[], negative: Pattern[], dynamic: boolean): Task { diff --git a/src/settings.spec.ts b/src/settings.spec.ts index c9dcc838..eee2cdb7 100644 --- a/src/settings.spec.ts +++ b/src/settings.spec.ts @@ -26,6 +26,7 @@ describe('Settings', () => { assert.ok(settings.globstar); assert.ok(settings.onlyFiles); assert.ok(settings.unique); + assert.ok(!settings.includePatternBaseDirectory); assert.strictEqual(settings.concurrency, os.cpus().length); assert.strictEqual(settings.cwd, process.cwd()); }); @@ -38,16 +39,27 @@ describe('Settings', () => { assert.ok(!settings.onlyFiles); }); - it('should set the "onlyFiles" option when the "onlyDirectories" is enabled', () => { + it('should set the "onlyFiles" option when the "onlyDirectories" option is enabled', () => { const settings = new Settings({ - onlyDirectories: true + onlyDirectories: true, + onlyFiles: true }); assert.ok(!settings.onlyFiles); assert.ok(settings.onlyDirectories); }); - it('should set the "objectMode" option when the "stats" is enabled', () => { + it('should disable the "includePatternBaseDirectory" option when the "onlyFiles" option is enabled', () => { + const settings = new Settings({ + onlyFiles: true, + includePatternBaseDirectory: true + }); + + assert.ok(settings.onlyFiles); + assert.ok(!settings.includePatternBaseDirectory); + }); + + it('should set the "objectMode" option when the "stats" option is enabled', () => { const settings = new Settings({ stats: true }); diff --git a/src/settings.ts b/src/settings.ts index 3000541a..3d277958 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -152,6 +152,12 @@ export type Options = { * @default true */ unique?: boolean; + /** + * Include basic pattern directories in the search results. + * + * @default false + */ + includePatternBaseDirectory?: boolean; }; export default class Settings { @@ -176,12 +182,17 @@ export default class Settings { public readonly suppressErrors: boolean = this._getValue(this._options.suppressErrors, false); public readonly throwErrorOnBrokenSymbolicLink: boolean = this._getValue(this._options.throwErrorOnBrokenSymbolicLink, false); public readonly unique: boolean = this._getValue(this._options.unique, true); + public readonly includePatternBaseDirectory: boolean = this._getValue(this._options.includePatternBaseDirectory, false); constructor(private readonly _options: Options = {}) { if (this.onlyDirectories) { this.onlyFiles = false; } + if (this.onlyFiles) { + this.includePatternBaseDirectory = false; + } + if (this.stats) { this.objectMode = true; } diff --git a/src/tests/e2e/options/include-pattern-base-directory.e2e.ts b/src/tests/e2e/options/include-pattern-base-directory.e2e.ts new file mode 100644 index 00000000..5d6d6a81 --- /dev/null +++ b/src/tests/e2e/options/include-pattern-base-directory.e2e.ts @@ -0,0 +1,43 @@ +import * as runner from '../runner'; + +runner.suite('Options IncludePatternBaseDirectory', { + tests: [ + { + pattern: 'fixtures/**', + options: { + onlyFiles: false, + includePatternBaseDirectory: true + } + }, + { + pattern: './', + options: { + cwd: 'fixtures', + onlyFiles: false, + includePatternBaseDirectory: true + } + }, + { + pattern: 'fixtures/**', + options: { + ignore: ['**'], + onlyFiles: false, + includePatternBaseDirectory: true + } + }, + { + pattern: 'fixtures/{first,second}/**', + options: { + onlyFiles: false, + includePatternBaseDirectory: true + } + }, + { + pattern: 'fixtures/{first,}/**', + options: { + onlyFiles: false, + includePatternBaseDirectory: true + } + } + ] +});