From 8b9849aeedc05ea74e28ff70e0ce8090c6a8c8fd Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 17:32:14 +0100 Subject: [PATCH] refactor: streamline dynamic import hash updates for public JS files --- scripts/bump-version.js | 115 +++++++++++++++------------------------- 1 file changed, 43 insertions(+), 72 deletions(-) diff --git a/scripts/bump-version.js b/scripts/bump-version.js index de7c8f45..fffa86ed 100644 --- a/scripts/bump-version.js +++ b/scripts/bump-version.js @@ -180,99 +180,70 @@ function updateIndexHtmlHashes(changedFiles, newVersion, dry) { console.log(` src/index.html hashes updated for:\n - ${updated.join("\n - ")}`); } else { console.log( - const publicRoot = path.join(repoRoot, "public"); - - function walk(dir) { - for (const entry of fs.readdirSync(dir, {withFileTypes: true})) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - // Skip third-party vendor bundles under public/libs/** - const relFromPublic = path - .relative(publicRoot, full) - .replace(/\\/g, "/"); - if (relFromPublic === "libs" || relFromPublic.startsWith("libs/")) { - continue; - } - walk(full); - } else if (entry.isFile() && entry.name.endsWith(".js")) { - results.push(path.relative(repoRoot, full).replace(/\\/g, "/")); - } - } - } - - walk(publicRoot); + ` src/index.html (changed files have no ?v= entry: ${changedFiles.map(f => f.replace("public/", "")).join(", ")}` ); } } -/** Returns all .js file paths (relative to repo root, with forward slashes) under public/. */ -function getAllPublicJsFiles() { - const results = []; - function walk(dir) { - for (const entry of fs.readdirSync(dir, {withFileTypes: true})) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - walk(full); - } else if (entry.isFile() && entry.name.endsWith(".js")) { - results.push(path.relative(repoRoot, full).replace(/\\/g, "/")); - } - } - } - walk(path.join(repoRoot, "public")); - return results; -} - /** - * Scans all public/**\/*.js files for relative dynamic-import ?v= references - * (e.g. import("../dynamic/supporters.js?v=1.97.14")) and updates any that - * point to one of the changed files. + * For each changed public JS file, scans ALL other public JS files for + * dynamic import() calls that reference it via a relative ?v= path, and + * updates the hash to newVersion. + * + * Example: public/modules/dynamic/installation.js changed → + * main.js: import("./modules/dynamic/installation.js?v=1.89.19") + * → import("./modules/dynamic/installation.js?v=1.113.4") */ function updatePublicJsDynamicImportHashes(changedFiles, newVersion, dry) { if (changedFiles.length === 0) { - console.log(" public/**/*.js (no changed public/*.js files detected)"); + console.log(" public/**/*.js (no changed files, skipping dynamic import hashes)"); return; - const replacement = `${quote}${relImportPath}?v=${newVersion}${quote}`; - // Only record and apply an update if the version actually changes - if (match === replacement) { - return match; - } - if (!updatedMap[relJsFile]) updatedMap[relJsFile] = []; - updatedMap[relJsFile].push(relImportPath); - return replacement; - const publicJsFiles = getAllPublicJsFiles(); + } + + // Absolute paths of every changed file for O(1) lookup + const changedAbsPaths = new Set(changedFiles.map(f => path.join(repoRoot, f))); + + // Collect all public JS files, skipping public/libs (third-party) + const publicRoot = path.join(repoRoot, "public"); + const allJsFiles = []; + (function walk(dir) { + for (const entry of fs.readdirSync(dir, {withFileTypes: true})) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (path.relative(publicRoot, full).replace(/\\/g, "/") === "libs") continue; + walk(full); + } else if (entry.isFile() && entry.name.endsWith(".js")) { + allJsFiles.push(full); + } + } + })(publicRoot); + const updatedMap = {}; - for (const relJsFile of publicJsFiles) { - const absJsFile = path.join(repoRoot, relJsFile); + for (const absJsFile of allJsFiles) { const content = readFile(absJsFile); - const dir = path.dirname(absJsFile); - - const pattern = /(['"])(\.\.?\/[^'"]*)\?v=[0-9.]+\1/g; + // Matches: import("../path/file.js?v=1.2.3") or import('../path/file.js?v=1.2.3') + const pattern = /(['"])(\.{1,2}\/[^'"?]+)\?v=[0-9.]+\1/g; + let anyChanged = false; const newContent = content.replace(pattern, (match, quote, relImportPath) => { - // Strip any query string defensively before resolving the path - const cleanImportPath = relImportPath.split("?")[0]; - const absImport = path.resolve(dir, cleanImportPath); - const repoRelImport = path.relative(repoRoot, absImport).replace(/\\/g, "/"); - if (changedSet.has(repoRelImport)) { - if (!updatedMap[relJsFile]) updatedMap[relJsFile] = []; - updatedMap[relJsFile].push(relImportPath); - return `${quote}${relImportPath}?v=${newVersion}${quote}`; - } - return match; + const absImport = path.resolve(path.dirname(absJsFile), relImportPath); + if (!changedAbsPaths.has(absImport)) return match; + const repoRelFile = path.relative(repoRoot, absJsFile).replace(/\\/g, "/"); + if (!updatedMap[repoRelFile]) updatedMap[repoRelFile] = []; + updatedMap[repoRelFile].push(relImportPath); + anyChanged = true; + return `${quote}${relImportPath}?v=${newVersion}${quote}`; }); - - if (updatedMap[relJsFile] && !dry) { - writeFile(absJsFile, newContent); - } + if (anyChanged && !dry) writeFile(absJsFile, newContent); } if (Object.keys(updatedMap).length > 0) { const lines = Object.entries(updatedMap) .map(([file, refs]) => ` ${file}:\n - ${refs.join("\n - ")}`) .join("\n"); - console.log(` public/**/*.js hashes updated:\n${lines}`); + console.log(` public/**/*.js dynamic import hashes updated:\n${lines}`); } else { - console.log(" public/**/*.js (no dynamic import ?v= hashes needed updating)"); + console.log(" public/**/*.js (no dynamic import ?v= hashes to update)"); } }