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.
This commit is contained in:
Shahed Nasser
2024-04-01 09:13:44 +00:00
committed by GitHub
parent e58e81fd25
commit fe1d3a4a78
13 changed files with 285 additions and 87 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"create-medusa-app": patch
---
feat(create-medusa-app): Add a `--verbose` option.
@@ -42,6 +42,7 @@ export type CreateOptions = {
migrations?: boolean migrations?: boolean
directoryPath?: string directoryPath?: string
withNextjsStarter?: boolean withNextjsStarter?: boolean
verbose?: boolean
v2?: boolean v2?: boolean
} }
@@ -55,6 +56,7 @@ export default async ({
migrations, migrations,
directoryPath, directoryPath,
withNextjsStarter = false, withNextjsStarter = false,
verbose = false,
v2 = false, v2 = false,
}: CreateOptions) => { }: CreateOptions) => {
track("CREATE_CLI_CMA") track("CREATE_CLI_CMA")
@@ -68,6 +70,7 @@ export default async ({
processManager, processManager,
message: "", message: "",
title: "", title: "",
verbose,
} }
const dbName = !skipDb && !dbUrl ? `medusa-${nanoid(4)}` : "" const dbName = !skipDb && !dbUrl ? `medusa-${nanoid(4)}` : ""
let isProjectCreated = false let isProjectCreated = false
@@ -103,6 +106,7 @@ export default async ({
? await getDbClientAndCredentials({ ? await getDbClientAndCredentials({
dbName, dbName,
dbUrl, dbUrl,
verbose,
}) })
: { client: null, dbConnectionString: "" } : { client: null, dbConnectionString: "" }
isDbInitialized = true isDbInitialized = true
@@ -115,6 +119,7 @@ export default async ({
browser, browser,
migrations, migrations,
installNextjs, installNextjs,
verbose,
}) })
logMessage({ logMessage({
@@ -136,6 +141,7 @@ export default async ({
repoUrl, repoUrl,
abortController, abortController,
spinner, spinner,
verbose,
v2, v2,
}) })
} catch { } catch {
@@ -152,6 +158,7 @@ export default async ({
directoryName: projectPath, directoryName: projectPath,
abortController, abortController,
factBoxOptions, factBoxOptions,
verbose,
}) })
: "" : ""
@@ -187,6 +194,7 @@ export default async ({
onboardingType: installNextjs ? "nextjs" : "default", onboardingType: installNextjs ? "nextjs" : "default",
nextjsDirectory, nextjsDirectory,
client, client,
verbose,
v2, v2,
}) })
} catch (e: any) { } catch (e: any) {
@@ -225,9 +233,10 @@ export default async ({
}) })
if (installNextjs && nextjsDirectory) { if (installNextjs && nextjsDirectory) {
void startNextjsStarter({ startNextjsStarter({
directory: nextjsDirectory, directory: nextjsDirectory,
abortController, abortController,
verbose,
}) })
} }
} catch (e) { } catch (e) {
+5
View File
@@ -38,6 +38,11 @@ program
"Install the Next.js starter along with the Medusa backend", "Install the Next.js starter along with the Medusa backend",
false false
) )
.option(
"--verbose",
"Show all logs of underlying commands. Useful for debugging.",
false
)
.option( .option(
"--v2", "--v2",
"Install Medusa with the V2 feature flag enabled. WARNING: Medusa V2 is still in development and shouldn't be used in production.", "Install Medusa with the V2 feature flag enabled. WARNING: Medusa V2 is still in development and shouldn't be used in production.",
@@ -1,4 +1,4 @@
import promiseExec from "./promise-exec.js" import execute from "./execute.js"
import { Ora } from "ora" import { Ora } from "ora"
import { isAbortError } from "./create-abort-controller.js" import { isAbortError } from "./create-abort-controller.js"
import logMessage from "./log-message.js" import logMessage from "./log-message.js"
@@ -9,6 +9,7 @@ type CloneRepoOptions = {
directoryName?: string directoryName?: string
repoUrl?: string repoUrl?: string
abortController?: AbortController abortController?: AbortController
verbose?: boolean
v2?: boolean v2?: boolean
} }
@@ -19,15 +20,19 @@ export default async function cloneRepo({
directoryName = "", directoryName = "",
repoUrl, repoUrl,
abortController, abortController,
verbose = false,
v2 = false, v2 = false,
}: CloneRepoOptions) { }: CloneRepoOptions) {
await promiseExec( await execute(
`git clone ${repoUrl || DEFAULT_REPO}${ [
v2 ? ` -b ${V2_BRANCH}` : "" `git clone ${repoUrl || DEFAULT_REPO}${
} ${directoryName}`, v2 ? ` -b ${V2_BRANCH}` : ""
{ } ${directoryName}`,
signal: abortController?.signal, {
} signal: abortController?.signal,
},
],
{ verbose }
) )
} }
@@ -36,12 +41,14 @@ export async function runCloneRepo({
repoUrl, repoUrl,
abortController, abortController,
spinner, spinner,
verbose = false,
v2 = false, v2 = false,
}: { }: {
projectName: string projectName: string
repoUrl: string repoUrl: string
abortController: AbortController abortController: AbortController
spinner: Ora spinner: Ora
verbose?: boolean
v2?: boolean v2?: boolean
}) { }) {
try { try {
@@ -49,6 +56,7 @@ export async function runCloneRepo({
directoryName: projectName, directoryName: projectName,
repoUrl, repoUrl,
abortController, abortController,
verbose,
v2, v2,
}) })
@@ -8,3 +8,9 @@ export default (processManager: ProcessManager) => {
export const isAbortError = (e: any) => export const isAbortError = (e: any) =>
e !== null && "code" in e && e.code === "ABORT_ERR" e !== null && "code" in e && e.code === "ABORT_ERR"
export const getAbortError = () => {
return {
code: "ABORT_ERR",
}
}
@@ -52,7 +52,13 @@ export async function runCreateDb({
return newClient return newClient
} }
async function getForDbName(dbName: string): Promise<{ async function getForDbName({
dbName,
verbose = false,
}: {
dbName: string
verbose?: boolean
}): Promise<{
client: pg.Client client: pg.Client
dbConnectionString: string dbConnectionString: string
}> { }> {
@@ -66,6 +72,12 @@ async function getForDbName(dbName: string): Promise<{
password: postgresPassword, password: postgresPassword,
}) })
} catch (e) { } 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 // ask for the user's postgres credentials
const answers = await inquirer.prompt([ 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 client: pg.Client
dbConnectionString: string dbConnectionString: string
}> { }> {
@@ -125,6 +143,12 @@ async function getForDbUrl(dbUrl: string): Promise<{
connectionString: dbUrl, connectionString: dbUrl,
}) })
} catch (e) { } catch (e) {
if (verbose) {
logMessage({
message: `The following error occured when connecting to the database: ${e}`,
type: "verbose",
})
}
logMessage({ logMessage({
message: `Couldn't connect to PostgreSQL using the database URL you passed. Make sure it's correct and try again.`, message: `Couldn't connect to PostgreSQL using the database URL you passed. Make sure it's correct and try again.`,
type: "error", type: "error",
@@ -140,13 +164,21 @@ async function getForDbUrl(dbUrl: string): Promise<{
export async function getDbClientAndCredentials({ export async function getDbClientAndCredentials({
dbName = "", dbName = "",
dbUrl = "", dbUrl = "",
verbose = false,
}): Promise<{ }): Promise<{
client: pg.Client client: pg.Client
dbConnectionString: string dbConnectionString: string
verbose?: boolean
}> { }> {
if (dbName) { if (dbName) {
return await getForDbName(dbName) return await getForDbName({
dbName,
verbose,
})
} else { } else {
return await getForDbUrl(dbUrl) return await getForDbUrl({
dbUrl,
verbose,
})
} }
} }
@@ -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<typeof promiseExec>
type SpawnParams = [string, SpawnSyncOptions]
const execute = async (
command: SpawnParams | PromiseExecParams,
{ verbose = false, needOutput = false }: VerboseOptions
): Promise<ExecuteOptions> => {
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
+61 -24
View File
@@ -10,6 +10,7 @@ export type FactBoxOptions = {
processManager: ProcessManager processManager: ProcessManager
message?: string message?: string
title?: string title?: string
verbose?: boolean
} }
const facts = [ const facts = [
@@ -38,25 +39,41 @@ export const getFact = () => {
return facts[randIndex] return facts[randIndex]
} }
export const showFact = (spinner: Ora, title: string) => { export const showFact = ({
spinner,
title,
verbose,
}: Pick<FactBoxOptions, "spinner" | "verbose"> & {
title: string
}) => {
const fact = getFact() const fact = getFact()
spinner.text = `${title}\n${boxen(`${fact}`, { if (verbose) {
title: chalk.cyan(`${emojify(":bulb:")} Medusa Tips`), spinner.stopAndPersist({
titleAlignment: "center", symbol: chalk.cyan("⠋"),
textAlignment: "center", text: title,
padding: 1, })
margin: 1, } 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 = ( export const createFactBox = ({
spinner: Ora, spinner,
title: string, title,
processManager: ProcessManager processManager,
): NodeJS.Timeout => { verbose,
showFact(spinner, title) }: Pick<FactBoxOptions, "spinner" | "processManager" | "verbose"> & {
title: string
}): NodeJS.Timeout => {
showFact({ spinner, title, verbose })
const interval = setInterval(() => { const interval = setInterval(() => {
showFact(spinner, title) showFact({ spinner, title, verbose })
}, 10000) }, 10000)
processManager.addInterval(interval) processManager.addInterval(interval)
@@ -64,13 +81,20 @@ export const createFactBox = (
return interval return interval
} }
export const resetFactBox = ( export const resetFactBox = ({
interval: NodeJS.Timeout | null, interval,
spinner: Ora, spinner,
successMessage: string, successMessage,
processManager: ProcessManager, processManager,
newTitle,
verbose,
}: Pick<
FactBoxOptions,
"interval" | "spinner" | "processManager" | "verbose"
> & {
successMessage: string
newTitle?: string newTitle?: string
): NodeJS.Timeout | null => { }): NodeJS.Timeout | null => {
if (interval) { if (interval) {
clearInterval(interval) clearInterval(interval)
} }
@@ -78,7 +102,12 @@ export const resetFactBox = (
spinner.succeed(chalk.green(successMessage)).start() spinner.succeed(chalk.green(successMessage)).start()
let newInterval = null let newInterval = null
if (newTitle) { if (newTitle) {
newInterval = createFactBox(spinner, newTitle, processManager) newInterval = createFactBox({
spinner,
title: newTitle,
processManager,
verbose,
})
} }
return newInterval return newInterval
@@ -90,10 +119,18 @@ export function displayFactBox({
processManager, processManager,
title = "", title = "",
message = "", message = "",
verbose = false,
}: FactBoxOptions): NodeJS.Timeout | null { }: FactBoxOptions): NodeJS.Timeout | null {
if (!message) { 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,
})
} }
@@ -4,7 +4,7 @@ import { logger } from "./logger.js"
type LogOptions = { type LogOptions = {
message: string message: string
type?: "error" | "success" | "info" | "warning" type?: "error" | "success" | "info" | "warning" | "verbose"
} }
export default ({ message, type = "info" }: LogOptions) => { export default ({ message, type = "info" }: LogOptions) => {
@@ -18,6 +18,9 @@ export default ({ message, type = "info" }: LogOptions) => {
case "warning": case "warning":
logger.warning(chalk.yellow(message)) logger.warning(chalk.yellow(message))
break break
case "verbose":
logger.info(`${chalk.bgYellowBright("VERBOSE LOG:")} ${message}`)
break
case "error": case "error":
program.error(chalk.bold.red(message)) program.error(chalk.bold.red(message))
} }
@@ -1,9 +1,10 @@
import inquirer from "inquirer" 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 { FactBoxOptions, displayFactBox } from "./facts.js"
import fs from "fs" import fs from "fs"
import path from "path" import path from "path"
import { customAlphabet, nanoid } from "nanoid" import { customAlphabet } from "nanoid"
import { isAbortError } from "./create-abort-controller.js" import { isAbortError } from "./create-abort-controller.js"
import logMessage from "./log-message.js" import logMessage from "./log-message.js"
@@ -26,12 +27,14 @@ type InstallOptions = {
directoryName: string directoryName: string
abortController?: AbortController abortController?: AbortController
factBoxOptions: FactBoxOptions factBoxOptions: FactBoxOptions
verbose?: boolean
} }
export async function installNextjsStarter({ export async function installNextjsStarter({
directoryName, directoryName,
abortController, abortController,
factBoxOptions, factBoxOptions,
verbose = false,
}: InstallOptions): Promise<string> { }: InstallOptions): Promise<string> {
factBoxOptions.interval = displayFactBox({ factBoxOptions.interval = displayFactBox({
...factBoxOptions, ...factBoxOptions,
@@ -53,15 +56,18 @@ export async function installNextjsStarter({
} }
try { try {
await promiseExec( await execute(
`npx create-next-app -e ${NEXTJS_REPO} ${nextjsDirectory}`, [
{ `npx create-next-app -e ${NEXTJS_REPO} ${nextjsDirectory}`,
signal: abortController?.signal, {
env: { signal: abortController?.signal,
...process.env, env: {
npm_config_yes: "yes", ...process.env,
npm_config_yes: "yes",
},
}, },
} ],
{ verbose }
) )
} catch (e) { } catch (e) {
if (isAbortError(e)) { if (isAbortError(e)) {
@@ -90,18 +96,21 @@ export async function installNextjsStarter({
type StartOptions = { type StartOptions = {
directory: string directory: string
abortController?: AbortController abortController?: AbortController
verbose?: boolean
} }
export async function startNextjsStarter({ export function startNextjsStarter({
directory, directory,
abortController, abortController,
verbose = false,
}: StartOptions) { }: StartOptions) {
try { const childProcess = exec(`npm run dev`, {
await promiseExec(`npm run dev`, { cwd: directory,
cwd: directory, signal: abortController?.signal,
signal: abortController?.signal, })
})
} catch { if (verbose) {
// ignore abort errors childProcess.stdout?.pipe(process.stdout)
childProcess.stderr?.pipe(process.stderr)
} }
} }
@@ -2,7 +2,7 @@ import chalk from "chalk"
import fs from "fs" import fs from "fs"
import path from "path" import path from "path"
import { Ora } from "ora" import { Ora } from "ora"
import promiseExec from "./promise-exec.js" import execute from "./execute.js"
import { EOL } from "os" import { EOL } from "os"
import { displayFactBox, FactBoxOptions } from "./facts.js" import { displayFactBox, FactBoxOptions } from "./facts.js"
import ProcessManager from "./process-manager.js" import ProcessManager from "./process-manager.js"
@@ -25,6 +25,7 @@ type PrepareOptions = {
onboardingType?: "default" | "nextjs" onboardingType?: "default" | "nextjs"
nextjsDirectory?: string nextjsDirectory?: string
client: Client | null client: Client | null
verbose?: boolean
v2?: boolean v2?: boolean
} }
@@ -42,6 +43,7 @@ export default async ({
onboardingType = "default", onboardingType = "default",
nextjsDirectory = "", nextjsDirectory = "",
client, client,
verbose = false,
v2 = false, v2 = false,
}: PrepareOptions) => { }: PrepareOptions) => {
// initialize execution options // initialize execution options
@@ -64,6 +66,7 @@ export default async ({
processManager, processManager,
message: "", message: "",
title: "", title: "",
verbose,
} }
// initialize the invite token to return // initialize the invite token to return
@@ -91,11 +94,13 @@ export default async ({
await processManager.runProcess({ await processManager.runProcess({
process: async () => { process: async () => {
try { try {
await promiseExec(`yarn`, execOptions) await execute([`yarn`, execOptions], { verbose })
} catch (e) { } catch (e) {
// yarn isn't available // yarn isn't available
// use npm // use npm
await promiseExec(`npm install --legacy-peer-deps`, execOptions) await execute([`npm install --legacy-peer-deps`, execOptions], {
verbose,
})
} }
}, },
ignoreERESOLVE: true, ignoreERESOLVE: true,
@@ -127,11 +132,11 @@ export default async ({
await processManager.runProcess({ await processManager.runProcess({
process: async () => { process: async () => {
try { try {
await promiseExec(`yarn build`, execOptions) await execute([`yarn build`, execOptions], { verbose })
} catch (e) { } catch (e) {
// yarn isn't available // yarn isn't available
// use npm // use npm
await promiseExec(`npm run build`, execOptions) await execute([`npm run build`, execOptions], { verbose })
} }
}, },
ignoreERESOLVE: true, ignoreERESOLVE: true,
@@ -148,9 +153,9 @@ export default async ({
// run migrations // run migrations
await processManager.runProcess({ await processManager.runProcess({
process: async () => { process: async () => {
const proc = await promiseExec( const proc = await execute(
"npx @medusajs/medusa-cli@latest migrations run", ["npx @medusajs/medusa-cli@latest migrations run", npxOptions],
npxOptions { verbose, needOutput: true }
) )
if (client) { if (client) {
@@ -169,7 +174,7 @@ export default async ({
} }
// ensure that migrations actually ran in case of an uncaught error // ensure that migrations actually ran in case of an uncaught error
if (errorOccurred) { if (errorOccurred && (proc.stderr || proc.stdout)) {
throw new Error( throw new Error(
`An error occurred while running migrations: ${ `An error occurred while running migrations: ${
proc.stderr || proc.stdout proc.stderr || proc.stdout
@@ -195,12 +200,18 @@ export default async ({
await processManager.runProcess({ await processManager.runProcess({
process: async () => { process: async () => {
const proc = await promiseExec( const proc = await execute(
`npx @medusajs/medusa-cli@latest user -e ${admin.email} --invite`, [
npxOptions `npx @medusajs/medusa-cli@latest user -e ${admin.email} --invite`,
npxOptions,
],
{ verbose, needOutput: true }
) )
// get invite token from stdout // get invite token from stdout
const match = proc.stdout.match(/Invite token: (?<token>.+)/) const match = (proc.stdout as string).match(
/Invite token: (?<token>.+)/
)
inviteToken = match?.groups?.token inviteToken = match?.groups?.token
}, },
}) })
@@ -232,12 +243,15 @@ export default async ({
await processManager.runProcess({ await processManager.runProcess({
process: async () => { process: async () => {
await promiseExec( await execute(
`npx @medusajs/medusa-cli@latest seed --seed-file=${path.join( [
"data", `npx @medusajs/medusa-cli@latest seed --seed-file=${path.join(
"seed.json" "data",
)}`, "seed.json"
npxOptions )}`,
npxOptions,
],
{ verbose }
) )
}, },
}) })
@@ -257,12 +271,15 @@ export default async ({
await processManager.runProcess({ await processManager.runProcess({
process: async () => { process: async () => {
await promiseExec( await execute(
`npx @medusajs/medusa-cli@latest seed --seed-file=${path.join( [
"data", `npx @medusajs/medusa-cli@latest seed --seed-file=${path.join(
"seed-onboarding.json" "data",
)}`, "seed-onboarding.json"
npxOptions )}`,
npxOptions,
],
{ verbose }
) )
}, },
}) })
@@ -1,6 +0,0 @@
import { exec } from "child_process"
import util from "util"
const promiseExec = util.promisify(exec)
export default promiseExec
@@ -17,4 +17,5 @@ export default ({ directory, abortController }: StartOptions) => {
}) })
childProcess.stdout?.pipe(process.stdout) childProcess.stdout?.pipe(process.stdout)
childProcess.stderr?.pipe(process.stderr)
} }