[READ-ONLY] a fast, modern browser for the npm registry
at main 180 lines 5.8 kB view raw
1/** 2 * Utilities for detecting install scripts in package.json. 3 * 4 * Install scripts (preinstall, install, postinstall) run automatically 5 * when a package is installed as a dependency - important for security awareness. 6 * 7 * Also extracts npx package calls from those scripts. 8 */ 9 10import type { InstallScriptsInfo } from '#shared/types' 11 12// Scripts that run when installing a package as a dependency 13const INSTALL_SCRIPTS = new Set(['preinstall', 'install', 'postinstall']) 14 15// Pattern to match npx commands with various flags 16// Captures the package name (with optional scope and version) 17const NPX_PATTERN = /\bnpx\s+(?:--?\w+(?:=\S+)?\s+)*(@?[\w.-]+(?:\/[\w.-]+)?(?:@[\w.^~<>=|-]+)?)/g 18 19// Pattern to extract package name and version from captured group 20const PACKAGE_VERSION_PATTERN = /^(@[\w.-]+\/[\w.-]+|[\w.-]+)(?:@(.+))?$/ 21 22/** 23 * Extract packages from npx calls in install scripts. 24 * Only considers preinstall, install, postinstall - scripts that run for end-users. 25 * 26 * @param scripts - The scripts object from package.json 27 * @returns Record of package name to version (or "latest" if none specified) 28 */ 29export function extractNpxDependencies( 30 scripts: Record<string, string> | undefined, 31): Record<string, string> { 32 if (!scripts) return {} 33 34 const npxPackages: Record<string, string> = {} 35 36 for (const [scriptName, script] of Object.entries(scripts)) { 37 // Only check scripts that run during installation 38 if (!INSTALL_SCRIPTS.has(scriptName)) continue 39 // Reset regex state 40 NPX_PATTERN.lastIndex = 0 41 42 let match: RegExpExecArray | null 43 while ((match = NPX_PATTERN.exec(script)) !== null) { 44 const captured = match[1] 45 if (!captured) continue 46 47 // Extract package name and version 48 const parsed = PACKAGE_VERSION_PATTERN.exec(captured) 49 if (parsed && parsed[1]) { 50 const packageName = parsed[1] 51 const version = parsed[2] || 'latest' 52 53 // Skip common built-in commands that aren't packages 54 if (isBuiltinCommand(packageName)) continue 55 56 // Only add if not already present (first occurrence wins) 57 if (!(packageName in npxPackages)) { 58 npxPackages[packageName] = version 59 } 60 } 61 } 62 } 63 64 return npxPackages 65} 66 67/** 68 * Check if a command is a built-in/common command that isn't an npm package 69 */ 70function isBuiltinCommand(name: string): boolean { 71 const builtins = new Set([ 72 // Common shell commands that might be mistakenly captured 73 'env', 74 'node', 75 'npm', 76 'yarn', 77 'pnpm', 78 // npx flags that might look like packages 79 'yes', 80 'no', 81 'quiet', 82 'shell', 83 ]) 84 return builtins.has(name) 85} 86 87/** 88 * Extract install script information from package.json scripts. 89 * Returns info about which install scripts exist and any npx packages they call. 90 * 91 * @param scripts - The scripts object from package.json 92 * @returns Info about install scripts and npx dependencies, or null if no install scripts 93 */ 94export function extractInstallScriptsInfo( 95 scripts: Record<string, string> | undefined, 96): InstallScriptsInfo | null { 97 if (!scripts) return null 98 99 const presentScripts: ('preinstall' | 'install' | 'postinstall')[] = [] 100 const content: Record<string, string> = {} 101 102 for (const scriptName of INSTALL_SCRIPTS) { 103 if (scripts[scriptName]) { 104 presentScripts.push(scriptName as 'preinstall' | 'install' | 'postinstall') 105 content[scriptName] = scripts[scriptName] 106 } 107 } 108 109 if (presentScripts.length === 0) return null 110 111 return { 112 scripts: presentScripts, 113 content, 114 npxDependencies: extractNpxDependencies(scripts), 115 } 116} 117 118/** 119 * Pattern to match scripts that are just `node <file-path>` 120 * Captures the file path (relative paths with alphanumeric chars, dots, hyphens, underscores, and slashes) 121 */ 122const NODE_SCRIPT_PATTERN = /^node\s+([\w./-]+)$/ 123 124/** 125 * Get the file path for an install script link. 126 * - If the script is `node <file-path>`, returns that file path 127 * - Otherwise, returns 'package.json' 128 * 129 * @param scriptContent - The content of the script 130 * @returns The file path to link to in the code tab 131 */ 132export function getInstallScriptFilePath(scriptContent: string): string { 133 const match = NODE_SCRIPT_PATTERN.exec(scriptContent) 134 135 if (match?.[1]) { 136 // Script is `node <file-path>`, link to that file 137 // Normalize path: strip leading ./ 138 const filePath = match[1].replace(/^\.\//, '') 139 140 // Fall back to package.json if path contains navigational elements (the client-side routing can't handle these well) 141 if (filePath.includes('../') || filePath.includes('./')) { 142 return 'package.json' 143 } 144 145 return filePath 146 } 147 148 // Default: link to package.json 149 return 'package.json' 150} 151 152/** 153 * Parse an install script into a prefix and a linkable file path. 154 * - If the script is `node <file-path>`, returns { prefix: 'node ', filePath: '<file-path>' } 155 * so only the file path portion can be rendered as a link. 156 * - Otherwise, returns null (the entire script content should link to package.json). 157 * 158 * @param scriptContent - The content of the script 159 * @returns Parsed parts, or null if no node file path was extracted 160 */ 161export function parseNodeScript( 162 scriptContent: string, 163): { prefix: string; filePath: string } | null { 164 const match = NODE_SCRIPT_PATTERN.exec(scriptContent) 165 166 if (match?.[1]) { 167 const filePath = match[1].replace(/^\.\//, '') 168 169 // Fall back if path contains navigational elements 170 if (filePath.includes('../') || filePath.includes('./')) { 171 return null 172 } 173 174 // Reconstruct the prefix (everything before the captured file path) 175 const prefix = scriptContent.slice(0, match.index + match[0].indexOf(match[1])) 176 return { prefix, filePath } 177 } 178 179 return null 180}