forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import { describe, expect, it } from 'vitest'
2import {
3 getExecutableInfo,
4 getRunCommand,
5 getRunCommandParts,
6} from '../../../../app/utils/run-command'
7import { isBinaryOnlyPackage, isCreatePackage } from '../../../../shared/utils/binary-detection'
8import type { JsrPackageInfo } from '../../../../shared/types/jsr'
9
10describe('executable detection and run commands', () => {
11 const jsrNotAvailable: JsrPackageInfo = { exists: false }
12
13 describe('getExecutableInfo', () => {
14 it('returns hasExecutable: false for undefined bin', () => {
15 const info = getExecutableInfo('some-package', undefined)
16 expect(info).toEqual({
17 primaryCommand: '',
18 commands: [],
19 hasExecutable: false,
20 })
21 })
22
23 it('handles string bin format (package name becomes command)', () => {
24 const info = getExecutableInfo('eslint', './bin/eslint.js')
25 expect(info).toEqual({
26 primaryCommand: 'eslint',
27 commands: ['eslint'],
28 hasExecutable: true,
29 })
30 })
31
32 it('handles object bin format with single command', () => {
33 const info = getExecutableInfo('cowsay', { cowsay: './index.js' })
34 expect(info).toEqual({
35 primaryCommand: 'cowsay',
36 commands: ['cowsay'],
37 hasExecutable: true,
38 })
39 })
40
41 it('handles object bin format with multiple commands', () => {
42 const info = getExecutableInfo('typescript', {
43 tsc: './bin/tsc',
44 tsserver: './bin/tsserver',
45 })
46 expect(info).toEqual({
47 primaryCommand: 'tsc',
48 commands: ['tsc', 'tsserver'],
49 hasExecutable: true,
50 })
51 })
52
53 it('prefers command matching package name as primary', () => {
54 const info = getExecutableInfo('eslint', {
55 'eslint-cli': './cli.js',
56 'eslint': './index.js',
57 })
58 expect(info.primaryCommand).toBe('eslint')
59 })
60
61 it('prefers command matching base name for scoped packages', () => {
62 const info = getExecutableInfo('@scope/myapp', {
63 'myapp': './index.js',
64 'myapp-extra': './extra.js',
65 })
66 expect(info.primaryCommand).toBe('myapp')
67 })
68
69 it('returns empty for empty bin object', () => {
70 const info = getExecutableInfo('some-package', {})
71 expect(info).toEqual({
72 primaryCommand: '',
73 commands: [],
74 hasExecutable: false,
75 })
76 })
77 })
78
79 describe('getRunCommandParts', () => {
80 // Default behavior uses local execute (for installed packages)
81 it.each([
82 ['npm', ['npx', 'eslint']],
83 ['pnpm', ['pnpm', 'exec', 'eslint']],
84 ['yarn', ['npx', 'eslint']],
85 ['bun', ['bunx', 'eslint']],
86 ['deno', ['deno', 'run', 'npm:eslint']],
87 ['vlt', ['vlx', 'eslint']],
88 ] as const)('%s (local) → %s', (pm, expected) => {
89 expect(
90 getRunCommandParts({
91 packageName: 'eslint',
92 packageManager: pm,
93 jsrInfo: jsrNotAvailable,
94 }),
95 ).toEqual(expected)
96 })
97
98 // Binary-only packages use remote execute (download & run)
99 it.each([
100 ['npm', ['npx', 'create-vite']],
101 ['pnpm', ['pnpm', 'dlx', 'create-vite']],
102 ['yarn', ['yarn', 'dlx', 'create-vite']],
103 ['bun', ['bunx', 'create-vite']],
104 ['deno', ['deno', 'run', 'npm:create-vite']],
105 ['vlt', ['vlx', 'create-vite']],
106 ] as const)('%s (remote) → %s', (pm, expected) => {
107 expect(
108 getRunCommandParts({
109 packageName: 'create-vite',
110 packageManager: pm,
111 jsrInfo: jsrNotAvailable,
112 isBinaryOnly: true,
113 }),
114 ).toEqual(expected)
115 })
116
117 it('uses command name directly for multi-bin packages', () => {
118 const parts = getRunCommandParts({
119 packageName: 'typescript',
120 packageManager: 'npm',
121 command: 'tsserver',
122 jsrInfo: jsrNotAvailable,
123 })
124 // npx tsserver runs the tsserver command (not npx typescript/tsserver)
125 expect(parts).toEqual(['npx', 'tsserver'])
126 })
127
128 it('uses base name directly when command matches package base name', () => {
129 const parts = getRunCommandParts({
130 packageName: '@scope/myapp',
131 packageManager: 'npm',
132 command: 'myapp',
133 jsrInfo: jsrNotAvailable,
134 })
135 expect(parts).toEqual(['npx', '@scope/myapp'])
136 })
137
138 it('returns empty array for invalid package manager', () => {
139 const parts = getRunCommandParts({
140 packageName: 'eslint',
141 packageManager: 'invalid' as any,
142 jsrInfo: jsrNotAvailable,
143 })
144 expect(parts).toEqual([])
145 })
146 })
147
148 describe('getRunCommand', () => {
149 it('generates full run command string', () => {
150 expect(
151 getRunCommand({
152 packageName: 'eslint',
153 packageManager: 'npm',
154 jsrInfo: jsrNotAvailable,
155 }),
156 ).toBe('npx eslint')
157 })
158
159 it('generates correct bun run command with specific command', () => {
160 expect(
161 getRunCommand({
162 packageName: 'typescript',
163 packageManager: 'bun',
164 command: 'tsserver',
165 jsrInfo: jsrNotAvailable,
166 }),
167 ).toBe('bunx tsserver')
168 })
169
170 it('joined parts match getRunCommand output', () => {
171 const options = {
172 packageName: 'eslint',
173 packageManager: 'pnpm' as const,
174 jsrInfo: jsrNotAvailable,
175 }
176 const parts = getRunCommandParts(options)
177 const command = getRunCommand(options)
178 expect(parts.join(' ')).toBe(command)
179 })
180 })
181
182 describe('isBinaryOnlyPackage', () => {
183 it('returns true for create-* packages', () => {
184 expect(isBinaryOnlyPackage({ name: 'create-vite' })).toBe(true)
185 expect(isBinaryOnlyPackage({ name: 'create-next-app' })).toBe(true)
186 })
187
188 it('returns true for scoped create packages', () => {
189 expect(isBinaryOnlyPackage({ name: '@vue/create-app' })).toBe(true)
190 expect(isBinaryOnlyPackage({ name: '@scope/create-something' })).toBe(true)
191 })
192
193 it('returns true for packages with bin but no entry points', () => {
194 expect(
195 isBinaryOnlyPackage({
196 name: 'degit',
197 bin: { degit: './bin.js' },
198 }),
199 ).toBe(true)
200 })
201
202 it('returns false for packages with bin AND entry points', () => {
203 expect(
204 isBinaryOnlyPackage({
205 name: 'eslint',
206 bin: { eslint: './bin/eslint.js' },
207 main: './lib/api.js',
208 }),
209 ).toBe(false)
210 })
211
212 it('returns false for packages with exports', () => {
213 expect(
214 isBinaryOnlyPackage({
215 name: 'some-package',
216 bin: { cmd: './bin.js' },
217 exports: { '.': './index.js' },
218 }),
219 ).toBe(false)
220 })
221
222 it('returns false for packages without bin', () => {
223 expect(
224 isBinaryOnlyPackage({
225 name: 'lodash',
226 main: './lodash.js',
227 }),
228 ).toBe(false)
229 })
230 })
231
232 describe('isCreatePackage', () => {
233 it('returns true for create-* packages', () => {
234 expect(isCreatePackage('create-vite')).toBe(true)
235 expect(isCreatePackage('create-next-app')).toBe(true)
236 })
237
238 it('returns true for scoped create packages', () => {
239 expect(isCreatePackage('@vue/create-app')).toBe(true)
240 })
241
242 it('returns false for regular packages', () => {
243 expect(isCreatePackage('eslint')).toBe(false)
244 expect(isCreatePackage('lodash')).toBe(false)
245 expect(isCreatePackage('@scope/utils')).toBe(false)
246 })
247 })
248})