Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,5 +438,137 @@ 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, match] of matchersGenerator) {
* for (const file of files) {
* if (!processedFiles.has(file) && match(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<readonly [glob: string, match: (path: string) => 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;

const format = buildFormat(cwd, props.root, inputOptions.absolute);

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;
}
}

export { convertPathToPattern, escapePath, isDynamicPattern } from './utils.ts';
Loading