Fix bundle.py. TODO: clean code (sanitize.. ?)
authorBenjamin Auder <benjamin.auder@somewhere>
Tue, 5 May 2026 13:44:37 +0000 (15:44 +0200)
committerBenjamin Auder <benjamin.auder@somewhere>
Tue, 5 May 2026 13:44:37 +0000 (15:44 +0200)
bundle.py
js/app.js
js/sanitize.js
js/server.js
start.sh

index 0464fd9..1a3bdf5 100755 (executable)
--- a/bundle.py
+++ b/bundle.py
@@ -3,18 +3,22 @@
 import os
 import hashlib
 import shutil
+import json
+import re
 
 # --- Configuration ---
 SOURCE_DIR = "."
 DEST_DIR = "dist"
-EXTENSIONS_TO_HASH = [".js", ".css", ".svg"]
-EXTENSIONS_TO_UPDATE = [".html", ".js", ".css"]
+EXTENSIONS_TO_HASH = [".html", ".js", ".css", ".svg"] #all text files
+EXTENSIONS_TO_UPDATE = [".html", ".js", ".css"] #.svg don't contain refs
+DYNAMIC_LOAD_EXTENSIONS = (".html", ".js", ".css") #loaded from app.js
 
-# Fichiers et dossiers à ignorer totalement
+IGNORE_FILE_UPDATE = {"app.js"}
+# Files and folders to totally ignore
 IGNORE_FILES = {
-    "LICENSE", "README.md", "TODO", "bundle.py",
-    "initialize.sh", "package-lock.json", "package.json",
-    "start.sh", "stop.sh", ".gitignore"
+    "LICENSE", "README.md", "TODO", "bundle.py", "parameters.js.dist",
+    "initialize.sh", "package-lock.json", "package.json", "server.js",
+    "start.sh", "stop.sh", ".gitignore", "assets.zip", "extras.zip", ".pid"
 }
 IGNORE_DIRS = {".git", "node_modules", DEST_DIR}
 
@@ -26,30 +30,31 @@ def get_file_hash(filepath):
     return hasher.hexdigest()[:8]
 
 def run_bundle():
-    # Nettoyage propre du dossier de destination
+    # Clean destination folder
     if os.path.exists(DEST_DIR):
         shutil.rmtree(DEST_DIR)
     os.makedirs(DEST_DIR)
 
     hash_map = {}
 
-    # 1. Parcours et copie sélective
+    # 1. Walk the tree and do selective copies
     for root, dirs, files in os.walk(SOURCE_DIR):
-        # On filtre les dossiers à ignorer sur place pour que walk ne s'y aventure pas
+        # Filter folders to ignore so that walk() doesn't step in
         dirs[:] = [d for d in dirs if d not in IGNORE_DIRS]
-       
+
         for file in files:
             if file in IGNORE_FILES:
                 continue
-               
+            
             ext = os.path.splitext(file)[1]
             rel_path = os.path.relpath(os.path.join(root, file), SOURCE_DIR)
-           
-            # Déterminer le nom de destination (avec ou sans hash)
-            if ext in EXTENSIONS_TO_HASH:
+
+            # Determine dest name (with or without hash)
+            if ext in EXTENSIONS_TO_HASH and file != "index.html":
                 h = get_file_hash(os.path.join(root, file))
                 new_name = f"{os.path.splitext(file)[0]}.{h}{ext}"
-                hash_map[rel_path] = new_name
+                new_rel_path = os.path.relpath(os.path.join(root, new_name), SOURCE_DIR)
+                hash_map[rel_path] = new_rel_path
             else:
                 new_name = file
 
@@ -57,21 +62,19 @@ def run_bundle():
             os.makedirs(os.path.dirname(dest_path), exist_ok=True)
             shutil.copy2(os.path.join(root, file), dest_path)
 
-    # 2. Mise à jour des références
+    # 2. Update references
     for root, dirs, files in os.walk(DEST_DIR):
         for file in files:
-            if os.path.splitext(file)[1] in EXTENSIONS_TO_UPDATE:
+            print(file)
+            if os.path.splitext(file)[1] in EXTENSIONS_TO_UPDATE and not re.match(r'^app\.[^\.]+\.js$', file):
                 file_path = os.path.join(root, file)
                 with open(file_path, 'r', encoding='utf-8') as f:
                     content = f.read()
 
