From 769249a159daf615d375915761dce54bc407187d Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Tue, 5 May 2026 15:44:37 +0200 Subject: [PATCH] Fix bundle.py. TODO: clean code (sanitize.. ?) --- bundle.py | 59 ++++++----- js/app.js | 258 ++++++++++++++++++++++++++----------------------- js/sanitize.js | 5 +- js/server.js | 4 +- start.sh | 3 + 5 files changed, 179 insertions(+), 150 deletions(-) diff --git a/bundle.py b/bundle.py index 0464fd9..1a3bdf5 100755 --- 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() diff --git a/js/app.js b/js/app.js index 43cdbc6..7d30a32 100644 --- 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 = ``; + // Create SVG icons with a string, inserted securely. + const infoIcon = h('div', { id: 'upLeftInfos', onclick: toggleGameInfos }); + infoIcon.innerHTML = ``; - const stopIcon = h('div', { id: 'upRightStop', onclick: confirmStopGame }); - stopIcon.innerHTML = ` - + const stopIcon = h('div', { id: 'upRightStop', onclick: confirmStopGame }); + stopIcon.innerHTML = ` + `; - 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() { diff --git a/js/sanitize.js b/js/sanitize.js index b1aa8d8..fe553f8 100644 --- a/js/sanitize.js +++ b/js/sanitize.js @@ -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) '"': '"', "'": ''' }; - 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) diff --git a/js/server.js b/js/server.js index 3dd6b4a..85e6066 100644 --- a/js/server.js +++ b/js/server.js @@ -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); diff --git a/start.sh b/start.sh index fc6d664..a602c23 100755 --- 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 & -- 2.53.0