diff --git a/.changeset/modern-cycles-prove.md b/.changeset/modern-cycles-prove.md new file mode 100644 index 0000000000..fe9e6f6e38 --- /dev/null +++ b/.changeset/modern-cycles-prove.md @@ -0,0 +1,5 @@ +--- +"@vue-storefront/cli": patch +--- + +[CHANGED] Added back the previously removed `m2-only` command. diff --git a/packages/cli/src/commands/m2-only.ts b/packages/cli/src/commands/m2-only.ts new file mode 100644 index 0000000000..dfffa003a1 --- /dev/null +++ b/packages/cli/src/commands/m2-only.ts @@ -0,0 +1,52 @@ +import { intro } from "@clack/prompts"; +import { Command } from "@oclif/core"; +import picocolors from "picocolors"; +import { t } from "i18next"; +import { initLogger } from "../domains/generate/logging/logger"; +import { + checkDocker, + getMagentoDomainName, +} from "../domains/generate/magento2/docker"; +import { getMagentoDetails } from "../domains/generate/magento2/functions"; +import { installMagento } from "../domains/generate/magento2/installMagento"; +import { simpleLog } from "../domains/generate/magento2/functions/terminalHelpers"; + +export default class M2Only extends Command { + static override description = "Install local Magento 2 instance"; + + static override examples = ["<%= config.bin %> <%= command.id %>"]; + + static override flags = {}; + + static override args = []; + + async run(): Promise { + const { writeLog, deleteLog } = initLogger(); + + intro("Welcome to the Magento 2 local instance installer!"); + + await checkDocker(writeLog); + + const { magentoDirName, magentoAccessKey, magentoSecretKey } = + await getMagentoDetails(); + + const magentoDomain = await getMagentoDomainName( + t("command.generate_store.magento.domain") + ); + + await installMagento({ + isInstallMagento: true, + magentoDirName, + magentoDomain, + magentoAccessKey, + magentoSecretKey, + writeLog, + }); + + deleteLog(); + + simpleLog("Happy coding! 🎉", picocolors.green); + + this.exit(0); + } +} diff --git a/packages/cli/src/domains/generate/logging/logger.ts b/packages/cli/src/domains/generate/logging/logger.ts new file mode 100644 index 0000000000..d6c0292e0b --- /dev/null +++ b/packages/cli/src/domains/generate/logging/logger.ts @@ -0,0 +1,18 @@ +import fs from "fs"; + +export const initLogger = () => { + const logFile = fs.createWriteStream("CLI_logs.txt", { flags: "a" }); + + const writeLog = (message: string) => { + logFile.write(`${message}\n`); + }; + + const deleteLog = () => { + fs.unlinkSync("CLI_logs.txt"); + }; + + return { + writeLog, + deleteLog, + }; +}; diff --git a/packages/cli/src/domains/generate/magento2/docker/checkDocker.ts b/packages/cli/src/domains/generate/magento2/docker/checkDocker.ts new file mode 100644 index 0000000000..be53526676 --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/docker/checkDocker.ts @@ -0,0 +1,42 @@ +import { spawn } from "child_process"; +import { t } from "i18next"; +import { + logSimpleErrorMessage, + logSimpleInfoMessage, + simpleLog, +} from "../functions/terminalHelpers"; + +/** Checking if Docker is installed and running on user's machine */ +const checkDocker = async ( + writeLog: (message: string) => void +): Promise => { + const docker = + process.platform === "darwin" + ? spawn("docker", ["info"]) + : spawn("sudo", ["docker", "info"]); + + docker.stderr.on("data", (data) => { + writeLog(data.toString()); + simpleLog(data.toString()); + }); + + const isDockerInstalled = await new Promise((resolve) => { + docker.on("close", (code) => resolve(code === 0)); + }); + + if (!isDockerInstalled) { + writeLog( + "Docker is not installed or not running. Please make sure that prerequisites are complied with and run command again." + ); + logSimpleErrorMessage( + "Docker is not installed or not running. Please make sure that prerequisites are complied with and run command again." + ); + logSimpleInfoMessage(t("command.generate_store.magento.failed_log")); + process.exit(1); + } else { + writeLog("🐳 Docker is installed and running."); + logSimpleInfoMessage("🐳 Docker is installed and running."); + } +}; + +export default checkDocker; diff --git a/packages/cli/src/domains/generate/magento2/docker/checkExistingDockerContainers.ts b/packages/cli/src/domains/generate/magento2/docker/checkExistingDockerContainers.ts new file mode 100644 index 0000000000..ac44138e9b --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/docker/checkExistingDockerContainers.ts @@ -0,0 +1,16 @@ +import execa from "execa"; + +const checkExistingDockerContainers = async (magentoDirName = "server") => { + const execaFunc = + process.platform === "darwin" + ? execa("docker", ["container", "ls", "--format", "{{.Names}}"]) + : execa("sudo", ["docker", "container", "ls", "--format", "{{.Names}}"]); + + const { stdout } = await execaFunc; + + const isExistingDockerContainers = stdout.includes(magentoDirName); + + return isExistingDockerContainers; +}; + +export default checkExistingDockerContainers; diff --git a/packages/cli/src/domains/generate/magento2/docker/index.ts b/packages/cli/src/domains/generate/magento2/docker/index.ts new file mode 100644 index 0000000000..636bd7182d --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/docker/index.ts @@ -0,0 +1,5 @@ +export { default as checkDocker } from "./checkDocker"; +export { default as installMagentoImage } from "./installMagentoImage"; +export { default as getMagentoDomainName } from "../prompts/getMagentoDomain"; +export { default as checkExistingDockerContainers } from "./checkExistingDockerContainers"; +export { default as removeDockerContainer } from "./removeDocker"; diff --git a/packages/cli/src/domains/generate/magento2/docker/installMagentoImage.ts b/packages/cli/src/domains/generate/magento2/docker/installMagentoImage.ts new file mode 100644 index 0000000000..65ab27bdde --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/docker/installMagentoImage.ts @@ -0,0 +1,97 @@ +import { spawn } from "child_process"; + +import { note, spinner } from "@clack/prompts"; +import picocolors from "picocolors"; +import { t } from "i18next"; +import { + logSimpleErrorMessage, + logSimpleInfoMessage, +} from "../functions/terminalHelpers"; +import removeDockerContainer from "./removeDocker"; + +/** Handles Magento 2 Docker Image installation */ +const installMagentoImage = async ( + magentoDirName: string, + magentoDomainName: string, + writeLog: (message: string) => void +): Promise => { + const options = { + cwd: magentoDirName, + }; + + const sp = spinner(); + + return new Promise((resolve) => { + const curl = spawn( + "curl", + [ + "-s", + "https://raw.githubusercontent.com/markshust/docker-magento/master/lib/onelinesetup", + ], + options + ); + const bash = spawn("bash", ["-s", "--", magentoDomainName], options); + + let stdout = ""; + + note(t("command.generate_store.magento.note_long")); + + sp.start( + picocolors.cyan(t("command.generate_store.progress.docker_start")) + ); + + curl.stdout.pipe(bash.stdin); + + bash.stdout.on("data", (data) => { + if ( + data.toString().toLowerCase().includes("system") && + data.toString().toLowerCase().includes("password") + ) { + sp.stop( + picocolors.yellow(t("command.generate_store.magento.password")) + ); + } + + if (data.toString().includes("Restarting containers to apply updates")) { + sp.start( + picocolors.cyan(t("command.generate_store.progress.docker_start")) + ); + } + }); + + bash.stderr.on("data", async (data) => { + stdout += data.toString(); + if (stdout.includes("port is already allocated")) { + sp.stop(); + logSimpleErrorMessage(t("command.generate_store.magento.port_busy")); + // delete the directory + await removeDockerContainer(magentoDirName); + } + }); + + bash.on("exit", async (code) => { + if (code === 0) { + sp.stop( + picocolors.green(t("command.generate_store.progress.docker_end")) + ); + resolve(1); + } else { + sp.stop( + picocolors.red(t("command.generate_store.progress.docker_failed")) + ); + + if ( + stdout.includes('Project directory "/var/www/html/." is not empty') + ) { + note(t("command.generate_store.magento.image_exists")); + } + // create a log file + writeLog(stdout); + + logSimpleInfoMessage(t("command.generate_store.magento.failed_log")); + } + }); + }); +}; + +export default installMagentoImage; diff --git a/packages/cli/src/domains/generate/magento2/docker/removeDocker.ts b/packages/cli/src/domains/generate/magento2/docker/removeDocker.ts new file mode 100644 index 0000000000..6e5d4b070e --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/docker/removeDocker.ts @@ -0,0 +1,19 @@ +import { spawn } from "child_process"; +import fs from "fs"; + +// rewrite with exec +const removeDockerContainer = async (magentoDirName: string): Promise => { + const options = { + cwd: magentoDirName, + }; + + return new Promise(() => { + const removeDocker = spawn("docker-compose", ["rm", "-f"], options); + + removeDocker.on("exit", () => { + fs.rmdirSync(magentoDirName, { recursive: true }); + }); + }); +}; + +export default removeDockerContainer; diff --git a/packages/cli/src/domains/generate/magento2/functions/checkNode.ts b/packages/cli/src/domains/generate/magento2/functions/checkNode.ts new file mode 100644 index 0000000000..0758f40538 --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/functions/checkNode.ts @@ -0,0 +1,42 @@ +import { spawn } from "child_process"; +import { t } from "i18next"; +import { + logSimpleErrorMessage, + logSimpleSuccessMessage, +} from "./terminalHelpers"; + +const checkNodeVersion = (nodeString: string): boolean => { + const nodeVersion = nodeString.split("v")[1]?.split(".")[0]; + const subNodeVersion = nodeString.split("v")[1]?.split(".")[1]; + + if (Number(nodeVersion) === 16 && Number(subNodeVersion) >= 13) { + return true; + } + + return false; +}; + +/** Checking if Node version is correct as per prerequisites */ +const checkNode = async ( + writeLog: (message: string) => void +): Promise => { + const node = spawn("node", ["-v"]); + + return await new Promise((resolve) => { + node.stdout.on("data", (data) => { + writeLog(data.toString()); + if (!checkNodeVersion(data.toString())) { + logSimpleErrorMessage(t("command.generate_store.magento.node_not_ok")); + process.exit(1); + } + }); + + node.on("close", () => { + writeLog(t("command.generate_store.magento.node_ok")); + logSimpleSuccessMessage(t("command.generate_store.magento.node_ok")); + resolve(); + }); + }); +}; + +export default checkNode; diff --git a/packages/cli/src/domains/generate/magento2/functions/checkYarn.ts b/packages/cli/src/domains/generate/magento2/functions/checkYarn.ts new file mode 100644 index 0000000000..839f65cd50 --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/functions/checkYarn.ts @@ -0,0 +1,54 @@ +import { spawn } from "child_process"; +import { t } from "i18next"; +import { + logSimpleErrorMessage, + logSimpleInfoMessage, + simpleLog, +} from "./terminalHelpers"; + +const checkYarnVersion = (yarnString: string): boolean => { + const yarnVersion = yarnString.split(".")[0]; + + if (Number(yarnVersion) === 1 || Number(yarnVersion) === 2) { + return true; + } + + return false; +}; + +/** Checking if Yarn is installed */ +const checkYarn = async ( + writeLog: (message: string) => void +): Promise => { + const yarn = + process.platform === "win32" + ? spawn("yarn.cmd", ["--version"]) + : spawn("yarn", ["--version"]); + + yarn.stdout.on("data", (data) => { + if (!checkYarnVersion(data.toString())) { + writeLog(t("command.generate_store.magento.yarn_not_ok")); + logSimpleErrorMessage(t("command.generate_store.magento.yarn_not_ok")); + logSimpleInfoMessage(t("command.generate_store.magento.failed_log")); + process.exit(1); + } + }); + + yarn.stderr.on("data", (data) => { + writeLog(data.toString()); + simpleLog(data.toString()); + }); + + const isYarnVersionCorrect = await new Promise((resolve) => { + yarn.on("close", (code) => resolve(code === 0)); + }); + + if (!isYarnVersionCorrect) { + writeLog(t("command.generate_store.magento.yarn_not_ok")); + logSimpleErrorMessage(t("command.generate_store.magento.yarn_not_ok")); + logSimpleInfoMessage(t("command.generate_store.magento.failed_log")); + process.exit(1); + } +}; + +export default checkYarn; diff --git a/packages/cli/src/domains/generate/magento2/functions/copyAuth.ts b/packages/cli/src/domains/generate/magento2/functions/copyAuth.ts new file mode 100644 index 0000000000..456ba67860 --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/functions/copyAuth.ts @@ -0,0 +1,37 @@ +import { spawn } from "child_process"; +import fs from "fs"; +import path from "path"; + +/** Copy auth.json file to Docker container */ +const copyAuth = async ( + magentoDirName: string, + accessKey: string, + secretKey: string +) => { + const options = { + cwd: magentoDirName, + }; + + const authFile = await fs.readFileSync( + path.join(magentoDirName, "src/auth.json.sample"), + "utf-8" + ); + + await fs.writeFileSync( + path.join(magentoDirName, "src/auth.json"), + authFile + .replace(//g, accessKey) + .replace(//g, secretKey), + "utf-8" + ); + + const copyToContainer = spawn( + "bin/copytocontainer", + ["src/auth.json"], + options + ); + + copyToContainer.on("close", () => undefined); +}; + +export default copyAuth; diff --git a/packages/cli/src/domains/generate/magento2/functions/copyEnv.ts b/packages/cli/src/domains/generate/magento2/functions/copyEnv.ts new file mode 100644 index 0000000000..5f75f20424 --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/functions/copyEnv.ts @@ -0,0 +1,37 @@ +import fs from "fs"; +import picocolors from "picocolors"; +import path from "path"; +import { simpleLog } from "./terminalHelpers"; + +const copyEnv = async (vsfDirName: string, magentoDomain?: string) => { + try { + await fs.copyFileSync( + path.join(vsfDirName, ".env.example"), + path.join(vsfDirName, ".env") + ); + + if (magentoDomain) { + const envFile = await fs.readFileSync( + path.join(vsfDirName, ".env"), + "utf8" + ); + + const result = envFile.replace( + /{YOUR_SITE_FRONT_URL}/g, + magentoDomain.replace(/\/$/, "") + ); + + fs.writeFileSync(path.join(vsfDirName, ".env"), result, "utf8"); + } + } catch { + simpleLog( + "No .env file available. Please check that your git repository is a valid Vue Storefront project", + picocolors.red + ); + process.exit(1); + } + + fs.unlinkSync(path.join(vsfDirName, ".env.example")); +}; + +export default copyEnv; diff --git a/packages/cli/src/domains/generate/magento2/functions/getMagentoDetails.ts b/packages/cli/src/domains/generate/magento2/functions/getMagentoDetails.ts new file mode 100644 index 0000000000..8444920b3d --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/functions/getMagentoDetails.ts @@ -0,0 +1,71 @@ +import fs from "fs"; +import { t } from "i18next"; +import { note } from "@clack/prompts"; +import confirmOverwrite from "../prompts/confirmOverwrite"; +import getMagentoDirName from "../prompts/getMagentoDirName"; +import isMagentoKeys from "../prompts/isMagentoKeys"; +import { logSimpleErrorMessage, simpleLog } from "./terminalHelpers"; +import handleMagentoKeys from "../prompts/handleMagentoKeys"; + +const getMagentoDetails = async (projectName?: string) => { + let magentoAccessKey: string; + let magentoSecretKey: string; + + note(t("command.generate_store.magento.info")); + + let magentoDirName = await getMagentoDirName( + t("command.generate_store.magento.directory") + ); + + if (magentoDirName === projectName) { + logSimpleErrorMessage( + t("command.generate_store.magento.error.same_dir", { + magentoDirName, + projectName, + }) + ); + + magentoDirName = await getMagentoDirName( + t("command.generate_store.magento.directory") + ); + } + + if (!fs.existsSync(magentoDirName)) { + fs.mkdirSync(magentoDirName); + } else { + magentoDirName = await confirmOverwrite({ + message: t("command.generate_store.magento.overwrite", { + magentoDirName, + }), + magentoDirName, + }); + } + + const hasMagentoKeys = await isMagentoKeys( + t("command.generate_store.magento.access_keys") + ); + + if (hasMagentoKeys) { + simpleLog(t("command.generate_store.magento.provide_keys")); + const { accessKey, secretKey } = await handleMagentoKeys(); + + magentoAccessKey = accessKey; + magentoSecretKey = secretKey; + } else { + simpleLog(t("command.generate_store.magento.no_keys")); + + simpleLog(t("command.generate_store.magento.provide_keys")); + const { accessKey, secretKey } = await handleMagentoKeys(); + + magentoAccessKey = accessKey; + magentoSecretKey = secretKey; + } + + return { + magentoDirName, + magentoAccessKey, + magentoSecretKey, + }; +}; + +export default getMagentoDetails; diff --git a/packages/cli/src/domains/generate/magento2/functions/handleGraphQL.ts b/packages/cli/src/domains/generate/magento2/functions/handleGraphQL.ts new file mode 100644 index 0000000000..f990262e6a --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/functions/handleGraphQL.ts @@ -0,0 +1,81 @@ +import { spawn } from "child_process"; +import fs from "fs"; +import { spinner } from "@clack/prompts"; +import picocolors from "picocolors"; +import { t } from "i18next"; +import { logSimpleInfoMessage } from "./terminalHelpers"; + +/** Install and enable GraphQL Magento module */ +const handleGraphQL = async ( + magentoDirName: string, + writeLog: (message: string) => void +) => { + const options = { + cwd: magentoDirName, + shell: true, + }; + + const sp = spinner(); + + const increaseQueryDepthAndComplexity = async () => { + const data = fs.readFileSync( + `${magentoDirName}/src/vendor/magento/module-graph-ql/etc/di.xml`, + "utf8" + ); + + const result = data.replace( + /300<\/argument>/g, + '1500' + ); + + fs.writeFileSync( + `${magentoDirName}/src/vendor/magento/module-graph-ql/etc/di.xml`, + result, + "utf8" + ); + + fs.writeFileSync( + `${magentoDirName}/src/vendor/magento/module-graph-ql/etc/di.xml`, + result, + "utf8" + ); + }; + + return new Promise((resolve, reject) => { + const child = spawn( + "bin/composer require caravelx/module-graphql-config && bin/magento module:enable Caravel_GraphQlConfig && bin/magento setup:upgrade && bin/magento setup:di:compile && bin/magento setup:static-content:deploy -f", + options + ); + + sp.start( + picocolors.cyan(t("command.generate_store.progress.graphql_start")) + ); + + child.stdout.on("data", (data) => { + writeLog(data.toString()); + }); + + child.stderr.on("data", (data) => { + writeLog(data.toString()); + }); + + child.on("exit", async (code) => { + console.log(picocolors.red(code)); + if (code === 0) { + await increaseQueryDepthAndComplexity(); + sp.stop( + picocolors.green(t("command.generate_store.progress.graphql_end")) + ); + resolve(1); + } else { + sp.stop( + picocolors.red(t("command.generate_store.progress.graphql_failed")) + ); + logSimpleInfoMessage(t("command.generate_store.magento.failed_log")); + reject(); + } + }); + }); +}; + +export default handleGraphQL; diff --git a/packages/cli/src/domains/generate/magento2/functions/handleSampleData.ts b/packages/cli/src/domains/generate/magento2/functions/handleSampleData.ts new file mode 100644 index 0000000000..6b3b5aa3d1 --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/functions/handleSampleData.ts @@ -0,0 +1,54 @@ +import { spawn } from "child_process"; +import { spinner } from "@clack/prompts"; +import picocolors from "picocolors"; +import { t } from "i18next"; + +/** Generate sample data and upgrade */ +const handleSampleData = async ( + magentoDirName: string, + writeLog: (message: string) => void +) => { + const options = { + cwd: magentoDirName, + shell: true, + }; + + const sp = spinner(); + + return new Promise((resolve, reject) => { + const sampleData = spawn( + "bin/magento sampledata:deploy && bin/magento setup:upgrade", + options + ); + + sp.start( + picocolors.cyan(t("command.generate_store.progress.sample_data_start")) + ); + + sampleData.stdout.on("data", (data) => { + writeLog(data.toString()); + }); + + sampleData.stderr.on("data", (data) => { + writeLog(data.toString()); + }); + + sampleData.on("close", (code) => { + if (code === 0) { + sp.stop( + picocolors.green(t("command.generate_store.progress.sample_data_end")) + ); + resolve(1); + } else { + sp.stop( + picocolors.red( + t("command.generate_store.progress.sample_data_failed") + ) + ); + reject(); + } + }); + }); +}; + +export default handleSampleData; diff --git a/packages/cli/src/domains/generate/magento2/functions/index.ts b/packages/cli/src/domains/generate/magento2/functions/index.ts new file mode 100644 index 0000000000..e41753eccc --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/functions/index.ts @@ -0,0 +1,12 @@ +export { default as installMg2Prompt } from "../prompts/isInstallMagento"; +export { default as getMagentoDirName } from "../prompts/getMagentoDirName"; +export { default as confirmOverwrite } from "../prompts/confirmOverwrite"; +export { default as isMagentoKeys } from "../prompts/isMagentoKeys"; +export { default as handleMagentoKeys } from "../prompts/handleMagentoKeys"; +export { default as copyAuth } from "./copyAuth"; +export { default as handleGraphQL } from "./handleGraphQL"; +export { default as isGenerateSampleData } from "../prompts/isGenerateSampleData"; +export { default as handleSampleData } from "./handleSampleData"; +export { default as copyEnv } from "./copyEnv"; +export { default as installDeps } from "./installDeps"; +export { default as getMagentoDetails } from "./getMagentoDetails"; diff --git a/packages/cli/src/domains/generate/magento2/functions/installDeps.ts b/packages/cli/src/domains/generate/magento2/functions/installDeps.ts new file mode 100644 index 0000000000..e1503abeee --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/functions/installDeps.ts @@ -0,0 +1,41 @@ +import { spawn } from "child_process"; +import { spinner } from "@clack/prompts"; +import picocolors from "picocolors"; +import { t } from "i18next"; + +/** Generate sample data and upgrade */ +const installDeps = async ( + vsfDirName: string, + writeLog: (message: string) => void +) => { + const options = { + cwd: vsfDirName, + }; + + const sp = spinner(); + + return new Promise((resolve) => { + const install = spawn("yarn", options); + + sp.start( + picocolors.cyan(t("command.generate_store.progress.install_deps_start")) + ); + + install.stdout.on("data", (data) => { + writeLog(data.toString()); + }); + + install.stderr.on("data", (data) => { + writeLog(data.toString()); + }); + + install.on("close", () => { + sp.stop( + picocolors.green(t("command.generate_store.progress.install_deps_end")) + ); + resolve(1); + }); + }); +}; + +export default installDeps; diff --git a/packages/cli/src/domains/generate/magento2/functions/terminalHelpers.ts b/packages/cli/src/domains/generate/magento2/functions/terminalHelpers.ts new file mode 100644 index 0000000000..b693aa1355 --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/functions/terminalHelpers.ts @@ -0,0 +1,32 @@ +import picocolors from "picocolors"; +import { spinner } from "@clack/prompts"; + +export function simpleLog( + message: string, + pc?: (pcMessage: string) => string +): void { + const sp = spinner(); + if (pc) { + sp.start(pc(message)); + sp.stop(pc(message)); + } else { + sp.start(message); + sp.stop(message); + } +} + +export function logSimpleSuccessMessage(message: string): void { + simpleLog(message, picocolors.green); +} + +export function logSimpleErrorMessage(message: string): void { + simpleLog(message, picocolors.red); +} + +export function logSimpleWarningMessage(message: string): void { + simpleLog(message, picocolors.yellow); +} + +export function logSimpleInfoMessage(message: string): void { + simpleLog(message, picocolors.cyan); +} diff --git a/packages/cli/src/domains/generate/magento2/installMagento.ts b/packages/cli/src/domains/generate/magento2/installMagento.ts new file mode 100644 index 0000000000..039aa7500d --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/installMagento.ts @@ -0,0 +1,46 @@ +import { note } from "@clack/prompts"; +import { t } from "i18next"; +import { installMagentoImage } from "./docker"; + +import { + copyAuth, + handleGraphQL, + handleSampleData, + isGenerateSampleData, +} from "./functions"; + +import { logSimpleSuccessMessage } from "./functions/terminalHelpers"; + +interface MagentoDetails { + isInstallMagento: boolean; + magentoDirName: string; + magentoDomain: string; + magentoAccessKey: string; + magentoSecretKey: string; + writeLog: (message: string) => void; +} + +/** Function responsible for all Magento 2 installation process */ +export const installMagento = async ({ + magentoDirName, + magentoDomain, + magentoAccessKey, + magentoSecretKey, + writeLog, +}: MagentoDetails) => { + await installMagentoImage(magentoDirName, magentoDomain, writeLog); + await copyAuth(magentoDirName, magentoAccessKey, magentoSecretKey); + await handleGraphQL(magentoDirName, writeLog); + + const isGenerateData = await isGenerateSampleData( + t("command.generate_store.magento.sample_data") + ); + + if (isGenerateData) { + await handleSampleData(magentoDirName, writeLog); + } else { + note(t("command.generate_store.magento.sample_data_note")); + } + + logSimpleSuccessMessage(t("command.generate_store.magento.success")); +}; diff --git a/packages/cli/src/domains/generate/magento2/prompts/confirmOverwrite.ts b/packages/cli/src/domains/generate/magento2/prompts/confirmOverwrite.ts new file mode 100644 index 0000000000..4ea57f0826 --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/prompts/confirmOverwrite.ts @@ -0,0 +1,52 @@ +import fs from "fs"; +import { confirm, isCancel, spinner } from "@clack/prompts"; +import { t } from "i18next"; +import picocolors from "picocolors"; +import { + logSimpleInfoMessage, + logSimpleWarningMessage, +} from "../functions/terminalHelpers"; + +/** The answers expected in the form of 'inquirer'. */ +type Arguments = { + message: string; + magentoDirName: string; +}; + +/** Prompts user if they want to overwrite the directory */ +const confirmOverwrite = async ({ + message, + magentoDirName, +}: Arguments): Promise => { + let newMagentoDirName = ""; + const overwrite = await confirm({ + message, + }); + + if (isCancel(overwrite)) { + logSimpleWarningMessage(t("command.generate_store.message.canceled")); + process.exit(0); + } + + const sp = spinner(); + + if (overwrite) { + sp.start( + picocolors.cyan(t("command.generate_store.progress.delete_start")) + ); + await fs.rmdirSync(magentoDirName, { recursive: true }); + await fs.mkdirSync(magentoDirName); + sp.stop(picocolors.green(t("command.generate_store.progress.delete_end"))); + newMagentoDirName = magentoDirName; + } + + if (!overwrite) { + logSimpleInfoMessage(t("command.generate_store.progress.create_dir")); + newMagentoDirName = magentoDirName + new Date().getTime().toString(); + fs.mkdirSync(newMagentoDirName); + } + + return newMagentoDirName; +}; + +export default confirmOverwrite; diff --git a/packages/cli/src/domains/generate/magento2/prompts/getMagentoDirName.ts b/packages/cli/src/domains/generate/magento2/prompts/getMagentoDirName.ts new file mode 100644 index 0000000000..31d4256d87 --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/prompts/getMagentoDirName.ts @@ -0,0 +1,42 @@ +import { t } from "i18next"; +import isReasonableFilename from "reasonable-filename"; + +import { text, isCancel } from "@clack/prompts"; +import { logSimpleWarningMessage } from "../functions/terminalHelpers"; +import { formatToProjectName } from "../../project-name"; +import { checkExistingDockerContainers } from "../docker"; + +/** Prompt user to enter Magento directory name */ +const getMagentoDirName = async (message: string): Promise => { + const magentoDirName = await text({ + message, + initialValue: "magento", + validate: (value?: string): string | void => { + if (!value?.trim()) { + return t("domain.project_name.is_empty"); + } + + if (!isReasonableFilename(value)) { + return t("domain.project_name.is_not_directory"); + } + return undefined; + }, + }); + + if (isCancel(magentoDirName)) { + logSimpleWarningMessage(t("command.generate_store.message.canceled")); + process.exit(0); + } + + const existingContainers = await checkExistingDockerContainers( + formatToProjectName(magentoDirName as string) + ); + + if (existingContainers) { + return getMagentoDirName(t("command.generate_store.magento.docker_exists")); + } + + return formatToProjectName(magentoDirName as string); +}; + +export default getMagentoDirName; diff --git a/packages/cli/src/domains/generate/magento2/prompts/getMagentoDomain.ts b/packages/cli/src/domains/generate/magento2/prompts/getMagentoDomain.ts new file mode 100644 index 0000000000..4aa5273cb4 --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/prompts/getMagentoDomain.ts @@ -0,0 +1,34 @@ +import { t } from "i18next"; +import { text, isCancel } from "@clack/prompts"; +import { logSimpleWarningMessage } from "../functions/terminalHelpers"; + +/** Gets a Magento domain name and checks for validity. */ +const getMagentoDomainName = async (message: string): Promise => { + const magentoDomainName = await text({ + message, + initialValue: "magento.test", + validate(value: string): string | void { + if (!value?.trim()) { + return t("domain.project_name.is_empty"); + } + + const domainNameRegex = + // eslint-disable-next-line no-useless-escape + /^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9\-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$/; + + if (!domainNameRegex.test(value)) { + return t("command.generate_store.magento.invalid_domain"); + } + return undefined; + }, + }); + + if (isCancel(magentoDomainName)) { + logSimpleWarningMessage(t("command.generate_store.message.canceled")); + process.exit(0); + } + + return magentoDomainName as string; +}; + +export default getMagentoDomainName; diff --git a/packages/cli/src/domains/generate/magento2/prompts/handleMagentoKeys.ts b/packages/cli/src/domains/generate/magento2/prompts/handleMagentoKeys.ts new file mode 100644 index 0000000000..aaa1d73c1b --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/prompts/handleMagentoKeys.ts @@ -0,0 +1,95 @@ +import { password, isCancel } from "@clack/prompts"; +import { t } from "i18next"; +import fs from "fs"; +import path from "path"; +import os from "os"; +import { logSimpleWarningMessage } from "../functions/terminalHelpers"; + +const homeDir = os.homedir(); + +/** The answers expected in the form of 'inquirer'. */ +type MagentoKeys = { + accessKey: string; + secretKey: string; +}; + +/** Handle input for Magento 2 access keys */ +const handleMagentoKeys = async (): Promise => { + const accessKey = await password({ + message: t("command.generate_store.magento.access_key"), + }); + + const secretKey = await password({ + message: t("command.generate_store.magento.secret_key"), + }); + + if (isCancel(accessKey || secretKey)) { + logSimpleWarningMessage(t("command.generate_store.message.canceled")); + process.exit(0); + } + + // creating auth.json file in home directory for composer + await new Promise((resolve) => { + // if .composer directory does not exist, create it + if (!fs.existsSync(path.join(homeDir, ".composer"))) { + fs.mkdirSync(path.join(homeDir, ".composer")); + } + + // check if file exists + if (fs.existsSync(path.join(homeDir, ".composer", "auth.json"))) { + // read file + const authFile = fs.readFileSync( + path.join(homeDir, ".composer", "auth.json"), + "utf-8" + ); + + // parse JSON + const authJson = JSON.parse(authFile); + + // check if keys are already in file + authJson["http-basic"] = { + "repo.magento.com": { + username: accessKey, + password: secretKey, + }, + }; + + // write file + fs.writeFileSync( + path.join(homeDir, ".composer", "auth.json"), + JSON.stringify(authJson, null, 2), + "utf-8" + ); + } else { + // create file + const authJson = { + "http-basic": { + "repo.magento.com": { + username: accessKey, + password: secretKey, + }, + }, + "bitbucket-oauth": {}, + "github-oauth": {}, + "gitlab-oauth": {}, + "gitlab-token": {}, + bearer: {}, + }; + + fs.writeFileSync( + path.join(homeDir, ".composer", "auth.json"), + JSON.stringify(authJson, null, 2), + "utf-8" + ); + } + + resolve(1); + }); + + return { + accessKey: accessKey as string, + secretKey: secretKey as string, + }; +}; + +export default handleMagentoKeys; diff --git a/packages/cli/src/domains/generate/magento2/prompts/isGenerateSampleData.ts b/packages/cli/src/domains/generate/magento2/prompts/isGenerateSampleData.ts new file mode 100644 index 0000000000..cd4f713556 --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/prompts/isGenerateSampleData.ts @@ -0,0 +1,20 @@ +import { t } from "i18next"; +import { confirm, isCancel } from "@clack/prompts"; +import { logSimpleWarningMessage } from "../functions/terminalHelpers"; + +/** Pormpt user if they want to generate sample data */ +const isGenerateSampleData = async (message: string): Promise => { + const isGenerate = await confirm({ + message, + initialValue: true, + }); + + if (isCancel(isGenerate)) { + logSimpleWarningMessage(t("command.generate_store.message.canceled")); + process.exit(0); + } + + return isGenerate as boolean; +}; + +export default isGenerateSampleData; diff --git a/packages/cli/src/domains/generate/magento2/prompts/isInstallMagento.ts b/packages/cli/src/domains/generate/magento2/prompts/isInstallMagento.ts new file mode 100644 index 0000000000..854d1172f4 --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/prompts/isInstallMagento.ts @@ -0,0 +1,15 @@ +import { confirm, note } from "@clack/prompts"; +import { t } from "i18next"; + +/** Prompt user if they want to install Magento 2 locally. */ +const isInstallMagento = async (message: string): Promise => { + note(t("command.generate_store.magento.install_note")); + + const isInstallMagentoFn = await confirm({ + message, + }); + + return isInstallMagentoFn as boolean; +}; + +export default isInstallMagento; diff --git a/packages/cli/src/domains/generate/magento2/prompts/isMagentoKeys.ts b/packages/cli/src/domains/generate/magento2/prompts/isMagentoKeys.ts new file mode 100644 index 0000000000..bbb0538ab1 --- /dev/null +++ b/packages/cli/src/domains/generate/magento2/prompts/isMagentoKeys.ts @@ -0,0 +1,20 @@ +import { t } from "i18next"; +import { confirm, isCancel } from "@clack/prompts"; +import { logSimpleWarningMessage } from "../functions/terminalHelpers"; + +/** Pormpt user is they have Magento 2 keys */ +const isMagentoKeys = async (message: string): Promise => { + const hasMagentoAccessKeys = await confirm({ + message, + initialValue: true, + }); + + if (isCancel(hasMagentoAccessKeys)) { + logSimpleWarningMessage(t("command.generate_store.message.canceled")); + process.exit(0); + } + + return hasMagentoAccessKeys; +}; + +export default isMagentoKeys; diff --git a/packages/cli/src/domains/generate/project-name/formatToProjectName.ts b/packages/cli/src/domains/generate/project-name/formatToProjectName.ts new file mode 100644 index 0000000000..f710caa300 --- /dev/null +++ b/packages/cli/src/domains/generate/project-name/formatToProjectName.ts @@ -0,0 +1,6 @@ +/** Formats received string value into a project name. */ +const formatToProjectName = (value: string) => { + return value.toLowerCase().replace(/\s+/g, "-"); +}; + +export default formatToProjectName; diff --git a/packages/cli/src/domains/generate/project-name/getProjectName.ts b/packages/cli/src/domains/generate/project-name/getProjectName.ts new file mode 100644 index 0000000000..104b817d3f --- /dev/null +++ b/packages/cli/src/domains/generate/project-name/getProjectName.ts @@ -0,0 +1,32 @@ +import { t } from "i18next"; +// import inquirer from 'inquirer'; +import isReasonableFilename from "reasonable-filename"; +import { text, isCancel } from "@clack/prompts"; +import formatToProjectName from "./formatToProjectName"; + +import { logSimpleWarningMessage } from "../magento2/functions/terminalHelpers"; + +const getProjectName = async (message: string): Promise => { + const projectName = await text({ + message, + validate: (value?: string): string | void => { + if (!value?.trim()) { + return t("domain.project_name.is_empty"); + } + + if (!isReasonableFilename(value)) { + return t("domain.project_name.is_not_directory"); + } + return undefined; + }, + }); + + if (isCancel(projectName)) { + logSimpleWarningMessage(t("command.generate_store.message.canceled")); + process.exit(0); + } + + return formatToProjectName(projectName as string); +}; + +export default getProjectName; diff --git a/packages/cli/src/domains/generate/project-name/index.ts b/packages/cli/src/domains/generate/project-name/index.ts new file mode 100644 index 0000000000..e04098ce96 --- /dev/null +++ b/packages/cli/src/domains/generate/project-name/index.ts @@ -0,0 +1,2 @@ +export { default as getProjectName } from "./getProjectName"; +export { default as formatToProjectName } from "./formatToProjectName";