Skip to content

Commit

Permalink
Merge pull request #77 from jslicense/arborist
Browse files Browse the repository at this point in the history
Replace read-package-tree with npm's Arborist (close #64)
  • Loading branch information
kemitchell committed Aug 24, 2022
2 parents d47a1f5 + b9e0ce6 commit e79776f
Show file tree
Hide file tree
Showing 23 changed files with 5,506 additions and 3,064 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Expand Up @@ -5,7 +5,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: ['10', '11', '12', '13', '14']
node: ['12', '13', '14', '15', '16', '17', '18']
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
Expand Down
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
**/.package-lock.json
191 changes: 41 additions & 150 deletions index.js
@@ -1,17 +1,13 @@
module.exports = licensee

var Arborist = require('@npmcli/arborist')
var blueOakList = require('@blueoak/list')
var correctLicenseMetadata = require('correct-license-metadata')
var has = require('has')
var npmLicenseCorrections = require('npm-license-corrections')
var osi = require('spdx-osi')
var parse = require('spdx-expression-parse')
var parseJSON = require('json-parse-errback')
var readPackageTree = require('read-package-tree')
var runParallel = require('run-parallel')
var satisfies = require('semver').satisfies
var simpleConcat = require('simple-concat')
var spawn = require('child_process').spawn
var spdxAllowed = require('spdx-whitelisted')

function licensee (configuration, path, callback) {
Expand All @@ -37,113 +33,24 @@ function licensee (configuration, path, callback) {
) {
callback(new Error('No licenses or packages allowed.'))
} else {
if (configuration.productionOnly) {
// In order to ignore devDependencies, we need to read:
//
// 1. the dependencies-only dependency graph, from
// `npm ls --json --production`
//
// 2. the structure of `node_modules` and `package.json`
// files within it, with read-package-tree.
//
// `npm ls` calls read-package-tree internally, but does
// lots of npm-specific post-processing to produce the
// dependency tree. Calling read-package-tree twice, at
// the same time, is far from efficient. But it works,
// and doing so helps keep this package small.
runParallel({
dependencies: readDependencyList,
packages: readFilesystemTree
}, function (error, trees) {
if (error) callback(error)
else withTrees(trees.packages, trees.dependencies)
})
} else {
// If we are analyzing _all_ installed dependencies,
// and don't care whether they're devDependencies
// or not, just read `node_modules`. We don't need
// the dependency graph.
readFilesystemTree(function (error, packages) {
if (error) callback(error)
else {
if (configuration.filterPackages) {
packages = configuration.filterPackages(packages)
}
withTrees(packages, false)
var arborist = new Arborist({ path })
arborist.loadActual({ forceActual: true })
.then(function (tree) {
var dependencies = Array.from(tree.inventory.values())
.filter(function (dependency) {
return !dependency.isProjectRoot
})
if (configuration.filterPackages) {
dependencies = configuration.filterPackages(dependencies)
}
callback(null, findIssues(configuration, dependencies))
})
.catch(function (error) {
return callback(error)
})
}
}

function withTrees (packages, dependencies) {
callback(null, findIssues(
configuration, packages, dependencies, []
))
}

function readDependencyList (done) {
var executable = process.platform === 'win32' ? 'npm.cmd' : 'npm'
var child = spawn(
executable, ['ls', '--production', '--json'], { cwd: path }
)
var outputError
var json
simpleConcat(child.stdout, function (error, buffer) {
if (error) outputError = error
else json = buffer
})
child.once('error', function (error) {
outputError = error
})
child.once('close', function (code) {
if (outputError) {
done(outputError)
} else {
if (code !== 0) console.error('Warning: npm exited with status ' + code)

parseJSON(json, function (error, graph) {
if (error) return done(error)
if (!has(graph, 'dependencies')) {
done(new Error('cannot interpret npm ls --json output'))
} else {
var flattened = {}
flattenDependencyTree(graph.dependencies, flattened)
done(null, flattened)
}
})
}
})
}

function readFilesystemTree (done) {
readPackageTree(path, function (error, tree) {
if (error) return done(error)
done(null, tree.children)
})
}
}

var KEY_PREFIX = '.'

function flattenDependencyTree (graph, object) {
Object.keys(graph).forEach(function (name) {
var node = graph[name]
var version = node.version
var key = KEY_PREFIX + name
if (
has(object, key) &&
object[key].indexOf(version) === -1
) {
object[key].push(version)
} else {
object[key] = [version]
}
if (has(node, 'dependencies')) {
flattenDependencyTree(node.dependencies, object)
}
})
}

function validConfiguration (configuration) {
return (
isObject(configuration) &&
Expand All @@ -169,62 +76,46 @@ function isString (argument) {
return typeof argument === 'string'
}

function findIssues (configuration, children, dependencies, results) {
if (Array.isArray(children)) {
children.forEach(function (child) {
if (
!configuration.productionOnly ||
appearsIn(child, dependencies)
) {
var result = resultForPackage(configuration, child)
// Deduplicate.
var existing = results.find(function (existing) {
return (
existing.name === result.name &&
existing.version === result.version
)
})
if (existing) {
if (existing.duplicates) {
existing.duplicates.push(result)
} else {
existing.duplicates = [result]
}
} else {
results.push(result)
}
findIssues(configuration, child, dependencies, results)
}
if (child.children) {
findIssues(configuration, child.children, dependencies, results)
}
function findIssues (configuration, dependencies) {
var results = []
dependencies.forEach(function (dependency) {
if (
configuration.productionOnly &&
dependency.dev
) return
var result = resultForPackage(configuration, dependency)
// Deduplicate.
var existing = results.find(function (existing) {
return (
existing.name === result.name &&
existing.version === result.version
)
})
return results
} else return results
}

function appearsIn (installed, dependencies) {
var name = installed.package.name
var key = KEY_PREFIX + name
var version = installed.package.version
return (
has(dependencies, key) &&
dependencies[key].indexOf(version) !== -1
)
if (existing) {
if (existing.duplicates) {
existing.duplicates.push(result)
} else {
existing.duplicates = [result]
}
} else {
results.push(result)
}
})
return results
}

function resultForPackage (configuration, tree) {
var packageAllowlist = configuration.packages || {}
var result = {
name: tree.package.name,
version: tree.version,
license: tree.package.license,
author: tree.package.author,
contributors: tree.package.contributors,
repository: tree.package.repository,
homepage: tree.package.homepage,
version: tree.package.version,
parent: tree.parent,
path: tree.path
path: tree.realpath
}

// Find and apply any manual license metadata correction.
Expand Down

0 comments on commit e79776f

Please sign in to comment.