diff --git a/packages/cli/create-medusa-app/src/commands/create-refactoring.ts b/packages/cli/create-medusa-app/src/commands/create-refactoring.ts deleted file mode 100644 index c6d1a8bc34191..0000000000000 --- a/packages/cli/create-medusa-app/src/commands/create-refactoring.ts +++ /dev/null @@ -1,522 +0,0 @@ -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 - -interface ProjectOptions { - repoUrl?: string - seed?: boolean - skipDb?: boolean - dbUrl?: string - browser?: boolean - migrations?: boolean - directoryPath?: string - withNextjsStarter?: boolean - verbose?: boolean -} - -interface ProjectCreator { - create(): Promise -} - -// Base class for common project functionality -abstract class BaseProjectCreator { - protected spinner: Ora - protected processManager: ProcessManager - protected abortController: AbortController - protected factBoxOptions: FactBoxOptions - protected projectName: string - protected projectPath: string - protected isProjectCreated: boolean = false - protected printedMessage: boolean = false - - constructor( - projectName: string, - protected options: ProjectOptions, - protected args: string[] - ) { - this.spinner = ora() - this.processManager = new ProcessManager() - this.abortController = createAbortController(this.processManager) - this.projectName = projectName - const basePath = - typeof options.directoryPath === "string" ? options.directoryPath : "" - this.projectPath = path.join(basePath, projectName) - - this.factBoxOptions = { - interval: null, - spinner: this.spinner, - processManager: this.processManager, - message: "", - title: "", - verbose: options.verbose || false, - } - } - - protected getProjectPath(projectName: string): string { - return path.join(this.options.directoryPath ?? "", projectName) - } - - protected abstract showSuccessMessage(): void - - protected abstract setupProcessManager(): void -} - -// Plugin Project Creator -class PluginProjectCreator - extends BaseProjectCreator - implements ProjectCreator -{ - constructor(projectName: string, options: ProjectOptions, args: string[]) { - super(projectName, options, args) - this.setupProcessManager() - } - - async create(): Promise { - track("CREATE_CLI_CMP") - - logMessage({ - message: `${emojify( - ":rocket:" - )} Starting plugin setup, this may take a few minutes.`, - }) - - this.spinner.start() - this.factBoxOptions.interval = displayFactBox({ - ...this.factBoxOptions, - title: "Setting up plugin...", - }) - - try { - await this.cloneAndPreparePlugin() - this.spinner.succeed(chalk.green("Plugin Prepared")) - this.showSuccessMessage() - } catch (e: any) { - this.handleError(e) - } - } - - private async cloneAndPreparePlugin(): Promise { - await runCloneRepo({ - projectName: this.projectPath, - repoUrl: this.options.repoUrl ?? "", - abortController: this.abortController, - spinner: this.spinner, - verbose: this.options.verbose, - }) - - this.factBoxOptions.interval = displayFactBox({ - ...this.factBoxOptions, - message: "Created plugin directory", - }) - - await prepareProject({ - isPlugin: true, - directory: this.projectPath, - projectName: this.projectName, - spinner: this.spinner, - processManager: this.processManager, - abortController: this.abortController, - verbose: this.options.verbose, - }) - } - - private handleError(e: any): void { - if (isAbortError(e)) { - process.exit() - } - - this.spinner.stop() - logMessage({ - message: `An error occurred while preparing plugin: ${e}`, - type: "error", - }) - } - - protected showSuccessMessage(): void { - logMessage({ - message: boxen( - chalk.green( - `Change to the \`${this.projectName}\` directory to explore your Medusa plugin.${EOL}${EOL}Check out the Medusa plugin 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", - } - ), - }) - } - - protected setupProcessManager(): void { - this.processManager.onTerminated(async () => { - this.spinner.stop() - - if (!this.printedMessage && this.isProjectCreated) { - this.printedMessage = true - this.showSuccessMessage() - } - return - }) - } -} - -// Medusa Project Creator -class MedusaProjectCreator - extends BaseProjectCreator - implements ProjectCreator -{ - private client: any = null - private dbConnectionString: string = "" - private isDbInitialized: boolean = false - private nextjsDirectory: string = "" - private inviteToken?: string - - constructor(projectName: string, options: ProjectOptions, args: string[]) { - super(projectName, options, args) - this.setupProcessManager() - } - - async create(): Promise { - track("CREATE_CLI_CMA") - - try { - await this.initializeProject() - await this.setupProject() - await this.startServices() - } catch (e: any) { - this.handleError(e) - } - } - - private async initializeProject(): Promise { - const installNextjs = - this.options.withNextjsStarter || (await askForNextjsStarter()) - - if (!this.options.skipDb) { - await this.setupDatabase() - } - - logMessage({ - message: `${emojify( - ":rocket:" - )} Starting project setup, this may take a few minutes.`, - }) - - this.spinner.start() - - this.factBoxOptions.interval = displayFactBox({ - ...this.factBoxOptions, - title: "Setting up project...", - }) - - try { - await runCloneRepo({ - projectName: this.projectPath, - repoUrl: this.options.repoUrl ?? "", - abortController: this.abortController, - spinner: this.spinner, - verbose: this.options.verbose, - }) - - this.factBoxOptions.interval = displayFactBox({ - ...this.factBoxOptions, - message: "Created project directory", - }) - - if (installNextjs) { - this.nextjsDirectory = await installNextjsStarter({ - directoryName: this.projectPath, - abortController: this.abortController, - factBoxOptions: this.factBoxOptions, - verbose: this.options.verbose, - processManager: this.processManager, - }) - } - } catch (e) { - throw e - } - } - - private async setupDatabase(): Promise { - const dbName = `medusa-${slugify(this.projectName)}` - const { client, dbConnectionString, ...rest } = - await getDbClientAndCredentials({ - dbName, - dbUrl: this.options.dbUrl, - verbose: this.options.verbose, - }) - - this.client = client - this.dbConnectionString = dbConnectionString - this.isDbInitialized = true - - if (!this.options.dbUrl) { - this.factBoxOptions.interval = displayFactBox({ - ...this.factBoxOptions, - title: "Creating database...", - }) - - this.client = await runCreateDb({ - client: this.client, - dbName, - spinner: this.spinner, - }) - - this.factBoxOptions.interval = displayFactBox({ - ...this.factBoxOptions, - message: `Database ${dbName} created`, - }) - } - } - - private async setupProject(): Promise { - try { - this.inviteToken = await prepareProject({ - isPlugin: false, - directory: this.projectPath, - dbConnectionString: this.dbConnectionString, - seed: this.options.seed, - spinner: this.spinner, - processManager: this.processManager, - abortController: this.abortController, - skipDb: this.options.skipDb, - migrations: this.options.migrations, - onboardingType: this.nextjsDirectory ? "nextjs" : "default", - nextjsDirectory: this.nextjsDirectory, - client: this.client, - verbose: this.options.verbose, - }) - } finally { - await this.client?.end() - } - - this.spinner.succeed(chalk.green("Project Prepared")) - } - - private async startServices(): Promise { - if (this.options.skipDb || !this.options.browser) { - this.showSuccessMessage() - process.exit() - } - - logMessage({ - message: "Starting Medusa...", - }) - - startMedusa({ - directory: this.projectPath, - abortController: this.abortController, - }) - - if (this.nextjsDirectory) { - startNextjsStarter({ - directory: this.nextjsDirectory, - abortController: this.abortController, - verbose: this.options.verbose, - }) - } - - this.isProjectCreated = true - - await this.openBrowser() - } - - private async openBrowser(): Promise { - await waitOn({ - resources: ["http://localhost:9000/health"], - }).then(async () => { - open( - this.inviteToken - ? `http://localhost:9000/app/invite?token=${this.inviteToken}&first_run=true` - : "http://localhost:9000/app" - ) - }) - } - - private handleError(e: any): void { - if (isAbortError(e)) { - process.exit() - } - - this.spinner.stop() - logMessage({ - message: `An error occurred: ${e}`, - type: "error", - }) - } - - protected showSuccessMessage(): void { - logMessage({ - message: boxen( - chalk.green( - `Change to the \`${ - this.projectName - }\` directory to explore your Medusa project.${EOL}${EOL}Start your Medusa app again with the following command:${EOL}${EOL}yarn dev${EOL}${EOL}${ - this.inviteToken - ? `After you start the Medusa app, you can set a password for your admin user with the URL http://localhost:7001/invite?token=${this.inviteToken}&first_run=true${EOL}${EOL}` - : "" - }${ - this.nextjsDirectory?.length - ? `The Next.js Starter storefront was installed in the \`${this.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", - } - ), - }) - } - - protected setupProcessManager(): void { - this.processManager.onTerminated(async () => { - this.spinner.stop() - - // prevent an error from occurring if - // client hasn't been declared yet - if (this.isDbInitialized && this.client) { - await this.client.end() - } - - if (!this.printedMessage && this.isProjectCreated) { - this.printedMessage = true - this.showSuccessMessage() - } - return - }) - } -} - -// Project Factory -class ProjectCreatorFactory { - static async create( - args: string[], - options: ProjectOptions - ): Promise { - const isPlugin = args.indexOf("--plugin") !== -1 - if (isPlugin) { - args.splice(args.indexOf("--plugin"), 1) - } - - ProjectCreatorFactory.validateNodeVersion() - - const projectName = await ProjectCreatorFactory.getProjectName( - args, - options.directoryPath, - isPlugin - ) - - return isPlugin - ? new PluginProjectCreator(projectName, options, args) - : new MedusaProjectCreator(projectName, options, args) - } - - private static validateNodeVersion(): void { - 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", - }) - } - } - - private static async getProjectName( - args: string[], - directoryPath?: string, - isPlugin?: boolean - ): Promise { - let askProjectName = args.length === 0 - if (args.length > 0) { - const projectPath = path.join(directoryPath || "", args[0]) - if ( - fs.existsSync(projectPath) && - fs.lstatSync(projectPath).isDirectory() - ) { - logMessage({ - message: `A directory already exists with the name ${ - args[0] - }. Please enter a different ${isPlugin ? "plugin" : "project"} name.`, - type: "warn", - }) - askProjectName = true - } - } - - return askProjectName - ? await askForProjectName(directoryPath, isPlugin) - : args[0] - } -} - -async function askForProjectName( - directoryPath?: string, - isPlugin?: boolean -): Promise { - const { projectName } = await inquirer.prompt([ - { - type: "input", - name: "projectName", - message: `What's the name of your ${isPlugin ? "plugin" : "project"}?`, - default: "my-medusa-store", - filter: (input) => { - return slugify(input).toLowerCase() - }, - validate: (input) => { - if (!input.length) { - return `Please enter a ${isPlugin ? "plugin" : "project"} name` - } - const projectPath = path.join(directoryPath || "", input) - return fs.existsSync(projectPath) && - fs.lstatSync(projectPath).isDirectory() - ? `A directory already exists with the same name. Please enter a different ${ - isPlugin ? "plugin" : "project" - } name.` - : true - }, - }, - ]) - return projectName -} - -// Main entry point -export default async (args: string[], options: ProjectOptions) => { - const projectCreator = await ProjectCreatorFactory.create(args, options) - await projectCreator.create() -} diff --git a/packages/cli/create-medusa-app/src/commands/create.ts b/packages/cli/create-medusa-app/src/commands/create.ts index d91ee9dca7913..c61aa440c41c1 100644 --- a/packages/cli/create-medusa-app/src/commands/create.ts +++ b/packages/cli/create-medusa-app/src/commands/create.ts @@ -32,7 +32,7 @@ import { const slugify = slugifyType.default -export type CreateOptions = { +interface ProjectOptions { repoUrl?: string seed?: boolean skipDb?: boolean @@ -42,411 +42,444 @@ export type CreateOptions = { directoryPath?: string withNextjsStarter?: boolean verbose?: boolean + plugin?: boolean } -export default async ( - args: string[], - { - repoUrl = "", - seed, - skipDb, - dbUrl, - browser, - migrations, - directoryPath, - withNextjsStarter = false, - verbose = false, - }: CreateOptions -) => { - let isPlugin = false - if (args.indexOf("--plugin") !== -1) { - isPlugin = true - args.splice(args.indexOf("--plugin"), 1) - } +interface ProjectCreator { + create(): Promise +} - 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", - }) +// Base class for common project functionality +abstract class BaseProjectCreator { + protected spinner: Ora + protected processManager: ProcessManager + protected abortController: AbortController + protected factBoxOptions: FactBoxOptions + protected projectName: string + protected projectPath: string + protected isProjectCreated: boolean = false + protected printedMessage: boolean = false + + constructor( + projectName: string, + protected options: ProjectOptions, + protected args: string[] + ) { + this.spinner = ora() + this.processManager = new ProcessManager() + this.abortController = createAbortController(this.processManager) + this.projectName = projectName + const basePath = + typeof options.directoryPath === "string" ? options.directoryPath : "" + this.projectPath = path.join(basePath, projectName) + + this.factBoxOptions = { + interval: null, + spinner: this.spinner, + processManager: this.processManager, + message: "", + title: "", + verbose: options.verbose || false, + } } - if (isPlugin) { - createPluginProject(args, { - verbose, - directoryPath, - repoUrl, - }) - } else { - createMedusaProject(args, { - repoUrl, - seed, - skipDb, - dbUrl, - browser, - migrations, - directoryPath, - withNextjsStarter, - verbose, - }) + protected getProjectPath(projectName: string): string { + return path.join(this.options.directoryPath ?? "", projectName) } + + protected abstract showSuccessMessage(): void + + protected abstract setupProcessManager(): void } -async function createPluginProject( - args: string[], - { - verbose = false, - directoryPath, - repoUrl = "", - }: { - verbose?: boolean - directoryPath?: string - repoUrl?: string +// Plugin Project Creator +class PluginProjectCreator + extends BaseProjectCreator + implements ProjectCreator +{ + constructor(projectName: string, options: ProjectOptions, args: string[]) { + super(projectName, options, args) + this.setupProcessManager() } -) { - track("CREATE_CLI_CMP") - - 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 printedMessage = false - processManager.onTerminated(async () => { - spinner.stop() + async create(): Promise { + track("CREATE_CLI_CMP") - // this ensures that the message isn't printed twice to the user - if (!printedMessage && isProjectCreated) { - printedMessage = true - showPluginSuccessMessage(projectName) - } + logMessage({ + message: `${emojify( + ":rocket:" + )} Starting plugin setup, this may take a few minutes.`, + }) - return - }) + this.spinner.start() + this.factBoxOptions.interval = displayFactBox({ + ...this.factBoxOptions, + title: "Setting up plugin...", + }) - let askProjectName = args.length === 0 - if (args.length > 0) { - // check if plugin 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 plugin name.`, - type: "warn", - }) - askProjectName = true + try { + await this.cloneAndPreparePlugin() + this.spinner.succeed(chalk.green("Plugin Prepared")) + this.showSuccessMessage() + } catch (e: any) { + this.handleError(e) } } - const projectName = askProjectName - ? await askForProjectName(directoryPath, true) - : args[0] - const projectPath = getProjectPath(projectName, directoryPath) - - track("CMP_OPTIONS", { - verbose, - }) - - logMessage({ - message: `${emojify( - ":rocket:" - )} Starting plugin setup, this may take a few minutes.`, - }) - - spinner.start() - - factBoxOptions.interval = displayFactBox({ - ...factBoxOptions, - title: "Setting up plugin...", - }) - - try { + private async cloneAndPreparePlugin(): Promise { await runCloneRepo({ - projectName: projectPath, - repoUrl, - abortController, - spinner, - verbose, + projectName: this.projectPath, + repoUrl: this.options.repoUrl ?? "", + abortController: this.abortController, + spinner: this.spinner, + verbose: this.options.verbose, + isPlugin: true, }) - } catch { - return - } - factBoxOptions.interval = displayFactBox({ - ...factBoxOptions, - message: "Created plugin directory", - }) + this.factBoxOptions.interval = displayFactBox({ + ...this.factBoxOptions, + message: "Created plugin directory", + }) - // prepare plugin - try { await prepareProject({ isPlugin: true, - directory: projectPath, - projectName, - spinner, - processManager, - abortController, - verbose, + directory: this.projectPath, + projectName: this.projectName, + spinner: this.spinner, + processManager: this.processManager, + abortController: this.abortController, + verbose: this.options.verbose, }) - } catch (e: any) { + } + + private handleError(e: any): void { if (isAbortError(e)) { process.exit() } - spinner.stop() + this.spinner.stop() logMessage({ message: `An error occurred while preparing plugin: ${e}`, type: "error", }) + } - return + protected showSuccessMessage(): void { + logMessage({ + message: boxen( + chalk.green( + `Change to the \`${this.projectName}\` directory to explore your Medusa plugin.${EOL}${EOL}Check out the Medusa plugin 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", + } + ), + }) } - spinner.succeed(chalk.green("Plugin Prepared")) + protected setupProcessManager(): void { + this.processManager.onTerminated(async () => { + this.spinner.stop() - showPluginSuccessMessage(projectPath) - process.exit() + if (!this.printedMessage && this.isProjectCreated) { + this.printedMessage = true + this.showSuccessMessage() + } + return + }) + } } -async function createMedusaProject( - args: string[], - { - repoUrl = "", - seed, - skipDb, - dbUrl, - browser, - migrations, - directoryPath, - withNextjsStarter = false, - verbose = false, - }: CreateOptions -) { - 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, +// Medusa Project Creator +class MedusaProjectCreator + extends BaseProjectCreator + implements ProjectCreator +{ + private client: any = null + private dbConnectionString: string = "" + private isDbInitialized: boolean = false + private nextjsDirectory: string = "" + private inviteToken?: string + + constructor(projectName: string, options: ProjectOptions, args: string[]) { + super(projectName, options, args) + this.setupProcessManager() } - 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() + + async create(): Promise { + track("CREATE_CLI_CMA") + + try { + await this.initializeProject() + await this.setupProject() + await this.startServices() + } catch (e: any) { + this.handleError(e) } + } - // 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) + private async initializeProject(): Promise { + const installNextjs = + this.options.withNextjsStarter || (await askForNextjsStarter()) + + if (!this.options.skipDb) { + await this.setupDatabase() } - return - }) + logMessage({ + message: `${emojify( + ":rocket:" + )} Starting project setup, this may take a few minutes.`, + }) - 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", + this.spinner.start() + + this.factBoxOptions.interval = displayFactBox({ + ...this.factBoxOptions, + title: "Setting up project...", + }) + + try { + await runCloneRepo({ + projectName: this.projectPath, + repoUrl: this.options.repoUrl ?? "", + abortController: this.abortController, + spinner: this.spinner, + verbose: this.options.verbose, + }) + + this.factBoxOptions.interval = displayFactBox({ + ...this.factBoxOptions, + message: "Created project directory", }) - askProjectName = true + + if (installNextjs) { + this.nextjsDirectory = await installNextjsStarter({ + directoryName: this.projectPath, + abortController: this.abortController, + factBoxOptions: this.factBoxOptions, + verbose: this.options.verbose, + processManager: this.processManager, + }) + } + } catch (e) { + throw e } } - const projectName = askProjectName - ? await askForProjectName(directoryPath, false) - : args[0] - const projectPath = getProjectPath(projectName, directoryPath) - const installNextjs = withNextjsStarter || (await askForNextjsStarter()) + private async setupDatabase(): Promise { + const dbName = `medusa-${slugify(this.projectName)}` + const { client, dbConnectionString, ...rest } = + await getDbClientAndCredentials({ + dbName, + dbUrl: this.options.dbUrl, + verbose: this.options.verbose, + }) + + this.client = client + this.dbConnectionString = dbConnectionString + this.isDbInitialized = true - let dbName = !skipDb && !dbUrl ? `medusa-${slugify(projectName)}` : "" + if (!this.options.dbUrl) { + this.factBoxOptions.interval = displayFactBox({ + ...this.factBoxOptions, + title: "Creating database...", + }) - let { client, dbConnectionString, ...rest } = !skipDb - ? await getDbClientAndCredentials({ + this.client = await runCreateDb({ + client: this.client, dbName, - dbUrl, - verbose, + spinner: this.spinner, }) - : { 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, + this.factBoxOptions.interval = displayFactBox({ + ...this.factBoxOptions, + message: `Database ${dbName} created`, }) - : "" + } + } - if (client && !dbUrl) { - factBoxOptions.interval = displayFactBox({ - ...factBoxOptions, - title: "Creating database...", - }) - client = await runCreateDb({ client, dbName, spinner }) + private async setupProject(): Promise { + try { + this.inviteToken = await prepareProject({ + isPlugin: false, + directory: this.projectPath, + dbConnectionString: this.dbConnectionString, + seed: this.options.seed, + spinner: this.spinner, + processManager: this.processManager, + abortController: this.abortController, + skipDb: this.options.skipDb, + migrations: this.options.migrations, + onboardingType: this.nextjsDirectory ? "nextjs" : "default", + nextjsDirectory: this.nextjsDirectory, + client: this.client, + verbose: this.options.verbose, + }) + } finally { + await this.client?.end() + } - factBoxOptions.interval = displayFactBox({ - ...factBoxOptions, - message: `Database ${dbName} created`, - }) + this.spinner.succeed(chalk.green("Project Prepared")) } - // prepare project - let inviteToken: string | undefined = undefined - try { - inviteToken = await prepareProject({ - isPlugin: false, - directory: projectPath, - dbName, - dbConnectionString, - seed, - spinner, - processManager, - abortController, - skipDb, - migrations, - onboardingType: installNextjs ? "nextjs" : "default", - nextjsDirectory, - client, - verbose, - }) - } catch (e: any) { - if (isAbortError(e)) { + private async startServices(): Promise { + if (this.options.skipDb || !this.options.browser) { + this.showSuccessMessage() process.exit() } - spinner.stop() logMessage({ - message: `An error occurred while preparing project: ${e}`, - type: "error", + message: "Starting Medusa...", }) - 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, + directory: this.projectPath, + abortController: this.abortController, }) - if (installNextjs && nextjsDirectory) { + if (this.nextjsDirectory) { startNextjsStarter({ - directory: nextjsDirectory, - abortController, - verbose, + directory: this.nextjsDirectory, + abortController: this.abortController, + verbose: this.options.verbose, }) } - } catch (e) { + + this.isProjectCreated = true + + await this.openBrowser() + } + + private async openBrowser(): Promise { + await waitOn({ + resources: ["http://localhost:9000/health"], + }).then(async () => { + open( + this.inviteToken + ? `http://localhost:9000/app/invite?token=${this.inviteToken}&first_run=true` + : "http://localhost:9000/app" + ) + }) + } + + private handleError(e: any): void { if (isAbortError(e)) { process.exit() } + this.spinner.stop() logMessage({ - message: `An error occurred while starting Medusa`, + message: `An error occurred: ${e}`, type: "error", }) + } - return + protected showSuccessMessage(): void { + logMessage({ + message: boxen( + chalk.green( + `Change to the \`${ + this.projectName + }\` directory to explore your Medusa project.${EOL}${EOL}Start your Medusa app again with the following command:${EOL}${EOL}yarn dev${EOL}${EOL}${ + this.inviteToken + ? `After you start the Medusa app, you can set a password for your admin user with the URL http://localhost:7001/invite?token=${this.inviteToken}&first_run=true${EOL}${EOL}` + : "" + }${ + this.nextjsDirectory?.length + ? `The Next.js Starter storefront was installed in the \`${this.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", + } + ), + }) } - isProjectCreated = true + protected setupProcessManager(): void { + this.processManager.onTerminated(async () => { + this.spinner.stop() + + // prevent an error from occurring if + // client hasn't been declared yet + if (this.isDbInitialized && this.client) { + await this.client.end() + } - 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" + if (!this.printedMessage && this.isProjectCreated) { + this.printedMessage = true + this.showSuccessMessage() + } + return + }) + } +} + +// Project Factory +class ProjectCreatorFactory { + static async create( + args: string[], + options: ProjectOptions + ): Promise { + ProjectCreatorFactory.validateNodeVersion() + + const projectName = await ProjectCreatorFactory.getProjectName( + args, + options.directoryPath, + options.plugin ) - }) + + return options.plugin + ? new PluginProjectCreator(projectName, options, args) + : new MedusaProjectCreator(projectName, options, args) + } + + private static validateNodeVersion(): void { + 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", + }) + } + } + + private static async getProjectName( + args: string[], + directoryPath?: string, + isPlugin?: boolean + ): Promise { + let askProjectName = args.length === 0 + if (args.length > 0) { + const projectPath = path.join(directoryPath || "", args[0]) + if ( + fs.existsSync(projectPath) && + fs.lstatSync(projectPath).isDirectory() + ) { + logMessage({ + message: `A directory already exists with the name ${ + args[0] + }. Please enter a different ${isPlugin ? "plugin" : "project"} name.`, + type: "warn", + }) + askProjectName = true + } + } + + return askProjectName + ? await askForProjectName(directoryPath, isPlugin) + : args[0] + } } async function askForProjectName( @@ -458,7 +491,7 @@ async function askForProjectName( type: "input", name: "projectName", message: `What's the name of your ${isPlugin ? "plugin" : "project"}?`, - default: "my-medusa-store", + default: isPlugin ? "my-medusa-plugin" : "my-medusa-store", filter: (input) => { return slugify(input).toLowerCase() }, @@ -466,7 +499,7 @@ async function askForProjectName( if (!input.length) { return `Please enter a ${isPlugin ? "plugin" : "project"} name` } - const projectPath = getProjectPath(input, directoryPath) + const projectPath = path.join(directoryPath || "", input) return fs.existsSync(projectPath) && fs.lstatSync(projectPath).isDirectory() ? `A directory already exists with the same name. Please enter a different ${ @@ -479,59 +512,7 @@ async function askForProjectName( 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 showPluginSuccessMessage(projectName: string) { - logMessage({ - message: boxen( - chalk.green( - `Change to the \`${projectName}\` directory to explore your Medusa plugin.${EOL}${EOL}Check out the Medusa plugin 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` +export default async (args: string[], options: ProjectOptions) => { + const projectCreator = await ProjectCreatorFactory.create(args, options) + await projectCreator.create() } diff --git a/packages/cli/create-medusa-app/src/index.ts b/packages/cli/create-medusa-app/src/index.ts index 36db681b7d180..e0219c53fe9a4 100644 --- a/packages/cli/create-medusa-app/src/index.ts +++ b/packages/cli/create-medusa-app/src/index.ts @@ -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 of repository to use to setup project.") .option("--seed", "Seed the created database with demo data.") .option( diff --git a/packages/cli/create-medusa-app/src/utils/clone-repo.ts b/packages/cli/create-medusa-app/src/utils/clone-repo.ts index 18e169483e34b..9177f8e13b676 100644 --- a/packages/cli/create-medusa-app/src/utils/clone-repo.ts +++ b/packages/cli/create-medusa-app/src/utils/clone-repo.ts @@ -10,20 +10,29 @@ type CloneRepoOptions = { repoUrl?: string abortController?: AbortController verbose?: boolean + isPlugin?: boolean } const DEFAULT_REPO = "https://github.com/medusajs/medusa-starter-default" +const DEFAULT_PLUGIN_REPO = "https://github.com/medusajs/medusa-starter-plugin" const BRANCH = "master" +const PLUGIN_BRANCH = "main" export default async function cloneRepo({ directoryName = "", repoUrl, abortController, verbose = false, + isPlugin = false, }: CloneRepoOptions) { + const defaultRepo = isPlugin ? DEFAULT_PLUGIN_REPO : DEFAULT_REPO + const branch = isPlugin ? PLUGIN_BRANCH : BRANCH + await execute( [ - `git clone ${repoUrl || DEFAULT_REPO} -b ${BRANCH} ${directoryName} --depth 1`, + `git clone ${ + repoUrl || defaultRepo + } -b ${branch} ${directoryName} --depth 1`, { signal: abortController?.signal, }, @@ -38,12 +47,14 @@ export async function runCloneRepo({ abortController, spinner, verbose = false, + isPlugin = false, }: { projectName: string repoUrl: string abortController: AbortController spinner: Ora verbose?: boolean + isPlugin?: boolean }) { try { await cloneRepo({ @@ -51,6 +62,7 @@ export async function runCloneRepo({ repoUrl, abortController, verbose, + isPlugin, }) deleteGitDirectory(projectName) diff --git a/packages/cli/create-medusa-app/src/utils/prepare-project.ts b/packages/cli/create-medusa-app/src/utils/prepare-project.ts index 4e05c9c8d232f..ccd8004dd7029 100644 --- a/packages/cli/create-medusa-app/src/utils/prepare-project.ts +++ b/packages/cli/create-medusa-app/src/utils/prepare-project.ts @@ -85,35 +85,11 @@ async function preparePlugin({ const packageJsonPath = path.join(directory, "package.json") const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) - // TODO: update scripts to use the plugin commands - // Update name packageJson.name = projectName - // Remove @medusajs/medusa dependency if it exists - if (packageJson.dependencies?.["@medusajs/medusa"]) { - delete packageJson.dependencies["@medusajs/medusa"] - } - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)) - // Remote unwanted files - const filesToRemove = [ - path.join(directory, "instrumentation.ts"), - path.join(directory, "medusa-config.ts"), - path.join(directory, "integration-tests"), - ] - - filesToRemove.forEach((file) => { - if (fs.existsSync(file)) { - if (fs.lstatSync(file).isDirectory()) { - fs.rmSync(file, { recursive: true }) - } else { - fs.unlinkSync(file) - } - } - }) - factBoxOptions.interval = displayFactBox({ ...factBoxOptions, spinner, @@ -141,26 +117,6 @@ async function preparePlugin({ message: "Installed Dependencies", }) - factBoxOptions.interval = displayFactBox({ - ...factBoxOptions, - title: "Building Plugin...", - }) - - await processManager.runProcess({ - process: async () => { - try { - await execute([`yarn build`, execOptions], { verbose }) - } catch (e) { - // yarn isn't available - // use npm - await execute([`npm run build`, execOptions], { verbose }) - } - }, - ignoreERESOLVE: true, - }) - - displayFactBox({ ...factBoxOptions, message: "Plugin Built" }) - displayFactBox({ ...factBoxOptions, message: "Finished Preparation" }) }