Skip to content

Commit

Permalink
feat(watch): Add lerna watch command (#3466)
Browse files Browse the repository at this point in the history
  • Loading branch information
fahslaj committed Jan 5, 2023
1 parent 99a2469 commit 008b995
Show file tree
Hide file tree
Showing 21 changed files with 874 additions and 115 deletions.
2 changes: 1 addition & 1 deletion commands/version/package.json
Expand Up @@ -48,7 +48,7 @@
"@lerna/run-topologically": "file:../../utils/run-topologically",
"@lerna/temp-write": "file:../../utils/temp-write",
"@lerna/validation-error": "file:../../core/validation-error",
"@nrwl/devkit": ">=14.8.6 < 16",
"@nrwl/devkit": ">=15.4.2 < 16",
"chalk": "^4.1.0",
"dedent": "^0.7.0",
"load-json-file": "^6.2.0",
Expand Down
49 changes: 49 additions & 0 deletions core/lerna/commands/watch/README.md
@@ -0,0 +1,49 @@
# `lerna watch`

> Watch for changes within packages and execute commands from the root of the repository
Install [lerna](https://www.npmjs.com/package/lerna) for access to the `lerna` CLI.

## Usage

```sh
$ lerna watch -- <command>
```

The values `$LERNA_PACKAGE_NAME` and `$LERNA_FILE_CHANGES` will be replaced with the package and the file(s) that changed, respectively. If multiple file changes are detected in one cycle, then `$LERNA_FILE_CHANGES` will list them all, separated by spaces.

> 💡 When using `$LERNA_PACKAGE_NAME` and `$LERNA_FILE_CHANGES`, you will need to escape the dollar sign with a backslash (`\`). See the [examples](#examples) below.
### Examples

Watch all packages and echo the package name and the files that changed:

```sh
$ lerna watch -- echo \$LERNA_PACKAGE_NAME \$LERNA_FILE_CHANGES
```

Watch only packages "package-1", "package-3" and their dependencies:

```sh
$ lerna watch --scope "package-{1,3}" --include-dependencies -- echo \$LERNA_PACKAGE_NAME \$LERNA_FILE_CHANGES
```

Watch only package "package-4" and its dependencies and run the `test` script for the package that changed:

```sh
$ lerna watch --scope="package-4" --include-dependencies -- lerna run test --scope=\$LERNA_PACKAGE_NAME
```

When using `npx`, the `-c` option must be used if also providing variables for substitution:

```sh
$ npx -c 'lerna watch -- echo \$LERNA_PACKAGE_NAME \$LERNA_FILE_CHANGES'
```

## Options

`lerna watch` accepts all [filter flags](https://www.npmjs.com/package/@lerna/filter-options). Filter flags can be used to select specific packages to watch. See the [examples](#examples) above.

### `--verbose`

Run `lerna watch` in verbose mode, where commands are logged before execution.
39 changes: 39 additions & 0 deletions core/lerna/commands/watch/command.js
@@ -0,0 +1,39 @@
// @ts-check

"use strict";

const { filterOptions } = require("@lerna/filter-options");

/**
* @see https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module
*/
exports.command = "watch";

exports.describe = "Runs a command whenever packages or their dependents change.";

exports.builder = (yargs) => {
yargs
.parserConfiguration({
"populate--": true,
"strip-dashed": true,
})
.option("command", { type: "string", hidden: true })
.option("verbose", {
type: "boolean",
description: "Run watch mode in verbose mode, where commands are logged before execution.",
})
.middleware((args) => {
const { "--": doubleDash } = args;
if (doubleDash && Array.isArray(doubleDash)) {
// eslint-disable-next-line no-param-reassign
args.command = doubleDash.join(" ");
}
}, true);

return filterOptions(yargs);
};

exports.handler = function handler(argv) {
// eslint-disable-next-line global-require
return require(".")(argv);
};
64 changes: 64 additions & 0 deletions core/lerna/commands/watch/index.js
@@ -0,0 +1,64 @@
// @ts-check

"use strict";

const { Command } = require("@lerna/command");
const { getFilteredPackages } = require("@lerna/filter-options");
const { ValidationError } = require("@lerna/validation-error");
const { watch } = require("nx/src/command-line/watch");
const { readNxJson } = require("nx/src/config/configuration");

module.exports = factory;

const getNxProjectNamesFromLernaPackageNames = (packageNames) => {
const nxJson = readNxJson();
const nxConfiguredNpmScope = nxJson.npmScope;

return nxConfiguredNpmScope
? packageNames.map((name) => name.replace(`@${nxConfiguredNpmScope}/`, ""))
: packageNames;
};

function factory(argv) {
return new WatchCommand(argv);
}

class WatchCommand extends Command {
get requiresGit() {
return false;
}

async initialize() {
if (!this.options.command) {
throw new ValidationError("ENOCOMMAND", "A command to execute is required");
}

this.filteredPackages = await getFilteredPackages(this.packageGraph, this.execOpts, this.options);

this.count = this.filteredPackages.length;
this.packagePlural = this.count === 1 ? "package" : "packages";
}

async execute() {
this.logger.info(
"watch",
"Executing command %j on changes in %d %s.",
this.options.command,
this.count,
this.packagePlural
);

const projectNames = getNxProjectNamesFromLernaPackageNames(this.filteredPackages.map((p) => p.name));

await watch({
command: this.options.command,
projectNameEnvName: "LERNA_PACKAGE_NAME",
fileChangesEnvName: "LERNA_FILE_CHANGES",
includeDependentProjects: false, // dependent projects are accounted for via lerna filter options
projects: projectNames,
verbose: this.options.verbose,
});
}
}

module.exports.WatchCommand = WatchCommand;
2 changes: 2 additions & 0 deletions core/lerna/index.js
Expand Up @@ -22,6 +22,7 @@ const versionCmd = require("@lerna/version/command");

const repairCmd = require("./commands/repair/command");
const addCachingCmd = require("./commands/add-caching/command");
const watchCmd = require("./commands/watch/command");

const pkg = require("./package.json");

Expand Down Expand Up @@ -51,6 +52,7 @@ function main(argv) {
.command(publishCmd)
.command(repairCmd)
.command(runCmd)
.command(watchCmd)
.command(versionCmd)
.parse(argv, context);
}
6 changes: 4 additions & 2 deletions core/lerna/package.json
Expand Up @@ -51,19 +51,21 @@
"@lerna/create": "file:../../commands/create",
"@lerna/diff": "file:../../commands/diff",
"@lerna/exec": "file:../../commands/exec",
"@lerna/filter-options": "file:../filter-options",
"@lerna/import": "file:../../commands/import",
"@lerna/info": "file:../../commands/info",
"@lerna/init": "file:../../commands/init",
"@lerna/link": "file:../../commands/link",
"@lerna/list": "file:../../commands/list",
"@lerna/publish": "file:../../commands/publish",
"@lerna/run": "file:../../commands/run",
"@lerna/validation-error": "file:../validation-error",
"@lerna/version": "file:../../commands/version",
"@nrwl/devkit": ">=14.8.6 < 16",
"@nrwl/devkit": ">=15.4.2 < 16",
"import-local": "^3.0.2",
"inquirer": "^8.2.4",
"npmlog": "^6.0.2",
"nx": ">=14.8.6 < 16",
"nx": ">=15.4.2 < 16",
"typescript": "^3 || ^4"
}
}
27 changes: 27 additions & 0 deletions core/lerna/schemas/lerna-schema.json
Expand Up @@ -1120,6 +1120,33 @@
"$ref": "#/$defs/filters/continueIfNoMatch"
}
}
},
"watch": {
"type": "object",
"description": "Options for the `watch` command.",
"properties": {
"scope": {
"$ref": "#/$defs/filters/scope"
},
"ignore": {
"$ref": "#/$defs/filters/ignore"
},
"private": {
"$ref": "#/$defs/filters/private"
},
"since": {
"$ref": "#/$defs/filters/since"
},
"excludeDependents": {
"$ref": "#/$defs/filters/excludeDependents"
},
"includeDependents": {
"$ref": "#/$defs/filters/includeDependents"
},
"includeDependencies": {
"$ref": "#/$defs/filters/includeDependencies"
}
}
}
}
},
Expand Down
18 changes: 18 additions & 0 deletions e2e/watch/.eslintrc.json
@@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
18 changes: 18 additions & 0 deletions e2e/watch/jest.config.ts
@@ -0,0 +1,18 @@
/* eslint-disable */
export default {
displayName: "e2e-watch",
preset: "../../jest.preset.js",
globals: {
"ts-jest": {
tsconfig: "<rootDir>/tsconfig.spec.json",
},
},
transform: {
"^.+\\.[tj]s$": "ts-jest",
},
moduleFileExtensions: ["ts", "js", "html"],
coverageDirectory: "../../coverage/e2e/watch",
maxWorkers: 1,
testTimeout: 60000,
setupFiles: ["<rootDir>/src/test-setup.ts"],
};
52 changes: 52 additions & 0 deletions e2e/watch/project.json
@@ -0,0 +1,52 @@
{
"name": "e2e-watch",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"tags": [],
"targets": {
"e2e": {
"executor": "nx:run-commands",
"options": {
"commands": [
{
"command": "npm run e2e-start-local-registry"
},
{
"command": "npm run e2e-build-package-publish"
},
{
"command": "E2E_ROOT=$(npx ts-node scripts/set-e2e-root.ts) nx run-e2e-tests e2e-watch"
}
],
"parallel": false
}
},
"run-e2e-tests-process": {
"executor": "nx:run-commands",
"options": {
"commands": [
{
"command": "E2E_ROOT=$(npx ts-node scripts/set-e2e-root.ts) nx run-e2e-tests e2e-watch",
"description": "This additional wrapper target exists so that we can ensure that the e2e tests run in a dedicated process with enough memory"
}
],
"parallel": false
}
},
"run-e2e-tests": {
"executor": "@nrwl/jest:jest",
"options": {
"jestConfig": "e2e/watch/jest.config.ts",
"passWithNoTests": true,
"runInBand": true
},
"outputs": ["{workspaceRoot}/coverage/e2e/watch"]
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["e2e/watch/**/*.ts"]
}
}
}
}
1 change: 1 addition & 0 deletions e2e/watch/src/test-setup.ts
@@ -0,0 +1 @@
jest.retryTimes(3);

0 comments on commit 008b995

Please sign in to comment.