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) }