Skip to content

Commit

Permalink
fix(compiler): handle @supports blocks when scoping css (#4572)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
tanner-reits committed Jul 17, 2023
1 parent efc3b22 commit 18ed5fc
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 9 deletions.
83 changes: 74 additions & 9 deletions src/utils/shadow-css.ts
Expand Up @@ -72,9 +72,29 @@ const _shadowDOMSelectorsRe = [/::shadow/g, /::content/g];

const _selectorReSuffix = '([>\\s~+[.,{:][\\s\\S]*)?$';
const _polyfillHostRe = /-shadowcsshost/gim;
const _colonHostRe = /:host/gim;
const _colonSlottedRe = /::slotted/gim;
const _colonHostContextRe = /:host-context/gim;

/**
* Little helper for generating a regex that will match a specified
* CSS selector when that selector is _not_ a part of a `@supports` rule.
*
* The pattern will match the provided `selector` (i.e. ':host', ':host-context', etc.)
* when that selector is not a part of a `@supports` selector rule _or_ if the selector
* is a part of the rule's declaration.
*
* For instance, if we create the regex with the selector ':host-context':
* - '@supports selector(:host-context())' will return no matches (starts with '@supports')
* - '@supports selector(:host-context()) { :host-context() { ... }}' will match the second ':host-context' (part of declaration)
* - ':host-context() { ... }' will match ':host-context' (selector is not a '@supports' rule)
* - ':host() { ... }' will return no matches (selector doesn't match selector used to create regex)
*
* @param selector The CSS selector we want to match for replacement
* @returns A look-behind regex containing the selector
*/
const createSupportsRuleRe = (selector: string) =>
new RegExp(`((?<!(^@supports(.*)))|(?<=\{.*))(${selector}\\b)`, 'gim');
const _colonSlottedRe = createSupportsRuleRe('::slotted');
const _colonHostRe = createSupportsRuleRe(':host');
const _colonHostContextRe = createSupportsRuleRe(':host-context');

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

Expand Down Expand Up @@ -153,12 +173,57 @@ const escapeBlocks = (input: string) => {
return strEscapedBlocks;
};

const insertPolyfillHostInCssText = (selector: string) => {
selector = selector
.replace(_colonHostContextRe, _polyfillHostContext)
.replace(_colonHostRe, _polyfillHost)
.replace(_colonSlottedRe, _polyfillSlotted);
return selector;
/**
* Replaces certain strings within the CSS with placeholders
* that will later be replaced with class selectors appropriate
* for the level of encapsulation (shadow or scoped).
*
* When performing these replacements, we want to ignore selectors that are a
* part of an `@supports` rule. Replacing these selectors will result in invalid
* CSS that gets passed to autoprefixer/postcss once the placeholders are replaced.
* For example, a rule like:
*
* ```css
* @supports selector(:host()) {
* :host {
* color: red;
* }
* }
* ```
*
* Should be converted to:
*
* ```css
* @supports selector(:host()) {
* -shadowcsshost {
* color: red;
* }
* }
* ```
*
* The order the regex replacements happen in matters since we match
* against a whole selector word so we need to match all of `:host-context`
* before we try to replace `:host`. Otherwise the pattern for `:host` would match
* `:host-context` resulting in something like `:-shadowcsshost-context`.
*
* @param cssText A CSS string for a component
* @returns The modified CSS string
*/
const insertPolyfillHostInCssText = (cssText: string) => {
// These replacements use a special syntax with the `$1`. When the replacement
// occurs, `$1` maps to the content of the string leading up to the selector
// to be replaced.
//
// Otherwise, we will replace all the preceding content in addition to the
// selector because of the lookbehind in the regex.
//
// e.g. `/*!@___0___*/:host {}` => `/*!@___0___*/--shadowcsshost {}`
cssText = cssText
.replace(_colonHostContextRe, `$1${_polyfillHostContext}`)
.replace(_colonHostRe, `$1${_polyfillHost}`)
.replace(_colonSlottedRe, `$1${_polyfillSlotted}`);

return cssText;
};

const convertColonRule = (cssText: string, regExp: RegExp, partReplacer: Function) => {
Expand Down
19 changes: 19 additions & 0 deletions src/utils/test/scope-css.spec.ts
Expand Up @@ -206,6 +206,12 @@ describe('ShadowCss', function () {
expect(s(':host.class:before {}', 'a')).toEqual('.class.a-h:before {}');
expect(s(':host(:not(p)):before {}', 'a')).toEqual('.a-h:not(p):before {}');
});

it('should not replace the selector in a `@supports` rule', () => {
expect(s('@supports selector(:host()) {:host {color: red; }}', 'a')).toEqual(
'@supports selector(:host()) {.a-h {color:red;}}'
);
});
});

describe(':host-context', () => {
Expand All @@ -231,6 +237,13 @@ describe('ShadowCss', function () {
expect(s(':host-context([a="b"]) {}', 'a')).toEqual('[a="b"].a-h, [a="b"] .a-h {}');
expect(s(':host-context([a=b]) {}', 'a')).toEqual('[a=b].a-h, [a="b"] .a-h {}');
});

it('should not replace the selector in a `@supports` rule', () => {
expect(s('@supports selector(:host-context(.class1)) {:host-context(.class1) {color: red; }}', 'a')).toEqual(
'@supports selector(:host-context(.class1)) {.class1.a-h, .class1 .a-h {color:red;}}'
);
});
``;
});

describe('::slotted', () => {
Expand Down Expand Up @@ -307,6 +320,12 @@ describe('ShadowCss', function () {
const r = s('::slotted(ul), ::slotted(li) {}', 'sc-ion-tag', true);
expect(r).toEqual('/*!@::slotted(ul), ::slotted(li)*/.sc-ion-tag-s > ul, .sc-ion-tag-s > li {}');
});

it('should not replace the selector in a `@supports` rule', () => {
expect(s('@supports selector(::slotted(*)) {::slotted(*) {color: red; }}', 'sc-cmp')).toEqual(
'@supports selector(::slotted(*)) {.sc-cmp-s > * {color:red;}}'
);
});
});

describe('convertScopedToShadow', () => {
Expand Down

0 comments on commit 18ed5fc

Please sign in to comment.