From 0789349f660a07e5ce88b4c511248b4e561f11fa Mon Sep 17 00:00:00 2001 From: Morris Brodersen Date: Sun, 19 Nov 2023 14:48:31 +0100 Subject: [PATCH] add local development server --- README.md | 108 ++++++++---- dev/client.js | 30 ++++ dev/server.mjs | 153 +++++++++++++++++ package-lock.json | 421 +++------------------------------------------- package.json | 7 +- 5 files changed, 286 insertions(+), 433 deletions(-) create mode 100644 dev/client.js create mode 100644 dev/server.mjs diff --git a/README.md b/README.md index 57af5c9..cff473a 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ _Intermediate understanding of the web platform is required to follow through._ - [8. Appendix](#8-appendix) - [8.1. Links](#81-links) - [8.2. Response](#82-response) + - [8.3. Local Development Server](#83-local-development-server) - [9. Changelog](#9-changelog) ## 1. Motivation @@ -225,45 +226,45 @@ provide behavior and rendering for the target element. Here's a "Hello, World!" example of mount functions: ```js -// define mount function -// loosely mapped to ".hello-world" +// Define mount function +// Loosely mapped to ".hello-world" export function HelloWorld(el) { - // define initial state + // Define initial state const state = { title: 'Hello, World!', description: 'An example vanilla component', counter: 0, }; - // set rigid base HTML + // Set rigid base HTML el.innerHTML = `

`; - // mount sub-components + // Mount sub-components el.querySelectorAll('.my-counter').forEach(MyCounter); - // attach event listeners + // Attach event listeners el.addEventListener('modifyCounter', (e) => update({ counter: state.counter + e.detail }), ); - // initial update + // Initial update update(); - // define idempotent update function + // Define idempotent update function function update(next) { - // update state - // optionally optimize, e.g. bail out if state hasn't changed + // Update state + // Optionally optimize, e.g. bail out if state hasn't changed Object.assign(state, next); - // update own HTML + // Update own HTML el.querySelector('.title').innerText = state.title; el.querySelector('.description').innerText = state.description; - // pass data to sub-scomponents + // Pass data to sub-scomponents el.querySelector('.my-counter').dispatchEvent( new CustomEvent('updateMyCounter', { detail: { value: state.counter }, @@ -272,15 +273,15 @@ export function HelloWorld(el) { } } -// define another component -// loosely mapped to ".my-counter" +// Define another component +// Loosely mapped to ".my-counter" export function MyCounter(el) { - // define initial state + // Define initial state const state = { value: 0, }; - // set rigid base HTML + // Set rigid base HTML el.innerHTML = `

@@ -289,10 +290,10 @@ export function MyCounter(el) {

`; - // attach event listeners + // Attach event listeners el.querySelector('.increment').addEventListener('click', () => { - // dispatch an action - // use .detail to transport data + // Dispatch an action + // Use .detail to transport data el.dispatchEvent( new CustomEvent('modifyCounter', { detail: 1, @@ -302,8 +303,8 @@ export function MyCounter(el) { }); el.querySelector('.decrement').addEventListener('click', () => { - // dispatch an action - // use .detail to transport data + // Dispatch an action + // Use .detail to transport data el.dispatchEvent( new CustomEvent('modifyCounter', { detail: -1, @@ -314,7 +315,7 @@ export function MyCounter(el) { el.addEventListener('updateMyCounter', (e) => update(e.detail)); - // define idempotent update function + // Define idempotent update function function update(next) { Object.assign(state, next); @@ -322,8 +323,8 @@ export function MyCounter(el) { } } -// mount HelloWorld component(s) -// any
in the document will be mounted +// Mount HelloWorld component(s) +// Any
in the document will be mounted document.querySelectorAll('.hello-world').forEach(HelloWorld); ``` @@ -427,37 +428,37 @@ export function TodoList(el) { const container = el.querySelector('.items'); - // mark current children for removal + // Mark current children for removal const obsolete = new Set(container.children); - // map current children by data-key + // Map current children by data-key const childrenByKey = new Map(); obsolete.forEach((child) => childrenByKey.set(child.getAttribute('data-key'), child), ); - // build new list of child elements from data + // Build new list of child elements from data const children = state.items.map((item) => { - // find existing child by data-key + // Find existing child by data-key let child = childrenByKey.get(item.id); if (child) { - // if child exists, keep it + // If child exists, keep it obsolete.delete(child); } else { - // otherwise, create new child + // Otherwise, create new child child = document.createElement('div'); child.classList.add('todo-item'); - // set data-key + // Set data-key child.setAttribute('data-key', item.id); - // mount component + // Mount component TodoItem(child); } - // update child + // Update child child.dispatchEvent( new CustomEvent('updateTodoItem', { detail: { item: item } }), ); @@ -465,10 +466,10 @@ export function TodoList(el) { return child; }); - // remove obsolete children + // Remove obsolete children obsolete.forEach((child) => container.removeChild(child)); - // (re-)insert new list of children + // (Re-)insert new list of children children.forEach((child, index) => { if (child !== container.children[index]) { container.insertBefore(child, container.children[index]); @@ -478,7 +479,7 @@ export function TodoList(el) { } ``` -It's very verbose and has lots of opportunity to introduce bugs. +It's very verbose, with lots of opportunity to introduce bugs. Compared to a simple loop in JSX, this seems insane. It is quite performant as it does minimal work but is otherwise messy; definitely a candidate for a utility function or library. @@ -652,7 +653,7 @@ I suspect a fully equivalent clone to be well below 10000 LOC, though._ would justify a helper. - Listening to and dispatching events is slightly verbose. - Although not used in this study, - event delegation is not trivial to implement without code duplication. + event delegation seems not trivial to implement without code duplication. Eliminating verbosities through build steps and a minimal set of helpers would reduce the comparably low code size (see above) even further. @@ -821,10 +822,43 @@ Projects I've inspected for drag & drop architecture: Thanks! +#### 8.3. Local Development Server + +_The local development server was added in 2023 and was not used during the initial study in 2020._ + +One thing I came to cherish in my professional work is +_hot reloading_ when changing source files. +Hot reloading provides fast feedback during development, +especially useful when fine-tuning visuals. + +I've implemented a minimal local development server (~200 LOC) with support for hot reloading: + +- Changes to stylesheets or images will hot replace the changed resources. +- Other changes (e.g. JavaScript or HTML) will cause a full page reload. + +While it's not proper [hot module replacement](https://webpack.js.org/concepts/hot-module-replacement/) +(which requires immense infrastructure), +it required zero changes to the application source +and provides a similar experience +(in particular because page reloads are fast). + +You can try it out by + +- installing Node.js (>= 20), +- checking out the repository, +- running `npm install`, +- and running `npm run dev`. + +Note that the local development server is highly experimental and is likely lacking +some features to be generally usable. See [/dev](./dev) for the implementation. +Feedback is highly appreciated. + ## 9. Changelog ### 11/2023 +- Add development server with hot reloading +- Fix some visual issues - Update dependencies ### 05/2023 diff --git a/dev/client.js b/dev/client.js new file mode 100644 index 0000000..7ed4e4e --- /dev/null +++ b/dev/client.js @@ -0,0 +1,30 @@ +const socket = new WebSocket( + `${(location.protocol === 'http:' ? 'ws://' : 'wss://') + location.host}/`, +); + +socket.addEventListener('message', (message) => { + if (!message.data) return; + + const data = JSON.parse(message.data); + + let reload = true; + + // hot reload stylesheets + document.querySelectorAll('link[rel=stylesheet]').forEach((el) => { + if (el.getAttribute('href') === data.url) { + el.setAttribute('href', data.url); + reload = false; + } + }); + + // hot reload images + document.querySelectorAll('img').forEach((el) => { + if (el.getAttribute('src') === data.url) { + el.setAttribute('src', data.url); + reload = false; + } + }); + + // otherwise, reload page + if (reload) location.reload(); +}); diff --git a/dev/server.mjs b/dev/server.mjs new file mode 100644 index 0000000..3e8a427 --- /dev/null +++ b/dev/server.mjs @@ -0,0 +1,153 @@ +/* eslint-env node */ +/* eslint-disable no-console */ +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as http from 'http'; +import mime from 'mime'; +import * as path from 'path'; +import WebSocket, { WebSocketServer } from 'ws'; + +// Options + +const argv = process.argv.slice(2); +const webroot = path.resolve(argv.shift()); +const port = parseInt(process.env.PORT, 10) || 8080; + +// File transforms + +const clientJS = fs.readFileSync( + import.meta.resolve('./client.js').replace(/^file:\/\//, ''), +); + +function transformFileContents(file, contents) { + const ext = path.extname(file); + + if (ext === '.html') { + return contents + .toString() + .replace( + '', + ` `, + ); + } + + return contents; +} + +// Static file resolution + +const fileCache = new Map(); + +async function readFileCached(file) { + const cached = fileCache.get(file); + if (cached) return cached; + + const promise = readFile(file); + fileCache.set(file, promise); + + return promise; +} + +async function readFile(file) { + const stat = await fs.promises.lstat(file); + + if (stat.isDirectory()) { + file = path.join(file, 'index.html'); + } + + const contents = transformFileContents( + file, + await fs.promises.readFile(file), + ); + const contentType = mime.getType(file) ?? 'application/octet-stream'; + const version = crypto.createHash('sha1').update(contents).digest('base64'); + + return { contents, contentType, version }; +} + +function invalidateFile(file) { + fileCache.delete(file); +} + +// HTTP server + +const server = http.createServer(async (req, res) => { + if (req.method !== 'GET' && req.method !== 'HEAD') { + res.setHeader('content-type', 'text/plain'); + res.writeHead(405); + res.end('405 Method not allowed'); + + return; + } + + const url = new URL(req.url, `http://localhost:${port}`); + + try { + let file = path.join(webroot, path.resolve('.', url.pathname)); + let { contents, contentType, version } = await readFileCached(file); + + if (req.headers['if-none-match'] === version) { + res.writeHead(304); + res.end(); + + return; + } + + res.setHeader('content-type', contentType); + res.setHeader('etag', version); + res.writeHead(200); + + if (req.method === 'HEAD') { + res.end(); + } else { + res.end(contents); + } + } catch (err) { + if (err.code === 'ENOENT') { + res.setHeader('content-type', 'text/plain'); + res.writeHead(404); + res.end('404 Not found'); + } else { + console.error(err); + + res.setHeader('content-type', 'text/plain'); + res.writeHead(500); + res.end(`500 Internal server error: ${err.message}`); + } + } +}); + +server.on('listening', () => { + console.log(`Serving ${webroot} on port ${server.address().port}`); +}); + +server.listen(port); + +// WebSocket server + +const wsClients = new Set(); +const wsServer = new WebSocketServer({ server }); + +wsServer.on('connection', (client) => { + wsClients.add(client); +}); + +function broadcast(message) { + for (const wsClient of wsClients) { + if (wsClient.readyState === WebSocket.OPEN) { + wsClient.send(JSON.stringify(message)); + } else { + wsClients.delete(wsClient); + wsClient.terminate(); + } + } +} + +// File watcher + +const fileWatcher = fs.watch(webroot, { recursive: true }); + +fileWatcher.on('change', (_, filename) => { + invalidateFile(path.join(webroot, filename)); + broadcast({ type: 'modified', url: filename }); +}); diff --git a/package-lock.json b/package-lock.json index f75656e..8d23834 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,12 @@ "@playwright/test": "^1.33.0", "eslint": "^8.20.0", "eslint-plugin-compat": "^4.0.2", - "http-server": "^14.1.1", + "mime": "^3.0.0", "prettier": "^3.1.0", "stylelint": "^15.6.1", "stylelint-config-standard": "^34.0.0", - "stylelint-rscss": "^0.4.0" + "stylelint-rscss": "^0.4.0", + "ws": "^8.14.2" }, "engines": { "node": ">=20" @@ -561,33 +562,12 @@ "node": ">=8" } }, - "node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "dependencies": { - "lodash": "^4.17.14" - } - }, "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 }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dev": true, - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -642,20 +622,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -773,15 +739,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/corser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", - "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -925,20 +882,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1168,12 +1111,6 @@ "node": ">=0.10.0" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1305,26 +1242,6 @@ "deprecated": "flatten is deprecated in favor of utility frameworks such as lodash.", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1354,21 +1271,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1480,18 +1382,6 @@ "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", "dev": true }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -1516,42 +1406,6 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -1564,15 +1418,6 @@ "node": ">= 0.4" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -1585,18 +1430,6 @@ "node": ">=10" } }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "dev": true, - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/html-tags": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", @@ -1609,59 +1442,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-server": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", - "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", - "dev": true, - "dependencies": { - "basic-auth": "^2.0.1", - "chalk": "^4.1.2", - "corser": "^2.0.1", - "he": "^1.2.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy": "^1.18.1", - "mime": "^1.6.0", - "minimist": "^1.2.6", - "opener": "^1.5.1", - "portfinder": "^1.0.28", - "secure-compare": "3.0.1", - "union": "~0.5.0", - "url-join": "^4.0.1" - }, - "bin": { - "http-server": "bin/http-server" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -1935,12 +1715,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -2060,15 +1834,15 @@ } }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "dev": true, "bin": { "mime": "cli.js" }, "engines": { - "node": ">=4" + "node": ">=10.0.0" } }, "node_modules/min-indent": { @@ -2092,15 +1866,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minimist-options": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", @@ -2115,18 +1880,6 @@ "node": ">= 6" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2187,15 +1940,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2205,15 +1949,6 @@ "wrappy": "1" } }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "dev": true, - "bin": { - "opener": "bin/opener-bin.js" - } - }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -2375,29 +2110,6 @@ "node": ">=16" } }, - "node_modules/portfinder": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", - "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", - "dev": true, - "dependencies": { - "async": "^2.6.4", - "debug": "^3.2.7", - "mkdirp": "^0.5.6" - }, - "engines": { - "node": ">= 0.12.0" - } - }, - "node_modules/portfinder/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -2500,21 +2212,6 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2631,12 +2328,6 @@ "node": ">=0.10.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2694,24 +2385,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/secure-compare": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", - "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", - "dev": true - }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -2727,21 +2400,6 @@ "node": ">=10" } }, - "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2763,20 +2421,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -3171,18 +2815,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/union": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", - "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", - "dev": true, - "dependencies": { - "qs": "^6.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/uniq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", @@ -3228,12 +2860,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "dev": true - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -3250,18 +2876,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3296,6 +2910,27 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 95ec9c3..2a6565d 100644 --- a/package.json +++ b/package.json @@ -26,17 +26,18 @@ "format-check": "prettier --check .", "lint": "eslint public", "lint-styles": "stylelint public/styles/*", - "serve": "http-server -c-1 public", + "dev": "node ./dev/server.mjs public", "test": "playwright test" }, "devDependencies": { "@playwright/test": "^1.33.0", "eslint": "^8.20.0", "eslint-plugin-compat": "^4.0.2", - "http-server": "^14.1.1", + "mime": "^3.0.0", "prettier": "^3.1.0", "stylelint": "^15.6.1", "stylelint-config-standard": "^34.0.0", - "stylelint-rscss": "^0.4.0" + "stylelint-rscss": "^0.4.0", + "ws": "^8.14.2" } }