[READ-ONLY] a fast, modern browser for the npm registry
at main 346 lines 9.1 kB view raw
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})