egoist / poi

⚡A zero-config bundler for JavaScript applications.
https://poi.js.org
MIT License
5.23k stars 255 forks source link

HTTP/2 Server push support #213

Closed abelovic closed 7 years ago

abelovic commented 7 years ago

Do you want to request a feature or report a bug?

feature

If this is a feature request, what is motivation or use case for changing the behavior?

Support HTTP/2 server push

Netlify just came out with support for this (I'm sure other hosting providers will soon too). They requires you to add the following to a _headers file like:

/server-push-path
  Link: </js/example-script.js>; rel=preload; as=script
  Link: </css/example-style.css>; rel=preload; as=style

https://www.netlify.com/blog/2017/07/18/http/2-server-push-on-netlify/

Is there a way to get the dynamic names from the webpack files into another file? Maybe something like you do with the html files i.e. maybe process those the same way if we add them to a special folder?

<% (htmlWebpackPlugin.options.description) %>

Or is it better to just create this file inside poi.config.js? If so how what is the recommended way to do this and how do I extract the name dynamically inside the config?

File should look something like this:

/server-push-path
  Link: </client.607b49a9.css>; rel=preload; as=style
  Link: </manifest.b949c5c6.js>; rel=preload; as=script
  Link: </vendor.f791af86.js>; rel=preload; as=script
  Link: </client.78c0a4a5.js>; rel=preload; as=script

Here is my config:

const path = require('path');
const OfflinePlugin = require('offline-plugin');
const webpack = require('webpack');

module.exports = (options, req) => ({

  host: '0.0.0.0',
  port: 4000,
  entry: './src/index.js',
  html: {
    template: './src/index.ejs'
  },
  devServer: {
    host: '0.0.0.0',
    port: 4000
  },
  extendWebpack(config) {

    config.resolve.alias
      .set('components', path.resolve('./src/components'))
      .set( 'TweenLite', path.resolve('./src/vendor/gsap/TweenLite') );

    config.plugin('provide')
      .use(webpack.ProvidePlugin, [{
        TweenLite: 'gsap'
      }]);

    // inject offline-plugin in production build. Make it the last plugin added
    if (options.mode === 'production') {

      config.plugin('offline')
        .use(OfflinePlugin, [{
          ServiceWorker: {
            events: true
          }
        }]);
    }

    return config
  }
});

Please mention other relevant information such as the browser version, Node.js version, Poi version and Operating System.

Chrome v59 Node v6.11.1 Poi 9.1.3 Windows 10

egoist commented 7 years ago

I think this can be done in a standalone webpack plugin or via html-webpack-plugin

btw _headers file seems like a netlify-only feature, I'm not sure if other hosts will support http/2 header compression in the same way.

abelovic commented 7 years ago

@egoist - yes it is only netlify who do serverless hosting which is why they do it this way

I'm trying to write the file dynamically but can't figure out how to get the hashed file names from the config. Is there a way to get this (they all have a different chunkhash)?

egoist commented 7 years ago

Yep. See my last comment

egoist commented 7 years ago

I wrote an example webpack plugin for you, see https://gist.github.com/egoist/969cd7e8c0673eabba2123cdbb24a0e3

abelovic commented 7 years ago

@egoist - thanks for this!

I was trying to use the optimize-chunks compilation event but I think your way is much better because it catches resources in the static folder as well :)

I did a few changes i.e. did a filter before map so we didn't generate types in the list that were undefined and also wrote the actual file to the dist folder.

In case others find it useful this is what I changed:

const fs = require('fs');

const getType = v => {
  if (v.endsWith('.js')) return 'script';
  if (v.endsWith('.css')) return 'style';
};

function HeadersPlugin({destination}) {
  this.destination = destination
}

HeadersPlugin.prototype.apply = function (compiler) {

    compiler.plugin('done', stats => {

      const res = stats.toJson();
      const { assets, publicPath } = res;

      const result = `/server-push-path
          ${assets
            .filter(asset => {

              if(getType(asset.name) !== undefined) return asset

            }).map(asset => {

                return ` Link: <${publicPath}${asset.name}>; rel=preload; as=${getType(asset.name)}`
            })
            .join('\n')
          }`;

      fs.writeFile(this.destination, new Buffer(result), (err) => {

        if (err) throw 'Error writing _headers file: ' + err;

        console.log('_headers file written successfully');
      });
    });
};

module.exports = HeadersPlugin;