Skip to content

Commit 540e643

Browse files
alan-agius4dylhunn
authored andcommitted
fix(compiler): do not remove comments in component styles (#50346)
Prior to this commit, comments in CSS were being removed. This caused inline sourcemaps to break to the shift in lines. This caused sourcemaps to break in the ESBuild based builder as this always adds comments at the top of the file with the filename. Example ```css /* src/app/app.component.scss */ * { color: red; background: transparent; } /*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8uL3NyYy9hcHAvYXBwLmNvbXBvbmVudC5zY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBOzs7Ozs7Ozs7Q0FBQTtBQVdBO0VBQ0UsVUFBQTtFQUNBLHVCQUFBO0FBREYiLCJzb3VyY2VzQ29udGVudCI6WyIvL01FRElBIFFVRVJZIE1BTkFHRVJcbi8qXG4gIDAgLSA2MDA6IFBob25lXG4gIDYwMCAtIDkwMDogVGFibGV0IHBvcnRyYWl0XG4gIDkwMCAtIDEyMDA6IFRhYmxldCBsYW5kc2NhcGVcbiAgMTIwMCAtIDE4MDA6IE5vcm1hbCBzdHlsZXNcbiAgMTgwMCsgOiBCaWcgRGVza3RvcFxuICAxZW0gPSAxNnB4XG4gIFRoZSBzbWFsbGVyIGRldmljZSBydWxlcyBhbHdheXMgc2hvdWxkIHdyaXRlIGJlbG93IHRoZSBiaWdnZXIgZGV2aWNlIHJ1bGVzXG4gIEZpeGluZyBPcmRlciA9PiBCYXNlICsgVHlwb2dyYXBoeSA+PiBHZW5lcmFsIExheW91dCArIEdyaWQgPj4gUGFnZSBMYXlvdXQgKyBDb21wb25lbnRcbiovXG5cbioge1xuICBjb2xvcjogcmVkO1xuICBiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudDtcbn1cbiJdLCJzb3VyY2VSb290IjoiIn0= */ ``` Closes #50308 PR Close #50346
1 parent 57d47b3 commit 540e643

2 files changed

Lines changed: 69 additions & 49 deletions

File tree

packages/compiler/src/shadow_css.ts

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,30 @@ export class ShadowCss {
138138
* The hostSelector is the attribute added to the host itself.
139139
*/
140140
shimCssText(cssText: string, selector: string, hostSelector: string = ''): string {
141-
const commentsWithHash = extractCommentsWithHash(cssText);
142-
cssText = stripComments(cssText);
143-
cssText = this._insertDirectives(cssText);
141+
// **NOTE**: Do not strip comments as this will cause component sourcemaps to break
142+
// due to shift in lines.
143+
144+
// Collect comments and replace them with a placeholder, this is done to avoid complicating
145+
// the rule parsing RegExp and keep it safer.
146+
const comments: string[] = [];
147+
cssText = cssText.replace(_commentRe, (m) => {
148+
if (m.match(_commentWithHashRe)) {
149+
comments.push(m);
150+
} else {
151+
// Replace non hash comments with empty lines.
152+
// This is done so that we do not leak any senstive data in comments.
153+
const newLinesMatches = m.match(_newLinesRe);
154+
comments.push((newLinesMatches?.join('') ?? '') + '\n');
155+
}
144156

157+
return COMMENT_PLACEHOLDER;
158+
});
159+
160+
cssText = this._insertDirectives(cssText);
145161
const scopedCssText = this._scopeCssText(cssText, selector, hostSelector);
146-
return [scopedCssText, ...commentsWithHash].join('\n');
162+
// Add back comments at the original position.
163+
let commentIdx = 0;
164+
return scopedCssText.replace(_commentWithHashPlaceHolderRe, () => comments[commentIdx++]);
147165
}
148166

149167
private _insertDirectives(cssText: string): string {
@@ -434,7 +452,7 @@ export class ShadowCss {
434452
return cssText.replace(_cssColonHostRe, (_, hostSelectors: string, otherSelectors: string) => {
435453
if (hostSelectors) {
436454
const convertedSelectors: string[] = [];
437-
const hostSelectorArray = hostSelectors.split(',').map(p => p.trim());
455+
const hostSelectorArray = hostSelectors.split(',').map((p) => p.trim());
438456
for (const hostSelector of hostSelectorArray) {
439457
if (!hostSelector) break;
440458
const convertedSelector =
@@ -464,7 +482,7 @@ export class ShadowCss {
464482
* .foo<scopeName> .bar { ... }
465483
*/
466484
private _convertColonHostContext(cssText: string): string {
467-
return cssText.replace(_cssColonHostContextReGlobal, selectorText => {
485+
return cssText.replace(_cssColonHostContextReGlobal, (selectorText) => {
468486
// We have captured a selector that contains a `:host-context` rule.
469487

470488
// For backward compatibility `:host-context` may contain a comma separated list of selectors.
@@ -478,12 +496,12 @@ export class ShadowCss {
478496
// Execute `_cssColonHostContextRe` over and over until we have extracted all the
479497
// `:host-context` selectors from this selector.
480498
let match: RegExpExecArray|null;
481-
while (match = _cssColonHostContextRe.exec(selectorText)) {
499+
while ((match = _cssColonHostContextRe.exec(selectorText))) {
482500
// `match` = [':host-context(<selectors>)<rest>', <selectors>, <rest>]
483501

484502
// The `<selectors>` could actually be a comma separated list: `:host-context(.one, .two)`.
485503
const newContextSelectors =
486-
(match[1] ?? '').trim().split(',').map(m => m.trim()).filter(m => m !== '');
504+
(match[1] ?? '').trim().split(',').map((m) => m.trim()).filter((m) => m !== '');
487505

488506
// We must duplicate the current selector group for each of these new selectors.
489507
// For example if the current groups are:
@@ -507,8 +525,7 @@ export class ShadowCss {
507525
repeatGroups(contextSelectorGroups, newContextSelectors.length);
508526
for (let i = 0; i < newContextSelectors.length; i++) {
509527
for (let j = 0; j < contextSelectorGroupsLength; j++) {
510-
contextSelectorGroups[j + (i * contextSelectorGroupsLength)].push(
511-
newContextSelectors[i]);
528+
contextSelectorGroups[j + i * contextSelectorGroupsLength].push(newContextSelectors[i]);
512529
}
513530
}
514531

@@ -520,7 +537,7 @@ export class ShadowCss {
520537
// selectors that `:host-context` can match. See `combineHostContextSelectors()` for more
521538
// info about how this is done.
522539
return contextSelectorGroups
523-
.map(contextSelectors => combineHostContextSelectors(contextSelectors, selectorText))
540+
.map((contextSelectors) => combineHostContextSelectors(contextSelectors, selectorText))
524541
.join(', ');
525542
});
526543
}
@@ -574,7 +591,7 @@ export class ShadowCss {
574591
* ```
575592
*/
576593
private _stripScopingSelectors(cssText: string): string {
577-
return processRules(cssText, rule => {
594+
return processRules(cssText, (rule) => {
578595
const selector = rule.selector.replace(_shadowDeepSelectors, ' ')
579596
.replace(_polyfillHostNoCombinatorRe, ' ');
580597
return new CssRule(selector, rule.content);
@@ -583,7 +600,7 @@ export class ShadowCss {
583600

584601
private _scopeSelector(selector: string, scopeSelector: string, hostSelector: string): string {
585602
return selector.split(',')
586-
.map(part => part.trim().split(_shadowDeepSelectors))
603+
.map((part) => part.trim().split(_shadowDeepSelectors))
587604
.map((deepParts) => {
588605
const [shallowPart, ...otherParts] = deepParts;
589606
const applyScope = (shallowPart: string) => {
@@ -802,22 +819,18 @@ const _polyfillHostRe = /-shadowcsshost/gim;
802819
const _colonHostRe = /:host/gim;
803820
const _colonHostContextRe = /:host-context/gim;
804821

822+
const _newLinesRe = /\r?\n/g;
805823
const _commentRe = /\/\*[\s\S]*?\*\//g;
824+
const _commentWithHashRe = /\/\*\s*#\s*source(Mapping)?URL=/g;
825+
const COMMENT_PLACEHOLDER = '%COMMENT%';
826+
const _commentWithHashPlaceHolderRe = new RegExp(COMMENT_PLACEHOLDER, 'g');
806827

807828
const _placeholderRe = /__ph-(\d+)__/g;
808829

809-
function stripComments(input: string): string {
810-
return input.replace(_commentRe, '');
811-
}
812-
813-
const _commentWithHashRe = /\/\*\s*#\s*source(Mapping)?URL=[\s\S]+?\*\//g;
814-
815-
function extractCommentsWithHash(input: string): string[] {
816-
return input.match(_commentWithHashRe) || [];
817-
}
818-
819830
const BLOCK_PLACEHOLDER = '%BLOCK%';
820-
const _ruleRe = /(\s*)([^;\{\}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g;
831+
const _ruleRe = new RegExp(
832+
`(\\s*(?:${COMMENT_PLACEHOLDER}\\s*)*)([^;\\{\\}]+?)(\\s*)((?:{%BLOCK%}?\\s*;?)|(?:\\s*;))`,
833+
'g');
821834
const CONTENT_PAIRS = new Map([['{', '}']]);
822835

823836
const COMMA_IN_PLACEHOLDER = '%COMMA_IN_PLACEHOLDER%';
@@ -865,6 +878,7 @@ function escapeBlocks(
865878
let blockStartIndex = -1;
866879
let openChar: string|undefined;
867880
let closeChar: string|undefined;
881+
868882
for (let i = 0; i < input.length; i++) {
869883
const char = input[i];
870884
if (char === '\\') {
@@ -888,12 +902,14 @@ function escapeBlocks(
888902
resultParts.push(input.substring(nonBlockStartIndex, blockStartIndex));
889903
}
890904
}
905+
891906
if (blockStartIndex !== -1) {
892907
escapedBlocks.push(input.substring(blockStartIndex));
893908
resultParts.push(placeholder);
894909
} else {
895910
resultParts.push(input.substring(nonBlockStartIndex));
896911
}
912+
897913
return new StringWithEscapedBlocks(resultParts.join(''), escapedBlocks);
898914
}
899915

@@ -1025,7 +1041,6 @@ function unescapeQuotes(str: string, isQuoted: boolean): string {
10251041
*
10261042
* And so on...
10271043
*
1028-
* @param hostMarker the string that selects the host element.
10291044
* @param contextSelectors an array of context selectors that will be combined.
10301045
* @param otherSelectors the rest of the selectors that are not context selectors.
10311046
*/

packages/compiler/test/shadow_css/shadow_css_spec.ts

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -96,30 +96,6 @@ describe('ShadowCss', () => {
9696
expect(css).toEqualCss('div[contenta] {height:calc(100% - 55px);}');
9797
});
9898

99-
it('should strip comments', () => {
100-
expect(shim('/* x */b {c}', 'contenta')).toEqualCss('b[contenta] {c}');
101-
});
102-
103-
it('should ignore special characters in comments', () => {
104-
expect(shim('/* {;, */b {c}', 'contenta')).toEqualCss('b[contenta] {c}');
105-
});
106-
107-
it('should support multiline comments', () => {
108-
expect(shim('/* \n */b {c}', 'contenta')).toEqualCss('b[contenta] {c}');
109-
});
110-
111-
it('should keep sourceMappingURL comments', () => {
112-
expect(shim('b {c}/*# sourceMappingURL=data:x */', 'contenta'))
113-
.toEqualCss('b[contenta] {c} /*# sourceMappingURL=data:x */');
114-
expect(shim('b {c}/* #sourceMappingURL=data:x */', 'contenta'))
115-
.toEqualCss('b[contenta] {c} /* #sourceMappingURL=data:x */');
116-
});
117-
118-
it('should keep sourceURL comments', () => {
119-
expect(shim('/*# sourceMappingURL=data:x */b {c}/*# sourceURL=xxx */', 'contenta'))
120-
.toEqualCss('b[contenta] {c} /*# sourceMappingURL=data:x */ /*# sourceURL=xxx */');
121-
});
122-
12399
it('should shim rules with quoted content', () => {
124100
const styleStr = 'div {background-image: url("a.jpg"); color: red;}';
125101
const css = shim(styleStr, 'contenta');
@@ -137,4 +113,33 @@ describe('ShadowCss', () => {
137113
const css = shim(styleStr, 'contenta');
138114
expect(css).toEqualCss('div[contenta]::after { content:"{}"}');
139115
});
116+
117+
describe('comments', () => {
118+
// Comments should be kept in the same position as otherwise inline sourcemaps break due to
119+
// shift in lines.
120+
it('should replace multiline comments with newline', () => {
121+
expect(shim('/* b {c} */ b {c}', 'contenta')).toBe('\n b[contenta] {c}');
122+
});
123+
124+
it('should replace multiline comments with newline in the original position', () => {
125+
expect(shim('/* b {c}\n */ b {c}', 'contenta')).toBe('\n\n b[contenta] {c}');
126+
});
127+
128+
it('should replace comments with newline in the original position', () => {
129+
expect(shim('/* b {c} */ b {c} /* a {c} */ a {c}', 'contenta'))
130+
.toBe('\n b[contenta] {c} \n a[contenta] {c}');
131+
});
132+
133+
it('should keep sourceMappingURL comments', () => {
134+
expect(shim('b {c} /*# sourceMappingURL=data:x */', 'contenta'))
135+
.toBe('b[contenta] {c} /*# sourceMappingURL=data:x */');
136+
expect(shim('b {c}/* #sourceMappingURL=data:x */', 'contenta'))
137+
.toBe('b[contenta] {c}/* #sourceMappingURL=data:x */');
138+
});
139+
140+
it('should handle adjacent comments', () => {
141+
expect(shim('/* comment 1 */ /* comment 2 */ b {c}', 'contenta'))
142+
.toBe('\n \n b[contenta] {c}');
143+
});
144+
});
140145
});

0 commit comments

Comments
 (0)