Skip to content

Commit

Permalink
Adds brotli support for modern javascript (#674)
Browse files Browse the repository at this point in the history
* WIP adding sw as a webpack dependency

* adding sw-config

* Brotli support for modern javascript

* adding brotli plugin to webpack

* fixing conditional plugins

* lint fixes

* fixing error

* bug-fix

* WIP fix watch mode

* fix watch mode

* add the capability to build sw from user land

* fixing comments

* fixing spacing

* fixing in place mutation

* no multi compilers

* Update run-webpack.js

* removing unwanted files

* Update sw-plugin.js

* Update sw-plugin.js

* making changes for workbox v4

* fixes for regexp

* addressing comments

* precacing only index.html

* Update run-webpack.js

* Update run-webpack.js

* fixing kluer dep

* fixing package.json

* fixing unhashed bundle bug

* no json parse

* fixing comments
  • Loading branch information
prateekbh authored Apr 5, 2019
1 parent 1cf03b6 commit efdb448
Show file tree
Hide file tree
Showing 7 changed files with 1,735 additions and 1,333 deletions.
9 changes: 8 additions & 1 deletion packages/cli/lib/commands/build.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const rimraf = require('rimraf');
const { resolve } = require('path');
const { promisify } = require('util');
const { isDir, error } = require('../util');
const { isDir, error, warn } = require('../util');
const runWebpack = require('../lib/webpack/run-webpack');

const toBool = val => val === void 0 || (val === 'false' ? false : val);
Expand All @@ -22,6 +22,13 @@ module.exports = async function(src, argv) {
);
}

if (argv.brotli) {
warn(
'⚛️ ATTENTION! You have enabled BROTLI support. ' +
"In order for this to work correctly, make sure .js.br files are served with 'content-encoding: br' header."
);
}

if (argv.clean === void 0) {
let dest = resolve(cwd, argv.dest);
await promisify(rimraf)(dest);
Expand Down
1 change: 1 addition & 0 deletions packages/cli/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ prog
)
.option('-c, --config', 'Path to custom CLI config', 'preact.config.js')
.option('--esm', 'Builds ES-2015 bundles for your code.', true)
.option('--brotli', 'Adds brotli redirects to the service worker.', false)
.option('--inline-css', 'Adds critical css to the prerendered markup.', true)
.option('-v, --verbose', 'Verbose output')
.action(commands.build);
Expand Down
46 changes: 46 additions & 0 deletions packages/cli/lib/lib/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
self.__precacheManifest = [].concat(self.__precacheManifest || []);

/* global workbox */
/** We are sure brotli is enabled for browsers supporting script type=module
* so we do brotli support only for them.
* We can do brolti support for other browsers but there is no good way of
* feature detect the same at the time of pre-caching.
*/
if (process.env.ENABLE_BROTLI && process.env.ES_BUILD) {
// Alter the precache manifest to precache brotli files instead of gzip files.
self.__precacheManifest = self.__precacheManifest.map(asset => {
if (/.*.js$/.test(asset.url)) {
asset.url = asset.url.replace(/.esm.js$/, '.esm.js.br');
}
return asset;
});

class BrotliRedirectPlugin {
// Before saving the response in cache, we need to treat the headers.
async cacheWillUpdate({ response }) {
const clonedResponse = response.clone();
if (/.js.br(\?.*)?$/.test(clonedResponse.url)) {
const headers = new Headers(clonedResponse.headers);
headers.set('content-type', 'application/javascript');
return new Response(await clonedResponse.text(), { headers });
}
return response;
}
}
workbox.precaching.addPlugins([new BrotliRedirectPlugin()]);
}

const precacheOptions = {};
if (process.env.ENABLE_BROTLI) {
precacheOptions['urlManipulation'] = ({ url }) => {
if (/.esm.js$/.test(url.href)) {
url.href = url.href + '.br';
}
return [url];
};
}

