feat(create-medusa-app): update command for a better onboarding experience (#4215)
* feat(create-medusa-app): update command for a better onboarding experience * use medusa-telemetry for tracking * update used snapshot * added changeset * update yarn.lock * increased facts timer * updated snapshot version * show facts throughout installation + add first_run to url * added message after server termination * print message only once * added github to process terminated message * address pr feedback * added onboarding seeding * fix for npm install
This commit is contained in:
1 parent
8676ee7a2e
commit
c04d93cd04
29 files changed
+1856
-660
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-medusa-app": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(create-medusa-app): update command for a better onboarding experience
|
||||||
@@ -19,6 +19,7 @@ packages/*
|
|||||||
!packages/stock-location
|
!packages/stock-location
|
||||||
!packages/cache-redis
|
!packages/cache-redis
|
||||||
!packages/cache-inmemory
|
!packages/cache-inmemory
|
||||||
|
!packages/create-medusa-app
|
||||||
|
|
||||||
|
|
||||||
**/models/*
|
**/models/*
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ module.exports = {
|
|||||||
"./packages/stock-location/tsconfig.spec.json",
|
"./packages/stock-location/tsconfig.spec.json",
|
||||||
"./packages/cache-redis/tsconfig.spec.json",
|
"./packages/cache-redis/tsconfig.spec.json",
|
||||||
"./packages/cache-inmemory/tsconfig.spec.json",
|
"./packages/cache-inmemory/tsconfig.spec.json",
|
||||||
|
"./packages/create-medusa-app/tsconfig.json",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
my-medusa-store
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://www.medusajs.com">
|
||||||
|
<img alt="Medusa" src="https://user-images.githubusercontent.com/7554214/153162406-bf8fd16f-aa98-4604-b87b-e13ab4baf604.png" width="100" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<h1 align="center">
|
||||||
|
create-medusa-app
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<h4 align="center">
|
||||||
|
<a href="https://docs.medusajs.com">Documentation</a> |
|
||||||
|
<a href="https://www.medusajs.com">Website</a>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
An open source composable commerce engine built for developers.
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/medusajs/medusa/blob/master/LICENSE">
|
||||||
|
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Medusa is released under the MIT license." />
|
||||||
|
</a>
|
||||||
|
<a href="https://circleci.com/gh/medusajs/medusa">
|
||||||
|
<img src="https://circleci.com/gh/medusajs/medusa.svg?style=shield" alt="Current CircleCI build status." />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/medusajs/medusa/blob/master/CONTRIBUTING.md">
|
||||||
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="PRs welcome!" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.producthunt.com/posts/medusa"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Day-%23DA552E" alt="Product Hunt"></a>
|
||||||
|
<a href="https://discord.gg/xpCwq3Kfn8">
|
||||||
|
<img src="https://img.shields.io/badge/chat-on%20discord-7289DA.svg" alt="Discord Chat" />
|
||||||
|
</a>
|
||||||
|
<a href="https://twitter.com/intent/follow?screen_name=medusajs">
|
||||||
|
<img src="https://img.shields.io/twitter/follow/medusajs.svg?label=Follow%20@medusajs" alt="Follow @medusajs" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Using this NPX command, you can setup a Medusa backend and admin along with a PostgreSQL database in simple steps.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Run the following command in your terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx create-medusa-app@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, answer the prompted questions to setup your PostgreSQL database and Medusa project. Once the setup is done, the Medusa admin dashboard will open in your default browser.
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Description | Default value |
|
||||||
|
|--------------------|-------------------------------------------------------|------------------------------------------------------|
|
||||||
|
| `--repo-url <url>` | Create Medusa project from a different repository URL | `https://github.com/medusajs/medusa-starter-default` |
|
||||||
|
| `--seed` | Using this option seeds the database with demo data | false |
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#! /usr/bin/env node
|
|
||||||
|
|
||||||
const { run } = require("./dist")
|
|
||||||
|
|
||||||
run().catch((e) => {
|
|
||||||
console.warn(e)
|
|
||||||
})
|
|
||||||
@@ -1,55 +1,62 @@
|
|||||||
{
|
{
|
||||||
"name": "create-medusa-app",
|
"name": "create-medusa-app",
|
||||||
"version": "0.0.10",
|
"version": "0.0.10",
|
||||||
"main": "dist/index.js",
|
"description": "Create a Medusa project using a single command.",
|
||||||
"bin": "cli.js",
|
"type": "module",
|
||||||
|
"exports": "./dist/index.js",
|
||||||
|
"bin": "dist/index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
|
||||||
"dist/index.js",
|
|
||||||
"cli.js"
|
|
||||||
],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "microbundle -i src/index.ts --no-pkg-main --target=node -f=cjs --sourcemap=false --compress --alias worker_threads=@ascorbic/worker-threads-shim",
|
"dev": "ts-node --esm src/index.ts",
|
||||||
"watch": "microbundle -i src/index.ts --no-pkg-main --target=node -f=cjs --sourcemap=false --alias worker_threads=@ascorbic/worker-threads-shim --watch",
|
"build": "tsc",
|
||||||
"prepare": "yarn build"
|
"watch": "tsc --watch",
|
||||||
|
"prepare": "cross-env NODE_ENV=production yarn run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.15.4"
|
"boxen": "^7.1.0",
|
||||||
|
"chalk": "^5.2.0",
|
||||||
|
"commander": "^10.0.1",
|
||||||
|
"inquirer": "^9.2.2",
|
||||||
|
"medusa-telemetry": "^0.0.16",
|
||||||
|
"nanoid": "^4.0.2",
|
||||||
|
"node-fetch": "^3.3.1",
|
||||||
|
"open": "^9.1.0",
|
||||||
|
"ora": "^6.3.0",
|
||||||
|
"pg": "^8.10.0",
|
||||||
|
"slugify": "^1.6.6",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"validator": "^13.9.0",
|
||||||
|
"wait-on": "^7.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ascorbic/worker-threads-shim": "^1.0.0",
|
|
||||||
"@babel/runtime": "^7.15.4",
|
|
||||||
"@types/chalk": "^2.2.0",
|
"@types/chalk": "^2.2.0",
|
||||||
"@types/commander": "^2.12.2",
|
"@types/commander": "^2.12.2",
|
||||||
"@types/configstore": "^4.0.0",
|
"@types/configstore": "^6.0.0",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/inquirer": "^9.0.3",
|
||||||
"@types/node": "^14.18.36",
|
"@types/pg": "^8.6.6",
|
||||||
"ansi-wordwrap": "^1.0.2",
|
"@types/uuid": "^9.0.1",
|
||||||
"chalk": "^4.1.2",
|
"@types/validator": "^13.7.17",
|
||||||
"commander": "^8.1.0",
|
"@types/wait-on": "^5.3.1",
|
||||||
"common-tags": "^1.8.2",
|
"@typescript-eslint/eslint-plugin": "^5.59.5",
|
||||||
|
"@typescript-eslint/parser": "^5.59.5",
|
||||||
"configstore": "^6.0.0",
|
"configstore": "^6.0.0",
|
||||||
"enquirer": "^2.3.6",
|
"eslint": "^8.40.0",
|
||||||
"execa": "^5.1.1",
|
"eslint-config-google": "^0.14.0",
|
||||||
"fs-exists-cached": "^1.0.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"fs-extra": "^10.0.0",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"hosted-git-info": "^4.0.2",
|
"prettier": "^2.8.8",
|
||||||
"is-valid-path": "^0.1.1",
|
"ts-node": "^10.9.1",
|
||||||
"microbundle": "^0.13.3",
|
"typescript": "^5.0.4"
|
||||||
"node-fetch": "^2.6.9",
|
},
|
||||||
"prompts": "^2.4.2",
|
"engines": {
|
||||||
"string-length": "^4.0.2",
|
"node": ">=14.16"
|
||||||
"terminal-link": "^2.1.1",
|
|
||||||
"tiny-spin": "^1.0.2",
|
|
||||||
"url": "^0.11.0",
|
|
||||||
"uuid": "3.4.0"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/medusajs/medusa.git",
|
"url": "https://github.com/medusajs/medusa.git",
|
||||||
"directory": "packages/create-medusa-app"
|
"directory": "packages/create-medusa-app"
|
||||||
},
|
},
|
||||||
"author": "Sebastian Rindom <seb@medusa-commerce.com>",
|
"author": "Medusa",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
import inquirer from "inquirer"
|
||||||
|
import slugifyType from "slugify"
|
||||||
|
import chalk from "chalk"
|
||||||
|
import pg from "pg"
|
||||||
|
import createDb from "../utils/create-db.js"
|
||||||
|
import postgresClient from "../utils/postgres-client.js"
|
||||||
|
import cloneRepo from "../utils/clone-repo.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 formatConnectionString from "../utils/format-connection-string.js"
|
||||||
|
import ora from "ora"
|
||||||
|
import fs from "fs"
|
||||||
|
import { nanoid } from "nanoid"
|
||||||
|
import isEmailImported from "validator/lib/isEmail.js"
|
||||||
|
import logMessage from "../utils/log-message.js"
|
||||||
|
import onProcessTerminated from "../utils/on-process-terminated.js"
|
||||||
|
import createAbortController, {
|
||||||
|
isAbortError,
|
||||||
|
} from "../utils/create-abort-controller.js"
|
||||||
|
import { track } from "medusa-telemetry"
|
||||||
|
import { createFactBox, resetFactBox } from "../utils/facts.js"
|
||||||
|
import boxen from "boxen"
|
||||||
|
|
||||||
|
const slugify = slugifyType.default
|
||||||
|
const isEmail = isEmailImported.default
|
||||||
|
|
||||||
|
type CreateOptions = {
|
||||||
|
repoUrl?: string
|
||||||
|
seed?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async ({ repoUrl = "", seed }: CreateOptions) => {
|
||||||
|
track("CREATE_CLI")
|
||||||
|
if (repoUrl) {
|
||||||
|
track("STARTER_SELECTED", { starter: repoUrl })
|
||||||
|
}
|
||||||
|
if (seed) {
|
||||||
|
track("SEED_SELECTED", { seed })
|
||||||
|
}
|
||||||
|
const abortController = createAbortController()
|
||||||
|
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
validate: (input) => {
|
||||||
|
if (!input.length) {
|
||||||
|
return "Please enter a project name"
|
||||||
|
}
|
||||||
|
return fs.existsSync(input) && fs.lstatSync(input).isDirectory()
|
||||||
|
? "A directory already exists with the same name. Please enter a different project name."
|
||||||
|
: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
let client: pg.Client | undefined
|
||||||
|
let dbConnectionString = ""
|
||||||
|
let postgresUsername = "postgres"
|
||||||
|
let postgresPassword = ""
|
||||||
|
|
||||||
|
// try to log in with default db username and password
|
||||||
|
try {
|
||||||
|
client = await postgresClient({
|
||||||
|
user: postgresUsername,
|
||||||
|
password: postgresPassword,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
// ask for the user's postgres credentials
|
||||||
|
const answers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "postgresUsername",
|
||||||
|
message: "Enter your Postgres username",
|
||||||
|
default: "postgres",
|
||||||
|
validate: (input) => {
|
||||||
|
return typeof input === "string" && input.length > 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "password",
|
||||||
|
name: "postgresPassword",
|
||||||
|
message: "Enter your Postgres password",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
postgresUsername = answers.postgresUsername
|
||||||
|
postgresPassword = answers.postgresPassword
|
||||||
|
|
||||||
|
try {
|
||||||
|
client = await postgresClient({
|
||||||
|
user: postgresUsername,
|
||||||
|
password: postgresPassword,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
logMessage({
|
||||||
|
message:
|
||||||
|
"Couldn't connect to PostgreSQL. Make sure you have PostgreSQL installed and the credentials you provided are correct.\n\n" +
|
||||||
|
"You can learn how to install PostgreSQL here: https://docs.medusajs.com/development/backend/prepare-environment#postgresql",
|
||||||
|
type: "error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { adminEmail } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "adminEmail",
|
||||||
|
message: "Enter an email for your admin dashboard user",
|
||||||
|
default: !seed ? "admin@medusa-test.com" : undefined,
|
||||||
|
validate: (input) => {
|
||||||
|
return typeof input === "string" && input.length > 0 && isEmail(input)
|
||||||
|
? true
|
||||||
|
: "Please enter a valid email"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const spinner = ora().start()
|
||||||
|
|
||||||
|
onProcessTerminated(() => spinner.stop())
|
||||||
|
|
||||||
|
let interval: NodeJS.Timer | null = createFactBox(
|
||||||
|
spinner,
|
||||||
|
"Setting up project..."
|
||||||
|
)
|
||||||
|
|
||||||
|
// clone repository
|
||||||
|
try {
|
||||||
|
await cloneRepo({
|
||||||
|
directoryName: projectName,
|
||||||
|
repoUrl,
|
||||||
|
abortController,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
if (isAbortError(e)) {
|
||||||
|
process.exit()
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.stop()
|
||||||
|
logMessage({
|
||||||
|
message: `An error occurred while setting up your project: ${e}`,
|
||||||
|
type: "error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interval = resetFactBox(
|
||||||
|
interval,
|
||||||
|
spinner,
|
||||||
|
"Created project directory",
|
||||||
|
"Creating database..."
|
||||||
|
)
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
const dbName = `medusa-${nanoid(4)}`
|
||||||
|
// create postgres database
|
||||||
|
try {
|
||||||
|
await createDb({
|
||||||
|
client,
|
||||||
|
db: dbName,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
spinner.stop()
|
||||||
|
logMessage({
|
||||||
|
message: `An error occurred while trying to create your database: ${e}`,
|
||||||
|
type: "error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// format connection string
|
||||||
|
dbConnectionString = formatConnectionString({
|
||||||
|
user: postgresUsername,
|
||||||
|
password: postgresPassword,
|
||||||
|
host: client.host,
|
||||||
|
db: dbName,
|
||||||
|
})
|
||||||
|
|
||||||
|
resetFactBox(interval, spinner, `Database ${dbName} created`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare project
|
||||||
|
let inviteToken: string | undefined = undefined
|
||||||
|
try {
|
||||||
|
inviteToken = await prepareProject({
|
||||||
|
directory: projectName,
|
||||||
|
dbConnectionString,
|
||||||
|
admin: {
|
||||||
|
email: adminEmail,
|
||||||
|
},
|
||||||
|
seed,
|
||||||
|
spinner,
|
||||||
|
abortController,
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
if (isAbortError(e)) {
|
||||||
|
process.exit()
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.stop()
|
||||||
|
logMessage({
|
||||||
|
message: `An error occurred while preparing project: ${e}`,
|
||||||
|
type: "error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.succeed(chalk.green("Project Prepared"))
|
||||||
|
|
||||||
|
// close db connection
|
||||||
|
await client?.end()
|
||||||
|
|
||||||
|
// start backend
|
||||||
|
logMessage({
|
||||||
|
message: "Starting Medusa...",
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
startMedusa({
|
||||||
|
directory: projectName,
|
||||||
|
abortController,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
if (isAbortError(e)) {
|
||||||
|
process.exit()
|
||||||
|
}
|
||||||
|
|
||||||
|
logMessage({
|
||||||
|
message: `An error occurred while starting Medusa`,
|
||||||
|
type: "error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// the SIGINT event is triggered twice once the backend runs
|
||||||
|
// this ensures that the message isn't printed twice to the user
|
||||||
|
let printedMessage = false
|
||||||
|
|
||||||
|
onProcessTerminated(() => {
|
||||||
|
if (!printedMessage) {
|
||||||
|
printedMessage = true
|
||||||
|
console.log(
|
||||||
|
boxen(
|
||||||
|
chalk.green(
|
||||||
|
`Change to the \`${projectName}\` directory to explore your Medusa project.
|
||||||
|
|
||||||
|
Check out the Medusa documentation to start your development:
|
||||||
|
https://docs.medusajs.com/
|
||||||
|
|
||||||
|
Star us on GitHub if you like what we're building:
|
||||||
|
https://github.com/medusajs/medusa/stargazers`
|
||||||
|
),
|
||||||
|
{
|
||||||
|
titleAlignment: "center",
|
||||||
|
textAlignment: "center",
|
||||||
|
padding: 1,
|
||||||
|
margin: 1,
|
||||||
|
float: "center",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,136 +1,11 @@
|
|||||||
import Commander from "commander"
|
#!/usr/bin/env node
|
||||||
import path from "path"
|
import { program } from "commander"
|
||||||
|
import create from "./commands/create.js"
|
||||||
|
|
||||||
import { prompt } from "enquirer"
|
program
|
||||||
import { newStarter } from "./new-starter"
|
.description("Create a new Medusa project")
|
||||||
import { track } from "./track"
|
.option("--repo-url <url>", "URL of repository to use to setup project.")
|
||||||
|
.option("--seed", "Seed the created database with demo data.")
|
||||||
|
.parse()
|
||||||
|
|
||||||
import pkg from "../package.json"
|
void create(program.opts())
|
||||||
|
|
||||||
let projectPath: string = ""
|
|
||||||
|
|
||||||
const questions = {
|
|
||||||
projectRoot: {
|
|
||||||
type: "input",
|
|
||||||
name: "projectRoot",
|
|
||||||
message: "Where should your project be installed?",
|
|
||||||
initial: "my-medusa-store",
|
|
||||||
},
|
|
||||||
starter: {
|
|
||||||
type: "select",
|
|
||||||
name: "starter",
|
|
||||||
message: "Which Medusa starter would you like to install?",
|
|
||||||
choices: ["medusa-starter-default", "medusa-starter-contentful", "Other"],
|
|
||||||
},
|
|
||||||
starterUrl: {
|
|
||||||
type: "input",
|
|
||||||
name: "starterUrl",
|
|
||||||
message: "Where is the starter located? (URL or path)",
|
|
||||||
},
|
|
||||||
storefront: {
|
|
||||||
type: "select",
|
|
||||||
name: "storefront",
|
|
||||||
message: "Which storefront starter would you like to install?",
|
|
||||||
choices: [
|
|
||||||
"Next.js Starter",
|
|
||||||
"medusa.express (Next.js)",
|
|
||||||
"None",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const program = new Commander.Command(pkg.name)
|
|
||||||
.version(pkg.version)
|
|
||||||
.action((name) => (projectPath = name))
|
|
||||||
.option(`-r --root`, `The directory to install your Medusa app`)
|
|
||||||
.option(
|
|
||||||
`-s --starter-url`,
|
|
||||||
`A GitHub URL to a repository that contains a Medusa starter project to bootstrap from`
|
|
||||||
)
|
|
||||||
.option(`-v --verbose`, `Show all installation output`)
|
|
||||||
.parse(process.argv)
|
|
||||||
|
|
||||||
const getStorefrontStarter = (starter: string): string => {
|
|
||||||
const selected = starter.toLowerCase()
|
|
||||||
switch (selected) {
|
|
||||||
case "next.js starter":
|
|
||||||
return "https://github.com/medusajs/nextjs-starter-medusa"
|
|
||||||
case "medusa.express (next.js)":
|
|
||||||
return "https://github.com/medusajs/medusa-express-nextjs"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const run = async (): Promise<void> => {
|
|
||||||
track("CREATE_CLI")
|
|
||||||
|
|
||||||
if (typeof projectPath === "string") {
|
|
||||||
projectPath = projectPath.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { projectRoot } = (await prompt(questions.projectRoot)) as {
|
|
||||||
projectRoot: string
|
|
||||||
}
|
|
||||||
let { starter } = (await prompt(questions.starter)) as {
|
|
||||||
starter: string
|
|
||||||
}
|
|
||||||
|
|
||||||
if (starter === "Other") {
|
|
||||||
const { starterUrl } = (await prompt(questions.starterUrl)) as {
|
|
||||||
starterUrl: string
|
|
||||||
}
|
|
||||||
starter = starterUrl
|
|
||||||
} else {
|
|
||||||
starter = `medusajs/${starter}`
|
|
||||||
}
|
|
||||||
track("STARTER_SELECTED", { starter })
|
|
||||||
|
|
||||||
const progOptions = program.opts()
|
|
||||||
|
|
||||||
const { storefront } = (await prompt(questions.storefront)) as {
|
|
||||||
storefront: string
|
|
||||||
}
|
|
||||||
track("STOREFRONT_SELECTED", { storefront })
|
|
||||||
|
|
||||||
await newStarter({
|
|
||||||
starter,
|
|
||||||
root: path.join(projectRoot, `backend`),
|
|
||||||
verbose: progOptions.verbose,
|
|
||||||
})
|
|
||||||
|
|
||||||
const hasStorefront = storefront.toLowerCase() !== "none"
|
|
||||||
if (hasStorefront) {
|
|
||||||
const storefrontStarter = getStorefrontStarter(storefront)
|
|
||||||
await newStarter({
|
|
||||||
starter: storefrontStarter,
|
|
||||||
root: path.join(projectRoot, `storefront`),
|
|
||||||
verbose: progOptions.verbose,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`
|
|
||||||
Your project is ready 🚀. To start your Medusa project:
|
|
||||||
|
|
||||||
Medusa Backend
|
|
||||||
1. Change to the backend directory: cd ${projectRoot}/backend
|
|
||||||
2. Create a PostgreSQL database and make sure your PostgreSQL server is running.
|
|
||||||
3. Add the following environment variable to the \`.env\` file:
|
|
||||||
DATABASE_URL=postgres://localhost/medusa-store # This is the default URL, change it to your own database URL
|
|
||||||
4. Run migrations:
|
|
||||||
npx @medusajs/medusa-cli@latest migrations run
|
|
||||||
5. Optionally seed database with dummy data:
|
|
||||||
npx @medusajs/medusa-cli@latest seed -f ./data/seed.json
|
|
||||||
6. Start backend:
|
|
||||||
npx @medusajs/medusa-cli@latest develop
|
|
||||||
`)
|
|
||||||
|
|
||||||
if (hasStorefront) {
|
|
||||||
console.log(`
|
|
||||||
Storefront
|
|
||||||
1. Run the backend as explained above.
|
|
||||||
2. Change to the storefront directory: cd ${projectRoot}/storefront
|
|
||||||
3. Run storefront: yarn dev
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
import { execSync } from "child_process"
|
|
||||||
import execa from "execa"
|
|
||||||
import { spin } from "tiny-spin"
|
|
||||||
import { sync as existsSync } from "fs-exists-cached"
|
|
||||||
import fs from "fs-extra"
|
|
||||||
import hostedGitInfo from "hosted-git-info"
|
|
||||||
import isValid from "is-valid-path"
|
|
||||||
import sysPath from "path"
|
|
||||||
import url from "url"
|
|
||||||
|
|
||||||
import { reporter } from "./reporter"
|
|
||||||
import { getConfigStore } from "./get-config-store"
|
|
||||||
|
|
||||||
const packageManagerConfigKey = `cli.packageManager`
|
|
||||||
|
|
||||||
export const getPackageManager = (npmConfigUserAgent) => {
|
|
||||||
const configStore = getConfigStore()
|
|
||||||
const actualPackageManager = configStore.get(packageManagerConfigKey)
|
|
||||||
|
|
||||||
if (actualPackageManager) {
|
|
||||||
return actualPackageManager
|
|
||||||
}
|
|
||||||
|
|
||||||
if (npmConfigUserAgent?.includes(`yarn`)) {
|
|
||||||
configStore.set(packageManagerConfigKey, `yarn`)
|
|
||||||
return `yarn`
|
|
||||||
}
|
|
||||||
|
|
||||||
configStore.set(packageManagerConfigKey, `npm`)
|
|
||||||
return `npm`
|
|
||||||
}
|
|
||||||
|
|
||||||
const spawnWithArgs = (file, args, options) =>
|
|
||||||
execa(file, args, { stdio: "ignore", preferLocal: false, ...options })
|
|
||||||
|
|
||||||
const spawn = (cmd, options) => {
|
|
||||||
const [file, ...args] = cmd.split(/\s+/)
|
|
||||||
return spawnWithArgs(file, args, options)
|
|
||||||
}
|
|
||||||
// Checks the existence of yarn package
|
|
||||||
// We use yarnpkg instead of yarn to avoid conflict with Hadoop yarn
|
|
||||||
// Refer to https://github.com/yarnpkg/yarn/issues/673
|
|
||||||
const checkForYarn = () => {
|
|
||||||
try {
|
|
||||||
execSync(`yarnpkg --version`, { stdio: `ignore` })
|
|
||||||
return true
|
|
||||||
} catch (e) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAlreadyGitRepository = async () => {
|
|
||||||
try {
|
|
||||||
return await spawn(`git rev-parse --is-inside-work-tree`, {
|
|
||||||
stdio: `pipe`,
|
|
||||||
}).then((output) => output.stdout === `true`)
|
|
||||||
} catch (err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize newly cloned directory as a git repo
|
|
||||||
const gitInit = async (rootPath) => {
|
|
||||||
reporter.info(`Initialising git in ${rootPath}`)
|
|
||||||
|
|
||||||
return await spawn(`git init`, { cwd: rootPath })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a .gitignore file if it is missing in the new directory
|
|
||||||
const maybeCreateGitIgnore = async (rootPath) => {
|
|
||||||
if (existsSync(sysPath.join(rootPath, `.gitignore`))) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reporter.info(`Creating minimal .gitignore in ${rootPath}`)
|
|
||||||
await fs.writeFile(
|
|
||||||
sysPath.join(rootPath, `.gitignore`),
|
|
||||||
`.cache\nnode_modules\npublic\n`
|
|
||||||
)
|
|
||||||
reporter.success(`Created .gitignore in ${rootPath}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an initial git commit in the new directory
|
|
||||||
const createInitialGitCommit = async (rootPath, starterUrl) => {
|
|
||||||
reporter.info(`Create initial git commit in ${rootPath}`)
|
|
||||||
|
|
||||||
await spawn(`git add -A`, { cwd: rootPath })
|
|
||||||
// use execSync instead of spawn to handle git clients using
|
|
||||||
// pgp signatures (with password)
|
|
||||||
try {
|
|
||||||
execSync(`git commit -m "Initial commit from medusa: (${starterUrl})"`, {
|
|
||||||
cwd: rootPath,
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
// Remove git support if initial commit fails
|
|
||||||
reporter.warn(`Initial git commit failed - removing git support\n`)
|
|
||||||
fs.removeSync(sysPath.join(rootPath, `.git`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executes `npm install` or `yarn install` in rootPath.
|
|
||||||
const install = async (rootPath, verbose) => {
|
|
||||||
const prevDir = process.cwd()
|
|
||||||
|
|
||||||
const stop = spin(`Installing packages...`)
|
|
||||||
console.log() // Add some space
|
|
||||||
|
|
||||||
process.chdir(rootPath)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (getPackageManager() === `yarn` && checkForYarn()) {
|
|
||||||
await fs.remove(`package-lock.json`)
|
|
||||||
await spawn(`yarnpkg`, { stdio: verbose ? `inherit` : `ignore` })
|
|
||||||
} else {
|
|
||||||
await fs.remove(`yarn.lock`)
|
|
||||||
await spawn(`npm install`, { stdio: verbose ? `inherit` : `ignore` })
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
stop()
|
|
||||||
console.log()
|
|
||||||
reporter.success(`Packages installed`)
|
|
||||||
process.chdir(prevDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ignored = (path) => !/^\.(git|hg)$/.test(sysPath.basename(path))
|
|
||||||
|
|
||||||
// Copy starter from file system.
|
|
||||||
const copy = async (starterPath, rootPath) => {
|
|
||||||
// Chmod with 755.
|
|
||||||
// 493 = parseInt('755', 8)
|
|
||||||
await fs.ensureDir(rootPath, { mode: 493 })
|
|
||||||
|
|
||||||
if (!existsSync(starterPath)) {
|
|
||||||
throw new Error(`starter ${starterPath} doesn't exist`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (starterPath === `.`) {
|
|
||||||
throw new Error(
|
|
||||||
`You can't create a starter from the existing directory. If you want to
|
|
||||||
create a new project in the current directory, the trailing dot isn't
|
|
||||||
necessary. If you want to create a project from a local starter, run
|
|
||||||
something like "medusa new my-medusa-store ../local-medusa-starter"`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stop = spin(`Creating new site from local starter: ${starterPath}`)
|
|
||||||
|
|
||||||
reporter.info(`Copying local starter to ${rootPath} ...`)
|
|
||||||
|
|
||||||
await fs.copy(starterPath, rootPath, { filter: ignored })
|
|
||||||
|
|
||||||
stop()
|
|
||||||
console.log()
|
|
||||||
reporter.success(`Created starter directory layout`)
|
|
||||||
console.log() // Add some space
|
|
||||||
|
|
||||||
await install(rootPath)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clones starter from URI.
|
|
||||||
const clone = async (hostInfo, rootPath, keepGit, verbose = false) => {
|
|
||||||
let url
|
|
||||||
// Let people use private repos accessed over SSH.
|
|
||||||
if (hostInfo.getDefaultRepresentation() === `sshurl`) {
|
|
||||||
url = hostInfo.ssh({ noCommittish: true })
|
|
||||||
// Otherwise default to normal git syntax.
|
|
||||||
} else {
|
|
||||||
url = hostInfo.https({ noCommittish: true, noGitPlus: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const branch = hostInfo.committish ? [`-b`, hostInfo.committish] : []
|
|
||||||
|
|
||||||
const stop = spin(`Creating new project from git: ${url}`)
|
|
||||||
|
|
||||||
const args = [
|
|
||||||
`clone`,
|
|
||||||
...branch,
|
|
||||||
url,
|
|
||||||
rootPath,
|
|
||||||
`--recursive`,
|
|
||||||
`--depth=1`,
|
|
||||||
].filter((arg) => Boolean(arg))
|
|
||||||
|
|
||||||
await execa(`git`, args, {})
|
|
||||||
.then(() => {
|
|
||||||
stop()
|
|
||||||
console.log()
|
|
||||||
reporter.success(`Created starter directory layout`)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
stop()
|
|
||||||
console.log()
|
|
||||||
reporter.error(`Failed to clone repository`)
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!keepGit) {
|
|
||||||
await fs.remove(sysPath.join(rootPath, `.git`))
|
|
||||||
}
|
|
||||||
|
|
||||||
await install(rootPath, verbose)
|
|
||||||
const isGit = await isAlreadyGitRepository()
|
|
||||||
if (!isGit) await gitInit(rootPath)
|
|
||||||
await maybeCreateGitIgnore(rootPath)
|
|
||||||
if (!isGit) await createInitialGitCommit(rootPath, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPaths = async (starterPath, rootPath) => {
|
|
||||||
let selectedOtherStarter = false
|
|
||||||
|
|
||||||
// set defaults if no root or starter has been set yet
|
|
||||||
rootPath = rootPath || process.cwd()
|
|
||||||
starterPath = starterPath || `medusajs/medusa-starter-default`
|
|
||||||
|
|
||||||
return { starterPath, rootPath, selectedOtherStarter }
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupEnvVars = async (rootPath) => {
|
|
||||||
const templatePath = sysPath.join(rootPath, ".env.template")
|
|
||||||
const destination = sysPath.join(rootPath, ".env")
|
|
||||||
if (existsSync(templatePath)) {
|
|
||||||
fs.renameSync(templatePath, destination)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main function that clones or copies the starter.
|
|
||||||
*/
|
|
||||||
export const newStarter = async (args) => {
|
|
||||||
const { starter, root, verbose, keepGit } = args
|
|
||||||
|
|
||||||
const { starterPath, rootPath, selectedOtherStarter } = await getPaths(
|
|
||||||
starter,
|
|
||||||
root
|
|
||||||
)
|
|
||||||
|
|
||||||
const urlObject = url.parse(rootPath)
|
|
||||||
|
|
||||||
if (urlObject.protocol && urlObject.host) {
|
|
||||||
const isStarterAUrl =
|
|
||||||
starter && !url.parse(starter).hostname && !url.parse(starter).protocol
|
|
||||||
|
|
||||||
if (/medusa-starter/gi.test(rootPath) && isStarterAUrl) {
|
|
||||||
reporter.panic({
|
|
||||||
id: `10000`,
|
|
||||||
context: {
|
|
||||||
starter,
|
|
||||||
rootPath,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reporter.panic({
|
|
||||||
id: `10001`,
|
|
||||||
context: {
|
|
||||||
rootPath,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValid(rootPath)) {
|
|
||||||
reporter.panic({
|
|
||||||
id: `10002`,
|
|
||||||
context: {
|
|
||||||
path: sysPath.resolve(rootPath),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existsSync(sysPath.join(rootPath, `package.json`))) {
|
|
||||||
reporter.panic({
|
|
||||||
id: `10003`,
|
|
||||||
context: {
|
|
||||||
rootPath,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostedInfo = hostedGitInfo.fromUrl(starterPath)
|
|
||||||
if (hostedInfo) {
|
|
||||||
await clone(hostedInfo, rootPath, keepGit, verbose)
|
|
||||||
} else {
|
|
||||||
await copy(starterPath, rootPath, verbose)
|
|
||||||
}
|
|
||||||
|
|
||||||
await setupEnvVars(rootPath)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
export const panicHandler = (panicData = {}) => {
|
|
||||||
const { id, context } = panicData
|
|
||||||
switch (id) {
|
|
||||||
case "10000":
|
|
||||||
return {
|
|
||||||
message: `Looks like you provided a URL as your project name. Try "medusa new my-medusa-store ${context.rootPath}" instead.`,
|
|
||||||
}
|
|
||||||
case "10001":
|
|
||||||
return {
|
|
||||||
message: `Looks like you provided a URL as your project name. Try "medusa new my-medusa-store ${context.rootPath}" instead.`,
|
|
||||||
}
|
|
||||||
case "10002":
|
|
||||||
return {
|
|
||||||
message: `Could not create project because ${context.path} is not a valid path.`,
|
|
||||||
}
|
|
||||||
case "10003":
|
|
||||||
return {
|
|
||||||
message: `Directory ${context.rootPath} is already a Node project.`,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
message: "Unknown error",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import c from "ansi-colors"
|
|
||||||
import { panicHandler } from "./panic-handler"
|
|
||||||
|
|
||||||
export const reporter = {
|
|
||||||
info: (message: string): void => console.log(message),
|
|
||||||
verbose: (message: string): void => console.log(message),
|
|
||||||
log: (message: string): void => console.log(message),
|
|
||||||
success: (message: string): void =>
|
|
||||||
console.log(c.green(c.symbols.check + ` `) + message),
|
|
||||||
error: (message: string): void =>
|
|
||||||
console.error(c.red(c.symbols.cross + ` `) + message),
|
|
||||||
panic: (panicData: { id: string; context: any }): never => {
|
|
||||||
const { message } = panicHandler(panicData)
|
|
||||||
console.error(message)
|
|
||||||
process.exit(1)
|
|
||||||
},
|
|
||||||
warn: (message: string): void => console.warn(message),
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import fetch from "node-fetch"
|
|
||||||
import uuidv4 from "uuid/v4"
|
|
||||||
import { getConfigStore } from "./get-config-store"
|
|
||||||
|
|
||||||
const store = getConfigStore()
|
|
||||||
const medusaCliVersion = require(`../package.json`).version
|
|
||||||
|
|
||||||
const analyticsApi =
|
|
||||||
process.env.MEDUSA_TELEMETRY_API ||
|
|
||||||
`https://telemetry.medusa-commerce.com/batch`
|
|
||||||
|
|
||||||
const getMachineId = (): string => {
|
|
||||||
let machineId = store.get(`telemetry.machine_id`)
|
|
||||||
|
|
||||||
if (typeof machineId !== `string`) {
|
|
||||||
machineId = uuidv4()
|
|
||||||
store.set(`telemetry.machine_id`, machineId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return machineId
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionId = uuidv4()
|
|
||||||
|
|
||||||
export const track = (eventType: string, args?: any): void => {
|
|
||||||
fetch(analyticsApi, {
|
|
||||||
method: `POST`,
|
|
||||||
headers: {
|
|
||||||
"content-type": `application/json`,
|
|
||||||
"user-agent": `create-medusa-app:${medusaCliVersion}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
timestamp: new Date(),
|
|
||||||
batch: [
|
|
||||||
{
|
|
||||||
type: eventType,
|
|
||||||
timestamp: new Date(),
|
|
||||||
sessionId,
|
|
||||||
machine_id: getMachineId(),
|
|
||||||
component_id: `create-medusa-app`,
|
|
||||||
cli_version: medusaCliVersion,
|
|
||||||
properties: args,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
}).catch(() => {}) /* do nothing, it's telemetry */
|
|
||||||
}
|
|
||||||
+1
-4
@@ -1,4 +1 @@
|
|||||||
declare module "stream-filter"
|
declare module "medusa-telemetry"
|
||||||
declare module "ansi-wordwrap"
|
|
||||||
declare module "uuid/v4"
|
|
||||||
declare module "node-fetch"
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import promiseExec from "./promise-exec.js"
|
||||||
|
|
||||||
|
type CloneRepoOptions = {
|
||||||
|
directoryName?: string
|
||||||
|
repoUrl?: string
|
||||||
|
abortController?: AbortController
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO change default repo URL
|
||||||
|
const DEFAULT_REPO =
|
||||||
|
"https://github.com/medusajs/medusa-starter-default -b feat/onboarding"
|
||||||
|
|
||||||
|
export default async ({
|
||||||
|
directoryName = "",
|
||||||
|
repoUrl,
|
||||||
|
abortController,
|
||||||
|
}: CloneRepoOptions) => {
|
||||||
|
await promiseExec(`git clone ${repoUrl || DEFAULT_REPO} ${directoryName}`, {
|
||||||
|
signal: abortController?.signal,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import onProcessTerminated from "./on-process-terminated.js"
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
onProcessTerminated(() => abortController.abort())
|
||||||
|
return abortController
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isAbortError = (e: any) =>
|
||||||
|
e !== null && "code" in e && e.code === "ABORT_ERR"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import pg from "pg"
|
||||||
|
|
||||||
|
type CreateDbOptions = {
|
||||||
|
client: pg.Client
|
||||||
|
db: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async ({ client, db }: CreateDbOptions) => {
|
||||||
|
await client.query(`CREATE DATABASE "${db}"`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import boxen from "boxen"
|
||||||
|
import chalk from "chalk"
|
||||||
|
import { Ora } from "ora"
|
||||||
|
import onProcessTerminated from "./on-process-terminated.js"
|
||||||
|
|
||||||
|
const facts = [
|
||||||
|
"Plugins allow you to integrate third-party services for payment, fulfillment, notifications, and more.",
|
||||||
|
"You can specify a product's availability in one or more sales channels.",
|
||||||
|
"Payment and shipping options and providers can be configured per region.",
|
||||||
|
"Tax-inclusive pricing allows you to set prices for products, shipping options, and more without having to worry about calculating taxes.",
|
||||||
|
"Medusa provides multi-currency and region support, with full control over prices for each currency and region.",
|
||||||
|
"You can organize customers by customer groups and set special prices for them.",
|
||||||
|
"You can specify the inventory of products per location and sales channel.",
|
||||||
|
"Publishable-API Keys allow you to send requests to the backend within a scoped resource.",
|
||||||
|
"You can create custom endpoints by creating a TypeScript file under the src/api directory.",
|
||||||
|
"You can listen to events to perform asynchronous actions using Subscribers.",
|
||||||
|
"An entity represents a table in the database. You can create a table by creating a custom entity and migration.",
|
||||||
|
"Medusa's store endpoint paths are prefixed by /store. The admin endpoints are prefixed by /admin.",
|
||||||
|
"Medusa provides a JavaScript client and a React library that you can use to build a storefront or a custom admin.",
|
||||||
|
"Services are classes with methods related to an entity or functionality. You can create a custom service in a TypeScript file under src/services.",
|
||||||
|
"Modules allow you to replace an entire functionality with your custom logic.",
|
||||||
|
"The event bus module is responsible for triggering events and relaying them to subscribers.",
|
||||||
|
"The cache module is responsible for caching data that requires heavy computation.",
|
||||||
|
]
|
||||||
|
|
||||||
|
export const getFact = () => {
|
||||||
|
const randIndex = Math.floor(Math.random() * facts.length)
|
||||||
|
|
||||||
|
return facts[randIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showFact = (spinner: Ora, title: string) => {
|
||||||
|
const fact = getFact()
|
||||||
|
spinner.text = `${boxen(fact, {
|
||||||
|
title: chalk.cyan(title),
|
||||||
|
titleAlignment: "center",
|
||||||
|
textAlignment: "center",
|
||||||
|
padding: 1,
|
||||||
|
margin: 1,
|
||||||
|
float: "center",
|
||||||
|
})}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createFactBox = (spinner: Ora, title: string): NodeJS.Timer => {
|
||||||
|
spinner.spinner = {
|
||||||
|
frames: [""],
|
||||||
|
}
|
||||||
|
showFact(spinner, title)
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
showFact(spinner, title)
|
||||||
|
}, 10000)
|
||||||
|
|
||||||
|
onProcessTerminated(() => clearInterval(interval))
|
||||||
|
|
||||||
|
return interval
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resetFactBox = (
|
||||||
|
interval: NodeJS.Timer | null,
|
||||||
|
spinner: Ora,
|
||||||
|
successMessage: string,
|
||||||
|
newTitle?: string
|
||||||
|
): NodeJS.Timer | null => {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.spinner = "dots"
|
||||||
|
spinner.succeed(chalk.green(successMessage)).start()
|
||||||
|
let newInterval = null
|
||||||
|
if (newTitle) {
|
||||||
|
newInterval = createFactBox(spinner, newTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newInterval
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
type ConnectionStringOptions = {
|
||||||
|
user?: string
|
||||||
|
password?: string
|
||||||
|
host?: string
|
||||||
|
db: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ({ user, password, host, db }: ConnectionStringOptions) => {
|
||||||
|
let connection = `postgres://`
|
||||||
|
if (user) {
|
||||||
|
connection += user
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
connection += `:${password}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user || password) {
|
||||||
|
connection += "@"
|
||||||
|
}
|
||||||
|
|
||||||
|
connection += `${host}/${db}`
|
||||||
|
|
||||||
|
return connection
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -0,0 +1,23 @@
|
|||||||
|
import chalk from "chalk"
|
||||||
|
import { program } from "commander"
|
||||||
|
|
||||||
|
type LogOptions = {
|
||||||
|
message: string
|
||||||
|
type?: "error" | "success" | "info" | "warning"
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ({ message, type = "info" }: LogOptions) => {
|
||||||
|
switch (type) {
|
||||||
|
case "info":
|
||||||
|
console.log(chalk.white(message))
|
||||||
|
break
|
||||||
|
case "success":
|
||||||
|
console.log(chalk.green(message))
|
||||||
|
break
|
||||||
|
case "warning":
|
||||||
|
console.log(chalk.yellow(message))
|
||||||
|
break
|
||||||
|
case "error":
|
||||||
|
program.error(chalk.bold.red(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default (fn: Function) => {
|
||||||
|
process.on("SIGTERM", () => fn())
|
||||||
|
process.on("SIGINT", () => fn())
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import pg from "pg"
|
||||||
|
const { Client } = pg
|
||||||
|
|
||||||
|
type PostgresConnection = {
|
||||||
|
user?: string
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (connect: PostgresConnection) => {
|
||||||
|
const client = new Client(connect)
|
||||||
|
|
||||||
|
await client.connect()
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import chalk from "chalk"
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import { Ora } from "ora"
|
||||||
|
import promiseExec from "./promise-exec.js"
|
||||||
|
import { EOL } from "os"
|
||||||
|
import runProcess from "./run-process.js"
|
||||||
|
import { createFactBox, resetFactBox } from "./facts.js"
|
||||||
|
|
||||||
|
type PrepareOptions = {
|
||||||
|
directory: string
|
||||||
|
dbConnectionString: string
|
||||||
|
admin?: {
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
seed?: boolean
|
||||||
|
spinner: Ora
|
||||||
|
abortController?: AbortController
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async ({
|
||||||
|
directory,
|
||||||
|
dbConnectionString,
|
||||||
|
admin,
|
||||||
|
seed,
|
||||||
|
spinner,
|
||||||
|
abortController,
|
||||||
|
}: PrepareOptions) => {
|
||||||
|
// initialize execution options
|
||||||
|
const execOptions = {
|
||||||
|
cwd: directory,
|
||||||
|
signal: abortController?.signal,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}`
|
||||||
|
)
|
||||||
|
|
||||||
|
let interval: NodeJS.Timer | null = createFactBox(
|
||||||
|
spinner,
|
||||||
|
"Installing dependencies..."
|
||||||
|
)
|
||||||
|
|
||||||
|
await runProcess({
|
||||||
|
process: async () => {
|
||||||
|
try {
|
||||||
|
await promiseExec(`yarn`, execOptions)
|
||||||
|
} catch (e) {
|
||||||
|
// yarn isn't available
|
||||||
|
// use npm
|
||||||
|
await promiseExec(`npm install --legacy-peer-deps`, execOptions)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ignoreERESOLVE: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
interval = resetFactBox(
|
||||||
|
interval,
|
||||||
|
spinner,
|
||||||
|
"Installed Dependencies",
|
||||||
|
"Running Migrations...."
|
||||||
|
)
|
||||||
|
|
||||||
|
// run migrations
|
||||||
|
await runProcess({
|
||||||
|
process: async () => {
|
||||||
|
await promiseExec(
|
||||||
|
"npx -y @medusajs/medusa-cli@latest migrations run",
|
||||||
|
execOptions
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
interval = resetFactBox(interval, spinner, "Ran Migrations")
|
||||||
|
|
||||||
|
if (admin) {
|
||||||
|
// create admin user
|
||||||
|
interval = createFactBox(spinner, "Creating an admin user...")
|
||||||
|
|
||||||
|
await runProcess({
|
||||||
|
process: async () => {
|
||||||
|
const proc = await promiseExec(
|
||||||
|
// TODO replace with latest version
|
||||||
|
`npx -y @medusajs/medusa-cli@1.3.16-snapshot-20230605093446 user -e ${admin.email} --invite`,
|
||||||
|
execOptions
|
||||||
|
)
|
||||||
|
// get invite token from stdout
|
||||||
|
const match = proc.stdout.match(/Invite token: (?<token>.+)/)
|
||||||
|
inviteToken = match?.groups?.token
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
interval = resetFactBox(interval, spinner, "Created admin user")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seed) {
|
||||||
|
interval = createFactBox(spinner, "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..."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.start()
|
||||||
|
return inviteToken
|
||||||
|
}
|
||||||
|
|
||||||
|
await runProcess({
|
||||||
|
process: async () => {
|
||||||
|
await promiseExec(
|
||||||
|
`npx -y @medusajs/medusa-cli@latest seed --seed-file=${path.join(
|
||||||
|
"data",
|
||||||
|
"seed.json"
|
||||||
|
)}`,
|
||||||
|
execOptions
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
resetFactBox(interval, spinner, "Seeded database with demo data")
|
||||||
|
} else if (
|
||||||
|
fs.existsSync(path.join(directory, "data", "seed-onboarding.json"))
|
||||||
|
) {
|
||||||
|
// seed the database with onboarding seed
|
||||||
|
interval = createFactBox(spinner, "Finish preparation...")
|
||||||
|
|
||||||
|
await runProcess({
|
||||||
|
process: async () => {
|
||||||
|
await promiseExec(
|
||||||
|
`npx -y @medusajs/medusa-cli@latest seed --seed-file=${path.join(
|
||||||
|
"data",
|
||||||
|
"seed-onboarding.json"
|
||||||
|
)}`,
|
||||||
|
execOptions
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
resetFactBox(interval, spinner, "Finished Preparation")
|
||||||
|
}
|
||||||
|
|
||||||
|
return inviteToken
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { exec } from "child_process"
|
||||||
|
import util from "util"
|
||||||
|
|
||||||
|
const promiseExec = util.promisify(exec)
|
||||||
|
|
||||||
|
export default promiseExec
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
type ProcessOptions = {
|
||||||
|
process: Function
|
||||||
|
ignoreERESOLVE?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// when running commands with npx or npm sometimes they
|
||||||
|
// terminate with EAGAIN error unexpectedly
|
||||||
|
// this utility function allows retrying the process if
|
||||||
|
// EAGAIN occurs, or otherwise throw the error that occurs
|
||||||
|
export default async ({ process, ignoreERESOLVE }: ProcessOptions) => {
|
||||||
|
let processError = false
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
await process()
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
typeof error === "object" &&
|
||||||
|
error !== null &&
|
||||||
|
"code" in error &&
|
||||||
|
error?.code === "EAGAIN"
|
||||||
|
) {
|
||||||
|
processError = true
|
||||||
|
} else if (
|
||||||
|
ignoreERESOLVE &&
|
||||||
|
typeof error === "object" &&
|
||||||
|
error !== null &&
|
||||||
|
"code" in error &&
|
||||||
|
error?.code === "ERESOLVE"
|
||||||
|
) {
|
||||||
|
// ignore error
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (processError)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { exec } from "child_process"
|
||||||
|
|
||||||
|
type StartOptions = {
|
||||||
|
directory: string
|
||||||
|
abortController?: AbortController
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ({ directory, abortController }: StartOptions) => {
|
||||||
|
const childProcess = exec(`npx -y @medusajs/medusa-cli develop`, {
|
||||||
|
cwd: directory,
|
||||||
|
signal: abortController?.signal,
|
||||||
|
})
|
||||||
|
|
||||||
|
childProcess.stdout?.pipe(process.stdout)
|
||||||
|
}
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2015",
|
"target": "ESNext",
|
||||||
"module": "commonjs",
|
"module": "ESNext",
|
||||||
"allowJs": true,
|
"moduleResolution": "node16",
|
||||||
"declaration": false,
|
"outDir": "./dist",
|
||||||
"outDir": "./dist",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true
|
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.json", "src/**/*.js"],
|
"include": ["src"],
|
||||||
"exclude": ["**/__tests__/**/*"]
|
"ts-node": {
|
||||||
|
"esm": true,
|
||||||
|
"experimentalSpecifierResolution": "node",
|
||||||
|
"transpileOnly": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user