diff --git a/.eslintrc.json b/.eslintrc.json index e364f99a62..054c887c94 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,8 @@ { "root": true, + "parserOptions": { + "ecmaVersion": 2020 + }, "extends": ["airbnb-base", "plugin:jest/recommended", "plugin:node/recommended", "prettier"], "plugins": ["jest", "node"], "ignorePatterns": ["website/**/*"], diff --git a/commands/info/README.md b/commands/info/README.md index 9b343e179e..d75ad8d325 100644 --- a/commands/info/README.md +++ b/commands/info/README.md @@ -6,7 +6,7 @@ Install [lerna](https://www.npmjs.com/package/lerna) for access to the `lerna` C ## Usage -The `info` prints local environment information that proves to be useful especially while submitting bug reports. +The `info` command prints local environment information that proves to be useful especially while submitting bug reports. `lerna info` diff --git a/core/command/index.js b/core/command/index.js index 3838bd79cb..e74f419830 100644 --- a/core/command/index.js +++ b/core/command/index.js @@ -19,7 +19,7 @@ const { warnIfHanging } = require("./lib/warn-if-hanging"); const DEFAULT_CONCURRENCY = os.cpus().length; class Command { - constructor(_argv) { + constructor(_argv, { skipValidations } = { skipValidations: false }) { log.pause(); log.heading = "lerna"; @@ -49,7 +49,10 @@ class Command { chain = chain.then(() => this.configureOptions()); chain = chain.then(() => this.configureProperties()); chain = chain.then(() => this.configureLogging()); - chain = chain.then(() => this.runValidations()); + // For the special "repair" command we want to intitialize everything but don't want to run validations as that will end up becoming cyclical + if (!skipValidations) { + chain = chain.then(() => this.runValidations()); + } chain = chain.then(() => this.runPreparations()); chain = chain.then(() => this.runCommand()); diff --git a/core/lerna/commands/repair/command.js b/core/lerna/commands/repair/command.js new file mode 100644 index 0000000000..0fa41f0861 --- /dev/null +++ b/core/lerna/commands/repair/command.js @@ -0,0 +1,29 @@ +// @ts-check + +"use strict"; + +/** + * @see https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module + */ +exports.command = "repair"; + +exports.describe = "Runs automated migrations to repair the state of a lerna repo"; + +exports.builder = (yargs) => { + yargs.options({ + /** + * equivalent to --loglevel=verbose, but added explicitly here because the repair() + * output will potentially contain instructions to run with --verbose + */ + verbose: { + hidden: true, + type: "boolean", + }, + }); + return yargs; +}; + +exports.handler = function handler(argv) { + // eslint-disable-next-line global-require + return require(".")(argv); +}; diff --git a/core/lerna/commands/repair/index.js b/core/lerna/commands/repair/index.js new file mode 100644 index 0000000000..d14561e34b --- /dev/null +++ b/core/lerna/commands/repair/index.js @@ -0,0 +1,56 @@ +// @ts-check + +"use strict"; + +const { Command } = require("@lerna/command"); +const log = require("npmlog"); +const { repair } = require("nx/src/command-line/repair"); +const migrationsJson = require("../../migrations.json"); + +module.exports = factory; + +function factory(argv) { + return new RepairCommand(argv); +} + +class RepairCommand extends Command { + constructor(argv) { + super(argv, { skipValidations: true }); + } + + // eslint-disable-next-line class-methods-use-this + initialize() {} + + async execute() { + this.configureNxOutput(); + const verbose = this.options?.verbose ? true : log.level === "verbose"; + + const lernaMigrations = Object.entries(migrationsJson.generators).map(([name, migration]) => { + return /** @type {const} */ ({ + package: "lerna", + cli: "nx", + name, + description: migration.description, + version: migration.version, + }); + }); + + await repair({ verbose }, lernaMigrations); + } + + configureNxOutput() { + try { + // eslint-disable-next-line global-require + const nxOutput = require("nx/src/utils/output"); + nxOutput.output.cliName = "Lerna"; + nxOutput.output.formatCommand = (taskId) => taskId; + return nxOutput; + } catch (err) { + this.logger.error("There was a critical issue when trying to execute the repair command."); + // Rethrow so that the lerna logger can automatically handle the unexpected error + throw err; + } + } +} + +module.exports.RepairCommand = RepairCommand; diff --git a/core/lerna/index.js b/core/lerna/index.js index dadfe8a407..a1bdc95de1 100644 --- a/core/lerna/index.js +++ b/core/lerna/index.js @@ -1,3 +1,5 @@ +// @ts-check + "use strict"; const cli = require("@lerna/cli"); @@ -18,6 +20,8 @@ const publishCmd = require("@lerna/publish/command"); const runCmd = require("@lerna/run/command"); const versionCmd = require("@lerna/version/command"); +const repairCmd = require("./commands/repair/command"); + const pkg = require("./package.json"); module.exports = main; @@ -27,6 +31,7 @@ function main(argv) { lernaVersion: pkg.version, }; + // @ts-ignore return cli() .command(addCmd) .command(bootstrapCmd) @@ -41,6 +46,7 @@ function main(argv) { .command(linkCmd) .command(listCmd) .command(publishCmd) + .command(repairCmd) .command(runCmd) .command(versionCmd) .parse(argv, context); diff --git a/core/lerna/migrations.json b/core/lerna/migrations.json new file mode 100644 index 0000000000..c58cd30fd0 --- /dev/null +++ b/core/lerna/migrations.json @@ -0,0 +1,10 @@ +{ + "generators": { + "noop": { + "cli": "nx", + "version": "5.3.0", + "description": "Noop example migration, can be removed once a real migration is created", + "implementation": "./migrations/noop/noop" + } + } +} diff --git a/core/lerna/migrations/noop/noop.js b/core/lerna/migrations/noop/noop.js new file mode 100644 index 0000000000..c0f703b8ea --- /dev/null +++ b/core/lerna/migrations/noop/noop.js @@ -0,0 +1,6 @@ +// @ts-check + +// eslint-disable-next-line no-unused-vars +exports.default = async function generator(tree) { + // This is a noop migration just to show how one would be written until the first real implementation is in place. +}; diff --git a/core/lerna/migrations/noop/noop.test.js b/core/lerna/migrations/noop/noop.test.js new file mode 100644 index 0000000000..6f58b557c1 --- /dev/null +++ b/core/lerna/migrations/noop/noop.test.js @@ -0,0 +1,16 @@ +// @ts-check + +const { createTreeWithEmptyWorkspace } = require("@nrwl/devkit/testing"); +const { default: noopMigration } = require("./noop"); + +describe("noop migration", () => { + let tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it("should be runnable and not throw", async () => { + await expect(noopMigration(tree)).resolves.toBeUndefined(); + }); +}); diff --git a/core/lerna/package.json b/core/lerna/package.json index e343215d24..cbfb887891 100644 --- a/core/lerna/package.json +++ b/core/lerna/package.json @@ -19,7 +19,10 @@ "files": [ "index.js", "cli.js", - "schemas/lerna-schema.json" + "schemas/lerna-schema.json", + "migrations", + "commands", + "migrations.json" ], "engines": { "node": "^14.15.0 || >=16.0.0" @@ -32,12 +35,16 @@ "url": "git+https://github.com/lerna/lerna.git", "directory": "core/lerna" }, + "nx-migrations": { + "migrations": "./migrations.json" + }, "scripts": { "test": "echo \"Run tests from root\" && exit 1" }, "dependencies": { "@lerna/add": "file:../../commands/add", "@lerna/bootstrap": "file:../../commands/bootstrap", + "@lerna/command": "file:../command", "@lerna/changed": "file:../../commands/changed", "@lerna/clean": "file:../../commands/clean", "@lerna/cli": "file:../cli", @@ -52,6 +59,7 @@ "@lerna/publish": "file:../../commands/publish", "@lerna/run": "file:../../commands/run", "@lerna/version": "file:../../commands/version", + "@nrwl/devkit": ">=14.8.1 < 16", "import-local": "^3.0.2", "npmlog": "^6.0.2", "nx": ">=14.8.1 < 16", diff --git a/e2e/tests/lerna-repair/lerna-repair.spec.ts b/e2e/tests/lerna-repair/lerna-repair.spec.ts new file mode 100644 index 0000000000..f64e414008 --- /dev/null +++ b/e2e/tests/lerna-repair/lerna-repair.spec.ts @@ -0,0 +1,38 @@ +import { Fixture } from "../../utils/fixture"; +import { normalizeEnvironment } from "../../utils/snapshot-serializer-utils"; + +expect.addSnapshotSerializer({ + serialize(str) { + return normalizeEnvironment(str); + }, + test(val) { + return val != null && typeof val === "string"; + }, +}); + +describe("lerna-repair", () => { + let fixture: Fixture; + + beforeAll(async () => { + fixture = await Fixture.create({ + name: "lerna-repair", + packageManager: "npm", + initializeGit: true, + runLernaInit: true, + installDependencies: true, + }); + }); + afterAll(() => fixture.destroy()); + + it("should run any existing migrations", async () => { + const output = await fixture.lerna("repair"); + + expect(output.combinedOutput).toMatchInlineSnapshot(` + lerna notice cli v999.9.9-e2e.0 + + > Lerna No changes were necessary. This workspace is up to date! + + + `); + }); +}); diff --git a/nx.json b/nx.json index cf87e4e23e..493d4997ce 100644 --- a/nx.json +++ b/nx.json @@ -1,4 +1,5 @@ { + "$schema": "./node_modules/nx/schemas/nx-schema.json", "tasksRunnerOptions": { "default": { "runner": "@nrwl/nx-cloud", diff --git a/package-lock.json b/package-lock.json index 7265a3a2a7..e801fca045 100644 --- a/package-lock.json +++ b/package-lock.json @@ -538,6 +538,7 @@ "@lerna/changed": "file:../../commands/changed", "@lerna/clean": "file:../../commands/clean", "@lerna/cli": "file:../cli", + "@lerna/command": "file:../command", "@lerna/create": "file:../../commands/create", "@lerna/diff": "file:../../commands/diff", "@lerna/exec": "file:../../commands/exec", @@ -549,6 +550,7 @@ "@lerna/publish": "file:../../commands/publish", "@lerna/run": "file:../../commands/run", "@lerna/version": "file:../../commands/version", + "@nrwl/devkit": ">=14.8.1 < 16", "import-local": "^3.0.2", "npmlog": "^6.0.2", "nx": ">=14.8.1 < 16", @@ -2993,7 +2995,6 @@ "version": "14.8.1", "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-14.8.1.tgz", "integrity": "sha512-fyelIwzFQwf2UyBLDftKxzytqp4D0zw57uQ6fnw4FZ+oOYmnraEn7B9INqu9HGjSo234QhB8l/VUGcvXp6CTwA==", - "dev": true, "dependencies": { "@phenomnomnominal/tsquery": "4.1.1", "ejs": "^3.1.7", @@ -3007,8 +3008,7 @@ "node_modules/@nrwl/devkit/node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "node_modules/@nrwl/nx-cloud": { "version": "14.7.0", @@ -3313,7 +3313,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-4.1.1.tgz", "integrity": "sha512-jjMmK1tnZbm1Jq5a7fBliM4gQwjxMU7TFoRNwIyzwlO+eHPRCFv/Nv+H/Gi1jc3WR7QURG8D5d0Tn12YGrUqBQ==", - "dev": true, "dependencies": { "esquery": "^1.0.1" }, @@ -4498,8 +4497,7 @@ "node_modules/async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", - "dev": true + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, "node_modules/asynckit": { "version": "0.4.0", @@ -6875,7 +6873,6 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", - "dev": true, "dependencies": { "jake": "^10.8.5" }, @@ -7713,7 +7710,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -7725,7 +7721,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true, "engines": { "node": ">=4.0" } @@ -8188,7 +8183,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, "dependencies": { "minimatch": "^5.0.1" } @@ -8197,7 +8191,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -8206,7 +8199,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", - "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -10142,7 +10134,6 @@ "version": "10.8.5", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", - "dev": true, "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", @@ -20334,7 +20325,6 @@ "version": "14.8.1", "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-14.8.1.tgz", "integrity": "sha512-fyelIwzFQwf2UyBLDftKxzytqp4D0zw57uQ6fnw4FZ+oOYmnraEn7B9INqu9HGjSo234QhB8l/VUGcvXp6CTwA==", - "dev": true, "requires": { "@phenomnomnominal/tsquery": "4.1.1", "ejs": "^3.1.7", @@ -20345,8 +20335,7 @@ "tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" } } }, @@ -20599,7 +20588,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-4.1.1.tgz", "integrity": "sha512-jjMmK1tnZbm1Jq5a7fBliM4gQwjxMU7TFoRNwIyzwlO+eHPRCFv/Nv+H/Gi1jc3WR7QURG8D5d0Tn12YGrUqBQ==", - "dev": true, "requires": { "esquery": "^1.0.1" } @@ -21524,8 +21512,7 @@ "async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", - "dev": true + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, "asynckit": { "version": "0.4.0", @@ -23394,7 +23381,6 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", - "dev": true, "requires": { "jake": "^10.8.5" } @@ -24027,7 +24013,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, "requires": { "estraverse": "^5.1.0" }, @@ -24035,8 +24020,7 @@ "estraverse": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" } } }, @@ -24410,7 +24394,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, "requires": { "minimatch": "^5.0.1" }, @@ -24419,7 +24402,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "requires": { "balanced-match": "^1.0.0" } @@ -24428,7 +24410,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", - "dev": true, "requires": { "brace-expansion": "^2.0.1" } @@ -25876,7 +25857,6 @@ "version": "10.8.5", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", - "dev": true, "requires": { "async": "^3.2.3", "chalk": "^4.0.2", @@ -27368,6 +27348,7 @@ "@lerna/changed": "file:../../commands/changed", "@lerna/clean": "file:../../commands/clean", "@lerna/cli": "file:../cli", + "@lerna/command": "file:../command", "@lerna/create": "file:../../commands/create", "@lerna/diff": "file:../../commands/diff", "@lerna/exec": "file:../../commands/exec", @@ -27379,6 +27360,7 @@ "@lerna/publish": "file:../../commands/publish", "@lerna/run": "file:../../commands/run", "@lerna/version": "file:../../commands/version", + "@nrwl/devkit": ">=14.8.1 < 16", "import-local": "^3.0.2", "npmlog": "^6.0.2", "nx": ">=14.8.1 < 16",