@@ -20,36 +20,46 @@ const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/;
20
20
const CSP_MEDIA_ATTR = 'ngCspMedia' ;
21
21
22
22
/**
23
- * Script text used to change the media value of the link tags.
23
+ * Script that dynamically updates the ` media` attribute of `< link>` tags based on a custom attribute (`CSP_MEDIA_ATTR`) .
24
24
*
25
25
* NOTE:
26
26
* We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)`
27
- * because this does not always fire on Chome .
27
+ * because load events are not always triggered reliably on Chrome .
28
28
* See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256
29
+ *
30
+ * The script:
31
+ * - Ensures the event target is a `<link>` tag with the `CSP_MEDIA_ATTR` attribute.
32
+ * - Updates the `media` attribute with the value of `CSP_MEDIA_ATTR` and then removes the attribute.
33
+ * - Removes the event listener when all relevant `<link>` tags have been processed.
34
+ * - Uses event capturing (the `true` parameter) since load events do not bubble up the DOM.
29
35
*/
30
- const LINK_LOAD_SCRIPT_CONTENT = [
31
- '(() => {' ,
32
- ` const CSP_MEDIA_ATTR = '${ CSP_MEDIA_ATTR } ';` ,
33
- ' const documentElement = document.documentElement;' ,
34
- ' const listener = (e) => {' ,
35
- ' const target = e.target;' ,
36
- ` if (!target || target.tagName !== 'LINK' || !target.hasAttribute(CSP_MEDIA_ATTR)) {` ,
37
- ' return;' ,
38
- ' }' ,
39
-
40
- ' target.media = target.getAttribute(CSP_MEDIA_ATTR);' ,
41
- ' target.removeAttribute(CSP_MEDIA_ATTR);' ,
42
-
43
- // Remove onload listener when there are no longer styles that need to be loaded.
44
- ' if (!document.head.querySelector(`link[${CSP_MEDIA_ATTR}]`)) {' ,
45
- ` documentElement.removeEventListener('load', listener);` ,
46
- ' }' ,
47
- ' };' ,
48
-
49
- // We use an event with capturing (the true parameter) because load events don't bubble.
50
- ` documentElement.addEventListener('load', listener, true);` ,
51
- '})();' ,
52
- ] . join ( '\n' ) ;
36
+ const LINK_LOAD_SCRIPT_CONTENT = `
37
+ (() => {
38
+ const CSP_MEDIA_ATTR = '${ CSP_MEDIA_ATTR } ';
39
+ const documentElement = document.documentElement;
40
+
41
+ // Listener for load events on link tags.
42
+ const listener = (e) => {
43
+ const target = e.target;
44
+ if (
45
+ !target ||
46
+ target.tagName !== 'LINK' ||
47
+ !target.hasAttribute(CSP_MEDIA_ATTR)
48
+ ) {
49
+ return;
50
+ }
51
+
52
+ target.media = target.getAttribute(CSP_MEDIA_ATTR);
53
+ target.removeAttribute(CSP_MEDIA_ATTR);
54
+
55
+ if (!document.head.querySelector(\`link[\${CSP_MEDIA_ATTR}]\`)) {
56
+ documentElement.removeEventListener('load', listener);
57
+ }
58
+ };
59
+
60
+ documentElement.addEventListener('load', listener, true);
61
+ })();
62
+ ` . trim ( ) ;
53
63
54
64
export interface InlineCriticalCssProcessOptions {
55
65
outputPath : string ;
@@ -85,22 +95,22 @@ interface PartialDocument {
85
95
querySelector ( selector : string ) : PartialHTMLElement | null ;
86
96
}
87
97
88
- /** Signature of the `Critters.embedLinkedStylesheet` method. */
89
- type EmbedLinkedStylesheetFn = (
90
- link : PartialHTMLElement ,
91
- document : PartialDocument ,
92
- ) => Promise < unknown > ;
98
+ /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
93
99
94
- class CrittersExtended extends Critters {
100
+ // We use Typescript declaration merging because `embedLinkedStylesheet` it's not declared in
101
+ // the `Critters` types which means that we can't call the `super` implementation.
102
+ interface CrittersBase {
103
+ embedLinkedStylesheet ( link : PartialHTMLElement , document : PartialDocument ) : Promise < unknown > ;
104
+ }
105
+ class CrittersBase extends Critters { }
106
+ /* eslint-enable @typescript-eslint/no-unsafe-declaration-merging */
107
+
108
+ class CrittersExtended extends CrittersBase {
95
109
readonly warnings : string [ ] = [ ] ;
96
110
readonly errors : string [ ] = [ ] ;
97
- private initialEmbedLinkedStylesheet : EmbedLinkedStylesheetFn ;
98
111
private addedCspScriptsDocuments = new WeakSet < PartialDocument > ( ) ;
99
112
private documentNonces = new WeakMap < PartialDocument , string | null > ( ) ;
100
113
101
- // Inherited from `Critters`, but not exposed in the typings.
102
- protected declare embedLinkedStylesheet : EmbedLinkedStylesheetFn ;
103
-
104
114
constructor (
105
115
private readonly optionsExtended : InlineCriticalCssProcessorOptions &
106
116
InlineCriticalCssProcessOptions ,
@@ -119,17 +129,11 @@ class CrittersExtended extends Critters {
119
129
reduceInlineStyles : false ,
120
130
mergeStylesheets : false ,
121
131
// Note: if `preload` changes to anything other than `media`, the logic in
122
- // `embedLinkedStylesheetOverride ` will have to be updated.
132
+ // `embedLinkedStylesheet ` will have to be updated.
123
133
preload : 'media' ,
124
134
noscriptFallback : true ,
125
135
inlineFonts : true ,
126
136
} ) ;
127
-
128
- // We can't use inheritance to override `embedLinkedStylesheet`, because it's not declared in
129
- // the `Critters` .d.ts which means that we can't call the `super` implementation. TS doesn't
130
- // allow for `super` to be cast to a different type.
131
- this . initialEmbedLinkedStylesheet = this . embedLinkedStylesheet ;
132
- this . embedLinkedStylesheet = this . embedLinkedStylesheetOverride ;
133
137
}
134
138
135
139
public override readFile ( path : string ) : Promise < string > {
@@ -142,7 +146,10 @@ class CrittersExtended extends Critters {
142
146
* Override of the Critters `embedLinkedStylesheet` method
143
147
* that makes it work with Angular's CSP APIs.
144
148
*/
145
- private embedLinkedStylesheetOverride : EmbedLinkedStylesheetFn = async ( link , document ) => {
149
+ override async embedLinkedStylesheet (
150
+ link : PartialHTMLElement ,
151
+ document : PartialDocument ,
152
+ ) : Promise < unknown > {
146
153
if ( link . getAttribute ( 'media' ) === 'print' && link . next ?. name === 'noscript' ) {
147
154
// Workaround for https://github.com/GoogleChromeLabs/critters/issues/64
148
155
// NB: this is only needed for the webpack based builders.
@@ -154,7 +161,7 @@ class CrittersExtended extends Critters {
154
161
}
155
162
}
156
163
157
- const returnValue = await this . initialEmbedLinkedStylesheet ( link , document ) ;
164
+ const returnValue = await super . embedLinkedStylesheet ( link , document ) ;
158
165
const cspNonce = this . findCspNonce ( document ) ;
159
166
160
167
if ( cspNonce ) {
@@ -182,7 +189,7 @@ class CrittersExtended extends Critters {
182
189
}
183
190
184
191
return returnValue ;
185
- } ;
192
+ }
186
193
187
194
/**
188
195
* Finds the CSP nonce for a specific document.
0 commit comments