Skip to content

Commit ff34d6c

Browse files
fritzywraithgar
authored andcommitted
feat(cache): initial implementation of ls and rm
PR-URL: #3592 Credit: @fritzy Close: #3592 Reviewed-by: @nlf
1 parent 8183976 commit ff34d6c

File tree

4 files changed

+382
-31
lines changed

4 files changed

+382
-31
lines changed

lib/cache.js

+107-21
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,59 @@ const log = require('npmlog')
44
const pacote = require('pacote')
55
const path = require('path')
66
const rimraf = promisify(require('rimraf'))
7+
const semver = require('semver')
78
const BaseCommand = require('./base-command.js')
9+
const npa = require('npm-package-arg')
10+
const jsonParse = require('json-parse-even-better-errors')
11+
12+
const searchCachePackage = async (path, spec, cacheKeys) => {
13+
const parsed = npa(spec)
14+
if (parsed.rawSpec !== '' && parsed.type === 'tag')
15+
throw new Error(`Cannot list cache keys for a tagged package.`)
16+
const searchMFH = new RegExp(`^make-fetch-happen:request-cache:.*(?<!/[@a-zA-Z]+)/${parsed.name}/-/(${parsed.name}[^/]+.tgz)$`)
17+
const searchPack = new RegExp(`^make-fetch-happen:request-cache:.*/${parsed.escapedName}$`)
18+
const results = new Set()
19+
cacheKeys = new Set(cacheKeys)
20+
for (const key of cacheKeys) {
21+
// match on the public key registry url format
22+
if (searchMFH.test(key)) {
23+
// extract the version from the filename
24+
const filename = key.match(searchMFH)[1]
25+
const noExt = filename.slice(0, -4)
26+
const noScope = `${parsed.name.split('/').pop()}-`
27+
const ver = noExt.slice(noScope.length)
28+
if (semver.satisfies(ver, parsed.rawSpec))
29+
results.add(key)
30+
continue
31+
}
32+
// is this key a packument?
33+
if (!searchPack.test(key))
34+
continue
35+
36+
results.add(key)
37+
let packument, details
38+
try {
39+
details = await cacache.get(path, key)
40+
packument = jsonParse(details.data)
41+
} catch (_) {
42+
// if we couldn't parse the packument, abort
43+
continue
44+
}
45+
if (!packument.versions || typeof packument.versions !== 'object')
46+
continue
47+
// assuming this is a packument
48+
for (const ver of Object.keys(packument.versions)) {
49+
if (semver.satisfies(ver, parsed.rawSpec)) {
50+
if (packument.versions[ver].dist
51+
&& typeof packument.versions[ver].dist === 'object'
52+
&& packument.versions[ver].dist.tarball !== undefined
53+
&& cacheKeys.has(`make-fetch-happen:request-cache:${packument.versions[ver].dist.tarball}`))
54+
results.add(`make-fetch-happen:request-cache:${packument.versions[ver].dist.tarball}`)
55+
}
56+
}
57+
}
58+
return results
59+
}
860

961
class Cache extends BaseCommand {
1062
static get description () {
@@ -29,21 +81,24 @@ class Cache extends BaseCommand {
2981
'add <tarball url>',
3082
'add <git url>',
3183
'add <name>@<version>',
32-
'clean',
84+
'clean [<key>]',
85+
'ls [<name>@<version>]',
3386
'verify',
3487
]
3588
}
3689

3790
async completion (opts) {
3891
const argv = opts.conf.argv.remain
3992
if (argv.length === 2)
40-
return ['add', 'clean', 'verify']
93+
return ['add', 'clean', 'verify', 'ls', 'delete']
4194

4295
// TODO - eventually...
4396
switch (argv[2]) {
4497
case 'verify':
4598
case 'clean':
4699
case 'add':
100+
case 'ls':
101+
case 'delete':
47102
return []
48103
}
49104
}
@@ -61,34 +116,47 @@ class Cache extends BaseCommand {
61116
return await this.add(args)
62117
case 'verify': case 'check':
63118
return await this.verify()
119+
case 'ls':
120+
return await this.ls(args)
64121
default:
65122
throw Object.assign(new Error(this.usage), { code: 'EUSAGE' })
66123
}
67124
}
68125

