From 2dbb1ca7bc1285c956e027539441d1769d8e6fb8 Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Sun, 20 Oct 2024 22:22:13 +0200 Subject: [PATCH] feat(providence): experimental support for fs.glob + ignore support in optimisedGlob --- .changeset/forty-hotels-watch.md | 6 + .../src/program/utils/optimised-glob.js | 185 +++++-- .../program/utils/optimised-glob.test.js | 479 ++++++++++-------- 3 files changed, 416 insertions(+), 254 deletions(-) create mode 100644 .changeset/forty-hotels-watch.md diff --git a/.changeset/forty-hotels-watch.md b/.changeset/forty-hotels-watch.md new file mode 100644 index 000000000..a939928c7 --- /dev/null +++ b/.changeset/forty-hotels-watch.md @@ -0,0 +1,6 @@ +--- +'providence-analytics': patch +--- + +- support `ignore: string[]` globs in optimisedGlob +- experimental `fs.glob` support under the hood in optimisedGlob diff --git a/packages-node/providence-analytics/src/program/utils/optimised-glob.js b/packages-node/providence-analytics/src/program/utils/optimised-glob.js index 8230758b0..60dd3f8d2 100644 --- a/packages-node/providence-analytics/src/program/utils/optimised-glob.js +++ b/packages-node/providence-analytics/src/program/utils/optimised-glob.js @@ -7,9 +7,21 @@ import { toPosixPath } from './to-posix-path.js'; import { memoize } from './memoize.js'; /** + * @typedef {nodeFs.Dirent & { path:string; parentPath:string }} DirentWithPath * @typedef {nodeFs} FsLike - * @typedef {nodeFs.Dirent & {path:string;parentPath:string}} DirentWithPath - * @typedef {{onlyDirectories:boolean;onlyFiles:boolean;deep:number;suppressErrors:boolean;fs: FsLike;cwd:string;absolute:boolean;extglob:boolean;}} FastGlobtions + * @typedef {{ + * onlyDirectories: boolean; + * suppressErrors: boolean; + * onlyFiles: boolean; + * absolute: boolean; + * extglob: boolean; + * ignore: string[]; + * unique: boolean; + * deep: number; + * dot: boolean; + * cwd: string; + * fs: FsLike; + * }} FastGlobtions */ const [nodeMajor] = process.versions.node.split('.').map(Number); @@ -109,6 +121,65 @@ export const parseGlobToRegex = memoize( }, ); +/** + * @template T + * @param {T[]} arr + * @returns {T[]} + */ +function toUniqueArray(arr) { + return Array.from(new Set(arr)); +} + +/** + * @param {DirentWithPath} dirent + * @param {{cwd:string}} cfg + * @returns {string} + */ +function direntToLocalPath(dirent, { cwd }) { + const folder = (dirent.parentPath || dirent.path).replace(/(^.*)\/(.*\..*$)/, '$1'); + return toPosixPath(path.join(folder, dirent.name)).replace( + new RegExp(`^${toPosixPath(cwd)}/`), + '', + ); +} + +/** + * @param {{dirent:nodeFs.Dirent;relativeToCwdPath:string}[]} matchedEntries + * @param {FastGlobtions} options + * @returns {string[]} + */ +function postprocessOptions(matchedEntries, options) { + const allFileOrDirectoryEntries = matchedEntries.filter(({ dirent }) => + options.onlyDirectories ? dirent.isDirectory() : dirent.isFile(), + ); + + let filteredPaths = allFileOrDirectoryEntries.map(({ relativeToCwdPath }) => relativeToCwdPath); + + if (!options.dot) { + filteredPaths = filteredPaths.filter( + f => !f.split('/').some(folderOrFile => folderOrFile.startsWith('.')), + ); + } + + if (options.absolute) { + filteredPaths = filteredPaths.map(f => toPosixPath(path.join(options.cwd, f))); + if (process.platform === 'win32') { + const driveChar = path.win32.resolve(options.cwd).slice(0, 1).toUpperCase(); + filteredPaths = filteredPaths.map(f => `${driveChar}:${f}`); + } + } + + if (options.deep !== Infinity) { + filteredPaths = filteredPaths.filter(f => f.split('/').length <= options.deep + 2); + } + + const result = options.unique ? toUniqueArray(filteredPaths) : filteredPaths; + return result.sort((a, b) => { + const pathDiff = a.split('/').length - b.split('/').length; + return pathDiff !== 0 ? pathDiff : a.localeCompare(b); + }); +} + const getStartPath = memoize( /** * @param {string} glob @@ -129,6 +200,7 @@ const getStartPath = memoize( ); let isCacheEnabled = false; +let isExperimentalFsGlobEnabled = false; /** @type {{[path:string]:DirentWithPath[]}} */ const cache = {}; @@ -179,15 +251,34 @@ const getAllDirentsRelativeToCwd = memoize( }); const allDirEntsRelativeToCwd = allDirentsRelativeToStartPath.map(dirent => ({ - relativeToCwdPath: toPosixPath( - path.join(dirent.parentPath || dirent.path, dirent.name), - ).replace(`${toPosixPath(options.cwd)}/`, ''), + relativeToCwdPath: direntToLocalPath(dirent, { cwd: options.cwd }), dirent, })); + return allDirEntsRelativeToCwd; }, ); +/** + * @param {string|string[]} globOrGlobs + * @param {{ fs: FsLike; cwd: string; exclude?: Function; stats?: boolean }} cfg + * @returns {Promise} + */ +async function nativeGlob(globOrGlobs, { fs, cwd, exclude, stats }) { + // @ts-expect-error + const asyncGenResult = await fs.promises.glob(globOrGlobs, { + withFileTypes: true, + cwd, + ...(exclude ? { exclude } : {}), + }); + const results = []; + for await (const dirent of asyncGenResult) { + if (dirent.name === '.' || dirent.isDirectory()) continue; // eslint-disable-line no-continue + results.push(stats ? dirent : direntToLocalPath(dirent, { cwd })); + } + return results; +} + /** * Lightweight glob implementation. * It's a drop-in replacement for globby, but it's faster, a few hundred lines of code and has no dependencies. @@ -209,7 +300,8 @@ export async function optimisedGlob(globOrGlobs, providedOptions = {}) { unique: true, sync: false, dot: false, - // TODO: ignore, throwErrorOnBrokenSymbolicLink, markDirectories, objectMode, onlyDirectories, onlyFiles, stats + ignore: [], + // Add if needed: throwErrorOnBrokenSymbolicLink, markDirectories, objectMode, stats // https://github.com/mrmlnc/fast-glob?tab=readme-ov-file ...providedOptions, }; @@ -219,7 +311,46 @@ export async function optimisedGlob(globOrGlobs, providedOptions = {}) { options.onlyDirectories = true; } - const globs = Array.isArray(globOrGlobs) ? Array.from(new Set(globOrGlobs)) : [globOrGlobs]; + const regularGlobs = Array.isArray(globOrGlobs) ? globOrGlobs : [globOrGlobs]; + const ignoreGlobs = options.ignore.map((/** @type {string} */ g) => + g.startsWith('!') ? g : `!${g}`, + ); + + const optionsNotSupportedByNativeGlob = ['onlyDirectories', 'dot']; + const doesConfigAllowNative = !optionsNotSupportedByNativeGlob.some(opt => options[opt]); + if (isExperimentalFsGlobEnabled && options.fs.promises.glob && doesConfigAllowNative) { + const negativeGlobs = [...ignoreGlobs, ...regularGlobs.filter(r => r.startsWith('!'))].map(r => + r.slice(1), + ); + + const negativeResults = negativeGlobs.length + ? /** @type {string[]} */ (await nativeGlob(negativeGlobs, options)) + : []; + const positiveGlobs = regularGlobs.filter(r => !r.startsWith('!')); + + const result = /** @type {DirentWithPath[]} */ ( + await nativeGlob(positiveGlobs, { + cwd: options.cwd, + fs: options.fs, + stats: true, + // we cannot use the exclude option here, because it's not working correctly + }) + ); + + const direntsFiltered = result.filter( + dirent => !negativeResults.includes(direntToLocalPath(dirent, { cwd: options.cwd })), + ); + + return postprocessOptions( + direntsFiltered.map(dirent => ({ + dirent, + relativeToCwdPath: direntToLocalPath(dirent, { cwd: options.cwd }), + })), + options, + ); + } + + const globs = toUniqueArray([...regularGlobs, ...ignoreGlobs]); /** @type {RegExp[]} */ const matchRegexesNegative = []; @@ -269,37 +400,7 @@ export async function optimisedGlob(globOrGlobs, providedOptions = {}) { !matchRegexesNegative.some(globReNeg => globReNeg.test(globEntry.relativeToCwdPath)), ); - const allFileOrDirectoryEntries = matchedEntries.filter(({ dirent }) => - options.onlyDirectories ? dirent.isDirectory() : dirent.isFile(), - ); - - let filteredPaths = allFileOrDirectoryEntries.map(({ relativeToCwdPath }) => relativeToCwdPath); - - if (!options.dot) { - filteredPaths = filteredPaths.filter( - f => !f.split('/').some(folderOrFile => folderOrFile.startsWith('.')), - ); - } - - if (options.absolute) { - filteredPaths = filteredPaths.map(f => toPosixPath(path.join(options.cwd, f))); - - if (process.platform === 'win32') { - const driveChar = path.win32.resolve(options.cwd).slice(0, 1).toUpperCase(); - filteredPaths = filteredPaths.map(f => `${driveChar}:${f}`); - } - } - - if (options.deep !== Infinity) { - filteredPaths = filteredPaths.filter(f => f.split('/').length <= options.deep + 2); - } - - const result = options.unique ? Array.from(new Set(filteredPaths)) : filteredPaths; - - const res = result.sort((a, b) => { - const pathDiff = a.split('/').length - b.split('/').length; - return pathDiff !== 0 ? pathDiff : a.localeCompare(b); - }); + const res = postprocessOptions(matchedEntries, options); // It could happen the fs changes with the next call, so we clear the cache getAllDirentsRelativeToCwd.clearCache(); @@ -311,3 +412,11 @@ export async function optimisedGlob(globOrGlobs, providedOptions = {}) { optimisedGlob.disableCache = () => { isCacheEnabled = false; }; + +optimisedGlob.enableExperimentalFsGlob = () => { + isExperimentalFsGlobEnabled = true; +}; + +optimisedGlob.disableExperimentalFsGlob = () => { + isExperimentalFsGlobEnabled = false; +}; diff --git a/packages-node/providence-analytics/test-node/program/utils/optimised-glob.test.js b/packages-node/providence-analytics/test-node/program/utils/optimised-glob.test.js index 0e77f9344..f0bcb2917 100644 --- a/packages-node/providence-analytics/test-node/program/utils/optimised-glob.test.js +++ b/packages-node/providence-analytics/test-node/program/utils/optimised-glob.test.js @@ -1,9 +1,7 @@ import path from 'path'; - import { globby } from 'globby'; -// eslint-disable-next-line import/no-extraneous-dependencies import { expect } from 'chai'; -// eslint-disable-next-line import/no-extraneous-dependencies +// @ts-expect-error import mockFs from 'mock-fs'; import { optimisedGlob } from '../../../src/program/utils/optimised-glob.js'; @@ -11,8 +9,12 @@ import { optimisedGlob } from '../../../src/program/utils/optimised-glob.js'; const measurePerf = process.argv.includes('--measure-perf'); /** - * @param {*} patterns - * @param {*} options + * @typedef {import('../../../src/program/utils/optimised-glob.js').FastGlobtions} FastGlobtions + */ + +/** + * @param {string|string[]} patterns + * @param {Partial} options * @returns {Promise} */ async function runOptimisedGlobAndCheckGlobbyParity(patterns, options) { @@ -43,237 +45,282 @@ async function runOptimisedGlobAndCheckGlobbyParity(patterns, options) { return optimisedGlobResult; } -describe('optimisedGlob', () => { - const testCfg = { - cwd: '/fakeFs', - }; - - beforeEach(() => { - const fakeFs = { - '/fakeFs/my/folder/some/file.js': 'content', - '/fakeFs/my/folder/lvl1/some/file.js': 'content', - '/fakeFs/my/folder/lvl1/lvl2/some/file.js': 'content', - '/fakeFs/my/folder/lvl1/lvl2/lvl3/some/file.js': 'content', - '/fakeFs/my/folder/some/file.d.ts': 'content', - '/fakeFs/my/folder/lvl1/some/file.d.ts': 'content', - '/fakeFs/my/folder/lvl1/lvl2/some/file.d.ts': 'content', - '/fakeFs/my/folder/lvl1/lvl2/lvl3/some/file.d.ts': 'content', - - '/fakeFs/my/folder/some/anotherFile.js': 'content', - '/fakeFs/my/folder/lvl1/some/anotherFile.js': 'content', - '/fakeFs/my/folder/lvl1/lvl2/some/anotherFile.js': 'content', - '/fakeFs/my/folder/lvl1/lvl2/lvl3/some/anotherFile.js': 'content', - '/fakeFs/my/folder/some/anotherFile.d.ts': 'content', - '/fakeFs/my/folder/lvl1/some/anotherFile.d.ts': 'content', - '/fakeFs/my/folder/lvl1/lvl2/some/anotherFile.d.ts': 'content', - '/fakeFs/my/folder/lvl1/lvl2/lvl3/some/anotherFile.d.ts': 'content', - - '/fakeFs/my/.hiddenFile.js': 'content', +function runSuiteForOptimisedGlob() { + describe('optimisedGlob', () => { + const testCfg = { + cwd: '/fakeFs', }; - mockFs(fakeFs); - }); - afterEach(() => { - mockFs.restore(); - }); + beforeEach(() => { + const fakeFs = { + '/fakeFs/my/folder/some/file.js': 'content', + '/fakeFs/my/folder/lvl1/some/file.js': 'content', + '/fakeFs/my/folder/lvl1/lvl2/some/file.js': 'content', + '/fakeFs/my/folder/lvl1/lvl2/lvl3/some/file.js': 'content', + '/fakeFs/my/folder/some/file.d.ts': 'content', + '/fakeFs/my/folder/lvl1/some/file.d.ts': 'content', + '/fakeFs/my/folder/lvl1/lvl2/some/file.d.ts': 'content', + '/fakeFs/my/folder/lvl1/lvl2/lvl3/some/file.d.ts': 'content', - describe('Star patterns', () => { - it('supports double asterisk like "my/folder/**/some/file.js" ', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity( - 'my/folder/**/some/file.js', - testCfg, - ); + '/fakeFs/my/folder/some/anotherFile.js': 'content', + '/fakeFs/my/folder/lvl1/some/anotherFile.js': 'content', + '/fakeFs/my/folder/lvl1/lvl2/some/anotherFile.js': 'content', + '/fakeFs/my/folder/lvl1/lvl2/lvl3/some/anotherFile.js': 'content', + '/fakeFs/my/folder/some/anotherFile.d.ts': 'content', + '/fakeFs/my/folder/lvl1/some/anotherFile.d.ts': 'content', + '/fakeFs/my/folder/lvl1/lvl2/some/anotherFile.d.ts': 'content', + '/fakeFs/my/folder/lvl1/lvl2/lvl3/some/anotherFile.d.ts': 'content', - expect(files).to.deep.equal([ - 'my/folder/some/file.js', - 'my/folder/lvl1/some/file.js', - 'my/folder/lvl1/lvl2/some/file.js', - 'my/folder/lvl1/lvl2/lvl3/some/file.js', - ]); + '/fakeFs/my/.hiddenFile.js': 'content', + }; + mockFs(fakeFs); }); - it('supports single asterisk like "my/folder/*/some/file.js" ', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity('my/folder/*/some/file.js', testCfg); - - expect(files).to.deep.equal(['my/folder/lvl1/some/file.js']); + afterEach(() => { + mockFs.restore(); }); - it('supports filenames like "my/folder/lvl1/some/*il*.js" ', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity( - 'my/folder/lvl1/some/*il*.js', - testCfg, - ); + describe('Star patterns', () => { + it('supports double asterisk like "my/folder/**/some/file.js" ', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity( + 'my/folder/**/some/file.js', + testCfg, + ); - expect(files).to.deep.equal([ - 'my/folder/lvl1/some/anotherFile.js', - 'my/folder/lvl1/some/file.js', - ]); - }); - - it('supports globs starting with a star like "**/some/file.js" ', async () => { - const filesDoubleStar = await runOptimisedGlobAndCheckGlobbyParity( - '**/some/file.js', - testCfg, - ); - - expect(filesDoubleStar).to.deep.equal([ - 'my/folder/some/file.js', - 'my/folder/lvl1/some/file.js', - 'my/folder/lvl1/lvl2/some/file.js', - 'my/folder/lvl1/lvl2/lvl3/some/file.js', - ]); - - const filesSingleStar = await runOptimisedGlobAndCheckGlobbyParity( - '*/folder/some/file.js', - testCfg, - ); - - expect(filesSingleStar).to.deep.equal(['my/folder/some/file.js']); - }); - - it('gives empty output when location does not exist" ', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity('my/folder/**/some/file.js', { - ...testCfg, - cwd: '/nonExisting/path', // this will not exist - }); - - expect(files).to.deep.equal([]); - }); - - it('omits hidden files" ', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity('*/*/*/*', testCfg); - - expect(files).to.deep.equal([ - 'my/folder/some/anotherFile.d.ts', - 'my/folder/some/anotherFile.js', - 'my/folder/some/file.d.ts', - 'my/folder/some/file.js', - ]); - }); - }); - - describe('Accolade patterns', () => { - it('works with filenames like "my/folder/*/some/file.{js,d.ts}" ', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity( - 'my/folder/*/some/file.{js,d.ts}', - testCfg, - ); - - expect(files).to.deep.equal(['my/folder/lvl1/some/file.d.ts', 'my/folder/lvl1/some/file.js']); - }); - }); - - describe('Multiple globs', () => { - it('accepts an array of globs, like ["my/folder/*/some/file.js", "my/folder/lvl1/*/some/file.js"]', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity( - ['my/folder/*/some/file.js', 'my/folder/lvl1/*/some/file.js'], - testCfg, - ); - - expect(files).to.deep.equal([ - 'my/folder/lvl1/some/file.js', - 'my/folder/lvl1/lvl2/some/file.js', - ]); - }); - - it('accepts negative globs, like ["my/folder/**/some/file.js", "!my/folder/*/some/file.js"]', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity( - ['my/folder/**/some/file.js', '!my/folder/*/some/file.js'], - testCfg, - ); - - expect(files).to.deep.equal([ - 'my/folder/some/file.js', - 'my/folder/lvl1/lvl2/some/file.js', - 'my/folder/lvl1/lvl2/lvl3/some/file.js', - ]); - }); - }); - - describe('Options', () => { - it('"absolute" returns full system paths', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity('my/folder/*/some/file.{js,d.ts}', { - ...testCfg, - absolute: true, - }); - - if (process.platform === 'win32') { - const driveLetter = path.win32.resolve(testCfg.cwd).slice(0, 1).toUpperCase(); expect(files).to.deep.equal([ - `${driveLetter}:/fakeFs/my/folder/lvl1/some/file.d.ts`, - `${driveLetter}:/fakeFs/my/folder/lvl1/some/file.js`, + 'my/folder/some/file.js', + 'my/folder/lvl1/some/file.js', + 'my/folder/lvl1/lvl2/some/file.js', + 'my/folder/lvl1/lvl2/lvl3/some/file.js', ]); - } else { + }); + + it('supports single asterisk like "my/folder/*/some/file.js" ', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity( + 'my/folder/*/some/file.js', + testCfg, + ); + + expect(files).to.deep.equal(['my/folder/lvl1/some/file.js']); + }); + + it('supports filenames like "my/folder/lvl1/some/*il*.js" ', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity( + 'my/folder/lvl1/some/*il*.js', + testCfg, + ); + expect(files).to.deep.equal([ - '/fakeFs/my/folder/lvl1/some/file.d.ts', - '/fakeFs/my/folder/lvl1/some/file.js', + 'my/folder/lvl1/some/anotherFile.js', + 'my/folder/lvl1/some/file.js', ]); - } - }); - - it('"cwd" changes relative starting point of glob', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity('folder/*/some/file.{js,d.ts}', { - ...testCfg, - cwd: '/fakeFs/my', }); - expect(files).to.deep.equal(['folder/lvl1/some/file.d.ts', 'folder/lvl1/some/file.js']); - }); + it('supports globs starting with a star like "**/some/file.js" ', async () => { + const filesDoubleStar = await runOptimisedGlobAndCheckGlobbyParity( + '**/some/file.js', + testCfg, + ); - it('"onlyDirectories" returns only directories/folders', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity('my/folder/*/some', { - ...testCfg, - onlyDirectories: true, + expect(filesDoubleStar).to.deep.equal([ + 'my/folder/some/file.js', + 'my/folder/lvl1/some/file.js', + 'my/folder/lvl1/lvl2/some/file.js', + 'my/folder/lvl1/lvl2/lvl3/some/file.js', + ]); + + const filesSingleStar = await runOptimisedGlobAndCheckGlobbyParity( + '*/folder/some/file.js', + testCfg, + ); + + expect(filesSingleStar).to.deep.equal(['my/folder/some/file.js']); }); - expect(files).to.deep.equal(['my/folder/lvl1/some']); - }); - - it('"onlyFiles" returns only files', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity('my/folder/*/some', { - ...testCfg, - onlyFiles: true, - }); - - expect(files).to.deep.equal([]); - }); - - it('"deep" limits the level of results', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity('my/folder/**', { - ...testCfg, - onlyDirectories: true, - deep: 1, - }); - expect(files).to.deep.equal(['my/folder/lvl1', 'my/folder/some']); - - const files2 = await runOptimisedGlobAndCheckGlobbyParity('my/folder/**', { - ...testCfg, - onlyDirectories: true, - deep: 2, - }); - - expect(files2).to.deep.equal([ - 'my/folder/lvl1', - 'my/folder/some', - 'my/folder/lvl1/lvl2', - 'my/folder/lvl1/some', - ]); - }); - - it('"dot" allows hidden files" ', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity('*/*', { ...testCfg, dot: true }); - - expect(files).to.deep.equal(['my/.hiddenFile.js']); - }); - - it.skip('"suppressErrors" throws errors when paths do not exist', async () => { - expect(async () => - optimisedGlob('my/folder/**/some/file.js', { + it('gives empty output when location does not exist" ', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity('my/folder/**/some/file.js', { ...testCfg, cwd: '/nonExisting/path', // this will not exist - suppressErrors: false, - }), - ).to.throw(); + }); + + expect(files).to.deep.equal([]); + }); + + it('omits hidden files" ', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity('*/*/*/*', testCfg); + + expect(files).to.deep.equal([ + 'my/folder/some/anotherFile.d.ts', + 'my/folder/some/anotherFile.js', + 'my/folder/some/file.d.ts', + 'my/folder/some/file.js', + ]); + }); + }); + + describe('Accolade patterns', () => { + it('works with filenames like "my/folder/*/some/file.{js,d.ts}" ', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity( + 'my/folder/*/some/file.{js,d.ts}', + testCfg, + ); + + expect(files).to.deep.equal([ + 'my/folder/lvl1/some/file.d.ts', + 'my/folder/lvl1/some/file.js', + ]); + }); + }); + + describe('Multiple globs', () => { + it('accepts an array of globs, like ["my/folder/*/some/file.js", "my/folder/lvl1/*/some/file.js"]', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity( + ['my/folder/*/some/file.js', 'my/folder/lvl1/*/some/file.js'], + testCfg, + ); + + expect(files).to.deep.equal([ + 'my/folder/lvl1/some/file.js', + 'my/folder/lvl1/lvl2/some/file.js', + ]); + }); + + it('accepts negative globs, like ["my/folder/**/some/file.js", "!my/folder/*/some/file.js"]', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity( + ['my/folder/**/some/file.js', '!my/folder/*/some/file.js'], + testCfg, + ); + + expect(files).to.deep.equal([ + 'my/folder/some/file.js', + 'my/folder/lvl1/lvl2/some/file.js', + 'my/folder/lvl1/lvl2/lvl3/some/file.js', + ]); + }); + }); + + describe('Options', () => { + it('"absolute" returns full system paths', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity( + 'my/folder/*/some/file.{js,d.ts}', + { + ...testCfg, + absolute: true, + }, + ); + + if (process.platform === 'win32') { + const driveLetter = path.win32.resolve(testCfg.cwd).slice(0, 1).toUpperCase(); + expect(files).to.deep.equal([ + `${driveLetter}:/fakeFs/my/folder/lvl1/some/file.d.ts`, + `${driveLetter}:/fakeFs/my/folder/lvl1/some/file.js`, + ]); + } else { + expect(files).to.deep.equal([ + '/fakeFs/my/folder/lvl1/some/file.d.ts', + '/fakeFs/my/folder/lvl1/some/file.js', + ]); + } + }); + + it('"cwd" changes relative starting point of glob', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity('folder/*/some/file.{js,d.ts}', { + ...testCfg, + cwd: '/fakeFs/my', + }); + + expect(files).to.deep.equal(['folder/lvl1/some/file.d.ts', 'folder/lvl1/some/file.js']); + }); + + it('"onlyDirectories" returns only directories/folders', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity('my/folder/*/some', { + ...testCfg, + onlyDirectories: true, + }); + + expect(files).to.deep.equal(['my/folder/lvl1/some']); + }); + + it('"onlyFiles" returns only files', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity('my/folder/*/some', { + ...testCfg, + onlyFiles: true, + }); + + expect(files).to.deep.equal([]); + }); + + it('"deep" limits the level of results', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity('my/folder/**', { + ...testCfg, + onlyDirectories: true, + deep: 1, + }); + expect(files).to.deep.equal(['my/folder/lvl1', 'my/folder/some']); + + const files2 = await runOptimisedGlobAndCheckGlobbyParity('my/folder/**', { + ...testCfg, + onlyDirectories: true, + deep: 2, + }); + + expect(files2).to.deep.equal([ + 'my/folder/lvl1', + 'my/folder/some', + 'my/folder/lvl1/lvl2', + 'my/folder/lvl1/some', + ]); + }); + + it('"dot" allows hidden files" ', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity('*/*', { ...testCfg, dot: true }); + + expect(files).to.deep.equal(['my/.hiddenFile.js']); + }); + + it('"ignore" filters out files" ', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity('**', { + ...testCfg, + ignore: ['**/lvl1/**'], + }); + + expect(files).to.deep.equal([ + 'my/folder/some/anotherFile.d.ts', + 'my/folder/some/anotherFile.js', + 'my/folder/some/file.d.ts', + 'my/folder/some/file.js', + ]); + }); + + it.skip('"suppressErrors" throws errors when paths do not exist', async () => { + expect(async () => + optimisedGlob('my/folder/**/some/file.js', { + ...testCfg, + cwd: '/nonExisting/path', // this will not exist + suppressErrors: false, + }), + ).to.throw(); + }); }); }); +} + +runSuiteForOptimisedGlob(); + +describe('Native glob', () => { + const [nodeMajor] = process.versions.node.split('.').map(Number); + if (nodeMajor < 22) { + console.warn('Skipping native glob tests because Node.js version is too low.'); + return; + } + + before(() => { + optimisedGlob.enableExperimentalFsGlob(); + }); + + after(() => { + optimisedGlob.disableExperimentalFsGlob(); + }); + + runSuiteForOptimisedGlob(); });