chore(): start moving some packages to the core directory (#7215)

This commit is contained in:
Adrien de Peretti
2024-05-03 13:37:41 +02:00
committed by GitHub
parent fdee748eed
commit bbccd6481d
1436 changed files with 275 additions and 756 deletions

View File

@@ -0,0 +1,657 @@
/*
* Adapted from https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-cli/src/init-starter.ts
*/
import { execSync } from "child_process"
import execa from "execa"
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 prompts from "prompts"
import { Pool } from "pg"
import url from "url"
import { createDatabase } from "pg-god"
import { track } from "medusa-telemetry"
import inquirer from "inquirer"
import reporter from "../reporter"
import { getPackageManager, setPackageManager } from "../util/package-manager"
import { PanicId } from "../reporter/panic-handler"
import { clearProject } from "../util/clear-project"
import path from "path"
const removeUndefined = (obj) => {
return Object.fromEntries(
Object.entries(obj)
.filter(([_, v]) => v != null)
.map(([k, v]) => [k, v === Object(v) ? removeUndefined(v) : v])
)
}
const spawnWithArgs = (file, args, options) =>
execa(file, args, { stdio: `inherit`, 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
}
const gignore = reporter.activity(
`Creating minimal .gitignore in ${rootPath}`
)
await fs.writeFile(
sysPath.join(rootPath, `.gitignore`),
`.cache\nnode_modules\npublic\n`
)
reporter.success(gignore, `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) => {
const prevDir = process.cwd()
reporter.info(`Installing packages...`)
console.log() // Add some space
process.chdir(rootPath)
const npmConfigUserAgent = process.env.npm_config_user_agent
try {
if (!getPackageManager()) {
if (npmConfigUserAgent?.includes(`yarn`)) {
setPackageManager(`yarn`)
} else {
setPackageManager(`npm`)
}
}
if (getPackageManager() === `yarn` && checkForYarn()) {
await fs.remove(`package-lock.json`)
await spawn(`yarnpkg`, {})
} else {
await fs.remove(`yarn.lock`)
await spawn(`npm install`, {})
}
} finally {
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"`
)
}
reporter.info(`Creating new site from local starter: ${starterPath}`)
const copyActivity = reporter.activity(
`Copying local starter to ${rootPath} ...`
)
await fs.copy(starterPath, rootPath, { filter: ignored })
reporter.success(copyActivity, `Created starter directory layout`)
console.log() // Add some space
await install(rootPath)
return true
}
// Clones starter from URI.
const clone = async (hostInfo, rootPath, v2 = 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 = v2 ? [`-b`, "feat/v2"] :
hostInfo.committish ? [`-b`, hostInfo.committish] : []
const createAct = reporter.activity(`Creating new project from git: ${url}`)
const args = [
`clone`,
...branch,
url,
rootPath,
`--recursive`,
`--depth=1`,
].filter((arg) => Boolean(arg))
await execa(`git`, args, {})
.then(() => {
reporter.success(createAct, `Created starter directory layout`)
})
.catch((err) => {
reporter.failure(createAct, `Failed to clone repository`)
throw err
})
await fs.remove(sysPath.join(rootPath, `.git`))
await install(rootPath)
const isGit = await isAlreadyGitRepository()
if (!isGit) await gitInit(rootPath)
await maybeCreateGitIgnore(rootPath)
if (!isGit) await createInitialGitCommit(rootPath, url)
}
const getMedusaConfig = (rootPath) => {
try {
const configPath = sysPath.join(rootPath, "medusa-config.js")
if (existsSync(configPath)) {
const resolved = sysPath.resolve(configPath)
const configModule = require(resolved)
return configModule
}
throw Error()
} catch (err) {
console.log(err)
reporter.warn(
`Couldn't find a medusa-config.js file; please double check that you have the correct starter installed`
)
}
return {}
}
const getPaths = async (starterPath, rootPath, v2 = false) => {
let selectedOtherStarter = false
// if no args are passed, prompt user for path and starter
if (!starterPath && !rootPath) {
const response = await prompts.prompt([
{
type: `text`,
name: `path`,
message: `What is your project called?`,
initial: `my-medusa-store`,
},
!v2 && {
type: `select`,
name: `starter`,
message: `What starter would you like to use?`,
choices: [
{ title: `medusa-starter-default`, value: `medusa-starter-default` },
{ title: `(Use a different starter)`, value: `different` },
],
initial: 0,
},
])
// exit gracefully if responses aren't provided
if ((!v2 && !response.starter) || !response.path.trim()) {
throw new Error(
`Please mention both starter package and project name along with path(if its not in the root)`
)
}
selectedOtherStarter = response.starter === `different`
starterPath = `medusajs/${v2 ? "medusa-starter-default" : response.starter}`
rootPath = response.path
}
// 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 successMessage = (path) => {
reporter.info(`Your new Medusa project is ready for you! To start developing run:
cd ${path}
medusa develop
`)
}
const defaultDBCreds = {
user: process.env.USER || "postgres",
database: "postgres",
password: "",
port: 5432,
host: "localhost",
}
const verifyPgCreds = async (creds) => {
const pool = new Pool(creds)
return new Promise((resolve, reject) => {
pool.query("SELECT NOW()", (err, res) => {
pool.end()
if (err) {
reject(err)
} else {
resolve(res)
}
})
})
}
const interactiveDbCreds = async (dbName, dbCreds = {}) => {
const credentials = Object.assign({}, defaultDBCreds, dbCreds)
const collecting = true
while (collecting) {
const result = await inquirer
.prompt([
{
type: "list",
name: "continueWithDefault",
message: `
Will attempt to setup Postgres database "${dbName}" with credentials:
user: ${credentials.user}
password: ***
port: ${credentials.port}
host: ${credentials.host}
Do you wish to continue with these credentials?
`,
choices: [`Continue`, `Change credentials`, `Skip database setup`],
},
{
type: "input",
when: ({ continueWithDefault }) =>
continueWithDefault === `Change credentials`,
name: "user",
default: credentials.user,
message: `DB user`,
},
{
type: "password",
when: ({ continueWithDefault }) =>
continueWithDefault === `Change credentials`,
name: "password",
default: credentials.password,
message: `DB password`,
},
{
type: "number",
when: ({ continueWithDefault }) =>
continueWithDefault === `Change credentials`,
name: "port",
default: credentials.port,
message: `DB port`,
},
{
type: "input",
when: ({ continueWithDefault }) =>
continueWithDefault === `Change credentials`,
name: "host",
default: credentials.host,
message: `DB host`,
},
])
.then(async (answers) => {
const collectedCreds = Object.assign({}, credentials, {
user: answers.user,
password: answers.password,
host: answers.host,
port: answers.port,
})
switch (answers.continueWithDefault) {
case "Continue": {
const done = await verifyPgCreds(credentials).catch((_) => false)
if (done) {
return credentials
}
return false
}
case "Change credentials": {
const done = await verifyPgCreds(collectedCreds).catch((_) => false)
if (done) {
return collectedCreds
}
return false
}
default:
return null
}
})
if (result !== false) {
return result
}
console.log("\n\nCould not verify DB credentials - please try again\n\n")
}
return
}
const setupDB = async (dbName, dbCreds = {}) => {
const credentials = Object.assign({}, defaultDBCreds, dbCreds)
const dbActivity = reporter.activity(`Setting up database "${dbName}"...`)
await createDatabase(
{
databaseName: dbName,
errorIfExist: true,
},
credentials
)
.then(() => {
reporter.success(dbActivity, `Created database "${dbName}"`)
})
.catch((err) => {
if (err.name === "PDG_ERR::DuplicateDatabase") {
reporter.success(
dbActivity,
`Database ${dbName} already exists; skipping setup`
)
} else {
reporter.failure(dbActivity, `Skipping database setup.`)
reporter.warn(
`Failed to setup database; install PostgresQL or make sure to manage your database connection manually`
)
console.error(err)
}
})
}
const setupEnvVars = async (rootPath, dbName, dbCreds = {}) => {
const templatePath = sysPath.join(rootPath, ".env.template")
const destination = sysPath.join(rootPath, ".env")
if (existsSync(templatePath)) {
fs.renameSync(templatePath, destination)
}
const credentials = Object.assign({}, defaultDBCreds, dbCreds)
let dbUrl = ""
if (
credentials.user !== defaultDBCreds.user ||
credentials.password !== defaultDBCreds.password
) {
dbUrl = `postgres://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}/${dbName}`
} else {
dbUrl = `postgres://${credentials.host}:${credentials.port}/${dbName}`
}
fs.appendFileSync(destination, `DATABASE_URL=${dbUrl}\n`)
}
const runMigrations = async (rootPath) => {
const migrationActivity = reporter.activity("Applying database migrations...")
const cliPath = sysPath.join(
`node_modules`,
`@medusajs`,
`medusa-cli`,
`cli.js`
)
return await execa(cliPath, [`migrations`, `run`], {
cwd: rootPath,
})
.then(() => {
reporter.success(migrationActivity, "Database migrations completed.")
})
.catch((err) => {
reporter.failure(
migrationActivity,
"Failed to migrate database you must complete migration manually before starting your server."
)
console.error(err)
})
}
const attemptSeed = async (rootPath) => {
const seedActivity = reporter.activity("Seeding database")
const pkgPath = sysPath.resolve(rootPath, "package.json")
if (existsSync(pkgPath)) {
const pkg = require(pkgPath)
if (pkg.scripts && pkg.scripts.seed) {
const proc = execa(getPackageManager(), [`run`, `seed`], {
cwd: rootPath,
})
// Useful for development
// proc.stdout.pipe(process.stdout)
await proc
.then(() => {
reporter.success(seedActivity, "Seed completed")
})
.catch((err) => {
reporter.failure(seedActivity, "Failed to complete seed; skipping")
console.error(err)
})
} else {
reporter.failure(
seedActivity,
"Starter doesn't provide a seed command; skipping."
)
}
} else {
reporter.failure(seedActivity, "Could not find package.json")
}
}
/**
* Main function that clones or copies the starter.
*/
export const newStarter = async (args) => {
track("CLI_NEW")
const {
starter,
root,
skipDb,
skipMigrations,
skipEnv,
seed,
useDefaults,
dbUser,
dbDatabase,
dbPass,
dbPort,
dbHost,
v2
} = args
const dbCredentials = removeUndefined({
user: dbUser,
database: dbDatabase,
password: dbPass,
port: dbPort,
host: dbHost,
})
const { starterPath, rootPath, selectedOtherStarter } = await getPaths(
starter,
root,
v2
)
const urlObject = url.parse(rootPath)
if (selectedOtherStarter) {
reporter.info(
`Find the url of the Medusa starter you wish to create and run:
medusa new ${rootPath} [url-to-starter]
`
)
return
}
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: PanicId.InvalidProjectName,
context: {
starter,
rootPath,
},
})
return
}
reporter.panic({
id: PanicId.InvalidProjectName,
context: {
rootPath,
},
})
return
}
if (!isValid(rootPath)) {
reporter.panic({
id: PanicId.InvalidPath,
context: {
path: sysPath.resolve(rootPath),
},
})
return
}
if (existsSync(sysPath.join(rootPath, `package.json`))) {
reporter.panic({
id: PanicId.AlreadyNodeProject,
context: {
rootPath,
},
})
return
}
const hostedInfo = hostedGitInfo.fromUrl(starterPath)
if (hostedInfo) {
await clone(hostedInfo, rootPath, v2)
} else {
await copy(starterPath, rootPath)
}
track("CLI_NEW_LAYOUT_COMPLETED")
let creds = dbCredentials
const dbName = `medusa-db-${Math.random().toString(36).substring(2, 7)}` // generate random 5 character string
if (!useDefaults && !skipDb && !skipEnv) {
creds = await interactiveDbCreds(dbName, dbCredentials)
}
if (creds === null) {
reporter.info(
"Skipping automatic database setup. Please note that you need to create a database and run migrations before you can run your Medusa backend"
)
} else {
if (!skipDb) {
track("CLI_NEW_SETUP_DB")
await setupDB(dbName, creds)
}
if (!skipEnv) {
track("CLI_NEW_SETUP_ENV")
await setupEnvVars(rootPath, dbName, creds)
}
if (!skipMigrations) {
track("CLI_NEW_RUN_MIGRATIONS")
await runMigrations(rootPath)
}
if (seed) {
track("CLI_NEW_SEED_DB")
await attemptSeed(rootPath)
}
}
if (!selectedOtherStarter) {
reporter.info("Final project preparations...")
// remove demo files
clearProject(rootPath)
// remove .git directory
fs.rmSync(path.join(rootPath, '.git'), {
recursive: true,
force: true,
})
}
successMessage(rootPath)
track("CLI_NEW_SUCCEEDED")
}

