22 * @fileoverview Comprehensive tests for npm lockfile parsers
33 *
44 * Tests cover npm package-lock.json formats:
5- * - v1 (legacy dependencies format - NOT supported, returns empty )
5+ * - v1 (legacy dependencies tree format, parsed via fromDependenciesTree )
66 * - v2 (current format with packages field)
77 * - v3 (same as v2, optimized for npm 7+)
8- *
9- * Note: This parser only supports v2/v3 format (packages field).
10- * v1 format uses dependencies field and is not supported.
118 */
129
1310import assert from 'node:assert/strict' ;
@@ -17,7 +14,7 @@ import { describe, test } from 'node:test';
1714import { fileURLToPath } from 'node:url' ;
1815
1916// Public API
20- import { fromPackageLock , parseLockfileKey } from '../../src/parsers/npm.js' ;
17+ import { fromDependenciesTree , fromPackageLock , parseLockfileKey } from '../../src/parsers/npm.js' ;
2118
2219const __dirname = dirname ( fileURLToPath ( import . meta. url ) ) ;
2320const decodedDir = join ( __dirname , '..' , 'decoded' , 'npm' ) ;
@@ -125,13 +122,22 @@ describe('npm parsers', () => {
125122 // ============================================================================
126123 describe ( 'fromPackageLock' , ( ) => {
127124 describe ( '[npm-01] version detection' , ( ) => {
128- test ( 'returns empty for v1 format (uses dependencies, not packages) ' , ( ) => {
125+ test ( 'parses v1 format via dependencies tree fallback ' , ( ) => {
129126 const content = loadFixture ( 'package-lock.json.v1' ) ;
130127 const deps = [ ...fromPackageLock ( content ) ] ;
131128
132- // v1 format uses dependencies field, not packages
133- // Our parser only supports v2/v3 (packages field)
134- assert . equal ( deps . length , 0 , 'v1 format should return empty (not supported)' ) ;
129+ // v1 format falls back to fromDependenciesTree
130+ assert . ok ( deps . length > 0 , `v1 should yield deps, got ${ deps . length } ` ) ;
131+
132+ // Every dep should have name and version
133+ for ( const dep of deps ) {
134+ assert . ok ( dep . name , 'Every dep should have name' ) ;
135+ assert . ok ( dep . version , 'Every dep should have version' ) ;
136+ }
137+
138+ // Most deps should have integrity (the v1 fixture has them)
139+ const withIntegrity = deps . filter ( d => d . integrity ) ;
140+ assert . ok ( withIntegrity . length > deps . length * 0.9 , 'Most deps should have integrity' ) ;
135141 } ) ;
136142
137143 test ( 'parses v2 format' , ( ) => {
@@ -390,4 +396,211 @@ describe('npm parsers', () => {
390396 } ) ;
391397 } ) ;
392398 } ) ;
399+
400+ // ============================================================================
401+ // fromDependenciesTree tests (v1 lockfile support)
402+ // ============================================================================
403+ describe ( 'fromDependenciesTree' , ( ) => {
404+ test ( 'parses flat dependencies' , ( ) => {
405+ const lockfile = {
406+ lockfileVersion : 1 ,
407+ dependencies : {
408+ lodash : {
409+ version : '4.17.21' ,
410+ resolved : 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz' ,
411+ integrity : 'sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ=='
412+ } ,
413+ express : {
414+ version : '4.18.0' ,
415+ resolved : 'https://registry.npmjs.org/express/-/express-4.18.0.tgz'
416+ }
417+ }
418+ } ;
419+
420+ const deps = [ ...fromDependenciesTree ( lockfile ) ] ;
421+ assert . equal ( deps . length , 2 ) ;
422+ assert . equal ( deps [ 0 ] . name , 'lodash' ) ;
423+ assert . equal ( deps [ 0 ] . version , '4.17.21' ) ;
424+ assert . equal ( deps [ 0 ] . resolved , 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz' ) ;
425+ assert . ok ( deps [ 0 ] . integrity ) ;
426+ assert . equal ( deps [ 1 ] . name , 'express' ) ;
427+ assert . equal ( deps [ 1 ] . version , '4.18.0' ) ;
428+ } ) ;
429+
430+ test ( 'walks nested dependencies recursively' , ( ) => {
431+ const lockfile = {
432+ lockfileVersion : 1 ,
433+ dependencies : {
434+ base : {
435+ version : '1.0.0' ,
436+ resolved : 'https://registry.npmjs.org/base/-/base-1.0.0.tgz' ,
437+ dependencies : {
438+ 'define-property' : {
439+ version : '1.0.0' ,
440+ resolved : 'https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz' ,
441+ dependencies : {
442+ 'is-descriptor' : {
443+ version : '1.0.0' ,
444+ resolved : 'https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.0.tgz'
445+ }
446+ }
447+ }
448+ }
449+ }
450+ }
451+ } ;
452+
453+ const deps = [ ...fromDependenciesTree ( lockfile ) ] ;
454+ assert . equal ( deps . length , 3 ) ;
455+
456+ const names = deps . map ( d => d . name ) ;
457+ assert . ok ( names . includes ( 'base' ) ) ;
458+ assert . ok ( names . includes ( 'define-property' ) ) ;
459+ assert . ok ( names . includes ( 'is-descriptor' ) ) ;
460+ } ) ;
461+
462+ test ( 'skips entries without version' , ( ) => {
463+ const lockfile = {
464+ lockfileVersion : 1 ,
465+ dependencies : {
466+ lodash : { version : '4.17.21' } ,
467+ broken : { }
468+ }
469+ } ;
470+
471+ const deps = [ ...fromDependenciesTree ( lockfile ) ] ;
472+ assert . equal ( deps . length , 1 ) ;
473+ assert . equal ( deps [ 0 ] . name , 'lodash' ) ;
474+ } ) ;
475+
476+ test ( 'handles empty dependencies object' , ( ) => {
477+ const lockfile = { lockfileVersion : 1 , dependencies : { } } ;
478+ const deps = [ ...fromDependenciesTree ( lockfile ) ] ;
479+ assert . equal ( deps . length , 0 ) ;
480+ } ) ;
481+
482+ test ( 'handles missing dependencies field' , ( ) => {
483+ const lockfile = { lockfileVersion : 1 } ;
484+ const deps = [ ...fromDependenciesTree ( lockfile ) ] ;
485+ assert . equal ( deps . length , 0 ) ;
486+ } ) ;
487+
488+ test ( 'accepts JSON string input' , ( ) => {
489+ const content = JSON . stringify ( {
490+ lockfileVersion : 1 ,
491+ dependencies : {
492+ lodash : { version : '4.17.21' }
493+ }
494+ } ) ;
495+
496+ const deps = [ ...fromDependenciesTree ( content ) ] ;
497+ assert . equal ( deps . length , 1 ) ;
498+ } ) ;
499+
500+ test ( 'yields resolved when present' , ( ) => {
501+ const lockfile = {
502+ lockfileVersion : 1 ,
503+ dependencies : {
504+ lodash : {
505+ version : '4.17.21' ,
506+ resolved : 'https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz'
507+ }
508+ }
509+ } ;
510+
511+ const deps = [ ...fromDependenciesTree ( lockfile ) ] ;
512+ assert . equal ( deps [ 0 ] . resolved , 'https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz' ) ;
513+ } ) ;
514+
515+ test ( 'omits resolved when absent' , ( ) => {
516+ const lockfile = {
517+ lockfileVersion : 1 ,
518+ dependencies : {
519+ lodash : { version : '4.17.21' }
520+ }
521+ } ;
522+
523+ const deps = [ ...fromDependenciesTree ( lockfile ) ] ;
524+ assert . equal ( deps [ 0 ] . resolved , undefined ) ;
525+ } ) ;
526+
527+ test ( 'parses v1 fixture with correct count' , ( ) => {
528+ const content = loadFixture ( 'package-lock.json.v1' ) ;
529+ const deps = [ ...fromDependenciesTree ( content ) ] ;
530+
531+ // The v1 fixture (meteor-guide) has 345 top-level + 273 nested = 618 total
532+ assert . ok ( deps . length > 300 , `Expected >300 deps, got ${ deps . length } ` ) ;
533+
534+ // Verify structure
535+ for ( const dep of deps ) {
536+ assert . ok ( dep . name , 'Every dep should have name' ) ;
537+ assert . ok ( dep . version , 'Every dep should have version' ) ;
538+ }
539+
540+ // Most should have resolved
541+ const withResolved = deps . filter ( d => d . resolved ) ;
542+ assert . ok ( withResolved . length > deps . length * 0.9 , 'Most deps should have resolved' ) ;
543+ } ) ;
544+
545+ test ( 'fromPackageLock delegates to fromDependenciesTree for v1' , ( ) => {
546+ const lockfile = {
547+ lockfileVersion : 1 ,
548+ dependencies : {
549+ lodash : {
550+ version : '4.17.21' ,
551+ resolved : 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz'
552+ }
553+ }
554+ } ;
555+
556+ // fromPackageLock should fall back to fromDependenciesTree
557+ const viaPL = [ ...fromPackageLock ( lockfile ) ] ;
558+ const viaDT = [ ...fromDependenciesTree ( lockfile ) ] ;
559+
560+ assert . equal ( viaPL . length , viaDT . length ) ;
561+ assert . deepEqual (
562+ viaPL . map ( d => `${ d . name } @${ d . version } ` ) ,
563+ viaDT . map ( d => `${ d . name } @${ d . version } ` )
564+ ) ;
565+ } ) ;
566+
567+ test ( 'fromPackageLock prefers packages over dependencies for v2' , ( ) => {
568+ // v2 lockfiles have both packages and dependencies
569+ // fromPackageLock should use packages, not dependencies
570+ const lockfile = {
571+ lockfileVersion : 2 ,
572+ packages : {
573+ '' : { name : 'root' , version : '1.0.0' } ,
574+ 'node_modules/lodash' : { version : '4.17.21' }
575+ } ,
576+ dependencies : {
577+ lodash : { version : '4.17.20' } // different version to prove packages wins
578+ }
579+ } ;
580+
581+ const deps = [ ...fromPackageLock ( lockfile ) ] ;
582+ assert . equal ( deps . length , 1 ) ;
583+ assert . equal ( deps [ 0 ] . version , '4.17.21' , 'Should use packages version, not dependencies' ) ;
584+ } ) ;
585+
586+ test ( 'handles scoped packages in v1 format' , ( ) => {
587+ const lockfile = {
588+ lockfileVersion : 1 ,
589+ dependencies : {
590+ '@babel/core' : {
591+ version : '7.23.0' ,
592+ resolved : 'https://registry.npmjs.org/@babel/core/-/core-7.23.0.tgz'
593+ } ,
594+ '@types/node' : {
595+ version : '20.0.0'
596+ }
597+ }
598+ } ;
599+
600+ const deps = [ ...fromDependenciesTree ( lockfile ) ] ;
601+ assert . equal ( deps . length , 2 ) ;
602+ assert . ok ( deps . some ( d => d . name === '@babel/core' ) ) ;
603+ assert . ok ( deps . some ( d => d . name === '@types/node' ) ) ;
604+ } ) ;
605+ } ) ;
393606} ) ;
0 commit comments