Skip to content

Commit ea33b90

Browse files
authored
Merge pull request #506 from devforth/test
Test
2 parents 68691ac + ea5fc77 commit ea33b90

File tree

2 files changed

+108
-15
lines changed

2 files changed

+108
-15
lines changed

adminforth/modules/codeInjector.ts

Lines changed: 94 additions & 3 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;
@@ -992,7 +1037,7 @@ class CodeInjector implements ICodeInjector {
9921037

9931038
const buildHash = await this.tryReadFile(path.join(serveDir, '.adminforth_build_hash'));
9941039
const messagesHash = await this.tryReadFile(path.join(serveDir, '.adminforth_messages_hash'));
995-
1040+
console.log({buildHash, sourcesHash});
9961041
const skipBuild = buildHash === sourcesHash;
9971042
const skipExtract = messagesHash === sourcesHash;
9981043

@@ -1026,8 +1071,52 @@ class CodeInjector implements ICodeInjector {
10261071
}
10271072

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

@@ -1036,6 +1125,8 @@ class CodeInjector implements ICodeInjector {
10361125

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

adminforth/spa/vite.config.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,24 @@ export default defineConfig({
5353
// by default servers doesnt returns dotfiles
5454
// so if we'll generate file with name like ".pnpm-BLnlxqcJ.js"
5555
// it won't be loaded
56-
assetFileNames: (assetInfo) => {
57-
return assetInfo.name?.startsWith('.pnpm')
58-
? `assets/${assetInfo.name.replace(/^\.pnpm/, 'pnpm')}`
59-
: `assets/${assetInfo.name}`;
56+
assetFileNames: (chunkInfo) => {
57+
if (chunkInfo.name && chunkInfo.name.startsWith('.pnpm')) {
58+
return `assets/pnpm-${chunkInfo.name.slice(6)}-[hash].[ext]`;
59+
}
60+
return 'assets/[name]-[hash].[ext]';
6061
},
61-
6262
entryFileNames: (chunkInfo) => {
63-
return chunkInfo.name?.startsWith('.pnpm')
64-
? `assets/${chunkInfo.name.replace(/^\.pnpm/, 'pnpm')}.js`
65-
: `assets/${chunkInfo.name}.js`;
63+
if (chunkInfo.name && chunkInfo.name.startsWith('.pnpm')) {
64+
return `assets/pnpm-${chunkInfo.name.slice(6)}-[hash].js`;
65+
}
66+
return 'assets/[name]-[hash].js';
6667
},
6768
chunkFileNames: (chunkInfo) => {
68-
return chunkInfo.name?.startsWith('.pnpm')
69-
? `assets/${chunkInfo.name.replace(/^\.pnpm/, 'pnpm')}.js`
70-
: `assets/${chunkInfo.name}.js`;
71-
},
69+
if (chunkInfo.name && chunkInfo.name.startsWith('.pnpm')) {
70+
return `assets/pnpm-${chunkInfo.name.slice(6)}-[hash].js`;
71+
}
72+
return 'assets/[name]-[hash].js';
73+
}
7274
},
7375
},
7476
},

0 commit comments

Comments
 (0)