Skip to content

Commit 247ee1d

Browse files
committed
feat(cache): add npx commands
1 parent d18d422 commit 247ee1d

File tree

5 files changed

+684
-48
lines changed

5 files changed

+684
-48
lines changed

docs/lib/content/commands/npm-cache.md

+27-32
Original file line numberDiff line numberDiff line change
@@ -10,53 +10,47 @@ description: Manipulates packages cache
1010

1111
### Description
1212

13-
Used to add, list, or clean the npm cache folder.
13+
Used to add, list, or clean the `npm cache` folder.
14+
Also used to view info about entries in the `npm exec` (aka `npx`) cache folder.
15+
16+
#### `npm cache`
1417

1518
* add:
16-
Add the specified packages to the local cache. This command is primarily
17-
intended to be used internally by npm, but it can provide a way to
18-
add data to the local installation cache explicitly.
19+
Add the specified packages to the local cache. This command is primarily intended to be used internally by npm, but it can provide a way to add data to the local installation cache explicitly.
1920

2021
* clean:
21-
Delete all data out of the cache folder. Note that this is typically
22-
unnecessary, as npm's cache is self-healing and resistant to data
23-
corruption issues.
22+
Delete a single entry or all entries out of the cache folder. Note that this is typically unnecessary, as npm's cache is self-healing and resistant to data corruption issues.
23+
24+
* ls:
25+
List given entries or all entries in the local cache.
2426

2527
* verify:
26-
Verify the contents of the cache folder, garbage collecting any unneeded
27-
data, and verifying the integrity of the cache index and all cached data.
28+
Verify the contents of the cache folder, garbage collecting any unneeded data, and verifying the integrity of the cache index and all cached data.
29+
30+
#### `npm cache npx`
31+
32+
* ls:
33+
List all entries in the npx cache.
34+
35+
* rm:
36+
Remove given entries or all entries from the npx cache.
37+
38+
* info:
39+
Get detailed information about given entries in the npx cache.
2840

2941
### Details
3042

