diff --git a/.changeset/tall-ligers-rhyme.md b/.changeset/tall-ligers-rhyme.md new file mode 100644 index 000000000..d23488037 --- /dev/null +++ b/.changeset/tall-ligers-rhyme.md @@ -0,0 +1,5 @@ +--- +'providence-analytics': patch +--- + +support fullPath notation 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 60dd3f8d2..0d27cebca 100644 --- a/packages-node/providence-analytics/src/program/utils/optimised-glob.js +++ b/packages-node/providence-analytics/src/program/utils/optimised-glob.js @@ -130,6 +130,14 @@ function toUniqueArray(arr) { return Array.from(new Set(arr)); } +/** + * @param {string} glob + * @returns {boolean} + */ +function isRootGlob(glob) { + return glob.startsWith('/') || glob.startsWith('!/') || Boolean(glob.match(/^([A-Z]:\\|\\\\)/)); +} + /** * @param {DirentWithPath} dirent * @param {{cwd:string}} cfg @@ -162,7 +170,10 @@ function postprocessOptions(matchedEntries, options) { } if (options.absolute) { - filteredPaths = filteredPaths.map(f => toPosixPath(path.join(options.cwd, f))); + filteredPaths = filteredPaths.map(f => { + const isRootPath = isRootGlob(f); + return isRootPath ? toPosixPath(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}`); @@ -183,19 +194,17 @@ function postprocessOptions(matchedEntries, options) { const getStartPath = memoize( /** * @param {string} glob + * @returns {string} */ glob => { const reservedChars = ['?', '[', ']', '{', '}', ',', '.', '*']; - let hasFoundReservedChar = false; - return glob - .split('/') - .map(part => { - if (hasFoundReservedChar) return undefined; - hasFoundReservedChar = reservedChars.some(reservedChar => part.includes(reservedChar)); - return hasFoundReservedChar ? undefined : part; - }) - .filter(Boolean) - .join('/'); + const startPathParts = []; + for (const part of glob.split('/')) { + const hasReservedChar = reservedChars.some(reservedChar => part.includes(reservedChar)); + if (hasReservedChar) break; + startPathParts.push(part); + } + return startPathParts.join('/'); }, ); @@ -315,9 +324,12 @@ export async function optimisedGlob(globOrGlobs, providedOptions = {}) { const ignoreGlobs = options.ignore.map((/** @type {string} */ g) => g.startsWith('!') ? g : `!${g}`, ); + const globs = toUniqueArray([...regularGlobs, ...ignoreGlobs]); const optionsNotSupportedByNativeGlob = ['onlyDirectories', 'dot']; - const doesConfigAllowNative = !optionsNotSupportedByNativeGlob.some(opt => options[opt]); + const hasGlobWithFullPath = globs.some(isRootGlob); + const doesConfigAllowNative = + !optionsNotSupportedByNativeGlob.some(opt => options[opt]) && !hasGlobWithFullPath; if (isExperimentalFsGlobEnabled && options.fs.promises.glob && doesConfigAllowNative) { const negativeGlobs = [...ignoreGlobs, ...regularGlobs.filter(r => r.startsWith('!'))].map(r => r.slice(1), @@ -350,8 +362,6 @@ export async function optimisedGlob(globOrGlobs, providedOptions = {}) { ); } - const globs = toUniqueArray([...regularGlobs, ...ignoreGlobs]); - /** @type {RegExp[]} */ const matchRegexesNegative = []; /** @type {RegExp[]} */ @@ -377,11 +387,13 @@ export async function optimisedGlob(globOrGlobs, providedOptions = {}) { // Search for the "deepest" starting point in the filesystem that we can use to search the fs const startPath = getStartPath(globNormalized); - const fullStartPath = path.join(options.cwd, startPath); + const isRootPath = isRootGlob(startPath); + const cwd = isRootPath ? '/' : options.cwd; + const fullStartPath = path.join(cwd, startPath); try { const allDirEntsRelativeToCwd = await getAllDirentsRelativeToCwd(fullStartPath, { - cwd: options.cwd, + cwd, fs: options.fs, }); 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 f0bcb2917..7b7e02edf 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 @@ -40,6 +40,8 @@ async function runOptimisedGlobAndCheckGlobbyParity(patterns, options) { ); } + console.debug({ optimisedGlobResult, globbyResult }); + expect(optimisedGlobResult).to.deep.equal(globbyResult); return optimisedGlobResult; @@ -302,6 +304,38 @@ function runSuiteForOptimisedGlob() { ).to.throw(); }); }); + + describe('Edge cases', () => { + it('allows prefixing a system path (regardless of cwd)', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity( + '/fakeFs/my/folder/*/some/file.{js,d.ts}', + testCfg, + ); + expect(files).to.deep.equal([ + '/fakeFs/my/folder/lvl1/some/file.d.ts', + '/fakeFs/my/folder/lvl1/some/file.js', + ]); + // With non-existing cwd + const files2 = await runOptimisedGlobAndCheckGlobbyParity( + '/fakeFs/my/folder/*/some/file.{js,d.ts}', + { + ...testCfg, + cwd: '/nonExisting/path', // this will not exist + }, + ); + expect(files2).to.deep.equal([ + '/fakeFs/my/folder/lvl1/some/file.d.ts', + '/fakeFs/my/folder/lvl1/some/file.js', + ]); + + // // With negative globs: this is not supported by globby + // const files3 = await runOptimisedGlobAndCheckGlobbyParity( + // ['!/fakeFs/my/**/*.d.ts', '/fakeFs/my/folder/*/some/file.{js,d.ts}'], + // testCfg, + // ); + // expect(files3).to.deep.equal(['/fakeFs/my/folder/lvl1/some/file.js']); + }); + }); }); }