-                # On remplace les anciennes références par les nouvelles
-                # On trie par longueur décroissante pour éviter de remplacer "style.css" dans "mon-style.css"
-                for old_rel_path in sorted(hash_map.keys(), key=len, reverse=True):
-                    old_name = os.path.basename(old_rel_path)
-                    new_hashed_name = hash_map[old_rel_path]
-                    if old_name in content:
-                        content = content.replace(old_name, new_hashed_name)
+                # Replace old refs with new ones
+                for old_rel_path in hash_map.keys():
+                    if old_rel_path in content:
+                        content = content.replace(old_rel_path, hash_map[old_rel_path])
 
                 with open(file_path, 'w', encoding='utf-8') as f:
                     f.write(content)
@@ -79,5 +82,13 @@ def run_bundle():
     print(f"Build terminé dans /{DEST_DIR}")
     print(f"Fichiers hashés : {len(hash_map)}")
 
+    # 3. Write hash_map to manifest file:
+    hash_manifest = {
+        k: v for k, v in hash_map.items()
+        if k.startswith("variants/") and k.endswith(DYNAMIC_LOAD_EXTENSIONS)
+    }
+    with open('dist/manifest.json', 'w') as f:
+        json.dump(hash_manifest, f, indent=2)
+
 if __name__ == "__main__":
     run_bundle()
index 43cdbc6..7d30a32 100644 (file)
--- a/js/app.js
+++ b/js/app.js
@@ -47,6 +47,20 @@ setActive(true);
 inputName.onblur = () => setActive(false);
 inputName.onfocus = () => setActive(true);
 
+// TODO: a website update will break manifest() function (need page reload)
+let manifest_dict = {};
+async function manifest(path) {
+  if (!Params.dev) {
+    if (Object.keys(manifest_dict).length == 0) {
+      // Load map filename.js --> filename.hash.js
+      const res = await fetch('/manifest.json');
+      manifest_dict = await res.json();
+    }
+    return manifest_dict[path];
+  }
+  return path;
+}
+
 /////////
 // Utils
 
@@ -135,7 +149,7 @@ function replaceAliases() {
 }
 
 // Play with a friend (or not ^^)
-function showNewGameForm() {
+async function showNewGameForm() {
   const vname = $.getElementById("selectVariant").value;
   if (vname == "_random")
     alert("Select a variant first");
@@ -143,11 +157,11 @@ function showNewGameForm() {
     $.getElementById("gameLink").innerHTML = "";
     $.getElementById("selectColor").selectedIndex = 0;
     toggleVisible("newGameForm");
-    import(`/variants/${vname}/class.js`).then(module => {
-      window.V = module.default;
-      replaceAliases();
-      prepareOptions();
-    });
+    const module =
+      await import('/' + await manifest(`variants/${vname}/class.js`));
+    window.V = module.default;
+    replaceAliases();
+    prepareOptions();
   }
 }
 function backToNormalSeek() {
@@ -253,58 +267,58 @@ function getGameLink() {
   });
 }
 
