From f8138afa363c3aedb8087db6c0c2a3ed01b66324 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Tue, 5 Jul 2022 09:33:46 +0200 Subject: [PATCH] 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. --- packages/medusa-dev-cli/package.json | 4 +- .../medusa-dev-cli/src/feature-flags/index.js | 178 +++++++++++++ .../src/feature-flags/template.js | 13 + packages/medusa-dev-cli/src/index.js | 251 ++++++++++-------- 4 files changed, 325 insertions(+), 121 deletions(-) create mode 100644 packages/medusa-dev-cli/src/feature-flags/index.js create mode 100644 packages/medusa-dev-cli/src/feature-flags/template.js diff --git a/packages/medusa-dev-cli/package.json b/packages/medusa-dev-cli/package.json index 6794224221..193539da50 100644 --- a/packages/medusa-dev-cli/package.json +++ b/packages/medusa-dev-cli/package.json @@ -29,9 +29,7 @@ "cross-env": "^7.0.3" }, "homepage": "https://github.com/medusajs/medusa/tree/master/packages/medusa-dev-cli#readme", - "keywords": [ - "medusa" - ], + "keywords": ["medusa"], "license": "MIT", "main": "index.js", "repository": { diff --git a/packages/medusa-dev-cli/src/feature-flags/index.js b/packages/medusa-dev-cli/src/feature-flags/index.js new file mode 100644 index 0000000000..31067a14ce --- /dev/null +++ b/packages/medusa-dev-cli/src/feature-flags/index.js @@ -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 ", + 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 ", + 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 + } + `) +} diff --git a/packages/medusa-dev-cli/src/feature-flags/template.js b/packages/medusa-dev-cli/src/feature-flags/template.js new file mode 100644 index 0000000000..56866c6bf5 --- /dev/null +++ b/packages/medusa-dev-cli/src/feature-flags/template.js @@ -0,0 +1,13 @@ +export const featureFlagTemplate = ({ + key, + description, + defaultValue, + envKey, +}) => { + return `export default { + key: "${key}", + description: "${description}", + default_value: ${defaultValue}, + env_key: "${envKey}", +}` +} diff --git a/packages/medusa-dev-cli/src/index.js b/packages/medusa-dev-cli/src/index.js index f6e35c852a..df092bb6e6 100644 --- a/packages/medusa-dev-cli/src/index.js +++ b/packages/medusa-dev-cli/src/index.js @@ -3,128 +3,137 @@ const Configstore = require(`configstore`) const pkg = require(`../package.json`) const _ = require(`lodash`) +const yargs = require(`yargs/yargs`) const path = require(`path`) const os = require(`os`) +const fs = require(`fs-extra`) const watch = require(`./watch`) const { getVersionInfo } = require(`./utils/version`) -const argv = require(`yargs`) - .usage(`Usage: medusa-dev [options]`) - .alias(`q`, `quiet`) - .nargs(`q`, 0) - .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. +const { buildFFCli } = require("./feature-flags") + +const cli = yargs() + +cli.command({ + command: `*`, + description: `Start the Medusa dev CLI`, + builder: (yargs) => { + yargs + .usage(`Usage: medusa-dev [options]`) + .alias(`q`, `quiet`) + .nargs(`q`, 0) + .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.` - ) - .nargs(`force-install`, 0) - .describe( - `force-install`, - `Disables copying files into node_modules and forces usage of local npm repository.` - ) - .nargs(`external-registry`, 0) - .describe( - `external-registry`, - `Run 'yarn add' commands without the --registry flag.` - ) - .alias(`C`, `copy-all`) - .nargs(`C`, 0) - .describe( - `C`, - `Copy all contents in packages/ instead of just medusa packages` - ) - .array(`packages`) - .describe(`packages`, `Explicitly specify packages to copy`) - .help(`h`) - .alias(`h`, `help`) - .nargs(`v`, 0) - .alias(`v`, `version`) - .describe(`v`, `Print the currently installed version of Medusa Dev CLI`).argv + ) + .nargs(`force-install`, 0) + .describe( + `force-install`, + `Disables copying files into node_modules and forces usage of local npm repository.` + ) + .nargs(`external-registry`, 0) + .describe( + `external-registry`, + `Run 'yarn add' commands without the --registry flag.` + ) + .alias(`C`, `copy-all`) + .nargs(`C`, 0) + .describe( + `C`, + `Copy all contents in packages/ instead of just medusa packages` + ) + .array(`packages`) + .describe(`packages`, `Explicitly specify packages to copy`) + .help(`h`) + .alias(`h`, `help`) + .nargs(`v`, 0) + .alias(`v`, `version`) + .describe(`v`, `Print the currently installed version of Medusa Dev CLI`) + }, + handler: async (argv) => { + const conf = new Configstore(pkg.name) -if (argv.version) { - console.log(getVersionInfo()) - process.exit() -} + if (argv.version) { + console.log(getVersionInfo()) + 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 (pathToRepo.includes(`~`)) { - pathToRepo = path.join(os.homedir(), pathToRepo.split(`~`).pop()) - } - conf.set(`medusa-location`, path.resolve(pathToRepo)) - process.exit() -} + if (!havePackageJsonFile) { + console.error(`Current folder must have a package.json file!`) + process.exit() + } -const havePackageJsonFile = fs.existsSync(`package.json`) + const medusaLocation = conf.get(`medusa-location`) -if (!havePackageJsonFile) { - console.error(`Current folder must have a package.json file!`) - process.exit() -} - -const medusaLocation = conf.get(`medusa-location`) - -if (!medusaLocation) { - console.error( - ` + 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() -} - -// 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`) - ) ) - - if (localPkg?.name) { - packageNameToPath.set( - localPkg.name, - path.join(medusaLocation, `packages`, dirName) - ) - return localPkg.name - } - } catch (error) { - // fallback to generic one + process.exit() } - packageNameToPath.set( - dirName, - path.join(medusaLocation, `packages`, dirName) + // 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`) + ) + ) + + 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`)) -// intersect dependencies with monoRepoPackages to get list of packages that are used -const localPackages = _.intersection( - monoRepoPackages, - Object.keys(_.merge({}, localPkg.dependencies, localPkg.devDependencies)) -) - -if (!argv.packages && _.isEmpty(localPackages)) { - console.error( - ` + if (!argv.packages && _.isEmpty(localPackages)) { + console.error( + ` You haven't got any medusa dependencies into your current package.json You probably want to pass in a list of packages to start 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, medusa-dev will pick them up. ` - ) - if (!argv.forceInstall) { - process.exit() - } else { - console.log( - `Continuing other dependencies installation due to "--forceInstall" flag` - ) - } -} + ) + if (!argv.forceInstall) { + process.exit() + } else { + console.log( + `Continuing other dependencies installation due to "--forceInstall" flag` + ) + } + } -watch(medusaLocation, argv.packages, { - localPackages, - quiet: argv.quiet, - scanOnce: argv.scanOnce, - forceInstall: argv.forceInstall, - monoRepoPackages, - packageNameToPath, - externalRegistry: argv.externalRegistry, + watch(medusaLocation, argv.packages, { + localPackages, + quiet: argv.quiet, + scanOnce: argv.scanOnce, + forceInstall: argv.forceInstall, + monoRepoPackages, + packageNameToPath, + externalRegistry: argv.externalRegistry, + }) + }, }) + +buildFFCli(cli) + +cli.parse(process.argv.slice(2))