View File

@@ -0,0 +1,415 @@
import { sync as existsSync } from "fs-exists-cached"
import { setTelemetryEnabled } from "medusa-telemetry"
import path from "path"
import resolveCwd from "resolve-cwd"
import { didYouMean } from "./did-you-mean"
import { getLocalMedusaVersion } from "./util/version"
import { newStarter } from "./commands/new"
import reporter from "./reporter"
const yargs = require(`yargs`)
const handlerP =
(fn) =>
(...args) => {
Promise.resolve(fn(...args)).then(
() => process.exit(0),
(err) => console.log(err)
)
}
function buildLocalCommands(cli, isLocalProject) {
const defaultHost = `localhost`
const defaultPort = `9000`
const directory = path.resolve(`.`)
const projectInfo = { directory }
const useYarn = existsSync(path.join(directory, `yarn.lock`))
if (isLocalProject) {
projectInfo["sitePackageJson"] = require(path.join(
directory,
`package.json`
))
}
function resolveLocalCommand(command) {
if (!isLocalProject) {
cli.showHelp()
}
try {
const cmdPath = resolveCwd.silent(
`@medusajs/medusa/dist/commands/${command}`
)!
return require(cmdPath).default
} catch (err) {
if (!process.env.NODE_ENV?.startsWith("prod")) {
console.log("--------------- ERROR ---------------------")
console.log(err)
console.log("-------------------------------------------")
}
cli.showHelp()
}
}
function getCommandHandler(command, handler) {
return (argv) => {
const localCmd = resolveLocalCommand(command)
const args = { ...argv, ...projectInfo, useYarn }
return handler ? handler(args, localCmd) : localCmd(args)
}
}
cli
.command({
command: `new [root] [starter]`,
builder: (_) =>
_.option(`seed`, {
type: `boolean`,
describe: `If flag is set the command will attempt to seed the database after setup.`,
default: false,
})
.option(`y`, {
type: `boolean`,
alias: "useDefaults",
describe: `If flag is set the command will not interactively collect database credentials`,
default: false,
})
.option(`skip-db`, {
type: `boolean`,
describe: `If flag is set the command will not attempt to complete database setup`,
default: false,
})
.option(`skip-migrations`, {
type: `boolean`,
describe: `If flag is set the command will not attempt to complete database migration`,
default: false,
})
.option(`skip-env`, {
type: `boolean`,
describe: `If flag is set the command will not attempt to populate .env`,
default: false,
})
.option(`db-user`, {
type: `string`,
describe: `The database user to use for database setup and migrations.`,
})
.option(`db-database`, {
type: `string`,
describe: `The database use for database setup and migrations.`,
})
.option(`db-pass`, {
type: `string`,
describe: `The database password to use for database setup and migrations.`,
})
.option(`db-port`, {
type: `number`,
describe: `The database port to use for database setup and migrations.`,
})
.option(`db-host`, {
type: `string`,
describe: `The database host to use for database setup and migrations.`,
})
.option(`v2`, {
type: `boolean`,
describe: `Install Medusa with the V2 feature flag enabled. WARNING: Medusa V2 is still in development and shouldn't be used in production.`,
default: false
}),
desc: `Create a new Medusa project.`,
handler: handlerP(newStarter),
})
.command({
command: `telemetry`,
describe: `Enable or disable collection of anonymous usage data.`,
builder: (yargs) =>
yargs
.option(`enable`, {
type: `boolean`,
description: `Enable telemetry (default)`,
})
.option(`disable`, {
type: `boolean`,
description: `Disable telemetry`,
}),
handler: handlerP(({ enable, disable }) => {
const enabled = Boolean(enable) || !disable
setTelemetryEnabled(enabled)
reporter.info(
`Telemetry collection ${enabled ? `enabled` : `disabled`}`
)
}),
})
.command({
command: `seed`,
desc: `Migrates and populates the database with the provided file.`,
builder: (_) =>
_.option(`f`, {
alias: `seed-file`,
type: `string`,
describe: `Path to the file where the seed is defined.`,
required: true,
}).option(`m`, {
alias: `migrate`,
type: `boolean`,
default: true,
describe: `Flag to indicate if migrations should be run prior to seeding the database`,
}),
handler: handlerP(
getCommandHandler(`seed`, (args, cmd) => {
process.env.NODE_ENV ??= `development`
return cmd(args)
})
),
})
.command({
command: `migrations [action]`,
desc: `Manage migrations from the core and your own project`,
builder: {
action: {
demand: true,
choices: ["run", "revert", "show"],
},
},
handler: handlerP(
getCommandHandler(`migrate`, (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
return cmd(args)
})
),
})
.command({
command: `develop`,
desc: `Start development server. Watches file and rebuilds when something changes`,
builder: (_) =>
_.option(`H`, {
alias: `host`,
type: `string`,
default: defaultHost,
describe: `Set host. Defaults to ${defaultHost}`,
}).option(`p`, {
alias: `port`,
type: `string`,
default: process.env.PORT || defaultPort,
describe: process.env.PORT
? `Set port. Defaults to ${process.env.PORT} (set by env.PORT) (otherwise defaults ${defaultPort})`
: `Set port. Defaults to ${defaultPort}`,
}),
handler: handlerP(
getCommandHandler(`develop`, (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
// Return an empty promise to prevent handlerP from exiting early.
// The development server shouldn't ever exit until the user directly
// kills it so this is fine.
return new Promise((resolve) => {})
})
),
})
.command({
command: `start`,
desc: `Start development server.`,
builder: (_) =>
_.option(`H`, {
alias: `host`,
type: `string`,
default: defaultHost,
describe: `Set host. Defaults to ${defaultHost}`,
}).option(`p`, {
alias: `port`,
type: `string`,
default: process.env.PORT || defaultPort,
describe: process.env.PORT
? `Set port. Defaults to ${process.env.PORT} (set by env.PORT) (otherwise defaults ${defaultPort})`
: `Set port. Defaults to ${defaultPort}`,
}),
handler: handlerP(
getCommandHandler(`start`, (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
// Return an empty promise to prevent handlerP from exiting early.
// The development server shouldn't ever exit until the user directly
// kills it so this is fine.
return new Promise((resolve) => {})
})
),
})
.command({
command: `start-cluster`,
desc: `Start development server in cluster mode (beta).`,
builder: (_) =>
_.option(`H`, {
alias: `host`,
type: `string`,
default: defaultHost,
describe: `Set host. Defaults to ${defaultHost}`,
})
.option(`p`, {
alias: `port`,
type: `string`,
default: process.env.PORT || defaultPort,
describe: process.env.PORT
? `Set port. Defaults to ${process.env.PORT} (set by env.PORT) (otherwise defaults ${defaultPort})`
: `Set port. Defaults to ${defaultPort}`,
})
.option(`c`, {
alias: `cpus`,
type: `number`,
default: process.env.CPUS,
describe:
"Set number of cpus to use. Defaults to max number of cpus available on the system (set by env.CPUS)",
}),
handler: handlerP(
getCommandHandler(`start-cluster`, (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
// Return an empty promise to prevent handlerP from exiting early.
// The development server shouldn't ever exit until the user directly
// kills it so this is fine.
return new Promise((resolve) => {})
})
),
})
.command({
command: `user`,
desc: `Create a user`,
builder: (_) =>
_.option(`e`, {
alias: `email`,
type: `string`,
describe: `User's email.`,
})
.option(`p`, {
alias: `password`,
type: `string`,
describe: `User's password.`,
})
.option(`i`, {
alias: `id`,
type: `string`,
describe: `User's id.`,
})
.option(`invite`, {
type: `boolean`,
describe: `If flag is set, an invitation will be created instead of a new user and the invite token will be returned.`,
default: false,
}),
handler: handlerP(
getCommandHandler(`user`, (args, cmd) => {
cmd(args)
// Return an empty promise to prevent handlerP from exiting early.
// The development server shouldn't ever exit until the user directly
// kills it so this is fine.
return new Promise((resolve) => {})
})
),
})
}
function isLocalMedusaProject() {
let inMedusaProject = false
try {
const { dependencies, devDependencies } = require(path.resolve(
`./package.json`
))
inMedusaProject = !!(
(dependencies && dependencies["@medusajs/medusa"]) ||
(devDependencies && devDependencies["@medusajs/medusa"])
)
} catch (err) {
// ignore
}
return inMedusaProject
}
function getVersionInfo() {
const { version } = require(`../package.json`)
const isMedusaProject = isLocalMedusaProject()
if (isMedusaProject) {
let medusaVersion = getLocalMedusaVersion()
if (!medusaVersion) {
medusaVersion = `unknown`
}
return `Medusa CLI version: ${version}
Medusa version: ${medusaVersion}
Note: this is the Medusa version for the site at: ${process.cwd()}`
} else {
return `Medusa CLI version: ${version}`
}
}
export default (argv) => {
const cli = yargs()
const isLocalProject = isLocalMedusaProject()
cli
.scriptName(`medusa`)
.usage(`Usage: $0 <command> [options]`)
.alias(`h`, `help`)
.alias(`v`, `version`)
.option(`verbose`, {
default: false,
type: `boolean`,
describe: `Turn on verbose output`,
global: true,
})
.option(`no-color`, {
alias: `no-colors`,
default: false,
type: `boolean`,
describe: `Turn off the color in output`,
global: true,
})
.option(`json`, {
describe: `Turn on the JSON logger`,
default: false,
type: `boolean`,
global: true,
})
buildLocalCommands(cli, isLocalProject)
try {
cli.version(
`version`,
`Show the version of the Medusa CLI and the Medusa package in the current project`,
getVersionInfo()
)
} catch (e) {
// ignore
}
return cli
.wrap(cli.terminalWidth())
.demandCommand(1, `Pass --help to see all available commands and options.`)
.strict()
.fail((msg, err, yargs) => {
const availableCommands = yargs
.getCommands()
.map((commandDescription) => {
const [command] = commandDescription
return command.split(` `)[0]
})
const arg = argv.slice(2)[0]
const suggestion = arg ? didYouMean(arg, availableCommands) : ``
if (!process.env.NODE_ENV?.startsWith("prod")) {
console.log("--------------- ERROR ---------------------")
console.log(err)
console.log("-------------------------------------------")
}
cli.showHelp()
reporter.info(suggestion)
reporter.info(msg)
})
.parse(argv.slice(2))
}

