Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@11ty/eleventy-dev-server

Package Overview
Dependencies
Maintainers
1
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@11ty/eleventy-dev-server - npm Package Compare versions

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();

157

client/reload-client.js

@@ -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

@@ -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);

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc