From d0f552db83640b3203703277a458e3f07cd2d22e Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Tue, 19 Aug 2025 14:05:52 +0300 Subject: [PATCH 1/5] chore(tools): cache icon builds --- packages/tools/icons-collection/nps.js | 5 +- packages/tools/lib/icons-hash/index.mjs | 148 ++++++++++++++++++++++++ packages/tools/package.json | 3 +- yarn.lock | 5 + 4 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 packages/tools/lib/icons-hash/index.mjs diff --git a/packages/tools/icons-collection/nps.js b/packages/tools/icons-collection/nps.js index be76782e8393..3c7004f3eb52 100644 --- a/packages/tools/icons-collection/nps.js +++ b/packages/tools/icons-collection/nps.js @@ -47,10 +47,10 @@ const getScripts = (options) => { const scripts = { clean: "rimraf dist && rimraf src/generated", copy: copyAssetsCmd, - generate: `${tsCrossEnv} nps clean copy build.i18n build.icons build.jsonImports copyjson`, + generate: `(node "${LIB}/icons-hash/index.mjs" check) || (${tsCrossEnv} nps clean copy build.i18n build.icons build.jsonImports copyjson build.hashes)`, copyjson: "copy-and-watch \"src/generated/**/*.json\" dist/generated/", build: { - default: `${tsCrossEnv} nps clean copy build.i18n typescript build.icons build.jsonImports`, + default: `(node "${LIB}/icons-hash/index.mjs" check) || (${tsCrossEnv} nps clean copy build.i18n typescript build.icons build.jsonImports build.hashes)`, i18n: { default: "nps build.i18n.defaultsjs build.i18n.json", defaultsjs: `mkdirp dist/generated/i18n && node "${LIB}/i18n/defaults.js" src/i18n src/generated/i18n`, @@ -61,6 +61,7 @@ const getScripts = (options) => { i18n: `node "${LIB}/generate-json-imports/i18n.js" src/generated/assets/i18n src/generated/json-imports`, }, icons: createJSImportsCmd, + hashes: `node "${LIB}/icons-hash/index.mjs" save` }, typescript: tsCommand, }; diff --git a/packages/tools/lib/icons-hash/index.mjs b/packages/tools/lib/icons-hash/index.mjs new file mode 100644 index 000000000000..06977a269ef5 --- /dev/null +++ b/packages/tools/lib/icons-hash/index.mjs @@ -0,0 +1,148 @@ +import fs from "fs/promises"; +import path from "path"; +import ignore from "ignore"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// ------------------- +// FNV-1a 32-bit hash +// ------------------- +function fnv1aHash(str) { + let hash = 0x811c9dc5; + for (let i = 0; i < str.length; i++) { + hash ^= str.charCodeAt(i); + hash = (hash * 0x01000193) >>> 0; + } + return hash.toString(16); +} + +async function findGitignoreFiles(startDir) { + const gitignores = []; + let currentDir = path.resolve(startDir); + while (true) { + const candidate = path.join(currentDir, ".gitignore"); + try { + await fs.access(candidate); + gitignores.push(candidate); + } catch { } + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) break; + currentDir = parentDir; + } + return gitignores; +} + +async function loadIgnoreRules(dir) { + const files = await findGitignoreFiles(dir); + const ig = ignore(); + for (const file of files) { + const content = await fs.readFile(file, "utf8"); + ig.add(content); + } + return ig; +} + +async function walkDir(dir, ig, baseDir) { + const results = []; + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const absPath = path.join(dir, entry.name); + let relPath = path.relative(baseDir, absPath).replace(/\\/g, "/"); // normalize for .gitignore + + if (ig.ignores(relPath) || relPath.startsWith("dist/")) continue; + + if (entry.isDirectory()) { + results.push(...await walkDir(absPath, ig, baseDir)); + } else { + results.push(relPath); + } + } + return results; +} + +// Hash file content + mtime +async function hashFile(filePath) { + const stat = await fs.stat(filePath); + const content = await fs.readFile(filePath, "utf8"); + return fnv1aHash(String(stat.mtimeMs) + content); +} + +function getRepoName(repoPath) { + return repoPath.split("/").pop(); +} + +async function computeHashes(repoPath, ig) { + const files = await walkDir(repoPath, ig, repoPath); + const hashEntries = await Promise.all( + files.map(async (file) => { + const absPath = path.join(repoPath, file); + const hash = await hashFile(absPath); + return [path.relative(process.cwd(), absPath), hash]; + }) + ); + return Object.fromEntries(hashEntries); +} + +async function saveHashes(repoPath, ig) { + const distPath = path.join(repoPath, "dist"); + await fs.mkdir(distPath, { recursive: true }); + const ui5iconsHashPath = path.join(distPath, ".ui5iconsHash"); + + const hashes = { + ...(await computeHashes(repoPath, ig)), + ...(await computeHashes(path.resolve(__dirname, "../../"), ig)), + }; + + await fs.writeFile(ui5iconsHashPath, JSON.stringify(hashes, null, 2), "utf8"); + console.log(`Saved build hashes for the ${getRepoName(repoPath)} package.`); +} + +async function checkHashes(repoPath, ig) { + const ui5iconsHashPath = path.join(repoPath, "dist", ".ui5iconsHash"); + let oldHashes = {}; + try { + const raw = await fs.readFile(ui5iconsHashPath, "utf8"); + oldHashes = JSON.parse(raw); + } catch { + console.log(`No build hashes found for the ${getRepoName(repoPath)} package. Building it now.`); + process.exit(1); + } + + const newHashes = { + ...(await computeHashes(repoPath, ig)), + ...(await computeHashes(path.resolve(__dirname, "../../"), ig)), + }; + + let changed = false; + for (const file of new Set([...Object.keys(oldHashes), ...Object.keys(newHashes)])) { + if (oldHashes[file] !== newHashes[file]) { + changed = true; + } + } + + if (!changed) { + console.log(`No changes detected in the ${getRepoName(repoPath)} package.`); + } else { + console.log(`Changes detected in the ${getRepoName(repoPath)} package. Rebuilding it.`); + process.exit(2); + } +} + +async function main() { + const mode = process.argv[2]; + if (!["save", "check"].includes(mode)) { + console.error("Usage: node hashes.js "); + process.exit(1); + } + + const repoPath = process.cwd(); + const ig = await loadIgnoreRules(repoPath); + + if (mode === "save") await saveHashes(repoPath, ig); + if (mode === "check") await checkHashes(repoPath, ig); +} + +main().catch(console.error); diff --git a/packages/tools/package.json b/packages/tools/package.json index d967f09594ff..9fbc86bb864f 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -68,7 +68,8 @@ "slash": "3.0.0", "vite": "^5.4.8", "vite-plugin-istanbul": "^6.0.2", - "wdio-chromedriver-service": "^7.3.2" + "wdio-chromedriver-service": "^7.3.2", + "ignore": "^7.0.5" }, "peerDependencies": { "chromedriver": "*", diff --git a/yarn.lock b/yarn.lock index 76595f4416af..e73ef6c9aa81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11414,6 +11414,11 @@ ignore@^5.0.4, ignore@^5.1.4, ignore@^5.1.9, ignore@^5.2.0, ignore@^5.2.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +ignore@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" + integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + image-size@^1.0.2: version "1.2.1" resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.2.1.tgz#ee118aedfe666db1a6ee12bed5821cde3740276d" From c6adc76d8ae5b429ac615588de7ff261e30f07b6 Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Tue, 19 Aug 2025 14:07:51 +0300 Subject: [PATCH 2/5] chore: add comments --- packages/tools/lib/icons-hash/index.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/tools/lib/icons-hash/index.mjs b/packages/tools/lib/icons-hash/index.mjs index 06977a269ef5..c654c88a5ba7 100644 --- a/packages/tools/lib/icons-hash/index.mjs +++ b/packages/tools/lib/icons-hash/index.mjs @@ -91,6 +91,7 @@ async function saveHashes(repoPath, ig) { await fs.mkdir(distPath, { recursive: true }); const ui5iconsHashPath = path.join(distPath, ".ui5iconsHash"); + // Cache the hashes for both the icons and tools packages, since the output depends on the content of both. const hashes = { ...(await computeHashes(repoPath, ig)), ...(await computeHashes(path.resolve(__dirname, "../../"), ig)), @@ -111,6 +112,7 @@ async function checkHashes(repoPath, ig) { process.exit(1); } + // Compare the hashes for both the icons and tools packages, since the output depends on the content of both. const newHashes = { ...(await computeHashes(repoPath, ig)), ...(await computeHashes(path.resolve(__dirname, "../../"), ig)), From e00ff266e8bb09628c23efe0f54904a8214f3f52 Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Mon, 15 Sep 2025 11:16:04 +0300 Subject: [PATCH 3/5] chore: simply --- packages/tools/icons-collection/nps.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/tools/icons-collection/nps.js b/packages/tools/icons-collection/nps.js index 3c7004f3eb52..5d7a1eb84ec5 100644 --- a/packages/tools/icons-collection/nps.js +++ b/packages/tools/icons-collection/nps.js @@ -16,6 +16,8 @@ const createIconImportsCommand = (options) => { return command; } +const hashesCheck = cmd => `(node "${LIB}/icons-hash/index.mjs" check) || (${cmd})`; + const copyIconAssetsCommand = (options) => { if (!options.versions) { return { @@ -47,10 +49,10 @@ const getScripts = (options) => { const scripts = { clean: "rimraf dist && rimraf src/generated", copy: copyAssetsCmd, - generate: `(node "${LIB}/icons-hash/index.mjs" check) || (${tsCrossEnv} nps clean copy build.i18n build.icons build.jsonImports copyjson build.hashes)`, + generate: hashesCheck(`${tsCrossEnv} nps clean copy build.i18n build.icons build.jsonImports copyjson build.hashes`), copyjson: "copy-and-watch \"src/generated/**/*.json\" dist/generated/", build: { - default: `(node "${LIB}/icons-hash/index.mjs" check) || (${tsCrossEnv} nps clean copy build.i18n typescript build.icons build.jsonImports build.hashes)`, + default: hashesCheck(`${tsCrossEnv} nps clean copy build.i18n typescript build.icons build.jsonImports build.hashes`), i18n: { default: "nps build.i18n.defaultsjs build.i18n.json", defaultsjs: `mkdirp dist/generated/i18n && node "${LIB}/i18n/defaults.js" src/i18n src/generated/i18n`, From 1a4e55b50a35042aeed307cebc52842fcb7cf5ca Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Mon, 15 Sep 2025 11:23:52 +0300 Subject: [PATCH 4/5] chore: clean --- packages/tools/icons-collection/nps.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/tools/icons-collection/nps.js b/packages/tools/icons-collection/nps.js index 5d7a1eb84ec5..f668e8b5a9aa 100644 --- a/packages/tools/icons-collection/nps.js +++ b/packages/tools/icons-collection/nps.js @@ -7,7 +7,7 @@ const createIconImportsCommand = (options) => { return `node "${LIB}/create-icons/index.js" "${options.collectionName}"`; } - const command = { default: "nps" }; + const command = { default: "nps" }; options.versions.forEach((v) => { command.default += ` build.icons.create${v}`; command[`create${v}`] = `node "${LIB}/create-icons/index.js" "${options.collectionName}" "${v}"`; @@ -16,18 +16,18 @@ const createIconImportsCommand = (options) => { return command; } -const hashesCheck = cmd => `(node "${LIB}/icons-hash/index.mjs" check) || (${cmd})`; +const hashesCheck = cmd => `(node "${LIB}/icons-hash/index.mjs" check) || (${cmd}) && (node "${LIB}/icons-hash/index.mjs" save)`; const copyIconAssetsCommand = (options) => { if (!options.versions) { - return { + return { default: "nps copy.json-imports copy.icon-collection", "json-imports": `node "${LIB}/copy-and-watch/index.js" --silent "src/**/*.js" dist/`, "icon-collection": `node "${LIB}/copy-and-watch/index.js" --silent "src/*.json" src/generated/assets/`, } } - const command = { + const command = { default: "nps copy.json-imports ", "json-imports": `node "${LIB}/copy-and-watch/index.js" --silent "src/**/*.js" dist/`, }; @@ -49,10 +49,10 @@ const getScripts = (options) => { const scripts = { clean: "rimraf dist && rimraf src/generated", copy: copyAssetsCmd, - generate: hashesCheck(`${tsCrossEnv} nps clean copy build.i18n build.icons build.jsonImports copyjson build.hashes`), + generate: hashesCheck(`${tsCrossEnv} nps clean copy build.i18n build.icons build.jsonImports copyjson`), copyjson: "copy-and-watch \"src/generated/**/*.json\" dist/generated/", build: { - default: hashesCheck(`${tsCrossEnv} nps clean copy build.i18n typescript build.icons build.jsonImports build.hashes`), + default: hashesCheck(`${tsCrossEnv} nps clean copy build.i18n typescript build.icons build.jsonImports`), i18n: { default: "nps build.i18n.defaultsjs build.i18n.json", defaultsjs: `mkdirp dist/generated/i18n && node "${LIB}/i18n/defaults.js" src/i18n src/generated/i18n`, @@ -63,7 +63,6 @@ const getScripts = (options) => { i18n: `node "${LIB}/generate-json-imports/i18n.js" src/generated/assets/i18n src/generated/json-imports`, }, icons: createJSImportsCmd, - hashes: `node "${LIB}/icons-hash/index.mjs" save` }, typescript: tsCommand, }; From 69917af1028ae45160021785b037c91092544454 Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Mon, 15 Sep 2025 14:36:45 +0300 Subject: [PATCH 5/5] chore: rename file --- packages/tools/icons-collection/nps.js | 2 +- packages/tools/lib/icons-hash/{index.mjs => icons-hash.mjs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/tools/lib/icons-hash/{index.mjs => icons-hash.mjs} (100%) diff --git a/packages/tools/icons-collection/nps.js b/packages/tools/icons-collection/nps.js index f668e8b5a9aa..69b097848a56 100644 --- a/packages/tools/icons-collection/nps.js +++ b/packages/tools/icons-collection/nps.js @@ -16,7 +16,7 @@ const createIconImportsCommand = (options) => { return command; } -const hashesCheck = cmd => `(node "${LIB}/icons-hash/index.mjs" check) || (${cmd}) && (node "${LIB}/icons-hash/index.mjs" save)`; +const hashesCheck = cmd => `(node "${LIB}/icons-hash/icons-hash.mjs" check) || (${cmd} && node "${LIB}/icons-hash/icons-hash.mjs" save)`; const copyIconAssetsCommand = (options) => { if (!options.versions) { diff --git a/packages/tools/lib/icons-hash/index.mjs b/packages/tools/lib/icons-hash/icons-hash.mjs similarity index 100% rename from packages/tools/lib/icons-hash/index.mjs rename to packages/tools/lib/icons-hash/icons-hash.mjs