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:
Sebastian Rindom
2022-07-05 09:33:46 +02:00
committed by GitHub
parent f9c3218aac
commit f8138afa36
4 changed files with 325 additions and 121 deletions

View File

@@ -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": {

View 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
}
`)
}

View 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}",
}`
}

View File

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