Commit 9f351156 by Dominik Prokop Committed by Ryan McKinley

grafana/toolkit: bundle plugins with webpack (#17850)

parent 164fb13d
...@@ -2,10 +2,45 @@ ...@@ -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. 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 ## 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 ## 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 ### 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: 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 ...@@ -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: 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) - [`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] ## Prettier [todo]
## Development mode [todo] ## Development mode [todo]
`grafana-toolkit plugin:dev [--watch]`
TODO TODO
- Enable rollup watch on extension sources - Enable rollup watch on extension sources
...@@ -19,53 +19,58 @@ ...@@ -19,53 +19,58 @@
"author": "Grafana Labs", "author": "Grafana Labs",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@babel/core": "7.4.5",
"@babel/preset-env": "7.4.5",
"@types/execa": "^0.9.0", "@types/execa": "^0.9.0",
"@types/inquirer": "^6.0.3", "@types/inquirer": "^6.0.3",
"@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/prettier": "^1.16.4",
"@types/react-dev-utils": "^9.0.1",
"@types/semver": "^6.0.0", "@types/semver": "^6.0.0",
"@types/webpack": "4.4.34",
"axios": "0.19.0", "axios": "0.19.0",
"babel-loader": "8.0.6",
"chalk": "^2.4.2", "chalk": "^2.4.2",
"commander": "^2.20.0", "commander": "^2.20.0",
"concurrently": "4.1.0", "concurrently": "4.1.0",
"copy-webpack-plugin": "5.0.3",
"css-loader": "^3.0.0",
"execa": "^1.0.0", "execa": "^1.0.0",
"glob": "^7.1.4", "glob": "^7.1.4",
"html-loader": "0.5.5",
"inquirer": "^6.3.1", "inquirer": "^6.3.1",
"jest": "24.8.0",
"jest-cli": "^24.8.0", "jest-cli": "^24.8.0",
"jest-coverage-badges": "^1.1.2",
"lodash": "4.17.11", "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", "ora": "^3.4.0",
"prettier": "^1.17.1", "prettier": "^1.17.1",
"react-dev-utils": "^9.0.1",
"replace-in-file": "^4.1.0", "replace-in-file": "^4.1.0",
"rollup": "^1.14.2", "replace-in-file-webpack-plugin": "^1.0.6",
"rollup-plugin-commonjs": "^10.0.0", "sass-loader": "7.1.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", "semver": "^6.1.1",
"simple-git": "^1.112.0", "simple-git": "^1.112.0",
"ts-node": "^8.2.0", "style-loader": "^0.23.1",
"tslint": "5.14.0" "terser-webpack-plugin": "^1.3.0",
},
"peerDependencies": {
"jest": "24.8.0",
"ts-jest": "24.0.2", "ts-jest": "24.0.2",
"ts-loader": "6.0.4",
"ts-node": "^8.2.0",
"tslib": "1.10.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": { "resolutions": {
"@types/lodash": "4.14.119", "@types/lodash": "4.14.119"
"rollup-plugin-typescript2": "0.21.1"
}, },
"devDependencies": { "devDependencies": {
"@types/glob": "^7.1.1", "@types/glob": "^7.1.1"
"rollup-watch": "^4.3.1"
} }
} }
...@@ -130,10 +130,11 @@ export const run = (includeInternalScripts = false) => { ...@@ -130,10 +130,11 @@ export const run = (includeInternalScripts = false) => {
program program
.command('plugin:dev') .command('plugin:dev')
.option('-w, --watch', 'Run plugin development mode with watch enabled')
.description('Starts plugin dev mode') .description('Starts plugin dev mode')
.action(async cmd => { .action(async cmd => {
await execTask(pluginDevTask)({ await execTask(pluginDevTask)({
watch: true, watch: !!cmd.watch,
}); });
}); });
......
...@@ -4,16 +4,32 @@ import execa = require('execa'); ...@@ -4,16 +4,32 @@ import execa = require('execa');
import path = require('path'); import path = require('path');
import fs = require('fs'); import fs = require('fs');
import glob = require('glob'); import glob = require('glob');
import * as rollup from 'rollup';
import { inputOptions, outputOptions } from '../../config/rollup.plugin.config';
import { useSpinner } from '../utils/useSpinner'; import { useSpinner } from '../utils/useSpinner';
import { Linter, Configuration, RuleFailure } from 'tslint'; import { Linter, Configuration, RuleFailure } from 'tslint';
import { testPlugin } from './plugin/tests'; import { testPlugin } from './plugin/tests';
import { bundlePlugin as bundleFn, PluginBundleOptions } from './plugin/bundle';
interface PrecommitOptions {} interface PrecommitOptions {}
export const bundlePlugin = useSpinner<PluginBundleOptions>('Compiling...', async options => await bundleFn(options));
// @ts-ignore // @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 // @ts-ignore
const typecheckPlugin = useSpinner<void>('Typechecking', async () => { const typecheckPlugin = useSpinner<void>('Typechecking', async () => {
...@@ -64,21 +80,14 @@ const lintPlugin = useSpinner<void>('Linting', 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 () => { const pluginBuildRunner: TaskRunner<PrecommitOptions> = async () => {
// console.log('asasas')
await clean(); await clean();
await prepare();
// @ts-ignore // @ts-ignore
await lintPlugin(); await lintPlugin();
await testPlugin({ updateSnapshot: false, coverage: false }); await testPlugin({ updateSnapshot: false, coverage: false });
// @ts-ignore await bundlePlugin({ watch: false, production: true });
await bundlePlugin();
}; };
export const pluginBuildTask = new Task<PrecommitOptions>('Build plugin', pluginBuildRunner); export const pluginBuildTask = new Task<PrecommitOptions>('Build plugin', pluginBuildRunner);
import { Task, TaskRunner } from './task'; 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 pluginDevRunner: TaskRunner<PluginBundleOptions> = async options => {
if (options.watch) {
await bundleFn(options);
} else {
const result = await bundlePlugin(options); const result = await bundlePlugin(options);
return result; return result;
}
}; };
export const pluginDevTask = new Task<PluginBundleOptions>('Dev plugin', pluginDevRunner); export const pluginDevTask = new Task<PluginBundleOptions>('Dev plugin', pluginDevRunner);
import path = require('path'); import path = require('path');
import * as jestCLI from 'jest-cli'; import fs = require('fs');
import * as rollup from 'rollup'; import webpack = require('webpack');
import { inputOptions, outputOptions } from '../../../config/rollup.plugin.config'; import { getWebpackConfig } from '../../../config/webpack.plugin.config';
import formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
import clearConsole = require('react-dev-utils/clearConsole');
export interface PluginBundleOptions { export interface PluginBundleOptions {
watch: boolean; watch: boolean;
production?: boolean;
} }
export const bundlePlugin = async ({ watch }: PluginBundleOptions) => { // 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) { if (watch) {
const watcher = rollup.watch([ console.log('Started watching plugin for changes...');
{ compiler.watch({}, (err, stats) => {});
...inputOptions(),
output: outputOptions, compiler.hooks.invalid.tap('invalid', () => {
watch: { clearConsole();
chokidar: true, console.log('Compiling...');
clearScreen: true, });
},
}, 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 { } else {
// @ts-ignore compiler.run((err: Error, stats: webpack.Stats) => {
const bundle = await rollup.rollup(inputOptions()); if (err) {
// TODO: we can work on more verbose output reject(err.message);
await bundle.generate(outputOptions); }
await bundle.write(outputOptions); if (stats.hasErrors()) {
stats.compilation.errors.forEach(e => {
console.log(e.message);
});
reject('Build failed');
}
resolve();
});
} }
});
return webpackPromise;
}; };
...@@ -56,6 +56,7 @@ const moveFiles = () => { ...@@ -56,6 +56,7 @@ const moveFiles = () => {
'CHANGELOG.md', 'CHANGELOG.md',
'bin/grafana-toolkit.dist.js', 'bin/grafana-toolkit.dist.js',
'src/config/tsconfig.plugin.json', 'src/config/tsconfig.plugin.json',
'src/config/tsconfig.plugin.local.json',
'src/config/tslint.plugin.json', 'src/config/tslint.plugin.json',
]; ];
// @ts-ignore // @ts-ignore
......
...@@ -27,8 +27,6 @@ export const jestConfig = () => { ...@@ -27,8 +27,6 @@ export const jestConfig = () => {
'^.+\\.(ts|tsx)$': 'ts-jest', '^.+\\.(ts|tsx)$': 'ts-jest',
}, },
moduleDirectories: ['node_modules', 'src'], moduleDirectories: ['node_modules', 'src'],
rootDir: process.cwd(),
roots: ['<rootDir>/src'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
setupFiles, setupFiles,
globals: { 'ts-jest': { isolatedModules: true } }, globals: { 'ts-jest': { isolatedModules: true } },
......
// @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*');
}
},
};
}
{
"extends": "./node_modules/@grafana/toolkit/src/config/tsconfig.plugin.json",
"include": ["src", "types"],
"compilerOptions": {
"rootDir": "./src",
"baseUrl": "./src",
"typeRoots": ["./node_modules/@types"]
}
}
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'
// }
// }
};
};
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();
});
});
});
});
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'],
},
];
};
options: options:
formatter: stylish formatter: stylish
rules: rules:
quotes: quotes:
- 0 - 0
......
...@@ -7,6 +7,7 @@ module.exports = function(config) { ...@@ -7,6 +7,7 @@ module.exports = function(config) {
src: [ src: [
'public/sass/**/*.scss', 'public/sass/**/*.scss',
'packages/**/*.scss', 'packages/**/*.scss',
'!**/node_modules/**/*.scss'
], ],
}; };
}; };
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