Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/support lockfile v3 #169

Merged
merged 3 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rich-monkeys-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'lockfile-lint-api': minor
---

Add support for npm lockfile v3 format
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "a",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages2": {
"": {
"name": "a",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"debug": "^4.3.4"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved2": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "a",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "a",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"debug": "^4.3.4"
}
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"name": "a",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "a",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"debug": "^4.3.4"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"resolved": "http://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
"integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/core/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"packages/lockfile-lint/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"packages/lockfile-lint/node_modules/cliui": {
"version": "7.0.4",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
}
}
}

53 changes: 53 additions & 0 deletions packages/lockfile-lint-api/__tests__/parseNpmLockfile3.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* eslint-disable no-new */
'use strict'

const ParseLockfile = require('../src/ParseLockfile')
const path = require('path')

describe('ParseLockfile v3 Npm', () => {
it('parsing an npm lockfile returns an object with packages', () => {
const mockNpmLockfilePath = path.join(__dirname, './__fixtures__/package-lock-v3.json')
const options = {
lockfilePath: mockNpmLockfilePath,
lockfileType: 'npm'
}
const parser = new ParseLockfile(options)
const lockfile = parser.parseSync()

expect(lockfile.type).toEqual('success')
expect(lockfile.object).toEqual(
expect.objectContaining({
'@ampproject/remapping@2.2.1-6896846cb1525fe356a0a09c28e387a034d364ab': expect.any(Object),
'argparse@2.0.1-d71b6b43bc0d034f7b86d6d1149d6a43be13f2ed': expect.any(Object),
'cliui@7.0.4-a26e875b912c887d1429fe2e16ed66bcbe362d61': expect.any(Object),
'debug@4.3.4-c15d73814bfc58727435c213e23203e48f036cd9': expect.any(Object),
'ms@2.1.2-d6934ce87f6e568c4f5d6d9f6e5c5697992e7b91': expect.any(Object)
})
)
})

it('parsing an npm lockfile with invalid content throws an error', () => {
const mockNpmLockfilePath = path.join(__dirname, './__fixtures__/bad-package-lock-v3.json')
const options = {
lockfilePath: mockNpmLockfilePath,
lockfileType: 'npm'
}
const parser = new ParseLockfile(options)
expect(() => parser.parseSync()).toThrowError(
`Unable to parse npm lockfile "${mockNpmLockfilePath}"`
)
})

it('parsing an npm lockfile with no packages doesnt trigger an error', () => {
const mockNpmLockfilePath = path.join(__dirname, './__fixtures__/package-lock-v3-empty.json')
const options = {
lockfilePath: mockNpmLockfilePath,
lockfileType: 'npm'
}
const parser = new ParseLockfile(options)
const lockfile = parser.parseSync()

expect(lockfile.type).toEqual('success')
expect(lockfile.object).toEqual({})
})
})
81 changes: 65 additions & 16 deletions packages/lockfile-lint-api/src/ParseLockfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ function checkSampleContent (lockfile, isYarnBerry) {
const [sampleKey, sampleValue] = Object.entries(lockfile)[isYarnBerry ? 1 : 0]
return (
sampleKey.match(/.*@.*/) &&
(sampleValue &&
typeof sampleValue === 'object' &&
sampleValue.hasOwnProperty('version') &&
(sampleValue.hasOwnProperty('resolved') || sampleValue.hasOwnProperty('resolution')))
sampleValue &&
typeof sampleValue === 'object' &&
sampleValue.hasOwnProperty('version') &&
(sampleValue.hasOwnProperty('resolved') || sampleValue.hasOwnProperty('resolution'))
)
}
/**
Expand Down Expand Up @@ -174,7 +174,20 @@ class ParseLockfile {

// transform original format of npm's package-json to match yarns
// so we have a unified format to validate against
const npmDepsTree = packageJsonParsed.dependencies
// const npmDepsTree = packageJsonParsed.dependencies
let npmDepsTree = null

if (
packageJsonParsed.dependencies &&
Object.keys(packageJsonParsed.dependencies).length > 0
) {
npmDepsTree = packageJsonParsed.dependencies
}

if (packageJsonParsed.packages && Object.keys(packageJsonParsed.packages).length > 0) {
npmDepsTree = packageJsonParsed.packages
}

flattenedDepTree = npmDepsTree ? this._flattenNpmDepsTree(npmDepsTree) : {}
} catch (error) {
throw new ParsingError(PARSE_NPMLOCKFILE_FAILED, this.options.lockfilePath, error)
Expand All @@ -188,25 +201,61 @@ class ParseLockfile {

_flattenNpmDepsTree (npmDepsTree, npmDepMap = {}) {
for (const [depName, depMetadata] of Object.entries(npmDepsTree)) {
const depMetadataShortend = {
version: depMetadata.version,
resolved: depMetadata.resolved ? depMetadata.resolved : depMetadata.version,
integrity: depMetadata.integrity,
requires: depMetadata.requires
}
const hashedDepValues = hash(depMetadataShortend)
// only evaluate dependency metadata if it's an object with actual metadata
// @TODO potentially, this entry can be just a dependency name and version
// which would inject a new dependency on npm install - warn based on diff?
if (typeof depMetadata === 'object' && depName.length > 0) {
const depMetadataShortend = {
version: depMetadata.version,
resolved: depMetadata.resolved ? depMetadata.resolved : depMetadata.version,
integrity: depMetadata.integrity,
requires: depMetadata.requires
}
const hashedDepValues = hash(depMetadataShortend)

npmDepMap[`${depName}@${depMetadata.version}-${hashedDepValues}`] = depMetadataShortend
// @TODO should we implement a clean package name
// or stay aligned with npm's lockfile reporting of full package path on disk?
// it has advantages in monorepos, such as reporting something like:
// packages/lockfile-lint/node_modules/yargs-parser
// instead of just
// yargs-parser
//
// npm package-lock.json v3 has depName set to path on disk, i.e:
// "node_modules/@babel/compat-data": {
// "version": "7.22.5",
// ..}
// we strip off the 'node_modules/' suffix to print pretty package name
// let depNameClean = depName
// if (depName.indexOf('node_modules/') === 0) {
// depNameClean = depName.substring('node_modules/'.length)
// }
const depNameClean = this.extractedPackageName(depName)

const nestedDepsTree = depMetadata.dependencies
npmDepMap[`${depNameClean}@${depMetadata.version}-${hashedDepValues}`] = depMetadataShortend

if (nestedDepsTree && Object.keys(nestedDepsTree).length !== 0) {
this._flattenNpmDepsTree(nestedDepsTree, npmDepMap)
const nestedDepsTree = depMetadata.dependencies

if (nestedDepsTree && Object.keys(nestedDepsTree).length !== 0) {
this._flattenNpmDepsTree(nestedDepsTree, npmDepMap)
}
}
}

return npmDepMap
}

extractedPackageName (packageName) {
const parts = packageName.split('/')
const lastIndex = parts.lastIndexOf('node_modules')

if (lastIndex === -1) {
// If "node_modules" is not found, return the last part of the input
return parts[parts.length - 1]
} else {
// If "node_modules" is found, return the part after it
return parts.slice(lastIndex + 1).join('/')
}
}
}

module.exports = ParseLockfile
Loading