Browse Source

Add weback-dev-server with hot/hmr support

* adds `npm start` / `yarn start` script
* starts a webpack-dev-server using the dev config, served on :3333
* hot reloading (HMR) for react/styles, not working for angular code
* new entry `dev.ts` for dynamic imports of CSS theme (ExtractText does
not work with HMR)
* TS loader pipeline moved out of common to add HMR for react
* applied `hot()` to some react containers (that's their new default
 export, named exports remains for testing)
* added sections to README
* updated yarn.lock
David Kaltschmidt 7 years ago
parent
commit
cc5d7002b0

+ 2 - 0
.gitignore

@@ -1,8 +1,10 @@
 node_modules
 node_modules
 npm-debug.log
 npm-debug.log
+yarn-error.log
 coverage/
 coverage/
 .aws-config.json
 .aws-config.json
 awsconfig
 awsconfig
+/.awcache
 /dist
 /dist
 /public/build
 /public/build
 /public/views/index.html
 /public/views/index.html

+ 11 - 0
README.md

@@ -39,12 +39,21 @@ go run build.go build
 
 
 For this you need nodejs (v.6+).
 For this you need nodejs (v.6+).
 
 
+To build the assets, rebuild on file change, and serve them by Grafana's webserver (http://localhost:3000):
 ```bash
 ```bash
 npm install -g yarn
 npm install -g yarn
 yarn install --pure-lockfile
 yarn install --pure-lockfile
 npm run watch
 npm run watch
 ```
 ```
 
 
+Build the assets, rebuild on file change with Hot Module Replacement (HMR), and serve them by webpack-dev-server (http://localhost:3333):
+```bash
+yarn start
+# OR set a theme
+env GRAFANA_THEME=light yarn start
+```
+Note: HMR for Angular is not supported. If you edit files in the Angular part of the app, the whole page will reload.
+
 Run tests 
 Run tests 
 ```bash
 ```bash
 npm run jest
 npm run jest
@@ -55,6 +64,8 @@ Run karma tests
 npm run karma
 npm run karma
 ```
 ```
 
 
+Run
+
 ### Recompile backend on source change
 ### Recompile backend on source change
 
 
 To rebuild on source change.
 To rebuild on source change.

+ 6 - 0
package.json

@@ -23,6 +23,7 @@
     "babel-core": "^6.26.0",
     "babel-core": "^6.26.0",
     "babel-loader": "^7.1.2",
     "babel-loader": "^7.1.2",
     "babel-preset-es2015": "^6.24.1",
     "babel-preset-es2015": "^6.24.1",
+    "clean-webpack-plugin": "^0.1.19",
     "css-loader": "^0.28.7",
     "css-loader": "^0.28.7",
     "enzyme": "^3.1.0",
     "enzyme": "^3.1.0",
     "enzyme-adapter-react-16": "^1.0.1",
     "enzyme-adapter-react-16": "^1.0.1",
@@ -54,6 +55,7 @@
     "grunt-usemin": "3.1.1",
     "grunt-usemin": "3.1.1",
     "grunt-webpack": "^3.0.2",
     "grunt-webpack": "^3.0.2",
     "html-loader": "^0.5.1",
     "html-loader": "^0.5.1",
+    "html-webpack-harddisk-plugin": "^0.2.0",
     "html-webpack-plugin": "^2.30.1",
     "html-webpack-plugin": "^2.30.1",
     "husky": "^0.14.3",
     "husky": "^0.14.3",
     "jest": "^22.0.4",
     "jest": "^22.0.4",
@@ -80,10 +82,12 @@
     "postcss-loader": "^2.0.6",
     "postcss-loader": "^2.0.6",
     "postcss-reporter": "^5.0.0",
     "postcss-reporter": "^5.0.0",
     "prettier": "1.9.2",
     "prettier": "1.9.2",
+    "react-hot-loader": "^4.0.1",
     "react-test-renderer": "^16.0.0",
     "react-test-renderer": "^16.0.0",
     "sass-lint": "^1.10.2",
     "sass-lint": "^1.10.2",
     "sass-loader": "^6.0.6",
     "sass-loader": "^6.0.6",
     "sinon": "1.17.6",
     "sinon": "1.17.6",
+    "style-loader": "^0.20.3",
     "systemjs": "0.20.19",
     "systemjs": "0.20.19",
     "systemjs-plugin-css": "^0.1.36",
     "systemjs-plugin-css": "^0.1.36",
     "ts-jest": "^22.0.0",
     "ts-jest": "^22.0.0",
@@ -94,11 +98,13 @@
     "webpack": "^3.10.0",
     "webpack": "^3.10.0",
     "webpack-bundle-analyzer": "^2.9.0",
     "webpack-bundle-analyzer": "^2.9.0",
     "webpack-cleanup-plugin": "^0.5.1",
     "webpack-cleanup-plugin": "^0.5.1",
+    "webpack-dev-server": "2.11.1",
     "webpack-merge": "^4.1.0",
     "webpack-merge": "^4.1.0",
     "zone.js": "^0.7.2"
     "zone.js": "^0.7.2"
   },
   },
   "scripts": {
   "scripts": {
     "dev": "webpack --progress --colors --config scripts/webpack/webpack.dev.js",
     "dev": "webpack --progress --colors --config scripts/webpack/webpack.dev.js",
+    "start": "webpack-dev-server --progress --colors --config scripts/webpack/webpack.dev.js",
     "watch": "webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js",
     "watch": "webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js",
     "build": "grunt build",
     "build": "grunt build",
     "test": "grunt test",
     "test": "grunt test",

+ 3 - 0
public/app/containers/AlertRuleList/AlertRuleList.tsx

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import { hot } from 'react-hot-loader';
 import classNames from 'classnames';
 import classNames from 'classnames';
 import { inject, observer } from 'mobx-react';
 import { inject, observer } from 'mobx-react';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
@@ -173,3 +174,5 @@ export class AlertRuleItem extends React.Component<AlertRuleItemProps, any> {
     );
     );
   }
   }
 }
 }
