From 78b1e0b48514e04512ba5c690eccfc301d842c25 Mon Sep 17 00:00:00 2001 From: venus Date: Sun, 13 Sep 2026 01:59:49 -0500 Subject: [PATCH] feat(docker): build and serve with Node and npm instead of Nginx --- .gitignore | 1 + Dockerfile | 32 +++----- README.md | 7 +- docker-compose.yml | 4 +- nginx.conf | 30 -------- package-lock.json | 138 +---------------------------------- package.json | 7 +- server.js | 136 ++++++++++++++++++++++++++++++++++ test/build-integrity.test.js | 7 +- test/docker-config.test.js | 39 ++++++++++ test/server.test.js | 87 ++++++++++++++++++++++ test/site-config.test.js | 6 +- 12 files changed, 294 insertions(+), 200 deletions(-) delete mode 100644 nginx.conf create mode 100644 server.js create mode 100644 test/docker-config.test.js create mode 100644 test/server.test.js diff --git a/.gitignore b/.gitignore index 1353688..8d8e73d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ _site/ .DS_Store *.log .env +docker-compose.yml diff --git a/Dockerfile b/Dockerfile index 0758b2c..124fb5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,35 +1,27 @@ -# Stage 1: Build static site with Node -FROM node:22-alpine AS builder +FROM node:22-alpine WORKDIR /app -# Dynamic build arguments for domain & full URL (required, no fallback) +# Dynamic build arguments & environment variables ARG SITE_DOMAIN ARG SITE_URL ENV SITE_DOMAIN=$SITE_DOMAIN ENV SITE_URL=$SITE_URL +ENV PORT=8080 +ENV NODE_ENV=production -# Copy dependency definitions and install +# Install dependencies COPY package*.json ./ RUN npm ci -# Copy source files and compile static output via Eleventy +# Copy application source files COPY . . + +# Build static production assets RUN npm run build -# Stage 2: Serve static output via lightweight Alpine Nginx -FROM nginx:1.25-alpine +# Expose web server port +EXPOSE 8080 -# Remove default Nginx configuration -RUN rm /etc/nginx/conf.d/default.conf - -# Copy custom Nginx security & clean-URL configuration -COPY nginx.conf /etc/nginx/conf.d/default.conf - -# Copy compiled static website files from builder stage -COPY --from=builder /app/_site /usr/share/nginx/html - -# Expose HTTP port -EXPOSE 80 - -CMD ["nginx", "-g", "daemon off;"] +# Serve site with npm +CMD ["npm", "start"] diff --git a/README.md b/README.md index 1e2b49e..54d104f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Inspired by native terminal environments, modal text editors (Zed, Ghostty), and ### 1.1 Static Generation via Eleventy (11ty) - **Engine:** Fast, pre-rendered static HTML and minimal client-side runtime using **Eleventy** and **Nunjucks (`.njk`)** templates. -- **Performance:** Instant time-to-first-byte (TTFB), zero client-side framework bloat, and pure static asset delivery via Alpine Nginx. +- **Performance:** Instant time-to-first-byte (TTFB), zero client-side framework bloat, and pure static asset delivery via high-performance native Node runtime. - **Syntax Highlighting & Code Blocks:** Terminal-styled code blocks with title bars displaying filenames, language tags, and high-contrast monospace code. ### 1.2 Dynamic Obsidian Publishing Pipeline @@ -70,8 +70,9 @@ Adhering to the principles in [`sample-designs/design-specification.md`](./sampl │ └── posts/ # Markdown content ingested from Obsidian ├── .eleventy.js # Eleventy configuration, markdown-it plugins, and shortcodes ├── package.json # Project dependencies & build scripts -├── Dockerfile # Alpine Nginx container definition -├── docker-compose.yml # Multi-container orchestration (web + ingestion API) +├── server.js # Native Node static web server & clean-URL router +├── Dockerfile # Node container definition (built & served via npm) +├── docker-compose.yml # Container orchestration (web gateway) └── AGENTS.md # Project-level agent instructions & pair programming guide ``` diff --git a/docker-compose.yml b/docker-compose.yml index bb65ac9..53378ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,9 +15,9 @@ services: - SITE_DOMAIN=${SITE_DOMAIN:?Error: SITE_DOMAIN is not set. Please define SITE_DOMAIN in .env} - SITE_URL=${SITE_URL:-https://${SITE_DOMAIN}} ports: - - "8080:80" + - "8080:8080" healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:80/"] + test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"] interval: 30s timeout: 5s retries: 3 diff --git a/nginx.conf b/nginx.conf deleted file mode 100644 index f15a8db..0000000 --- a/nginx.conf +++ /dev/null @@ -1,30 +0,0 @@ -server { - listen 80; - server_name localhost; - root /usr/share/nginx/html; - index index.html; - - # Gzip Compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_proxied any; - gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml image/svg+xml; - - # Security Headers - add_header X-Content-Type-Options "nosniff" always; - add_header X-Frame-Options "DENY" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - - # Clean URL Routing - location / { - try_files $uri $uri/ $uri.html =404; - } - - # Static Assets Caching - location /assets/ { - expires 30d; - add_header Cache-Control "public, max-age=2592000, immutable"; - } -} diff --git a/package-lock.json b/package-lock.json index 11e3681..6b53ec4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "river-rooks-blog", "version": "1.0.0", "license": "MIT", - "devDependencies": { + "dependencies": { "@11ty/eleventy": "^3.1.6", "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2", "luxon": "^3.5.0", @@ -20,7 +20,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@11ty/dependency-tree/-/dependency-tree-4.0.2.tgz", "integrity": "sha512-RTF6VTZHatYf7fSZBUN3RKwiUeJh5dhWV61gDPrHhQF2/gzruAkYz8yXuvGLx3w3ZBKreGrR+MfYpSVkdbdbLA==", - "dev": true, "license": "MIT", "dependencies": { "@11ty/eleventy-utils": "^2.0.1" @@ -30,7 +29,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@11ty/dependency-tree-esm/-/dependency-tree-esm-2.0.4.tgz", "integrity": "sha512-MYKC0Ac77ILr1HnRJalzKDlb9Z8To3kXQCltx299pUXXUFtJ1RIONtULlknknqW8cLe19DLVgmxVCtjEFm7h0A==", - "dev": true, "license": "MIT", "dependencies": { "@11ty/eleventy-utils": "^2.0.7", @@ -43,7 +41,6 @@ "version": "3.1.6", "resolved": "https://registry.npmjs.org/@11ty/eleventy/-/eleventy-3.1.6.tgz", "integrity": "sha512-ZlSiR1PLdS2lv7TelBgWAhcvMiLNZkPBlLEb+lh7kGYZ+Mk0bo9qcYgVsewvw9W7Em0RH3wd01h5fAstNDh0zA==", - "dev": true, "license": "MIT", "dependencies": { "@11ty/dependency-tree": "^4.0.2", @@ -95,7 +92,6 @@ "version": "2.0.8", "resolved": "https://registry.npmjs.org/@11ty/eleventy-dev-server/-/eleventy-dev-server-2.0.8.tgz", "integrity": "sha512-15oC5M1DQlCaOMUq4limKRYmWiGecDaGwryr7fTE/oM9Ix8siqMvWi+I8VjsfrGr+iViDvWcH/TVI6D12d93mA==", - "dev": true, "license": "MIT", "dependencies": { "@11ty/eleventy-utils": "^2.0.1", @@ -126,7 +122,6 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.7.tgz", "integrity": "sha512-QK1tRFBhQdZASnYU8GMzpTdsMMFLVAkuU0gVVILqNyp09xJJZb81kAS3AFrNrwBCsgLxTdWHJ8N64+OTTsoKkA==", - "dev": true, "license": "MIT", "dependencies": { "@11ty/eleventy-utils": "^2.0.2", @@ -145,7 +140,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-syntaxhighlight/-/eleventy-plugin-syntaxhighlight-5.0.2.tgz", "integrity": "sha512-T6xVVRDJuHlrFMHbUiZkHjj5o1IlLzZW+1IL9eUsyXFU7rY2ztcYhZew/64vmceFFpQwzuSfxQOXxTJYmKkQ+A==", - "dev": true, "license": "MIT", "dependencies": { "prismjs": "^1.30.0" @@ -159,7 +153,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@11ty/eleventy-utils/-/eleventy-utils-2.0.7.tgz", "integrity": "sha512-6QE+duqSQ0GY9rENXYb4iPR4AYGdrFpqnmi59tFp9VrleOl0QSh8VlBr2yd6dlhkdtj7904poZW5PvGr9cMiJQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -173,7 +166,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@11ty/lodash-custom/-/lodash-custom-4.17.21.tgz", "integrity": "sha512-Mqt6im1xpb1Ykn3nbcCovWXK3ggywRJa+IXIdoz4wIIK+cvozADH63lexcuPpGS/gJ6/m2JxyyXDyupkMr5DHw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -187,7 +179,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@11ty/posthtml-urls/-/posthtml-urls-1.0.3.tgz", "integrity": "sha512-1YvhnkaNlFnnJic1rBMWmTC2adbuy+JQiBfl1Hecr1Wjjik1pQZmGyk/eC9zKX/FQv52s2Nht1Gi/UwhYqrBeg==", - "dev": true, "license": "MIT", "dependencies": { "evaluate-value": "^2.0.0", @@ -203,7 +194,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@11ty/recursive-copy/-/recursive-copy-4.0.4.tgz", "integrity": "sha512-oI7m8pa7/IAU/3lqRU9vjBbs20iKFo7x+1K9kT3aVira6scc1X9MjBdgLCHzLJeJ7iB6wydioA+kr9/qPnvmlQ==", - "dev": true, "license": "ISC", "dependencies": { "errno": "^1.0.0", @@ -219,7 +209,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", - "dev": true, "license": "MIT", "dependencies": { "@sindresorhus/transliterate": "^1.0.0", @@ -236,7 +225,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", - "dev": true, "license": "MIT", "dependencies": { "escape-string-regexp": "^5.0.0" @@ -252,7 +240,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "dev": true, "license": "MIT", "peer": true }, @@ -260,7 +247,6 @@ "version": "14.2.0", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz", "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -272,7 +258,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "dev": true, "license": "MIT", "peer": true }, @@ -280,14 +265,12 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", - "dev": true, "license": "MIT" }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -300,7 +283,6 @@ "version": "8.3.5", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -313,7 +295,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -327,7 +308,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -340,28 +320,24 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, "license": "MIT" }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/bcp-47": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.1.tgz", "integrity": "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==", - "dev": true, "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", @@ -377,7 +353,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -388,7 +363,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-2.3.0.tgz", "integrity": "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==", - "dev": true, "license": "MIT", "dependencies": { "bcp-47": "^2.0.0", @@ -403,7 +377,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -416,7 +389,6 @@ "version": "1.1.18", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -427,7 +399,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -440,7 +411,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -465,7 +435,6 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -475,14 +444,12 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -500,7 +467,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -510,7 +476,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -520,7 +485,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", @@ -535,7 +499,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, "license": "BSD-2-Clause", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -545,7 +508,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, "funding": [ { "type": "github", @@ -558,7 +520,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.2.0" @@ -574,7 +535,6 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^1.0.1", @@ -589,14 +549,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -606,7 +564,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -619,7 +576,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/errno/-/errno-1.0.0.tgz", "integrity": "sha512-3zV5mFS1E8/1bPxt/B0xxzI1snsg3uSCIh6Zo1qKg6iMw93hzPANk9oBFzSFBFrwuVoQuE3rLoouAUfwOAj1wQ==", - "dev": true, "license": "MIT", "dependencies": { "prr": "~1.0.1" @@ -632,14 +588,12 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -652,7 +606,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/esm-import-transformer/-/esm-import-transformer-3.0.5.tgz", "integrity": "sha512-1GKLvfuMnnpI75l8c6sHoz0L3Z872xL5akGuBudgqTDPv4Vy6f2Ec7jEMKTxlqWl/3kSvNbHELeimJtnqgYniw==", - "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.15.0" @@ -662,7 +615,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -676,7 +628,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -686,7 +637,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/evaluate-value/-/evaluate-value-2.0.0.tgz", "integrity": "sha512-VonfiuDJc0z4sOO7W0Pd130VLsXN6vmBWZlrog1mCb/o7o/Nl5Lr25+Kj/nkCCAhG+zqeeGjxhkK9oHpkgTHhQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -696,7 +646,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, "license": "MIT", "dependencies": { "is-extendable": "^0.1.0" @@ -709,7 +658,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -727,7 +675,6 @@ "version": "10.1.6", "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">= 10.4.0" @@ -737,7 +684,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -750,7 +696,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -769,7 +714,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -779,14 +723,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -796,7 +738,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -811,7 +752,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -824,7 +764,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "dev": true, "license": "MIT", "dependencies": { "js-yaml": "^3.13.1", @@ -840,7 +779,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" @@ -850,7 +788,6 @@ "version": "3.15.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -864,7 +801,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", - "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -884,7 +820,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -897,7 +832,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-equiv-refresh/-/http-equiv-refresh-2.0.1.tgz", "integrity": "sha512-XJpDL/MLkV3dKwLzHwr2dY05dYNfBNlyPu4STQ8WvKCFdc6vC5tPXuq28of663+gHVg03C+16pHHs/+FmmDjcw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -907,7 +841,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -928,14 +861,12 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -946,7 +877,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "dev": true, "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", @@ -961,7 +891,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -974,7 +903,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -985,7 +913,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -995,7 +922,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1005,7 +931,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -1018,14 +943,12 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==", - "dev": true, "license": "ISC" }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -1035,7 +958,6 @@ "version": "3.1.6", "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.6.tgz", "integrity": "sha512-ZFar/L4ngX7wZh2QX+Fiftmuf0igWJsrJtfizrovWifF1gAWkfmRa5Z1m0LQZbm0hKCHRDYhLRSLFrSqNe4EJA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0" @@ -1045,7 +967,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "dev": true, "funding": [ { "type": "github", @@ -1068,7 +989,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1078,7 +998,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1088,7 +1007,6 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1098,7 +1016,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", - "dev": true, "funding": [ { "type": "github", @@ -1118,7 +1035,6 @@ "version": "10.29.0", "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.29.0.tgz", "integrity": "sha512-pCVOhs6FLAR8su3ItJ07diN26t6W5dHQRnmTMy8HPyTFuv1+oSCVJIGp5pGjfQyOZfh50KswvKtMTp6p4JEIdw==", - "dev": true, "license": "MIT", "dependencies": { "commander": "^10.0.0" @@ -1139,14 +1055,12 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/list-to-array/-/list-to-array-1.1.0.tgz", "integrity": "sha512-+dAZZ2mM+/m+vY9ezfoueVvrgnHIGi5FvgSymbIgJOFwiznWyA59mav95L+Mc6xPtL3s9gm5eNTlNtxJLbNM1g==", - "dev": true, "license": "MIT" }, "node_modules/luxon": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1156,7 +1070,6 @@ "version": "14.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", - "dev": true, "funding": [ { "type": "github", @@ -1184,7 +1097,6 @@ "version": "9.2.1", "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-9.2.1.tgz", "integrity": "sha512-p6APiLJDFAW2GEvaavDvhIBn7jrX2jLv77NkBGgNacFTurbORYc4pyYySg/mI6mpR6cHQuAtzKtmqgQr4K8dsQ==", - "dev": true, "license": "Unlicense", "peerDependencies": { "@types/markdown-it": "*", @@ -1195,7 +1107,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -1208,14 +1119,12 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", - "dev": true, "license": "MIT" }, "node_modules/mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -1228,7 +1137,6 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -1238,7 +1146,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -1255,7 +1162,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -1268,7 +1174,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1278,7 +1183,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -1288,28 +1192,24 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/morphdom": { "version": "2.7.8", "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.8.tgz", "integrity": "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg==", - "dev": true, "license": "MIT" }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/node-retrieve-globals": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/node-retrieve-globals/-/node-retrieve-globals-6.0.1.tgz", "integrity": "sha512-j0DeFuZ/Wg3VlklfbxUgZF/mdHMTEiEipBb3q0SpMMbHaV3AVfoUQF8UGxh1s/yjqO0TgRZd4Pi/x2yRqoQ4Eg==", - "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.14.1", @@ -1321,7 +1221,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1331,7 +1230,6 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "a-sync-waterfall": "^1.0.0", @@ -1357,7 +1255,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -1367,7 +1264,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -1380,14 +1276,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", - "dev": true, "license": "MIT" }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -1397,7 +1291,6 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1410,7 +1303,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", - "dev": true, "license": "MIT", "dependencies": { "semver-compare": "^1.0.0" @@ -1420,7 +1312,6 @@ "version": "0.16.7", "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz", "integrity": "sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==", - "dev": true, "license": "MIT", "dependencies": { "posthtml-parser": "^0.11.0", @@ -1434,7 +1325,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/posthtml-match-helper/-/posthtml-match-helper-2.0.3.tgz", "integrity": "sha512-p9oJgTdMF2dyd7WE54QI1LvpBIkNkbSiiECKezNnDVYhGhD1AaOnAkw0Uh0y5TW+OHO8iBdSqnd8Wkpb6iUqmw==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -1447,7 +1337,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.11.0.tgz", "integrity": "sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==", - "dev": true, "license": "MIT", "dependencies": { "htmlparser2": "^7.1.1" @@ -1460,7 +1349,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz", "integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==", - "dev": true, "license": "MIT", "dependencies": { "is-json": "^2.0.1" @@ -1473,7 +1361,6 @@ "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1483,14 +1370,12 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true, "license": "MIT" }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1500,7 +1385,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -1514,7 +1398,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -1527,7 +1410,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -1540,7 +1422,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "dev": true, "license": "MIT", "dependencies": { "extend-shallow": "^2.0.1", @@ -1554,7 +1435,6 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1567,14 +1447,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, "license": "MIT" }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.3", @@ -1601,14 +1479,12 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, "license": "ISC" }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1618,7 +1494,6 @@ "version": "1.6.9", "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz", "integrity": "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.0.0" @@ -1628,14 +1503,12 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/ssri": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/ssri/-/ssri-11.0.0.tgz", "integrity": "sha512-aZpUoMN/Jj2MqA4vMCeiKGnc/8SuSyHbGSBdgFbZxP8OJGF/lFkIuElzPxsN0q8TQQ+prw3P4EDfB3TBHHgfXw==", - "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.3" @@ -1648,7 +1521,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -1658,7 +1530,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1668,7 +1539,6 @@ "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -1685,7 +1555,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -1698,7 +1567,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -1708,14 +1576,12 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -1725,14 +1591,12 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", - "dev": true, "license": "MIT" }, "node_modules/ws": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index a8a0e59..3c1d52a 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,10 @@ "scripts": { "dev": "eleventy --serve --watch", "build": "eleventy", + "serve": "node server.js", + "start": "node server.js", "test": "node --test 'test/**/*.test.js'", - "clean": "rm -rf _site", - "start": "npm run dev" + "clean": "rm -rf _site" }, "keywords": [ "eleventy", @@ -20,7 +21,7 @@ ], "author": "River Rooks", "license": "MIT", - "devDependencies": { + "dependencies": { "@11ty/eleventy": "^3.1.6", "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2", "luxon": "^3.5.0", diff --git a/server.js b/server.js new file mode 100644 index 0000000..84afa63 --- /dev/null +++ b/server.js @@ -0,0 +1,136 @@ +import http from "node:http"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const MIME_TYPES = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".mjs": "application/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".xml": "application/xml; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".webp": "image/webp", + ".gif": "image/gif", + ".ico": "image/x-icon", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".ttf": "font/ttf", + ".txt": "text/plain; charset=utf-8" +}; + +/** + * Creates the HTTP server instance for serving static files from _site + * @param {string} [customSiteDir] Optional custom directory to serve from + * @returns {http.Server} + */ +export function createServer(customSiteDir) { + const siteDir = path.resolve(customSiteDir || path.join(process.cwd(), "_site")); + + return http.createServer((req, res) => { + // Standard security headers + res.setHeader("X-Content-Type-Options", "nosniff"); + res.setHeader("X-Frame-Options", "DENY"); + res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); + + const parsedUrl = new URL(req.url, "http://localhost"); + + // Health check endpoint for container orchestrators & telemetry + if (parsedUrl.pathname === "/healthz" || parsedUrl.pathname === "/health") { + res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify({ status: "ok", uptime: process.uptime() })); + return; + } + + // Path normalization & security validation against directory traversal + let decodedPath = ""; + try { + decodedPath = decodeURIComponent(parsedUrl.pathname); + } catch { + res.writeHead(400, { "Content-Type": "text/plain" }); + res.end("Bad Request"); + return; + } + + const resolvedPath = path.resolve(siteDir, `.${decodedPath}`); + + // Boundary check: ensure target path is within siteDir + if (!resolvedPath.startsWith(siteDir)) { + res.writeHead(403, { "Content-Type": "text/plain" }); + res.end("Forbidden"); + return; + } + + let targetFile = resolvedPath; + + // Directory index resolution + if (fs.existsSync(targetFile) && fs.statSync(targetFile).isDirectory()) { + targetFile = path.join(targetFile, "index.html"); + } + + // Clean URL extensionless fallback (/posts/my-post -> /posts/my-post.html) + if (!fs.existsSync(targetFile) && fs.existsSync(`${targetFile}.html`)) { + targetFile = `${targetFile}.html`; + } + + // Check if target file exists and is a regular file + if (fs.existsSync(targetFile) && fs.statSync(targetFile).isFile()) { + const ext = path.extname(targetFile).toLowerCase(); + const contentType = MIME_TYPES[ext] || "application/octet-stream"; + + // Cache control: assets get long-term caching, html gets short + if (decodedPath.startsWith("/assets/")) { + res.setHeader("Cache-Control", "public, max-age=2592000, immutable"); + } else { + res.setHeader("Cache-Control", "public, max-age=0, must-revalidate"); + } + + res.writeHead(200, { "Content-Type": contentType }); + const stream = fs.createReadStream(targetFile); + stream.pipe(res); + return; + } + + // 404 Not Found fallback + const custom404 = path.join(siteDir, "404.html"); + if (fs.existsSync(custom404)) { + res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" }); + fs.createReadStream(custom404).pipe(res); + } else { + res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); + res.end("404 Not Found"); + } + }); +} + +// Direct CLI execution handler +const currentFilePath = fileURLToPath(import.meta.url); +if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) { + const port = parseInt(process.env.PORT || "8080", 10); + const host = process.env.HOST || "0.0.0.0"; + const siteDir = path.resolve(process.cwd(), "_site"); + + if (!fs.existsSync(siteDir)) { + console.warn(`[Static Server] Warning: '${siteDir}' does not exist yet. Run 'npm run build' first.`); + } + + const server = createServer(); + server.listen(port, host, () => { + console.log(`[Static Server] Serving '${siteDir}' on http://${host}:${port}`); + }); + + // Graceful termination handling + const shutdown = (signal) => { + console.log(`\n[Static Server] Received ${signal}. Shutting down gracefully...`); + server.close(() => { + process.exit(0); + }); + }; + + process.on("SIGINT", () => shutdown("SIGINT")); + process.on("SIGTERM", () => shutdown("SIGTERM")); +} diff --git a/test/build-integrity.test.js b/test/build-integrity.test.js index 3d20b7f..4bdd6d2 100644 --- a/test/build-integrity.test.js +++ b/test/build-integrity.test.js @@ -23,8 +23,11 @@ function getAllHtmlFiles(dir) { test("Build & Link Integrity: Output inspection and link verification", () => { // Ensure fresh build exists - if (!fs.existsSync(siteDir)) { - execSync("npm run build", { stdio: "pipe" }); + if (!fs.existsSync(path.join(siteDir, "index.html"))) { + execSync("npm run build", { + stdio: "pipe", + env: { ...process.env, SITE_DOMAIN: process.env.SITE_DOMAIN || "riverrooks.dev" } + }); } // 1. Core Pages Exist diff --git a/test/docker-config.test.js b/test/docker-config.test.js new file mode 100644 index 0000000..6ae1ab7 --- /dev/null +++ b/test/docker-config.test.js @@ -0,0 +1,39 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +test("Docker Configuration: Dockerfile enforces Node runtime and excludes Nginx", () => { + const dockerfilePath = path.resolve(process.cwd(), "Dockerfile"); + assert.ok(fs.existsSync(dockerfilePath), "Dockerfile must exist"); + + const content = fs.readFileSync(dockerfilePath, "utf-8"); + + // Must use Node 22 alpine + assert.match(content, /FROM\s+node:22-alpine/i, "Dockerfile must use node:22-alpine base image"); + + // Must NOT contain nginx base images or references + assert.doesNotMatch(content, /FROM\s+nginx/i, "Dockerfile must not use nginx base image"); + assert.doesNotMatch(content, /nginx/i, "Dockerfile must not contain nginx directives"); + + // Must build static site and serve with npm + assert.match(content, /RUN\s+npm\s+run\s+build/, "Dockerfile must run npm build"); + assert.match(content, /EXPOSE\s+8080/, "Dockerfile must expose port 8080"); + assert.match(content, /CMD\s*\[\s*["']npm["']\s*,\s*["']start["']\s*\]/, "Dockerfile must start via npm start"); +}); + +test("Docker Configuration: docker-compose.yml configures Node service correctly", () => { + const composePath = path.resolve(process.cwd(), "docker-compose.yml"); + assert.ok(fs.existsSync(composePath), "docker-compose.yml must exist"); + + const content = fs.readFileSync(composePath, "utf-8"); + + // Verifies port mapping to 8080 + assert.match(content, /8080:8080/, "docker-compose.yml must map port 8080 to container 8080"); + + // Verifies healthcheck targets 8080 + assert.match(content, /localhost:8080/, "Healthcheck must probe port 8080"); + + // Verifies domain argument enforcement + assert.match(content, /SITE_DOMAIN/, "docker-compose.yml must require SITE_DOMAIN"); +}); diff --git a/test/server.test.js b/test/server.test.js new file mode 100644 index 0000000..38bdb12 --- /dev/null +++ b/test/server.test.js @@ -0,0 +1,87 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; +import { execSync } from "node:child_process"; +import { createServer } from "../server.js"; + +const TEST_PORT = 8999; +const BASE_URL = `http://127.0.0.1:${TEST_PORT}`; + +// Helper to perform HTTP GET requests +function get(urlPath) { + return new Promise((resolve, reject) => { + http.get(`${BASE_URL}${urlPath}`, (res) => { + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => { + resolve({ + statusCode: res.statusCode, + headers: res.headers, + body: data + }); + }); + }).on("error", reject); + }); +} + +test("Node Static Server: lifecycle, routing, and adversarial security tests", async (t) => { + // Ensure _site exists before running server tests + if (!fs.existsSync(path.resolve(process.cwd(), "_site/index.html"))) { + execSync("npm run build", { + stdio: "pipe", + env: { ...process.env, SITE_DOMAIN: "riverrooks.dev" } + }); + } + + const server = createServer(); + await new Promise((resolve) => server.listen(TEST_PORT, "127.0.0.1", resolve)); + + t.after(() => { + return new Promise((resolve) => server.close(resolve)); + }); + + await t.test("serves root index.html with 200 OK and text/html", async () => { + const res = await get("/"); + assert.equal(res.statusCode, 200); + assert.match(res.headers["content-type"], /text\/html/); + assert.match(res.body, //i); + // Verify security headers + assert.equal(res.headers["x-content-type-options"], "nosniff"); + assert.equal(res.headers["x-frame-options"], "DENY"); + }); + + await t.test("serves clean directory URL (/dispatches/)", async () => { + const res = await get("/dispatches/"); + assert.equal(res.statusCode, 200); + assert.match(res.headers["content-type"], /text\/html/); + }); + + await t.test("serves static assets with appropriate Content-Type", async () => { + const res = await get("/assets/css/main.css"); + assert.equal(res.statusCode, 200); + assert.match(res.headers["content-type"], /text\/css/); + }); + + await t.test("responds with 200 OK JSON on /healthz", async () => { + const res = await get("/healthz"); + assert.equal(res.statusCode, 200); + assert.match(res.headers["content-type"], /application\/json/); + const body = JSON.parse(res.body); + assert.equal(body.status, "ok"); + }); + + await t.test("returns 404 Not Found for non-existent routes", async () => { + const res = await get("/non-existent-file-xyz.html"); + assert.equal(res.statusCode, 404); + }); + + await t.test("adversarial: blocks path traversal attempts", async () => { + const res = await get("/../../../../etc/passwd"); + // Must either return 403 Forbidden or 404 Not Found, never 200 + assert.notEqual(res.statusCode, 200); + assert.ok(res.statusCode === 403 || res.statusCode === 404); + assert.doesNotMatch(res.body, /root:x:/); + }); +}); diff --git a/test/site-config.test.js b/test/site-config.test.js index 4291948..d2f9a2c 100644 --- a/test/site-config.test.js +++ b/test/site-config.test.js @@ -48,9 +48,9 @@ test("Site Config: correctly parses SITE_DOMAIN and derives service subdomains", assert.equal(chefService.url, "https://chef.testcluster.internal"); // Verify navigation links - const navGateway = site.nav.find((n) => n.label === "Gateway"); - assert.ok(navGateway, "Nav Gateway link exists"); - assert.equal(navGateway.url, "/"); + const navHome = site.nav.find((n) => n.label === "Home"); + assert.ok(navHome, "Nav Home link exists"); + assert.equal(navHome.url, "/"); const navGit = site.nav.find((n) => n.label === "Gitea"); assert.ok(navGit, "Nav Gitea link exists");