Skip to content

Commit 8b438ba

Browse files
authored
Merge pull request #503 from devforth/feature/AdminForth/1192/still-warming-up-message-again
feat: make code injector to notify user, that there are new files or …
2 parents 68691ac + 37ea4db commit 8b438ba

File tree

1 file changed

+92
-2
lines changed

1 file changed

+92
-2
lines changed

adminforth/modules/codeInjector.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,51 @@ class CodeInjector implements ICodeInjector {
963963
return md5hash(hashes.join(''));
964964
}
965965

966+
// Compute a map of file relative paths -> md5 hash of file contents and return it.
967+
// Skips directories/files that are ignored by computeSourcesHash (node_modules, dist, i18n files).
968+
async computeSourcesHashMap(folderPath: string = this.spaTmpPath(), rootFolder: string = this.spaTmpPath(), map: { [key: string]: string } = {}): Promise<{ [key: string]: string }> {
969+
const files = await fs.promises.readdir(folderPath, { withFileTypes: true });
970+
971+
await Promise.all(
972+
files.map(async (file) => {
973+
const filePath = path.join(folderPath, file.name);
974+
975+
// 🚫 Skip big/dynamic folders or files
976+
if (file.name === 'node_modules' || file.name === 'dist' ||
977+
file.name === 'i18n-messages.json' || file.name === 'i18n-empty.json' || file.name === 'hashes.json') {
978+
return;
979+
}
980+
981+
if (file.isDirectory()) {
982+
return await this.computeSourcesHashMap(filePath, rootFolder, map);
983+
} else {
984+
try {
985+
const content = await fs.promises.readFile(filePath, 'utf-8');
986+
const hash = md5hash(content);
987+
// store relative path using forward slashes for portability
988+
const rel = path.relative(rootFolder, filePath).split(path.sep).join('/');
989+
map[rel] = hash;
990+
} catch (e) {
991+
// If a file can't be read (binary or permission), log and continue
992+
afLogger.trace(`🪲File ${filePath} read error: ${e}`);
993+
return;
994+
}
995+
}
996+
})
997+
);
998+
999+
return map;
1000+
}
1001+
1002+
// Convenience helper: compute per-file hashes and save them into hashes.json in the spa tmp dir
1003+
async saveSourcesHashesToFile(outputFileName: string = 'hashes.json', hashMap: { [key: string]: string } = {}): Promise<string> {
1004+
const root = this.spaTmpPath();
1005+
const outPath = path.join(root, outputFileName);
1006+
await fs.promises.writeFile(outPath, JSON.stringify(hashMap, null, 2), 'utf-8');
1007+
afLogger.trace(`🪲 Saved sources hashes to ${outPath}`);
1008+
return outPath;
1009+
}
1010+
9661011
async bundleNow({ hotReload = false }: { hotReload: boolean }) {
9671012
afLogger.info(`${this.adminforth.formatAdminForth()} Bundling ${hotReload ? 'and listening for changes (🔥 Hotreload)' : ' (no hot reload)'}`);
9681013
this.adminforth.runningHotReload = hotReload;
@@ -1026,8 +1071,51 @@ class CodeInjector implements ICodeInjector {
10261071
}
10271072

10281073
if (!hotReload) {
1029-
if (!skipBuild) {
1030-
1074+
if (!skipBuild) {
1075+
let oldHashForFiles = null;
1076+
try {
1077+
oldHashForFiles = await fs.promises.readFile(path.join(this.spaTmpPath(), 'hashes.json'), 'utf-8');
1078+
} catch (e) {
1079+
// ignore if file doesn't exist, it is only for debugging
1080+
console.log(`Build cache not found, building now (downtime) please consider running npx adminforth bundle at build time to avoid downtimes at runtime`);
1081+
}
1082+
const root = this.spaTmpPath();
1083+
const hashMap = await this.computeSourcesHashMap(root, root, {});
1084+
if (oldHashForFiles) {
1085+
const parsedOldHashForFiles = JSON.parse(oldHashForFiles);
1086+
const logsToDisplay = [];
1087+
logsToDisplay.push(`Build cache exists but is outdated:`);
1088+
for(const [file, hash] of Object.entries(hashMap)) {
1089+
if (!parsedOldHashForFiles[file]) {
1090+
logsToDisplay.push(` - file ${file} - does not exist in cache but exists in runtime`);
1091+
} else if (parsedOldHashForFiles[file] !== hash) {
1092+
logsToDisplay.push(` - file ${file} - content in cache is different then in runtime`);
1093+
}
1094+
}
1095+
/**
1096+
* Currently we can't detect, if file was removed,
1097+
* because we can only add files to the tpm folder but not remove them,
1098+
* so if file existed before and now doesn't exist, we will not detect it
1099+
*/
1100+
1101+
// for(const [file, hash] of Object.entries(parsedOldHashForFiles)) {
1102+
// console.log(`checking file ${file} in old hash: ${hash}`);
1103+
// console.log(`checking file ${file} in new hash: ${hashMap[file]}`);
1104+
// if (!hashMap[file]) {
1105+
// logsToDisplay.push(` - file ${file} - exists in cache but does not exist in runtime`);
1106+
// }
1107+
// }
1108+
1109+
logsToDisplay.push(`If you are running in production now, then the cache loss is a downtime issue.`);
1110+
logsToDisplay.push(`If you have npx adminforth bundle in build time, then this issue might be caused by conditional instantiation of plugins:`)
1111+
logsToDisplay.push(`Please avoid constructions like (process.env.SOME_KEY ? new Plugin(...) ) because if you will miss SOME_KEY in build time build cache and functionality fails.`);
1112+
if (logsToDisplay.length > 4) {
1113+
for(const log of logsToDisplay) {
1114+
console.log(log);
1115+
}
1116+
}
1117+
}
1118+
10311119
// TODO probably add option to build with tsh check (plain 'build')
10321120
await this.runPackageManagerShell({command: 'run build-only', cwd});
10331121

@@ -1036,6 +1124,8 @@ class CodeInjector implements ICodeInjector {
10361124

10371125
// save hash
10381126
await fs.promises.writeFile(path.join(serveDir, '.adminforth_build_hash'), sourcesHash);
1127+
// save sources hashes to file for later debugging if needed
1128+
await this.saveSourcesHashesToFile('hashes.json', hashMap);
10391129
} else {
10401130
afLogger.info(`Skipping AdminForth SPA bundling - already completed for the current sources.`);
10411131
}

0 commit comments

Comments
 (0)