+
+export default hot(module)(AlertRuleList);

+ 3 - 0
public/app/containers/ManageDashboards/FolderPermissions.tsx

@@ -1,4 +1,5 @@
 import React, { Component } from 'react';
 import React, { Component } from 'react';
+import { hot } from 'react-hot-loader';
 import { inject, observer } from 'mobx-react';
 import { inject, observer } from 'mobx-react';
 import { toJS } from 'mobx';
 import { toJS } from 'mobx';
 import IContainerProps from 'app/containers/IContainerProps';
 import IContainerProps from 'app/containers/IContainerProps';
@@ -72,3 +73,5 @@ export class FolderPermissions extends Component<IContainerProps, any> {
     );
     );
   }
   }
 }
 }
+
+export default hot(module)(FolderPermissions);

+ 3 - 0
public/app/containers/ManageDashboards/FolderSettings.tsx

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import { hot } from 'react-hot-loader';
 import { inject, observer } from 'mobx-react';
 import { inject, observer } from 'mobx-react';
 import { toJS } from 'mobx';
 import { toJS } from 'mobx';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
@@ -156,3 +157,5 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
     );
     );
   }
   }
 }
 }
+
+export default hot(module)(FolderSettings);

+ 3 - 0
public/app/containers/ServerStats/ServerStats.tsx

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import { hot } from 'react-hot-loader';
 import { inject, observer } from 'mobx-react';
 import { inject, observer } from 'mobx-react';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import IContainerProps from 'app/containers/IContainerProps';
 import IContainerProps from 'app/containers/IContainerProps';
@@ -43,3 +44,5 @@ function StatItem(stat) {
     </tr>
     </tr>
   );
   );
 }
 }
+
+export default hot(module)(ServerStats);

+ 9 - 0
public/app/dev.ts

@@ -0,0 +1,9 @@
+import app from './app';
+
+/*
+Import theme CSS based on env vars, e.g.: `env GRAFANA_THEME=light yarn start`
+*/
+declare var GRAFANA_THEME: any;
+require('../sass/grafana.' + GRAFANA_THEME + '.scss');
+
+app.init();

+ 4 - 4
public/app/routes/routes.ts

@@ -1,9 +1,9 @@
 import './dashboard_loaders';
 import './dashboard_loaders';
 import './ReactContainer';
 import './ReactContainer';
-import { ServerStats } from 'app/containers/ServerStats/ServerStats';
-import { AlertRuleList } from 'app/containers/AlertRuleList/AlertRuleList';
-import { FolderSettings } from 'app/containers/ManageDashboards/FolderSettings';
-import { FolderPermissions } from 'app/containers/ManageDashboards/FolderPermissions';
+import ServerStats from 'app/containers/ServerStats/ServerStats';
+import AlertRuleList from 'app/containers/AlertRuleList/AlertRuleList';
+import FolderSettings from 'app/containers/ManageDashboards/FolderSettings';
+import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions';
 
 
 /** @ngInject **/
 /** @ngInject **/
 export function setupAngularRoutes($routeProvider, $locationProvider) {
 export function setupAngularRoutes($routeProvider, $locationProvider) {

+ 2 - 0
public/sass/base/_fonts.scss

@@ -29,6 +29,7 @@
   unicode-range: U+1f00-1fff;
   unicode-range: U+1f00-1fff;
 }
 }
 /* greek */
 /* greek */
+/* not available
 @font-face {
 @font-face {
   font-family: 'Roboto';
   font-family: 'Roboto';
   font-style: normal;
   font-style: normal;
@@ -37,6 +38,7 @@
     url(../fonts/roboto/u0TOpm082MNkS5K0Q4rhqvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
     url(../fonts/roboto/u0TOpm082MNkS5K0Q4rhqvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
   unicode-range: U+0370-03ff;
   unicode-range: U+0370-03ff;
 }
 }
+*/
 /* vietnamese */
 /* vietnamese */
 @font-face {
 @font-face {
   font-family: 'Roboto';
   font-family: 'Roboto';

+ 10 - 4
scripts/webpack/sass.rule.js

@@ -2,16 +2,16 @@
 
 
 const ExtractTextPlugin = require("extract-text-webpack-plugin");
 const ExtractTextPlugin = require("extract-text-webpack-plugin");
 
 
-module.exports = function(options) {
+module.exports = function (options, extractSass) {
   return {
   return {
     test: /\.scss$/,
     test: /\.scss$/,
-    use: ExtractTextPlugin.extract({
+    use: (extractSass || ExtractTextPlugin).extract({
       use: [
       use: [
         {
         {
           loader: 'css-loader',
           loader: 'css-loader',
           options: {
           options: {
             importLoaders: 2,
             importLoaders: 2,
-            url: false,
+            url: options.preserveUrl,
             sourceMap: options.sourceMap,
             sourceMap: options.sourceMap,
             minimize: options.minimize,
             minimize: options.minimize,
           }
           }
@@ -23,8 +23,14 @@ module.exports = function(options) {
             config: { path: __dirname + '/postcss.config.js' }
             config: { path: __dirname + '/postcss.config.js' }
           }
           }
         },
         },
-        { loader:'sass-loader', options: { sourceMap: options.sourceMap } }
+        { loader: 'sass-loader', options: { sourceMap: options.sourceMap } }
       ],
       ],
+      fallback: [{
+        loader: 'style-loader',
+        options: {
+          sourceMap: true
+        }
+      }]
     })
     })
   };
   };
 }
 }

+ 4 - 23
scripts/webpack/webpack.common.js

@@ -1,5 +1,5 @@
 const path = require('path');
 const path = require('path');
-const {CheckerPlugin} = require('awesome-typescript-loader')
+const { CheckerPlugin } = require('awesome-typescript-loader');
 
 
 module.exports = {
 module.exports = {
   target: 'web',
   target: 'web',
@@ -11,8 +11,8 @@ module.exports = {
   },
   },
   output: {
   output: {
     path: path.resolve(__dirname, '../../public/build'),
     path: path.resolve(__dirname, '../../public/build'),
-    filename: '[name].[chunkhash].js',
-    publicPath: "public/build/",
+    filename: '[name].[hash].js',
+    publicPath: "/public/build/",
   },
   },
   resolve: {
   resolve: {
     extensions: ['.ts', '.tsx', '.es6', '.js', '.json'],
     extensions: ['.ts', '.tsx', '.es6', '.js', '.json'],
@@ -28,25 +28,6 @@ module.exports = {
   },
   },
   module: {
   module: {
     rules: [
     rules: [
-      {
-        test: /\.tsx?$/,
-        enforce: 'pre',
-        exclude: /node_modules/,
-        use: {
-          loader: 'tslint-loader',
-          options: {
-            emitErrors: true,
-            typeCheck: false,
-          }
-        }
-      },
-      {
-        test: /\.tsx?$/,
-        exclude: /node_modules/,
-        use: [
-          { loader: "awesome-typescript-loader" }
-        ]
-      },
       {
       {
         test: require.resolve('jquery'),
         test: require.resolve('jquery'),
         use: [
         use: [
@@ -64,7 +45,7 @@ module.exports = {
         test: /\.html$/,
         test: /\.html$/,
         exclude: /index\.template.html/,
         exclude: /index\.template.html/,
         use: [
         use: [
-          { loader:'ngtemplate-loader?relativeTo=' + (path.resolve(__dirname, '../../public')) + '&prefix=public'},
+          { loader: 'ngtemplate-loader?relativeTo=' + (path.resolve(__dirname, '../../public')) + '&prefix=public' },
           {
           {
             loader: 'html-loader',
             loader: 'html-loader',
             options: {
             options: {

+ 89 - 10
scripts/webpack/webpack.dev.js

@@ -5,39 +5,118 @@ const common = require('./webpack.common.js');
 const path = require('path');
 const path = require('path');
 const webpack = require('webpack');
 const webpack = require('webpack');
 const HtmlWebpackPlugin = require("html-webpack-plugin");
 const HtmlWebpackPlugin = require("html-webpack-plugin");
+const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
 const ExtractTextPlugin = require("extract-text-webpack-plugin");
 const ExtractTextPlugin = require("extract-text-webpack-plugin");
-const WebpackCleanupPlugin = require('webpack-cleanup-plugin');
+const CleanWebpackPlugin = require('clean-webpack-plugin');
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
 
-module.exports = merge(common, {
-  devtool: "cheap-module-source-map",
+const TARGET = process.env.npm_lifecycle_event;
+const HOT = TARGET === 'start';
+
+const extractSass = new ExtractTextPlugin({
+  filename: "grafana.[name].css",
+  disable: HOT
+});
 
 
-  entry: {
+const entries = HOT ? {
+  app: [
+    'webpack-dev-server/client?http://localhost:3333',
+    './public/app/dev.ts',
+  ],
+  vendor: require('./dependencies'),
+} : {
+    app: './public/app/index.ts',
     dark: './public/sass/grafana.dark.scss',
     dark: './public/sass/grafana.dark.scss',
     light: './public/sass/grafana.light.scss',
     light: './public/sass/grafana.light.scss',
     vendor: require('./dependencies'),
     vendor: require('./dependencies'),
+  };
+
+module.exports = merge(common, {
+  devtool: "cheap-module-source-map",
+
+  entry: entries,
+
+  resolve: {
+    extensions: ['.scss', '.ts', '.tsx', '.es6', '.js', '.json', '.svg', '.woff2', '.png'],
+  },
+
+  devServer: {
+    publicPath: '/public/build/',
+    hot: HOT,
+    port: 3333,
+    proxy: {
+      '!/public/build': 'http://localhost:3000'
+    }
   },
   },
 
 
   module: {
   module: {
     rules: [
     rules: [
+      {
+        test: /\.tsx?$/,
+        enforce: 'pre',
+        exclude: /node_modules/,
+        use: {
+          loader: 'tslint-loader',
+          options: {
+            emitErrors: true,
+            typeCheck: false,
+          }
+        }
+      },
+      {
+        test: /\.tsx?$/,
+        exclude: /node_modules/,
+        use: [
+          {
+            loader: 'babel-loader',
+            options: {
+              plugins: [
+                'react-hot-loader/babel',
+              ],
+            },
+          },
+          {
+            loader: 'awesome-typescript-loader',
+            options: {
+              useCache: true,
+            },
+          }
+        ]
+      },
       require('./sass.rule.js')({
       require('./sass.rule.js')({
-        sourceMap: false, minimize: false
-      })
+        sourceMap: true, minimize: false, preserveUrl: true
+      }, extractSass),
+      {
+        test: /\.(ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
+        loader: 'file-loader'
+      },
+      {
+        test: /\.(png|jpg|gif)$/,
+        use: [
+          {
+            loader: 'file-loader',
+            options: {}
+          }
+        ]
+      },
     ]
     ]
   },
   },
 
 
   plugins: [
   plugins: [
-    new ExtractTextPlugin({ // define where to save the file
-      filename: 'grafana.[name].css',
-      allChunks: true,
-    }),
+    new CleanWebpackPlugin('../public/build', { allowExternal: true }),
+    extractSass,
     new HtmlWebpackPlugin({
     new HtmlWebpackPlugin({
       filename: path.resolve(__dirname, '../../public/views/index.html'),
       filename: path.resolve(__dirname, '../../public/views/index.html'),
       template: path.resolve(__dirname, '../../public/views/index.template.html'),
       template: path.resolve(__dirname, '../../public/views/index.template.html'),
       inject: 'body',
       inject: 'body',
       chunks: ['manifest', 'vendor', 'app'],
       chunks: ['manifest', 'vendor', 'app'],
+      alwaysWriteToDisk: HOT
     }),
     }),
+    new HtmlWebpackHarddiskPlugin(),
+    new webpack.NamedModulesPlugin(),
+    new webpack.HotModuleReplacementPlugin(),
     new webpack.DefinePlugin({
     new webpack.DefinePlugin({
+      'GRAFANA_THEME': JSON.stringify(process.env.GRAFANA_THEME || 'dark'),
       'process.env': {
       'process.env': {
         'NODE_ENV': JSON.stringify('development')
         'NODE_ENV': JSON.stringify('development')
       }
       }

+ 22 - 3
scripts/webpack/webpack.prod.js

@@ -20,8 +20,27 @@ module.exports = merge(common, {
 
 
   module: {
   module: {
     rules: [
     rules: [
+      {
+        test: /\.tsx?$/,
+        enforce: 'pre',
+        exclude: /node_modules/,
+        use: {
+          loader: 'tslint-loader',
+          options: {
+            emitErrors: true,
+            typeCheck: false,
+          }
+        }
+      },
+      {
+        test: /\.tsx?$/,
+        exclude: /node_modules/,
+        use: [
+          { loader: "awesome-typescript-loader" }
+        ]
+      },
       require('./sass.rule.js')({
       require('./sass.rule.js')({
-        sourceMap: false, minimize: true
+        sourceMap: false, minimize: true, preserveUrl: false
       })
       })
     ]
     ]
   },
   },
@@ -55,8 +74,8 @@ module.exports = merge(common, {
     new webpack.optimize.CommonsChunkPlugin({
     new webpack.optimize.CommonsChunkPlugin({
       names: ['vendor', 'manifest'],
       names: ['vendor', 'manifest'],
     }),
     }),
-    function() {
-      this.plugin("done", function(stats) {
+    function () {
+      this.plugin("done", function (stats) {
         if (stats.compilation.errors && stats.compilation.errors.length) {
         if (stats.compilation.errors && stats.compilation.errors.length) {
           console.log(stats.compilation.errors);
           console.log(stats.compilation.errors);
           process.exit(1);
           process.exit(1);

+ 10 - 2
scripts/webpack/webpack.test.js

@@ -9,8 +9,16 @@ config = merge(common, {
     'react/lib/ExecutionEnvironment': true,
     'react/lib/ExecutionEnvironment': true,
     'react/lib/ReactContext': true,
     'react/lib/ReactContext': true,
   },
   },
-  node: {
-    fs: 'empty'
+  module: {
+    rules: [
+      {
+        test: /\.tsx?$/,
+        exclude: /node_modules/,
+        use: [
+          { loader: "awesome-typescript-loader" }
+        ]
+      },
+    ]
   },
   },
   plugins: [
   plugins: [
     new webpack.SourceMapDevToolPlugin({
     new webpack.SourceMapDevToolPlugin({

File diff suppressed because it is too large
+ 513 - 10
yarn.lock


Some files were not shown because too many files changed in this diff