Commit f6130db0 by Steven Vachon Committed by GitHub

@grafana/toolkit: cleanup (#21441)

* Clarify readme for extended webpack config

* Add support for ES2019 features

* Build task: path.resolve → resolvePath

* Build task: avoid fs race conditions

* Build task: use async fs functions

* Build task: use rimraf directly

... because depending on the workspace parent to have it installed is fragile, and a child process is always slower.

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