/
add.ts
369 lines (296 loc) Β· 17.1 KB
/
add.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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli';
import {Cache, Configuration, Descriptor, formatUtils, LightReport, MessageName} from '@yarnpkg/core';
import {Project, Workspace, Ident, InstallMode} from '@yarnpkg/core';
import {structUtils} from '@yarnpkg/core';
import {PortablePath} from '@yarnpkg/fslib';
import {Command, Option, Usage, UsageError} from 'clipanion';
import {prompt} from 'enquirer';
import * as t from 'typanion';
import * as suggestUtils from '../suggestUtils';
import {Hooks} from '..';
// eslint-disable-next-line arca/no-default-export
export default class AddCommand extends BaseCommand {
static paths = [
[`add`],
];
static usage: Usage = Command.Usage({
description: `add dependencies to the project`,
details: `
This command adds a package to the package.json for the nearest workspace.
- If it didn't exist before, the package will by default be added to the regular \`dependencies\` field, but this behavior can be overriden thanks to the \`-D,--dev\` flag (which will cause the dependency to be added to the \`devDependencies\` field instead) and the \`-P,--peer\` flag (which will do the same but for \`peerDependencies\`).
- If the package was already listed in your dependencies, it will by default be upgraded whether it's part of your \`dependencies\` or \`devDependencies\` (it won't ever update \`peerDependencies\`, though).
- If set, the \`--prefer-dev\` flag will operate as a more flexible \`-D,--dev\` in that it will add the package to your \`devDependencies\` if it isn't already listed in either \`dependencies\` or \`devDependencies\`, but it will also happily upgrade your \`dependencies\` if that's what you already use (whereas \`-D,--dev\` would throw an exception).
- If set, the \`-O,--optional\` flag will add the package to the \`optionalDependencies\` field and, in combination with the \`-P,--peer\` flag, it will add the package as an optional peer dependency. If the package was already listed in your \`dependencies\`, it will be upgraded to \`optionalDependencies\`. If the package was already listed in your \`peerDependencies\`, in combination with the \`-P,--peer\` flag, it will be upgraded to an optional peer dependency: \`"peerDependenciesMeta": { "<package>": { "optional": true } }\`
- If the added package doesn't specify a range at all its \`latest\` tag will be resolved and the returned version will be used to generate a new semver range (using the \`^\` modifier by default unless otherwise configured via the \`defaultSemverRangePrefix\` configuration, or the \`~\` modifier if \`-T,--tilde\` is specified, or no modifier at all if \`-E,--exact\` is specified). Two exceptions to this rule: the first one is that if the package is a workspace then its local version will be used, and the second one is that if you use \`-P,--peer\` the default range will be \`*\` and won't be resolved at all.
- If the added package specifies a range (such as \`^1.0.0\`, \`latest\`, or \`rc\`), Yarn will add this range as-is in the resulting package.json entry (in particular, tags such as \`rc\` will be encoded as-is rather than being converted into a semver range).
If the \`--cached\` option is used, Yarn will preferably reuse the highest version already used somewhere within the project, even if through a transitive dependency.
If the \`-i,--interactive\` option is used (or if the \`preferInteractive\` settings is toggled on) the command will first try to check whether other workspaces in the project use the specified package and, if so, will offer to reuse them.
If the \`--mode=<mode>\` option is set, Yarn will change which artifacts are generated. The modes currently supported are:
- \`skip-build\` will not run the build scripts at all. Note that this is different from setting \`enableScripts\` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.
- \`update-lockfile\` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.
For a compilation of all the supported protocols, please consult the dedicated page from our website: https://yarnpkg.com/protocols.
`,
examples: [[
`Add a regular package to the current workspace`,
`$0 add lodash`,
], [
`Add a specific version for a package to the current workspace`,
`$0 add lodash@1.2.3`,
], [
`Add a package from a GitHub repository (the master branch) to the current workspace using a URL`,
`$0 add lodash@https://github.com/lodash/lodash`,
], [
`Add a package from a GitHub repository (the master branch) to the current workspace using the GitHub protocol`,
`$0 add lodash@github:lodash/lodash`,
], [
`Add a package from a GitHub repository (the master branch) to the current workspace using the GitHub protocol (shorthand)`,
`$0 add lodash@lodash/lodash`,
], [
`Add a package from a specific branch of a GitHub repository to the current workspace using the GitHub protocol (shorthand)`,
`$0 add lodash-es@lodash/lodash#es`,
]],
});
json = Option.Boolean(`--json`, false, {
description: `Format the output as an NDJSON stream`,
});
fixed = Option.Boolean(`-F,--fixed`, false, {
description: `Store dependency tags as-is instead of resolving them`,
});
exact = Option.Boolean(`-E,--exact`, false, {
description: `Don't use any semver modifier on the resolved range`,
});
tilde = Option.Boolean(`-T,--tilde`, false, {
description: `Use the \`~\` semver modifier on the resolved range`,
});
caret = Option.Boolean(`-C,--caret`, false, {
description: `Use the \`^\` semver modifier on the resolved range`,
});
dev = Option.Boolean(`-D,--dev`, false, {
description: `Add a package as a dev dependency`,
});
peer = Option.Boolean(`-P,--peer`, false, {
description: `Add a package as a peer dependency`,
});
optional = Option.Boolean(`-O,--optional`, false, {
description: `Add / upgrade a package to an optional regular / peer dependency`,
});
preferDev = Option.Boolean(`--prefer-dev`, false, {
description: `Add / upgrade a package to a dev dependency`,
});
interactive = Option.Boolean(`-i,--interactive`, {
description: `Reuse the specified package from other workspaces in the project`,
});
cached = Option.Boolean(`--cached`, false, {
description: `Reuse the highest version already used somewhere within the project`,
});
mode = Option.String(`--mode`, {
description: `Change what artifacts installs generate`,
validator: t.isEnum(InstallMode),
});
silent = Option.Boolean(`--silent`, {hidden: true});
packages = Option.Rest();
async execute() {
const configuration = await Configuration.find(this.context.cwd, this.context.plugins);
const {project, workspace} = await Project.find(configuration, this.context.cwd);
const cache = await Cache.find(configuration);
if (!workspace)
throw new WorkspaceRequiredError(project.cwd, this.context.cwd);
await project.restoreInstallState({
restoreResolutions: false,
});
const fixed = this.fixed;
const interactive = this.interactive ?? configuration.get(`preferInteractive`);
const reuse = interactive || configuration.get(`preferReuse`);
const modifier = suggestUtils.getModifier(this, project);
const strategies = [
reuse ?
suggestUtils.Strategy.REUSE
: undefined,
suggestUtils.Strategy.PROJECT,
this.cached ?
suggestUtils.Strategy.CACHE
: undefined,
suggestUtils.Strategy.LATEST,
].filter((strategy): strategy is suggestUtils.Strategy => typeof strategy !== `undefined`);
const maxResults = interactive
? Infinity
: 1;
const allSuggestions = await Promise.all(this.packages.map(async pseudoDescriptor => {
const request = pseudoDescriptor.match(/^\.{0,2}\//)
? await suggestUtils.extractDescriptorFromPath(pseudoDescriptor as PortablePath, {cwd: this.context.cwd, workspace})
: structUtils.tryParseDescriptor(pseudoDescriptor);
const unsupportedPrefix = pseudoDescriptor.match(/^(https?:|git@github)/);
if (unsupportedPrefix)
throw new UsageError(`It seems you are trying to add a package using a ${formatUtils.pretty(configuration, `${unsupportedPrefix[0]}...`, formatUtils.Type.RANGE)} url; we now require package names to be explicitly specified.\nTry running the command again with the package name prefixed: ${formatUtils.pretty(configuration, `yarn add`, formatUtils.Type.CODE)} ${formatUtils.pretty(configuration, structUtils.makeDescriptor(structUtils.makeIdent(null, `my-package`), `${unsupportedPrefix[0]}...`), formatUtils.Type.DESCRIPTOR)}`);
if (!request)
throw new UsageError(`The ${formatUtils.pretty(configuration, pseudoDescriptor, formatUtils.Type.CODE)} string didn't match the required format (package-name@range). Did you perhaps forget to explicitly reference the package name?`);
const targetList = suggestTargetList(workspace, request, {
dev: this.dev,
peer: this.peer,
preferDev: this.preferDev,
optional: this.optional,
});
const results = await Promise.all(targetList.map(async target => {
const suggestedDescriptors = await suggestUtils.getSuggestedDescriptors(request, {project, workspace, cache, fixed, target, modifier, strategies, maxResults});
return {request, suggestedDescriptors, target};
}));
return results;
})).then(results => results.flat());
const checkReport = await LightReport.start({
configuration,
stdout: this.context.stdout,
suggestInstall: false,
}, async report => {
for (const {request, suggestedDescriptors: {suggestions, rejections}} of allSuggestions) {
const nonNullSuggestions = suggestions.filter(suggestion => {
return suggestion.descriptor !== null;
});
if (nonNullSuggestions.length === 0) {
const [firstError] = rejections;
if (typeof firstError === `undefined`)
throw new Error(`Assertion failed: Expected an error to have been set`);
if (!project.configuration.get(`enableNetwork`))
report.reportError(MessageName.CANT_SUGGEST_RESOLUTIONS, `${structUtils.prettyDescriptor(configuration, request)} can't be resolved to a satisfying range (note: network resolution has been disabled)`);
else
report.reportError(MessageName.CANT_SUGGEST_RESOLUTIONS, `${structUtils.prettyDescriptor(configuration, request)} can't be resolved to a satisfying range`);
report.reportSeparator();
report.reportExceptionOnce(firstError);
}
}
});
if (checkReport.hasErrors())
return checkReport.exitCode();
let askedQuestions = false;
const afterWorkspaceDependencyAdditionList: Array<[
Workspace,
suggestUtils.Target,
Descriptor,
Array<suggestUtils.Strategy>,
]> = [];
const afterWorkspaceDependencyReplacementList: Array<[
Workspace,
suggestUtils.Target,
Descriptor,
Descriptor,
]> = [];
for (const {suggestedDescriptors: {suggestions}, target} of allSuggestions) {
let selected: Descriptor;
const nonNullSuggestions = suggestions.filter(suggestion => {
return suggestion.descriptor !== null;
}) as Array<suggestUtils.Suggestion>;
const firstSuggestedDescriptor = nonNullSuggestions[0].descriptor;
const areAllTheSame = nonNullSuggestions.every(suggestion => structUtils.areDescriptorsEqual(suggestion.descriptor, firstSuggestedDescriptor));
if (nonNullSuggestions.length === 1 || areAllTheSame) {
selected = firstSuggestedDescriptor;
} else {
askedQuestions = true;
({answer: selected} = await prompt({
type: `select`,
name: `answer`,
message: `Which range do you want to use?`,
choices: suggestions.map(({descriptor, name, reason}) => descriptor ? {
name,
hint: reason,
descriptor,
} : {
name,
hint: reason,
disabled: true,
}),
onCancel: () => process.exit(130),
result(name: string) {
// @ts-expect-error: The enquirer types don't include find
return this.find(name, `descriptor`);
},
stdin: this.context.stdin as NodeJS.ReadStream,
stdout: this.context.stdout as NodeJS.WriteStream,
}));
}
const current = workspace.manifest[target].get(selected.identHash);
if (typeof current === `undefined` || current.descriptorHash !== selected.descriptorHash) {
workspace.manifest[target].set(
selected.identHash,
selected,
);
if (this.optional) {
if (target === `dependencies`) {
workspace.manifest.ensureDependencyMeta({
...selected,
range: `unknown`,
}).optional = true;
} else if (target === `peerDependencies`) {
workspace.manifest.ensurePeerDependencyMeta({
...selected,
range: `unknown`,
}).optional = true;
}
}
if (typeof current === `undefined`) {
afterWorkspaceDependencyAdditionList.push([
workspace,
target,
selected,
strategies,
]);
} else {
afterWorkspaceDependencyReplacementList.push([
workspace,
target,
current,
selected,
]);
}
}
}
await configuration.triggerMultipleHooks(
(hooks: Hooks) => hooks.afterWorkspaceDependencyAddition,
afterWorkspaceDependencyAdditionList,
);
await configuration.triggerMultipleHooks(
(hooks: Hooks) => hooks.afterWorkspaceDependencyReplacement,
afterWorkspaceDependencyReplacementList,
);
if (askedQuestions)
this.context.stdout.write(`\n`);
return await project.installWithNewReport({
json: this.json,
stdout: this.context.stdout,
quiet: this.context.quiet,
}, {
cache,
mode: this.mode,
});
}
}
function suggestTargetList(workspace: Workspace, ident: Ident, {dev, peer, preferDev, optional}: {dev: boolean, peer: boolean, preferDev: boolean, optional: boolean}) {
const hasRegular = workspace.manifest[suggestUtils.Target.REGULAR].has(ident.identHash);
const hasDev = workspace.manifest[suggestUtils.Target.DEVELOPMENT].has(ident.identHash);
const hasPeer = workspace.manifest[suggestUtils.Target.PEER].has(ident.identHash);
if ((dev || peer) && hasRegular)
throw new UsageError(`Package "${structUtils.prettyIdent(workspace.project.configuration, ident)}" is already listed as a regular dependency - remove the -D,-P flags or remove it from your dependencies first`);
if (!dev && !peer && hasPeer)
throw new UsageError(`Package "${structUtils.prettyIdent(workspace.project.configuration, ident)}" is already listed as a peer dependency - use either of -D or -P, or remove it from your peer dependencies first`);
if (optional && hasDev)
throw new UsageError(`Package "${structUtils.prettyIdent(workspace.project.configuration, ident)}" is already listed as a dev dependency - remove the -O flag or remove it from your dev dependencies first`);
if (optional && !peer && hasPeer)
throw new UsageError(`Package "${structUtils.prettyIdent(workspace.project.configuration, ident)}" is already listed as a peer dependency - remove the -O flag or add the -P flag or remove it from your peer dependencies first`);
if ((dev || preferDev) && optional)
throw new UsageError(`Package "${structUtils.prettyIdent(workspace.project.configuration, ident)}" cannot simultaneously be a dev dependency and an optional dependency`);
// When the program executes this line, the command is expected to be legal
const targetList = [];
if (peer)
targetList.push(suggestUtils.Target.PEER);
if (dev || preferDev)
targetList.push(suggestUtils.Target.DEVELOPMENT);
if (optional)
targetList.push(suggestUtils.Target.REGULAR);
// The user explicitly define the targets
if (targetList.length > 0)
return targetList;
// The user does not define the targets, find it from the `workspace.manifest`
if (hasDev)
return [suggestUtils.Target.DEVELOPMENT];
if (hasPeer)
return [suggestUtils.Target.PEER];
return [suggestUtils.Target.REGULAR];
}