refactor(platform-server): event contract script should follow event dispatch script (#55502)

This commit fixes an issue where event contract init script was injected into the page before the inlined event dispatch script. That resulted in runtime exceptions, since event contract relies on some code being present on a page already.

PR Close #55502
This commit is contained in:
Andrew Kushnir 2024-04-23 18:15:32 -07:00
parent fc90549c9b
commit 307bc1d6b6
2 changed files with 40 additions and 17 deletions

View file

@ -9,6 +9,7 @@
import {
APP_ID,
ApplicationRef,
CSP_NONCE,
InjectionToken,
PlatformRef,
Provider,
@ -19,7 +20,6 @@ import {
ɵIS_HYDRATION_DOM_REUSE_ENABLED as IS_HYDRATION_DOM_REUSE_ENABLED,
ɵSSR_CONTENT_INTEGRITY_MARKER as SSR_CONTENT_INTEGRITY_MARKER,
ɵwhenStable as whenStable,
CSP_NONCE,
} from '@angular/core';
import {PlatformState} from './platform_state';
@ -55,12 +55,20 @@ function createServerPlatform(options: PlatformOptions): PlatformRef {
]);
}
/**
* Finds and returns inlined event dispatch script if it exists.
* See the `EVENT_DISPATCH_SCRIPT_ID` const docs for additional info.
*/
function findEventDispatchScript(doc: Document) {
return doc.getElementById(EVENT_DISPATCH_SCRIPT_ID);
}
/**
* Removes inlined event dispatch script if it exists.
* See the `EVENT_DISPATCH_SCRIPT_ID` const docs for additional info.
*/
function removeEventDispatchScript(doc: Document) {
doc.getElementById(EVENT_DISPATCH_SCRIPT_ID)?.remove();
findEventDispatchScript(doc)?.remove();
}
/**
@ -99,13 +107,20 @@ function insertEventRecordScript(
eventTypesToBeReplayed: Set<string>,
nonce: string | null,
): void {
const events = Array.from(eventTypesToBeReplayed);
// This is defined in packages/core/primitives/event-dispatch/contract_binary.ts
const replayScript = `window.__jsaction_bootstrap('ngContracts', document.body, ${JSON.stringify(
appId,
)}, ${JSON.stringify(events)});`;
const script = createScript(doc, replayScript, nonce);
doc.body.insertBefore(script, doc.body.firstChild);
const eventDispatchScript = findEventDispatchScript(doc);
if (eventDispatchScript) {
const events = Array.from(eventTypesToBeReplayed);
// This is defined in packages/core/primitives/event-dispatch/contract_binary.ts
const replayScriptContents = `window.__jsaction_bootstrap('ngContracts', document.body, ${JSON.stringify(
appId,
)}, ${JSON.stringify(events)});`;
const replayScript = createScript(doc, replayScriptContents, nonce);
// Insert replay script right after inlined event dispatch script, since it
// relies on `__jsaction_bootstrap` to be defined in the global scope.
eventDispatchScript.after(replayScript);
}
}
async function _render(platformRef: PlatformRef, applicationRef: ApplicationRef): Promise<string> {

View file

@ -97,7 +97,7 @@ describe('event replay', () => {
selector: 'app',
template: `
<div (click)="onClick()" id="1">
<div (blur)="onClick()" id="2"></div>
<div (blur)="onClick()" id="2"></div>
</div>
`,
})
@ -106,14 +106,12 @@ describe('event replay', () => {
onBlur = blurSpy;
}
const docContents = `<html><head></head><body><app></app></body></html>`;
const docContents = `<html><head></head><body>${EVENT_DISPATCH_SCRIPT}<app></app></body></html>`;
const html = await ssr(SimpleComponent, {doc: docContents});
const ssrContents = getAppContents(html);
expect(
ssrContents.startsWith(
`<script>window.__jsaction_bootstrap('ngContracts', document.body, "ng", ["click","blur"]);</script>`,
),
).toBeTrue();
expect(ssrContents).toContain(
`<script>window.__jsaction_bootstrap('ngContracts', document.body, "ng", ["click","blur"]);</script>`,
);
expect(ssrContents).toContain(
'<div id="1" jsaction="click:"><div id="2" jsaction="blur:"></div></div>',
);
@ -145,7 +143,9 @@ describe('event replay', () => {
onClick() {}
}
const doc = `<html><head></head><body><app ngCspNonce="{{nonce}}"></app></body></html>`;
const doc =
`<html><head></head><body>${EVENT_DISPATCH_SCRIPT}` +
`<app ngCspNonce="{{nonce}}"></app></body></html>`;
const html = await ssr(SimpleComponent, {doc});
expect(getAppContents(html)).toContain(
'<script nonce="{{nonce}}">window.__jsaction_bootstrap',
@ -206,6 +206,14 @@ describe('event replay', () => {
expect(hasJSActionAttrs(ssrContents)).toBeTrue();
expect(hasEventDispatchScript(ssrContents)).toBeTrue();
// Verify that inlined event delegation script goes first and
// event contract setup goes second (since it uses some code from
// the inlined script).
expect(ssrContents).toContain(
`<script type="text/javascript" id="ng-event-dispatch-contract"></script>` +
`<script>window.__jsaction_bootstrap('ngContracts', document.body, "ng", ["click"]);</script>`,
);
});
});
});