Skip to content

Commit 1fe8998

Browse files
committed
feat(releases): upgrade to picomatch 4.0.3 and use for glob matching
- Add pnpm override for picomatch 4.0.3 - Refactor wildcard implementation in releases/github.ts to use picomatch - Replace parseWildcardPattern() with createMatcher() using picomatch.isMatch() - Support full glob syntax including braces, multiple wildcards, and globstar - Maintain backward compatibility with prefix/suffix objects and RegExp patterns - Update JSDoc to document glob support and provide examples - Add comprehensive picomatch integration tests covering: - Simple wildcard patterns - Multiple wildcards - Brace expansion - Exact matches - Prefix/suffix wildcards - Globstar patterns - Case sensitivity Fix external-imports script bugs: - Correct distDir path resolution (was pointing to scripts/dist instead of dist) - Fix regex pattern consumption issue by creating separate test pattern - Add comment periods for consistency with codebase style The new implementation provides more robust pattern matching using picomatch while maintaining the same API. Existing code using simple wildcards like 'yoga-sync-*.mjs' will continue to work, but can now also use advanced glob features like '{yoga,models}-*.{mjs,js}' for more flexible matching.
1 parent 1c46fea commit 1fe8998

5 files changed

Lines changed: 135 additions & 87 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,7 @@
787787
"@sigstore/sign": "4.1.0",
788788
"ansi-regex": "6.2.2",
789789
"lru-cache": "11.2.2",
790+
"picomatch": "4.0.3",
790791
"semver": "7.7.2",
791792
"string-width": "8.1.0",
792793
"strip-ansi": "7.1.2",

pnpm-lock.yaml

Lines changed: 5 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/fix/external-imports.mjs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { externalPackages, scopedPackages } from '../build-externals/config.mjs'
1515
const logger = getDefaultLogger()
1616

1717
const __dirname = path.dirname(fileURLToPath(import.meta.url))
18-
const distDir = path.resolve(__dirname, '..', 'dist')
18+
const distDir = path.resolve(__dirname, '..', '..', 'dist')
1919
const distExternalDir = path.join(distDir, 'external')
2020

