Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ cache:
# NOTE: When updates are made to compiled *.js files, it will automatically bypass this browser cache, because
# all compiled *.js files include a unique hash in their name which updates when content is modified.
control: max-age=604800 # revalidate browser
# These static files should not be cached (paths relative to dist/browser, including the leading slash)
noCacheFiles:
- '/index.html'
autoSync:
defaultTime: 0
maxBufferSize: 100
Expand Down Expand Up @@ -441,6 +444,7 @@ themes:
# - name: BASE_THEME_NAME
#
- name: dspace
prefetch: true
headTags:
- tagName: link
attributes:
Expand Down
55 changes: 48 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
"ngx-pagination": "6.0.3",
"ngx-skeleton-loader": "^11.3.0",
"ngx-ui-switch": "^16.1.0",
"node-html-parser": "^7.0.1",
"nouislider": "^15.7.1",
"orejime": "^2.3.1",
"pem": "1.14.8",
Expand Down
15 changes: 12 additions & 3 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
AppConfig,
} from './src/config/app-config.interface';
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
import { ServerHashedFileMapping } from './src/modules/dynamic-hash/hashed-file-mapping.server';
import { logStartupMessage } from './startup-message';
import { TOKENITEM } from '@dspace/core/auth/models/auth-token-info.model';
import { CommonEngine } from '@angular/ssr/node';
Expand All @@ -68,7 +69,11 @@ const indexHtml = join(DIST_FOLDER, 'index.html');

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

const appConfig: AppConfig = buildAppConfig(join(DIST_FOLDER, 'assets/config.json'));
const configJson = join(DIST_FOLDER, 'assets/config.json');
const hashedFileMapping = new ServerHashedFileMapping(DIST_FOLDER, 'index.html');
const appConfig: AppConfig = buildAppConfig(configJson, hashedFileMapping);
appConfig.themes.forEach(themeConfig => hashedFileMapping.addThemeStyle(themeConfig.name, themeConfig.prefetch));
hashedFileMapping.save();

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

res.send(html);
res.set('Cache-Control', 'no-cache, no-store').send(html);
}


