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 19, 2024
1 parent b1dffa4 commit 008be53
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 8 deletions.
27 changes: 26 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';

/**
* 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 const represents the "id" attribute value.
*/
export const JSACTION_SCRIPT_ID = 'ng-event-dispatch-contract';

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

/**
* Removes inlined JSAction script if it exists.
* See the `JSACTION_SCRIPT_ID` const docs for additional info.
*/
function removeJSActionScript(doc: Document) {
const jsaScript = doc.getElementById(JSACTION_SCRIPT_ID);
if (jsaScript) {
jsaScript.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 +122,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 JSAction script
// (which was injected by the build process) from the HTML.
removeJSActionScript(doc);
}
}

Expand Down
85 changes: 78 additions & 7 deletions packages/platform-server/test/event_replay_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,23 @@ 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 {JSACTION_SCRIPT_ID, renderApplication} from '../src/utils';

import {getAppContents} from './dom_utils';

/** Represents a <script> tag added by the build process to inject JSAction logic */
const JSACTION_SCRIPT = `<script type="text/javascript" id="${JSACTION_SCRIPT_ID}"></script>`;

/** Checks whether JSAction script is present in the generated HTML */
function hasJSActionScript(content: string) {
return content.includes(JSACTION_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 @@ -49,14 +62,15 @@ describe('event replay', () => {
component: Type<unknown>,
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>${JSACTION_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('jsaction 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>${JSACTION_SCRIPT}<app></app></body></html>`;
const html = await ssr(SimpleComponent, {doc});
const ssrContents = getAppContents(html);

expect(hasJSActionAttrs(ssrContents)).toBeFalse();
expect(hasJSActionScript(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>${JSACTION_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(hasJSActionScript(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>${JSACTION_SCRIPT}<app></app></body></html>`;
const html = await ssr(SimpleComponent, {doc});
const ssrContents = getAppContents(html);

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

0 comments on commit 008be53

Please sign in to comment.