[READ-ONLY] a fast, modern browser for the npm registry
at main 311 lines 8.3 kB view raw
1/** 2 * Package analysis utilities for detecting module format and TypeScript support 3 */ 4 5export type ModuleFormat = 'esm' | 'cjs' | 'dual' | 'unknown' 6 7export type TypesStatus = 8 | { kind: 'included' } 9 | { kind: '@types'; packageName: string; deprecated?: string } 10 | { kind: 'none' } 11 12export interface PackageAnalysis { 13 moduleFormat: ModuleFormat 14 types: TypesStatus 15 engines?: Record<string, string> 16 /** Associated create-* package if it exists */ 17 createPackage?: CreatePackageInfo 18} 19 20/** 21 * Extended package.json fields not in @npm/types 22 * These are commonly used but not included in the official types 23 */ 24export interface ExtendedPackageJson { 25 name?: string 26 version?: string 27 type?: 'module' | 'commonjs' 28 main?: string 29 module?: string 30 types?: string 31 typings?: string 32 exports?: PackageExports 33 engines?: Record<string, string> 34 dependencies?: Record<string, string> 35 devDependencies?: Record<string, string> 36 peerDependencies?: Record<string, string> 37 /** npm maintainers (returned by registry API) */ 38 maintainers?: Array<{ name: string; email?: string }> 39 /** Repository info (returned by registry API) */ 40 repository?: { url?: string; type?: string; directory?: string } 41} 42 43export type PackageExports = string | null | { [key: string]: PackageExports } | PackageExports[] 44 45/** 46 * Detect the module format of a package based on package.json fields 47 */ 48export function detectModuleFormat(pkg: ExtendedPackageJson): ModuleFormat { 49 const hasExports = pkg.exports != null 50 const hasModule = !!pkg.module 51 const hasMain = !!pkg.main 52 const isTypeModule = pkg.type === 'module' 53 const isTypeCommonjs = pkg.type === 'commonjs' || !pkg.type 54 55 // Check exports field for dual format indicators 56 if (hasExports && pkg.exports) { 57 const exportInfo = analyzeExports(pkg.exports) 58 59 if (exportInfo.hasImport && exportInfo.hasRequire) { 60 return 'dual' 61 } 62 63 if (exportInfo.hasImport || exportInfo.hasModule) { 64 // Has ESM exports, check if also has CJS 65 if (hasMain && !isTypeModule) { 66 return 'dual' 67 } 68 return 'esm' 69 } 70 71 if (exportInfo.hasRequire) { 72 // Has CJS exports, check if also has ESM 73 if (hasModule) { 74 return 'dual' 75 } 76 return 'cjs' 77 } 78 79 // exports field exists but doesn't use import/require conditions 80 // Fall through to other detection methods 81 } 82 83 // Legacy detection without exports field 84 if (hasModule && hasMain) { 85 // Check for dual packages (has module field and main points to cjs) 86 const mainIsCJS = pkg.main?.endsWith('.cjs') || (pkg.main?.endsWith('.js') && !isTypeModule) 87 88 return mainIsCJS ? 'dual' : 'esm' 89 } 90 91 if (hasModule || isTypeModule) { 92 return 'esm' 93 } 94 95 if (hasMain || isTypeCommonjs) { 96 return 'cjs' 97 } 98 99 return 'unknown' 100} 101 102interface ExportsAnalysis { 103 hasImport: boolean 104 hasRequire: boolean 105 hasModule: boolean 106 hasTypes: boolean 107} 108 109/** 110 * Recursively analyze exports field for module format indicators 111 */ 112function analyzeExports(exports: PackageExports, depth = 0): ExportsAnalysis { 113 const result: ExportsAnalysis = { 114 hasImport: false, 115 hasRequire: false, 116 hasModule: false, 117 hasTypes: false, 118 } 119 120 // Prevent infinite recursion 121 if (depth > 10) return result 122 123 if (exports === null || exports === undefined) { 124 return result 125 } 126 127 if (typeof exports === 'string') { 128 // Check file extension for format hints 129 if (exports.endsWith('.mjs') || exports.endsWith('.mts') || exports.endsWith('.json')) { 130 result.hasImport = true 131 } else if (exports.endsWith('.cjs') || exports.endsWith('.cts')) { 132 result.hasRequire = true 133 } 134 if (exports.endsWith('.d.ts') || exports.endsWith('.d.mts') || exports.endsWith('.d.cts')) { 135 result.hasTypes = true 136 } 137 return result 138 } 139 140 if (Array.isArray(exports)) { 141 for (const item of exports) { 142 const subResult = analyzeExports(item, depth + 1) 143 mergeExportsAnalysis(result, subResult) 144 } 145 return result 146 } 147 148 if (typeof exports === 'object') { 149 for (const [key, value] of Object.entries(exports)) { 150 // Check condition keys 151 if (key === 'import') { 152 result.hasImport = true 153 } else if (key === 'require') { 154 result.hasRequire = true 155 } else if (key === 'module') { 156 result.hasModule = true 157 } else if (key === 'types') { 158 result.hasTypes = true 159 } 160 161 // Recurse into nested exports 162 const subResult = analyzeExports(value, depth + 1) 163 mergeExportsAnalysis(result, subResult) 164 } 165 } 166 167 return result 168} 169 170function mergeExportsAnalysis(target: ExportsAnalysis, source: ExportsAnalysis): void { 171 target.hasImport = target.hasImport || source.hasImport 172 target.hasRequire = target.hasRequire || source.hasRequire 173 target.hasModule = target.hasModule || source.hasModule 174 target.hasTypes = target.hasTypes || source.hasTypes 175} 176 177/** Info about a related package (@types or create-*) */ 178export interface RelatedPackageInfo { 179 packageName: string 180 deprecated?: string 181} 182 183export type TypesPackageInfo = RelatedPackageInfo 184export type CreatePackageInfo = RelatedPackageInfo 185 186/** 187 * Get the create-* package name for a given package. 188 * e.g., "vite" -> "create-vite", "@scope/foo" -> "@scope/create-foo" 189 */ 190export function getCreatePackageName(packageName: string): string { 191 if (packageName.startsWith('@')) { 192 // Scoped package: @scope/name -> @scope/create-name 193 const slashIndex = packageName.indexOf('/') 194 const scope = packageName.slice(0, slashIndex) 195 const name = packageName.slice(slashIndex + 1) 196 return `${scope}/create-${name}` 197 } 198 return `create-${packageName}` 199} 200 201/** 202 * Extract the short name from a create-* package for display. 203 * e.g., "create-vite" -> "vite", "@scope/create-foo" -> "foo" 204 */ 205export function getCreateShortName(createPackageName: string): string { 206 if (createPackageName.startsWith('@')) { 207 // @scope/create-foo -> foo 208 const slashIndex = createPackageName.indexOf('/') 209 const name = createPackageName.slice(slashIndex + 1) 210 if (name.startsWith('create-')) { 211 return name.slice('create-'.length) 212 } 213 return name 214 } 215 // create-vite -> vite 216 if (createPackageName.startsWith('create-')) { 217 return createPackageName.slice('create-'.length) 218 } 219 return createPackageName 220} 221 222/** 223 * Detect TypeScript types status for a package 224 */ 225export function detectTypesStatus( 226 pkg: ExtendedPackageJson, 227 typesPackageInfo?: TypesPackageInfo, 228): TypesStatus { 229 // Check for built-in types 230 if (pkg.types || pkg.typings) { 231 return { kind: 'included' } 232 } 233 234 // Check exports field for types 235 if (pkg.exports) { 236 const exportInfo = analyzeExports(pkg.exports) 237 if (exportInfo.hasTypes) { 238 return { kind: 'included' } 239 } 240 } 241 242 // Check for @types package 243 if (typesPackageInfo) { 244 return { 245 kind: '@types', 246 packageName: typesPackageInfo.packageName, 247 deprecated: typesPackageInfo.deprecated, 248 } 249 } 250 251 return { kind: 'none' } 252} 253 254/** 255 * Check if a package has built-in TypeScript types 256 * (without needing to check for @types packages) 257 */ 258export function hasBuiltInTypes(pkg: ExtendedPackageJson): boolean { 259 // Check types/typings field 260 if (pkg.types || pkg.typings) { 261 return true 262 } 263 264 // Check exports field for types 265 if (pkg.exports) { 266 const exportInfo = analyzeExports(pkg.exports) 267 if (exportInfo.hasTypes) { 268 return true 269 } 270 } 271 272 return false 273} 274 275/** 276 * Get the @types package name for a given package 277 */ 278export function getTypesPackageName(packageName: string): string { 279 if (packageName.startsWith('@')) { 280 // Scoped package: @scope/name -> @types/scope__name 281 return `@types/${packageName.slice(1).replace('/', '__')}` 282 } 283 return `@types/${packageName}` 284} 285 286/** 287 * Options for package analysis 288 */ 289export interface AnalyzePackageOptions { 290 typesPackage?: TypesPackageInfo 291 createPackage?: CreatePackageInfo 292} 293 294/** 295 * Analyze a package and return structured analysis 296 */ 297export function analyzePackage( 298 pkg: ExtendedPackageJson, 299 options?: AnalyzePackageOptions, 300): PackageAnalysis { 301 const moduleFormat = detectModuleFormat(pkg) 302 303 const types = detectTypesStatus(pkg, options?.typesPackage) 304 305 return { 306 moduleFormat, 307 types, 308 engines: pkg.engines, 309 createPackage: options?.createPackage, 310 } 311}