Commit 73ef8649 by Dominik Prokop

Minor refactor of cli tasks (core start, gui publishing)

parent cee5f030
...@@ -123,10 +123,10 @@ ...@@ -123,10 +123,10 @@
}, },
"scripts": { "scripts": {
"dev": "webpack --progress --colors --mode development --config scripts/webpack/webpack.dev.js", "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": "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 --hot --theme", "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 --hot", "start:ignoreTheme": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts core:start --hot",
"watch": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --theme -d watch,start", "watch": "yarn start -d watch,start core:start --watchTheme ",
"build": "grunt build", "build": "grunt build",
"test": "grunt test", "test": "grunt test",
"tslint": "tslint -c tslint.json --project tsconfig.json", "tslint": "tslint -c tslint.json --project tsconfig.json",
...@@ -136,8 +136,11 @@ ...@@ -136,8 +136,11 @@
"storybook": "cd packages/grafana-ui && yarn storybook", "storybook": "cd packages/grafana-ui && yarn storybook",
"themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts", "themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts",
"prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"", "prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"",
"gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --build", "gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:build",
"gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --release" "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": { "husky": {
"hooks": { "hooks": {
......
import program from 'commander'; import program from 'commander';
import chalk from 'chalk';
import { execTask } from './utils/execTask'; 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 program
.option('-h, --hot', 'Runs front-end with hot reload enabled') .command('core:start')
.option('-t, --theme', 'Watches for theme changes and regenerates variables.scss files') .option('-h, --hot', 'Run front-end with HRM enabled')
.option('-d, --depreciate <scripts>', 'Inform about npm script deprecation', v => v.split(',')) .option('-t, --watchTheme', 'Watch for theme changes and regenerate variables.scss files')
.option('-b, --build', 'Created @grafana/ui build') .description('Starts Grafana front-end in development mode with watch enabled')
.option('-r, --release', 'Releases @grafana/ui to npm') .action(async cmd => {
.parse(process.argv); await execTask(startTask)({
watchThemes: cmd.theme,
hot: cmd.hot,
});
});
if (program.build) { program
execTask('grafanaui.build'); .command('gui:build')
} else if (program.release) { .description('Builds @grafana/ui package to packages/grafana-ui/dist')
execTask('grafanaui.release'); .action(async cmd => {
} else { await execTask(buildTask)();
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: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!`
)
);
} }
import concurrently from 'concurrently'; import concurrently from 'concurrently';
import { Task } from '..'; import { Task, TaskRunner } from './task';
interface StartTaskOptions { interface StartTaskOptions {
watchThemes: boolean; watchThemes: boolean;
hot: boolean; hot: boolean;
} }
const startTask: Task<StartTaskOptions> = async ({ watchThemes, hot }) => { const startTaskRunner: TaskRunner<StartTaskOptions> = async ({ watchThemes, hot }) => {
const jobs = []; const jobs = [
watchThemes && {
if (watchThemes) {
jobs.push({
command: 'nodemon -e ts -w ./packages/grafana-ui/src/themes -x yarn run themes:generate', command: 'nodemon -e ts -w ./packages/grafana-ui/src/themes -x yarn run themes:generate',
name: 'SASS variables generator', name: 'SASS variables generator',
}); },
} hot
? {
if (!hot) { command: 'webpack-dev-server --progress --colors --mode development --config scripts/webpack/webpack.hot.js',
jobs.push({ name: 'Dev server',
command: 'webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js', }
name: 'Webpack', : {
}); command: 'webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js',
} else { name: 'Webpack',
jobs.push({ },
command: 'webpack-dev-server --progress --colors --mode development --config scripts/webpack/webpack.hot.js', ];
name: 'Dev server',
});
}
try { try {
await concurrently(jobs, { await concurrently(jobs.filter(job => !!job), {
killOthers: ['failure', 'failure'], killOthers: ['failure', 'failure'],
}); });
} catch (e) { } catch (e) {
...@@ -38,4 +33,6 @@ const startTask: Task<StartTaskOptions> = async ({ watchThemes, hot }) => { ...@@ -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);
import execa from 'execa'; import execa from 'execa';
import fs from 'fs'; import fs from 'fs';
import { Task } from '..';
import { changeCwdToGrafanaUi, restoreCwd } from '../utils/cwd'; import { changeCwdToGrafanaUi, restoreCwd } from '../utils/cwd';
import chalk from 'chalk'; import chalk from 'chalk';
import { startSpinner } from '../utils/startSpinner'; import { useSpinner } from '../utils/useSpinner';
import { Task, TaskRunner } from './task';
let distDir, cwd; let distDir, cwd;
const clean = async () => { const clean = useSpinner<void>('Cleaning', async () => await execa('npm', ['run', 'clean']));
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 rollup = async () => { const compile = useSpinner<void>('Compiling sources', () => execa('tsc', ['-p', './tsconfig.build.json']));
const spinner = startSpinner('Bundling');
try { const rollup = useSpinner<void>('Bundling', () => execa('npm', ['run', 'build']));
await execa('npm', ['run', 'build']);
spinner.succeed();
} catch (e) {
spinner.fail();
}
};
export const savePackage = async (path, pkg) => {
const spinner = startSpinner('Updating package.json');
export const savePackage = useSpinner<{
path: string;
pkg: {};
}>('Updating package.json', async ({ path, pkg }) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.writeFile(path, JSON.stringify(pkg, null, 2), err => { fs.writeFile(path, JSON.stringify(pkg, null, 2), err => {
if (err) { if (err) {
spinner.fail();
console.error(err);
reject(err); reject(err);
return; return;
} }
spinner.succeed();
resolve(); resolve();
}); });
}); });
}; });
const preparePackage = async pkg => { const preparePackage = async pkg => {
pkg.main = 'index.js'; pkg.main = 'index.js';
pkg.types = 'index.d.ts'; 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 files = ['README.md', 'CHANGELOG.md', 'index.js'];
const spinner = startSpinner(`Moving ${files.join(', ')} files`); return useSpinner<void>(`Moving ${files.join(', ')} files`, async () => {
const promises = files.map(file => {
const promises = files.map(file => { return new Promise((resolve, reject) => {
return fs.copyFile(`${cwd}/${file}`, `${distDir}/${file}`, err => { fs.copyFile(`${cwd}/${file}`, `${distDir}/${file}`, err => {
if (err) { if (err) {
console.error(err); reject(err);
return; return;
} }
resolve();
});
});
}); });
});
try {
await Promise.all(promises); await Promise.all(promises);
spinner.succeed(); })();
} catch (e) {
spinner.fail();
}
}; };
const buildTask: Task<void> = async () => { const buildTaskRunner: TaskRunner<void> = async () => {
cwd = changeCwdToGrafanaUi(); cwd = changeCwdToGrafanaUi();
distDir = `${cwd}/dist`; distDir = `${cwd}/dist`;
const pkg = require(`${cwd}/package.json`); const pkg = require(`${cwd}/package.json`);
console.log(chalk.yellow(`Building ${pkg.name} (package.json version: ${pkg.version})`));
console.log(chalk.yellow(`Building ${pkg.name} @ ${pkg.version}`));
await clean(); await clean();
await compile(); await compile();
...@@ -100,4 +71,6 @@ const buildTask: Task<void> = async () => { ...@@ -100,4 +71,6 @@ const buildTask: Task<void> = async () => {
restoreCwd(); restoreCwd();
}; };
export default buildTask; export const buildTask = new Task<void>();
buildTask.setName('@grafana/ui build');
buildTask.setRunner(buildTaskRunner);
import execa from 'execa'; import execa from 'execa';
import { Task } from '..';
import { execTask } from '../utils/execTask'; import { execTask } from '../utils/execTask';
import { changeCwdToGrafanaUiDist, changeCwdToGrafanaUi } from '../utils/cwd'; import { changeCwdToGrafanaUiDist, changeCwdToGrafanaUi } from '../utils/cwd';
import semver from 'semver'; import semver from 'semver';
import inquirer from 'inquirer'; import inquirer from 'inquirer';
import chalk from 'chalk'; import chalk from 'chalk';
import { startSpinner } from '../utils/startSpinner'; import { useSpinner } from '../utils/useSpinner';
import { savePackage } from './grafanaui.build'; 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 () => { const promptBumpType = async () => {
return inquirer.prompt<{ type: VersionBumpType }>([ return inquirer.prompt<{ type: VersionBumpType }>([
...@@ -16,7 +20,7 @@ const promptBumpType = async () => { ...@@ -16,7 +20,7 @@ const promptBumpType = async () => {
type: 'list', type: 'list',
message: 'Select version bump', message: 'Select version bump',
name: 'type', name: 'type',
choices: ['patch', 'minor', 'major'], choices: ['prerelease', 'patch', 'minor', 'major'],
validate: answer => { validate: answer => {
if (answer.length < 1) { if (answer.length < 1) {
return 'You must choose something'; return 'You must choose something';
...@@ -28,13 +32,13 @@ const promptBumpType = async () => { ...@@ -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 }>([ return inquirer.prompt<{ id: string }>([
{ {
type: 'list', type: 'list',
message: 'Is this a prerelease?', message: message,
name: 'id', name: 'id',
choices: ['no', 'alpha', 'beta'], choices: allowNo ? ['no', 'alpha', 'beta'] : ['alpha', 'beta'],
validate: answer => { validate: answer => {
if (answer.length < 1) { if (answer.length < 1) {
return 'You must choose something'; return 'You must choose something';
...@@ -57,47 +61,31 @@ const promptConfirm = async (message?: string) => { ...@@ -57,47 +61,31 @@ const promptConfirm = async (message?: string) => {
]); ]);
}; };
const bumpVersion = async (version: string) => { const bumpVersion = (version: string) =>
const spinner = startSpinner(`Saving version ${version} to package.json`); useSpinner<void>(`Saving version ${version} to package.json`, async () => {
changeCwdToGrafanaUi(); changeCwdToGrafanaUi();
try {
await execa('npm', ['version', version]); await execa('npm', ['version', version]);
spinner.succeed(); changeCwdToGrafanaUiDist();
} catch (e) { const pkg = require(`${process.cwd()}/package.json`);
console.log(e); pkg.version = version;
spinner.fail(); await savePackage({ path: `${process.cwd()}/package.json`, pkg });
} })();
changeCwdToGrafanaUiDist(); const publishPackage = (name: string, version: string) =>
const pkg = require(`${process.cwd()}/package.json`); useSpinner<void>(`Publishing ${name} @ ${version} to npm registry...`, async () => {
pkg.version = version; changeCwdToGrafanaUiDist();
await savePackage(`${process.cwd()}/package.json`, pkg); console.log(chalk.yellowBright.bold(`\nReview dist package.json before proceeding!\n`));
}; const { confirmed } = await promptConfirm('Are you ready to publish to npm?');
const publishPackage = async (name: string, version: string) => { if (!confirmed) {
changeCwdToGrafanaUiDist(); process.exit();
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 {
await execa('npm', ['publish', '--access', 'public']); await execa('npm', ['publish', '--access', 'public']);
spinner.succeed(); })();
} catch (e) {
console.log(e); const releaseTaskRunner: TaskRunner<ReleaseTaskOptions> = async ({ publishToNpm }) => {
spinner.fail(); await execTask(buildTask)();
process.exit(1);
}
};
const releaseTask: Task<void> = async () => {
await execTask('grafanaui.build');
let releaseConfirmed = false; let releaseConfirmed = false;
let nextVersion; let nextVersion;
changeCwdToGrafanaUiDist(); changeCwdToGrafanaUiDist();
...@@ -108,12 +96,17 @@ const releaseTask: Task<void> = async () => { ...@@ -108,12 +96,17 @@ const releaseTask: Task<void> = async () => {
do { do {
const { type } = await promptBumpType(); const { type } = await promptBumpType();
const { id } = await promptPrereleaseId(); console.log(type);
if (type === 'prerelease') {
if (id !== 'no') { const { id } = await promptPrereleaseId('What kind of prerelease?', false);
nextVersion = semver.inc(pkg.version, `pre${type}`, id); nextVersion = semver.inc(pkg.version, type, id);
} else { } 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}`)); console.log(chalk.yellowBright.bold(`You are going to release a new version of ${pkg.name}`));
...@@ -124,10 +117,22 @@ const releaseTask: Task<void> = async () => { ...@@ -124,10 +117,22 @@ const releaseTask: Task<void> = async () => {
} while (!releaseConfirmed); } while (!releaseConfirmed);
await bumpVersion(nextVersion); await bumpVersion(nextVersion);
await publishPackage(pkg.name, nextVersion);
console.log(chalk.green(`\nVersion ${nextVersion} of ${pkg.name} succesfully released!`)); if (publishToNpm) {
console.log(chalk.yellow(`\nUpdated @grafana/ui/package.json with version bump created - COMMIT THIS FILE!`)); 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);
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);
};
}
import { Task } from '..'; import { Task } from '../tasks/task';
import chalk from 'chalk';
export const execTask = async <T>(taskName, options?: T) => { export const execTask = <TOptions>(task: Task<TOptions>) => async (options: TOptions) => {
const task = await import(`${__dirname}/../tasks/${taskName}.ts`); console.log(chalk.yellow(`Running ${chalk.bold(task.name)} task`));
return task.default(options) as Task<T>; task.setOptions(options);
try {
console.group();
await task.exec();
console.groupEnd();
} catch (e) {
console.log(e);
process.exit(1);
}
}; };
import ora from 'ora';
export const startSpinner = (label: string) => {
const spinner = new ora(label);
spinner.start();
return spinner;
};
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);
}
}
};
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment