Skip to content

Commit

Permalink
fix(platform-browser): collect external component styles from server …
Browse files Browse the repository at this point in the history
…rendering (#59031)

SSR generated component styles used in development environments will add
external styles via link elements to the HTML. However, the runtime would
previously not collect these link elements for reuse with rendered components.
This would result in two copies of the link elements present in the DOM. In
isolation this is not problematic as it is only present in development mode.
Unfortunately, the Vite-based CSS HMR functionality used by the Angular CLI
only updates the first stylesheet it finds and leaves other instances of the
stylesheet in place. This behavior causes the styles to be left in an
inconsistent state. This could be considered a defect within Vite as it should
update all relevant styles to maintain consistency but ideally there should
not be two instances in the Angular SSR case. To avoid the Vite issue, the
runtime will now collect SSR generated external styles and reuse them.

PR Close #59031
  • Loading branch information
clydin authored and alxhub committed Dec 6, 2024
1 parent 6fd8a20 commit 52be351
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 10 deletions.
29 changes: 19 additions & 10 deletions packages/platform-browser/src/dom/shared_styles_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,31 @@ function createStyleElement(style: string, doc: Document): HTMLStyleElement {
* identifier attribute (`ng-app-id`) to the provide identifier and adds usage records for each.
* @param doc An HTML DOM document instance.
* @param appId A string containing an Angular application identifer.
* @param usages A Map object for tracking style usage.
* @param inline A Map object for tracking inline (defined via `styles` in component decorator) style usage.
* @param external A Map object for tracking external (defined via `styleUrls` in component decorator) style usage.
*/
function addServerStyles(
doc: Document,
appId: string,
usages: Map<string, UsageRecord<HTMLStyleElement>>,
inline: Map<string, UsageRecord<HTMLStyleElement>>,
external: Map<string, UsageRecord<HTMLLinkElement>>,
): void {
const styleElements = doc.head?.querySelectorAll<HTMLStyleElement>(
`style[${APP_ID_ATTRIBUTE_NAME}="${appId}"]`,
const elements = doc.head?.querySelectorAll<HTMLStyleElement | HTMLLinkElement>(
`style[${APP_ID_ATTRIBUTE_NAME}="${appId}"],link[${APP_ID_ATTRIBUTE_NAME}="${appId}"]`,
);

if (styleElements) {
for (const styleElement of styleElements) {
if (styleElement.textContent) {
styleElement.removeAttribute(APP_ID_ATTRIBUTE_NAME);
usages.set(styleElement.textContent, {usage: 0, elements: [styleElement]});
if (elements) {
for (const styleElement of elements) {
styleElement.removeAttribute(APP_ID_ATTRIBUTE_NAME);
if (styleElement instanceof HTMLLinkElement) {
// Only use filename from href
// The href is build time generated with a unique value to prevent duplicates.
external.set(styleElement.href.slice(styleElement.href.lastIndexOf('/') + 1), {
usage: 0,
elements: [styleElement],
});
} else if (styleElement.textContent) {
inline.set(styleElement.textContent, {usage: 0, elements: [styleElement]});
}
}
}
Expand Down Expand Up @@ -123,7 +132,7 @@ export class SharedStylesHost implements OnDestroy {
@Inject(PLATFORM_ID) platformId: object = {},
) {
this.isServer = isPlatformServer(platformId);
addServerStyles(doc, appId, this.inline);
addServerStyles(doc, appId, this.inline, this.external);
this.hosts.add(doc.head);
}

Expand Down
27 changes: 27 additions & 0 deletions packages/platform-browser/test/dom/shared_styles_host_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ describe('SharedStylesHost', () => {
ssh.addHost(someHost);
expect(someHost.innerHTML).toEqual('<style nonce="{% nonce %}">a {};</style>');
});

it(`should reuse SSR generated element`, () => {
const style = doc.createElement('style');
style.setAttribute('ng-app-id', 'app-id');
style.textContent = 'a {};';
doc.head.appendChild(style);

ssh = new SharedStylesHost(doc, 'app-id');
ssh.addStyles(['a {};']);
expect(doc.head.innerHTML).toContain('<style ng-style-reused="">a {};</style>');
expect(doc.head.innerHTML).not.toContain('ng-app-id');
});
});

describe('external', () => {
Expand Down Expand Up @@ -114,5 +126,20 @@ describe('SharedStylesHost', () => {
'<link rel="stylesheet" href="component-1.css?ngcomp=ng-app-c123456789">',
);
});

it(`should reuse SSR generated element`, () => {
const link = doc.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', 'component-1.css');
link.setAttribute('ng-app-id', 'app-id');
doc.head.appendChild(link);

ssh = new SharedStylesHost(doc, 'app-id');
ssh.addStyles([], ['component-1.css']);
expect(doc.head.innerHTML).toContain(
'<link rel="stylesheet" href="component-1.css" ng-style-reused="">',
);
expect(doc.head.innerHTML).not.toContain('ng-app-id');
});
});
});

0 comments on commit 52be351

Please sign in to comment.