1
0
mirror of https://github.com/morris/vanilla-todo.git synced 2025-08-30 09:10:14 +02:00

add local development server

This commit is contained in:
Morris Brodersen
2023-11-19 14:48:31 +01:00
parent b279225a3a
commit 0789349f66
5 changed files with 286 additions and 433 deletions

108
README.md
View File

@@ -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 = `
<h1 class="title"></h1>
<p class="description"></p>
<div class="my-counter"></div>
`;
// 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 = `
<p>
<span class="value"></span>
@@ -289,10 +290,10 @@ export function MyCounter(el) {
</p>
`;
// 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 <div class="hello-world"></div> in the document will be mounted
// Mount HelloWorld component(s)
// Any <div class="hello-world"></div> 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

30
dev/client.js Normal file
View File

@@ -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();
});

153
dev/server.mjs Normal file
View File

@@ -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(
'</title>',
`</title> <script type="module">${clientJS}</script>`,
);
}
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 });
});

421
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}