Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
cache.js364 linesDownload Raw Back to commands
1const fs = require('node:fs/promises')
2const { join } = require('node:path')
3const cacache = require('cacache')
4const pacote = require('pacote')
5const semver = require('semver')
6const npa = require('npm-package-arg')
7const jsonParse = require('json-parse-even-better-errors')
8const localeCompare = require('@isaacs/string-locale-compare')('en')
9const { log, output } = require('proc-log')
10const PkgJson = require('@npmcli/package-json')
11const abbrev = require('abbrev')
12const BaseCommand = require('../base-cmd.js')
13
14const searchCachePackage = async (path, parsed, cacheKeys) => {
15  const searchMFH = new RegExp(`^make-fetch-happen:request-cache:.*(?<!/[@a-zA-Z]+)/${parsed.name}/-/(${parsed.name}[^/]+.tgz)$`)
16  const searchPack = new RegExp(`^make-fetch-happen:request-cache:.*/${parsed.escapedName}$`)
17  const results = new Set()
18  cacheKeys = new Set(cacheKeys)
19  for (const key of cacheKeys) {
20    // match on the public key registry url format
21    if (searchMFH.test(key)) {
22      // extract the version from the filename
23      const filename = key.match(searchMFH)[1]
24      const noExt = filename.slice(0, -4)
25      const noScope = `${parsed.name.split('/').pop()}-`
26      const ver = noExt.slice(noScope.length)
27      if (semver.satisfies(ver, parsed.rawSpec)) {
28        results.add(key)
29      }
30      continue
31    }
32    // is this key a packument?
33    if (!searchPack.test(key)) {
34      continue
35    }
36
37    results.add(key)
38    let packument, details
39    try {
40      details = await cacache.get(path, key)
41      packument = jsonParse(details.data)
42    } catch {
43      // if we couldn't parse the packument, abort
44      continue
45    }
46    if (!packument.versions || typeof packument.versions !== 'object') {
47      continue
48    }
49
50    // assuming this is a packument
51    for (const ver of Object.keys(packument.versions)) {
52      if (semver.satisfies(ver, parsed.rawSpec)) {
53        if (packument.versions[ver].dist &&
54          typeof packument.versions[ver].dist === 'object' &&
55          packument.versions[ver].dist.tarball !== undefined &&
56          cacheKeys.has(`make-fetch-happen:request-cache:${packument.versions[ver].dist.tarball}`)
57        ) {
58          results.add(`make-fetch-happen:request-cache:${packument.versions[ver].dist.tarball}`)
59        }
60      }
61    }
62  }
63  return results
64}
65
66class Cache extends BaseCommand {
67  static description = 'Manipulates packages and npx cache'
68  static name = 'cache'
69  static params = ['cache']
70  static usage = [
71    'add <package-spec>',
72    'clean [<key>]',
73    'ls [<name>@<version>]',
74    'verify',
75    'npx ls',
76    'npx rm [<key>...]',
77    'npx info <key>...',
78  ]
79
80  static async completion (opts) {
81    const argv = opts.conf.argv.remain
82    if (argv.length === 2) {
83      return ['add', 'clean', 'verify', 'ls', 'npx']
84    }
85
86    // TODO - eventually...
87    switch (argv[2]) {
88      case 'verify':
89      case 'clean':
90      case 'add':
91      case 'ls':
92        return []
93    }
94  }
95
96  async exec (args) {
97    const cmd = args.shift()
98    switch (cmd) {
99      case 'rm': case 'clear': case 'clean':
100        return await this.clean(args)
101      case 'add':
102        return await this.add(args)
103      case 'verify': case 'check':
104        return await this.verify()
105      case 'ls':
106        return await this.ls(args)
107      case 'npx':
108        return await this.npx(args)
109      default:
110        throw this.usageError()
111    }
112  }
113
114  // npm cache npx
115  async npx ([cmd, ...keys]) {
116    switch (cmd) {
117      case 'ls':
118        return await this.npxLs(keys)
119      case 'rm':
120        return await this.npxRm(keys)
121      case 'info':
122        return await this.npxInfo(keys)
123      default:
124        throw this.usageError()
125    }
126  }
127
128  // npm cache clean [spec]*
129  async clean (args) {
130    // this is a derived value
131    const cachePath = this.npm.flatOptions.cache
132    if (args.length === 0) {
133      if (!this.npm.config.get('force')) {
134        throw new Error(`As of npm@5, the npm cache self-heals from corruption issues by treating integrity mismatches as cache misses.
135As a result, data extracted from the cache is guaranteed to be valid.
136If you want to make sure everything is consistent, use \`npm cache verify\` instead.
137Deleting the cache can only make npm go slower, and is not likely to correct any problems you may be encountering!
138
139On 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.
140
141If you're sure you want to delete the entire cache, rerun this command with --force.`)
142      }
143      return fs.rm(cachePath, { recursive: true, force: true })
144    }
145    for (const key of args) {
146      let entry
147      try {
148        entry = await cacache.get(cachePath, key)
149      } catch {
150        log.warn('cache', `Not Found: ${key}`)
151        break
152      }
153      output.standard(`Deleted: ${key}`)
154      await cacache.rm.entry(cachePath, key)
155      // XXX this could leave other entries without content!
156      await cacache.rm.content(cachePath, entry.integrity)
157    }
158  }
159
160  // npm cache add <tarball-url>...
161  // npm cache add <pkg> <ver>...
162  // npm cache add <tarball>...
163  // npm cache add <folder>...
164  async add (args) {
165    log.silly('cache add', 'args', args)
166    if (args.length === 0) {
167      throw this.usageError('First argument to `add` is required')
168    }
169
170    await Promise.all(args.map(async spec => {
171      log.silly('cache add', 'spec', spec)
172      // 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.
173      await pacote.tarball.stream(spec, stream => {
174        stream.resume()
175        return stream.promise()
176      }, { ...this.npm.flatOptions, _isRoot: true })
177
178      await pacote.manifest(spec, {
179        ...this.npm.flatOptions,
180        fullMetadata: true,
181        _isRoot: true,
182      })
183    }))
184  }
185
186  async verify () {
187    // this is a derived value
188    const cachePath = this.npm.flatOptions.cache
189    const prefix = cachePath.indexOf(process.env.HOME) === 0
190      ? `~${cachePath.slice(process.env.HOME.length)}`
191      : cachePath
192    const stats = await cacache.verify(cachePath)
193    output.standard(`Cache verified and compressed (${prefix})`)
194    output.standard(`Content verified: ${stats.verifiedContent} (${stats.keptSize} bytes)`)
195    if (stats.badContentCount) {
196      output.standard(`Corrupted content removed: ${stats.badContentCount}`)
197    }
198    if (stats.reclaimedCount) {
199      output.standard(`Content garbage-collected: ${stats.reclaimedCount} (${stats.reclaimedSize} bytes)`)
200    }
201    if (stats.missingContent) {
202      output.standard(`Missing content: ${stats.missingContent}`)
203    }
204    output.standard(`Index entries: ${stats.totalEntries}`)
205    output.standard(`Finished in ${stats.runTime.total / 1000}s`)
206  }
207
208  // npm cache ls [<spec> ...]
209  async ls (specs) {
210    // This is a derived value
211    const { cache: cachePath } = this.npm.flatOptions
212    const cacheKeys = Object.keys(await cacache.ls(cachePath))
213    if (specs.length > 0) {
214      // get results for each package spec specified
215      const results = new Set()
216      for (const spec of specs) {
217        const parsed = npa(spec)
218        if (parsed.rawSpec !== '' && parsed.type === 'tag') {
219          throw this.usageError('Cannot list cache keys for a tagged package.')
220        }
221        const keySet = await searchCachePackage(cachePath, parsed, cacheKeys)
222        for (const key of keySet) {
223          results.add(key)
224        }
225      }
226      [...results].sort(localeCompare).forEach(key => output.standard(key))
227      return
228    }
229    cacheKeys.sort(localeCompare).forEach(key => output.standard(key))
230  }
231
232  async #npxCache (keys = []) {
233    // This is a derived value
234    const { npxCache } = this.npm.flatOptions
235    let dirs
236    try {
237      dirs = await fs.readdir(npxCache, { encoding: 'utf-8' })
238    } catch {
239      output.standard('npx cache does not exist')
240      return
241    }
242    const cache = {}
243    const { default: pMap } = await import('p-map')
244    await pMap(dirs, async e => {
245      const pkgPath = join(npxCache, e)
246      cache[e] = {
247        hash: e,
248        path: pkgPath,
249        valid: false,
250      }
251      try {
252        const pkgJson = await PkgJson.load(pkgPath)
253        cache[e].package = pkgJson.content
254        cache[e].valid = true
255      } catch {
256        // Defaults to not valid already
257      }
258    }, { concurrency: 20 })
259    if (!keys.length) {
260      return cache
261    }
262    const result = {}
263    const abbrevs = abbrev(Object.keys(cache))
264    for (const key of keys) {
265      if (!abbrevs[key]) {
266        throw this.usageError(`Invalid npx key ${key}`)
267      }
268      result[abbrevs[key]] = cache[abbrevs[key]]
269    }
270    return result
271  }
272
273  async npxLs () {
274    const cache = await this.#npxCache()
275    for (const key in cache) {
276      const { hash, valid, package: pkg } = cache[key]
277      let result = `${hash}:`
278      if (!valid) {
279        result = `${result} (empty/invalid)`
280      } else if (pkg?._npx) {
281        result = `${result} ${pkg._npx.packages.join(', ')}`
282      } else {
283        result = `${result} (unknown)`
284      }
285      output.standard(result)
286    }
287  }
288
289  async npxRm (keys) {
290    if (!keys.length) {
291      if (!this.npm.config.get('force')) {
292        throw this.usageError('Please use --force to remove entire npx cache')
293      }
294      const { npxCache } = this.npm.flatOptions
295      if (!this.npm.config.get('dry-run')) {
296        return fs.rm(npxCache, { recursive: true, force: true })
297      }
298    }
299
300    const cache = await this.#npxCache(keys)
301    for (const key in cache) {
302      const { path: cachePath } = cache[key]
303      output.standard(`Removing npx key at ${cachePath}`)
304      if (!this.npm.config.get('dry-run')) {
305        await fs.rm(cachePath, { recursive: true })
306      }
307    }
308  }
309
310  async npxInfo (keys) {
311    const chalk = this.npm.chalk
312    if (!keys.length) {
313      throw this.usageError()
314    }
315    const cache = await this.#npxCache(keys)
316    const Arborist = require('@npmcli/arborist')
317    for (const key in cache) {
318      const { hash, path, package: pkg } = cache[key]
319      let valid = cache[key].valid
320      const results = []
321      try {
322        if (valid) {
323          const arb = new Arborist({ path })
324          const tree = await arb.loadVirtual()
325          if (pkg._npx) {
326            results.push('packages:')
327            for (const p of pkg._npx.packages) {
328              const parsed = npa(p)
329              if (parsed.type === 'directory') {
330                // in the tree the spec is relative, even if the dependency spec is absolute, so we can't find it by name or spec.
331                results.push(`- ${chalk.cyan(p)}`)
332              } else {
333                results.push(`- ${chalk.cyan(p)} (${chalk.blue(tree.children.get(parsed.name).pkgid)})`)
334              }
335            }
336          } else {
337            results.push('packages: (unknown)')
338            results.push(`dependencies:`)
339            for (const dep in pkg.dependencies) {
340              const child = tree.children.get(dep)
341              if (child.isLink) {
342                results.push(`- ${chalk.cyan(child.realpath)}`)
343              } else {
344                results.push(`- ${chalk.cyan(child.pkgid)}`)
345              }
346            }
347          }
348        }
349      } catch (ex) {
350        valid = false
351      }
352      const v = valid ? chalk.green('valid') : chalk.red('invalid')
353      output.standard(`${v} npx cache entry with key ${chalk.blue(hash)}`)
354      output.standard(`location: ${chalk.blue(path)}`)
355      if (valid) {
356        output.standard(results.join('\n'))
357      }
358      output.standard()
359    }
360  }
361}
362
363module.exports = Cache
364 
codekingpro/portable-devtools · Team Ai