From 99c594686c88c2fe95f28c3bc68c94244454b39f Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Fri, 5 Jun 2026 06:52:28 +0300 Subject: [PATCH 1/2] Remove minimatch from cli-kit; use native path.matchesGlob Co-Authored-By: Claude --- .changeset/remove-cli-kit-minimatch.md | 5 +++ packages/cli-kit/package.json | 1 - packages/cli-kit/src/public/node/fs.test.ts | 36 +++++++++++++++++++++ packages/cli-kit/src/public/node/fs.ts | 36 ++++++++++++++++++--- pnpm-lock.yaml | 3 -- 5 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 .changeset/remove-cli-kit-minimatch.md diff --git a/.changeset/remove-cli-kit-minimatch.md b/.changeset/remove-cli-kit-minimatch.md new file mode 100644 index 00000000000..08d1b5d3300 --- /dev/null +++ b/.changeset/remove-cli-kit-minimatch.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli-kit': patch +--- + +Remove the minimatch dependency from cli-kit glob matching. diff --git a/packages/cli-kit/package.json b/packages/cli-kit/package.json index 1725431d06d..6418c311f67 100644 --- a/packages/cli-kit/package.json +++ b/packages/cli-kit/package.json @@ -143,7 +143,6 @@ "liquidjs": "10.26.0", "lodash": "4.17.23", "macaddress": "0.5.3", - "minimatch": "9.0.8", "mrmime": "1.0.1", "network-interfaces": "1.1.0", "node-abort-controller": "3.1.1", diff --git a/packages/cli-kit/src/public/node/fs.test.ts b/packages/cli-kit/src/public/node/fs.test.ts index d30612b5b96..38674abc480 100644 --- a/packages/cli-kit/src/public/node/fs.test.ts +++ b/packages/cli-kit/src/public/node/fs.test.ts @@ -22,6 +22,7 @@ import { copyDirectoryContents, symlink, fileRealPath, + matchGlob, } from './fs.js' import {joinPath, normalizePath} from './path.js' import * as array from '../common/array.js' @@ -319,6 +320,41 @@ describe('glob', () => { }) }) +describe('matchGlob', () => { + test('matches liquid file patterns with native Node glob semantics', () => { + expect(matchGlob('theme.liquid', '*.liquid')).toBe(true) + expect(matchGlob('layout/theme.liquid', '*.liquid')).toBe(false) + expect(matchGlob('layout/theme.liquid', '**/*.liquid')).toBe(true) + expect(matchGlob('assets/theme.css', '*.liquid')).toBe(false) + }) + + test('matches watch-style globs used by extension file watching', () => { + expect(matchGlob('src/main.rs', 'src/**/*.rs')).toBe(true) + expect(matchGlob('src/lib/main.rs', 'src/**/*.rs')).toBe(true) + expect(matchGlob('src/main.ts', 'src/**/*.rs')).toBe(false) + expect(matchGlob('/tmp/my-function/src/main.rs', '/tmp/my-function/src/**/*.rs')).toBe(true) + expect(matchGlob('/tmp/my-function/src/lib/main.rs', '/tmp/my-function/src/**/*.rs')).toBe(true) + }) + + test('matches project config selection globs', () => { + expect(matchGlob('extensions/my-ext/shopify.extension.toml', 'extensions/*/*.extension.toml')).toBe(true) + expect(matchGlob('extensions/nested/my-ext/shopify.extension.toml', 'extensions/*/*.extension.toml')).toBe(false) + expect(matchGlob('web/shopify.web.toml', '**/shopify.web.toml')).toBe(true) + }) + + test('supports matchBase for theme ignore patterns', () => { + expect(matchGlob('assets/theme.css', '*.css', {matchBase: true, noglobstar: true})).toBe(true) + expect(matchGlob('assets/theme.css', '*.liquid', {matchBase: true, noglobstar: true})).toBe(false) + }) + + test('supports noglobstar for theme ignore patterns', () => { + expect( + matchGlob('templates/customers/account.json', 'templates/**/*.json', {matchBase: true, noglobstar: true}), + ).toBe(true) + expect(matchGlob('templates/404.json', 'templates/**/*.json', {matchBase: true, noglobstar: true})).toBe(false) + }) +}) + describe('detectEOL', () => { test('detects the EOL of a file', async () => { // Given diff --git a/packages/cli-kit/src/public/node/fs.ts b/packages/cli-kit/src/public/node/fs.ts index 5948c9f6bb6..266eec86255 100644 --- a/packages/cli-kit/src/public/node/fs.ts +++ b/packages/cli-kit/src/public/node/fs.ts @@ -16,8 +16,9 @@ import { import {sep, join} from 'pathe' import {findUp as internalFindUp, findUpSync as internalFindUpSync} from 'find-up' -import {minimatch} from 'minimatch' import fastGlobLib from 'fast-glob' +// eslint-disable-next-line no-restricted-imports -- Node 22 native glob matching is not wrapped by cli-kit path helpers. +import * as nodePath from 'node:path' import { mkdirSync as fsMkdirSync, readFileSync as fsReadFileSync, @@ -688,10 +689,12 @@ export function findPathUpSync( } export interface MatchGlobOptions { - matchBase: boolean - noglobstar: boolean + matchBase?: boolean + noglobstar?: boolean } +const {matchesGlob} = nodePath as typeof nodePath & {matchesGlob(path: string, pattern: string): boolean} + /** * Matches a key against a glob pattern. * @param key - The key to match. @@ -699,8 +702,31 @@ export interface MatchGlobOptions { * @param options - The options to refine the matching approach. * @returns true if the key matches the pattern, false otherwise. */ -export function matchGlob(key: string, pattern: string, options?: MatchGlobOptions): boolean { - return minimatch(key, pattern, options) +export function matchGlob(key: string, pattern: string, options: MatchGlobOptions = {}): boolean { + const effectivePattern = options.noglobstar ? patternWithoutGlobstars(pattern) : pattern + + if (matchesGlob(key, effectivePattern)) return true + + if (options.matchBase && !hasPathSeparator(effectivePattern)) { + return matchesGlob(baseName(key), effectivePattern) + } + + return false +} + +function patternWithoutGlobstars(pattern: string): string { + return pattern + .split(/([/\\])/) + .map((part) => (part === '**' ? '*' : part)) + .join('') +} + +function hasPathSeparator(path: string): boolean { + return path.includes('/') || path.includes('\\') +} + +function baseName(path: string): string { + return path.split(/[/\\]/).pop() ?? path } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6355cc39894..990ab02abb2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -420,9 +420,6 @@ importers: macaddress: specifier: 0.5.3 version: 0.5.3 - minimatch: - specifier: 9.0.8 - version: 9.0.8 mrmime: specifier: 1.0.1 version: 1.0.1 From 6736becbb4636218ec51e8e9fe666510f2d9f115 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Sun, 7 Jun 2026 16:30:48 +0300 Subject: [PATCH 2/2] Route cli-kit fs path helpers through path.ts; upgrade pathe to 2.0.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fs.ts imported sep/join/basename from pathe and matchesGlob from node:path directly, bypassing the path.ts wrapper that exists for cross-env consistency. Upgrading pathe to 2.0.3 brings a native, typed, cross-env matchesGlob, so path.ts can wrap it too — fs.ts no longer touches node:path or pathe directly. Also drops the hand-rolled baseName in favor of pathe's cross-env basename, and pathe 2 fixes the Windows PATH delimiter (now ';') used by system.ts's binary safety check. Removes the now-redundant changeset since the change is transparent to users. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/remove-cli-kit-minimatch.md | 5 ----- package.json | 2 +- packages/cli-kit/package.json | 2 +- packages/cli-kit/src/public/node/fs.ts | 19 +++++-------------- packages/cli-kit/src/public/node/path.ts | 18 ++++++++++++++++++ pnpm-lock.yaml | 20 +++++--------------- 6 files changed, 30 insertions(+), 36 deletions(-) delete mode 100644 .changeset/remove-cli-kit-minimatch.md diff --git a/.changeset/remove-cli-kit-minimatch.md b/.changeset/remove-cli-kit-minimatch.md deleted file mode 100644 index 08d1b5d3300..00000000000 --- a/.changeset/remove-cli-kit-minimatch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@shopify/cli-kit': patch ---- - -Remove the minimatch dependency from cli-kit glob matching. diff --git a/package.json b/package.json index 20bed9b068a..74dac1fd9b9 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "nx": "22.7.0", "oclif": "4.23.0", "octokit-plugin-create-pull-request": "^3.12.2", - "pathe": "1.1.1", + "pathe": "2.0.3", "pin-github-action": "^3.3.1", "rimraf": "^6.1.3", "ts-node": "^10.9.1", diff --git a/packages/cli-kit/package.json b/packages/cli-kit/package.json index 6418c311f67..0041641407c 100644 --- a/packages/cli-kit/package.json +++ b/packages/cli-kit/package.json @@ -148,7 +148,7 @@ "node-abort-controller": "3.1.1", "node-fetch": "3.3.2", "open": "8.4.2", - "pathe": "1.1.2", + "pathe": "2.0.3", "react": "19.2.4", "semver": "7.6.3", "stacktracey": "2.1.8", diff --git a/packages/cli-kit/src/public/node/fs.ts b/packages/cli-kit/src/public/node/fs.ts index 266eec86255..bc16cabae91 100644 --- a/packages/cli-kit/src/public/node/fs.ts +++ b/packages/cli-kit/src/public/node/fs.ts @@ -1,5 +1,5 @@ import {outputContent, outputToken, outputDebug} from './output.js' -import {joinPath, normalizePath, resolvePath} from './path.js' +import {basename, joinPath, matchesGlob, normalizePath, resolvePath, sep} from './path.js' import {OverloadParameters} from '../../private/common/ts/overloaded-parameters.js' import {getRandomName, RandomNameFamily} from '../common/string.js' import {systemTempDir} from '../../private/node/temp-dir.js' @@ -14,11 +14,8 @@ import { // @ts-ignore } from 'fs-extra/esm' -import {sep, join} from 'pathe' import {findUp as internalFindUp, findUpSync as internalFindUpSync} from 'find-up' import fastGlobLib from 'fast-glob' -// eslint-disable-next-line no-restricted-imports -- Node 22 native glob matching is not wrapped by cli-kit path helpers. -import * as nodePath from 'node:path' import { mkdirSync as fsMkdirSync, readFileSync as fsReadFileSync, @@ -68,7 +65,7 @@ import type {Pattern, Options as GlobOptions} from 'fast-glob' */ export function stripUpPath(path: string, strip: number): string { const parts = path.split(sep) - return join(...parts.slice(strip)) + return joinPath(...parts.slice(strip)) } /** @@ -77,7 +74,7 @@ export function stripUpPath(path: string, strip: number): string { * @param callback - The callback that receives the temporary directory. */ export async function inTemporaryDirectory(callback: (tmpDir: string) => T | Promise): Promise { - const tmpDir = await fsMkdtemp(join(systemTempDir, 'tmp-')) + const tmpDir = await fsMkdtemp(joinPath(systemTempDir, 'tmp-')) try { return await callback(tmpDir) } finally { @@ -90,7 +87,7 @@ export async function inTemporaryDirectory(callback: (tmpDir: string) => T | * @returns - The path to the temporary directory. */ export function tempDirectory(): string { - return fsMkdtempSync(join(systemTempDir, 'tmp-')) + return fsMkdtempSync(joinPath(systemTempDir, 'tmp-')) } /** @@ -693,8 +690,6 @@ export interface MatchGlobOptions { noglobstar?: boolean } -const {matchesGlob} = nodePath as typeof nodePath & {matchesGlob(path: string, pattern: string): boolean} - /** * Matches a key against a glob pattern. * @param key - The key to match. @@ -708,7 +703,7 @@ export function matchGlob(key: string, pattern: string, options: MatchGlobOption if (matchesGlob(key, effectivePattern)) return true if (options.matchBase && !hasPathSeparator(effectivePattern)) { - return matchesGlob(baseName(key), effectivePattern) + return matchesGlob(basename(key), effectivePattern) } return false @@ -725,10 +720,6 @@ function hasPathSeparator(path: string): boolean { return path.includes('/') || path.includes('\\') } -function baseName(path: string): string { - return path.split(/[/\\]/).pop() ?? path -} - /** * Read a directory. * @param path - The path to read. diff --git a/packages/cli-kit/src/public/node/path.ts b/packages/cli-kit/src/public/node/path.ts index f3721780110..ddc0e171a9d 100644 --- a/packages/cli-kit/src/public/node/path.ts +++ b/packages/cli-kit/src/public/node/path.ts @@ -8,6 +8,8 @@ import { extname as extnamePathe, parse, isAbsolute, + matchesGlob as matchesGlobPathe, + sep as patheSep, } from 'pathe' import {fileURLToPath} from 'url' // eslint-disable-next-line n/prefer-global/url @@ -95,6 +97,22 @@ export function extname(path: string): string { return extnamePathe(path) } +/** + * The platform-agnostic path segment separator (always `/`, via pathe). + */ +export const sep = patheSep + +/** + * Determines whether a path matches a glob pattern. + * + * @param path - The path to match. + * @param pattern - The glob pattern (or patterns) to match against. + * @returns Whether the path matches the pattern. + */ +export function matchesGlob(path: string, pattern: string | string[]): boolean { + return matchesGlobPathe(path, pattern) +} + /** * Parses a path into its components (root, dir, base, ext, name). * diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 990ab02abb2..6b502e9c356 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,8 +122,8 @@ importers: specifier: ^3.12.2 version: 3.13.1 pathe: - specifier: 1.1.1 - version: 1.1.1 + specifier: 2.0.3 + version: 2.0.3 pin-github-action: specifier: ^3.3.1 version: 3.4.0 @@ -436,8 +436,8 @@ importers: specifier: 8.4.2 version: 8.4.2 pathe: - specifier: 1.1.2 - version: 1.1.2 + specifier: 2.0.3 + version: 2.0.3 react: specifier: 19.2.4 version: 19.2.4 @@ -7669,14 +7669,8 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - pathe@1.1.1: - resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} - - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, tarball: https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz} pathval@2.0.1: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} @@ -17667,10 +17661,6 @@ snapshots: path-type@4.0.0: {} - pathe@1.1.1: {} - - pathe@1.1.2: {} - pathe@2.0.3: {} pathval@2.0.1: {}