From fe1d3a4a78385bb356539d6f9cba5ce2012f5da8 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Mon, 1 Apr 2024 12:13:44 +0300 Subject: [PATCH] feat(create-medusa-app): Add a `--verbose` option. (#6027) ## What Adds a `--verbose` option that shows the output of all underlying processes in real-time. ## Why This is helpful for testing and debugging issues, especially issues that the community runs into. We can ask community members to pass the `--verbose` option and provide us with the outputted logs if they face problems. ## Caveats When installing the Next.js starter then terminating the process, the main and child processes don't receive the abort signal as it seems to occur in the child process. This leads to the command continuing but then running into an error in the next step. As this option is only used for debugging, I don't think it's a big issue. ## Testing Run the `create-medusa-app` snapshot below with `--verbose` option. Or, change to the `packages/create-medusa-app` directory and run: ```bash yarn dev --directory-path ~/some-dir --verbose ``` > The `--directory-path` option in this case is necessary as installing the medusa backend in the current `packages/create-medusa-app` directory leads to errors related to yarn workspaces. --- .changeset/three-mice-jump.md | 5 ++ .../create-medusa-app/src/commands/create.ts | 11 ++- packages/create-medusa-app/src/index.ts | 5 ++ .../create-medusa-app/src/utils/clone-repo.ts | 24 ++++-- .../src/utils/create-abort-controller.ts | 6 ++ .../create-medusa-app/src/utils/create-db.ts | 40 ++++++++- .../create-medusa-app/src/utils/execute.ts | 72 ++++++++++++++++ packages/create-medusa-app/src/utils/facts.ts | 85 +++++++++++++------ .../src/utils/log-message.ts | 5 +- .../src/utils/nextjs-utils.ts | 45 ++++++---- .../src/utils/prepare-project.ts | 67 +++++++++------ .../src/utils/promise-exec.ts | 6 -- .../src/utils/start-medusa.ts | 1 + 13 files changed, 285 insertions(+), 87 deletions(-) create mode 100644 .changeset/three-mice-jump.md create mode 100644 packages/create-medusa-app/src/utils/execute.ts delete mode 100644 packages/create-medusa-app/src/utils/promise-exec.ts diff --git a/.changeset/three-mice-jump.md b/.changeset/three-mice-jump.md new file mode 100644 index 0000000000..63ecd7ecfc --- /dev/null +++ b/.changeset/three-mice-jump.md @@ -0,0 +1,5 @@ +--- +"create-medusa-app": patch +--- + +feat(create-medusa-app): Add a `--verbose` option. diff --git a/packages/create-medusa-app/src/commands/create.ts b/packages/create-medusa-app/src/commands/create.ts index 80d71ccae6..0660246a73 100644 --- a/packages/create-medusa-app/src/commands/create.ts +++ b/packages/create-medusa-app/src/commands/create.ts @@ -42,6 +42,7 @@ export type CreateOptions = { migrations?: boolean directoryPath?: string withNextjsStarter?: boolean + verbose?: boolean v2?: boolean } @@ -55,6 +56,7 @@ export default async ({ migrations, directoryPath, withNextjsStarter = false, + verbose = false, v2 = false, }: CreateOptions) => { track("CREATE_CLI_CMA") @@ -68,6 +70,7 @@ export default async ({ processManager, message: "", title: "", + verbose, } const dbName = !skipDb && !dbUrl ? `medusa-${nanoid(4)}` : "" let isProjectCreated = false @@ -103,6 +106,7 @@ export default async ({ ? await getDbClientAndCredentials({ dbName, dbUrl, + verbose, }) : { client: null, dbConnectionString: "" } isDbInitialized = true @@ -115,6 +119,7 @@ export default async ({ browser, migrations, installNextjs, + verbose, }) logMessage({ @@ -136,6 +141,7 @@ export default async ({ repoUrl, abortController, spinner, + verbose, v2, }) } catch { @@ -152,6 +158,7 @@ export default async ({ directoryName: projectPath, abortController, factBoxOptions, + verbose, }) : "" @@ -187,6 +194,7 @@ export default async ({ onboardingType: installNextjs ? "nextjs" : "default", nextjsDirectory, client, + verbose, v2, }) } catch (e: any) { @@ -225,9 +233,10 @@ export default async ({ }) if (installNextjs && nextjsDirectory) { - void startNextjsStarter({ + startNextjsStarter({ directory: nextjsDirectory, abortController, + verbose, }) } } catch (e) { diff --git a/packages/create-medusa-app/src/index.ts b/packages/create-medusa-app/src/index.ts index 3963e5e098..c0adc65024 100644 --- a/packages/create-medusa-app/src/index.ts +++ b/packages/create-medusa-app/src/index.ts @@ -38,6 +38,11 @@ program "Install the Next.js starter along with the Medusa backend", false ) + .option( + "--verbose", + "Show all logs of underlying commands. Useful for debugging.", + false + ) .option( "--v2", "Install Medusa with the V2 feature flag enabled. WARNING: Medusa V2 is still in development and shouldn't be used in production.", diff --git a/packages/create-medusa-app/src/utils/clone-repo.ts b/packages/create-medusa-app/src/utils/clone-repo.ts index f0a85efe16..ddb3b5cd2a 100644 --- a/packages/create-medusa-app/src/utils/clone-repo.ts +++ b/packages/create-medusa-app/src/utils/clone-repo.ts @@ -1,4 +1,4 @@ -import promiseExec from "./promise-exec.js" +import execute from "./execute.js" import { Ora } from "ora" import { isAbortError } from "./create-abort-controller.js" import logMessage from "./log-message.js" @@ -9,6 +9,7 @@ type CloneRepoOptions = { directoryName?: string repoUrl?: string abortController?: AbortController + verbose?: boolean v2?: boolean } @@ -19,15 +20,19 @@ export default async function cloneRepo({ directoryName = "", repoUrl, abortController, + verbose = false, v2 = false, }: CloneRepoOptions) { - await promiseExec( - `git clone ${repoUrl || DEFAULT_REPO}${ - v2 ? ` -b ${V2_BRANCH}` : "" - } ${directoryName}`, - { - signal: abortController?.signal, - } + await execute( + [ + `git clone ${repoUrl || DEFAULT_REPO}${ + v2 ? ` -b ${V2_BRANCH}` : "" + } ${directoryName}`, + { + signal: abortController?.signal, + }, + ], + { verbose } ) } @@ -36,12 +41,14 @@ export async function runCloneRepo({ repoUrl, abortController, spinner, + verbose = false, v2 = false, }: { projectName: string repoUrl: string abortController: AbortController spinner: Ora + verbose?: boolean v2?: boolean }) { try { @@ -49,6 +56,7 @@ export async function runCloneRepo({ directoryName: projectName, repoUrl, abortController, + verbose, v2, }) diff --git a/packages/create-medusa-app/src/utils/create-abort-controller.ts b/packages/create-medusa-app/src/utils/create-abort-controller.ts index 0e78f05420..4473516a05 100644 --- a/packages/create-medusa-app/src/utils/create-abort-controller.ts +++ b/packages/create-medusa-app/src/utils/create-abort-controller.ts @@ -8,3 +8,9 @@ export default (processManager: ProcessManager) => { export const isAbortError = (e: any) => e !== null && "code" in e && e.code === "ABORT_ERR" + +export const getAbortError = () => { + return { + code: "ABORT_ERR", + } +} diff --git a/packages/create-medusa-app/src/utils/create-db.ts b/packages/create-medusa-app/src/utils/create-db.ts index f07a29a74f..cc4fc2df87 100644 --- a/packages/create-medusa-app/src/utils/create-db.ts +++ b/packages/create-medusa-app/src/utils/create-db.ts @@ -52,7 +52,13 @@ export async function runCreateDb({ return newClient } -async function getForDbName(dbName: string): Promise<{ +async function getForDbName({ + dbName, + verbose = false, +}: { + dbName: string + verbose?: boolean +}): Promise<{ client: pg.Client dbConnectionString: string }> { @@ -66,6 +72,12 @@ async function getForDbName(dbName: string): Promise<{ password: postgresPassword, }) } catch (e) { + if (verbose) { + logMessage({ + message: `The following error occured when connecting to the database: ${e}`, + type: "verbose", + }) + } // ask for the user's postgres credentials const answers = await inquirer.prompt([ { @@ -114,7 +126,13 @@ async function getForDbName(dbName: string): Promise<{ } } -async function getForDbUrl(dbUrl: string): Promise<{ +async function getForDbUrl({ + dbUrl, + verbose = false, +}: { + dbUrl: string + verbose?: boolean +}): Promise<{ client: pg.Client dbConnectionString: string }> { @@ -125,6 +143,12 @@ async function getForDbUrl(dbUrl: string): Promise<{ connectionString: dbUrl, }) } catch (e) { + if (verbose) { + logMessage({ + message: `The following error occured when connecting to the database: ${e}`, + type: "verbose", + }) + } logMessage({ message: `Couldn't connect to PostgreSQL using the database URL you passed. Make sure it's correct and try again.`, type: "error", @@ -140,13 +164,21 @@ async function getForDbUrl(dbUrl: string): Promise<{ export async function getDbClientAndCredentials({ dbName = "", dbUrl = "", + verbose = false, }): Promise<{ client: pg.Client dbConnectionString: string + verbose?: boolean }> { if (dbName) { - return await getForDbName(dbName) + return await getForDbName({ + dbName, + verbose, + }) } else { - return await getForDbUrl(dbUrl) + return await getForDbUrl({ + dbUrl, + verbose, + }) } } diff --git a/packages/create-medusa-app/src/utils/execute.ts b/packages/create-medusa-app/src/utils/execute.ts new file mode 100644 index 0000000000..c5a93f1504 --- /dev/null +++ b/packages/create-medusa-app/src/utils/execute.ts @@ -0,0 +1,72 @@ +import { exec, spawnSync, SpawnSyncOptions } from "child_process" +import util from "util" +import { getAbortError } from "./create-abort-controller.js" + +const promiseExec = util.promisify(exec) + +type ExecuteOptions = { + stdout?: string + stderr?: string +} + +type VerboseOptions = { + verbose?: boolean + // Since spawn doesn't allow us to both retrieve the + // output and output it live without using events, + // enabling this option, which is only useful if `verbose` is `true`, + // defers the output of the process until after the process is executed + // instead of outputting the log in realtime, which is the default. + // it prioritizes retrieving the output over outputting it in real-time. + needOutput?: boolean +} + +type PromiseExecParams = Parameters +type SpawnParams = [string, SpawnSyncOptions] + +const execute = async ( + command: SpawnParams | PromiseExecParams, + { verbose = false, needOutput = false }: VerboseOptions +): Promise => { + if (verbose) { + const [commandStr, options] = command as SpawnParams + const childProcess = spawnSync(commandStr, { + ...options, + shell: true, + stdio: needOutput + ? "pipe" + : [process.stdin, process.stdout, process.stderr], + }) + + if (childProcess.error) { + throw childProcess.error + } + + if ( + childProcess.signal && + ["SIGINT", "SIGTERM"].includes(childProcess.signal) + ) { + console.log("abortingggg") + throw getAbortError() + } + + if (needOutput) { + console.log( + childProcess.stdout?.toString() || childProcess.stderr?.toString() + ) + } + + return { + stdout: childProcess.stdout?.toString() || "", + stderr: childProcess.stderr?.toString() || "", + } + } else { + const childProcess = await promiseExec(...(command as PromiseExecParams)) + + return { + stdout: childProcess.stdout as string, + stderr: childProcess.stderr as string, + } + } +} + +export default execute diff --git a/packages/create-medusa-app/src/utils/facts.ts b/packages/create-medusa-app/src/utils/facts.ts index f8dcea3c6c..3ca96c236a 100644 --- a/packages/create-medusa-app/src/utils/facts.ts +++ b/packages/create-medusa-app/src/utils/facts.ts @@ -10,6 +10,7 @@ export type FactBoxOptions = { processManager: ProcessManager message?: string title?: string + verbose?: boolean } const facts = [ @@ -38,25 +39,41 @@ export const getFact = () => { return facts[randIndex] } -export const showFact = (spinner: Ora, title: string) => { +export const showFact = ({ + spinner, + title, + verbose, +}: Pick & { + title: string +}) => { const fact = getFact() - spinner.text = `${title}\n${boxen(`${fact}`, { - title: chalk.cyan(`${emojify(":bulb:")} Medusa Tips`), - titleAlignment: "center", - textAlignment: "center", - padding: 1, - margin: 1, - })}` + if (verbose) { + spinner.stopAndPersist({ + symbol: chalk.cyan("⠋"), + text: title, + }) + } else { + spinner.text = `${title}\n${boxen(`${fact}`, { + title: chalk.cyan(`${emojify(":bulb:")} Medusa Tips`), + titleAlignment: "center", + textAlignment: "center", + padding: 1, + margin: 1, + })}` + } } -export const createFactBox = ( - spinner: Ora, - title: string, - processManager: ProcessManager -): NodeJS.Timeout => { - showFact(spinner, title) +export const createFactBox = ({ + spinner, + title, + processManager, + verbose, +}: Pick & { + title: string +}): NodeJS.Timeout => { + showFact({ spinner, title, verbose }) const interval = setInterval(() => { - showFact(spinner, title) + showFact({ spinner, title, verbose }) }, 10000) processManager.addInterval(interval) @@ -64,13 +81,20 @@ export const createFactBox = ( return interval } -export const resetFactBox = ( - interval: NodeJS.Timeout | null, - spinner: Ora, - successMessage: string, - processManager: ProcessManager, +export const resetFactBox = ({ + interval, + spinner, + successMessage, + processManager, + newTitle, + verbose, +}: Pick< + FactBoxOptions, + "interval" | "spinner" | "processManager" | "verbose" +> & { + successMessage: string newTitle?: string -): NodeJS.Timeout | null => { +}): NodeJS.Timeout | null => { if (interval) { clearInterval(interval) } @@ -78,7 +102,12 @@ export const resetFactBox = ( spinner.succeed(chalk.green(successMessage)).start() let newInterval = null if (newTitle) { - newInterval = createFactBox(spinner, newTitle, processManager) + newInterval = createFactBox({ + spinner, + title: newTitle, + processManager, + verbose, + }) } return newInterval @@ -90,10 +119,18 @@ export function displayFactBox({ processManager, title = "", message = "", + verbose = false, }: FactBoxOptions): NodeJS.Timeout | null { if (!message) { - return createFactBox(spinner, title, processManager) + return createFactBox({ spinner, title, processManager, verbose }) } - return resetFactBox(interval, spinner, message, processManager, title) + return resetFactBox({ + interval, + spinner, + successMessage: message, + processManager, + newTitle: title, + verbose, + }) } diff --git a/packages/create-medusa-app/src/utils/log-message.ts b/packages/create-medusa-app/src/utils/log-message.ts index c9955dd851..62602f8917 100644 --- a/packages/create-medusa-app/src/utils/log-message.ts +++ b/packages/create-medusa-app/src/utils/log-message.ts @@ -4,7 +4,7 @@ import { logger } from "./logger.js" type LogOptions = { message: string - type?: "error" | "success" | "info" | "warning" + type?: "error" | "success" | "info" | "warning" | "verbose" } export default ({ message, type = "info" }: LogOptions) => { @@ -18,6 +18,9 @@ export default ({ message, type = "info" }: LogOptions) => { case "warning": logger.warning(chalk.yellow(message)) break + case "verbose": + logger.info(`${chalk.bgYellowBright("VERBOSE LOG:")} ${message}`) + break case "error": program.error(chalk.bold.red(message)) } diff --git a/packages/create-medusa-app/src/utils/nextjs-utils.ts b/packages/create-medusa-app/src/utils/nextjs-utils.ts index c89185b782..e6b2153199 100644 --- a/packages/create-medusa-app/src/utils/nextjs-utils.ts +++ b/packages/create-medusa-app/src/utils/nextjs-utils.ts @@ -1,9 +1,10 @@ import inquirer from "inquirer" -import promiseExec from "./promise-exec.js" +import { exec } from "child_process" +import execute from "./execute.js" import { FactBoxOptions, displayFactBox } from "./facts.js" import fs from "fs" import path from "path" -import { customAlphabet, nanoid } from "nanoid" +import { customAlphabet } from "nanoid" import { isAbortError } from "./create-abort-controller.js" import logMessage from "./log-message.js" @@ -26,12 +27,14 @@ type InstallOptions = { directoryName: string abortController?: AbortController factBoxOptions: FactBoxOptions + verbose?: boolean } export async function installNextjsStarter({ directoryName, abortController, factBoxOptions, + verbose = false, }: InstallOptions): Promise { factBoxOptions.interval = displayFactBox({ ...factBoxOptions, @@ -53,15 +56,18 @@ export async function installNextjsStarter({ } try { - await promiseExec( - `npx create-next-app -e ${NEXTJS_REPO} ${nextjsDirectory}`, - { - signal: abortController?.signal, - env: { - ...process.env, - npm_config_yes: "yes", + await execute( + [ + `npx create-next-app -e ${NEXTJS_REPO} ${nextjsDirectory}`, + { + signal: abortController?.signal, + env: { + ...process.env, + npm_config_yes: "yes", + }, }, - } + ], + { verbose } ) } catch (e) { if (isAbortError(e)) { @@ -90,18 +96,21 @@ export async function installNextjsStarter({ type StartOptions = { directory: string abortController?: AbortController + verbose?: boolean } -export async function startNextjsStarter({ +export function startNextjsStarter({ directory, abortController, + verbose = false, }: StartOptions) { - try { - await promiseExec(`npm run dev`, { - cwd: directory, - signal: abortController?.signal, - }) - } catch { - // ignore abort errors + const childProcess = exec(`npm run dev`, { + cwd: directory, + signal: abortController?.signal, + }) + + if (verbose) { + childProcess.stdout?.pipe(process.stdout) + childProcess.stderr?.pipe(process.stderr) } } diff --git a/packages/create-medusa-app/src/utils/prepare-project.ts b/packages/create-medusa-app/src/utils/prepare-project.ts index 1dd041ae59..ccd144d421 100644 --- a/packages/create-medusa-app/src/utils/prepare-project.ts +++ b/packages/create-medusa-app/src/utils/prepare-project.ts @@ -2,7 +2,7 @@ import chalk from "chalk" import fs from "fs" import path from "path" import { Ora } from "ora" -import promiseExec from "./promise-exec.js" +import execute from "./execute.js" import { EOL } from "os" import { displayFactBox, FactBoxOptions } from "./facts.js" import ProcessManager from "./process-manager.js" @@ -25,6 +25,7 @@ type PrepareOptions = { onboardingType?: "default" | "nextjs" nextjsDirectory?: string client: Client | null + verbose?: boolean v2?: boolean } @@ -42,6 +43,7 @@ export default async ({ onboardingType = "default", nextjsDirectory = "", client, + verbose = false, v2 = false, }: PrepareOptions) => { // initialize execution options @@ -64,6 +66,7 @@ export default async ({ processManager, message: "", title: "", + verbose, } // initialize the invite token to return @@ -91,11 +94,13 @@ export default async ({ await processManager.runProcess({ process: async () => { try { - await promiseExec(`yarn`, execOptions) + await execute([`yarn`, execOptions], { verbose }) } catch (e) { // yarn isn't available // use npm - await promiseExec(`npm install --legacy-peer-deps`, execOptions) + await execute([`npm install --legacy-peer-deps`, execOptions], { + verbose, + }) } }, ignoreERESOLVE: true, @@ -127,11 +132,11 @@ export default async ({ await processManager.runProcess({ process: async () => { try { - await promiseExec(`yarn build`, execOptions) + await execute([`yarn build`, execOptions], { verbose }) } catch (e) { // yarn isn't available // use npm - await promiseExec(`npm run build`, execOptions) + await execute([`npm run build`, execOptions], { verbose }) } }, ignoreERESOLVE: true, @@ -148,9 +153,9 @@ export default async ({ // run migrations await processManager.runProcess({ process: async () => { - const proc = await promiseExec( - "npx @medusajs/medusa-cli@latest migrations run", - npxOptions + const proc = await execute( + ["npx @medusajs/medusa-cli@latest migrations run", npxOptions], + { verbose, needOutput: true } ) if (client) { @@ -169,7 +174,7 @@ export default async ({ } // ensure that migrations actually ran in case of an uncaught error - if (errorOccurred) { + if (errorOccurred && (proc.stderr || proc.stdout)) { throw new Error( `An error occurred while running migrations: ${ proc.stderr || proc.stdout @@ -195,12 +200,18 @@ export default async ({ await processManager.runProcess({ process: async () => { - const proc = await promiseExec( - `npx @medusajs/medusa-cli@latest user -e ${admin.email} --invite`, - npxOptions + const proc = await execute( + [ + `npx @medusajs/medusa-cli@latest user -e ${admin.email} --invite`, + npxOptions, + ], + { verbose, needOutput: true } ) + // get invite token from stdout - const match = proc.stdout.match(/Invite token: (?.+)/) + const match = (proc.stdout as string).match( + /Invite token: (?.+)/ + ) inviteToken = match?.groups?.token }, }) @@ -232,12 +243,15 @@ export default async ({ await processManager.runProcess({ process: async () => { - await promiseExec( - `npx @medusajs/medusa-cli@latest seed --seed-file=${path.join( - "data", - "seed.json" - )}`, - npxOptions + await execute( + [ + `npx @medusajs/medusa-cli@latest seed --seed-file=${path.join( + "data", + "seed.json" + )}`, + npxOptions, + ], + { verbose } ) }, }) @@ -257,12 +271,15 @@ export default async ({ await processManager.runProcess({ process: async () => { - await promiseExec( - `npx @medusajs/medusa-cli@latest seed --seed-file=${path.join( - "data", - "seed-onboarding.json" - )}`, - npxOptions + await execute( + [ + `npx @medusajs/medusa-cli@latest seed --seed-file=${path.join( + "data", + "seed-onboarding.json" + )}`, + npxOptions, + ], + { verbose } ) }, }) diff --git a/packages/create-medusa-app/src/utils/promise-exec.ts b/packages/create-medusa-app/src/utils/promise-exec.ts deleted file mode 100644 index 40230a68bc..0000000000 --- a/packages/create-medusa-app/src/utils/promise-exec.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { exec } from "child_process" -import util from "util" - -const promiseExec = util.promisify(exec) - -export default promiseExec diff --git a/packages/create-medusa-app/src/utils/start-medusa.ts b/packages/create-medusa-app/src/utils/start-medusa.ts index cf4a0a458e..6aa8af83ed 100644 --- a/packages/create-medusa-app/src/utils/start-medusa.ts +++ b/packages/create-medusa-app/src/utils/start-medusa.ts @@ -17,4 +17,5 @@ export default ({ directory, abortController }: StartOptions) => { }) childProcess.stdout?.pipe(process.stdout) + childProcess.stderr?.pipe(process.stderr) }