Files
medusa-store/packages/cli/create-medusa-app/src/commands/create.ts
Harminder Virk 48e00169d2 breaking: move shared HTTP utils to the framework (#9402)
Fixes: FRMW-2728, FRMW-2729

After this PR gets merged the following middleware will be exported from the `@medusajs/framework/http` import path.

- applyParamsAsFilters
- clearFiltersByKey
- applyDefaultFilters
- setContext
- getQueryConfig
- httpCompression
- maybeApplyLinkFilter
- refetchEntities
- unlessPath
- validateBody
- validateQuery

Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
2024-10-03 09:42:00 +00:00

332 lines
8.3 KiB
TypeScript

import inquirer from "inquirer"
import slugifyType from "slugify"
import chalk from "chalk"
import { getDbClientAndCredentials, runCreateDb } from "../utils/create-db.js"
import prepareProject from "../utils/prepare-project.js"
import startMedusa from "../utils/start-medusa.js"
import open from "open"
import waitOn from "wait-on"
import ora, { Ora } from "ora"
import fs from "fs"
import path from "path"
import logMessage from "../utils/log-message.js"
import createAbortController, {
isAbortError,
} from "../utils/create-abort-controller.js"
import { track } from "medusa-telemetry"
import boxen from "boxen"
import { emojify } from "node-emoji"
import ProcessManager from "../utils/process-manager.js"
import { displayFactBox, FactBoxOptions } from "../utils/facts.js"
import { EOL } from "os"
import { runCloneRepo } from "../utils/clone-repo.js"
import {
askForNextjsStarter,
installNextjsStarter,
startNextjsStarter,
} from "../utils/nextjs-utils.js"
import {
getNodeVersion,
MIN_SUPPORTED_NODE_VERSION,
} from "../utils/node-version.js"
const slugify = slugifyType.default
export type CreateOptions = {
repoUrl?: string
seed?: boolean
skipDb?: boolean
dbUrl?: string
browser?: boolean
migrations?: boolean
directoryPath?: string
withNextjsStarter?: boolean
verbose?: boolean
}
export default async ({
repoUrl = "",
seed,
skipDb,
dbUrl,
browser,
migrations,
directoryPath,
withNextjsStarter = false,
verbose = false,
}: CreateOptions) => {
const nodeVersion = getNodeVersion()
if (nodeVersion < MIN_SUPPORTED_NODE_VERSION) {
logMessage({
message: `Medusa requires at least v20 of Node.js. You're using v${nodeVersion}. Please install at least v20 and try again: https://nodejs.org/en/download`,
type: "error",
})
}
track("CREATE_CLI_CMA")
const spinner: Ora = ora()
const processManager = new ProcessManager()
const abortController = createAbortController(processManager)
const factBoxOptions: FactBoxOptions = {
interval: null,
spinner,
processManager,
message: "",
title: "",
verbose,
}
let isProjectCreated = false
let isDbInitialized = false
let printedMessage = false
let nextjsDirectory = ""
processManager.onTerminated(async () => {
spinner.stop()
// prevent an error from occurring if
// client hasn't been declared yet
if (isDbInitialized && client) {
await client.end()
}
// the SIGINT event is triggered twice once the backend runs
// this ensures that the message isn't printed twice to the user
if (!printedMessage && isProjectCreated) {
printedMessage = true
showSuccessMessage(projectName, undefined, nextjsDirectory)
}
return
})
const projectName = await askForProjectName(directoryPath)
const projectPath = getProjectPath(projectName, directoryPath)
const installNextjs = withNextjsStarter || (await askForNextjsStarter())
let dbName = !skipDb && !dbUrl ? `medusa-${slugify(projectName)}` : ""
let { client, dbConnectionString, ...rest } = !skipDb
? await getDbClientAndCredentials({
dbName,
dbUrl,
verbose,
})
: { client: null, dbConnectionString: "" }
if ("dbName" in rest) {
dbName = rest.dbName as string
}
isDbInitialized = true
track("CMA_OPTIONS", {
repoUrl,
seed,
skipDb,
browser,
migrations,
installNextjs,
verbose,
})
logMessage({
message: `${emojify(
":rocket:"
)} Starting project setup, this may take a few minutes.`,
})
spinner.start()
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
title: "Setting up project...",
})
try {
await runCloneRepo({
projectName: projectPath,
repoUrl,
abortController,
spinner,
verbose,
})
} catch {
return
}
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
message: "Created project directory",
})
nextjsDirectory = installNextjs
? await installNextjsStarter({
directoryName: projectPath,
abortController,
factBoxOptions,
verbose,
processManager,
})
: ""
if (client && !dbUrl) {
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
title: "Creating database...",
})
client = await runCreateDb({ client, dbName, spinner })
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
message: `Database ${dbName} created`,
})
}
// prepare project
let inviteToken: string | undefined = undefined
try {
inviteToken = await prepareProject({
directory: projectPath,
dbName,
dbConnectionString,
seed,
spinner,
processManager,
abortController,
skipDb,
migrations,
onboardingType: installNextjs ? "nextjs" : "default",
nextjsDirectory,
client,
verbose,
})
} catch (e: any) {
if (isAbortError(e)) {
process.exit()
}
spinner.stop()
logMessage({
message: `An error occurred while preparing project: ${e}`,
type: "error",
})
return
} finally {
// close db connection
await client?.end()
}
spinner.succeed(chalk.green("Project Prepared"))
if (skipDb || !browser) {
showSuccessMessage(projectPath, inviteToken, nextjsDirectory)
process.exit()
}
// start backend
logMessage({
message: "Starting Medusa...",
})
try {
startMedusa({
directory: projectPath,
abortController,
})
if (installNextjs && nextjsDirectory) {
startNextjsStarter({
directory: nextjsDirectory,
abortController,
verbose,
})
}
} catch (e) {
if (isAbortError(e)) {
process.exit()
}
logMessage({
message: `An error occurred while starting Medusa`,
type: "error",
})
return
}
isProjectCreated = true
await waitOn({
resources: ["http://localhost:9000/health"],
}).then(async () => {
open(
inviteToken
? `http://localhost:9000/app/invite?token=${inviteToken}&first_run=true`
: "http://localhost:9000/app"
)
})
}
async function askForProjectName(directoryPath?: string): Promise<string> {
const { projectName } = await inquirer.prompt([
{
type: "input",
name: "projectName",
message: "What's the name of your project?",
default: "my-medusa-store",
filter: (input) => {
return slugify(input).toLowerCase()
},
validate: (input) => {
if (!input.length) {
return "Please enter a project name"
}
const projectPath = getProjectPath(input, directoryPath)
return fs.existsSync(projectPath) &&
fs.lstatSync(projectPath).isDirectory()
? "A directory already exists with the same name. Please enter a different project name."
: true
},
},
])
return projectName
}
function showSuccessMessage(
projectName: string,
inviteToken?: string,
nextjsDirectory?: string
) {
logMessage({
message: boxen(
chalk.green(
// eslint-disable-next-line prettier/prettier
`Change to the \`${projectName}\` directory to explore your Medusa project.${EOL}${EOL}Start your Medusa app again with the following command:${EOL}${EOL}yarn dev${EOL}${EOL}${
inviteToken
? `After you start the Medusa app, you can set a password for your admin user with the URL ${getInviteUrl(
inviteToken
)}${EOL}${EOL}`
: ""
}${
nextjsDirectory?.length
? `The Next.js Starter storefront was installed in the \`${nextjsDirectory}\` directory. Change to that directory and start it with the following command:${EOL}${EOL}npm run dev${EOL}${EOL}`
: ""
}Check out the Medusa documentation to start your development:${EOL}${EOL}https://docs.medusajs.com/${EOL}${EOL}Star us on GitHub if you like what we're building:${EOL}${EOL}https://github.com/medusajs/medusa/stargazers`
),
{
titleAlignment: "center",
textAlignment: "center",
padding: 1,
margin: 1,
float: "center",
}
),
})
}
function getProjectPath(projectName: string, directoryPath?: string) {
return path.join(directoryPath || "", projectName)
}
function getInviteUrl(inviteToken: string) {
return `http://localhost:7001/invite?token=${inviteToken}&first_run=true`
}