1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
|
const Arborist = require('@npmcli/arborist')
const { readFileSync } = require('fs')
const { join } = require('path')
const log = require('proc-log')
const { run, CWD, pkg, fs } = require('./util.js')
// Generates our dependency graph documents in DEPENDENCIES.md.
// To re-create npm-cli-repos.txt run:
// npx -p @npmcli/stafftools gh repos --json | json -a name | sort > scripts/npm-cli-repos.txt
const repos = readFileSync(join(CWD, 'scripts', 'npm-cli-repos.txt'), 'utf-8').trim().split('\n')
// these have a different package name than the repo name, and are ours.
const aliases = {
semver: 'node-semver',
abbrev: 'abbrev-js',
}
// These are entries in npm-cli-repos.txt that correlate to namespaced repos.
// If we see a bare package with just this name, it's NOT ours
const namespaced = [
'arborist',
'config',
'disparity-colors',
'eslint-config',
'exec',
'fs',
'git',
'installed-package-contents',
'lint',
'map-workspaces',
'metavuln-calculator',
'move-file',
'name-from-folder',
'node-gyp',
'package-json',
'promise-spawn',
'run-script',
'template-oss',
]
function isOurs (name) {
if (name.startsWith('libnpm')) {
return true
}
if (name.startsWith('@npmcli')) {
return true
}
if (aliases[name]) {
return true
}
// this will prevent e.g. `fs` from being mistaken as ours
if (namespaced.includes(name)) {
return false
}
if (repos.includes(name)) {
return true
}
return false
}
function escapeName (name) {
if (name.startsWith('@')) {
return `${stripName(name)}["${name}"]`
}
return name
}
function stripName (name) {
if (name.startsWith('@')) {
const parts = name.slice(1).split('/')
return `${parts[0]}-${parts[1]}`
}
return name
}
const main = async function () {
// add all of the cli's public workspaces as package names
for (const { name, pkg: ws } of await pkg.mapWorkspaces()) {
if (!ws.private) {
repos.push(name)
}
}
const arborist = new Arborist({ prefix: CWD, path: CWD })
const tree = await arborist.loadVirtual({ path: CWD, name: 'npm' })
tree.name = 'npm'
const [annotationsOurs, heirarchyOurs] = walk(tree, true)
const [annotationsAll] = walk(tree, false)
const out = [
'# npm dependencies',
'',
'## `github.com/npm/` only',
'```mermaid',
'graph LR;',
...annotationsOurs.sort(),
'```',
'',
'## all dependencies',
'```mermaid',
'graph LR;',
...annotationsAll.sort(),
'```',
'',
'## npm dependency heirarchy',
'',
'These are the groups of dependencies in npm that depend on each other.',
'Each group depends on packages lower down the chain, nothing depends on',
'packages higher up the chain.',
'',
` - ${heirarchyOurs.reverse().join('\n - ')}`,
]
return fs.writeFile(join(CWD, 'DEPENDENCIES.md'), out.join('\n'))
}
const walk = function (tree, onlyOurs) {
const annotations = [] // mermaid dependency annotations
const dependedBy = {}
iterate(tree, dependedBy, annotations, onlyOurs)
const allDeps = new Set(Object.keys(dependedBy))
const foundDeps = new Set()
const heirarchy = []
if (onlyOurs) {
while (allDeps.size) {
log.silly('SIZE', allDeps.size)
const level = []
for (const dep of allDeps) {
log.silly(dep, '::', [...dependedBy[dep]].join(', '))
log.silly('-'.repeat(80))
if (!dependedBy[dep].size) {
level.push(dep)
foundDeps.add(dep)
}
}
log.silly('LEVEL', level.length)
log.silly('FOUND', foundDeps.size)
for (const dep of allDeps) {
for (const found of foundDeps) {
allDeps.delete(found)
dependedBy[dep].delete(found)
}
}
log.silly('SIZE', allDeps.size)
if (!level.length) {
const remaining = `Remaining deps: ${[...allDeps.keys()]}`
throw new Error(`Would do an infinite loop here, need to debug. ${remaining}`)
}
heirarchy.push(level.join(', '))
log.silly('HIEARARCHY', heirarchy.length)
log.silly('='.repeat(80))
}
}
return [annotations, heirarchy]
}
const iterate = function (node, dependedBy, annotations, onlyOurs) {
if (!dependedBy[node.packageName]) {
dependedBy[node.packageName] = new Set()
}
for (const [name, edge] of node.edgesOut) {
if (
(!onlyOurs || isOurs(name)) && !node.dev
) {
if (!dependedBy[node.packageName].has(edge.name)) {
dependedBy[node.packageName].add(edge.name)
annotations.push(` ${stripName(node.packageName)}-->${escapeName(edge.name)};`)
if (edge.to) {
iterate(edge.to.target, dependedBy, annotations, onlyOurs)
}
}
}
}
}
run(main)
|