-
Notifications
You must be signed in to change notification settings - Fork 388
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CXSPA-2066
- Loading branch information
Showing
311 changed files
with
5,018 additions
and
1,120 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
core-libs/setup/ssr/engine/__snapshots__/cx-common-engine.spec.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`CxCommonEngine should handle APP_INITIALIZER errors the standard Angular way and throw if any occurred 1`] = ` | ||
"R3InjectorError(TokenServerModule)[InjectionToken SOME_TOKEN -> InjectionToken SOME_TOKEN]: | ||
NullInjectorError: No provider for InjectionToken SOME_TOKEN!" | ||
`; | ||
|
||
exports[`CxCommonEngine should handle errors propagated from SSR 1`] = `"test error"`; | ||
|
||
exports[`CxCommonEngine should not override providers passed to options 1`] = `"<html data-critters-container><head></head><body><cx-token ng-version="17.0.5" ng-server-context="ssr">message:test</cx-token></body></html>"`; | ||
|
||
exports[`CxCommonEngine should return html if no errors 1`] = `"<html data-critters-container><head></head><body><cx-mock ng-version="17.0.5" ng-server-context="ssr">some template</cx-mock></body></html>"`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
// eslint-disable-next-line import/no-unassigned-import | ||
import '@angular/compiler'; | ||
|
||
import { Component, InjectionToken, NgModule, inject } from '@angular/core'; | ||
import { BrowserModule } from '@angular/platform-browser'; | ||
import { ServerModule } from '@angular/platform-server'; | ||
import { PROPAGATE_ERROR_TO_SERVER } from '../error-handling/error-response/propagate-error-to-server'; | ||
import { CxCommonEngine } from './cx-common-engine'; | ||
|
||
// Test how the CxCommonEngine handles successful server-side rendering | ||
@Component({ selector: 'cx-mock', template: 'some template' }) | ||
export class SuccessComponent {} | ||
|
||
@NgModule({ | ||
imports: [BrowserModule, ServerModule], | ||
declarations: [SuccessComponent], | ||
bootstrap: [SuccessComponent], | ||
}) | ||
export class SuccessServerModule {} | ||
|
||
// Test how the CxCommonEngine handles propagated error | ||
@Component({ | ||
selector: 'cx-response', | ||
template: ``, | ||
}) | ||
export class WithPropagatedErrorComponent { | ||
constructor() { | ||
inject(PROPAGATE_ERROR_TO_SERVER)(new Error('test error')); | ||
} | ||
} | ||
|
||
@NgModule({ | ||
imports: [BrowserModule, ServerModule], | ||
declarations: [WithPropagatedErrorComponent], | ||
bootstrap: [WithPropagatedErrorComponent], | ||
}) | ||
export class WithPropagatedErrorServerModule {} | ||
|
||
// Test that the CxCommonEngine doesn't override providers | ||
// If SOME_TOKEN not provided, test how the CxCommonEngine handles APP_INITIALIZER errors | ||
export const SOME_TOKEN = new InjectionToken<string>('SOME_TOKEN'); | ||
|
||
@Component({ | ||
selector: 'cx-token', | ||
template: `message:{{ someToken }}`, | ||
}) | ||
export class TokenComponent { | ||
someToken = inject(SOME_TOKEN); | ||
} | ||
|
||
@NgModule({ | ||
imports: [BrowserModule, ServerModule], | ||
declarations: [TokenComponent], | ||
bootstrap: [TokenComponent], | ||
}) | ||
export class TokenServerModule {} | ||
|
||
describe('CxCommonEngine', () => { | ||
let engine: CxCommonEngine; | ||
|
||
beforeAll(() => { | ||
jest.spyOn(console, 'error').mockImplementation(); | ||
jest.spyOn(console, 'log').mockImplementation(); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should return html if no errors', async () => { | ||
engine = new CxCommonEngine({ | ||
bootstrap: SuccessServerModule, | ||
}); | ||
|
||
const html = await engine.render({ | ||
url: 'http://localhost:4200', | ||
document: '<cx-mock></cx-mock>', | ||
}); | ||
|
||
// Cannot use `.toMatchInlineSnapshot()` due to bug in jest: | ||
// see: https://github.com/thymikee/jest-preset-angular/issues/1084 | ||
expect(html).toMatchSnapshot(); | ||
}); | ||
|
||
it('should not override providers passed to options', async () => { | ||
engine = new CxCommonEngine({ | ||
bootstrap: TokenServerModule, | ||
}); | ||
|
||
const html = await engine.render({ | ||
url: 'http://localhost:4200', | ||
document: '<cx-token></cx-token>', | ||
providers: [{ provide: SOME_TOKEN, useValue: 'test' }], | ||
}); | ||
|
||
// Cannot use `.toMatchInlineSnapshot()` due to bug in jest: | ||
// see: https://github.com/thymikee/jest-preset-angular/issues/1084 | ||
expect(html).toMatchSnapshot(); | ||
}); | ||
|
||
it('should handle APP_INITIALIZER errors the standard Angular way and throw if any occurred', async () => { | ||
engine = new CxCommonEngine({ | ||
bootstrap: TokenServerModule, | ||
}); | ||
|
||
// Cannot use `.toMatchInlineSnapshot()` due to bug in jest: | ||
// see: https://github.com/thymikee/jest-preset-angular/issues/1084 | ||
await expect( | ||
engine.render({ | ||
url: 'http://localhost:4200', | ||
document: '<cx-token></cx-token>', | ||
}) | ||
).rejects.toThrowErrorMatchingSnapshot(); | ||
}); | ||
|
||
it('should handle errors propagated from SSR', async () => { | ||
engine = new CxCommonEngine({ | ||
bootstrap: WithPropagatedErrorServerModule, | ||
}); | ||
|
||
// Cannot use `.toMatchInlineSnapshot()` due to bug in jest: | ||
// see: https://github.com/thymikee/jest-preset-angular/issues/1084 | ||
await expect( | ||
engine.render({ | ||
url: 'http://localhost:4200', | ||
document: '<cx-response></cx-response>', | ||
}) | ||
).rejects.toThrowErrorMatchingSnapshot(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* | ||
* SPDX-FileCopyrightText: 2024 SAP Spartacus team <[email protected]> | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { | ||
CommonEngine, | ||
CommonEngineOptions, | ||
CommonEngineRenderOptions, | ||
} from '@angular/ssr'; | ||
import { PROPAGATE_ERROR_TO_SERVER } from '../error-handling/error-response/propagate-error-to-server'; | ||
|
||
/** | ||
* The Spartacus extension of the Angular's `CommonEngine`. It is able to handle the propagated server responses caught during server-side rendering of a Spartacus app. | ||
* For reference, see Angular's source code: https://github.com/angular/angular-cli/blob/6cf866225ab09f8b4b3803c000b632bed8448ce4/packages/angular/ssr/src/common-engine.ts#L56 | ||
* | ||
* @extends {CommonEngine} | ||
*/ | ||
export class CxCommonEngine extends CommonEngine { | ||
constructor(options?: CommonEngineOptions) { | ||
super(options); | ||
} | ||
|
||
/** | ||
* @override | ||
* Renders for the given options. | ||
* If an error is populated from the rendered applications | ||
* (via `PROPAGATE_ERROR_TO_SERVER` callback), then such an error | ||
* will be thrown and the result promise rejected - but only AFTER the rendering is complete. | ||
* In other words, at first an error occurs and it's captured, then we wait until the rendering completes | ||
* and ONLY then we reject the promise with the payload being the encountered error. | ||
* | ||
* Note: if more errors are captured during the rendering, only the first one will be used | ||
* as the payload of the rejected promise, others won't. | ||
* | ||
* @param {CommonEngineRenderOptions} options - The options to render. | ||
* @returns {Promise<string>} Promise which resolves with the rendered HTML as a string | ||
* OR rejects with the error, if any is propagated from the rendered app. | ||
*/ | ||
override async render(options: CommonEngineRenderOptions): Promise<string> { | ||
let error: undefined | unknown; | ||
|
||
return super | ||
.render({ | ||
...options, | ||
providers: [ | ||
{ | ||
provide: PROPAGATE_ERROR_TO_SERVER, | ||
useFactory: () => { | ||
return (propagatedError: unknown) => { | ||
// We're interested only the first propagated error, so we use `??=` instead of `=`: | ||
error ??= propagatedError; | ||
}; | ||
}, | ||
}, | ||
...(options.providers ?? []), | ||
], | ||
}) | ||
.then((html: string) => { | ||
if (error) { | ||
throw error; | ||
} | ||
return html; | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
/* | ||
* SPDX-FileCopyrightText: 2023 SAP Spartacus team <[email protected]> | ||
* SPDX-FileCopyrightText: 2024 SAP Spartacus team <[email protected]> | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/* | ||
* SPDX-FileCopyrightText: 2024 SAP Spartacus team <[email protected]> | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
export * from './propagate-error-to-server'; |
22 changes: 22 additions & 0 deletions
22
core-libs/setup/ssr/error-handling/error-response/propagate-error-to-server.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/* | ||
* SPDX-FileCopyrightText: 2024 SAP Spartacus team <[email protected]> | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { InjectionToken } from '@angular/core'; | ||
|
||
/** | ||
* Propagates the given error object to the higher layer in the server. | ||
* | ||
* It's meant to propagate errors for example to ExpressJS layer when using SSR | ||
* or to a Prerendering Worker when using Server Prerendering. | ||
* Currently, it's provided OOTB only in SSR (not prerendering), in the `CxCommonEngine` class. | ||
* | ||
* Note: We need it until Angular implements a proper propagation of async errors | ||
* from an app to the the higher layer in the server. | ||
* For more, see the Angular issue https://github.com/angular/angular/issues/33642 | ||
*/ | ||
export const PROPAGATE_ERROR_TO_SERVER = new InjectionToken< | ||
(error: unknown) => void | ||
>('PROPAGATE_ERROR_RESPONSE'); |
56 changes: 56 additions & 0 deletions
56
core-libs/setup/ssr/error-handling/express-error-handlers/express-error-handlers.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { HttpErrorResponse } from '@angular/common/http'; | ||
import { CmsPageNotFoundOutboundHttpError } from '@spartacus/core'; | ||
import { defaultExpressErrorHandlers } from './express-error-handlers'; | ||
|
||
describe('expressErrorHandlers', () => { | ||
let documentContent: string; | ||
let req: any; | ||
let res: any; | ||
let next: any; | ||
|
||
beforeEach(() => { | ||
documentContent = 'some document content'; | ||
req = {}; | ||
res = { | ||
set: jest.fn(), | ||
status: jest.fn().mockReturnThis(), | ||
send: jest.fn(), | ||
}; | ||
}); | ||
|
||
it('should do nothing if headers are already sent', () => { | ||
const err = new HttpErrorResponse({ | ||
error: 'Page not found', | ||
}); | ||
const errorRequestHandler = defaultExpressErrorHandlers(documentContent); | ||
res.headersSent = true; | ||
|
||
errorRequestHandler(err, req, res, next); | ||
|
||
expect(res.set).not.toHaveBeenCalled(); | ||
expect(res.status).not.toHaveBeenCalled(); | ||
expect(res.send).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should handle CmsPageNotFoundOutboundHttpError', () => { | ||
const err = new CmsPageNotFoundOutboundHttpError('Page not found'); | ||
const errorRequestHandler = defaultExpressErrorHandlers(documentContent); | ||
|
||
errorRequestHandler(err, req, res, next); | ||
|
||
expect(res.set).toHaveBeenCalledWith('Cache-Control', 'no-store'); | ||
expect(res.status).toHaveBeenCalledWith(404); | ||
expect(res.send).toHaveBeenCalledWith(documentContent); | ||
}); | ||
|
||
it('should handle unknown error', () => { | ||
const err = new Error('unknown error'); | ||
const errorRequestHandler = defaultExpressErrorHandlers(documentContent); | ||
|
||
errorRequestHandler(err, req, res, next); | ||
|
||
expect(res.set).toHaveBeenCalledWith('Cache-Control', 'no-store'); | ||
expect(res.status).toHaveBeenCalledWith(500); | ||
expect(res.send).toHaveBeenCalledWith(documentContent); | ||
}); | ||
}); |
Oops, something went wrong.