View File

@@ -0,0 +1,18 @@
import meant from "meant"
export function didYouMean(scmd, commands): string {
const bestSimilarity = meant(scmd, commands).map((str) => {
return ` ${str}`
})
if (bestSimilarity.length === 0) return ``
if (bestSimilarity.length === 1) {
return `\nDid you mean this?\n ${bestSimilarity[0]}\n`
} else {
return (
[`\nDid you mean one of these?`]
.concat(bestSimilarity.slice(0, 3))
.join(`\n`) + `\n`
)
}
}

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env node
import "core-js/stable"
import "regenerator-runtime/runtime"
import os from "os"
import util from "util"
import createCli from "./create-cli"
const useJsonLogger = process.argv.slice(2).some((arg) => arg.includes(`json`))
if (useJsonLogger) {
process.env.GATSBY_LOGGER = `json`
}
// Ensure stable runs on Windows when started from different shells (i.e. c:\dir vs C:\dir)
if (os.platform() === `win32`) {
// ensureWindowsDriveLetterIsUppercase()
}
// Check if update is available
// updateNotifier({ pkg }).notify({ isGlobal: true })
const MIN_NODE_VERSION = `10.13.0`
process.on(`unhandledRejection`, (reason) => {
// This will exit the process in newer Node anyway so lets be consistent
// across versions and crash
// reason can be anything, it can be a message, an object, ANYTHING!
// we convert it to an error object, so we don't crash on structured error validation
if (!(reason instanceof Error)) {
reason = new Error(util.format(reason))
}
console.log(reason)
// report.panic(`UNHANDLED REJECTION`, reason as Error)
})
process.on(`uncaughtException`, (error) => {
console.log(error)
// report.panic(`UNHANDLED EXCEPTION`, error)
})
createCli(process.argv)

