Skip to content

Commit aa70d57

Browse files
committed
Use fingerprinted file name for microfrontend entrypoint bundle
Enables us to add a caching header to the response that fetches the entrypoint bundle of a microfrontend like. Since the file name of the entrypoint bundle contains a md5 hash based on the content of the file, we know that if a bundle with the same name has been fetched by the browser, it is unchanged. E.g. caching for one year: Cache-Control: max-age=31536000 The main.js file name we used before did not allow for caching, while still ensuring that all customers always get the latest micro frontend code.
1 parent 72f63c1 commit aa70d57

File tree

6 files changed

+87
-11
lines changed

6 files changed

+87
-11
lines changed

angular.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@
141141
"builder": "ngx-build-plus:build",
142142
"options": {
143143
"singleBundle": true,
144-
"outputHashing": "none",
144+
"outputHashing": "all",
145145
"outputPath": "projects/bookings/dist",
146146
"index": "projects/bookings/src/index.html",
147147
"main": "projects/bookings/src/main.ts",

projects/bookings/netlify.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[build]
22
publish = "dist"
3-
command = "npm ci && npm run lint bookings && npm run test bookings && npm run build bookings -- --configuration production"
3+
command = "npm ci && npm run lint bookings && npm run test bookings && npm run build bookings -- --configuration production && node scripts/generate-frontend-meta-json.js buildDir='projects/bookings/dist'"

projects/train-platform/src/micro-frontends/bookings-host.module.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ import { LoadMicroFrontendGuard } from './load-micro-frontend.guard';
66
import { MicroFrontendLanguageDirective } from './micro-frontend-language.directive';
77
import { MicroFrontendRoutingDirective } from './micro-frontend-routing.directive';
88

9-
const getMicrofrontendBundleUrl = (frontendName: 'bookings') =>
10-
`/frontends/${frontendName}/main.js`;
9+
const getMicrofrontendBundleUrl = async (frontendName: 'bookings') => {
10+
const metaDataJsonUrl = `/frontends/${frontendName}/frontend-meta.json`;
11+
const frontendMetaData = await fetch(metaDataJsonUrl).then((response) =>
12+
response.json()
13+
);
14+
const entryPointBundleName = frontendMetaData.entryPointBundleName;
15+
return `/frontends/${frontendName}/${entryPointBundleName}`;
16+
};
1117

1218
@NgModule({
1319
declarations: [
@@ -24,7 +30,7 @@ const getMicrofrontendBundleUrl = (frontendName: 'bookings') =>
2430
data: {
2531
bundleUrl: environment.production
2632
? getMicrofrontendBundleUrl('bookings')
27-
: 'http://localhost:4201/main.js',
33+
: Promise.resolve('http://localhost:4201/main.js'),
2834
},
2935
},
3036
]),

projects/train-platform/src/micro-frontends/load-micro-frontend.guard.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ export class LoadMicroFrontendGuard implements CanActivate {
99
) {}
1010

1111
canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
12-
const bundleUrl = route.data.bundleUrl as unknown;
13-
if (!(typeof bundleUrl === 'string')) {
12+
const bundleUrl: Promise<string> = route.data.bundleUrl;
13+
if (!(typeof bundleUrl.then === 'function')) {
1414
console.error(`
15-
The LoadMicroFrontendGuard is missing information on which bundle to load.
16-
Did you forget to provide a bundleUrl: string as data to the route?
15+
You need to provide the Promise which loads the frontend-meta.json
16+
and maps to the entryPointBundleName to the LoadMicroFrontendGuard.
17+
Did you forget to provide the bundleUrl as route data?
1718
`);
1819
return Promise.resolve(false);
1920
}

projects/train-platform/src/micro-frontends/micro-frontend-registry.service.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ export class MicroFrontendRegistryService {
3434
/**
3535
* Loads the given bundle if not already loaded, registering its custom elements in the browser.
3636
*
37-
* @param bundleUrl The url of the bundle, can be absolute or relative to the domain + base href.
37+
* @param bundleUrl$ The url of the bundle, can be absolute or relative to the domain + base href.
3838
*/
39-
async loadBundle(bundleUrl: string): Promise<boolean> {
39+
async loadBundle(bundleUrl$: Promise<string>): Promise<boolean> {
40+
const bundleUrl = await bundleUrl$;
4041
if (['LOADING', 'LOADED'].includes(this.getLoadingState(bundleUrl))) {
4142
return true;
4243
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* eslint-env es6 */
2+
/*
3+
* HOW TO USE:
4+
* First execute the build command for your frontend, e.g. nx run reports:build
5+
* Now call this script and provide it with the build output directory:
6+
* node generate-frontend-meta-json.js buildDir='projects/bookings/dist'
7+
*
8+
* It will write a "frontend-meta.json" into the provided directory.
9+
*/
10+
11+
const path = require("path");
12+
const fs = require("fs");
13+
14+
const projectRoot = path.resolve(__dirname, "../");
15+
const buildDirectory = path.resolve(projectRoot, getBuildDirectoryInput());
16+
17+
fs.readdir(buildDirectory, (err, filesInBuildDirectory) => {
18+
if (err) {
19+
throw new Error(`Directory ${buildDirectory} could not be found`);
20+
}
21+
22+
const frontendMetaData = generateFrontendMetaData(filesInBuildDirectory);
23+
const targetFile = path.resolve(buildDirectory, "frontend-meta.json");
24+
const frontendMetaDataString = JSON.stringify(frontendMetaData);
25+
fs.writeFileSync(targetFile, frontendMetaDataString);
26+
27+
console.log(
28+
`Wrote the following data to ${targetFile}: ${frontendMetaDataString}`
29+
);
30+
});
31+
32+
/**
33+
* @typedef FrontendMetaData
34+
* @type {object}
35+
* @property {string} entryPointBundleName - The name of the bundle used as entrypoint for the frontend application
36+
*/
37+
38+
/**
39+
* @param {string[]} filesInBuildDirectory
40+
* @returns {FrontendMetaData}
41+
*/
42+
function generateFrontendMetaData(filesInBuildDirectory) {
43+
const mainBundleName = filesInBuildDirectory.find((fileName) => {
44+
return fileName.startsWith("main.") && fileName.endsWith(".js");
45+
});
46+
47+
if (!mainBundleName) {
48+
throw new Error("Could not find the entrypoint main bundle");
49+
}
50+
51+
return {
52+
entryPointBundleName: mainBundleName,
53+
};
54+
}
55+
56+
/**
57+
* @returns {string}
58+
*/
59+
function getBuildDirectoryInput() {
60+
const firstInput = process.argv[2];
61+
const [paramName, paramValue] = firstInput.split("=");
62+
if (paramName !== "buildDir") {
63+
throw new Error(
64+
`You need to provide a value for the "buildDir" parameter. E.g. "node generate-frontend-meta-json.js buildDir='dist/apps/reports'`
65+
);
66+
}
67+
return paramValue;
68+
}

0 commit comments

Comments
 (0)