Skip to content

Commit dfaed6a

Browse files
authored
Merge pull request #4030 from ybnd/cache-bust-dynamic-configuration-9.0
[Port dspace-9_x] Cache-bust dynamic configuration files and theme CSS
2 parents 92554db + e5d6395 commit dfaed6a

15 files changed

Lines changed: 370 additions & 13 deletions

config/config.example.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ cache:
9090
# NOTE: When updates are made to compiled *.js files, it will automatically bypass this browser cache, because
9191
# all compiled *.js files include a unique hash in their name which updates when content is modified.
9292
control: max-age=604800 # revalidate browser
93+
# These static files should not be cached (paths relative to dist/browser, including the leading slash)
94+
noCacheFiles:
95+
- '/index.html'
9396
autoSync:
9497
defaultTime: 0
9598
maxBufferSize: 100
@@ -451,6 +454,7 @@ themes:
451454
# - name: BASE_THEME_NAME
452455
#
453456
- name: dspace
457+
prefetch: true
454458
headTags:
455459
- tagName: link
456460
attributes:

package-lock.json

Lines changed: 44 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
"ngx-pagination": "6.0.3",
143143
"ngx-skeleton-loader": "^11.3.0",
144144
"ngx-ui-switch": "^16.1.0",
145+
"node-html-parser": "^7.0.1",
145146
"nouislider": "^15.7.1",
146147
"orejime": "^2.3.3",
147148
"pem": "1.14.8",

server.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
AppConfig,
4949
} from './src/config/app-config.interface';
5050
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
51+
import { ServerHashedFileMapping } from './src/modules/dynamic-hash/hashed-file-mapping.server';
5152
import { logStartupMessage } from './startup-message';
5253
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
5354
import { CommonEngine } from '@angular/ssr/node';
@@ -69,7 +70,11 @@ const indexHtml = join(DIST_FOLDER, 'index.html');
6970

7071
const cookieParser = require('cookie-parser');
7172

72-
const appConfig: AppConfig = buildAppConfig(join(DIST_FOLDER, 'assets/config.json'));
73+
const configJson = join(DIST_FOLDER, 'assets/config.json');
74+
const hashedFileMapping = new ServerHashedFileMapping(DIST_FOLDER, 'index.html');
75+
const appConfig: AppConfig = buildAppConfig(configJson, hashedFileMapping);
76+
appConfig.themes.forEach(themeConfig => hashedFileMapping.addThemeStyle(themeConfig.name, themeConfig.prefetch));
77+
hashedFileMapping.save();
7378

7479
// cache of SSR pages for known bots, only enabled in production mode
7580
let botCache: LRUCache<string, any>;
@@ -329,7 +334,7 @@ function clientSideRender(req, res) {
329334
html = html.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
330335
}
331336

332-
res.send(html);
337+
res.set('Cache-Control', 'no-cache, no-store').send(html);
333338
}
334339

335340

@@ -340,7 +345,11 @@ function clientSideRender(req, res) {
340345
*/
341346
function addCacheControl(req, res, next) {
342347
// instruct browser to revalidate
343-
res.header('Cache-Control', environment.cache.control || 'max-age=604800');
348+
if (environment.cache.noCacheFiles.includes(req.originalUrl)) {
349+
res.header('Cache-Control', 'no-cache, no-store');
350+
} else {
351+
res.header('Cache-Control', environment.cache.control || 'max-age=604800');
352+
}
344353
next();
345354
}
346355

src/app/app.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import {
3535
} from '../config/app-config.interface';
3636
import { StoreDevModules } from '../config/store/devtools';
3737
import { environment } from '../environments/environment';
38+
import { HashedFileMapping } from '../modules/dynamic-hash/hashed-file-mapping';
39+
import { BrowserHashedFileMapping } from '../modules/dynamic-hash/hashed-file-mapping.browser';
3840
import { EagerThemesModule } from '../themes/eager-themes.module';
3941
import { appEffects } from './app.effects';
4042
import { MENUS } from './app.menus';
@@ -155,6 +157,10 @@ export const commonAppConfig: ApplicationConfig = {
155157
useClass: DspaceRestInterceptor,
156158
multi: true,
157159
},
160+
{
161+
provide: HashedFileMapping,
162+
useClass: BrowserHashedFileMapping,
163+
},
158164
// register the dynamic matcher used by form. MUST be provided by the app module
159165
...DYNAMIC_MATCHER_PROVIDERS,
160166

src/app/shared/theme-support/theme.service.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Inject,
44
Injectable,
55
Injector,
6+
Optional,
67
} from '@angular/core';
78
import {
89
ActivatedRouteSnapshot,
@@ -39,6 +40,7 @@ import {
3940
ThemeConfig,
4041
} from '../../../config/theme.config';
4142
import { environment } from '../../../environments/environment';
43+
import { HashedFileMapping } from '../../../modules/dynamic-hash/hashed-file-mapping';
4244
import { LinkService } from '../../core/cache/builders/link.service';
4345
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
4446
import { RemoteData } from '../../core/data/remote-data';
@@ -103,6 +105,7 @@ export class ThemeService {
103105
@Inject(GET_THEME_CONFIG_FOR_FACTORY) private gtcf: (str) => ThemeConfig,
104106
private router: Router,
105107
@Inject(DOCUMENT) private document: any,
108+
@Optional() private hashedFileMapping: HashedFileMapping,
106109
) {
107110
// Create objects from the theme configs in the environment file
108111
this.themes = environment.themes.map((themeConfig: ThemeConfig) => themeFactory(themeConfig, injector));
@@ -225,10 +228,14 @@ export class ThemeService {
225228
// automatically updated if we add nodes later
226229
const currentThemeLinks = Array.from(head.getElementsByClassName('theme-css'));
227230
const link = this.document.createElement('link');
231+
const themeCSS = `${encodeURIComponent(themeName)}-theme.css`;
228232
link.setAttribute('rel', 'stylesheet');
229233
link.setAttribute('type', 'text/css');
230234
link.setAttribute('class', 'theme-css');
231-
link.setAttribute('href', `${encodeURIComponent(themeName)}-theme.css`);
235+
link.setAttribute(
236+
'href',
237+
this.hashedFileMapping?.resolve(themeCSS) ?? themeCSS,
238+
);
232239
// wait for the new css to download before removing the old one to prevent a
233240
// flash of unstyled content
234241
link.onload = () => {

src/config/cache-config.interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export interface CacheConfig extends Config {
77
};
88
// Cache-Control HTTP Header
99
control: string;
10+
// These static files should not be cached (paths relative to dist/browser, including the leading slash)
11+
noCacheFiles: string[]
1012
autoSync: AutoSyncConfig;
1113
// In-memory caches of server-side rendered (SSR) content. These caches can be used to limit the frequency
1214
// of re-generating SSR pages to improve performance.

src/config/config.server.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import {
1717
isEmpty,
1818
isNotEmpty,
1919
} from '../app/shared/empty.util';
20+
import { ServerHashedFileMapping } from '../modules/dynamic-hash/hashed-file-mapping.server';
2021
import { AppConfig } from './app-config.interface';
22+
import { BuildConfig } from './build-config.interface';
2123
import { Config } from './config.interface';
2224
import { mergeConfig } from './config.util';
2325
import { DefaultAppConfig } from './default-app-config';
@@ -178,6 +180,7 @@ const buildBaseUrl = (config: ServerConfig): void => {
178180
}
179181
};
180182

183+
181184
/**
182185
* Build app config with the following chain of override.
183186
*
@@ -188,7 +191,7 @@ const buildBaseUrl = (config: ServerConfig): void => {
188191
* @param destConfigPath optional path to save config file
189192
* @returns app config
190193
*/
191-
export const buildAppConfig = (destConfigPath?: string): AppConfig => {
194+
export const buildAppConfig = (destConfigPath?: string, mapping?: ServerHashedFileMapping): AppConfig => {
192195
// start with default app config
193196
const appConfig: AppConfig = new DefaultAppConfig();
194197

@@ -256,7 +259,21 @@ export const buildAppConfig = (destConfigPath?: string): AppConfig => {
256259
buildBaseUrl(appConfig.rest);
257260

258261
if (isNotEmpty(destConfigPath)) {
259-
writeFileSync(destConfigPath, JSON.stringify(appConfig, null, 2));
262+
const content = JSON.stringify(appConfig, null, 2);
263+
264+
writeFileSync(destConfigPath, content);
265+
if (mapping !== undefined) {
266+
mapping.add(destConfigPath, content);
267+
if (!(appConfig as BuildConfig).ssr?.enabled) {
268+
// If we're serving for CSR we can retrieve the configuration before JS is loaded/executed
269+
mapping.addHeadLink({
270+
path: destConfigPath,
271+
rel: 'preload',
272+
as: 'fetch',
273+
crossorigin: 'anonymous',
274+
});
275+
}
276+
}
260277

261278
console.log(`Angular ${bold('config.json')} file generated correctly at ${bold(destConfigPath)} \n`);
262279
}

src/config/default-app-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ export class DefaultAppConfig implements AppConfig {
8282
},
8383
// Cache-Control HTTP Header
8484
control: 'max-age=604800', // revalidate browser
85+
// These static files should not be cached (paths relative to dist/browser, including the leading slash)
86+
noCacheFiles: [
87+
'/index.html', // see https://web.dev/articles/http-cache#unversioned-urls
88+
],
8589
autoSync: {
8690
defaultTime: 0,
8791
maxBufferSize: 100,

src/config/theme.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export interface NamedThemeConfig extends Config {
1313
* A list of HTML tags that should be added to the HEAD section of the document, whenever this theme is active.
1414
*/
1515
headTags?: HeadTagConfig[];
16+
17+
/**
18+
* Whether this theme's CSS should be prefetched in CSR mode
19+
*/
20+
prefetch?: boolean;
1621
}
1722

1823
/**

0 commit comments

Comments
 (0)