Skip to content

Commit dc6eae0

Browse files
committed
fix: don't rebuild SPA if built already done with same hashes of source (when hotReaload: false). Allows to simplify production deploy - users can leave .bundleNow call in code and it will not cause downtime if bundle called in build time (e.g. with npx adminforth bundle). Same optimization for messages extraction
1 parent 97c46ff commit dc6eae0

File tree

2 files changed

+79
-17
lines changed

2 files changed

+79
-17
lines changed

adminforth/modules/codeInjector.ts

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ class CodeInjector implements ICodeInjector {
212212
}
213213

214214
async prepareSources() {
215+
// collects all files and folders into SPA_TMP_DIR
216+
215217
// check spa tmp folder exists and create if not
216218
try {
217219
await fs.promises.access(this.spaTmpPath(), fs.constants.F_OK);
@@ -734,6 +736,39 @@ class CodeInjector implements ICodeInjector {
734736
this.allWatchers.push(watcher);
735737
}
736738

739+
async tryReadFile(filePath: string) {
740+
try {
741+
const content = await fs.promises.readFile(filePath, 'utf-8');
742+
return content;
743+
} catch (e) {
744+
// file does not exist
745+
process.env.HEAVY_DEBUG && console.log(`🪲File ${filePath} does not exist, returning null`);
746+
return null;
747+
}
748+
}
749+
750+
async computeSourcesHash(folderPath: string = this.spaTmpPath()) {
751+
const files = await fs.promises.readdir(folderPath, { withFileTypes: true });
752+
const hashes = await Promise.all(
753+
files.map(async (file) => {
754+
const filePath = path.join(folderPath, file.name);
755+
756+
// 🚫 Skip node_modules
757+
if (file.name === 'node_modules' || file.name === 'dist') {
758+
return '';
759+
}
760+
761+
if (file.isDirectory()) {
762+
return this.computeSourcesHash(filePath);
763+
} else {
764+
const content = await fs.promises.readFile(filePath, 'utf-8');
765+
return md5hash(content);
766+
}
767+
})
768+
);
769+
return md5hash(hashes.join(''));
770+
}
771+
737772
async bundleNow({ hotReload = false }: { hotReload: boolean }) {
738773
console.log(`${this.adminforth.formatAdminForth()} Bundling ${hotReload ? 'and listening for changes (🔥 Hotreload)' : ' (no hot reload)'}`);
739774
this.adminforth.runningHotReload = hotReload;
@@ -754,28 +789,55 @@ class CodeInjector implements ICodeInjector {
754789
}
755790

756791
const cwd = this.spaTmpPath();
792+
const serveDir = this.getServeDir();
757793

758794

759-
await this.runNpmShell({command: 'run i18n:extract', cwd});
795+
const sourcesHash = await this.computeSourcesHash(this.spaTmpPath());
796+
797+
const buildHash = await this.tryReadFile(path.join(serveDir, '.adminforth_build_hash'));
798+
const messagesHash = await this.tryReadFile(path.join(serveDir, '.adminforth_messages_hash'));
760799

761-
// probably add option to build with tsh check (plain 'build')
762-
const serveDir = this.getServeDir();
763-
// remove serveDir if exists
764-
try {
765-
await fs.promises.rm(serveDir, { recursive: true });
766-
} catch (e) {
767-
// ignore
800+
const skipBuild = buildHash === sourcesHash;
801+
const skipExtract = messagesHash === sourcesHash;
802+
803+
804+
805+
if (!skipExtract) {
806+
await this.runNpmShell({command: 'run i18n:extract', cwd});
807+
808+
// create serveDir if not exists
809+
await fs.promises.mkdir(serveDir, { recursive: true });
810+
811+
// copy i18n messages to serve dir
812+
await fsExtra.copy(path.join(cwd, 'i18n-messages.json'), path.join(serveDir, 'i18n-messages.json'));
813+
814+
// save hash
815+
await fs.promises.writeFile(path.join(serveDir, '.adminforth_messages_hash'), sourcesHash);
816+
} else {
817+
console.log(`Skipping AdminForth i18n messages extraction - it is already done for these sources set`);
768818
}
769-
await fs.promises.mkdir(serveDir, { recursive: true });
770-
771-
// copy i18n messages to serve dir
772-
await fsExtra.copy(path.join(cwd, 'i18n-messages.json'), path.join(serveDir, 'i18n-messages.json'));
773819

774820
if (!hotReload) {
775-
await this.runNpmShell({command: 'run build-only', cwd});
821+
if (!skipBuild) {
822+
// remove serveDir if exists
823+
try {
824+
await fs.promises.rm(serveDir, { recursive: true });
825+
} catch (e) {
826+
// ignore
827+
}
828+
await fs.promises.mkdir(serveDir, { recursive: true });
829+
830+
// TODO probably add option to build with tsh check (plain 'build')
831+
await this.runNpmShell({command: 'run build-only', cwd});
832+
833+
// coy dist to serveDir
834+
await fsExtra.copy(path.join(cwd, 'dist'), serveDir, { recursive: true });
776835

777-
// coy dist to serveDir
778-
await fsExtra.copy(path.join(cwd, 'dist'), serveDir, { recursive: true });
836+
// save hash
837+
await fs.promises.writeFile(path.join(serveDir, '.adminforth_build_hash'), sourcesHash);
838+
} else {
839+
console.log(`Skipping AdminForth SPA bundle - it is already done for these sources set`);
840+
}
779841
} else {
780842

781843
const command = 'run dev';

dev-demo/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,8 @@ const port = process.env.PORT || 3000;
341341
(async () => {
342342
console.log('🅿️ Bundling AdminForth...');
343343
// needed to compile SPA. Call it here or from a build script e.g. in Docker build time to reduce downtime
344-
await admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development'});
345-
console.log('Bundling AdminForth done. For faster serving consider calling bundleNow() from a build script.');
344+
await admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development1'});
345+
console.log('Bundling AdminForth SPA done.');
346346
})();
347347

348348

0 commit comments

Comments
 (0)