Files
medusa-store/packages/cli/medusa-cli/src/create-cli.ts
Adrien de Peretti eace10e66b fix(medusa,cli): Resources not correctly loaded when running the migration scripts (#11108)
FIXES FRMW-2888

**What**
Since the migration scripts runner run as part of the migrate script, we were cleaning up the resources but we didn't take into consideration that links are loading automagically by just importing the links. The issue is that once a link have been imported, the next import will rely on the cached module and therefore when cleaning the resources, re importing those links is not enough. Instead, we are now treating the run-script as a separate command that we run during running the migration so that resources are isolated and loaded according to the need of each script themselves
2025-01-23 16:33:50 +00:00

623 lines
19 KiB
TypeScript

import { sync as existsSync } from "fs-exists-cached"
import { setTelemetryEnabled } from "@medusajs/telemetry"
import path from "path"
import resolveCwd from "resolve-cwd"
import { newStarter } from "./commands/new"
import { didYouMean } from "./did-you-mean"
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 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((s: string) => console.log(s))
}
try {
const cmdPath = resolveCwd.silent(`@medusajs/medusa/commands/${command}`)!
return require(cmdPath).default
} catch (err) {
console.error(err)
cli.showHelp((s: string) => console.error(s))
}
}
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,
})
.option(`branch`, {
type: `string`,
describe: `The branch of the git repository to clone.`,
}),
desc: `Create a new Medusa project.`,
handler: handlerP(newStarter),
})
.command({
command: "db:setup",
desc: "Create the database, run migrations and sync links",
builder: (builder) => {
builder.option("db", {
type: "string",
describe: "Specify the name of the database you want to create",
})
builder.option("interactive", {
type: "boolean",
default: true,
describe:
"Display prompts. Use --no-interactive flag to run the command without prompts",
})
builder.option("skip-links", {
type: "boolean",
describe: "Do not sync links",
})
builder.option("execute-all-links", {
type: "boolean",
describe:
"Skip prompts and execute all (including unsafe) actions from sync links",
})
builder.option("execute-safe-links", {
type: "boolean",
describe:
"Skip prompts and execute only safe actions from sync links",
})
},
handler: handlerP(
getCommandHandler("db/setup", (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
return cmd(args)
})
),
})
.command({
command: "db:create",
desc: "Create the database used by your application",
builder: (builder) => {
builder.option("db", {
type: "string",
describe: "Specify the name of the database you want to create",
})
builder.option("interactive", {
type: "boolean",
default: true,
describe:
"Display prompts. Use --no-interactive flag to run the command without prompts",
})
},
handler: handlerP(
getCommandHandler("db/create", (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
return cmd(args)
})
),
})
.command({
command: "db:migrate",
desc: "Migrate the database by executing pending migrations",
builder: (builder) => {
builder.option("skip-scripts", {
type: "boolean",
describe: "Do not run migration scripts",
})
builder.option("skip-links", {
type: "boolean",
describe: "Do not sync links",
})
builder.option("execute-all-links", {
type: "boolean",
describe:
"Skip prompts and execute all (including unsafe) actions from sync links",
})
builder.option("execute-safe-links", {
type: "boolean",
describe:
"Skip prompts and execute only safe actions from sync links",
})
},
handler: handlerP(
getCommandHandler("db/migrate", (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
return cmd(args)
})
),
})
.command({
command: "db:migrate:scripts",
desc: "Run all migration scripts",
handler: handlerP(
getCommandHandler("db/run-scripts", (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
return cmd(args)
})
),
})
.command({
command: "db:rollback [modules...]",
desc: "Rollback last batch of executed migrations for a given module",
builder: {
modules: {
type: "array",
description: "Modules for which to rollback migrations",
demand: true,
},
},
handler: handlerP(
getCommandHandler("db/rollback", (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
return cmd(args)
})
),
})
.command({
command: "db:generate [modules...]",
desc: "Generate migrations for a given module",
builder: {
modules: {
type: "array",
description: "Modules for which to generate migration files",
demand: true,
},
},
handler: handlerP(
getCommandHandler("db/generate", (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
return cmd(args)
})
),
})
.command({
command: "plugin:db:generate",
desc: "Generate migrations for modules in a plugin",
handler: handlerP(
getCommandHandler("plugin/db/generate", (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
return cmd(args)
})
),
})
.command({
command: "db:sync-links",
desc: "Sync database schema with the links defined by your application and Medusa core",
builder: (builder) => {
builder.option("execute-all", {
type: "boolean",
describe: "Skip prompts and execute all (including unsafe) actions",
})
builder.option("execute-safe", {
type: "boolean",
describe: "Skip prompts and execute only safe actions",
})
},
handler: handlerP(
getCommandHandler("db/sync-links", (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
return cmd(args)
})
),
})
.command({
command: "plugin:build",
desc: "Build plugin source for publishing to a package registry",
handler: handlerP(
getCommandHandler("plugin/build", (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
return new Promise((resolve) => {})
})
),
})
.command({
command: "plugin:develop",
desc: "Start plugin development process in watch mode. Changes will be re-published to the local packages registry",
handler: handlerP(
getCommandHandler("plugin/develop", (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
return new Promise(() => {})
})
),
})
.command({
command: "plugin:publish",
desc: "Publish the plugin to the local packages registry",
handler: handlerP(
getCommandHandler("plugin/publish", (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
return new Promise(() => {})
})
),
})
.command({
command: "plugin:add [plugin_names...]",
desc: "Add the specified plugin to the project from the local packages registry",
builder: {
plugin_names: {
type: "array",
description: "The name of the plugins to add",
demand: true,
},
},
handler: handlerP(
getCommandHandler("plugin/add", (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
return new Promise(() => {})
})
),
})
.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: `develop`,
desc: `Start development server. Watches file and rebuilds when something changes`,
builder: (_) =>
_.option("types", {
type: "boolean",
default: true,
describe:
"Generate automated types for modules inside the .medusa directory",
})
.option(`H`, {
alias: `host`,
type: `string`,
default: process.env.HOST,
describe: process.env.HOST
? `Set host. Defaults to ${process.env.HOST} (set by env.HOST)`
: "",
})
.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(() => {})
})
),
})
.command({
command: `start`,
desc: `Start production server.`,
builder: (_) =>
_.option("types", {
type: "boolean",
default: false,
describe:
"Generate automated types for modules inside the .medusa directory",
})
.option(`H`, {
alias: `host`,
type: `string`,
default: process.env.HOST,
describe: process.env.HOST
? `Set host. Defaults to ${process.env.HOST} (set by env.HOST)`
: ``,
})
.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(`cluster`, {
type: `number`,
describe:
"Start the Node.js server in cluster mode. You can specify the number of cpus to use, which defaults to (env.CPUS)",
}),
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: "build",
desc: "Build your project.",
builder: (_) =>
_.option("admin-only", {
default: false,
type: "boolean",
describe:
"Only build the admin to serve it separately (outDir .medusa/admin)",
}),
handler: handlerP(
getCommandHandler(`build`, (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
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) => {})
})
),
})
.command({
command: `exec [file] [args..]`,
desc: `Run a function defined in a file.`,
handler: handlerP(
getCommandHandler(`exec`, (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 = ""
try {
medusaVersion = require(path.join(
process.cwd(),
`node_modules`,
`@medusajs/medusa`,
`package.json`
)).version
} catch (e) {
/* noop */
}
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 (msg) {
reporter.error(msg)
console.log()
}
if (suggestion) {
reporter.info(suggestion)
console.log()
}
if (err) {
console.error("--------------- ERROR ---------------------")
console.error(err)
console.error("-------------------------------------------")
}
cli.showHelp((s: string) => console.error(s))
process.exit(1)
})
.parse(argv.slice(2))
}