Skip to content

Commit

Permalink
feat(repair): add lerna repair command
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesHenry committed Sep 1, 2022
1 parent 674ffd3 commit e69d1e8
Show file tree
Hide file tree
Showing 14 changed files with 205 additions and 31 deletions.
3 changes: 3 additions & 0 deletions .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/**/*"],
Expand Down
2 changes: 1 addition & 1 deletion commands/info/README.md
Expand Up @@ -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`

Expand Down
7 changes: 5 additions & 2 deletions core/command/index.js
Expand Up @@ -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";

Expand Down Expand Up @@ -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());

Expand Down
18 changes: 18 additions & 0 deletions core/lerna/__tests__/repair-command.test.js
@@ -0,0 +1,18 @@
// @ts-check

"use strict";

const path = require("path");
const { loggingOutput } = require("@lerna-test/helpers/logging-output");

// file under test
const lernaRepair = require("@lerna-test/helpers").commandRunner(require("../commands/repair/command"));

describe("repair", () => {
it("should output the result of running migrations", async () => {
// project fixture is irrelevant, no actual changes are made
await lernaRepair(path.resolve(__dirname, "../../.."))();

expect(loggingOutput("info")).toContain("No changes were necessary. This workspace is up to date!");
});
});
29 changes: 29 additions & 0 deletions 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);
};
56 changes: 56 additions & 0 deletions 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;
6 changes: 6 additions & 0 deletions core/lerna/index.js
@@ -1,3 +1,5 @@
// @ts-check

"use strict";

const cli = require("@lerna/cli");
Expand All @@ -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;
Expand All @@ -27,6 +31,7 @@ function main(argv) {
lernaVersion: pkg.version,
};

// @ts-ignore
return cli()
.command(addCmd)
.command(bootstrapCmd)
Expand All @@ -41,6 +46,7 @@ function main(argv) {
.command(linkCmd)
.command(listCmd)
.command(publishCmd)
.command(repairCmd)
.command(runCmd)
.command(versionCmd)
.parse(argv, context);
Expand Down
10 changes: 10 additions & 0 deletions 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"
}
}
}
6 changes: 6 additions & 0 deletions 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.
};
16 changes: 16 additions & 0 deletions 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();
});
});
10 changes: 9 additions & 1 deletion core/lerna/package.json
Expand Up @@ -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"
Expand All @@ -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",
Expand All @@ -52,6 +59,7 @@
"@lerna/publish": "file:../../commands/publish",
"@lerna/run": "file:../../commands/run",
"@lerna/version": "file:../../commands/version",
"@nrwl/devkit": ">=14.5.8 < 16",
"import-local": "^3.0.2",
"npmlog": "^6.0.2",
"nx": ">=14.6.1 < 16",
Expand Down
36 changes: 36 additions & 0 deletions e2e/tests/lerna-repair/lerna-repair.spec.ts
@@ -0,0 +1,36 @@
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 info repair No changes were necessary. This workspace is up to date!
`);
});
});
1 change: 1 addition & 0 deletions nx.json
@@ -1,4 +1,5 @@
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/nx-cloud",
Expand Down

0 comments on commit e69d1e8

Please sign in to comment.