Commit 742e0d56 by Dominik Prokop Committed by GitHub

Toolkit: moved front end cli scripts to separate package and introduced very…

Toolkit: moved front end cli scripts to separate package and introduced very early version of plugin tools

* Move cli to grafana-toolkit

* Moving packages, fixing ts

* Add basics of plugin build task

* Add toolkit build task

* Circle - use node 10 for test-frontend

* Prettier fix

* First attempt for having shared tsconfig for plugins

* Add enzyme as peer depencency

* Do not expose internal commands when using toolkit from npm package

* Introduce plugin linting

* Fix missing file

* Fix shim extenstion

* Remove rollup typings

* Add tslint as dependency

* Toolkit - use the same versions of enzyme and tslint as core does

* Remove include property from plugin tsconfig

* Take failed suites into consideration when tests failed

* Set ts-jest preset for jest

* Cleanup tsconfig.plugins

* Add plugin:test task

* Rename file causing build failute

* Fixing those missed renames

* Add ts as peer dependency

* Remove enzyme dependency and tweak test plugin task

* Allow jest options overrides via package.json config

* Improvements

* Remove rollup node packages

* TMP : Fix ts errors when linked

* use local tslint if it exists

* support coverage commands

* Fix merge

* fix build

* Some minors

* Make jest pass when no tests discovered
parent ead4b1f5
......@@ -19,14 +19,11 @@
"@emotion/core": "10.0.10",
"@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1",
"@types/angular": "1.6.54",
"@types/chalk": "2.2.0",
"@types/classnames": "2.2.7",
"@types/clipboard": "2.0.1",
"@types/commander": "2.12.2",
"@types/d3": "4.13.1",
"@types/enzyme": "3.9.0",
"@types/expect-puppeteer": "3.3.1",
"@types/inquirer": "0.0.43",
"@types/jest": "24.0.13",
"@types/jquery": "1.10.35",
"@types/lodash": "4.14.123",
......@@ -49,16 +46,13 @@
"babel-jest": "24.8.0",
"babel-loader": "8.0.5",
"babel-plugin-angularjs-annotate": "0.10.0",
"chalk": "2.4.2",
"clean-webpack-plugin": "2.0.0",
"concurrently": "4.1.0",
"css-loader": "2.1.1",
"enzyme": "3.9.0",
"enzyme-adapter-react-16": "1.11.2",
"enzyme-to-json": "3.3.5",
"es6-promise": "3.3.1",
"es6-shim": "0.35.5",
"execa": "1.0.0",
"expect-puppeteer": "4.1.1",
"expect.js": "0.2.0",
"expose-loader": "0.7.5",
......@@ -83,7 +77,6 @@
"html-webpack-harddisk-plugin": "1.0.1",
"html-webpack-plugin": "3.2.0",
"husky": "1.3.1",
"inquirer": "6.2.2",
"jest": "24.8.0",
"jest-date-mock": "1.0.7",
"lint-staged": "8.1.5",
......@@ -98,7 +91,6 @@
"node-sass": "4.11.0",
"npm": "6.9.0",
"optimize-css-assets-webpack-plugin": "5.0.1",
"ora": "3.2.0",
"phantomjs-prebuilt": "2.1.16",
"pixelmatch": "4.0.2",
"pngjs": "3.4.0",
......@@ -115,8 +107,6 @@
"rimraf": "2.6.3",
"sass-lint": "1.12.1",
"sass-loader": "7.1.0",
"semver": "5.7.0",
"simple-git": "^1.112.0",
"sinon": "1.17.6",
"style-loader": "0.23.1",
"systemjs": "0.20.19",
......@@ -140,9 +130,9 @@
},
"scripts": {
"dev": "webpack --progress --colors --mode development --config scripts/webpack/webpack.dev.js",
"start": "npm run cli -- core:start --watchTheme",
"start:hot": "npm run cli -- core:start --hot --watchTheme",
"start:ignoreTheme": "npm run cli -- core:start --hot",
"start": "grafana-toolkit core:start --watchTheme",
"start:hot": "grafana-toolkit core:start --hot --watchTheme",
"start:ignoreTheme": "grafana-toolkit core:start --hot",
"watch": "yarn start -d watch,start core:start --watchTheme ",
"build": "grunt build",
"test": "grunt test",
......@@ -153,16 +143,15 @@
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
"storybook": "cd packages/grafana-ui && yarn storybook",
"storybook:build": "cd packages/grafana-ui && yarn storybook:build",
"themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts",
"prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"",
"prettier:write": "prettier --list-different \"**/*.{ts,tsx,scss}\" --write",
"cli": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts",
"gui:tslint": "tslint -c ./packages/grafana-ui/tslint.json --project ./packages/grafana-ui/tsconfig.json",
"gui:build": "npm run cli -- gui:build",
"gui:releasePrepare": "npm run cli -- gui:release",
"gui:build": "grafana-toolkit gui:build",
"gui:releasePrepare": "grafana-toolkit gui:release",
"gui:publish": "cd packages/grafana-ui/dist && npm publish --access public",
"gui:release": "npm run cli -- gui:release -p --createVersionCommit",
"precommit": "npm run cli -- precommit"
"gui:release": "grafana-toolkit gui:release -p --createVersionCommit",
"precommit": "grafana-toolkit precommit",
"themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts"
},
"husky": {
"hooks": {
......
# Grafana Toolkit
Make sure to run `yarn install` before trying anything! Otherwise you may see unknown command grafana-toolkit and spend a while tracking that down.
## Internal development
For development use `yarn link`. First, navigate to `packages/grafana-toolkit` and run `yarn link`. Then, in your project use `yarn link @grafana/toolkit` to use linked version.
## Grafana extensions development with grafana-toolkit overview
### Typescript
To configure Typescript create `tsconfig.json` file in the root dir of your app. grafana-toolkit comes with default tsconfig located in `packages/grafana-toolkit/src/config/tsconfig.plugin.ts`. In order for Typescript to be able to pickup your source files you need to extend that config as follows:
```json
{
"extends": "./node_modules/@grafana/toolkit/src/config/tsconfig.plugin.json",
"include": ["src"],
"compilerOptions": {
"rootDir": "./src",
"typeRoots": ["./node_modules/@types"]
}
}
```
### TSLint
grafana-toolkit comes with default config for TSLint, that's located in `packages/grafana-toolkit/src/config/tslint.plugin.ts`. As for now there is now way to customise TSLint config.
### Tests
grafana-toolkit comes with Jest as a test runner. It runs tests according to common config locted in `packages/grafana-toolkit/src/config/jest.plugin.config.ts`.
For now the config is not extendable, but our goal is to enable custom jest config via jest.config or package.json file. This might be required in the future if you want to use i.e. `enzyme-to-json` snapshots serializer. For that particular serializer we can also utilise it's API and add initialisation in the setup files (https://github.com/adriantoine/enzyme-to-json#serializer-in-unit-tests). We need to test that approach first.
#### Jest setup
We are not opinionated about tool used for implmenting tests. Internally at Grafana we use Enzyme. If you want to configure Enzyme as a testing utility, you need to configure enzyme-adapter-react. To do so, you need to create `[YOUR_APP]/config/jest-setup.ts` file that will provide React/Enzyme setup. Simply copy following code into that file to get Enzyme working with React:
```ts
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
```
grafana-toolkit will use that file as Jest's setup file. You can also setup Jest with shims of your needs by creating `jest-shim.ts` file in the same directory: `[YOUR_APP]/config/jest-shim.ts`
Adidtionaly, you can also provide additional Jest config via package.json file. For more details please refer to [Jest docs](https://jest-bot.github.io/jest/docs/configuration.html#verbose-boolean). Currently we support following properties:
- [`snapshotSerializers`](https://jest-bot.github.io/jest/docs/configuration.html#snapshotserializers-array-string)
## Prettier [todo]
## Development mode [todo]
TODO
- Enable rollup watch on extension sources
#!/usr/bin/env node
// This bin is used for cli installed from npm
require('../src/cli/index.js').run();
#!/usr/bin/env node
var path = require('path') ;
// This bin is used for cli executed internally
var tsProjectPath = path.resolve(__dirname, '../tsconfig.json');
require('ts-node').register({
project: tsProjectPath
});
require('../src/cli/index.ts').run(true);
{
"name": "@grafana/toolkit",
"version": "6.3.0-alpha.2",
"description": "Grafana Toolkit",
"keywords": [
"typescript",
"react",
"react-component"
],
"bin": {
"grafana-toolkit": "./bin/grafana-toolkit.js"
},
"scripts": {
"tslint": "tslint -c tslint.json --project tsconfig.json",
"typecheck": "tsc --noEmit",
"precommit": "npm run tslint & npm run typecheck",
"clean": "rimraf ./dist ./compiled"
},
"author": "Grafana Labs",
"license": "Apache-2.0",
"dependencies": {
"@types/execa": "^0.9.0",
"@types/inquirer": "^6.0.3",
"@types/jest": "24.0.13",
"@types/jest-cli": "^23.6.0",
"@types/node": "^12.0.4",
"@types/prettier": "^1.16.4",
"@types/semver": "^6.0.0",
"axios": "0.19.0",
"chalk": "^2.4.2",
"commander": "^2.20.0",
"concurrently": "4.1.0",
"execa": "^1.0.0",
"glob": "^7.1.4",
"inquirer": "^6.3.1",
"jest-cli": "^24.8.0",
"lodash": "4.17.11",
"ora": "^3.4.0",
"prettier": "^1.17.1",
"replace-in-file": "^4.1.0",
"rollup": "^1.14.2",
"rollup-plugin-commonjs": "^10.0.0",
"rollup-plugin-copy-glob": "^0.3.0",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^5.1.0",
"rollup-plugin-sourcemaps": "^0.4.2",
"rollup-plugin-terser": "^5.0.0",
"rollup-plugin-typescript2": "^0.21.1",
"rollup-plugin-visualizer": "^1.1.1",
"semver": "^6.1.1",
"simple-git": "^1.112.0",
"ts-node": "^8.2.0",
"tslint": "5.14.0"
},
"peerDependencies": {
"jest": "24.8.0",
"ts-jest": "24.0.2",
"tslib": "1.10.0",
"typescript": "3.5.1"
},
"resolutions": {
"@types/lodash": "4.14.119",
"rollup-plugin-typescript2": "0.21.1"
},
"devDependencies": {
"@types/glob": "^7.1.1",
"rollup-watch": "^4.3.1"
}
}
// @ts-ignore
import program from 'commander';
import { execTask } from './utils/execTask';
import chalk from 'chalk';
......@@ -6,13 +7,19 @@ import { buildTask } from './tasks/grafanaui.build';
import { releaseTask } from './tasks/grafanaui.release';
import { changelogTask } from './tasks/changelog';
import { cherryPickTask } from './tasks/cherrypick';
import { closeMilestoneTask } from './tasks/closeMilestone';
import { precommitTask } from './tasks/precommit';
import { templateTask } from './tasks/template';
import { pluginBuildTask } from './tasks/plugin.build';
import { toolkitBuildTask } from './tasks/toolkit.build';
import { pluginTestTask } from './tasks/plugin.tests';
import { searchTestDataSetupTask } from './tasks/searchTestDataSetup';
import { closeMilestoneTask } from './tasks/closeMilestone';
import { pluginDevTask } from './tasks/plugin.dev';
program.option('-d, --depreciate <scripts>', 'Inform about npm script deprecation', v => v.split(','));
program
export const run = (includeInternalScripts = false) => {
if (includeInternalScripts) {
program.option('-d, --depreciate <scripts>', 'Inform about npm script deprecation', v => v.split(','));
program
.command('core:start')
.option('-h, --hot', 'Run front-end with HRM enabled')
.option('-t, --watchTheme', 'Watch for theme changes and regenerate variables.scss files')
......@@ -24,14 +31,15 @@ program
});
});
program
program
.command('gui:build')
.description('Builds @grafana/ui package to packages/grafana-ui/dist')
.action(async cmd => {
// @ts-ignore
await execTask(buildTask)();
});
program
program
.command('gui:release')
.description('Prepares @grafana/ui release (and publishes to npm on demand)')
.option('-p, --publish', 'Publish @grafana/ui to npm registry')
......@@ -45,7 +53,7 @@ program
});
});
program
program
.command('changelog')
.option('-m, --milestone <milestone>', 'Specify milestone')
.description('Builds changelog markdown')
......@@ -60,14 +68,44 @@ program
});
});
program
program
.command('cherrypick')
.description('Helps find commits to cherry pick')
.action(async cmd => {
await execTask(cherryPickTask)({});
});
program
program
.command('precommit')
.description('Executes checks')
.action(async cmd => {
await execTask(precommitTask)({});
});
program
.command('debug:template')
.description('Just testing')
.action(async cmd => {
await execTask(templateTask)({});
});
program
.command('toolkit:build')
.description('Prepares grafana/toolkit dist package')
.action(async cmd => {
// @ts-ignore
await execTask(toolkitBuildTask)();
});
program
.command('searchTestData')
.option('-c, --count <number_of_dashboards>', 'Specify number of dashboards')
.description('Setup test data for search')
.action(async cmd => {
await execTask(searchTestDataSetupTask)({ count: cmd.count });
});
program
.command('close-milestone')
.option('-m, --milestone <milestone>', 'Specify milestone')
.description('Helps ends a milestone by removing the cherry-pick label and closing it')
......@@ -81,28 +119,48 @@ program
milestone: cmd.milestone,
});
});
}
program
.command('precommit')
.description('Executes checks')
program
.command('plugin:build')
.description('Prepares plugin dist package')
.action(async cmd => {
await execTask(precommitTask)({});
await execTask(pluginBuildTask)({});
});
program
.command('searchTestData')
.option('-c, --count <number_of_dashboards>', 'Specify number of dashboards')
.description('Setup test data for search')
program
.command('plugin:dev')
.description('Starts plugin dev mode')
.action(async cmd => {
await execTask(searchTestDataSetupTask)({ count: cmd.count });
await execTask(pluginDevTask)({
watch: true,
});
});
program.parse(process.argv);
program
.command('plugin:test')
.option('-u, --updateSnapshot', 'Run snapshots update')
.option('--coverage', 'Run code coverage')
.description('Executes plugin tests')
.action(async cmd => {
await execTask(pluginTestTask)({
updateSnapshot: !!cmd.updateSnapshot,
coverage: !!cmd.coverage,
});
});
if (program.depreciate && program.depreciate.length === 2) {
program.on('command:*', () => {
console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' '));
process.exit(1);
});
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 _ from 'lodash';
import axios from 'axios';
// @ts-ignore
import * as _ from 'lodash';
import { Task, TaskRunner } from './task';
import GithubClient from '../utils/githubClient';
......@@ -27,11 +29,11 @@ const changelogTaskRunner: TaskRunner<ChangelogOptions> = async ({ milestone })
const issues = res.data;
const bugs = _.sortBy(
issues.filter(item => {
issues.filter((item: any) => {
if (item.title.match(/fix|fixes/i)) {
return true;
}
if (item.labels.find(label => label.name === 'type/bug')) {
if (item.labels.find((label: any) => label.name === 'type/bug')) {
return true;
}
return false;
......@@ -39,7 +41,7 @@ const changelogTaskRunner: TaskRunner<ChangelogOptions> = async ({ milestone })
'title'
);
const notBugs = _.sortBy(issues.filter(item => !bugs.find(bug => bug === item)), 'title');
const notBugs = _.sortBy(issues.filter((item: any) => !bugs.find((bug: any) => bug === item)), 'title');
let markdown = '';
......@@ -65,7 +67,7 @@ const changelogTaskRunner: TaskRunner<ChangelogOptions> = async ({ milestone })
function getMarkdownLineForIssue(item: any) {
const githubGrafanaUrl = 'https://github.com/grafana/grafana';
let markdown = '';
const title = item.title.replace(/^([^:]*)/, (match, g1) => {
const title = item.title.replace(/^([^:]*)/, (_match: any, g1: any) => {
return `**${g1}**`;
});
......@@ -78,6 +80,4 @@ function getMarkdownLineForIssue(item: any) {
return markdown;
}
export const changelogTask = new Task<ChangelogOptions>();
changelogTask.setName('Changelog generator task');
changelogTask.setRunner(changelogTaskRunner);
export const changelogTask = new Task<ChangelogOptions>('Changelog generator task', changelogTaskRunner);
......@@ -15,7 +15,7 @@ const cherryPickRunner: TaskRunner<CherryPickOptions> = async () => {
});
// sort by closed date ASC
res.data.sort(function(a, b) {
res.data.sort((a: any, b: any) => {
return new Date(a.closed_at).getTime() - new Date(b.closed_at).getTime();
});
......@@ -42,6 +42,4 @@ const cherryPickRunner: TaskRunner<CherryPickOptions> = async () => {
console.log(commands);
};
export const cherryPickTask = new Task<CherryPickOptions>();
cherryPickTask.setName('Cherry pick task');
cherryPickTask.setRunner(cherryPickRunner);
export const cherryPickTask = new Task<CherryPickOptions>('Cherry pick task', cherryPickRunner);
......@@ -70,6 +70,7 @@ const closeMilestoneTaskRunner: TaskRunner<CloseMilestoneOptions> = async ({ mil
}
};
export const closeMilestoneTask = new Task<CloseMilestoneOptions>();
closeMilestoneTask.setName('Close Milestone generator task');
closeMilestoneTask.setRunner(closeMilestoneTaskRunner);
export const closeMilestoneTask = new Task<CloseMilestoneOptions>(
'Close Milestone generator task',
closeMilestoneTaskRunner
);
//@ts-ignore
import concurrently from 'concurrently';
import { Task, TaskRunner } from './task';
......@@ -33,6 +34,4 @@ const startTaskRunner: TaskRunner<StartTaskOptions> = async ({ watchThemes, hot
}
};
export const startTask = new Task<StartTaskOptions>();
startTask.setName('Core startTask');
startTask.setRunner(startTaskRunner);
export const startTask = new Task<StartTaskOptions>('Core startTask', startTaskRunner);
import execa from 'execa';
import fs from 'fs';
import execa = require('execa');
// @ts-ignore
import * as fs from 'fs';
import { changeCwdToGrafanaUi, restoreCwd } from '../utils/cwd';
import chalk from 'chalk';
import { useSpinner } from '../utils/useSpinner';
import { Task, TaskRunner } from './task';
let distDir, cwd;
let distDir: string, cwd: string;
// @ts-ignore
export const clean = useSpinner<void>('Cleaning', async () => await execa('npm', ['run', 'clean']));
// @ts-ignore
const compile = useSpinner<void>('Compiling sources', () => execa('tsc', ['-p', './tsconfig.build.json']));
// @ts-ignore
const rollup = useSpinner<void>('Bundling', () => execa('npm', ['run', 'build']));
export const savePackage = useSpinner<{
interface SavePackageOptions {
path: string;
pkg: {};
}>('Updating package.json', async ({ path, pkg }) => {
}
// @ts-ignore
export const savePackage = useSpinner<SavePackageOptions>(
'Updating package.json',
// @ts-ignore
async ({ path, pkg }: SavePackageOptions) => {
return new Promise((resolve, reject) => {
fs.writeFile(path, JSON.stringify(pkg, null, 2), err => {
if (err) {
......@@ -26,9 +36,10 @@ export const savePackage = useSpinner<{
resolve();
});
});
});
}
);
const preparePackage = async pkg => {
const preparePackage = async (pkg: any) => {
pkg.main = 'index.js';
pkg.types = 'index.d.ts';
await savePackage({
......@@ -39,6 +50,7 @@ const preparePackage = async pkg => {
const moveFiles = () => {
const files = ['README.md', 'CHANGELOG.md', 'index.js'];
// @ts-ignore
return useSpinner<void>(`Moving ${files.join(', ')} files`, async () => {
const promises = files.map(file => {
return new Promise((resolve, reject) => {
......@@ -71,6 +83,4 @@ const buildTaskRunner: TaskRunner<void> = async () => {
restoreCwd();
};
export const buildTask = new Task<void>();
buildTask.setName('@grafana/ui build');
buildTask.setRunner(buildTaskRunner);
export const buildTask = new Task<void>('@grafana/ui build', buildTaskRunner);
import execa from 'execa';
import execa = require('execa');
import { execTask } from '../utils/execTask';
import { changeCwdToGrafanaUiDist, changeCwdToGrafanaUi, restoreCwd } from '../utils/cwd';
import semver from 'semver';
import inquirer from 'inquirer';
import { ReleaseType, inc } from 'semver';
import { prompt } from 'inquirer';
import chalk from 'chalk';
import { useSpinner } from '../utils/useSpinner';
import { savePackage, buildTask, clean } from './grafanaui.build';
import { TaskRunner, Task } from './task';
type VersionBumpType = 'prerelease' | 'patch' | 'minor' | 'major';
interface ReleaseTaskOptions {
publishToNpm: boolean;
usePackageJsonVersion: boolean;
......@@ -16,43 +17,29 @@ interface ReleaseTaskOptions {
}
const promptBumpType = async () => {
return inquirer.prompt<{ type: VersionBumpType }>([
return prompt<{ type: VersionBumpType }>([
{
type: 'list',
message: 'Select version bump',
name: 'type',
choices: ['prerelease', 'patch', 'minor', 'major'],
validate: answer => {
if (answer.length < 1) {
return 'You must choose something';
}
return true;
},
},
]);
};
const promptPrereleaseId = async (message = 'Is this a prerelease?', allowNo = true) => {
return inquirer.prompt<{ id: string }>([
return prompt<{ id: string }>([
{
type: 'list',
message: message,
name: 'id',
choices: allowNo ? ['no', 'alpha', 'beta'] : ['alpha', 'beta'],
validate: answer => {
if (answer.length < 1) {
return 'You must choose something';
}
return true;
},
},
]);
};
const promptConfirm = async (message?: string) => {
return inquirer.prompt<{ confirmed: boolean }>([
return prompt<{ confirmed: boolean }>([
{
type: 'confirm',
message: message || 'Is that correct?',
......@@ -64,11 +51,18 @@ const promptConfirm = async (message?: string) => {
// Since Grafana core depends on @grafana/ui highly, we run full check before release
const runChecksAndTests = async () =>
// @ts-ignore
useSpinner<void>(`Running checks and tests`, async () => {
try {
await execa('npm', ['run', 'test']);
} catch (e) {
console.log(e);
throw e;
}
})();
const bumpVersion = (version: string) =>
// @ts-ignore
useSpinner<void>(`Saving version ${version} to package.json`, async () => {
changeCwdToGrafanaUi();
await execa('npm', ['version', version]);
......@@ -79,6 +73,7 @@ const bumpVersion = (version: string) =>
})();
const publishPackage = (name: string, version: string) =>
// @ts-ignore
useSpinner<void>(`Publishing ${name} @ ${version} to npm registry...`, async () => {
changeCwdToGrafanaUiDist();
await execa('npm', ['publish', '--access', 'public']);
......@@ -95,6 +90,7 @@ const ensureMasterBranch = async () => {
};
const prepareVersionCommitAndPush = async (version: string) =>
// @ts-ignore
useSpinner<void>('Commiting and pushing @grafana/ui version update', async () => {
await execa.stdout('git', ['commit', '-a', '-m', `Upgrade @grafana/ui version to v${version}`]);
await execa.stdout('git', ['push']);
......@@ -106,6 +102,7 @@ const releaseTaskRunner: TaskRunner<ReleaseTaskOptions> = async ({
createVersionCommit,
}) => {
changeCwdToGrafanaUi();
// @ts-ignore
await clean(); // Clean previous build if exists
restoreCwd();
......@@ -117,7 +114,7 @@ const releaseTaskRunner: TaskRunner<ReleaseTaskOptions> = async ({
await runChecksAndTests();
await execTask(buildTask)();
await execTask(buildTask)({} as any);
let releaseConfirmed = false;
let nextVersion;
......@@ -133,13 +130,13 @@ const releaseTaskRunner: TaskRunner<ReleaseTaskOptions> = async ({
console.log(type);
if (type === 'prerelease') {
const { id } = await promptPrereleaseId('What kind of prerelease?', false);
nextVersion = semver.inc(pkg.version, type, id);
nextVersion = inc(pkg.version, type, id as any);
} else {
const { id } = await promptPrereleaseId();
if (id !== 'no') {
nextVersion = semver.inc(pkg.version, `pre${type}`, id);
nextVersion = inc(pkg.version, `pre${type}` as ReleaseType, id as any);
} else {
nextVersion = semver.inc(pkg.version, type);
nextVersion = inc(pkg.version, type as ReleaseType);
}
}
} else {
......@@ -190,6 +187,4 @@ const releaseTaskRunner: TaskRunner<ReleaseTaskOptions> = async ({
}
};
export const releaseTask = new Task<ReleaseTaskOptions>();
releaseTask.setName('@grafana/ui release');
releaseTask.setRunner(releaseTaskRunner);
export const releaseTask = new Task<ReleaseTaskOptions>('@grafana/ui release', releaseTaskRunner);
import { Task, TaskRunner } from './task';
// @ts-ignore
import execa = require('execa');
import path = require('path');
import fs = require('fs');
import glob = require('glob');
import * as rollup from 'rollup';
import { inputOptions, outputOptions } from '../../config/rollup.plugin.config';
import { useSpinner } from '../utils/useSpinner';
import { Linter, Configuration, RuleFailure } from 'tslint';
import { testPlugin } from './plugin/tests';
interface PrecommitOptions {}
// @ts-ignore
export const clean = useSpinner<void>('Cleaning', async () => await execa('rimraf', ['./dist']));
// @ts-ignore
const typecheckPlugin = useSpinner<void>('Typechecking', async () => {
await execa('tsc', ['--noEmit']);
});
// @ts-ignore
const lintPlugin = useSpinner<void>('Linting', async () => {
let tsLintConfigPath = path.resolve(process.cwd(), 'tslint.json');
if (!fs.existsSync(tsLintConfigPath)) {
tsLintConfigPath = path.resolve(__dirname, '../../config/tslint.plugin.json');
}
const globPattern = path.resolve(process.cwd(), 'src/**/*.+(ts|tsx)');
const sourcesToLint = glob.sync(globPattern);
const options = {
fix: true, // or fail
formatter: 'json',
};
const configuration = Configuration.findConfiguration(tsLintConfigPath).results;
const lintResults = sourcesToLint
.map(fileName => {
const linter = new Linter(options);
const fileContents = fs.readFileSync(fileName, 'utf8');
linter.lint(fileName, fileContents, configuration);
return linter.getResult();
})
.filter(result => {
return result.errorCount > 0 || result.warningCount > 0;
});
if (lintResults.length > 0) {
console.log('\n');
const failures = lintResults.reduce<RuleFailure[]>((failures, result) => {
return [...failures, ...result.failures];
}, []);
failures.forEach(f => {
// tslint:disable-next-line
console.log(
`${f.getRuleSeverity() === 'warning' ? 'WARNING' : 'ERROR'}: ${f.getFileName().split('src')[1]}[${
f.getStartPosition().getLineAndCharacter().line
}:${f.getStartPosition().getLineAndCharacter().character}]: ${f.getFailure()}`
);
});
console.log('\n');
throw new Error(`${failures.length} linting errors found in ${lintResults.length} files`);
}
});
const bundlePlugin = useSpinner<void>('Bundling plugin', async () => {
// @ts-ignore
const bundle = await rollup.rollup(inputOptions());
// TODO: we can work on more verbose output
await bundle.generate(outputOptions);
await bundle.write(outputOptions);
});
const pluginBuildRunner: TaskRunner<PrecommitOptions> = async () => {
await clean();
// @ts-ignore
await lintPlugin();
await testPlugin({ updateSnapshot: false, coverage: false });
// @ts-ignore
await bundlePlugin();
};
export const pluginBuildTask = new Task<PrecommitOptions>('Build plugin', pluginBuildRunner);
import { Task, TaskRunner } from './task';
import { bundlePlugin, PluginBundleOptions } from './plugin/bundle';
const pluginDevRunner: TaskRunner<PluginBundleOptions> = async options => {
const result = await bundlePlugin(options);
return result;
};
export const pluginDevTask = new Task<PluginBundleOptions>('Dev plugin', pluginDevRunner);
import { Task, TaskRunner } from './task';
import { testPlugin, PluginTestOptions } from './plugin/tests';
const pluginTestRunner: TaskRunner<PluginTestOptions> = async options => {
await testPlugin(options);
};
export const pluginTestTask = new Task<PluginTestOptions>('Test plugin', pluginTestRunner);
import path = require('path');
import * as jestCLI from 'jest-cli';
import * as rollup from 'rollup';
import { inputOptions, outputOptions } from '../../../config/rollup.plugin.config';
export interface PluginBundleOptions {
watch: boolean;
}
export const bundlePlugin = async ({ watch }: PluginBundleOptions) => {
if (watch) {
const watcher = rollup.watch([
{
...inputOptions(),
output: outputOptions,
watch: {
chokidar: true,
clearScreen: true,
},
},
]);
} else {
// @ts-ignore
const bundle = await rollup.rollup(inputOptions());
// TODO: we can work on more verbose output
await bundle.generate(outputOptions);
await bundle.write(outputOptions);
}
};
import path = require('path');
import * as jestCLI from 'jest-cli';
import { useSpinner } from '../../utils/useSpinner';
import { jestConfig } from '../../../config/jest.plugin.config';
export interface PluginTestOptions {
updateSnapshot: boolean;
coverage: boolean;
}
export const testPlugin = useSpinner<PluginTestOptions>('Running tests', async ({ updateSnapshot, coverage }) => {
const testConfig = jestConfig();
testConfig.updateSnapshot = updateSnapshot;
testConfig.coverage = coverage;
const results = await jestCLI.runCLI(testConfig as any, [process.cwd()]);
if (results.results.numFailedTests > 0 || results.results.numFailedTestSuites > 0) {
throw new Error('Tests failed');
}
});
import { Task, TaskRunner } from './task';
import chalk from 'chalk';
// @ts-ignore
import get from 'lodash/get';
// @ts-ignore
import flatten from 'lodash/flatten';
import execa = require('execa');
const simpleGit = require('simple-git/promise')(process.cwd());
......@@ -28,13 +30,18 @@ const tasks = {
const precommitRunner: TaskRunner<PrecommitOptions> = async () => {
const status = await simpleGit.status();
const sassFiles = status.files.filter(
file => (file.path as string).match(/^[a-zA-Z0-9\_\-\/]+(\.scss)$/g) || file.path.indexOf('.sass-lint.yml') > -1
(file: any) =>
(file.path as string).match(/^[a-zA-Z0-9\_\-\/]+(\.scss)$/g) || file.path.indexOf('.sass-lint.yml') > -1
);
const tsFiles = status.files.filter(file => (file.path as string).match(/^[a-zA-Z0-9\_\-\/]+(\.(ts|tsx))$/g));
const testFiles = status.files.filter(file => (file.path as string).match(/^[a-zA-Z0-9\_\-\/]+(\.test.(ts|tsx))$/g));
const goTestFiles = status.files.filter(file => (file.path as string).match(/^[a-zA-Z0-9\_\-\/]+(\_test.go)$/g));
const grafanaUiFiles = tsFiles.filter(file => (file.path as string).indexOf('grafana-ui') > -1);
const tsFiles = status.files.filter((file: any) => (file.path as string).match(/^[a-zA-Z0-9\_\-\/]+(\.(ts|tsx))$/g));
const testFiles = status.files.filter((file: any) =>
(file.path as string).match(/^[a-zA-Z0-9\_\-\/]+(\.test.(ts|tsx))$/g)
);
const goTestFiles = status.files.filter((file: any) =>
(file.path as string).match(/^[a-zA-Z0-9\_\-\/]+(\_test.go)$/g)
);
const grafanaUiFiles = tsFiles.filter((file: any) => (file.path as string).indexOf('grafana-ui') > -1);
const grafanaUIFilesChangedOnly = tsFiles.length > 0 && tsFiles.length - grafanaUiFiles.length === 0;
const coreFilesChangedOnly = tsFiles.length > 0 && grafanaUiFiles.length === 0;
......@@ -69,13 +76,13 @@ const precommitRunner: TaskRunner<PrecommitOptions> = async () => {
const task = execa('grunt', gruntTasks);
// @ts-ignore
const stream = task.stdout;
if (stream) {
stream.pipe(process.stdout);
}
return task;
}
console.log(chalk.yellow('Skipping precommit checks, not front-end changes detected'));
return;
};
export const precommitTask = new Task<PrecommitOptions>();
precommitTask.setName('Precommit task');
precommitTask.setRunner(precommitRunner);
export const precommitTask = new Task<PrecommitOptions>('Precommit task', precommitRunner);
import axios from 'axios';
import _ from 'lodash';
import { Task, TaskRunner } from './task';
interface SearchTestDataSetupOptions {
......@@ -14,7 +13,7 @@ const client = axios.create({
},
});
export async function getUser(user): Promise<any> {
export async function getUser(user: any): Promise<any> {
console.log('Creating user ' + user.name);
const search = await client.get('/users/search', {
params: { query: user.login },
......@@ -112,6 +111,7 @@ const searchTestDataSetupRunnner: TaskRunner<SearchTestDataSetupOptions> = async
}
};
export const searchTestDataSetupTask = new Task<SearchTestDataSetupOptions>();
searchTestDataSetupTask.setName('Search test data setup');
searchTestDataSetupTask.setRunner(searchTestDataSetupRunnner);
export const searchTestDataSetupTask = new Task<SearchTestDataSetupOptions>(
'Search test data setup',
searchTestDataSetupRunnner
);
export type TaskRunner<T> = (options: T) => Promise<any>;
export class Task<TOptions> {
name: string;
runner: (options: TOptions) => Promise<any>;
options: TOptions;
options: TOptions = {} as any;
setName = name => {
constructor(public name: string, public runner: TaskRunner<TOptions>) {}
setName = (name: string) => {
this.name = name;
};
......@@ -13,7 +12,7 @@ export class Task<TOptions> {
this.runner = runner;
};
setOptions = options => {
setOptions = (options: TOptions) => {
this.options = options;
};
......
import { Task, TaskRunner } from './task';
interface TemplateOptions {}
const templateRunner: TaskRunner<TemplateOptions> = async () => {
console.log('Template task');
};
export const templateTask = new Task<TemplateOptions>('Template task', templateRunner);
import execa = require('execa');
import * as fs from 'fs';
import { changeCwdToGrafanaUi, restoreCwd, changeCwdToGrafanaToolkit } from '../utils/cwd';
import chalk from 'chalk';
import { useSpinner } from '../utils/useSpinner';
import { Task, TaskRunner } from './task';
let distDir: string, cwd: string;
// @ts-ignore
export const clean = useSpinner<void>('Cleaning', async () => await execa('npm', ['run', 'clean']));
// @ts-ignore
const compile = useSpinner<void>('Compiling sources', async () => {
try {
await execa('tsc', ['-p', './tsconfig.json']);
} catch (e) {
console.log(e);
throw e;
}
});
// @ts-ignore
export const savePackage = useSpinner<{
path: string;
pkg: {};
// @ts-ignore
}>('Updating package.json', async ({ path, pkg }) => {
return new Promise((resolve, reject) => {
fs.writeFile(path, JSON.stringify(pkg, null, 2), err => {
if (err) {
reject(err);
return;
}
resolve();
});
});
});
const preparePackage = async (pkg: any) => {
pkg.bin = {
'grafana-toolkit': './bin/grafana-toolkit.dist.js',
};
await savePackage({
path: `${cwd}/dist/package.json`,
pkg,
});
};
const moveFiles = () => {
const files = [
'README.md',
'CHANGELOG.md',
'bin/grafana-toolkit.dist.js',
'src/config/tsconfig.plugin.json',
'src/config/tslint.plugin.json',
];
// @ts-ignore
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();
});
});
});
await Promise.all(promises);
})();
};
const toolkitBuildTaskRunner: TaskRunner<void> = async () => {
cwd = changeCwdToGrafanaToolkit();
distDir = `${cwd}/dist`;
const pkg = require(`${cwd}/package.json`);
console.log(chalk.yellow(`Building ${pkg.name} (package.json version: ${pkg.version})`));
await clean();
await compile();
await preparePackage(pkg);
fs.mkdirSync('./dist/bin');
await moveFiles();
restoreCwd();
};
export const toolkitBuildTask = new Task<void>('@grafana/toolkit build', toolkitBuildTaskRunner);
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"module": "commonjs"
}
}
......@@ -5,6 +5,11 @@ export const changeCwdToGrafanaUi = () => {
return process.cwd();
};
export const changeCwdToGrafanaToolkit = () => {
process.chdir(`${cwd}/packages/grafana-toolkit`);
return process.cwd();
};
export const changeCwdToGrafanaUiDist = () => {
process.chdir(`${cwd}/packages/grafana-ui/dist`);
};
......
......@@ -20,6 +20,7 @@ describe('GithubClient', () => {
describe('#client', () => {
it('it should contain a client', () => {
// @ts-ignore
const spy = jest.spyOn(GithubClient.prototype, 'createClient').mockImplementation(() => fakeClient);
const github = new GithubClient();
......@@ -40,6 +41,7 @@ describe('GithubClient', () => {
process.env.GITHUB_USERNAME = username;
process.env.GITHUB_ACCESS_TOKEN = token;
// @ts-ignore
const spy = jest.spyOn(GithubClient.prototype, 'createClient').mockImplementation(() => fakeClient);
const github = new GithubClient(true);
......@@ -57,6 +59,7 @@ describe('GithubClient', () => {
describe('when the credentials are not defined', () => {
it('should throw an error', () => {
expect(() => {
// tslint:disable-next-line
new GithubClient(true);
}).toThrow(/operation needs a GITHUB_USERNAME and GITHUB_ACCESS_TOKEN environment variables/);
});
......
......@@ -10,7 +10,7 @@ export const useSpinner = <T>(spinnerLabel: string, fn: FnToSpin<T>, killProcess
await fn(options);
spinner.succeed();
} catch (e) {
spinner.fail(e);
spinner.fail(e.message || e);
if (killProcess) {
process.exit(1);
}
......
import path = require('path');
import fs = require('fs');
const whitelistedJestConfigOverrides = ['snapshotSerializers'];
export const jestConfig = () => {
const jestConfigOverrides = require(path.resolve(process.cwd(), 'package.json')).jest;
const blacklistedOverrides = jestConfigOverrides
? Object.keys(jestConfigOverrides).filter(override => whitelistedJestConfigOverrides.indexOf(override) === -1)
: [];
if (blacklistedOverrides.length > 0) {
console.error("\ngrafana-toolkit doesn't support following Jest options: ", blacklistedOverrides);
console.log('Supported Jest options are: ', JSON.stringify(whitelistedJestConfigOverrides));
throw new Error('Provided Jest config is not supported');
}
const shimsFilePath = path.resolve(process.cwd(), 'config/jest-shim.ts');
const setupFilePath = path.resolve(process.cwd(), 'config/jest-setup.ts');
const setupFile = fs.existsSync(setupFilePath) ? setupFilePath : undefined;
const shimsFile = fs.existsSync(shimsFilePath) ? shimsFilePath : undefined;
const setupFiles = [setupFile, shimsFile].filter(f => f);
const defaultJestConfig = {
preset: 'ts-jest',
verbose: false,
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
moduleDirectories: ['node_modules', 'src'],
rootDir: process.cwd(),
roots: ['<rootDir>/src'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
setupFiles,
globals: { 'ts-jest': { isolatedModules: true } },
coverageReporters: ['json-summary', 'text', 'lcov'],
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!**/node_modules/**', '!**/vendor/**'],
updateSnapshot: false,
passWithNoTests: true,
};
return {
...defaultJestConfig,
...jestConfigOverrides,
};
};
// @ts-ignore
import resolve from 'rollup-plugin-node-resolve';
// @ts-ignore
import commonjs from 'rollup-plugin-commonjs';
// @ts-ignore
import sourceMaps from 'rollup-plugin-sourcemaps';
// @ts-ignore
import typescript from 'rollup-plugin-typescript2';
// @ts-ignore
import json from 'rollup-plugin-json';
// @ts-ignore
import copy from 'rollup-plugin-copy-glob';
// @ts-ignore
import { terser } from 'rollup-plugin-terser';
// @ts-ignore
import visualizer from 'rollup-plugin-visualizer';
// @ts-ignore
const replace = require('replace-in-file');
const pkg = require(`${process.cwd()}/package.json`);
const path = require('path');
const fs = require('fs');
const tsConfig = require(`${__dirname}/tsconfig.plugin.json`);
import { OutputOptions, InputOptions, GetManualChunk } from 'rollup';
const { PRODUCTION } = process.env;
export const outputOptions: OutputOptions = {
dir: 'dist',
format: 'amd',
sourcemap: true,
chunkFileNames: '[name].js',
};
const findModuleTs = (base: string, files?: string[], result?: string[]) => {
files = files || fs.readdirSync(base);
result = result || [];
if (files) {
files.forEach(file => {
const newbase = path.join(base, file);
if (fs.statSync(newbase).isDirectory()) {
result = findModuleTs(newbase, fs.readdirSync(newbase), result);
} else {
if (file.indexOf('module.ts') > -1) {
// @ts-ignore
result.push(newbase);
}
}
});
}
return result;
};
const getModuleFiles = () => {
return findModuleTs(path.resolve(process.cwd(), 'src'));
};
const getManualChunk: GetManualChunk = (id: string) => {
// id == absolute path
if (id.endsWith('module.ts')) {
const idx = id.indexOf('/src/');
if (idx > 0) {
const p = id.substring(idx + 5, id.lastIndexOf('.'));
console.log('MODULE:', id, p);
return p;
}
}
console.log('shared:', id);
return 'shared';
};
const getExternals = () => {
// Those are by default exported by Grafana
const defaultExternals = [
'jquery',
'lodash',
'moment',
'rxjs',
'd3',
'react',
'react-dom',
'@grafana/ui',
'@grafana/runtime',
'@grafana/data',
];
const toolkitConfig = require(path.resolve(process.cwd(), 'package.json')).grafanaToolkit;
const userDefinedExternals = (toolkitConfig && toolkitConfig.externals) || [];
return [...defaultExternals, ...userDefinedExternals];
};
export const inputOptions = (): InputOptions => {
const inputFiles = getModuleFiles();
return {
input: inputFiles,
manualChunks: inputFiles.length > 1 ? getManualChunk : undefined,
external: getExternals(),
plugins: [
// Allow json resolution
json(),
// globals(),
// builtins(),
// Compile TypeScript files
typescript({
typescript: require('typescript'),
objectHashIgnoreUnknownHack: true,
tsconfigDefaults: tsConfig,
}),
// Allow node_modules resolution, so you can use 'external' to control
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve(),
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
commonjs(),
// Resolve source maps to the original source
sourceMaps(),
// Minify
PRODUCTION && terser(),
// Copy files
copy([{ files: 'src/**/*.{json,svg,png,html}', dest: 'dist' }], { verbose: true }),
// Help avoid including things accidentally
visualizer({
filename: 'dist/stats.html',
title: 'Plugin Stats',
}),
// Custom callback when we are done
finish(),
],
};
};
function finish() {
return {
name: 'finish',
buildEnd() {
const files = 'dist/plugin.json';
replace.sync({
files: files,
from: /%VERSION%/g,
to: pkg.version,
});
replace.sync({
files: files,
from: /%TODAY%/g,
to: new Date().toISOString().substring(0, 10),
});
if (PRODUCTION) {
console.log('*minified*');
}
},
};
}
{
"compilerOptions": {
"moduleResolution": "node",
"target": "es5",
"lib": ["es6", "dom"],
"module": "esnext",
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitUseStrict": false,
"noUnusedLocals": true,
"strictNullChecks": true,
"skipLibCheck": true,
"removeComments": false,
"jsx": "react",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
"noEmitHelpers": true,
"inlineSourceMap": false,
"sourceMap": true,
"emitDecoratorMetadata": false,
"experimentalDecorators": true,
"downlevelIteration": true,
"pretty": true
}
}
{
"rules": {
"array-type": [true, "array-simple"],
"arrow-return-shorthand": true,
"ban": [true, { "name": "Array", "message": "tsstyle#array-constructor" }],
"ban-types": [
true,
["Object", "Use {} instead."],
["String", "Use 'string' instead."],
["Number", "Use 'number' instead."],
["Boolean", "Use 'boolean' instead."]
],
"interface-name": [true, "never-prefix"],
"no-string-throw": true,
"no-unused-expression": true,
"no-unused-variable": false,
"no-use-before-declare": false,
"no-duplicate-variable": true,
"curly": true,
"class-name": true,
"semicolon": [true, "always", "ignore-bound-class-methods"],
"triple-equals": [true, "allow-null-check"],
"comment-format": [false, "check-space"],
"eofline": true,
"forin": false,
"indent": [true, "spaces", 2],
"jsdoc-format": true,
"label-position": true,
"max-line-length": [true, 150],
"member-access": [true, "no-public"],
"new-parens": true,
"no-angle-bracket-type-assertion": true,
"no-arg": true,
"no-bitwise": false,
"no-conditional-assignment": true,
"no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
"no-construct": true,
"no-debugger": true,
"no-empty": false,
"no-eval": true,
"no-inferrable-types": true,
"no-namespace": [true, "allow-declarations"],
"no-reference": true,
"no-shadowed-variable": false,
"no-string-literal": false,
"no-switch-case-fall-through": false,
"no-trailing-whitespace": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [true, "check-open-brace", "check-catch", "check-else"],
"only-arrow-functions": [true, "allow-declarations", "allow-named-functions"],
"prefer-const": true,
"radix": true,
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": [
true,
"check-format",
"ban-keywords",
"allow-leading-underscore",
"allow-trailing-underscore",
"allow-pascal-case"
],
"use-isnan": true,
"whitespace": [true, "check-branch", "check-decl", "check-type", "check-preblock"]
}
}
{
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"],
"compilerOptions": {
"module": "commonjs",
"rootDirs": ["."],
"outDir": "dist/src",
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"typeRoots": ["./node_modules/@types"],
"skipLibCheck": true, // Temp workaround for Duplicate identifier tsc errors,
"removeComments": false,
"esModuleInterop": true,
"lib": ["es2015", "es2017.string"]
}
}
{
"extends": "../../tslint.json",
"rules": {
"import-blacklist": [true, ["^@grafana/runtime.*"]]
}
}
import fs from 'fs';
import * as fs from 'fs';
import darkTheme from '@grafana/ui/src/themes/dark';
import lightTheme from '@grafana/ui/src/themes/light';
import defaultTheme from '@grafana/ui/src/themes/default';
......
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