Finish refactoring
authorBenjamin Auder <benjamin.auder@somewhere>
Mon, 4 May 2026 21:29:43 +0000 (23:29 +0200)
committerBenjamin Auder <benjamin.auder@somewhere>
Mon, 4 May 2026 21:29:43 +0000 (23:29 +0200)
js/app.js
js/server.js
package-lock.json
package.json

index 4a92707..3e3b7bd 100644 (file)
--- a/js/app.js
+++ b/js/app.js
@@ -121,6 +121,15 @@ function cancelRematch() {
     toggleVisible("newGame");
 }
 
+let lastAliases = [];
+function replaceAliases() {
+  for (const k of lastAliases)
+    delete window[k];
+  for (const [k, v] of Object.entries(V.Aliases))
+    window[k] = v;
+  lastAliases = Object.keys(V.Aliases);
+}
+
 // Play with a friend (or not ^^)
 function showNewGameForm() {
   const vname = $.getElementById("selectVariant").value;
@@ -132,13 +141,12 @@ function showNewGameForm() {
     toggleVisible("newGameForm");
     import(`/variants/${vname}/class.js`).then(module => {
       window.V = module.default;
-      for (const [k, v] of Object.entries(V.Aliases))
-        window[k] = v;
+      replaceAliases();
       prepareOptions();
     });
   }
 }
-function backToNormalSeek() { //TODO: index.html......
+function backToNormalSeek() {
   toggleVisible("newGame");
 }
 
@@ -218,65 +226,6 @@ function prepareOptions() {
   }
 }
 
-function prepareOptions() {
-  options = {};
-  let optHtml = "";
-  if (V.Options.select) {
-    optHtml += V.Options.select.map(select => { return `
-      <div class="option-select">
-        <label for="var_${select.variable}">${select.label}</label>
-        <div class="select">
-          <select id="var_${select.variable}">` +
-          select.options.map(option => { return `
-            <option
-              value="${option.value}"
-              ${option.value == select.defaut ? " selected" : ""}
-            >
-              ${option.label}
-            </option>`;
-          }).join("") + `
-          </select>
-          <span class="focus"></span>
-        </div>
-      </div>`;
-    }).join("");
-  }
-  if (V.Options.input) {
-    optHtml += V.Options.input.map(input => { return `
-      <div class="option-input">
-        <label class="input">
-          <input id="var_${input.variable}"
-                 type="${input.type}"
-                 ${input.type == "checkbox" && input.defaut
-                   ? "checked"
-                   : 'value="' + input.defaut + '"'}
-          />
-          <span class="spacer"></span>
-          <span>${input.label}</span>
-        </label>
-      </div>`;
-    }).join("");
-  }
-  if (V.Options.styles) {
-    optHtml += '<div class="words">';
-    let i = 0;
-    const stylesLength = V.Options.styles.length;
-    while (i < stylesLength) {
-      optHtml += '<div class="row">';
-      for (let j=i; j<i+4; j++) {
-        if (j == stylesLength)
-          break;
-        const style = V.Options.styles[j];
-        optHtml += `<span onClick="toggleStyle(event, this)">${style}</span>`;
-      }
-      optHtml += "</div>";
-      i += 4;
-    }
-    optHtml += "</div>";
-  }
-  $.getElementById("gameOptions").innerHTML = optHtml;
-}
-
 function getGameLink() {
   const vname = $.getElementById("selectVariant").value;
   const color = $.getElementById("selectColor").value;
@@ -300,17 +249,14 @@ function getGameLink() {
   });
 }
 
