diff --git a/.changeset/quiet-seas-occur.md b/.changeset/quiet-seas-occur.md new file mode 100644 index 0000000000..b4a7572171 --- /dev/null +++ b/.changeset/quiet-seas-occur.md @@ -0,0 +1,7 @@ +--- +"create-medusa-app": patch +"@medusajs/utils": patch +"@medusajs/medusa-cli": patch +--- + +feat(create-medusa-app, utils, medusa-cli): add database options + remove util from `@medusajs/utils` diff --git a/packages/create-medusa-app/package.json b/packages/create-medusa-app/package.json index 44173a134f..92cb117112 100644 --- a/packages/create-medusa-app/package.json +++ b/packages/create-medusa-app/package.json @@ -13,10 +13,10 @@ "prepublishOnly": "cross-env NODE_ENV=production tsc --build" }, "dependencies": { - "@medusajs/utils": "^1.9.3", "boxen": "^5", "chalk": "^5.2.0", "commander": "^10.0.1", + "glob": "^7.1.6", "inquirer": "^9.2.2", "medusa-telemetry": "^0.0.16", "nanoid": "^4.0.2", diff --git a/packages/create-medusa-app/src/commands/create.ts b/packages/create-medusa-app/src/commands/create.ts index 368e2d1877..17602092ec 100644 --- a/packages/create-medusa-app/src/commands/create.ts +++ b/packages/create-medusa-app/src/commands/create.ts @@ -8,6 +8,7 @@ import open from "open" import waitOn from "wait-on" import ora, { Ora } from "ora" import fs from "fs" +import path from "path" import isEmailImported from "validator/lib/isEmail.js" import logMessage from "../utils/log-message.js" import createAbortController, { @@ -31,6 +32,11 @@ export type CreateOptions = { // commander passed --no-boilerplate as boilerplate boilerplate?: boolean stable?: boolean + skipDb?: boolean + dbUrl?: string + browser?: boolean + migrations?: boolean + directoryPath?: string } export default async ({ @@ -38,6 +44,11 @@ export default async ({ seed, boilerplate, stable, + skipDb, + dbUrl, + browser, + migrations, + directoryPath, }: CreateOptions) => { track("CREATE_CLI") if (repoUrl) { @@ -57,7 +68,7 @@ export default async ({ message: "", title: "", } - const dbName = `medusa-${nanoid(4)}` + const dbName = !skipDb && !dbUrl ? `medusa-${nanoid(4)}` : "" let isProjectCreated = false let isDbInitialized = false let printedMessage = false @@ -74,29 +85,24 @@ export default async ({ // this ensures that the message isn't printed twice to the user if (!printedMessage && isProjectCreated) { printedMessage = true - logMessage({ - message: boxen( - chalk.green( - `Change to the \`${projectName}\` directory to explore your Medusa project.${EOL}${EOL}Start your Medusa app again with the following command:${EOL}${EOL}npx @medusajs/medusa-cli develop${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", - } - ), - }) + showSuccessMessage(projectName) } return }) - const projectName = await askForProjectName() - const { client, dbConnectionString } = await getDbClientAndCredentials(dbName) + const projectName = await askForProjectName(directoryPath) + const projectPath = getProjectPath(projectName, directoryPath) + const adminEmail = + !skipDb && migrations ? await askForAdminEmail(seed, boilerplate) : "" + + const { client, dbConnectionString } = !skipDb + ? await getDbClientAndCredentials({ + dbName, + dbUrl, + }) + : { client: null, dbConnectionString: "" } isDbInitialized = true - const adminEmail = await askForAdminEmail(seed, boilerplate) logMessage({ message: `${emojify( @@ -113,7 +119,7 @@ export default async ({ try { await runCloneRepo({ - projectName, + projectName: projectPath, repoUrl, abortController, spinner, @@ -126,21 +132,26 @@ export default async ({ factBoxOptions.interval = displayFactBox({ ...factBoxOptions, message: "Created project directory", - title: "Creating database...", }) - await runCreateDb({ client, dbName, spinner }) + if (client && !dbUrl) { + factBoxOptions.interval = displayFactBox({ + ...factBoxOptions, + title: "Creating database...", + }) + await runCreateDb({ client, dbName, spinner }) - factBoxOptions.interval = displayFactBox({ - ...factBoxOptions, - message: `Database ${dbName} created`, - }) + factBoxOptions.interval = displayFactBox({ + ...factBoxOptions, + message: `Database ${dbName} created`, + }) + } // prepare project let inviteToken: string | undefined = undefined try { inviteToken = await prepareProject({ - directory: projectName, + directory: projectPath, dbConnectionString, admin: { email: adminEmail, @@ -150,6 +161,8 @@ export default async ({ spinner, processManager, abortController, + skipDb, + migrations, }) } catch (e: any) { if (isAbortError(e)) { @@ -170,6 +183,11 @@ export default async ({ spinner.succeed(chalk.green("Project Prepared")) + if (skipDb || !browser) { + showSuccessMessage(projectPath, inviteToken) + process.exit() + } + // start backend logMessage({ message: "Starting Medusa...", @@ -177,7 +195,7 @@ export default async ({ try { startMedusa({ - directory: projectName, + directory: projectPath, abortController, }) } catch (e) { @@ -208,7 +226,7 @@ export default async ({ ) } -async function askForProjectName(): Promise { +async function askForProjectName(directoryPath?: string): Promise { const { projectName } = await inquirer.prompt([ { type: "input", @@ -222,7 +240,9 @@ async function askForProjectName(): Promise { if (!input.length) { return "Please enter a project name" } - return fs.existsSync(input) && fs.lstatSync(input).isDirectory() + 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 }, @@ -251,3 +271,29 @@ async function askForAdminEmail( return adminEmail } + +function showSuccessMessage(projectName: string, inviteToken?: 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}npx @medusajs/medusa-cli develop${EOL}${EOL}${inviteToken ? `${EOL}${EOL}After you start the Medusa app, you can set a password for your admin user with the URL ${getInviteUrl(inviteToken)}${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` +} diff --git a/packages/create-medusa-app/src/index.ts b/packages/create-medusa-app/src/index.ts index 15c2a277ab..755ebbf81b 100644 --- a/packages/create-medusa-app/src/index.ts +++ b/packages/create-medusa-app/src/index.ts @@ -14,6 +14,29 @@ program "--stable", "Install the latest stable version. This removes all onboarding features" ) + .option( + "--skip-db", + "Skips creating the database, running migrations, and seeding, and subsequently skips opening the browser.", + false + ) + .option( + "--db-url ", + "Skips database creation and sets the database URL to the provided URL. Throws an error if can't connect to the database. Will still run migrations and open the admin after project creation." + ) + .option( + "--no-migrations", + "Skips running migrations, creating admin user, and seeding. If used, it's expected that you pass the --db-url option with a url of a database that has all necessary migrations. Otherwise, unexpected errors will occur.", + true + ) + .option( + "--no-browser", + "Disables opening the browser at the end of the project creation and only shows success message.", + true + ) + .option( + "--directory-path ", + "Specify the directory path to install the project in." + ) .parse() void create(program.opts()) diff --git a/packages/utils/src/cli/clear-project.ts b/packages/create-medusa-app/src/utils/clear-project.ts similarity index 87% rename from packages/utils/src/cli/clear-project.ts rename to packages/create-medusa-app/src/utils/clear-project.ts index 241f9a4241..391d20fc2a 100644 --- a/packages/utils/src/cli/clear-project.ts +++ b/packages/create-medusa-app/src/utils/clear-project.ts @@ -8,10 +8,12 @@ export function clearProject(directory: string) { path.join(directory, `src`, `**/onboarding/`) ) const typeFiles = glob.sync(path.join(directory, `src`, `types`)) - const srcFiles = glob.sync(path.join(directory, `src`, `**/*.{ts,tsx,js,jsx}`)) + const srcFiles = glob.sync( + path.join(directory, `src`, `**/*.{ts,tsx,js,jsx}`) + ) const files = [...adminFiles, ...onboardingFiles, ...typeFiles, ...srcFiles] - + files.forEach((file) => fs.rmSync(file, { recursive: true, diff --git a/packages/create-medusa-app/src/utils/create-db.ts b/packages/create-medusa-app/src/utils/create-db.ts index 5cc6a60dc2..c9998d3b97 100644 --- a/packages/create-medusa-app/src/utils/create-db.ts +++ b/packages/create-medusa-app/src/utils/create-db.ts @@ -40,7 +40,7 @@ export async function runCreateDb({ } } -export async function getDbClientAndCredentials(dbName: string): Promise<{ +async function getForDbName(dbName: string): Promise<{ client: pg.Client dbConnectionString: string }> { @@ -101,3 +101,40 @@ export async function getDbClientAndCredentials(dbName: string): Promise<{ dbConnectionString, } } + +async function getForDbUrl(dbUrl: string): Promise<{ + client: pg.Client + dbConnectionString: string +}> { + let client!: pg.Client + + try { + client = await postgresClient({ + connectionString: dbUrl, + }) + } catch (e) { + logMessage({ + message: `Couldn't connect to PostgreSQL using the database URL you passed. Make sure it's correct and try again.`, + type: "error", + }) + } + + return { + client, + dbConnectionString: dbUrl, + } +} + +export async function getDbClientAndCredentials({ + dbName = "", + dbUrl = "", +}): Promise<{ + client: pg.Client + dbConnectionString: string +}> { + if (dbName) { + return await getForDbName(dbName) + } else { + return await getForDbUrl(dbUrl) + } +} diff --git a/packages/create-medusa-app/src/utils/postgres-client.ts b/packages/create-medusa-app/src/utils/postgres-client.ts index dfa37e23c9..603d9d4a9e 100644 --- a/packages/create-medusa-app/src/utils/postgres-client.ts +++ b/packages/create-medusa-app/src/utils/postgres-client.ts @@ -4,6 +4,7 @@ const { Client } = pg type PostgresConnection = { user?: string password?: string + connectionString?: string } export default async (connect: PostgresConnection) => { diff --git a/packages/create-medusa-app/src/utils/prepare-project.ts b/packages/create-medusa-app/src/utils/prepare-project.ts index d561f4db49..40335933d5 100644 --- a/packages/create-medusa-app/src/utils/prepare-project.ts +++ b/packages/create-medusa-app/src/utils/prepare-project.ts @@ -5,8 +5,8 @@ import { Ora } from "ora" import promiseExec from "./promise-exec.js" import { EOL } from "os" import { displayFactBox, FactBoxOptions } from "./facts.js" -import { clearProject } from "@medusajs/utils" import ProcessManager from "./process-manager.js" +import { clearProject } from "./clear-project.js" type PrepareOptions = { directory: string @@ -19,6 +19,8 @@ type PrepareOptions = { spinner: Ora processManager: ProcessManager abortController?: AbortController + skipDb?: boolean + migrations?: boolean } export default async ({ @@ -30,6 +32,8 @@ export default async ({ spinner, processManager, abortController, + skipDb, + migrations, }: PrepareOptions) => { // initialize execution options const execOptions = { @@ -56,11 +60,13 @@ export default async ({ // initialize the invite token to return let inviteToken: string | undefined = undefined - // add connection string to project - fs.appendFileSync( - path.join(directory, `.env`), - `DATABASE_TYPE=postgres${EOL}DATABASE_URL=${dbConnectionString}` - ) + if (!skipDb) { + // add connection string to project + fs.appendFileSync( + path.join(directory, `.env`), + `DATABASE_TYPE=postgres${EOL}DATABASE_URL=${dbConnectionString}` + ) + } factBoxOptions.interval = displayFactBox({ ...factBoxOptions, @@ -119,36 +125,39 @@ export default async ({ }) displayFactBox({ ...factBoxOptions, message: "Project Built" }) - factBoxOptions.interval = displayFactBox({ - ...factBoxOptions, - title: "Running Migrations...", - }) - // run migrations - await processManager.runProcess({ - process: async () => { - const proc = await promiseExec( - "npx @medusajs/medusa-cli@latest migrations run", - npxOptions - ) + if (!skipDb && migrations) { + factBoxOptions.interval = displayFactBox({ + ...factBoxOptions, + title: "Running Migrations...", + }) - // ensure that migrations actually ran in case of an uncaught error - if (!proc.stdout.includes("Migrations completed")) { - throw new Error( - `An error occurred while running migrations: ${ - proc.stderr || proc.stdout - }` + // run migrations + await processManager.runProcess({ + process: async () => { + const proc = await promiseExec( + "npx @medusajs/medusa-cli@latest migrations run", + npxOptions ) - } - }, - }) - factBoxOptions.interval = displayFactBox({ - ...factBoxOptions, - message: "Ran Migrations", - }) + // ensure that migrations actually ran in case of an uncaught error + if (!proc.stdout.includes("Migrations completed")) { + throw new Error( + `An error occurred while running migrations: ${ + proc.stderr || proc.stdout + }` + ) + } + }, + }) - if (admin) { + factBoxOptions.interval = displayFactBox({ + ...factBoxOptions, + message: "Ran Migrations", + }) + } + + if (admin && !skipDb && migrations) { // create admin user factBoxOptions.interval = displayFactBox({ ...factBoxOptions, @@ -173,61 +182,63 @@ export default async ({ }) } - if (seed || !boilerplate) { - factBoxOptions.interval = displayFactBox({ - ...factBoxOptions, - title: "Seeding database...", - }) + if (!skipDb && migrations) { + if (seed || !boilerplate) { + factBoxOptions.interval = displayFactBox({ + ...factBoxOptions, + title: "Seeding database...", + }) - // check if a seed file exists in the project - if (!fs.existsSync(path.join(directory, "data", "seed.json"))) { - spinner - ?.warn( - chalk.yellow( - "Seed file was not found in the project. Skipping seeding..." + // check if a seed file exists in the project + if (!fs.existsSync(path.join(directory, "data", "seed.json"))) { + spinner + ?.warn( + chalk.yellow( + "Seed file was not found in the project. Skipping seeding..." + ) ) - ) - .start() - return inviteToken + .start() + return inviteToken + } + + await processManager.runProcess({ + process: async () => { + await promiseExec( + `npx @medusajs/medusa-cli@latest seed --seed-file=${path.join( + "data", + "seed.json" + )}`, + npxOptions + ) + }, + }) + + displayFactBox({ + ...factBoxOptions, + message: "Seeded database with demo data", + }) + } else if ( + fs.existsSync(path.join(directory, "data", "seed-onboarding.json")) + ) { + // seed the database with onboarding seed + factBoxOptions.interval = displayFactBox({ + ...factBoxOptions, + title: "Finish preparation...", + }) + + await processManager.runProcess({ + process: async () => { + await promiseExec( + `npx @medusajs/medusa-cli@latest seed --seed-file=${path.join( + "data", + "seed-onboarding.json" + )}`, + npxOptions + ) + }, + }) } - await processManager.runProcess({ - process: async () => { - await promiseExec( - `npx @medusajs/medusa-cli@latest seed --seed-file=${path.join( - "data", - "seed.json" - )}`, - npxOptions - ) - }, - }) - - displayFactBox({ - ...factBoxOptions, - message: "Seeded database with demo data", - }) - } else if ( - fs.existsSync(path.join(directory, "data", "seed-onboarding.json")) - ) { - // seed the database with onboarding seed - factBoxOptions.interval = displayFactBox({ - ...factBoxOptions, - title: "Finish preparation...", - }) - - await processManager.runProcess({ - process: async () => { - await promiseExec( - `npx @medusajs/medusa-cli@latest seed --seed-file=${path.join( - "data", - "seed-onboarding.json" - )}`, - npxOptions - ) - }, - }) - displayFactBox({ ...factBoxOptions, message: "Finished Preparation" }) } diff --git a/packages/medusa-cli/package.json b/packages/medusa-cli/package.json index b5a0c9e9ab..1f9ba7fc1a 100644 --- a/packages/medusa-cli/package.json +++ b/packages/medusa-cli/package.json @@ -44,6 +44,7 @@ "execa": "^5.1.1", "fs-exists-cached": "^1.0.0", "fs-extra": "^10.0.0", + "glob": "^7.1.6", "hosted-git-info": "^4.0.2", "inquirer": "^8.0.0", "is-valid-path": "^0.1.1", diff --git a/packages/medusa-cli/src/commands/new.ts b/packages/medusa-cli/src/commands/new.ts index e54f88e10f..bab3f26b90 100644 --- a/packages/medusa-cli/src/commands/new.ts +++ b/packages/medusa-cli/src/commands/new.ts @@ -18,8 +18,8 @@ import inquirer from "inquirer" import reporter from "../reporter" import { getPackageManager, setPackageManager } from "../util/package-manager" -import { clearProject } from "@medusajs/utils" import { PanicId } from "../reporter/panic-handler" +import { clearProject } from "../util/clear-project" const removeUndefined = (obj) => { return Object.fromEntries( diff --git a/packages/medusa-cli/src/util/clear-project.ts b/packages/medusa-cli/src/util/clear-project.ts new file mode 100644 index 0000000000..391d20fc2a --- /dev/null +++ b/packages/medusa-cli/src/util/clear-project.ts @@ -0,0 +1,25 @@ +import fs from "fs" +import glob from "glob" +import path from "path" + +export function clearProject(directory: string) { + const adminFiles = glob.sync(path.join(directory, `src`, `admin/**/*`)) + const onboardingFiles = glob.sync( + path.join(directory, `src`, `**/onboarding/`) + ) + const typeFiles = glob.sync(path.join(directory, `src`, `types`)) + const srcFiles = glob.sync( + path.join(directory, `src`, `**/*.{ts,tsx,js,jsx}`) + ) + + const files = [...adminFiles, ...onboardingFiles, ...typeFiles, ...srcFiles] + + files.forEach((file) => + fs.rmSync(file, { + recursive: true, + force: true, + }) + ) + // add empty typescript file to avoid build errors + fs.openSync(path.join(directory, "src", "index.ts"), "w") +} diff --git a/packages/utils/package.json b/packages/utils/package.json index 524b8f1dbf..5d9f2cc514 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -29,7 +29,6 @@ }, "dependencies": { "awilix": "^8.0.1", - "glob": "^7.1.6", "ulid": "^2.3.0" }, "scripts": { diff --git a/packages/utils/src/cli/index.ts b/packages/utils/src/cli/index.ts deleted file mode 100644 index 13d04c3410..0000000000 --- a/packages/utils/src/cli/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./clear-project" \ No newline at end of file diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 17a3dbd961..e850b04728 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,5 +1,4 @@ export * from "./bundles" -export * from "./cli" export * from "./common" export * from "./decorators" export * from "./event-bus" diff --git a/yarn.lock b/yarn.lock index 2746e76d1e..de37f9090c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6269,6 +6269,7 @@ __metadata: execa: ^5.1.1 fs-exists-cached: ^1.0.0 fs-extra: ^10.0.0 + glob: ^7.1.6 hosted-git-info: ^4.0.2 inquirer: ^8.0.0 is-valid-path: ^0.1.1 @@ -6572,7 +6573,6 @@ __metadata: awilix: ^8.0.1 cross-env: ^5.2.1 express: ^4.18.2 - glob: ^7.1.6 jest: ^25.5.4 rimraf: ^5.0.1 ts-jest: ^25.5.1 @@ -17901,7 +17901,6 @@ __metadata: version: 0.0.0-use.local resolution: "create-medusa-app@workspace:packages/create-medusa-app" dependencies: - "@medusajs/utils": ^1.9.3 "@types/chalk": ^2.2.0 "@types/commander": ^2.12.2 "@types/configstore": ^6.0.0 @@ -17921,6 +17920,7 @@ __metadata: eslint-config-google: ^0.14.0 eslint-config-prettier: ^8.8.0 eslint-plugin-prettier: ^4.2.1 + glob: ^7.1.6 inquirer: ^9.2.2 medusa-telemetry: ^0.0.16 nanoid: ^4.0.2