Browse Source

Merge pull request #15783 from grafana/cli/refactor-commands

Minor refactor of cli tasks (core start, @grafana/ui publishing)
Torkel Ödegaard 6 năm trước cách đây
mục cha
commit
72d5215c65

+ 9 - 6
package.json

@@ -123,10 +123,10 @@
   },
   "scripts": {
     "dev": "webpack --progress --colors --mode development --config scripts/webpack/webpack.dev.js",
-    "start": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --theme",
-    "start:hot": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --hot --theme",
-    "start:ignoreTheme": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --hot",
-    "watch": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --theme -d watch,start",
+    "start": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts core:start --watchTheme",
+    "start:hot": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts core:start --hot --watchTheme",
+    "start:ignoreTheme": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts core:start --hot",
+    "watch": "yarn start -d watch,start core:start --watchTheme ",
     "build": "grunt build",
     "test": "grunt test",
     "tslint": "tslint -c tslint.json --project tsconfig.json",
@@ -136,8 +136,11 @@
     "storybook": "cd packages/grafana-ui && yarn storybook",
     "themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts",
     "prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"",
-    "gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --build",
-    "gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --release"
+    "gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:build",
+    "gui:releasePrepare": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release",
+    "gui:publish": "cd packages/grafana-ui/dist && npm publish --access public",
+    "gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release -p",
+    "cli:help": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --help"
   },
   "husky": {
     "hooks": {

+ 41 - 25
scripts/cli/index.ts

@@ -1,33 +1,49 @@
 import program from 'commander';
-import chalk from 'chalk';
 import { execTask } from './utils/execTask';
+import chalk from 'chalk';
+import { startTask } from './tasks/core.start';
+import { buildTask } from './tasks/grafanaui.build';
+import { releaseTask } from './tasks/grafanaui.release';
 
-export type Task<T> = (options: T) => Promise<void>;
+program.option('-d, --depreciate <scripts>', 'Inform about npm script deprecation', v => v.split(','));
 
-// TODO: Refactor to commander commands
-// This will enable us to have command scoped options and limit the ifs below
 program
-  .option('-h, --hot', 'Runs front-end with hot reload enabled')
-  .option('-t, --theme', 'Watches for theme changes and regenerates variables.scss files')
-  .option('-d, --depreciate <scripts>', 'Inform about npm script deprecation', v => v.split(','))
-  .option('-b, --build', 'Created @grafana/ui build')
-  .option('-r, --release', 'Releases @grafana/ui to npm')
-  .parse(process.argv);
+  .command('core:start')
+  .option('-h, --hot', 'Run front-end with HRM enabled')
+  .option('-t, --watchTheme', 'Watch for theme changes and regenerate variables.scss files')
+  .description('Starts Grafana front-end in development mode with watch enabled')
+  .action(async cmd => {
+    await execTask(startTask)({
+      watchThemes: cmd.theme,
+      hot: cmd.hot,
+    });
+  });
 
-if (program.build) {
-  execTask('grafanaui.build');
-} else if (program.release) {
-  execTask('grafanaui.release');
-} else {
-  if (program.depreciate && program.depreciate.length === 2) {
-    console.log(
-      chalk.yellow.bold(
-        `[NPM script depreciation] ${program.depreciate[0]} is deprecated! Use ${program.depreciate[1]} instead!`
-      )
-    );
-  }
-  execTask('core.start', {
-    watchThemes: !!program.theme,
-    hot: !!program.hot,
+program
+  .command('gui:build')
+  .description('Builds @grafana/ui package to packages/grafana-ui/dist')
+  .action(async cmd => {
+    await execTask(buildTask)();
   });
+
+program
+  .command('gui:release')
+  .description('Prepares @grafana/ui release (and publishes to npm on demand)')
+  .option('-p, --publish', 'Publish @grafana/ui to npm registry')
+  .option('-u, --usePackageJsonVersion', 'Use version specified in package.json')
+  .action(async cmd => {
+    await execTask(releaseTask)({
+      publishToNpm: !!cmd.publish,
+      usePackageJsonVersion: !!cmd.usePackageJsonVersion,
+    });
+  });
+
+program.parse(process.argv);
+
+if (program.depreciate && program.depreciate.length === 2) {
+  console.log(
+    chalk.yellow.bold(
+      `[NPM script depreciation] ${program.depreciate[0]} is deprecated! Use ${program.depreciate[1]} instead!`
+    )
+  );
 }

+ 19 - 22
scripts/cli/tasks/core.start.ts

@@ -1,35 +1,30 @@
 import concurrently from 'concurrently';
-import { Task } from '..';
+import { Task, TaskRunner } from './task';
 
 interface StartTaskOptions {
   watchThemes: boolean;
   hot: boolean;
 }
 
-const startTask: Task<StartTaskOptions> = async ({ watchThemes, hot }) => {
-  const jobs = [];
-
-  if (watchThemes) {
-    jobs.push({
+const startTaskRunner: TaskRunner<StartTaskOptions> = async ({ watchThemes, hot }) => {
+  const jobs = [
+    watchThemes && {
       command: 'nodemon -e ts -w ./packages/grafana-ui/src/themes -x yarn run themes:generate',
       name: 'SASS variables generator',
-    });
-  }
-
-  if (!hot) {
-    jobs.push({
-      command: 'webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js',
-      name: 'Webpack',
-    });
-  } else {
-    jobs.push({
-      command: 'webpack-dev-server --progress --colors --mode development --config scripts/webpack/webpack.hot.js',
-      name: 'Dev server',
-    });
-  }
+    },
+    hot
+      ? {
+          command: 'webpack-dev-server --progress --colors --mode development --config scripts/webpack/webpack.hot.js',
+          name: 'Dev server',
+        }
+      : {
+          command: 'webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js',
+          name: 'Webpack',
+        },
+  ];
 
   try {
-    await concurrently(jobs, {
+    await concurrently(jobs.filter(job => !!job), {
       killOthers: ['failure', 'failure'],
     });
   } catch (e) {
@@ -38,4 +33,6 @@ const startTask: Task<StartTaskOptions> = async ({ watchThemes, hot }) => {
   }
 };
 
-export default startTask;
+export const startTask = new Task<StartTaskOptions>();
+startTask.setName('Core startTask');
+startTask.setRunner(startTaskRunner);

+ 32 - 59
scripts/cli/tasks/grafanaui.build.ts

@@ -1,95 +1,66 @@
 import execa from 'execa';
 import fs from 'fs';
-import { Task } from '..';
 import { changeCwdToGrafanaUi, restoreCwd } from '../utils/cwd';
 import chalk from 'chalk';
-import { startSpinner } from '../utils/startSpinner';
+import { useSpinner } from '../utils/useSpinner';
+import { Task, TaskRunner } from './task';
 
 let distDir, cwd;
 
-const clean = async () => {
-  const spinner = startSpinner('Cleaning');
-  try {
-    await execa('npm', ['run', 'clean']);
-    spinner.succeed();
-  } catch (e) {
-    spinner.fail();
-    throw e;
-  }
-};
-
-const compile = async () => {
-  const spinner = startSpinner('Compiling sources');
-  try {
-    await execa('tsc', ['-p', './tsconfig.build.json']);
-    spinner.succeed();
-  } catch (e) {
-    console.log(e);
-    spinner.fail();
-  }
-};
+const clean = useSpinner<void>('Cleaning', async () => await execa('npm', ['run', 'clean']));
 
-const rollup = async () => {
-  const spinner = startSpinner('Bundling');
+const compile = useSpinner<void>('Compiling sources', () => execa('tsc', ['-p', './tsconfig.build.json']));
 
-  try {
-    await execa('npm', ['run', 'build']);
-    spinner.succeed();
-  } catch (e) {
-    spinner.fail();
-  }
-};
-
-export const savePackage = async (path, pkg) => {
-  const spinner = startSpinner('Updating package.json');
+const rollup = useSpinner<void>('Bundling', () => execa('npm', ['run', 'build']));
 
+export const savePackage = useSpinner<{
+  path: string;
+  pkg: {};
+}>('Updating package.json', async ({ path, pkg }) => {
   return new Promise((resolve, reject) => {
     fs.writeFile(path, JSON.stringify(pkg, null, 2), err => {
       if (err) {
-        spinner.fail();
-        console.error(err);
         reject(err);
         return;
       }
-      spinner.succeed();
       resolve();
     });
   });
-};
+});
 
 const preparePackage = async pkg => {
   pkg.main = 'index.js';
   pkg.types = 'index.d.ts';
-  await savePackage(`${cwd}/dist/package.json`, pkg);
+  await savePackage({
+    path: `${cwd}/dist/package.json`,
+    pkg,
+  });
 };
 
-const moveFiles = async () => {
+const moveFiles = () => {
   const files = ['README.md', 'CHANGELOG.md', 'index.js'];
-  const spinner = startSpinner(`Moving ${files.join(', ')} files`);
-
-  const promises = files.map(file => {
-    return fs.copyFile(`${cwd}/${file}`, `${distDir}/${file}`, err => {
-      if (err) {
-        console.error(err);
-        return;
-      }
+  return useSpinner<void>(`Moving ${files.join(', ')} files`, async () => {
+    const promises = files.map(file => {
+      return new Promise((resolve, reject) => {
+        fs.copyFile(`${cwd}/${file}`, `${distDir}/${file}`, err => {
+          if (err) {
+            reject(err);
+            return;
+          }
+          resolve();
+        });
+      });
     });
-  });
 
-  try {
     await Promise.all(promises);
-    spinner.succeed();
-  } catch (e) {
-    spinner.fail();
-  }
+  })();
 };
 
-const buildTask: Task<void> = async () => {
+const buildTaskRunner: TaskRunner<void> = async () => {
   cwd = changeCwdToGrafanaUi();
   distDir = `${cwd}/dist`;
   const pkg = require(`${cwd}/package.json`);
-
-  console.log(chalk.yellow(`Building ${pkg.name} @ ${pkg.version}`));
+  console.log(chalk.yellow(`Building ${pkg.name} (package.json version: ${pkg.version})`));
 
   await clean();
   await compile();
@@ -100,4 +71,6 @@ const buildTask: Task<void> = async () => {
   restoreCwd();
 };
 
-export default buildTask;
+export const buildTask = new Task<void>();
+buildTask.setName('@grafana/ui build');
+buildTask.setRunner(buildTaskRunner);

+ 86 - 54
scripts/cli/tasks/grafanaui.release.ts

@@ -1,14 +1,19 @@
 import execa from 'execa';
-import { Task } from '..';
 import { execTask } from '../utils/execTask';
 import { changeCwdToGrafanaUiDist, changeCwdToGrafanaUi } from '../utils/cwd';
 import semver from 'semver';
 import inquirer from 'inquirer';
 import chalk from 'chalk';
-import { startSpinner } from '../utils/startSpinner';
-import { savePackage } from './grafanaui.build';
+import { useSpinner } from '../utils/useSpinner';
+import { savePackage, buildTask } from './grafanaui.build';
+import { TaskRunner, Task } from './task';
 
-type VersionBumpType = 'patch' | 'minor' | 'major';
+type VersionBumpType = 'prerelease' | 'patch' | 'minor' | 'major';
+
+interface ReleaseTaskOptions {
+  publishToNpm: boolean;
+  usePackageJsonVersion: boolean;
+}
 
 const promptBumpType = async () => {
   return inquirer.prompt<{ type: VersionBumpType }>([
@@ -16,7 +21,7 @@ const promptBumpType = async () => {
       type: 'list',
       message: 'Select version bump',
       name: 'type',
-      choices: ['patch', 'minor', 'major'],
+      choices: ['prerelease', 'patch', 'minor', 'major'],
       validate: answer => {
         if (answer.length < 1) {
           return 'You must choose something';
@@ -28,13 +33,13 @@ const promptBumpType = async () => {
   ]);
 };
 
-const promptPrereleaseId = async () => {
+const promptPrereleaseId = async (message = 'Is this a prerelease?', allowNo = true) => {
   return inquirer.prompt<{ id: string }>([
     {
       type: 'list',
-      message: 'Is this a prerelease?',
+      message: message,
       name: 'id',
-      choices: ['no', 'alpha', 'beta'],
+      choices: allowNo ? ['no', 'alpha', 'beta'] : ['alpha', 'beta'],
       validate: answer => {
         if (answer.length < 1) {
           return 'You must choose something';
@@ -57,47 +62,45 @@ const promptConfirm = async (message?: string) => {
   ]);
 };
 
-const bumpVersion = async (version: string) => {
-  const spinner = startSpinner(`Saving version ${version} to package.json`);
-  changeCwdToGrafanaUi();
-
-  try {
+const bumpVersion = (version: string) =>
+  useSpinner<void>(`Saving version ${version} to package.json`, async () => {
+    changeCwdToGrafanaUi();
     await execa('npm', ['version', version]);
-    spinner.succeed();
-  } catch (e) {
-    console.log(e);
-    spinner.fail();
-  }
-
-  changeCwdToGrafanaUiDist();
-  const pkg = require(`${process.cwd()}/package.json`);
-  pkg.version = version;
-  await savePackage(`${process.cwd()}/package.json`, pkg);
-};
-
-const publishPackage = async (name: string, version: string) => {
-  changeCwdToGrafanaUiDist();
-  console.log(chalk.yellowBright.bold(`\nReview dist package.json before proceeding!\n`));
-  const { confirmed } = await promptConfirm('Are you ready to publish to npm?');
-
-  if (!confirmed) {
-    process.exit();
-  }
+    changeCwdToGrafanaUiDist();
+    const pkg = require(`${process.cwd()}/package.json`);
+    pkg.version = version;
+    await savePackage({ path: `${process.cwd()}/package.json`, pkg });
+  })();
+
+const publishPackage = (name: string, version: string) =>
+  useSpinner<void>(`Publishing ${name} @ ${version} to npm registry...`, async () => {
+    changeCwdToGrafanaUiDist();
+    console.log(chalk.yellowBright.bold(`\nReview dist package.json before proceeding!\n`));
+    const { confirmed } = await promptConfirm('Are you ready to publish to npm?');
+
+    if (!confirmed) {
+      process.exit();
+    }
+    await execa('npm', ['publish', '--access', 'public']);
+  })();
 
-  const spinner = startSpinner(`Publishing ${name} @ ${version} to npm registry...`);
+const ensureMasterBranch = async () => {
+  const currentBranch = await execa.stdout('git', ['symbolic-ref', '--short', 'HEAD']);
+  const status = await execa.stdout('git', ['status', '--porcelain']);
 
-  try {
-    await execa('npm', ['publish', '--access', 'public']);
-    spinner.succeed();
-  } catch (e) {
-    console.log(e);
-    spinner.fail();
+  if (currentBranch !== 'master' && status !== '') {
+    console.error(chalk.red.bold('You need to be on clean master branch to release @grafana/ui'));
     process.exit(1);
   }
 };
 
-const releaseTask: Task<void> = async () => {
-  await execTask('grafanaui.build');
+const releaseTaskRunner: TaskRunner<ReleaseTaskOptions> = async ({ publishToNpm, usePackageJsonVersion }) => {
+  if (publishToNpm) {
+    await ensureMasterBranch();
+  }
+
+  await execTask(buildTask)();
+
   let releaseConfirmed = false;
   let nextVersion;
   changeCwdToGrafanaUiDist();
@@ -107,27 +110,56 @@ const releaseTask: Task<void> = async () => {
   console.log(`Current version: ${pkg.version}`);
 
   do {
-    const { type } = await promptBumpType();
-    const { id } = await promptPrereleaseId();
-
-    if (id !== 'no') {
-      nextVersion = semver.inc(pkg.version, `pre${type}`, id);
+    if (!usePackageJsonVersion) {
+      const { type } = await promptBumpType();
+      console.log(type);
+      if (type === 'prerelease') {
+        const { id } = await promptPrereleaseId('What kind of prerelease?', false);
+        nextVersion = semver.inc(pkg.version, type, id);
+      } else {
+        const { id } = await promptPrereleaseId();
+        if (id !== 'no') {
+          nextVersion = semver.inc(pkg.version, `pre${type}`, id);
+        } else {
+          nextVersion = semver.inc(pkg.version, type);
+        }
+      }
     } else {
-      nextVersion = semver.inc(pkg.version, type);
+      nextVersion = pkg.version;
     }
 
     console.log(chalk.yellowBright.bold(`You are going to release a new version of ${pkg.name}`));
-    console.log(chalk.green(`Version bump: ${pkg.version} ->`), chalk.bold.yellowBright(`${nextVersion}`));
+
+    if (usePackageJsonVersion) {
+      console.log(chalk.green(`Version based on package.json: `), chalk.bold.yellowBright(`${nextVersion}`));
+    } else {
+      console.log(chalk.green(`Version bump: ${pkg.version} ->`), chalk.bold.yellowBright(`${nextVersion}`));
+    }
+
     const { confirmed } = await promptConfirm();
 
     releaseConfirmed = confirmed;
   } while (!releaseConfirmed);
 
-  await bumpVersion(nextVersion);
-  await publishPackage(pkg.name, nextVersion);
+  if (!usePackageJsonVersion) {
+    await bumpVersion(nextVersion);
+  }
 
-  console.log(chalk.green(`\nVersion ${nextVersion} of ${pkg.name} succesfully released!`));
-  console.log(chalk.yellow(`\nUpdated @grafana/ui/package.json with version bump created - COMMIT THIS FILE!`));
+  if (publishToNpm) {
+    await publishPackage(pkg.name, nextVersion);
+    console.log(chalk.green(`\nVersion ${nextVersion} of ${pkg.name} succesfully released!`));
+    console.log(chalk.yellow(`\nUpdated @grafana/ui/package.json with version bump created - COMMIT THIS FILE!`));
+    process.exit();
+  } else {
+    console.log(
+      chalk.green(
+        `\nVersion ${nextVersion} of ${pkg.name} succesfully prepared for release. See packages/grafana-ui/dist`
+      )
+    );
+    console.log(chalk.green(`\nTo publish to npm registry run`), chalk.bold.blue(`npm run gui:publish`));
+  }
 };
 
-export default releaseTask;
+export const releaseTask = new Task<ReleaseTaskOptions>();
+releaseTask.setName('@grafana/ui release');
+releaseTask.setRunner(releaseTaskRunner);

+ 23 - 0
scripts/cli/tasks/task.ts

@@ -0,0 +1,23 @@
+export type TaskRunner<T> = (options: T) => Promise<void>;
+
+export class Task<TOptions> {
+  name: string;
+  runner: (options: TOptions) => Promise<void>;
+  options: TOptions;
+
+  setName = name => {
+    this.name = name;
+  };
+
+  setRunner = (runner: TaskRunner<TOptions>) => {
+    this.runner = runner;
+  };
+
+  setOptions = options => {
+    this.options = options;
+  };
+
+  exec = () => {
+    return this.runner(this.options);
+  };
+}

+ 13 - 4
scripts/cli/utils/execTask.ts

@@ -1,6 +1,15 @@
-import { Task } from '..';
+import { Task } from '../tasks/task';
+import chalk from 'chalk';
 
-export const execTask = async <T>(taskName, options?: T) => {
-  const task = await import(`${__dirname}/../tasks/${taskName}.ts`);
-  return task.default(options) as Task<T>;
+export const execTask = <TOptions>(task: Task<TOptions>) => async (options: TOptions) => {
+  console.log(chalk.yellow(`Running ${chalk.bold(task.name)} task`));
+  task.setOptions(options);
+  try {
+    console.group();
+    await task.exec();
+    console.groupEnd();
+  } catch (e) {
+    console.log(e);
+    process.exit(1);
+  }
 };

+ 0 - 7
scripts/cli/utils/startSpinner.ts

@@ -1,7 +0,0 @@
-import ora from 'ora';
-
-export const startSpinner = (label: string) => {
-  const spinner = new ora(label);
-  spinner.start();
-  return spinner;
-};

+ 20 - 0
scripts/cli/utils/useSpinner.ts

@@ -0,0 +1,20 @@
+import ora from 'ora';
+
+type FnToSpin<T> = (options: T) => Promise<void>;
+
+export const useSpinner = <T>(spinnerLabel: string, fn: FnToSpin<T>, killProcess = true) => {
+  return async (options: T) => {
+    const spinner = new ora(spinnerLabel);
+    spinner.start();
+    try {
+      await fn(options);
+      spinner.succeed();
+    } catch (e) {
+      spinner.fail();
+      console.log(e);
+      if (killProcess) {
+        process.exit(1);
+      }
+    }
+  };
+};