View File

@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Reporter handles "Error" signature correctly 1`] = `
Object {
"level": "error",
"message": "Error",
"stack": Any<Array>,
}
`;
exports[`Reporter handles "String" signature correctly 1`] = `
Object {
"level": "error",
"message": "Test log",
}
`;
exports[`Reporter handles "String, Error" signature correctly 1`] = `
Object {
"level": "error",
"message": "Test log",
"stack": Any<Array>,
}
`;

View File

@@ -0,0 +1,49 @@
import logger, { Reporter } from "../"
describe(`Reporter`, () => {
const winstonMock = {
log: jest.fn(),
}
const reporter = new Reporter({
logger: winstonMock,
activityLogger: {},
})
const getErrorMessages = fn =>
fn.mock.calls
.map(([firstArg]) => firstArg)
.filter(structuredMessage => structuredMessage.level === `error`)
beforeEach(() => {
winstonMock.log.mockClear()
})
it(`handles "String" signature correctly`, () => {
reporter.error("Test log")
const generated = getErrorMessages(winstonMock.log)[0]
expect(generated).toMatchSnapshot()
})
it(`handles "String, Error" signature correctly`, () => {
reporter.error("Test log", new Error("String Error"))
const generated = getErrorMessages(winstonMock.log)[0]
expect(generated).toMatchSnapshot({
stack: expect.any(Array),
})
})
it(`handles "Error" signature correctly`, () => {
reporter.error(new Error("Error"))
const generated = getErrorMessages(winstonMock.log)[0]
expect(generated).toMatchSnapshot({
stack: expect.any(Array),
})
})
})

View File

@@ -0,0 +1,332 @@
import stackTrace from "stack-trace"
import { ulid } from "ulid"
import winston from "winston"
import ora from "ora"
import { track } from "medusa-telemetry"
import { panicHandler } from "./panic-handler"
import * as Transport from "winston-transport"
const LOG_LEVEL = process.env.LOG_LEVEL || "silly"
const LOG_FILE = process.env.LOG_FILE || ""
const NODE_ENV = process.env.NODE_ENV || "development"
const IS_DEV = NODE_ENV.startsWith("dev")
let transports: Transport[] = []
if (!IS_DEV) {
transports.push(new winston.transports.Console())
} else {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.cli(),
winston.format.splat()
),
})
)
}
if (LOG_FILE) {
transports.push(
new winston.transports.File({
filename: LOG_FILE
})
)
}
const loggerInstance = winston.createLogger({
level: LOG_LEVEL,
levels: winston.config.npm.levels,
format: winston.format.combine(
winston.format.timestamp({
format: "YYYY-MM-DD HH:mm:ss",
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
transports,
})
export class Reporter {
protected activities_: Record<string, any>
protected loggerInstance_: winston.Logger
protected ora_: typeof ora
constructor({ logger, activityLogger }) {
this.activities_ = {}
this.loggerInstance_ = logger
this.ora_ = activityLogger
}
panic = (data) => {
const parsedPanic = panicHandler(data)
this.loggerInstance_.log({
level: "error",
details: data,
message: parsedPanic.message,
})
track("PANIC_ERROR_REACHED", {
id: data.id,
})
process.exit(1)
}
/**
* Determines if the logger should log at a given level.
* @param {string} level - the level to check if logger is configured for
* @return {boolean} whether we should log
*/
shouldLog = (level) => {
level = this.loggerInstance_.levels[level]
const logLevel = this.loggerInstance_.levels[this.loggerInstance_.level]
return level <= logLevel
}
/**
* Sets the log level of the logger.
* @param {string} level - the level to set the logger to
*/
setLogLevel = (level) => {
this.loggerInstance_.level = level
}
/**
* Resets the logger to the value specified by the LOG_LEVEL env var. If no
* LOG_LEVEL is set it defaults to "silly".
*/
unsetLogLevel = () => {
this.loggerInstance_.level = LOG_LEVEL
}
/**
* Begin an activity. In development an activity is displayed as a spinner;
* in other environments it will log the activity at the info level.
* @param {string} message - the message to log the activity under
* @param {any} config
* @returns {string} the id of the activity; this should be passed to do
* further operations on the activity such as success, failure, progress.
*/
activity = (message, config = {}) => {
const id = ulid()
if (IS_DEV && this.shouldLog("info")) {
const activity = this.ora_(message).start()
this.activities_[id] = {
activity,
config,
start: Date.now(),
}
return id
} else {
this.activities_[id] = {
start: Date.now(),
config,
}
this.loggerInstance_.log({
activity_id: id,
level: "info",
config,
message,
})
return id
}
}
/**
* Reports progress on an activity. In development this will update the
* activity log message, in other environments a log message will be issued
* at the info level. Logging will include the activityId.
* @param {string} activityId - the id of the activity as returned by activity
* @param {string} message - the message to log
*/
progress = (activityId, message) => {
const toLog = {
level: "info",
message,
}
if (typeof activityId === "string" && this.activities_[activityId]) {
const activity = this.activities_[activityId]
if (activity.activity) {
activity.text = message
} else {
toLog["activity_id"] = activityId
this.loggerInstance_.log(toLog)
}
} else {
this.loggerInstance_.log(toLog)
}
}
/**
* Logs an error. If an error object is provided the stack trace for the error
* will also be logged.
* @param {String | Error} messageOrError - can either be a string with a
* message to log the error under; or an error object.
* @param {Error?} error - an error object to log message with
*/
error = (messageOrError, error = null) => {
let message = messageOrError
if (typeof messageOrError === "object") {
message = messageOrError.message
error = messageOrError
}
const toLog = {
level: "error",
message,
}
if (error) {
toLog["stack"] = stackTrace.parse(error)
}
this.loggerInstance_.log(toLog)
// Give stack traces and details in dev
if (error && IS_DEV) {
console.log(error)
}
}
/**
* Reports failure of an activity. In development the activity will be udpated
* with the failure message in other environments the failure will be logged
* at the error level.
* @param {string} activityId - the id of the activity as returned by activity
* @param {string} message - the message to log
* @returns {object} data about the activity
*/
failure = (activityId, message) => {
const time = Date.now()
const toLog = {
level: "error",
message,
}
if (typeof activityId === "string" && this.activities_[activityId]) {
const activity = this.activities_[activityId]
if (activity.activity) {
activity.activity.fail(`${message} ${time - activity.start}`)
} else {
toLog["duration"] = time - activity.start
toLog["activity_id"] = activityId
this.loggerInstance_.log(toLog)
}
} else {
this.loggerInstance_.log(toLog)
}
if (this.activities_[activityId]) {
const activity = this.activities_[activityId]
return {
...activity,
duration: time - activity.start,
}
}
return null
}
/**
* Reports success of an activity. In development the activity will be udpated
* with the failure message in other environments the failure will be logged
* at the info level.
* @param {string} activityId - the id of the activity as returned by activity
* @param {string} message - the message to log
* @returns {Record<string, any>} data about the activity
*/
success = (activityId, message) => {
const time = Date.now()
const toLog = {
level: "info",
message,
}
if (typeof activityId === "string" && this.activities_[activityId]) {
const activity = this.activities_[activityId]
if (activity.activity) {
activity.activity.succeed(`${message} ${time - activity.start}ms`)
} else {
toLog["duration"] = time - activity.start
toLog["activity_id"] = activityId
this.loggerInstance_.log(toLog)
}
} else {
this.loggerInstance_.log(toLog)
}
if (this.activities_[activityId]) {
const activity = this.activities_[activityId]
return {
...activity,
duration: time - activity.start,
}
}
return null
}
/**
* Logs a message at the info level.
* @param {string} message - the message to log
*/
debug = (message) => {
this.loggerInstance_.log({
level: "debug",
message,
})
}
/**
* Logs a message at the info level.
* @param {string} message - the message to log
*/
info = (message) => {
this.loggerInstance_.log({
level: "info",
message,
})
}
/**
* Logs a message at the warn level.
* @param {string} message - the message to log
*/
warn = (message) => {
this.loggerInstance_.warn({
level: "warn",
message,
})
}
/**
* A wrapper around winston's log method.
*/
log = (...args) => {
if (args.length > 1) {
// @ts-ignore
this.loggerInstance_.log(...args)
} else {
let message = args[0]
this.loggerInstance_.log({
level: "info",
message,
})
}
}
}
const logger = new Reporter({
logger: loggerInstance,
activityLogger: ora,
})
export default logger

View File

@@ -0,0 +1,35 @@
export type PanicData = {
id: string
context: {
rootPath: string
path: string
}
}
export enum PanicId {
InvalidProjectName = "10000",
InvalidPath = "10002",
AlreadyNodeProject = "10003",
}
export const panicHandler = (panicData: PanicData = {} as 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 "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",
}
}
}

View File

@@ -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")
}

View File

@@ -0,0 +1,15 @@
import ConfigStore from "configstore"
import reporter from "../reporter"
const config = new ConfigStore(`medusa`, {}, { globalConfigPath: true })
const packageMangerConfigKey = `cli.packageManager`
export const getPackageManager = () => {
return config.get(packageMangerConfigKey)
}
export const setPackageManager = (packageManager) => {
config.set(packageMangerConfigKey, packageManager)
reporter.info(`Preferred package manager set to "${packageManager}"`)
}

View File

@@ -0,0 +1,5 @@
import { getMedusaVersion } from "medusa-core-utils"
export const getLocalMedusaVersion = (): string => {
return getMedusaVersion()
}