diff --git a/.changeset/flat-humans-hammer.md b/.changeset/flat-humans-hammer.md new file mode 100644 index 00000000..02b672d0 --- /dev/null +++ b/.changeset/flat-humans-hammer.md @@ -0,0 +1,5 @@ +--- +'@vanilla-extract/css': patch +--- + +Fixes a bug that caused invalid selectors to be generated when adjacent classnames contained a substring equal to another local classname diff --git a/packages/css/src/transformCss.test.ts b/packages/css/src/transformCss.test.ts index fe9fa25c..5debcbc1 100644 --- a/packages/css/src/transformCss.test.ts +++ b/packages/css/src/transformCss.test.ts @@ -2299,6 +2299,61 @@ describe('transformCss', () => { } `); }); + + it('should handle adjacent classnames containing a separate local classname as a substring', () => { + // Note that `classname2` starts and ends with the same character, so when two `classname1`s are + // adjacent, the resulting string will contain `classname2` as a substring + const classname1 = 'debugName_hash1'; + const classname2 = 'debugName_hash1d'; + + expect( + transformCss({ + composedClassLists: [], + localClassNames: [classname1, classname2], + + cssObjs: [ + { + type: 'local', + selector: classname1, + rule: { + selectors: { + ['&&']: { + background: 'black', + }, + [`${classname2}&`]: { + background: 'orange', + }, + [`&${classname2}&`]: { + background: 'orange', + }, + [`${classname2}${classname2}&`]: { + background: 'orange', + }, + }, + }, + }, + { + type: 'local', + selector: classname2, + rule: {}, + }, + ], + }).join('\n'), + ).toMatchInlineSnapshot(` + .debugName_hash1.debugName_hash1 { + background: black; + } + .debugName_hash1d.debugName_hash1 { + background: orange; + } + .debugName_hash1.debugName_hash1d.debugName_hash1 { + background: orange; + } + .debugName_hash1d.debugName_hash1d.debugName_hash1 { + background: orange; + } + `); + }); }); endFileScope(); diff --git a/packages/css/src/transformCss.ts b/packages/css/src/transformCss.ts index 6b0248ee..3750eaa0 100644 --- a/packages/css/src/transformCss.ts +++ b/packages/css/src/transformCss.ts @@ -317,11 +317,20 @@ class Stylesheet { const [endIndex, [firstMatch]] = results[i]; const startIndex = endIndex - firstMatch.length + 1; - if (startIndex >= lastReplaceIndex) { - // Class names can be substrings of other class names - // e.g. '_1g1ptzo1' and '_1g1ptzo10' - // If the startIndex >= lastReplaceIndex, then - // this is the case and this replace should be skipped + // Class names can be substrings of other class names + // e.g. '_1g1ptzo1' and '_1g1ptzo10' + // + // Additionally, concatenated classnames can contain substrings equal to other classnames + // e.g. '&&' where '&' is 'debugName_hash1' and 'debugName_hash1d' is also a local classname + // Before transforming the selector, this would look like `debugName_hash1debugName_hash1` + // which contains the substring `debugName_hash1d`’. + // + // In either of these cases, the last replace index will occur either before or within the + // current replacement range (from `startIndex` to `endIndex`). + // If this occurs, we skip the replacement to avoid transforming the selector incorrectly. + const skipReplacement = lastReplaceIndex <= endIndex; + + if (skipReplacement) { continue; }