-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
info.ts
249 lines (209 loc) Β· 9.07 KB
/
info.ts
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import * as npm from '@npm/types';
import {BaseCommand} from '@yarnpkg/cli';
import {Project, Configuration, structUtils, Descriptor, formatUtils} from '@yarnpkg/core';
import {StreamReport, MessageName, semverUtils} from '@yarnpkg/core';
import {Filename, npath, ppath} from '@yarnpkg/fslib';
import {npmHttpUtils} from '@yarnpkg/plugin-npm';
import {Command, Option, Usage, UsageError} from 'clipanion';
import semver from 'semver';
import {inspect} from 'util';
declare module '@npm/types' {
interface Packument {
users: Array<Record<string, boolean>>;
}
}
/**
* The combined type of `Packument` (without the `versions` field) and `PackumentVersion`
*/
type CombinedPackument = Omit<npm.Packument, 'versions'> & npm.PackumentVersion;
/**
* `CombinedPackument` with a `versions` field that is an array of tags
*/
interface PackageInformation extends CombinedPackument {
versions: Array<string>;
}
// eslint-disable-next-line arca/no-default-export
export default class NpmInfoCommand extends BaseCommand {
static paths = [
[`npm`, `info`],
];
static usage: Usage = Command.Usage({
category: `Npm-related commands`,
description: `show information about a package`,
details: `
This command fetches information about a package from the npm registry and prints it in a tree format.
The package does not have to be installed locally, but needs to have been published (in particular, local changes will be ignored even for workspaces).
Append \`@<range>\` to the package argument to provide information specific to the latest version that satisfies the range or to the corresponding tagged version. If the range is invalid or if there is no version satisfying the range, the command will print a warning and fall back to the latest version.
If the \`-f,--fields\` option is set, it's a comma-separated list of fields which will be used to only display part of the package information.
By default, this command won't return the \`dist\`, \`readme\`, and \`users\` fields, since they are often very long. To explicitly request those fields, explicitly list them with the \`--fields\` flag or request the output in JSON mode.
`,
examples: [[
`Show all available information about react (except the \`dist\`, \`readme\`, and \`users\` fields)`,
`yarn npm info react`,
], [
`Show all available information about react as valid JSON (including the \`dist\`, \`readme\`, and \`users\` fields)`,
`yarn npm info react --json`,
], [
`Show all available information about react@16.12.0`,
`yarn npm info react@16.12.0`,
], [
`Show all available information about react@next`,
`yarn npm info react@next`,
], [
`Show the description of react`,
`yarn npm info react --fields description`,
], [
`Show all available versions of react`,
`yarn npm info react --fields versions`,
], [
`Show the readme of react`,
`yarn npm info react --fields readme`,
], [
`Show a few fields of react`,
`yarn npm info react --fields homepage,repository`,
]],
});
fields = Option.String(`-f,--fields`, {
description: `A comma-separated list of manifest fields that should be displayed`,
});
json = Option.Boolean(`--json`, false, {
description: `Format the output as an NDJSON stream`,
});
packages = Option.Rest();
async execute() {
const configuration = await Configuration.find(this.context.cwd, this.context.plugins);
const {project} = await Project.find(configuration, this.context.cwd);
const fields = typeof this.fields !== `undefined`
? new Set([`name`, ...this.fields.split(/\s*,\s*/)])
: null;
const infos: Array<PackageInformation> = [];
let leadWithSeparator = false;
const report = await StreamReport.start({
configuration,
includeFooter: false,
json: this.json,
stdout: this.context.stdout,
}, async report => {
for (const identStr of this.packages) {
let descriptor: Descriptor;
if (identStr === `.`) {
const workspace = project.topLevelWorkspace;
if (!workspace.manifest.name)
throw new UsageError(`Missing ${formatUtils.pretty(configuration, `name`, formatUtils.Type.CODE)} field in ${npath.fromPortablePath(ppath.join(workspace.cwd, Filename.manifest))}`);
descriptor = structUtils.makeDescriptor(workspace.manifest.name, `unknown`);
} else {
descriptor = structUtils.parseDescriptor(identStr);
}
const identUrl = npmHttpUtils.getIdentUrl(descriptor);
const result = clean(await npmHttpUtils.get(identUrl, {
configuration,
ident: descriptor,
jsonResponse: true,
customErrorMessage: npmHttpUtils.customPackageError,
})) as npm.Packument;
const versions = Object.keys(result.versions).sort(semver.compareLoose);
const fallbackVersion = result[`dist-tags`].latest || versions[versions.length - 1];
// The latest version that satisfies `descriptor.range` (if it is a valid range), else `fallbackVersion`
let version: string = fallbackVersion;
const validRange = semverUtils.validRange(descriptor.range);
if (validRange) {
const maxSatisfyingVersion = semver.maxSatisfying(versions, validRange);
if (maxSatisfyingVersion !== null) {
version = maxSatisfyingVersion;
} else {
report.reportWarning(MessageName.UNNAMED, `Unmet range ${structUtils.prettyRange(configuration, descriptor.range)}; falling back to the latest version`);
leadWithSeparator = true;
}
} else if (Object.hasOwn(result[`dist-tags`], descriptor.range)) {
version = result[`dist-tags`][descriptor.range];
} else if (descriptor.range !== `unknown`) {
report.reportWarning(MessageName.UNNAMED, `Unknown tag ${structUtils.prettyRange(configuration, descriptor.range)}; falling back to the latest version`);
leadWithSeparator = true;
}
const release = result.versions[version];
/**
* The merging of
* @see `result` - The information from `registry.npmjs.org/<package>`
* @see `release` - The release corresponding to `version`
* @see `version` - The latest version that satisfies `descriptor.range` (if it is a valid range), else `fallbackVersion`
* @see `versions` - All version tags of a package, sorted in ascending order
*/
const packageInformation: Partial<PackageInformation> = {
...result,
...release,
version,
versions,
};
let serialized: any;
if (fields !== null) {
serialized = {};
for (const field of fields) {
// @ts-expect-error
const value = packageInformation[field];
if (typeof value !== `undefined`) {
serialized[field] = value;
} else {
report.reportWarning(MessageName.EXCEPTION, `The ${formatUtils.pretty(configuration, field, formatUtils.Type.CODE)} field doesn't exist inside ${structUtils.prettyIdent(configuration, descriptor)}'s information`);
leadWithSeparator = true;
continue;
}
}
} else {
// Remove long fields
if (!this.json) {
delete packageInformation.dist;
delete packageInformation.readme;
delete packageInformation.users;
}
serialized = packageInformation;
}
report.reportJson(serialized);
if (!this.json) {
infos.push(serialized);
}
}
});
// @ts-expect-error: The Node typings forgot one field
inspect.styles.name = `cyan`;
for (const serialized of infos) {
if (serialized !== infos[0] || leadWithSeparator)
this.context.stdout.write(`\n`);
this.context.stdout.write(`${inspect(serialized, {
depth: Infinity,
colors: true,
compact: false,
})}\n`);
}
return report.exitCode();
}
}
/**
* Removes hidden properties (the ones that start with an underscore) recursively.
*/
function clean(value: unknown): unknown {
if (Array.isArray(value)) {
const result: Array<unknown> = [];
for (let item of value) {
item = clean(item);
if (item) {
result.push(item);
}
}
return result;
} else if (typeof value === `object` && value !== null) {
const result: any = {};
for (const key of Object.keys(value)) {
if (key.startsWith(`_`))
continue;
const item = clean(value[key as keyof typeof value]);
if (item) {
result[key] = item;
}
}
return result;
} else if (value) {
return value;
} else {
return null;
}
}