diff options
Diffstat (limited to 'node_modules/@11ty/eleventy-dev-server')
| -rw-r--r-- | node_modules/@11ty/eleventy-dev-server/README.md | 60 | ||||
| -rw-r--r-- | node_modules/@11ty/eleventy-dev-server/cli.js | 89 | ||||
| -rw-r--r-- | node_modules/@11ty/eleventy-dev-server/client/reload-client.js | 336 | ||||
| -rwxr-xr-x | node_modules/@11ty/eleventy-dev-server/cmd.js | 77 | ||||
| -rw-r--r-- | node_modules/@11ty/eleventy-dev-server/package.json | 57 | ||||
| -rw-r--r-- | node_modules/@11ty/eleventy-dev-server/server.js | 1024 | ||||
| -rw-r--r-- | node_modules/@11ty/eleventy-dev-server/server/ipAddress.js | 9 | ||||
| -rw-r--r-- | node_modules/@11ty/eleventy-dev-server/server/wrapResponse.js | 130 |
8 files changed, 1782 insertions, 0 deletions
diff --git a/node_modules/@11ty/eleventy-dev-server/README.md b/node_modules/@11ty/eleventy-dev-server/README.md new file mode 100644 index 0000000..2d74590 --- /dev/null +++ b/node_modules/@11ty/eleventy-dev-server/README.md @@ -0,0 +1,60 @@ +<p align="center"><img src="https://www.11ty.dev/img/logo-github.svg" width="200" height="200" alt="11ty Logo"></p> + +# eleventy-dev-server đâĄď¸đđ + +A minimal, modern, generic, hot-reloading local web server to help web developers. + +## ⥠[Documentation](https://www.11ty.dev/docs/watch-serve/#eleventy-dev-server) + +- Please star [Eleventy on GitHub](https://github.com/11ty/eleventy/)! +- Follow us on Twitter [@eleven_ty](https://twitter.com/eleven_ty) +- Support [11ty on Open Collective](https://opencollective.com/11ty) +- [11ty on npm](https://www.npmjs.com/org/11ty) +- [11ty on GitHub](https://github.com/11ty) + +[](https://www.npmjs.com/package/@11ty/eleventy-dev-server) + +## Installation + +This is bundled with `@11ty/eleventy` (and you do not need to install it separately) in Eleventy v2.0. + +## CLI + +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`. + +```sh +npm install -g @11ty/eleventy-dev-server + +# Alternatively, install locally into your project +npm install @11ty/eleventy-dev-server +``` + +This package requires Node 18 or newer. + +### CLI Usage + +```sh +# Serve the current directory +npx @11ty/eleventy-dev-server + +# Serve a different subdirectory (also aliased as --input) +npx @11ty/eleventy-dev-server --dir=_site + +# Disable the `domdiff` feature +npx @11ty/eleventy-dev-server --domdiff=false + +# Full command list in the Help +npx @11ty/eleventy-dev-server --help +``` + +## Tests + +``` +npm run test +``` + +- We use the [ava JavaScript test runner](https://github.com/avajs/ava) ([Assertions documentation](https://github.com/avajs/ava/blob/master/docs/03-assertions.md)) + +## Changelog + +* `v2.0.0` bumps Node.js minimum to 18.
\ No newline at end of file diff --git a/node_modules/@11ty/eleventy-dev-server/cli.js b/node_modules/@11ty/eleventy-dev-server/cli.js new file mode 100644 index 0000000..04be1ca --- /dev/null +++ b/node_modules/@11ty/eleventy-dev-server/cli.js @@ -0,0 +1,89 @@ +const pkg = require("./package.json"); +const EleventyDevServer = require("./server.js"); + +const Logger = { + info: function(...args) { + console.log( "[11ty/eleventy-dev-server]", ...args ); + }, + error: function(...args) { + console.error( "[11ty/eleventy-dev-server]", ...args ); + }, + fatal: function(...args) { + Logger.error(...args); + process.exitCode = 1; + } +}; + +Logger.log = Logger.info; + +class Cli { + static getVersion() { + return pkg.version; + } + + static getHelp() { + return `Usage: + + eleventy-dev-server + eleventy-dev-server --dir=_site + eleventy-dev-server --port=3000 + +Arguments: + + --version + + --dir=. + Directory to serve (default: \`.\`) + + --input (alias for --dir) + + --port=8080 + Run the web server on this port (default: \`8080\`) + Will autoincrement if already in use. + + --domdiff (enabled, default) + --domdiff=false (disabled) + Apply HTML changes without a full page reload. + + --help`; + } + + static getDefaultOptions() { + return { + port: "8080", + input: ".", + domDiff: true, + } + } + + async serve(options = {}) { + this.options = Object.assign(Cli.getDefaultOptions(), options); + + this.server = EleventyDevServer.getServer("eleventy-dev-server-cli", this.options.input, { + // TODO allow server configuration extensions + showVersion: true, + logger: Logger, + domDiff: this.options.domDiff, + + // CLI watches all files in the folder by default + // this is different from Eleventy usage! + watch: [ this.options.input ], + }); + + this.server.serve(this.options.port); + + // TODO? send any errors here to the server too + // with server.sendError({ error }); + } + + close() { + if(this.server) { + return this.server.close(); + } + } +} + +module.exports = { + Logger, + Cli +} diff --git a/node_modules/@11ty/eleventy-dev-server/client/reload-client.js b/node_modules/@11ty/eleventy-dev-server/client/reload-client.js new file mode 100644 index 0000000..fc0ddd1 --- /dev/null +++ b/node_modules/@11ty/eleventy-dev-server/client/reload-client.js @@ -0,0 +1,336 @@ +class Util { + static pad(num, digits = 2) { + let zeroes = new Array(digits + 1).join(0); + return `${zeroes}${num}`.slice(-1 * digits); + } + + static log(message) { + Util.output("log", message); + } + static error(message, error) { + Util.output("error", message, error); + } + static output(type, ...messages) { + let now = new Date(); + let date = `${Util.pad(now.getUTCHours())}:${Util.pad( + now.getUTCMinutes() + )}:${Util.pad(now.getUTCSeconds())}.${Util.pad( + now.getUTCMilliseconds(), + 3 + )}`; + console[type](`[11ty][${date} UTC]`, ...messages); + } + + static capitalize(word) { + return word.substr(0, 1).toUpperCase() + word.substr(1); + } + + static matchRootAttributes(htmlContent) { + // Workaround for morphdom bug with attributes on <html> https://github.com/11ty/eleventy-dev-server/issues/6 + // Note also `childrenOnly: true` above + const parser = new DOMParser(); + let parsed = parser.parseFromString(htmlContent, "text/html"); + let parsedDoc = parsed.documentElement; + let newAttrs = parsedDoc.getAttributeNames(); + + let docEl = document.documentElement; + // Remove old + let removedAttrs = docEl.getAttributeNames().filter(name => !newAttrs.includes(name)); + for(let attr of removedAttrs) { + docEl.removeAttribute(attr); + } + + // Add new + for(let attr of newAttrs) { + docEl.setAttribute(attr, parsedDoc.getAttribute(attr)); + } + } + + static isEleventyLinkNodeMatch(from, to) { + // Issue #18 https://github.com/11ty/eleventy-dev-server/issues/18 + // Donât update a <link> if the _11ty searchParam is the only thing thatâs different + if(from.tagName !== "LINK" || to.tagName !== "LINK") { + return false; + } + + let oldWithoutHref = from.cloneNode(); + let newWithoutHref = to.cloneNode(); + + oldWithoutHref.removeAttribute("href"); + newWithoutHref.removeAttribute("href"); + + // if all other attributes besides href match + if(!oldWithoutHref.isEqualNode(newWithoutHref)) { + return false; + } + + let oldUrl = new URL(from.href); + let newUrl = new URL(to.href); + + // morphdom wants to force href="style.css?_11ty" => href="style.css" + let paramName = EleventyReload.QUERY_PARAM; + let isErasing = oldUrl.searchParams.has(paramName) && !newUrl.searchParams.has(paramName); + if(!isErasing) { + // not a match if _11ty has a new value (not being erased) + return false; + } + + oldUrl.searchParams.set(paramName, ""); + newUrl.searchParams.set(paramName, ""); + + // is a match if erasing and the rest of the href matches too + return oldUrl.toString() === newUrl.toString(); + } + + // https://github.com/patrick-steele-idem/morphdom/issues/178#issuecomment-652562769 + static runScript(source, target) { + let script = document.createElement('script'); + + // copy over the attributes + for(let attr of [...source.attributes]) { + script.setAttribute(attr.nodeName ,attr.nodeValue); + } + + script.innerHTML = source.innerHTML; + (target || source).replaceWith(script); + } + + static fullPageReload() { + Util.log(`Page reload initiated.`); + window.location.reload(); + } +} + +class EleventyReload { + static QUERY_PARAM = "_11ty"; + + static reloadTypes = { + css: (files, build = {}) => { + // Initiate a full page refresh if a CSS change is made but does match any stylesheet url + // `build.stylesheets` available in Eleventy v3.0.1-alpha.5+ + if(Array.isArray(build.stylesheets)) { + let match = false; + for (let link of document.querySelectorAll(`link[rel="stylesheet"]`)) { + if (link.href) { + let url = new URL(link.href); + if(build.stylesheets.includes(url.pathname)) { + match = true; + } + } + } + + if(!match) { + Util.fullPageReload(); + return; + } + } + + for (let link of document.querySelectorAll(`link[rel="stylesheet"]`)) { + if (link.href) { + let url = new URL(link.href); + url.searchParams.set(this.QUERY_PARAM, Date.now()); + link.href = url.toString(); + } + } + + Util.log(`CSS updated without page reload.`); + }, + default: async (files, build = {}) => { + let morphed = false; + let domdiffTemplates = (build?.templates || []).filter(({url, inputPath}) => { + return url === document.location.pathname && (files || []).includes(inputPath); + }); + + if(domdiffTemplates.length === 0) { + Util.fullPageReload(); + return; + } + + try { + // Important: using `./` allows the `.11ty` folder name to be changed + const { default: morphdom } = await import(`./morphdom.js`); + + for (let {url, inputPath, content} of domdiffTemplates) { + // Notable limitation: this wonât re-run script elements or JavaScript page lifecycle events (load/DOMContentLoaded) + morphed = true; + + morphdom(document.documentElement, content, { + childrenOnly: true, + onBeforeElUpdated: function (fromEl, toEl) { + if (fromEl.nodeName === "SCRIPT" && toEl.nodeName === "SCRIPT") { + if(toEl.innerHTML !== fromEl.innerHTML) { + Util.log(`JavaScript modified, reload initiated.`); + window.location.reload(); + } + + return false; + } + + // Speed-up trick from morphdom docs + // https://dom.spec.whatwg.org/#concept-node-equals + if (fromEl.isEqualNode(toEl)) { + return false; + } + + if(Util.isEleventyLinkNodeMatch(fromEl, toEl)) { + return false; + } + + return true; + }, + addChild: function(parent, child) { + // Declarative Shadow DOM https://github.com/11ty/eleventy-dev-server/issues/90 + if(child.nodeName === "TEMPLATE" && child.hasAttribute("shadowrootmode")) { + let root = parent.shadowRoot; + if(root) { + // remove all shadow root children + while(root.firstChild) { + root.removeChild(root.firstChild); + } + } + for(let newChild of child.content.childNodes) { + root.appendChild(newChild); + } + } else { + parent.appendChild(child); + } + }, + onNodeAdded: function (node) { + if (node.nodeName === 'SCRIPT') { + Util.log(`JavaScript added, reload initiated.`); + window.location.reload(); + } + }, + onElUpdated: function(node) { + // Re-attach custom elements + if(customElements.get(node.tagName.toLowerCase())) { + let placeholder = document.createElement("div"); + node.replaceWith(placeholder); + requestAnimationFrame(() => { + placeholder.replaceWith(node); + placeholder = undefined; + }); + } + } + }); + + Util.matchRootAttributes(content); + Util.log(`HTML delta applied without page reload.`); + } + } catch(e) { + Util.error( "Morphdom error", e ); + } + + if (!morphed) { + Util.fullPageReload(); + } + } + } + + constructor() { + this.connectionMessageShown = false; + this.reconnectEventCallback = this.reconnect.bind(this); + } + + init(options = {}) { + if (!("WebSocket" in window)) { + return; + } + + let documentUrl = new URL(document.location.href); + + let reloadPort = new URL(import.meta.url).searchParams.get("reloadPort"); + if(reloadPort) { + documentUrl.port = reloadPort; + } + + let { protocol, host } = documentUrl; + + // 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(); + }); + } + + reconnect() { + Util.log( "ReconnectingâŚ" ); + this.init({ mode: "reconnect" }); + } + + async onreload({ subtype, files, build }) { + if(!EleventyReload.reloadTypes[subtype]) { + subtype = "default"; + } + + await EleventyReload.reloadTypes[subtype](files, build); + } + + addReconnectListeners() { + this.removeReconnectListeners(); + + window.addEventListener("focus", this.reconnectEventCallback); + window.addEventListener("visibilitychange", this.reconnectEventCallback); + } + + removeReconnectListeners() { + window.removeEventListener("focus", this.reconnectEventCallback); + window.removeEventListener("visibilitychange", this.reconnectEventCallback); + } +} + +let reloader = new EleventyReload(); +reloader.init();
\ No newline at end of file diff --git a/node_modules/@11ty/eleventy-dev-server/cmd.js b/node_modules/@11ty/eleventy-dev-server/cmd.js new file mode 100755 index 0000000..695b845 --- /dev/null +++ b/node_modules/@11ty/eleventy-dev-server/cmd.js @@ -0,0 +1,77 @@ +#!/usr/bin/env node + +const pkg = require("./package.json"); + +// Node check +require("please-upgrade-node")(pkg, { + message: function (requiredVersion) { + return ( + "eleventy-dev-server requires Node " + + requiredVersion + + ". You will need to upgrade Node!" + ); + }, +}); + +const { Logger, Cli } = require("./cli.js"); + +const debug = require("debug")("Eleventy:DevServer"); + +try { + const defaults = Cli.getDefaultOptions(); + for(let key in defaults) { + if(key.toLowerCase() !== key) { + defaults[key.toLowerCase()] = defaults[key]; + delete defaults[key]; + } + } + + const argv = require("minimist")(process.argv.slice(2), { + string: [ + "dir", + "input", // alias for dir + "port", + ], + boolean: [ + "version", + "help", + "domdiff", + ], + default: defaults, + unknown: function (unknownArgument) { + throw new Error( + `We donât know what '${unknownArgument}' is. Use --help to see the list of supported commands.` + ); + }, + }); + + debug("command: eleventy-dev-server %o", argv); + + process.on("unhandledRejection", (error, promise) => { + Logger.fatal("Unhandled rejection in promise:", promise, error); + }); + process.on("uncaughtException", (error) => { + Logger.fatal("Uncaught exception:", error); + }); + + if (argv.version) { + console.log(Cli.getVersion()); + } else if (argv.help) { + console.log(Cli.getHelp()); + } else { + let cli = new Cli(); + + cli.serve({ + input: argv.dir || argv.input, + port: argv.port, + domDiff: argv.domdiff, + }); + + process.on("SIGINT", async () => { + await cli.close(); + process.exitCode = 0; + }); + } +} catch (e) { + Logger.fatal("Fatal Error:", e) +} diff --git a/node_modules/@11ty/eleventy-dev-server/package.json b/node_modules/@11ty/eleventy-dev-server/package.json new file mode 100644 index 0000000..e413c7c --- /dev/null +++ b/node_modules/@11ty/eleventy-dev-server/package.json @@ -0,0 +1,57 @@ +{ + "name": "@11ty/eleventy-dev-server", + "version": "2.0.8", + "description": "A minimal, modern, generic, hot-reloading local web server to help web developers.", + "main": "server.js", + "scripts": { + "test": "npx ava --verbose", + "sample": "node cmd.js --input=test/stubs" + }, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + }, + "bin": { + "eleventy-dev-server": "./cmd.js" + }, + "keywords": [ + "eleventy", + "server", + "cli" + ], + "publishConfig": { + "access": "public" + }, + "author": { + "name": "Zach Leatherman", + "email": "zachleatherman@gmail.com", + "url": "https://zachleat.com/" + }, + "repository": { + "type": "git", + "url": "git://github.com/11ty/eleventy-dev-server.git" + }, + "bugs": "https://github.com/11ty/eleventy-dev-server/issues", + "homepage": "https://github.com/11ty/eleventy-dev-server/", + "dependencies": { + "@11ty/eleventy-utils": "^2.0.1", + "chokidar": "^3.6.0", + "debug": "^4.4.0", + "finalhandler": "^1.3.1", + "mime": "^3.0.0", + "minimist": "^1.2.8", + "morphdom": "^2.7.4", + "please-upgrade-node": "^3.2.0", + "send": "^1.1.0", + "ssri": "^11.0.0", + "urlpattern-polyfill": "^10.0.0", + "ws": "^8.18.1" + }, + "devDependencies": { + "ava": "^6.2.0" + } +} diff --git a/node_modules/@11ty/eleventy-dev-server/server.js b/node_modules/@11ty/eleventy-dev-server/server.js new file mode 100644 index 0000000..9fbe898 --- /dev/null +++ b/node_modules/@11ty/eleventy-dev-server/server.js @@ -0,0 +1,1024 @@ +const path = require("node:path"); +const fs = require("node:fs"); + +const finalhandler = require("finalhandler"); +const WebSocket = require("ws"); +const { WebSocketServer } = WebSocket; +const mime = require("mime"); +const ssri = require("ssri"); +const send = require("send"); +const chokidar = require("chokidar"); +const { TemplatePath, isPlainObject } = require("@11ty/eleventy-utils"); + +const debug = require("debug")("Eleventy:DevServer"); + +const pkg = require("./package.json"); +const wrapResponse = require("./server/wrapResponse.js"); +const ipAddress = require("./server/ipAddress.js"); + +if (!globalThis.URLPattern) { + require("urlpattern-polyfill"); +} + +const DEFAULT_OPTIONS = { + port: 8080, + reloadPort: false, // Falsy uses same as `port` + liveReload: true, // Enable live reload at all + showAllHosts: false, // IP address based hosts (other than localhost) + 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 + https: {}, // `key` and `cert`, required for http/2 and https + domDiff: true, // Use morphdom to apply DOM diffing delta updates to HTML + showVersion: false, // Whether or not to show the server version on the command line. + 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 + aliases: {}, // Aliasing feature + indexFileName: "index.html", // Allow custom index file name + useCache: false, // Use a cache for file contents + headers: {}, // Set default response headers + messageOnStart: ({ hosts, startupTime, version, options }) => { + let hostsStr = " started"; + if(Array.isArray(hosts) && hosts.length > 0) { + // TODO what happens when the cert doesnât cover non-localhost hosts? + hostsStr = ` at ${hosts.join(" or ")}`; + } + + return `Server${hostsStr}${options.showVersion ? ` (v${version})` : ""}`; + }, + + onRequest: {}, // Maps URLPatterns to dynamic callback functions that run on a request from a client. + + // Example: + // "/foo/:name": function({ url, pattern, patternGroups }) { + // return { + // headers: { + // "Content-Type": "text/html", + // }, + // body: `${url} ${JSON.stringify(patternGroups)}` + // } + // } + + // Logger (fancier one is injected by Eleventy) + logger: { + info: console.log, + log: console.log, + error: console.error, + } +} + +class EleventyDevServer { + #watcher; + #serverClosing; + #serverState; + #readyPromise; + #readyResolve; + + static getServer(...args) { + return new EleventyDevServer(...args); + } + + constructor(name, dir, options = {}) { + debug("Creating new Dev Server instance.") + this.name = name; + this.normalizeOptions(options); + + this.fileCache = {}; + // Directory to serve + if(!dir) { + throw new Error("Missing `dir` to serve."); + } + this.dir = dir; + this.logger = this.options.logger; + this.getWatcher(); + + this.#readyPromise = new Promise((resolve) => { + this.#readyResolve = resolve; + }) + } + + 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 !== undefined) { + this.options.injectedScriptsFolder = options.folder; + delete this.options.folder; + } + if(options.domdiff !== undefined) { + this.options.domDiff = options.domdiff; + delete this.options.domdiff; + } + if(options.enabled !== undefined) { + this.options.liveReload = options.enabled; + delete this.options.enabled; + } + + this.options.pathPrefix = this.cleanupPathPrefix(this.options.pathPrefix); + } + + get watcher() { + if(this.#watcher) { + return 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() { + // only initialize watcher if watcher via getWatcher if has targets + // this.watcher in watchFiles() is a manual workaround + if(this.options.watch.length > 0) { + return this.watcher; + } + } + + watchFiles(files) { + if(Array.isArray(files) && files.length > 0) { + files = files.map(entry => TemplatePath.stripLeadingDotSlash(entry)); + + debug("Also watching %O", files); + this.watcher.add(files); + } + } + + cleanupPathPrefix(pathPrefix) { + if(!pathPrefix || pathPrefix === "/") { + return "/"; + } + if(!pathPrefix.startsWith("/")) { + pathPrefix = `/${pathPrefix}` + } + if(!pathPrefix.endsWith("/")) { + pathPrefix = `${pathPrefix}/`; + } + return pathPrefix; + } + + // Allowed list of files that can be served from outside `dir` + setAliases(aliases) { + if(aliases) { + this.passthroughAliases = aliases; + debug( "Setting aliases (emulated passthrough copy) %O", aliases ); + } + } + + matchPassthroughAlias(url) { + let aliases = Object.assign({}, this.options.aliases, this.passthroughAliases); + for(let targetUrl in aliases) { + if(!targetUrl) { + continue; + } + + let file = aliases[targetUrl]; + if(url.startsWith(targetUrl)) { + let inputDirectoryPath = file + url.slice(targetUrl.length); + + // e.g. addPassthroughCopy("img/") but <img src="/img/built/IdthKOzqFA-350.png"> + // generated by the image plugin (written to the output folder) + // If they do not exist in the input directory, this will fallback to the output directory. + if(fs.existsSync(inputDirectoryPath)) { + return inputDirectoryPath; + } + } + } + return false; + } + + isFileInDirectory(dir, file) { + let absoluteDir = TemplatePath.absolutePath(dir); + let absoluteFile = TemplatePath.absolutePath(file); + return absoluteFile.startsWith(absoluteDir); + } + + getOutputDirFilePath(filepath, filename = "") { + let computedPath; + if(filename === ".html") { + // avoid trailing slash for filepath/.html requests + let prefix = path.join(this.dir, filepath); + if(prefix.endsWith(path.sep)) { + prefix = prefix.substring(0, prefix.length - path.sep.length); + } + computedPath = prefix + filename; + } else { + computedPath = path.join(this.dir, filepath, filename); + } + + computedPath = decodeURIComponent(computedPath); + + if(!filename) { // is a direct URL request (not an implicit .html or index.html add) + let alias = this.matchPassthroughAlias(filepath); + + if(alias) { + if(!this.isFileInDirectory(path.resolve("."), alias)) { + throw new Error("Invalid path"); + } + + return alias; + } + } + + // Check that the file is in the output path (error if folks try use `..` in the filepath) + if(!this.isFileInDirectory(this.dir, computedPath)) { + throw new Error("Invalid path"); + } + + return computedPath; + } + + isOutputFilePathExists(rawPath) { + return fs.existsSync(rawPath) && !TemplatePath.isDirectorySync(rawPath); + } + + /* Use conventions documented here https://www.zachleat.com/web/trailing-slash/ + * resource.html exists: + * /resource matches + * /resource/ redirects to /resource + * resource/index.html exists: + * /resource redirects to /resource/ + * /resource/ matches + * both resource.html and resource/index.html exists: + * /resource matches /resource.html + * /resource/ matches /resource/index.html + */ + mapUrlToFilePath(url) { + // Note: `localhost` is not important here, any host would work + let u = new URL(url, "http://localhost/"); + url = u.pathname; + + // Remove PathPrefix from start of URL + if (this.options.pathPrefix !== "/") { + // Requests to root should redirect to new pathPrefix + if(url === "/") { + return { + statusCode: 302, + url: this.options.pathPrefix, + } + } + + // Requests to anything outside of root should fail with 404 + if (!url.startsWith(this.options.pathPrefix)) { + return { + statusCode: 404, + }; + } + + url = url.slice(this.options.pathPrefix.length - 1); + } + + let rawPath = this.getOutputDirFilePath(url); + if (this.isOutputFilePathExists(rawPath)) { + return { + statusCode: 200, + filepath: rawPath, + }; + } + + let indexHtmlPath = this.getOutputDirFilePath(url, this.options.indexFileName); + let indexHtmlExists = fs.existsSync(indexHtmlPath); + + let htmlPath = this.getOutputDirFilePath(url, ".html"); + let htmlExists = fs.existsSync(htmlPath); + + // /resource/ => /resource/index.html + if (indexHtmlExists && url.endsWith("/")) { + return { + statusCode: 200, + filepath: indexHtmlPath, + }; + } + // /resource => resource.html + if (htmlExists && !url.endsWith("/")) { + return { + statusCode: 200, + filepath: htmlPath, + }; + } + + // /resource => redirect to /resource/ + if (indexHtmlExists && !url.endsWith("/")) { + return { + statusCode: 301, + url: u.pathname + "/", + }; + } + + // /resource/ => redirect to /resource + if (htmlExists && url.endsWith("/")) { + return { + statusCode: 301, + url: u.pathname.substring(0, u.pathname.length - 1), + }; + } + + return { + statusCode: 404, + }; + } + + #readFile(filepath) { + if(this.options.useCache && this.fileCache[filepath]) { + return this.fileCache[filepath]; + } + + let contents = fs.readFileSync(filepath, { + encoding: this.options.encoding, + }); + + if(this.options.useCache) { + this.fileCache[filepath] = contents; + } + + return contents; + } + + #getFileContents(localpath, rootDir) { + let filepath; + let searchLocations = []; + + if(rootDir) { + searchLocations.push(TemplatePath.absolutePath(rootDir, localpath)); + } + + // fallbacks for file:../ installations + searchLocations.push(TemplatePath.absolutePath(__dirname, localpath)); + searchLocations.push(TemplatePath.absolutePath(__dirname, "../../../", localpath)); + + for(let loc of searchLocations) { + if(fs.existsSync(loc)) { + filepath = loc; + break; + } + } + + return this.#readFile(filepath); + } + + augmentContentWithNotifier(content, inlineContents = false, options = {}) { + let { integrityHash, scriptContents } = options; + if(!scriptContents) { + scriptContents = this.#getFileContents("./client/reload-client.js"); + } + if(!integrityHash) { + integrityHash = ssri.fromData(scriptContents); + } + + let searchParams = new URLSearchParams(); + if(this.options.reloadPort) { + searchParams.set("reloadPort", this.options.reloadPort); + } + + let searchParamsStr = searchParams.size > 0 ? `?${searchParams.toString()}` : ""; + + // 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.injectedScriptsFolder}/reload-client.js${searchParamsStr}">`}</script>`; + + if (content.includes("</head>")) { + return content.replace("</head>", `${script}</head>`); + } + + // If the HTML document contains an importmap, insert the module script after the importmap element + let importMapRegEx = /<script type=\\?importmap\\?[^>]*>(\n|.)*?<\/script>/gmi; + let importMapMatch = content.match(importMapRegEx)?.[0]; + + if (importMapMatch) { + return content.replace(importMapMatch, `${importMapMatch}${script}`); + } + + // <title> is the only *required* element in an HTML document + if (content.includes("</title>")) { + return content.replace("</title>", `</title>${script}`); + } + + // If youâve reached this section, your HTML is invalid! + // We want to be super forgiving here, because folks might be in-progress editing the document! + if (content.includes("</body>")) { + return content.replace("</body>", `${script}</body>`); + } + if (content.includes("</html>")) { + return content.replace("</html>", `${script}</html>`); + } + if (content.includes("<!doctype html>")) { + return content.replace("<!doctype html>", `<!doctype html>${script}`); + } + + // Notably, works without content at all!! + return (content || "") + script; + } + + getFileContentType(filepath, res) { + let contentType = res.getHeader("Content-Type"); + + // Content-Type might be already set via middleware + if (contentType) { + return contentType; + } + + let mimeType = mime.getType(filepath); + if (!mimeType) { + return; + } + + contentType = mimeType; + + // We only want to append charset if the header is not already set + if (contentType === "text/html") { + contentType = `text/html; charset=${this.options.encoding}`; + } + + return contentType; + } + + renderFile(filepath, res) { + let contents = fs.readFileSync(filepath); + let contentType = this.getFileContentType(filepath, res); + + for(const [key, value] of Object.entries(this.options.headers)){ + res.setHeader(key, value); + } + + if (!contentType) { + return res.end(contents); + } + + res.setHeader("Content-Type", contentType); + + if (contentType.startsWith("text/html")) { + // the string is important here, wrapResponse expects strings internally for HTML content (for now) + return res.end(contents.toString()); + } + + return res.end(contents); + } + + async eleventyDevServerMiddleware(req, res, next) { + if(this.#serverState === "CLOSING") { + return res.end(""); + } + + for(let urlPatternString in this.options.onRequest) { + let fn = this.options.onRequest[urlPatternString]; + let fullPath = this.getServerPath(urlPatternString); + let p = new URLPattern({ pathname: fullPath }); + + // request url should already include pathprefix. + let fullUrl = this.getServerUrlRaw("localhost", req.url); + let match = p.exec(fullUrl); + + let u = new URL(fullUrl); + + if(match) { + let result = await fn({ + url: u, + pattern: p, + patternGroups: match?.pathname?.groups || {}, + }); + + if(!result && result !== "") { + continue; + } + + if(typeof result === "string") { + return res.end(result); + } + + if(isPlainObject(result) || result instanceof Response) { + if(typeof result.status === "number") { + res.statusCode = result.status; + } + + if(result.headers instanceof Headers) { + for(let [key, value] of result.headers.entries()) { + res.setHeader(key, value); + } + } else if(isPlainObject(result.headers)) { + for(let key of Object.keys(result.headers)) { + res.setHeader(key, result.headers[key]); + } + } + + if(result instanceof Response) { + // no gzip/br compression here, uncompressed from fetch https://github.com/w3c/ServiceWorker/issues/339 + res.removeHeader("content-encoding"); + + let arrayBuffer = await result.arrayBuffer(); + res.setHeader("content-length", arrayBuffer.byteLength); + + let buffer = Buffer.from(arrayBuffer); + return res.end(buffer); + } + + return res.end(result.body || ""); + } + + throw new Error(`Invalid return type from \`onRequest\` pattern for ${urlPatternString}: expected string, object literal, or Response instance.`); + } + } // end onRequest + + if(req.url.startsWith(`/${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.injectedScriptsFolder}/morphdom.js`) { + if(this.options.domDiff) { + res.setHeader("Content-Type", mime.getType("js")); + let morphdomEsmPath = require.resolve("morphdom").replace("morphdom.js", "morphdom-esm.js"); + return res.end(this.#readFile(morphdomEsmPath)); + } + } + + next(); + } + + // This runs at the end of the middleware chain + eleventyProjectMiddleware(req, res) { + // Known issue with `finalhandler` and HTTP/2: + // UnsupportedWarning: Status message is not supported by HTTP/2 (RFC7540 8.1.2.4) + // https://github.com/pillarjs/finalhandler/pull/34 + + let lastNext = finalhandler(req, res, { + onerror: (e) => { + if (e.statusCode === 404) { + let localPath = TemplatePath.stripLeadingSubPath( + e.path, + TemplatePath.absolutePath(this.dir) + ); + this.logger.error( + `HTTP ${e.statusCode}: Template not found in output directory (${this.dir}): ${localPath}` + ); + } else { + this.logger.error(`HTTP ${e.statusCode}: ${e.message}`); + } + }, + }); + + // middleware (maybe a serverless request) already set a body upstream, skip this part + if(!res._shouldForceEnd) { + let match = this.mapUrlToFilePath(req.url); + debug( req.url, match ); + + if (match) { + if (match.statusCode === 200 && match.filepath) { + // Content-Range request, probably Safari trying to stream video + if (req.headers.range) { + return send(req, match.filepath).pipe(res); + } + + return this.renderFile(match.filepath, res); + } + + // Redirects, usually for trailing slash to .html stuff + if (match.url) { + res.statusCode = match.statusCode; + res.setHeader("Location", match.url); + return res.end(); + } + + let raw404Path = this.getOutputDirFilePath("404.html"); + if(match.statusCode === 404 && this.isOutputFilePathExists(raw404Path)) { + res.statusCode = match.statusCode; + res.isCustomErrorPage = true; + return this.renderFile(raw404Path, res); + } + } + } + + if(res.body && !res.bodyUsed) { + if(res._shouldForceEnd) { + res.end(); + } else { + let err = new Error("A response was never written to the stream. Are you missing a server middleware with `res.end()`?"); + err.statusCode = 500; + lastNext(err); + return; + } + } + + lastNext(); + } + + async onRequestHandler (req, res) { + res = wrapResponse(res, content => { + + // check to see if this is a client fetch and not a navigation + let isXHR = req.headers["sec-fetch-mode"] && req.headers["sec-fetch-mode"] != "navigate"; + + if(this.options.liveReload !== false && !isXHR) { + let scriptContents = this.#getFileContents("./client/reload-client.js"); + let integrityHash = ssri.fromData(scriptContents); + + // Bare (not-custom) finalhandler error pages have a Content-Security-Policy `default-src 'none'` that + // prevents the client script from executing, so we override it + if(res.statusCode !== 200 && !res.isCustomErrorPage) { + res.setHeader("Content-Security-Policy", `script-src '${integrityHash}'`); + } + return this.augmentContentWithNotifier(content, res.statusCode !== 200, { + scriptContents, + integrityHash + }); + } + + return content; + }); + + let middlewares = this.options.middleware || []; + middlewares = middlewares.slice(); + + // TODO because this runs at the very end of the middleware chain, + // if we move the static stuff up in the order we could use middleware to modify + // the static content in middleware! + middlewares.push(this.eleventyProjectMiddleware); + middlewares.reverse(); + + // Runs very first in the middleware chain + middlewares.push(this.eleventyDevServerMiddleware); + + let bound = []; + let next; + + for(let ware of middlewares) { + let fn; + if(next) { + fn = ware.bind(this, req, res, next); + } else { + fn = ware.bind(this, req, res); + } + bound.push(fn); + next = fn; + } + + bound.reverse(); + + let [first] = bound; + await first(); + } + + getHosts() { + let hosts = new Set(); + if(this.options.showAllHosts) { + for(let host of ipAddress()) { + hosts.add(this.getServerUrl(host)); + } + } + hosts.add(this.getServerUrl("localhost")); + return Array.from(hosts); + } + + get server() { + if (this._server) { + return this._server; + } + + this.start = Date.now(); + + // Check for secure server requirements, otherwise use HTTP + let { key, cert } = this.options.https; + if(key && cert) { + const { createSecureServer } = require("http2"); + + let options = { + allowHTTP1: true, + + // Credentials + key: fs.readFileSync(key), + cert: fs.readFileSync(cert), + }; + this._server = createSecureServer(options, this.onRequestHandler.bind(this)); + this._serverProtocol = "https:"; + } else { + const { createServer } = require("http"); + + this._server = createServer(this.onRequestHandler.bind(this)); + this._serverProtocol = "http:"; + } + + this.portRetryCount = 0; + this._server.on("error", (err) => { + if (err.code == "EADDRINUSE") { + if (this.portRetryCount < this.options.portReassignmentRetryCount) { + this.portRetryCount++; + debug( + "Server already using port %o, trying the next port %o. Retry number %o of %o", + err.port, + err.port + 1, + this.portRetryCount, + this.options.portReassignmentRetryCount + ); + this._serverListen(err.port + 1); + } else { + throw new Error( + `Tried ${this.options.portReassignmentRetryCount} different ports but they were all in use. You can a different starter port using --port on the command line.` + ); + } + } else { + this._serverErrorHandler(err); + } + }); + + this._server.on("listening", (e) => { + this.setupReloadNotifier(); + + let logMessageCallback = typeof this.options.messageOnStart === "function" ? this.options.messageOnStart : () => false; + let hosts = this.getHosts(); + let message = logMessageCallback({ + hosts, + localhostUrl: this.getServerUrl("localhost"), + options: this.options, + version: pkg.version, + startupTime: Date.now() - this.start, + }); + + if(message) { + this.logger.info(message); + } + + this.#readyResolve(); + }); + + return this._server; + } + + async ready() { + return this.#readyPromise; + } + + _serverListen(port) { + this.server.listen({ + port, + }); + } + + getServerPath(pathname) { + // duplicate slashes + if(this.options.pathPrefix.endsWith("/") && pathname.startsWith("/")) { + pathname = pathname.slice(1); + } + return `${this.options.pathPrefix}${pathname}`; + } + + getServerUrlRaw(host, pathname = "", isRaw = true) { + if(!this._server || !this._serverProtocol) { + throw new Error("Access to server url not yet available."); + } + + let address = this._server.address(); + if(!address?.port) { + throw new Error("Access to server port not yet available."); + } + + return `${this._serverProtocol}//${host}:${address.port}${isRaw ? pathname : this.getServerPath(pathname)}`; + } + + getServerUrl(host, pathname = "") { + return this.getServerUrlRaw(host, pathname, false); + } + + async getPort() { + return new Promise(resolve => { + this.server.on("listening", (e) => { + let { port } = this._server.address(); + resolve(port); + }); + }) + } + + serve(port) { + this.getWatcher(); + + this._serverListen(port); + } + + _serverErrorHandler(err) { + if (err.code == "EADDRINUSE") { + this.logger.error(`Server error: Port in use ${err.port}`); + } else { + this.logger.error(`Server error: ${err.message}`); + } + } + + // Websocket Notifications + setupReloadNotifier() { + let options = {}; + if(this.options.reloadPort) { + options.port = this.options.reloadPort; + } else { + // includes the port + options.server = this.server; + } + + let updateServer = new WebSocketServer(options); + + updateServer.on("connection", (ws) => { + this.sendUpdateNotification({ + type: "eleventy.status", + status: "connected", + }); + }); + + updateServer.on("error", (err) => { + this._serverErrorHandler(err); + }); + + this.updateServer = updateServer; + } + + // Broadcasts to all open browser windows + sendUpdateNotification(obj) { + if(!this.updateServer?.clients) { + return; + } + + for(let client of this.updateServer.clients) { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(obj)); + } + } + } + + // Helper for promisifying close methods with callbacks, like http.Server or ws.WebSocketServer. + async _closeServer(server) { + return new Promise((resolve, reject) => { + server.close(err => { + if (err) { + reject(err); + } + resolve(); + }); + + // Note: this method wonât exist for updateServer + if("closeAllConnections" in server) { + // Node 18.2+ + server.closeAllConnections(); + } + }); + } + + async close() { + // Prevent multiple invocations. + if (this.#serverClosing) { + return this.#serverClosing; + } + + // TODO would be awesome to set a delayed redirect when port changed to redirect to new _server_ + this.sendUpdateNotification({ + type: "eleventy.status", + status: "disconnected", + }); + + let promises = [] + if(this.updateServer) { + // Close all existing WS connections. + this.updateServer?.clients.forEach(socket => socket.close()); + promises.push(this._closeServer(this.updateServer)); + } + + if(this._server?.listening) { + promises.push(this._closeServer(this.server)); + } + + if(this.#watcher) { + promises.push(this.#watcher.close()); + this.#watcher = undefined; + } + + this.#serverClosing = Promise.all(promises).then(() => { + this.#serverState = "CLOSED"; + this.#serverClosing = undefined; + }); + + this.#serverState = "CLOSING"; + + return this.#serverClosing; + } + + sendError({ error }) { + this.sendUpdateNotification({ + type: "eleventy.error", + // Thanks https://stackoverflow.com/questions/18391212/is-it-not-possible-to-stringify-an-error-using-json-stringify + error: JSON.stringify(error, Object.getOwnPropertyNames(error)), + }); + } + + // 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(`/${this.options.indexFileName}`)) { + urls.push(path.slice(0, -1 * this.options.indexFileName.length)); + } else if(path.endsWith(".html")) { + urls.push(path.slice(0, -1 * ".html".length)); + } + + return urls; + } + + // returns [{ 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"))) { + // only if all changes are 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 = {}) { + let { subtype, files, build } = event; + if (build?.templates) { + build.templates = build.templates + .filter(entry => { + 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 + return (files || []).includes(entry.inputPath); + }); + } + + this.sendUpdateNotification({ + type: "eleventy.reload", + subtype, + files, + build, + }); + } +} + +module.exports = EleventyDevServer; diff --git a/node_modules/@11ty/eleventy-dev-server/server/ipAddress.js b/node_modules/@11ty/eleventy-dev-server/server/ipAddress.js new file mode 100644 index 0000000..aaddbee --- /dev/null +++ b/node_modules/@11ty/eleventy-dev-server/server/ipAddress.js @@ -0,0 +1,9 @@ +const os = require("node:os"); + +const INTERFACE_FAMILIES = ["IPv4"]; + +module.exports = function() { + return Object.values(os.networkInterfaces()).flat().filter(interface => { + return interface.internal === false && INTERFACE_FAMILIES.includes(interface.family); + }).map(interface => interface.address); +};
\ No newline at end of file diff --git a/node_modules/@11ty/eleventy-dev-server/server/wrapResponse.js b/node_modules/@11ty/eleventy-dev-server/server/wrapResponse.js new file mode 100644 index 0000000..5161904 --- /dev/null +++ b/node_modules/@11ty/eleventy-dev-server/server/wrapResponse.js @@ -0,0 +1,130 @@ +function getContentType(headers) { + if(!headers) { + return; + } + + for(let key in headers) { + if(key.toLowerCase() === "content-type") { + return headers[key]; + } + } +} + +// Inspired by `resp-modifier` https://github.com/shakyShane/resp-modifier/blob/4a000203c9db630bcfc3b6bb8ea2abc090ae0139/index.js +function wrapResponse(resp, transformHtml) { + resp._wrappedOriginalWrite = resp.write; + resp._wrappedOriginalWriteHead = resp.writeHead; + resp._wrappedOriginalEnd = resp.end; + + resp._wrappedHeaders = []; + resp._wrappedTransformHtml = transformHtml; + resp._hasEnded = false; + resp._shouldForceEnd = false; + + // Compatibility with web standards Response() + Object.defineProperty(resp, "body", { + // Returns write cache + get: function() { + if(typeof this._writeCache === "string") { + return this._writeCache; + } + }, + // Usage: + // res.body = ""; // overwrite existing content + // res.body += ""; // append to existing content, can also res.write("") to append + set: function(data) { + if(typeof data === "string") { + this._writeCache = data; + } + } + }); + + // Compatibility with web standards Response() + Object.defineProperty(resp, "bodyUsed", { + get: function() { + return this._hasEnded; + } + }) + + // Original signature writeHead(statusCode[, statusMessage][, headers]) + resp.writeHead = function(statusCode, ...args) { + let headers = args[args.length - 1]; + // statusMessage is a string + if(typeof headers !== "string") { + this._contentType = getContentType(headers); + } + + if((this._contentType || "").startsWith("text/html")) { + this._wrappedHeaders.push([statusCode, ...args]); + } else { + return this._wrappedOriginalWriteHead(statusCode, ...args); + } + return this; + } + + // data can be a String or Buffer + resp.write = function(data, ...args) { + if(typeof data === "string") { + if(!this._writeCache) { + this._writeCache = ""; + } + + // TODO encoding and callback args + this._writeCache += data; + } else { + // Buffers + return this._wrappedOriginalWrite(data, ...args); + } + return this; + } + + // data can be a String or Buffer + resp.end = function(data, encoding, callback) { + resp._hasEnded = true; + + if(typeof this._writeCache === "string" || typeof data === "string") { + // Strings + if(!this._writeCache) { + this._writeCache = ""; + } + if(typeof data === "string") { + this._writeCache += data; + } + + let result = this._writeCache; + + // Only transform HTML + // Note the âsetHeader versus writeHeadâ note on https://nodejs.org/api/http.html#responsewriteheadstatuscode-statusmessage-headers + let contentType = this._contentType || getContentType(this.getHeaders()); + if(contentType?.startsWith("text/html")) { + if(this._wrappedTransformHtml && typeof this._wrappedTransformHtml === "function") { + result = this._wrappedTransformHtml(result); + // uncompressed size: https://github.com/w3c/ServiceWorker/issues/339 + this.setHeader("Content-Length", Buffer.byteLength(result)); + } + } + + for(let headers of this._wrappedHeaders) { + this._wrappedOriginalWriteHead(...headers); + } + + this._writeCache = []; + this._wrappedOriginalWrite(result, encoding) + return this._wrappedOriginalEnd(callback); + } else { + // Buffer or Uint8Array + for(let headers of this._wrappedHeaders) { + this._wrappedOriginalWriteHead(...headers); + } + + if(data) { + this._wrappedOriginalWrite(data, encoding); + } + return this._wrappedOriginalEnd(callback); + } + } + + return resp; +} + +module.exports = wrapResponse;
\ No newline at end of file |
