Created
February 16, 2018 17:31
-
-
Save quantizor/fb9d02d4baba1d2f36a5c445dc125c5e to your computer and use it in GitHub Desktop.
Isomorphic webpack HMR
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
/** | |
* initial setup: | |
* | |
* npm i babel-register chalk compression express lodash node-persist openport react-dev-utils@~0.5.2 webpack webpack-dev-middleware webpack-hot-middleware | |
*/ | |
import 'babel-register'; | |
import chalk from 'chalk'; | |
import compression from 'compression'; | |
import express from 'express'; | |
import { isPlainObject, reduce } from 'lodash'; | |
import Module from 'module'; | |
import storage from 'node-persist'; | |
import openPort from 'openport'; | |
import path from 'path'; | |
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages'; | |
import open from 'react-dev-utils/openBrowser'; | |
import webpack from 'webpack'; | |
import HotModuleReplacementPlugin from 'webpack/lib/HotModuleReplacementPlugin'; | |
import webpackDevMiddleware from 'webpack-dev-middleware'; | |
import webpackHotMiddleware from 'webpack-hot-middleware'; | |
const __CI__ = !!process.env.CI; | |
const __DEV__ = !__CI__ && process.env.NODE_ENV !== 'production'; | |
function start({ | |
clientConfigPath, | |
openBrowser, | |
routesConfigPath, | |
startUrl = '', | |
}) { | |
port = port || await findPort(8080, 8086); | |
setPort(port); | |
const clientConfig = setupClientWebpackConfig(loadWebpackConfig(clientConfigPath)); | |
const clientCompiler = createCompiler('client', clientConfig); | |
const clientDevMiddleware = createDevMiddleware(clientCompiler); | |
const clientHotMiddleware = __DEV__ ? createHotMiddleware(clientCompiler) : undefined; | |
const routesConfig = loadWebpackConfig(routesConfigPath); | |
const routesCompiler = createCompiler('routes', routesConfig); | |
const routesDevMiddleware = createDevMiddleware(routesCompiler); | |
const boundGetRoutes = getRoutes(routesCompiler.options.output.path, routesDevMiddleware.fileSystem); | |
const boundGetAssetsForEntry = getAssetsForEntry(clientCompiler); | |
const server = express(); | |
app.use(compression()); | |
app.use(routesDevMiddleware); | |
app.use(clientDevMiddleware); | |
if (__DEV__) { | |
app.use(clientHotMiddleware); | |
} | |
server.use('*', routingHandler(boundGetRoutes, boundGetAssetsForEntry)); | |
server.listen(port, () => { | |
shared.utility.log(`Listening on port ${port}`); | |
}); | |
await Promise.all([ | |
clientCompiler.getStats(), | |
routesCompiler.getStats(), | |
]); | |
log.info(`Server running at http://localhost:${port}`); | |
if (openBrowser) { | |
open(`http://localhost:${port}${startUrl}`); | |
} | |
} | |
const toArray = value => Array.isArray(value) ? value : [value]; | |
const PREFIX = chalk.inverse.bold.blue(' dev-web-server '); | |
const log = { | |
info(message) { | |
console.log(PREFIX + ' ' + chalk.reset.bold(message)); | |
}, | |
error(message, errors) { | |
errors = toArray(errors); | |
console.log(PREFIX + ' ' + chalk.reset.bold.red(message)); | |
console.log(); | |
errors.forEach(e => { | |
console.log(e); | |
console.log(); | |
}); | |
}, | |
warning(message, warnings) { | |
warnings = toArray(warnings); | |
console.log(PREFIX + ' ' + chalk.reset.bold.yellow(message)); | |
console.log(); | |
warnings.forEach(w => { | |
console.log(w); | |
console.log(); | |
}); | |
}, | |
}; | |
function createDevMiddleware(compiler) { | |
try { | |
return webpackDevMiddleware(compiler, { | |
logLevel: 'silent', | |
publicPath: compiler.options.output.publicPath, | |
}); | |
} catch (err) { | |
log.error('Failed to setup webpack dev middleware', err); | |
process.exit(1); | |
} | |
} | |
function createHotMiddleware(compiler) { | |
try { | |
return webpackHotMiddleware(compiler, { | |
log: false, | |
}); | |
} catch (err) { | |
log.error('Failed to setup webpack hot middleware', err); | |
process.exit(1); | |
} | |
} | |
function createCompiler(name, config) { | |
let compiler; | |
try { | |
compiler = webpack(config); | |
} catch (err) { | |
log.error(`Failed to compile ${name}`, err); | |
return process.exit(1); | |
} | |
const context = { | |
callbacks: [], | |
isValid: false, | |
}; | |
compiler.plugin('compile', () => { | |
context.isValid = false; | |
log.info(`Compiling ${name}...`); | |
}); | |
compiler.plugin('invalid', () => { | |
context.isValid = false; | |
}); | |
compiler.plugin('done', stats => { | |
context.stats = stats.toJson({}, true); | |
context.isValid = true; | |
process.nextTick(() => { | |
if (!context.isValid) { | |
return; | |
} | |
const { callbacks } = context; | |
context.callbacks = []; | |
callbacks.forEach(cb => cb(context.stats)); | |
}); | |
const messages = formatWebpackMessages(context.stats); | |
if (!messages.errors.length) { | |
log.info(`Successfully compiled ${name}`); | |
} | |
if (messages.errors.length) { | |
log.error(`Failed to compile ${name}`, messages.errors); | |
return; | |
} | |
if (messages.warnings.length) { | |
log.warning(`Compiled ${name} with warnings`, messages.warnings); | |
} | |
}); | |
compiler.getStats = () => new Promise(resolve => { | |
if (context.isValid) { | |
return resolve(context.stats); | |
} | |
context.callbacks.push(resolve); | |
}); | |
return compiler; | |
} | |
function setupClientWebpackConfig(config) { | |
if (!__CI__) { | |
config.entry = addHotUrls(config.entry, [ | |
'react-hot-loader/patch', | |
'webpack-hot-middleware/client?reload=true', | |
]); | |
} | |
return config; | |
} | |
function addHotUrls(entry, hotUrls) { | |
if (typeof entry === 'string') { | |
return [ | |
...hotUrls, | |
entry, | |
]; | |
} | |
if (Array.isArray(entry)) { | |
const polyfillIndex = entry.findIndex(file => /polyfill/.test(file)); | |
if (polyfillIndex > -1) { | |
return [ | |
...entry.slice(0, polyfillIndex + 1), | |
...hotUrls, | |
...entry.slice(polyfillIndex + 1), | |
]; | |
} | |
return [ | |
...hotUrls, | |
...entry, | |
]; | |
} | |
if (isPlainObject(entry)) { | |
return reduce(entry, (modifiedEntry, value, key) => { | |
modifiedEntry[key] = addHotUrls(value, hotUrls); | |
return modifiedEntry; | |
}, {}); | |
} | |
} | |
function getAssetsForEntry(compiler) { | |
return async entry => { | |
const stats = await compiler.getStats(); | |
const assets = [].concat(stats.assetsByChunkName[entry]).map(asset => asset[0] !== '/' ? `/${asset}` : asset); | |
return { | |
cssUrls: assets.filter(asset => /\.css($|\?)/.test(asset)), | |
jsUrls: assets.filter(asset => /\.js($|\?)/.test(asset)), | |
}; | |
}; | |
} | |
const storageDir = path.resolve(path.join(__dirname, '..', '..', '.storage')); | |
const PORT_KEY = 'port'; | |
function findPort(startingPort, endingPort) { | |
return new Promise((resolve, reject) => { | |
openPort.find({ | |
startingPort, | |
endingPort, | |
}, (err, availablePort) => { | |
if (err) { | |
reject(err); | |
return; | |
} | |
resolve(availablePort); | |
}); | |
}); | |
} | |
const initStorage = () => storage.initSync({ | |
dir: storageDir, | |
}); | |
export function setPort(port) { | |
initStorage(); | |
storage.setItemSync(PORT_KEY, port); | |
} | |
export function getPort() { | |
initStorage(); | |
return storage.getItemSync(PORT_KEY); | |
} | |
function parseFile(path, src) { | |
const m = new Module(); | |
m.paths = module.paths; | |
m._compile(src, path); | |
return m.exports; | |
} | |
function requireFromFileSystem(fs, path) { | |
try { | |
const src = fs.readFileSync(path).toString('utf8'); | |
return parseFile(path, src); | |
} catch (err) { | |
// eslint-disable-next-line no-console | |
console.error(chalk.bold.red(err)); | |
} | |
} | |
function loadWebpackConfig(configPath) { | |
try { | |
// eslint-disable-next-line global-require, import/no-dynamic-require | |
let config = require(configPath); | |
if (config && config.default) { | |
config = config.default; | |
} | |
if (__DEV__) { | |
config = Object.assign({}, config, { | |
devServer: { | |
hot: true, | |
}, | |
plugins: Array.prototype.concat(config.plugins, new HotModuleReplacementPlugin()), | |
}); | |
} | |
return config; | |
} catch (err) { | |
log.error(`Failed to load webpack config from ${configPath}`, err); | |
process.exit(1); | |
} | |
} | |
function getRoutes(outputPath, fileSystem) { | |
return () => { | |
/** this setup assumes the SSR config is outputted to "routes.js" */ | |
return requireFromFileSystem(fileSystem, `${outputPath}/routes.js`).default; | |
}; | |
} | |
// the so-called "business logic middleware" | |
function routingHandler(getRoutes, getAssetsForEntry) { | |
return async (req, res, next) => { | |
const url = req.originalUrl; | |
const routes = getRoutes(); | |
// do something with the routes (they can take whatever shape you want, just a convention) | |
// this is generally where your application logic would start | |
}; | |
} | |
start({ | |
clientConfigPath: 'path/to/your/client/config.js', | |
routesConfigPath: 'path/to/your/routes/config.js', | |
/** want your default browser to automatically open? */ | |
openBrowser: true, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment