tiny-readdir-glob
Version:
A simple promisified recursive readdir function, with support for globs.
268 lines (138 loc) • 5.38 kB
text/typescript
/* IMPORT */
import path from 'node:path';
import zeptomatch from 'zeptomatch';
import {explodeStart, explodeEnd} from 'zeptomatch-explode';
import isStatic from 'zeptomatch-is-static';
import unescape from 'zeptomatch-unescape';
import type {ArrayMaybe} from './types';
/* MAIN */
const castArray = <T> ( value: T | T[] ): T[] => {
return Array.isArray ( value ) ? value : [value];
};
const globExplode = ( glob: string ): [paths: string[], glob: string] => {
if ( isStatic ( glob ) ) { // Handling it as a relative path, not a glob
return [[unescape ( glob )], '**/*'];
} else { // Handling it as an actual glob
const {statics, dynamic} = explodeStart ( glob );
return [statics, dynamic];
}
};
const globsExplode = ( globs: string[] ): [paths: string[], globs: string[]][] => { //TODO: Optimize this more, avoiding searching both in src/ and src/foo with the same glob
const results: [string[], string[]][] = [];
for ( const glob of globs ) {
const [paths, pathsGlob] = globExplode ( glob );
if ( !paths.length ) {
paths.push ( '' );
}
for ( const path of paths ) {
const existing = results.find ( result => result[0].includes ( path ) );
if ( existing ) {
if ( !existing[1].includes ( pathsGlob ) ) {
existing[1].push ( pathsGlob );
}
} else {
results.push ([ [path], [pathsGlob] ]);
}
}
}
return results;
};
const globCompile = ( glob: string ): (( rootPath: string, targetPath: string ) => boolean) => { //TODO: Optimize this more, accounting for more scenarios
if ( !glob || glob === '**/*' ) { // Trivial case
return () => true;
}
const {flexibleStart, flexibleEnd, statics, dynamic} = explodeEnd ( glob );
if ( dynamic === '**/*' && statics.length && !flexibleEnd ) { // Optimized case
return ( rootPath: string, targetPath: string ): boolean => {
for ( let i = 0, l = statics.length; i < l; i++ ) {
const end = statics[i];
if ( !targetPath.endsWith ( end ) ) continue;
if ( flexibleStart ) return true;
if ( targetPath.length === end.length ) return true;
if ( isPathSep ( targetPath[targetPath.length - end.length - 1 ] ) ) return true;
}
return false;
};
} else { // Unoptimized case
const re = zeptomatch.compile ( glob );
return ( rootPath: string, targetPath: string ): boolean => {
return re.test ( path.relative ( rootPath, targetPath ) );
};
}
};
const globsCompile = ( globs: string[] ): (( rootPath: string, targetPath: string ) => boolean) => {
const fns = globs.map ( globCompile );
return ( rootPath, targetPath ) => fns.some ( fn => fn ( rootPath, targetPath ) );
};
const globsPartition = ( globs: string[] ): [positives: string[], negatives: string[]] => {
const positives: string[] = [];
const negatives: string[] = [];
const bangsRe = /^!+/;
if ( globs.length ) {
for ( const glob of globs ) {
const match = glob.match ( bangsRe );
if ( match ) {
const bangsNr = match[0].length;
const bucket = bangsNr % 2 === 0 ? positives : negatives;
bucket.push ( glob.slice ( bangsNr ) );
} else {
positives.push ( glob );
}
}
if ( !positives.length ) {
positives.push ( '**' );
}
}
return [positives, negatives];
};
const ignoreCompile = ( rootPath: string, ignore?: ArrayMaybe<(( targetPath: string ) => boolean) | RegExp | string> ): ArrayMaybe<(( targetPath: string ) => boolean) | RegExp> | undefined => {
if ( !ignore ) return;
return castArray ( ignore ).map ( ignore => {
if ( !isString ( ignore ) ) return ignore;
const fn = globCompile ( ignore );
return ( targetPath: string ) => fn ( rootPath, targetPath );
});
};
const intersection = <T> ( sets: Set<T>[] ): Set<T> => {
if ( sets.length === 1 ) return sets[0];
const result = new Set<T> ();
for ( let i = 0, l = sets.length; i < l; i++ ) {
const set = sets[i];
for ( const value of set ) {
result.add ( value );
}
}
return result;
};
const isPathSep = ( char: string ): boolean => {
return char === '/' || char === '\\';
};
const isString = ( value: unknown ): value is string => {
return typeof value === 'string';
};
const uniq = <T> ( values: T[] ): T[] => {
if ( values.length < 2 ) return values;
return Array.from ( new Set ( values ) );
};
const uniqFlat = <T> ( values: T[][] ): T[] => {
if ( values.length === 1 ) return values[0];
return uniq ( values.flat () );
};
const uniqMergeConcat = <T> ( values: Record<string, T[]>[] ): Record<string, T[]> => {
if ( values.length === 1 ) return values[0];
const merged: Record<string, T[]> = {};
for ( let i = 0, l = values.length; i < l; i++ ) {
const value = values[i];
for ( const key in value ) {
const prev = merged[key];
const next = Array.isArray ( prev ) ? prev.concat ( value[key] ) : value[key];
merged[key] = next;
}
}
for ( const key in merged ) {
merged[key] = uniq ( merged[key] );
}
return merged;
};
/* EXPORT */
export {castArray, globExplode, globsExplode, globCompile, globsCompile, globsPartition, ignoreCompile, intersection, isPathSep, isString, uniq, uniqFlat, uniqMergeConcat};