forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import type { JsrPackageInfo } from '#shared/types/jsr'
2import { getPackageSpecifier, packageManagers } from './install-command'
3import type { PackageManagerId } from './install-command'
4
5/**
6 * Information about executable commands provided by a package.
7 */
8export interface ExecutableInfo {
9 /** Primary command name (typically the package name or first bin key) */
10 primaryCommand: string
11 /** All available command names */
12 commands: string[]
13 /** Whether this package has any executables */
14 hasExecutable: boolean
15}
16
17/**
18 * Extract executable command information from a package's bin field.
19 * Handles both string format ("bin": "./cli.js") and object format ("bin": { "cmd": "./cli.js" }).
20 */
21export function getExecutableInfo(
22 packageName: string,
23 bin: string | Record<string, string> | undefined,
24): ExecutableInfo {
25 if (!bin) {
26 return { primaryCommand: '', commands: [], hasExecutable: false }
27 }
28
29 // String format: package name becomes the command
30 if (typeof bin === 'string') {
31 return {
32 primaryCommand: packageName,
33 commands: [packageName],
34 hasExecutable: true,
35 }
36 }
37
38 // Object format: keys are command names
39 const commands = Object.keys(bin)
40 const firstCommand = commands[0]
41 if (!firstCommand) {
42 return { primaryCommand: '', commands: [], hasExecutable: false }
43 }
44
45 // Prefer command matching package name if it exists, otherwise use first
46 const baseName = packageName.startsWith('@') ? packageName.split('/')[1] : packageName
47 const primaryCommand = baseName && commands.includes(baseName) ? baseName : firstCommand
48
49 return {
50 primaryCommand,
51 commands,
52 hasExecutable: true,
53 }
54}
55
56export interface RunCommandOptions {
57 packageName: string
58 packageManager: PackageManagerId
59 version?: string | null
60 jsrInfo?: JsrPackageInfo | null
61 /** Specific command to run (for packages with multiple bin entries) */
62 command?: string
63 /** Whether this is a binary-only package (affects which execute command to use) */
64 isBinaryOnly?: boolean
65}
66
67/**
68 * Generate run command as an array of parts.
69 * First element is the package manager label (e.g., "pnpm"), rest are arguments.
70 * For example: ["pnpm", "exec", "eslint"] or ["pnpm", "dlx", "create-vite"]
71 */
72export function getRunCommandParts(options: RunCommandOptions): string[] {
73 const pm = packageManagers.find(p => p.id === options.packageManager)
74 if (!pm) return []
75
76 const spec = getPackageSpecifier(options)
77
78 // Choose execute command based on package type
79 const executeCmd = options.isBinaryOnly ? pm.executeRemote : pm.executeLocal
80 const executeParts = executeCmd.split(' ')
81
82 // For deno, always use the package specifier
83 if (options.packageManager === 'deno') {
84 return [...executeParts, spec]
85 }
86
87 // For local execute with specific command name different from package name
88 // e.g., `pnpm exec tsc` for typescript package
89 if (options.command && options.command !== options.packageName) {
90 const baseName = options.packageName.startsWith('@')
91 ? options.packageName.split('/')[1]
92 : options.packageName
93 // If command matches base package name, use the package spec
94 if (options.command === baseName) {
95 return [...executeParts, spec]
96 }
97 // Otherwise use the command name directly
98 return [...executeParts, options.command]
99 }
100
101 return [...executeParts, spec]
102}
103
104/**
105 * Generate the full run command for a package.
106 */
107export function getRunCommand(options: RunCommandOptions): string {
108 return getRunCommandParts(options).join(' ')
109}