Skip to content

Commit 0088140

Browse files
committed
fix(dlx): correct parsePackageSpec for scoped packages with versions
Fix bug where parsePackageSpec incorrectly strips the '@' prefix from scoped packages when they have versions. For example: - Before: '@coana-tech/cli@~14.12.51' → {name: 'coana-tech/cli', version: '~14.12.51'} - After: '@coana-tech/cli@~14.12.51' → {name: '@coana-tech/cli', version: '~14.12.51'} Also refactor external module lazy-loading: - Extract lazy-loading logic into separate modules (npm-package-arg.ts, pacote.ts, semver.ts) - Export getNpmPackageArg(), getPacote(), getSemver() functions - Update dlx-package.ts and versions.ts to use new external modules - Improves code organization and reusability This fixes the issue where installing @coana-tech/cli via dlx would fail with "Package not found: coana-tech/cli".
1 parent 3d47dae commit 0088140

File tree

5 files changed

+91
-28
lines changed

5 files changed

+91
-28
lines changed

src/dlx-package.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import path from 'node:path'
3636
import { WIN32 } from './constants/platform'
3737
import { getPacoteCachePath } from './constants/packages'
3838
import { generateCacheKey } from './dlx'
39-
import pacote from './external/pacote'
39+
import { getPacote } from './external/pacote'
4040
import { readJsonSync } from './fs'
4141
import { normalizePath } from './path'
4242
import { getSocketDlxDir } from './paths'
@@ -118,17 +118,19 @@ function parsePackageSpec(spec: string): {
118118
// Handle scoped packages (@scope/name@version).
119119
if (spec.startsWith('@')) {
120120
const parts = spec.split('@')
121+
// parts[0] is empty string (before leading @)
122+
// parts[1] is scope/name
123+
// parts[2] is version (if present)
121124
if (parts.length === 3) {
122-
// @scope@version -> Invalid, but handle gracefully.
123-
return { name: parts[1], version: parts[2] }
125+
// @scope/name@version.
126+
return { name: `@${parts[1]}`, version: parts[2] }
124127
}
125128
if (parts.length === 2) {
126129
// @scope/name with no version.
127130
return { name: `@${parts[1]}`, version: undefined }
128131
}
129-
// @scope/name@version.
130-
const scopeAndName = `@${parts[1]}`
131-
return { name: scopeAndName, version: parts[2] }
132+
// Fallback for malformed input.
133+
return { name: spec, version: undefined }
132134
}
133135

134136
// Handle unscoped packages (name@version).
@@ -205,7 +207,7 @@ async function ensurePackageInstalled(
205207
// Pacote leverages npm cache when available but doesn't require npm CLI.
206208
const pacoteCachePath = getPacoteCachePath()
207209
try {
208-
await pacote.extract(packageSpec, installedDir, {
210+
await getPacote().extract(packageSpec, installedDir, {
209211
// Use consistent pacote cache path (respects npm cache locations when available).
210212
cache: pacoteCachePath || path.join(packageDir, '.cache'),
211213
})

src/external/npm-package-arg.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Lazy-loaded npm-package-arg module.
3+
* Defers require() until first use to reduce startup time.
4+
*/
5+
6+
let _npmPackageArg: typeof import('npm-package-arg') | undefined
7+
8+
/**
9+
* Get the npm-package-arg module, loading it on first access.
10+
*/
11+
/*@__NO_SIDE_EFFECTS__*/
12+
export function getNpmPackageArg() {
13+
if (_npmPackageArg === undefined) {
14+
_npmPackageArg = /*@__PURE__*/ require('./npm-package-arg.js')
15+
}
16+
return _npmPackageArg as typeof import('npm-package-arg')
17+
}
18+
19+
// Default export for convenience.
20+
export default getNpmPackageArg

src/external/pacote.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Lazy-loaded pacote module.
3+
* Defers require() until first use to reduce startup time.
4+
*/
5+
6+
let _pacote: typeof import('pacote') | undefined
7+
8+
/**
9+
* Get the pacote module, loading it on first access.
10+
*/
11+
/*@__NO_SIDE_EFFECTS__*/
12+
export function getPacote() {
13+
if (_pacote === undefined) {
14+
_pacote = /*@__PURE__*/ require('./pacote.js')
15+
}
16+
return _pacote as typeof import('pacote')
17+
}
18+
19+
// Default export for convenience.
20+
export default getPacote

src/external/semver.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Lazy-loaded semver module.
3+
* Defers require() until first use to reduce startup time.
4+
* The 'semver' package is browser safe.
5+
*/
6+
7+
let _semver: typeof import('semver') | undefined
8+
9+
/**
10+
* Get the semver module, loading it on first access.
11+
*/
12+
/*@__NO_SIDE_EFFECTS__*/
13+
export function getSemver() {
14+
if (_semver === undefined) {
15+
_semver = /*@__PURE__*/ require('./semver.js')
16+
}
17+
return _semver as typeof import('semver')
18+
}
19+
20+
// Default export for convenience.
21+
export default getSemver

src/versions.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/** @fileoverview Version comparison and validation utilities for Socket ecosystem. */
22

3-
import semver from './external/semver'
3+
import { getSemver } from './external/semver'
44

55
/**
66
* Coerce a version string to valid semver format.
77
*/
88
export function coerceVersion(version: string): string | undefined {
9-
const coerced = semver.coerce(version)
9+
const coerced = getSemver().coerce(version)
1010
return coerced?.version
1111
}
1212

@@ -19,7 +19,7 @@ export function compareVersions(
1919
v2: string,
2020
): -1 | 0 | 1 | undefined {
2121
try {
22-
return semver.compare(v1, v2)
22+
return getSemver().compare(v1, v2)
2323
} catch {
2424
return undefined
2525
}
@@ -29,30 +29,30 @@ export function compareVersions(
2929
* Get all versions from an array that satisfy a semver range.
3030
*/
3131
export function filterVersions(versions: string[], range: string): string[] {
32-
return versions.filter(v => semver.satisfies(v, range))
32+
return versions.filter(v => getSemver().satisfies(v, range))
3333
}
3434

3535
/**
3636
* Get the major version number from a version string.
3737
*/
3838
export function getMajorVersion(version: string): number | undefined {
39-
const parsed = semver.parse(version)
39+
const parsed = getSemver().parse(version)
4040
return parsed?.major
4141
}
4242

4343
/**
4444
* Get the minor version number from a version string.
4545
*/
4646
export function getMinorVersion(version: string): number | undefined {
47-
const parsed = semver.parse(version)
47+
const parsed = getSemver().parse(version)
4848
return parsed?.minor
4949
}
5050

5151
/**
5252
* Get the patch version number from a version string.
5353
*/
5454
export function getPatchVersion(version: string): number | undefined {
55-
const parsed = semver.parse(version)
55+
const parsed = getSemver().parse(version)
5656
return parsed?.patch
5757
}
5858

@@ -71,21 +71,21 @@ export function incrementVersion(
7171
| 'prerelease',
7272
identifier?: string | undefined,
7373
): string | undefined {
74-
return semver.inc(version, release, identifier) || undefined
74+
return getSemver().inc(version, release, identifier) || undefined
7575
}
7676

7777
/**
7878
* Check if version1 equals version2.
7979
*/
8080
export function isEqual(version1: string, version2: string): boolean {
81-
return semver.eq(version1, version2)
81+
return getSemver().eq(version1, version2)
8282
}
8383

8484
/**
8585
* Check if version1 is greater than version2.
8686
*/
8787
export function isGreaterThan(version1: string, version2: string): boolean {
88-
return semver.gt(version1, version2)
88+
return getSemver().gt(version1, version2)
8989
}
9090

9191
/**
@@ -95,42 +95,42 @@ export function isGreaterThanOrEqual(
9595
version1: string,
9696
version2: string,
9797
): boolean {
98-
return semver.gte(version1, version2)
98+
return getSemver().gte(version1, version2)
9999
}
100100

101101
/**
102102
* Check if version1 is less than version2.
103103
*/
104104
export function isLessThan(version1: string, version2: string): boolean {
105-
return semver.lt(version1, version2)
105+
return getSemver().lt(version1, version2)
106106
}
107107

108108
/**
109109
* Check if version1 is less than or equal to version2.
110110
*/
111111
export function isLessThanOrEqual(version1: string, version2: string): boolean {
112-
return semver.lte(version1, version2)
112+
return getSemver().lte(version1, version2)
113113
}
114114

115115
/**
116116
* Validate if a string is a valid semantic version.
117117
*/
118118
export function isValidVersion(version: string): boolean {
119-
return semver.valid(version) !== null
119+
return getSemver().valid(version) !== null
120120
}
121121

122122
/**
123123
* Get the highest version from an array of versions.
124124
*/
125125
export function maxVersion(versions: string[]): string | undefined {
126-
return semver.maxSatisfying(versions, '*') || undefined
126+
return getSemver().maxSatisfying(versions, '*') || undefined
127127
}
128128

129129
/**
130130
* Get the lowest version from an array of versions.
131131
*/
132132
export function minVersion(versions: string[]): string | undefined {
133-
return semver.minSatisfying(versions, '*') || undefined
133+
return getSemver().minSatisfying(versions, '*') || undefined
134134
}
135135

136136
/**
@@ -145,7 +145,7 @@ export function parseVersion(version: string):
145145
build: readonly string[]
146146
}
147147
| undefined {
148-
const parsed = semver.parse(version)
148+
const parsed = getSemver().parse(version)
149149
if (!parsed) {
150150
return undefined
151151
}
@@ -162,21 +162,21 @@ export function parseVersion(version: string):
162162
* Check if a version satisfies a semver range.
163163
*/
164164
export function satisfiesVersion(version: string, range: string): boolean {
165-
return semver.satisfies(version, range)
165+
return getSemver().satisfies(version, range)
166166
}
167167

168168
/**
169169
* Sort versions in ascending order.
170170
*/
171171
export function sortVersions(versions: string[]): string[] {
172-
return semver.sort([...versions])
172+
return getSemver().sort([...versions])
173173
}
174174

175175
/**
176176
* Sort versions in descending order.
177177
*/
178178
export function sortVersionsDesc(versions: string[]): string[] {
179-
return semver.rsort([...versions])
179+
return getSemver().rsort([...versions])
180180
}
181181

182182
/**
@@ -195,7 +195,7 @@ export function versionDiff(
195195
| 'prerelease'
196196
| undefined {
197197
try {
198-
return semver.diff(version1, version2) || undefined
198+
return getSemver().diff(version1, version2) || undefined
199199
} catch {
200200
return undefined
201201
}

0 commit comments

Comments
 (0)