From 7a3e70bf8e166d2f10e0fecabc79ebc7830ab5ed Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Sun, 10 Jan 2021 21:06:22 +0100 Subject: [PATCH] feat(cli-plugin-workspaces): introduce workspace management commands --- package.json | 17 +- packages/cli-plugin-workspaces/CHANGELOG.md | 250 ++++++++++++++++++ packages/cli-plugin-workspaces/LICENSE | 21 ++ packages/cli-plugin-workspaces/README.md | 23 ++ .../cli-plugin-workspaces/commands/list.js | 33 +++ .../cli-plugin-workspaces/commands/run.js | 94 +++++++ .../cli-plugin-workspaces/commands/utils.js | 87 ++++++ packages/cli-plugin-workspaces/index.js | 77 ++++++ packages/cli-plugin-workspaces/package.json | 40 +++ tsconfig.build.json | 1 - webiny.root.js | 1 + 11 files changed, 634 insertions(+), 10 deletions(-) create mode 100644 packages/cli-plugin-workspaces/CHANGELOG.md create mode 100644 packages/cli-plugin-workspaces/LICENSE create mode 100644 packages/cli-plugin-workspaces/README.md create mode 100644 packages/cli-plugin-workspaces/commands/list.js create mode 100644 packages/cli-plugin-workspaces/commands/run.js create mode 100644 packages/cli-plugin-workspaces/commands/utils.js create mode 100644 packages/cli-plugin-workspaces/index.js create mode 100644 packages/cli-plugin-workspaces/package.json diff --git a/package.json b/package.json index 9d5abe42e82..a817cdb61a2 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "apps/site/code", "apps/theme", "api/code/fileManager/*", - "api/code/graphqlPlayground", "api/code/graphql", "api/code/headlessCMS", "api/code/pageBuilder/*", @@ -64,7 +63,7 @@ "flatten": "^1.0.2", "fs-extra": "^7.0.0", "get-stream": "^3.0.0", - "get-yarn-workspaces": "^1.0.2", + "get-yarn-ws": "^1.0.2", "git-cz": "^1.7.1", "glob": "^7.1.3", "globby": "^8.0.1", @@ -107,13 +106,13 @@ "check-ts-configs": "node scripts/checkTsConfigs.js", "eslint": "eslint \"**/*.{js,jsx,ts,tsx}\" --max-warnings=0", "eslint:fix": "yarn eslint --fix", - "build": "lerna run build --stream", - "build:apps": "lerna run build --scope=@webiny/app* --stream", - "build:api": "lerna run build --scope=@webiny/api* --scope=@webiny/handler* --stream", - "build:packages": "lerna run build --scope=@webiny/* --stream", - "watch:apps": "lerna run watch --scope=@webiny/app* --stream --parallel", - "watch:api": "lerna run watch --scope=@webiny/api* --stream --parallel", - "watch:packages": "lerna run watch --scope=@webiny/* --stream --parallel", + "build": "yarn webiny ws run build --folder=packages", + "build:apps": "yarn webiny ws run build --scope=@webiny/app*", + "build:api": "yarn webiny ws run build --scope=@webiny/api* --scope=@webiny/handler*", + "build:packages": "yarn webiny ws run build --folder=packages", + "watch:apps": "yarn webiny ws run watch --scope=@webiny/app*", + "watch:api": "yarn webiny ws run watch --scope=@webiny/api*", + "watch:packages": "yarn webiny ws run watch --folder=packages", "clear-dist": "yarn rimraf packages/*/dist", "contributors:add": "contreebutors add --username", "contributors:generate": "contreebutors render", diff --git a/packages/cli-plugin-workspaces/CHANGELOG.md b/packages/cli-plugin-workspaces/CHANGELOG.md new file mode 100644 index 00000000000..b5d460d6e86 --- /dev/null +++ b/packages/cli-plugin-workspaces/CHANGELOG.md @@ -0,0 +1,250 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [5.0.0-beta.52](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.51...v5.0.0-beta.52) (2021-01-08) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.51](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.50...v5.0.0-beta.51) (2021-01-08) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.50](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.49...v5.0.0-beta.50) (2021-01-08) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.49](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.48...v5.0.0-beta.49) (2021-01-08) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.48](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.47...v5.0.0-beta.48) (2021-01-08) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.47](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.46...v5.0.0-beta.47) (2021-01-08) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.46](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.45...v5.0.0-beta.46) (2021-01-08) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.45](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.44...v5.0.0-beta.45) (2021-01-08) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.44](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.43...v5.0.0-beta.44) (2021-01-08) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.36](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.35...v5.0.0-beta.36) (2021-01-08) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.35](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.34...v5.0.0-beta.35) (2021-01-08) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.34](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.33...v5.0.0-beta.34) (2021-01-08) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.33](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.32...v5.0.0-beta.33) (2021-01-08) + + +### Bug Fixes + +* remove any mention of MongoDb ([b3193cb](https://github.com/webiny/webiny-js/commit/b3193cb3016b58d8e6f95f7558af34a6f588b3c8)) + + + + + +# [5.0.0-beta.32](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.31...v5.0.0-beta.32) (2021-01-06) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.31](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.30...v5.0.0-beta.31) (2021-01-06) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.30](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.29...v5.0.0-beta.30) (2021-01-06) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.29](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.28...v5.0.0-beta.29) (2021-01-06) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.28](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.27...v5.0.0-beta.28) (2021-01-06) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.27](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.26...v5.0.0-beta.27) (2021-01-06) + + +### Bug Fixes + +* remove loading of env variables ([5b6b3db](https://github.com/webiny/webiny-js/commit/5b6b3dbc47cc2134fe03b5bf7fb1b0d4dc99aa5d)) + + + + + +# [5.0.0-beta.26](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.25...v5.0.0-beta.26) (2021-01-06) + + +### Bug Fixes + +* use loadEnvVariables ([10ed828](https://github.com/webiny/webiny-js/commit/10ed8282110f2b7226adb92bceb4a8acd0896cee)) + + + + + +# [5.0.0-beta.10](https://github.com/webiny/webiny-js/compare/v4.14.0...v5.0.0-beta.10) (2021-01-04) + + +### Bug Fixes + +* enable executing the command via plugin's `execute` property ([0000e8a](https://github.com/webiny/webiny-js/commit/0000e8afcb6f9d7af7d96effd90c050e28021b73)) +* remove progress bars and print commands output ([bedb072](https://github.com/webiny/webiny-js/commit/bedb072a80ee3e88ec416844b5f52826efbfb222)) +* rename `folder` to `path` ([43e7de0](https://github.com/webiny/webiny-js/commit/43e7de013ea2a161c5b670c9773995413a556477)) + + +### Features + +* add Elastic domain setup ([2205343](https://github.com/webiny/webiny-js/commit/220534345830ce216bc82667f2d3d6390df8d9e8)) +* introduce `cli-plugin-build` CLI plugin ([225040c](https://github.com/webiny/webiny-js/commit/225040c39f3b19f267bc99c33915f8023d3dbb46)) + + + + + +# [5.0.0-beta.1](https://github.com/webiny/webiny-js/compare/v5.0.0-beta.0...v5.0.0-beta.1) (2021-01-03) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0-beta.0](https://github.com/webiny/webiny-js/compare/v4.14.0...v5.0.0-beta.0) (2021-01-03) + + +### Bug Fixes + +* enable executing the command via plugin's `execute` property ([0000e8a](https://github.com/webiny/webiny-js/commit/0000e8afcb6f9d7af7d96effd90c050e28021b73)) +* remove progress bars and print commands output ([bedb072](https://github.com/webiny/webiny-js/commit/bedb072a80ee3e88ec416844b5f52826efbfb222)) +* rename `folder` to `path` ([43e7de0](https://github.com/webiny/webiny-js/commit/43e7de013ea2a161c5b670c9773995413a556477)) + + +### Features + +* add Elastic domain setup ([2205343](https://github.com/webiny/webiny-js/commit/220534345830ce216bc82667f2d3d6390df8d9e8)) +* introduce `cli-plugin-build` CLI plugin ([225040c](https://github.com/webiny/webiny-js/commit/225040c39f3b19f267bc99c33915f8023d3dbb46)) + + + + + +## [5.0.1-beta.1](https://github.com/webiny/webiny-js/compare/v5.0.1-beta.0...v5.0.1-beta.1) (2021-01-03) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +## [5.0.1-beta.0](https://github.com/webiny/webiny-js/compare/v5.0.0...v5.0.1-beta.0) (2021-01-03) + +**Note:** Version bump only for package @webiny/cli-plugin-build + + + + + +# [5.0.0](https://github.com/webiny/webiny-js/compare/v4.14.0...v5.0.0) (2021-01-03) + + +### Bug Fixes + +* enable executing the command via plugin's `execute` property ([0000e8a](https://github.com/webiny/webiny-js/commit/0000e8afcb6f9d7af7d96effd90c050e28021b73)) +* remove progress bars and print commands output ([bedb072](https://github.com/webiny/webiny-js/commit/bedb072a80ee3e88ec416844b5f52826efbfb222)) +* rename `folder` to `path` ([43e7de0](https://github.com/webiny/webiny-js/commit/43e7de013ea2a161c5b670c9773995413a556477)) + + +### Features + +* add Elastic domain setup ([2205343](https://github.com/webiny/webiny-js/commit/220534345830ce216bc82667f2d3d6390df8d9e8)) +* introduce `cli-plugin-build` CLI plugin ([225040c](https://github.com/webiny/webiny-js/commit/225040c39f3b19f267bc99c33915f8023d3dbb46)) diff --git a/packages/cli-plugin-workspaces/LICENSE b/packages/cli-plugin-workspaces/LICENSE new file mode 100644 index 00000000000..f772d04d4db --- /dev/null +++ b/packages/cli-plugin-workspaces/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Webiny + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/cli-plugin-workspaces/README.md b/packages/cli-plugin-workspaces/README.md new file mode 100644 index 00000000000..0f15a1399ac --- /dev/null +++ b/packages/cli-plugin-workspaces/README.md @@ -0,0 +1,23 @@ +# @webiny/cli-plugin-build +[![](https://img.shields.io/npm/dw/@webiny/cli-plugin-build.svg)](https://www.npmjs.com/package/@webiny/cli-plugin-build) +[![](https://img.shields.io/npm/v/@webiny/cli-plugin-build.svg)](https://www.npmjs.com/package/@webiny/cli-plugin-build) +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) + +A set of @webiny/cli plugins to deploy Webiny project using serverless components. + +## Install +``` +yarn add @webiny/cli-plugin-build +``` + +Add plugin to your project by editing `webiny.root.js`: + +```js +module.exports = { + projectName: "my-project", + cli: { + plugins: ["@webiny/cli-plugin-build"] + } +}; +``` diff --git a/packages/cli-plugin-workspaces/commands/list.js b/packages/cli-plugin-workspaces/commands/list.js new file mode 100644 index 00000000000..1da2971942e --- /dev/null +++ b/packages/cli-plugin-workspaces/commands/list.js @@ -0,0 +1,33 @@ +const { join } = require("path"); +const chalk = require("chalk"); +const { allPackages } = require("@webiny/project-utils/packages"); + +const outputJSON = obj => { + console.log(JSON.stringify(obj, null, 2)); +}; + +module.exports = async ({ json, withPath }) => { + const packages = allPackages().reduce((acc, folder) => { + const json = require(join(folder, "package.json")); + acc[json.name] = folder; + return acc; + }, {}); + + if (json) { + // `withPath` outputs a key => value object, containing workspace name and absolute path + if (withPath) { + outputJSON(packages); + } else { + outputJSON(Object.keys(packages)); + } + return; + } + + Object.keys(packages).forEach(name => { + if (withPath) { + console.log(`${chalk.green(name)} (${chalk.blue(packages[name])})`); + } else { + console.log(chalk.green(name)); + } + }); +}; diff --git a/packages/cli-plugin-workspaces/commands/run.js b/packages/cli-plugin-workspaces/commands/run.js new file mode 100644 index 00000000000..1dc5ce22690 --- /dev/null +++ b/packages/cli-plugin-workspaces/commands/run.js @@ -0,0 +1,94 @@ +const chalk = require("chalk"); +const randomColor = require("random-color"); +const execa = require("execa"); +const pMap = require("p-map"); +const { createGraph, getPackages, normalizeArray } = require("./utils"); + +const logLine = prefix => data => { + const line = data.toString().replace(/\s\s*$/gm, ""); + console.log(`${prefix}: ${line.replace("webiny: ", "")}`); +}; + +module.exports = async (inputs, context) => { + if (inputs.script === "watch") { + inputs.parallel = true; + } + const { script, scope, folder, parallel, stream } = inputs; + const scopes = normalizeArray(scope); + const folders = normalizeArray(folder); + + const runScript = pkg => { + return new Promise((resolve, reject) => { + const color = randomColor().hexString(); + const prefix = chalk.hex(color).bold(pkg.name); + const logger = logLine(prefix); + const process = execa("yarn", [script], { cwd: pkg.path }); + + if (stream) { + process.stdout.on("data", logger); + process.stderr.on("data", logger); + } + + process.on("exit", code => { + if (code === 0) { + return resolve(); + } + + context.error(`Script failed in package ${prefix}!`); + + reject(); + }); + }); + }; + + const running = new Set(); + + const runScriptTopologically = async (packages, graph) => { + const leafs = graph.sinks().filter(leaf => !running.has(leaf)); + if (!leafs.length) { + return; + } + + await pMap(leafs, name => { + running.add(name); + return runScript(packages.find(pkg => pkg.name === name)).then(() => { + graph.removeNode(name); + return runScriptTopologically(packages, graph); + }); + }); + }; + + const packages = getPackages({ script, scopes, folders }); + + if (!packages.length) { + context.info(`No workspaces satisfy the criteria to run the script!`); + context.info( + `Either the script ${chalk.green( + script + )} doesn't exist or none of the workspaces matched the filter.` + ); + return; + } + + if (!scopes.length) { + context.info(`Running %o in %o packages`, script, packages.length); + } else { + context.info( + `Running %o in %o packages %O`, + script, + packages.length, + packages.map(pkg => pkg.name) + ); + } + + if (parallel) { + context.info(`Running %o in parallel`, script); + await pMap(packages, pkg => runScript(pkg)); + return; + } + + // Build dependency graph + const graph = createGraph(packages); + + await runScriptTopologically(packages, graph); +}; diff --git a/packages/cli-plugin-workspaces/commands/utils.js b/packages/cli-plugin-workspaces/commands/utils.js new file mode 100644 index 00000000000..cb9390cbc66 --- /dev/null +++ b/packages/cli-plugin-workspaces/commands/utils.js @@ -0,0 +1,87 @@ +const { Graph, alg } = require("graphlib"); +const { join, resolve } = require("path"); +const multimatch = require("multimatch"); +const { allPackages } = require("@webiny/project-utils/packages"); + +const createGraph = packages => { + const graph = new Graph(); + const packageNames = packages.map(pkg => pkg.name); + + packages.forEach(({ json }) => { + graph.setNode(json.name, json); + }); + + packages.forEach(({ json }) => { + if (!json.dependencies && !json.devDependencies) { + return; + } + + [...Object.keys(json.dependencies || {}), ...Object.keys(json.devDependencies || {})].forEach(name => { + if (packageNames.includes(name)) { + graph.setEdge(json.name, name); + } + }); + }); + + validateGraph(graph); + + return graph; +}; + +const validateGraph = graph => { + const isAcyclic = alg.isAcyclic(graph); + if (!isAcyclic) { + const cycles = alg.findCycles(graph); + const msg = ["Your packages have circular dependencies:"]; + cycles.forEach((cycle, index) => { + let fromAToB = cycle.join(" --> "); + fromAToB = `${index + 1}. ${fromAToB}`; + const fromBToA = cycle.reverse().join(" <-- "); + const padLength = fromAToB.length + 4; + msg.push(fromAToB.padStart(padLength)); + msg.push(fromBToA.padStart(padLength)); + }, cycles); + throw new Error(msg.join("\n")); + } +}; + +const getPackages = ({ script, folders, scopes }) => { + return allPackages() + .filter(pkgPath => { + if (!folders.length) { + return true; + } + // Check if workspace path starts with any of the requested folders + return folders.some(folder => pkgPath.startsWith(resolve(folder))); + }) + .map(folder => { + const json = require(join(folder, "package.json")); + return { + json, + name: json.name, + path: folder + }; + }) + .filter(pkg => { + return Boolean(pkg.json.scripts && pkg.json.scripts[script]); + }) + .filter(pkg => { + if (!scopes.length) { + return true; + } + + const [match] = multimatch(pkg.name, scopes); + + return Boolean(match); + }); +}; + +const normalizeArray = value => { + return Array.isArray(value) ? value : [value].filter(Boolean); +}; + +module.exports = { + createGraph, + getPackages, + normalizeArray +}; diff --git a/packages/cli-plugin-workspaces/index.js b/packages/cli-plugin-workspaces/index.js new file mode 100644 index 00000000000..c6664a03f9e --- /dev/null +++ b/packages/cli-plugin-workspaces/index.js @@ -0,0 +1,77 @@ +module.exports = (options = {}) => ({ + type: "cli-command", + name: "cli-command-workspaces", + create({ yargs, context }) { + yargs.command( + ["workspaces ", "ws"], + `Tools to work with project workspaces.`, + wsCommand => { + wsCommand.command( + "list", + `List all project workspaces.`, + command => { + command.option("json", { + required: false, + describe: `Output as JSON.`, + type: "boolean", + default: false + }); + + command.option("withPath", { + required: false, + describe: `Includes full workspace path in the output.`, + type: "boolean", + default: false + }); + + command.example("$0 workspaces list"); + }, + async argv => { + await require("./commands/list")({ ...argv, options }, context); + process.exit(0); + } + ); + + wsCommand.command( + "run