workbox.precaching.precacheAndRoute(self.__precacheManifest, precacheOptions);
workbox.routing.registerNavigationRoute(
workbox.precaching.getCacheKeyForURL('/index.html')
);
110 changes: 110 additions & 0 deletions packages/cli/lib/lib/webpack/sw-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
const BabelEsmPlugin = require('babel-esm-plugin');
const { DefinePlugin } = require('webpack');
const fs = require('fs');
const { resolve } = require('path');
const { info } = require('../../util');
class SWBuilderPlugin {
constructor(config) {
const { src, brotli, esm } = config;
this.brotli_ = brotli;
this.esm_ = esm;
this.src_ = src;
}
apply(compiler) {
let swSrc = resolve(__dirname, '../sw.js');
const exists = fs.existsSync(resolve(`${this.src_}/sw.js`));
if (exists) {
if (exists) {
info(
'⚛️ Detected custom sw.js: compiling instead of default Service Worker.'
);
} else {
info('⚛️ No custom sw.js detected: compiling default Service Worker.');
}
}
compiler.hooks.make.tapAsync(
this.constructor.name,
(compilation, callback) => {
const outputOptions = compiler.options;
const plugins = [
new BabelEsmPlugin({
filename: '[name]-esm.js',
excludedPlugins: ['BabelEsmPlugin', this.constructor.name],
beforeStartExecution: plugins => {
plugins.forEach(plugin => {
if (plugin.constructor.name === 'DefinePlugin') {
if (!plugin.definitions)
throw Error(
'ESM Error: DefinePlugin found without definitions.'
);
plugin.definitions['process.env.ES_BUILD'] = true;
}
});
},
}),
new DefinePlugin({
'process.env.ENABLE_BROTLI': this.brotli_,
'process.env.ES_BUILD': false,
'process.env.NODE_ENV': 'production',
}),
];

/**
* We are deliberatly not passing plugins in createChildCompiler.
* All webpack does with plugins is to call `apply` method on them
* with the childCompiler.
* But by then we haven't given childCompiler a fileSystem or other options
* which a few plugins might expect while execution the apply method.
* We do call the `apply` method of all plugins by ourselves later in the code
*/
const childCompiler = compilation.createChildCompiler(
this.constructor.name
);

childCompiler.context = compiler.context;
childCompiler.options = Object.assign({}, outputOptions);
childCompiler.options.entry = {
sw: swSrc,
};
childCompiler.options.target = 'webworker';
childCompiler.options.output = Object.assign(
{},
childCompiler.options.output,
{ filename: '[name].js' }
);
childCompiler.options.output.filename = '[name].js';

// Call the `apply` method of all plugins by ourselves.
if (Array.isArray(plugins)) {
for (const plugin of plugins) {
plugin.apply(childCompiler);
}
}

childCompiler.apply(
new SingleEntryPlugin(compiler.context, swSrc, 'sw')
);

compilation.hooks.additionalAssets.tapAsync(
this.constructor.name,
childProcessDone => {
childCompiler.runAsChild((err, entries, childCompilation) => {
if (!err) {
compilation.assets = Object.assign(
childCompilation.assets,
compilation.assets
);
}
err && compilation.errors.push(err);
childProcessDone();
});
}
);
callback();
}
);
}
}

module.exports = SWBuilderPlugin;
92 changes: 61 additions & 31 deletions packages/cli/lib/lib/webpack/webpack-client-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const { existsSync } = require('fs');
const merge = require('webpack-merge');
const { filter } = require('minimatch');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
Expand All @@ -13,7 +12,10 @@ const renderHTMLPlugin = require('./render-html-plugin');
const PushManifestPlugin = require('./push-manifest');
const baseConfig = require('./webpack-base-config');
const BabelEsmPlugin = require('babel-esm-plugin');
const { InjectManifest } = require('workbox-webpack-plugin');
const BrotliPlugin = require('brotli-webpack-plugin');
const { normalizePath } = require('../../util');
const SWBuilderPlugin = require('./sw-plugin');

