@synstack/glob
Version:
Glob pattern matching and file filtering utilities
174 lines (156 loc) • 5.44 kB
text/typescript
import { glob as globAsync, globSync, type GlobOptions } from "glob";
import { minimatch } from "minimatch";
interface Options {
includes: string[];
excludes?: string[];
}
/**
* Ensures a glob pattern has a trailing slash to match directories only
* @param glob - The glob pattern to ensure has a trailing slash
* @returns The glob pattern with a trailing slash
*/
export function ensureDirTrailingSlash(glob: string) {
return glob.endsWith("/") ? glob : `${glob}/`;
}
/**
* Allows extracting values from a glob pattern
* @example **\/path/to/(*)/(*).ts => [string, string]
* @returns string[] or null if glob does not match
*
* _Note: glob capturing only works with single "*" widlcards_
*/
export function capture(glob: string, filePath: string) {
const baseRegex = minimatch.makeRe(glob);
if (!baseRegex) throw new InvalidGlobException(glob);
const capturingRegexString = baseRegex.source
.replaceAll("\\(", "(")
.replaceAll("\\)", ")")
.replaceAll("\\\\", "\\");
const regex = new RegExp(capturingRegexString, "g");
const matches = regex.exec(filePath);
if (!matches) return null;
return matches.slice(1);
}
function flatten(array: Array<string> | [Array<string>]): Array<string> {
return Array.isArray(array[0]) ? array[0] : (array as Array<string>);
}
/**
* @param filePath
* @param globs list of globs to match against globs prefixed with ! are excluded
* @returns boolean
*/
export function matches(
filePath: string,
...globs: Array<string> | [Array<string>]
): boolean {
const { includes, excludes } = sort(...globs);
return (
includes.some((globPattern) => minimatch(filePath, globPattern)) &&
!excludes.some((glob) => minimatch(filePath, glob, { dot: true }))
);
}
/**
* Split included and excluded globs, removing the "!" prefix along the way
* @param patterns a list of glob patterns, excluded globs start with "!"
*/
export function sort(...patterns: Array<string> | [Array<string>]) {
const _patterns = flatten(patterns);
const includes = _patterns.filter((glob) => !glob.startsWith("!"));
const excludes = _patterns
.filter((glob) => glob.startsWith("!"))
.map((glob) => glob.replace("!", ""));
return { includes, excludes };
}
/**
* Create a filter function resolving to true if path matches any of the globs
* @param patterns A list of glob patterns
*/
export function filterIncludedFactory(
...patterns: Array<string> | [Array<string>]
) {
return (path: string) =>
flatten(patterns).some((glob) => minimatch(path, glob, { dot: true }));
}
/**
* Create a filter function resolving to true as long as path doesn't match any of the globs
* @param patterns A list of glob patterns
*/
export function filterExcludedFactory(
...patterns: Array<string> | [Array<string>]
) {
return (path: string) =>
flatten(patterns).every((glob) => !minimatch(path, glob, { dot: true }));
}
/**
* Creates a filter function based on glob patterns or GlobOptions
* @param options Array of glob patterns with excluded patterns prefixed with "!" or an object of sorted glob patterns
* @returns A function that takes a path and returns true if it matches the glob patterns and none of the excluded patterns
*/
export function filterFactory(globs: Array<string>): (path: string) => boolean;
export function filterFactory(options: Options): (path: string) => boolean;
export function filterFactory(options: Options | Array<string>) {
const _options = options instanceof Array ? sort(...options) : options;
const filterIncluded = filterIncludedFactory(_options.includes ?? []);
const filterExcluded = filterExcludedFactory(_options.excludes ?? []);
return (path: string) => filterIncluded(path) && filterExcluded(path);
}
export class Glob {
public static cwd(this: void, cwd: string) {
return new Glob(cwd);
}
private readonly _cwd: string;
private readonly _options: GlobOptions;
protected constructor(
cwd: string = ".",
options: GlobOptions = {
nodir: true,
},
) {
this._cwd = cwd;
this._options = options;
}
/**
* Set advanced options for the glob search
* @param options GlobOptions
* @returns A new Glob instance with the updated options
*/
public options(options: GlobOptions) {
return new Glob(this._cwd, options);
}
/**
* Executes a glob search and return the matching files
*/
public find(...patterns: Array<string> | [Array<string>]) {
const _patterns = flatten(patterns);
const { includes, excludes } = sort(_patterns);
return globAsync(includes, {
ignore: excludes,
nodir: this._options.nodir ?? true,
cwd: this._cwd,
...this._options,
}) as Promise<string[]>;
}
/**
* Synchronously executes a glob search and return the matching files
*/
public findSync(...patterns: Array<string> | [Array<string>]) {
const _patterns = flatten(patterns);
const { includes, excludes } = sort(_patterns);
return globSync(includes, {
ignore: excludes,
nodir: this._options.nodir ?? true,
cwd: this._cwd,
...this._options,
}) as string[];
}
}
export class InvalidGlobException extends Error {
constructor(glob: string) {
super(`Invalid glob: ${glob}`);
}
}
/**
* Creates a Glob instance with the provided working directory
* @param cwd Path to the working directory, defaults to the current working directory
*/
export const cwd = Glob.cwd;