69126
// npm cache clean [pkg]*
70127
async clean (args) {
71-
if (args.length)
72-
throw new Error('npm cache clear does not accept arguments')
73-
74128
const cachePath = path.join(this.npm.cache, '_cacache')
75-
if (!this.npm.config.get('force')) {
76-
throw new Error(`As of npm@5, the npm cache self-heals from corruption issues
77-
by treating integrity mismatches as cache misses. As a result,
78-
data extracted from the cache is guaranteed to be valid. If you
79-
want to make sure everything is consistent, use \`npm cache verify\`
80-
instead. Deleting the cache can only make npm go slower, and is
81-
not likely to correct any problems you may be encountering!
82-
83-
On the other hand, if you're debugging an issue with the installer,
84-
or race conditions that depend on the timing of writing to an empty
85-
cache, you can use \`npm install --cache /tmp/empty-cache\` to use a
86-
temporary cache instead of nuking the actual one.
87-
88-
If you're sure you want to delete the entire cache, rerun this command
89-
with --force.`)
129+
if (args.length === 0) {
130+
if (!this.npm.config.get('force')) {
131+
throw new Error(`As of npm@5, the npm cache self-heals from corruption issues
132+
by treating integrity mismatches as cache misses. As a result,
133+
data extracted from the cache is guaranteed to be valid. If you
134+
want to make sure everything is consistent, use \`npm cache verify\`
135+
instead. Deleting the cache can only make npm go slower, and is
136+
not likely to correct any problems you may be encountering!
137+
138+
On the other hand, if you're debugging an issue with the installer,
139+
or race conditions that depend on the timing of writing to an empty
140+
cache, you can use \`npm install --cache /tmp/empty-cache\` to use a
141+
temporary cache instead of nuking the actual one.
142+
143+
If you're sure you want to delete the entire cache, rerun this command
144+
with --force.`)
145+
}
146+
return rimraf(cachePath)
147+
}
148+
for (const key of args) {
149+
let entry
150+
try {
151+
entry = await cacache.get(cachePath, key)
152+
} catch (err) {
153+
this.npm.log.warn(`Not Found: ${key}`)
154+
break
155+
}
156+
this.npm.output(`Deleted: ${key}`)
157+
await cacache.rm.entry(cachePath, key)
158+
await cacache.rm.content(cachePath, entry.integrity)
90159
}
91-
return rimraf(cachePath)
92160
}
93161

94162
// npm cache add <tarball-url>...
@@ -131,6 +199,24 @@ with --force.`)
131199
this.npm.output(`Index entries: ${stats.totalEntries}`)
132200
this.npm.output(`Finished in ${stats.runTime.total / 1000}s`)
133201
}
202+
203+
// npm cache ls [--package <spec> ...]
204+
async ls (specs) {
205+
const cachePath = path.join(this.npm.cache, '_cacache')
206+
const cacheKeys = Object.keys(await cacache.ls(cachePath))
207+
if (specs.length > 0) {
208+
// get results for each package spec specified
209+
const results = new Set()
210+
for (const spec of specs) {
211+
const keySet = await searchCachePackage(cachePath, spec, cacheKeys)
212+
for (const key of keySet)
213+
results.add(key)
214+
}
215+
[...results].sort((a, b) => a.localeCompare(b, 'en')).forEach(key => this.npm.output(key))
216+
return
217+
}
218+
cacheKeys.sort((a, b) => a.localeCompare(b, 'en')).forEach(key => this.npm.output(key))
219+
}
134220
}
135221

136222
module.exports = Cache

tap-snapshots/test/lib/load-all-commands.js.test.cjs

+2-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ npm cache add <folder>
102102
npm cache add <tarball url>
103103
npm cache add <git url>
104104
npm cache add <name>@<version>
105-
npm cache clean
105+
npm cache clean [<key>]
106+
npm cache ls [<name>@<version>]
106107
npm cache verify
107108
108109
Options:

tap-snapshots/test/lib/utils/npm-usage.js.test.cjs

+2-1
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,8 @@ All commands:
251251
npm cache add <tarball url>
252252
npm cache add <git url>
253253
npm cache add <name>@<version>
254-
npm cache clean
254+
npm cache clean [<key>]
255+
npm cache ls [<name>@<version>]
255256
npm cache verify
256257
257258
Options:

0 commit comments

Comments
 (0)