Skip to content

Commit 26699ba

Browse files
committed
Use npm ls for pnpm workspace support
1 parent a30e606 commit 26699ba

File tree

2 files changed

+177
-91
lines changed

2 files changed

+177
-91
lines changed

src/commands/optimize.ts

Lines changed: 151 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -158,76 +158,127 @@ const updateManifestByAgent: Record<Agent, AgentModifyManifestFn> = {
158158
}
159159
}
160160

161+
type AgentListDepsOptions = {
162+
npmExecPath?: string
163+
rootPath?: string
164+
}
161165
type AgentListDepsFn = (
162166
agentExecPath: string,
163167
cwd: string,
164-
rootPath: string
168+
options?: AgentListDepsOptions
165169
) => Promise<string>
166170

167-
const lsByAgent: Record<Agent, AgentListDepsFn> = {
168-
async bun(agentExecPath: string, cwd: string, _rootPath: string) {
169-
try {
170-
// Bun does not support filtering by production packages yet.
171-
// https://github.com/oven-sh/bun/issues/8283
172-
return (await spawn(agentExecPath, ['pm', 'ls', '--all'], { cwd })).stdout
173-
} catch {}
174-
return ''
175-
},
176-
async npm(agentExecPath: string, cwd: string, rootPath: string) {
177-
try {
178-
let { stdout } = await spawn(
179-
agentExecPath,
180-
['ls', '--parseable', '--omit', 'dev', '--all'],
181-
{ cwd }
182-
)
183-
stdout = stdout.trim()
184-
stdout = stdout.replaceAll(cwd, '')
185-
stdout = rootPath === cwd ? stdout : stdout.replaceAll(rootPath, '')
186-
return stdout.replaceAll('\\', '/')
187-
} catch {}
188-
return ''
189-
},
190-
async pnpm(agentExecPath: string, cwd: string, rootPath: string) {
191-
try {
192-
let { stdout } = await spawn(
193-
agentExecPath,
194-
['ls', '--parseable', '--prod', '--depth', 'Infinity'],
195-
{ cwd }
196-
)
197-
stdout = stdout.trim()
198-
stdout = stdout.replaceAll(cwd, '')
199-
stdout = rootPath === cwd ? stdout : stdout.replaceAll(rootPath, '')
200-
return stdout.replaceAll('\\', '/')
201-
} catch {}
202-
return ''
203-
},
204-
async 'yarn/berry'(agentExecPath: string, cwd: string, _rootPath: string) {
205-
try {
206-
return (
207-
// Yarn Berry does not support filtering by production packages yet.
208-
// https://github.com/yarnpkg/berry/issues/5117
209-
(
210-
await spawn(agentExecPath, ['info', '--recursive', '--name-only'], {
211-
cwd
212-
})
171+
const lsByAgent = (() => {
172+
function cleanupParseable(
173+
stdout: string,
174+
cwd: string,
175+
rootPath?: string
176+
): string {
177+
stdout = stdout.trim()
178+
stdout = stdout.replaceAll(cwd, '')
179+
if (rootPath && rootPath !== cwd) {
180+
stdout = stdout.replaceAll(rootPath, '')
181+
}
182+
return stdout.replaceAll('\\', '/')
183+
}
184+
185+
async function npmLs(npmExecPath: string, cwd: string, rootPath?: string) {
186+
return cleanupParseable(
187+
(
188+
await spawn(
189+
npmExecPath,
190+
['ls', '--parseable', '--omit', 'dev', '--all'],
191+
{ cwd }
192+
)
193+
).stdout,
194+
cwd,
195+
rootPath
196+
)
197+
}
198+
return <Record<Agent, AgentListDepsFn>>{
199+
async bun(agentExecPath: string, cwd: string) {
200+
try {
201+
// Bun does not support filtering by production packages yet.
202+
// https://github.com/oven-sh/bun/issues/8283
203+
return (await spawn(agentExecPath!, ['pm', 'ls', '--all'], { cwd }))
204+
.stdout
205+
} catch {}
206+
return ''
207+
},
208+
async npm(
209+
agentExecPath: string,
210+
cwd: string,
211+
options: AgentListDepsOptions
212+
) {
213+
const { rootPath } = <AgentListDepsOptions>{ __proto__: null, ...options }
214+
try {
215+
return await npmLs(agentExecPath, cwd, rootPath)
216+
} catch {}
217+
return ''
218+
},
219+
async pnpm(
220+
agentExecPath: string,
221+
cwd: string,
222+
options: AgentListDepsOptions
223+
) {
224+
const { npmExecPath, rootPath } = <AgentListDepsOptions>{
225+
__proto__: null,
226+
...options
227+
}
228+
let stdout = ''
229+
if (npmExecPath && npmExecPath !== 'npm') {
230+
try {
231+
stdout = await npmLs(npmExecPath, cwd, rootPath)
232+
} catch (e: any) {
233+
if (e?.stderr?.includes('code ELSPROBLEMS')) {
234+
stdout = e?.stdout
235+
}
236+
}
237+
} else {
238+
try {
239+
stdout = cleanupParseable(
240+
(
241+
await spawn(
242+
agentExecPath,
243+
['ls', '--parseable', '--prod', '--depth', 'Infinity'],
244+
{ cwd }
245+
)
246+
).stdout,
247+
cwd,
248+
rootPath
249+
)
250+
} catch {}
251+
}
252+
return stdout
253+
},
254+
async 'yarn/berry'(agentExecPath: string, cwd: string) {
255+
try {
256+
return (
257+
// Yarn Berry does not support filtering by production packages yet.
258+
// https://github.com/yarnpkg/berry/issues/5117
259+
(
260+
await spawn(agentExecPath, ['info', '--recursive', '--name-only'], {
261+
cwd
262+
})
263+
).stdout.trim()
264+
)
265+
} catch {}
266+
return ''
267+
},
268+
async 'yarn/classic'(agentExecPath: string, cwd: string) {
269+
try {
270+
// However, Yarn Classic does support it.
271+
// https://github.com/yarnpkg/yarn/releases/tag/v1.0.0
272+
// > Fix: Excludes dev dependencies from the yarn list output when the
273+
// environment is production
274+
return (
275+
await spawn(agentExecPath, ['list', '--prod'], { cwd })
213276
).stdout.trim()
214-
)
215-
} catch {}
216-
return ''
217-
},
218-
async 'yarn/classic'(agentExecPath: string, cwd: string, _rootPath: string) {
219-
try {
220-
// However, Yarn Classic does support it.
221-
// https://github.com/yarnpkg/yarn/releases/tag/v1.0.0
222-
// > Fix: Excludes dev dependencies from the yarn list output when the
223-
// environment is production
224-
return (
225-
await spawn(agentExecPath, ['list', '--prod'], { cwd })
226-
).stdout.trim()
227-
} catch {}
228-
return ''
277+
} catch {}
278+
return ''
279+
}
229280
}
230-
}
281+
})()
231282

232283
type AgentDepsIncludesFn = (stdout: string, name: string) => boolean
233284

@@ -319,6 +370,7 @@ type AddOverridesConfig = {
319370
agentExecPath: string
320371
lockSrc: string
321372
manifestEntries: ManifestEntry[]
373+
npmExecPath: string
322374
pkgJson?: EditablePackageJson | undefined
323375
pkgPath: string
324376
pin?: boolean | undefined
@@ -330,6 +382,17 @@ type AddOverridesState = {
330382
added: Set<string>
331383
spinner?: Ora | undefined
332384
updated: Set<string>
385+
warnedPnpmWorkspaceRequiresNpm: boolean
386+
}
387+
388+
function createAddOverridesState(initials?: any): AddOverridesState {
389+
return {
390+
added: new Set(),
391+
spinner: undefined,
392+
updated: new Set(),
393+
warnedPnpmWorkspaceRequiresNpm: false,
394+
...initials
395+
}
333396
}
334397

335398
async function addOverrides(
@@ -338,17 +401,14 @@ async function addOverrides(
338401
agentExecPath,
339402
lockSrc,
340403
manifestEntries,
404+
npmExecPath,
341405
pin,
342406
pkgJson: editablePkgJson,
343407
pkgPath,
344408
prod,
345409
rootPath
346410
}: AddOverridesConfig,
347-
state: AddOverridesState = {
348-
added: new Set(),
349-
spinner: undefined,
350-
updated: new Set()
351-
}
411+
state = createAddOverridesState()
352412
): Promise<AddOverridesState> {
353413
if (editablePkgJson === undefined) {
354414
editablePkgJson = await EditablePackageJson.load(pkgPath)
@@ -357,26 +417,39 @@ async function addOverrides(
357417
const pkgJson: Readonly<PackageJsonContent> = editablePkgJson.content
358418
const isRoot = pkgPath === rootPath
359419
const isLockScanned = isRoot && !prod
420+
const relPath = path.relative(rootPath, pkgPath)
421+
const workspaces = await getWorkspaces(agent, pkgPath, pkgJson)
422+
const isWorkspace = !!workspaces
423+
if (
424+
isWorkspace &&
425+
agent === 'pnpm' &&
426+
npmExecPath === 'npm' &&
427+
!state.warnedPnpmWorkspaceRequiresNpm
428+
) {
429+
state.warnedPnpmWorkspaceRequiresNpm = true
430+
console.log(
431+
`⚠️ ${COMMAND_TITLE}: pnpm workspace support requires \`npm ls\`, falling back to \`pnpm list\``
432+
)
433+
}
360434
const thingToScan = isLockScanned
361435
? lockSrc
362-
: await lsByAgent[agent](agentExecPath, pkgPath, rootPath)
436+
: await lsByAgent[agent](agentExecPath, pkgPath, { npmExecPath, rootPath })
363437
const thingScanner = isLockScanned
364438
? lockIncludesByAgent[agent]
365439
: depsIncludesByAgent[agent]
366440
const depEntries = getDependencyEntries(pkgJson)
367-
const workspaces = await getWorkspaces(agent, pkgPath, pkgJson)
368-
const isWorkspace = !!workspaces
441+
369442
const overridesDataObjects = <GetOverridesResult[]>[]
370443
if (pkgJson['private'] || isWorkspace) {
371444
overridesDataObjects.push(getOverridesDataByAgent[agent](pkgJson))
372445
} else {
373446
overridesDataObjects.push(
374-
getOverridesDataByAgent['npm'](pkgJson),
447+
getOverridesDataByAgent.npm(pkgJson),
375448
getOverridesDataByAgent['yarn/classic'](pkgJson)
376449
)
377450
}
378451
if (spinner) {
379-
spinner.text = `Adding overrides${isRoot ? '' : ` to ${path.relative(rootPath, pkgPath)}`}...`
452+
spinner.text = `Adding overrides${relPath ? ` to ${relPath}` : ''}...`
380453
}
381454
const depAliasMap = new Map<string, { id: string; version: string }>()
382455
// Chunk package names to process them in parallel 3 at a time.
@@ -470,16 +543,13 @@ async function addOverrides(
470543
agentExecPath,
471544
lockSrc,
472545
manifestEntries,
546+
npmExecPath,
473547
pin,
474548
pkgPath: path.dirname(wsPkgJsonPath),
475549
prod,
476550
rootPath
477551
},
478-
{
479-
added: new Set(),
480-
spinner,
481-
updated: new Set()
482-
}
552+
createAddOverridesState({ spinner })
483553
)
484554
for (const regPkgName of added) {
485555
state.added.add(regPkgName)
@@ -575,6 +645,7 @@ export const optimize: CliSubcommand = {
575645
lockPath,
576646
lockSrc,
577647
minimumNodeVersion,
648+
npmExecPath,
578649
pkgJson,
579650
pkgPath,
580651
supported
@@ -617,11 +688,7 @@ export const optimize: CliSubcommand = {
617688
)
618689
}
619690
const spinner = ora('Socket optimizing...')
620-
const state: AddOverridesState = {
621-
added: new Set(),
622-
spinner,
623-
updated: new Set()
624-
}
691+
const state = createAddOverridesState({ spinner })
625692
spinner.start()
626693
const nodeRange = `>=${minimumNodeVersion}`
627694
const manifestEntries = manifestNpmOverrides.filter(({ 1: data }) =>
@@ -633,6 +700,7 @@ export const optimize: CliSubcommand = {
633700
agentExecPath,
634701
lockSrc,
635702
manifestEntries,
703+
npmExecPath,
636704
pin,
637705
pkgJson,
638706
pkgPath,

src/utils/package-manager-detector.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,25 @@ const numericCollator = new Intl.Collator(undefined, {
3030
})
3131
const { compare: alphaNumericComparator } = numericCollator
3232

33+
async function getAgentExecPath(agent: Agent): Promise<string> {
34+
return (await which(agent, { nothrow: true })) ?? agent
35+
}
36+
37+
async function getAgentVersion(
38+
agentExecPath: string,
39+
cwd: string
40+
): Promise<SemVer | undefined> {
41+
let result
42+
try {
43+
result =
44+
semver.coerce(
45+
// All package managers support the "--version" flag.
46+
(await spawn(agentExecPath, ['--version'], { cwd })).stdout
47+
) ?? undefined
48+
} catch {}
49+
return result
50+
}
51+
3352
const maintainedNodeVersions = (() => {
3453
// Under the hood browserlist uses the node-releases package which is out of date:
3554
// https://github.com/chicoxyzzy/node-releases/issues/37
@@ -145,6 +164,7 @@ export type DetectResult = Readonly<{
145164
lockPath: string | undefined
146165
lockSrc: string | undefined
147166
minimumNodeVersion: string
167+
npmExecPath: string
148168
pkgJson: EditablePackageJson | undefined
149169
pkgPath: string | undefined
150170
supported: boolean
@@ -202,15 +222,12 @@ export async function detect({
202222
agent = 'npm'
203223
onUnknown?.(pkgManager)
204224
}
205-
const agentExecPath = (await which(agent, { nothrow: true })) ?? agent
225+
const agentExecPath = await getAgentExecPath(agent)
226+
227+
const npmExecPath =
228+
agent === 'npm' ? agentExecPath : await getAgentExecPath('npm')
206229
if (agentVersion === undefined) {
207-
try {
208-
agentVersion =
209-
semver.coerce(
210-
// All package managers support the "--version" flag.
211-
(await spawn(agentExecPath, ['--version'], { cwd })).stdout
212-
) ?? undefined
213-
} catch {}
230+
agentVersion = await getAgentVersion(agentExecPath, cwd)
214231
}
215232
if (agent === 'yarn/classic' && (agentVersion?.major ?? 0) > 1) {
216233
agent = 'yarn/berry'
@@ -269,6 +286,7 @@ export async function detect({
269286
lockPath,
270287
lockSrc,
271288
minimumNodeVersion,
289+
npmExecPath,
272290
pkgJson: editablePkgJson,
273291
pkgPath,
274292
supported: targets.browser || targets.node,

0 commit comments

Comments
 (0)