2121
// Build list of all external packages to rewrite
@@ -60,18 +60,21 @@ async function fixFileImports(filePath, verbose = false) {
6060
const externalPrefix = getExternalPathPrefix(filePath)
6161

6262
for (const pkg of allExternalPackages) {
63-
// Escape special regex characters in package name
63+
// Escape special regex characters in package name.
6464
const escapedPkg = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
6565

66-
// Match require('pkg') or require("pkg")
67-
// Don't match if it's already pointing to ./external/ or ../external/
66+
// Match require('pkg') or require("pkg").
67+
// Don't match if it's already pointing to ./external/ or ../external/.
6868
const requirePattern = new RegExp(
6969
`require\\((['"])(?!\\.\\.?\\/external\\/)${escapedPkg}\\1\\)`,
7070
'g',
7171
)
7272

73-
if (requirePattern.test(content)) {
74-
// Replace with require('./external/pkg') or require('../external/pkg')
73+
// Create a new regex for testing to avoid consuming matches.
74+
const testPattern = new RegExp(requirePattern.source)
75+
76+
if (testPattern.test(content)) {
77+
// Replace with require('./external/pkg') or require('../external/pkg').
7578
const replacement = `require('${externalPrefix}/${pkg}')`
7679
content = content.replace(requirePattern, replacement)
7780
modified = true
@@ -105,7 +108,7 @@ async function processDirectory(dir, verbose = false) {
105108
for (const entry of entries) {
106109
const fullPath = path.join(dir, entry.name)
107110

108-
// Skip the external directory itself
111+
// Skip the external directory itself.
109112
if (entry.isDirectory() && fullPath === distExternalDir) {
110113
continue
111114
}
@@ -120,7 +123,7 @@ async function processDirectory(dir, verbose = false) {
120123
}
121124
}
122125
} catch (error) {
123-
// Skip directories that don't exist
126+
// Skip directories that don't exist.
124127
if (error.code !== 'ENOENT') {
125128
throw error
126129
}

src/releases/github.ts

Lines changed: 54 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import { chmodSync, existsSync } from 'fs'
66
import { readFile, writeFile } from 'fs/promises'
77

8+
import picomatch from 'picomatch'
9+
810
import { safeMkdir } from '../fs.js'
911
import { httpDownload, httpRequest } from '../http-request.js'
1012
import { getDefaultLogger } from '../logger.js'
@@ -269,15 +271,19 @@ export function getAuthHeaders(): Record<string, string> {
269271
/**
270272
* Pattern for matching release assets.
271273
* Can be either:
272-
* - A string with wildcard (*) for simple glob patterns (e.g., 'yoga-sync-*.mjs')
273-
* - A prefix/suffix pair for explicit matching
274+
* - A string with glob pattern syntax
275+
* - A prefix/suffix pair for explicit matching (backward compatible)
274276
* - A RegExp for complex patterns
275277
*
276-
* String patterns support a single wildcard (*) which matches any characters:
277-
* - 'yoga-sync-*.mjs' → prefix: 'yoga-sync-', suffix: '.mjs'
278-
* - 'models-*.tar.gz' → prefix: 'models-', suffix: '.tar.gz'
279-
* - '*-models.tar.gz' → prefix: '', suffix: '-models.tar.gz'
280-
* - 'yoga-*' → prefix: 'yoga-', suffix: ''
278+
* String patterns support full glob syntax via picomatch.
279+
* Examples:
280+
* - Simple wildcard: yoga-sync-*.mjs matches yoga-sync-abc123.mjs
281+
* - Complex: models-*.tar.gz matches models-2024-01-15.tar.gz
282+
* - Prefix wildcard: *-models.tar.gz matches foo-models.tar.gz
283+
* - Suffix wildcard: yoga-* matches yoga-layout
284+
* - Brace expansion: {yoga,models}-*.{mjs,js} matches yoga-abc.mjs or models-xyz.js
285+
*
286+
* For backward compatibility, prefix/suffix objects are still supported but glob patterns are recommended.
281287
*/
282288
export type AssetPattern = string | { prefix: string; suffix: string } | RegExp
283289

@@ -292,55 +298,39 @@ export interface FindReleaseAssetResult {
292298
}
293299

294300
/**
295-
* Parse a wildcard pattern string into prefix/suffix components.
296-
* Supports a single wildcard (*) character.
301+
* Create a matcher function for a pattern using picomatch for glob patterns
302+
* or simple prefix/suffix matching for object patterns.
297303
*
298-
* @param pattern - Pattern string with optional wildcard (e.g., 'yoga-sync-*.mjs')
299-
* @returns Prefix/suffix pair for matching
300-
* @throws Error if pattern contains multiple wildcards
304+
* @param pattern - Pattern to match (string glob, prefix/suffix object, or RegExp)
305+
* @returns Function that tests if a string matches the pattern
301306
*
302307
* @example
303308
* ```ts
304-
* parseWildcardPattern('yoga-sync-*.mjs')
305-
* // Returns: { prefix: 'yoga-sync-', suffix: '.mjs' }
306-
*
307-
* parseWildcardPattern('models-*.tar.gz')
308-
* // Returns: { prefix: 'models-', suffix: '.tar.gz' }
309+
* const matcher = createMatcher('yoga-sync-*.mjs')
310+
* matcher('yoga-sync-abc123.mjs') // true
311+
* matcher('models-xyz.tar.gz') // false
309312
*
310-
* parseWildcardPattern('*-models.tar.gz')
311-
* // Returns: { prefix: '', suffix: '-models.tar.gz' }
312-
*
313-
* parseWildcardPattern('yoga-*')
314-
* // Returns: { prefix: 'yoga-', suffix: '' }
315-
*
316-
* parseWildcardPattern('exact-name.txt')
317-
* // Returns: { prefix: 'exact-name.txt', suffix: '' } (exact match)
313+
* const matcher2 = createMatcher({ prefix: 'yoga-', suffix: '.mjs' })
314+
* matcher2('yoga-sync.mjs') // true
315+
* matcher2('yoga-layout.js') // false
318316
* ```
319317
*/
320-
function parseWildcardPattern(pattern: string): {
321-
prefix: string
322-
suffix: string
323-
} {
324-
const wildcardIndex = pattern.indexOf('*')
325-
326-
// No wildcard - treat as exact match (prefix only).
327-
if (wildcardIndex === -1) {
328-
return { prefix: pattern, suffix: '' }
318+
function createMatcher(
319+
pattern: string | { prefix: string; suffix: string } | RegExp,
320+
): (input: string) => boolean {
321+
if (typeof pattern === 'string') {
322+
// Use picomatch for glob pattern matching.
323+
const isMatch = picomatch(pattern)
324+
return (input: string) => isMatch(input)
329325
}
330326

331-
// Check for multiple wildcards.
332-
const lastWildcardIndex = pattern.lastIndexOf('*')
333-
if (wildcardIndex !== lastWildcardIndex) {
334-
throw new Error(
335-
`Pattern contains multiple wildcards: ${pattern}. Only single wildcard (*) is supported.`,
336-
)
327+
if (pattern instanceof RegExp) {
328+
return (input: string) => pattern.test(input)
337329
}
338330

339-
// Split at wildcard position.
340-
const prefix = pattern.slice(0, wildcardIndex)
341-
const suffix = pattern.slice(wildcardIndex + 1)
342-
343-
return { prefix, suffix }
331+
// Prefix/suffix object pattern (backward compatible).
332+
const { prefix, suffix } = pattern
333+
return (input: string) => input.startsWith(prefix) && input.endsWith(suffix)
344334
}
345335

346336
/**
@@ -349,14 +339,14 @@ function parseWildcardPattern(pattern: string): {
349339
* then finds the first asset matching the provided pattern.
350340
*
351341
* @param toolPrefix - Tool name prefix to search for (e.g., 'yoga-layout-')
352-
* @param assetPattern - Pattern to match asset names (string with wildcard, prefix/suffix object, or RegExp)
342+
* @param assetPattern - Pattern to match asset names (glob string, prefix/suffix object, or RegExp)
353343
* @param repoConfig - Repository configuration (owner/repo)
354344
* @param options - Additional options
355345
* @returns Result with tag and asset name, or null if not found
356346
*
357347
* @example
358348
* ```ts
359-
* // Find yoga-sync asset with wildcard pattern
349+
* // Find yoga-sync asset with glob pattern
360350
* const result = await findReleaseAsset(
361351
* 'yoga-layout-',
362352
* 'yoga-sync-*.mjs',
@@ -367,7 +357,7 @@ function parseWildcardPattern(pattern: string): {
367357
*
368358
* @example
369359
* ```ts
370-
* // Find models tar.gz with wildcard pattern
360+
* // Find models tar.gz with glob pattern
371361
* const result = await findReleaseAsset(
372362
* 'models-',
373363
* 'models-*.tar.gz',
@@ -377,6 +367,16 @@ function parseWildcardPattern(pattern: string): {
377367
*
378368
* @example
379369
* ```ts
370+
* // Find asset with glob braces pattern
371+
* const result = await findReleaseAsset(
372+
* 'yoga-layout-',
373+
* 'yoga-{sync,layout}-*.{mjs,js}',
374+
* { owner: 'SocketDev', repo: 'socket-btm' }
375+
* )
376+
* ```
377+
*
378+
* @example
379+
* ```ts
380380
* // Find asset with object pattern (backward compatible)
381381
* const result = await findReleaseAsset(
382382
* 'yoga-layout-',
@@ -404,13 +404,8 @@ export async function findReleaseAsset(
404404
const { owner, repo } = repoConfig
405405
const { quiet = false } = options
406406

407-
// Normalize string patterns to prefix/suffix objects.
408-
let normalizedPattern: { prefix: string; suffix: string } | RegExp
409-
if (typeof assetPattern === 'string') {
410-
normalizedPattern = parseWildcardPattern(assetPattern)
411-
} else {
412-
normalizedPattern = assetPattern
413-
}
407+
// Create matcher function for the pattern.
408+
const isMatch = createMatcher(assetPattern)
414409

415410
return await pRetry(
416411
async () => {
@@ -435,20 +430,10 @@ export async function findReleaseAsset(
435430
continue
436431
}
437432

438-
// Find matching asset in this release.
439-
let matchingAsset: { name: string } | undefined
440-
441-
if (normalizedPattern instanceof RegExp) {
442-
matchingAsset = assets.find((a: { name: string }) =>
443-
normalizedPattern.test(a.name),
444-
)
445-
} else {
446-
const { prefix, suffix } = normalizedPattern
447-
matchingAsset = assets.find(
448-
(a: { name: string }) =>
449-
a.name.startsWith(prefix) && a.name.endsWith(suffix),
450-
)
451-
}
433+
// Find matching asset in this release using the matcher function.
434+
const matchingAsset = assets.find((a: { name: string }) =>
435+
isMatch(a.name),
436+
)
452437

453438
if (matchingAsset) {
454439
if (!quiet) {

test/unit/releases-github.test.mts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import { describe, expect, it } from 'vitest'
66

7+
import picomatch from 'picomatch'
8+
79
import {
810
getAuthHeaders,
911
SOCKET_BTM_REPO,
@@ -114,4 +116,66 @@ describe('releases/github', () => {
114116
}
115117
})
116118
})
119+
120+
describe('picomatch integration', () => {
121+
it('should match simple wildcard patterns', () => {
122+
const isMatch = picomatch('yoga-sync-*.mjs')
123+
expect(isMatch('yoga-sync-abc123.mjs')).toBe(true)
124+
expect(isMatch('yoga-sync-2024-01-15.mjs')).toBe(true)
125+
expect(isMatch('models-xyz.tar.gz')).toBe(false)
126+
expect(isMatch('yoga-sync.js')).toBe(false)
127+
})
128+
129+
it('should match patterns with multiple wildcards', () => {
130+
const isMatch = picomatch('models-*-*.tar.gz')
131+
expect(isMatch('models-2024-01-15.tar.gz')).toBe(true)
132+
expect(isMatch('models-foo-bar.tar.gz')).toBe(true)
133+
expect(isMatch('models-xyz.tar.gz')).toBe(false)
134+
})
135+
136+
it('should match patterns with braces', () => {
137+
const isMatch = picomatch('yoga-{sync,layout}-*.{mjs,js}')
138+
expect(isMatch('yoga-sync-abc.mjs')).toBe(true)
139+
expect(isMatch('yoga-layout-xyz.js')).toBe(true)
140+
expect(isMatch('yoga-sync-abc.ts')).toBe(false)
141+
expect(isMatch('yoga-other-xyz.mjs')).toBe(false)
142+
})
143+
144+
it('should match exact patterns without wildcards', () => {
145+
const isMatch = picomatch('exact-name.txt')
146+
expect(isMatch('exact-name.txt')).toBe(true)
147+
expect(isMatch('exact-name.md')).toBe(false)
148+
expect(isMatch('other-name.txt')).toBe(false)
149+
})
150+
151+
it('should match patterns starting with wildcard', () => {
152+
const isMatch = picomatch('*-models.tar.gz')
153+
expect(isMatch('foo-models.tar.gz')).toBe(true)
154+
expect(isMatch('bar-models.tar.gz')).toBe(true)
155+
expect(isMatch('models.tar.gz')).toBe(false)
156+
})
157+
158+
it('should match patterns ending with wildcard', () => {
159+
const isMatch = picomatch('yoga-*')
160+
expect(isMatch('yoga-sync')).toBe(true)
161+
expect(isMatch('yoga-layout')).toBe(true)
162+
expect(isMatch('yoga-')).toBe(true)
163+
expect(isMatch('models-sync')).toBe(false)
164+
})
165+
166+
it('should support double-star globstar patterns', () => {
167+
const isMatch = picomatch('**/*.mjs')
168+
expect(isMatch('yoga-sync.mjs')).toBe(true)
169+
expect(isMatch('dir/yoga-sync.mjs')).toBe(true)
170+
expect(isMatch('deep/nested/dir/file.mjs')).toBe(true)
171+
expect(isMatch('file.js')).toBe(false)
172+
})
173+
174+
it('should be case-sensitive by default', () => {
175+
const isMatch = picomatch('yoga-sync-*.mjs')
176+
expect(isMatch('yoga-sync-ABC.mjs')).toBe(true)
177+
expect(isMatch('Yoga-Sync-abc.mjs')).toBe(false)
178+
expect(isMatch('YOGA-SYNC-abc.MJS')).toBe(false)
179+
})
180+
})
117181
})

0 commit comments

Comments
 (0)