forked from medusajs/medusa
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Scaffold plugin in create-medusa-app (medusajs#10908)
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
Showing
10 changed files
with
681 additions
and
377 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"create-medusa-app": patch | ||
--- | ||
|
||
Feat/plugin scaffolding |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.