Repository: tailwindlabs/tailwindcss (TypeScript)

Issue:
Dynamic nested groups names webpack error
**What version of Tailwind CSS are you using?**

I'm using the latest v3.2.4

**What build tool (or framework if it abstracts the build tool) are you using?**

I'm using Next.js (v13.1.1) with Webpack (v5.74.0)

**What version of Node.js are you using?**

v18.12.1

**What browser are you using?**

Chrome

**What operating system are you using?**

Windows

**Reproduction URL**

[CodeSandbox bug repoduction](https://codesandbox.io/p/sandbox/gifted-clarke-l6ite9?file=%2Fcomponents%2FContainer.tsx&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A27%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A27%7D%5D)

**Describe your issue**

I tried to assign names to groups by props dynamically on runtime but it failed to compile with this message:

`./node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[3].oneOf[11].use[1]!./node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[3].oneOf[11].use[2]!./styles/globals.css
TypeError: Cannot read properties of undefined (reading '5')
    at resolveMatches.next (<anonymous>)
    at Function.from (<anonymous>)
    at runMicrotasks (<anonymous>)`

When i concatenate the `id` prop to a group with `+` operator this error don't come up but nested group doesn't work. I tried every plugin out there but nothing worked naming groups dynamically by props.


Hints:
Hey! You can’t use concatenation or interpolation to create class names, as Tailwind generates all of your styles at build time and needs to be able to statically extract every class you’re using from your templates:

https://tailwindcss.com/docs/content-configuration#dynamic-class-names

It shouldn’t cause the build to fail or crash though, so will definitely look into that.
Here's a super minimal reproduction that triggers the error even in Play:

https://play.tailwindcss.com/s8ydiMPxcN
Hey! Thanks for that information, i do a little change and it's working now.
Going to keep this open so we can diagnose the crash 👍🏻

Patch:
diff --git a/src/corePlugins.js b/src/corePlugins.js
--- a/src/corePlugins.js
+++ b/src/corePlugins.js
@@ -150,9 +150,13 @@ export let variantPlugins = {
 
     let variants = {
       group: (_, { modifier }) =>
-        modifier ? [`:merge(.group\\/${modifier})`, ' &'] : [`:merge(.group)`, ' &'],
+        modifier
+          ? [`:merge(.group\\/${escapeClassName(modifier)})`, ' &']
+          : [`:merge(.group)`, ' &'],
       peer: (_, { modifier }) =>
-        modifier ? [`:merge(.peer\\/${modifier})`, ' ~ &'] : [`:merge(.peer)`, ' ~ &'],
+        modifier
+          ? [`:merge(.peer\\/${escapeClassName(modifier)})`, ' ~ &']
+          : [`:merge(.peer)`, ' ~ &'],
     }
 
     for (let [name, fn] of Object.entries(variants)) {
diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js
--- a/src/lib/setupContextUtils.js
+++ b/src/lib/setupContextUtils.js
@@ -54,32 +54,50 @@ function normalizeOptionTypes({ type = 'any', ...options }) {
 }
 
 function parseVariantFormatString(input) {
-  if (input.includes('{')) {
-    if (!isBalanced(input)) throw new Error(`Your { and } are unbalanced.`)
-
-    return input
-      .split(/{(.*)}/gim)
-      .flatMap((line) => parseVariantFormatString(line))
-      .filter(Boolean)
-  }
-
-  return [input.trim()]
-}
-
-function isBalanced(input) {
-  let count = 0
-
-  for (let char of input) {
-    if (char === '{') {
-      count++
+  /** @type {string[]} */
+  let parts = []
+
+  // When parsing whitespace around special characters are insignificant
+  // However, _inside_ of a variant they could be
+  // Because the selector could look like this
+  // @media { &[data-name="foo bar"] }
+  // This is why we do not skip whitespace
+
+  let current = ''
+  let depth = 0
+
+  for (let idx = 0; idx < input.length; idx++) {
+    let char = input[idx]
+
+    if (char === '\\') {
+      // Escaped characters are not special
+      current += '\\' + input[++idx]
+    } else if (char === '{') {
+      // Nested rule: start
+      ++depth
+      parts.push(current.trim())
+      current = ''
     } else if (char === '}') {
-      if (--count < 0) {
-        return false // unbalanced
+      // Nested rule: end
+      if (--depth < 0) {
+        throw new Error(`Your { and } are unbalanced.`)
       }
+
+      parts.push(current.trim())
+      current = ''
+    } else {
+      // Normal character
+      current += char
     }
   }
 
-  return count === 0
+  if (current.length > 0) {
+    parts.push(current.trim())
+  }
+
+  parts = parts.filter((part) => part !== '')
+
+  return parts
 }
 
 function insertInto(list, value, { before = [] } = {}) {


Test patch:
diff --git a/tests/basic-usage.test.js b/tests/basic-usage.test.js
--- a/tests/basic-usage.test.js
+++ b/tests/basic-usage.test.js
@@ -689,3 +689,27 @@ it('Ring color utilities are generated when using respectDefaultRingColorOpacity
     `)
   })
 })
+
+it('should not crash when group names contain special characters', () => {
+  let config = {
+    future: { respectDefaultRingColorOpacity: true },
+    content: [
+      {
+        raw: '<div class="group/${id}"><div class="group-hover/${id}:visible"></div></div>',
+      },
+    ],
+    corePlugins: { preflight: false },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .group\/\$\{id\}:hover .group-hover\/\$\{id\}\:visible {
+        visibility: visible;
+      }
+    `)
+  })
+})