Skip to content

Commit

Permalink
feat: table for mapping routes between ICM and PWA
Browse files Browse the repository at this point in the history
  • Loading branch information
dhhyi committed Jan 31, 2020
1 parent 5feb876 commit 68f0c49
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 13 deletions.
79 changes: 68 additions & 11 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const { AppServerModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModul
import { Environment } from 'src/environments/environment.model';
const environment: Environment = require('./dist/server/main').environment;

// tslint:disable-next-line: ban-specific-imports
import { HybridMappingEntry } from 'src/hybrid/default-url-mapping-table';
const HYBRID_MAPPING_TABLE: HybridMappingEntry[] = require('./dist/server/main').HYBRID_MAPPING_TABLE;

const logging = !!process.env.LOGGING;

// Express server
Expand Down Expand Up @@ -58,7 +62,7 @@ app.engine(
'html',
ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [provideModuleMap(LAZY_MODULE_MAP)],
providers: [provideModuleMap(LAZY_MODULE_MAP), { provide: 'SSR_HYBRID', useValue: !!process.env.SSR_HYBRID }],
})
);

Expand Down Expand Up @@ -121,15 +125,9 @@ const icmProxy = proxy(ICM_BASE_URL, {
preserveHostHdr: true,
});

if (process.env.PROXY_ICM) {
console.log("making ICM available for all requests to '/INTERSHOP'");
app.use('/INTERSHOP', icmProxy);
}

