Skip to content

Instantly share code, notes, and snippets.

@quantizor
Created February 16, 2018 17:31
Show Gist options
  • Save quantizor/fb9d02d4baba1d2f36a5c445dc125c5e to your computer and use it in GitHub Desktop.
Save quantizor/fb9d02d4baba1d2f36a5c445dc125c5e to your computer and use it in GitHub Desktop.
Isomorphic webpack HMR
/**
* 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