-
-
-
 function fillGameInfos(gameInfos, oppIndex) {
   fetch(`/variants/${gameInfos.vname}/rules.html`)
     .then(res => res.text())
     .then(txt => {
       const container = $.getElementById("gameInfos");
-      container.innerHTML = ""; // Nettoyage initial
+      container.innerHTML = ""; //initial cleaning
 
-      // 1. Infos Joueurs
+      // 1. Players infos
       const playerDiv = h('div', { class: 'players-info' }, [
         h('p', null, [
           h('span', { class: 'bold', textContent: gameInfos.vdisp }),
@@ -318,9 +264,10 @@ function fillGameInfos(gameInfos, oppIndex) {
         ])
       ]);
 
-      // 2. Traitement des Options (Filtrage + Groupement par 4)
+      // 2. Options treatment (Filtering + Group by 4)
       const optionsInfos = h('div', { class: 'options-info' });
-      const activeOptions = Object.entries(gameInfos.options).filter(opt => !!opt[1]);
+      const activeOptions =
+        Object.entries(gameInfos.options).filter(opt => !!opt[1]);
       
       let i = 0;
       while (i < activeOptions.length) {
@@ -334,72 +281,28 @@ function fillGameInfos(gameInfos, oppIndex) {
         i += 4;
       }
 
-      // 3. Règles (on garde innerHTML ici car le HTML vient de ton fichier local rules.html)
+      // 3. Rules (keeping innerHTML here because trusted from file rules.html)
       const rulesDiv = h('div', { class: 'rules' });
       rulesDiv.innerHTML = txt;
 
-      // 4. Bouton de retour
+      // 4. Game infos button
       const btnWrap = h('div', { class: 'btn-wrap' }, [
-        h('button', { 
-          onclick: toggleGameInfos, 
-          textContent: "Back to game" 
+        h('button', {
+          onclick: toggleGameInfos,
+          textContent: "Back to game"
         })
       ]);
 
-      // Assemblage final
+      // Final assembling
       container.append(
-        playerDiv, 
-        activeOptions.length > 0 ? optionsInfos : null, 
-        rulesDiv, 
+        playerDiv,
+        activeOptions.length > 0 ? optionsInfos : null,
+        rulesDiv,
         btnWrap
       );
     });
 }
 
-
-
-function fillGameInfos(gameInfos, oppIndex) {
-  fetch(`/variants/${gameInfos.vname}/rules.html`)
-  .then(res => res.text())
-  .then(txt => {
-    let htmlContent = `
-      <div class="players-info">
-        <p>
-          <span class="bold">${gameInfos.vdisp}</span>
-          <span>vs. ${gameInfos.players[oppIndex].name}</span>
-        </p>
-      </div>`;
-    const _options = Object.entries(gameInfos.options);
-    if (_options.length > 0) {
-      htmlContent += '<div class="options-info">';
-      let i = 0;
-      while (i < _options.length) {
-        htmlContent += '<div class="row">';
-        for (let j=i; j<i+4; j++) {
-          if (j == _options.length)
-            break;
-          const opt = _options[j];
-          if (!opt[1]) //includes 0 and false (lighter display)
-            continue;
-          htmlContent +=
-            '<span class="option">' +
-            (opt[1] === true ? opt[0] : `${opt[0]}:${opt[1]}`) + " " +
-            "</span>";
-        }
-        htmlContent += "</div>";
-        i += 4;
-      }
-      htmlContent += "</div>";
-    }
-    htmlContent += `
-      <div class="rules">${txt}</div>
-      <div class="btn-wrap">
-        <button onClick="toggleGameInfos()">Back to game</button>
-      </div>`;
-    $.getElementById("gameInfos").innerHTML = htmlContent;
-  });
-}
-
 ////////////////
 // Communication
 
@@ -491,14 +394,20 @@ const messageCenter = (msg) => {
     // Game vs. friend just created on server: share link now
     case "gamecreated": {
       const link = `${Params.http_server}/#${obj.gid}`;
-      $.getElementById("gameLink").innerHTML = `
-        <p>
-          <a href="${getWhatsApp(link)}">WhatsApp</a>
-          /
-          <span onClick="copyClipboard('${link}')">ToClipboard</span>
-        </p>
-        <p>${link}</p>
-      `;
+      const container = $.getElementById("gameLink");
+      container.innerHTML = ""; //emptying
+      container.append(
+        h('p', null, [
+          h('a', { href: getWhatsApp(link), textContent: "WhatsApp" }),
+          " / ",
+          h('span', {
+            onclick: () => copyClipboard(link),
+            textContent: "ToClipboard",
+            //style: "cursor:pointer; text-decoration:underline"
+          })
+        ]),
+        h('p', { textContent: link })
+      );
       break;
     }
     // Game vs. friend joined after 1 minute (try again!)
@@ -641,100 +550,86 @@ const afterPlay = (move_s, newTurn, ops) => {
   }
 };
 
-
-
-
-
-
-
+let vr = null, playerColor, lastVname = undefined;
 function initializeGame(obj) {
-  const container = $.getElementById("boardContainer");
-  container.innerHTML = ""; // Nettoyage
+  const options = obj.options || {};
 
-  // Créer les boutons de contrôle proprement
-  const infoBtn = createSVGButton("upLeftInfos", toggleGameInfos);
-  const stopBtn = createSVGButton("upRightStop", confirmStopGame);
-  const board = $.createElement("div");
-  board.className = "chessboard";
+  // 1. Dynamic loading of variant js module
+  import(`/variants/${obj.vname}/class.js`).then(module => {
+    window.V = module.default;
 
-  container.append(infoBtn, stopBtn, board);
+    // Export aliases in global scope (used by variants classes)
+    replaceAliases();
 
+    // 2. Dynamic management of CSS (Unload old / Load new)
+    if (lastVname !== obj.vname) {
+      if (lastVname) {
+        const oldCss = $.getElementById(`${lastVname}_css`);
+        if (oldCss)
+          oldCss.remove();
+      }
+      $.head.append(
+        h('link', {
+          id: `${obj.vname}_css`,
+          rel: 'stylesheet',
+          href: `/variants/${obj.vname}/style.css`
+        })
+      );
+      lastVname = obj.vname;
+    }
 
+    playerColor = (sid == obj.players[0].sid ? 'w' : 'b');
 
+    // 3. Building Board Container
+    const container = $.getElementById("boardContainer");
+    container.innerHTML = ""; // On vide proprement l'ancien plateau
 
+    // Create SVG icons with a string, inserted securely.
+    const infoIcon = h('div', { id: 'upLeftInfos', onclick: toggleGameInfos });
+    infoIcon.innerHTML = `<svg viewBox="0.5 0.5 100 100"><path d="M50.5,0.5c-27.614,0-50,22.386-50,50c0,27.614,22.386,50,50,50s50-22.386,50-50C100.5,22.886,78.114,0.5,50.5,0.5z M60.5,85.5h-20v-40h20V85.5z M50.5,35.5c-5.523,0-10-4.477-10-10s4.477-10,10-10c5.522,0,10,4.477,10,10S56.022,35.5,50.5,35.5z"/></svg>`;
 
+    const stopIcon = h('div', { id: 'upRightStop', onclick: confirmStopGame });
+    stopIcon.innerHTML = `<svg viewBox="0 0 533.333 533.333"><path d="M528.468,428.468c-0.002-0.002-0.004-0.004-0.006-0.005L366.667,266.666l161.795-161.797c0.002-0.002,0.004-0.003,0.006-0.005c1.741-1.742,3.001-3.778,3.809-5.946c2.211-5.925,0.95-12.855-3.814-17.62l-76.431-76.43 c-4.765-4.763-11.694-6.024-17.619-3.812c-2.167,0.807-4.203,2.066-5.946,3.807L266.667,166.666 L104.87,4.869c-5.945-3.807-92.993-1.156-81.3,4.869 L4.869,81.3c-4.764,4.765-6.024,11.694-3.813,17.619l161.797,161.796L4.869,428.464c3.813,17.619,81.3,528.464,98.92,532.277c161.796-161.797l161.795,161.797c5.927,2.212,17.619-3.813l76.43-76.432c3.815-17.62 C531.469,432.246,528.468,428.468z"/></svg>`;
 
+    const board = h('div', { class: 'chessboard' });
 
-let vr = null, playerColor, lastVname = undefined;
-function initializeGame(obj) {
-  const options = obj.options || {};
-  import(`/variants/${obj.vname}/class.js`).then(module => {
-    window.V = module.default;
-    for (const [k, v] of Object.entries(V.Aliases))
-      window[k] = v;
-    if (lastVname != obj.vname) {
-      // Load CSS + unload potential previous one.
-      if (lastVname)
-        document.getElementById(lastVname + "_css").remove();
-      $.getElementsByTagName("head")[0].insertAdjacentHTML(
-        "beforeend",
-        `<link id="${obj.vname + '_css'}" rel="stylesheet"
-               href="/variants/${obj.vname}/style.css"/>`);
-      lastVname = obj.vname;
-    }
-    playerColor = (sid == obj.players[0].sid ? "w" : "b");
-    // Init + remove potential extra DOM elements from a previous game:
-    document.getElementById("boardContainer").innerHTML = `
-      <div id="upLeftInfos"
-           onClick="toggleGameInfos()">
-        <svg version="1.1"
-             viewBox="0.5 0.5 100 100">
-          <g>
-            <path d="M50.5,0.5c-27.614,0-50,22.386-50,50c0,27.614,22.386,50,50,50s50-22.386,50-50C100.5,22.886,78.114,0.5,50.5,0.5z M60.5,85.5h-20v-40h20V85.5z M50.5,35.5c-5.523,0-10-4.477-10-10s4.477-10,10-10c5.522,0,10,4.477,10,10S56.022,35.5,50.5,35.5z"/>
-          </g>
-        </svg>
-      </div>
-      <div id="upRightStop"
-           onClick="confirmStopGame()">
-        <svg version="1.1"
-             viewBox="0 0 533.333 533.333">
-          <g>
-            <path d="M528.468,428.468c-0.002-0.002-0.004-0.004-0.006-0.005L366.667,266.666l161.795-161.797 c0.002-0.002,0.004-0.003,0.006-0.005c1.741-1.742,3.001-3.778,3.809-5.946c2.211-5.925,0.95-12.855-3.814-17.62l-76.431-76.43 c-4.765-4.763-11.694-6.024-17.619-3.812c-2.167,0.807-4.203,2.066-5.946,3.807c0,0.002-0.002,0.003-0.005,0.005L266.667,166.666 L104.87,4.869c-0.002-0.002-0.003-0.003-0.005-0.005c-1.743-1.74-3.778-3-5.945-3.807C92.993-1.156,86.065,0.105,81.3,4.869 L4.869,81.3c-4.764,4.765-6.024,11.694-3.813,17.619c0.808,2.167,2.067,4.205,3.808,5.946c0.002,0.001,0.003,0.003,0.005,0.005 l161.797,161.796L4.869,428.464c-0.001,0.002-0.003,0.003-0.004,0.005c-1.741,1.742-3,3.778-3.809,5.945 c-2.212,5.924-0.951,12.854,3.813,17.619L81.3,528.464c4.766,4.765,11.694,6.025,17.62,3.813c2.167-0.809,4.203-2.068,5.946-3.809 c0.001-0.002,0.003-0.003,0.005-0.005l161.796-161.797l161.795,161.797c0.003,0.001,0.005,0.003,0.007,0.004 c1.743,1.741,3.778,3.001,5.944,3.81c5.927,2.212,12.856,0.951,17.619-3.813l76.43-76.432c4.766-4.765,6.026-11.696,3.815-17.62 C531.469,432.246,530.209,430.21,528.468,428.468z"/>
-          </g>
-        </svg>
-      </div>
-      <div class="chessboard"></div>`;
+    container.append(infoIcon, stopIcon, board);
+
+    // 4. Initialize game engine (vr)
     if (vr)
-      // Avoid interferences:
       vr.removeListeners();
+
     vr = new V({
-      seed: obj.seed, //may be null if FEN already exists (running game)
+      seed: obj.seed,
       fen: obj.fen,
       element: "boardContainer",
       color: playerColor,
       afterPlay: afterPlay,
       options: options
     });
+
+    // 5. Handling game state
     const gameCreation = !obj.fen;
     if (gameCreation) {
-      // Both players set FEN, in case of one is offline
-      send("setfen", {gid: obj.gid, fen: vr.getFen()});
+      send("setfen", { gid: obj.gid, fen: vr.getFen() });
       localStorage.setItem("gid", obj.gid);
     }
-    const select = $.getElementById("selectVariant");
-    obj.vdisp = "";
-    for (let i=0; i<select.options.length; i++) {
-      if (select.options[i].value == obj.vname) {
-        obj.vdisp = select.options[i].text;
-        break;
-      }
-    }
+
+    // 6. Update variant's informations (vdisp)
+    const variantOption = Array.from($.getElementById("selectVariant").options)
+                               .find(opt => opt.value === obj.vname);
+    obj.vdisp = variantOption ? variantOption.text : obj.vname;
+
     const playerIndex = (playerColor == "w" ? 0 : 1);
     fillGameInfos(obj, 1 - playerIndex);
-    if (obj.players[playerIndex].randvar && gameCreation)
+
+    // 7. Final output
+    if (obj.players[playerIndex].randvar && gameCreation) {
       toggleVisible("gameInfos");
+    }
     else
       toggleVisible("boardContainer");
+
     toggleTurnIndicator(vr.turn == playerColor);
   });
 }
index 0199fdb..3dd6b4a 100644 (file)
@@ -1,5 +1,5 @@
-const params = require("./js/parameters.js");
-const sanitize = require("./js/sanitize.js");
+const params = require("./parameters.js");
+const sanitize = require("./sanitize.js");
 const WebSocket = require("ws");
 const wss = new WebSocket.Server({
   port: params.socket_port,
index 05d4d29..b657351 100644 (file)
@@ -9,7 +9,8 @@
         "ws": "^7.5.3"
       },
       "devDependencies": {
-        "chokidar": "^3.5.3"
+        "chokidar": "^3.5.3",
+        "nodemon": "^3.1.14"
       }
     },
     "node_modules/anymatch": {
         "node": ">= 8"
       }
     },
+    "node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
     "node_modules/binary-extensions": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
         "node": ">=8"
       }
     },
+    "node_modules/brace-expansion": {
+      "version": "5.0.5",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+      "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
     "node_modules/braces": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
         "fsevents": "~2.3.2"
       }
     },
+    "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"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/fill-range": {
       "version": "7.1.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
         "node": ">= 6"
       }
     },
+    "node_modules/has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/ignore-by-default": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+      "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/is-binary-path": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
         "node": ">=0.12.0"
       }
     },
