chore(): start moving some packages to the core directory (#7215)
This commit is contained in:
committed by
GitHub
parent
fdee748eed
commit
bbccd6481d
@@ -0,0 +1,25 @@
|
||||
import fs from "fs"
|
||||
import glob from "glob"
|
||||
import path from "path"
|
||||
|
||||
export function clearProject(directory: string) {
|
||||
const adminFiles = glob.sync(path.join(directory, `src`, `admin/**/*`))
|
||||
const onboardingFiles = glob.sync(
|
||||
path.join(directory, `src`, `**/onboarding/`)
|
||||
)
|
||||
const typeFiles = glob.sync(path.join(directory, `src`, `types`))
|
||||
const srcFiles = glob.sync(
|
||||
path.join(directory, `src`, `**/*.{ts,tsx,js,jsx}`)
|
||||
)
|
||||
|
||||
const files = [...adminFiles, ...onboardingFiles, ...typeFiles, ...srcFiles]
|
||||
|
||||
files.forEach((file) =>
|
||||
fs.rmSync(file, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
})
|
||||
)
|
||||
// add empty typescript file to avoid build errors
|
||||
fs.openSync(path.join(directory, "src", "index.ts"), "w")
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import execute from "./execute.js"
|
||||
import { Ora } from "ora"
|
||||
import { isAbortError } from "./create-abort-controller.js"
|
||||
import logMessage from "./log-message.js"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
type CloneRepoOptions = {
|
||||
directoryName?: string
|
||||
repoUrl?: string
|
||||
abortController?: AbortController
|
||||
verbose?: boolean
|
||||
v2?: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_REPO = "https://github.com/medusajs/medusa-starter-default"
|
||||
const V2_BRANCH = "feat/v2"
|
||||
|
||||
export default async function cloneRepo({
|
||||
directoryName = "",
|
||||
repoUrl,
|
||||
abortController,
|
||||
verbose = false,
|
||||
v2 = false,
|
||||
}: CloneRepoOptions) {
|
||||
await execute(
|
||||
[
|
||||
`git clone ${repoUrl || DEFAULT_REPO}${
|
||||
v2 ? ` -b ${V2_BRANCH}` : ""
|
||||
} ${directoryName}`,
|
||||
{
|
||||
signal: abortController?.signal,
|
||||
},
|
||||
],
|
||||
{ verbose }
|
||||
)
|
||||
}
|
||||
|
||||
export async function runCloneRepo({
|
||||
projectName,
|
||||
repoUrl,
|
||||
abortController,
|
||||
spinner,
|
||||
verbose = false,
|
||||
v2 = false,
|
||||
}: {
|
||||
projectName: string
|
||||
repoUrl: string
|
||||
abortController: AbortController
|
||||
spinner: Ora
|
||||
verbose?: boolean
|
||||
v2?: boolean
|
||||
}) {
|
||||
try {
|
||||
await cloneRepo({
|
||||
directoryName: projectName,
|
||||
repoUrl,
|
||||
abortController,
|
||||
verbose,
|
||||
v2,
|
||||
})
|
||||
|
||||
deleteGitDirectory(projectName)
|
||||
} catch (e) {
|
||||
if (isAbortError(e)) {
|
||||
process.exit()
|
||||
}
|
||||
|
||||
spinner.stop()
|
||||
logMessage({
|
||||
message: `An error occurred while setting up your project: ${e}`,
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function deleteGitDirectory(projectDirectory: string) {
|
||||
fs.rmSync(path.join(projectDirectory, ".git"), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import ProcessManager from "./process-manager.js"
|
||||
|
||||
export default (processManager: ProcessManager) => {
|
||||
const abortController = new AbortController()
|
||||
processManager.onTerminated(() => abortController.abort())
|
||||
return abortController
|
||||
}
|
||||
|
||||
export const isAbortError = (e: any) =>
|
||||
e !== null && "code" in e && e.code === "ABORT_ERR"
|
||||
|
||||
export const getAbortError = () => {
|
||||
return {
|
||||
code: "ABORT_ERR",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { EOL } from "os"
|
||||
import pg from "pg"
|
||||
import postgresClient from "./postgres-client.js"
|
||||
import inquirer from "inquirer"
|
||||
import logMessage from "./log-message.js"
|
||||
import formatConnectionString from "./format-connection-string.js"
|
||||
import { Ora } from "ora"
|
||||
import { getCurrentOs } from "./get-current-os.js"
|
||||
|
||||
type CreateDbOptions = {
|
||||
client: pg.Client
|
||||
db: string
|
||||
}
|
||||
|
||||
export default async function createDb({ client, db }: CreateDbOptions) {
|
||||
await client.query(`CREATE DATABASE "${db}"`)
|
||||
}
|
||||
|
||||
export async function runCreateDb({
|
||||
client,
|
||||
dbName,
|
||||
spinner,
|
||||
}: {
|
||||
client: pg.Client
|
||||
dbName: string
|
||||
spinner: Ora
|
||||
}): Promise<pg.Client> {
|
||||
let newClient = client
|
||||
|
||||
try {
|
||||
// create postgres database
|
||||
await createDb({
|
||||
client,
|
||||
db: dbName,
|
||||
})
|
||||
|
||||
// create a new connection with database selected
|
||||
await client.end()
|
||||
newClient = await postgresClient({
|
||||
user: client.user,
|
||||
password: client.password,
|
||||
database: dbName,
|
||||
})
|
||||
} catch (e) {
|
||||
spinner.stop()
|
||||
logMessage({
|
||||
message: `An error occurred while trying to create your database: ${e}`,
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
|
||||
return newClient
|
||||
}
|
||||
|
||||
async function getForDbName({
|
||||
dbName,
|
||||
verbose = false,
|
||||
}: {
|
||||
dbName: string
|
||||
verbose?: boolean
|
||||
}): Promise<{
|
||||
client: pg.Client
|
||||
dbConnectionString: string
|
||||
}> {
|
||||
let client!: pg.Client
|
||||
let postgresUsername = "postgres"
|
||||
let postgresPassword = ""
|
||||
|
||||
try {
|
||||
client = await postgresClient({
|
||||
user: postgresUsername,
|
||||
password: postgresPassword,
|
||||
})
|
||||
} catch (e) {
|
||||
if (verbose) {
|
||||
logMessage({
|
||||
message: `The following error occured when connecting to the database: ${e}`,
|
||||
type: "verbose",
|
||||
})
|
||||
}
|
||||
// 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 because of the following error: ${e}.${EOL}${EOL}Make sure you have PostgreSQL installed and the credentials you provided are correct.${EOL}${EOL}You can learn how to install PostgreSQL here: https://docs.medusajs.com/development/backend/prepare-environment?os=${getCurrentOs()}#postgresql${EOL}${EOL}If you keep running into this issue despite having PostgreSQL installed, please check out our troubleshooting guidelines: https://docs.medusajs.com/troubleshooting/database-error`,
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// format connection string
|
||||
const dbConnectionString = formatConnectionString({
|
||||
user: postgresUsername,
|
||||
password: postgresPassword,
|
||||
host: client!.host,
|
||||
db: dbName,
|
||||
})
|
||||
|
||||
return {
|
||||
client,
|
||||
dbConnectionString,
|
||||
}
|
||||
}
|
||||
|
||||
async function getForDbUrl({
|
||||
dbUrl,
|
||||
verbose = false,
|
||||
}: {
|
||||
dbUrl: string
|
||||
verbose?: boolean
|
||||
}): Promise<{
|
||||
client: pg.Client
|
||||
dbConnectionString: string
|
||||
}> {
|
||||
let client!: pg.Client
|
||||
|
||||
try {
|
||||
client = await postgresClient({
|
||||
connectionString: dbUrl,
|
||||
})
|
||||
} catch (e) {
|
||||
if (verbose) {
|
||||
logMessage({
|
||||
message: `The following error occured when connecting to the database: ${e}`,
|
||||
type: "verbose",
|
||||
})
|
||||
}
|
||||
logMessage({
|
||||
message: `Couldn't connect to PostgreSQL using the database URL you passed. Make sure it's correct and try again.`,
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
client,
|
||||
dbConnectionString: dbUrl,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDbClientAndCredentials({
|
||||
dbName = "",
|
||||
dbUrl = "",
|
||||
verbose = false,
|
||||
}): Promise<{
|
||||
client: pg.Client
|
||||
dbConnectionString: string
|
||||
verbose?: boolean
|
||||
}> {
|
||||
if (dbName) {
|
||||
return await getForDbName({
|
||||
dbName,
|
||||
verbose,
|
||||
})
|
||||
} else {
|
||||
return await getForDbUrl({
|
||||
dbUrl,
|
||||
verbose,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { exec, spawnSync, SpawnSyncOptions } from "child_process"
|
||||
import util from "util"
|
||||
import { getAbortError } from "./create-abort-controller.js"
|
||||
|
||||
const promiseExec = util.promisify(exec)
|
||||
|
||||
type ExecuteOptions = {
|
||||
stdout?: string
|
||||
stderr?: string
|
||||
}
|
||||
|
||||
type VerboseOptions = {
|
||||
verbose?: boolean
|
||||
// Since spawn doesn't allow us to both retrieve the
|
||||
// output and output it live without using events,
|
||||
// enabling this option, which is only useful if `verbose` is `true`,
|
||||
// defers the output of the process until after the process is executed
|
||||
// instead of outputting the log in realtime, which is the default.
|
||||
// it prioritizes retrieving the output over outputting it in real-time.
|
||||
needOutput?: boolean
|
||||
}
|
||||
|
||||
type PromiseExecParams = Parameters<typeof promiseExec>
|
||||
type SpawnParams = [string, SpawnSyncOptions]
|
||||
|
||||
const execute = async (
|
||||
command: SpawnParams | PromiseExecParams,
|
||||
{ verbose = false, needOutput = false }: VerboseOptions
|
||||
): Promise<ExecuteOptions> => {
|
||||
if (verbose) {
|
||||
const [commandStr, options] = command as SpawnParams
|
||||
const childProcess = spawnSync(commandStr, {
|
||||
...options,
|
||||
shell: true,
|
||||
stdio: needOutput
|
||||
? "pipe"
|
||||
: [process.stdin, process.stdout, process.stderr],
|
||||
})
|
||||
|
||||
if (childProcess.error) {
|
||||
throw childProcess.error
|
||||
}
|
||||
|
||||
if (
|
||||
childProcess.signal &&
|
||||
["SIGINT", "SIGTERM"].includes(childProcess.signal)
|
||||
) {
|
||||
console.log("abortingggg")
|
||||
throw getAbortError()
|
||||
}
|
||||
|
||||
if (needOutput) {
|
||||
console.log(
|
||||
childProcess.stdout?.toString() || childProcess.stderr?.toString()
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
stdout: childProcess.stdout?.toString() || "",
|
||||
stderr: childProcess.stderr?.toString() || "",
|
||||
}
|
||||
} else {
|
||||
const childProcess = await promiseExec(...(command as PromiseExecParams))
|
||||
|
||||
return {
|
||||
stdout: childProcess.stdout as string,
|
||||
stderr: childProcess.stderr as string,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default execute
|
||||
@@ -0,0 +1,136 @@
|
||||
import boxen from "boxen"
|
||||
import chalk from "chalk"
|
||||
import { emojify } from "node-emoji"
|
||||
import { Ora } from "ora"
|
||||
import ProcessManager from "./process-manager.js"
|
||||
|
||||
export type FactBoxOptions = {
|
||||
interval: NodeJS.Timeout | null
|
||||
spinner: Ora
|
||||
processManager: ProcessManager
|
||||
message?: string
|
||||
title?: string
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
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,
|
||||
title,
|
||||
verbose,
|
||||
}: Pick<FactBoxOptions, "spinner" | "verbose"> & {
|
||||
title: string
|
||||
}) => {
|
||||
const fact = getFact()
|
||||
if (verbose) {
|
||||
spinner.stopAndPersist({
|
||||
symbol: chalk.cyan("⠋"),
|
||||
text: title,
|
||||
})
|
||||
} else {
|
||||
spinner.text = `${title}\n${boxen(`${fact}`, {
|
||||
title: chalk.cyan(`${emojify(":bulb:")} Medusa Tips`),
|
||||
titleAlignment: "center",
|
||||
textAlignment: "center",
|
||||
padding: 1,
|
||||
margin: 1,
|
||||
})}`
|
||||
}
|
||||
}
|
||||
|
||||
export const createFactBox = ({
|
||||
spinner,
|
||||
title,
|
||||
processManager,
|
||||
verbose,
|
||||
}: Pick<FactBoxOptions, "spinner" | "processManager" | "verbose"> & {
|
||||
title: string
|
||||
}): NodeJS.Timeout => {
|
||||
showFact({ spinner, title, verbose })
|
||||
const interval = setInterval(() => {
|
||||
showFact({ spinner, title, verbose })
|
||||
}, 10000)
|
||||
|
||||
processManager.addInterval(interval)
|
||||
|
||||
return interval
|
||||
}
|
||||
|
||||
export const resetFactBox = ({
|
||||
interval,
|
||||
spinner,
|
||||
successMessage,
|
||||
processManager,
|
||||
newTitle,
|
||||
verbose,
|
||||
}: Pick<
|
||||
FactBoxOptions,
|
||||
"interval" | "spinner" | "processManager" | "verbose"
|
||||
> & {
|
||||
successMessage: string
|
||||
newTitle?: string
|
||||
}): NodeJS.Timeout | null => {
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
|
||||
spinner.succeed(chalk.green(successMessage)).start()
|
||||
let newInterval = null
|
||||
if (newTitle) {
|
||||
newInterval = createFactBox({
|
||||
spinner,
|
||||
title: newTitle,
|
||||
processManager,
|
||||
verbose,
|
||||
})
|
||||
}
|
||||
|
||||
return newInterval
|
||||
}
|
||||
|
||||
export function displayFactBox({
|
||||
interval,
|
||||
spinner,
|
||||
processManager,
|
||||
title = "",
|
||||
message = "",
|
||||
verbose = false,
|
||||
}: FactBoxOptions): NodeJS.Timeout | null {
|
||||
if (!message) {
|
||||
return createFactBox({ spinner, title, processManager, verbose })
|
||||
}
|
||||
|
||||
return resetFactBox({
|
||||
interval,
|
||||
spinner,
|
||||
successMessage: message,
|
||||
processManager,
|
||||
newTitle: title,
|
||||
verbose,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
type ConnectionStringOptions = {
|
||||
user?: string
|
||||
password?: string
|
||||
host?: string
|
||||
db: string
|
||||
}
|
||||
|
||||
export function encodeDbValue(value: string): string {
|
||||
return encodeURIComponent(value)
|
||||
}
|
||||
|
||||
export default ({ user, password, host, db }: ConnectionStringOptions) => {
|
||||
let connection = `postgres://`
|
||||
if (user) {
|
||||
connection += encodeDbValue(user)
|
||||
}
|
||||
|
||||
if (password) {
|
||||
connection += `:${encodeDbValue(password)}`
|
||||
}
|
||||
|
||||
if (user || password) {
|
||||
connection += "@"
|
||||
}
|
||||
|
||||
connection += `${host}/${db}`
|
||||
|
||||
return connection
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import Configstore from "configstore"
|
||||
|
||||
let config: Configstore
|
||||
|
||||
export const getConfigStore = (): Configstore => {
|
||||
if (!config) {
|
||||
config = new Configstore(
|
||||
`medusa`,
|
||||
{},
|
||||
{
|
||||
globalConfigPath: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export const getCurrentOs = (): string => {
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
return "macos"
|
||||
case "linux":
|
||||
return "linux"
|
||||
default:
|
||||
return "windows"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import chalk from "chalk"
|
||||
import { program } from "commander"
|
||||
import { logger } from "./logger.js"
|
||||
|
||||
type LogOptions = {
|
||||
message: string
|
||||
type?: "error" | "success" | "info" | "warning" | "verbose"
|
||||
}
|
||||
|
||||
export default ({ message, type = "info" }: LogOptions) => {
|
||||
switch (type) {
|
||||
case "info":
|
||||
logger.info(chalk.white(message))
|
||||
break
|
||||
case "success":
|
||||
logger.info(chalk.green(message))
|
||||
break
|
||||
case "warning":
|
||||
logger.warning(chalk.yellow(message))
|
||||
break
|
||||
case "verbose":
|
||||
logger.info(`${chalk.bgYellowBright("VERBOSE LOG:")} ${message}`)
|
||||
break
|
||||
case "error":
|
||||
program.error(chalk.bold.red(message))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import winston from "winston"
|
||||
|
||||
const consoleTransport = new winston.transports.Console({
|
||||
format: winston.format.printf((log) => log.message),
|
||||
})
|
||||
const options = {
|
||||
transports: [consoleTransport],
|
||||
}
|
||||
|
||||
export const logger = winston.createLogger(options)
|
||||
@@ -0,0 +1,116 @@
|
||||
import inquirer from "inquirer"
|
||||
import { exec } from "child_process"
|
||||
import execute from "./execute.js"
|
||||
import { FactBoxOptions, displayFactBox } from "./facts.js"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { customAlphabet } from "nanoid"
|
||||
import { isAbortError } from "./create-abort-controller.js"
|
||||
import logMessage from "./log-message.js"
|
||||
|
||||
const NEXTJS_REPO = "https://github.com/medusajs/nextjs-starter-medusa"
|
||||
|
||||
export async function askForNextjsStarter(): Promise<boolean> {
|
||||
const { installNextjs } = await inquirer.prompt([
|
||||
{
|
||||
type: "confirm",
|
||||
name: "installNextjs",
|
||||
message: `Would you like to create the Next.js storefront? You can also create it later`,
|
||||
default: false,
|
||||
},
|
||||
])
|
||||
|
||||
return installNextjs
|
||||
}
|
||||
|
||||
type InstallOptions = {
|
||||
directoryName: string
|
||||
abortController?: AbortController
|
||||
factBoxOptions: FactBoxOptions
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export async function installNextjsStarter({
|
||||
directoryName,
|
||||
abortController,
|
||||
factBoxOptions,
|
||||
verbose = false,
|
||||
}: InstallOptions): Promise<string> {
|
||||
factBoxOptions.interval = displayFactBox({
|
||||
...factBoxOptions,
|
||||
title: "Installing Next.js Storefront...",
|
||||
})
|
||||
|
||||
let nextjsDirectory = `${directoryName}-storefront`
|
||||
|
||||
if (
|
||||
fs.existsSync(nextjsDirectory) &&
|
||||
fs.lstatSync(nextjsDirectory).isDirectory()
|
||||
) {
|
||||
// append a random number to the directory name
|
||||
nextjsDirectory += `-${customAlphabet(
|
||||
// npm throws an error if the directory name has an uppercase letter
|
||||
"123456789abcdefghijklmnopqrstuvwxyz",
|
||||
4
|
||||
)()}`
|
||||
}
|
||||
|
||||
try {
|
||||
await execute(
|
||||
[
|
||||
`npx create-next-app -e ${NEXTJS_REPO} ${nextjsDirectory}`,
|
||||
{
|
||||
signal: abortController?.signal,
|
||||
env: {
|
||||
...process.env,
|
||||
npm_config_yes: "yes",
|
||||
},
|
||||
},
|
||||
],
|
||||
{ verbose }
|
||||
)
|
||||
} catch (e) {
|
||||
if (isAbortError(e)) {
|
||||
process.exit()
|
||||
}
|
||||
|
||||
logMessage({
|
||||
message: `An error occurred while installing Next.js storefront: ${e}`,
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
|
||||
fs.renameSync(
|
||||
path.join(nextjsDirectory, ".env.template"),
|
||||
path.join(nextjsDirectory, ".env.local")
|
||||
)
|
||||
|
||||
displayFactBox({
|
||||
...factBoxOptions,
|
||||
message: `Installed Next.js Starter successfully in the ${nextjsDirectory} directory.`,
|
||||
})
|
||||
|
||||
return nextjsDirectory
|
||||
}
|
||||
|
||||
type StartOptions = {
|
||||
directory: string
|
||||
abortController?: AbortController
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export function startNextjsStarter({
|
||||
directory,
|
||||
abortController,
|
||||
verbose = false,
|
||||
}: StartOptions) {
|
||||
const childProcess = exec(`npm run dev`, {
|
||||
cwd: directory,
|
||||
signal: abortController?.signal,
|
||||
})
|
||||
|
||||
if (verbose) {
|
||||
childProcess.stdout?.pipe(process.stdout)
|
||||
childProcess.stderr?.pipe(process.stderr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import pg from "pg"
|
||||
const { Client } = pg
|
||||
|
||||
type PostgresConnection = {
|
||||
user?: string
|
||||
password?: string
|
||||
connectionString?: string
|
||||
database?: string
|
||||
}
|
||||
|
||||
export default async (connect: PostgresConnection) => {
|
||||
const client = new Client(connect)
|
||||
|
||||
await client.connect()
|
||||
|
||||
return client
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
import chalk from "chalk"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { Ora } from "ora"
|
||||
import execute from "./execute.js"
|
||||
import { EOL } from "os"
|
||||
import { displayFactBox, FactBoxOptions } from "./facts.js"
|
||||
import ProcessManager from "./process-manager.js"
|
||||
import { clearProject } from "./clear-project.js"
|
||||
import type { Client } from "pg"
|
||||
|
||||
type PrepareOptions = {
|
||||
directory: string
|
||||
dbConnectionString: string
|
||||
admin?: {
|
||||
email: string
|
||||
}
|
||||
seed?: boolean
|
||||
boilerplate?: boolean
|
||||
spinner: Ora
|
||||
processManager: ProcessManager
|
||||
abortController?: AbortController
|
||||
skipDb?: boolean
|
||||
migrations?: boolean
|
||||
onboardingType?: "default" | "nextjs"
|
||||
nextjsDirectory?: string
|
||||
client: Client | null
|
||||
verbose?: boolean
|
||||
v2?: boolean
|
||||
}
|
||||
|
||||
export default async ({
|
||||
directory,
|
||||
dbConnectionString,
|
||||
admin,
|
||||
seed,
|
||||
boilerplate,
|
||||
spinner,
|
||||
processManager,
|
||||
abortController,
|
||||
skipDb,
|
||||
migrations,
|
||||
onboardingType = "default",
|
||||
nextjsDirectory = "",
|
||||
client,
|
||||
verbose = false,
|
||||
v2 = false,
|
||||
}: PrepareOptions) => {
|
||||
// initialize execution options
|
||||
const execOptions = {
|
||||
cwd: directory,
|
||||
signal: abortController?.signal,
|
||||
}
|
||||
|
||||
const npxOptions = {
|
||||
...execOptions,
|
||||
env: {
|
||||
...process.env,
|
||||
npm_config_yes: "yes",
|
||||
},
|
||||
}
|
||||
|
||||
const factBoxOptions: FactBoxOptions = {
|
||||
interval: null,
|
||||
spinner,
|
||||
processManager,
|
||||
message: "",
|
||||
title: "",
|
||||
verbose,
|
||||
}
|
||||
|
||||
// initialize the invite token to return
|
||||
let inviteToken: string | undefined = undefined
|
||||
|
||||
if (!skipDb) {
|
||||
let env = `DATABASE_TYPE=postgres${EOL}DATABASE_URL=${dbConnectionString}${EOL}MEDUSA_ADMIN_ONBOARDING_TYPE=${onboardingType}${EOL}STORE_CORS=http://localhost:8000,http://localhost:7001`
|
||||
if (v2) {
|
||||
env += `${EOL}POSTGRES_URL=${dbConnectionString}`
|
||||
}
|
||||
if (nextjsDirectory) {
|
||||
env += `${EOL}MEDUSA_ADMIN_ONBOARDING_NEXTJS_DIRECTORY=${nextjsDirectory}`
|
||||
}
|
||||
// add connection string to project
|
||||
fs.appendFileSync(path.join(directory, `.env`), env)
|
||||
}
|
||||
|
||||
factBoxOptions.interval = displayFactBox({
|
||||
...factBoxOptions,
|
||||
spinner,
|
||||
title: "Installing dependencies...",
|
||||
processManager,
|
||||
})
|
||||
|
||||
await processManager.runProcess({
|
||||
process: async () => {
|
||||
try {
|
||||
await execute([`yarn`, execOptions], { verbose })
|
||||
} catch (e) {
|
||||
// yarn isn't available
|
||||
// use npm
|
||||
await execute([`npm install --legacy-peer-deps`, execOptions], {
|
||||
verbose,
|
||||
})
|
||||
}
|
||||
},
|
||||
ignoreERESOLVE: true,
|
||||
})
|
||||
|
||||
factBoxOptions.interval = displayFactBox({
|
||||
...factBoxOptions,
|
||||
message: "Installed Dependencies",
|
||||
})
|
||||
|
||||
if (!boilerplate) {
|
||||
factBoxOptions.interval = displayFactBox({
|
||||
...factBoxOptions,
|
||||
title: "Preparing Project Directory...",
|
||||
})
|
||||
// delete files and directories related to onboarding
|
||||
clearProject(directory)
|
||||
displayFactBox({
|
||||
...factBoxOptions,
|
||||
message: "Prepared Project Directory",
|
||||
})
|
||||
}
|
||||
|
||||
factBoxOptions.interval = displayFactBox({
|
||||
...factBoxOptions,
|
||||
title: "Building Project...",
|
||||
})
|
||||
|
||||
await processManager.runProcess({
|
||||
process: async () => {
|
||||
try {
|
||||
await execute([`yarn build`, execOptions], { verbose })
|
||||
} catch (e) {
|
||||
// yarn isn't available
|
||||
// use npm
|
||||
await execute([`npm run build`, execOptions], { verbose })
|
||||
}
|
||||
},
|
||||
ignoreERESOLVE: true,
|
||||
})
|
||||
|
||||
displayFactBox({ ...factBoxOptions, message: "Project Built" })
|
||||
|
||||
if (!skipDb && migrations) {
|
||||
factBoxOptions.interval = displayFactBox({
|
||||
...factBoxOptions,
|
||||
title: "Running Migrations...",
|
||||
})
|
||||
|
||||
// run migrations
|
||||
await processManager.runProcess({
|
||||
process: async () => {
|
||||
const proc = await execute(
|
||||
["npx @medusajs/medusa-cli@latest migrations run", npxOptions],
|
||||
{ verbose, needOutput: true }
|
||||
)
|
||||
|
||||
if (client) {
|
||||
// check the migrations table is in the database
|
||||
// to ensure that migrations ran
|
||||
let errorOccurred = false
|
||||
try {
|
||||
const migrations = await client.query(
|
||||
`SELECT * FROM "${v2 ? "mikro_orm_migrations" : "migrations"}"`
|
||||
)
|
||||
errorOccurred = migrations.rowCount == 0
|
||||
} catch (e) {
|
||||
// avoid error thrown if the migrations table
|
||||
// doesn't exist
|
||||
errorOccurred = true
|
||||
}
|
||||
|
||||
// ensure that migrations actually ran in case of an uncaught error
|
||||
if (errorOccurred && (proc.stderr || proc.stdout)) {
|
||||
throw new Error(
|
||||
`An error occurred while running migrations: ${
|
||||
proc.stderr || proc.stdout
|
||||
}`
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
factBoxOptions.interval = displayFactBox({
|
||||
...factBoxOptions,
|
||||
message: "Ran Migrations",
|
||||
})
|
||||
}
|
||||
|
||||
if (admin && !skipDb && migrations && !v2) {
|
||||
// create admin user
|
||||
factBoxOptions.interval = displayFactBox({
|
||||
...factBoxOptions,
|
||||
title: "Creating an admin user...",
|
||||
})
|
||||
|
||||
await processManager.runProcess({
|
||||
process: async () => {
|
||||
const proc = await execute(
|
||||
[
|
||||
`npx @medusajs/medusa-cli@latest user -e ${admin.email} --invite`,
|
||||
npxOptions,
|
||||
],
|
||||
{ verbose, needOutput: true }
|
||||
)
|
||||
|
||||
// get invite token from stdout
|
||||
const match = (proc.stdout as string).match(
|
||||
/Invite token: (?<token>.+)/
|
||||
)
|
||||
inviteToken = match?.groups?.token
|
||||
},
|
||||
})
|
||||
|
||||
factBoxOptions.interval = displayFactBox({
|
||||
...factBoxOptions,
|
||||
message: "Created admin user",
|
||||
})
|
||||
}
|
||||
|
||||
if (!skipDb && migrations) {
|
||||
if (seed || !boilerplate) {
|
||||
factBoxOptions.interval = displayFactBox({
|
||||
...factBoxOptions,
|
||||
title: "Seeding database...",
|
||||
})
|
||||
|
||||
// check if a seed file exists in the project
|
||||
if (!fs.existsSync(path.join(directory, "data", "seed.json"))) {
|
||||
spinner
|
||||
?.warn(
|
||||
chalk.yellow(
|
||||
"Seed file was not found in the project. Skipping seeding..."
|
||||
)
|
||||
)
|
||||
.start()
|
||||
return inviteToken
|
||||
}
|
||||
|
||||
await processManager.runProcess({
|
||||
process: async () => {
|
||||
await execute(
|
||||
[
|
||||
`npx @medusajs/medusa-cli@latest seed --seed-file=${path.join(
|
||||
"data",
|
||||
"seed.json"
|
||||
)}`,
|
||||
npxOptions,
|
||||
],
|
||||
{ verbose }
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
displayFactBox({
|
||||
...factBoxOptions,
|
||||
message: "Seeded database with demo data",
|
||||
})
|
||||
} else if (
|
||||
fs.existsSync(path.join(directory, "data", "seed-onboarding.json"))
|
||||
) {
|
||||
// seed the database with onboarding seed
|
||||
factBoxOptions.interval = displayFactBox({
|
||||
...factBoxOptions,
|
||||
title: "Finish preparation...",
|
||||
})
|
||||
|
||||
await processManager.runProcess({
|
||||
process: async () => {
|
||||
await execute(
|
||||
[
|
||||
`npx @medusajs/medusa-cli@latest seed --seed-file=${path.join(
|
||||
"data",
|
||||
"seed-onboarding.json"
|
||||
)}`,
|
||||
npxOptions,
|
||||
],
|
||||
{ verbose }
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
displayFactBox({ ...factBoxOptions, message: "Finished Preparation" })
|
||||
}
|
||||
|
||||
return inviteToken
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
type ProcessOptions = {
|
||||
process: Function
|
||||
ignoreERESOLVE?: boolean
|
||||
}
|
||||
|
||||
export default class ProcessManager {
|
||||
intervals: NodeJS.Timeout[] = []
|
||||
static MAX_RETRIES = 3
|
||||
|
||||
constructor() {
|
||||
this.onTerminated(() => {
|
||||
this.intervals.forEach((interval) => {
|
||||
clearInterval(interval)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onTerminated(fn: () => Promise<void> | void) {
|
||||
process.on("SIGTERM", () => fn())
|
||||
process.on("SIGINT", () => fn())
|
||||
}
|
||||
|
||||
addInterval(interval: NodeJS.Timeout) {
|
||||
this.intervals.push(interval)
|
||||
}
|
||||
|
||||
// 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
|
||||
async runProcess({ process, ignoreERESOLVE }: ProcessOptions) {
|
||||
let processError = false
|
||||
let retries = 0
|
||||
do {
|
||||
++retries
|
||||
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 && retries <= ProcessManager.MAX_RETRIES)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { exec } from "child_process"
|
||||
|
||||
type StartOptions = {
|
||||
directory: string
|
||||
abortController?: AbortController
|
||||
}
|
||||
|
||||
export default ({ directory, abortController }: StartOptions) => {
|
||||
const childProcess = exec(`npx @medusajs/medusa-cli@latest develop`, {
|
||||
cwd: directory,
|
||||
signal: abortController?.signal,
|
||||
env: {
|
||||
...process.env,
|
||||
OPEN_BROWSER: "false",
|
||||
npm_config_yes: "yes",
|
||||
},
|
||||
})
|
||||
|
||||
childProcess.stdout?.pipe(process.stdout)
|
||||
childProcess.stderr?.pipe(process.stderr)
|
||||
}
|
||||
Reference in New Issue
Block a user