-function fillGameInfos(gameInfos, oppIndex) {
-  fetch(`/variants/${gameInfos.vname}/rules.html`)
-    .then(res => res.text())
-    .then(txt => {
-      const container = $.getElementById("gameInfos");
-      container.innerHTML = ""; //initial cleaning
-
-      // 1. Players infos
-      const playerDiv = h('div', { class: 'players-info' }, [
-        h('p', null, [
-          h('span', { class: 'bold', textContent: gameInfos.vdisp }),
-          h('span', { textContent: ` vs. ${gameInfos.players[oppIndex].name}` })
-        ])
-      ]);
-
-      // 2. Options treatment (Filtering + Group by 4)
-      const optionsInfos = h('div', { class: 'options-info' });
-      const activeOptions =
-        Object.entries(gameInfos.options).filter(opt => !!opt[1]);
-
-      let i = 0;
-      while (i < activeOptions.length) {
-        const row = h('div', { class: 'row' });
-        for (let j = i; j < i + 4 && j < activeOptions.length; j++) {
-          const [key, val] = activeOptions[j];
-          const label = (val === true ? key : `${key}:${val}`);
-          row.append(h('span', { class: 'option', textContent: label + " " }));
-        }
-        optionsInfos.append(row);
-        i += 4;
-      }
-
-      // 3. Rules (keeping innerHTML here because trusted from file rules.html)
-      const rulesDiv = h('div', { class: 'rules' });
-      rulesDiv.innerHTML = txt;
-
-      // 4. Game infos button
-      const btnWrap = h('div', { class: 'btn-wrap' }, [
-        h('button', {
-          onclick: toggleGameInfos,
-          textContent: "Back to game"
-        })
-      ]);
+async function fillGameInfos(gameInfos, oppIndex) {
+  const rulesPath = await manifest(`variants/${gameInfos.vname}/rules.html`);
+  const res = await fetch('/' + rulesPath);
+  const txt = await res.text();
+
+  const container = $.getElementById("gameInfos");
+  container.innerHTML = ""; //initial cleaning
+
+  // 1. Players infos
+  const playerDiv = h('div', { class: 'players-info' }, [
+    h('p', null, [
+      h('span', { class: 'bold', textContent: gameInfos.vdisp }),
+      h('span', { textContent: ` vs. ${gameInfos.players[oppIndex].name}` })
+    ])
+  ]);
+
+  // 2. Options treatment (Filtering + Group by 4)
+  const optionsInfos = h('div', { class: 'options-info' });
+  const activeOptions =
+    Object.entries(gameInfos.options).filter(opt => !!opt[1]);
+
+  let i = 0;
+  while (i < activeOptions.length) {
+    const row = h('div', { class: 'row' });
+    for (let j = i; j < i + 4 && j < activeOptions.length; j++) {
+      const [key, val] = activeOptions[j];
+      const label = (val === true ? key : `${key}:${val}`);
+      row.append(h('span', { class: 'option', textContent: label + " " }));
+    }
+    optionsInfos.append(row);
+    i += 4;
+  }
 
-      // Final assembling
-      container.append(
-        playerDiv,
-        //activeOptions.length > 0 ? optionsInfos : null,
-        rulesDiv,
-        btnWrap
-      );
-    });
+  // 3. Rules (keeping innerHTML here because trusted from file rules.html)
+  const rulesDiv = h('div', { class: 'rules' });
+  rulesDiv.innerHTML = txt;
+
+  // 4. Game infos button
+  const btnWrap = h('div', { class: 'btn-wrap' }, [
+    h('button', {
+      onclick: toggleGameInfos,
+      textContent: "Back to game"
+    })
+  ]);
+
+  // Final assembling
+  container.append(
+    playerDiv,
+    //activeOptions.length > 0 ? optionsInfos : null,
+    rulesDiv,
+    btnWrap
+  );
 }
 
 ////////////////
@@ -555,89 +569,89 @@ const afterPlay = (move_s, newTurn, ops) => {
 };
 
 let vr = null, playerColor, lastVname = undefined;
-function initializeGame(obj) {
+async function initializeGame(obj) {
   const options = obj.options || {};
 
   // 1. Dynamic loading of variant js module
-  import(`/variants/${obj.vname}/class.js`).then(module => {
-    window.V = module.default;
-
-    // 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;
+  const module = await import('/' + await manifest(`variants/${obj.vname}/class.js`));
+  window.V = module.default;
+
+  // 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();
     }
+    const cssPath = await manifest(`variants/${obj.vname}/style.css`);
+    $.head.append(
+      h('link', {
+        id: `${obj.vname}_css`,
+        rel: 'stylesheet',
+        href: '/' + cssPath
+      })
+    );
+    lastVname = obj.vname;
+  }
 
-    playerColor = (sid == obj.players[0].sid ? 'w' : 'b');
+  playerColor = (sid == obj.players[0].sid ? 'w' : 'b');
 
-    // 3. Building Board Container
-    const container = $.getElementById("boardContainer");
-    container.innerHTML = ""; // On vide proprement l'ancien plateau
+  // 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>`;
+  // 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" xmlns="http://www.w3.org/2000/svg">
-  <path d="M528.468,428.468 L366.667,266.666 L528.462,104.869 C533.227,100.104 533.227,92.373 528.462,87.608 L445.725,4.871 C440.96,-0.106 433.229,-0.106 428.464,4.871 L266.667,166.668 L104.87,4.871 C100.105,-0.106 92.374,-0.106 87.609,4.871 L4.872,87.608 C-0.105,92.373 -0.105,100.104 4.872,104.869 L166.669,266.666 L4.872,428.463 C-0.105,433.228 -0.105,440.959 4.872,445.724 L87.609,528.461 C92.374,533.226 100.105,533.226 104.87,528.461 L266.667,366.664 L428.464,528.461 C433.229,533.226 440.96,533.226 445.725,528.461 L528.462,445.724 C533.227,440.959 533.227,433.228 528.468,428.468 Z" fill="currentColor"/>
+  const stopIcon = h('div', { id: 'upRightStop', onclick: confirmStopGame });
+  stopIcon.innerHTML = `<svg viewBox="0 0 533.333 533.333" xmlns="http://www.w3.org/2000/svg">
+<path d="M528.468,428.468 L366.667,266.666 L528.462,104.869 C533.227,100.104 533.227,92.373 528.462,87.608 L445.725,4.871 C440.96,-0.106 433.229,-0.106 428.464,4.871 L266.667,166.668 L104.87,4.871 C100.105,-0.106 92.374,-0.106 87.609,4.871 L4.872,87.608 C-0.105,92.373 -0.105,100.104 4.872,104.869 L166.669,266.666 L4.872,428.463 C-0.105,433.228 -0.105,440.959 4.872,445.724 L87.609,528.461 C92.374,533.226 100.105,533.226 104.87,528.461 L266.667,366.664 L428.464,528.461 C433.229,533.226 440.96,533.226 445.725,528.461 L528.462,445.724 C533.227,440.959 533.227,433.228 528.468,428.468 Z" fill="currentColor"/>
 </svg>`;
 
-    const board = h('div', { class: 'chessboard' });
+  const board = h('div', { class: 'chessboard' });
 
-    container.append(infoIcon, stopIcon, board);
+  container.append(infoIcon, stopIcon, board);
 
-    // 4. Initialize game engine (vr)
-    if (vr)
-      vr.removeListeners();
+  // 4. Initialize game engine (vr)
+  if (vr)
+    vr.removeListeners();
 
-    vr = new V({
-      seed: obj.seed,
-      fen: obj.fen,
-      element: "boardContainer",
-      color: playerColor,
-      afterPlay: afterPlay,
-      options: options
-    });
+  vr = new V({
+    seed: obj.seed,
+    fen: obj.fen,
+    element: "boardContainer",
+    color: playerColor,
+    afterPlay: afterPlay,
+    options: options
+  });
 
-    // 5. Handling game state
-    const gameCreation = !obj.fen;
-    if (gameCreation) {
-      send("setfen", { gid: obj.gid, fen: vr.getFen() });
-      localStorage.setItem("gid", obj.gid);
-    }
+  // 5. Handling game state
+  const gameCreation = !obj.fen;
+  if (gameCreation) {
+    send("setfen", { gid: obj.gid, fen: vr.getFen() });
+    localStorage.setItem("gid", obj.gid);
+  }
 
-    // 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;
+  // 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);
+  const playerIndex = (playerColor == "w" ? 0 : 1);
+  fillGameInfos(obj, 1 - playerIndex);
 
-    // 7. Final output
-    if (obj.players[playerIndex].randvar && gameCreation) {
-      toggleVisible("gameInfos");
-    }
-    else
-      toggleVisible("boardContainer");
+  // 7. Final output
+  if (obj.players[playerIndex].randvar && gameCreation) {
+    toggleVisible("gameInfos");
+  }
+  else
+    toggleVisible("boardContainer");
 
-    toggleTurnIndicator(vr.turn == playerColor);
-  });
+  toggleTurnIndicator(vr.turn == playerColor);
 }
 
 function confirmStopGame() {
index b1aa8d8..fe553f8 100644 (file)
@@ -1,4 +1,4 @@
-const sanitize = function(str, maxLength = 100)
+const sanitize = function(str, maxLength = 100, relax = false)
 {
   if (typeof str !== 'string') return "";
   // 1. Cut string to avoid memory overload
@@ -11,7 +11,8 @@ const sanitize = function(str, maxLength = 100)
     '"': '&quot;',
     "'": '&#039;'
   };
-  return cleaned.replace(/[&<>"']/g, m => map[m]);
+  const regexp = relax ? /[<>]/g : /[&<>"']/g
+  return cleaned.replace(regexp, m => map[m]);
 }
 
 // Next line for usage on server (Node.js)
index 3dd6b4a..85e6066 100644 (file)
@@ -98,10 +98,10 @@ wss.on("connection", (socket, req) => {
       if (obj.vname != "_random" && !variants.find(v => v.name == obj.vname))
         return; //unknown variant name
     }
-    if (obj.name)
+    if (obj.name) //TODO: probably overkill..
       obj.name = sanitize(obj.name, 30);
     if (obj.fen)
-      obj.fen = sanitize(obj.fen, 500);
+      obj.fen = sanitize(obj.fen, 500, true);
     if (obj.gid)
       obj.gid = sanitize(obj.gid, 20);
 
index fc6d664..a602c23 100755 (executable)
--- a/start.sh
+++ b/start.sh
@@ -2,4 +2,7 @@
 
 nodemon -w js/server.js &
 echo $! > .pid
+if [ $# -ge 2 ] && [ "$1" = "dev" ]; then
+  cd dist/
+fi
 php -S localhost:8000 &