├── .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 | 
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 | - [](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 = /