| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- const fs = require('node:fs/promises')
- const { join } = require('node:path')
- const cacache = require('cacache')
- const pacote = require('pacote')
- const semver = require('semver')
- const npa = require('npm-package-arg')
- const jsonParse = require('json-parse-even-better-errors')
- const localeCompare = require('@isaacs/string-locale-compare')('en')
- const { log, output } = require('proc-log')
- const PkgJson = require('@npmcli/package-json')
- const abbrev = require('abbrev')
- const BaseCommand = require('../base-cmd.js')
- const searchCachePackage = async (path, parsed, cacheKeys) => {
- const searchMFH = new RegExp(`^make-fetch-happen:request-cache:.*(?<!/[@a-zA-Z]+)/${parsed.name}/-/(${parsed.name}[^/]+.tgz)$`)
- const searchPack = new RegExp(`^make-fetch-happen:request-cache:.*/${parsed.escapedName}$`)
- const results = new Set()
- cacheKeys = new Set(cacheKeys)
- for (const key of cacheKeys) {
- // match on the public key registry url format
- if (searchMFH.test(key)) {
- // extract the version from the filename
- const filename = key.match(searchMFH)[1]
- const noExt = filename.slice(0, -4)
- const noScope = `${parsed.name.split('/').pop()}-`
- const ver = noExt.slice(noScope.length)
- if (semver.satisfies(ver, parsed.rawSpec)) {
- results.add(key)
- }
- continue
- }
- // is this key a packument?
- if (!searchPack.test(key)) {
- continue
- }
- results.add(key)
- let packument, details
- try {
- details = await cacache.get(path, key)
- packument = jsonParse(details.data)
- } catch {
- // if we couldn't parse the packument, abort
- continue
- }
- if (!packument.versions || typeof packument.versions !== 'object') {
- continue
- }
- // assuming this is a packument
- for (const ver of Object.keys(packument.versions)) {
- if (semver.satisfies(ver, parsed.rawSpec)) {
- if (packument.versions[ver].dist &&
- typeof packument.versions[ver].dist === 'object' &&
- packument.versions[ver].dist.tarball !== undefined &&
- cacheKeys.has(`make-fetch-happen:request-cache:${packument.versions[ver].dist.tarball}`)
- ) {
- results.add(`make-fetch-happen:request-cache:${packument.versions[ver].dist.tarball}`)
- }
- }
- }
- }
- return results
- }
- class Cache extends BaseCommand {
- static description = 'Manipulates packages and npx cache'
- static name = 'cache'
- static params = ['cache']
- static usage = [
- 'add <package-spec>',
- 'clean [<key>]',
- 'ls [<name>@<version>]',
- 'verify',
- 'npx ls',
- 'npx rm [<key>...]',
- 'npx info <key>...',
- ]
- static async completion (opts) {
- const argv = opts.conf.argv.remain
- if (argv.length === 2) {
- return ['add', 'clean', 'verify', 'ls', 'npx']
- }
- // TODO - eventually...
- switch (argv[2]) {
- case 'verify':
- case 'clean':
- case 'add':
- case 'ls':
- return []
- }
- }
- async exec (args) {
- const cmd = args.shift()
- switch (cmd) {
- case 'rm': case 'clear': case 'clean':
- return await this.clean(args)
- case 'add':
- return await this.add(args)
- case 'verify': case 'check':
- return await this.verify()
- case 'ls':
- return await this.ls(args)
- case 'npx':
- return await this.npx(args)
- default:
- throw this.usageError()
- }
- }
- // npm cache npx
- async npx ([cmd, ...keys]) {
- switch (cmd) {
- case 'ls':
- return await this.npxLs(keys)
- case 'rm':
- return await this.npxRm(keys)
- case 'info':
- return await this.npxInfo(keys)
- default:
- throw this.usageError()
- }
- }
- // npm cache clean [spec]*
- async clean (args) {
- // this is a derived value
- const cachePath = this.npm.flatOptions.cache
- if (args.length === 0) {
- if (!this.npm.config.get('force')) {
- throw new Error(`As of npm@5, the npm cache self-heals from corruption issues by treating integrity mismatches as cache misses.
- As a result, data extracted from the cache is guaranteed to be valid.
- If you want to make sure everything is consistent, use \`npm cache verify\` instead.
- Deleting the cache can only make npm go slower, and is not likely to correct any problems you may be encountering!
- On the other hand, if you're debugging an issue with the installer, or race conditions that depend on the timing of writing to an empty cache, you can use \`npm install --cache /tmp/empty-cache\` to use a temporary cache instead of removing the actual one.
- If you're sure you want to delete the entire cache, rerun this command with --force.`)
- }
- return fs.rm(cachePath, { recursive: true, force: true })
- }
- for (const key of args) {
- let entry
- try {
- entry = await cacache.get(cachePath, key)
- } catch {
- log.warn('cache', `Not Found: ${key}`)
- break
- }
- output.standard(`Deleted: ${key}`)
- await cacache.rm.entry(cachePath, key)
- // XXX this could leave other entries without content!
- await cacache.rm.content(cachePath, entry.integrity)
- }
- }
- // npm cache add <tarball-url>...
- // npm cache add <pkg> <ver>...
- // npm cache add <tarball>...
- // npm cache add <folder>...
- async add (args) {
- log.silly('cache add', 'args', args)
- if (args.length === 0) {
- throw this.usageError('First argument to `add` is required')
- }
- await Promise.all(args.map(async spec => {
- log.silly('cache add', 'spec', spec)
- // we ask pacote for the thing, and then just throw the data away so that it tee-pipes it into the cache like it does for a normal request.
- await pacote.tarball.stream(spec, stream => {
- stream.resume()
- return stream.promise()
- }, { ...this.npm.flatOptions, _isRoot: true })
- await pacote.manifest(spec, {
- ...this.npm.flatOptions,
- fullMetadata: true,
- _isRoot: true,
- })
- }))
- }
- async verify () {
- // this is a derived value
- const cachePath = this.npm.flatOptions.cache
- const prefix = cachePath.indexOf(process.env.HOME) === 0
- ? `~${cachePath.slice(process.env.HOME.length)}`
- : cachePath
- const stats = await cacache.verify(cachePath)
- output.standard(`Cache verified and compressed (${prefix})`)
- output.standard(`Content verified: ${stats.verifiedContent} (${stats.keptSize} bytes)`)
- if (stats.badContentCount) {
- output.standard(`Corrupted content removed: ${stats.badContentCount}`)
- }
- if (stats.reclaimedCount) {
- output.standard(`Content garbage-collected: ${stats.reclaimedCount} (${stats.reclaimedSize} bytes)`)
- }
- if (stats.missingContent) {
- output.standard(`Missing content: ${stats.missingContent}`)
- }
- output.standard(`Index entries: ${stats.totalEntries}`)
- output.standard(`Finished in ${stats.runTime.total / 1000}s`)
- }
- // npm cache ls [<spec> ...]
- async ls (specs) {
- // This is a derived value
- const { cache: cachePath } = this.npm.flatOptions
- const cacheKeys = Object.keys(await cacache.ls(cachePath))
- if (specs.length > 0) {
- // get results for each package spec specified
- const results = new Set()
- for (const spec of specs) {
- const parsed = npa(spec)
- if (parsed.rawSpec !== '' && parsed.type === 'tag') {
- throw this.usageError('Cannot list cache keys for a tagged package.')
- }
- const keySet = await searchCachePackage(cachePath, parsed, cacheKeys)
- for (const key of keySet) {
- results.add(key)
- }
- }
- [...results].sort(localeCompare).forEach(key => output.standard(key))
- return
- }
- cacheKeys.sort(localeCompare).forEach(key => output.standard(key))
- }
- async #npxCache (keys = []) {
- // This is a derived value
- const { npxCache } = this.npm.flatOptions
- let dirs
- try {
- dirs = await fs.readdir(npxCache, { encoding: 'utf-8' })
- } catch {
- output.standard('npx cache does not exist')
- return
- }
- const cache = {}
- const { default: pMap } = await import('p-map')
- await pMap(dirs, async e => {
- const pkgPath = join(npxCache, e)
- cache[e] = {
- hash: e,
- path: pkgPath,
- valid: false,
- }
- try {
- const pkgJson = await PkgJson.load(pkgPath)
- cache[e].package = pkgJson.content
- cache[e].valid = true
- } catch {
- // Defaults to not valid already
- }
- }, { concurrency: 20 })
- if (!keys.length) {
- return cache
- }
- const result = {}
- const abbrevs = abbrev(Object.keys(cache))
- for (const key of keys) {
- if (!abbrevs[key]) {
- throw this.usageError(`Invalid npx key ${key}`)
- }
- result[abbrevs[key]] = cache[abbrevs[key]]
- }
- return result
- }
- async npxLs () {
- const cache = await this.#npxCache()
- for (const key in cache) {
- const { hash, valid, package: pkg } = cache[key]
- let result = `${hash}:`
- if (!valid) {
- result = `${result} (empty/invalid)`
- } else if (pkg?._npx) {
- result = `${result} ${pkg._npx.packages.join(', ')}`
- } else {
- result = `${result} (unknown)`
- }
- output.standard(result)
- }
- }
- async npxRm (keys) {
- if (!keys.length) {
- if (!this.npm.config.get('force')) {
- throw this.usageError('Please use --force to remove entire npx cache')
- }
- const { npxCache } = this.npm.flatOptions
- if (!this.npm.config.get('dry-run')) {
- return fs.rm(npxCache, { recursive: true, force: true })
- }
- }
- const cache = await this.#npxCache(keys)
- for (const key in cache) {
- const { path: cachePath } = cache[key]
- output.standard(`Removing npx key at ${cachePath}`)
- if (!this.npm.config.get('dry-run')) {
- await fs.rm(cachePath, { recursive: true })
- }
- }
- }
- async npxInfo (keys) {
- const chalk = this.npm.chalk
- if (!keys.length) {
- throw this.usageError()
- }
- const cache = await this.#npxCache(keys)
- const Arborist = require('@npmcli/arborist')
- for (const key in cache) {
- const { hash, path, package: pkg } = cache[key]
- let valid = cache[key].valid
- const results = []
- try {
- if (valid) {
- const arb = new Arborist({ path })
- const tree = await arb.loadVirtual()
- if (pkg._npx) {
- results.push('packages:')
- for (const p of pkg._npx.packages) {
- const parsed = npa(p)
- if (parsed.type === 'directory') {
- // in the tree the spec is relative, even if the dependency spec is absolute, so we can't find it by name or spec.
- results.push(`- ${chalk.cyan(p)}`)
- } else {
- results.push(`- ${chalk.cyan(p)} (${chalk.blue(tree.children.get(parsed.name).pkgid)})`)
- }
- }
- } else {
- results.push('packages: (unknown)')
- results.push(`dependencies:`)
- for (const dep in pkg.dependencies) {
- const child = tree.children.get(dep)
- if (child.isLink) {
- results.push(`- ${chalk.cyan(child.realpath)}`)
- } else {
- results.push(`- ${chalk.cyan(child.pkgid)}`)
- }
- }
- }
- }
- } catch (ex) {
- valid = false
- }
- const v = valid ? chalk.green('valid') : chalk.red('invalid')
- output.standard(`${v} npx cache entry with key ${chalk.blue(hash)}`)
- output.standard(`location: ${chalk.blue(path)}`)
- if (valid) {
- output.standard(results.join('\n'))
- }
- output.standard()
- }
- }
- }
- module.exports = Cache
|