feat(providence): experimental support for fs.glob + ignore support in optimisedGlob
This commit is contained in:
parent
370b357bd3
commit
2dbb1ca7bc
3 changed files with 416 additions and 254 deletions
6
.changeset/forty-hotels-watch.md
Normal file
6
.changeset/forty-hotels-watch.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
'providence-analytics': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
- support `ignore: string[]` globs in optimisedGlob
|
||||||
|
- experimental `fs.glob` support under the hood in optimisedGlob
|
||||||
|
|
@ -7,9 +7,21 @@ import { toPosixPath } from './to-posix-path.js';
|
||||||
import { memoize } from './memoize.js';
|
import { memoize } from './memoize.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @typedef {nodeFs.Dirent & { path:string; parentPath:string }} DirentWithPath
|
||||||
* @typedef {nodeFs} FsLike
|
* @typedef {nodeFs} FsLike
|
||||||
* @typedef {nodeFs.Dirent & {path:string;parentPath:string}} DirentWithPath
|
* @typedef {{
|
||||||
* @typedef {{onlyDirectories:boolean;onlyFiles:boolean;deep:number;suppressErrors:boolean;fs: FsLike;cwd:string;absolute:boolean;extglob:boolean;}} FastGlobtions
|
* 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);
|
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(
|
const getStartPath = memoize(
|
||||||
/**
|
/**
|
||||||
* @param {string} glob
|
* @param {string} glob
|
||||||
|
|
@ -129,6 +200,7 @@ const getStartPath = memoize(
|
||||||
);
|
);
|
||||||
|
|
||||||
let isCacheEnabled = false;
|
let isCacheEnabled = false;
|
||||||
|
let isExperimentalFsGlobEnabled = false;
|
||||||
/** @type {{[path:string]:DirentWithPath[]}} */
|
/** @type {{[path:string]:DirentWithPath[]}} */
|
||||||
const cache = {};
|
const cache = {};
|
||||||
|
|
||||||
|
|
@ -179,15 +251,34 @@ const getAllDirentsRelativeToCwd = memoize(
|
||||||
});
|
});
|
||||||
|
|
||||||
const allDirEntsRelativeToCwd = allDirentsRelativeToStartPath.map(dirent => ({
|
const allDirEntsRelativeToCwd = allDirentsRelativeToStartPath.map(dirent => ({
|
||||||
relativeToCwdPath: toPosixPath(
|
relativeToCwdPath: direntToLocalPath(dirent, { cwd: options.cwd }),
|
||||||
path.join(dirent.parentPath || dirent.path, dirent.name),
|
|
||||||
).replace(`${toPosixPath(options.cwd)}/`, ''),
|
|
||||||
dirent,
|
dirent,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return allDirEntsRelativeToCwd;
|
return allDirEntsRelativeToCwd;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string|string[]} globOrGlobs
|
||||||
|
* @param {{ fs: FsLike; cwd: string; exclude?: Function; stats?: boolean }} cfg
|
||||||
|
* @returns {Promise<string[]|DirentWithPath[]>}
|
||||||
|
*/
|
||||||
|
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.
|
* 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.
|
* 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,
|
unique: true,
|
||||||
sync: false,
|
sync: false,
|
||||||
dot: 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
|
// https://github.com/mrmlnc/fast-glob?tab=readme-ov-file
|
||||||
...providedOptions,
|
...providedOptions,
|
||||||
};
|
};
|
||||||
|
|
@ -219,7 +311,46 @@ export async function optimisedGlob(globOrGlobs, providedOptions = {}) {
|
||||||
options.onlyDirectories = true;
|
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[]} */
|
/** @type {RegExp[]} */
|
||||||
const matchRegexesNegative = [];
|
const matchRegexesNegative = [];
|
||||||
|
|
@ -269,37 +400,7 @@ export async function optimisedGlob(globOrGlobs, providedOptions = {}) {
|
||||||
!matchRegexesNegative.some(globReNeg => globReNeg.test(globEntry.relativeToCwdPath)),
|
!matchRegexesNegative.some(globReNeg => globReNeg.test(globEntry.relativeToCwdPath)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const allFileOrDirectoryEntries = matchedEntries.filter(({ dirent }) =>
|
const res = postprocessOptions(matchedEntries, options);
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
// It could happen the fs changes with the next call, so we clear the cache
|
// It could happen the fs changes with the next call, so we clear the cache
|
||||||
getAllDirentsRelativeToCwd.clearCache();
|
getAllDirentsRelativeToCwd.clearCache();
|
||||||
|
|
@ -311,3 +412,11 @@ export async function optimisedGlob(globOrGlobs, providedOptions = {}) {
|
||||||
optimisedGlob.disableCache = () => {
|
optimisedGlob.disableCache = () => {
|
||||||
isCacheEnabled = false;
|
isCacheEnabled = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
optimisedGlob.enableExperimentalFsGlob = () => {
|
||||||
|
isExperimentalFsGlobEnabled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
optimisedGlob.disableExperimentalFsGlob = () => {
|
||||||
|
isExperimentalFsGlobEnabled = false;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { globby } from 'globby';
|
import { globby } from 'globby';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// @ts-expect-error
|
||||||
import mockFs from 'mock-fs';
|
import mockFs from 'mock-fs';
|
||||||
|
|
||||||
import { optimisedGlob } from '../../../src/program/utils/optimised-glob.js';
|
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');
|
const measurePerf = process.argv.includes('--measure-perf');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {*} patterns
|
* @typedef {import('../../../src/program/utils/optimised-glob.js').FastGlobtions} FastGlobtions
|
||||||
* @param {*} options
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string|string[]} patterns
|
||||||
|
* @param {Partial<FastGlobtions>} options
|
||||||
* @returns {Promise<string[]>}
|
* @returns {Promise<string[]>}
|
||||||
*/
|
*/
|
||||||
async function runOptimisedGlobAndCheckGlobbyParity(patterns, options) {
|
async function runOptimisedGlobAndCheckGlobbyParity(patterns, options) {
|
||||||
|
|
@ -43,237 +45,282 @@ async function runOptimisedGlobAndCheckGlobbyParity(patterns, options) {
|
||||||
return optimisedGlobResult;
|
return optimisedGlobResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('optimisedGlob', () => {
|
function runSuiteForOptimisedGlob() {
|
||||||
const testCfg = {
|
describe('optimisedGlob', () => {
|
||||||
cwd: '/fakeFs',
|
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',
|
|
||||||
};
|
};
|
||||||
mockFs(fakeFs);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
beforeEach(() => {
|
||||||
mockFs.restore();
|
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', () => {
|
'/fakeFs/my/folder/some/anotherFile.js': 'content',
|
||||||
it('supports double asterisk like "my/folder/**/some/file.js" ', async () => {
|
'/fakeFs/my/folder/lvl1/some/anotherFile.js': 'content',
|
||||||
const files = await runOptimisedGlobAndCheckGlobbyParity(
|
'/fakeFs/my/folder/lvl1/lvl2/some/anotherFile.js': 'content',
|
||||||
'my/folder/**/some/file.js',
|
'/fakeFs/my/folder/lvl1/lvl2/lvl3/some/anotherFile.js': 'content',
|
||||||
testCfg,
|
'/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([
|
'/fakeFs/my/.hiddenFile.js': 'content',
|
||||||
'my/folder/some/file.js',
|
};
|
||||||
'my/folder/lvl1/some/file.js',
|
mockFs(fakeFs);
|
||||||
'my/folder/lvl1/lvl2/some/file.js',
|
|
||||||
'my/folder/lvl1/lvl2/lvl3/some/file.js',
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports single asterisk like "my/folder/*/some/file.js" ', async () => {
|
afterEach(() => {
|
||||||
const files = await runOptimisedGlobAndCheckGlobbyParity('my/folder/*/some/file.js', testCfg);
|
mockFs.restore();
|
||||||
|
|
||||||
expect(files).to.deep.equal(['my/folder/lvl1/some/file.js']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports filenames like "my/folder/lvl1/some/*il*.js" ', async () => {
|
describe('Star patterns', () => {
|
||||||
const files = await runOptimisedGlobAndCheckGlobbyParity(
|
it('supports double asterisk like "my/folder/**/some/file.js" ', async () => {
|
||||||
'my/folder/lvl1/some/*il*.js',
|
const files = await runOptimisedGlobAndCheckGlobbyParity(
|
||||||
testCfg,
|
'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([
|
expect(files).to.deep.equal([
|
||||||
`${driveLetter}:/fakeFs/my/folder/lvl1/some/file.d.ts`,
|
'my/folder/some/file.js',
|
||||||
`${driveLetter}:/fakeFs/my/folder/lvl1/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([
|
expect(files).to.deep.equal([
|
||||||
'/fakeFs/my/folder/lvl1/some/file.d.ts',
|
'my/folder/lvl1/some/anotherFile.js',
|
||||||
'/fakeFs/my/folder/lvl1/some/file.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 () => {
|
expect(filesDoubleStar).to.deep.equal([
|
||||||
const files = await runOptimisedGlobAndCheckGlobbyParity('my/folder/*/some', {
|
'my/folder/some/file.js',
|
||||||
...testCfg,
|
'my/folder/lvl1/some/file.js',
|
||||||
onlyDirectories: true,
|
'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('gives empty output when location does not exist" ', async () => {
|
||||||
});
|
const files = await runOptimisedGlobAndCheckGlobbyParity('my/folder/**/some/file.js', {
|
||||||
|
|
||||||
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', {
|
|
||||||
...testCfg,
|
...testCfg,
|
||||||
cwd: '/nonExisting/path', // this will not exist
|
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();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue