├── .gitignore ├── test ├── stubs │ ├── route2.html │ ├── route3.html │ ├── route space.html │ ├── route1 │ │ ├── index.html │ │ └── custom-index.html │ ├── route3 │ │ └── index.html │ ├── sample.html │ ├── alternative │ │ └── test │ │ │ └── .gitkeep │ ├── index.php │ ├── zach’s.html │ ├── with-css │ │ ├── style.css │ │ └── index.html │ ├── index.html │ ├── custom-index.html │ ├── html-with-js.html │ ├── petite-vue.html │ └── with-import-map.html ├── sample │ └── test’s │ │ └── sample.txt ├── testServer.js └── testServerRequests.js ├── .npmignore ├── server ├── ipAddress.js └── wrapResponse.js ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── package.json ├── README.md ├── cmd.cjs ├── cli.js ├── client └── reload-client.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /test/stubs/route2.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/stubs/route3.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .* 3 | -------------------------------------------------------------------------------- /test/stubs/route space.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/stubs/route1/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/stubs/route3/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/stubs/sample.html: -------------------------------------------------------------------------------- 1 | SAMPLE -------------------------------------------------------------------------------- /test/sample/test’s/sample.txt: -------------------------------------------------------------------------------- 1 | Testing! -------------------------------------------------------------------------------- /test/stubs/alternative/test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/stubs/index.php: -------------------------------------------------------------------------------- 1 | SAMPLE PHP 2 | -------------------------------------------------------------------------------- /test/stubs/route1/custom-index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/stubs/zach’s.html: -------------------------------------------------------------------------------- 1 | This is a test -------------------------------------------------------------------------------- /test/stubs/with-css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: blue; 3 | } -------------------------------------------------------------------------------- /test/stubs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | test/stubs/index.html 11 | 12 | -------------------------------------------------------------------------------- /server/ipAddress.js: -------------------------------------------------------------------------------- 1 | import os from "node:os"; 2 | 3 | const INTERFACE_FAMILIES = ["IPv4"]; 4 | 5 | export default function() { 6 | return Object.values(os.networkInterfaces()).flat().filter(({family, internal}) => { 7 | return internal === false && INTERFACE_FAMILIES.includes(family); 8 | }).map(({ address }) => address); 9 | }; -------------------------------------------------------------------------------- /test/stubs/custom-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | test/stubs/custom-index.html 11 | 12 | -------------------------------------------------------------------------------- /test/stubs/with-css/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | BODY TEXT 12 | 13 | -------------------------------------------------------------------------------- /test/stubs/html-with-js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

I love

11 | 15 | 16 | -------------------------------------------------------------------------------- /test/stubs/petite-vue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
test ljskdlfjlkdsjflk
11 |
test ljskdlfjlkdsjflk
12 |
test ljskdlfjlkdsjflk
13 | 14 | -------------------------------------------------------------------------------- /test/stubs/with-import-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 16 | 17 | 18 | This page has an importmap 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node Unit Tests 2 | on: 3 | push: 4 | branches-ignore: 5 | - "gh-pages" 6 | jobs: 7 | build: 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: ["ubuntu-latest", "macos-latest", "windows-latest"] 12 | node: ["20", "22", "24"] 13 | name: Node.js ${{ matrix.node }} on ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Setup node 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node }} 20 | # cache: npm 21 | - run: npm install 22 | - run: npm test 23 | env: 24 | YARN_GPG: no 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release to npm 2 | on: 3 | release: 4 | types: [published] 5 | permissions: read-all 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | environment: GitHub Publish 10 | permissions: 11 | contents: read 12 | id-token: write 13 | steps: 14 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # 4.1.7 15 | - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # 4.0.3 16 | with: 17 | node-version: "22" 18 | registry-url: 'https://registry.npmjs.org' 19 | - run: npm install -g npm@latest 20 | - run: npm ci 21 | - run: npm test 22 | - if: ${{ github.event.release.tag_name != '' && env.NPM_PUBLISH_TAG != '' }} 23 | run: npm publish --provenance --access=public --tag=${{ env.NPM_PUBLISH_TAG }} 24 | env: 25 | NPM_PUBLISH_TAG: ${{ contains(github.event.release.tag_name, '-alpha.') && 'canary' || contains(github.event.release.tag_name, '-beta.') && 'beta' || 'latest' }} 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@11ty/eleventy-dev-server", 3 | "version": "3.0.0-alpha.6", 4 | "description": "A minimal, modern, generic, hot-reloading local web server to help web developers.", 5 | "main": "server.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "npx ava --verbose", 9 | "sample": "node cmd.cjs --input=test/stubs" 10 | }, 11 | "license": "MIT", 12 | "engines": { 13 | "node": ">=20.19" 14 | }, 15 | "funding": { 16 | "type": "opencollective", 17 | "url": "https://opencollective.com/11ty" 18 | }, 19 | "bin": { 20 | "eleventy-dev-server": "./cmd.cjs" 21 | }, 22 | "keywords": [ 23 | "eleventy", 24 | "server", 25 | "cli" 26 | ], 27 | "publishConfig": { 28 | "access": "public" 29 | }, 30 | "author": { 31 | "name": "Zach Leatherman", 32 | "email": "zachleatherman@gmail.com", 33 | "url": "https://zachleat.com/" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git://github.com/11ty/eleventy-dev-server.git" 38 | }, 39 | "bugs": "https://github.com/11ty/eleventy-dev-server/issues", 40 | "homepage": "https://github.com/11ty/eleventy-dev-server/", 41 | "dependencies": { 42 | "@11ty/eleventy-utils": "^2.0.7", 43 | "@11ty/node-version-check": "^1.0.1", 44 | "chokidar": "^5.0.0", 45 | "debug": "^4.4.3", 46 | "finalhandler": "^2.1.1", 47 | "mime": "^4.1.0", 48 | "minimist": "^1.2.8", 49 | "morphdom": "^2.7.7", 50 | "send": "^1.2.0", 51 | "ssri": "^13.0.0", 52 | "urlpattern-polyfill": "^10.1.0", 53 | "ws": "^8.18.3" 54 | }, 55 | "devDependencies": { 56 | "ava": "^6.4.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

11ty Logo

2 | 3 | # eleventy-dev-server 🕚⚡️🎈🐀 4 | 5 | A minimal, modern, generic, hot-reloading local web server to help web developers. 6 | 7 | ## ➡ [Documentation](https://www.11ty.dev/docs/watch-serve/#eleventy-dev-server) 8 | 9 | - Please star [Eleventy on GitHub](https://github.com/11ty/eleventy/)! 10 | - [![npm Version](https://img.shields.io/npm/v/@11ty/eleventy-dev-server.svg?style=for-the-badge)](https://www.npmjs.com/package/@11ty/eleventy-dev-server) 11 | 12 | ## Installation 13 | 14 | This is bundled with `@11ty/eleventy` (and you do not need to install it separately) in Eleventy v2.0 and newer. 15 | 16 | ## CLI 17 | 18 | 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`. 19 | 20 | ```sh 21 | npm install -g @11ty/eleventy-dev-server 22 | 23 | # Alternatively, install locally into your project 24 | npm install @11ty/eleventy-dev-server 25 | ``` 26 | 27 | This package requires Node 18 or newer. 28 | 29 | ### CLI Usage 30 | 31 | ```sh 32 | # Serve the current directory 33 | npx @11ty/eleventy-dev-server 34 | 35 | # Serve a different subdirectory (also aliased as --input) 36 | npx @11ty/eleventy-dev-server --dir=_site 37 | 38 | # Disable the `domdiff` feature 39 | npx @11ty/eleventy-dev-server --domdiff=false 40 | 41 | # Full command list in the Help 42 | npx @11ty/eleventy-dev-server --help 43 | ``` 44 | 45 | ## Tests 46 | 47 | ``` 48 | npm run test 49 | ``` 50 | 51 | - 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)) 52 | 53 | ## Changelog 54 | 55 | - `v3.0.0` bumps Node.js minimum to 20, [`chokidar@4` drops support for globs in `watch` option](https://github.com/paulmillr/chokidar#upgrading) 56 | - `v2.0.0` bumps Node.js minimum to 18. -------------------------------------------------------------------------------- /cmd.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const pkg = require("./package.json"); 4 | 5 | // Node check 6 | require("please-upgrade-node")(pkg, { 7 | message: function (requiredVersion) { 8 | return ( 9 | "eleventy-dev-server requires Node " + 10 | requiredVersion + 11 | ". You will need to upgrade Node!" 12 | ); 13 | }, 14 | }); 15 | 16 | const { Logger, Cli } = require("./cli.js"); 17 | 18 | const debug = require("debug")("Eleventy:DevServer"); 19 | 20 | try { 21 | const defaults = Cli.getDefaultOptions(); 22 | for(let key in defaults) { 23 | if(key.toLowerCase() !== key) { 24 | defaults[key.toLowerCase()] = defaults[key]; 25 | delete defaults[key]; 26 | } 27 | } 28 | 29 | const argv = require("minimist")(process.argv.slice(2), { 30 | string: [ 31 | "dir", 32 | "input", // alias for dir 33 | "port", 34 | ], 35 | boolean: [ 36 | "version", 37 | "help", 38 | "domdiff", 39 | ], 40 | default: defaults, 41 | unknown: function (unknownArgument) { 42 | throw new Error( 43 | `We don’t know what '${unknownArgument}' is. Use --help to see the list of supported commands.` 44 | ); 45 | }, 46 | }); 47 | 48 | debug("command: eleventy-dev-server %o", argv); 49 | 50 | process.on("unhandledRejection", (error, promise) => { 51 | Logger.fatal("Unhandled rejection in promise:", promise, error); 52 | }); 53 | process.on("uncaughtException", (error) => { 54 | Logger.fatal("Uncaught exception:", error); 55 | }); 56 | 57 | if (argv.version) { 58 | console.log(Cli.getVersion()); 59 | } else if (argv.help) { 60 | console.log(Cli.getHelp()); 61 | } else { 62 | let cli = new Cli(); 63 | 64 | cli.serve({ 65 | input: argv.dir || argv.input, 66 | port: argv.port, 67 | domDiff: argv.domdiff, 68 | }); 69 | 70 | process.on("SIGINT", async () => { 71 | await cli.close(); 72 | process.exitCode = 0; 73 | }); 74 | } 75 | } catch (e) { 76 | Logger.fatal("Fatal Error:", e) 77 | } 78 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | import { createRequire } from "node:module"; 2 | import EleventyDevServer from "./server.js"; 3 | 4 | const require = createRequire(import.meta.url); 5 | const pkg = require("./package.json"); 6 | 7 | export const Logger = { 8 | info(...args) { 9 | console.log( "[11ty/eleventy-dev-server]", ...args ); 10 | }, 11 | error(...args) { 12 | console.error( "[11ty/eleventy-dev-server]", ...args ); 13 | }, 14 | fatal(...args) { 15 | Logger.error(...args); 16 | process.exitCode = 1; 17 | }, 18 | log(...args) { 19 | return Logger.info(...args); 20 | } 21 | }; 22 | 23 | export class Cli { 24 | static getVersion() { 25 | return pkg.version; 26 | } 27 | 28 | static getHelp() { 29 | return `Usage: 30 | 31 | eleventy-dev-server 32 | eleventy-dev-server --dir=_site 33 | eleventy-dev-server --port=3000 34 | 35 | Arguments: 36 | 37 | --version 38 | 39 | --dir=. 40 | Directory to serve (default: \`.\`) 41 | 42 | --input (alias for --dir) 43 | 44 | --port=8080 45 | Run the web server on this port (default: \`8080\`) 46 | Will autoincrement if already in use. 47 | 48 | --domdiff (enabled, default) 49 | --domdiff=false (disabled) 50 | Apply HTML changes without a full page reload. 51 | 52 | --help`; 53 | } 54 | 55 | static getDefaultOptions() { 56 | return { 57 | port: "8080", 58 | input: ".", 59 | domDiff: true, 60 | } 61 | } 62 | 63 | async serve(options = {}) { 64 | this.options = Object.assign(Cli.getDefaultOptions(), options); 65 | 66 | this.server = EleventyDevServer.getServer("eleventy-dev-server-cli", this.options.input, { 67 | // TODO allow server configuration extensions 68 | showVersion: true, 69 | logger: Logger, 70 | domDiff: this.options.domDiff, 71 | 72 | // CLI watches all files in the folder by default 73 | // this is different from Eleventy usage! 74 | watch: [ this.options.input ], 75 | }); 76 | 77 | this.server.serve(this.options.port); 78 | 79 | // TODO? send any errors here to the server too 80 | // with server.sendError({ error }); 81 | } 82 | 83 | close() { 84 | if(this.server) { 85 | return this.server.close(); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /server/wrapResponse.js: -------------------------------------------------------------------------------- 1 | function getContentType(headers) { 2 | if(!headers) { 3 | return; 4 | } 5 | 6 | for(let key in headers) { 7 | if(key.toLowerCase() === "content-type") { 8 | return headers[key]; 9 | } 10 | } 11 | } 12 | 13 | // Inspired by `resp-modifier` https://github.com/shakyShane/resp-modifier/blob/4a000203c9db630bcfc3b6bb8ea2abc090ae0139/index.js 14 | export default function wrapResponse(resp, transformHtml) { 15 | resp._wrappedOriginalWrite = resp.write; 16 | resp._wrappedOriginalWriteHead = resp.writeHead; 17 | resp._wrappedOriginalEnd = resp.end; 18 | 19 | resp._wrappedHeaders = []; 20 | resp._wrappedTransformHtml = transformHtml; 21 | resp._hasEnded = false; 22 | resp._shouldForceEnd = false; 23 | 24 | // Compatibility with web standards Response() 25 | Object.defineProperty(resp, "body", { 26 | // Returns write cache 27 | get: function() { 28 | if(typeof this._writeCache === "string") { 29 | return this._writeCache; 30 | } 31 | }, 32 | // Usage: 33 | // res.body = ""; // overwrite existing content 34 | // res.body += ""; // append to existing content, can also res.write("") to append 35 | set: function(data) { 36 | if(typeof data === "string") { 37 | this._writeCache = data; 38 | } 39 | } 40 | }); 41 | 42 | // Compatibility with web standards Response() 43 | Object.defineProperty(resp, "bodyUsed", { 44 | get: function() { 45 | return this._hasEnded; 46 | } 47 | }) 48 | 49 | // Original signature writeHead(statusCode[, statusMessage][, headers]) 50 | resp.writeHead = function(statusCode, ...args) { 51 | let headers = args[args.length - 1]; 52 | // statusMessage is a string 53 | if(typeof headers !== "string") { 54 | this._contentType = getContentType(headers); 55 | } 56 | 57 | if((this._contentType || "").startsWith("text/html")) { 58 | this._wrappedHeaders.push([statusCode, ...args]); 59 | } else { 60 | return this._wrappedOriginalWriteHead(statusCode, ...args); 61 | } 62 | return this; 63 | } 64 | 65 | // data can be a String or Buffer 66 | resp.write = function(data, ...args) { 67 | if(typeof data === "string") { 68 | if(!this._writeCache) { 69 | this._writeCache = ""; 70 | } 71 | 72 | // TODO encoding and callback args 73 | this._writeCache += data; 74 | } else { 75 | // Buffers 76 | return this._wrappedOriginalWrite(data, ...args); 77 | } 78 | return this; 79 | } 80 | 81 | // data can be a String or Buffer 82 | resp.end = function(data, encoding, callback) { 83 | resp._hasEnded = true; 84 | 85 | if(typeof this._writeCache === "string" || typeof data === "string") { 86 | // Strings 87 | if(!this._writeCache) { 88 | this._writeCache = ""; 89 | } 90 | if(typeof data === "string") { 91 | this._writeCache += data; 92 | } 93 | 94 | let result = this._writeCache; 95 | 96 | // Only transform HTML 97 | // Note the “setHeader versus writeHead” note on https://nodejs.org/api/http.html#responsewriteheadstatuscode-statusmessage-headers 98 | let contentType = this._contentType || getContentType(this.getHeaders()); 99 | if(contentType?.startsWith("text/html")) { 100 | if(this._wrappedTransformHtml && typeof this._wrappedTransformHtml === "function") { 101 | result = this._wrappedTransformHtml(result); 102 | // uncompressed size: https://github.com/w3c/ServiceWorker/issues/339 103 | this.setHeader("Content-Length", Buffer.byteLength(result)); 104 | } 105 | } 106 | 107 | for(let headers of this._wrappedHeaders) { 108 | this._wrappedOriginalWriteHead(...headers); 109 | } 110 | 111 | this._writeCache = []; 112 | this._wrappedOriginalWrite(result, encoding) 113 | return this._wrappedOriginalEnd(callback); 114 | } else { 115 | // Buffer or Uint8Array 116 | for(let headers of this._wrappedHeaders) { 117 | this._wrappedOriginalWriteHead(...headers); 118 | } 119 | 120 | if(data) { 121 | this._wrappedOriginalWrite(data, encoding); 122 | } 123 | return this._wrappedOriginalEnd(callback); 124 | } 125 | } 126 | 127 | return resp; 128 | } 129 | -------------------------------------------------------------------------------- /test/testServer.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import test from "ava"; 3 | import EleventyDevServer from "../server.js"; 4 | 5 | function getOptions(options = {}) { 6 | options.logger = { 7 | info: function() {}, 8 | log: function() {}, 9 | error: function() {}, 10 | }; 11 | return options; 12 | } 13 | 14 | function testNormalizeFilePath(filepath) { 15 | return filepath.split("/").join(path.sep); 16 | } 17 | 18 | test("Url mappings for resource/index.html", async (t) => { 19 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions()); 20 | 21 | t.deepEqual(server.mapUrlToFilePath("/route1/"), { 22 | statusCode: 200, 23 | filepath: testNormalizeFilePath("test/stubs/route1/index.html") 24 | }); 25 | 26 | t.deepEqual(server.mapUrlToFilePath("/route1"), { 27 | statusCode: 301, 28 | url: "/route1/" 29 | }); 30 | 31 | t.deepEqual(server.mapUrlToFilePath("/route1.html"), { 32 | statusCode: 404 33 | }); 34 | 35 | t.deepEqual(server.mapUrlToFilePath("/route1/index.html"), { 36 | statusCode: 200, 37 | filepath: testNormalizeFilePath("test/stubs/route1/index.html") 38 | }); 39 | 40 | await server.close(); 41 | }); 42 | 43 | test("Url mappings for resource.html", async (t) => { 44 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions()); 45 | 46 | t.deepEqual(server.mapUrlToFilePath("/route2/"), { 47 | statusCode: 301, 48 | url: "/route2" 49 | }); 50 | 51 | t.deepEqual(server.mapUrlToFilePath("/route2/index.html"), { 52 | statusCode: 404 53 | }); 54 | 55 | t.deepEqual(server.mapUrlToFilePath("/route2"), { 56 | statusCode: 200, 57 | filepath: testNormalizeFilePath("test/stubs/route2.html") 58 | }); 59 | 60 | t.deepEqual(server.mapUrlToFilePath("/route2.html"), { 61 | statusCode: 200, 62 | filepath: testNormalizeFilePath("test/stubs/route2.html",) 63 | }); 64 | 65 | await server.close(); 66 | }); 67 | 68 | test("Url mappings for resource.html and resource/index.html", async (t) => { 69 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions()); 70 | 71 | // Production mismatch warning: Netlify 301 redirects to /route3 here 72 | t.deepEqual(server.mapUrlToFilePath("/route3/"), { 73 | statusCode: 200, 74 | filepath: testNormalizeFilePath("test/stubs/route3/index.html",) 75 | }); 76 | 77 | t.deepEqual(server.mapUrlToFilePath("/route3/index.html"), { 78 | statusCode: 200, 79 | filepath: testNormalizeFilePath("test/stubs/route3/index.html",) 80 | }); 81 | 82 | t.deepEqual(server.mapUrlToFilePath("/route3"), { 83 | statusCode: 200, 84 | filepath: testNormalizeFilePath("test/stubs/route3.html") 85 | }); 86 | 87 | t.deepEqual(server.mapUrlToFilePath("/route3.html"), { 88 | statusCode: 200, 89 | filepath: testNormalizeFilePath("test/stubs/route3.html",) 90 | }); 91 | 92 | await server.close(); 93 | }); 94 | 95 | test("Url mappings for missing resource", async (t) => { 96 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions()); 97 | 98 | // 404s 99 | t.deepEqual(server.mapUrlToFilePath("/does-not-exist/"), { 100 | statusCode: 404 101 | }); 102 | 103 | await server.close(); 104 | }); 105 | 106 | test("Url mapping for a filename with a space in it", async (t) => { 107 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions()); 108 | 109 | t.deepEqual(server.mapUrlToFilePath("/route space.html"), { 110 | statusCode: 200, 111 | filepath: testNormalizeFilePath("test/stubs/route space.html",) 112 | }); 113 | 114 | await server.close(); 115 | }); 116 | 117 | test("matchPassthroughAlias", async (t) => { 118 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions()); 119 | 120 | // url => project root input 121 | server.setAliases({ 122 | // works with directories 123 | "/img": "./test/stubs/img", 124 | "/elsewhere": "./test/stubs/alternative", 125 | // or full paths 126 | "/elsewhere/index.css": "./test/stubs/with-css/style.css", 127 | }); 128 | 129 | // No map entry 130 | t.is(server.matchPassthroughAlias("/"), false); 131 | t.is(server.matchPassthroughAlias("/index.html"), false); // file exists 132 | 133 | // File exists 134 | t.is(server.matchPassthroughAlias("/elsewhere"), "./test/stubs/alternative"); 135 | t.is(server.matchPassthroughAlias("/elsewhere/test"), "./test/stubs/alternative/test"); 136 | 137 | // Map entry exists but file does not exist 138 | t.is(server.matchPassthroughAlias("/elsewhere/test.png"), false); 139 | t.is(server.matchPassthroughAlias("/elsewhere/another.css"), false); 140 | 141 | // Map entry exists, file exists 142 | t.is(server.matchPassthroughAlias("/elsewhere/index.css"), "./test/stubs/with-css/style.css"); 143 | 144 | await server.close(); 145 | }); 146 | 147 | 148 | test("pathPrefix matching", async (t) => { 149 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions({ 150 | pathPrefix: "/pathprefix/" 151 | })); 152 | 153 | t.deepEqual(server.mapUrlToFilePath("/pathprefix/route1/"), { 154 | statusCode: 200, 155 | filepath: testNormalizeFilePath("test/stubs/route1/index.html") 156 | }); 157 | 158 | t.deepEqual(server.mapUrlToFilePath("/pathprefix/"), { 159 | statusCode: 200, 160 | filepath: testNormalizeFilePath("test/stubs/index.html") 161 | }); 162 | 163 | // `/` should redirect to pathprefix 164 | t.deepEqual(server.mapUrlToFilePath("/"), { 165 | statusCode: 302, 166 | url: '/pathprefix/', 167 | }); 168 | 169 | await server.close(); 170 | }); 171 | 172 | test("pathPrefix without leading slash", async (t) => { 173 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions({ 174 | pathPrefix: "pathprefix/" 175 | })); 176 | 177 | t.deepEqual(server.mapUrlToFilePath("/pathprefix/route1/"), { 178 | statusCode: 200, 179 | filepath: testNormalizeFilePath("test/stubs/route1/index.html") 180 | }); 181 | 182 | t.deepEqual(server.mapUrlToFilePath("/pathprefix/"), { 183 | statusCode: 200, 184 | filepath: testNormalizeFilePath("test/stubs/index.html") 185 | }); 186 | 187 | // `/` should redirect to pathprefix 188 | t.deepEqual(server.mapUrlToFilePath("/"), { 189 | statusCode: 302, 190 | url: '/pathprefix/', 191 | }); 192 | 193 | await server.close(); 194 | }); 195 | 196 | test("pathPrefix without trailing slash", async (t) => { 197 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions({ 198 | pathPrefix: "/pathprefix" 199 | })); 200 | 201 | t.deepEqual(server.mapUrlToFilePath("/pathprefix/route1/"), { 202 | statusCode: 200, 203 | filepath: testNormalizeFilePath("test/stubs/route1/index.html") 204 | }); 205 | 206 | t.deepEqual(server.mapUrlToFilePath("/pathprefix/"), { 207 | statusCode: 200, 208 | filepath: testNormalizeFilePath("test/stubs/index.html") 209 | }); 210 | 211 | // `/` should redirect to pathprefix 212 | t.deepEqual(server.mapUrlToFilePath("/"), { 213 | statusCode: 302, 214 | url: '/pathprefix/', 215 | }); 216 | 217 | await server.close(); 218 | }); 219 | 220 | test("pathPrefix without leading or trailing slash", async (t) => { 221 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions({ 222 | pathPrefix: "pathprefix" 223 | })); 224 | 225 | t.deepEqual(server.mapUrlToFilePath("/pathprefix/route1/"), { 226 | statusCode: 200, 227 | filepath: testNormalizeFilePath("test/stubs/route1/index.html") 228 | }); 229 | 230 | t.deepEqual(server.mapUrlToFilePath("/pathprefix/"), { 231 | statusCode: 200, 232 | filepath: testNormalizeFilePath("test/stubs/index.html") 233 | }); 234 | 235 | // `/` should redirect to pathprefix 236 | t.deepEqual(server.mapUrlToFilePath("/"), { 237 | statusCode: 302, 238 | url: '/pathprefix/', 239 | }); 240 | 241 | await server.close(); 242 | }); 243 | 244 | test("indexFileName option: serve custom index when provided", async (t) => { 245 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions({ indexFileName: 'custom-index.html' })); 246 | 247 | t.deepEqual(server.mapUrlToFilePath("/"), { 248 | statusCode: 200, 249 | filepath: testNormalizeFilePath("test/stubs/custom-index.html"), 250 | }); 251 | 252 | 253 | t.deepEqual(server.mapUrlToFilePath("/route1/"), { 254 | statusCode: 200, 255 | filepath: testNormalizeFilePath("test/stubs/route1/custom-index.html"), 256 | }); 257 | 258 | await server.close(); 259 | }); 260 | 261 | test("indexFileName option: return 404 when custom index file doesn't exist", async (t) => { 262 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions({ indexFileName: 'does-not-exist.html' })); 263 | 264 | t.deepEqual(server.mapUrlToFilePath("/"), { 265 | statusCode: 404, 266 | }); 267 | 268 | await server.close(); 269 | }); 270 | 271 | test("Test watch getter", async (t) => { 272 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions()); 273 | 274 | t.truthy(server.watcher); 275 | 276 | await server.close(); 277 | }); 278 | -------------------------------------------------------------------------------- /test/testServerRequests.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import http from "http"; 3 | import EleventyDevServer from "../server.js"; 4 | 5 | function getOptions(options = {}) { 6 | options.logger = { 7 | info: function() {}, 8 | log: function() {}, 9 | error: function() {}, 10 | }; 11 | options.portReassignmentRetryCount = 100; 12 | return options; 13 | } 14 | 15 | async function makeRequestTo(t, server, path) { 16 | let port = await server.getPort(); 17 | 18 | return new Promise(resolve => { 19 | const options = { 20 | hostname: 'localhost', 21 | port, 22 | path, 23 | method: 'GET', 24 | }; 25 | 26 | http.get(options, (res) => { 27 | const { statusCode } = res; 28 | if(statusCode !== 200) { 29 | throw new Error("Invalid status code" + statusCode); 30 | } 31 | 32 | res.setEncoding('utf8'); 33 | 34 | let rawData = ''; 35 | res.on('data', (chunk) => { rawData += chunk; }); 36 | res.on('end', () => { 37 | t.true( true ); 38 | resolve(rawData); 39 | }); 40 | }).on('error', (e) => { 41 | console.error(`Got error: ${e.message}`); 42 | }); 43 | }) 44 | } 45 | 46 | async function fetchHeadersForRequest(t, server, path, extras) { 47 | let port = await server.getPort(); 48 | 49 | return new Promise(resolve => { 50 | const options = { 51 | hostname: 'localhost', 52 | port, 53 | path, 54 | method: 'GET', 55 | ...extras, 56 | }; 57 | 58 | // Available status codes can be found here: http.STATUS_CODES 59 | const successCodes = [ 60 | 200, // OK 61 | 206, // Partial Content 62 | ]; 63 | http.get(options, (res) => { 64 | const { statusCode } = res; 65 | if (!successCodes.includes(statusCode)) { 66 | throw new Error("Invalid status code " + statusCode); 67 | } 68 | 69 | let headers = res.headers; 70 | resolve(headers); 71 | 72 | }).on('error', (e) => { 73 | console.error(`Got error: ${e.message}`); 74 | }); 75 | }) 76 | } 77 | 78 | test("Standard request", async (t) => { 79 | let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions()); 80 | server.serve(8100); 81 | 82 | let data = await makeRequestTo(t, server, "/sample"); 83 | t.true(data.includes("`; 464 | 465 | if (content.includes("")) { 466 | return content.replace("", `${script}`); 467 | } 468 | 469 | // If the HTML document contains an importmap, insert the module script after the importmap element 470 | let importMapRegEx = /