Skip to content

Commit

Permalink
fix: use lockfile walker to find all dependencies in the lock file
Browse files Browse the repository at this point in the history
  • Loading branch information
Weyert de Boer authored and zkochan committed Nov 17, 2022
1 parent 7a38c8c commit 3c542e5
Show file tree
Hide file tree
Showing 25 changed files with 1,095 additions and 1,346 deletions.
21 changes: 7 additions & 14 deletions packages/licenses/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,20 @@
"@pnpm/logger": "^5.0.0"
},
"dependencies": {
"@pnpm/client": "workspace:*",
"@pnpm/constants": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/lockfile-file": "workspace:*",
"@pnpm/lockfile-utils": "workspace:*",
"@pnpm/manifest-utils": "workspace:*",
"@pnpm/matcher": "workspace:*",
"@pnpm/modules-yaml": "workspace:*",
"@pnpm/npm-resolver": "workspace:*",
"@pnpm/pick-registry-for-package": "workspace:*",
"@pnpm/read-package-json": "workspace:*",
"@pnpm/types": "workspace:*",
"dependency-path": "workspace:*",
"p-limit": "^3.1.0",
"ramda": "npm:@pnpm/ramda@0.28.1",
"semver": "^7.3.8"
"@pnpm/lockfile-types": "workspace:*",
"@pnpm/lockfile-utils": "workspace:*",
"@pnpm/lockfile-walker": "workspace:*",
"dependency-path": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/lockfile-file": "workspace:*"
},
"devDependencies": {
"@pnpm/licenses": "workspace:*",
"@pnpm/constants": "workspace:*",
"@types/ramda": "0.28.15",
"@types/semver": "7.3.12",
"npm-run-all": "^4.1.5"
},
"funding": "https://opencollective.com/pnpm",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ function coerceToString (field: unknown): string | null {
return typeof field === 'string' || field === string ? string : null
}

