mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
grafana/toolkit: bundle plugins with webpack (#17850)
This commit is contained in:
parent
164fb13d99
commit
9f351156c3
@ -2,10 +2,45 @@
|
||||
|
||||
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.
|
||||
For development use `yarn link`. First, navigate to `packages/grafana-toolkit` and run `yarn link`. Then, in your project run
|
||||
```
|
||||
yarn add babel-loader ts-loader css-loader style-loader sass-loader html-loader node-sass @babel/preset-env @babel/core & yarn link @grafana/toolkit
|
||||
```
|
||||
|
||||
Note, that for development purposes we are adding `babel-loader ts-loader style-loader sass-loader html-loader node-sass @babel/preset-env @babel/core` packages to your extension. This is due to the specific behavior of `yarn link` which does not install dependencies of linked packages and webpack is having hard time trying to load its extensions.
|
||||
|
||||
TODO: Experiment with [yalc](https://github.com/whitecolor/yalc) for linking packages
|
||||
|
||||
### Publishing to npm
|
||||
The publish process is now manual. Follow the steps to publish @grafana/toolkit to npm
|
||||
1. From Grafana root dir: `./node_modules/.bin/grafana-toolkit toolkit:build`
|
||||
2. `cd packages/grafana-toolkit/dist`
|
||||
3. Open `package.json`, change version according to current version on npm (https://www.npmjs.com/package/@grafana/toolkit)
|
||||
4. Run `npm publish --tag next` - for dev purposes we now publish on `next` channel
|
||||
|
||||
Note, that for publishing you need to be part of Grafana npm org and you need to be logged in to npm in your terminal (`npm login`).
|
||||
|
||||
|
||||
## Grafana extensions development with grafana-toolkit overview
|
||||
### Available tasks
|
||||
#### `grafana-toolkit plugin:test`
|
||||
Runs Jest against your codebase. See [Tests](#tests) for more details.
|
||||
|
||||
Available options:
|
||||
- `-u, --updateSnapshot` - performs snapshots update
|
||||
- `--coverage` - reports code coverage
|
||||
|
||||
#### `grafana-toolkit plugin:dev`
|
||||
Compiles plugin in development mode.
|
||||
|
||||
Available options:
|
||||
- `-w, --watch` - runs `plugin:dev` task in watch mode
|
||||
#### `grafana-toolkit plugin:build`
|
||||
Compiles plugin in production mode
|
||||
|
||||
|
||||
### 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:
|
||||
@ -44,9 +79,30 @@ grafana-toolkit will use that file as Jest's setup file. You can also setup Jest
|
||||
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)
|
||||
|
||||
|
||||
## Working with CSS
|
||||
We support pure css, SASS and CSS in JS approach (via Emotion).
|
||||
|
||||
1. Single css/sass file
|
||||
Create your css/sass file and import it in your plugin entry point (typically module.ts):
|
||||
|
||||
```ts
|
||||
import 'path/to/your/css_or_sass
|
||||
```
|
||||
The styles will be injected via `style` tag during runtime.
|
||||
|
||||
2. Theme css/sass files
|
||||
If you want to provide different stylesheets for Dark/Light theme, create `dark.[css|scss]` and `light.[css|scss]` files in `src/styles` directory of your plugin. Based on that we will generate stylesheets that will end up in `dist/styles` directory.
|
||||
|
||||
TODO: add note about loadPluginCss
|
||||
|
||||
3. Emotion
|
||||
TODO
|
||||
|
||||
## Prettier [todo]
|
||||
|
||||
## Development mode [todo]
|
||||
`grafana-toolkit plugin:dev [--watch]`
|
||||
TODO
|
||||
- Enable rollup watch on extension sources
|
||||
|
||||
|
@ -19,53 +19,58 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "7.4.5",
|
||||
"@babel/preset-env": "7.4.5",
|
||||
"@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/react-dev-utils": "^9.0.1",
|
||||
"@types/semver": "^6.0.0",
|
||||
"@types/webpack": "4.4.34",
|
||||
"axios": "0.19.0",
|
||||
"babel-loader": "8.0.6",
|
||||
"chalk": "^2.4.2",
|
||||
"commander": "^2.20.0",
|
||||
"concurrently": "4.1.0",
|
||||
"copy-webpack-plugin": "5.0.3",
|
||||
"css-loader": "^3.0.0",
|
||||
"execa": "^1.0.0",
|
||||
"glob": "^7.1.4",
|
||||
"html-loader": "0.5.5",
|
||||
"inquirer": "^6.3.1",
|
||||
"jest": "24.8.0",
|
||||
"jest-cli": "^24.8.0",
|
||||
"jest-coverage-badges": "^1.1.2",
|
||||
"lodash": "4.17.11",
|
||||
"mini-css-extract-plugin": "^0.7.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"ora": "^3.4.0",
|
||||
"prettier": "^1.17.1",
|
||||
"react-dev-utils": "^9.0.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",
|
||||
"replace-in-file-webpack-plugin": "^1.0.6",
|
||||
"sass-loader": "7.1.0",
|
||||
"semver": "^6.1.1",
|
||||
"simple-git": "^1.112.0",
|
||||
"ts-node": "^8.2.0",
|
||||
"tslint": "5.14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"jest": "24.8.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"terser-webpack-plugin": "^1.3.0",
|
||||
"ts-jest": "24.0.2",
|
||||
"ts-loader": "6.0.4",
|
||||
"ts-node": "^8.2.0",
|
||||
"tslib": "1.10.0",
|
||||
"typescript": "3.5.1"
|
||||
"tslint": "5.14.0",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typescript": "3.5.1",
|
||||
"webpack": "4.35.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/lodash": "4.14.119",
|
||||
"rollup-plugin-typescript2": "0.21.1"
|
||||
"@types/lodash": "4.14.119"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/glob": "^7.1.1",
|
||||
"rollup-watch": "^4.3.1"
|
||||
"@types/glob": "^7.1.1"
|
||||
}
|
||||
}
|
||||
|
@ -130,10 +130,11 @@ export const run = (includeInternalScripts = false) => {
|
||||
|
||||
program
|
||||
.command('plugin:dev')
|
||||
.option('-w, --watch', 'Run plugin development mode with watch enabled')
|
||||
.description('Starts plugin dev mode')
|
||||
.action(async cmd => {
|
||||
await execTask(pluginDevTask)({
|
||||
watch: true,
|
||||
watch: !!cmd.watch,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -4,16 +4,32 @@ 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';
|
||||
import { bundlePlugin as bundleFn, PluginBundleOptions } from './plugin/bundle';
|
||||
interface PrecommitOptions {}
|
||||
|
||||
export const bundlePlugin = useSpinner<PluginBundleOptions>('Compiling...', async options => await bundleFn(options));
|
||||
|
||||
// @ts-ignore
|
||||
export const clean = useSpinner<void>('Cleaning', async () => await execa('rimraf', ['./dist']));
|
||||
export const clean = useSpinner<void>('Cleaning', async () => await execa('rimraf', [`${process.cwd()}/dist`]));
|
||||
|
||||
export const prepare = useSpinner<void>('Preparing', async () => {
|
||||
// Make sure a local tsconfig exists. Otherwise this will work, but have odd behavior
|
||||
const tsConfigPath = path.resolve(process.cwd(), 'tsconfig.json');
|
||||
if (!fs.existsSync(tsConfigPath)) {
|
||||
const defaultTsConfigPath = path.resolve(__dirname, '../../config/tsconfig.plugin.local.json');
|
||||
fs.copyFile(defaultTsConfigPath, tsConfigPath, err => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
console.log('Created tsconfig.json file');
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const typecheckPlugin = useSpinner<void>('Typechecking', async () => {
|
||||
@ -64,21 +80,14 @@ const lintPlugin = useSpinner<void>('Linting', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
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 () => {
|
||||
// console.log('asasas')
|
||||
await clean();
|
||||
await prepare();
|
||||
// @ts-ignore
|
||||
await lintPlugin();
|
||||
await testPlugin({ updateSnapshot: false, coverage: false });
|
||||
// @ts-ignore
|
||||
await bundlePlugin();
|
||||
await bundlePlugin({ watch: false, production: true });
|
||||
};
|
||||
|
||||
export const pluginBuildTask = new Task<PrecommitOptions>('Build plugin', pluginBuildRunner);
|
||||
|
@ -1,9 +1,18 @@
|
||||
import { Task, TaskRunner } from './task';
|
||||
import { bundlePlugin, PluginBundleOptions } from './plugin/bundle';
|
||||
import { bundlePlugin as bundleFn, PluginBundleOptions } from './plugin/bundle';
|
||||
import { useSpinner } from '../utils/useSpinner';
|
||||
|
||||
const bundlePlugin = useSpinner<PluginBundleOptions>('Bundling plugin in dev mode', options => {
|
||||
return bundleFn(options);
|
||||
});
|
||||
|
||||
const pluginDevRunner: TaskRunner<PluginBundleOptions> = async options => {
|
||||
const result = await bundlePlugin(options);
|
||||
return result;
|
||||
if (options.watch) {
|
||||
await bundleFn(options);
|
||||
} else {
|
||||
const result = await bundlePlugin(options);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
export const pluginDevTask = new Task<PluginBundleOptions>('Dev plugin', pluginDevRunner);
|
||||
|
@ -1,29 +1,71 @@
|
||||
import path = require('path');
|
||||
import * as jestCLI from 'jest-cli';
|
||||
import * as rollup from 'rollup';
|
||||
import { inputOptions, outputOptions } from '../../../config/rollup.plugin.config';
|
||||
import fs = require('fs');
|
||||
import webpack = require('webpack');
|
||||
import { getWebpackConfig } from '../../../config/webpack.plugin.config';
|
||||
import formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||
import clearConsole = require('react-dev-utils/clearConsole');
|
||||
|
||||
export interface PluginBundleOptions {
|
||||
watch: boolean;
|
||||
production?: 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);
|
||||
}
|
||||
// export const bundlePlugin = useSpinner<PluginBundleOptions>('Bundle plugin', ({ watch }) => {
|
||||
export const bundlePlugin = async ({ watch, production }: PluginBundleOptions) => {
|
||||
const compiler = webpack(
|
||||
getWebpackConfig({
|
||||
watch,
|
||||
production,
|
||||
})
|
||||
);
|
||||
|
||||
const webpackPromise = new Promise<void>((resolve, reject) => {
|
||||
if (watch) {
|
||||
console.log('Started watching plugin for changes...');
|
||||
compiler.watch({}, (err, stats) => {});
|
||||
|
||||
compiler.hooks.invalid.tap('invalid', () => {
|
||||
clearConsole();
|
||||
console.log('Compiling...');
|
||||
});
|
||||
|
||||
compiler.hooks.done.tap('done', stats => {
|
||||
clearConsole();
|
||||
const output = formatWebpackMessages(stats.toJson());
|
||||
|
||||
if (!output.errors.length && !output.warnings.length) {
|
||||
console.log('Compiled successfully!');
|
||||
}
|
||||
|
||||
if (output.errors.length) {
|
||||
console.log('Compilation failed!');
|
||||
output.errors.forEach(e => console.log(e));
|
||||
if (output.warnings.length) {
|
||||
console.log('Warnings:');
|
||||
output.warnings.forEach(w => console.log(w));
|
||||
}
|
||||
}
|
||||
if (output.errors.length === 0 && output.warnings.length) {
|
||||
console.log('Compiled with warnings!');
|
||||
output.warnings.forEach(w => console.log(w));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
compiler.run((err: Error, stats: webpack.Stats) => {
|
||||
if (err) {
|
||||
reject(err.message);
|
||||
}
|
||||
if (stats.hasErrors()) {
|
||||
stats.compilation.errors.forEach(e => {
|
||||
console.log(e.message);
|
||||
});
|
||||
|
||||
reject('Build failed');
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return webpackPromise;
|
||||
};
|
||||
|
@ -56,6 +56,7 @@ const moveFiles = () => {
|
||||
'CHANGELOG.md',
|
||||
'bin/grafana-toolkit.dist.js',
|
||||
'src/config/tsconfig.plugin.json',
|
||||
'src/config/tsconfig.plugin.local.json',
|
||||
'src/config/tslint.plugin.json',
|
||||
];
|
||||
// @ts-ignore
|
||||
|
@ -27,8 +27,6 @@ export const jestConfig = () => {
|
||||
'^.+\\.(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 } },
|
||||
|
@ -1,160 +0,0 @@
|
||||
// @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*');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./node_modules/@grafana/toolkit/src/config/tsconfig.plugin.json",
|
||||
"include": ["src", "types"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"baseUrl": "./src",
|
||||
"typeRoots": ["./node_modules/@types"]
|
||||
}
|
||||
}
|
203
packages/grafana-toolkit/src/config/webpack.plugin.config.ts
Normal file
203
packages/grafana-toolkit/src/config/webpack.plugin.config.ts
Normal file
@ -0,0 +1,203 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const ReplaceInFileWebpackPlugin = require('replace-in-file-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
import * as webpack from 'webpack';
|
||||
import { hasThemeStylesheets, getStyleLoaders, getStylesheetEntries } from './webpack/loaders';
|
||||
|
||||
interface WebpackConfigurationOptions {
|
||||
watch?: boolean;
|
||||
production?: boolean;
|
||||
}
|
||||
type WebpackConfigurationGetter = (options: WebpackConfigurationOptions) => webpack.Configuration;
|
||||
|
||||
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 = (id: string) => {
|
||||
if (id.endsWith('module.ts') || id.endsWith('module.tsx')) {
|
||||
const idx = id.indexOf('/src/');
|
||||
if (idx > 0) {
|
||||
const name = id.substring(idx + 5, id.lastIndexOf('.'));
|
||||
|
||||
return {
|
||||
name,
|
||||
module: id,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getEntries = () => {
|
||||
const entries: { [key: string]: string } = {};
|
||||
const modules = getModuleFiles();
|
||||
|
||||
modules.forEach(modFile => {
|
||||
const mod = getManualChunk(modFile);
|
||||
// @ts-ignore
|
||||
entries[mod.name] = mod.module;
|
||||
});
|
||||
return {
|
||||
...entries,
|
||||
...getStylesheetEntries(),
|
||||
};
|
||||
};
|
||||
|
||||
const getCommonPlugins = (options: WebpackConfigurationOptions) => {
|
||||
const packageJson = require(path.resolve(process.cwd(), 'package.json'));
|
||||
return [
|
||||
new MiniCssExtractPlugin({
|
||||
// both options are optional
|
||||
filename: 'styles/[name].css',
|
||||
}),
|
||||
new webpack.optimize.OccurrenceOrderPlugin(true),
|
||||
new CopyWebpackPlugin(
|
||||
[
|
||||
{ from: 'plugin.json', to: '.' },
|
||||
{ from: '../README.md', to: '.' },
|
||||
{ from: '../LICENSE', to: '.' },
|
||||
{ from: 'img/*', to: '.' },
|
||||
{ from: '**/*.json', to: '.' },
|
||||
{ from: '**/*.svg', to: '.' },
|
||||
{ from: '**/*.png', to: '.' },
|
||||
{ from: '**/*.html', to: '.' },
|
||||
],
|
||||
{ logLevel: options.watch ? 'silent' : 'warn' }
|
||||
),
|
||||
|
||||
new ReplaceInFileWebpackPlugin([
|
||||
{
|
||||
dir: 'dist',
|
||||
files: ['plugin.json', 'README.md'],
|
||||
rules: [
|
||||
{
|
||||
search: '%VERSION%',
|
||||
replace: packageJson.version,
|
||||
},
|
||||
{
|
||||
search: '%TODAY%',
|
||||
replace: new Date().toISOString().substring(0, 10),
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
];
|
||||
};
|
||||
|
||||
export const getWebpackConfig: WebpackConfigurationGetter = options => {
|
||||
const plugins = getCommonPlugins(options);
|
||||
const optimization: { [key: string]: any } = {};
|
||||
|
||||
if (options.production) {
|
||||
optimization.minimizer = [new TerserPlugin(), new OptimizeCssAssetsPlugin()];
|
||||
}
|
||||
|
||||
return {
|
||||
mode: options.production ? 'production' : 'development',
|
||||
target: 'web',
|
||||
node: {
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
},
|
||||
context: path.join(process.cwd(), 'src'),
|
||||
devtool: 'source-map',
|
||||
entry: getEntries(),
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.join(process.cwd(), 'dist'),
|
||||
libraryTarget: 'amd',
|
||||
},
|
||||
|
||||
performance: { hints: false },
|
||||
externals: [
|
||||
'lodash',
|
||||
'jquery',
|
||||
'moment',
|
||||
'slate',
|
||||
'prismjs',
|
||||
'slate-plain-serializer',
|
||||
'slate-react',
|
||||
'react',
|
||||
'react-dom',
|
||||
'rxjs',
|
||||
'd3',
|
||||
'@grafana/ui',
|
||||
'@grafana/runtime',
|
||||
'@grafana/data',
|
||||
// @ts-ignore
|
||||
(context, request, callback) => {
|
||||
let prefix = 'app/';
|
||||
if (request.indexOf(prefix) === 0) {
|
||||
return callback(null, request);
|
||||
}
|
||||
prefix = 'grafana/';
|
||||
if (request.indexOf(prefix) === 0) {
|
||||
return callback(null, request.substr(prefix.length));
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
callback();
|
||||
},
|
||||
],
|
||||
plugins,
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js'],
|
||||
modules: [path.resolve(process.cwd(), 'src'), 'node_modules'],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loaders: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: { presets: ['@babel/preset-env'] },
|
||||
},
|
||||
'ts-loader',
|
||||
],
|
||||
exclude: /(node_modules)/,
|
||||
},
|
||||
...getStyleLoaders(),
|
||||
{
|
||||
test: /\.html$/,
|
||||
exclude: [/node_modules/],
|
||||
use: {
|
||||
loader: 'html-loader',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization,
|
||||
// optimization: {
|
||||
// splitChunks: {
|
||||
// chunks: 'all',
|
||||
// name: 'shared'
|
||||
// }
|
||||
// }
|
||||
};
|
||||
};
|
47
packages/grafana-toolkit/src/config/webpack/loaders.test.ts
Normal file
47
packages/grafana-toolkit/src/config/webpack/loaders.test.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { getStylesheetEntries, hasThemeStylesheets } from './loaders';
|
||||
|
||||
describe('Loaders', () => {
|
||||
describe('stylesheet helpers', () => {
|
||||
const logSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
afterAll(() => {
|
||||
logSpy.mockRestore();
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('getStylesheetEntries', () => {
|
||||
it('returns entries for dark and light theme', () => {
|
||||
const result = getStylesheetEntries(`${__dirname}/mocks/ok`);
|
||||
expect(Object.keys(result)).toHaveLength(2);
|
||||
});
|
||||
it('throws on theme files duplicates', () => {
|
||||
const result = () => {
|
||||
getStylesheetEntries(`${__dirname}/mocks/duplicates`);
|
||||
};
|
||||
expect(result).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasThemeStylesheets', () => {
|
||||
it('throws when only one theme file is defined', () => {
|
||||
const result = () => {
|
||||
hasThemeStylesheets(`${__dirname}/mocks/missing-theme-file`);
|
||||
};
|
||||
expect(result).toThrow();
|
||||
});
|
||||
|
||||
it('returns false when no theme files present', () => {
|
||||
const result = hasThemeStylesheets(`${__dirname}/mocks/no-theme-files`);
|
||||
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns true when theme files present', () => {
|
||||
const result = hasThemeStylesheets(`${__dirname}/mocks/ok`);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
92
packages/grafana-toolkit/src/config/webpack/loaders.ts
Normal file
92
packages/grafana-toolkit/src/config/webpack/loaders.ts
Normal file
@ -0,0 +1,92 @@
|
||||
const fs = require('fs');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
const supportedExtensions = ['css', 'scss'];
|
||||
|
||||
const getStylesheetPaths = (root: string = process.cwd()) => {
|
||||
return [`${root}/src/styles/light`, `${root}/src/styles/dark`];
|
||||
};
|
||||
|
||||
export const getStylesheetEntries = (root: string = process.cwd()) => {
|
||||
const stylesheetsPaths = getStylesheetPaths(root);
|
||||
const entries: { [key: string]: string } = {};
|
||||
supportedExtensions.forEach(e => {
|
||||
stylesheetsPaths.forEach(p => {
|
||||
const entryName = p.split('/').slice(-1)[0];
|
||||
if (fs.existsSync(`${p}.${e}`)) {
|
||||
if (entries[entryName]) {
|
||||
console.log(`\nSeems like you have multiple files for ${entryName} theme:`);
|
||||
console.log(entries[entryName]);
|
||||
console.log(`${p}.${e}`);
|
||||
throw new Error('Duplicated stylesheet');
|
||||
} else {
|
||||
entries[entryName] = `${p}.${e}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return entries;
|
||||
};
|
||||
|
||||
export const hasThemeStylesheets = (root: string = process.cwd()) => {
|
||||
const stylesheetsPaths = [`${root}/src/styles/light`, `${root}/src/styles/dark`];
|
||||
const stylesheetsSummary: boolean[] = [];
|
||||
|
||||
const result = stylesheetsPaths.reduce((acc, current) => {
|
||||
if (fs.existsSync(`${current}.css`) || fs.existsSync(`${current}.scss`)) {
|
||||
stylesheetsSummary.push(true);
|
||||
return acc && true;
|
||||
} else {
|
||||
stylesheetsSummary.push(false);
|
||||
return false;
|
||||
}
|
||||
}, true);
|
||||
|
||||
const hasMissingStylesheets = stylesheetsSummary.filter(s => s).length === 1;
|
||||
|
||||
// seems like there is one theme file defined only
|
||||
if (result === false && hasMissingStylesheets) {
|
||||
console.error('\nWe think you want to specify theme stylesheet, but it seems like there is something missing...');
|
||||
stylesheetsSummary.forEach((s, i) => {
|
||||
if (s) {
|
||||
console.log(stylesheetsPaths[i], 'discovered');
|
||||
} else {
|
||||
console.log(stylesheetsPaths[i], 'missing');
|
||||
}
|
||||
});
|
||||
|
||||
throw new Error('Stylesheet missing!');
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getStyleLoaders = () => {
|
||||
const shouldExtractCss = hasThemeStylesheets();
|
||||
|
||||
const executiveLoader = shouldExtractCss
|
||||
? {
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
}
|
||||
: 'style-loader';
|
||||
|
||||
const cssLoader = {
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
sourceMap: true,
|
||||
},
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [executiveLoader, cssLoader],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [executiveLoader, cssLoader, 'sass-loader'],
|
||||
},
|
||||
];
|
||||
};
|
@ -1,6 +1,5 @@
|
||||
options:
|
||||
formatter: stylish
|
||||
|
||||
rules:
|
||||
quotes:
|
||||
- 0
|
||||
|
@ -7,6 +7,7 @@ module.exports = function(config) {
|
||||
src: [
|
||||
'public/sass/**/*.scss',
|
||||
'packages/**/*.scss',
|
||||
'!**/node_modules/**/*.scss'
|
||||
],
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user