Expand All @@ -335,7 +340,11 @@ function clientSideRender(req, res) {
*/
function addCacheControl(req, res, next) {
// instruct browser to revalidate
res.header('Cache-Control', environment.cache.control || 'max-age=604800');
if (environment.cache.noCacheFiles.includes(req.originalUrl)) {
res.header('Cache-Control', 'no-cache, no-store');
} else {
res.header('Cache-Control', environment.cache.control || 'max-age=604800');
}
next();
}

Expand Down
9 changes: 8 additions & 1 deletion src/app/shared/theme-support/theme.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Inject,
Injectable,
Injector,
Optional,
} from '@angular/core';
import {
ActivatedRouteSnapshot,
Expand Down Expand Up @@ -62,6 +63,7 @@ import {
} from 'rxjs/operators';

import { environment } from '../../../environments/environment';
import { HashedFileMapping } from '../../../modules/dynamic-hash/hashed-file-mapping';
import { GET_THEME_CONFIG_FOR_FACTORY } from '../object-collection/shared/listable-object/listable-object.decorator';
import {
SetThemeAction,
Expand Down Expand Up @@ -105,6 +107,7 @@ export class ThemeService {
@Inject(GET_THEME_CONFIG_FOR_FACTORY) private gtcf: (str) => ThemeConfig,
private router: Router,
@Inject(DOCUMENT) private document: any,
@Optional() private hashedFileMapping: HashedFileMapping,
@Inject(APP_CONFIG) private appConfig: BuildConfig,
) {
// Create objects from the theme configs in the environment file
Expand Down Expand Up @@ -228,10 +231,14 @@ export class ThemeService {
// automatically updated if we add nodes later
const currentThemeLinks = Array.from(head.getElementsByClassName('theme-css'));
const link = this.document.createElement('link');
const themeCSS = `${encodeURIComponent(themeName)}-theme.css`;
link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css');
link.setAttribute('class', 'theme-css');
link.setAttribute('href', `${encodeURIComponent(themeName)}-theme.css`);
link.setAttribute(
'href',
this.hashedFileMapping?.resolve(themeCSS) ?? themeCSS,
);
// wait for the new css to download before removing the old one to prevent a
// flash of unstyled content
link.onload = () => {
Expand Down
2 changes: 2 additions & 0 deletions src/config/cache-config.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export interface CacheConfig extends Config {
};
// Cache-Control HTTP Header
control: string;
// These static files should not be cached (paths relative to dist/browser, including the leading slash)
noCacheFiles: string[]
autoSync: AutoSyncConfig;
// In-memory caches of server-side rendered (SSR) content. These caches can be used to limit the frequency
// of re-generating SSR pages to improve performance.
Expand Down
20 changes: 18 additions & 2 deletions src/config/config.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import {
} from 'colors';
import { load } from 'js-yaml';

import { ServerHashedFileMapping } from '../modules/dynamic-hash/hashed-file-mapping.server';
import { AppConfig } from './app-config.interface';
import { BuildConfig } from './build-config.interface';
import { Config } from './config.interface';
import { mergeConfig } from './config.util';
import { DefaultAppConfig } from './default-app-config';
Expand Down Expand Up @@ -179,7 +181,7 @@ const buildBaseUrl = (config: ServerConfig): void => {
* @param destConfigPath optional path to save config file
* @returns app config
*/
export const buildAppConfig = (destConfigPath?: string): AppConfig => {
export const buildAppConfig = (destConfigPath?: string, mapping?: ServerHashedFileMapping): AppConfig => {
// start with default app config
const appConfig: AppConfig = new DefaultAppConfig();

Expand Down Expand Up @@ -247,7 +249,21 @@ export const buildAppConfig = (destConfigPath?: string): AppConfig => {
buildBaseUrl(appConfig.rest);

if (isNotEmpty(destConfigPath)) {
writeFileSync(destConfigPath, JSON.stringify(appConfig, null, 2));
const content = JSON.stringify(appConfig, null, 2);

writeFileSync(destConfigPath, content);
if (mapping !== undefined) {
mapping.add(destConfigPath, content);
if (!(appConfig as BuildConfig).ssr?.enabled) {
// If we're serving for CSR we can retrieve the configuration before JS is loaded/executed
mapping.addHeadLink({
path: destConfigPath,
rel: 'preload',
as: 'fetch',
crossorigin: 'anonymous',
});
}
}

console.log(`Angular ${bold('config.json')} file generated correctly at ${bold(destConfigPath)} \n`);
}
Expand Down
4 changes: 4 additions & 0 deletions src/config/default-app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ export class DefaultAppConfig implements AppConfig {
},
// Cache-Control HTTP Header
control: 'max-age=604800', // revalidate browser
// These static files should not be cached (paths relative to dist/browser, including the leading slash)
noCacheFiles: [
'/index.html', // see https://web.dev/articles/http-cache#unversioned-urls
],
autoSync: {
defaultTime: 0,
maxBufferSize: 100,
Expand Down
5 changes: 5 additions & 0 deletions src/config/theme.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export interface NamedThemeConfig extends Config {
* A list of HTML tags that should be added to the HEAD section of the document, whenever this theme is active.
*/
headTags?: HeadTagConfig[];

/**
* Whether this theme's CSS should be prefetched in CSR mode
*/
prefetch?: boolean;
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/environments/environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export const environment: BuildConfig = {
},
// msToLive: 1000, // 15 minutes
control: 'max-age=60',
// These static files should not be cached (paths relative to dist/browser, including the leading slash)
noCacheFiles: [
'/index.html', // see https://web.dev/articles/http-cache#unversioned-urls
],
autoSync: {
defaultTime: 0,
maxBufferSize: 100,
Expand Down
4 changes: 3 additions & 1 deletion src/main.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { extendEnvironmentWithAppConfig } from '@dspace/config/config.util';
import { AppComponent } from './app/app.component';
import { environment } from './environments/environment';
import { browserAppConfig } from './modules/app/browser-app.config';
import { BrowserHashedFileMapping } from './modules/dynamic-hash/hashed-file-mapping.browser';

const hashedFileMapping = new BrowserHashedFileMapping(document);
/*const bootstrap = () => platformBrowserDynamic()
.bootstrapModule(BrowserAppModule, {});*/
const bootstrap = () => bootstrapApplication(AppComponent, browserAppConfig);
Expand All @@ -33,7 +35,7 @@ const main = () => {
return bootstrap();
} else {
// Configuration must be fetched explicitly
return fetch('assets/config.json')
return fetch(hashedFileMapping.resolve('assets/config.json'))
.then((response) => response.json())
.then((config: AppConfig) => {
// extend environment with app config for browser when not prerendered
Expand Down
46 changes: 46 additions & 0 deletions src/modules/dynamic-hash/hashed-file-mapping.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import { DOCUMENT } from '@angular/common';
import {
Inject,
Injectable,
Optional,
} from '@angular/core';
import { hasValue } from '@dspace/shared/utils/empty.util';
import isObject from 'lodash/isObject';
import isString from 'lodash/isString';

import {
HashedFileMapping,
ID,
} from './hashed-file-mapping';

/**
* Client-side implementation of {@link HashedFileMapping}.
* Reads out the mapping from index.html before the app is bootstrapped.
* Afterwards, {@link resolve} can be used to grab the latest file.
*/
@Injectable()
export class BrowserHashedFileMapping extends HashedFileMapping {
constructor(
@Optional() @Inject(DOCUMENT) protected document: any,
) {
super();
const element = document?.querySelector(`script#${ID}`);

if (hasValue(element?.textContent)) {
const mapping = JSON.parse(element.textContent);

if (isObject(mapping)) {
Object.entries(mapping)
.filter(([key, value]) => isString(key) && isString(value))
.forEach(([plainPath, hashPath]) => this.map.set(plainPath, hashPath));
}
}
}
}
Loading
Loading