Compare commits

...

5 Commits

@ -215,6 +215,12 @@ Arguments:
## Changelog ## Changelog
### v1.0.7 (May 8, 2020)
- __Patch:__ Now uses `inject-lr` for LiveReload script injection instead of a custom implementation, to make it more robust in edge cases.
- __Patch:__ Budo-served resources now bypass the Express app entirely, and therefore it should now be possible to have a catch-all 404 handler in your Express app without breaking `express-budo`.
- __Patch:__ Now waits with sending out a LiveReload event for a bit, so that when `budo-express` is used with an external restarter such as Nodemon, there's no longer a race condition between the browser reloading and the server process having restarted (which would result in an error page). Currently uses a fixed timeout, which is not ideal; if you're still seeing this problem, please file an issue!
### v1.0.6 (December 7, 2020) ### v1.0.6 (December 7, 2020)
- __Patch:__ No longer crashes when headers have already been sent; though in some cases this may break auto-reloading. If you can find a better solution for livereload injection, please file a PR! - __Patch:__ No longer crashes when headers have already been sent; though in some cases this may break auto-reloading. If you can find a better solution for livereload injection, please file a PR!

@ -5,116 +5,87 @@ const defaultValue = require("default-value");
const assureArray = require("assure-array"); const assureArray = require("assure-array");
const path = require("path"); const path = require("path");
const entities = require("entities"); const entities = require("entities");
const util = require("util"); const stacked = require("stacked");
const injectLRScript = require("inject-lr-script");
let reloadClientTag = Buffer.from("<script src=\"/budo/livereload.js\"></script>"); function createExpressMiddleware(app, fullBundlePath) {
let handler = stacked();
function monkeyPatchEnd(res) { handler.use(injectLRScript({ local: true, path: "/budo/livereload.js" }));
// NOTE: This is a hack, to auto-inject the Budo livereload client in any outgoing HTML response (but only in development mode).
let end = res.end.bind(res);
res.end = function monkeyPatchedEnd(... args) { handler.use((req, res, next) => {
let originalChunk = args[0]; // Ensure that we never intercept budo-served resources. Otherwise, the Express app might have something like a 404 handler or "always require login" middleware that stops requests for budo assets from ever coming out the other end of the Express app.
let prefix; if (req.url !== "/budo/livereload.js" && req.url !== `/${fullBundlePath}`) {
app.handle(req, res, (err) => {
if (originalChunk == null || typeof originalChunk === "string" || Buffer.isBuffer(originalChunk)) { if (err != null && err instanceof Error) {
if (!res.headersSent) { res
// If headers have already been sent, we'll just have to hope that the browser will still look at our appended tag, as we can't change the response size anymore... .status(500)
// TODO: Make this more robust in the future .set("content-type", "text/html")
let typeHeader = res.getHeader("content-type"); .send(`<pre>${entities.escape(err.stack)}</pre>`);
} else {
if (typeHeader != null && typeHeader.startsWith("text/html")) { next(err);
let contentLength = res.getHeader("content-length");
if (contentLength != null) {
// Compensate for the additional bytes introduced by our injected script tag
res.setHeader("content-length", parseInt(contentLength) + reloadClientTag.length);
}
prefix = reloadClientTag;
} }
}
// Reset the `end` method back to the original method; we don't need to get in the way anymore
res.end = end;
let originalChunkAsBuffer = (typeof originalChunk === "string")
? Buffer.from(originalChunk)
: originalChunk;
if (prefix != null) {
let chunk = (originalChunkAsBuffer == null)
? reloadClientTag
: Buffer.concat([ originalChunkAsBuffer, reloadClientTag ]);
end(chunk, ... args.slice(1));
} else {
end(... args);
}
} else {
throw new Error(`Expected a buffer or string in 'end', but got something else: ${util.inspect(args[0])}`);
}
};
}
function createExpressMiddleware(app) {
return function expressMiddleware(req, res, next) {
try {
monkeyPatchEnd(res);
} catch (error) {
process.nextTick(() => {
throw error;
}); });
} else {
next();
} }
});
app.handle(req, res, (err) => {
if (err != null && err instanceof Error) {
res
.status(500)
.send(`${reloadClientTag}<pre>${entities.escape(err.stack)}</pre>`);
} else {
next(err);
}
});
};
} }
module.exports = function ({ options, staticPath, staticBasePath, entryPaths, host }) { module.exports = function ({ options, staticPath, staticBasePath, entryPaths, host }) {
let middlewareList; let middlewareList;
let fullBundlePath = (options.staticPrefix != null)
? path.join(options.staticPrefix, options.bundlePath)
: options.bundlePath;
if (options.middleware != null) { if (options.middleware != null) {
middlewareList = assureArray(options.middleware).concat([ middlewareList = assureArray(options.middleware).concat([
createExpressMiddleware(options.expressApp) createExpressMiddleware(options.expressApp, fullBundlePath)
]); ]);
} else { } else {
middlewareList = [ middlewareList = [
createExpressMiddleware(options.expressApp) createExpressMiddleware(options.expressApp, fullBundlePath)
]; ];
} }
let budoOptions = { let budoOptions = {
watchGlob: staticPath(options.livereloadPattern),
live: options.livereloadPattern,
livePort: defaultValue(options.livereloadPort, 35729), livePort: defaultValue(options.livereloadPort, 35729),
stream: defaultValue(options.stream, process.stdout), stream: defaultValue(options.stream, process.stdout),
port: options.port, port: options.port,
host: host, host: host,
dir: staticBasePath, dir: staticBasePath,
serve: (options.staticPrefix != null) serve: fullBundlePath,
? path.join(options.staticPrefix, options.bundlePath)
: options.bundlePath,
browserify: options.browserify, browserify: options.browserify,
middleware: middlewareList, middleware: middlewareList,
debug: defaultValue(options.sourceMaps, true), debug: defaultValue(options.sourceMaps, true),
verbose: true
}; };
let devServer = budo(entryPaths, budoOptions).on("connect", (event) => { let devServer = budo(entryPaths, budoOptions)
let reloadServer = event.webSocketServer; .watch(staticPath(options.livereloadPattern), {
awaitWriteFinish: {
// NOTE: This is mostly just a hack to make sure that the process has time to get restarted (by eg. nodemon) before sending out a LiveReload update. Need to investigate whether there's a less hacky way to do this.
stabilityThreshold: 130
}
})
.live()
.on("watch", (event, file) => {
if (event === "change" || event === "add") {
devServer.reload(file);
}
})
.on("pending", () => {
// This event is called when the bundle is being regenerated, ie. after watchify has detected a change in the input files
devServer.reload(fullBundlePath);
})
.on("connect", (event) => {
let reloadServer = event.webSocketServer;
reloadServer.once("connection", (_socket) => { reloadServer.once("connection", (_socket) => {
// This is to make sure the browser also reloads after the process has auto-restarted (eg. using `nodemon`) // This is to make sure the browser also reloads after the process has auto-restarted (eg. using `nodemon`)
console.log("Triggering initial page refresh for new client..."); console.log("Triggering initial page refresh for new client...");
devServer.reload("*"); devServer.reload("*");
});
}); });
});
}; };

