@11ty/eleventy-dev-server
Advanced tools
Comparing version 1.0.0-canary.14 to 1.0.0
93
cli.js
@@ -20,2 +20,4 @@ const chokidar = require("chokidar"); | ||
Logger.log = Logger.info; | ||
class Cli { | ||
@@ -30,3 +32,3 @@ static getVersion() { | ||
eleventy-dev-server | ||
eleventy-dev-server --input=_site | ||
eleventy-dev-server --dir=_site | ||
eleventy-dev-server --port=3000 | ||
@@ -38,13 +40,14 @@ | ||
--input=. | ||
--dir=. | ||
Directory to serve (default: \`.\`) | ||
--input (alias for --dir) | ||
--port=8080 | ||
Run the --serve web server on this port (default: \`8080\`) | ||
Run the web server on this port (default: \`8080\`) | ||
Will autoincrement if already in use. | ||
--domdiff (enabled) | ||
--domdiff=true (enabled) | ||
--domdiff (enabled, default) | ||
--domdiff=false (disabled) | ||
Apply HTML changes without a full page reload. (default: \`true\`) | ||
Apply HTML changes without a full page reload. | ||
@@ -70,2 +73,6 @@ --help`; | ||
domdiff: this.options.domdiff, | ||
// CLI watches all files in the folder by default | ||
// this is different from Eleventy usage! | ||
watch: [ this.options.input ], | ||
}); | ||
@@ -75,23 +82,2 @@ | ||
this.watcher = chokidar.watch( this.options.input, { | ||
ignored: ["**/node_modules/**", ".git"], | ||
// TODO allow chokidar configuration extensions | ||
ignoreInitial: true, | ||
// same values as Eleventy | ||
awaitWriteFinish: { | ||
stabilityThreshold: 150, | ||
pollInterval: 25, | ||
}, | ||
}); | ||
this.watcher.on("change", (path) => { | ||
Logger.info( "File modified:", path ); | ||
this.reload(path); | ||
}); | ||
this.watcher.on("add", (path) => { | ||
Logger.info( "File added:", path ); | ||
this.reload(path); | ||
}); | ||
// TODO? send any errors here to the server too | ||
@@ -101,56 +87,3 @@ // with server.sendError({ error }); | ||
// reverse of server.js->mapUrlToFilePath | ||
// /resource/ <= /resource/index.html | ||
// /resource <= resource.html | ||
getUrlsFromFilePath(path) { | ||
if(this.options.input) { | ||
if(this.options.input === ".") { | ||
path = `/${path}` | ||
} else { | ||
path = path.slice(this.options.input.length); | ||
} | ||
} | ||
let urls = []; | ||
urls.push(path); | ||
if(path.endsWith("/index.html")) { | ||
urls.push(path.slice(0, -1 * "index.html".length)); | ||
} else if(path.endsWith(".html")) { | ||
urls.push(path.slice(0, -1 * ".html".length)); | ||
} | ||
return urls; | ||
} | ||
// [{ url, inputPath, content }] | ||
getBuildTemplatesFromFilePath(path) { | ||
let urls = this.getUrlsFromFilePath(path); | ||
let obj = { | ||
inputPath: path, | ||
content: fs.readFileSync(path, "utf8"), | ||
} | ||
return urls.map(url => { | ||
return Object.assign({ url }, obj); | ||
}); | ||
} | ||
reload(path) { | ||
if(!this.server) { | ||
return; | ||
} | ||
this.server.reload({ | ||
files: [path], | ||
subtype: path && path.endsWith(".css") ? "css" : undefined, | ||
build: { | ||
templates: this.getBuildTemplatesFromFilePath(path) | ||
} | ||
}); | ||
} | ||
close() { | ||
if(this.watcher) { | ||
this.watcher.close(); | ||
} | ||
if(this.server) { | ||
@@ -157,0 +90,0 @@ this.server.close(); |
@@ -99,9 +99,80 @@ class Util { | ||
class EleventyReload { | ||
static reconnect(e) { | ||
if (document.visibilityState === "visible") { | ||
EleventyReload.init({ mode: "reconnect" }); | ||
constructor() { | ||
this.connectionMessageShown = false; | ||
this.reconnectEventCallback = this.reconnect.bind(this); | ||
} | ||
init(options = {}) { | ||
if (!("WebSocket" in window)) { | ||
return; | ||
} | ||
let { protocol, host } = new URL(document.location.href); | ||
// works with http (ws) and https (wss) | ||
let websocketProtocol = protocol.replace("http", "ws"); | ||
let socket = new WebSocket(`${websocketProtocol}//${host}`); | ||
socket.addEventListener("message", async (event) => { | ||
try { | ||
let data = JSON.parse(event.data); | ||
// Util.log( JSON.stringify(data, null, 2) ); | ||
let { type } = data; | ||
if (type === "eleventy.reload") { | ||
await this.onreload(data); | ||
} else if (type === "eleventy.msg") { | ||
Util.log(`${data.message}`); | ||
} else if (type === "eleventy.error") { | ||
// Log Eleventy build errors | ||
// Extra parsing for Node Error objects | ||
let e = JSON.parse(data.error); | ||
Util.error(`Build error: ${e.message}`, e); | ||
} else if (type === "eleventy.status") { | ||
// Full page reload on initial reconnect | ||
if (data.status === "connected" && options.mode === "reconnect") { | ||
window.location.reload(); | ||
} | ||
if(data.status === "connected") { | ||
// With multiple windows, only show one connection message | ||
if(!this.isConnected) { | ||
Util.log(Util.capitalize(data.status)); | ||
} | ||
this.connectionMessageShown = true; | ||
} else { | ||
if(data.status === "disconnected") { | ||
this.addReconnectListeners(); | ||
} | ||
Util.log(Util.capitalize(data.status)); | ||
} | ||
} else { | ||
Util.log("Unknown event type", data); | ||
} | ||
} catch (e) { | ||
Util.error(`Error parsing ${event.data}: ${e.message}`, e); | ||
} | ||
}); | ||
socket.addEventListener("open", () => { | ||
// no reconnection when the connect is already open | ||
this.removeReconnectListeners(); | ||
}); | ||
socket.addEventListener("close", () => { | ||
this.connectionMessageShown = false; | ||
this.addReconnectListeners(); | ||
}); | ||
} | ||
static async onreload({ subtype, files, build }) { | ||
reconnect() { | ||
Util.log( "Reconnecting…" ); | ||
this.init({ mode: "reconnect" }); | ||
} | ||
async onreload({ subtype, files, build }) { | ||
if (subtype === "css") { | ||
@@ -175,76 +246,16 @@ for (let link of document.querySelectorAll(`link[rel="stylesheet"]`)) { | ||
static init(options = {}) { | ||
if (!("WebSocket" in window)) { | ||
return; | ||
} | ||
addReconnectListeners() { | ||
this.removeReconnectListeners(); | ||
Util.log("Trying to connect…"); | ||
let { protocol, host } = new URL(document.location.href); | ||
// works with http (ws) and https (wss) | ||
let websocketProtocol = protocol.replace("http", "ws"); | ||
// TODO add a path here so that it doesn’t collide with any app websockets | ||
let socket = new WebSocket(`${websocketProtocol}//${host}`); | ||
// Related to #26 | ||
// socket.addEventListener("error", (e) => { | ||
// Util.error(`Error connecting:`, e); | ||
// }); | ||
// TODO add special handling for disconnect or document focus to retry | ||
socket.addEventListener("message", async function (event) { | ||
try { | ||
let data = JSON.parse(event.data); | ||
// Util.log( JSON.stringify(data, null, 2) ); | ||
let { type } = data; | ||
if (type === "eleventy.reload") { | ||
await EleventyReload.onreload(data); | ||
} else if (type === "eleventy.msg") { | ||
Util.log(`${data.message}`); | ||
} else if (type === "eleventy.error") { | ||
// Log Eleventy build errors | ||
// Extra parsing for Node Error objects | ||
let e = JSON.parse(data.error); | ||
Util.error(`Build error: ${e.message}`, e); | ||
} else if (type === "eleventy.status") { | ||
// Full page reload on initial reconnect | ||
if (data.status === "connected" && options.mode === "reconnect") { | ||
window.location.reload(); | ||
} | ||
Util.log(Util.capitalize(data.status)); | ||
} else { | ||
Util.log("Unknown event type", data); | ||
} | ||
} catch (e) { | ||
Util.error(`Error parsing ${event.data}: ${e.message}`, e); | ||
} | ||
}); | ||
socket.addEventListener("open", (event) => { | ||
EleventyReload.applyReconnectListeners("remove"); | ||
}); | ||
socket.addEventListener("close", (event) => { | ||
EleventyReload.applyReconnectListeners("remove"); | ||
EleventyReload.applyReconnectListeners("add"); | ||
}); | ||
window.addEventListener("focus", this.reconnectEventCallback); | ||
window.addEventListener("visibilitychange", this.reconnectEventCallback); | ||
} | ||
static applyReconnectListeners(mode) { | ||
let method = "addEventListener"; | ||
if (mode === "remove") { | ||
method = "removeEventListener"; | ||
} | ||
window[method]("focus", EleventyReload.reconnect); | ||
window[method]("visibilitychange", EleventyReload.reconnect); | ||
removeReconnectListeners() { | ||
window.removeEventListener("focus", this.reconnectEventCallback); | ||
window.removeEventListener("visibilitychange", this.reconnectEventCallback); | ||
} | ||
} | ||
// TODO remove this? | ||
// Util.log("Page reload.", Date.now()); | ||
EleventyReload.init(); | ||
let reloader = new EleventyReload(); | ||
reloader.init(); |
@@ -23,3 +23,4 @@ #!/usr/bin/env node | ||
string: [ | ||
"input", | ||
"dir", | ||
"input", // alias for dir | ||
"port", | ||
@@ -57,3 +58,3 @@ ], | ||
cli.serve({ | ||
input: argv.input, | ||
input: argv.dir || argv.input, | ||
port: argv.port, | ||
@@ -60,0 +61,0 @@ domdiff: argv.domdiff, |
{ | ||
"name": "@11ty/eleventy-dev-server", | ||
"version": "1.0.0-canary.14", | ||
"version": "1.0.0", | ||
"description": "A minimal, modern, generic, hot-reloading local web server to help web developers.", | ||
@@ -41,17 +41,17 @@ "main": "server.js", | ||
"dependencies": { | ||
"@11ty/eleventy-utils": "^1.0.0", | ||
"@11ty/eleventy-utils": "^1.0.1", | ||
"chokidar": "^3.5.3", | ||
"debug": "^4.3.3", | ||
"debug": "^4.3.4", | ||
"dev-ip": "^1.0.1", | ||
"finalhandler": "^1.1.2", | ||
"finalhandler": "^1.2.0", | ||
"mime": "^3.0.0", | ||
"minimist": "^1.2.6", | ||
"minimist": "^1.2.7", | ||
"morphdom": "^2.6.1", | ||
"please-upgrade-node": "^3.2.0", | ||
"ssri": "^8.0.1", | ||
"ws": "^8.5.0" | ||
"ws": "^8.12.0" | ||
}, | ||
"devDependencies": { | ||
"ava": "^4.0.1" | ||
"ava": "^5.1.0" | ||
} | ||
} |
@@ -19,10 +19,8 @@ <p align="center"><img src="https://www.11ty.dev/img/logo-github.svg" width="200" height="200" alt="11ty Logo"></p> | ||
You _do not need to install this_ separately—it is bundled with `@11ty/eleventy` starting with Eleventy v2.0.0. | ||
This is bundled with `@11ty/eleventy` (and you do not need to install it separately) in Eleventy v2.0. | ||
## CLI | ||
As of `1.0.0-canary.10` we now include a CLI for the Eleventy Dev Server. | ||
Eleventy Dev Server now also includes a CLI. The CLI is for **standalone** (non-Eleventy) use only: separate installation is unnecessary if you’re using this server with `@11ty/eleventy`. | ||
This is for **standalone** use only. Installation is unnecessary if you’re using this with Eleventy. | ||
```sh | ||
@@ -35,2 +33,4 @@ npm install -g @11ty/eleventy-dev-server | ||
This package requires Node 14 or newer. | ||
### CLI Usage | ||
@@ -42,4 +42,4 @@ | ||
# Serve a different subdirectory | ||
npx @11ty/eleventy-dev-server --input=_site | ||
# Serve a different subdirectory (also aliased as --input) | ||
npx @11ty/eleventy-dev-server --dir=_site | ||
@@ -46,0 +46,0 @@ # Disable the `domdiff` feature |
213
server.js
@@ -5,6 +5,8 @@ const pkg = require("./package.json"); | ||
const finalhandler = require("finalhandler"); | ||
const { WebSocketServer } = require("ws"); | ||
const WebSocket = require("ws"); | ||
const { WebSocketServer } = WebSocket; | ||
const mime = require("mime"); | ||
const ssri = require("ssri"); | ||
const devip = require("dev-ip"); | ||
const chokidar = require("chokidar"); | ||
const { TemplatePath } = require("@11ty/eleventy-utils"); | ||
@@ -16,8 +18,7 @@ | ||
const serverCache = {}; | ||
const DEFAULT_OPTIONS = { | ||
port: 8080, | ||
enabled: true, // Enable live reload at all | ||
liveReload: true, // Enable live reload at all | ||
showAllHosts: false, // IP address based hosts (other than localhost) | ||
folder: ".11ty", // Change the name of the special folder used for injected scripts | ||
injectedScriptsFolder: ".11ty", // Change the name of the special folder used for injected scripts | ||
portReassignmentRetryCount: 10, // number of times to increment the port if in use | ||
@@ -28,4 +29,4 @@ https: {}, // `key` and `cert`, required for http/2 and https | ||
encoding: "utf-8", // Default file encoding | ||
pathPrefix: "/", // May be overridden by Eleventy, adds a virtual base directory to your project | ||
watch: [], // Globs to pass to separate dev server chokidar for watching | ||
@@ -35,2 +36,3 @@ // Logger (fancier one is injected by Eleventy) | ||
info: console.log, | ||
log: console.log, | ||
error: console.error, | ||
@@ -42,17 +44,10 @@ } | ||
static getServer(...args) { | ||
let [name] = args; | ||
// TODO what if previously cached server has new/different dir or options | ||
if (!serverCache[name]) { | ||
serverCache[name] = new EleventyDevServer(...args); | ||
} | ||
return serverCache[name]; | ||
return new EleventyDevServer(...args); | ||
} | ||
constructor(name, dir, options = {}) { | ||
debug("Creating new Dev Server instance.") | ||
this.name = name; | ||
this.options = Object.assign({}, DEFAULT_OPTIONS, options); | ||
this.options.pathPrefix = this.cleanupPathPrefix(this.options.pathPrefix); | ||
this.normalizeOptions(options); | ||
this.fileCache = {}; | ||
@@ -65,4 +60,75 @@ // Directory to serve | ||
this.logger = this.options.logger; | ||
if(this.options.watch.length > 0) { | ||
this.getWatcher(); | ||
} | ||
} | ||
normalizeOptions(options = {}) { | ||
this.options = Object.assign({}, DEFAULT_OPTIONS, options); | ||
// better names for options https://github.com/11ty/eleventy-dev-server/issues/41 | ||
if(options.folder) { | ||
this.options.injectedScriptsFolder = options.folder; | ||
delete this.options.folder; | ||
} | ||
if(options.domdiff) { | ||
this.options.domDiff = options.domdiff; | ||
delete this.options.domdiff; | ||
} | ||
if(options.enabled) { | ||
this.options.liveReload = options.enabled; | ||
delete this.options.enabled; | ||
} | ||
this.options.pathPrefix = this.cleanupPathPrefix(this.options.pathPrefix); | ||
} | ||
get watcher() { | ||
if(!this._watcher) { | ||
debug("Watching %O", this.options.watch); | ||
// TODO if using Eleventy and `watch` option includes output folder (_site) this will trigger two update events! | ||
this._watcher = chokidar.watch(this.options.watch, { | ||
// TODO allow chokidar configuration extensions (or re-use the ones in Eleventy) | ||
ignored: ["**/node_modules/**", ".git"], | ||
ignoreInitial: true, | ||
// same values as Eleventy | ||
awaitWriteFinish: { | ||
stabilityThreshold: 150, | ||
pollInterval: 25, | ||
}, | ||
}); | ||
this._watcher.on("change", (path) => { | ||
this.logger.log( `File changed: ${path} (skips build)` ); | ||
this.reloadFiles([path]); | ||
}); | ||
this._watcher.on("add", (path) => { | ||
this.logger.log( `File added: ${path} (skips build)` ); | ||
this.reloadFiles([path]); | ||
}); | ||
} | ||
return this._watcher; | ||
} | ||
getWatcher() { | ||
return this.watcher; | ||
} | ||
watchFiles(files) { | ||
if(files && (!Array.isArray(files) || files.length > 0)) { | ||
if(typeof files === "string") { | ||
files = [files]; | ||
} | ||
files = files.map(entry => TemplatePath.stripLeadingDotSlash(entry)); | ||
debug("Also watching %O", files); | ||
this.watcher.add(files); | ||
} | ||
} | ||
cleanupPathPrefix(pathPrefix) { | ||
@@ -83,3 +149,6 @@ if(!pathPrefix || pathPrefix === "/") { | ||
setAliases(aliases) { | ||
this.passthroughAliases = aliases; | ||
if(aliases) { | ||
this.passthroughAliases = aliases; | ||
debug( "Setting aliases (emulated passthrough copy) %O", aliases ); | ||
} | ||
} | ||
@@ -174,3 +243,3 @@ | ||
return { | ||
statusCode: 301, | ||
statusCode: 302, | ||
url: this.options.pathPrefix, | ||
@@ -281,3 +350,3 @@ } | ||
// This isn’t super necessary because it’s a local file, but it’s included anyway | ||
let script = `<script type="module" integrity="${integrityHash}"${inlineContents ? `>${scriptContents}` : ` src="/${this.options.folder}/reload-client.js">`}</script>`; | ||
let script = `<script type="module" integrity="${integrityHash}"${inlineContents ? `>${scriptContents}` : ` src="/${this.options.injectedScriptsFolder}/reload-client.js">`}</script>`; | ||
@@ -336,9 +405,9 @@ if (content.includes("</head>")) { | ||
eleventyDevServerMiddleware(req, res, next) { | ||
if(req.url === `/${this.options.folder}/reload-client.js`) { | ||
if(this.options.enabled) { | ||
if(req.url === `/${this.options.injectedScriptsFolder}/reload-client.js`) { | ||
if(this.options.liveReload) { | ||
res.setHeader("Content-Type", mime.getType("js")); | ||
return res.end(this._getFileContents("./client/reload-client.js")); | ||
} | ||
} else if(req.url === `/${this.options.folder}/morphdom.js`) { | ||
if(this.options.domdiff) { | ||
} else if(req.url === `/${this.options.injectedScriptsFolder}/morphdom.js`) { | ||
if(this.options.domDiff) { | ||
res.setHeader("Content-Type", mime.getType("js")); | ||
@@ -413,3 +482,3 @@ return res.end(this._getFileContents("./node_modules/morphdom/dist/morphdom-esm.js", path.resolve("."))); | ||
res = wrapResponse(res, content => { | ||
if(this.options.enabled !== false) { | ||
if(this.options.liveReload !== false) { | ||
let scriptContents = this._getFileContents("./client/reload-client.js"); | ||
@@ -515,2 +584,3 @@ let integrityHash = ssri.fromData(scriptContents); | ||
this.setupReloadNotifier(); | ||
let { port } = this._server.address(); | ||
@@ -547,2 +617,4 @@ | ||
serve(port) { | ||
this.getWatcher(); | ||
this._serverListen(port); | ||
@@ -562,2 +634,3 @@ } | ||
let updateServer = new WebSocketServer({ | ||
// includes the port | ||
server: this.server, | ||
@@ -567,4 +640,2 @@ }); | ||
updateServer.on("connection", (ws) => { | ||
this.updateNotifier = ws; | ||
this.sendUpdateNotification({ | ||
@@ -583,6 +654,13 @@ type: "eleventy.status", | ||
// Broadcasts to all open browser windows | ||
sendUpdateNotification(obj) { | ||
if (this.updateNotifier) { | ||
this.updateNotifier.send(JSON.stringify(obj)); | ||
if(!this.updateServer?.clients) { | ||
return; | ||
} | ||
for(let client of this.updateServer.clients) { | ||
if (client.readyState === WebSocket.OPEN) { | ||
client.send(JSON.stringify(obj)); | ||
} | ||
} | ||
} | ||
@@ -603,2 +681,6 @@ | ||
} | ||
if(this._watcher) { | ||
this._watcher.close(); | ||
delete this._watcher; | ||
} | ||
} | ||
@@ -614,2 +696,74 @@ | ||
// reverse of mapUrlToFilePath | ||
// /resource/ <= /resource/index.html | ||
// /resource <= resource.html | ||
getUrlsFromFilePath(path) { | ||
if(this.dir === ".") { | ||
path = `/${path}` | ||
} else { | ||
path = path.slice(this.dir.length); | ||
} | ||
let urls = []; | ||
urls.push(path); | ||
if(path.endsWith("/index.html")) { | ||
urls.push(path.slice(0, -1 * "index.html".length)); | ||
} else if(path.endsWith(".html")) { | ||
urls.push(path.slice(0, -1 * ".html".length)); | ||
} | ||
return urls; | ||
} | ||
// [{ url, inputPath, content }] | ||
getBuildTemplatesFromFilePath(path) { | ||
// We can skip this for non-html files, dom-diffing will not apply | ||
if(!path.endsWith(".html")) { | ||
return []; | ||
} | ||
let urls = this.getUrlsFromFilePath(path); | ||
let obj = { | ||
inputPath: path, | ||
content: fs.readFileSync(path, "utf8"), | ||
} | ||
return urls.map(url => { | ||
return Object.assign({ url }, obj); | ||
}); | ||
} | ||
reloadFiles(files, useDomDiffingForHtml = true) { | ||
if(!Array.isArray(files)) { | ||
throw new Error("reloadFiles method requires an array of file paths."); | ||
} | ||
let subtype; | ||
if(!files.some((entry) => !entry.endsWith(".css"))) { | ||
// all css changes | ||
subtype = "css"; | ||
} | ||
let templates = []; | ||
if(useDomDiffingForHtml && this.options.domDiff) { | ||
for(let filePath of files) { | ||
if(!filePath.endsWith(".html")) { | ||
continue; | ||
} | ||
for(let templateEntry of this.getBuildTemplatesFromFilePath(filePath)) { | ||
templates.push(templateEntry); | ||
} | ||
} | ||
} | ||
this.reload({ | ||
files, | ||
subtype, | ||
build: { | ||
templates | ||
} | ||
}); | ||
} | ||
reload(event) { | ||
@@ -620,6 +774,7 @@ let { subtype, files, build } = event; | ||
.filter(entry => { | ||
if(!this.options.domdiff) { | ||
if(!this.options.domDiff) { | ||
// Don’t include any files if the dom diffing option is disabled | ||
return false; | ||
} | ||
// Filter to only include watched templates that were updated | ||
@@ -626,0 +781,0 @@ return (files || []).includes(entry.inputPath); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
40982
1104
0
Updated@11ty/eleventy-utils@^1.0.1
Updateddebug@^4.3.4
Updatedfinalhandler@^1.2.0
Updatedminimist@^1.2.7
Updatedws@^8.12.0