forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
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}