Skip to content

Commit c6bf2b7

Browse files
committed
Don't hash or cache the index file
This didn't work well in one specific case: requests without a path (e.g. https://demo.dspace.org) Such requests would result in a 304 redirect directly to index.html, losing the hashed file mapping and causing it to get cached. Then it could remain in the cache across rebuilds. Solutions: - Don't try to hash index.html, but modify it in place - Introduce configuration to disable caching for specific static files and apply this to index.html to prevent similar problems - Don't let browsers cache index.html when it's served for CSR under another path
1 parent 928e932 commit c6bf2b7

File tree

8 files changed

+58
-18
lines changed

8 files changed

+58
-18
lines changed

config/config.example.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ cache:
3838
# NOTE: When updates are made to compiled *.js files, it will automatically bypass this browser cache, because
3939
# all compiled *.js files include a unique hash in their name which updates when content is modified.
4040
control: max-age=604800 # revalidate browser
41+
# These static files should not be cached (paths relative to dist/browser, including the leading slash)
42+
noCacheFiles:
43+
- '/index.html'
4144
autoSync:
4245
defaultTime: 0
4346
maxBufferSize: 100

server.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ function ngApp(req, res) {
261261
*/
262262
function serverSideRender(req, res, sendToUser: boolean = true) {
263263
// Render the page via SSR (server side rendering)
264-
res.render(hashedFileMapping.resolve(indexHtml), {
264+
res.render(indexHtml, {
265265
req,
266266
res,
267267
preboot: environment.universal.preboot,
@@ -303,7 +303,11 @@ function serverSideRender(req, res, sendToUser: boolean = true) {
303303
* @param res current response
304304
*/
305305
function clientSideRender(req, res) {
306-
res.sendFile(hashedFileMapping.resolve(indexHtml));
306+
res.sendFile(indexHtml, {
307+
headers: {
308+
'Cache-Control': 'no-cache, no-store',
309+
},
310+
});
307311
}
308312

309313

@@ -314,7 +318,11 @@ function clientSideRender(req, res) {
314318
*/
315319
function addCacheControl(req, res, next) {
316320
// instruct browser to revalidate
317-
res.header('Cache-Control', environment.cache.control || 'max-age=604800');
321+
if (environment.cache.noCacheFiles.includes(req.originalUrl)) {
322+
res.header('Cache-Control', 'no-cache, no-store');
323+
} else {
324+
res.header('Cache-Control', environment.cache.control || 'max-age=604800');
325+
}
318326
next();
319327
}
320328

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { of as observableOf } from 'rxjs';
22
import { TestBed } from '@angular/core/testing';
33
import { provideMockActions } from '@ngrx/effects/testing';
4-
import { HashedFileMapping } from '../../../modules/dynamic-hash/hashed-file-mapping';
54
import { LinkService } from '../../core/cache/builders/link.service';
65
import { hot } from 'jasmine-marbles';
76
import { SetThemeAction } from './theme.actions';

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: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ 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 { environment } from '../environments/environment';
65
import { ServerHashedFileMapping } from '../modules/dynamic-hash/hashed-file-mapping.server';
76

87
import { AppConfig } from './app-config.interface';
@@ -175,7 +174,7 @@ const buildBaseUrl = (config: ServerConfig): void => {
175174
*/
176175
export const buildAppConfig = (destConfigPath?: string, mapping?: ServerHashedFileMapping): AppConfig => {
177176
// start with default app config
178-
const appConfig: BuildConfig = new DefaultAppConfig() as BuildConfig;
177+
const appConfig: AppConfig = new DefaultAppConfig();
179178

180179
// determine which dist app config by environment
181180
const env = getEnvironment();
@@ -245,9 +244,14 @@ export const buildAppConfig = (destConfigPath?: string, mapping?: ServerHashedFi
245244
writeFileSync(destConfigPath, content);
246245
if (mapping !== undefined) {
247246
mapping.add(destConfigPath, content);
248-
if (!appConfig.universal.preboot) {
247+
if (!(appConfig as BuildConfig).universal?.preboot) {
249248
// If we're serving for CSR we can retrieve the configuration before JS is loaded/executed
250-
mapping.addHeadLink(destConfigPath, 'preload', 'fetch', 'anonymous');
249+
mapping.addHeadLink({
250+
path: destConfigPath,
251+
rel: 'preload',
252+
as: 'fetch',
253+
crossorigin: 'anonymous',
254+
});
251255
}
252256
}
253257

src/config/default-app-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ export class DefaultAppConfig implements AppConfig {
7070
},
7171
// Cache-Control HTTP Header
7272
control: 'max-age=604800', // revalidate browser
73+
// These static files should not be cached (paths relative to dist/browser, including the leading slash)
74+
noCacheFiles: [
75+
'/index.html', // see https://web.dev/articles/http-cache#unversioned-urls
76+
],
7377
autoSync: {
7478
defaultTime: 0,
7579
maxBufferSize: 100,

src/environments/environment.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export const environment: BuildConfig = {
5151
},
5252
// msToLive: 1000, // 15 minutes
5353
control: 'max-age=60',
54+
// These static files should not be cached (paths relative to dist/browser, including the leading slash)
55+
noCacheFiles: [
56+
'/index.html', // see https://web.dev/articles/http-cache#unversioned-urls
57+
],
5458
autoSync: {
5559
defaultTime: 0,
5660
maxBufferSize: 100,

src/modules/dynamic-hash/hashed-file-mapping.server.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ import {
2828
ID,
2929
} from './hashed-file-mapping';
3030

31+
const HEAD_LINK_CLASS = 'hfm';
32+
33+
interface HeadLink {
34+
path: string;
35+
rel: string;
36+
as: string;
37+
crossorigin?: string;
38+
}
39+
3140
/**
3241
* Server-side implementation of {@link HashedFileMapping}.
3342
* Registers dynamically hashed files and stores them in index.html for the browser to use.
@@ -36,7 +45,7 @@ export class ServerHashedFileMapping extends HashedFileMapping {
3645
public readonly indexPath: string;
3746
private readonly indexContent: string;
3847

39-
protected readonly headLinks: Set<string> = new Set();
48+
protected readonly headLinks: Set<HeadLink> = new Set();
4049

4150
constructor(
4251
private readonly root: string,
@@ -113,7 +122,11 @@ export class ServerHashedFileMapping extends HashedFileMapping {
113122

114123
// We know this CSS is likely needed, so wecan avoid a FOUC by retrieving it in advance
115124
// Angular does the same for global styles, but doesn't "know" about out themes
116-
this.addHeadLink(p, 'prefetch', 'style');
125+
this.addHeadLink({
126+
path: p,
127+
rel: 'prefetch',
128+
as: 'style',
129+
});
117130

118131
this.ensureCompressedFilesAssumingUnchangedContent(p, hp, '.br');
119132
this.ensureCompressedFilesAssumingUnchangedContent(p, hp, '.gz');
@@ -123,14 +136,17 @@ export class ServerHashedFileMapping extends HashedFileMapping {
123136
/**
124137
* Include a head link for a given resource to the index HTML.
125138
*/
126-
addHeadLink(path: string, rel: string, as: string, crossorigin?: string) {
127-
const href = relative(this.root, this.resolve(path));
139+
addHeadLink(headLink: HeadLink) {
140+
this.headLinks.add(headLink);
141+
}
128142

129-
if (hasValue(crossorigin)) {
130-
this.headLinks.add(`<link rel="${rel}" as="${as}" crossorigin="${crossorigin}" href="${href}">`);
143+
private renderHeadLink(link: HeadLink): string {
144+
const href = relative(this.root, this.resolve(link.path));
131145

146+
if (hasValue(link.crossorigin)) {
147+
return `<link rel="${link.rel}" as="${link.as}" href="${href}" crossorigin="${link.crossorigin}" class="${HEAD_LINK_CLASS}">`;
132148
} else {
133-
this.headLinks.add(`<link rel="${rel}" as="${as}" href="${href}">`);
149+
return `<link rel="${link.rel}" as="${link.as}" href="${href}" class="${HEAD_LINK_CLASS}">`;
134150
}
135151
}
136152

@@ -155,15 +171,15 @@ export class ServerHashedFileMapping extends HashedFileMapping {
155171
}, {});
156172

157173
let root = parse(this.indexContent);
158-
root.querySelector(`script#${ID}`)?.remove();
174+
root.querySelectorAll(`script#${ID}, link.${HEAD_LINK_CLASS}`)?.forEach(e => e.remove());
159175
root.querySelector('head')
160176
.appendChild(`<script id="${ID}" type="application/json">${JSON.stringify(out)}</script>` as any);
161177

162178
for (const headLink of this.headLinks) {
163179
root.querySelector('head')
164-
.appendChild(headLink as any);
180+
.appendChild(this.renderHeadLink(headLink) as any);
165181
}
166182

167-
this.add(this.indexPath, root.toString());
183+
writeFileSync(this.indexPath, root.toString());
168184
}
169185
}

0 commit comments

Comments
 (0)