From 196dde21110070dcd6fc5c539178e770b93bb947 Mon Sep 17 00:00:00 2001 From: Torathion Date: Sun, 16 Feb 2025 22:50:12 +0100 Subject: [PATCH 1/9] change: overhaul fdir options building --- src/fdir.ts | 82 ++++++++++++++++++++++++++++++ src/index.ts | 137 +++------------------------------------------------ src/types.ts | 30 +++++++++++ src/utils.ts | 7 +-- 4 files changed, 120 insertions(+), 136 deletions(-) create mode 100644 src/fdir.ts create mode 100644 src/types.ts diff --git a/src/fdir.ts b/src/fdir.ts new file mode 100644 index 0000000..9ccec12 --- /dev/null +++ b/src/fdir.ts @@ -0,0 +1,82 @@ +import { posix } from 'node:path'; +import { fdir, type PathsOutput } from 'fdir'; +import type { APIBuilder } from 'fdir/dist/builder/api-builder'; +import picomatch from "picomatch"; +import type { GlobOptions, InternalProps, PartialMatcherOptions, ProcessedPatterns } from "./types"; +import { getPartialMatcher, log } from "./utils.ts"; + +// #region getRelativePath +// TODO: this is slow, find a better way to do this +export function getRelativePath(path: string, cwd: string, root: string): string { + return posix.relative(cwd, `${root}/${path}`) || '.'; +} +// #endregion + +// #region processPath +function processPath(path: string, cwd: string, root: string, isDirectory: boolean, absolute?: boolean) { + const relativePath = absolute ? path.slice(root.length + 1) || '.' : path; + + if (root === cwd) { + return isDirectory && relativePath !== '.' ? relativePath.slice(0, -1) : relativePath; + } + + return getRelativePath(relativePath, cwd, root); +} +// #endregion processPath + +// #region formatPaths +export function formatPaths(paths: string[], cwd: string, root: string): string[] { + for (let i = paths.length - 1; i >= 0; i--) { + const path = paths[i]; + paths[i] = getRelativePath(path, cwd, root) + (!path || path.endsWith('/') ? '/' : ''); + } + return paths; +} +// #endregion formatPaths + +// #region buildFdir +export function buildFdir(options: GlobOptions, props: InternalProps, processed: ProcessedPatterns, cwd: string, root: string): APIBuilder { + const nocase = options.caseSensitiveMatch === false; + + const matcher = picomatch(processed.match, { + dot: options.dot, + nocase, + ignore: processed.ignore + }); + + const partialMatcherOptions: PartialMatcherOptions = { dot: options.dot, nocase }; + const ignore = picomatch(processed.ignore, partialMatcherOptions); + const partialMatcher = getPartialMatcher(processed.match, partialMatcherOptions); + + const { absolute, onlyDirectories, debug } = options + const followSymlinks = options.followSymbolicLinks === false; + + return new fdir({ + filters: [(p, isDirectory) => { + const path = processPath(p, cwd, root, isDirectory, absolute); + const matches = matcher(path); + if (debug && matches) { + log(`matched ${path}`); + } + return matches; + }], + exclude: (_, p) => { + const relativePath = processPath(p, cwd, root, true, true); + const skipped = (relativePath !== '.' && !partialMatcher(relativePath)) || ignore(relativePath); + if (debug && !skipped) { + log(`crawling ${p}`); + } + return skipped; + }, + pathSeparator: '/', + relativePaths: !absolute, + resolvePaths: absolute, + includeBasePath: absolute, + resolveSymlinks: !followSymlinks, + excludeSymlinks: followSymlinks, + excludeFiles: onlyDirectories, + includeDirs: onlyDirectories || options.onlyFiles === false, + maxDepth: options.deep && Math.round(options.deep - props.depthOffset) + }).crawl(root); +} +// #endregion buildFdir diff --git a/src/index.ts b/src/index.ts index 9def92f..2a96438 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,33 +1,12 @@ import path, { posix } from 'node:path'; -import { type Options as FdirOptions, fdir } from 'fdir'; -import picomatch from 'picomatch'; -import { escapePath, getPartialMatcher, isDynamicPattern, log, splitPattern } from './utils.ts'; +import { escapePath, isDynamicPattern, log, splitPattern } from './utils.ts'; +import type { GlobOptions, InternalProps, ProcessedPatterns } from './types.ts'; +import { buildFdir, formatPaths } from './fdir.ts'; const PARENT_DIRECTORY = /^(\/?\.\.)+/; const ESCAPING_BACKSLASHES = /\\(?=[()[\]{}!*+?@|])/g; const BACKSLASHES = /\\/g; -export interface GlobOptions { - absolute?: boolean; - cwd?: string; - patterns?: string | string[]; - ignore?: string | string[]; - dot?: boolean; - deep?: number; - followSymbolicLinks?: boolean; - caseSensitiveMatch?: boolean; - expandDirectories?: boolean; - onlyDirectories?: boolean; - onlyFiles?: boolean; - debug?: boolean; -} - -interface InternalProps { - root: string; - commonPath: string[] | null; - depthOffset: number; -} - function normalizePattern( pattern: string, expandDirectories: boolean, @@ -92,7 +71,7 @@ function processPatterns( { patterns, ignore = [], expandDirectories = true }: GlobOptions, cwd: string, props: InternalProps -) { +): ProcessedPatterns { if (typeof patterns === 'string') { patterns = [patterns]; } else if (!patterns) { @@ -131,29 +110,6 @@ function processPatterns( return { match: matchPatterns, ignore: ignorePatterns }; } -// TODO: this is slow, find a better way to do this -function getRelativePath(path: string, cwd: string, root: string) { - return posix.relative(cwd, `${root}/${path}`) || '.'; -} - -function processPath(path: string, cwd: string, root: string, isDirectory: boolean, absolute?: boolean) { - const relativePath = absolute ? path.slice(root.length + 1) || '.' : path; - - if (root === cwd) { - return isDirectory && relativePath !== '.' ? relativePath.slice(0, -1) : relativePath; - } - - return getRelativePath(relativePath, cwd, root); -} - -function formatPaths(paths: string[], cwd: string, root: string) { - for (let i = paths.length - 1; i >= 0; i--) { - const path = paths[i]; - paths[i] = getRelativePath(path, cwd, root) + (!path || path.endsWith('/') ? '/' : ''); - } - return paths; -} - function crawl(options: GlobOptions, cwd: string, sync: false): Promise; function crawl(options: GlobOptions, cwd: string, sync: true): string[]; function crawl(options: GlobOptions, cwd: string, sync: boolean) { @@ -169,101 +125,22 @@ function crawl(options: GlobOptions, cwd: string, sync: boolean) { return sync ? [] : Promise.resolve([]); } - const props = { + const props: InternalProps = { root: cwd, commonPath: null, depthOffset: 0 }; const processed = processPatterns(options, cwd, props); - const nocase = options.caseSensitiveMatch === false; - - if (options.debug) { - log('internal processing patterns:', processed); - } - - const matcher = picomatch(processed.match, { - dot: options.dot, - nocase, - ignore: processed.ignore - }); - - const ignore = picomatch(processed.ignore, { - dot: options.dot, - nocase - }); - - const partialMatcher = getPartialMatcher(processed.match, { - dot: options.dot, - nocase - }); - - const fdirOptions: Partial = { - // use relative paths in the matcher - filters: [ - options.debug - ? (p, isDirectory) => { - const path = processPath(p, cwd, props.root, isDirectory, options.absolute); - const matches = matcher(path); - - if (matches) { - log(`matched ${path}`); - } - - return matches; - } - : (p, isDirectory) => matcher(processPath(p, cwd, props.root, isDirectory, options.absolute)) - ], - exclude: options.debug - ? (_, p) => { - const relativePath = processPath(p, cwd, props.root, true, true); - const skipped = (relativePath !== '.' && !partialMatcher(relativePath)) || ignore(relativePath); - - if (!skipped) { - log(`crawling ${p}`); - } - - return skipped; - } - : (_, p) => { - const relativePath = processPath(p, cwd, props.root, true, true); - return (relativePath !== '.' && !partialMatcher(relativePath)) || ignore(relativePath); - }, - pathSeparator: '/', - relativePaths: true, - resolveSymlinks: true - }; - - if (options.deep) { - fdirOptions.maxDepth = Math.round(options.deep - props.depthOffset); - } - - if (options.absolute) { - fdirOptions.relativePaths = false; - fdirOptions.resolvePaths = true; - fdirOptions.includeBasePath = true; - } - - if (options.followSymbolicLinks === false) { - fdirOptions.resolveSymlinks = false; - fdirOptions.excludeSymlinks = true; - } - - if (options.onlyDirectories) { - fdirOptions.excludeFiles = true; - fdirOptions.includeDirs = true; - } else if (options.onlyFiles === false) { - fdirOptions.includeDirs = true; - } - props.root = props.root.replace(BACKSLASHES, ''); const root = props.root; if (options.debug) { + log('internal processing patterns:', processed); log('internal properties:', props); } - const api = new fdir(fdirOptions).crawl(root); + const api = buildFdir(options, props, processed, cwd, root); if (cwd === root || options.absolute) { return sync ? api.sync() : api.withPromise(); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..38f0c79 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,30 @@ +export interface GlobOptions { + absolute?: boolean; + cwd?: string; + patterns?: string | string[]; + ignore?: string | string[]; + dot?: boolean; + deep?: number; + followSymbolicLinks?: boolean; + caseSensitiveMatch?: boolean; + expandDirectories?: boolean; + onlyDirectories?: boolean; + onlyFiles?: boolean; + debug?: boolean; +} + +export interface InternalProps { + root: string; + commonPath: string[] | null; + depthOffset: number; +} + +export interface ProcessedPatterns { + match: string[]; + ignore: string[]; +} + +export interface PartialMatcherOptions { + dot?: boolean; + nocase?: boolean; +} diff --git a/src/utils.ts b/src/utils.ts index 275789b..56610df 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,10 +1,5 @@ import picomatch, { type Matcher } from 'picomatch'; - -// #region PARTIAL MATCHER -export interface PartialMatcherOptions { - dot?: boolean; - nocase?: boolean; -} +import type { PartialMatcherOptions } from './types'; // the result of over 4 months of figuring stuff out and a LOT of help export function getPartialMatcher(patterns: string[], options?: PartialMatcherOptions): Matcher { From 5381f1adaec5b57172e7ea82ce92e8085f0ca603 Mon Sep 17 00:00:00 2001 From: Torathion Date: Sun, 16 Feb 2025 22:52:29 +0100 Subject: [PATCH 2/9] perf: optimize getPartialMatcher --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 56610df..1bf4751 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -20,7 +20,7 @@ export function getPartialMatcher(patterns: string[], options?: PartialMatcherOp return (input: string) => { // no need to `splitPattern` as this is indeed not a pattern const inputParts = input.split('/'); - for (let i = 0; i < patterns.length; i++) { + for (let i = 0; i < patternsCount; i++) { const patternParts = patternsParts[i]; const regex = regexes[i]; const inputPatternCount = inputParts.length; From 7b92ffa6331f74f895709c2ef70363b06cc5c652 Mon Sep 17 00:00:00 2001 From: Torathion Date: Wed, 19 Feb 2025 16:06:03 +0100 Subject: [PATCH 3/9] perf: overhaul tinyglobby options building --- src/fdir.ts | 76 ++++++++++++++++++++++++++-------------------------- src/index.ts | 76 ++++++++++++++++++++++++---------------------------- src/types.ts | 10 ++++--- 3 files changed, 79 insertions(+), 83 deletions(-) diff --git a/src/fdir.ts b/src/fdir.ts index 9ccec12..8c698b1 100644 --- a/src/fdir.ts +++ b/src/fdir.ts @@ -36,47 +36,47 @@ export function formatPaths(paths: string[], cwd: string, root: string): string[ // #region buildFdir export function buildFdir(options: GlobOptions, props: InternalProps, processed: ProcessedPatterns, cwd: string, root: string): APIBuilder { - const nocase = options.caseSensitiveMatch === false; + const nocase = options.caseSensitiveMatch === false; - const matcher = picomatch(processed.match, { - dot: options.dot, - nocase, - ignore: processed.ignore - }); + const matcher = picomatch(processed.match, { + dot: options.dot, + nocase, + ignore: processed.ignore + }); - const partialMatcherOptions: PartialMatcherOptions = { dot: options.dot, nocase }; - const ignore = picomatch(processed.ignore, partialMatcherOptions); - const partialMatcher = getPartialMatcher(processed.match, partialMatcherOptions); + const partialMatcherOptions: PartialMatcherOptions = { dot: options.dot, nocase }; + const ignore = picomatch(processed.ignore, partialMatcherOptions); + const partialMatcher = getPartialMatcher(processed.match, partialMatcherOptions); - const { absolute, onlyDirectories, debug } = options - const followSymlinks = options.followSymbolicLinks === false; + const { absolute, onlyDirectories, debug } = options + const followSymlinks = options.followSymbolicLinks === false; - return new fdir({ - filters: [(p, isDirectory) => { - const path = processPath(p, cwd, root, isDirectory, absolute); - const matches = matcher(path); - if (debug && matches) { - log(`matched ${path}`); - } - return matches; - }], - exclude: (_, p) => { - const relativePath = processPath(p, cwd, root, true, true); - const skipped = (relativePath !== '.' && !partialMatcher(relativePath)) || ignore(relativePath); - if (debug && !skipped) { - log(`crawling ${p}`); - } - return skipped; - }, - pathSeparator: '/', - relativePaths: !absolute, - resolvePaths: absolute, - includeBasePath: absolute, - resolveSymlinks: !followSymlinks, - excludeSymlinks: followSymlinks, - excludeFiles: onlyDirectories, - includeDirs: onlyDirectories || options.onlyFiles === false, - maxDepth: options.deep && Math.round(options.deep - props.depthOffset) - }).crawl(root); + return new fdir({ + filters: [(p, isDirectory) => { + const path = processPath(p, cwd, root, isDirectory, absolute); + const matches = matcher(path); + if (debug && matches) { + log(`matched ${path}`); + } + return matches; + }], + exclude: (_, p) => { + const relativePath = processPath(p, cwd, root, true, true); + const skipped = (relativePath !== '.' && !partialMatcher(relativePath)) || ignore(relativePath); + if (debug && !skipped) { + log(`crawling ${p}`); + } + return skipped; + }, + pathSeparator: '/', + relativePaths: !absolute, + resolvePaths: absolute, + includeBasePath: absolute, + resolveSymlinks: !followSymlinks, + excludeSymlinks: followSymlinks, + excludeFiles: onlyDirectories, + includeDirs: onlyDirectories || options.onlyFiles === false, + maxDepth: options.deep && Math.round(options.deep - props.depthOffset) + }).crawl(root); } // #endregion buildFdir diff --git a/src/index.ts b/src/index.ts index 2a96438..7141f50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import path, { posix } from 'node:path'; import { escapePath, isDynamicPattern, log, splitPattern } from './utils.ts'; -import type { GlobOptions, InternalProps, ProcessedPatterns } from './types.ts'; +import type { GlobOptions, Input, InternalProps, ProcessedPatterns } from './types.ts'; import { buildFdir, formatPaths } from './fdir.ts'; const PARENT_DIRECTORY = /^(\/?\.\.)+/; @@ -110,18 +110,27 @@ function processPatterns( return { match: matchPatterns, ignore: ignorePatterns }; } -function crawl(options: GlobOptions, cwd: string, sync: false): Promise; -function crawl(options: GlobOptions, cwd: string, sync: true): string[]; -function crawl(options: GlobOptions, cwd: string, sync: boolean) { +function getOptions(input: Input, options?: Partial): GlobOptions { + const opts = Array.isArray(input) || typeof input === 'string' ? { ...options, patterns: input } : input; + opts.cwd = (opts.cwd ? path.resolve(opts.cwd) : process.cwd()).replace(BACKSLASHES, '/'); + return opts as GlobOptions +} + +function crawl(input: Input, options: Partial | undefined, sync: false): Promise; +function crawl(input: Input, options: Partial | undefined, sync: true): string[]; +function crawl(input: Input, options: Partial | undefined, sync: boolean) { + const opts = getOptions(input, options); + const cwd = opts.cwd; + if (process.env.TINYGLOBBY_DEBUG) { - options.debug = true; + opts.debug = true; } - if (options.debug) { - log('globbing with options:', options, 'cwd:', cwd); + if (opts.debug) { + log('globbing with options:', opts, 'cwd:', cwd); } - if (Array.isArray(options.patterns) && options.patterns.length === 0) { + if (Array.isArray(opts.patterns) && opts.patterns.length === 0) { return sync ? [] : Promise.resolve([]); } @@ -131,57 +140,42 @@ function crawl(options: GlobOptions, cwd: string, sync: boolean) { depthOffset: 0 }; - const processed = processPatterns(options, cwd, props); + const processed = processPatterns(opts, cwd, props); props.root = props.root.replace(BACKSLASHES, ''); const root = props.root; - if (options.debug) { + if (opts.debug) { log('internal processing patterns:', processed); log('internal properties:', props); } - const api = buildFdir(options, props, processed, cwd, root); + const api = buildFdir(opts, props, processed, cwd, root); - if (cwd === root || options.absolute) { + if (cwd === root || opts.absolute) { return sync ? api.sync() : api.withPromise(); } return sync ? formatPaths(api.sync(), cwd, root) : api.withPromise().then(paths => formatPaths(paths, cwd, root)); } -export function glob(patterns: string | string[], options?: Omit): Promise; -export function glob(options: GlobOptions): Promise; -export async function glob( - patternsOrOptions: string | string[] | GlobOptions, - options?: GlobOptions -): Promise { - if (patternsOrOptions && options?.patterns) { - throw new Error('Cannot pass patterns as both an argument and an option'); +function validateInput(input: Input, options?: Partial) { + if (input && options?.patterns) { + throw new Error('Cannot pass patterns as both an argument and an option.') } - - const opts = - Array.isArray(patternsOrOptions) || typeof patternsOrOptions === 'string' - ? { ...options, patterns: patternsOrOptions } - : patternsOrOptions; - const cwd = opts.cwd ? path.resolve(opts.cwd).replace(BACKSLASHES, '/') : process.cwd().replace(BACKSLASHES, '/'); - - return crawl(opts, cwd, false); } -export function globSync(patterns: string | string[], options?: Omit): string[]; -export function globSync(options: GlobOptions): string[]; -export function globSync(patternsOrOptions: string | string[] | GlobOptions, options?: GlobOptions): string[] { - if (patternsOrOptions && options?.patterns) { - throw new Error('Cannot pass patterns as both an argument and an option'); - } - - const opts = - Array.isArray(patternsOrOptions) || typeof patternsOrOptions === 'string' - ? { ...options, patterns: patternsOrOptions } - : patternsOrOptions; - const cwd = opts.cwd ? path.resolve(opts.cwd).replace(BACKSLASHES, '/') : process.cwd().replace(BACKSLASHES, '/'); +export function glob(patterns: string | string[], options?: Omit, 'patterns'>): Promise; +export function glob(options: Partial): Promise; +export async function glob(input: Input, options?: Partial): Promise { + validateInput(input, options); + return crawl(input, options, false); +} - return crawl(opts, cwd, true); +export function globSync(patterns: string | string[], options?: Omit, 'patterns'>): string[]; +export function globSync(options: Partial): string[]; +export function globSync(input: Input, options?: Partial): string[] { + validateInput(input, options); + return crawl(input, options, true); } export { convertPathToPattern, escapePath, isDynamicPattern } from './utils.ts'; diff --git a/src/types.ts b/src/types.ts index 38f0c79..3e2172c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,7 @@ export interface GlobOptions { absolute?: boolean; - cwd?: string; - patterns?: string | string[]; + cwd: string; + patterns: string | string[]; ignore?: string | string[]; dot?: boolean; deep?: number; @@ -20,11 +20,13 @@ export interface InternalProps { } export interface ProcessedPatterns { - match: string[]; - ignore: string[]; + match: string[]; + ignore: string[]; } export interface PartialMatcherOptions { dot?: boolean; nocase?: boolean; } + +export type Input = string | string[] | Partial; From a31df9704e9eb1d87215e2b6155d2fe90e1bd312 Mon Sep 17 00:00:00 2001 From: Torathion Date: Wed, 19 Feb 2025 16:12:08 +0100 Subject: [PATCH 4/9] change: move validateInput into crawl --- src/index.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7141f50..99f17f5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -110,6 +110,12 @@ function processPatterns( return { match: matchPatterns, ignore: ignorePatterns }; } +function validateInput(input: Input, options?: Partial) { + if (input && options?.patterns) { + throw new Error('Cannot pass patterns as both an argument and an option.') + } +} + function getOptions(input: Input, options?: Partial): GlobOptions { const opts = Array.isArray(input) || typeof input === 'string' ? { ...options, patterns: input } : input; opts.cwd = (opts.cwd ? path.resolve(opts.cwd) : process.cwd()).replace(BACKSLASHES, '/'); @@ -119,6 +125,7 @@ function getOptions(input: Input, options?: Partial): GlobOptions { function crawl(input: Input, options: Partial | undefined, sync: false): Promise; function crawl(input: Input, options: Partial | undefined, sync: true): string[]; function crawl(input: Input, options: Partial | undefined, sync: boolean) { + validateInput(input, options); const opts = getOptions(input, options); const cwd = opts.cwd; @@ -158,23 +165,15 @@ function crawl(input: Input, options: Partial | undefined, sync: bo return sync ? formatPaths(api.sync(), cwd, root) : api.withPromise().then(paths => formatPaths(paths, cwd, root)); } -function validateInput(input: Input, options?: Partial) { - if (input && options?.patterns) { - throw new Error('Cannot pass patterns as both an argument and an option.') - } -} - export function glob(patterns: string | string[], options?: Omit, 'patterns'>): Promise; export function glob(options: Partial): Promise; export async function glob(input: Input, options?: Partial): Promise { - validateInput(input, options); return crawl(input, options, false); } export function globSync(patterns: string | string[], options?: Omit, 'patterns'>): string[]; export function globSync(options: Partial): string[]; export function globSync(input: Input, options?: Partial): string[] { - validateInput(input, options); return crawl(input, options, true); } From 5269a6d09fc39bd09859385249bdc65f1026a2c5 Mon Sep 17 00:00:00 2001 From: Torathion Date: Wed, 19 Feb 2025 17:02:37 +0100 Subject: [PATCH 5/9] change: centralize more options logic --- src/index.ts | 63 ++++++++++++++++++++-------------------------------- src/types.ts | 6 +++-- src/utils.ts | 6 +++++ 3 files changed, 34 insertions(+), 41 deletions(-) diff --git a/src/index.ts b/src/index.ts index 99f17f5..508fde1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import path, { posix } from 'node:path'; -import { escapePath, isDynamicPattern, log, splitPattern } from './utils.ts'; +import { ensureStringArray, escapePath, isDynamicPattern, log, splitPattern } from './utils.ts'; import type { GlobOptions, Input, InternalProps, ProcessedPatterns } from './types.ts'; import { buildFdir, formatPaths } from './fdir.ts'; @@ -7,19 +7,14 @@ const PARENT_DIRECTORY = /^(\/?\.\.)+/; const ESCAPING_BACKSLASHES = /\\(?=[()[\]{}!*+?@|])/g; const BACKSLASHES = /\\/g; -function normalizePattern( - pattern: string, - expandDirectories: boolean, - cwd: string, - props: InternalProps, - isIgnore: boolean -) { +function normalizePattern(pattern: string, props: InternalProps, isIgnore: boolean): string { + const cwd = props.cwd let result: string = pattern; if (pattern.endsWith('/')) { result = pattern.slice(0, -1); } // using a directory as entry should match all files inside it - if (!result.endsWith('*') && expandDirectories) { + if (!result.endsWith('*') && props.expandDirs) { result += '/**'; } @@ -29,12 +24,12 @@ function normalizePattern( result = posix.normalize(result); } - const parentDirectoryMatch = PARENT_DIRECTORY.exec(result); - if (parentDirectoryMatch?.[0]) { - const potentialRoot = posix.join(cwd, parentDirectoryMatch[0]); + const parentDir = PARENT_DIRECTORY.exec(result)?.[0]; + if (parentDir) { + const potentialRoot = posix.join(cwd, parentDir); if (props.root.length > potentialRoot.length) { props.root = potentialRoot; - props.depthOffset = -(parentDirectoryMatch[0].length + 1) / 3; + props.depthOffset = -(parentDir.length + 1) / 3; } } else if (!isIgnore && props.depthOffset >= 0) { const parts = splitPattern(result); @@ -67,43 +62,28 @@ function normalizePattern( return result; } -function processPatterns( - { patterns, ignore = [], expandDirectories = true }: GlobOptions, - cwd: string, - props: InternalProps -): ProcessedPatterns { - if (typeof patterns === 'string') { - patterns = [patterns]; - } else if (!patterns) { - // tinyglobby exclusive behavior, should be considered deprecated - patterns = ['**/*']; - } - - if (typeof ignore === 'string') { - ignore = [ignore]; - } - +function processPatterns(opts: GlobOptions, props: InternalProps): ProcessedPatterns { const matchPatterns: string[] = []; const ignorePatterns: string[] = []; - for (const pattern of ignore) { + for (const pattern of opts.ignore) { if (!pattern) { continue; } // don't handle negated patterns here for consistency with fast-glob if (pattern[0] !== '!' || pattern[1] === '(') { - ignorePatterns.push(normalizePattern(pattern, expandDirectories, cwd, props, true)); + ignorePatterns.push(normalizePattern(pattern, props, true)); } } - for (const pattern of patterns) { + for (const pattern of opts.patterns) { if (!pattern) { continue; } if (pattern[0] !== '!' || pattern[1] === '(') { - matchPatterns.push(normalizePattern(pattern, expandDirectories, cwd, props, false)); + matchPatterns.push(normalizePattern(pattern, props, false)); } else if (pattern[1] !== '!' || pattern[2] === '(') { - ignorePatterns.push(normalizePattern(pattern.slice(1), expandDirectories, cwd, props, true)); + ignorePatterns.push(normalizePattern(pattern.slice(1), props, true)); } } @@ -117,8 +97,14 @@ function validateInput(input: Input, options?: Partial) { } function getOptions(input: Input, options?: Partial): GlobOptions { - const opts = Array.isArray(input) || typeof input === 'string' ? { ...options, patterns: input } : input; + const opts = { + // patterns: ['**/*'] is tinyglobby exclusive behavior, should be considered deprecated + ...{ expandDirectories: true, debug: !!process.env.TINYGLOBBY_DEBUG, ignore: [], patterns: ['**/*'] }, + ...(Array.isArray(input) || typeof input === 'string' ? { ...options, patterns: input } : input) + }; opts.cwd = (opts.cwd ? path.resolve(opts.cwd) : process.cwd()).replace(BACKSLASHES, '/'); + opts.ignore = ensureStringArray(opts.ignore) + opts.patterns = ensureStringArray(opts.patterns) return opts as GlobOptions } @@ -129,9 +115,6 @@ function crawl(input: Input, options: Partial | undefined, sync: bo const opts = getOptions(input, options); const cwd = opts.cwd; - if (process.env.TINYGLOBBY_DEBUG) { - opts.debug = true; - } if (opts.debug) { log('globbing with options:', opts, 'cwd:', cwd); @@ -142,12 +125,14 @@ function crawl(input: Input, options: Partial | undefined, sync: bo } const props: InternalProps = { + cwd, + expandDirs: opts.expandDirectories, root: cwd, commonPath: null, depthOffset: 0 }; - const processed = processPatterns(opts, cwd, props); + const processed = processPatterns(opts, props); props.root = props.root.replace(BACKSLASHES, ''); const root = props.root; diff --git a/src/types.ts b/src/types.ts index 3e2172c..cdcb5bd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,20 +2,22 @@ export interface GlobOptions { absolute?: boolean; cwd: string; patterns: string | string[]; - ignore?: string | string[]; + ignore: string | string[]; dot?: boolean; deep?: number; followSymbolicLinks?: boolean; caseSensitiveMatch?: boolean; - expandDirectories?: boolean; + expandDirectories: boolean; onlyDirectories?: boolean; onlyFiles?: boolean; debug?: boolean; } export interface InternalProps { + cwd: string root: string; commonPath: string[] | null; + expandDirs: boolean, depthOffset: number; } diff --git a/src/utils.ts b/src/utils.ts index 1bf4751..33986f8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -134,3 +134,9 @@ export function log(...tasks: unknown[]): void { console.log(`[tinyglobby ${new Date().toLocaleTimeString('es')}]`, ...tasks); } // #endregion + +// #region ensureStringArray +export function ensureStringArray(value: string | string[]): string[] { + return typeof value === 'string' ? [value] : value +} +// #endregion ensureStringArray From 7af95a76ad5f00f8f684aad36f1b645eda5fa733 Mon Sep 17 00:00:00 2001 From: Torathion Date: Wed, 19 Feb 2025 17:15:04 +0100 Subject: [PATCH 6/9] revert: InternalProps extension --- src/index.ts | 14 ++++++-------- src/types.ts | 2 -- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 508fde1..98cfe78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,14 +7,14 @@ const PARENT_DIRECTORY = /^(\/?\.\.)+/; const ESCAPING_BACKSLASHES = /\\(?=[()[\]{}!*+?@|])/g; const BACKSLASHES = /\\/g; -function normalizePattern(pattern: string, props: InternalProps, isIgnore: boolean): string { - const cwd = props.cwd +function normalizePattern(pattern: string, props: InternalProps, opts: GlobOptions, isIgnore: boolean): string { + const cwd = opts.cwd let result: string = pattern; if (pattern.endsWith('/')) { result = pattern.slice(0, -1); } // using a directory as entry should match all files inside it - if (!result.endsWith('*') && props.expandDirs) { + if (!result.endsWith('*') && opts.expandDirectories) { result += '/**'; } @@ -72,7 +72,7 @@ function processPatterns(opts: GlobOptions, props: InternalProps): ProcessedPatt } // don't handle negated patterns here for consistency with fast-glob if (pattern[0] !== '!' || pattern[1] === '(') { - ignorePatterns.push(normalizePattern(pattern, props, true)); + ignorePatterns.push(normalizePattern(pattern, props, opts, true)); } } @@ -81,9 +81,9 @@ function processPatterns(opts: GlobOptions, props: InternalProps): ProcessedPatt continue; } if (pattern[0] !== '!' || pattern[1] === '(') { - matchPatterns.push(normalizePattern(pattern, props, false)); + matchPatterns.push(normalizePattern(pattern, props, opts, false)); } else if (pattern[1] !== '!' || pattern[2] === '(') { - ignorePatterns.push(normalizePattern(pattern.slice(1), props, true)); + ignorePatterns.push(normalizePattern(pattern.slice(1), props, opts, true)); } } @@ -125,8 +125,6 @@ function crawl(input: Input, options: Partial | undefined, sync: bo } const props: InternalProps = { - cwd, - expandDirs: opts.expandDirectories, root: cwd, commonPath: null, depthOffset: 0 diff --git a/src/types.ts b/src/types.ts index cdcb5bd..09ba138 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,10 +14,8 @@ export interface GlobOptions { } export interface InternalProps { - cwd: string root: string; commonPath: string[] | null; - expandDirs: boolean, depthOffset: number; } From cb011871fc4bd91b83252b3ff6fc0003aa6d98f6 Mon Sep 17 00:00:00 2001 From: Torathion Date: Wed, 19 Feb 2025 19:04:56 +0100 Subject: [PATCH 7/9] perf: inline validateInput --- src/index.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 98cfe78..1086378 100644 --- a/src/index.ts +++ b/src/index.ts @@ -90,12 +90,6 @@ function processPatterns(opts: GlobOptions, props: InternalProps): ProcessedPatt return { match: matchPatterns, ignore: ignorePatterns }; } -function validateInput(input: Input, options?: Partial) { - if (input && options?.patterns) { - throw new Error('Cannot pass patterns as both an argument and an option.') - } -} - function getOptions(input: Input, options?: Partial): GlobOptions { const opts = { // patterns: ['**/*'] is tinyglobby exclusive behavior, should be considered deprecated @@ -111,7 +105,10 @@ function getOptions(input: Input, options?: Partial): GlobOptions { function crawl(input: Input, options: Partial | undefined, sync: false): Promise; function crawl(input: Input, options: Partial | undefined, sync: true): string[]; function crawl(input: Input, options: Partial | undefined, sync: boolean) { - validateInput(input, options); + if (input && options?.patterns) { + throw new Error('Cannot pass patterns as both an argument and an option.') + } + const opts = getOptions(input, options); const cwd = opts.cwd; From 7d112ed56021ca64ac1dd723d0514c69f7f2ed1e Mon Sep 17 00:00:00 2001 From: Torathion Date: Wed, 19 Feb 2025 19:27:44 +0100 Subject: [PATCH 8/9] perf: remove redundant check --- src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1086378..ccc4985 100644 --- a/src/index.ts +++ b/src/index.ts @@ -112,12 +112,11 @@ function crawl(input: Input, options: Partial | undefined, sync: bo const opts = getOptions(input, options); const cwd = opts.cwd; - if (opts.debug) { log('globbing with options:', opts, 'cwd:', cwd); } - if (Array.isArray(opts.patterns) && opts.patterns.length === 0) { + if (!opts.patterns.length) { return sync ? [] : Promise.resolve([]); } From 4bb3b667ef48a72a4cb21470733ca16a6f6458b1 Mon Sep 17 00:00:00 2001 From: Torathion Date: Wed, 19 Feb 2025 20:56:05 +0100 Subject: [PATCH 9/9] change: extend defaultOptions --- src/fdir.ts | 12 +++++------- src/index.ts | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/fdir.ts b/src/fdir.ts index 8c698b1..a4b22ba 100644 --- a/src/fdir.ts +++ b/src/fdir.ts @@ -36,7 +36,8 @@ export function formatPaths(paths: string[], cwd: string, root: string): string[ // #region buildFdir export function buildFdir(options: GlobOptions, props: InternalProps, processed: ProcessedPatterns, cwd: string, root: string): APIBuilder { - const nocase = options.caseSensitiveMatch === false; + const { absolute, debug, followSymbolicLinks, onlyDirectories } = options + const nocase = !options.caseSensitiveMatch; const matcher = picomatch(processed.match, { dot: options.dot, @@ -48,9 +49,6 @@ export function buildFdir(options: GlobOptions, props: InternalProps, processed: const ignore = picomatch(processed.ignore, partialMatcherOptions); const partialMatcher = getPartialMatcher(processed.match, partialMatcherOptions); - const { absolute, onlyDirectories, debug } = options - const followSymlinks = options.followSymbolicLinks === false; - return new fdir({ filters: [(p, isDirectory) => { const path = processPath(p, cwd, root, isDirectory, absolute); @@ -72,10 +70,10 @@ export function buildFdir(options: GlobOptions, props: InternalProps, processed: relativePaths: !absolute, resolvePaths: absolute, includeBasePath: absolute, - resolveSymlinks: !followSymlinks, - excludeSymlinks: followSymlinks, + resolveSymlinks: followSymbolicLinks, + excludeSymlinks: !followSymbolicLinks, excludeFiles: onlyDirectories, - includeDirs: onlyDirectories || options.onlyFiles === false, + includeDirs: onlyDirectories || !options.onlyFiles, maxDepth: options.deep && Math.round(options.deep - props.depthOffset) }).crawl(root); } diff --git a/src/index.ts b/src/index.ts index ccc4985..5926c46 100644 --- a/src/index.ts +++ b/src/index.ts @@ -90,10 +90,21 @@ function processPatterns(opts: GlobOptions, props: InternalProps): ProcessedPatt return { match: matchPatterns, ignore: ignorePatterns }; } +// Only the literal type doesn't emit any typescript errors +const defaultOptions = { + expandDirectories: true, + debug: !!process.env.TINYGLOBBY_DEBUG, + ignore: [], + // tinyglobby exclusive behavior, should be considered deprecated + patterns: ['**/*'], + caseSensitiveMatch: true, + followSymbolicLinks: true, + onlyFiles: true +} + function getOptions(input: Input, options?: Partial): GlobOptions { const opts = { - // patterns: ['**/*'] is tinyglobby exclusive behavior, should be considered deprecated - ...{ expandDirectories: true, debug: !!process.env.TINYGLOBBY_DEBUG, ignore: [], patterns: ['**/*'] }, + ...defaultOptions, ...(Array.isArray(input) || typeof input === 'string' ? { ...options, patterns: input } : input) }; opts.cwd = (opts.cwd ? path.resolve(opts.cwd) : process.cwd()).replace(BACKSLASHES, '/');