Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Scaffold plugin in create-medusa-app #10908

Merged
merged 16 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silent-keys-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-medusa-app": patch
---

Feat/plugin scaffolding
355 changes: 10 additions & 345 deletions packages/cli/create-medusa-app/src/commands/create.ts
Original file line number Diff line number Diff line change
@@ -1,347 +1,12 @@
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 "@medusajs/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 (
args: string[],
{
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
})

let askProjectName = args.length === 0
if (args.length > 0) {
// check if project directory already exists
const projectPath = getProjectPath(args[0], directoryPath)
if (fs.existsSync(projectPath) && fs.lstatSync(projectPath).isDirectory()) {
logMessage({
message: `A directory already exists with the name ${args[0]}. Please enter a different project name.`,
type: "warn",
})
askProjectName = true
}
}

const projectName = askProjectName ? await askForProjectName(directoryPath) : args[0]
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`
ProjectCreatorFactory,
ProjectOptions,
} from "../utils/project-creator/index.js"

/**
* Command handler to create a Medusa project or plugin
*/
export default async (args: string[], options: ProjectOptions) => {
const projectCreator = await ProjectCreatorFactory.create(args, options)
await projectCreator.create()
}
3 changes: 2 additions & 1 deletion packages/cli/create-medusa-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { program } from "commander"
import create from "./commands/create.js"

program
.description("Create a new Medusa project")
.description("Create a new Medusa project or plugin")
.argument("[project-name]", "Name of the project to create.")
.option("--plugin", "Create a plugin instead of a project.")
.option("--repo-url <url>", "URL of repository to use to setup project.")
.option("--seed", "Seed the created database with demo data.")
.option(
Expand Down
Loading
Loading