feat(medusa-dev-cli): adds helpers to manage feature flags (#1770)
**Usage** **Create a new feature flag** ``` $ medusa-dev ff create [name of flag] -d [description of what flag is for] ``` Will put a new file in `packages/medusa/src/loaders/feature-flags/[kebab-cased-flag-name].ts` and fill out the details. **List feature flags** ``` $ medusa-dev ff list ``` Note: your Medusa repo must be built for the flags to show up **Delete a feature flag** ``` $ medusa-dev ff delete [name of flag] ``` Will delete a file at `packages/medusa/src/loaders/feature-flags/[kebab-cased-flag-name].ts` if it exists.
This commit is contained in:
@@ -29,9 +29,7 @@
|
|||||||
"cross-env": "^7.0.3"
|
"cross-env": "^7.0.3"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/medusajs/medusa/tree/master/packages/medusa-dev-cli#readme",
|
"homepage": "https://github.com/medusajs/medusa/tree/master/packages/medusa-dev-cli#readme",
|
||||||
"keywords": [
|
"keywords": ["medusa"],
|
||||||
"medusa"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
178
packages/medusa-dev-cli/src/feature-flags/index.js
Normal file
178
packages/medusa-dev-cli/src/feature-flags/index.js
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import path from "path"
|
||||||
|
import glob from "glob"
|
||||||
|
import fs from "fs"
|
||||||
|
import Configstore from "configstore"
|
||||||
|
import { kebabCase, snakeCase } from "lodash"
|
||||||
|
import { featureFlagTemplate } from "./template"
|
||||||
|
import pkg from "../../package.json"
|
||||||
|
|
||||||
|
export const buildFFCli = (cli) => {
|
||||||
|
cli.command({
|
||||||
|
command: `ff`,
|
||||||
|
desc: "Manage Medusa feature flags",
|
||||||
|
builder: (yargs) => {
|
||||||
|
yargs
|
||||||
|
.command({
|
||||||
|
command: "create <name>",
|
||||||
|
desc: "Create a new feature flag",
|
||||||
|
builder: {
|
||||||
|
name: {
|
||||||
|
demandOption: true,
|
||||||
|
coerce: (name) => kebabCase(name),
|
||||||
|
description: "Name of the feature flag",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
alias: "d",
|
||||||
|
demandOption: true,
|
||||||
|
description: "Description of the feature flag",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (argv) => {
|
||||||
|
const medusaLocation = getRepoRoot()
|
||||||
|
const featureFlagPath = buildPath(argv.name, medusaLocation)
|
||||||
|
|
||||||
|
if (fs.existsSync(featureFlagPath)) {
|
||||||
|
console.error(`Feature flag already exists: ${featureFlagPath}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const flagSettings = collectSettings(argv.name, argv.description)
|
||||||
|
writeFeatureFlag(flagSettings, featureFlagPath)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.command({
|
||||||
|
command: "list",
|
||||||
|
desc: "List available feature flags",
|
||||||
|
handler: async () => {
|
||||||
|
const medusaLocation = getRepoRoot()
|
||||||
|
const flagGlob = buildFlagsGlob(medusaLocation)
|
||||||
|
|
||||||
|
const featureFlags = glob.sync(flagGlob, {
|
||||||
|
ignore: ["**/index.*"],
|
||||||
|
})
|
||||||
|
const flagData = featureFlags.map((flag) => {
|
||||||
|
const flagSettings = readFeatureFlag(flag)
|
||||||
|
return {
|
||||||
|
...flagSettings,
|
||||||
|
file_name: path.basename(flag, ".js"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.table(flagData)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.command({
|
||||||
|
command: "delete <name>",
|
||||||
|
desc: "Delete a feature flag",
|
||||||
|
builder: {
|
||||||
|
name: {
|
||||||
|
demand: true,
|
||||||
|
coerce: (name) => kebabCase(name),
|
||||||
|
description: "Name of the feature flag",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (argv) => {
|
||||||
|
const medusaLocation = getRepoRoot()
|
||||||
|
const featureFlagPath = buildPath(argv.name, medusaLocation)
|
||||||
|
|
||||||
|
if (fs.existsSync(featureFlagPath)) {
|
||||||
|
fs.unlinkSync(featureFlagPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Feature flag deleted: ${featureFlagPath}`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.demandCommand(1, "Please specify an action")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRepoRoot = () => {
|
||||||
|
const conf = new Configstore(pkg.name)
|
||||||
|
const medusaLocation = conf.get(`medusa-location`)
|
||||||
|
|
||||||
|
if (!medusaLocation) {
|
||||||
|
console.error(
|
||||||
|
`
|
||||||
|
You haven't set the path yet to your cloned
|
||||||
|
version of medusa. Do so now by running:
|
||||||
|
medusa-dev --set-path-to-repo /path/to/my/cloned/version/medusa
|
||||||
|
`
|
||||||
|
)
|
||||||
|
process.exit()
|
||||||
|
}
|
||||||
|
|
||||||
|
return medusaLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
const readFeatureFlag = (flagPath) => {
|
||||||
|
const flagSettings = require(flagPath).default
|
||||||
|
return flagSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildFlagsGlob = (repoRoot) => {
|
||||||
|
return path.join(
|
||||||
|
repoRoot,
|
||||||
|
"packages",
|
||||||
|
"medusa",
|
||||||
|
"dist",
|
||||||
|
"loaders",
|
||||||
|
"feature-flags",
|
||||||
|
`*.js`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildPath = (kebabCaseName, repoRoot) => {
|
||||||
|
return path.join(
|
||||||
|
repoRoot,
|
||||||
|
"packages",
|
||||||
|
"medusa",
|
||||||
|
"src",
|
||||||
|
"loaders",
|
||||||
|
"feature-flags",
|
||||||
|
`${kebabCaseName}.ts`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectSettings = (name, description) => {
|
||||||
|
const snakeCaseName = snakeCase(name)
|
||||||
|
return {
|
||||||
|
key: snakeCaseName,
|
||||||
|
description: description,
|
||||||
|
defaultValue: false,
|
||||||
|
envKey: `MEDUSA_FF_${snakeCaseName.toUpperCase()}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeFeatureFlag = (settings, featureFlagPath) => {
|
||||||
|
const featureFlag = featureFlagTemplate(settings)
|
||||||
|
fs.writeFileSync(featureFlagPath, featureFlag)
|
||||||
|
logFeatureFlagUsage(featureFlagPath, settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
const logFeatureFlagUsage = (flagPath, flagSettings) => {
|
||||||
|
console.log(`Feature flag created: ${flagPath}`)
|
||||||
|
console.log(`
|
||||||
|
To use this feature flag, add the following to your medusa-config.js:
|
||||||
|
|
||||||
|
{
|
||||||
|
...,
|
||||||
|
featureFlags: {
|
||||||
|
${flagSettings.key}: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
or set the environment variable:
|
||||||
|
|
||||||
|
export ${flagSettings.envKey}=true
|
||||||
|
|
||||||
|
To add guarded code use the featureFlagRouter:
|
||||||
|
|
||||||
|
if (featureFlagRouter.isEnabled("${flagSettings.key}")) {
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
13
packages/medusa-dev-cli/src/feature-flags/template.js
Normal file
13
packages/medusa-dev-cli/src/feature-flags/template.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export const featureFlagTemplate = ({
|
||||||
|
key,
|
||||||
|
description,
|
||||||
|
defaultValue,
|
||||||
|
envKey,
|
||||||
|
}) => {
|
||||||
|
return `export default {
|
||||||
|
key: "${key}",
|
||||||
|
description: "${description}",
|
||||||
|
default_value: ${defaultValue},
|
||||||
|
env_key: "${envKey}",
|
||||||
|
}`
|
||||||
|
}
|
||||||
@@ -3,128 +3,137 @@
|
|||||||
const Configstore = require(`configstore`)
|
const Configstore = require(`configstore`)
|
||||||
const pkg = require(`../package.json`)
|
const pkg = require(`../package.json`)
|
||||||
const _ = require(`lodash`)
|
const _ = require(`lodash`)
|
||||||
|
const yargs = require(`yargs/yargs`)
|
||||||
const path = require(`path`)
|
const path = require(`path`)
|
||||||
const os = require(`os`)
|
const os = require(`os`)
|
||||||
|
const fs = require(`fs-extra`)
|
||||||
const watch = require(`./watch`)
|
const watch = require(`./watch`)
|
||||||
const { getVersionInfo } = require(`./utils/version`)
|
const { getVersionInfo } = require(`./utils/version`)
|
||||||
const argv = require(`yargs`)
|
const { buildFFCli } = require("./feature-flags")
|
||||||
.usage(`Usage: medusa-dev [options]`)
|
|
||||||
.alias(`q`, `quiet`)
|
const cli = yargs()
|
||||||
.nargs(`q`, 0)
|
|
||||||
.describe(`q`, `Do not output copy file information`)
|
cli.command({
|
||||||
.alias(`s`, `scan-once`)
|
command: `*`,
|
||||||
.nargs(`s`, 0)
|
description: `Start the Medusa dev CLI`,
|
||||||
.describe(`s`, `Scan once. Do not start file watch`)
|
builder: (yargs) => {
|
||||||
.alias(`p`, `set-path-to-repo`)
|
yargs
|
||||||
.nargs(`p`, 1)
|
.usage(`Usage: medusa-dev [options]`)
|
||||||
.describe(
|
.alias(`q`, `quiet`)
|
||||||
`p`,
|
.nargs(`q`, 0)
|
||||||
`Set path to medusa repository.
|
.describe(`q`, `Do not output copy file information`)
|
||||||
|
.alias(`s`, `scan-once`)
|
||||||
|
.nargs(`s`, 0)
|
||||||
|
.describe(`s`, `Scan once. Do not start file watch`)
|
||||||
|
.alias(`p`, `set-path-to-repo`)
|
||||||
|
.nargs(`p`, 1)
|
||||||
|
.describe(
|
||||||
|
`p`,
|
||||||
|
`Set path to medusa repository.
|
||||||
You typically only need to configure this once.`
|
You typically only need to configure this once.`
|
||||||
)
|
)
|
||||||
.nargs(`force-install`, 0)
|
.nargs(`force-install`, 0)
|
||||||
.describe(
|
.describe(
|
||||||
`force-install`,
|
`force-install`,
|
||||||
`Disables copying files into node_modules and forces usage of local npm repository.`
|
`Disables copying files into node_modules and forces usage of local npm repository.`
|
||||||
)
|
)
|
||||||
.nargs(`external-registry`, 0)
|
.nargs(`external-registry`, 0)
|
||||||
.describe(
|
.describe(
|
||||||
`external-registry`,
|
`external-registry`,
|
||||||
`Run 'yarn add' commands without the --registry flag.`
|
`Run 'yarn add' commands without the --registry flag.`
|
||||||
)
|
)
|
||||||
.alias(`C`, `copy-all`)
|
.alias(`C`, `copy-all`)
|
||||||
.nargs(`C`, 0)
|
.nargs(`C`, 0)
|
||||||
.describe(
|
.describe(
|
||||||
`C`,
|
`C`,
|
||||||
`Copy all contents in packages/ instead of just medusa packages`
|
`Copy all contents in packages/ instead of just medusa packages`
|
||||||
)
|
)
|
||||||
.array(`packages`)
|
.array(`packages`)
|
||||||
.describe(`packages`, `Explicitly specify packages to copy`)
|
.describe(`packages`, `Explicitly specify packages to copy`)
|
||||||
.help(`h`)
|
.help(`h`)
|
||||||
.alias(`h`, `help`)
|
.alias(`h`, `help`)
|
||||||
.nargs(`v`, 0)
|
.nargs(`v`, 0)
|
||||||
.alias(`v`, `version`)
|
.alias(`v`, `version`)
|
||||||
.describe(`v`, `Print the currently installed version of Medusa Dev CLI`).argv
|
.describe(`v`, `Print the currently installed version of Medusa Dev CLI`)
|
||||||
|
},
|
||||||
|
handler: async (argv) => {
|
||||||
|
const conf = new Configstore(pkg.name)
|
||||||
|
|
||||||
if (argv.version) {
|
if (argv.version) {
|
||||||
console.log(getVersionInfo())
|
console.log(getVersionInfo())
|
||||||
process.exit()
|
process.exit()
|
||||||
}
|
}
|
||||||
|
|
||||||
const conf = new Configstore(pkg.name)
|
let pathToRepo = argv.setPathToRepo
|
||||||
|
|
||||||
const fs = require(`fs-extra`)
|
if (pathToRepo) {
|
||||||
|
if (pathToRepo.includes(`~`)) {
|
||||||
|
pathToRepo = path.join(os.homedir(), pathToRepo.split(`~`).pop())
|
||||||
|
}
|
||||||
|
conf.set(`medusa-location`, path.resolve(pathToRepo))
|
||||||
|
process.exit()
|
||||||
|
}
|
||||||
|
|
||||||
let pathToRepo = argv.setPathToRepo
|
const havePackageJsonFile = fs.existsSync(`package.json`)
|
||||||
|
|
||||||
if (pathToRepo) {
|
if (!havePackageJsonFile) {
|
||||||
if (pathToRepo.includes(`~`)) {
|
console.error(`Current folder must have a package.json file!`)
|
||||||
pathToRepo = path.join(os.homedir(), pathToRepo.split(`~`).pop())
|
process.exit()
|
||||||
}
|
}
|
||||||
conf.set(`medusa-location`, path.resolve(pathToRepo))
|
|
||||||
process.exit()
|
|
||||||
}
|
|
||||||
|
|
||||||
const havePackageJsonFile = fs.existsSync(`package.json`)
|
const medusaLocation = conf.get(`medusa-location`)
|
||||||
|
|
||||||
if (!havePackageJsonFile) {
|
if (!medusaLocation) {
|
||||||
console.error(`Current folder must have a package.json file!`)
|
console.error(
|
||||||
process.exit()
|
`
|
||||||
}
|
|
||||||
|
|
||||||
const medusaLocation = conf.get(`medusa-location`)
|
|
||||||
|
|
||||||
if (!medusaLocation) {
|
|
||||||
console.error(
|
|
||||||
`
|
|
||||||
You haven't set the path yet to your cloned
|
You haven't set the path yet to your cloned
|
||||||
version of medusa. Do so now by running:
|
version of medusa. Do so now by running:
|
||||||
medusa-dev --set-path-to-repo /path/to/my/cloned/version/medusa
|
medusa-dev --set-path-to-repo /path/to/my/cloned/version/medusa
|
||||||
`
|
`
|
||||||
)
|
|
||||||
process.exit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// get list of packages from monorepo
|
|
||||||
const packageNameToPath = new Map()
|
|
||||||
const monoRepoPackages = fs
|
|
||||||
.readdirSync(path.join(medusaLocation, `packages`))
|
|
||||||
.map((dirName) => {
|
|
||||||
try {
|
|
||||||
const localPkg = JSON.parse(
|
|
||||||
fs.readFileSync(
|
|
||||||
path.join(medusaLocation, `packages`, dirName, `package.json`)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
process.exit()
|
||||||
if (localPkg?.name) {
|
|
||||||
packageNameToPath.set(
|
|
||||||
localPkg.name,
|
|
||||||
path.join(medusaLocation, `packages`, dirName)
|
|
||||||
)
|
|
||||||
return localPkg.name
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// fallback to generic one
|
|
||||||
}
|
}
|
||||||
|
|
||||||
packageNameToPath.set(
|
// get list of packages from monorepo
|
||||||
dirName,
|
const packageNameToPath = new Map()
|
||||||
path.join(medusaLocation, `packages`, dirName)
|
const monoRepoPackages = fs
|
||||||
|
.readdirSync(path.join(medusaLocation, `packages`))
|
||||||
|
.map((dirName) => {
|
||||||
|
try {
|
||||||
|
const localPkg = JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(medusaLocation, `packages`, dirName, `package.json`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (localPkg?.name) {
|
||||||
|
packageNameToPath.set(
|
||||||
|
localPkg.name,
|
||||||
|
path.join(medusaLocation, `packages`, dirName)
|
||||||
|
)
|
||||||
|
return localPkg.name
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// fallback to generic one
|
||||||
|
}
|
||||||
|
|
||||||
|
packageNameToPath.set(
|
||||||
|
dirName,
|
||||||
|
path.join(medusaLocation, `packages`, dirName)
|
||||||
|
)
|
||||||
|
return dirName
|
||||||
|
})
|
||||||
|
|
||||||
|
const localPkg = JSON.parse(fs.readFileSync(`package.json`))
|
||||||
|
// intersect dependencies with monoRepoPackages to get list of packages that are used
|
||||||
|
const localPackages = _.intersection(
|
||||||
|
monoRepoPackages,
|
||||||
|
Object.keys(_.merge({}, localPkg.dependencies, localPkg.devDependencies))
|
||||||
)
|
)
|
||||||
return dirName
|
|
||||||
})
|
|
||||||
|
|
||||||
const localPkg = JSON.parse(fs.readFileSync(`package.json`))
|
if (!argv.packages && _.isEmpty(localPackages)) {
|
||||||
// intersect dependencies with monoRepoPackages to get list of packages that are used
|
console.error(
|
||||||
const localPackages = _.intersection(
|
`
|
||||||
monoRepoPackages,
|
|
||||||
Object.keys(_.merge({}, localPkg.dependencies, localPkg.devDependencies))
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!argv.packages && _.isEmpty(localPackages)) {
|
|
||||||
console.error(
|
|
||||||
`
|
|
||||||
You haven't got any medusa dependencies into your current package.json
|
You haven't got any medusa dependencies into your current package.json
|
||||||
You probably want to pass in a list of packages to start
|
You probably want to pass in a list of packages to start
|
||||||
developing on! For example:
|
developing on! For example:
|
||||||
@@ -132,22 +141,28 @@ medusa-dev --packages medusa medusa-js
|
|||||||
If you prefer to place them in your package.json dependencies instead,
|
If you prefer to place them in your package.json dependencies instead,
|
||||||
medusa-dev will pick them up.
|
medusa-dev will pick them up.
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
if (!argv.forceInstall) {
|
if (!argv.forceInstall) {
|
||||||
process.exit()
|
process.exit()
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
`Continuing other dependencies installation due to "--forceInstall" flag`
|
`Continuing other dependencies installation due to "--forceInstall" flag`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(medusaLocation, argv.packages, {
|
watch(medusaLocation, argv.packages, {
|
||||||
localPackages,
|
localPackages,
|
||||||
quiet: argv.quiet,
|
quiet: argv.quiet,
|
||||||
scanOnce: argv.scanOnce,
|
scanOnce: argv.scanOnce,
|
||||||
forceInstall: argv.forceInstall,
|
forceInstall: argv.forceInstall,
|
||||||
monoRepoPackages,
|
monoRepoPackages,
|
||||||
packageNameToPath,
|
packageNameToPath,
|
||||||
externalRegistry: argv.externalRegistry,
|
externalRegistry: argv.externalRegistry,
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
buildFFCli(cli)
|
||||||
|
|
||||||
|
cli.parse(process.argv.slice(2))
|
||||||
|
|||||||
Reference in New Issue
Block a user