[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at e38b8ba477e434abd2aa34fe6c3395f6febf3e44 309 lines 8.2 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 // Has both module (ESM) and main (CJS) fields 86 return 'dual' 87 } 88 89 if (hasModule || isTypeModule) { 90 return 'esm' 91 } 92 93 if (hasMain || isTypeCommonjs) { 94 return 'cjs' 95 } 96 97 return 'unknown' 98} 99 100interface ExportsAnalysis { 101 hasImport: boolean 102 hasRequire: boolean 103 hasModule: boolean 104 hasTypes: boolean 105} 106 107/** 108 * Recursively analyze exports field for module format indicators 109 */ 110function analyzeExports(exports: PackageExports, depth = 0): ExportsAnalysis { 111 const result: ExportsAnalysis = { 112 hasImport: false, 113 hasRequire: false, 114 hasModule: false, 115 hasTypes: false, 116 } 117 118 // Prevent infinite recursion 119 if (depth > 10) return result 120 121 if (exports === null || exports === undefined) { 122 return result 123 } 124 125 if (typeof exports === 'string') { 126 // Check file extension for format hints 127 if (exports.endsWith('.mjs') || exports.endsWith('.mts')) { 128 result.hasImport = true 129 } else if (exports.endsWith('.cjs') || exports.endsWith('.cts')) { 130 result.hasRequire = true 131 } 132 if (exports.endsWith('.d.ts') || exports.endsWith('.d.mts') || exports.endsWith('.d.cts')) { 133 result.hasTypes = true 134 } 135 return result 136 } 137 138 if (Array.isArray(exports)) { 139 for (const item of exports) { 140 const subResult = analyzeExports(item, depth + 1) 141 mergeExportsAnalysis(result, subResult) 142 } 143 return result 144 } 145 146 if (typeof exports === 'object') { 147 for (const [key, value] of Object.entries(exports)) { 148 // Check condition keys 149 if (key === 'import') { 150 result.hasImport = true 151 } else if (key === 'require') { 152 result.hasRequire = true 153 } else if (key === 'module') { 154 result.hasModule = true 155 } else if (key === 'types') { 156 result.hasTypes = true 157 } 158 159 // Recurse into nested exports 160 const subResult = analyzeExports(value, depth + 1) 161 mergeExportsAnalysis(result, subResult) 162 } 163 } 164 165 return result 166} 167 168function mergeExportsAnalysis(target: ExportsAnalysis, source: ExportsAnalysis): void { 169 target.hasImport = target.hasImport || source.hasImport 170 target.hasRequire = target.hasRequire || source.hasRequire 171 target.hasModule = target.hasModule || source.hasModule 172 target.hasTypes = target.hasTypes || source.hasTypes 173} 174 175/** Info about a related package (@types or create-*) */ 176export interface RelatedPackageInfo { 177 packageName: string 178 deprecated?: string 179} 180 181export type TypesPackageInfo = RelatedPackageInfo 182export type CreatePackageInfo = RelatedPackageInfo 183 184/** 185 * Get the create-* package name for a given package. 186 * e.g., "vite" -> "create-vite", "@scope/foo" -> "@scope/create-foo" 187 */ 188export function getCreatePackageName(packageName: string): string { 189 if (packageName.startsWith('@')) { 190 // Scoped package: @scope/name -> @scope/create-name 191 const slashIndex = packageName.indexOf('/') 192 const scope = packageName.slice(0, slashIndex) 193 const name = packageName.slice(slashIndex + 1) 194 return `${scope}/create-${name}` 195 } 196 return `create-${packageName}` 197} 198 199/** 200 * Extract the short name from a create-* package for display. 201 * e.g., "create-vite" -> "vite", "@scope/create-foo" -> "foo" 202 */ 203export function getCreateShortName(createPackageName: string): string { 204 if (createPackageName.startsWith('@')) { 205 // @scope/create-foo -> foo 206 const slashIndex = createPackageName.indexOf('/') 207 const name = createPackageName.slice(slashIndex + 1) 208 if (name.startsWith('create-')) { 209 return name.slice('create-'.length) 210 } 211 return name 212 } 213 // create-vite -> vite 214 if (createPackageName.startsWith('create-')) { 215 return createPackageName.slice('create-'.length) 216 } 217 return createPackageName 218} 219 220/** 221 * Detect TypeScript types status for a package 222 */ 223export function detectTypesStatus( 224 pkg: ExtendedPackageJson, 225 typesPackageInfo?: TypesPackageInfo, 226): TypesStatus { 227 // Check for built-in types 228 if (pkg.types || pkg.typings) { 229 return { kind: 'included' } 230 } 231 232 // Check exports field for types 233 if (pkg.exports) { 234 const exportInfo = analyzeExports(pkg.exports) 235 if (exportInfo.hasTypes) { 236 return { kind: 'included' } 237 } 238 } 239 240 // Check for @types package 241 if (typesPackageInfo) { 242 return { 243 kind: '@types', 244 packageName: typesPackageInfo.packageName, 245 deprecated: typesPackageInfo.deprecated, 246 } 247 } 248 249 return { kind: 'none' } 250} 251 252/** 253 * Check if a package has built-in TypeScript types 254 * (without needing to check for @types packages) 255 */ 256export function hasBuiltInTypes(pkg: ExtendedPackageJson): boolean { 257 // Check types/typings field 258 if (pkg.types || pkg.typings) { 259 return true 260 } 261 262 // Check exports field for types 263 if (pkg.exports) { 264 const exportInfo = analyzeExports(pkg.exports) 265 if (exportInfo.hasTypes) { 266 return true 267 } 268 } 269 270 return false 271} 272 273/** 274 * Get the @types package name for a given package 275 */ 276export function getTypesPackageName(packageName: string): string { 277 if (packageName.startsWith('@')) { 278 // Scoped package: @scope/name -> @types/scope__name 279 return `@types/${packageName.slice(1).replace('/', '__')}` 280 } 281 return `@types/${packageName}` 282} 283 284/** 285 * Options for package analysis 286 */ 287export interface AnalyzePackageOptions { 288 typesPackage?: TypesPackageInfo 289 createPackage?: CreatePackageInfo 290} 291 292/** 293 * Analyze a package and return structured analysis 294 */ 295export function analyzePackage( 296 pkg: ExtendedPackageJson, 297 options?: AnalyzePackageOptions, 298): PackageAnalysis { 299 const moduleFormat = detectModuleFormat(pkg) 300 301 const types = detectTypesStatus(pkg, options?.typesPackage) 302 303 return { 304 moduleFormat, 305 types, 306 engines: pkg.engines, 307 createPackage: options?.createPackage, 308 } 309}