Skip to content

Commit 62cbbc5

Browse files
committed
refactor(releases): expose createMatcher and sort methods alphabetically
Exposes createMatcher utility function for pattern matching (glob, prefix/suffix, RegExp). Reorganizes all releases module functions alphabetically per coding standards (constants/types first, private functions second, exported functions last).
1 parent f2c6336 commit 62cbbc5

File tree

2 files changed

+214
-215
lines changed

2 files changed

+214
-215
lines changed

src/releases/github.ts

Lines changed: 187 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -10,84 +10,6 @@ import { getDefaultLogger } from '../logger.js'
1010
import { pRetry } from '../promises.js'
1111
import { spawn } from '../spawn.js'
1212

13-
const logger = getDefaultLogger()
14-
15-
/**
16-
* Retry configuration for GitHub API requests.
17-
* Uses exponential backoff to handle transient failures and rate limiting.
18-
*/
19-
const RETRY_CONFIG = Object.freeze({
20-
__proto__: null,
21-
// Exponential backoff: delay doubles with each retry (5s, 10s, 20s).
22-
backoffFactor: 2,
23-
// Initial delay before first retry.
24-
baseDelayMs: 5000,
25-
// Maximum number of retry attempts (excluding initial request).
26-
retries: 2,
27-
})
28-
29-
let _fs: typeof import('node:fs') | undefined
30-
let _path: typeof import('node:path') | undefined
31-
32-
/**
33-
* Create a matcher function for a pattern using picomatch for glob patterns
34-
* or simple prefix/suffix matching for object patterns.
35-
*
36-
* @param pattern - Pattern to match (string glob, prefix/suffix object, or RegExp)
37-
* @returns Function that tests if a string matches the pattern
38-
* @private
39-
*/
40-
function createMatcher(
41-
pattern: string | { prefix: string; suffix: string } | RegExp,
42-
): (input: string) => boolean {
43-
if (typeof pattern === 'string') {
44-
// Use picomatch for glob pattern matching.
45-
const isMatch = picomatch(pattern)
46-
return (input: string) => isMatch(input)
47-
}
48-
49-
if (pattern instanceof RegExp) {
50-
return (input: string) => pattern.test(input)
51-
}
52-
53-
// Prefix/suffix object pattern (backward compatible).
54-
const { prefix, suffix } = pattern
55-
return (input: string) => input.startsWith(prefix) && input.endsWith(suffix)
56-
}
57-
58-
/**
59-
* Lazily load the fs module to avoid Webpack errors.
60-
* Uses non-'node:' prefixed require to prevent Webpack bundling issues.
61-
*
62-
* @private
63-
*/
64-
/*@__NO_SIDE_EFFECTS__*/
65-
function getFs() {
66-
if (_fs === undefined) {
67-
// Use non-'node:' prefixed require to avoid Webpack errors.
68-
69-
_fs = /*@__PURE__*/ require('fs')
70-
}
71-
return _fs as typeof import('node:fs')
72-
}
73-
74-
/**
75-
* Lazily load the path module to avoid Webpack errors.
76-
* Uses non-'node:' prefixed require to prevent Webpack bundling issues.
77-
*
78-
* @returns The Node.js path module
79-
* @private
80-
*/
81-
/*@__NO_SIDE_EFFECTS__*/
82-
function getPath() {
83-
if (_path === undefined) {
84-
// Use non-'node:' prefixed require to avoid Webpack errors.
85-
86-
_path = /*@__PURE__*/ require('path')
87-
}
88-
return _path as typeof import('node:path')
89-
}
90-
9113
/**
9214
* Pattern for matching release assets.
9315
* Can be either:
@@ -151,6 +73,20 @@ export interface RepoConfig {
15173
repo: string
15274
}
15375

76+
/**
77+
* Retry configuration for GitHub API requests.
78+
* Uses exponential backoff to handle transient failures and rate limiting.
79+
*/
80+
const RETRY_CONFIG = Object.freeze({
81+
__proto__: null,
82+
// Exponential backoff: delay doubles with each retry (5s, 10s, 20s).
83+
backoffFactor: 2,
84+
// Initial delay before first retry.
85+
baseDelayMs: 5000,
86+
// Maximum number of retry attempts (excluding initial request).
87+
retries: 2,
88+
})
89+
15490
/**
15591
* Socket-btm GitHub repository configuration.
15692
*/
@@ -159,6 +95,179 @@ export const SOCKET_BTM_REPO = {
15995
repo: 'socket-btm',
16096
} as const
16197

