From 5f21b46bc21c27ce39e166c064455d8f1ed62863 Mon Sep 17 00:00:00 2001 From: isaacs Date: Sat, 25 Feb 2023 22:14:11 -0800 Subject: [PATCH] adding lots of tests, clean up types Also, this fixes a but preventing `**/..` from working properly when it's the last in the pattern. --- changelog.md | 7 +- package-lock.json | 14 +-- package.json | 6 +- src/glob.ts | 74 ++++++++------ src/ignore.ts | 21 ++-- src/index.ts | 147 +++++++++++++++++++++------- src/pattern.ts | 35 ++----- src/processor.ts | 20 +++- src/walker.ts | 33 ++++--- test/00-setup.ts | 10 ++ test/absolute-must-be-strings.ts | 8 ++ test/bash-results.ts | 54 +++++++++++ test/broken-symlink.ts | 5 +- test/cwd-noent.ts | 80 +++++++++++++++ test/ignore.ts | 22 ++++- test/mark.ts | 5 +- test/match-base.ts | 10 +- test/match-parent.ts | 25 +++++ test/match-root.ts | 8 ++ test/pattern.ts | 64 ++++++++++++ test/platform.ts | 57 +++++++++++ test/realpath.ts | 8 +- test/stream.ts | 161 +++++++++++++++++++++++++++++++ 23 files changed, 723 insertions(+), 151 deletions(-) create mode 100644 test/absolute-must-be-strings.ts create mode 100644 test/cwd-noent.ts create mode 100644 test/match-parent.ts create mode 100644 test/match-root.ts create mode 100644 test/pattern.ts create mode 100644 test/platform.ts create mode 100644 test/stream.ts diff --git a/changelog.md b/changelog.md index 8c8174a4..c4bdc0a6 100644 --- a/changelog.md +++ b/changelog.md @@ -9,11 +9,6 @@ changes. - Promise API instead of callbacks. - Accept pattern as string or array of strings. - Hybrid module distribution. - - **Note:** `module.exports` in CommonJS mode is an object, not a - function. Use the exported `default` or `glob` members to - access the default function export in CommonJS modes. - - Full TypeScript support. - Exported `Glob` class is no longer an event emitter. - Exported `Glob` class has `walk()`, `walkSync()`, `stream()`, @@ -69,7 +64,7 @@ changes. or vice versa, may thus result in more or fewer matches than expected. In general, it should only be used when the filesystem is known to differ from the platform default. -- `realpath:true` no longer implies `absolute:true`. The +- `realpath:true` no longer implies `absolute:true`. The relative path to the realpath will be emitted when `absolute` is not set. diff --git a/package-lock.json b/package-lock.json index 0695c9f3..c483f109 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", - "minimatch": "^7.1.3", + "minimatch": "^7.1.4", "minipass": "^4.2.1", "path-scurry": "^1.4.0" }, @@ -2920,9 +2920,9 @@ } }, "node_modules/minimatch": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.1.3.tgz", - "integrity": "sha512-kpcwpcyeYtgSzpOvUf+9RiaPgrqtR2NwuqejBV2VkWxR+KC8jMWTb76zSlVJXy6ypbY39u66Un4gTk0ryiXm2g==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.1.4.tgz", + "integrity": "sha512-dZdn8jDUB4Y3eu7hABT6IgLTMQ9cVf+vhhXjLAkuN40wRkweVxEpvnGYLYZUhNB0P+BbTOZDzo+1rCitOQWc3g==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -8568,9 +8568,9 @@ } }, "minimatch": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.1.3.tgz", - "integrity": "sha512-kpcwpcyeYtgSzpOvUf+9RiaPgrqtR2NwuqejBV2VkWxR+KC8jMWTb76zSlVJXy6ypbY39u66Un4gTk0ryiXm2g==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.1.4.tgz", + "integrity": "sha512-dZdn8jDUB4Y3eu7hABT6IgLTMQ9cVf+vhhXjLAkuN40wRkweVxEpvnGYLYZUhNB0P+BbTOZDzo+1rCitOQWc3g==", "requires": { "brace-expansion": "^2.0.1" } diff --git a/package.json b/package.json index e6b9ccb4..21ad2b8d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "Isaac Z. Schlueter (http://blog.izs.me/)", "name": "glob", - "description": "a little globber", + "description": "the most correct and second fastest glob implementation in JavaScript", "version": "9.0.0-0", "repository": { "type": "git", @@ -17,7 +17,7 @@ }, "require": { "types": "./dist/cjs/index-cjs.d.ts", - "default": "./dist/cjs/index-cjs.js" + "default": "./dist/cjs/index.js" } } }, @@ -59,7 +59,7 @@ }, "dependencies": { "fs.realpath": "^1.0.0", - "minimatch": "^7.1.3", + "minimatch": "^7.1.4", "minipass": "^4.2.1", "path-scurry": "^1.4.0" }, diff --git a/src/glob.ts b/src/glob.ts index 5a9ee483..bf2f3f9b 100644 --- a/src/glob.ts +++ b/src/glob.ts @@ -24,18 +24,26 @@ const defaultPlatform: NodeJS.Platform = ? process.platform : 'linux' -export interface GlobOptions extends MinimatchOptions { - ignore?: string | string[] | Ignore +export interface GlobOptions { + absolute?: boolean + allowWindowsEscape?: boolean + cwd?: string + dot?: boolean follow?: boolean + ignore?: string | string[] | Ignore mark?: boolean + matchBase?: boolean + nobrace?: boolean + nocase?: boolean nodir?: boolean - cwd?: string + noext?: boolean + noglobstar?: boolean + platform?: NodeJS.Platform realpath?: boolean - absolute?: boolean - withFileTypes?: boolean scurry?: PathScurry - platform?: NodeJS.Platform signal?: AbortSignal + windowsPathsNoEscape?: boolean + withFileTypes?: boolean } export type GlobOptionsWithFileTypesTrue = GlobOptions & { @@ -57,7 +65,7 @@ type Result = Opts extends GlobOptionsWithFileTypesTrue : Opts extends GlobOptionsWithFileTypesUnset ? string : string | Path -type Results = Result[] +export type Results = Result[] type FileTypes = Opts extends GlobOptionsWithFileTypesTrue ? true @@ -80,7 +88,6 @@ export class Glob { globSet: GlobSet globParts: GlobParts realpath: boolean - nonull: boolean absolute: boolean matchBase: boolean windowsPathsNoEscape: boolean @@ -95,6 +102,8 @@ export class Glob { platform: NodeJS.Platform patterns: Pattern[] signal?: AbortSignal + nobrace: boolean + noext: boolean constructor(pattern: string | string[], opts: Opts) { this.withFileTypes = !!opts.withFileTypes as FileTypes @@ -104,24 +113,16 @@ export class Glob { this.nodir = !!opts.nodir this.mark = !!opts.mark this.cwd = opts.cwd || '' + this.nobrace = !!opts.nobrace + this.noext = !!opts.noext this.realpath = !!opts.realpath - this.nonull = !!opts.nonull this.absolute = !!opts.absolute this.noglobstar = !!opts.noglobstar this.matchBase = !!opts.matchBase - // if we're returning Path objects, we can't do nonull, because - // the pattern is a string, not a Path - if (this.withFileTypes) { - if (this.nonull) { - throw new TypeError( - 'cannot set nonull:true and withFileTypes:true' - ) - } - if (this.absolute) { - throw new Error('cannot set absolute:true and withFileTypes:true') - } + if (this.withFileTypes && this.absolute) { + throw new Error('cannot set absolute:true and withFileTypes:true') } if (typeof pattern === 'string') { @@ -149,6 +150,12 @@ export class Glob { this.opts = { ...opts, platform: this.platform } if (opts.scurry) { this.scurry = opts.scurry + if ( + opts.nocase !== undefined && + opts.nocase !== opts.scurry.nocase + ) { + throw new Error('nocase option contradicts provided scurry option') + } } else { const Scurry = opts.platform === 'win32' @@ -170,13 +177,18 @@ export class Glob { const mmo: MinimatchOptions = { // default nocase based on platform - nocase: this.nocase, ...opts, - nonegate: true, - nocomment: true, + dot: this.dot, + matchBase: this.matchBase, + nobrace: this.nobrace, + nocase: this.nocase, nocaseMagicOnly: true, + nocomment: true, + noext: this.noext, + nonegate: true, optimizationLevel: 2, platform: this.platform, + windowsPathsNoEscape: this.windowsPathsNoEscape, } const mms = this.pattern.map(p => new Minimatch(p, mmo)) @@ -224,8 +236,8 @@ export class Glob { return [...matches] } - stream(): Minipass> - stream(): Minipass { + stream(): Minipass, Result> + stream(): Minipass { return new GlobStream(this.patterns, this.scurry.cwd, { ...this.opts, platform: this.platform, @@ -233,8 +245,8 @@ export class Glob { }).stream() } - streamSync(): Minipass> - streamSync(): Minipass { + streamSync(): Minipass, Result> + streamSync(): Minipass { return new GlobStream(this.patterns, this.scurry.cwd, { ...this.opts, platform: this.platform, @@ -242,17 +254,17 @@ export class Glob { }).streamSync() } - iteratorSync(): Generator, void, void> { + iterateSync(): Generator, void, void> { return this.streamSync()[Symbol.iterator]() } [Symbol.iterator]() { - return this.iteratorSync() + return this.iterateSync() } - iterator(): AsyncGenerator, void, void> { + iterate(): AsyncGenerator, void, void> { return this.stream()[Symbol.asyncIterator]() } [Symbol.asyncIterator]() { - return this.iterator() + return this.iterate() } } diff --git a/src/ignore.ts b/src/ignore.ts index e9433d44..d2fbb0d8 100644 --- a/src/ignore.ts +++ b/src/ignore.ts @@ -16,8 +16,6 @@ const defaultPlatform: NodeJS.Platform = : 'linux' export class Ignore { - platform: NodeJS.Platform - nocase: boolean relative: Minimatch[] relativeChildren: Minimatch[] absolute: Minimatch[] @@ -25,19 +23,26 @@ export class Ignore { constructor( ignored: string[], - { platform = defaultPlatform, nocase }: GlobWalkerOpts + { + nobrace, + nocase, + noext, + noglobstar, + platform = defaultPlatform, + }: GlobWalkerOpts ) { - this.platform = platform - this.nocase = !!nocase this.relative = [] this.absolute = [] this.relativeChildren = [] this.absoluteChildren = [] const mmopts = { - platform: this.platform, - optimizationLevel: 2, dot: true, + nobrace, nocase, + noext, + noglobstar, + optimizationLevel: 2, + platform, } // this is a little weird, but it gives us a clean set of optimized @@ -57,7 +62,7 @@ export class Ignore { for (let i = 0; i < mm.set.length; i++) { const parsed = mm.set[i] const globParts = mm.globParts[i] - const p = new Pattern(parsed, globParts, 0, this.platform) + const p = new Pattern(parsed, globParts, 0, platform) const m = new Minimatch(p.globString(), mmopts) const children = globParts[globParts.length - 1] === '**' const absolute = p.isAbsolute() diff --git a/src/index.ts b/src/index.ts index a64972de..ed539dc7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,49 +1,124 @@ -import Minipass from 'minipass' -import { Glob, GlobOptions } from './glob.js' +import { + Glob, + GlobOptions, + GlobOptionsWithFileTypesFalse, + GlobOptionsWithFileTypesTrue, + GlobOptionsWithFileTypesUnset, + Results, +} from './glob.js' import { hasMagic } from './has-magic.js' +import { + GWOFileTypesFalse, + GWOFileTypesTrue, + GWOFileTypesUnset, + MatchStream, +} from './walker.js' -export const globStreamSync = ( +export function globStreamSync( + pattern: string | string[], + options: GlobOptionsWithFileTypesTrue +): MatchStream +export function globStreamSync( + pattern: string | string[], + options: GlobOptionsWithFileTypesFalse +): MatchStream +export function globStreamSync( + pattern: string | string[], + options: GlobOptionsWithFileTypesUnset +): MatchStream +export function globStreamSync( + pattern: string | string[], + options: GlobOptions +): MatchStream +export function globStreamSync( pattern: string | string[], options: GlobOptions = {} -): Minipass => - new Glob(pattern, { ...options, withFileTypes: false }).streamSync() +) { + return new Glob(pattern, options).streamSync() +} -export const globStream = Object.assign( - ( - pattern: string | string[], - options: GlobOptions = {} - ): Minipass => - new Glob(pattern, { ...options, withFileTypes: false }).stream(), - { sync: globStreamSync } -) +export function globStream( + pattern: string | string[], + options: GlobOptionsWithFileTypesFalse +): MatchStream +export function globStream( + pattern: string | string[], + options: GlobOptionsWithFileTypesTrue +): MatchStream +export function globStream( + pattern: string | string[], + options?: GlobOptionsWithFileTypesUnset | undefined +): MatchStream +export function globStream( + pattern: string | string[], + options: GlobOptions +): MatchStream +export function globStream( + pattern: string | string[], + options: GlobOptions = {} +) { + return new Glob(pattern, options).stream() +} -export const globSync = Object.assign( - (pattern: string | string[], options: GlobOptions = {}): string[] => - new Glob(pattern, { ...options, withFileTypes: false }).walkSync(), - { stream: globStreamSync } -) +export function globSync( + pattern: string | string[], + options: GlobOptionsWithFileTypesFalse +): Results +export function globSync( + pattern: string | string[], + options: GlobOptionsWithFileTypesTrue +): Results +export function globSync( + pattern: string | string[], + options?: GlobOptionsWithFileTypesUnset | undefined +): Results +export function globSync( + pattern: string | string[], + options: GlobOptions +): Results +export function globSync( + pattern: string | string[], + options: GlobOptions = {} +) { + return new Glob(pattern, options).walkSync() +} -export const glob = Object.assign( - async ( - pattern: string | string[], - options: GlobOptions = {} - ): Promise => - new Glob(pattern, { ...options, withFileTypes: false }).walk(), - { - sync: globSync, - globSync, - stream: globStream, - streamSync: globStreamSync, - globStream, - globStreamSync, - Glob, - hasMagic, - } -) +export async function glob( + pattern: string | string[], + options?: GlobOptionsWithFileTypesUnset | undefined +): Promise> +export async function glob( + pattern: string | string[], + options: GlobOptionsWithFileTypesTrue +): Promise> +export async function glob( + pattern: string | string[], + options: GlobOptionsWithFileTypesFalse +): Promise> +export async function glob( + pattern: string | string[], + options: GlobOptions +): Promise> +export async function glob( + pattern: string | string[], + options: GlobOptions = {} +) { + return new Glob(pattern, options).walk() +} /* c8 ignore start */ export { Glob } from './glob.js' export type { GlobOptions } from './glob.js' export { hasMagic } from './has-magic.js' -export default glob /* c8 ignore stop */ +export default Object.assign(glob, { + glob: glob, + sync: globSync, + globSync, + stream: globStream, + streamSync: globStreamSync, + globStream, + globStreamSync, + Glob, + hasMagic, +}) diff --git a/src/pattern.ts b/src/pattern.ts index cec75e99..676d61c3 100644 --- a/src/pattern.ts +++ b/src/pattern.ts @@ -53,7 +53,7 @@ export class Pattern { throw new TypeError('mismatched pattern list and glob list lengths') } this.length = patternList.length - if (index >= this.length) { + if (index < 0 || index >= this.length) { throw new TypeError('index out of range') } this.#patternList = patternList @@ -61,18 +61,6 @@ export class Pattern { this.#index = index this.#platform = platform - // if the current item is not globstar, and the next item is .., skip ahead - if ( - this.#patternList[this.#index] !== GLOBSTAR && - this.#patternList[this.#index] !== '..' && - this.#patternList[this.#index] !== '.' && - this.#patternList[this.#index] !== '' && - this.#patternList[this.#index + 1] === '..' && - this.length > this.#index + 2 - ) { - this.#index += 2 - } - // normalize root entries of absolute patterns on initial creation. if (this.#index === 0) { // c: => ['c:/'] @@ -84,15 +72,16 @@ export class Pattern { // /etc => ['/', 'etc'] // / => ['/'] if (this.isUNC()) { - const [p1, p2, p3, ...prest] = this.#patternList - const [g1, g2, g3, ...grest] = this.#globList + // '' / '' / 'host' / 'share' + const [p0, p1, p2, p3, ...prest] = this.#patternList + const [g0, g1, g2, g3, ...grest] = this.#globList if (prest[0] === '') { // ends in / prest.shift() grest.shift() } - const p = [p1, p2, p3, ''].join('/') - const g = [g1, g2, g3, ''].join('/') + const p = [p0, p1, p2, p3, ''].join('/') + const g = [g0, g1, g2, g3, ''].join('/') this.#patternList = [p, ...prest] this.#globList = [g, ...grest] this.length = this.#patternList.length @@ -110,11 +99,6 @@ export class Pattern { this.#globList = [g, ...grest] this.length = this.#patternList.length } - } else { - // discard any empty path portions, except the last one. - while (this.#index < this.length - 1 && this.pattern() === '') { - this.#index++ - } } } @@ -128,17 +112,10 @@ export class Pattern { isGlobstar(): boolean { return this.#patternList[this.#index] === GLOBSTAR } - isGlobstarDotDot(): boolean { - return this.isGlobstar() && this.#globList[this.#index + 1] === '..' - } isRegExp(): boolean { return this.#patternList[this.#index] instanceof RegExp } - glob(): string { - return this.#globList[this.#index] - } - globString(): string { return (this.#globString = this.#globString || diff --git a/src/processor.ts b/src/processor.ts index 71f2279d..c2e6aab0 100644 --- a/src/processor.ts +++ b/src/processor.ts @@ -55,7 +55,13 @@ class SubWalks { } else this.store.set(target, [pattern]) } get(target: Path): Pattern[] { - return this.store.get(target) || [] + const subs = this.store.get(target) + /* c8 ignore start */ + if (!subs) { + throw new Error('attempting to walk unknown path') + } + /* c8 ignore stop */ + return subs } entries(): [Path, Pattern[]][] { return this.keys().map(k => [k, this.store.get(k) as Pattern[]]) @@ -91,9 +97,6 @@ export class Processor { // first item in patterns is the filter for (let [t, pattern] of processingSet) { - if (this.hasWalkedCache.hasWalked(t, pattern)) { - continue - } this.hasWalkedCache.storeWalked(t, pattern) const root = pattern.root() @@ -165,7 +168,11 @@ export class Processor { this.matches.add(t, absolute, rp === '' || rp === '.') } else { if (rp === '..') { + // this would mean you're matching **/.. at the fs root, + // and no thanks, I'm not gonna test that specific case. + /* c8 ignore start */ const tp = t.parent || t + /* c8 ignore stop */ if (!rrest) this.matches.add(tp, absolute, true) else if (!this.hasWalkedCache.hasWalked(tp, rrest)) { this.subwalks.add(tp, rrest) @@ -252,6 +259,11 @@ export class Processor { rp !== '.' ) { this.testString(e, rp, rest.rest(), absolute) + } else if (rp === '..') { + /* c8 ignore start */ + const ep = e.parent || e + /* c8 ignore stop */ + this.subwalks.add(ep, rest) } else if (rp instanceof RegExp) { this.testRegExp(e, rp, rest.rest(), absolute) } diff --git a/src/walker.ts b/src/walker.ts index 0db3d68f..382746c7 100644 --- a/src/walker.ts +++ b/src/walker.ts @@ -12,16 +12,23 @@ import { Processor } from './processor.js' export interface GlobWalkerOpts { absolute?: boolean - realpath?: boolean - nodir?: boolean - mark?: boolean - withFileTypes?: boolean - signal?: AbortSignal + allowWindowsEscape?: boolean + cwd?: string + dot?: boolean + follow?: boolean ignore?: string | string[] | Ignore - platform?: NodeJS.Platform + mark?: boolean + matchBase?: boolean + nobrace?: boolean nocase?: boolean - follow?: boolean - dot?: boolean + nodir?: boolean + noext?: boolean + noglobstar?: boolean + platform?: NodeJS.Platform + realpath?: boolean + signal?: AbortSignal + windowsPathsNoEscape?: boolean + withFileTypes?: boolean } export type GWOFileTypesTrue = GlobWalkerOpts & { @@ -52,12 +59,12 @@ export type Matches = O extends GWOFileTypesTrue export type MatchStream = O extends GWOFileTypesTrue - ? Minipass + ? Minipass : O extends GWOFileTypesFalse - ? Minipass + ? Minipass : O extends GWOFileTypesUnset - ? Minipass - : Minipass + ? Minipass + : Minipass const makeIgnore = ( ignore: string | string[] | Ignore, @@ -516,6 +523,8 @@ export class GlobStream< : this.path if (target) { this.walkCBSync(target, this.patterns, () => this.results.end()) + } else { + this.results.end() } return this.results } diff --git a/test/00-setup.ts b/test/00-setup.ts index 76fc3de2..e8e91947 100644 --- a/test/00-setup.ts +++ b/test/00-setup.ts @@ -93,6 +93,16 @@ if (process.platform === 'win32' || !process.env.TEST_REGEN) { '{/tmp/glob-test/*,*}', // evil owl face! how you taunt me! 'a/!(symlink)/**', 'a/symlink/a/**/*', + // this one we don't quite match bash, because when bash + // applies the .. to the symlink walked by **, it effectively + // resets the symlink walk limit, and that is just a step too + // far for an edge case no one knows or cares about, even for + // an obsessive perfectionist like me. + // './a/**/../*/**', + 'a/!(symlink)/**/..', + 'a/!(symlink)/**/../', + 'a/!(symlink)/**/../*', + 'a/!(symlink)/**/../*/*', ] const bashOutput: { [k: string]: string[] } = {} diff --git a/test/absolute-must-be-strings.ts b/test/absolute-must-be-strings.ts new file mode 100644 index 00000000..cc18e034 --- /dev/null +++ b/test/absolute-must-be-strings.ts @@ -0,0 +1,8 @@ +import { Glob } from '../' +import t from 'tap' +t.throws(() => { + new Glob('.', { + withFileTypes: true, + absolute: true, + }) +}) diff --git a/test/bash-results.ts b/test/bash-results.ts index 14a1650f..76d5641e 100644 --- a/test/bash-results.ts +++ b/test/bash-results.ts @@ -190,4 +190,58 @@ export const bashResults: { [path: string]: string[] } = { 'a/symlink/a/b/c', 'a/symlink/a/b/c/a', ], + 'a/!(symlink)/**/..': [ + 'a', + 'a/abcdef', + 'a/abcfed', + 'a/b', + 'a/bc', + 'a/c', + 'a/c/d', + 'a/cb', + ], + 'a/!(symlink)/**/../': [ + 'a', + 'a/abcdef', + 'a/abcfed', + 'a/b', + 'a/bc', + 'a/c', + 'a/c/d', + 'a/cb', + ], + 'a/!(symlink)/**/../*': [ + 'a/abcdef', + 'a/abcdef/g', + 'a/abcfed', + 'a/abcfed/g', + 'a/b', + 'a/b/c', + 'a/bc', + 'a/bc/e', + 'a/c', + 'a/c/d', + 'a/c/d/c', + 'a/cb', + 'a/cb/e', + 'a/symlink', + 'a/x', + 'a/z', + ], + 'a/!(symlink)/**/../*/*': [ + 'a/abcdef/g', + 'a/abcdef/g/h', + 'a/abcfed/g', + 'a/abcfed/g/h', + 'a/b/c', + 'a/b/c/d', + 'a/bc/e', + 'a/bc/e/f', + 'a/c/d', + 'a/c/d/c', + 'a/c/d/c/b', + 'a/cb/e', + 'a/cb/e/f', + 'a/symlink/a', + ], } diff --git a/test/broken-symlink.ts b/test/broken-symlink.ts index ca1ca89e..044834ff 100644 --- a/test/broken-symlink.ts +++ b/test/broken-symlink.ts @@ -1,7 +1,7 @@ import { relative } from 'path' import t from 'tap' import { glob } from '../' -import type { GlobOptions } from '../src/index.js' +import { GlobOptionsWithFileTypesUnset } from '../src/glob.js' if (process.platform === 'win32') { t.plan(0, 'skip on windows') @@ -32,9 +32,8 @@ const patterns = [ `${dir}/a/broken-link/!(asdf)`, ] -const opts: (GlobOptions | undefined)[] = [ +const opts: (GlobOptionsWithFileTypesUnset | undefined)[] = [ undefined, - { nonull: true }, { mark: true }, { follow: true }, ] diff --git a/test/cwd-noent.ts b/test/cwd-noent.ts new file mode 100644 index 00000000..7438b117 --- /dev/null +++ b/test/cwd-noent.ts @@ -0,0 +1,80 @@ +import { resolve } from 'path' +import t from 'tap' +import { Glob } from '../' +const cwd = resolve(__dirname, 'fixtures/does-not-exist') + +t.test('walk', async t => { + const g = new Glob('**', { cwd }) + t.same(await g.walk(), []) +}) + +t.test('walkSync', t => { + const g = new Glob('**', { cwd }) + t.same(g.walkSync(), []) + t.end() +}) + +t.test('stream', async t => { + const g = new Glob('**', { cwd }) + const s = g.stream() + s.on('data', () => t.fail('should not get entries')) + t.same(await s.collect(), []) +}) + +t.test('streamSync', t => { + const g = new Glob('**', { cwd }) + const s = g.streamSync() + const c: string[] = [] + s.on('data', p => { + t.fail('should not get entries') + c.push(p) + }) + s.on('end', () => { + t.same(c, []) + t.end() + }) +}) + +t.test('iterate', async t => { + const g = new Glob('**', { cwd }) + const s = g.iterate() + const c: string[] = [] + for await (const p of s) { + c.push(p) + t.fail('should not get entries') + } + t.same(c, []) +}) + +t.test('iterateSync', async t => { + const g = new Glob('**', { cwd }) + const s = g.iterateSync() + const c: string[] = [] + for (const p of s) { + c.push(p) + t.fail('should not get entries') + } + t.same(c, []) + t.end() +}) + +t.test('for await', async t => { + const g = new Glob('**', { cwd }) + const c: string[] = [] + for await (const p of g) { + c.push(p) + t.fail('should not get entries') + } + t.same(c, []) +}) + +t.test('iterateSync', async t => { + const g = new Glob('**', { cwd }) + const c: string[] = [] + for (const p of g) { + c.push(p) + t.fail('should not get entries') + } + t.same(c, []) + t.end() +}) diff --git a/test/ignore.ts b/test/ignore.ts index 4f8a9b31..c6163c9d 100644 --- a/test/ignore.ts +++ b/test/ignore.ts @@ -6,8 +6,11 @@ import glob from '../' import type { GlobOptions } from '../src/index.js' import { sep } from 'path' -const alphasort = (a:string, b:string) => a.localeCompare(b, 'en') -const j = (a: string[]) => a.map(s => s.split('/').join(sep)).sort(alphasort) +const alphasort = (a: string, b: string) => a.localeCompare(b, 'en') +const j = (a: string[]) => + a.map(s => s.split('/').join(sep)).sort(alphasort) + +process.chdir(__dirname + '/fixtures') // [pattern, ignore, expect, opt (object) or cwd (string)] type Case = [ @@ -29,10 +32,16 @@ const cases: Case[] = [ j(['abcdef', 'abcfed', 'bc', 'c', 'cb', 'symlink', 'x', 'z']), 'a', ], - ['*', 'b*', j(['abcdef', 'abcfed', 'c', 'cb', 'symlink', 'x', 'z']), 'a'], + [ + '*', + 'b*', + j(['abcdef', 'abcfed', 'c', 'cb', 'symlink', 'x', 'z']), + 'a', + ], ['b/**', 'b/c/d', j(['b', 'b/c']), 'a'], ['b/**', 'd', j(['b', 'b/c', 'b/c/d']), 'a'], ['b/**', 'b/c/**', ['b'], 'a'], + ['b/**', (process.cwd() + '/a/b/c/**').split(sep).join('/'), ['b'], 'a'], ['**/d', 'b/c/d', j(['c/d']), 'a'], [ 'a/**/[gh]', @@ -320,10 +329,13 @@ const cases: Case[] = [ ], ['*/.abcdef', 'a/**', []], ['a/*/.y/b', 'a/x/**', j(['a/z/.y/b'])], + [ + 'a/*/.y/b', + (process.cwd() + '/a/x/**').split(sep).join('/'), + j(['a/z/.y/b']), + ], ] -process.chdir(__dirname + '/fixtures') - for (const c of cases) { const [pattern, ignore, ex, optCwd] = c const expect = ( diff --git a/test/mark.ts b/test/mark.ts index d1e3a151..298bd924 100644 --- a/test/mark.ts +++ b/test/mark.ts @@ -2,9 +2,10 @@ import t from 'tap' import glob from '../' process.chdir(__dirname + '/fixtures') -const alphasort = (a:string, b:string) => a.localeCompare(b, 'en') +const alphasort = (a: string, b: string) => a.localeCompare(b, 'en') import { sep } from 'path' -const j = (a: string[]) => a.map(s=>s.split('/').join(sep)).sort(alphasort) +const j = (a: string[]) => + a.map(s => s.split('/').join(sep)).sort(alphasort) t.test('mark with cwd', async t => { const pattern = '*/*' diff --git a/test/match-base.ts b/test/match-base.ts index 80995d9a..bd53f83f 100644 --- a/test/match-base.ts +++ b/test/match-base.ts @@ -3,8 +3,9 @@ import glob from '../' import { resolve } from 'path' import { sep } from 'path' -const alphasort = (a:string, b:string) => a.localeCompare(b, 'en') -const j = (a: string[]) => a.map(s => s.split('/').join(sep)).sort(alphasort) +const alphasort = (a: string, b: string) => a.localeCompare(b, 'en') +const j = (a: string[]) => + a.map(s => s.split('/').join(sep)).sort(alphasort) const fixtureDir = resolve(__dirname, 'fixtures') @@ -19,7 +20,10 @@ t.test('chdir', async t => { const origCwd = process.cwd() process.chdir(fixtureDir) t.teardown(() => process.chdir(origCwd)) - t.same(glob.sync(pattern, { matchBase: true }).sort(alphasort), j(expect)) + t.same( + glob.sync(pattern, { matchBase: true }).sort(alphasort), + j(expect) + ) t.same( (await glob(pattern, { matchBase: true })).sort(alphasort), j(expect) diff --git a/test/match-parent.ts b/test/match-parent.ts new file mode 100644 index 00000000..f49db67b --- /dev/null +++ b/test/match-parent.ts @@ -0,0 +1,25 @@ +import t from 'tap' +import { PathScurry } from 'path-scurry' +import { Glob } from '../' +const scurry = new PathScurry() +t.test('/', t => { + const g = new Glob('/', { withFileTypes: true, scurry }) + const m = g.walkSync() + t.equal(m.length, 1) + t.equal(m[0], scurry.cwd.resolve('/')) + t.end() +}) +t.test('/..', t => { + const g = new Glob('/..', { withFileTypes: true, scurry }) + const m = g.walkSync() + t.equal(m.length, 1) + t.equal(m[0], scurry.cwd.resolve('/')) + t.end() +}) +t.test('/../../../../../', t => { + const g = new Glob('/../../../../../', { withFileTypes: true, scurry }) + const m = g.walkSync() + t.equal(m.length, 1) + t.equal(m[0], scurry.cwd.resolve('/')) + t.end() +}) diff --git a/test/match-root.ts b/test/match-root.ts new file mode 100644 index 00000000..df8a5ef8 --- /dev/null +++ b/test/match-root.ts @@ -0,0 +1,8 @@ +import t from 'tap' +import { PathScurry } from 'path-scurry' +import { Glob } from '../' +const scurry = new PathScurry() +const g = new Glob('/', { withFileTypes: true, scurry }) +const m = g.walkSync() +t.equal(m.length, 1) +t.equal(m[0], scurry.cwd.resolve('/')) diff --git a/test/pattern.ts b/test/pattern.ts new file mode 100644 index 00000000..6b3cb22d --- /dev/null +++ b/test/pattern.ts @@ -0,0 +1,64 @@ +import { GLOBSTAR } from 'minimatch' +import t from 'tap' +import { Glob } from '../' +import { Pattern } from '../dist/cjs/pattern' + +t.same( + new Glob( + [ + '//host/share///x/*', + '//host/share/', + '//host/share', + '//?/z:/x/*', + '//?/z:/', + '//?/z:', + 'c:/x/*', + 'c:/', + ], + { platform: 'win32' } + ).patterns.map(p => [p.globString(), p.root()]), + [ + ['//host/share/x/*', '//host/share/'], + ['//host/share/', '//host/share/'], + ['//host/share/', '//host/share/'], + ['//?/z:/x/*', '//?/z:/'], + ['//?/z:/', '//?/z:/'], + ['//?/z:/', '//?/z:/'], + ['c:/x/*', 'c:/'], + ['c:/', 'c:/'], + ] +) +t.throws(() => { + new Pattern([], ['x'], 0, process.platform) +}) + +t.throws(() => { + new Pattern(['x'], [], 0, process.platform) +}) + +t.throws(() => { + new Pattern(['x'], ['x'], 2, process.platform) +}) + +t.throws(() => { + new Pattern(['x'], ['x'], -1, process.platform) +}) + +t.throws(() => { + new Pattern(['x', 'x'], ['x', 'x', 'x'], 0, process.platform) +}) + +const s = new Pattern(['x'], ['x'], 0, process.platform) +const g = new Pattern([GLOBSTAR], ['**'], 0, process.platform) +const r = new Pattern([/./], ['?'], 0, process.platform) +t.equal(s.isString(), true) +t.equal(g.isString(), false) +t.equal(r.isString(), false) + +t.equal(s.isGlobstar(), false) +t.equal(g.isGlobstar(), true) +t.equal(r.isGlobstar(), false) + +t.equal(s.isRegExp(), false) +t.equal(g.isRegExp(), false) +t.equal(r.isRegExp(), true) diff --git a/test/platform.ts b/test/platform.ts new file mode 100644 index 00000000..5c087b32 --- /dev/null +++ b/test/platform.ts @@ -0,0 +1,57 @@ +import t from 'tap' + +import { + PathScurry, + PathScurryDarwin, + PathScurryPosix, + PathScurryWin32, +} from 'path-scurry' +import { Glob } from '../' + +t.test('default platform is process.platform', t => { + const g = new Glob('.', {}) + t.equal(g.platform, process.platform) + t.end() +}) + +t.test('default linux when not found', async t => { + const prop = Object.getOwnPropertyDescriptor(process, 'platform') + if (!prop) throw new Error('no platform?') + t.teardown(() => { + Object.defineProperty(process, 'platform', prop) + }) + Object.defineProperty(process, 'platform', { + value: null, + configurable: true, + }) + const { Glob } = t.mock('../', {}) + const g = new Glob('.', {}) + t.equal(g.platform, 'linux') + t.end() +}) + +t.test('set platform, get appropriate scurry object', t => { + t.equal( + new Glob('.', { platform: 'darwin' }).scurry.constructor, + PathScurryDarwin + ) + t.equal( + new Glob('.', { platform: 'linux' }).scurry.constructor, + PathScurryPosix + ) + t.equal( + new Glob('.', { platform: 'win32' }).scurry.constructor, + PathScurryWin32 + ) + t.equal(new Glob('.', {}).scurry.constructor, PathScurry) + t.end() +}) + +t.test('set scurry, sets nocase and scurry', t => { + const scurry = new PathScurryWin32('.') + t.throws(() => new Glob('.', { scurry, nocase: false })) + const g = new Glob('.', { scurry }) + t.equal(g.scurry, scurry) + t.equal(g.nocase, true) + t.end() +}) diff --git a/test/realpath.ts b/test/realpath.ts index 9561bdc1..f0e9da2f 100644 --- a/test/realpath.ts +++ b/test/realpath.ts @@ -3,7 +3,7 @@ import * as fsp from 'fs/promises' import { resolve } from 'path' import t from 'tap' import glob from '../' -import type { GlobOptions } from '../src/index.js' +import { GlobOptionsWithFileTypesUnset } from '../src/glob' const alphasort = (a: string, b: string) => a.localeCompare(b, 'en') @@ -19,7 +19,11 @@ if (process.platform === 'win32') { // options, results // realpath:true set on each option - type Case = [options: GlobOptions, results: string[], pattern?: string] + type Case = [ + options: GlobOptionsWithFileTypesUnset, + results: string[], + pattern?: string + ] const cases: Case[] = [ [{}, ['a/symlink', 'a/symlink/a', 'a/symlink/a/b']], diff --git a/test/stream.ts b/test/stream.ts new file mode 100644 index 00000000..37ca8072 --- /dev/null +++ b/test/stream.ts @@ -0,0 +1,161 @@ +import { resolve, sep } from 'path' +import t from 'tap' +import { Glob } from '../' +const fixture = resolve(__dirname, 'fixtures/a') +const j = (a: string[]) => a.map(a => a.split('/').join(sep)) +const expect = j([ + '', + 'z', + 'x', + 'symlink', + 'cb', + 'c', + 'bc', + 'b', + 'abcfed', + 'abcdef', + 'symlink/a', + 'symlink/a/b', + 'symlink/a/b/c', + 'cb/e', + 'cb/e/f', + 'c/d', + 'c/d/c', + 'c/d/c/b', + 'bc/e', + 'bc/e/f', + 'b/c', + 'b/c/d', + 'abcfed/g', + 'abcfed/g/h', + 'abcdef/g', + 'abcdef/g/h', +]) + +t.test('stream', t => { + let sync: boolean = true + const s = new Glob('./**', { cwd: fixture }) + const stream = s.stream() + const e = new Set(expect) + stream.on('data', c => { + t.equal(e.has(c), true, JSON.stringify(c)) + e.delete(c) + }) + stream.on('end', () => { + t.equal(e.size, 0, 'saw all entries') + t.equal(sync, false, 'did not finish in one tick') + const d = new Glob('./**', s) + const dream = d.stream() + const f = new Set(expect) + dream.on('data', c => { + t.equal(f.has(c), true, JSON.stringify(c)) + f.delete(c) + }) + dream.on('end', () => { + t.equal(f.size, 0, 'saw all entries') + t.end() + }) + }) + sync = false +}) + +t.test('streamSync', t => { + let sync: boolean = true + const s = new Glob('./**', { cwd: fixture }) + const stream = s.streamSync() + const e = new Set(expect) + stream.on('data', c => { + t.equal(e.has(c), true, JSON.stringify(c)) + e.delete(c) + }) + stream.on('end', () => { + t.equal(e.size, 0, 'saw all entries') + const d = new Glob('./**', s) + const dream = d.streamSync() + const f = new Set(expect) + dream.on('data', c => { + t.equal(f.has(c), true, JSON.stringify(c)) + f.delete(c) + }) + dream.on('end', () => { + t.equal(f.size, 0, 'saw all entries') + t.equal(sync, true, 'finished synchronously') + t.end() + }) + }) + sync = false +}) + +t.test('iterate', async t => { + const s = new Glob('./**', { cwd: fixture }) + const e = new Set(expect) + for await (const c of s.iterate()) { + t.equal(e.has(c), true, JSON.stringify(c)) + e.delete(c) + } + t.equal(e.size, 0, 'saw all entries') + + const f = new Set(expect) + const d = new Glob('./**', s) + for await (const c of d.iterate()) { + t.equal(f.has(c), true, JSON.stringify(c)) + f.delete(c) + } + t.equal(f.size, 0, 'saw all entries') +}) + +t.test('iterateSync', t => { + const s = new Glob('./**', { cwd: fixture }) + const e = new Set(expect) + for (const c of s.iterateSync()) { + t.equal(e.has(c), true, JSON.stringify(c)) + e.delete(c) + } + t.equal(e.size, 0, 'saw all entries') + + const f = new Set(expect) + const d = new Glob('./**', s) + for (const c of d.iterateSync()) { + t.equal(f.has(c), true, JSON.stringify(c)) + f.delete(c) + } + t.equal(f.size, 0, 'saw all entries') + t.end() +}) + +t.test('for await', async t => { + const s = new Glob('./**', { cwd: fixture }) + const e = new Set(expect) + for await (const c of s) { + t.equal(e.has(c), true, JSON.stringify(c)) + e.delete(c) + } + t.equal(e.size, 0, 'saw all entries') + + const f = new Set(expect) + const d = new Glob('./**', s) + for await (const c of d) { + t.equal(f.has(c), true, JSON.stringify(c)) + f.delete(c) + } + t.equal(f.size, 0, 'saw all entries') +}) + +t.test('for of', t => { + const s = new Glob('./**', { cwd: fixture }) + const e = new Set(expect) + for (const c of s) { + t.equal(e.has(c), true, JSON.stringify(c)) + e.delete(c) + } + t.equal(e.size, 0, 'saw all entries') + + const f = new Set(expect) + const d = new Glob('./**', s) + for (const c of d) { + t.equal(f.has(c), true, JSON.stringify(c)) + f.delete(c) + } + t.equal(f.size, 0, 'saw all entries') + t.end() +})