Skip to content

Commit 18ed5fc

Browse files
authoredJul 17, 2023
fix(compiler): handle @supports blocks when scoping css (#4572)
* ignore `@supports` rules in "polyfill" replacements * update regex to work with multiple selectors on a line * handle entire css rule as a single line * add unit tests for `@supports` rules * add detail to regex matches
1 parent efc3b22 commit 18ed5fc

File tree

2 files changed

+93
-9
lines changed

2 files changed

+93
-9
lines changed
 

‎src/utils/shadow-css.ts

+74-9
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,29 @@ const _shadowDOMSelectorsRe = [/::shadow/g, /::content/g];
7272

7373
const _selectorReSuffix = '([>\\s~+[.,{:][\\s\\S]*)?$';
7474
const _polyfillHostRe = /-shadowcsshost/gim;
75-
const _colonHostRe = /:host/gim;
76-
const _colonSlottedRe = /::slotted/gim;
77-
const _colonHostContextRe = /:host-context/gim;
75+
76+
/**
77+
* Little helper for generating a regex that will match a specified
78+
* CSS selector when that selector is _not_ a part of a `@supports` rule.
79+
*
80+
* The pattern will match the provided `selector` (i.e. ':host', ':host-context', etc.)
81+
* when that selector is not a part of a `@supports` selector rule _or_ if the selector
82+
* is a part of the rule's declaration.
83+
*
84+
* For instance, if we create the regex with the selector ':host-context':
85+
* - '@supports selector(:host-context())' will return no matches (starts with '@supports')
86+
* - '@supports selector(:host-context()) { :host-context() { ... }}' will match the second ':host-context' (part of declaration)
87+
* - ':host-context() { ... }' will match ':host-context' (selector is not a '@supports' rule)
88+
* - ':host() { ... }' will return no matches (selector doesn't match selector used to create regex)
89+
*
90+
* @param selector The CSS selector we want to match for replacement
91+
* @returns A look-behind regex containing the selector
92+
*/
93+
const createSupportsRuleRe = (selector: string) =>
94+
new RegExp(`((?<!(^@supports(.*)))|(?<=\{.*))(${selector}\\b)`, 'gim');
95+
const _colonSlottedRe = createSupportsRuleRe('::slotted');
96+
const _colonHostRe = createSupportsRuleRe(':host');
97+
const _colonHostContextRe = createSupportsRuleRe(':host-context');
7898

7999
const _commentRe = /\/\*\s*[\s\S]*?\*\//g;
80100

@@ -153,12 +173,57 @@ const escapeBlocks = (input: string) => {
153173
return strEscapedBlocks;
154174
};
155175

156-
const insertPolyfillHostInCssText = (selector: string) => {
157-
selector = selector
158-
.replace(_colonHostContextRe, _polyfillHostContext)
159-
.replace(_colonHostRe, _polyfillHost)
160-
.replace(_colonSlottedRe, _polyfillSlotted);
161-
return selector;
176+
/**
177+
* Replaces certain strings within the CSS with placeholders
178+
* that will later be replaced with class selectors appropriate
179+
* for the level of encapsulation (shadow or scoped).
180+
*
181+
* When performing these replacements, we want to ignore selectors that are a
182+
* part of an `@supports` rule. Replacing these selectors will result in invalid
183+
* CSS that gets passed to autoprefixer/postcss once the placeholders are replaced.
184+
* For example, a rule like:
185+
*
186+
* ```css
187+
* @supports selector(:host()) {
188+
* :host {
189+
* color: red;
190+
* }
191+
* }
192+
* ```
193+
*
194+
* Should be converted to:
195+
*
196+
* ```css
197+
* @supports selector(:host()) {
198+
* -shadowcsshost {
199+
* color: red;
200+
* }
201+
* }
202+
* ```
203+
*
204+
* The order the regex replacements happen in matters since we match
205+
* against a whole selector word so we need to match all of `:host-context`
206+
* before we try to replace `:host`. Otherwise the pattern for `:host` would match
207+
* `:host-context` resulting in something like `:-shadowcsshost-context`.
208+
*
209+
* @param cssText A CSS string for a component
210+
* @returns The modified CSS string
211+
*/
212+
const insertPolyfillHostInCssText = (cssText: string) => {
213+
// These replacements use a special syntax with the `$1`. When the replacement
214+
// occurs, `$1` maps to the content of the string leading up to the selector
215+
// to be replaced.
216+
//
217+
// Otherwise, we will replace all the preceding content in addition to the
218+
// selector because of the lookbehind in the regex.
219+
//
220+
// e.g. `/*!@___0___*/:host {}` => `/*!@___0___*/--shadowcsshost {}`
221+
cssText = cssText
222+
.replace(_colonHostContextRe, `$1${_polyfillHostContext}`)
223+
.replace(_colonHostRe, `$1${_polyfillHost}`)
224+
.replace(_colonSlottedRe, `$1${_polyfillSlotted}`);
225+
226+
return cssText;
162227
};
163228

164229
const convertColonRule = (cssText: string, regExp: RegExp, partReplacer: Function) => {

‎src/utils/test/scope-css.spec.ts

+19
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,12 @@ describe('ShadowCss', function () {
206206
expect(s(':host.class:before {}', 'a')).toEqual('.class.a-h:before {}');
207207
expect(s(':host(:not(p)):before {}', 'a')).toEqual('.a-h:not(p):before {}');
208208
});
209+
210+
it('should not replace the selector in a `@supports` rule', () => {
211+
expect(s('@supports selector(:host()) {:host {color: red; }}', 'a')).toEqual(
212+
'@supports selector(:host()) {.a-h {color:red;}}'
213+
);
214+
});
209215
});
210216

211217
describe(':host-context', () => {
@@ -231,6 +237,13 @@ describe('ShadowCss', function () {
231237
expect(s(':host-context([a="b"]) {}', 'a')).toEqual('[a="b"].a-h, [a="b"] .a-h {}');
232238
expect(s(':host-context([a=b]) {}', 'a')).toEqual('[a=b].a-h, [a="b"] .a-h {}');
233239
});
240+
241+
it('should not replace the selector in a `@supports` rule', () => {
242+
expect(s('@supports selector(:host-context(.class1)) {:host-context(.class1) {color: red; }}', 'a')).toEqual(
243+
'@supports selector(:host-context(.class1)) {.class1.a-h, .class1 .a-h {color:red;}}'
244+
);
245+
});
246+
``;
234247
});
235248

236249
describe('::slotted', () => {
@@ -307,6 +320,12 @@ describe('ShadowCss', function () {
307320
const r = s('::slotted(ul), ::slotted(li) {}', 'sc-ion-tag', true);
308321
expect(r).toEqual('/*!@::slotted(ul), ::slotted(li)*/.sc-ion-tag-s > ul, .sc-ion-tag-s > li {}');
309322
});
323+
324+
it('should not replace the selector in a `@supports` rule', () => {
325+
expect(s('@supports selector(::slotted(*)) {::slotted(*) {color: red; }}', 'sc-cmp')).toEqual(
326+
'@supports selector(::slotted(*)) {.sc-cmp-s > * {color:red;}}'
327+
);
328+
});
310329
});
311330

312331
describe('convertScopedToShadow', () => {

0 commit comments

Comments
 (0)
Please sign in to comment.