Skip to content

Commit 2f712bc

Browse files
fix(utils): remove lookbehind regexp for better performance (#6236)
* fix(utils): remove lookbehind regexp for better performance * prettier * add performance test * CI is slower * prettier * more * don't fail me on that
1 parent b7e410b commit 2f712bc

File tree

2 files changed

+46
-3
lines changed

2 files changed

+46
-3
lines changed

src/utils/shadow-css.ts

+36-2
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,26 @@ const _polyfillHostRe = /-shadowcsshost/gim;
9292
* @param selector The CSS selector we want to match for replacement
9393
* @returns A look-behind regex containing the selector
9494
*/
95-
const createSupportsRuleRe = (selector: string) =>
96-
new RegExp(`((?<!(^@supports(.*)))|(?<=\{.*))(${selector}\\b)`, 'gim');
95+
const createSupportsRuleRe = (selector: string) => {
96+
// We need to match any occurrence of the selector that's NOT inside @supports selector(...)
97+
const safeSelector = escapeRegExpSpecialCharacters(selector);
98+
99+
// This regex needs to:
100+
// 1. Skip selectors inside @supports selector(...) rule conditions
101+
// 2. Match selectors in normal CSS rules
102+
// 3. Match selectors inside declaration blocks of @supports rules
103+
104+
// To avoid matching selectors inside @supports selector() conditions, we need to carefully
105+
// construct the pattern to look for context that indicates we're NOT inside such a condition.
106+
return new RegExp(
107+
// First capture group: match any context before the selector that's not inside @supports selector()
108+
// Using negative lookahead to avoid matching inside @supports selector(...) condition
109+
`(^|[^@]|@(?!supports\\s+selector\\s*\\([^{]*?${safeSelector}))` +
110+
// Then match the selector
111+
`(${safeSelector}\\b)`,
112+
'g',
113+
);
114+
};
97115
const _colonSlottedRe = createSupportsRuleRe('::slotted');
98116
const _colonHostRe = createSupportsRuleRe(':host');
99117
const _colonHostContextRe = createSupportsRuleRe(':host-context');
@@ -212,6 +230,17 @@ const escapeBlocks = (input: string) => {
212230
* @returns The modified CSS string
213231
*/
214232
const insertPolyfillHostInCssText = (cssText: string) => {
233+
// Special handling for @supports selector() rules
234+
// We need to preserve the original selector in the condition but replace it in the declaration
235+
const supportsBlocks: string[] = [];
236+
237+
// First, extract and preserve @supports selector(...) conditions
238+
cssText = cssText.replace(/@supports\s+selector\s*\(\s*([^)]*)\s*\)/g, (_, selectorContent) => {
239+
const placeholder = `__supports_${supportsBlocks.length}__`;
240+
supportsBlocks.push(selectorContent);
241+
return `@supports selector(${placeholder})`;
242+
});
243+
215244
// These replacements use a special syntax with the `$1`. When the replacement
216245
// occurs, `$1` maps to the content of the string leading up to the selector
217246
// to be replaced.
@@ -225,6 +254,11 @@ const insertPolyfillHostInCssText = (cssText: string) => {
225254
.replace(_colonHostRe, `$1${_polyfillHost}`)
226255
.replace(_colonSlottedRe, `$1${_polyfillSlotted}`);
227256

257+
// Now restore the original @supports selector conditions
258+
supportsBlocks.forEach((originalSelector, index) => {
259+
cssText = cssText.replace(`__supports_${index}__`, originalSelector);
260+
});
261+
228262
return cssText;
229263
};
230264

0 commit comments

Comments
 (0)