const cleanFilename = name =>
name.replace(
Expand Down Expand Up @@ -215,42 +217,62 @@ function isProd(config) {
},
};

if (config.sw) {
if (config.esm) {
prodConfig.plugins.push(
new SWPrecacheWebpackPlugin({
filename: 'sw.js',
navigateFallback: 'index.html',
navigateFallbackWhitelist: [/^(?!\/__).*/],
minify: true,
stripPrefix: config.cwd,
staticFileGlobsIgnorePatterns: [
/\.esm\.js$/,
/polyfills(\..*)?\.js$/,
/\.map$/,
/push-manifest\.json$/,
/.DS_Store/,
/\.git/,
],
new BabelEsmPlugin({
filename: '[name].[chunkhash:5].esm.js',
chunkFilename: '[name].chunk.[chunkhash:5].esm.js',
excludedPlugins: ['BabelEsmPlugin', 'SWBuilderPlugin'],
beforeStartExecution: (plugins, newConfig) => {
const babelPlugins = newConfig.plugins;
newConfig.plugins = babelPlugins.filter(plugin => {
if (
Array.isArray(plugin) &&
plugin[0].indexOf('fast-async') !== -1
) {
return false;
}
return true;
});
plugins.forEach(plugin => {
if (
plugin.constructor.name === 'DefinePlugin' &&
plugin.definitions
) {
for (const definition in plugin.definitions) {
if (definition === 'process.env.ES_BUILD') {
plugin.definitions[definition] = true;
}
}
} else if (
plugin.constructor.name === 'DefinePlugin' &&
!plugin.definitions
) {
throw new Error(
'WebpackDefinePlugin found but not `process.env.ES_BUILD`.'
);
}
});
},
})
);
config.sw &&
prodConfig.plugins.push(
new InjectManifest({
swSrc: 'sw-esm.js',
include: [/index\.html$/, /\.esm.js$/, /\.css$/, /\.(png|jpg)$/],
precacheManifestFilename: 'precache-manifest.[manifestHash].esm.js',
})
);
}

if (config.esm && config.sw) {
if (config.sw) {
prodConfig.plugins.push(new SWBuilderPlugin(config));
prodConfig.plugins.push(
new SWPrecacheWebpackPlugin({
filename: 'sw-esm.js',
navigateFallback: 'index.html',
navigateFallbackWhitelist: [/^(?!\/__).*/],
minify: true,
stripPrefix: config.cwd,
staticFileGlobsIgnorePatterns: [
/(\.[\w]{5}\.js)/,
/polyfills(\..*)?\.js$/,
/\.map$/,
/push-manifest\.json$/,
/.DS_Store/,
/\.git/,
],
new InjectManifest({
swSrc: 'sw.js',
include: [/index\.html$/, /\.js$/, /\.css$/, /\.(png|jpg)$/],
exclude: [/\.esm\.js$/],
})
);
}
Expand All @@ -263,6 +285,14 @@ function isProd(config) {
prodConfig.plugins.push(new BundleAnalyzerPlugin());
}

if (config.brotli) {
prodConfig.plugins.push(
new BrotliPlugin({
test: /\.esm\.js$/,
})
);
}

return prodConfig;
}

Expand Down
4 changes: 3 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"babel-loader": "^8.0.5",
"babel-plugin-macros": "^2.4.5",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"brotli-webpack-plugin": "^1.0.0",
"console-clear": "^1.0.0",
"copy-webpack-plugin": "^5.0.1",
"critters-webpack-plugin": "^1.3.3",
Expand Down Expand Up @@ -129,6 +130,7 @@
"webpack-fix-style-only-entries": "^0.2.1",
"webpack-merge": "^4.1.0",
"webpack-plugin-replace": "^1.2.0",
"which": "^1.2.14"
"which": "^1.2.14",
"workbox-webpack-plugin": "^4.2.0"
}
}
Loading

0 comments on commit efdb448

Please sign in to comment.