123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- "use strict";
- const cloneDeep = require("clone-deep");
- const dedent = require("dedent");
- const execa = require("execa");
- const log = require("npmlog");
- const os = require("os");
- const { PackageGraph } = require("@lerna/package-graph");
- const { Project } = require("@lerna/project");
- const { writeLogFile } = require("@lerna/write-log-file");
- const { ValidationError } = require("@lerna/validation-error");
- const { cleanStack } = require("./lib/clean-stack");
- const { defaultOptions } = require("./lib/default-options");
- const { logPackageError } = require("./lib/log-package-error");
- const { warnIfHanging } = require("./lib/warn-if-hanging");
- const DEFAULT_CONCURRENCY = os.cpus().length;
- class Command {
- constructor(_argv) {
- log.pause();
- log.heading = "lerna";
- const argv = cloneDeep(_argv);
- log.silly("argv", argv);
- // "FooCommand" => "foo"
- this.name = this.constructor.name.replace(/Command$/, "").toLowerCase();
- // composed commands are called from other commands, like publish -> version
- this.composed = typeof argv.composed === "string" && argv.composed !== this.name;
- if (!this.composed) {
- // composed commands have already logged the lerna version
- log.notice("cli", `v${argv.lernaVersion}`);
- }
- // launch the command
- let runner = new Promise((resolve, reject) => {
- // run everything inside a Promise chain
- let chain = Promise.resolve();
- chain = chain.then(() => {
- this.project = new Project(argv.cwd);
- });
- chain = chain.then(() => this.configureEnvironment());
- chain = chain.then(() => this.configureOptions());
- chain = chain.then(() => this.configureProperties());
- chain = chain.then(() => this.configureLogging());
- chain = chain.then(() => this.runValidations());
- chain = chain.then(() => this.runPreparations());
- chain = chain.then(() => this.runCommand());
- chain.then(
- (result) => {
- warnIfHanging();
- resolve(result);
- },
- (err) => {
- if (err.pkg) {
- // Cleanly log specific package error details
- logPackageError(err, this.options.stream);
- } else if (err.name !== "ValidationError") {
- // npmlog does some funny stuff to the stack by default,
- // so pass it directly to avoid duplication.
- log.error("", cleanStack(err, this.constructor.name));
- }
- // ValidationError does not trigger a log dump, nor do external package errors
- if (err.name !== "ValidationError" && !err.pkg) {
- writeLogFile(this.project.rootPath);
- }
- warnIfHanging();
- // error code is handled by cli.fail()
- reject(err);
- }
- );
- });
- // passed via yargs context in tests, never actual CLI
- /* istanbul ignore else */
- if (argv.onResolved || argv.onRejected) {
- runner = runner.then(argv.onResolved, argv.onRejected);
- // when nested, never resolve inner with outer callbacks
- delete argv.onResolved; // eslint-disable-line no-param-reassign
- delete argv.onRejected; // eslint-disable-line no-param-reassign
- }
- // "hide" irrelevant argv keys from options
- for (const key of ["cwd", "$0"]) {
- Object.defineProperty(argv, key, { enumerable: false });
- }
- Object.defineProperty(this, "argv", {
- value: Object.freeze(argv),
- });
- Object.defineProperty(this, "runner", {
- value: runner,
- });
- }
- // proxy "Promise" methods to "private" instance
- then(onResolved, onRejected) {
- return this.runner.then(onResolved, onRejected);
- }
- /* istanbul ignore next */
- catch(onRejected) {
- return this.runner.catch(onRejected);
- }
- get requiresGit() {
- return true;
- }
- // Override this to inherit config from another command.
- // For example `changed` inherits config from `publish`.
- get otherCommandConfigs() {
- return [];
- }
- configureEnvironment() {
- // eslint-disable-next-line global-require
- const ci = require("is-ci");
- let loglevel;
- let progress;
- /* istanbul ignore next */
- if (ci || !process.stderr.isTTY) {
- log.disableColor();
- progress = false;
- } else if (!process.stdout.isTTY) {
- // stdout is being piped, don't log non-errors or progress bars
- progress = false;
- loglevel = "error";
- } else if (process.stderr.isTTY) {
- log.enableColor();
- log.enableUnicode();
- }
- Object.defineProperty(this, "envDefaults", {
- value: {
- ci,
- progress,
- loglevel,
- },
- });
- }
- configureOptions() {
- // Command config object normalized to "command" namespace
- const commandConfig = this.project.config.command || {};
- // The current command always overrides otherCommandConfigs
- const overrides = [this.name, ...this.otherCommandConfigs].map((key) => commandConfig[key]);
- this.options = defaultOptions(
- // CLI flags, which if defined overrule subsequent values
- this.argv,
- // Namespaced command options from `lerna.json`
- ...overrides,
- // Global options from `lerna.json`
- this.project.config,
- // Environmental defaults prepared in previous step
- this.envDefaults
- );
- }
- configureProperties() {
- const { concurrency, sort, maxBuffer } = this.options;
- this.concurrency = Math.max(1, +concurrency || DEFAULT_CONCURRENCY);
- this.toposort = sort === undefined || sort;
- /** @type {import("@lerna/child-process").ExecOpts} */
- this.execOpts = {
- cwd: this.project.rootPath,
- maxBuffer,
- };
- }
- configureLogging() {
- const { loglevel } = this.options;
- if (loglevel) {
- log.level = loglevel;
- }
- // handle log.success()
- log.addLevel("success", 3001, { fg: "green", bold: true });
- // create logger that subclasses use
- Object.defineProperty(this, "logger", {
- value: log.newGroup(this.name),
- });
- // emit all buffered logs at configured level and higher
- log.resume();
- }
- enableProgressBar() {
- /* istanbul ignore next */
- if (this.options.progress !== false) {
- log.enableProgress();
- }
- }
- gitInitialized() {
- const opts = {
- cwd: this.project.rootPath,
- // don't throw, just want boolean
- reject: false,
- // only return code, no stdio needed
- stdio: "ignore",
- };
- return execa.sync("git", ["rev-parse"], opts).exitCode === 0;
- }
- runValidations() {
- if ((this.options.since !== undefined || this.requiresGit) && !this.gitInitialized()) {
- throw new ValidationError("ENOGIT", "The git binary was not found, or this is not a git repository.");
- }
- if (!this.project.manifest) {
- throw new ValidationError("ENOPKG", "`package.json` does not exist, have you run `lerna init`?");
- }
- if (!this.project.version) {
- throw new ValidationError("ENOLERNA", "`lerna.json` does not exist, have you run `lerna init`?");
- }
- if (this.options.independent && !this.project.isIndependent()) {
- throw new ValidationError(
- "EVERSIONMODE",
- dedent`
- You ran lerna with --independent or -i, but the repository is not set to independent mode.
- To use independent mode you need to set lerna.json's "version" property to "independent".
- Then you won't need to pass the --independent or -i flags.
- `
- );
- }
- }
- runPreparations() {
- if (!this.composed && this.project.isIndependent()) {
- // composed commands have already logged the independent status
- log.info("versioning", "independent");
- }
- if (!this.composed && this.options.ci) {
- log.info("ci", "enabled");
- }
- let chain = Promise.resolve();
- chain = chain.then(() => this.project.getPackages());
- chain = chain.then((packages) => {
- this.packageGraph = new PackageGraph(packages);
- });
- return chain;
- }
- runCommand() {
- return Promise.resolve()
- .then(() => this.initialize())
- .then((proceed) => {
- if (proceed !== false) {
- return this.execute();
- }
- // early exits set their own exitCode (if non-zero)
- });
- }
- initialize() {
- throw new ValidationError(this.name, "initialize() needs to be implemented.");
- }
- execute() {
- throw new ValidationError(this.name, "execute() needs to be implemented.");
- }
- }
- module.exports.Command = Command;
|