Pārlūkot izejas kodu

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

Dominik Prokop 6 gadi atpakaļ
vecāks
revīzija
9f351156c3
23 mainītis faili ar 978 papildinājumiem un 357 dzēšanām
  1. 57 1
      packages/grafana-toolkit/README.md
  2. 26 21
      packages/grafana-toolkit/package.json
  3. 2 1
      packages/grafana-toolkit/src/cli/index.ts
  4. 22 13
      packages/grafana-toolkit/src/cli/tasks/plugin.build.ts
  5. 12 3
      packages/grafana-toolkit/src/cli/tasks/plugin.dev.ts
  6. 64 22
      packages/grafana-toolkit/src/cli/tasks/plugin/bundle.ts
  7. 1 0
      packages/grafana-toolkit/src/cli/tasks/toolkit.build.ts
  8. 0 2
      packages/grafana-toolkit/src/config/jest.plugin.config.ts
  9. 0 160
      packages/grafana-toolkit/src/config/rollup.plugin.config.ts
  10. 9 0
      packages/grafana-toolkit/src/config/tsconfig.plugin.local.json
  11. 203 0
      packages/grafana-toolkit/src/config/webpack.plugin.config.ts
  12. 47 0
      packages/grafana-toolkit/src/config/webpack/loaders.test.ts
  13. 92 0
      packages/grafana-toolkit/src/config/webpack/loaders.ts
  14. 0 0
      packages/grafana-toolkit/src/config/webpack/mocks/duplicates/src/styles/dark.css
  15. 0 0
      packages/grafana-toolkit/src/config/webpack/mocks/duplicates/src/styles/dark.scss
  16. 0 0
      packages/grafana-toolkit/src/config/webpack/mocks/duplicates/src/styles/light.css
  17. 0 0
      packages/grafana-toolkit/src/config/webpack/mocks/missing-theme-file/src/styles/light.css
  18. 0 0
      packages/grafana-toolkit/src/config/webpack/mocks/no-theme-files/src/styles/whatever.css
  19. 0 0
      packages/grafana-toolkit/src/config/webpack/mocks/ok/src/styles/dark.css
  20. 0 0
      packages/grafana-toolkit/src/config/webpack/mocks/ok/src/styles/light.css
  21. 0 1
      public/sass/.sass-lint.yml
  22. 1 0
      scripts/grunt/options/sasslint.js
  23. 442 133
      yarn.lock

+ 57 - 1
packages/grafana-toolkit/README.md

@@ -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
 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
 
 

+ 26 - 21
packages/grafana-toolkit/package.json

@@ -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",
-    "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",
     "semver": "^6.1.1",
     "simple-git": "^1.112.0",
     "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-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",
-    "rollup-plugin-typescript2": "0.21.1"
+    "@types/lodash": "4.14.119"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@types/glob": "^7.1.1",
-    "rollup-watch": "^4.3.1"
+    "@types/glob": "^7.1.1"
   }
   }
 }
 }

+ 2 - 1
packages/grafana-toolkit/src/cli/index.ts

@@ -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,
       });
       });
     });
     });
 
 

+ 22 - 13
packages/grafana-toolkit/src/cli/tasks/plugin.build.ts

@@ -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 () => {
   }
   }
 });
 });
 
 
-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();
+  await bundlePlugin({ watch: false, production: true });
 };
 };
 
 
 export const pluginBuildTask = new Task<PrecommitOptions>('Build plugin', pluginBuildRunner);
 export const pluginBuildTask = new Task<PrecommitOptions>('Build plugin', pluginBuildRunner);

+ 12 - 3
packages/grafana-toolkit/src/cli/tasks/plugin.dev.ts

@@ -1,9 +1,18 @@
 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 => {
-  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);
 export const pluginDevTask = new Task<PluginBundleOptions>('Dev plugin', pluginDevRunner);

+ 64 - 22
packages/grafana-toolkit/src/cli/tasks/plugin/bundle.ts

@@ -1,29 +1,71 @@
 import path = require('path');
 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 {
 export interface PluginBundleOptions {
   watch: boolean;
   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;
 };
 };

+ 1 - 0
packages/grafana-toolkit/src/cli/tasks/toolkit.build.ts

@@ -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

+ 0 - 2
packages/grafana-toolkit/src/config/jest.plugin.config.ts

@@ -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 } },

+ 0 - 160
packages/grafana-toolkit/src/config/rollup.plugin.config.ts

@@ -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*');
-      }
-    },
-  };
-}

+ 9 - 0
packages/grafana-toolkit/src/config/tsconfig.plugin.local.json

@@ -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 - 0
packages/grafana-toolkit/src/config/webpack.plugin.config.ts

@@ -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 - 0
packages/grafana-toolkit/src/config/webpack/loaders.test.ts

@@ -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 - 0
packages/grafana-toolkit/src/config/webpack/loaders.ts

@@ -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'],
+    },
+  ];
+};

+ 0 - 0
packages/grafana-toolkit/src/config/webpack/mocks/duplicates/src/styles/dark.css


+ 0 - 0
packages/grafana-toolkit/src/config/webpack/mocks/duplicates/src/styles/dark.scss


+ 0 - 0
packages/grafana-toolkit/src/config/webpack/mocks/duplicates/src/styles/light.css


+ 0 - 0
packages/grafana-toolkit/src/config/webpack/mocks/missing-theme-file/src/styles/light.css


+ 0 - 0
packages/grafana-toolkit/src/config/webpack/mocks/no-theme-files/src/styles/whatever.css


+ 0 - 0
packages/grafana-toolkit/src/config/webpack/mocks/ok/src/styles/dark.css


+ 0 - 0
packages/grafana-toolkit/src/config/webpack/mocks/ok/src/styles/light.css


+ 0 - 1
public/sass/.sass-lint.yml

@@ -1,6 +1,5 @@
 options:
 options:
   formatter: stylish
   formatter: stylish
-
 rules:
 rules:
   quotes:
   quotes:
     - 0
     - 0

+ 1 - 0
scripts/grunt/options/sasslint.js

@@ -7,6 +7,7 @@ module.exports = function(config) {
     src: [
     src: [
       'public/sass/**/*.scss',
       'public/sass/**/*.scss',
       'packages/**/*.scss',
       'packages/**/*.scss',
+      '!**/node_modules/**/*.scss'
     ],
     ],
   };
   };
 };
 };

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 442 - 133
yarn.lock


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels