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 12:13:44 +03:00
committed by GitHub
parent e58e81fd25
commit fe1d3a4a78
13 changed files with 285 additions and 87 deletions

View File

@@ -0,0 +1,5 @@
---
"create-medusa-app": patch
---
feat(create-medusa-app): Add a `--verbose` option.

View File

@@ -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) {

View File

@@ -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.",

View File

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

View File

@@ -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",
}
}

View File

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

View File

@@ -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

View File

@@ -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<FactBoxOptions, "spinner" | "verbose"> & {
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<FactBoxOptions, "spinner" | "processManager" | "verbose"> & {
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,
})
}

View File

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

View File

@@ -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<string> {
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)
}
}

View File

@@ -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: (?<token>.+)/)
const match = (proc.stdout as string).match(
/Invite token: (?<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 }
)
},
})

View File

@@ -1,6 +0,0 @@
import { exec } from "child_process"
import util from "util"
const promiseExec = util.promisify(exec)
export default promiseExec

View File

@@ -17,4 +17,5 @@ export default ({ directory, abortController }: StartOptions) => {
})
childProcess.stdout?.pipe(process.stdout)
childProcess.stderr?.pipe(process.stderr)
}