Skip to content

Commit

Permalink
refactor(platform-server): remove inlined JSAction script when no eve…
Browse files Browse the repository at this point in the history
…nts to replay

JSAction script is inlined into the HTML by the build process to avoid extra blocking request. The script looks like this:

```
<script type="text/javascript" id="ng-event-dispatch-contract">...</script>
```

This commit updates the logic to remove JSAction if event replay feature is disabled or if there are no events to replay.
  • Loading branch information
AndrewKushnir committed Apr 21, 2024
1 parent b1dffa4 commit e84e84b
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 11 deletions.
24 changes: 23 additions & 1 deletion packages/platform-server/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ import {platformServer} from './server';
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens';
import {createScript} from './transfer_state';

/**
* Event dispatch (JSAction) script is inlined into the HTML by the build
* process to avoid extra blocking request on a page. The script looks like this:
* ```
* <script type="text/javascript" id="ng-event-dispatch-contract">...</script>
* ```
* This const represents the "id" attribute value.
*/
export const EVENT_DISPATCH_SCRIPT_ID = 'ng-event-dispatch-contract';

interface PlatformOptions {
document?: string | Document;
url?: string;
Expand All @@ -44,14 +54,22 @@ function createServerPlatform(options: PlatformOptions): PlatformRef {
]);
}

/**
* 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();
}

/**
* Creates a marker comment node and append it into the `<body>`.
* Some CDNs have mechanisms to remove all comment node from HTML.
* This behaviour breaks hydration, so we'll detect on the client side if this
* marker comment is still available or else throw an error
*/
function appendSsrContentIntegrityMarker(doc: Document) {
// Adding a ng hydration marken comment
// Adding a ng hydration marker comment
const comment = doc.createComment(SSR_CONTENT_INTEGRITY_MARKER);
doc.body.firstChild
? doc.body.insertBefore(comment, doc.body.firstChild)
Expand Down Expand Up @@ -101,6 +119,10 @@ async function _render(platformRef: PlatformRef, applicationRef: ApplicationRef)
const eventTypesToBeReplayed = annotateForHydration(applicationRef, doc);
if (eventTypesToBeReplayed) {
insertEventRecordScript(environmentInjector.get(APP_ID), doc, eventTypesToBeReplayed);
} else {
// No events to replay, we should remove inlined event dispatch script
// (which was injected by the build process) from the HTML.
removeEventDispatchScript(doc);
}
}

Expand Down
91 changes: 81 additions & 10 deletions packages/platform-server/test/event_replay_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,26 @@ import {bootstrapApplication, provideClientHydration} from '@angular/platform-br
import {withEventReplay} from '@angular/platform-browser/src/hydration';

import {provideServerRendering} from '../public_api';
import {renderApplication} from '../src/utils';
import {EVENT_DISPATCH_SCRIPT_ID, renderApplication} from '../src/utils';

import {getAppContents} from './dom_utils';

/**
* Represents the <script> tag added by the build process to inject
* event dispatch (JSAction) logic.
*/
const EVENT_DISPATCH_SCRIPT = `<script type="text/javascript" id="${EVENT_DISPATCH_SCRIPT_ID}"></script>`;

/** Checks whether event dispatch script is present in the generated HTML */
function hasEventDispatchScript(content: string) {
return content.includes(EVENT_DISPATCH_SCRIPT_ID);
}

/** Checks whether there are any `jsaction` attributes present in the generated HTML */
function hasJSActionAttrs(content: string) {
return content.includes('jsaction="');
}

describe('event replay', () => {
beforeEach(() => {
if (getPlatform()) destroyPlatform();
Expand Down Expand Up @@ -47,16 +63,14 @@ describe('event replay', () => {
*/
async function ssr(
component: Type<unknown>,
options?: {
doc?: string;
},
options?: {doc?: string; enableEventReplay?: boolean},
): Promise<string> {
const defaultHtml = '<html><head></head><body><app></app></body></html>';
const providers = [
provideServerRendering(),
// @ts-ignore
provideClientHydration(withEventReplay()),
];
const enableEventReplay = options?.enableEventReplay ?? true;
const defaultHtml = `<html><head></head><body>${EVENT_DISPATCH_SCRIPT}<app></app></body></html>`;
const hydrationProviders = enableEventReplay
? provideClientHydration(withEventReplay())
: provideClientHydration();
const providers = [provideServerRendering(), hydrationProviders];

const bootstrap = () => bootstrapApplication(component, {providers});

Expand Down Expand Up @@ -90,6 +104,63 @@ describe('event replay', () => {
).toBeTrue();
expect(ssrContents).toContain('<div jsaction="click:"><div jsaction="blur:"></div></div>');
});

describe('event dispatch script', () => {
it('should not be present on a page if there are no events to replay', async () => {
@Component({
standalone: true,
selector: 'app',
template: 'Some text',
})
class SimpleComponent {}

const doc = `<html><head></head><body>${EVENT_DISPATCH_SCRIPT}<app></app></body></html>`;
const html = await ssr(SimpleComponent, {doc});
const ssrContents = getAppContents(html);

expect(hasJSActionAttrs(ssrContents)).toBeFalse();
expect(hasEventDispatchScript(ssrContents)).toBeFalse();
});

it('should not be present on a page where event replay is not enabled', async () => {
@Component({
standalone: true,
selector: 'app',
template: '<input (click)="onClick()" />',
})
class SimpleComponent {
onClick() {}
}

const doc = `<html><head></head><body>${EVENT_DISPATCH_SCRIPT}<app></app></body></html>`;
const html = await ssr(SimpleComponent, {doc, enableEventReplay: false});
const ssrContents = getAppContents(html);

// Expect that there are no JSAction artifacts in the HTML
// (even though there are events in a template), since event
// replay is disabled in the config.
expect(hasJSActionAttrs(ssrContents)).toBeFalse();
expect(hasEventDispatchScript(ssrContents)).toBeFalse();
});

it('should be retained if there are events to replay', async () => {
@Component({
standalone: true,
selector: 'app',
template: '<input (click)="onClick()" />',
})
class SimpleComponent {
onClick() {}
}

const doc = `<html><head></head><body>${EVENT_DISPATCH_SCRIPT}<app></app></body></html>`;
const html = await ssr(SimpleComponent, {doc});
const ssrContents = getAppContents(html);

expect(hasJSActionAttrs(ssrContents)).toBeTrue();
expect(hasEventDispatchScript(ssrContents)).toBeTrue();
});
});
});
});
});

0 comments on commit e84e84b

Please sign in to comment.