// All regular routes use the Universal engine
app.get('*', (req: express.Request, res: express.Response) => {
const angularUniversal = (req: express.Request, res: express.Response) => {
if (logging) {
console.log(`GET ${req.url}`);
console.log(`SSR ${req.originalUrl}`);
}
res.render(
'index',
Expand All @@ -140,14 +138,73 @@ app.get('*', (req: express.Request, res: express.Response) => {
(err: Error, html: string) => {
res.status(html ? res.statusCode : 500).send(html || err.message);
if (logging) {
console.log(`RES ${res.statusCode} ${req.url}`);
console.log(`RES ${res.statusCode} ${req.originalUrl}`);
if (err) {
console.log(err);
}
}
}
);
});
};

const hybridRedirect = (req, res, next) => {
const url = req.originalUrl;
let newUrl: string;
for (const entry of HYBRID_MAPPING_TABLE) {
const icmUrlRegex = new RegExp(entry.icm);
const pwaUrlRegex = new RegExp(entry.pwa);
if (icmUrlRegex.exec(url) && entry.handledBy === 'pwa') {
newUrl = url.replace(icmUrlRegex, '/' + entry.pwaBuild);
break;
} else if (pwaUrlRegex.exec(url) && entry.handledBy === 'icm') {
let locale;
if (/;lang=[\w_]+/.test(url)) {
const [, lang] = /;lang=([\w_]+)/.exec(url);
if (lang !== 'default') {
locale = environment.locales.find(loc => loc.lang === lang);
}
}
if (!locale) {
locale = environment.locales[0];
}
let channel;
if (/;channel=[^;]*/.test(url)) {
channel = /;channel=([^;]*)/.exec(url)[1];
} else {
channel = environment.icmChannel;
}
let application;
if (/;application=[^;]*/.test(url)) {
application = /;application=([^;]*)/.exec(url)[1];
} else {
application = environment.icmApplication || '-';
}
const parts = ['', environment.icmServerWeb, channel, locale.lang, application, locale.currency, entry.icmBuild];
newUrl = url.replace(pwaUrlRegex, parts.join('/')).replace(/;.*/g, '');
break;
}
}
if (newUrl) {
if (logging) {
console.log('RED', newUrl);
}
res.redirect(301, newUrl);
} else {
next();
}
};

if (process.env.SSR_HYBRID) {
app.use('*', hybridRedirect);
}

if (process.env.PROXY_ICM || process.env.SSR_HYBRID) {
console.log("making ICM available for all requests to '/INTERSHOP'");
app.use('/INTERSHOP', icmProxy);
}

// All regular routes use the Universal engine
app.use('*', angularUniversal);

if (process.env.SSL) {
const https = require('https');
Expand Down
5 changes: 3 additions & 2 deletions src/app/core/store/configuration/configuration.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class ConfigurationEffects {
this.stateProperties.getStateOrEnvOrDefault<string>('ICM_BASE_URL', 'icmBaseURL'),
this.stateProperties.getStateOrEnvOrDefault<string>('ICM_SERVER', 'icmServer'),
this.stateProperties.getStateOrEnvOrDefault<string>('ICM_SERVER_STATIC', 'icmServerStatic'),
this.stateProperties.getStateOrEnvOrDefault<string>('ICM_SERVER_WEB', 'icmServerWeb'),
this.stateProperties.getStateOrEnvOrDefault<string>('ICM_CHANNEL', 'icmChannel'),
this.stateProperties.getStateOrEnvOrDefault<string>('ICM_APPLICATION', 'icmApplication'),
this.stateProperties
Expand All @@ -53,8 +54,8 @@ export class ConfigurationEffects {
this.stateProperties.getStateOrEnvOrDefault<string>('THEME', 'theme').pipe(map(x => x || 'default'))
),
map(
([, baseURL, server, serverStatic, channel, application, features, theme]) =>
new ApplyConfiguration({ baseURL, server, serverStatic, channel, application, features, theme })
([, baseURL, server, serverStatic, serverWeb, channel, application, features, theme]) =>
new ApplyConfiguration({ baseURL, server, serverStatic, serverWeb, channel, application, features, theme })
)
);

Expand Down
2 changes: 2 additions & 0 deletions src/app/core/store/configuration/configuration.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface ConfigurationState {
baseURL?: string;
server?: string;
serverStatic?: string;
serverWeb?: string;
channel?: string;
application?: string;
features?: string[];
Expand All @@ -15,6 +16,7 @@ const initialState: ConfigurationState = {
baseURL: undefined,
server: undefined,
serverStatic: undefined,
serverWeb: undefined,
channel: undefined,
application: undefined,
features: [],
Expand Down
10 changes: 10 additions & 0 deletions src/app/core/store/configuration/configuration.selectors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createSelector } from '@ngrx/store';

import { getCoreState } from 'ish-core/store/core-store';
import { getCurrentLocale } from 'ish-core/store/locale';

export const getConfigurationState = createSelector(
getCoreState,
Expand All @@ -27,6 +28,15 @@ export const getICMStaticURL = createSelector(
: undefined
);

export const getICMWebURL = createSelector(
getConfigurationState,
getCurrentLocale,
(state, locale) => {
const parts = [state.serverWeb, state.channel, locale.lang, state.application || '-', locale.currency];
return parts.every(x => !!x) ? `/${parts.join('/')}` : undefined;
}
);

export const getICMBaseURL = createSelector(
getConfigurationState,
state => state.baseURL
Expand Down
2 changes: 2 additions & 0 deletions src/app/core/store/core-store.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { CountriesEffects } from './countries/countries.effects';
import { countriesReducer } from './countries/countries.reducer';
import { ErrorEffects } from './error/error.effects';
import { errorReducer } from './error/error.reducer';
import { HybridStoreModule } from './hybrid/hybrid-store.module';
import { LocaleEffects } from './locale/locale.effects';
import { localeReducer } from './locale/locale.reducer';
import { MessagesEffects } from './messages/messages.effects';
Expand Down Expand Up @@ -66,6 +67,7 @@ export const metaReducers: MetaReducer<any>[] = [ngrxStateTransferMeta];
CheckoutStoreModule,
ContentStoreModule,
EffectsModule.forRoot(coreEffects),
HybridStoreModule,
RestoreStoreModule,
ShoppingStoreModule,
StoreModule.forRoot(coreReducers, {
Expand Down
9 changes: 9 additions & 0 deletions src/app/core/store/hybrid/hybrid-store.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';

import { HybridEffects } from './hybrid.effects';

@NgModule({
imports: [EffectsModule.forFeature([HybridEffects])],
})
export class HybridStoreModule {}
55 changes: 55 additions & 0 deletions src/app/core/store/hybrid/hybrid.effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { Inject, Optional, PLATFORM_ID } from '@angular/core';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { NavigationStart, Router } from '@angular/router';
import { Actions, Effect } from '@ngrx/effects';
import { Store, select } from '@ngrx/store';
import { filter, map, take, tap, withLatestFrom } from 'rxjs/operators';

import { getICMWebURL } from 'ish-core/store/configuration';

import { HYBRID_MAPPING_TABLE } from '../../../../hybrid/default-url-mapping-table';

const SSR_HYBRID_STATE = makeStateKey<boolean>('ssrHybrid');

export class HybridEffects {
constructor(
private actions: Actions,
private router: Router,
private store$: Store<{}>,
@Inject(PLATFORM_ID) private platformId: string,
private transferState: TransferState,
@Optional() @Inject('SSR_HYBRID') private ssrHybridState: boolean
) {}

@Effect({ dispatch: false })
propagateSSRHybridPropToTransferState$ = this.actions.pipe(
take(1),
filter(() => isPlatformServer(this.platformId)),
filter(() => !!this.ssrHybridState),
tap(() => this.transferState.set(SSR_HYBRID_STATE, true))
);

@Effect({ dispatch: false })
hybridApproachRedirectToICM$ = this.router.events.pipe(
filter(() => isPlatformBrowser(this.platformId)),
filter(() => this.transferState.get(SSR_HYBRID_STATE, false)),
filter(event => event instanceof NavigationStart),
map((event: NavigationStart) => event.url),
withLatestFrom(this.store$.pipe(select(getICMWebURL))),
tap(([url, icmWebUrl]) => {
for (const entry of HYBRID_MAPPING_TABLE) {
if (entry.handledBy === 'pwa') {
continue;
}
const regex = new RegExp(entry.pwa);
if (regex.exec(url)) {
this.router.dispose();
const newUrl = url.replace(regex, `${icmWebUrl}/${entry.icmBuild}`);
location.assign(newUrl);
break;
}
}
})
);
}
3 changes: 3 additions & 0 deletions src/environments/environment.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export interface Environment {
icmBaseURL: string;
icmServer: string;
icmServerStatic: string;
icmServerWeb: string;

// application specific
icmChannel: string;
icmApplication?: string;

Expand Down
1 change: 1 addition & 0 deletions src/environments/environment.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const environment: Environment = {
icmBaseURL: 'https://intershoppwa.azurewebsites.net',
icmServer: 'INTERSHOP/rest/WFS',
icmServerStatic: 'INTERSHOP/static/WFS',
icmServerWeb: 'INTERSHOP/web/WFS',
icmChannel: 'inSPIRED-inTRONICS-Site',

/* FEATURE TOOGLES */
Expand Down
1 change: 1 addition & 0 deletions src/environments/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const environment: Environment = {
icmBaseURL: 'http://localhost:4200',
icmServer: 'INTERSHOP/rest/WFS',
icmServerStatic: 'INTERSHOP/static/WFS',
icmServerWeb: 'INTERSHOP/web/WFS',
icmChannel: 'inSPIRED-inTRONICS-Site',

mockServerAPI: true,
Expand Down
41 changes: 41 additions & 0 deletions src/hybrid/default-url-mapping-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { environment } from '../environments/environment';

const ICM_CONFIG_MATCH = `^/${environment.icmServerWeb}/(?<channel>[\\w-]+)/(?<lang>\\w+)/(?<application>[\\w-]+)/\\w+`;
const PWA_CONFIG_BUILD = ';channel=$<channel>;lang=$<lang>;application=$<application>;redirect=1';

export interface HybridMappingEntry {
/** ID for grouping */
id: string;
/** regex for detecting ICM URL */
icm: string;
/** regex for building ICM URL (w/o web url) */
icmBuild: string;
/** regex for detecting PWA URL */
pwa: string;
/** regex for building PWA URL */
pwaBuild: string;
/** handler */
handledBy: 'icm' | 'pwa';
}

/**
* Mapping table for running PWA and ICM in parallel
*/
export const HYBRID_MAPPING_TABLE: HybridMappingEntry[] = [
{
id: 'Home',
icm: `${ICM_CONFIG_MATCH}/(Default|ViewHomepage)-Start.*$`,
icmBuild: `ViewHomepage-Start`,
pwa: `^/home.*$`,
pwaBuild: `home${PWA_CONFIG_BUILD}`,
handledBy: 'pwa',
},
{
id: 'PDP',
icm: `${ICM_CONFIG_MATCH}/ViewProduct-Start.*(\\?|&)SKU=(?<sku>[\\w-]+).*$`,
icmBuild: `ViewProduct-Start?SKU=$<sku>`,
pwa: `^.*/product/(?<sku>[\\w-]+).*$`,
pwaBuild: `product/$<sku>${PWA_CONFIG_BUILD}`,
handledBy: 'icm',
},
];
1 change: 1 addition & 0 deletions src/main.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { AppServerModule } from './app/app.server.module';
export { ngExpressEngine } from '@nguniversal/express-engine';
export { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
export { environment } from './environments/environment';
export { HYBRID_MAPPING_TABLE } from './hybrid/default-url-mapping-table';
1 change: 1 addition & 0 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@
"^.*/cypress/.*$",
"^.*/src/environments/environment(\\.\\w+|)\\.ts$",
"^.*/src/ngrx-router/.*$",
"^.*/src/hybrid/default-url-mapping-table.ts$",
// core
"^.*/src/app/core/[a-z][a-z0-9-]+\\.module\\.ts",
"^.*/src/app/core/configurations/.*",
Expand Down

0 comments on commit 68f0c49

Please sign in to comment.