Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can you provide any larger examples of Kevin in the wild? #6

Open
erlandsona opened this issue Jun 2, 2020 · 5 comments
Open

Can you provide any larger examples of Kevin in the wild? #6

erlandsona opened this issue Jun 2, 2020 · 5 comments

Comments

@erlandsona
Copy link

I'm trying to spin up kevin as a proof of concept for our client and running into... after following the example... not really sure how to resolve.

:9275/:1 GET http://localhost:9275/ 404 (Not Found)
localhost/:1 Refused to load the image 'http://localhost:9275/favicon.ico' because it violates the following Content Security Policy directive: "default-src 'none'". Note that 'img-src' was not explicitly set, so 'default-src' is used as a fallback.

I read the article you posted and thought Kevin would be a great fit for our codebase as well. We've got about 3500 js files and webpack-dev-server takes anywhere from a minute or so to build on first launch and about 10-12 seconds for a given change in watch mode.

So the promise of being able to incrementally build portions of the app to speed up development time for a monorepo seems like a very promising solution for our needs as well.

@salemhilal
Copy link
Contributor

Hey, I'm glad you're interested in Kevin! I have a bunch of questions.

  1. Can you tell me a bit more about what your webpack config looks like?
  2. What are you expecting Kevin to serve?
  3. What does your development server look like? (In other words, how are you importing and using Kevin?)

@erlandsona
Copy link
Author

erlandsona commented Jun 2, 2020

Hey @salemhilal

  1. webpack.config.js
const path = require('path')
const childProcess = require('child_process')
const { EnvironmentPlugin, DefinePlugin, IgnorePlugin } = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CircularDependencyPlugin = require('circular-dependency-plugin')
const GenerateJsonPlugin = require('generate-json-webpack-plugin')
const { GenerateSW } = require('workbox-webpack-plugin')

// The environment that our application runs in, used to determine which
// configuration to load and to set environment flags
const APP_ENV = process.env.APP_ENV || 'development'

// For local development - what app is being served via the devServer, portal
// or clinet
const APP_TO_SERVE = process.env.APP_TO_SERVE || 'projectClient'

// Whether the application is being built in a development environment
const DEV = process.env.NODE_ENV !== 'production'

const CDN =
  APP_ENV === 'production'
    ? 'cdn'
    : APP_ENV === 'testing'
    ? 'testing-cdn'
    : 'staging-cdn'

// URLs for caching and html templating
const URL = {
  PROJECT_ICONS: `https://${CDN}.projecthealth.io/ahc-fonts/css/Project-Icons.min.css`,
  MATERIAL_ICONS: 'https://fonts.googleapis.com/icon?family=Material+Icons',
  ROBOTO_FONT: 'https://fonts.googleapis.com/css?family=Roboto|Roboto+Mono',
  KAUSHAN_FONT:
    'https://fonts.googleapis.com/css?family=Kaushan+Script&display=swap',
}

const entry = DEV
  ? {}
  : { auth: path.resolve(__dirname, 'src/utils/security/auth') }

const CLIENT = new HtmlWebpackPlugin({
  filename: 'index.html',
  inject: 'head',
  template: path.resolve(__dirname, 'src/index.html'),
  excludeChunks: ['auth', 'runtime~auth', 'portal', 'runtime~portal'],
  templateParameters: {
    URL,
  },
})

const PORTAL = new HtmlWebpackPlugin({
  filename: DEV ? 'index.html' : 'partner/index.html',
  inject: 'head',
  template: path.resolve(__dirname, 'src/index.html'),
  excludeChunks: ['auth', 'runtime~auth', 'main', 'runtime~main'],
  templateParameters: {
    URL,
  },
})

// Build a single index.html for loacl dev, for either the client or portal -
// and build both for deployment environments
const HTML_WEBPACK_PLUGINS = (() => {
  if (DEV && APP_TO_SERVE === 'partnerPortal') return [PORTAL]
  if (DEV) return [CLIENT]
  return [CLIENT, PORTAL]
})()

// Build a primary keycloak.json for either the client or portal -
// and build both for deployment environments
const {
  primary: PRIMARY_KEYCLOAK,
  portal: PORTAL_KEYCLOAK,
} = require(path.resolve(__dirname, 'keycloak', `${APP_ENV}.keycloak.js`))({
  BUILD_PORTAL: DEV && APP_TO_SERVE === 'portal',
})

