diff --git a/packages/grafana-toolkit/README.md b/packages/grafana-toolkit/README.md index 98d70593926..736f6974de0 100644 --- a/packages/grafana-toolkit/README.md +++ b/packages/grafana-toolkit/README.md @@ -128,21 +128,18 @@ Currently we support following Jest configuration properties: - [`moduleNameMapper`](https://jestjs.io/docs/en/configuration#modulenamemapper-object-string-string) ### How can I customize Webpack rules or plugins? -You can provide your own webpack configuration. -Provide a function implementing `CustomWebpackConfigurationGetter` in a file named `webpack.config.js`. - -You can import the correct interface and Options from `@grafana/toolkit/src/config`. - -Example +You can provide your own `webpack.config.js` file that exports a `getWebpackConfig` function. We recommend that you extend the standard configuration, but you are free to create your own: ```js -import CustomPlugin from 'custom-plugin'; +const CustomPlugin = require('custom-plugin'); -export const getWebpackConfig = (defaultConfig, options) => { - console.log('Custom config'); - defaultConfig.plugins.push(new CustomPlugin()) - return defaultConfig; -}; +module.exports.getWebpackConfig = (config, options) => ({ + ...config, + plugins: [ + ...config.plugins, + new CustomPlugin() + ] +}); ``` ### How can I style my plugin? diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index fc6d6fbe468..b568958fe2d 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -37,8 +37,10 @@ "@types/jest": "24.0.13", "@types/jest-cli": "^23.6.0", "@types/node": "^12.0.4", + "@types/prettier": "^1.16.4", "@types/puppeteer-core": "1.9.0", "@types/react-dev-utils": "^9.0.1", + "@types/rimraf": "^2.0.3", "@types/semver": "^6.0.0", "@types/tmp": "^0.1.0", "@types/webpack": "4.4.34", @@ -55,7 +57,7 @@ "execa": "^1.0.0", "expect-puppeteer": "4.1.1", "file-loader": "^4.0.0", - "glob": "^7.1.4", + "globby": "^10.0.1", "html-loader": "0.5.5", "html-webpack-plugin": "^3.2.0", "inquirer": "^6.3.1", @@ -79,6 +81,7 @@ "react-dev-utils": "^9.0.1", "replace-in-file": "^4.1.0", "replace-in-file-webpack-plugin": "^1.0.6", + "rimraf": "^3.0.0", "sass-loader": "7.1.0", "semver": "^6.1.1", "simple-git": "^1.112.0", @@ -94,10 +97,6 @@ "url-loader": "^2.0.1", "webpack": "4.35.0" }, - "devDependencies": { - "@types/glob": "^7.1.1", - "@types/prettier": "^1.16.4" - }, "_moduleAliases": { "puppeteer": "node_modules/puppeteer-core" } diff --git a/packages/grafana-toolkit/src/cli/tasks/plugin.build.ts b/packages/grafana-toolkit/src/cli/tasks/plugin.build.ts index 55586ae2d9b..eb2d5b16b8e 100644 --- a/packages/grafana-toolkit/src/cli/tasks/plugin.build.ts +++ b/packages/grafana-toolkit/src/cli/tasks/plugin.build.ts @@ -1,52 +1,56 @@ -import { Task, TaskRunner } from './task'; -import fs from 'fs'; - -// @ts-ignore -import execa = require('execa'); -import path = require('path'); -import glob = require('glob'); -import { Linter, Configuration, RuleFailure } from 'tslint'; import * as prettier from 'prettier'; - import { useSpinner } from '../utils/useSpinner'; import { testPlugin } from './plugin/tests'; +import { Task, TaskRunner } from './task'; +import rimrafCallback from 'rimraf'; +import { resolve as resolvePath } from 'path'; +import { promisify } from 'util'; +import globby from 'globby'; +import execa from 'execa'; +import { constants as fsConstants, promises as fs } from 'fs'; import { bundlePlugin as bundleFn, PluginBundleOptions } from './plugin/bundle'; +import { Configuration, Linter, LintResult, RuleFailure } from 'tslint'; + +const { access, copyFile, readFile, writeFile } = fs; +const { COPYFILE_EXCL, F_OK } = fsConstants; +const rimraf = promisify(rimrafCallback); + interface PluginBuildOptions { coverage: boolean; } + interface Fixable { fix?: boolean; } export const bundlePlugin = useSpinner('Compiling...', async options => await bundleFn(options)); -// @ts-ignore -export const clean = useSpinner('Cleaning', async () => await execa('rimraf', [`${process.cwd()}/dist`])); +export const clean = useSpinner('Cleaning', async () => await rimraf(`${process.cwd()}/dist`)); + +const copyIfNonExistent = (srcPath: string, destPath: string) => + copyFile(srcPath, destPath, COPYFILE_EXCL) + .then(() => console.log(`Created: ${destPath}`)) + .catch(error => { + if (error.code !== 'EEXIST') { + throw error; + } + }); export const prepare = useSpinner('Preparing', async () => { - // Make sure a local tsconfig exists. Otherwise this will work, but have odd behavior - let filePath = path.resolve(process.cwd(), 'tsconfig.json'); - if (!fs.existsSync(filePath)) { - const srcFile = path.resolve(__dirname, '../../config/tsconfig.plugin.local.json'); - fs.copyFile(srcFile, filePath, err => { - if (err) { - throw err; - } - console.log(`Created: ${filePath}`); - }); - } - // Make sure a local .prettierrc.js exists. Otherwise this will work, but have odd behavior - filePath = path.resolve(process.cwd(), '.prettierrc.js'); - if (!fs.existsSync(filePath)) { - const srcFile = path.resolve(__dirname, '../../config/prettier.plugin.rc.js'); - fs.copyFile(srcFile, filePath, err => { - if (err) { - throw err; - } - console.log(`Created: ${filePath}`); - }); - } - return Promise.resolve(); + await Promise.all([ + // Copy only if local tsconfig does not exist. Otherwise this will work, but have odd behavior + copyIfNonExistent( + resolvePath(process.cwd(), 'tsconfig.json'), + resolvePath(__dirname, '../../config/tsconfig.plugin.local.json') + ), + // Copy only if local prettierrc does not exist. Otherwise this will work, but have odd behavior + copyIfNonExistent( + resolvePath(process.cwd(), '.prettierrc.js'), + resolvePath(__dirname, '../../config/prettier.plugin.rc.js') + ), + ]); + + // Nothing is returned }); // @ts-ignore @@ -54,101 +58,97 @@ const typecheckPlugin = useSpinner('Typechecking', async () => { await execa('tsc', ['--noEmit']); }); -const getTypescriptSources = () => { - const globPattern = path.resolve(process.cwd(), 'src/**/*.+(ts|tsx)'); - return glob.sync(globPattern); -}; +const getTypescriptSources = () => globby(resolvePath(process.cwd(), 'src/**/*.+(ts|tsx)')); -const getStylesSources = () => { - const globPattern = path.resolve(process.cwd(), 'src/**/*.+(scss|css)'); - return glob.sync(globPattern); -}; +const getStylesSources = () => globby(resolvePath(process.cwd(), 'src/**/*.+(scss|css)')); export const prettierCheckPlugin = useSpinner('Prettier check', async ({ fix }) => { - const prettierConfig = require(path.resolve(__dirname, '../../config/prettier.plugin.config.json')); - const sources = [...getStylesSources(), ...getTypescriptSources()]; + // @todo remove explicit params when possible -- https://github.com/microsoft/TypeScript/issues/35626 + const [prettierConfig, paths] = await Promise.all([ + readFile(resolvePath(__dirname, '../../config/prettier.plugin.config.json'), 'utf8').then( + contents => JSON.parse(contents) as object + ), - const promises = sources.map((s, i) => { - return new Promise<{ path: string; failed: boolean }>((resolve, reject) => { - fs.readFile(s, (err, data) => { - let failed = false; - if (err) { - throw new Error(err.message); - } + Promise.all([getStylesSources(), getTypescriptSources()]).then(results => results.flat()), + ]); - const opts = { + const promises: Array> = paths.map(path => + readFile(path, 'utf8') + .then(contents => { + const config = { ...prettierConfig, - filepath: s, + filepath: path, }; - if (!prettier.check(data.toString(), opts)) { - if (fix) { - const fixed = prettier.format(data.toString(), opts); - if (fixed && fixed.length > 10) { - fs.writeFile(s, fixed, err => { - if (err) { - console.log('Error fixing ' + s, err); - failed = true; - } else { - console.log('Fixed: ' + s); - } - }); - } else { - console.log('No automatic fix for: ' + s); - failed = true; - } - } else { - failed = true; - } + + if (fix && !prettier.check(contents, config)) { + return prettier.format(contents, config); + } else { + return undefined; } + }) + .then(newContents => { + if (fix && newContents && newContents.length > 10) { + return writeFile(path, newContents) + .then(() => { + console.log(`Fixed: ${path}`); + return true; + }) + .catch(error => { + console.log(`Error fixing ${path}`, error); + return false; + }); + } else if (fix) { + console.log(`No automatic fix for: ${path}`); + return false; + } else { + return false; + } + }) + .then(success => ({ path, success })) + ); - resolve({ - path: s, - failed, - }); - }); - }); - }); + const failures = (await Promise.all(promises)).filter(({ success }) => !success); - const results = await Promise.all(promises); - const failures = results.filter(r => r.failed); - if (failures.length) { + if (failures.length > 0) { console.log('\nFix Prettier issues in following files:'); - failures.forEach(f => console.log(f.path)); + failures.forEach(({ path }) => console.log(path)); console.log('\nRun toolkit:dev to fix errors'); throw new Error('Prettier failed'); } }); -// @ts-ignore export const lintPlugin = useSpinner('Linting', async ({ fix }) => { - let tsLintConfigPath = path.resolve(process.cwd(), 'tslint.json'); - if (!fs.existsSync(tsLintConfigPath)) { - tsLintConfigPath = path.resolve(__dirname, '../../config/tslint.plugin.json'); + let tsLintConfigPath = resolvePath(process.cwd(), 'tslint.json'); + + try { + await access(tsLintConfigPath, F_OK); + } catch (error) { + tsLintConfigPath = resolvePath(__dirname, '../../config/tslint.plugin.json'); } + const options = { fix: fix === true, formatter: 'json', }; const configuration = Configuration.findConfiguration(tsLintConfigPath).results; - const sourcesToLint = getTypescriptSources(); + const sourcesToLint = await getTypescriptSources(); - const lintResults = sourcesToLint - .map(fileName => { + const lintPromises = sourcesToLint.map(fileName => + readFile(fileName, 'utf8').then(contents => { const linter = new Linter(options); - const fileContents = fs.readFileSync(fileName, 'utf8'); - linter.lint(fileName, fileContents, configuration); + linter.lint(fileName, contents, configuration); return linter.getResult(); }) - .filter(result => { - return result.errorCount > 0 || result.warningCount > 0; - }); + ); + + const lintResults: LintResult[] = (await Promise.all(lintPromises)).filter( + ({ errorCount, warningCount }) => errorCount > 0 || warningCount > 0 + ); if (lintResults.length > 0) { console.log('\n'); - const failures = lintResults.reduce((failures, result) => { - return [...failures, ...result.failures]; - }, []); + const failures: RuleFailure[] = lintResults.flat(); failures.forEach(f => { // tslint:disable-next-line console.log( diff --git a/packages/grafana-toolkit/tsconfig.json b/packages/grafana-toolkit/tsconfig.json index f888b9d3877..c38c2bb2fe1 100644 --- a/packages/grafana-toolkit/tsconfig.json +++ b/packages/grafana-toolkit/tsconfig.json @@ -9,7 +9,6 @@ "declaration": true, "declarationDir": "dist/src", "typeRoots": ["./node_modules/@types"], - "esModuleInterop": true, - "lib": ["es2015", "es2017.string", "dom"] + "esModuleInterop": true } } diff --git a/tsconfig.json b/tsconfig.json index 783f26b79d4..7b2c2a8f0c4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "moduleResolution": "node", "outDir": "public/dist", "target": "es5", - "lib": ["es6", "dom"], + "lib": ["es2019", "dom"], "rootDirs": ["public/"], "jsx": "react", "module": "esnext",