Skip to content

Commit 23a8b97

Browse files
authored
Merge pull request #3993 from ybnd/cache-bust-dynamic-configuration-7.6
[Port dspace-7_x] Cache-bust dynamic configuration files and theme CSS
2 parents 26a6b51 + 5f27668 commit 23a8b97

15 files changed

Lines changed: 354 additions & 10 deletions

config/config.example.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ cache:
8787
# NOTE: When updates are made to compiled *.js files, it will automatically bypass this browser cache, because
8888
# all compiled *.js files include a unique hash in their name which updates when content is modified.
8989
control: max-age=604800 # revalidate browser
90+
# These static files should not be cached (paths relative to dist/browser, including the leading slash)
91+
noCacheFiles:
92+
- '/index.html'
9093
autoSync:
9194
defaultTime: 0
9295
maxBufferSize: 100
@@ -382,6 +385,7 @@ themes:
382385
# - name: BASE_THEME_NAME
383386
#
384387
- name: dspace
388+
prefetch: true
385389
headTags:
386390
- tagName: link
387391
attributes:

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"ngx-skeleton-loader": "^7.0.0",
112112
"ngx-sortablejs": "^11.1.0",
113113
"ngx-ui-switch": "^14.1.0",
114+
"node-html-parser": "^7.0.1",
114115
"nouislider": "^15.8.1",
115116
"pem": "1.14.8",
116117
"reflect-metadata": "^0.2.2",

server.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { ServerAppModule } from './src/main.server';
5252
import { buildAppConfig } from './src/config/config.server';
5353
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
5454
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
55+
import { ServerHashedFileMapping } from './src/modules/dynamic-hash/hashed-file-mapping.server';
5556
import { logStartupMessage } from './startup-message';
5657
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
5758
import { SsrExcludePatterns } from './src/config/universal-config.interface';
@@ -68,7 +69,11 @@ const indexHtml = join(DIST_FOLDER, 'index.html');
6869

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

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

7378
// cache of SSR pages for known bots, only enabled in production mode
7479
let botCache: LRU<string, any>;
@@ -319,7 +324,7 @@ function clientSideRender(req, res) {
319324
html = html.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
320325
}
321326

322-
res.send(html);
327+
res.set('Cache-Control', 'no-cache, no-store').send(html);
323328
}
324329

325330

@@ -330,7 +335,11 @@ function clientSideRender(req, res) {
330335
*/
331336
function addCacheControl(req, res, next) {
332337
// instruct browser to revalidate
333-
res.header('Cache-Control', environment.cache.control || 'max-age=604800');
338+
if (environment.cache.noCacheFiles.includes(req.originalUrl)) {
339+
res.header('Cache-Control', 'no-cache, no-store');
340+
} else {
341+
res.header('Cache-Control', environment.cache.control || 'max-age=604800');
342+
}
334343
next();
335344
}
336345

src/app/app.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/sto
1010
import { TranslateModule } from '@ngx-translate/core';
1111
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
1212
import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core';
13+
import { HashedFileMapping } from '../modules/dynamic-hash/hashed-file-mapping';
14+
import { BrowserHashedFileMapping } from '../modules/dynamic-hash/hashed-file-mapping.browser';
1315

1416
import { AppRoutingModule } from './app-routing.module';
1517
import { AppComponent } from './app.component';
@@ -110,6 +112,10 @@ const PROVIDERS = [
110112
useClass: DspaceRestInterceptor,
111113
multi: true
112114
},
115+
{
116+
provide: HashedFileMapping,
117+
useClass: BrowserHashedFileMapping,
118+
},
113119
// register the dynamic matcher used by form. MUST be provided by the app module
114120
...DYNAMIC_MATCHER_PROVIDERS,
115121
];

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { Injectable, Inject, Injector } from '@angular/core';
1+
import {
2+
Injectable,
3+
Inject,
4+
Injector,
5+
Optional,
6+
} from '@angular/core';
27
import { Store, createFeatureSelector, createSelector, select } from '@ngrx/store';
38
import { BehaviorSubject, EMPTY, Observable, of as observableOf, from, concatMap } from 'rxjs';
9+
import { HashedFileMapping } from '../../../modules/dynamic-hash/hashed-file-mapping';
410
import { ThemeState } from './theme.reducer';
511
import { SetThemeAction, ThemeActionTypes } from './theme.actions';
612
import { defaultIfEmpty, expand, filter, map, switchMap, take, toArray } from 'rxjs/operators';
@@ -54,6 +60,7 @@ export class ThemeService {
5460
@Inject(GET_THEME_CONFIG_FOR_FACTORY) private gtcf: (str) => ThemeConfig,
5561
private router: Router,
5662
@Inject(DOCUMENT) private document: any,
63+
@Optional() private hashedFileMapping: HashedFileMapping,
5764
) {
5865
// Create objects from the theme configs in the environment file
5966
this.themes = environment.themes.map((themeConfig: ThemeConfig) => themeFactory(themeConfig, injector));
@@ -176,10 +183,14 @@ export class ThemeService {
176183
// automatically updated if we add nodes later
177184
const currentThemeLinks = Array.from(head.getElementsByClassName('theme-css'));
178185
const link = this.document.createElement('link');
186+
const themeCSS = `${encodeURIComponent(themeName)}-theme.css`;
179187
link.setAttribute('rel', 'stylesheet');
180188
link.setAttribute('type', 'text/css');
181189
link.setAttribute('class', 'theme-css');
182-
link.setAttribute('href', `${encodeURIComponent(themeName)}-theme.css`);
190+
link.setAttribute(
191+
'href',
192+
this.hashedFileMapping?.resolve(themeCSS) ?? themeCSS,
193+
);
183194
// wait for the new css to download before removing the old one to prevent a
184195
// flash of unstyled content
185196
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
@@ -2,8 +2,10 @@ import { red, blue, green, bold } from 'colors';
22
import { existsSync, readFileSync, writeFileSync } from 'fs';
33
import { load } from 'js-yaml';
44
import { join } from 'path';
5+
import { ServerHashedFileMapping } from '../modules/dynamic-hash/hashed-file-mapping.server';
56

67
import { AppConfig } from './app-config.interface';
8+
import { BuildConfig } from './build-config.interface';
79
import { Config } from './config.interface';
810
import { DefaultAppConfig } from './default-app-config';
911
import { ServerConfig } from './server-config.interface';
@@ -165,6 +167,7 @@ const buildBaseUrl = (config: ServerConfig): void => {
165167
}
166168
};
167169

170+
168171
/**
169172
* Build app config with the following chain of override.
170173
*
@@ -175,7 +178,7 @@ const buildBaseUrl = (config: ServerConfig): void => {
175178
* @param destConfigPath optional path to save config file
176179
* @returns app config
177180
*/
178-
export const buildAppConfig = (destConfigPath?: string): AppConfig => {
181+
export const buildAppConfig = (destConfigPath?: string, mapping?: ServerHashedFileMapping): AppConfig => {
179182
// start with default app config
180183
const appConfig: AppConfig = new DefaultAppConfig();
181184

@@ -243,7 +246,21 @@ export const buildAppConfig = (destConfigPath?: string): AppConfig => {
243246
buildBaseUrl(appConfig.rest);
244247

245248
if (isNotEmpty(destConfigPath)) {
246-
writeFileSync(destConfigPath, JSON.stringify(appConfig, null, 2));
249+
const content = JSON.stringify(appConfig, null, 2);
250+
251+
writeFileSync(destConfigPath, content);
252+
if (mapping !== undefined) {
253+
mapping.add(destConfigPath, content);
254+
if (!(appConfig as BuildConfig).universal?.preboot) {
255+
// If we're serving for CSR we can retrieve the configuration before JS is loaded/executed
256+
mapping.addHeadLink({
257+
path: destConfigPath,
258+
rel: 'preload',
259+
as: 'fetch',
260+
crossorigin: 'anonymous',
261+
});
262+
}
263+
}
247264

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

src/config/default-app-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ export class DefaultAppConfig implements AppConfig {
7676
},
7777
// Cache-Control HTTP Header
7878
control: 'max-age=604800', // revalidate browser
79+
// These static files should not be cached (paths relative to dist/browser, including the leading slash)
80+
noCacheFiles: [
81+
'/index.html', // see https://web.dev/articles/http-cache#unversioned-urls
82+
],
7983
autoSync: {
8084
defaultTime: 0,
8185
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
/**

src/environments/environment.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ export const environment: BuildConfig = {
7575
},
7676
// msToLive: 1000, // 15 minutes
7777
control: 'max-age=60',
78+
// These static files should not be cached (paths relative to dist/browser, including the leading slash)
79+
noCacheFiles: [
80+
'/index.html', // see https://web.dev/articles/http-cache#unversioned-urls
81+
],
7882
autoSync: {
7983
defaultTime: 0,
8084
maxBufferSize: 100,

0 commit comments

Comments
 (0)