Skip to content

Commit

Permalink
refactor(platform-server): event contract script should follow event …
Browse files Browse the repository at this point in the history
…dispatch script

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.
  • Loading branch information
AndrewKushnir committed Apr 24, 2024
1 parent 3bb0a42 commit dc5ff04
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 17 deletions.
33 changes: 24 additions & 9 deletions packages/platform-server/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {
APP_ID,
ApplicationRef,
CSP_NONCE,
InjectionToken,
PlatformRef,
Provider,
Expand All @@ -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';
Expand Down Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -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> {
Expand Down
24 changes: 16 additions & 8 deletions packages/platform-server/test/event_replay_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
`,
})
Expand All @@ -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>',
);
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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>`,
);
});
});
});
Expand Down

0 comments on commit dc5ff04

Please sign in to comment.