Currently Backpack is focused on providing the best development experience for Node.js server-side applications and libraries.
This was a deliberate decision.
The idea is that people should use tools like Create React App or Next.js for their frontend and then use Backpack to build out API's etc. My goal was to create a complimentary tool.
However, it appears that the community wants a some sort of drop-in solution like CRA's react-scripts but for isomorphic / universal apps. I'm not sure why it isn't more popular, but this is what the New York Times' kyt project aims to do. Personally, I don't care for the aesthetics of kyt (e.g. the emoji's in the console) and for some of the conventions (like extract text/css and the eslint-config airbnb), but those are just my opinions. Regardless, Backpack could technically accommodate universal apps with a few small, yet non-trivial modifications.
In my research, I've come up with two approaches to Universal Apps with Hot Module Replacement and server reloading worth considering:
(Note: these have been extracted from other React server-side rendered projects of mine. However, they are not yet drop-in replacements to backpack dev
. They do not currently handle custom webpack modifications like Backpack's currently does. These are just POC's).
1. Chokidar, Nodemon, Webpack-Hot-Middleware
This serves up client-side assets on another port like localhost:3001
. It would be up to the user to properly reference where the assets are served from in their apps. This isn't necessarily a bad thing though, as these frontend assets should ideally be served from a CDN in production anyways. We could handle this by providing a Webpack flag to the server for use in the application's HTML template such as BACKPACK_ASSETS_URL
.
// Proof of concept #1 dev.js
const nodemon = require('nodemon')
const path = require('path')
const chokidar = require('chokidar')
const express = require('express')
const webpack = require('webpack')
const url = require('url')
const once = require('ramda').once
const devMiddleware = require('webpack-dev-middleware')
const hotMiddleware = require('webpack-hot-middleware')
const serverConfig = require('../config/webpack.dev.server')
const clientConfig = require('../config/webpack.dev.client')
const { clientUrl, serverSrcPath } = require('../config/paths')
process.on('SIGINT', process.exit)
let clientCompiler, serverCompiler
const startServer = () => {
const serverPaths = Object
.keys(serverCompiler.options.entry)
.map(entry => path.join(serverCompiler.options.output.path, `${entry}.js`))
const mainPath = path.join(serverCompiler.options.output.path, 'main.js')
nodemon({ script: mainPath, watch: serverPaths, flags: [] })
.once('start', () => {
//console.log(`NODEMON: Server running at: ${'http://localhost:3000'}`)
//console.log('NODEMON: Development started')
})
// .on('restart', () => console.log('Server restarted'))
.on('quit', process.exit)
}
const afterClientCompile = once(() => {
// console.log('[WEBPACK-CLIENT]: Setup RHL')
// console.log('[WEBPACK-CLIENT]: Done compiling client')
})
const compileServer = () => serverCompiler.run(() => undefined)
clientCompiler = webpack(clientConfig, (err, stats) => {
if (err) return
afterClientCompile()
compileServer()
})
const startClient = () => {
const devOptions = clientCompiler.options.devServer
const app = express()
const webpackDevMiddleware = devMiddleware(clientCompiler, devOptions)
app.use(webpackDevMiddleware)
app.use(hotMiddleware(clientCompiler, {
log: () => {}
}))
app.listen(url.parse(clientUrl).port)
// console.log('[WEBPACK-CLIENT]: Started asset server on http://localhost:' + url.parse(clientUrl).port)
}
const startServerOnce = once(() => startServer())
const watcher = chokidar.watch([serverSrcPath])
watcher.on('ready', () => {
watcher
.on('add', compileServer)
.on('addDir', compileServer)
.on('change', compileServer)
.on('unlink', compileServer)
.on('unlinkDir', compileServer)
})
serverCompiler = webpack(serverConfig, (err, stats) => {
if (err) return
startServerOnce()
})
startClient()
2. BrowserSync, Proxy-Middleware, Webpack-Hot-Middleware
The following is heavily inspired by Ueno's React Starter project.. It uses a http-proxy and browser-sync to work out the ports. My only criticism of this technique is that browser-sync and Docker do not play nicely with each other at all (last time i checked). That is either irrelevant or a dealbreaker for people.
const path = require('path')
const url = require('url')
const bs = require('browser-sync').create();
const webpack = require('webpack')
const proxyMiddleware = require('http-proxy-middleware');
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const color = require('cli-color');
const debug = require('./debug');
const serverConfig = require('./webpack.dev.server')
const clientConfig = require('./webpack.dev.client')
const { clientUrl, serverSrcPath } = require('./buildConfig')
const domain = color.magentaBright('webpack');
// Get ports
const port = (parseInt(process.env.PORT, 10) || 3000) - 1;
const proxyPort = port + 1;
// Create compilers
const clientCompiler = webpack(clientConfig);
const serverCompiler = webpack(serverConfig);
// Logging
const log = (...args) => debug(domain, ...args);
// Build container
const build = {
failed: false,
first: true,
connections: [],
};
const devMiddleware = webpackDevMiddleware(clientCompiler, {
publicPath: '/',
noInfo: true,
quiet: true,
stats: {
timings: false,
version: false,
hash: false,
assets: false,
chunks: false,
colors: true,
},
});
serverCompiler.plugin('done', stats => {
if (stats.hasErrors()) {
log(color.red.bold('build failed'));
build.failed = true;
return;
}
if (build.failed) {
build.failed = false;
log(color.green('build fixed'));
}
log('built %s in %sms', stats.hash, stats.endTime - stats.startTime);
const opts = serverCompiler.options;
const outputPath = path.resolve(opts.output.path, `${Object.keys(opts.entry)[0]}.js`);
// Make sure our newly built server bundles aren't in the module cache.
Object.keys(require.cache).forEach((modulePath) => {
if (modulePath.indexOf(opts.output.path || outputPath) !== -1) {
delete require.cache[modulePath];
}
});
if (build.listener) {
// Close the last server listener
build.listener.close();
}
// Start the server
build.listener = require(outputPath).default; // eslint-disable-line
// Track all connections to our server so that we can close them when needed.
build.listener.on('connection', (connection) => {
// Fixes first request to the server when nothing has been hot reloaded
if (build.first) {
devMiddleware.invalidate();
build.first = false;
}
build.connections.push(connection);
connection.on('close', () => {
build.connections.splice(build.connections.indexOf(connection));
});
});
});
log(`started on ${color.blue.underline(`http://localhost:${proxyPort}`)}`);
serverCompiler.watch({
aggregateTimeout: 300,
poll: true,
}, () => undefined);
clientCompiler.watch({
aggregateTimeout: 300,
poll: true,
}, () => undefined);
// Initialize BrowserSync
bs.init({
port: proxyPort,
open: false,
notify: false,
logLevel: 'silent',
server: {
baseDir: './',
middleware: [
devMiddleware,
webpackHotMiddleware(clientCompiler, {
log: () => {}
}),
proxyMiddleware(p => !p.match('^/browser-sync'), {
target: `http://localhost:${port}`,
changeOrigin: true,
ws: true,
logLevel: 'warn',
}),
],
},
});
process.on('SIGTERM', () => {
if (build.listener) {
build.listener.close(() => {
log('closing %s connections', build.connections.length);
log('shutting down');
build.connections.forEach(conn => {
conn.destroy();
});
process.exit(0);
});
}
});
Anyways, those are some ideas. I'd love to get the discussion going. I've been thinking about this since reading @jlongster 's Backend Apps with Webpack