Skip to content

Commit

Permalink
feat: Scaffold plugin in create-medusa-app (medusajs#10908)
Browse files Browse the repository at this point in the history
RESOLVES FRMW-2862

**What**
This PR enable the `create-medusa-app` CLI to accept a new `--plugin` option to scaffold a plugin. This is complementary to all the plugin commands being created/adjusted separately to that pr.
Also, this pr brings a little refactoring around resource scaffolding, the idea was to contain the refactoring to a little area and not expend it to the entire create-medusa-app package to not disrupt and expand the scope for which the purpose was to introduce the plugin scaffolding capabilities

**Addition**
- medusa project will get their package.json name changed to the project name
- Remove build step from medusa project creation

**Plugin flow**

- in the plugin
  - `npx create-medsa-app --plugin`
  - `yarn dev`
- in the project
  - `yalc add plugin-name`
  - `yarn dev`

Any changes on the plugin will publish, push in the local registry which will fire the hot reload of the app and include the new changes from the plugin
  • Loading branch information
adrien2p authored Jan 13, 2025
1 parent b0f581c commit c895ed8
Show file tree
Hide file tree
Showing 10 changed files with 681 additions and 377 deletions.
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

0 comments on commit c895ed8

Please sign in to comment.