From 96b0404b1686a6de2c5853eee301c1afda0d02b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CJamesHenry=E2=80=9D?= Date: Thu, 29 Sep 2022 19:31:07 +0100 Subject: [PATCH 1/3] feat(core): add add-caching command --- core/lerna/commands/add-caching/command.js | 19 +++ core/lerna/commands/add-caching/index.js | 155 +++++++++++++++++++++ core/lerna/index.js | 2 + core/lerna/package.json | 1 + package-lock.json | 2 + 5 files changed, 179 insertions(+) create mode 100644 core/lerna/commands/add-caching/command.js create mode 100644 core/lerna/commands/add-caching/index.js diff --git a/core/lerna/commands/add-caching/command.js b/core/lerna/commands/add-caching/command.js new file mode 100644 index 0000000000..33afc56c31 --- /dev/null +++ b/core/lerna/commands/add-caching/command.js @@ -0,0 +1,19 @@ +// @ts-check + +"use strict"; + +/** + * @see https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module + */ +exports.command = "add-caching"; + +exports.describe = "Interactive prompt to generate task runner configuration"; + +exports.builder = (yargs) => { + return yargs; +}; + +exports.handler = function handler(argv) { + // eslint-disable-next-line global-require + return require(".")(argv); +}; diff --git a/core/lerna/commands/add-caching/index.js b/core/lerna/commands/add-caching/index.js new file mode 100644 index 0000000000..05b033d8f9 --- /dev/null +++ b/core/lerna/commands/add-caching/index.js @@ -0,0 +1,155 @@ +// @ts-check + +"use strict"; + +const { Command } = require("@lerna/command"); +const { writeJsonFile, readJsonFile, workspaceRoot, joinPathFragments } = require("@nrwl/devkit"); +const inquirer = require("inquirer"); +const log = require("npmlog"); + +module.exports = factory; + +function factory(argv) { + return new AddCachingCommand(argv); +} + +class AddCachingCommand extends Command { + constructor(argv) { + super(argv, { skipValidations: true }); + } + + initialize() { + if (this.options.useNx === false) { + this.logger.error( + "add-caching", + "The `add-caching` command is only available when using the Nx task runner" + ); + process.exit(1); + } + + const packages = this.packageGraph?.rawPackageList || []; + const uniqueScriptNames = new Set(); + for (const pkg of packages) { + for (const scriptName of Object.keys(pkg.scripts || {})) { + uniqueScriptNames.add(scriptName); + } + } + this.uniqueScriptNames = Array.from(uniqueScriptNames); + } + + async execute() { + this.logger.info( + "add-caching", + "Please answer the following questions about the scripts found in your workspace in order to generate task runner configuration" + ); + process.stdout.write("\n"); + + log.pause(); + + const { targetDefaults } = await inquirer.prompt([ + { + type: "checkbox", + name: "targetDefaults", + message: "Which of the following scripts need to be run in deterministic/topoglogical order?\n", + choices: this.uniqueScriptNames, + }, + ]); + + const { cacheableOperations } = await inquirer.prompt([ + { + type: "checkbox", + name: "cacheableOperations", + message: + "Which of the following scripts are cacheable? (Produce the same output given the same input)\n", + choices: this.uniqueScriptNames, + }, + ]); + + const scriptOutputs = {}; + + for (const scriptName of cacheableOperations) { + // eslint-disable-next-line no-await-in-loop + scriptOutputs[scriptName] = await inquirer.prompt([ + { + type: "input", + name: scriptName, + message: `Where within a particular project directory does the "${scriptName}" script write its outputs?\n`, + default: "dist", + validate: (input) => { + if (!input) { + return "Please provide a project relative path"; + } + return true; + }, + }, + ]); + } + + log.resume(); + + process.stdout.write("\n"); + + this.convertAnswersToNxConfig({ cacheableOperations, targetDefaults, scriptOutputs }); + + this.logger.success("add-caching", "Successfully updated task runner configuration in `nx.json`"); + + this.logger.info( + "add-caching", + "Learn more about task runner configuration here: https://lerna.js.org/docs/concepts/task-pipeline-configuration" + ); + } + + convertAnswersToNxConfig(answers) { + const nxJsonPath = joinPathFragments(workspaceRoot, "nx.json"); + let nxJson = {}; + try { + nxJson = readJsonFile(nxJsonPath); + } catch (e) {} + + nxJson.tasksRunnerOptions = nxJson.tasksRunnerOptions || {}; + nxJson.tasksRunnerOptions.default = nxJson.tasksRunnerOptions.default || {}; + nxJson.tasksRunnerOptions.default.runner = + nxJson.tasksRunnerOptions.default.runner || "nx/tasks-runners/default"; + nxJson.tasksRunnerOptions.default.options = nxJson.tasksRunnerOptions.default.options || {}; + + if (nxJson.tasksRunnerOptions.default.options.cacheableOperations) { + this.logger.warn( + "add-caching", + "The `tasksRunnerOptions.default.cacheableOperations` property already exists in `nx.json` and will be overwritten by your answers" + ); + } + + nxJson.tasksRunnerOptions.default.options.cacheableOperations = answers.cacheableOperations; + + if (nxJson.targetDefaults) { + this.logger.warn( + "add-caching", + "The `targetDefaults` property already exists in `nx.json` and will be overwritten by your answers" + ); + } + + nxJson.targetDefaults = nxJson.targetDefaults || {}; + + for (const scriptName of answers.targetDefaults) { + nxJson.targetDefaults[scriptName] = nxJson.targetDefaults[scriptName] || {}; + nxJson.targetDefaults[scriptName] = { dependsOn: [`^${scriptName}`] }; + } + + for (const [scriptName, scriptAnswerData] of Object.entries(answers.scriptOutputs)) { + nxJson.targetDefaults[scriptName] = nxJson.targetDefaults[scriptName] || {}; + nxJson.targetDefaults[scriptName].outputs = [`{projectRoot}/${scriptAnswerData[scriptName]}`]; + } + + writeJsonFile(nxJsonPath, nxJson); + } + + // eslint-disable-next-line class-methods-use-this + normalizePathInput(pathInput) { + if (pathInput.startsWith("/")) { + return pathInput.substring(1); + } + return pathInput; + } +} + +module.exports.AddCachingCommand = AddCachingCommand; diff --git a/core/lerna/index.js b/core/lerna/index.js index a1bdc95de1..ee3751d8b9 100644 --- a/core/lerna/index.js +++ b/core/lerna/index.js @@ -21,6 +21,7 @@ const runCmd = require("@lerna/run/command"); const versionCmd = require("@lerna/version/command"); const repairCmd = require("./commands/repair/command"); +const addCachingCmd = require("./commands/add-caching/command"); const pkg = require("./package.json"); @@ -34,6 +35,7 @@ function main(argv) { // @ts-ignore return cli() .command(addCmd) + .command(addCachingCmd) .command(bootstrapCmd) .command(changedCmd) .command(cleanCmd) diff --git a/core/lerna/package.json b/core/lerna/package.json index cbfb887891..cf5c69b42e 100644 --- a/core/lerna/package.json +++ b/core/lerna/package.json @@ -61,6 +61,7 @@ "@lerna/version": "file:../../commands/version", "@nrwl/devkit": ">=14.8.1 < 16", "import-local": "^3.0.2", + "inquirer": "^8.2.4", "npmlog": "^6.0.2", "nx": ">=14.8.1 < 16", "typescript": "^3 || ^4" diff --git a/package-lock.json b/package-lock.json index e801fca045..4d47598504 100644 --- a/package-lock.json +++ b/package-lock.json @@ -552,6 +552,7 @@ "@lerna/version": "file:../../commands/version", "@nrwl/devkit": ">=14.8.1 < 16", "import-local": "^3.0.2", + "inquirer": "^8.2.4", "npmlog": "^6.0.2", "nx": ">=14.8.1 < 16", "typescript": "^3 || ^4" @@ -27362,6 +27363,7 @@ "@lerna/version": "file:../../commands/version", "@nrwl/devkit": ">=14.8.1 < 16", "import-local": "^3.0.2", + "inquirer": "^8.2.4", "npmlog": "^6.0.2", "nx": ">=14.8.1 < 16", "typescript": "^3 || ^4" From a9e9fe9344c0857dd88a3d8cd356549b6dfa5e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CJamesHenry=E2=80=9D?= Date: Thu, 29 Sep 2022 20:46:26 +0100 Subject: [PATCH 2/3] chore: fix lint errors --- core/lerna/commands/add-caching/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/lerna/commands/add-caching/index.js b/core/lerna/commands/add-caching/index.js index 05b033d8f9..6247dfa5fa 100644 --- a/core/lerna/commands/add-caching/index.js +++ b/core/lerna/commands/add-caching/index.js @@ -24,6 +24,7 @@ class AddCachingCommand extends Command { "add-caching", "The `add-caching` command is only available when using the Nx task runner" ); + // eslint-disable-next-line no-process-exit process.exit(1); } @@ -104,7 +105,8 @@ class AddCachingCommand extends Command { let nxJson = {}; try { nxJson = readJsonFile(nxJsonPath); - } catch (e) {} + // eslint-disable-next-line no-empty + } catch {} nxJson.tasksRunnerOptions = nxJson.tasksRunnerOptions || {}; nxJson.tasksRunnerOptions.default = nxJson.tasksRunnerOptions.default || {}; From aa16de09f27b7e58801e0e86d3c43ca347a4f34f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CJamesHenry=E2=80=9D?= Date: Thu, 29 Sep 2022 22:34:47 +0100 Subject: [PATCH 3/3] chore: update messaging --- core/lerna/commands/add-caching/index.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/core/lerna/commands/add-caching/index.js b/core/lerna/commands/add-caching/index.js index 6247dfa5fa..7879da7d44 100644 --- a/core/lerna/commands/add-caching/index.js +++ b/core/lerna/commands/add-caching/index.js @@ -22,7 +22,7 @@ class AddCachingCommand extends Command { if (this.options.useNx === false) { this.logger.error( "add-caching", - "The `add-caching` command is only available when using the Nx task runner" + "The `add-caching` command is only available when using the Nx task runner (do not set `useNx` to `false` in `lerna.json`)" ); // eslint-disable-next-line no-process-exit process.exit(1); @@ -61,7 +61,7 @@ class AddCachingCommand extends Command { type: "checkbox", name: "cacheableOperations", message: - "Which of the following scripts are cacheable? (Produce the same output given the same input)\n", + "Which of the following scripts are cacheable? (Produce the same output given the same input, e.g. build, test and lint usually are, serve and start are not)\n", choices: this.uniqueScriptNames, }, ]); @@ -74,14 +74,7 @@ class AddCachingCommand extends Command { { type: "input", name: scriptName, - message: `Where within a particular project directory does the "${scriptName}" script write its outputs?\n`, - default: "dist", - validate: (input) => { - if (!input) { - return "Please provide a project relative path"; - } - return true; - }, + message: `Does the "${scriptName}" script create any outputs? If not, leave blank, otherwise provide a path relative to a project root (e.g. dist, lib, build, coverage)\n`, }, ]); } @@ -98,6 +91,10 @@ class AddCachingCommand extends Command { "add-caching", "Learn more about task runner configuration here: https://lerna.js.org/docs/concepts/task-pipeline-configuration" ); + this.logger.info( + "add-caching", + "Note that the legacy task runner options of --sort, --no-sort and --parallel no longer apply. Learn more here: https://lerna.js.org/docs/recipes/using-lerna-powered-by-nx-to-run-tasks" + ); } convertAnswersToNxConfig(answers) { @@ -138,6 +135,10 @@ class AddCachingCommand extends Command { } for (const [scriptName, scriptAnswerData] of Object.entries(answers.scriptOutputs)) { + if (!scriptAnswerData[scriptName]) { + // eslint-disable-next-line no-continue + continue; + } nxJson.targetDefaults[scriptName] = nxJson.targetDefaults[scriptName] || {}; nxJson.targetDefaults[scriptName].outputs = [`{projectRoot}/${scriptAnswerData[scriptName]}`]; }