From 9a433b50ff8f9e610fe0ba242d035b3f5c8a2f28 Mon Sep 17 00:00:00 2001 From: William Welling <8352733+wwelling@users.noreply.github.com> Date: Thu, 5 May 2022 16:13:25 -0500 Subject: [PATCH] support base path --- package.json | 6 ++-- scripts/base-href.ts | 36 +++++++++++++++++++ server.ts | 32 +++++++++++++---- src/app/app.component.spec.ts | 2 +- src/app/app.component.ts | 2 +- src/app/app.module.ts | 14 ++++---- src/app/core/auth/auth.service.spec.ts | 8 ++--- src/app/core/auth/auth.service.ts | 4 +-- src/app/core/locale/locale.service.ts | 2 +- src/app/core/reload/reload.guard.spec.ts | 10 ++++-- src/app/core/reload/reload.guard.ts | 13 +++++-- .../page-internal-server-error.component.html | 2 +- .../link-menu-item.component.spec.ts | 3 +- .../menu-item/link-menu-item.component.ts | 3 +- .../notification.component.spec.ts | 2 +- src/index.csr.html | 2 +- src/styles/_bootstrap_variables.scss | 4 ++- .../home-news/home-news.component.scss | 4 ++- 18 files changed, 111 insertions(+), 38 deletions(-) create mode 100644 scripts/base-href.ts diff --git a/package.json b/package.json index 75e22b40f3..46ec0a3324 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,11 @@ "start:dev": "nodemon --exec \"cross-env NODE_ENV=development yarn run serve\"", "start:prod": "yarn run build:prod && cross-env NODE_ENV=production yarn run serve:ssr", "start:mirador:prod": "yarn run build:mirador && yarn run start:prod", - "serve": "ng serve -c development", + "preserve": "yarn base-href", + "serve": "ng serve --configuration development", "serve:ssr": "node dist/server/main", "analyze": "webpack-bundle-analyzer dist/browser/stats.json", - "build": "ng build -c development", + "build": "ng build --configuration development", "build:stats": "ng build --stats-json", "build:prod": "yarn run build:ssr", "build:ssr": "ng build --configuration production && ng run dspace-angular:server:production", @@ -37,6 +38,7 @@ "cypress:open": "cypress open", "cypress:run": "cypress run", "env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts", + "base-href": "ts-node --project ./tsconfig.ts-node.json scripts/base-href.ts", "check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./" }, "browser": { diff --git a/scripts/base-href.ts b/scripts/base-href.ts new file mode 100644 index 0000000000..aee547b46d --- /dev/null +++ b/scripts/base-href.ts @@ -0,0 +1,36 @@ +import * as fs from 'fs'; +import { join } from 'path'; + +import { AppConfig } from '../src/config/app-config.interface'; +import { buildAppConfig } from '../src/config/config.server'; + +/** + * Script to set baseHref as `ui.nameSpace` for development mode. Adds `baseHref` to angular.json build options. + * + * Usage (see package.json): + * + * yarn base-href + */ + +const appConfig: AppConfig = buildAppConfig(); + +const angularJsonPath = join(process.cwd(), 'angular.json'); + +if (!fs.existsSync(angularJsonPath)) { + console.error(`Error:\n${angularJsonPath} does not exist\n`); + process.exit(1); +} + +try { + const angularJson = require(angularJsonPath); + + const baseHref = `${appConfig.ui.nameSpace}${appConfig.ui.nameSpace.endsWith('/') ? '' : '/'}`; + + console.log(`Setting baseHref to ${baseHref} in angular.json`); + + angularJson.projects['dspace-angular'].architect.build.options.baseHref = baseHref; + + fs.writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n'); +} catch (e) { + console.error(e); +} diff --git a/server.ts b/server.ts index bc840fb73c..455eef6aef 100644 --- a/server.ts +++ b/server.ts @@ -66,6 +66,8 @@ extendEnvironmentWithAppConfig(environment, appConfig); // The Express app is exported so that it can be used by serverless Functions. export function app() { + const router = express.Router(); + /* * Create a new express application */ @@ -133,7 +135,11 @@ export function app() { /** * Proxy the sitemaps */ - server.use('/sitemap**', createProxyMiddleware({ target: `${environment.rest.baseUrl}/sitemaps`, changeOrigin: true })); + router.use('/sitemap**', createProxyMiddleware({ + target: `${environment.rest.baseUrl}/sitemaps`, + pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), + changeOrigin: true + })); /** * Checks if the rateLimiter property is present @@ -151,14 +157,16 @@ export function app() { /* * Serve static resources (images, i18n messages, …) */ - server.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false })); + router.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false })); /* * Fallthrough to the IIIF viewer (must be included in the build). */ - server.use('/iiif', express.static(IIIF_VIEWER, {index:false})); + router.use('/iiif', express.static(IIIF_VIEWER, { index: false })); // Register the ngApp callback function to handle incoming requests - server.get('*', ngApp); + router.get('*', ngApp); + + server.use(environment.ui.nameSpace, router); return server; } @@ -191,13 +199,25 @@ function ngApp(req, res) { if (hasValue(err)) { console.warn('Error details : ', err); } - res.sendFile(DIST_FOLDER + '/index.html'); + res.render(indexHtml, { + req, + providers: [{ + provide: APP_BASE_HREF, + useValue: req.baseUrl + }] + }); } }); } else { // If preboot is disabled, just serve the client console.log('Universal off, serving for direct CSR'); - res.sendFile(DIST_FOLDER + '/index.html'); + res.render(indexHtml, { + req, + providers: [{ + provide: APP_BASE_HREF, + useValue: req.baseUrl + }] + }); } } diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index a892e34a5a..01cc960323 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -187,7 +187,7 @@ describe('App component', () => { link.setAttribute('rel', 'stylesheet'); link.setAttribute('type', 'text/css'); link.setAttribute('class', 'theme-css'); - link.setAttribute('href', '/custom-theme.css'); + link.setAttribute('href', 'custom-theme.css'); expect(headSpy.appendChild).toHaveBeenCalledWith(link); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index db217ce161..29c742bd57 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -268,7 +268,7 @@ export class AppComponent implements OnInit, AfterViewInit { link.setAttribute('rel', 'stylesheet'); link.setAttribute('type', 'text/css'); link.setAttribute('class', 'theme-css'); - link.setAttribute('href', `/${encodeURIComponent(themeName)}-theme.css`); + link.setAttribute('href', `${encodeURIComponent(themeName)}-theme.css`); // wait for the new css to download before removing the old one to prevent a // flash of unstyled content link.onload = () => { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index c133efdd5c..50b25e0bfa 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,4 +1,4 @@ -import { APP_BASE_HREF, CommonModule } from '@angular/common'; +import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common'; import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import { APP_INITIALIZER, NgModule } from '@angular/core'; import { AbstractControl } from '@angular/forms'; @@ -66,9 +66,11 @@ export function getConfig() { return environment; } -export function getBase(appConfig: AppConfig) { - return appConfig.ui.nameSpace; -} +const getBaseHref = (document: Document, appConfig: AppConfig): string => { + const baseTag = document.querySelector('head > base'); + baseTag.setAttribute('href', `${appConfig.ui.nameSpace}${appConfig.ui.nameSpace.endsWith('/') ? '' : '/'}`); + return baseTag.getAttribute('href'); +}; export function getMetaReducers(appConfig: AppConfig): MetaReducer[] { return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers; @@ -107,8 +109,8 @@ const PROVIDERS = [ }, { provide: APP_BASE_HREF, - useFactory: getBase, - deps: [APP_CONFIG] + useFactory: getBaseHref, + deps: [DOCUMENT, APP_CONFIG] }, { provide: USER_PROVIDED_META_REDUCERS, diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index ced8bb94c8..77cc015958 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -368,25 +368,25 @@ describe('AuthService test', () => { it('should redirect to reload with redirect url', () => { authService.navigateToRedirectUrl('/collection/123'); // Reload with redirect URL set to /collection/123 - expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/collection/123')))); + expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('reload/[0-9]*\\?redirect=' + encodeURIComponent('/collection/123')))); }); it('should redirect to reload with /home', () => { authService.navigateToRedirectUrl('/home'); // Reload with redirect URL set to /home - expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/home')))); + expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('reload/[0-9]*\\?redirect=' + encodeURIComponent('/home')))); }); it('should redirect to regular reload and not to /login', () => { authService.navigateToRedirectUrl('/login'); // Reload without a redirect URL - expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*(?!\\?)$'))); + expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('reload/[0-9]*(?!\\?)$'))); }); it('should redirect to regular reload when no redirect url is found', () => { authService.navigateToRedirectUrl(undefined); // Reload without a redirect URL - expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*(?!\\?)$'))); + expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('reload/[0-9]*(?!\\?)$'))); }); describe('impersonate', () => { diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index f89fa21681..5ab0edf64f 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -447,8 +447,8 @@ export class AuthService { */ public navigateToRedirectUrl(redirectUrl: string) { // Don't do redirect if already on reload url - if (!hasValue(redirectUrl) || !redirectUrl.includes('/reload/')) { - let url = `/reload/${new Date().getTime()}`; + if (!hasValue(redirectUrl) || !redirectUrl.includes('reload/')) { + let url = `reload/${new Date().getTime()}`; if (isNotEmpty(redirectUrl) && !redirectUrl.startsWith(LOGIN_ROUTE)) { url += `?redirect=${encodeURIComponent(redirectUrl)}`; } diff --git a/src/app/core/locale/locale.service.ts b/src/app/core/locale/locale.service.ts index 1052021479..68d2839d42 100644 --- a/src/app/core/locale/locale.service.ts +++ b/src/app/core/locale/locale.service.ts @@ -192,7 +192,7 @@ export class LocaleService { this.routeService.getCurrentUrl().pipe(take(1)).subscribe((currentURL) => { // Hard redirect to the reload page with a unique number behind it // so that all state is definitely lost - this._window.nativeWindow.location.href = `/reload/${new Date().getTime()}?redirect=` + encodeURIComponent(currentURL); + this._window.nativeWindow.location.href = `reload/${new Date().getTime()}?redirect=` + encodeURIComponent(currentURL); }); } diff --git a/src/app/core/reload/reload.guard.spec.ts b/src/app/core/reload/reload.guard.spec.ts index 317245bafa..6146f51572 100644 --- a/src/app/core/reload/reload.guard.spec.ts +++ b/src/app/core/reload/reload.guard.spec.ts @@ -1,13 +1,17 @@ -import { ReloadGuard } from './reload.guard'; import { Router } from '@angular/router'; +import { AppConfig } from '../../../config/app-config.interface'; +import { DefaultAppConfig } from '../../../config/default-app-config'; +import { ReloadGuard } from './reload.guard'; describe('ReloadGuard', () => { let guard: ReloadGuard; let router: Router; + let appConfig: AppConfig; beforeEach(() => { router = jasmine.createSpyObj('router', ['parseUrl', 'createUrlTree']); - guard = new ReloadGuard(router); + appConfig = new DefaultAppConfig(); + guard = new ReloadGuard(router, appConfig); }); describe('canActivate', () => { @@ -27,7 +31,7 @@ describe('ReloadGuard', () => { it('should create a UrlTree with the redirect URL', () => { guard.canActivate(route, undefined); - expect(router.parseUrl).toHaveBeenCalledWith(redirectUrl); + expect(router.parseUrl).toHaveBeenCalledWith(redirectUrl.substring(1)); }); }); diff --git a/src/app/core/reload/reload.guard.ts b/src/app/core/reload/reload.guard.ts index 78f9dcf642..1e99a5687a 100644 --- a/src/app/core/reload/reload.guard.ts +++ b/src/app/core/reload/reload.guard.ts @@ -1,5 +1,6 @@ +import { Inject, Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; -import { Injectable } from '@angular/core'; +import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface'; import { isNotEmpty } from '../../shared/empty.util'; /** @@ -8,7 +9,10 @@ import { isNotEmpty } from '../../shared/empty.util'; */ @Injectable() export class ReloadGuard implements CanActivate { - constructor(private router: Router) { + constructor( + private router: Router, + @Inject(APP_CONFIG) private appConfig: AppConfig, + ) { } /** @@ -18,7 +22,10 @@ export class ReloadGuard implements CanActivate { */ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): UrlTree { if (isNotEmpty(route.queryParams.redirect)) { - return this.router.parseUrl(route.queryParams.redirect); + const url = route.queryParams.redirect.startsWith(this.appConfig.ui.nameSpace) + ? route.queryParams.redirect.substring(this.appConfig.ui.nameSpace.length) + : route.queryParams.redirect; + return this.router.parseUrl(url); } else { return this.router.createUrlTree(['home']); } diff --git a/src/app/page-internal-server-error/page-internal-server-error.component.html b/src/app/page-internal-server-error/page-internal-server-error.component.html index 4995afc80b..8629873dae 100644 --- a/src/app/page-internal-server-error/page-internal-server-error.component.html +++ b/src/app/page-internal-server-error/page-internal-server-error.component.html @@ -5,6 +5,6 @@

{{"500.help" | translate}}


- {{"500.link.home-page" | translate}} + {{"500.link.home-page" | translate}}

diff --git a/src/app/shared/menu/menu-item/link-menu-item.component.spec.ts b/src/app/shared/menu/menu-item/link-menu-item.component.spec.ts index e9552f9173..96444a5447 100644 --- a/src/app/shared/menu/menu-item/link-menu-item.component.spec.ts +++ b/src/app/shared/menu/menu-item/link-menu-item.component.spec.ts @@ -4,7 +4,6 @@ import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { LinkMenuItemComponent } from './link-menu-item.component'; import { RouterLinkDirectiveStub } from '../../testing/router-link-directive.stub'; -import { environment } from '../../../../environments/environment'; import { QueryParamsDirectiveStub } from '../../testing/query-params-directive.stub'; import { RouterStub } from '../../testing/router.stub'; import { Router } from '@angular/router'; @@ -58,7 +57,7 @@ describe('LinkMenuItemComponent', () => { const routerLinkQuery = linkDes.map((de) => de.injector.get(RouterLinkDirectiveStub)); expect(routerLinkQuery.length).toBe(1); - expect(routerLinkQuery[0].routerLink).toBe(environment.ui.nameSpace + link); + expect(routerLinkQuery[0].routerLink).toBe(link); }); it('should have the right queryParams attribute', () => { diff --git a/src/app/shared/menu/menu-item/link-menu-item.component.ts b/src/app/shared/menu/menu-item/link-menu-item.component.ts index 09f08c4531..c9a60f0c28 100644 --- a/src/app/shared/menu/menu-item/link-menu-item.component.ts +++ b/src/app/shared/menu/menu-item/link-menu-item.component.ts @@ -2,7 +2,6 @@ import { Component, Inject, Input, OnInit } from '@angular/core'; import { LinkMenuItemModel } from './models/link.model'; import { rendersMenuItemForType } from '../menu-item.decorator'; import { isNotEmpty } from '../../empty.util'; -import { environment } from '../../../../environments/environment'; import { MenuItemType } from '../menu-item-type.model'; import { Router } from '@angular/router'; @@ -30,7 +29,7 @@ export class LinkMenuItemComponent implements OnInit { getRouterLink() { if (this.hasLink) { - return environment.ui.nameSpace + this.item.link; + return this.item.link; } return undefined; } diff --git a/src/app/shared/notifications/notification/notification.component.spec.ts b/src/app/shared/notifications/notification/notification.component.spec.ts index 8101f51329..f6de303726 100644 --- a/src/app/shared/notifications/notification/notification.component.spec.ts +++ b/src/app/shared/notifications/notification/notification.component.spec.ts @@ -98,7 +98,7 @@ describe('NotificationComponent', () => { it('should have html content', () => { fixture = TestBed.createComponent(NotificationComponent); comp = fixture.componentInstance; - const htmlContent = 'test'; + const htmlContent = 'test'; comp.notification = { id: '1', type: NotificationType.Info, diff --git a/src/index.csr.html b/src/index.csr.html index d23cb2cae3..b1ef4343b1 100644 --- a/src/index.csr.html +++ b/src/index.csr.html @@ -13,6 +13,6 @@ - + diff --git a/src/styles/_bootstrap_variables.scss b/src/styles/_bootstrap_variables.scss index 4c631a294a..20c0b3a679 100644 --- a/src/styles/_bootstrap_variables.scss +++ b/src/styles/_bootstrap_variables.scss @@ -6,7 +6,9 @@ $sidebar-items-width: 250px !default; $total-sidebar-width: $collapsed-sidebar-width + $sidebar-items-width !default; /* Fonts */ -$fa-font-path: "/assets/fonts" !default; +// Starting this url with a caret (^) allows it to be a relative path based on UI's deployment path +// See https://github.com/angular/angular-cli/issues/12797#issuecomment-598534241 +$fa-font-path: "^assets/fonts" !default; /* Images */ $image-path: "../assets/images" !default; diff --git a/src/themes/dspace/app/home-page/home-news/home-news.component.scss b/src/themes/dspace/app/home-page/home-news/home-news.component.scss index 5e89f6b62f..7af326fe26 100644 --- a/src/themes/dspace/app/home-page/home-news/home-news.component.scss +++ b/src/themes/dspace/app/home-page/home-news/home-news.component.scss @@ -6,7 +6,9 @@ color: white; background-color: var(--bs-info); position: relative; - background-image: url('/assets/dspace/images/banner.jpg'); + // Starting this url with a caret (^) allows it to be a relative path based on UI's deployment path + // See https://github.com/angular/angular-cli/issues/12797#issuecomment-598534241 + background-image: url('^assets/dspace/images/banner.jpg'); background-size: cover; .container {