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}
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
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)
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()
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
}
// Play with a friend (or not ^^)
-function showNewGameForm() {
+async function showNewGameForm() {
const vname = $.getElementById("selectVariant").value;
if (vname == "_random")
alert("Select a variant first");
$.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() {
});
}
-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
+ );
}
////////////////
};
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() {