+    "node_modules/minimatch": {
+      "version": "10.2.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+      "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "brace-expansion": "^5.0.5"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "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/nodemon": {
+      "version": "3.1.14",
+      "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+      "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^3.5.2",
+        "debug": "^4",
+        "ignore-by-default": "^1.0.1",
+        "minimatch": "^10.2.1",
+        "pstree.remy": "^1.1.8",
+        "semver": "^7.5.3",
+        "simple-update-notifier": "^2.0.0",
+        "supports-color": "^5.5.0",
+        "touch": "^3.1.0",
+        "undefsafe": "^2.0.5"
+      },
+      "bin": {
+        "nodemon": "bin/nodemon.js"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/nodemon"
+      }
+    },
     "node_modules/normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
       }
     },
     "node_modules/picomatch": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "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"
       },
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/pstree.remy": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+      "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/readdirp": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
         "node": ">=8.10.0"
       }
     },
+    "node_modules/semver": {
+      "version": "7.7.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+      "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/simple-update-notifier": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+      "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "semver": "^7.5.3"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
         "node": ">=8.0"
       }
     },
+    "node_modules/touch": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+      "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "nodetouch": "bin/nodetouch.js"
+      }
+    },
+    "node_modules/undefsafe": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+      "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/ws": {
       "version": "7.5.10",
       "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
         "picomatch": "^2.0.4"
       }
     },
