123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- "use strict";
- const dedent = require("dedent");
- const npa = require("npm-package-arg");
- const pMap = require("p-map");
- const path = require("path");
- const pacote = require("pacote");
- const semver = require("semver");
- const { Command } = require("@lerna/command");
- const npmConf = require("@lerna/npm-conf");
- const bootstrap = require("@lerna/bootstrap");
- const { ValidationError } = require("@lerna/validation-error");
- const { getFilteredPackages } = require("@lerna/filter-options");
- const { getRangeToReference } = require("./lib/get-range-to-reference");
- module.exports = factory;
- function factory(argv) {
- return new AddCommand(argv);
- }
- class AddCommand extends Command {
- get requiresGit() {
- return false;
- }
- get dependencyType() {
- if (this.options.dev) {
- return "devDependencies";
- }
- if (this.options.peer) {
- return "peerDependencies";
- }
- return "dependencies";
- }
- initialize() {
- this.spec = npa(this.options.pkg);
- this.dirs = new Set(this.options.globs.map((fp) => path.resolve(this.project.rootPath, fp)));
- this.selfSatisfied = this.packageSatisfied();
- // https://docs.npmjs.com/misc/config#save-prefix
- this.savePrefix = this.options.exact ? "" : "^";
- if (this.packageGraph.has(this.spec.name) && !this.selfSatisfied) {
- const available = this.packageGraph.get(this.spec.name).version;
- throw new ValidationError(
- "ENOTSATISFIED",
- dedent`
- Requested range not satisfiable:
- ${this.spec.name}@${this.spec.fetchSpec} (available: ${available})
- `
- );
- }
- let chain = Promise.resolve();
- chain = chain.then(() => this.getPackageVersion());
- chain = chain.then((version) => {
- if (version == null) {
- throw new ValidationError(
- "ENOTSATISFIED",
- dedent`
- Requested package has no version: ${this.spec.name}
- `
- );
- }
- this.spec.version = version;
- });
- chain = chain.then(() => getFilteredPackages(this.packageGraph, this.execOpts, this.options));
- chain = chain.then((filteredPackages) => {
- this.filteredPackages = filteredPackages;
- });
- chain = chain.then(() => this.collectPackagesToChange());
- chain = chain.then((packagesToChange) => {
- this.packagesToChange = packagesToChange;
- });
- return chain.then(() => {
- const proceed = this.packagesToChange.length > 0;
- if (!proceed) {
- this.logger.warn(`No packages found where ${this.spec.name} can be added.`);
- }
- return proceed;
- });
- }
- execute() {
- const numberOfPackages = `${this.packagesToChange.length} package${
- this.packagesToChange.length > 1 ? "s" : ""
- }`;
- this.logger.info("", `Adding ${this.spec.name} in ${numberOfPackages}`);
- let chain = Promise.resolve();
- chain = chain.then(() => this.makeChanges());
- if (this.options.bootstrap !== false) {
- chain = chain.then(() => {
- const argv = Object.assign({}, this.options, {
- args: [],
- cwd: this.project.rootPath,
- // silence initial cli version logging, etc
- composed: "add",
- // NEVER pass filter-options, it is very bad
- scope: undefined,
- ignore: undefined,
- private: undefined,
- since: undefined,
- excludeDependents: undefined,
- includeDependents: undefined,
- includeDependencies: undefined,
- });
- return bootstrap(argv);
- });
- }
- return chain;
- }
- collectPackagesToChange() {
- const { name: targetName } = this.spec;
- let result = this.filteredPackages;
- // Skip packages that only would install themselves
- if (this.packageGraph.has(targetName)) {
- result = result.filter((pkg) => pkg.name !== targetName);
- }
- // Skip packages that are not selected by dir globs
- if (this.dirs.size) {
- result = result.filter((pkg) => this.dirs.has(pkg.location));
- }
- // Skip packages without actual changes to manifest
- result = result.filter((pkg) => {
- const deps = this.getPackageDeps(pkg);
- // Check if one of the packages to install necessitates a change to pkg's manifest
- if (!(targetName in deps)) {
- return true;
- }
- return getRangeToReference(this.spec, deps, pkg.location, this.savePrefix) !== deps[targetName];
- });
- return result;
- }
- makeChanges() {
- const { name: targetName } = this.spec;
- return pMap(this.packagesToChange, (pkg) => {
- const deps = this.getPackageDeps(pkg);
- const range = getRangeToReference(this.spec, deps, pkg.location, this.savePrefix);
- this.logger.verbose("add", `${targetName}@${range} to ${this.dependencyType} in ${pkg.name}`);
- deps[targetName] = range;
- return pkg.serialize();
- });
- }
- getPackageDeps(pkg) {
- let deps = pkg.get(this.dependencyType);
- if (!deps) {
- deps = {};
- pkg.set(this.dependencyType, deps);
- }
- return deps;
- }
- getPackageVersion() {
- if (this.selfSatisfied) {
- const node = this.packageGraph.get(this.spec.name);
- return Promise.resolve(this.spec.saveRelativeFileSpec ? node.location : node.version);
- }
- // @see https://github.com/zkat/pacote/blob/latest/lib/util/opt-check.js
- const opts = npmConf({
- includeDeprecated: false,
- // we can't pass everything, as our --scope conflicts with pacote's --scope
- registry: this.options.registry,
- });
- return pacote.manifest(this.spec, opts.snapshot).then((pkg) => pkg.version);
- }
- packageSatisfied() {
- const { name, fetchSpec } = this.spec;
- const pkg = this.packageGraph.get(name);
- if (!pkg) {
- return false;
- }
- // an explicit "file:packages/foo" always saves as a relative "file:../foo"
- if (this.spec.type === "directory" && fetchSpec === pkg.location) {
- this.spec.saveRelativeFileSpec = true;
- return true;
- }
- // existing relative file spec means local dep should be added the same way
- this.spec.saveRelativeFileSpec = Array.from(this.packageGraph.values()).some(
- (node) =>
- node.localDependencies.size &&
- Array.from(node.localDependencies.values()).some((resolved) => resolved.type === "directory")
- );
- if (fetchSpec === "latest") {
- return true;
- }
- return semver.intersects(pkg.version, fetchSpec);
- }
- }
- module.exports.AddCommand = AddCommand;
|