Skip to content

Commit ae0802d

Browse files
clydinalxhub
authored andcommitted
fix(platform-browser): collect external component styles from server 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
1 parent c521ce1 commit ae0802d

File tree

2 files changed

+46
-10
lines changed

2 files changed

+46
-10
lines changed

packages/platform-browser/src/dom/shared_styles_host.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,22 +57,31 @@ function createStyleElement(style: string, doc: Document): HTMLStyleElement {
5757
* identifier attribute (`ng-app-id`) to the provide identifier and adds usage records for each.
5858
* @param doc An HTML DOM document instance.
5959
* @param appId A string containing an Angular application identifer.
60-
* @param usages A Map object for tracking style usage.
60+
* @param inline A Map object for tracking inline (defined via `styles` in component decorator) style usage.
61+
* @param external A Map object for tracking external (defined via `styleUrls` in component decorator) style usage.
6162
*/
6263
function addServerStyles(
6364
doc: Document,
6465
appId: string,
65-
usages: Map<string, UsageRecord<HTMLStyleElement>>,
66+
inline: Map<string, UsageRecord<HTMLStyleElement>>,
67+
external: Map<string, UsageRecord<HTMLLinkElement>>,
6668
): void {
67-
const styleElements = doc.head?.querySelectorAll<HTMLStyleElement>(
68-
`style[${APP_ID_ATTRIBUTE_NAME}="${appId}"]`,
69+
const elements = doc.head?.querySelectorAll<HTMLStyleElement | HTMLLinkElement>(
70+
`style[${APP_ID_ATTRIBUTE_NAME}="${appId}"],link[${APP_ID_ATTRIBUTE_NAME}="${appId}"]`,
6971
);
7072

71-
if (styleElements) {
72-
for (const styleElement of styleElements) {
73-
if (styleElement.textContent) {
74-
styleElement.removeAttribute(APP_ID_ATTRIBUTE_NAME);
75-
usages.set(styleElement.textContent, {usage: 0, elements: [styleElement]});
73+
if (elements) {
74+
for (const styleElement of elements) {
75+
styleElement.removeAttribute(APP_ID_ATTRIBUTE_NAME);
76+
if (styleElement instanceof HTMLLinkElement) {
77+
// Only use filename from href
78+
// The href is build time generated with a unique value to prevent duplicates.
79+
external.set(styleElement.href.slice(styleElement.href.lastIndexOf('/') + 1), {
80+
usage: 0,
81+
elements: [styleElement],
82+
});
83+
} else if (styleElement.textContent) {
84+
inline.set(styleElement.textContent, {usage: 0, elements: [styleElement]});
7685
}
7786
}
7887
}
@@ -123,7 +132,7 @@ export class SharedStylesHost implements OnDestroy {
123132
@Inject(PLATFORM_ID) platformId: object = {},
124133
) {
125134
this.isServer = isPlatformServer(platformId);
126-
addServerStyles(doc, appId, this.inline);
135+
addServerStyles(doc, appId, this.inline, this.external);
127136
this.hosts.add(doc.head);
128137
}
129138

packages/platform-browser/test/dom/shared_styles_host_spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ describe('SharedStylesHost', () => {
6161
ssh.addHost(someHost);
6262
expect(someHost.innerHTML).toEqual('<style nonce="{% nonce %}">a {};</style>');
6363
});
64+
65+
it(`should reuse SSR generated element`, () => {
66+
const style = doc.createElement('style');
67+
style.setAttribute('ng-app-id', 'app-id');
68+
style.textContent = 'a {};';
69+
doc.head.appendChild(style);
70+
71+
ssh = new SharedStylesHost(doc, 'app-id');
72+
ssh.addStyles(['a {};']);
73+
expect(doc.head.innerHTML).toContain('<style ng-style-reused="">a {};</style>');
74+
expect(doc.head.innerHTML).not.toContain('ng-app-id');
75+
});
6476
});
6577

6678
describe('external', () => {
@@ -114,5 +126,20 @@ describe('SharedStylesHost', () => {
114126
'<link rel="stylesheet" href="component-1.css?ngcomp=ng-app-c123456789">',
115127
);
116128
});
129+
130+
it(`should reuse SSR generated element`, () => {
131+
const link = doc.createElement('link');
132+
link.setAttribute('rel', 'stylesheet');
133+
link.setAttribute('href', 'component-1.css');
134+
link.setAttribute('ng-app-id', 'app-id');
135+
doc.head.appendChild(link);
136+
137+
ssh = new SharedStylesHost(doc, 'app-id');
138+
ssh.addStyles([], ['component-1.css']);
139+
expect(doc.head.innerHTML).toContain(
140+
'<link rel="stylesheet" href="component-1.css" ng-style-reused="">',
141+
);
142+
expect(doc.head.innerHTML).not.toContain('ng-app-id');
143+
});
117144
});
118145
});

0 commit comments

Comments
 (0)