+    "balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "dev": true
+    },
     "binary-extensions": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
       "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
       "dev": true
     },
+    "brace-expansion": {
+      "version": "5.0.5",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+      "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^4.0.2"
+      }
+    },
     "braces": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
         "readdirp": "~3.6.0"
       }
     },
+    "debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "requires": {
+        "ms": "^2.1.3"
+      }
+    },
     "fill-range": {
       "version": "7.1.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
         "is-glob": "^4.0.1"
       }
     },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+      "dev": true
+    },
+    "ignore-by-default": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+      "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+      "dev": true
+    },
     "is-binary-path": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
       "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
       "dev": true
     },
+    "minimatch": {
+      "version": "10.2.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+      "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "^5.0.5"
+      }
+    },
+    "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
+    },
+    "nodemon": {
+      "version": "3.1.14",
+      "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+      "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+      "dev": true,
+      "requires": {
+        "chokidar": "^3.5.2",
+        "debug": "^4",
+        "ignore-by-default": "^1.0.1",
+        "minimatch": "^10.2.1",
+        "pstree.remy": "^1.1.8",
+        "semver": "^7.5.3",
+        "simple-update-notifier": "^2.0.0",
+        "supports-color": "^5.5.0",
+        "touch": "^3.1.0",
+        "undefsafe": "^2.0.5"
+      }
+    },
     "normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
       "dev": true
     },
     "picomatch": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "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
+    },
+    "pstree.remy": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+      "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
       "dev": true
     },
     "readdirp": {
         "picomatch": "^2.2.1"
       }
     },
+    "semver": {
+      "version": "7.7.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+      "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+      "dev": true
+    },
+    "simple-update-notifier": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+      "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+      "dev": true,
+      "requires": {
+        "semver": "^7.5.3"
+      }
+    },
+    "supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dev": true,
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
     "to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
         "is-number": "^7.0.0"
       }
     },
+    "touch": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+      "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+      "dev": true
+    },
+    "undefsafe": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+      "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+      "dev": true
+    },
     "ws": {
       "version": "7.5.10",
       "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
index f76813d..acf1faa 100644 (file)
@@ -1,5 +1,5 @@
 {
-  "main": "server.js",
+  "main": "js/server.js",
   "dependencies": {
     "ws": "^7.5.3"
   },
@@ -8,6 +8,7 @@
     "stop": "./stop.sh"
   },
   "devDependencies": {
-    "chokidar": "^3.5.3"
+    "chokidar": "^3.5.3",
+    "nodemon": "^3.1.14"
   }
 }