From 858fba92aeb00b59d6c28dca33482a37fc59bc1a Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 26 Sep 2025 18:39:01 +0200 Subject: [PATCH 1/5] feat: add new `compileGlobs` helper function --- src/index.ts | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/src/index.ts b/src/index.ts index d089160..fee5755 100644 --- a/src/index.ts +++ b/src/index.ts @@ -438,5 +438,132 @@ export function globSync(patternsOrOptions: string | readonly string[] | GlobOpt } return formatPaths(crawler.sync(), relative); } +/** + * Compiles glob patterns into matcher functions using the exact same logic as `glob` and `globSync`. + * + * This is an advanced utility function designed to be a **companion** to `glob` and `globSync`. + * Its primary use case is to enable advanced post-processing of the files returned by a scan. + * + * For example, since the order of files from a glob scan is not guaranteed, this function + * provides the necessary tools to implement **deterministic sorting**. By yielding a matcher for + * each original pattern, you can iterate through them in their intended order of precedence + * and sort the results of a `globSync` call accordingly. + * + * This function is key because it uses the **exact same internal pattern normalization and + * option processing as `glob` and `globSync`**. This guarantees that your post-processing + * logic (like sorting) will be perfectly consistent with the file scan that produced the results. + * + * A key benefit of this approach is **decoupling**. Your code only depends on + * the returned matcher's signature `(path: string) => boolean`, not on the + * underlying matching library (currently `picomatch`). If `tinyglobby` were to + * switch to a different matching engine in the future, your code using this + * function would continue to work without any changes. + * + * @param patternsOrOptions The glob pattern(s) or a full `GlobOptions` object. + * @param options The options object if the first argument is the pattern(s). + * @yields A readonly tuple `[glob, matcher]` containing: + * - `glob`: The normalized and processed glob pattern. + * - `matcher`: The pre-compiled matcher function for that specific pattern. + * @returns A generator that yields the `[glob, matcher]` tuples. + * + * @example Implementing deterministic sorting of `globSync` results + * ```javascript + * // Assume the following file structure: + * // /project + * // └── src + * // └── components + * // ├── common + * // │ ├── Button.js + * // │ └── Card.js + * // └── overrides + * // └── Button.js + * + * import { globSync, compileGlobs } from 'tinyglobby'; + * + * // 1. Define your globs and options ONCE. + * // The order of this array defines the desired sorting precedence. + * const globs = [ + * `src/components/overrides/**`, // Highest priority + * 'src/components/common/**', // Normal priority + * ]; + * const options = { cwd: '/project', absolute: true }; + * + * // 2. Scan the filesystem using the defined globs. + * // `globSync` uses the patterns to find files but does not guarantee order. + * const files = globSync(globs, options); + * // Let's assume `files` is now (in a non-deterministic order): + * // [ + * // '/project/src/components/common/Button.js', + * // '/project/src/components/common/Card.js', + * // '/project/src/components/overrides/Button.js' + * // ] + * + * // 3. Compile the exact same globs to get matchers in their intended order. + * const matchersGenerator = compileGlobs(globs, options); + * + * // 4. Use the generated matchers to sort the file list. + * const sortedFiles = []; + * const processedFiles = new Set(); + * + * for (const [glob, matcher] of matchersGenerator) { + * for (const file of files) { + * if (!processedFiles.has(file) && matcher(file)) { + * processedFiles.add(file); + * sortedFiles.push(file); + * } + * } + * } + * + * console.log(sortedFiles); + * // The correctly sorted output, respecting the original glob order: + * // [ + * // '/project/src/components/overrides/Button.js', + * // '/project/src/components/common/Button.js', + * // '/project/src/components/common/Card.js' + * // ] + * ``` + */ +export function* compileGlobs( + patternsOrOptions: string | readonly string[] | GlobOptions, options?: GlobOptions +): Generator boolean], undefined, void> { + if (patternsOrOptions && options?.patterns) { + throw new Error('Cannot pass patterns as both an argument and an option'); + } + + const isModern = isReadonlyArray(patternsOrOptions) || typeof patternsOrOptions === 'string'; + const inputOptions = (isModern ? options : patternsOrOptions) || {}; + const patterns = isModern ? patternsOrOptions : patternsOrOptions.patterns; + + const useOptions = process.env.TINYGLOBBY_DEBUG ? { ...inputOptions, debug: true } : inputOptions; + const cwd = normalizeCwd(useOptions.cwd); + if (useOptions.debug) { + log('globbing with:', { patterns, options: useOptions, cwd }); + } + + const props = { + root: cwd, + commonPath: null, + depthOffset: 0 + }; + + const processed = processPatterns({ ...useOptions, patterns }, cwd, props); + + if (useOptions.debug) { + log('internal processing patterns:', processed); + } + + const matchOptions = { + dot: useOptions.dot, + nobrace: useOptions.braceExpansion === false, + nocase: useOptions.caseSensitiveMatch === false, + noextglob: useOptions.extglob === false, + noglobstar: useOptions.globstar === false, + posix: true + } satisfies PicomatchOptions; + + for (const match of processed.match) { + yield [match, picomatch(match, { ...matchOptions, ignore: processed.ignore })] as const; + } +} export { convertPathToPattern, escapePath, isDynamicPattern } from './utils.ts'; From cf4f3bded1f8f4b94a68cdbf25380a8324d0955b Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 26 Sep 2025 19:31:07 +0200 Subject: [PATCH 2/5] chore: clenup --- src/index.ts | 74 +++++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/index.ts b/src/index.ts index fee5755..861c836 100644 --- a/src/index.ts +++ b/src/index.ts @@ -505,9 +505,9 @@ export function globSync(patternsOrOptions: string | readonly string[] | GlobOpt * const sortedFiles = []; * const processedFiles = new Set(); * - * for (const [glob, matcher] of matchersGenerator) { + * for (const [glob, match] of matchersGenerator) { * for (const file of files) { - * if (!processedFiles.has(file) && matcher(file)) { + * if (!processedFiles.has(file) && match(file)) { * processedFiles.add(file); * sortedFiles.push(file); * } @@ -524,46 +524,48 @@ export function globSync(patternsOrOptions: string | readonly string[] | GlobOpt * ``` */ export function* compileGlobs( - patternsOrOptions: string | readonly string[] | GlobOptions, options?: GlobOptions -): Generator boolean], undefined, void> { - if (patternsOrOptions && options?.patterns) { - throw new Error('Cannot pass patterns as both an argument and an option'); - } + patternsOrOptions: string | readonly string[] | GlobOptions, options?: GlobOptions +): Generator boolean], undefined, void> { + if (patternsOrOptions && options?.patterns) { + throw new Error('Cannot pass patterns as both an argument and an option'); + } - const isModern = isReadonlyArray(patternsOrOptions) || typeof patternsOrOptions === 'string'; - const inputOptions = (isModern ? options : patternsOrOptions) || {}; - const patterns = isModern ? patternsOrOptions : patternsOrOptions.patterns; + const isModern = isReadonlyArray(patternsOrOptions) || typeof patternsOrOptions === 'string'; + const inputOptions = (isModern ? options : patternsOrOptions) || {}; + const patterns = isModern ? patternsOrOptions : patternsOrOptions.patterns; - const useOptions = process.env.TINYGLOBBY_DEBUG ? { ...inputOptions, debug: true } : inputOptions; - const cwd = normalizeCwd(useOptions.cwd); - if (useOptions.debug) { - log('globbing with:', { patterns, options: useOptions, cwd }); - } + const useOptions = process.env.TINYGLOBBY_DEBUG ? { ...inputOptions, debug: true } : inputOptions; + const cwd = normalizeCwd(useOptions.cwd); + if (useOptions.debug) { + log('globbing with:', { patterns, options: useOptions, cwd }); + } - const props = { - root: cwd, - commonPath: null, - depthOffset: 0 - }; + const processed = processPatterns( + { ...useOptions, patterns }, + cwd, + { + root: cwd, + commonPath: null, + depthOffset: 0 + } + ); - const processed = processPatterns({ ...useOptions, patterns }, cwd, props); + if (useOptions.debug) { + log('internal processing patterns:', processed); + } - if (useOptions.debug) { - log('internal processing patterns:', processed); - } + const matchOptions = { + dot: useOptions.dot, + nobrace: useOptions.braceExpansion === false, + nocase: useOptions.caseSensitiveMatch === false, + noextglob: useOptions.extglob === false, + noglobstar: useOptions.globstar === false, + posix: true + } satisfies PicomatchOptions; - const matchOptions = { - dot: useOptions.dot, - nobrace: useOptions.braceExpansion === false, - nocase: useOptions.caseSensitiveMatch === false, - noextglob: useOptions.extglob === false, - noglobstar: useOptions.globstar === false, - posix: true - } satisfies PicomatchOptions; - - for (const match of processed.match) { - yield [match, picomatch(match, { ...matchOptions, ignore: processed.ignore })] as const; - } + for (const match of processed.match) { + yield [match, picomatch(match, { ...matchOptions, ignore: processed.ignore })] as const; + } } export { convertPathToPattern, escapePath, isDynamicPattern } from './utils.ts'; From fc774426f043502fa2a9dab16dbf36378e3588be Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 26 Sep 2025 21:28:53 +0200 Subject: [PATCH 3/5] chore: apply internal logic via buildFormat --- src/index.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 861c836..8d72b26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -540,14 +540,16 @@ export function* compileGlobs( log('globbing with:', { patterns, options: useOptions, cwd }); } + const props = { + root: cwd, + commonPath: null, + depthOffset: 0 + } + const processed = processPatterns( { ...useOptions, patterns }, cwd, - { - root: cwd, - commonPath: null, - depthOffset: 0 - } + props ); if (useOptions.debug) { @@ -563,8 +565,15 @@ export function* compileGlobs( posix: true } satisfies PicomatchOptions; + const format = buildFormat(cwd, props.root, inputOptions.absolute); + for (const match of processed.match) { - yield [match, picomatch(match, { ...matchOptions, ignore: processed.ignore })] as const; + const isMatch = picomatch(match, { ...matchOptions, ignore: processed.ignore }); + + yield [ + match, + (filePath: string): boolean => isMatch(format(filePath, false)) + ] as const; } } From bc64566fea890465e8ce9dd2c3804e2b0188618b Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 26 Sep 2025 21:39:35 +0200 Subject: [PATCH 4/5] chore: fx lint --- src/index.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8d72b26..9e257d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -524,7 +524,8 @@ export function globSync(patternsOrOptions: string | readonly string[] | GlobOpt * ``` */ export function* compileGlobs( - patternsOrOptions: string | readonly string[] | GlobOptions, options?: GlobOptions + patternsOrOptions: string | readonly string[] | GlobOptions, + options?: GlobOptions ): Generator boolean], undefined, void> { if (patternsOrOptions && options?.patterns) { throw new Error('Cannot pass patterns as both an argument and an option'); @@ -546,11 +547,7 @@ export function* compileGlobs( depthOffset: 0 } - const processed = processPatterns( - { ...useOptions, patterns }, - cwd, - props - ); + const processed = processPatterns({ ...useOptions, patterns }, cwd, props); if (useOptions.debug) { log('internal processing patterns:', processed); @@ -570,10 +567,7 @@ export function* compileGlobs( for (const match of processed.match) { const isMatch = picomatch(match, { ...matchOptions, ignore: processed.ignore }); - yield [ - match, - (filePath: string): boolean => isMatch(format(filePath, false)) - ] as const; + yield [match, (filePath: string): boolean => isMatch(format(filePath, false))] as const; } } From 4a7c5059c14c3370e480e865f1cc53f3150ff1eb Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 26 Sep 2025 21:49:20 +0200 Subject: [PATCH 5/5] chore: . --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 9e257d7..60682c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -545,7 +545,7 @@ export function* compileGlobs( root: cwd, commonPath: null, depthOffset: 0 - } + }; const processed = processPatterns({ ...useOptions, patterns }, cwd, props);