Repository: tailwindlabs/tailwindcss (TypeScript)

Issue:
Arbitrary Values Not Supported for Special-Character Prefixes (e.g., `@`) in 3.1
## What version of Tailwind CSS are you using?

3.1.4

## What build tool (or framework if it abstracts the build tool) are you using?

`next@12.1.6`

## What version of Node.js are you using?

v16.14.0

## What browser are you using?

Chrome

## What operating system are you using?

MacOS

## Reproduction URL

https://play.tailwindcss.com/0B55HN10nd

## Describe your issue

Unlike in `tailwindcss@3.0.x`, arbitrary values no longer work as of v3.1.x if a special character (e.g., `@`) is used as a prefix. One can see that the regular class names work just fine, but not the arbitrary `content` styles. Related to #8601. (I actually just augmented their example for this use case.)


Hints:

Patch:
diff --git a/src/lib/defaultExtractor.js b/src/lib/defaultExtractor.js
--- a/src/lib/defaultExtractor.js
+++ b/src/lib/defaultExtractor.js
@@ -1,4 +1,4 @@
-import { flagEnabled } from '../featureFlags.js'
+import { flagEnabled } from '../featureFlags'
 import * as regex from './regex'
 
 export function defaultExtractor(context) {
@@ -22,6 +22,10 @@ export function defaultExtractor(context) {
 function* buildRegExps(context) {
   let separator = context.tailwindConfig.separator
   let variantGroupingEnabled = flagEnabled(context.tailwindConfig, 'variantGrouping')
+  let prefix =
+    context.tailwindConfig.prefix !== ''
+      ? regex.optional(regex.pattern([/-?/, regex.escape(context.tailwindConfig.prefix)]))
+      : ''
 
   let utility = regex.any([
     // Arbitrary properties
@@ -88,6 +92,8 @@ function* buildRegExps(context) {
       // Important (optional)
       /!?/,
 
+      prefix,
+
       variantGroupingEnabled
         ? regex.any([
             // Or any of those things but grouped separated by commas


Test patch:
diff --git a/tests/prefix.test.js b/tests/prefix.test.js
--- a/tests/prefix.test.js
+++ b/tests/prefix.test.js
@@ -400,3 +400,120 @@ it('supports prefixed utilities using arbitrary values', async () => {
     }
   `)
 })
+
+it('supports non-word prefixes (1)', async () => {
+  let config = {
+    prefix: '@',
+    content: [
+      {
+        raw: html`
+          <div class="@underline"></div>
+          <div class="@bg-black"></div>
+          <div class="@[color:red]"></div>
+          <div class="hover:before:@content-['Hovering']"></div>
+          <div class="my-utility"></div>
+          <div class="foo"></div>
+
+          <!-- these won't be detected -->
+          <div class="overline"></div>
+        `,
+      },
+    ],
+    corePlugins: { preflight: false },
+  }
+
+  let input = css`
+    @tailwind utilities;
+    @layer utilities {
+      .my-utility {
+        color: orange;
+      }
+    }
+    .foo {
+      @apply @text-white;
+      @apply [background-color:red];
+    }
+  `
+
+  const result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .\@bg-black {
+      --tw-bg-opacity: 1;
+      background-color: rgb(0 0 0 / var(--tw-bg-opacity));
+    }
+    .\@underline {
+      text-decoration-line: underline;
+    }
+    .my-utility {
+      color: orange;
+    }
+    .foo {
+      --tw-text-opacity: 1;
+      color: rgb(255 255 255 / var(--tw-text-opacity));
+      background-color: red;
+    }
+    .hover\:before\:\@content-\[\'Hovering\'\]:hover::before {
+      --tw-content: 'Hovering';
+      content: var(--tw-content);
+    }
+  `)
+})
+
+it('supports non-word prefixes (2)', async () => {
+  let config = {
+    prefix: '@]$',
+    content: [
+      {
+        raw: html`
+          <div class="@]$underline"></div>
+          <div class="@]$bg-black"></div>
+          <div class="@]$[color:red]"></div>
+          <div class="hover:before:@]$content-['Hovering']"></div>
+          <div class="my-utility"></div>
+          <div class="foo"></div>
+
+          <!-- these won't be detected -->
+          <div class="overline"></div>
+        `,
+      },
+    ],
+    corePlugins: { preflight: false },
+  }
+
+  let input = css`
+    @tailwind utilities;
+    @layer utilities {
+      .my-utility {
+        color: orange;
+      }
+    }
+    .foo {
+      @apply @]$text-white;
+      @apply [background-color:red];
+    }
+  `
+
+  const result = await run(input, config)
+
+  // TODO: The class `.hover\:before\:\@\]\$content-\[\'Hovering\'\]:hover::before` is not generated
+  // This happens because of the parenthesis/brace/bracket clipping performed on candidates
+
+  expect(result.css).toMatchFormattedCss(css`
+    .\@\]\$bg-black {
+      --tw-bg-opacity: 1;
+      background-color: rgb(0 0 0 / var(--tw-bg-opacity));
+    }
+    .\@\]\$underline {
+      text-decoration-line: underline;
+    }
+    .my-utility {
+      color: orange;
+    }
+    .foo {
+      --tw-text-opacity: 1;
+      color: rgb(255 255 255 / var(--tw-text-opacity));
+      background-color: red;
+    }
+  `)
+})