Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/tools/icons-collection/nps.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand All @@ -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,
};
Expand Down
150 changes: 150 additions & 0 deletions packages/tools/lib/icons-hash/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
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");

// 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)),
};

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);
}

// 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)),
};

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 <save|check>");
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);
3 changes: 2 additions & 1 deletion packages/tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "*",
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading