forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import { describe, expect, it } from 'vitest'
2import {
3 getInstallCommand,
4 getInstallCommandParts,
5 getPackageSpecifier,
6 getExecuteCommand,
7 getExecuteCommandParts,
8 getDevDependencyFlag,
9} from '../../../../app/utils/install-command'
10import type { JsrPackageInfo } from '../../../../shared/types/jsr'
11
12describe('install command generation', () => {
13 // Test fixtures
14 const unscopedPackage = 'lodash'
15 const scopedPackage = '@trpc/server'
16
17 const jsrAvailable: JsrPackageInfo = {
18 exists: true,
19 scope: 'trpc',
20 name: 'server',
21 url: 'https://jsr.io/@trpc/server',
22 latestVersion: '10.0.0',
23 }
24
25 const jsrNotAvailable: JsrPackageInfo = {
26 exists: false,
27 }
28
29 describe('getPackageSpecifier', () => {
30 describe('unscoped package (lodash) - not on JSR', () => {
31 it.each([
32 ['npm', 'lodash'],
33 ['pnpm', 'lodash'],
34 ['yarn', 'lodash'],
35 ['bun', 'lodash'],
36 ['deno', 'npm:lodash'],
37 ['vlt', 'lodash'],
38 ] as const)('%s → %s', (pm, expected) => {
39 expect(
40 getPackageSpecifier({
41 packageName: unscopedPackage,
42 packageManager: pm,
43 jsrInfo: jsrNotAvailable,
44 }),
45 ).toBe(expected)
46 })
47 })
48
49 describe('scoped package (@trpc/server) - available on JSR', () => {
50 it.each([
51 ['npm', '@trpc/server'],
52 ['pnpm', '@trpc/server'],
53 ['yarn', '@trpc/server'],
54 ['bun', '@trpc/server'],
55 ['deno', 'jsr:@trpc/server'], // Native JSR specifier preferred
56 ['vlt', '@trpc/server'],
57 ] as const)('%s → %s', (pm, expected) => {
58 expect(
59 getPackageSpecifier({
60 packageName: scopedPackage,
61 packageManager: pm,
62 jsrInfo: jsrAvailable,
63 }),
64 ).toBe(expected)
65 })
66 })
67
68 describe('scoped package (@vue/shared) - NOT on JSR', () => {
69 it.each([
70 ['npm', '@vue/shared'],
71 ['pnpm', '@vue/shared'],
72 ['yarn', '@vue/shared'],
73 ['bun', '@vue/shared'],
74 ['deno', 'npm:@vue/shared'], // Falls back to npm: compat
75 ['vlt', '@vue/shared'],
76 ] as const)('%s → %s', (pm, expected) => {
77 expect(
78 getPackageSpecifier({
79 packageName: '@vue/shared',
80 packageManager: pm,
81 jsrInfo: jsrNotAvailable,
82 }),
83 ).toBe(expected)
84 })
85 })
86 })
87
88 describe('getInstallCommand', () => {
89 describe('unscoped package without version', () => {
90 it.each([
91 ['npm', 'npm install lodash'],
92 ['pnpm', 'pnpm add lodash'],
93 ['yarn', 'yarn add lodash'],
94 ['bun', 'bun add lodash'],
95 ['deno', 'deno add npm:lodash'],
96 ['vlt', 'vlt install lodash'],
97 ] as const)('%s → %s', (pm, expected) => {
98 expect(
99 getInstallCommand({
100 packageName: unscopedPackage,
101 packageManager: pm,
102 jsrInfo: jsrNotAvailable,
103 }),
104 ).toBe(expected)
105 })
106 })
107
108 describe('unscoped package with version', () => {
109 it.each([
110 ['npm', 'npm install lodash@4.17.21'],
111 ['pnpm', 'pnpm add lodash@4.17.21'],
112 ['yarn', 'yarn add lodash@4.17.21'],
113 ['bun', 'bun add lodash@4.17.21'],
114 ['deno', 'deno add npm:lodash@4.17.21'],
115 ['vlt', 'vlt install lodash@4.17.21'],
116 ] as const)('%s → %s', (pm, expected) => {
117 expect(
118 getInstallCommand({
119 packageName: unscopedPackage,
120 packageManager: pm,
121 version: '4.17.21',
122 jsrInfo: jsrNotAvailable,
123 }),
124 ).toBe(expected)
125 })
126 })
127
128 describe('dev dependency installs', () => {
129 it.each([
130 ['npm', 'npm install -D eslint'],
131 ['pnpm', 'pnpm add -D eslint'],
132 ['yarn', 'yarn add -D eslint'],
133 ['bun', 'bun add -d eslint'],
134 ['deno', 'deno add -D npm:eslint'],
135 ['vlt', 'vlt install -D eslint'],
136 ] as const)('%s → %s', (pm, expected) => {
137 expect(
138 getInstallCommand({
139 packageName: 'eslint',
140 packageManager: pm,
141 jsrInfo: jsrNotAvailable,
142 dev: true,
143 }),
144 ).toBe(expected)
145 })
146 })
147
148 describe('scoped package on JSR without version', () => {
149 it.each([
150 ['npm', 'npm install @trpc/server'],
151 ['pnpm', 'pnpm add @trpc/server'],
152 ['yarn', 'yarn add @trpc/server'],
153 ['bun', 'bun add @trpc/server'],
154 ['deno', 'deno add jsr:@trpc/server'], // Native JSR preferred
155 ['vlt', 'vlt install @trpc/server'],
156 ] as const)('%s → %s', (pm, expected) => {
157 expect(
158 getInstallCommand({
159 packageName: scopedPackage,
160 packageManager: pm,
161 jsrInfo: jsrAvailable,
162 }),
163 ).toBe(expected)
164 })
165 })
166
167 describe('scoped package on JSR with version', () => {
168 it.each([
169 ['npm', 'npm install @trpc/server@10.0.0'],
170 ['pnpm', 'pnpm add @trpc/server@10.0.0'],
171 ['yarn', 'yarn add @trpc/server@10.0.0'],
172 ['bun', 'bun add @trpc/server@10.0.0'],
173 ['deno', 'deno add jsr:@trpc/server@10.0.0'], // Native JSR with version
174 ['vlt', 'vlt install @trpc/server@10.0.0'],
175 ] as const)('%s → %s', (pm, expected) => {
176 expect(
177 getInstallCommand({
178 packageName: scopedPackage,
179 packageManager: pm,
180 version: '10.0.0',
181 jsrInfo: jsrAvailable,
182 }),
183 ).toBe(expected)
184 })
185 })
186
187 describe('scoped package NOT on JSR', () => {
188 it.each([
189 ['npm', 'npm install @vue/shared'],
190 ['pnpm', 'pnpm add @vue/shared'],
191 ['yarn', 'yarn add @vue/shared'],
192 ['bun', 'bun add @vue/shared'],
193 ['deno', 'deno add npm:@vue/shared'], // Falls back to npm: compat
194 ['vlt', 'vlt install @vue/shared'],
195 ] as const)('%s → %s', (pm, expected) => {
196 expect(
197 getInstallCommand({
198 packageName: '@vue/shared',
199 packageManager: pm,
200 jsrInfo: jsrNotAvailable,
201 }),
202 ).toBe(expected)
203 })
204 })
205 })
206
207 describe('getInstallCommandParts', () => {
208 it('returns correct parts for npm without version', () => {
209 const parts = getInstallCommandParts({
210 packageName: 'lodash',
211 packageManager: 'npm',
212 jsrInfo: jsrNotAvailable,
213 })
214 expect(parts).toEqual(['npm', 'install', 'lodash'])
215 })
216
217 it('returns correct parts for npm with version', () => {
218 const parts = getInstallCommandParts({
219 packageName: 'lodash',
220 packageManager: 'npm',
221 version: '4.17.21',
222 jsrInfo: jsrNotAvailable,
223 })
224 expect(parts).toEqual(['npm', 'install', 'lodash@4.17.21'])
225 })
226
227 it('returns correct parts for npm with dev flag', () => {
228 const parts = getInstallCommandParts({
229 packageName: 'eslint',
230 packageManager: 'npm',
231 jsrInfo: jsrNotAvailable,
232 dev: true,
233 })
234 expect(parts).toEqual(['npm', 'install', '-D', 'eslint'])
235 })
236
237 it('returns correct parts for deno with jsr: prefix when available', () => {
238 const parts = getInstallCommandParts({
239 packageName: '@trpc/server',
240 packageManager: 'deno',
241 jsrInfo: jsrAvailable,
242 })
243 expect(parts).toEqual(['deno', 'add', 'jsr:@trpc/server'])
244 })
245
246 it('returns correct parts for bun with lowercase dev flag', () => {
247 const parts = getInstallCommandParts({
248 packageName: 'eslint',
249 packageManager: 'bun',
250 jsrInfo: jsrNotAvailable,
251 dev: true,
252 })
253 expect(parts).toEqual(['bun', 'add', '-d', 'eslint'])
254 })
255
256 it('returns correct parts for deno with npm: prefix when not on JSR', () => {
257 const parts = getInstallCommandParts({
258 packageName: 'lodash',
259 packageManager: 'deno',
260 jsrInfo: jsrNotAvailable,
261 })
262 expect(parts).toEqual(['deno', 'add', 'npm:lodash'])
263 })
264
265 it('returns correct parts for vlt', () => {
266 const parts = getInstallCommandParts({
267 packageName: 'lodash',
268 packageManager: 'vlt',
269 jsrInfo: jsrNotAvailable,
270 })
271 expect(parts).toEqual(['vlt', 'install', 'lodash'])
272 })
273
274 it('joined parts match getInstallCommand output', () => {
275 const options = {
276 packageName: '@trpc/server',
277 packageManager: 'pnpm' as const,
278 version: '10.0.0',
279 jsrInfo: jsrAvailable,
280 }
281 const parts = getInstallCommandParts(options)
282 const command = getInstallCommand(options)
283 expect(parts.join(' ')).toBe(command)
284 })
285 })
286
287 describe('getDevDependencyFlag', () => {
288 it('returns lowercase flag only for bun', () => {
289 expect(getDevDependencyFlag('bun')).toBe('-d')
290 expect(getDevDependencyFlag('npm')).toBe('-D')
291 expect(getDevDependencyFlag('deno')).toBe('-D')
292 })
293 })
294
295 describe('edge cases', () => {
296 it('handles null jsrInfo same as not available for deno', () => {
297 expect(
298 getPackageSpecifier({
299 packageName: 'lodash',
300 packageManager: 'deno',
301 jsrInfo: null,
302 }),
303 ).toBe('npm:lodash')
304 })
305
306 it('handles undefined jsrInfo same as not available for deno', () => {
307 expect(
308 getPackageSpecifier({
309 packageName: 'lodash',
310 packageManager: 'deno',
311 jsrInfo: undefined,
312 }),
313 ).toBe('npm:lodash')
314 })
315
316 it('handles jsrInfo with exists:true but missing scope/name for deno', () => {
317 const partialJsr: JsrPackageInfo = {
318 exists: true,
319 // Missing scope and name
320 }
321 expect(
322 getPackageSpecifier({
323 packageName: '@foo/bar',
324 packageManager: 'deno',
325 jsrInfo: partialJsr,
326 }),
327 ).toBe('npm:@foo/bar')
328 })
329
330 it('getInstallCommandParts returns empty array for invalid package manager', () => {
331 const parts = getInstallCommandParts({
332 packageName: 'lodash',
333 packageManager: 'invalid' as any,
334 })
335 expect(parts).toEqual([])
336 })
337 })
338
339 describe('getExecuteCommandParts', () => {
340 describe('local execute (isBinaryOnly: false)', () => {
341 it.each([
342 ['npm', ['npx', 'eslint']],
343 ['pnpm', ['pnpm', 'exec', 'eslint']],
344 ['yarn', ['npx', 'eslint']],
345 ['bun', ['bunx', 'eslint']],
346 ['deno', ['deno', 'run', 'npm:eslint']],
347 ['vlt', ['vlx', 'eslint']],
348 ] as const)('%s → %s', (pm, expected) => {
349 expect(
350 getExecuteCommandParts({
351 packageName: 'eslint',
352 packageManager: pm,
353 isBinaryOnly: false,
354 }),
355 ).toEqual(expected)
356 })
357 })
358
359 describe('remote execute (isBinaryOnly: true)', () => {
360 it.each([
361 ['npm', ['npx', 'degit']],
362 ['pnpm', ['pnpm', 'dlx', 'degit']],
363 ['yarn', ['yarn', 'dlx', 'degit']],
364 ['bun', ['bunx', 'degit']],
365 ['deno', ['deno', 'run', 'npm:degit']],
366 ['vlt', ['vlx', 'degit']],
367 ] as const)('%s → %s', (pm, expected) => {
368 expect(
369 getExecuteCommandParts({
370 packageName: 'degit',
371 packageManager: pm,
372 isBinaryOnly: true,
373 }),
374 ).toEqual(expected)
375 })
376 })
377
378 describe('create-* packages (isCreatePackage: true)', () => {
379 it.each([
380 ['npm', ['npm', 'create', 'vite']],
381 ['pnpm', ['pnpm', 'create', 'vite']],
382 ['yarn', ['yarn', 'create', 'vite']],
383 ['bun', ['bun', 'create', 'vite']],
384 ['deno', ['deno', 'run', 'vite']],
385 ['vlt', ['vlx', 'vite']],
386 ] as const)('%s → %s', (pm, expected) => {
387 expect(
388 getExecuteCommandParts({
389 packageName: 'create-vite',
390 packageManager: pm,
391 isCreatePackage: true,
392 }),
393 ).toEqual(expected)
394 })
395 })
396
397 describe('scoped create-* packages', () => {
398 it('handles @scope/create-app pattern', () => {
399 expect(
400 getExecuteCommandParts({
401 packageName: '@vue/create-app',
402 packageManager: 'npm',
403 isCreatePackage: true,
404 }),
405 ).toEqual(['npm', 'create', 'app'])
406 })
407 })
408 })
409
410 describe('getExecuteCommand', () => {
411 it('generates full execute command string for local execute', () => {
412 expect(
413 getExecuteCommand({
414 packageName: 'eslint',
415 packageManager: 'pnpm',
416 isBinaryOnly: false,
417 }),
418 ).toBe('pnpm exec eslint')
419 })
420
421 it('generates full execute command string for remote execute', () => {
422 expect(
423 getExecuteCommand({
424 packageName: 'degit',
425 packageManager: 'pnpm',
426 isBinaryOnly: true,
427 }),
428 ).toBe('pnpm dlx degit')
429 })
430
431 it('generates create command for create-* packages', () => {
432 expect(
433 getExecuteCommand({
434 packageName: 'create-vite',
435 packageManager: 'pnpm',
436 isCreatePackage: true,
437 }),
438 ).toBe('pnpm create vite')
439 })
440 })
441})