diff --git a/package.json b/package.json index 9e0b88804fd..c5136cf5e02 100644 --- a/package.json +++ b/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": { diff --git a/scripts/cli/index.ts b/scripts/cli/index.ts index f980944e2cd..3e54dc97a07 100644 --- a/scripts/cli/index.ts +++ b/scripts/cli/index.ts @@ -1,33 +1,47 @@ 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 = (options: T) => Promise; +program.option('-d, --depreciate ', '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 ', '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') + .action(async cmd => { + await execTask(releaseTask)({ + publishToNpm: !!cmd.publish, + }); + }); + +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!` + ) + ); } diff --git a/scripts/cli/tasks/core.start.ts b/scripts/cli/tasks/core.start.ts index 4e546c71b8c..dbd2dfe7119 100644 --- a/scripts/cli/tasks/core.start.ts +++ b/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 = async ({ watchThemes, hot }) => { - const jobs = []; - - if (watchThemes) { - jobs.push({ +const startTaskRunner: TaskRunner = 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 = async ({ watchThemes, hot }) => { } }; -export default startTask; +export const startTask = new Task(); +startTask.setName('Core startTask'); +startTask.setRunner(startTaskRunner); diff --git a/scripts/cli/tasks/grafanaui.build.ts b/scripts/cli/tasks/grafanaui.build.ts index f892c13e115..6fce809bef9 100644 --- a/scripts/cli/tasks/grafanaui.build.ts +++ b/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('Cleaning', async () => await execa('npm', ['run', 'clean'])); -const rollup = async () => { - const spinner = startSpinner('Bundling'); +const compile = useSpinner('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('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(`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 = async () => { +const buildTaskRunner: TaskRunner = 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 = async () => { restoreCwd(); }; -export default buildTask; +export const buildTask = new Task(); +buildTask.setName('@grafana/ui build'); +buildTask.setRunner(buildTaskRunner); diff --git a/scripts/cli/tasks/grafanaui.release.ts b/scripts/cli/tasks/grafanaui.release.ts index 412c96e7fb5..b363ace35e0 100644 --- a/scripts/cli/tasks/grafanaui.release.ts +++ b/scripts/cli/tasks/grafanaui.release.ts @@ -1,14 +1,18 @@ 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; +} const promptBumpType = async () => { return inquirer.prompt<{ type: VersionBumpType }>([ @@ -16,7 +20,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 +32,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 +61,31 @@ 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(`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(); - } - - const spinner = startSpinner(`Publishing ${name} @ ${version} to npm registry...`); - - try { + 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(`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']); - spinner.succeed(); - } catch (e) { - console.log(e); - spinner.fail(); - process.exit(1); - } -}; + })(); + +const releaseTaskRunner: TaskRunner = async ({ publishToNpm }) => { + await execTask(buildTask)(); -const releaseTask: Task = async () => { - await execTask('grafanaui.build'); let releaseConfirmed = false; let nextVersion; changeCwdToGrafanaUiDist(); @@ -108,12 +96,17 @@ const releaseTask: Task = async () => { do { const { type } = await promptBumpType(); - const { id } = await promptPrereleaseId(); - - if (id !== 'no') { - nextVersion = semver.inc(pkg.version, `pre${type}`, id); + console.log(type); + if (type === 'prerelease') { + const { id } = await promptPrereleaseId('What kind of prerelease?', false); + nextVersion = semver.inc(pkg.version, type, id); } else { - nextVersion = semver.inc(pkg.version, type); + const { id } = await promptPrereleaseId(); + if (id !== 'no') { + nextVersion = semver.inc(pkg.version, `pre${type}`, id); + } else { + nextVersion = semver.inc(pkg.version, type); + } } console.log(chalk.yellowBright.bold(`You are going to release a new version of ${pkg.name}`)); @@ -124,10 +117,22 @@ const releaseTask: Task = async () => { } while (!releaseConfirmed); await bumpVersion(nextVersion); - 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!`)); + 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(); +releaseTask.setName('@grafana/ui release'); +releaseTask.setRunner(releaseTaskRunner); diff --git a/scripts/cli/tasks/task.ts b/scripts/cli/tasks/task.ts new file mode 100644 index 00000000000..d88860b7017 --- /dev/null +++ b/scripts/cli/tasks/task.ts @@ -0,0 +1,23 @@ +export type TaskRunner = (options: T) => Promise; + +export class Task { + name: string; + runner: (options: TOptions) => Promise; + options: TOptions; + + setName = name => { + this.name = name; + }; + + setRunner = (runner: TaskRunner) => { + this.runner = runner; + }; + + setOptions = options => { + this.options = options; + }; + + exec = () => { + return this.runner(this.options); + }; +} diff --git a/scripts/cli/utils/execTask.ts b/scripts/cli/utils/execTask.ts index 36071134331..f404206b7a9 100644 --- a/scripts/cli/utils/execTask.ts +++ b/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 (taskName, options?: T) => { - const task = await import(`${__dirname}/../tasks/${taskName}.ts`); - return task.default(options) as Task; +export const execTask = (task: Task) => 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); + } }; diff --git a/scripts/cli/utils/startSpinner.ts b/scripts/cli/utils/startSpinner.ts deleted file mode 100644 index ce895dec722..00000000000 --- a/scripts/cli/utils/startSpinner.ts +++ /dev/null @@ -1,7 +0,0 @@ -import ora from 'ora'; - -export const startSpinner = (label: string) => { - const spinner = new ora(label); - spinner.start(); - return spinner; -}; diff --git a/scripts/cli/utils/useSpinner.ts b/scripts/cli/utils/useSpinner.ts new file mode 100644 index 00000000000..48167e4ec2a --- /dev/null +++ b/scripts/cli/utils/useSpinner.ts @@ -0,0 +1,20 @@ +import ora from 'ora'; + +type FnToSpin = (options: T) => Promise; + +export const useSpinner = (spinnerLabel: string, fn: FnToSpin, 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); + } + } + }; +};