diff --git a/.changeset/rude-tips-smash.md b/.changeset/rude-tips-smash.md new file mode 100644 index 000000000..bd7ccf6f0 --- /dev/null +++ b/.changeset/rude-tips-smash.md @@ -0,0 +1,5 @@ +--- +'providence-analytics': patch +--- + +fix normalization of native glob dirent.parentPath for 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 0d27cebca..2e49a9509 100644 --- a/packages-node/providence-analytics/src/program/utils/optimised-glob.js +++ b/packages-node/providence-analytics/src/program/utils/optimised-glob.js @@ -144,7 +144,13 @@ function isRootGlob(glob) { * @returns {string} */ function direntToLocalPath(dirent, { cwd }) { - const folder = (dirent.parentPath || dirent.path).replace(/(^.*)\/(.*\..*$)/, '$1'); + const parentPath = toPosixPath(dirent.parentPath || dirent.path); + // Since `fs.glob` can return parent paths with files included, we need to strip the file part + const parts = parentPath.split('/'); + const lastPart = parts[parts.length - 1]; + const isLastPartFile = !lastPart.startsWith('.') && lastPart.split('.').length > 1; + const folder = isLastPartFile ? parts.slice(0, parts.length - 1).join('/') : parentPath; + return toPosixPath(path.join(folder, dirent.name)).replace( new RegExp(`^${toPosixPath(cwd)}/`), '', @@ -270,7 +276,7 @@ const getAllDirentsRelativeToCwd = memoize( /** * @param {string|string[]} globOrGlobs - * @param {{ fs: FsLike; cwd: string; exclude?: Function; stats?: boolean }} cfg + * @param {Partial & {exclude?:Function; stats: boolean; cwd:string}} cfg * @returns {Promise} */ async function nativeGlob(globOrGlobs, { fs, cwd, exclude, stats }) { @@ -288,6 +294,54 @@ async function nativeGlob(globOrGlobs, { fs, cwd, exclude, stats }) { return results; } +/** + * + * @param {{globs: string[]; ignoreGlobs: string[]; options: FastGlobtions; regularGlobs:string[]}} config + * @returns {Promise} + */ +async function getNativeGlobResults({ globs, options, ignoreGlobs, regularGlobs }) { + const optionsNotSupportedByNativeGlob = ['onlyDirectories', 'dot']; + const hasGlobWithFullPath = globs.some(isRootGlob); + const doesConfigAllowNative = + !optionsNotSupportedByNativeGlob.some(opt => options[opt]) && !hasGlobWithFullPath; + + if (!doesConfigAllowNative) return undefined; + + const negativeGlobs = [...ignoreGlobs, ...regularGlobs.filter(r => r.startsWith('!'))].map(r => + r.slice(1), + ); + + const negativeResults = negativeGlobs.length + ? // @ts-ignore + /** @type {string[]} */ (await nativeGlob(negativeGlobs, options)) + : []; + const positiveGlobs = regularGlobs.filter(r => !r.startsWith('!')); + + const result = /** @type {DirentWithPath[]} */ ( + await nativeGlob(positiveGlobs, { + cwd: /** @type {string} */ (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: /** @type {string} */ (options.cwd) }), + ), + ); + + return postprocessOptions( + direntsFiltered.map(dirent => ({ + dirent, + relativeToCwdPath: direntToLocalPath(dirent, { cwd: /** @type {string} */ (options.cwd) }), + })), + options, + ); +} + /** * 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. @@ -326,40 +380,10 @@ export async function optimisedGlob(globOrGlobs, providedOptions = {}) { ); const globs = toUniqueArray([...regularGlobs, ...ignoreGlobs]); - const optionsNotSupportedByNativeGlob = ['onlyDirectories', 'dot']; - 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), - ); - - 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, - ); + if (isExperimentalFsGlobEnabled && options.fs?.promises.glob) { + const params = { regularGlobs, ignoreGlobs, options, globs }; + const nativeGlobResults = await getNativeGlobResults(params); + if (nativeGlobResults) return nativeGlobResults; } /** @type {RegExp[]} */ @@ -390,7 +414,6 @@ export async function optimisedGlob(globOrGlobs, providedOptions = {}) { const isRootPath = isRootGlob(startPath); const cwd = isRootPath ? '/' : options.cwd; const fullStartPath = path.join(cwd, startPath); - try { const allDirEntsRelativeToCwd = await getAllDirentsRelativeToCwd(fullStartPath, { cwd, 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 7b7e02edf..a6b0683ef 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,8 +40,6 @@ async function runOptimisedGlobAndCheckGlobbyParity(patterns, options) { ); } - console.debug({ optimisedGlobResult, globbyResult }); - expect(optimisedGlobResult).to.deep.equal(globbyResult); return optimisedGlobResult; @@ -74,6 +72,7 @@ function runSuiteForOptimisedGlob() { '/fakeFs/my/folder/lvl1/lvl2/lvl3/some/anotherFile.d.ts': 'content', '/fakeFs/my/.hiddenFile.js': 'content', + '/fakeFs/my/.hiddenFolder/file.js': 'content', }; mockFs(fakeFs); }); @@ -275,9 +274,17 @@ function runSuiteForOptimisedGlob() { }); it('"dot" allows hidden files" ', async () => { - const files = await runOptimisedGlobAndCheckGlobbyParity('*/*', { ...testCfg, dot: true }); - + const files = await runOptimisedGlobAndCheckGlobbyParity('*/*', { + ...testCfg, + dot: true, + }); expect(files).to.deep.equal(['my/.hiddenFile.js']); + + const files2 = await runOptimisedGlobAndCheckGlobbyParity('*/*/*', { + ...testCfg, + dot: true, + }); + expect(files2).to.deep.equal(['my/.hiddenFolder/file.js']); }); it('"ignore" filters out files" ', async () => { @@ -335,6 +342,15 @@ function runSuiteForOptimisedGlob() { // ); // expect(files3).to.deep.equal(['/fakeFs/my/folder/lvl1/some/file.js']); }); + + it('starts from hidden folders', async () => { + const files = await runOptimisedGlobAndCheckGlobbyParity('**/*', { + ...testCfg, + cwd: '/fakeFs/my/.hiddenFolder', + dot: true, + }); + expect(files).to.deep.equal(['file.js']); + }); }); }); }