Skip to content

Commit 036f9aa

Browse files
authored
Merge branch 'v1.x' into jdalton/issue-911
2 parents 99f023f + afbe90a commit 036f9aa

File tree

3 files changed

+189
-4
lines changed

3 files changed

+189
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
._.DS_Store
33
Thumbs.db
44
/.env
5+
/.env.local
56
/.nvm
67
/.rollup.cache
78
/.type-coverage

src/utils/dlx.mts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,12 @@ export async function spawnDlx(
115115
let spawnArgs: string[]
116116

117117
if (pm === PNPM) {
118-
spawnArgs = ['dlx']
118+
spawnArgs = []
119+
// The --silent flag must come before dlx, not after.
120+
if (silent) {
121+
spawnArgs.push(FLAG_SILENT)
122+
}
123+
spawnArgs.push('dlx')
119124
if (force) {
120125
// For pnpm, set dlx-cache-max-age to 0 via env to force fresh download.
121126
// This ensures we always get the latest version within the range.
@@ -130,9 +135,6 @@ export async function spawnDlx(
130135
},
131136
}
132137
}
133-
if (silent) {
134-
spawnArgs.push(FLAG_SILENT)
135-
}
136138
spawnArgs.push(packageString, ...args)
137139

138140
const shadowPnpmBin = /*@__PURE__*/ require(constants.shadowPnpmBinPath)

src/utils/dlx.test.mts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { createRequire } from 'node:module'
2+
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4+
5+
import constants from '../constants.mts'
6+
import { spawnDlx } from './dlx.mts'
7+
8+
import type { DlxPackageSpec } from './dlx.mts'
9+
10+
const require = createRequire(import.meta.url)
11+
12+
describe('utils/dlx', () => {
13+
describe('spawnDlx', () => {
14+
let mockShadowPnpmBin: ReturnType<typeof vi.fn>
15+
let mockShadowNpxBin: ReturnType<typeof vi.fn>
16+
let mockShadowYarnBin: ReturnType<typeof vi.fn>
17+
18+
beforeEach(() => {
19+
// Create mock functions that return a promise with spawnPromise.
20+
const createMockBin = () =>
21+
vi.fn().mockResolvedValue({
22+
spawnPromise: Promise.resolve({ stdout: '', stderr: '' }),
23+
})
24+
25+
mockShadowPnpmBin = createMockBin()
26+
mockShadowNpxBin = createMockBin()
27+
mockShadowYarnBin = createMockBin()
28+
29+
// Mock the require calls for shadow binaries.
30+
vi.spyOn(require, 'resolve').mockImplementation((id: string) => {
31+
if (id === constants.shadowPnpmBinPath) {
32+
return id
33+
}
34+
if (id === constants.shadowNpxBinPath) {
35+
return id
36+
}
37+
if (id === constants.shadowYarnBinPath) {
38+
return id
39+
}
40+
throw new Error(`Unexpected require: ${id}`)
41+
})
42+
43+
// @ts-ignore
44+
require.cache[constants.shadowPnpmBinPath] = {
45+
exports: mockShadowPnpmBin,
46+
}
47+
// @ts-ignore
48+
require.cache[constants.shadowNpxBinPath] = { exports: mockShadowNpxBin }
49+
// @ts-ignore
50+
require.cache[constants.shadowYarnBinPath] = {
51+
exports: mockShadowYarnBin,
52+
}
53+
})
54+
55+
afterEach(() => {
56+
vi.restoreAllMocks()
57+
// Clean up require cache.
58+
// @ts-ignore
59+
delete require.cache[constants.shadowPnpmBinPath]
60+
// @ts-ignore
61+
delete require.cache[constants.shadowNpxBinPath]
62+
// @ts-ignore
63+
delete require.cache[constants.shadowYarnBinPath]
64+
})
65+
66+
it('should place --silent before dlx for pnpm', async () => {
67+
const packageSpec: DlxPackageSpec = {
68+
name: '@coana-tech/cli',
69+
version: '~1.0.0',
70+
}
71+
72+
await spawnDlx(packageSpec, ['run', '/some/path'], {
73+
agent: 'pnpm',
74+
silent: true,
75+
})
76+
77+
expect(mockShadowPnpmBin).toHaveBeenCalledTimes(1)
78+
const [spawnArgs] = mockShadowPnpmBin.mock.calls[0]
79+
80+
// Verify that --silent comes before dlx.
81+
expect(spawnArgs[0]).toBe('--silent')
82+
expect(spawnArgs[1]).toBe('dlx')
83+
expect(spawnArgs[2]).toBe('@coana-tech/cli@~1.0.0')
84+
expect(spawnArgs[3]).toBe('run')
85+
expect(spawnArgs[4]).toBe('/some/path')
86+
})
87+
88+
it('should not add --silent for pnpm when silent is false', async () => {
89+
const packageSpec: DlxPackageSpec = {
90+
name: '@coana-tech/cli',
91+
version: '1.0.0',
92+
}
93+
94+
await spawnDlx(packageSpec, ['run', '/some/path'], {
95+
agent: 'pnpm',
96+
silent: false,
97+
})
98+
99+
expect(mockShadowPnpmBin).toHaveBeenCalledTimes(1)
100+
const [spawnArgs] = mockShadowPnpmBin.mock.calls[0]
101+
102+
// Verify that --silent is not present.
103+
expect(spawnArgs[0]).toBe('dlx')
104+
expect(spawnArgs[1]).toBe('@coana-tech/cli@1.0.0')
105+
expect(spawnArgs[2]).toBe('run')
106+
expect(spawnArgs[3]).toBe('/some/path')
107+
})
108+
109+
it('should default silent to true for pnpm when version is not pinned', async () => {
110+
const packageSpec: DlxPackageSpec = {
111+
name: '@coana-tech/cli',
112+
version: '~1.0.0',
113+
}
114+
115+
await spawnDlx(packageSpec, ['run', '/some/path'], { agent: 'pnpm' })
116+
117+
expect(mockShadowPnpmBin).toHaveBeenCalledTimes(1)
118+
const [spawnArgs] = mockShadowPnpmBin.mock.calls[0]
119+
120+
// Verify that --silent is automatically added for unpinned versions.
121+
expect(spawnArgs[0]).toBe('--silent')
122+
expect(spawnArgs[1]).toBe('dlx')
123+
})
124+
125+
it('should place --silent after --yes for npm', async () => {
126+
const packageSpec: DlxPackageSpec = {
127+
name: '@coana-tech/cli',
128+
version: '~1.0.0',
129+
}
130+
131+
await spawnDlx(packageSpec, ['run', '/some/path'], {
132+
agent: 'npm',
133+
silent: true,
134+
})
135+
136+
expect(mockShadowNpxBin).toHaveBeenCalledTimes(1)
137+
const [spawnArgs] = mockShadowNpxBin.mock.calls[0]
138+
139+
// For npm/npx, --yes comes first, then --silent.
140+
expect(spawnArgs[0]).toBe('--yes')
141+
expect(spawnArgs[1]).toBe('--silent')
142+
expect(spawnArgs[2]).toBe('@coana-tech/cli@~1.0.0')
143+
expect(spawnArgs[3]).toBe('run')
144+
expect(spawnArgs[4]).toBe('/some/path')
145+
})
146+
147+
it('should set npm_config_dlx_cache_max_age env var for pnpm when force is true', async () => {
148+
const packageSpec: DlxPackageSpec = {
149+
name: '@coana-tech/cli',
150+
version: '1.0.0',
151+
}
152+
153+
await spawnDlx(packageSpec, ['run', '/some/path'], {
154+
agent: 'pnpm',
155+
force: true,
156+
})
157+
158+
expect(mockShadowPnpmBin).toHaveBeenCalledTimes(1)
159+
const [, options] = mockShadowPnpmBin.mock.calls[0]
160+
161+
// Verify that the env var is set to force cache bypass.
162+
expect(options.env).toBeDefined()
163+
expect(options.env.npm_config_dlx_cache_max_age).toBe('0')
164+
})
165+
166+
it('should handle pinned version without silent flag by default', async () => {
167+
const packageSpec: DlxPackageSpec = {
168+
name: '@coana-tech/cli',
169+
version: '1.0.0',
170+
}
171+
172+
await spawnDlx(packageSpec, ['run', '/some/path'], { agent: 'pnpm' })
173+
174+
expect(mockShadowPnpmBin).toHaveBeenCalledTimes(1)
175+
const [spawnArgs] = mockShadowPnpmBin.mock.calls[0]
176+
177+
// For pinned versions, silent defaults to false.
178+
expect(spawnArgs[0]).toBe('dlx')
179+
expect(spawnArgs[1]).toBe('@coana-tech/cli@1.0.0')
180+
})
181+
})
182+
})

0 commit comments

Comments
 (0)