/**
*
* @param field
* @returns
*/
function parseLicenseManifestField (field: unknown) {
if (Array.isArray(field)) {
const licenses = field
Expand Down Expand Up @@ -85,17 +90,20 @@ async function parseLicense (packageInfo: {
return { name: license ?? 'Unknown' }
}

/**
*
* @param pkg
* @returns
*/
export async function getPkgInfo (
pkg: {
alias: string
name: string
version: string
prefix: string
}
): Promise<{
packageManifest: PackageManifest
packageInfo: {
alias: string
from: string
path: string
version: string
Expand All @@ -110,23 +118,27 @@ export async function getPkgInfo (
let manifest
let packageModulePath
let licenseInfo: LicenseInfo

if (pkg.name.length === 0) {
throw new Error('Missing package name')
}

try {
packageModulePath = path.join(pkg.prefix, 'node_modules', pkg.name)
packageModulePath = path.join(pkg.prefix, pkg.name)
const packageManifestPath = path.join(packageModulePath, 'package.json')
manifest = await readPkg(packageManifestPath)

licenseInfo = await parseLicense({ manifest, path: packageModulePath })

manifest.license = licenseInfo.name
} catch (err: any) { // eslint-disable-line
} catch (err: unknown) {
// This will probably never happen
throw new Error(`Failed to fetch manifest data for ${pkg.name}`)
}

return {
packageManifest: manifest,
packageInfo: {
alias: pkg.alias,
from: pkg.name,
path: packageModulePath,
version: pkg.version,
Expand Down
3 changes: 1 addition & 2 deletions packages/licenses/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { licensesDepsOfProjects } from './licensesDepsOfProjects'
export { LicensePackage } from './licenses'
export { LicensePackage, licences } from './licenses'
141 changes: 57 additions & 84 deletions packages/licenses/src/licenses.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { WANTED_LOCKFILE } from '@pnpm/constants'
import { PnpmError } from '@pnpm/error'
import {
getLockfileImporterId,
Lockfile,
} from '@pnpm/lockfile-file'
import { nameVerFromPkgSnapshot } from '@pnpm/lockfile-utils'
import { getAllDependenciesFromManifest } from '@pnpm/manifest-utils'
import {
DependenciesField,
DEPENDENCIES_FIELDS,
IncludedDependencies,
PackageManifest,
ProjectManifest,
Registries,
} from '@pnpm/types'
import * as dp from 'dependency-path'
import { getPkgInfo } from './getPkgInfo'
import { LicenseNode, lockfileToLicenseNodeTree } from './lockfileToLicenseNodeTree'

export interface LicensePackage {
alias: string
belongsTo: DependenciesField
version: string
packageManifest?: PackageManifest
Expand All @@ -30,15 +25,13 @@ export interface LicensePackage {

export type GetPackageInfoFunction = (
pkg: {
alias: string
name: string
version: string
prefix: string
}
) => Promise<{
packageManifest: PackageManifest
packageInfo: {
alias: string
from: string
path: string
version: string
Expand All @@ -51,98 +44,78 @@ export type GetPackageInfoFunction = (
}
}>

/**
* @private
* @param licenseNode
* @returns LicensePackage[]
*/
function getDependenciesFromLicenseNode (licenseNode: LicenseNode): LicensePackage[] {
if (!licenseNode.dependencies) {
return [
]
}

let dependencies: LicensePackage[] = []
for (const dependencyName in licenseNode.dependencies) {
const dependencyNode = licenseNode.dependencies[dependencyName]
const dependenciesOfNode = getDependenciesFromLicenseNode(dependencyNode)

dependencies = [
...dependencies,
...dependenciesOfNode,
{
belongsTo: dependencyNode.dev ? 'devDependencies' : 'dependencies',
version: dependencyNode.version as string,
packageManifest: dependencyNode.packageManifest,
packageName: dependencyName,
license: dependencyNode.license as string,
licenseContents: dependencyNode.licenseContents,
author: dependencyNode.vendorName as string,
packageDirectory: dependencyNode.path,
},
]
}

return dependencies
}

export async function licences (
opts: {
currentLockfile: Lockfile | null
getPackageInfo?: GetPackageInfoFunction
ignoreDependencies?: Set<string>
include?: IncludedDependencies
lockfileDir: string
manifest: ProjectManifest
getPackageInfo: GetPackageInfoFunction
match?: (dependencyName: string) => boolean
prefix: string
virtualStoreDir: string
modulesDir?: string
registries: Registries
wantedLockfile: Lockfile | null
}
): Promise<LicensePackage[]> {
if (packageHasNoDeps(opts.manifest)) return []
if (opts.wantedLockfile == null) {
throw new PnpmError('LICENSES_NO_LOCKFILE', `No lockfile in directory "${opts.lockfileDir}". Run \`pnpm install\` to generate one.`)
}
const allDeps = getAllDependenciesFromManifest(opts.manifest)
const importerId = getLockfileImporterId(opts.lockfileDir, opts.prefix)
const licenses: LicensePackage[] = []

await Promise.all(
DEPENDENCIES_FIELDS.map(async (depType) => {
if (
opts.include?.[depType] === false ||
(opts.wantedLockfile!.importers[importerId][depType] == null)
) return

let pkgs = Object.keys(opts.wantedLockfile!.importers[importerId][depType]!)
if (opts.match != null) {
pkgs = pkgs.filter((pkgName) => opts.match!(pkgName))
}

await Promise.all(
pkgs.map(async (alias) => {
if (!allDeps[alias]) return
const ref = opts.wantedLockfile!.importers[importerId][depType]![alias]
if (
ref.startsWith('file:') || // ignoring linked packages. (For backward compatibility)
opts.ignoreDependencies?.has(alias)
) {
return
}
const licenseNodeTree = await lockfileToLicenseNodeTree(opts.wantedLockfile, {
dir: opts.lockfileDir,
modulesDir: opts.modulesDir,
virtualStoreDir: opts.virtualStoreDir,
include: opts.include,
getPackageInfo: opts.getPackageInfo ?? getPkgInfo,
})

const relativeDepPath = dp.refToRelative(ref, alias)
const licensePackages = new Map<string, LicensePackage>()
for (const dependencyName in licenseNodeTree.dependencies) {
const licenseNode = licenseNodeTree.dependencies[dependencyName]
const dependenciesOfNode = getDependenciesFromLicenseNode(licenseNode)

// ignoring linked packages
if (relativeDepPath === null) return

const pkgSnapshot = opts.wantedLockfile!.packages?.[relativeDepPath]
if (pkgSnapshot == null) {
throw new Error(`Invalid ${WANTED_LOCKFILE} file. ${relativeDepPath} not found in packages field`)
}

const { name: packageName, version: packageVersion } = nameVerFromPkgSnapshot(relativeDepPath, pkgSnapshot)
const name = dp.parse(relativeDepPath).name ?? packageName

// Fetch the most recent package by the give name
const fetchPackageInfo = opts.getPackageInfo
const { packageManifest, packageInfo } = await fetchPackageInfo({
alias,
name,
version: packageVersion,
prefix: opts.prefix,
})

licenses.push({
alias,
belongsTo: depType,
version: packageVersion,
packageManifest,
packageName,
license: packageInfo.license,
licenseContents: packageInfo.licenseContents,
author: packageInfo.author,
packageDirectory: packageInfo.path,
})
})
)
dependenciesOfNode.forEach((dependencyNode) => {
licensePackages.set(dependencyNode.packageName, dependencyNode)
})
)

return licenses.sort((pkg1, pkg2) => pkg1.packageName.localeCompare(pkg2.packageName))
}

function packageHasNoDeps (manifest: ProjectManifest) {
return ((manifest.dependencies == null) || isEmpty(manifest.dependencies)) &&
((manifest.devDependencies == null) || isEmpty(manifest.devDependencies)) &&
((manifest.optionalDependencies == null) || isEmpty(manifest.optionalDependencies))
}
}

function isEmpty (obj: object) {
return Object.keys(obj).length === 0
// Get all non-duplicate dependencies of the project
const projectDependencies = Array.from(licensePackages.values())
return Array.from(projectDependencies).sort((pkg1, pkg2) => pkg1.packageName.localeCompare(pkg2.packageName))
}
67 changes: 0 additions & 67 deletions packages/licenses/src/licensesDepsOfProjects.ts

This file was deleted.

0 comments on commit 3c542e5

Please sign in to comment.