forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import { describe, expect, it } from 'vitest'
2import {
3 analyzePackage,
4 detectModuleFormat,
5 detectTypesStatus,
6 getCreatePackageName,
7 getCreateShortName,
8 getTypesPackageName,
9 hasBuiltInTypes,
10} from '../../../../shared/utils/package-analysis'
11
12describe('detectModuleFormat', () => {
13 it('detects ESM from type: module', () => {
14 expect(detectModuleFormat({ type: 'module', main: 'index.js' })).toBe('esm')
15 })
16
17 it('detects CJS from type: commonjs', () => {
18 expect(detectModuleFormat({ type: 'commonjs', main: 'index.js' })).toBe('cjs')
19 })
20
21 it('detects CJS when no type field (default)', () => {
22 expect(detectModuleFormat({ main: 'index.js' })).toBe('cjs')
23 })
24
25 it('detects dual from module + main fields', () => {
26 expect(detectModuleFormat({ module: 'index.mjs', main: 'index.js' })).toBe('dual')
27 })
28
29 it('detects dual from type + module + main fields', () => {
30 expect(detectModuleFormat({ type: 'module', module: 'index.js', main: 'index.cjs' })).toBe(
31 'dual',
32 )
33 })
34
35 it('detects esm from type + module + main fields', () => {
36 expect(detectModuleFormat({ type: 'module', module: 'index.js', main: 'index.js' })).toBe('esm')
37 })
38
39 it('detects ESM from module field without main', () => {
40 expect(detectModuleFormat({ module: 'index.mjs' })).toBe('esm')
41 })
42
43 it('detects dual from exports with import + require conditions', () => {
44 expect(
45 detectModuleFormat({
46 exports: {
47 '.': {
48 import: './index.mjs',
49 require: './index.cjs',
50 },
51 },
52 }),
53 ).toBe('dual')
54 })
55
56 it('detects ESM from exports with only import condition', () => {
57 expect(
58 detectModuleFormat({
59 type: 'module',
60 exports: {
61 '.': {
62 import: './index.js',
63 },
64 },
65 }),
66 ).toBe('esm')
67 })
68
69 it('detects CJS from exports with only require condition', () => {
70 expect(
71 detectModuleFormat({
72 exports: {
73 '.': {
74 require: './index.cjs',
75 },
76 },
77 }),
78 ).toBe('cjs')
79 })
80
81 it('detects dual from nested exports with both conditions', () => {
82 expect(
83 detectModuleFormat({
84 exports: {
85 '.': {
86 import: {
87 types: './dist/index.d.mts',
88 default: './dist/index.mjs',
89 },
90 require: {
91 types: './dist/index.d.ts',
92 default: './dist/index.cjs',
93 },
94 },
95 },
96 }),
97 ).toBe('dual')
98 })
99
100 it('returns cjs for empty package (npm default)', () => {
101 // npm treats packages without type field as CommonJS
102 expect(detectModuleFormat({})).toBe('cjs')
103 })
104
105 it('detect dual from JSON exports', () => {
106 expect(
107 detectModuleFormat({
108 main: 'test.json',
109 exports: {
110 '.': './test.json',
111 },
112 }),
113 ).toBe('dual')
114 })
115
116 it('detect esm from JSON exports', () => {
117 expect(
118 detectModuleFormat({
119 exports: {
120 '.': './test.json',
121 },
122 }),
123 ).toBe('esm')
124 })
125})
126
127describe('detectTypesStatus', () => {
128 it('detects included types from types field', () => {
129 expect(detectTypesStatus({ types: './index.d.ts' })).toEqual({ kind: 'included' })
130 })
131
132 it('detects included types from typings field', () => {
133 expect(detectTypesStatus({ typings: './index.d.ts' })).toEqual({ kind: 'included' })
134 })
135
136 it('detects included types from exports with types condition', () => {
137 expect(
138 detectTypesStatus({
139 exports: {
140 '.': {
141 types: './index.d.ts',
142 default: './index.js',
143 },
144 },
145 }),
146 ).toEqual({ kind: 'included' })
147 })
148
149 it('detects @types package when provided', () => {
150 expect(detectTypesStatus({}, { packageName: '@types/lodash' })).toEqual({
151 kind: '@types',
152 packageName: '@types/lodash',
153 })
154 })
155
156 it('includes deprecation info in @types detection', () => {
157 expect(
158 detectTypesStatus({}, { packageName: '@types/lodash', deprecated: 'Now included in lodash' }),
159 ).toEqual({
160 kind: '@types',
161 packageName: '@types/lodash',
162 deprecated: 'Now included in lodash',
163 })
164 })
165
166 it('returns none when no types detected', () => {
167 expect(detectTypesStatus({})).toEqual({ kind: 'none' })
168 })
169})
170
171describe('getTypesPackageName', () => {
172 it('handles unscoped package', () => {
173 expect(getTypesPackageName('lodash')).toBe('@types/lodash')
174 })
175
176 it('handles scoped package', () => {
177 expect(getTypesPackageName('@nuxt/kit')).toBe('@types/nuxt__kit')
178 })
179})
180
181describe('hasBuiltInTypes', () => {
182 it('returns true when types field is present', () => {
183 expect(hasBuiltInTypes({ types: './index.d.ts' })).toBe(true)
184 })
185
186 it('returns true when typings field is present', () => {
187 expect(hasBuiltInTypes({ typings: './index.d.ts' })).toBe(true)
188 })
189
190 it('returns true when exports has types condition', () => {
191 expect(
192 hasBuiltInTypes({
193 exports: {
194 '.': {
195 types: './index.d.ts',
196 default: './index.js',
197 },
198 },
199 }),
200 ).toBe(true)
201 })
202
203 it('returns false when no types are present', () => {
204 expect(hasBuiltInTypes({ main: 'index.js' })).toBe(false)
205 })
206
207 it('returns false for empty package', () => {
208 expect(hasBuiltInTypes({})).toBe(false)
209 })
210})
211
212describe('analyzePackage', () => {
213 it('analyzes Vue package correctly', () => {
214 const result = analyzePackage({
215 name: 'vue',
216 type: undefined,
217 main: 'index.js',
218 module: 'dist/vue.runtime.esm-bundler.js',
219 types: 'dist/vue.d.ts',
220 exports: {
221 '.': {
222 import: './dist/vue.runtime.esm-bundler.js',
223 require: './index.js',
224 },
225 },
226 })
227
228 expect(result.moduleFormat).toBe('dual')
229 expect(result.types).toEqual({ kind: 'included' })
230 })
231
232 it('analyzes ESM-only package correctly', () => {
233 const result = analyzePackage({
234 name: 'execa',
235 type: 'module',
236 exports: {
237 types: './index.d.ts',
238 default: './index.js',
239 },
240 })
241
242 expect(result.moduleFormat).toBe('esm')
243 expect(result.types).toEqual({ kind: 'included' })
244 })
245
246 it('includes engines when present', () => {
247 const result = analyzePackage({
248 name: 'test',
249 main: 'index.js',
250 engines: {
251 bun: '>=1.0.0',
252 node: '>=18',
253 npm: '>=9',
254 },
255 })
256
257 expect(result.engines).toEqual({
258 bun: '>=1.0.0',
259 node: '>=18',
260 npm: '>=9',
261 })
262 })
263
264 it('detects @types package when typesPackage info is provided', () => {
265 const result = analyzePackage(
266 { name: 'express', main: 'index.js' },
267 { typesPackage: { packageName: '@types/express' } },
268 )
269
270 expect(result.types).toEqual({ kind: '@types', packageName: '@types/express' })
271 })
272
273 it('includes deprecation info for @types package', () => {
274 const result = analyzePackage(
275 { name: 'express', main: 'index.js' },
276 { typesPackage: { packageName: '@types/express', deprecated: 'Use included types instead' } },
277 )
278
279 expect(result.types).toEqual({
280 kind: '@types',
281 packageName: '@types/express',
282 deprecated: 'Use included types instead',
283 })
284 })
285
286 it('includes createPackage when provided', () => {
287 const result = analyzePackage(
288 { name: 'vite', main: 'index.js' },
289 { createPackage: { packageName: 'create-vite' } },
290 )
291
292 expect(result.createPackage).toEqual({ packageName: 'create-vite' })
293 })
294
295 it('includes deprecation info for createPackage', () => {
296 const result = analyzePackage(
297 { name: 'foo', main: 'index.js' },
298 { createPackage: { packageName: 'create-foo', deprecated: 'Use different tool' } },
299 )
300
301 expect(result.createPackage).toEqual({
302 packageName: 'create-foo',
303 deprecated: 'Use different tool',
304 })
305 })
306})
307
308describe('getCreatePackageName', () => {
309 it('handles unscoped package', () => {
310 expect(getCreatePackageName('vite')).toBe('create-vite')
311 })
312
313 it('handles scoped package', () => {
314 expect(getCreatePackageName('@nuxt/app')).toBe('@nuxt/create-app')
315 })
316
317 it('handles single-word package', () => {
318 expect(getCreatePackageName('next')).toBe('create-next')
319 })
320
321 it('handles hyphenated package', () => {
322 expect(getCreatePackageName('solid-js')).toBe('create-solid-js')
323 })
324})
325
326describe('getCreateShortName', () => {
327 it('extracts name from unscoped create-* package', () => {
328 expect(getCreateShortName('create-vite')).toBe('vite')
329 })
330
331 it('extracts name from scoped create-* package', () => {
332 expect(getCreateShortName('@vue/create-app')).toBe('app')
333 })
334
335 it('returns full name if not a create-* package', () => {
336 expect(getCreateShortName('vite')).toBe('vite')
337 })
338
339 it('handles scoped package without create- prefix', () => {
340 expect(getCreateShortName('@scope/foo')).toBe('foo')
341 })
342
343 it('extracts name from create-next-app style packages', () => {
344 expect(getCreateShortName('create-next-app')).toBe('next-app')
345 })
346})