Skip to content

Commit 151315d

Browse files
committed
Add version range auto-force to dlx-package
Automatically force reinstall for version ranges to get latest within range: - Detect range operators: ^, ~, >, <, =, x, X, *, spaces, || - Exact versions (1.0.0) use cache if available - Range versions (^1.0.0) auto-force to get latest - User can override with explicit force: false Add 9 tests for version range detection covering all operator types.
1 parent 23b926e commit 151315d

File tree

2 files changed

+80
-1
lines changed

2 files changed

+80
-1
lines changed

src/dlx-package.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
* - Each unique spec gets its own directory: ~/.socket/_dlx/<hash>/
1010
* - Allows caching multiple versions of the same package
1111
*
12+
* Version range handling:
13+
* - Exact versions (1.0.0) use cache if available
14+
* - Range versions (^1.0.0, ~1.0.0) auto-force to get latest within range
15+
* - User can override with explicit force: false
16+
*
1217
* Key difference from dlx-binary.ts:
1318
* - dlx-binary.ts: Downloads standalone binaries from URLs
1419
* - dlx-package.ts: Installs npm packages from registries
@@ -25,6 +30,12 @@ import { getSocketDlxDir } from './paths'
2530
import type { SpawnExtra, SpawnOptions } from './spawn'
2631
import { spawn } from './spawn'
2732

33+
/**
34+
* Regex to check if a version string contains range operators.
35+
* Matches any version with range operators: ~, ^, >, <, =, x, X, *, spaces, or ||.
36+
*/
37+
const rangeOperatorsRegExp = /[~^><=xX* ]|\|\|/
38+
2839
export interface DlxPackageOptions {
2940
/**
3041
* Package to install (e.g., '@cyclonedx/cdxgen@10.0.0').
@@ -182,14 +193,16 @@ function findBinaryPath(
182193
*
183194
* This is the Socket equivalent of npx/pnpm dlx/yarn dlx, but using
184195
* our own cache directory (~/.socket/_dlx) and installation logic.
196+
*
197+
* Auto-forces reinstall for version ranges to get latest within range.
185198
*/
186199
export async function dlxPackage(
187200
args: readonly string[] | string[],
188201
options?: DlxPackageOptions | undefined,
189202
spawnExtra?: SpawnExtra | undefined,
190203
): Promise<DlxPackageResult> {
191204
const {
192-
force = false,
205+
force: userForce,
193206
package: packageSpec,
194207
spawnOptions,
195208
} = { __proto__: null, ...options } as DlxPackageOptions
@@ -198,6 +211,12 @@ export async function dlxPackage(
198211
const { name: packageName, version: packageVersion } =
199212
parsePackageSpec(packageSpec)
200213

214+
// Auto-force for version ranges to get latest within range.
215+
// User can still override with explicit force: false if they want cache.
216+
const isVersionRange =
217+
packageVersion !== undefined && rangeOperatorsRegExp.test(packageVersion)
218+
const force = userForce !== undefined ? userForce : isVersionRange
219+
201220
// Build full package spec for installation.
202221
const fullPackageSpec = packageVersion
203222
? `${packageName}@${packageVersion}`

test/dlx-package.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,4 +304,64 @@ describe('dlx-package', () => {
304304
expect(hash).toHaveLength(16)
305305
})
306306
})
307+
308+
describe('version range detection', () => {
309+
const rangeOperatorsRegExp = /[~^><=xX* ]|\|\|/
310+
311+
it('should detect caret ranges', () => {
312+
expect(rangeOperatorsRegExp.test('^1.0.0')).toBe(true)
313+
expect(rangeOperatorsRegExp.test('^11.0.0')).toBe(true)
314+
})
315+
316+
it('should detect tilde ranges', () => {
317+
expect(rangeOperatorsRegExp.test('~1.0.0')).toBe(true)
318+
expect(rangeOperatorsRegExp.test('~11.7.0')).toBe(true)
319+
})
320+
321+
it('should detect greater than ranges', () => {
322+
expect(rangeOperatorsRegExp.test('>1.0.0')).toBe(true)
323+
expect(rangeOperatorsRegExp.test('>=1.0.0')).toBe(true)
324+
})
325+
326+
it('should detect less than ranges', () => {
327+
expect(rangeOperatorsRegExp.test('<2.0.0')).toBe(true)
328+
expect(rangeOperatorsRegExp.test('<=2.0.0')).toBe(true)
329+
})
330+
331+
it('should detect wildcard ranges', () => {
332+
expect(rangeOperatorsRegExp.test('1.0.x')).toBe(true)
333+
expect(rangeOperatorsRegExp.test('1.0.X')).toBe(true)
334+
expect(rangeOperatorsRegExp.test('1.0.*')).toBe(true)
335+
})
336+
337+
it('should detect complex ranges', () => {
338+
expect(rangeOperatorsRegExp.test('>1.0.0 <2.0.0')).toBe(true)
339+
expect(rangeOperatorsRegExp.test('>=1.0.0 <=2.0.0')).toBe(true)
340+
expect(rangeOperatorsRegExp.test('1.0.0 || 2.0.0')).toBe(true)
341+
})
342+
343+
it('should not detect exact versions', () => {
344+
expect(rangeOperatorsRegExp.test('1.0.0')).toBe(false)
345+
expect(rangeOperatorsRegExp.test('11.7.0')).toBe(false)
346+
expect(rangeOperatorsRegExp.test('0.0.1')).toBe(false)
347+
})
348+
349+
it('should not detect versions with prerelease tags', () => {
350+
expect(rangeOperatorsRegExp.test('1.0.0-alpha')).toBe(false)
351+
expect(rangeOperatorsRegExp.test('1.0.0-beta.1')).toBe(false)
352+
expect(rangeOperatorsRegExp.test('1.0.0+build.123')).toBe(false)
353+
})
354+
355+
it('should handle packages with x in name correctly', () => {
356+
// Note: Regex matches 'x' character anywhere, but in real usage
357+
// we only test the version string, not the package name.
358+
// Package name '@cyclonedx/cdxgen' contains 'x' which would match,
359+
// but this is fine because we parse name and version separately.
360+
expect(rangeOperatorsRegExp.test('cyclonedx')).toBe(true) // Contains 'x'.
361+
expect(rangeOperatorsRegExp.test('express')).toBe(true) // Contains 'x'.
362+
363+
// In practice, we only test version strings.
364+
expect(rangeOperatorsRegExp.test('1.2.3')).toBe(false) // Exact version, no 'x'.
365+
})
366+
})
307367
})

0 commit comments

Comments
 (0)