diff --git a/.github/workflows/pullRequests.yml b/.github/workflows/pullRequests.yml index ba83a616cb3..e4dfa42ee24 100644 --- a/.github/workflows/pullRequests.yml +++ b/.github/workflows/pullRequests.yml @@ -3,7 +3,7 @@ # and run "github-actions-wac build" (or "ghawac build") to regenerate this file. # For more information, run "github-actions-wac --help". name: Pull Requests -'on': pull_request +"on": pull_request concurrency: group: pr-${{ github.event.pull_request.number }} cancel-in-progress: true @@ -19,7 +19,7 @@ jobs: - uses: webiny/action-conventional-commits@v1.3.0 runs-on: ubuntu-latest env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false validateCommitsDev: name: Validate commit messages (dev branch, 'feat' commits not allowed) @@ -34,7 +34,7 @@ jobs: allowed-commit-types: fix,docs,style,refactor,test,build,perf,ci,chore,revert,merge,wip runs-on: ubuntu-latest env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false constants: name: Create constants @@ -87,7 +87,7 @@ jobs: $GITHUB_OUTPUT runs-on: ubuntu-latest env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false assignMilestone: name: Assign milestone @@ -117,7 +117,7 @@ jobs: milestone: ${{ steps.get-milestone-to-assign.outputs.milestone }} runs-on: ubuntu-latest env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false build: name: Build @@ -149,7 +149,7 @@ jobs: path: ${{ github.base_ref }}/.webiny/cached-packages key: ${{ needs.constants.outputs.run-cache-key }} env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false staticCodeAnalysis: needs: @@ -190,7 +190,7 @@ jobs: working-directory: ${{ github.base_ref }} runs-on: ubuntu-latest env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false staticCodeAnalysisTs: name: Static code analysis (TypeScript) @@ -216,7 +216,7 @@ jobs: run: yarn cy:ts working-directory: ${{ github.base_ref }} env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false jestTestsNoStorageConstants: needs: @@ -244,7 +244,7 @@ jobs: echo '${{ steps.list-packages-to-jest-test.outputs.packages-to-jest-test }}' env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false jestTestsNoStorageRun: needs: @@ -265,7 +265,7 @@ jobs: }} runs-on: ${{ matrix.os }} env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false AWS_REGION: eu-central-1 if: needs.jestTestsNoStorageConstants.outputs.packages-to-jest-test != '[]' @@ -363,7 +363,7 @@ jobs: echo '${{ steps.list-packages-to-jest-test.outputs.packages-to-jest-test }}' env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false jestTestsddbRun: needs: @@ -383,7 +383,7 @@ jobs: fromJson(needs.jestTestsddbConstants.outputs.packages-to-jest-test) }} runs-on: ${{ matrix.os }} env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false AWS_REGION: eu-central-1 if: needs.jestTestsddbConstants.outputs.packages-to-jest-test != '[]' @@ -480,7 +480,7 @@ jobs: echo '${{ steps.list-packages-to-jest-test.outputs.packages-to-jest-test }}' env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false jestTestsddb-esRun: needs: @@ -501,7 +501,7 @@ jobs: }} runs-on: ${{ matrix.os }} env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false AWS_REGION: eu-central-1 AWS_ELASTIC_SEARCH_DOMAIN_NAME: ${{ secrets.AWS_ELASTIC_SEARCH_DOMAIN_NAME }} @@ -610,7 +610,7 @@ jobs: echo '${{ steps.list-packages-to-jest-test.outputs.packages-to-jest-test }}' env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false jestTestsddb-osRun: needs: @@ -631,7 +631,7 @@ jobs: }} runs-on: ${{ matrix.os }} env: - NODE_OPTIONS: '--max_old_space_size=4096' + NODE_OPTIONS: "--max_old_space_size=4096" YARN_ENABLE_IMMUTABLE_INSTALLS: false AWS_REGION: eu-central-1 AWS_ELASTIC_SEARCH_DOMAIN_NAME: ${{ secrets.AWS_OPEN_SEARCH_DOMAIN_NAME }} diff --git a/packages/cli/bin.js b/packages/cli/bin.js index 8ac8b98cbcd..e5c5b4db738 100755 --- a/packages/cli/bin.js +++ b/packages/cli/bin.js @@ -1,44 +1,100 @@ #!/usr/bin/env node -"use strict"; -const chalk = require("chalk"); -const execa = require("execa"); -const semver = require("semver"); -const currentNodeVersion = process.versions.node; +// Suppress punycode warnings. This is a known issue which we can't fix. +require("./utils/suppressPunycodeWarnings"); + +// Ensure system requirements are met. +require("@webiny/system-requirements").ensureSystemRequirements(); + +const yargs = require("yargs"); + +// Disable help processing until after plugins are imported. +yargs.help(false); + +// Loads environment variables from multiple sources. +require("./utils/loadEnvVariables"); + +const { blue, red, bold, bgYellow } = require("chalk"); +const context = require("./context"); +const { createCommands } = require("./commands"); + +yargs + .usage("Usage: $0 [options]") + .demandCommand(1) + .recommendCommands() + .scriptName("webiny") + .epilogue( + `To find more information, docs and tutorials, see ${blue("https://www.webiny.com/docs")}.` + ) + .epilogue(`Want to contribute? ${blue("https://github.com/webiny/webiny-js")}.`) + .fail(function (msg, error, yargs) { + if (msg) { + if (msg.includes("Not enough non-option arguments")) { + console.log(); + context.error(red("Command was not invoked as expected!")); + context.info( + `Some non-optional arguments are missing. See the usage examples printed below.` + ); + console.log(); + yargs.showHelp(); + return; + } + + if (msg.includes("Missing required argument")) { + const args = msg + .split(":")[1] + .split(",") + .map(v => v.trim()); + + console.log(); + context.error(red("Command was not invoked as expected!")); + context.info( + `Missing required argument(s): ${args + .map(arg => red(arg)) + .join(", ")}. See the usage examples printed below.` + ); + console.log(); + yargs.showHelp(); + return; + } + console.log(); + context.error(red("Command execution was aborted!")); + context.error(msg); + console.log(); -(async () => { - if (!semver.satisfies(currentNodeVersion, ">=20")) { - console.error( - chalk.red( - [ - `You are running Node.js ${currentNodeVersion}, but Webiny requires version >=20.`, - `Please switch to one of the required versions and try again.`, - `For more information, please visit https://www.webiny.com/docs/get-started/install-webiny#prerequisites.` - ].join(" ") - ) - ); - process.exit(1); - } - - try { - const { stdout } = await execa("yarn", ["--version"]); - /** - * TODO In 5.43.0 put >=4 as yarn version. - * This is because of the existing yarn version (before doing the webiny upgrade) is v3.x.x. - * When the upgrade is done (5.42.0), we can safely put to >=4. - */ - const satisfiesYarnVersion = ">=3"; - if (!semver.satisfies(stdout, satisfiesYarnVersion)) { - console.error(chalk.red(`"@webiny/cli" requires yarn 4!`)); process.exit(1); } - } catch (err) { - console.error(chalk.red(`"@webiny/cli" requires yarn 4!`)); - console.log( - `Run ${chalk.blue("yarn set version 4.5.3")} to install a compatible version of yarn.` - ); + + console.log(); + // Unfortunately, yargs doesn't provide passed args here, so we had to do it via process.argv. + const debugEnabled = process.argv.includes("--debug"); + if (debugEnabled) { + context.debug(error); + } else { + context.error(error.message); + } + + const gracefulError = error.cause?.gracefulError; + if (gracefulError instanceof Error) { + console.log(); + console.log(bgYellow(bold("💡 How can I resolve this?"))); + console.log(gracefulError.message); + } + + const plugins = context.plugins.byType("cli-command-error"); + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + plugin.handle({ + error, + context + }); + } + process.exit(1); - } + }); - require("./cli"); +(async () => { + await createCommands(yargs, context); + // Enable help and run the CLI. + yargs.help().argv; })(); diff --git a/packages/cli/cli.js b/packages/cli/cli.js deleted file mode 100644 index fa20770436b..00000000000 --- a/packages/cli/cli.js +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env node -const yargs = require("yargs"); - -// Disable help processing until after plugins are imported. -yargs.help(false); - -// Suppress punycode warnings. This is a known issue which we can't fix. -require("./utils/suppressPunycodeWarnings"); - -// Loads environment variables from multiple sources. -require("./utils/loadEnvVariables"); - -const { blue, red, bold, bgYellow } = require("chalk"); -const context = require("./context"); -const { createCommands } = require("./commands"); - -yargs - .usage("Usage: $0 [options]") - .demandCommand(1) - .recommendCommands() - .scriptName("webiny") - .epilogue( - `To find more information, docs and tutorials, see ${blue("https://www.webiny.com/docs")}.` - ) - .epilogue(`Want to contribute? ${blue("https://github.com/webiny/webiny-js")}.`) - .fail(function (msg, error, yargs) { - if (msg) { - if (msg.includes("Not enough non-option arguments")) { - console.log(); - context.error(red("Command was not invoked as expected!")); - context.info( - `Some non-optional arguments are missing. See the usage examples printed below.` - ); - console.log(); - yargs.showHelp(); - return; - } - - if (msg.includes("Missing required argument")) { - const args = msg - .split(":")[1] - .split(",") - .map(v => v.trim()); - - console.log(); - context.error(red("Command was not invoked as expected!")); - context.info( - `Missing required argument(s): ${args - .map(arg => red(arg)) - .join(", ")}. See the usage examples printed below.` - ); - console.log(); - yargs.showHelp(); - return; - } - console.log(); - context.error(red("Command execution was aborted!")); - context.error(msg); - console.log(); - - process.exit(1); - } - - console.log(); - // Unfortunately, yargs doesn't provide passed args here, so we had to do it via process.argv. - const debugEnabled = process.argv.includes("--debug"); - if (debugEnabled) { - context.debug(error); - } else { - context.error(error.message); - } - - const gracefulError = error.cause?.gracefulError; - if (gracefulError instanceof Error) { - console.log(); - console.log(bgYellow(bold("💡 How can I resolve this?"))); - console.log(gracefulError.message); - } - - const plugins = context.plugins.byType("cli-command-error"); - for (let i = 0; i < plugins.length; i++) { - const plugin = plugins[i]; - plugin.handle({ - error, - context - }); - } - - process.exit(1); - }); - -(async () => { - await createCommands(yargs, context); - // Enable help and run the CLI. - yargs.help().argv; -})(); diff --git a/packages/cli/commands/about/getNpmVersion.js b/packages/cli/commands/about/getNpmVersion.js index 37fd068a2f2..b168534ae3a 100644 --- a/packages/cli/commands/about/getNpmVersion.js +++ b/packages/cli/commands/about/getNpmVersion.js @@ -1,10 +1,5 @@ -const execa = require("execa"); +const { SystemRequirements } = require("@webiny/system-requirements"); module.exports.getNpmVersion = async () => { - try { - const { stdout } = await execa("npm", ["--version"]); - return stdout; - } catch (err) { - return ""; - } + return SystemRequirements.getNpmVersion(); }; diff --git a/packages/cli/commands/about/getNpxVersion.js b/packages/cli/commands/about/getNpxVersion.js index 64ca3d15674..80d71662f8d 100644 --- a/packages/cli/commands/about/getNpxVersion.js +++ b/packages/cli/commands/about/getNpxVersion.js @@ -1,10 +1,5 @@ -const execa = require("execa"); +const { SystemRequirements } = require("@webiny/system-requirements"); module.exports.getNpxVersion = async () => { - try { - const { stdout } = await execa("npx", ["--version"]); - return stdout; - } catch (err) { - return ""; - } + return SystemRequirements.getNpxVersion(); }; diff --git a/packages/cli/commands/about/getYarnVersion.js b/packages/cli/commands/about/getYarnVersion.js index 8fbf76a0a81..b87844255a1 100644 --- a/packages/cli/commands/about/getYarnVersion.js +++ b/packages/cli/commands/about/getYarnVersion.js @@ -1,10 +1,5 @@ -const execa = require("execa"); +const { SystemRequirements } = require("@webiny/system-requirements"); module.exports.getYarnVersion = async () => { - try { - const { stdout } = await execa("yarn", ["--version"]); - return stdout; - } catch (err) { - return ""; - } + return SystemRequirements.getYarnVersion(); }; diff --git a/packages/cli/package.json b/packages/cli/package.json index ff314a27fc9..dbaa7710d9f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -13,6 +13,7 @@ "author": "Pavel Denisjuk ", "description": "A tool to bootstrap a Webiny project.", "dependencies": { + "@webiny/system-requirements": "0.0.0", "@webiny/telemetry": "0.0.0", "@webiny/wcp": "0.0.0", "boolean": "^3.2.0", diff --git a/packages/create-webiny-project/bin.js b/packages/create-webiny-project/bin.js index e8f3ee8c08b..112420413cf 100755 --- a/packages/create-webiny-project/bin.js +++ b/packages/create-webiny-project/bin.js @@ -1,87 +1,107 @@ #!/usr/bin/env node -"use strict"; -const semver = require("semver"); -const chalk = require("chalk"); -const getYarnVersion = require("./utils/getYarnVersion"); -const getNpmVersion = require("./utils/getNpmVersion"); -const verifyConfig = require("./utils/verifyConfig"); +// Ensure system requirements are met. +require("@webiny/system-requirements").ensureSystemRequirements(); -(async () => { - const minNpmVersion = "10"; - const minYarnVersion = "1.22.21"; - /** - * Node - */ - const nodeVersion = process.versions.node; - if (!semver.satisfies(nodeVersion, `>=20`)) { - console.error( - chalk.red( - [ - `You are running Node.js ${nodeVersion}, but Webiny requires version >=20.`, - `Please switch to one of the required versions and try again.`, - "For more information, please visit https://www.webiny.com/docs/get-started/install-webiny#prerequisites." - ].join(" ") - ) - ); - process.exit(1); - } - /** - * npm - */ - try { - const npmVersion = await getNpmVersion(); - if (!semver.satisfies(npmVersion, `>=${minNpmVersion}`)) { - console.error( - chalk.red( - [ - `Webiny requires npm@^${minNpmVersion} or higher.`, - `Please run ${chalk.green( - "npm install npm@latest -g" - )}, to get the latest version.` - ].join("\n") - ) - ); - process.exit(1); - } - } catch (err) { - console.error(chalk.red(`Webiny depends on "npm".`)); +// Verify `.webiny` config file and continue. +require("./utils/ensureConfig").ensureConfig(); - console.log( - `Please visit https://docs.npmjs.com/try-the-latest-stable-version-of-npm to install ${chalk.green( - "npm" - )}.` - ); +const yargs = require("yargs"); +const packageJson = require("./package.json"); +const createProject = require("./utils/createProject"); - process.exit(1); - } +process.on("unhandledRejection", err => { + throw err; +}); - /** - * yarn - */ - try { - const yarnVersion = await getYarnVersion(); - if (!semver.satisfies(yarnVersion, `>=${minYarnVersion}`)) { - console.error( - chalk.red( - [ - `Webiny requires yarn@^${minYarnVersion} or higher.`, - `Please visit https://yarnpkg.com/ to install ${chalk.green("yarn")}.` - ].join("\n") - ) - ); - process.exit(1); +yargs + .usage("Usage: create-webiny-project [options]") + .version(packageJson.version) + .demandCommand(1) + .help() + .alias("help", "h") + .scriptName("create-webiny-project") + .fail(function (msg, err) { + if (msg) { + console.log(msg); + } + if (err) { + console.log(err); } - } catch (err) { - console.error( - chalk.red(`Webiny depends on "yarn" and its built-in support for workspaces.`) - ); - - console.log(`Please visit https://yarnpkg.com/ to install ${chalk.green("yarn")}.`); - process.exit(1); - } + }); + +// noinspection BadExpressionStatementJS +yargs.command( + "$0 [options]", + "Name of application and template to use", + yargs => { + yargs.positional("project-name", { + describe: "Project name" + }); + yargs.option("force", { + describe: "All project creation within an existing folder", + default: false, + type: "boolean", + demandOption: false + }); + yargs.option("template", { + describe: `Name of template to use, if no template is provided it will default to "aws" template`, + alias: "t", + type: "string", + default: "aws", + demandOption: false + }); + yargs.option("template-options", { + describe: `A JSON containing template-specific options (usually used in non-interactive environments)`, + default: null, + type: "string", + demandOption: false + }); + yargs.option("assign-to-yarnrc", { + describe: `A JSON containing additional options that will be assigned into the "yarnrc.yml" configuration file`, + default: null, + type: "string", + demandOption: false + }); + yargs.option("tag", { + describe: "NPM tag to use for @webiny packages", + type: "string", + default: "latest", + demandOption: false + }); + yargs.option("interactive", { + describe: "Enable interactive mode for all commands", + default: true, + type: "boolean", + demandOption: false + }); + yargs.option("log", { + describe: + "Creates a log file to see output of installation. Defaults to create-webiny-project-logs.txt in current directory", + alias: "l", + default: "create-webiny-project-logs.txt", + type: "string", + demandOption: false + }); + yargs.option("debug", { + describe: "Turn on debug logs", + default: false, + type: "boolean", + demandOption: false + }); + yargs.option("cleanup", { + describe: "If an error occurs upon project creation, deletes all generated files", + alias: "c", + default: true, + type: "boolean", + demandOption: false + }); - await verifyConfig(); - require("./index"); -})(); + yargs.example("$0 "); + yargs.example("$0 --template=aws"); + yargs.example("$0 --template=../path/to/template"); + yargs.example("$0 --log=./my-logs.txt"); + }, + argv => createProject(argv) +).argv; diff --git a/packages/create-webiny-project/package.json b/packages/create-webiny-project/package.json index 291c1b5fe57..f6fbd2ff9ec 100644 --- a/packages/create-webiny-project/package.json +++ b/packages/create-webiny-project/package.json @@ -13,6 +13,7 @@ "author": "Webiny Ltd.", "license": "MIT", "dependencies": { + "@webiny/system-requirements": "0.0.0", "@webiny/telemetry": "0.0.0", "chalk": "^4.1.2", "execa": "^5.1.1", diff --git a/packages/create-webiny-project/utils/verifyConfig.js b/packages/create-webiny-project/utils/ensureConfig.js similarity index 75% rename from packages/create-webiny-project/utils/verifyConfig.js rename to packages/create-webiny-project/utils/ensureConfig.js index 12ec508ba48..6d08a2a9660 100644 --- a/packages/create-webiny-project/utils/verifyConfig.js +++ b/packages/create-webiny-project/utils/ensureConfig.js @@ -5,12 +5,12 @@ const writeJson = require("write-json-file"); const configPath = path.join(os.homedir(), ".webiny", "config"); -module.exports = async () => { +const ensureConfig = () => { // Check user ID try { - const config = await readJson(configPath); + const config = readJson.sync(configPath); if (!config.id) { - throw Error("Invalid Webiny config!"); + throw Error("Invalid Webiny config."); } } catch (e) { const { v4: uuidv4 } = require("uuid"); @@ -18,3 +18,5 @@ module.exports = async () => { writeJson.sync(configPath, { id: uuidv4(), telemetry: true }); } }; + +module.exports = { ensureConfig }; diff --git a/packages/create-webiny-project/utils/getNpmVersion.js b/packages/create-webiny-project/utils/getNpmVersion.js deleted file mode 100644 index b207efe26e5..00000000000 --- a/packages/create-webiny-project/utils/getNpmVersion.js +++ /dev/null @@ -1,10 +0,0 @@ -const execa = require("execa"); - -module.exports = async () => { - try { - const { stdout } = await execa("npm", ["--version"]); - return stdout; - } catch (err) { - return ""; - } -}; diff --git a/packages/create-webiny-project/utils/getYarnVersion.js b/packages/create-webiny-project/utils/getYarnVersion.js deleted file mode 100644 index 271aac3fa56..00000000000 --- a/packages/create-webiny-project/utils/getYarnVersion.js +++ /dev/null @@ -1,10 +0,0 @@ -const execa = require("execa"); - -module.exports = async () => { - try { - const { stdout } = await execa("yarn", ["--version"]); - return stdout; - } catch (err) { - return ""; - } -}; diff --git a/packages/system-requirements/LICENSE b/packages/system-requirements/LICENSE new file mode 100644 index 00000000000..f772d04d4db --- /dev/null +++ b/packages/system-requirements/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/system-requirements/README.md b/packages/system-requirements/README.md new file mode 100644 index 00000000000..2c5f8cce665 --- /dev/null +++ b/packages/system-requirements/README.md @@ -0,0 +1,6 @@ +# `@webiny/system-requirements` + +This package contains utilities for checking system requirements for Webiny projects. + +> [!NOTE] +> This package is included in every Webiny project by default, and it's not meant to be used as a standalone package. diff --git a/packages/system-requirements/SystemRequirements.js b/packages/system-requirements/SystemRequirements.js new file mode 100644 index 00000000000..dcb04b2cc2d --- /dev/null +++ b/packages/system-requirements/SystemRequirements.js @@ -0,0 +1,77 @@ +const semver = require("semver"); +const execa = require("execa"); +const constraints = require("./constraints.json"); + +class SystemRequirements { + static validate() { + const nodeVersion = SystemRequirements.getNodeVersion(); + const yarnVersion = SystemRequirements.getYarnVersion(); + const npmVersion = SystemRequirements.getNpmVersion(); + const npxVersion = SystemRequirements.getNpxVersion(); + + const systemRequirements = { + valid: false, + node: { + currentVersion: nodeVersion, + requiredVersion: constraints.node, + valid: semver.satisfies(nodeVersion, constraints.node) + }, + npm: { + currentVersion: npmVersion, + requiredVersion: constraints.npm, + valid: semver.satisfies(npmVersion, constraints.npm) + }, + npx: { + currentVersion: npxVersion, + requiredVersion: constraints.npx, + valid: semver.satisfies(npxVersion, constraints.npx) + }, + yarn: { + currentVersion: yarnVersion, + requiredVersion: constraints.yarn, + valid: semver.satisfies(yarnVersion, constraints.yarn) + } + }; + + systemRequirements.valid = + systemRequirements.node.valid && + systemRequirements.npm.valid && + systemRequirements.npx.valid && + systemRequirements.yarn.valid; + + return systemRequirements; + } + + static getNodeVersion() { + return process.versions.node; + } + + static getNpmVersion() { + try { + const { stdout } = execa.sync("npm", ["--version"]); + return stdout; + } catch (err) { + return ""; + } + } + + static getNpxVersion() { + try { + const { stdout } = execa.sync("npx", ["--version"]); + return stdout; + } catch (err) { + return ""; + } + } + + static getYarnVersion() { + try { + const { stdout } = execa.sync("yarn", ["--version"]); + return stdout; + } catch (err) { + return ""; + } + } +} + +module.exports = { SystemRequirements }; diff --git a/packages/system-requirements/constraints.json b/packages/system-requirements/constraints.json new file mode 100644 index 00000000000..ac88719f638 --- /dev/null +++ b/packages/system-requirements/constraints.json @@ -0,0 +1,6 @@ +{ + "npm": ">=10", + "npx": ">=10", + "yarn": ">=1.22.21 || >=3", + "node": ">=20" +} diff --git a/packages/system-requirements/ensureSystemRequirements.js b/packages/system-requirements/ensureSystemRequirements.js new file mode 100644 index 00000000000..93c9073e532 --- /dev/null +++ b/packages/system-requirements/ensureSystemRequirements.js @@ -0,0 +1,93 @@ +const ensureSystemRequirements = () => { + // Just in case, we want to allow users to skip the system requirements check. + const skipSystemRequirementsCheck = process.argv.includes("--no-system-requirements-check"); + if (skipSystemRequirementsCheck) { + return; + } + + const { SystemRequirements } = require("./SystemRequirements"); + + const systemRequirements = SystemRequirements.validate(); + if (systemRequirements.valid) { + return; + } + + const chalk = require("chalk"); + + console.log( + [ + "One or more system requirements are not met.", + "Please make sure to install the required versions of the following tools:" + ].join("\n\n") + ); + + const Table = require("cli-table3"); + + // Create a table instance + const table = new Table({ + head: ["", "Current version", "Required version", "Valid"], + style: { head: ["bold"] }, + colWidths: [10, 20, 20, 10] + }); + + const IS_VALID_TEXT = `${chalk.green("\u2713")} Yes`; + const IS_INVALID_TEXT = `${chalk.red("\u2717")} No`; + + // Define the rows + const { node, npm, npx, yarn } = systemRequirements; + + const rows = [ + [ + "Node.js", + node.currentVersion, + node.requiredVersion, + node.valid ? IS_VALID_TEXT : IS_INVALID_TEXT + ].map(v => { + return node.valid ? v : chalk.red(v); + }), + [ + "NPM", + npm.currentVersion, + npm.requiredVersion, + npm.valid ? IS_VALID_TEXT : IS_INVALID_TEXT + ].map(v => { + return npm.valid ? v : chalk.red(v); + }), + [ + "NPX", + npx.currentVersion, + npx.requiredVersion, + npx.valid ? IS_VALID_TEXT : IS_INVALID_TEXT + ].map(v => { + return npx.valid ? v : chalk.red(v); + }), + [ + "Yarn", + yarn.currentVersion, + yarn.requiredVersion, + yarn.valid ? IS_VALID_TEXT : IS_INVALID_TEXT + ].map(v => { + return yarn.valid ? v : chalk.red(v); + }) + ]; + + // Add rows to the table + rows.forEach(row => table.push(row)); + + // Print the table to the console + console.log(table.toString()); + + console.log( + [ + "If you think this is a mistake, you can also try skipping", + "the system requirements checks by appending the", + `${chalk.red("--no-system-requirements-check")} flag.` + ].join(" ") + ); + + console.log(); + console.log("For more information, please visit https://webiny.link/prerequisites."); + process.exit(); +}; + +module.exports = { ensureSystemRequirements }; diff --git a/packages/system-requirements/index.js b/packages/system-requirements/index.js new file mode 100644 index 00000000000..5dc010910e8 --- /dev/null +++ b/packages/system-requirements/index.js @@ -0,0 +1,4 @@ +const { SystemRequirements } = require("./SystemRequirements"); +const { ensureSystemRequirements } = require("./ensureSystemRequirements"); + +module.exports = { SystemRequirements, ensureSystemRequirements }; diff --git a/packages/system-requirements/package.json b/packages/system-requirements/package.json new file mode 100644 index 00000000000..8b4bf3a4ad1 --- /dev/null +++ b/packages/system-requirements/package.json @@ -0,0 +1,26 @@ +{ + "name": "@webiny/system-requirements", + "version": "0.0.0", + "main": "index.js", + "bin": { + "webiny": "./bin.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/webiny/webiny-js.git", + "directory": "packages/cli" + }, + "author": "Webiny Ltd.", + "description": "Contains utilities for checking system requirements for Webiny projects", + "dependencies": { + "chalk": "^4.1.2", + "cli-table3": "^0.6.5", + "execa": "^5.0.0", + "semver": "^7.3.5" + }, + "license": "MIT", + "publishConfig": { + "access": "public", + "directory": "." + } +} diff --git a/yarn.lock b/yarn.lock index 0271a18dab0..3ec129c65b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15047,6 +15047,7 @@ __metadata: version: 0.0.0-use.local resolution: "@webiny/cli@workspace:packages/cli" dependencies: + "@webiny/system-requirements": "npm:0.0.0" "@webiny/telemetry": "npm:0.0.0" "@webiny/wcp": "npm:0.0.0" boolean: "npm:^3.2.0" @@ -15878,6 +15879,19 @@ __metadata: languageName: unknown linkType: soft +"@webiny/system-requirements@npm:0.0.0, @webiny/system-requirements@workspace:packages/system-requirements": + version: 0.0.0-use.local + resolution: "@webiny/system-requirements@workspace:packages/system-requirements" + dependencies: + chalk: "npm:^4.1.2" + cli-table3: "npm:^0.6.5" + execa: "npm:^5.0.0" + semver: "npm:^7.3.5" + bin: + webiny: ./bin.js + languageName: unknown + linkType: soft + "@webiny/tasks@npm:0.0.0, @webiny/tasks@workspace:packages/tasks": version: 0.0.0-use.local resolution: "@webiny/tasks@workspace:packages/tasks" @@ -19071,6 +19085,19 @@ __metadata: languageName: node linkType: hard +"cli-table3@npm:^0.6.5": + version: 0.6.5 + resolution: "cli-table3@npm:0.6.5" + dependencies: + "@colors/colors": "npm:1.5.0" + string-width: "npm:^4.2.0" + dependenciesMeta: + "@colors/colors": + optional: true + checksum: 10/8dca71256f6f1367bab84c33add3f957367c7c43750a9828a4212ebd31b8df76bd7419d386e3391ac7419698a8540c25f1a474584028f35b170841cde2e055c5 + languageName: node + linkType: hard + "cli-table3@npm:~0.6.1": version: 0.6.3 resolution: "cli-table3@npm:0.6.3" @@ -20049,6 +20076,7 @@ __metadata: version: 0.0.0-use.local resolution: "create-webiny-project@workspace:packages/create-webiny-project" dependencies: + "@webiny/system-requirements": "npm:0.0.0" "@webiny/telemetry": "npm:0.0.0" chalk: "npm:^4.1.2" execa: "npm:^5.1.1"