diff --git a/.dictionary.txt b/.dictionary.txt index 618e77c4..746fc476 100644 --- a/.dictionary.txt +++ b/.dictionary.txt @@ -1,4 +1,5 @@ attw +barx cefc codecov commitlintrc diff --git a/README.md b/README.md index 2ec03e58..a4d71e3c 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ import { isDeviceRoot, isSep, join, + matchesGlob, normalize, parse, relative, @@ -130,6 +131,7 @@ This package exports the following identifiers: - [`isDeviceRoot`](./src/lib/is-device-root.ts) - [`isSep`](./src/lib/is-sep.ts) - [`join`](./src/lib/join.ts) +- [`matchesGlob`](./src/lib/matches-glob.ts) - [`normalize`](./src/lib/normalize.ts) - [`parse`](./src/lib/parse.ts) - [`posix`](./src/pathe.ts) diff --git a/package.json b/package.json index a06a072c..0a38b9c4 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,9 @@ "typecheck:watch": "vitest --typecheck --mode=typecheck" }, "dependencies": { - "@flex-development/errnode": "3.1.1" + "@flex-development/errnode": "3.1.1", + "@types/micromatch": "4.0.9", + "micromatch": "4.0.8" }, "devDependencies": { "@arethetypeswrong/cli": "0.16.4", diff --git a/src/__snapshots__/index.e2e.snap b/src/__snapshots__/index.e2e.snap index ef278c62..96ea2583 100644 --- a/src/__snapshots__/index.e2e.snap +++ b/src/__snapshots__/index.e2e.snap @@ -16,6 +16,7 @@ exports[`e2e:pathe > should expose public api 1`] = ` "isDeviceRoot", "isSep", "join", + "matchesGlob", "normalize", "parse", "relative", diff --git a/src/interfaces/__tests__/platform-path.spec-d.ts b/src/interfaces/__tests__/platform-path.spec-d.ts index 8c8c756d..ef09ef0e 100644 --- a/src/interfaces/__tests__/platform-path.spec-d.ts +++ b/src/interfaces/__tests__/platform-path.spec-d.ts @@ -13,6 +13,6 @@ describe('unit-d:interfaces/PlatformPath', () => { type Subject = keyof TestSubject // Expect - expectTypeOf>().toEqualTypeOf<'matchesGlob'>() + expectTypeOf>().toEqualTypeOf() }) }) diff --git a/src/interfaces/platform-path.ts b/src/interfaces/platform-path.ts index 8bb0e171..919a480e 100644 --- a/src/interfaces/platform-path.ts +++ b/src/interfaces/platform-path.ts @@ -12,6 +12,7 @@ import type { Ext, Sep } from '@flex-development/pathe' +import type micromatch from 'micromatch' import type FormatInputPathObject from './format-input-path-object' import type ParsedPath from './parsed-path' @@ -158,6 +159,27 @@ interface PlatformPath { */ join(this: void, ...paths: string[]): string + /** + * Check if `path` matches `pattern`. + * + * @see {@linkcode micromatch.Options} + * @see {@linkcode micromatch.isMatch} + * + * @param {string} path + * The path to glob-match against + * @param {string | string[]} pattern + * Glob patterns to use for matching + * @param {micromatch.Options | null | undefined} [options] + * Options for matching + * @return {boolean} + * `true` if `path` matches `pattern`, `false` otherwise + */ + matchesGlob( + path: string, + pattern: string | string[], + options?: micromatch.Options | null | undefined + ): boolean + /** * Normalize `path`, resolving `'..'` and `'.'` segments. * diff --git a/src/lib/__tests__/matches-glob.functional.spec.ts b/src/lib/__tests__/matches-glob.functional.spec.ts new file mode 100644 index 00000000..210cce9b --- /dev/null +++ b/src/lib/__tests__/matches-glob.functional.spec.ts @@ -0,0 +1,63 @@ +/** + * @file Functional Tests - matchesGlob + * @module pathe/lib/tests/functional/matchesGlob + * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-glob.js + */ + +import process from '#internal/process' +import micromatch from 'micromatch' +import type { MockInstance } from 'vitest' +import testSubject from '../matches-glob' +import toPosix from '../to-posix' + +describe('functional:lib/matchesGlob', () => { + let spy: MockInstance + + beforeEach(() => { + spy = vi.spyOn(micromatch, 'isMatch') + }) + + it.each>([ + ['foo/bar/baz', 'foo/**'], + ['foo/bar/baz', 'foo/*/!bar/*/baz'], + ['foo/bar/baz', 'foo/[!bcr]ar/baz'], + ['foo/bar/baz', 'foo/[bc-r]ar/baz'], + ['foo/bar/baz', 'foo/[bcr]ar/baz'], + ['foo/bar/baz/boo', 'foo/[bc-r]ar/baz/*', { ignore: 'f*' }], + ['foo/bar/baz/boo', 'foo/[bc-r]ar/baz/*'], + ['foo/bar1/baz', 'foo/bar[0-9]/baz'], + ['foo/bar5/baz', 'foo/bar[0-9]/baz'], + ['foo/barx/baz', 'foo/bar[a-z]/baz'], + ['foo\\bar1\\baz', ['foo/bar[0-9]/baz']], + ['foo\\bar1\\baz', ['foo\\bar[0-9]\\baz']], + ['foo\\bar5\\baz', ['foo/bar[0-9]/baz']], + ['foo\\bar5\\baz', ['foo\\bar[0-9]\\baz']], + ['foo\\bar\\baz', 'foo\\*\\!bar\\*\\baz'], + ['foo\\bar\\baz', 'foo\\[!bcr]ar\\baz'], + ['foo\\bar\\baz', ['foo/**']], + ['foo\\bar\\baz', ['foo/[bc-r]ar/baz']], + ['foo\\bar\\baz', ['foo/[bcr]ar/baz']], + ['foo\\bar\\baz', ['foo\\**']], + ['foo\\bar\\baz', ['foo\\[bc-r]ar\\baz']], + ['foo\\bar\\baz', ['foo\\[bcr]ar\\baz']], + ['foo\\bar\\baz\\boo', 'foo\\[bc-r]ar\\baz\\*', { ignore: 'f*' }], + ['foo\\bar\\baz\\boo', ['foo/[bc-r]ar/baz/*']], + ['foo\\bar\\baz\\boo', ['foo\\[bc-r]ar\\baz\\*']], + ['foo\\barx\\baz', ['foo/bar[a-z]/baz']], + ['foo\\barx\\baz', ['foo\\bar[a-z]\\baz']] + ])('should call `micromatch.isMatch` (%#)', (path, pattern, options) => { + // Arrange + const pat: typeof pattern = Array.isArray(pattern) + ? pattern + : toPosix(pattern) + + // Act + testSubject(path, pattern, options) + + // Expect + expect(spy).toHaveBeenCalledOnce() + expect(spy.mock.lastCall?.[0]).to.eq(toPosix(path)) + expect(spy.mock.lastCall?.[1]).to.eq(pat) + expect(spy.mock.lastCall?.[2]).to.eql({ ...options, cwd: process.cwd() }) + }) +}) diff --git a/src/lib/index.ts b/src/lib/index.ts index 2e44962b..b712f2fc 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -17,6 +17,7 @@ export { default as isAbsolute } from './is-absolute' export { default as isDeviceRoot } from './is-device-root' export { default as isSep } from './is-sep' export { default as join } from './join' +export { default as matchesGlob } from './matches-glob' export { default as normalize } from './normalize' export { default as parse } from './parse' export { default as relative } from './relative' diff --git a/src/lib/matches-glob.ts b/src/lib/matches-glob.ts new file mode 100644 index 00000000..ee9be4c9 --- /dev/null +++ b/src/lib/matches-glob.ts @@ -0,0 +1,66 @@ +/** + * @file matchesGlob + * @module pathe/lib/matchesGlob + */ + +import process from '#internal/process' +import validateString from '#internal/validate-string' +import micromatch from 'micromatch' +import toPosix from './to-posix' + +/** + * Check if `path` matches `pattern`. + * + * @see {@linkcode micromatch.Options} + * @see {@linkcode micromatch.isMatch} + * + * @category + * core + * + * @param {string} path + * The path to glob-match against + * @param {string | string[]} pattern + * Glob patterns to use for matching + * @param {micromatch.Options | null | undefined} [options] + * Options for matching + * @return {boolean} + * `true` if `path` matches `pattern`, `false` otherwise + */ +function matchesGlob( + path: string, + pattern: string | string[], + options?: micromatch.Options | null | undefined +): boolean { + validateString(path, 'path') + + if (Array.isArray(pattern)) { + /** + * Current index in {@linkcode pattern}. + * + * @var {number} i + */ + let i: number = -1 + + while (++i < pattern.length) { + /** + * Current pattern. + * + * @const {string} pat + */ + const pat: string = pattern[i]! + + validateString(pat, `pattern[${i}]`) + pattern[i] = toPosix(pat) + } + } else { + validateString(pattern, 'pattern') + pattern = toPosix(pattern) + } + + return micromatch.isMatch(toPosix(path), pattern, { + ...options, + cwd: options?.cwd ?? process.cwd() + }) +} + +export default matchesGlob diff --git a/src/pathe.ts b/src/pathe.ts index b399d17a..e8a2051d 100644 --- a/src/pathe.ts +++ b/src/pathe.ts @@ -24,6 +24,7 @@ import { isDeviceRoot, isSep, join, + matchesGlob, normalize, parse, relative, @@ -49,6 +50,7 @@ const posix: PosixPlatformPath = { format, isAbsolute, join, + matchesGlob, normalize, parse, posix: {}, @@ -72,6 +74,7 @@ const win32: WindowsPlatformPath = { format, isAbsolute, join, + matchesGlob, normalize, parse, posix: {}, @@ -112,6 +115,7 @@ const pathe: Pathe = { isDeviceRoot, isSep, join, + matchesGlob, normalize, parse, posix, diff --git a/tsconfig.typecheck.json b/tsconfig.typecheck.json index 2d91b340..3ebf9596 100644 --- a/tsconfig.typecheck.json +++ b/tsconfig.typecheck.json @@ -5,6 +5,7 @@ "src/internal/process.d.mts", "typings/@faker-js/faker/global.d.ts", "typings/@types/node/process.d.ts", + "typings/typescript/lib.es5.d.ts", "vitest-env.d.ts" ], "include": [ diff --git a/typings/typescript/lib.es5.d.ts b/typings/typescript/lib.es5.d.ts new file mode 100644 index 00000000..155f25fc --- /dev/null +++ b/typings/typescript/lib.es5.d.ts @@ -0,0 +1,18 @@ +declare global { + interface ArrayConstructor { + /** + * Check if `value` is an array. + * + * @template {any} T + * Array item type + * + * @param {unknown} value + * Value to check + * @return {value is ReadonlyArray | T[]} + * `true` if `value` is an array + */ + isArray(value: unknown): value is T[] | readonly T[] + } +} + +export {} diff --git a/yarn.lock b/yarn.lock index d6bcf8be..d0f8f9ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1753,6 +1753,7 @@ __metadata: "@types/eslint": "npm:9.6.1" "@types/eslint__js": "npm:8.42.3" "@types/is-ci": "npm:3.0.4" + "@types/micromatch": "npm:4.0.9" "@types/node": "npm:22.5.5" "@types/node-notifier": "npm:8.0.5" "@typescript-eslint/eslint-plugin": "npm:8.6.1-alpha.5" @@ -1786,6 +1787,7 @@ __metadata: is-ci: "npm:3.0.1" jsonc-eslint-parser: "npm:2.4.0" lint-staged: "npm:15.2.10" + micromatch: "npm:4.0.8" node-notifier: "npm:10.0.1" prettier: "npm:3.3.3" pretty-bytes: "npm:6.1.1" @@ -2466,6 +2468,13 @@ __metadata: languageName: node linkType: hard +"@types/braces@npm:*": + version: 3.0.4 + resolution: "@types/braces@npm:3.0.4" + checksum: 10/7324497b6cc34c963c44d3f8516c67a83b749ab4f18defd9418b231b071af7ee8f0a0f345a52b204e867de80f684cabb21158512e1eaecbcebbabed1d1e357a3 + languageName: node + linkType: hard + "@types/concat-stream@npm:^2.0.0": version: 2.0.3 resolution: "@types/concat-stream@npm:2.0.3" @@ -2617,6 +2626,15 @@ __metadata: languageName: node linkType: hard +"@types/micromatch@npm:4.0.9": + version: 4.0.9 + resolution: "@types/micromatch@npm:4.0.9" + dependencies: + "@types/braces": "npm:*" + checksum: 10/324f4bcb4a7caa2048bdd650f442d2c24b5bf6bc95e4d63d4741bd234fdcf3cde140217bd477b2c02c7fb0034c7293037fd7b61429ace84e997dd3b4d3b2b2f7 + languageName: node + linkType: hard + "@types/ms@npm:*": version: 0.7.34 resolution: "@types/ms@npm:0.7.34" @@ -7995,7 +8013,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.4, micromatch@npm:^4.0.8, micromatch@npm:~4.0.8": +"micromatch@npm:4.0.8, micromatch@npm:^4.0.4, micromatch@npm:^4.0.8, micromatch@npm:~4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: