[READ-ONLY] a fast, modern browser for the npm registry
at main 441 lines 13 kB view raw
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})