Last active
June 16, 2021 14:56
-
-
Save idudinov/1814a4ce4647c1ec6f9d513a90a5f6b0 to your computer and use it in GitHub Desktop.
PrerenderPlugin for HtmlWebpackPlugin – Pre-renders html during Webpack build phase
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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