From 82a96ecd7bb0b862ecf8c3d1c5179171d8d1330b Mon Sep 17 00:00:00 2001 From: "yuqing.rysinal" Date: Thu, 14 May 2026 16:40:36 +0800 Subject: [PATCH] fix: handle tailwind v4 scoped css edge cases --- src/__tests__/source/scoped_css.test.ts | 11 ++++-- src/sandbox/scoped_css.ts | 47 +++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/__tests__/source/scoped_css.test.ts b/src/__tests__/source/scoped_css.test.ts index 316d5e5f6..a15518150 100644 --- a/src/__tests__/source/scoped_css.test.ts +++ b/src/__tests__/source/scoped_css.test.ts @@ -13,7 +13,6 @@ const md5 = (content: string) => { return createHash('md5').update(content).digest('hex') } - describe('source scoped_css', () => { let appCon: Element beforeAll(() => { @@ -101,12 +100,12 @@ describe('source scoped_css', () => { setAppName('test-app3') // 动态创建style const dynamicStyle = document.createElement('style') - dynamicStyle.textContent = '@font-face {font-family: test-font;} @media screen and (max-width: 300px) {body {background:lightblue;}} @supports (display: grid) {div {display: grid;}} @unknown {}' + dynamicStyle.textContent = '@font-face {font-family: test-font;} @property --tw-translate-x { syntax: "*"; inherits: false; initial-value: 0; } @media screen and (max-width: 300px) {body {background:lightblue;}} @supports (display: grid) {div {display: grid;}} @unknown {}' document.head.appendChild(dynamicStyle) defer(() => { - expect(dynamicStyle.textContent).toBe('@font-face {font-family: test-font;} @media screen and (max-width: 300px) {micro-app[name=test-app3] micro-app-body{background:lightblue;}} @supports (display: grid) {micro-app[name=test-app3] div{display: grid;}} micro-app[name=test-app3] @unknown{}') + expect(dynamicStyle.textContent).toBe('@font-face {font-family: test-font;} @property --tw-translate-x { syntax: "*"; inherits: false; initial-value: 0; } @media screen and (max-width: 300px) {micro-app[name=test-app3] micro-app-body{background:lightblue;}} @supports (display: grid) {micro-app[name=test-app3] div{display: grid;}} micro-app[name=test-app3] @unknown{}') resolve(true) }) }, false) @@ -405,6 +404,12 @@ describe('source scoped_css', () => { document.head.appendChild(dynamicStyle7) expect(dynamicStyle7.textContent).toBe('micro-app[name=test-app11] .test1, micro-app[name=test-app11] .test2{color: red}') + // keep commas inside pseudo-class arguments and escaped arbitrary values + const dynamicStyle8 = document.createElement('style') + dynamicStyle8.textContent = ':is(.a, .b):has(+ .c, + .d){} .grid-cols-\\[minmax\\(0\\,_1fr\\)\\]{grid-template-columns:minmax(0, 1fr)}' + document.head.appendChild(dynamicStyle8) + expect(dynamicStyle8.textContent).toBe('micro-app[name=test-app11] :is(.a, .b):has(+ .c, + .d){} micro-app[name=test-app11] .grid-cols-\\[minmax\\(0\\,_1fr\\)\\]{grid-template-columns:minmax(0, 1fr)}') + resolve(true) }, false) }) diff --git a/src/sandbox/scoped_css.ts b/src/sandbox/scoped_css.ts index 2f0f8df34..fc126d17f 100644 --- a/src/sandbox/scoped_css.ts +++ b/src/sandbox/scoped_css.ts @@ -115,7 +115,7 @@ class CSSParser { return match.replace(p2, mock) }) - return matchRes.replace(/(^|,[\n\s]*)([^,]+)/g, (_, separator, selector) => { + const scopeSelector = (separator: string, selector: string): string => { selector = trim(selector) selector = selector.replace(/\[[^\]=]+(?:=([^\]]+))?\]/g, (match:string, p1: string) => { if (attributeValues[p1]) { @@ -141,7 +141,42 @@ class CSSParser { } return separator + selector - }) + } + + const selectors: Array = [] + let selectorStart = 0 + let selectorSeparator = '' + let bracketDepth = 0 + let parenDepth = 0 + + for (let i = 0; i < matchRes.length; i++) { + const char = matchRes[i] + if (char === '\\') { + i++ + continue + } + + if (char === '[') { + bracketDepth++ + } else if (char === ']') { + bracketDepth = Math.max(0, bracketDepth - 1) + } else if (char === '(') { + parenDepth++ + } else if (char === ')') { + parenDepth = Math.max(0, parenDepth - 1) + } else if (char === ',' && bracketDepth === 0 && parenDepth === 0) { + selectors.push(scopeSelector(selectorSeparator, matchRes.slice(selectorStart, i))) + + const separatorMatch = /^,[\n\s]*/.exec(matchRes.slice(i)) + selectorSeparator = separatorMatch ? separatorMatch[0] : ',' + i += selectorSeparator.length - 1 + selectorStart = i + 1 + } + } + + selectors.push(scopeSelector(selectorSeparator, matchRes.slice(selectorStart))) + + return selectors.join('') } // https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration @@ -220,6 +255,7 @@ class CSSParser { this.pageRule() || this.hostRule() || this.fontFaceRule() || + this.propertyRule() || this.layerRule() } @@ -299,6 +335,13 @@ class CSSParser { return this.commonHandlerForAtRuleWithSelfRule('font-face') } + // https://developer.mozilla.org/en-US/docs/Web/CSS/@property + private propertyRule (): boolean | void { + if (!this.commonMatch(/^@property\s+([^{]+)/)) return false + + return this.commonHandlerForAtRuleWithSelfRule('property') + } + // https://developer.mozilla.org/en-US/docs/Web/CSS/@layer private layerRule (): boolean | void { if (!this.commonMatch(/^@layer\s*([^{;]+)/)) return false