98+
const logger = getDefaultLogger()
99+
100+
let _fs: typeof import('node:fs') | undefined
101+
let _path: typeof import('node:path') | undefined
102+
103+
/**
104+
* Lazily load the fs module to avoid Webpack errors.
105+
* Uses non-'node:' prefixed require to prevent Webpack bundling issues.
106+
*
107+
* @private
108+
*/
109+
/*@__NO_SIDE_EFFECTS__*/
110+
function getFs() {
111+
if (_fs === undefined) {
112+
// Use non-'node:' prefixed require to avoid Webpack errors.
113+
114+
_fs = /*@__PURE__*/ require('fs')
115+
}
116+
return _fs as typeof import('node:fs')
117+
}
118+
119+
/**
120+
* Lazily load the path module to avoid Webpack errors.
121+
* Uses non-'node:' prefixed require to prevent Webpack bundling issues.
122+
*
123+
* @returns The Node.js path module
124+
* @private
125+
*/
126+
/*@__NO_SIDE_EFFECTS__*/
127+
function getPath() {
128+
if (_path === undefined) {
129+
// Use non-'node:' prefixed require to avoid Webpack errors.
130+
131+
_path = /*@__PURE__*/ require('path')
132+
}
133+
return _path as typeof import('node:path')
134+
}
135+
136+
/**
137+
* Create a matcher function for a pattern using picomatch for glob patterns
138+
* or simple prefix/suffix matching for object patterns.
139+
*
140+
* @param pattern - Pattern to match (string glob, prefix/suffix object, or RegExp)
141+
* @returns Function that tests if a string matches the pattern
142+
*/
143+
export function createMatcher(
144+
pattern: string | { prefix: string; suffix: string } | RegExp,
145+
): (input: string) => boolean {
146+
if (typeof pattern === 'string') {
147+
// Use picomatch for glob pattern matching.
148+
const isMatch = picomatch(pattern)
149+
return (input: string) => isMatch(input)
150+
}
151+
152+
if (pattern instanceof RegExp) {
153+
return (input: string) => pattern.test(input)
154+
}
155+
156+
// Prefix/suffix object pattern (backward compatible).
157+
const { prefix, suffix } = pattern
158+
return (input: string) => input.startsWith(prefix) && input.endsWith(suffix)
159+
}
160+
161+
/**
162+
* Download a binary from any GitHub repository with version caching.
163+
*
164+
* @param config - Download configuration
165+
* @returns Path to the downloaded binary
166+
*/
167+
export async function downloadGitHubRelease(
168+
config: DownloadGitHubReleaseConfig,
169+
): Promise<string> {
170+
const {
171+
assetName,
172+
binaryName,
173+
cwd = process.cwd(),
174+
downloadDir = 'build/downloaded',
175+
owner,
176+
platformArch,
177+
quiet = false,
178+
removeMacOSQuarantine = true,
179+
repo,
180+
tag: explicitTag,
181+
toolName,
182+
toolPrefix,
183+
} = config
184+
185+
// Get release tag (either explicit or latest).
186+
let tag: string
187+
if (explicitTag) {
188+
tag = explicitTag
189+
} else if (toolPrefix) {
190+
const latestTag = await getLatestRelease(
191+
toolPrefix,
192+
{ owner, repo },
193+
{ quiet },
194+
)
195+
if (!latestTag) {
196+
throw new Error(`No ${toolPrefix} release found in ${owner}/${repo}`)
197+
}
198+
tag = latestTag
199+
} else {
200+
throw new Error('Either toolPrefix or tag must be provided')
201+
}
202+
203+
const path = getPath()
204+
// Resolve download directory (can be absolute or relative to cwd).
205+
const resolvedDownloadDir = path.isAbsolute(downloadDir)
206+
? downloadDir
207+
: path.join(cwd, downloadDir)
208+
209+
// Build download paths following socket-cli pattern.
210+
const binaryDir = path.join(resolvedDownloadDir, toolName, platformArch)
211+
const binaryPath = path.join(binaryDir, binaryName)
212+
const versionPath = path.join(binaryDir, '.version')
213+
214+
// Check if already downloaded.
215+
const fs = getFs()
216+
if (fs.existsSync(versionPath) && fs.existsSync(binaryPath)) {
217+
const cachedVersion = (
218+
await fs.promises.readFile(versionPath, 'utf8')
219+
).trim()
220+
if (cachedVersion === tag) {
221+
if (!quiet) {
222+
logger.info(`Using cached ${toolName} (${platformArch}): ${binaryPath}`)
223+
}
224+
return binaryPath
225+
}
226+
}
227+
228+
// Download the asset.
229+
if (!quiet) {
230+
logger.info(`Downloading ${toolName} for ${platformArch}...`)
231+
}
232+
await downloadReleaseAsset(
233+
tag,
234+
assetName,
235+
binaryPath,
236+
{ owner, repo },
237+
{ quiet },
238+
)
239+
240+
// Make executable on Unix-like systems.
241+
const isWindows = binaryName.endsWith('.exe')
242+
if (!isWindows) {
243+
fs.chmodSync(binaryPath, 0o755)
244+
245+
// Remove macOS quarantine attribute if present (only on macOS host for macOS target).
246+
if (
247+
removeMacOSQuarantine &&
248+
process.platform === 'darwin' &&
249+
platformArch.startsWith('darwin')
250+
) {
251+
try {
252+
await spawn('xattr', ['-d', 'com.apple.quarantine', binaryPath], {
253+
stdio: 'ignore',
254+
})
255+
} catch {
256+
// Ignore errors - attribute might not exist or xattr might not be available.
257+
}
258+
}
259+
}
260+
261+
// Write version file.
262+
await fs.promises.writeFile(versionPath, tag, 'utf8')
263+
264+
if (!quiet) {
265+
logger.info(`Downloaded ${toolName} to ${binaryPath}`)
266+
}
267+
268+
return binaryPath
269+
}
270+
162271
/**
163272
* Download a specific release asset.
164273
* Supports pattern matching for dynamic asset discovery.
@@ -404,113 +513,3 @@ export async function getReleaseAssetUrl(
404513
},
405514
)
406515
}
407-
408-
/**
409-
* Download a binary from any GitHub repository with version caching.
410-
*
411-
* @param config - Download configuration
412-
* @returns Path to the downloaded binary
413-
*/
414-
export async function downloadGitHubRelease(
415-
config: DownloadGitHubReleaseConfig,
416-
): Promise<string> {
417-
const {
418-
assetName,
419-
binaryName,
420-
cwd = process.cwd(),
421-
downloadDir = 'build/downloaded',
422-
owner,
423-
platformArch,
424-
quiet = false,
425-
removeMacOSQuarantine = true,
426-
repo,
427-
tag: explicitTag,
428-
toolName,
429-
toolPrefix,
430-
} = config
431-
432-
// Get release tag (either explicit or latest).
433-
let tag: string
434-
if (explicitTag) {
435-
tag = explicitTag
436-
} else if (toolPrefix) {
437-
const latestTag = await getLatestRelease(
438-
toolPrefix,
439-
{ owner, repo },
440-
{ quiet },
441-
)
442-
if (!latestTag) {
443-
throw new Error(`No ${toolPrefix} release found in ${owner}/${repo}`)
444-
}
445-
tag = latestTag
446-
} else {
447-
throw new Error('Either toolPrefix or tag must be provided')
448-
}
449-
450-
const path = getPath()
451-
// Resolve download directory (can be absolute or relative to cwd).
452-
const resolvedDownloadDir = path.isAbsolute(downloadDir)
453-
? downloadDir
454-
: path.join(cwd, downloadDir)
455-
456-
// Build download paths following socket-cli pattern.
457-
const binaryDir = path.join(resolvedDownloadDir, toolName, platformArch)
458-
const binaryPath = path.join(binaryDir, binaryName)
459-
const versionPath = path.join(binaryDir, '.version')
460-
461-
// Check if already downloaded.
462-
const fs = getFs()
463-
if (fs.existsSync(versionPath) && fs.existsSync(binaryPath)) {
464-
const cachedVersion = (
465-
await fs.promises.readFile(versionPath, 'utf8')
466-
).trim()
467-
if (cachedVersion === tag) {
468-
if (!quiet) {
469-
logger.info(`Using cached ${toolName} (${platformArch}): ${binaryPath}`)
470-
}
471-
return binaryPath
472-
}
473-
}
474-
475-
// Download the asset.
476-
if (!quiet) {
477-
logger.info(`Downloading ${toolName} for ${platformArch}...`)
478-
}
479-
await downloadReleaseAsset(
480-
tag,
481-
assetName,
482-
binaryPath,
483-
{ owner, repo },
484-
{ quiet },
485-
)
486-
487-
// Make executable on Unix-like systems.
488-
const isWindows = binaryName.endsWith('.exe')
489-
if (!isWindows) {
490-
fs.chmodSync(binaryPath, 0o755)
491-
492-
// Remove macOS quarantine attribute if present (only on macOS host for macOS target).
493-
if (
494-
removeMacOSQuarantine &&
495-
process.platform === 'darwin' &&
496-
platformArch.startsWith('darwin')
497-
) {
498-
try {
499-
await spawn('xattr', ['-d', 'com.apple.quarantine', binaryPath], {
500-
stdio: 'ignore',
501-
})
502-
} catch {
503-
// Ignore errors - attribute might not exist or xattr might not be available.
504-
}
505-
}
506-
}
507-
508-
// Write version file.
509-
await fs.promises.writeFile(versionPath, tag, 'utf8')
510-
511-
if (!quiet) {
512-
logger.info(`Downloaded ${toolName} to ${binaryPath}`)
513-
}
514-
515-
return binaryPath
516-
}

0 commit comments

Comments
 (0)