New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
require.ensure support #31
Conversation
This should fix #10. |
I'm glad I'm not alone trying to get this one sorted out.
So replace this:
with:
What I cannot make working is the actual replace before files are compiled from within static-site-generator-webpack-plugin plugin. Any help is appreciated. |
I don't think that would work. Because once you're in the plugin's |
Agree, that won't work. |
Pinging @markdalgleish: While this doesn't enable statically rendering async routes, it provides a fix for cases where |
FWIW I tried changing the scope to |
@jonathaningram: Probably you did something else like |
@herrstucki I am using webpack require(['./SimpleComponent'], function(Com) {
console.log(Com)
})
export default (locals, callback) => {
callback(null, 'todo')
} You can see it's doing the hot module thing and using (function webpackUniversalModuleDefinition(root, factory) {
if (typeof exports === 'object' && typeof module === 'object')
module.exports = factory()
else if (typeof define === 'function' && define.amd)
define([], factory)
else {
var a = factory()
for (var i in a)(typeof exports === 'object' ? exports : root)[i] = a[i]
}
})(this, function() {
return (function(modules) { // webpackBootstrap // install a JSONP callback for chunk loading
var parentJsonpFunction = window['webpackJsonp']
window['webpackJsonp'] = function webpackJsonpCallback(chunkIds, moreModules) { // add 'moreModules' to the modules object, // then flag all 'chunkIds' as loaded and fire callback
var moduleId, chunkId, i = 0,
callbacks = []
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i]
if (installedChunks[chunkId])
callbacks.push.apply(callbacks, installedChunks[chunkId])
installedChunks[chunkId] = 0
}
for (moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId]
}
if (parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules)
while (callbacks.length)
callbacks.shift().call(null, __webpack_require__)
}
var parentHotUpdateCallback = this['webpackHotUpdate']
this['webpackHotUpdate'] =
function webpackHotUpdateCallback(chunkId, moreModules) { // eslint-disable-line no-unused-vars
hotAddUpdateChunk(chunkId, moreModules)
if (parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules)
}
function hotDownloadUpdateChunk(chunkId) { // eslint-disable-line no-unused-vars
var head = document.getElementsByTagName('head')[0]
var script = document.createElement('script')
script.type = 'text/javascript'
script.charset = 'utf-8'
script.src = __webpack_require__.p + '' + chunkId + '.' + hotCurrentHash + '.hot-update.js'
head.appendChild(script)
}
function hotDownloadManifest(callback) { // eslint-disable-line no-unused-vars
if (typeof XMLHttpRequest === 'undefined')
return callback(new Error('No browser support'))
try {
var request = new XMLHttpRequest()
var requestPath = __webpack_require__.p + '' + hotCurrentHash + '.hot-update.json'
request.open('GET', requestPath, true)
request.timeout = 10000
request.send(null)
} catch (err) {
return callback(err)
}
request.onreadystatechange = function() {
if (request.readyState !== 4) return
if (request.status === 0) { // timeout
callback(new Error('Manifest request to ' + requestPath + ' timed out.'))
} else if (request.status === 404) { // no update available
callback()
} else if (request.status !== 200 && request.status !== 304) { // other failure
callback(new Error('Manifest request to ' + requestPath + ' failed.'))
} else { // success
try {
var update = JSON.parse(request.responseText)
} catch (e) {
callback(e)
return
}
callback(null, update)
}
}
}
var hotApplyOnUpdate = true
var hotCurrentHash = '85e82da68c44b4323f09' // eslint-disable-line no-unused-vars
var hotCurrentModuleData = {}
var hotCurrentParents = [] // eslint-disable-line no-unused-vars
function hotCreateRequire(moduleId) { // eslint-disable-line no-unused-vars
var me = installedModules[moduleId]
if (!me) return __webpack_require__
var fn = function(request) {
if (me.hot.active) {
if (installedModules[request]) {
if (installedModules[request].parents.indexOf(moduleId) < 0)
installedModules[request].parents.push(moduleId)
if (me.children.indexOf(request) < 0)
me.children.push(request)
} else hotCurrentParents = [moduleId]
} else {
console.warn('[HMR] unexpected require(' + request + ') from disposed module ' + moduleId)
hotCurrentParents = []
}
return __webpack_require__(request)
}
for (var name in __webpack_require__) {
if (Object.prototype.hasOwnProperty.call(__webpack_require__, name)) {
Object.defineProperty(fn, name, (function(name) {
return {
configurable: true,
enumerable: true,
get: function() {
return __webpack_require__[name]
},
set: function(value) {
__webpack_require__[name] = value
}
}
}(name)))
}
}
Object.defineProperty(fn, 'e', {
enumerable: true,
value: function(chunkId, callback) {
if (hotStatus === 'ready')
hotSetStatus('prepare')
hotChunksLoading++
__webpack_require__.e(chunkId, function() {
try {
callback.call(null, fn)
} finally {
finishChunkLoading()
}
function finishChunkLoading() {
hotChunksLoading--
if (hotStatus === 'prepare') {
if (!hotWaitingFilesMap[chunkId]) {
hotEnsureUpdateChunk(chunkId)
}
if (hotChunksLoading === 0 && hotWaitingFiles === 0) {
hotUpdateDownloaded()
}
}
}
})
}
})
return fn
}
function hotCreateModule(moduleId) { // eslint-disable-line no-unused-vars
var hot = { // private stuff
_acceptedDependencies: {},
_declinedDependencies: {},
_selfAccepted: false,
_selfDeclined: false,
_disposeHandlers: [], // Module API
active: true,
accept: function(dep, callback) {
if (typeof dep === 'undefined')
hot._selfAccepted = true
else if (typeof dep === 'function')
hot._selfAccepted = dep
else if (typeof dep === 'object')
for (var i = 0; i < dep.length; i++)
hot._acceptedDependencies[dep[i]] = callback
else
hot._acceptedDependencies[dep] = callback
},
decline: function(dep) {
if (typeof dep === 'undefined')
hot._selfDeclined = true
else if (typeof dep === 'number')
hot._declinedDependencies[dep] = true
else
for (var i = 0; i < dep.length; i++)
hot._declinedDependencies[dep[i]] = true
},
dispose: function(callback) {
hot._disposeHandlers.push(callback)
},
addDisposeHandler: function(callback) {
hot._disposeHandlers.push(callback)
},
removeDisposeHandler: function(callback) {
var idx = hot._disposeHandlers.indexOf(callback)
if (idx >= 0) hot._disposeHandlers.splice(idx, 1)
}, // Management API
check: hotCheck,
apply: hotApply,
status: function(l) {
if (!l) return hotStatus
hotStatusHandlers.push(l)
},
addStatusHandler: function(l) {
hotStatusHandlers.push(l)
},
removeStatusHandler: function(l) {
var idx = hotStatusHandlers.indexOf(l)
if (idx >= 0) hotStatusHandlers.splice(idx, 1)
}, //inherit from previous dispose call
data: hotCurrentModuleData[moduleId]
}
return hot
}
var hotStatusHandlers = []
var hotStatus = 'idle'
function hotSetStatus(newStatus) {
hotStatus = newStatus
for (var i = 0; i < hotStatusHandlers.length; i++)
hotStatusHandlers[i].call(null, newStatus)
} // while downloading
var hotWaitingFiles = 0
var hotChunksLoading = 0
var hotWaitingFilesMap = {}
var hotRequestedFilesMap = {}
var hotAvailibleFilesMap = {}
var hotCallback // The update info
var hotUpdate, hotUpdateNewHash
function toModuleId(id) {
var isNumber = (+id) + '' === id
return isNumber ? +id : id
}
function hotCheck(apply, callback) {
if (hotStatus !== 'idle') throw new Error('check() is only allowed in idle status')
if (typeof apply === 'function') {
hotApplyOnUpdate = false
callback = apply
} else {
hotApplyOnUpdate = apply
callback = callback || function(err) {
if (err) throw err
}
}
hotSetStatus('check')
hotDownloadManifest(function(err, update) {
if (err) return callback(err)
if (!update) {
hotSetStatus('idle')
callback(null, null)
return
}
hotRequestedFilesMap = {}
hotAvailibleFilesMap = {}
hotWaitingFilesMap = {}
for (var i = 0; i < update.c.length; i++)
hotAvailibleFilesMap[update.c[i]] = true
hotUpdateNewHash = update.h
hotSetStatus('prepare')
hotCallback = callback
hotUpdate = {}
for (var chunkId in installedChunks) { // eslint-disable-line no-lone-blocks
/*globals chunkId */
hotEnsureUpdateChunk(chunkId)
}
if (hotStatus === 'prepare' && hotChunksLoading === 0 && hotWaitingFiles === 0) {
hotUpdateDownloaded()
}
})
}
function hotAddUpdateChunk(chunkId, moreModules) { // eslint-disable-line no-unused-vars
if (!hotAvailibleFilesMap[chunkId] || !hotRequestedFilesMap[chunkId])
return
hotRequestedFilesMap[chunkId] = false
for (var moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
hotUpdate[moduleId] = moreModules[moduleId]
}
}
if (--hotWaitingFiles === 0 && hotChunksLoading === 0) {
hotUpdateDownloaded()
}
}
function hotEnsureUpdateChunk(chunkId) {
if (!hotAvailibleFilesMap[chunkId]) {
hotWaitingFilesMap[chunkId] = true
} else {
hotRequestedFilesMap[chunkId] = true
hotWaitingFiles++
hotDownloadUpdateChunk(chunkId)
}
}
function hotUpdateDownloaded() {
hotSetStatus('ready')
var callback = hotCallback
hotCallback = null
if (!callback) return
if (hotApplyOnUpdate) {
hotApply(hotApplyOnUpdate, callback)
} else {
var outdatedModules = []
for (var id in hotUpdate) {
if (Object.prototype.hasOwnProperty.call(hotUpdate, id)) {
outdatedModules.push(toModuleId(id))
}
}
callback(null, outdatedModules)
}
}
function hotApply(options, callback) {
if (hotStatus !== 'ready') throw new Error('apply() is only allowed in ready status')
if (typeof options === 'function') {
callback = options
options = {}
} else if (options && typeof options === 'object') {
callback = callback || function(err) {
if (err) throw err
}
} else {
options = {}
callback = callback || function(err) {
if (err) throw err
}
}
function getAffectedStuff(module) {
var outdatedModules = [module]
var outdatedDependencies = {}
var queue = outdatedModules.slice()
while (queue.length > 0) {
var moduleId = queue.pop()
var module = installedModules[moduleId]
if (!module || module.hot._selfAccepted)
continue
if (module.hot._selfDeclined) {
return new Error('Aborted because of self decline: ' + moduleId)
}
if (moduleId === 0) {
return
}
for (var i = 0; i < module.parents.length; i++) {
var parentId = module.parents[i]
var parent = installedModules[parentId]
if (parent.hot._declinedDependencies[moduleId]) {
return new Error('Aborted because of declined dependency: ' + moduleId + ' in ' + parentId)
}
if (outdatedModules.indexOf(parentId) >= 0) continue
if (parent.hot._acceptedDependencies[moduleId]) {
if (!outdatedDependencies[parentId])
outdatedDependencies[parentId] = []
addAllToSet(outdatedDependencies[parentId], [moduleId])
continue
}
delete outdatedDependencies[parentId]
outdatedModules.push(parentId)
queue.push(parentId)
}
}
return [outdatedModules, outdatedDependencies]
}
function addAllToSet(a, b) {
for (var i = 0; i < b.length; i++) {
var item = b[i]
if (a.indexOf(item) < 0)
a.push(item)
}
} // at begin all updates modules are outdated // the 'outdated' status can propagate to parents if they don't accept the children
var outdatedDependencies = {}
var outdatedModules = []
var appliedUpdate = {}
for (var id in hotUpdate) {
if (Object.prototype.hasOwnProperty.call(hotUpdate, id)) {
var moduleId = toModuleId(id)
var result = getAffectedStuff(moduleId)
if (!result) {
if (options.ignoreUnaccepted)
continue
hotSetStatus('abort')
return callback(new Error('Aborted because ' + moduleId + ' is not accepted'))
}
if (result instanceof Error) {
hotSetStatus('abort')
return callback(result)
}
appliedUpdate[moduleId] = hotUpdate[moduleId]
addAllToSet(outdatedModules, result[0])
for (var moduleId in result[1]) {
if (Object.prototype.hasOwnProperty.call(result[1], moduleId)) {
if (!outdatedDependencies[moduleId])
outdatedDependencies[moduleId] = []
addAllToSet(outdatedDependencies[moduleId], result[1][moduleId])
}
}
}
} // Store self accepted outdated modules to require them later by the module system
var outdatedSelfAcceptedModules = []
for (var i = 0; i < outdatedModules.length; i++) {
var moduleId = outdatedModules[i]
if (installedModules[moduleId] && installedModules[moduleId].hot._selfAccepted)
outdatedSelfAcceptedModules.push({
module: moduleId,
errorHandler: installedModules[moduleId].hot._selfAccepted
})
} // Now in 'dispose' phase
hotSetStatus('dispose')
var queue = outdatedModules.slice()
while (queue.length > 0) {
var moduleId = queue.pop()
var module = installedModules[moduleId]
if (!module) continue
var data = {} // Call dispose handlers
var disposeHandlers = module.hot._disposeHandlers
for (var j = 0; j < disposeHandlers.length; j++) {
var cb = disposeHandlers[j]
cb(data)
}
hotCurrentModuleData[moduleId] = data // disable module (this disables requires from this module)
module.hot.active = false // remove module from cache
delete installedModules[moduleId] // remove 'parents' references from all children
for (var j = 0; j < module.children.length; j++) {
var child = installedModules[module.children[j]]
if (!child) continue
var idx = child.parents.indexOf(moduleId)
if (idx >= 0) {
child.parents.splice(idx, 1)
}
}
} // remove outdated dependency from module children
for (var moduleId in outdatedDependencies) {
if (Object.prototype.hasOwnProperty.call(outdatedDependencies, moduleId)) {
var module = installedModules[moduleId]
var moduleOutdatedDependencies = outdatedDependencies[moduleId]
for (var j = 0; j < moduleOutdatedDependencies.length; j++) {
var dependency = moduleOutdatedDependencies[j]
var idx = module.children.indexOf(dependency)
if (idx >= 0) module.children.splice(idx, 1)
}
}
} // Not in 'apply' phase
hotSetStatus('apply')
hotCurrentHash = hotUpdateNewHash // insert new code
for (var moduleId in appliedUpdate) {
if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId]
}
} // call accept handlers
var error = null
for (var moduleId in outdatedDependencies) {
if (Object.prototype.hasOwnProperty.call(outdatedDependencies, moduleId)) {
var module = installedModules[moduleId]
var moduleOutdatedDependencies = outdatedDependencies[moduleId]
var callbacks = []
for (var i = 0; i < moduleOutdatedDependencies.length; i++) {
var dependency = moduleOutdatedDependencies[i]
var cb = module.hot._acceptedDependencies[dependency]
if (callbacks.indexOf(cb) >= 0) continue
callbacks.push(cb)
}
for (var i = 0; i < callbacks.length; i++) {
var cb = callbacks[i]
try {
cb(outdatedDependencies)
} catch (err) {
if (!error)
error = err
}
}
}
} // Load self accepted modules
for (var i = 0; i < outdatedSelfAcceptedModules.length; i++) {
var item = outdatedSelfAcceptedModules[i]
var moduleId = item.module
hotCurrentParents = [moduleId]
try {
__webpack_require__(moduleId)
} catch (err) {
if (typeof item.errorHandler === 'function') {
try {
item.errorHandler(err)
} catch (err) {
if (!error)
error = err
}
} else if (!error)
error = err
}
} // handle errors in accept handlers and self accepted module load
if (error) {
hotSetStatus('fail')
return callback(error)
}
hotSetStatus('idle')
callback(null, outdatedModules)
} // The module cache
var installedModules = {} // object to store loaded and loading chunks // '0' means 'already loaded' // Array means 'loading', array contains callbacks
var installedChunks = {
0: 0
} // The require function
function __webpack_require__(moduleId) { // Check if module is in cache
if (installedModules[moduleId])
return installedModules[moduleId].exports // Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
exports: {},
id: moduleId,
loaded: false,
hot: hotCreateModule(moduleId),
parents: hotCurrentParents,
children: []
} // Execute the module function
modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId)) // Flag the module as loaded
module.loaded = true // Return the exports of the module
return module.exports
} // This file contains only the entry chunk. // The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId, callback) { // '0' is the signal for 'already loaded'
if (installedChunks[chunkId] === 0)
return callback.call(null, __webpack_require__) // an array means 'currently loading'.
if (installedChunks[chunkId] !== undefined) {
installedChunks[chunkId].push(callback)
} else { // start chunk loading
installedChunks[chunkId] = [callback]
var head = document.getElementsByTagName('head')[0]
var script = document.createElement('script')
script.type = 'text/javascript'
script.charset = 'utf-8'
script.async = true
script.src = __webpack_require__.p + '' + chunkId + '.assets/' + hotCurrentHash + '.js'
head.appendChild(script)
}
} // expose the modules object (__webpack_modules__)
__webpack_require__.m = modules // expose the module cache
__webpack_require__.c = installedModules // __webpack_public_path__
__webpack_require__.p = '' // __webpack_hash__
__webpack_require__.h = function() {
return hotCurrentHash
} // Load entry module and return exports
return hotCreateRequire(0)(0)
})
([
/* 0 */
function(module, exports, __webpack_require__) {
Object.defineProperty(exports, '__esModule', {
value: true
})
if (typeof window !== 'undefined') {
__webpack_require__.e /* require */ (1, function(__webpack_require__) {
var __WEBPACK_AMD_REQUIRE_ARRAY__ = [__webpack_require__(1)]
(function(Com) {
console.log(Com)
}.apply(null, __WEBPACK_AMD_REQUIRE_ARRAY__))
})
}
exports.default = function(locals, callback) {
callback(null, 'todo')
}
}
])
}) |
@jonathaningram I'm sorry, my previous statement wasn't accurate: So, a simple React case where this works: class Foo extends React.Component {
componentDidMount() {
// Does not get called during static site build
require.ensure([], () => {
// This chunk will only be loaded when this component is mounted
const foo = require('./foo');
// ...
})
}
render() {
return <div />;
}
}
// In the static route handler …
ReactDOM.renderToString(<Foo />); Now, it probably won't work with React Router's I think it could work if webpack allowed a separate entry point for the static build with |
@herrstucki thanks for the clarification. Correct, in my tests I was putting the |
@herrstucki this looks like a nice tidy PR. My only concern is that this could be a breaking chage if you have any code in your bundle that detects whether you're in a node context or not by evaluating |
Yes, that's certainly a concern. The example you provide in the README uses Or we could allow to define the eval scope via the plugin's options, so it's opt-in. |
I think allowing a scope object to be passed in as an option is probably the safest and most flexible way forward here, but it will probably require an API change since this plugin arguably has too many arguments already. I'll need to think about this a little. |
@herrstucki how about we deprecate the current API in favour of something more flexible like this, which allows for a new StaticSiteGeneratorPlugin({
input: 'main',
paths: [
...
],
locals: {
...
},
scope: {
window: {}
}
}) |
@herrstucki maybe in this PR it'd be a good idea to add an extra |
Actually, your commits are good enough to merge, I'll make the changes myself. Thanks for the really great PR 😄 |
Just published |
This adds support for webpack's
require.ensure
which enables code splitting and async chunk loading.When the script (not the
require.ensure
itself) is evaluated, webpack expects awindow
object; this is solved by supplying an empty object tonode-eval
's scope.Note that it's not possible to use
require.ensure
during the static build (because it expects a DOM) but it's now possible to do code splitting with React Router like it's described in Ryan Florence's Welcome to the Future of Web Application Delivery or in something likecomponentDidMount
. For example:Potentially breaking if someone checks for
window
to do the client side render instead ofdocument
.