31-
npm stores cache data in an opaque directory within the configured `cache`,
32-
named `_cacache`. This directory is a
33-
[`cacache`](http://npm.im/cacache)-based content-addressable cache that
34-
stores all http request data as well as other package-related data. This
35-
directory is primarily accessed through `pacote`, the library responsible
36-
for all package fetching as of npm@5.
43+
npm stores cache data in an opaque directory within the configured `cache`, named `_cacache`. This directory is a [`cacache`](http://npm.im/cacache)-based content-addressable cache that stores all http request data as well as other package-related data. This directory is primarily accessed through `pacote`, the library responsible for all package fetching as of npm@5.
3744

38-
All data that passes through the cache is fully verified for integrity on
39-
both insertion and extraction. Cache corruption will either trigger an
40-
error, or signal to `pacote` that the data must be refetched, which it will
41-
do automatically. For this reason, it should never be necessary to clear
42-
the cache for any reason other than reclaiming disk space, thus why `clean`
43-
now requires `--force` to run.
45+
All data that passes through the cache is fully verified for integrity on both insertion and extraction. Cache corruption will either trigger an error, or signal to `pacote` that the data must be refetched, which it will do automatically. For this reason, it should never be necessary to clear the cache for any reason other than reclaiming disk space, thus why `clean` now requires `--force` to run.
4446

45-
There is currently no method exposed through npm to inspect or directly
46-
manage the contents of this cache. In order to access it, `cacache` must be
47-
used directly.
47+
There is currently no method exposed through npm to inspect or directly manage the contents of this cache. In order to access it, `cacache` must be used directly.
4848

49-
npm will not remove data by itself: the cache will grow as new packages are
50-
installed.
49+
npm will not remove data by itself: the cache will grow as new packages are installed.
5150

5251
### A note about the cache's design
5352

54-
The npm cache is strictly a cache: it should not be relied upon as a
55-
persistent and reliable data store for package data. npm makes no guarantee
56-
that a previously-cached piece of data will be available later, and will
57-
automatically delete corrupted contents. The primary guarantee that the
58-
cache makes is that, if it does return data, that data will be exactly the
59-
data that was inserted.
53+
The npm cache is strictly a cache: it should not be relied upon as a persistent and reliable data store for package data. npm makes no guarantee that a previously-cached piece of data will be available later, and will automatically delete corrupted contents. The primary guarantee that the cache makes is that, if it does return data, that data will be exactly the data that was inserted.
6054

6155
To run an offline verification of existing cache contents, use `npm cache
6256
verify`.
@@ -74,6 +68,7 @@ verify`.
7468
* [npm install](/commands/npm-install)
7569
* [npm publish](/commands/npm-publish)
7670
* [npm pack](/commands/npm-pack)
71+
* [npm exec](/commands/npm-exec)
7772
* https://npm.im/cacache
7873
* https://npm.im/pacote
7974
* https://npm.im/@npmcli/arborist

lib/commands/cache.js

+168-14
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
const cacache = require('cacache')
2-
const pacote = require('pacote')
31
const fs = require('node:fs/promises')
42
const { join } = require('node:path')
3+
const cacache = require('cacache')
4+
const pacote = require('pacote')
55
const semver = require('semver')
6-
const BaseCommand = require('../base-cmd.js')
76
const npa = require('npm-package-arg')
87
const jsonParse = require('json-parse-even-better-errors')
98
const localeCompare = require('@isaacs/string-locale-compare')('en')
109
const { log, output } = require('proc-log')
10+
const PkgJson = require('@npmcli/package-json')
11+
const BaseCommand = require('../base-cmd.js')
12+
const abbrev = require('abbrev')
1113

1214
const searchCachePackage = async (path, parsed, cacheKeys) => {
1315
const searchMFH = new RegExp(`^make-fetch-happen:request-cache:.*(?<!/[@a-zA-Z]+)/${parsed.name}/-/(${parsed.name}[^/]+.tgz)$`)
@@ -62,20 +64,23 @@ const searchCachePackage = async (path, parsed, cacheKeys) => {
6264
}
6365

6466
class Cache extends BaseCommand {
65-
static description = 'Manipulates packages cache'
67+
static description = 'Manipulates packages and npx cache'
6668
static name = 'cache'
6769
static params = ['cache']
6870
static usage = [
6971
'add <package-spec>',
7072
'clean [<key>]',
7173
'ls [<name>@<version>]',
7274
'verify',
75+
'npx ls',
76+
'npx rm [<key>...]',
77+
'npx info <key>...',
7378
]
7479

7580
static async completion (opts) {
7681
const argv = opts.conf.argv.remain
7782
if (argv.length === 2) {
78-
return ['add', 'clean', 'verify', 'ls']
83+
return ['add', 'clean', 'verify', 'ls', 'npx']
7984
}
8085

8186
// TODO - eventually...
@@ -99,14 +104,31 @@ class Cache extends BaseCommand {
99104
return await this.verify()
100105
case 'ls':
101106
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)
102123
default:
103124
throw this.usageError()
104125
}
105126
}
106127

107-
// npm cache clean [pkg]*
128+
// npm cache clean [spec]*
108129
async clean (args) {
109-
const cachePath = join(this.npm.cache, '_cacache')
130+
// this is a derived value
131+
const cachePath = this.npm.flatOptions.cache
110132
if (args.length === 0) {
111133
if (!this.npm.config.get('force')) {
112134
throw new Error(`As of npm@5, the npm cache self-heals from corruption issues
@@ -169,11 +191,12 @@ class Cache extends BaseCommand {
169191
}
170192

171193
async verify () {
172-
const cache = join(this.npm.cache, '_cacache')
173-
const prefix = cache.indexOf(process.env.HOME) === 0
174-
? `~${cache.slice(process.env.HOME.length)}`
175-
: cache
176-
const stats = await cacache.verify(cache)
194+
// this is a derived value
195+
const cachePath = this.npm.flatOptions.cache
196+
const prefix = cachePath.indexOf(process.env.HOME) === 0
197+
? `~${cachePath.slice(process.env.HOME.length)}`
198+
: cachePath
199+
const stats = await cacache.verify(cachePath)
177200
output.standard(`Cache verified and compressed (${prefix})`)
178201
output.standard(`Content verified: ${stats.verifiedContent} (${stats.keptSize} bytes)`)
179202
if (stats.badContentCount) {
@@ -189,9 +212,10 @@ class Cache extends BaseCommand {
189212
output.standard(`Finished in ${stats.runTime.total / 1000}s`)
190213
}
191214

192-
// npm cache ls [--package <spec> ...]
215+
// npm cache ls [<spec> ...]
193216
async ls (specs) {
194-
const cachePath = join(this.npm.cache, '_cacache')
217+
// This is a derived value
218+
const { cache: cachePath } = this.npm.flatOptions
195219
const cacheKeys = Object.keys(await cacache.ls(cachePath))
196220
if (specs.length > 0) {
197221
// get results for each package spec specified
@@ -211,6 +235,136 @@ class Cache extends BaseCommand {
211235
}
212236
cacheKeys.sort(localeCompare).forEach(key => output.standard(key))
213237
}
238+
239+
async #npxCache (keys = []) {
240+
// This is a derived value
241+
const { npxCache } = this.npm.flatOptions
242+
let dirs
243+
try {
244+
dirs = await fs.readdir(npxCache, { encoding: 'utf-8' })
245+
} catch {
246+
output.standard('npx cache does not exist')
247+
return
248+
}
249+
const cache = {}
250+
const { default: pMap } = await import('p-map')
251+
await pMap(dirs, async e => {
252+
const pkgPath = join(npxCache, e)
253+
cache[e] = {
254+
hash: e,
255+
path: pkgPath,
256+
valid: false,
257+
}
258+
try {
259+
const pkgJson = await PkgJson.load(pkgPath)
260+
cache[e].package = pkgJson.content
261+
cache[e].valid = true
262+
} catch {
263+
// Defaults to not valid already
264+
}
265+
}, { concurrency: 20 })
266+
if (!keys.length) {
267+
return cache
268+
}
269+
const result = {}
270+
const abbrevs = abbrev(Object.keys(cache))
271+
for (const key of keys) {
272+
if (!abbrevs[key]) {
273+
throw this.usageError(`Invalid npx key ${key}`)
274+
}
275+
result[abbrevs[key]] = cache[abbrevs[key]]
276+
}
277+
return result
278+
}
279+
280+
async npxLs () {
281+
const cache = await this.#npxCache()
282+
for (const key in cache) {
283+
const { hash, valid, package: pkg } = cache[key]
284+
let result = `${hash}:`
285+
if (!valid) {
286+
result = `${result} (empty/invalid)`
287+
} else if (pkg?._npx) {
288+
result = `${result} ${pkg._npx.packages.join(', ')}`
289+
} else {
290+
result = `${result} (unknown)`
291+
}
292+
output.standard(result)
293+
}
294+
}
295+
296+
async npxRm (keys) {
297+
if (!keys.length) {
298+
if (!this.npm.config.get('force')) {
299+
throw this.usageError('Please use --force to remove entire npx cache')
300+
}
301+
const { npxCache } = this.npm.flatOptions
302+
if (!this.npm.config.get('dry-run')) {
303+
return fs.rm(npxCache, { recursive: true, force: true })
304+
}
305+
}
306+
307+
const cache = await this.#npxCache(keys)
308+
for (const key in cache) {
309+
const { path: cachePath } = cache[key]
310+
output.standard(`Removing npx key at ${cachePath}`)
311+
if (!this.npm.config.get('dry-run')) {
312+
await fs.rm(cachePath, { recursive: true })
313+
}
314+
}
315+
}
316+
317+
async npxInfo (keys) {
318+
const chalk = this.npm.chalk
319+
if (!keys.length) {
320+
throw this.usageError()
321+
}
322+
const cache = await this.#npxCache(keys)
323+
const Arborist = require('@npmcli/arborist')
324+
for (const key in cache) {
325+
const { hash, path, package: pkg } = cache[key]
326+
let valid = cache[key].valid
327+
const results = []
328+
try {
329+
if (valid) {
330+
const arb = new Arborist({ path })
331+
const tree = await arb.loadVirtual()
332+
if (pkg._npx) {
333+
results.push('packages:')
334+
for (const p of pkg._npx.packages) {
335+
const parsed = npa(p)
336+
if (parsed.type === 'directory') {
337+
// in the tree the spec is relative, even if the dependency spec is absolute, so we can't find it by name or spec.
338+
results.push(`- ${chalk.cyan(p)}`)
339+
} else {
340+
results.push(`- ${chalk.cyan(p)} (${chalk.blue(tree.children.get(parsed.name).pkgid)})`)
341+
}
342+
}
343+
} else {
344+
results.push('packages: (unknown)')
345+
results.push(`dependencies:`)
346+
for (const dep in pkg.dependencies) {
347+
const child = tree.children.get(dep)
348+
if (child.isLink) {
349+
results.push(`- ${chalk.cyan(child.realpath)}`)
350+
} else {
351+
results.push(`- ${chalk.cyan(child.pkgid)}`)
352+
}
353+
}
354+
}
355+
}
356+
} catch (ex) {
357+
valid = false
358+
}
359+
const v = valid ? chalk.green('valid') : chalk.red('invalid')
360+
output.standard(`${v} npx cache entry with key ${chalk.blue(hash)}`)
361+
output.standard(`location: ${chalk.blue(path)}`)
362+
if (valid) {
363+
output.standard(results.join('\n'))
364+
}
365+
output.standard('')
366+
}
367+
}
214368
}
215369

216370
module.exports = Cache

0 commit comments

Comments
 (0)