Skip to content

Commit df875bc

Browse files
committed
refactor(dlx): use npm-package-arg for parsePackageSpec
Replace custom package spec parsing logic with npm's official npm-package-arg library for more robust and correct parsing of all package spec formats. Benefits: - Handles edge cases correctly (scoped packages, git URLs, file paths, etc.) - Supports all npm package spec formats (version, range, tag, git, file, etc.) - More maintainable - uses battle-tested npm tooling - Fallback to simple parsing if npm-package-arg fails This ensures proper parsing of specs like: - @coana-tech/cli@~14.12.51 - lodash@4.17.21 - @scope/pkg@latest - package@github:user/repo - file:./local-pkg
1 parent 0088140 commit df875bc

File tree

1 file changed

+29
-26
lines changed

1 file changed

+29
-26
lines changed

src/dlx-package.ts

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +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 { getNpmPackageArg } from './external/npm-package-arg'
3940
import { getPacote } from './external/pacote'
4041
import { readJsonSync } from './fs'
4142
import { normalizePath } from './path'
@@ -105,7 +106,7 @@ export interface DlxPackageResult {
105106
}
106107

107108
/**
108-
* Parse package spec into name and version.
109+
* Parse package spec into name and version using npm-package-arg.
109110
* Examples:
110111
* - 'lodash@4.17.21' → { name: 'lodash', version: '4.17.21' }
111112
* - '@scope/pkg@1.0.0' → { name: '@scope/pkg', version: '1.0.0' }
@@ -115,33 +116,35 @@ function parsePackageSpec(spec: string): {
115116
name: string
116117
version: string | undefined
117118
} {
118-
// Handle scoped packages (@scope/name@version).
119-
if (spec.startsWith('@')) {
120-
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)
124-
if (parts.length === 3) {
125-
// @scope/name@version.
126-
return { name: `@${parts[1]}`, version: parts[2] }
119+
try {
120+
const npa = getNpmPackageArg()
121+
const parsed = npa(spec)
122+
123+
// Extract version from different types of specs.
124+
// For registry specs, use fetchSpec (the version/range).
125+
// For git/file/etc, version will be undefined.
126+
const version =
127+
parsed.type === 'tag'
128+
? parsed.fetchSpec
129+
: parsed.type === 'version' || parsed.type === 'range'
130+
? parsed.fetchSpec
131+
: undefined
132+
133+
return {
134+
name: parsed.name || spec,
135+
version,
127136
}
128-
if (parts.length === 2) {
129-
// @scope/name with no version.
130-
return { name: `@${parts[1]}`, version: undefined }
137+
} catch {
138+
// Fallback to simple parsing if npm-package-arg fails.
139+
const atIndex = spec.lastIndexOf('@')
140+
if (atIndex === -1 || spec.startsWith('@')) {
141+
// No version or scoped package without version.
142+
return { name: spec, version: undefined }
143+
}
144+
return {
145+
name: spec.slice(0, atIndex),
146+
version: spec.slice(atIndex + 1),
131147
}
132-
// Fallback for malformed input.
133-
return { name: spec, version: undefined }
134-
}
135-
136-
// Handle unscoped packages (name@version).
137-
const atIndex = spec.lastIndexOf('@')
138-
if (atIndex === -1) {
139-
return { name: spec, version: undefined }
140-
}
141-
142-
return {
143-
name: spec.slice(0, atIndex),
144-
version: spec.slice(atIndex + 1),
145148
}
146149
}
147150

0 commit comments

Comments
 (0)