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:
5
.changeset/wise-eels-study.md
Normal file
5
.changeset/wise-eels-study.md
Normal file
@@ -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/cache-redis
|
||||
!packages/cache-inmemory
|
||||
!packages/create-medusa-app
|
||||
|
||||
|
||||
**/models/*
|
||||
|
||||
@@ -94,6 +94,7 @@ module.exports = {
|
||||
"./packages/stock-location/tsconfig.spec.json",
|
||||
"./packages/cache-redis/tsconfig.spec.json",
|
||||
"./packages/cache-inmemory/tsconfig.spec.json",
|
||||
"./packages/create-medusa-app/tsconfig.json",
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
|
||||
1
packages/create-medusa-app/.gitignore
vendored
1
packages/create-medusa-app/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
/dist
|
||||
yarn.lock
|
||||
my-medusa-store
|
||||
58
packages/create-medusa-app/README.md
Normal file
58
packages/create-medusa-app/README.md
Normal file
@@ -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",
|
||||
"version": "0.0.10",
|
||||
"main": "dist/index.js",
|
||||
"bin": "cli.js",
|
||||
"description": "Create a Medusa project using a single command.",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
"bin": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist/index.js",
|
||||
"cli.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "microbundle -i src/index.ts --no-pkg-main --target=node -f=cjs --sourcemap=false --compress --alias worker_threads=@ascorbic/worker-threads-shim",
|
||||
"watch": "microbundle -i src/index.ts --no-pkg-main --target=node -f=cjs --sourcemap=false --alias worker_threads=@ascorbic/worker-threads-shim --watch",
|
||||
"prepare": "yarn build"
|
||||
"dev": "ts-node --esm src/index.ts",
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch",
|
||||
"prepare": "cross-env NODE_ENV=production yarn run build"
|
||||
},
|
||||
"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": {
|
||||
"@ascorbic/worker-threads-shim": "^1.0.0",
|
||||
"@babel/runtime": "^7.15.4",
|
||||
"@types/chalk": "^2.2.0",
|
||||
"@types/commander": "^2.12.2",
|
||||
"@types/configstore": "^4.0.0",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/node": "^14.18.36",
|
||||
"ansi-wordwrap": "^1.0.2",
|
||||
"chalk": "^4.1.2",
|
||||
"commander": "^8.1.0",
|
||||
"common-tags": "^1.8.2",
|
||||
"@types/configstore": "^6.0.0",
|
||||
"@types/inquirer": "^9.0.3",
|
||||
"@types/pg": "^8.6.6",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@types/validator": "^13.7.17",
|
||||
"@types/wait-on": "^5.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.5",
|
||||
"@typescript-eslint/parser": "^5.59.5",
|
||||
"configstore": "^6.0.0",
|
||||
"enquirer": "^2.3.6",
|
||||
"execa": "^5.1.1",
|
||||
"fs-exists-cached": "^1.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"hosted-git-info": "^4.0.2",
|
||||
"is-valid-path": "^0.1.1",
|
||||
"microbundle": "^0.13.3",
|
||||
"node-fetch": "^2.6.9",
|
||||
"prompts": "^2.4.2",
|
||||
"string-length": "^4.0.2",
|
||||
"terminal-link": "^2.1.1",
|
||||
"tiny-spin": "^1.0.2",
|
||||
"url": "^0.11.0",
|
||||
"uuid": "3.4.0"
|
||||
"eslint": "^8.40.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"prettier": "^2.8.8",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa.git",
|
||||
"directory": "packages/create-medusa-app"
|
||||
},
|
||||
"author": "Sebastian Rindom <seb@medusa-commerce.com>",
|
||||
"author": "Medusa",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
278
packages/create-medusa-app/src/commands/create.ts
Normal file
278
packages/create-medusa-app/src/commands/create.ts
Normal file
@@ -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"
|
||||
import path from "path"
|
||||
#!/usr/bin/env node
|
||||
import { program } from "commander"
|
||||
import create from "./commands/create.js"
|
||||
|
||||
import { prompt } from "enquirer"
|
||||
import { newStarter } from "./new-starter"
|
||||
import { track } from "./track"
|
||||
program
|
||||
.description("Create a new Medusa project")
|
||||
.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"
|
||||
|
||||
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
|
||||
`)
|
||||
}
|
||||
}
|
||||
void create(program.opts())
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
5
packages/create-medusa-app/src/types.d.ts
vendored
5
packages/create-medusa-app/src/types.d.ts
vendored
@@ -1,4 +1 @@
|
||||
declare module "stream-filter"
|
||||
declare module "ansi-wordwrap"
|
||||
declare module "uuid/v4"
|
||||
declare module "node-fetch"
|
||||
declare module "medusa-telemetry"
|
||||
|
||||
21
packages/create-medusa-app/src/utils/clone-repo.ts
Normal file
21
packages/create-medusa-app/src/utils/clone-repo.ts
Normal file
@@ -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"
|
||||
10
packages/create-medusa-app/src/utils/create-db.ts
Normal file
10
packages/create-medusa-app/src/utils/create-db.ts
Normal file
@@ -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}"`)
|
||||
}
|
||||
76
packages/create-medusa-app/src/utils/facts.ts
Normal file
76
packages/create-medusa-app/src/utils/facts.ts
Normal file
@@ -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
|
||||
}
|
||||
23
packages/create-medusa-app/src/utils/log-message.ts
Normal file
23
packages/create-medusa-app/src/utils/log-message.ts
Normal file
@@ -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())
|
||||
}
|
||||
15
packages/create-medusa-app/src/utils/postgres-client.ts
Normal file
15
packages/create-medusa-app/src/utils/postgres-client.ts
Normal file
@@ -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
|
||||
}
|
||||
149
packages/create-medusa-app/src/utils/prepare-project.ts
Normal file
149
packages/create-medusa-app/src/utils/prepare-project.ts
Normal file
@@ -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
|
||||
}
|
||||
6
packages/create-medusa-app/src/utils/promise-exec.ts
Normal file
6
packages/create-medusa-app/src/utils/promise-exec.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { exec } from "child_process"
|
||||
import util from "util"
|
||||
|
||||
const promiseExec = util.promisify(exec)
|
||||
|
||||
export default promiseExec
|
||||
36
packages/create-medusa-app/src/utils/run-process.ts
Normal file
36
packages/create-medusa-app/src/utils/run-process.ts
Normal file
@@ -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)
|
||||
}
|
||||
15
packages/create-medusa-app/src/utils/start-medusa.ts
Normal file
15
packages/create-medusa-app/src/utils/start-medusa.ts
Normal file
@@ -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": {
|
||||
"target": "ES2015",
|
||||
"module": "commonjs",
|
||||
"allowJs": true,
|
||||
"declaration": false,
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node16",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.json", "src/**/*.js"],
|
||||
"exclude": ["**/__tests__/**/*"]
|
||||
"include": ["src"],
|
||||
"ts-node": {
|
||||
"esm": true,
|
||||
"experimentalSpecifierResolution": "node",
|
||||
"transpileOnly": true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user