etsy / kevin-middleware

This is an Express middleware that makes developing javascript in a monorepo easier.
MIT License
110 stars 5 forks source link

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

Open erlandsona opened 4 years ago

erlandsona commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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()),

      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$/),

], }

2. 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)
3. 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 `chmod`ing 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 commented 4 years ago

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 commented 4 years ago

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 commented 2 years ago

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!