module.exports = {
  name: 'client',
  mode: DEV ? 'development' : 'production',
  devtool: DEV ? 'cheap-module-eval-source-map' : 'source-map',
  entry: {
    ...entry,
    polyfill: '@babel/polyfill',
    main: path.resolve(__dirname, 'src/index'),
    portal: path.resolve(__dirname, 'src/partnerPortal/index'),
    // These are packages that we think will take up the bulk of the bundle
    // size, but will also change relatively infrequently
    vendor: [
      '@devexpress/dx-react-core',
      '@devexpress/dx-react-grid',
      '@devexpress/dx-react-grid-material-ui',
      '@material-ui/core',
      '@material-ui/icons',
      '@material-ui/styles',
      'axios',
      'history',
      'immutable',
      'jss',
      'keycloak-js',
      'moment',
      'react',
      'react-dom',
      'react-redux',
      'redux',
      'redux-form',
      'redux-immutable',
      'redux-observable',
      'redux-routable',
      'redux-routable-react',
      'rxjs',
    ],
  },
  output: {
    path: path.resolve(__dirname, 'build'),
    publicPath: '/',
    // Uses 'hash' in development to cache-bust
    filename: `[name].[${DEV ? '' : 'chunk'}hash].js`,
  },
  devServer: {
    port: 5000,
    publicPath: '/',
    contentBase: path.join(__dirname, 'public'),
    historyApiFallback: true,
  },
  watchOptions: {
    ignored: /node_modules/,
  },
  module: {
    strictExportPresence: true,
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          query: { cacheDirectory: true },
        },
      },
      {
        test: /\.css$/,
        use: [DEV ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader'],
      },
      {
        test: /\.md$/,
        use: 'raw-loader',
      },
      {
        test: /\.(png|svg|gif|jpe?g)$/i,
        use: [
          {
            loader: 'file-loader',
          },
        ],
      },
    ],
  },
  optimization: {
    runtimeChunk: true,
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: 'vendor',
          name: 'vendor',
          chunks: 'all',
        },
      },
    },
  },
  resolve: {
    alias: {
      '~': path.resolve(__dirname, 'src'),
      // Bucklescript compiled js output
      bs: path.resolve(__dirname, 'lib/es6_global/src'),
      'ahc-config': path.join(__dirname, 'src', 'ahc-config'),
      'redux-form': 'redux-form/immutable',
      'react-dom': '@hot-loader/react-dom',
    },
  },
  plugins: [
    new EnvironmentPlugin({
      NODE_ENV: 'development',
      APP_ENV: 'development',
    }),
    new DefinePlugin({
      __TEST__: APP_ENV === 'test',
      __DEV__: APP_ENV === 'development',
      __TESTING__: APP_ENV === 'testing',
      __STAGING__: APP_ENV === 'staging',
      __PROD__: APP_ENV === 'production',
      __EXPERIMENTAL__: !['staging', 'production'].includes(APP_ENV),
      __DEBUG_INFO__: {
        ['Built at']: JSON.stringify(new Date().toLocaleString()),
        ['Commit SHA']: JSON.stringify(
          childProcess.execSync('git rev-parse HEAD').toString()
        ),
      },
    }),
    new HtmlWebpackPlugin({
      filename: 'auth.html',
      inject: 'head',
      title: 'Project Healthcare Login',
      chunks: ['auth', 'runtime~auth', 'vendor'],
    }),
    ...HTML_WEBPACK_PLUGINS,
    new MiniCssExtractPlugin({
      filename: DEV ? '[name].css' : '[name].[hash].css',
      chunkFilename: DEV ? '[id].css' : '[id].[hash].css',
    }),
    new CircularDependencyPlugin({
      allowAsyncCycles: false,
      cwd: process.cwd(),
      exclude: /a\.js|node_modules/,
      failOnError: true,
    }),
    new GenerateSW({
      cacheId: 'project',
      clientsClaim: true,
      runtimeCaching: [
        {
          handler: 'CacheFirst',
          options: {
            cacheableResponse: { statuses: [0, 200] },
            cacheName: 'project-cdn-cache',
            expiration: { maxAgeSeconds: 24 * 60 * 60 },
          },
          urlPattern: new RegExp(Object.values(URL).join('|')),
        },
        {
          handler: 'CacheFirst',
          options: {
            cacheableResponse: { statuses: [0, 200] },
            cacheName: 'project-files-cache',
            expiration: { maxAgeSeconds: 24 * 60 * 60 },
          },
          urlPattern: /(.*)(favicon|keycloak\.json|site\.webmanifest)(.*)/,
        },
        {
          handler: 'CacheFirst',
          options: {
            cacheableResponse: { statuses: [0, 200] },
            cacheName: 'project-fonts-cache',
            expiration: { maxAgeSeconds: 24 * 60 * 60 },
          },
          urlPattern: /(.*)(ahc-fonts|gstatic)(.*)/,
        },
        {
          handler: 'NetworkFirst',
          options: {
            cacheableResponse: { statuses: [0, 200] },
            cacheName: 'project-json-cache',
            expiration: { maxAgeSeconds: 24 * 60 * 60 },
            networkTimeoutSeconds: 5,
          },
          // Endpoints required to load an Assessment
          urlPattern: /(.*)(actionable_items|field_values)(.*)/,
        },
      ],
      skipWaiting: true,
    }),
    new GenerateJsonPlugin('keycloak.json', PRIMARY_KEYCLOAK),
    new GenerateJsonPlugin('partner/keycloak.json', PORTAL_KEYCLOAK),
    // Exclude Moment.js locale files (except for the default "en" locale)
    new IgnorePlugin(/^\.\/locale$/, /moment$/),
  ],
}
  1. Ideally kevin would replace webpack-dev-server but only incrementally serve what it needs too? (Not really sure how it works in this regard or how to set things up to get kevin to do it's magic)
  2. I literally copy/pasted the example from the readme as a jumping off point. Haven't done much except fix the syntax error
const kevin = new Kevin(webpackConfig, {
  kevinPublicPath: 'http://localhost:3000', // this has kevinPublicPath = 'http://localhost'
})

Added "kevdev": "./kevin-dev-server.js", to package.json after chmoding the example and got it to boot but then when I went to localhost:9275 got the whole not serving assets error thing I mentioned above.

@joebeachjoebeach
Copy link
Contributor

Hey @erlandsona, thanks so much for providing examples — super helpful!

Thanks for catching the syntax error in the kevinPublicPath example, too — we'll update the docs to fix that. On that note, this also made me realize that there's another problem with our example — the kevinPublicPath port needs to match the app.listen(<port>) port, so they should both be 9275 or 3000.

Now on to answers:

I threw together a quick example of Kevin being used to manage multiple compilers. You can take a look here.

Something to clear up — Kevin is meant as an analog to webpack-dev-middleware and not webpack-dev-server. I'd recommend reading up on the differences, but a quick explanation is that webpack-dev-server offers a layer of abstraction on top of webpack-dev-middleware (but uses it under the hood). Using webpack-dev-middleware — and therefore using Kevin — requires a little more elbow grease than using webpack-dev-server.

When using Kevin as in your provided code, your server won't be serving any HTML — only JavaScript, so it kind of makes sense that you'd get a 404 by visiting the root route. In order to serve your HTML, you'll need to add an express static middleware after the Kevin middleware. You can see an example of this in the README (where we do app.use("ac/webpack/js" ...ac/webpack/js is just an example. I also made sure to demonstrate this in the example repo I linked to.

Happy to continue advising as you get this in a more workable state for yourself. As you go forward, to get the full benefit of Kevin, you'll want to split your config up into multiple configs, each of which is responsible for building entrypoints just for a portion of your app.

@erlandsona
Copy link
Author

Wanted to say thank you so much for your response.
I brought it up in our architecture meeting and we seem to think this would require more structural changes to implement. Those same changes should lead to webpack's hot realoading being able to handle file changes more gracefully. After which, kevin-middleware might serve us better. But due to the fact that we generate our HTML from webpack and it sounds my team agrees our issue is more structural we're gonna pursue a light re-structuring first then maybe come back to this.

@nvanselow
Copy link

Thanks for the example @joebeachjoebeach! I was slightly confused about how these webpack configs worked and seeing that example cleared it right up. I started building a similar middleware for our project and found this. Great stuff. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants