Skip to content

Instantly share code, notes, and snippets.

@idudinov
Last active June 16, 2021 14:56
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save idudinov/1814a4ce4647c1ec6f9d513a90a5f6b0 to your computer and use it in GitHub Desktop.
Save idudinov/1814a4ce4647c1ec6f9d513a90a5f6b0 to your computer and use it in GitHub Desktop.
PrerenderPlugin for HtmlWebpackPlugin – Pre-renders html during Webpack build phase
const HtmlWebpackPlugin = require('html-webpack-plugin');
const jsdom = require('jsdom');
/** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
/** @typedef {(import 'jsdom').ResourceLoaderConstructorOptions} ResourceLoaderConstructorOptions */
class PrerenderHtmlPlugin {
constructor(options) {
this._options = options || { };
this._location = this._options.documentUrl || 'http://localhost/';
}
/**
* apply is called by the webpack main compiler during the start phase
* @param {WebpackCompiler} compiler
*/
apply(compiler) {
compiler.hooks.compilation.tap('PrerenderHtmlPlugin',
/** @param {WebpackCompilation} */
(compilation) => {
// Staic Plugin interface |compilation |HOOK NAME | register listener
HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
'PrerenderHtmlPlugin',
(data, cb) => {
// get some custom data from the plugin's options
const pageRoute = data.plugin.options.page.route;
const url = this._location + pageRoute;
// console.log('\r\n\t ========== PrerenderHtmlPlugin ===> url =', url);
const virtualConsole = new jsdom.VirtualConsole({ omitJSDOMErrors: false }).sendTo(console);
const dom = new jsdom.JSDOM(data.html, {
// suppress console-proxied eval() errors, but keep console proxying
virtualConsole,
// `url` sets the value returned by `window.location`, `document.URL`...
// Useful for routers that depend on the current URL (such as react-router or reach-router)
url,
// don't track source locations for performance reasons
includeNodeLocations: false,
// don't allow inline event handlers & script tag exec
runScripts: 'outside-only',
pretendToBeVisual: true,
// resources: new CustomResourceLoader({}, compilation, this._location),
beforeParse(w) {
w.scrollTo = () => {};
// this can be used in client code
w.SSR = true;
// Inject a require shim
w.require = moduleId => {
const asset = compilation.assets[moduleId.replace(/^\.?\//g, '')];
if (!asset) {
throw Error(`Error: Module not found. attempted require("${moduleId}")`);
}
const mod = { exports: {} };
w.eval(`(function(exports, module, require){\n${asset.source()}\n})`)(mod.exports, mod, w.require);
return mod.exports;
};
// Add window.appReady event (instead of window.onload)
// w.eval("'use strict';var ild=!1,cbs=[],w=window;w.addEventListener('load',function(){ild=!0,cbs.forEach(function(a){try{a()}catch(b){}}),cbs=null}),w.appReady=function(a){a&&(ild?a():cbs.push(a))};");
},
});
const window = dom.window;
compilation.chunks.forEach(chunk => {
chunk.files.forEach(fileName => {
if (!fileName.endsWith('.js')) {
return;
}
// console.log('\r\n\t ========== PrerenderHtmlPlugin EXECUTING: ', chunk.id, fileName);
const asset = compilation.assets[fileName];
const js = asset.source();
// Execute main JS bundle
window.eval(js);
// console.log('\r\n\t ========== PrerenderHtmlPlugin EXECUTED: ', chunk.id, fileName);
});
});
let finished = false;
const finishRender = () => {
if (finished) {
return;
}
finished = true;
try {
data.html = dom.serialize();
} finally {
dom.window.close();
// console.log('\r\n\t ========== PrerenderHtmlPlugin FINISHED: ', data.plugin.options.page.name);
// Tell webpack to move on
cb(null, data);
}
};
// window.appReady(finishRender);
// it's safer just to wait a while
setTimeout(finishRender, 2000);
},
);
});
}
}
module.exports = PrerenderHtmlPlugin;
const HtmlWebPackPlugin = require('html-webpack-plugin');
const PrerenderPlugin = require('./prerenderPlugin');
module.exports = {
// ...
plugins: [
// ...
// no need to change any setting there and for html loaders
new HtmlWebPackPlugin({ /* ... */}),
// use this after HtmlWebPackPlugin
// but just one instance for any amount of HtmlWebPackPlugin instances
new PrerenderPlugin(),
],
};
// package.json
{
// ...
"devDependencies": {
// ...
"html-webpack-plugin": "^4.0.0-beta.2",
"jsdom": "^12.2.0",
// ...
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment