Skip to content

Commit 51a37cc

Browse files
authored
Merge pull request DSpace#4936 from atmire/cache-bust-dynamic-configuration_contribute-main
Cache-bust dynamic configuration files and theme CSS (port to 10.0)
2 parents 825b39f + 305be9d commit 51a37cc

File tree

15 files changed

+372
-13
lines changed

15 files changed

+372
-13
lines changed

config/config.example.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ cache:
9393
# NOTE: When updates are made to compiled *.js files, it will automatically bypass this browser cache, because
9494
# all compiled *.js files include a unique hash in their name which updates when content is modified.
9595
control: max-age=604800 # revalidate browser
96+
# These static files should not be cached (paths relative to dist/browser, including the leading slash)
97+
noCacheFiles:
98+
- '/index.html'
9699
autoSync:
97100
defaultTime: 0
98101
maxBufferSize: 100
@@ -516,6 +519,7 @@ themes:
516519
# - name: BASE_THEME_NAME
517520
#
518521
- name: dspace
522+
prefetch: true
519523
headTags:
520524
- tagName: link
521525
attributes:

package-lock.json

Lines changed: 48 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
toClientConfig,
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 '@dspace/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>;
@@ -333,7 +338,7 @@ function clientSideRender(req, res) {
333338
html = html.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
334339
}
335340

336-
res.send(html);
341+
res.set('Cache-Control', 'no-cache, no-store').send(html);
337342
}
338343

339344

@@ -344,7 +349,11 @@ function clientSideRender(req, res) {
344349
*/
345350
function addCacheControl(req, res, next) {
346351
// instruct browser to revalidate
347-
res.header('Cache-Control', environment.cache.control || 'max-age=604800');
352+
if (environment.cache.noCacheFiles.includes(req.originalUrl)) {
353+
res.header('Cache-Control', 'no-cache, no-store');
354+
} else {
355+
res.header('Cache-Control', environment.cache.control || 'max-age=604800');
356+
}
348357
next();
349358
}
350359

src/app/app.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
4646
import { provideEnvironmentNgxMask } from 'ngx-mask';
4747

4848
import { environment } from '../environments/environment';
49+
import { HashedFileMapping } from '../modules/dynamic-hash/hashed-file-mapping';
50+
import { BrowserHashedFileMapping } from '../modules/dynamic-hash/hashed-file-mapping.browser';
4951
import { appEffects } from './app.effects';
5052
import { MENUS } from './app.menus';
5153
import {
@@ -153,6 +155,10 @@ export const commonAppConfig: ApplicationConfig = {
153155
useClass: DspaceRestInterceptor,
154156
multi: true,
155157
},
158+
{
159+
provide: HashedFileMapping,
160+
useClass: BrowserHashedFileMapping,
161+
},
156162
// register the dynamic matcher used by form. MUST be provided by the app module
157163
...DYNAMIC_MATCHER_PROVIDERS,
158164

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,
@@ -62,6 +63,7 @@ import {
6263
} from 'rxjs/operators';
6364

6465
import { environment } from '../../../environments/environment';
66+
import { HashedFileMapping } from '../../../modules/dynamic-hash/hashed-file-mapping';
6567
import { GET_THEME_CONFIG_FOR_FACTORY } from '../object-collection/shared/listable-object/listable-object.decorator';
6668
import {
6769
SetThemeAction,
@@ -105,6 +107,7 @@ export class ThemeService {
105107
@Inject(GET_THEME_CONFIG_FOR_FACTORY) private gtcf: (str) => ThemeConfig,
106108
private router: Router,
107109
@Inject(DOCUMENT) private document: any,
110+
@Optional() private hashedFileMapping: HashedFileMapping,
108111
@Inject(APP_CONFIG) private appConfig: BuildConfig,
109112
) {
110113
// Create objects from the theme configs in the environment file
@@ -228,10 +231,14 @@ export class ThemeService {
228231
// automatically updated if we add nodes later
229232
const currentThemeLinks = Array.from(head.getElementsByClassName('theme-css'));
230233
const link = this.document.createElement('link');
234+
const themeCSS = `${encodeURIComponent(themeName)}-theme.css`;
231235
link.setAttribute('rel', 'stylesheet');
232236
link.setAttribute('type', 'text/css');
233237
link.setAttribute('class', 'theme-css');
234-
link.setAttribute('href', `${encodeURIComponent(themeName)}-theme.css`);
238+
link.setAttribute(
239+
'href',
240+
this.hashedFileMapping?.resolve(themeCSS) ?? themeCSS,
241+
);
235242
// wait for the new css to download before removing the old one to prevent a
236243
// flash of unstyled content
237244
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: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from 'node:fs';
66
import { join } from 'node:path';
77

8+
import { BuildConfig } from '@dspace/config/build-config.interface';
89
import {
910
isEmpty,
1011
isNotEmpty,
@@ -17,6 +18,7 @@ import {
1718
} from 'colors';
1819
import { load } from 'js-yaml';
1920

21+
import { ServerHashedFileMapping } from '../modules/dynamic-hash/hashed-file-mapping.server';
2022
import {
2123
AppConfig,
2224
toClientConfig,
@@ -191,7 +193,7 @@ const buildBaseUrl = (config: ServerConfig): void => {
191193
* @param destConfigPath optional path to save config file
192194
* @returns app config
193195
*/
194-
export const buildAppConfig = (destConfigPath?: string): AppConfig => {
196+
export const buildAppConfig = (destConfigPath?: string, mapping?: ServerHashedFileMapping): AppConfig => {
195197
// start with default app config
196198
const appConfig: AppConfig = new DefaultAppConfig();
197199

@@ -260,7 +262,20 @@ export const buildAppConfig = (destConfigPath?: string): AppConfig => {
260262

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

265280
console.info(`Angular ${bold('config.json')} file generated correctly at ${bold(destConfigPath)} \n`);
266281
}

src/config/default-app-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ export class DefaultAppConfig implements AppConfig {
9292
},
9393
// Cache-Control HTTP Header
9494
control: 'max-age=604800', // revalidate browser
95+
// These static files should not be cached (paths relative to dist/browser, including the leading slash)
96+
noCacheFiles: [
97+
'/index.html', // see https://web.dev/articles/http-cache#unversioned-urls
98+
],
9599
autoSync: {
96100
defaultTime: 0,
97101
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)