@ -1,7 +1,7 @@
{ {
"name": "budo-express", "name": "budo-express",
"description": "A small wrapper to integrate Budo with an Express application", "description": "A small wrapper to integrate Budo with an Express application",
"version": "1.0.6", "version": "1.0.7",
"main": "index.js", "main": "index.js",
"repository": "http://git.cryto.net/joepie91/budo-express.git", "repository": "http://git.cryto.net/joepie91/budo-express.git",
"bugs": { "bugs": {
@ -17,7 +17,9 @@
"chalk": "^3.0.0", "chalk": "^3.0.0",
"default-value": "^1.0.0", "default-value": "^1.0.0",
"entities": "^2.0.0", "entities": "^2.0.0",
"inject-lr-script": "^2.2.0",
"is-stream": "^2.0.0", "is-stream": "^2.0.0",
"stacked": "^1.1.1",
"validatem": "^0.2.0" "validatem": "^0.2.0"
}, },
"devDependencies": { "devDependencies": {

@ -1492,7 +1492,7 @@ inherits@2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
inject-lr-script@^2.1.0: inject-lr-script@^2.1.0, inject-lr-script@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/inject-lr-script/-/inject-lr-script-2.2.0.tgz#58d91cd99e5de1a3f172aa076f7db8651ee72db2" resolved "https://registry.yarnpkg.com/inject-lr-script/-/inject-lr-script-2.2.0.tgz#58d91cd99e5de1a3f172aa076f7db8651ee72db2"
integrity sha512-lFLjCOg2XP8233AiET5vFePo910vhNIkKHDzUptNhc+4Y7dsp/TNBiusUUpaxzaGd6UDHy0Lozfl9AwmteK6DQ== integrity sha512-lFLjCOg2XP8233AiET5vFePo910vhNIkKHDzUptNhc+4Y7dsp/TNBiusUUpaxzaGd6UDHy0Lozfl9AwmteK6DQ==

Loading…
Cancel
Save