Repository: tailwindlabs/tailwindcss (TypeScript)

Issue:
[JIT] Add support for "raw" purge content
### What version of Tailwind CSS are you using?

v2.1.1

### What build tool (or framework if it abstracts the build tool) are you using?

Maizzle v3.3.1

### What version of Node.js are you using?

v14.15.1

### What browser are you using?

Chrome

### What operating system are you using?

Windows 10

### Reproduction repository

https://github.com/maizzle/maizzle

### Describe your issue

Configuring the `purge` option in your build chain instead of `tailwind.config.js` results in:

```
TypeError: expected path to be a string
```

Sounds like it expects it to be in the `tailwind.config.js` file.

What I'm doing is definining a base config in the file where I compile Tailwind, like this:

```js
const coreConfig = {
  purge: {
    enabled: true,
    content: [
      'src/**/*.*',
    ],
  },
}
```

I then merge that with the config read from my `tailwind.config.js` and use it to compile Tailwind with PostCSS:

```js
// merge() is from lodash
// `tailwindConfigObject` is the contents of `tailwind.config.js`

return postcss([
  tailwindcss(merge(coreConfig, tailwindConfigObject)),
])
  .process(css, {from: undefined})
  .then(result => result.css)
```

The resulting config is correct, and it works fine without JIT. 

When I add `mode: 'jit'` in `tailwind.config.js`, it throws the error. As soon as I also add `purge: ['src/**/*.*']` in `tailwind.config.js`, JIT works as expected.


Hints:
Just tried to use an object config right in `tailwind.config.js` for `purge`:

```js
module.exports = {
  mode: 'jit',
  // purge: [
  //   'src/**/*.*',
  // ],
  purge: {
    enabled: true,
    content: [
      'src/**/*.*',
    ],
  },
}
```

... and the same error is thrown. I suppose object syntax just isn't supported yet here, that also being one of the reasons purgecss options like `safelist` aren't supported? 

Apologies if this has been covered somewhere, I couldn't find any issue/docs about it except the `safelist` thing.
> I suppose object syntax just isn't supported yet here

Providing an object with a `content` property is supported, and your example should work fine. Is there any chance you could provide a minimal reproduction? I can't see a purge config anywhere in the maizzle repo. Thanks!
The purge config is in the framework repo - my bad!

https://github.com/maizzle/framework/blob/0dabece8b036739a0bdd8ed83c31cf8b452a3e1f/src/generators/tailwindcss.js#L22-L29

I just [merge](https://github.com/maizzle/framework/blob/0dabece8b036739a0bdd8ed83c31cf8b452a3e1f/src/generators/tailwindcss.js#L62) whatever you have in tailwind.config.js on top of that.

So just adding `mode: 'jit'` in `tailwind.config.js`, without including a `purge` key with simplified array syntax (thus relying on the object syntax defined in the framework linked above), will hang 🤔
Created a repo that reproduces the issue, I've added instructions in the readme:

https://github.com/maizzle/jit-object-config
Thanks! It's because [you are using "raw" content](https://github.com/maizzle/framework/blob/0dabece8b036739a0bdd8ed83c31cf8b452a3e1f/src/generators/tailwindcss.js#L26), which isn't currently supported by JIT mode. We will probably add support for this at some point for consistency with PurgeCSS, but for now that's why it isn't working, sorry about that!
Ah, OK. Yeah I need `raw` when rendering templates programmatically so that Tailwind can purge based on the HTML string that I want to compile, since there are no file paths in that context.

Feel free to close the issue if you prefer, and thanks for looking into it! :)

Patch:
diff --git a/src/jit/lib/expandTailwindAtRules.js b/src/jit/lib/expandTailwindAtRules.js
--- a/src/jit/lib/expandTailwindAtRules.js
+++ b/src/jit/lib/expandTailwindAtRules.js
@@ -25,9 +25,12 @@ function getDefaultExtractor(fileExtension) {
   }
 }
 
-function getExtractor(fileName, tailwindConfig) {
+function getExtractor(tailwindConfig, fileExtension) {
   const purgeOptions = tailwindConfig && tailwindConfig.purge && tailwindConfig.purge.options
-  const fileExtension = path.extname(fileName).slice(1)
+
+  if (!fileExtension) {
+    return (purgeOptions && purgeOptions.defaultExtractor) || getDefaultExtractor()
+  }
 
   if (!purgeOptions) {
     return getDefaultExtractor(fileExtension)
@@ -208,11 +211,16 @@ export default function expandTailwindAtRules(context, registerDependency) {
     env.DEBUG && console.time('Reading changed files')
     for (let file of context.changedFiles) {
       let content = fs.readFileSync(file, 'utf8')
-      let extractor = getExtractor(file, context.tailwindConfig)
+      let extractor = getExtractor(context.tailwindConfig, path.extname(file).slice(1))
       getClassCandidates(content, extractor, contentMatchCache, candidates, seen)
     }
     env.DEBUG && console.timeEnd('Reading changed files')
 
+    for (let { content, extension } of context.rawContent) {
+      let extractor = getExtractor(context.tailwindConfig, extension)
+      getClassCandidates(content, extractor, contentMatchCache, candidates, seen)
+    }
+
     // ---
 
     // Generate the actual CSS
diff --git a/src/jit/lib/setupContext.js b/src/jit/lib/setupContext.js
--- a/src/jit/lib/setupContext.js
+++ b/src/jit/lib/setupContext.js
@@ -767,6 +767,10 @@ export default function setupContext(configOrPath) {
 
     process.env.DEBUG && console.log('Setting up new context...')
 
+    let purgeContent = Array.isArray(tailwindConfig.purge)
+      ? tailwindConfig.purge
+      : tailwindConfig.purge.content
+
     let context = {
       changedFiles: new Set(),
       ruleCache: new Set(),
@@ -781,10 +785,12 @@ export default function setupContext(configOrPath) {
       configPath: userConfigPath,
       tailwindConfig: tailwindConfig,
       configDependencies: new Set(),
-      candidateFiles: (Array.isArray(tailwindConfig.purge)
-        ? tailwindConfig.purge
-        : tailwindConfig.purge.content
-      ).map((path) => normalizePath(path)),
+      candidateFiles: purgeContent
+        .filter((item) => typeof item === 'string')
+        .map((path) => normalizePath(path)),
+      rawContent: purgeContent
+        .filter((item) => typeof item.raw === 'string')
+        .map(({ raw, extension }) => ({ content: raw, extension })),
       variantMap: new Map(),
       stylesheetCache: null,
       fileModifiedMap: new Map(),


Test patch:
diff --git a/tests/jit/raw-content.test.css b/tests/jit/raw-content.test.css
new file mode 100644
--- /dev/null
+++ b/tests/jit/raw-content.test.css
@@ -0,0 +1,757 @@
+* {
+  --tw-shadow: 0 0 #0000;
+  --tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
+  --tw-ring-offset-width: 0px;
+  --tw-ring-offset-color: #fff;
+  --tw-ring-color: rgba(59, 130, 246, 0.5);
+  --tw-ring-offset-shadow: 0 0 #0000;
+  --tw-ring-shadow: 0 0 #0000;
+}
+.container {
+  width: 100%;
+}
+@media (min-width: 640px) {
+  .container {
+    max-width: 640px;
+  }
+}
+@media (min-width: 768px) {
+  .container {
+    max-width: 768px;
+  }
+}
+@media (min-width: 1024px) {
+  .container {
+    max-width: 1024px;
+  }
+}
+@media (min-width: 1280px) {
+  .container {
+    max-width: 1280px;
+  }
+}
+@media (min-width: 1536px) {
+  .container {
+    max-width: 1536px;
+  }
+}
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  white-space: nowrap;
+  border-width: 0;
+}
+.pointer-events-none {
+  pointer-events: none;
+}
+.invisible {
+  visibility: hidden;
+}
+.absolute {
+  position: absolute;
+}
+.inset-0 {
+  top: 0px;
+  right: 0px;
+  bottom: 0px;
+  left: 0px;
+}
+.inset-y-4 {
+  top: 1rem;
+  bottom: 1rem;
+}
+.inset-x-2 {
+  left: 0.5rem;
+  right: 0.5rem;
+}
+.top-6 {
+  top: 1.5rem;
+}
+.right-8 {
+  right: 2rem;
+}
+.bottom-12 {
+  bottom: 3rem;
+}
+.left-16 {
+  left: 4rem;
+}
+.isolate {
+  isolation: isolate;
+}
+.isolation-auto {
+  isolation: auto;
+}
+.z-30 {
+  z-index: 30;
+}
+.order-last {
+  order: 9999;
+}
+.order-2 {
+  order: 2;
+}
+.col-span-3 {
+  grid-column: span 3 / span 3;
+}
+.col-start-1 {
+  grid-column-start: 1;
+}
+.col-end-4 {
+  grid-column-end: 4;
+}
+.row-span-2 {
+  grid-row: span 2 / span 2;
+}
+.row-start-3 {
+  grid-row-start: 3;
+}
+.row-end-5 {
+  grid-row-end: 5;
+}
+.float-right {
+  float: right;
+}
+.clear-left {
+  clear: left;
+}
+.m-4 {
+  margin: 1rem;
+}
+.my-2 {
+  margin-top: 0.5rem;
+  margin-bottom: 0.5rem;
+}
+.mx-auto {
+  margin-left: auto;
+  margin-right: auto;
+}
+.mt-0 {
+  margin-top: 0px;
+}
+.mr-1 {
+  margin-right: 0.25rem;
+}
+.mb-3 {
+  margin-bottom: 0.75rem;
+}
+.ml-4 {
+  margin-left: 1rem;
+}
+.box-border {
+  box-sizing: border-box;
+}
+.inline-grid {
+  display: inline-grid;
+}
+.hidden {
+  display: none;
+}
+.h-16 {
+  height: 4rem;
+}
+.max-h-screen {
+  max-height: 100vh;
+}
+.min-h-0 {
+  min-height: 0px;
+}
+.w-12 {
+  width: 3rem;
+}
+.min-w-min {
+  min-width: min-content;
+}
+.max-w-full {
+  max-width: 100%;
+}
+.flex-1 {
+  flex: 1 1 0%;
+}
+.flex-shrink {
+  flex-shrink: 1;
+}
+.flex-shrink-0 {
+  flex-shrink: 0;
+}
+.flex-grow {
+  flex-grow: 1;
+}
+.flex-grow-0 {
+  flex-grow: 0;
+}
+.table-fixed {
+  table-layout: fixed;
+}
+.border-collapse {
+  border-collapse: collapse;
+}
+.transform {
+  --tw-translate-x: 0;
+  --tw-translate-y: 0;
+  --tw-rotate: 0;
+  --tw-skew-x: 0;
+  --tw-skew-y: 0;
+  --tw-scale-x: 1;
+  --tw-scale-y: 1;
+  transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y))
+    rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y))
+    scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+.transform-gpu {
+  --tw-translate-x: 0;
+  --tw-translate-y: 0;
+  --tw-rotate: 0;
+  --tw-skew-x: 0;
+  --tw-skew-y: 0;
+  --tw-scale-x: 1;
+  --tw-scale-y: 1;
+  transform: translate3d(var(--tw-translate-x), var(--tw-translate-y), 0) rotate(var(--tw-rotate))
+    skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
+    scaleY(var(--tw-scale-y));
+}
+.origin-top-right {
+  transform-origin: top right;
+}
+.translate-x-5 {
+  --tw-translate-x: 1.25rem;
+}
+.-translate-x-4 {
+  --tw-translate-x: -1rem;
+}
+.translate-y-6 {
+  --tw-translate-y: 1.5rem;
+}
+.-translate-x-3 {
+  --tw-translate-x: -0.75rem;
+}
+.rotate-3 {
+  --tw-rotate: 3deg;
+}
+.skew-y-12 {
+  --tw-skew-y: 12deg;
+}
+.skew-x-12 {
+  --tw-skew-x: 12deg;
+}
+.scale-95 {
+  --tw-scale-x: 0.95;
+  --tw-scale-y: 0.95;
+}
+.animate-none {
+  animation: none;
+}
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+.animate-spin {
+  animation: spin 1s linear infinite;
+}
+.cursor-pointer {
+  cursor: pointer;
+}
+.select-none {
+  user-select: none;
+}
+.resize-none {
+  resize: none;
+}
+.list-inside {
+  list-style-position: inside;
+}
+.list-disc {
+  list-style-type: disc;
+}
+.appearance-none {
+  appearance: none;
+}
+.auto-cols-min {
+  grid-auto-columns: min-content;
+}
+.grid-flow-row {
+  grid-auto-flow: row;
+}
+.auto-rows-max {
+  grid-auto-rows: max-content;
+}
+.grid-cols-4 {
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+.grid-rows-3 {
+  grid-template-rows: repeat(3, minmax(0, 1fr));
+}
+.flex-row-reverse {
+  flex-direction: row-reverse;
+}
+.flex-wrap {
+  flex-wrap: wrap;
+}
+.place-content-start {
+  place-content: start;
+}
+.place-items-end {
+  place-items: end;
+}
+.content-center {
+  align-content: center;
+}
+.items-start {
+  align-items: flex-start;
+}
+.justify-center {
+  justify-content: center;
+}
+.justify-items-end {
+  justify-items: end;
+}
+.gap-4 {
+  gap: 1rem;
+}
+.gap-x-2 {
+  column-gap: 0.5rem;
+}
+.gap-y-3 {
+  row-gap: 0.75rem;
+}
+.space-x-4 > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-x-reverse: 0;
+  margin-right: calc(1rem * var(--tw-space-x-reverse));
+  margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
+}
+.space-y-3 > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-y-reverse: 0;
+  margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
+  margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
+}
+.space-y-reverse > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-y-reverse: 1;
+}
+.space-x-reverse > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-x-reverse: 1;
+}
+.divide-x-2 > :not([hidden]) ~ :not([hidden]) {
+  --tw-divide-x-reverse: 0;
+  border-right-width: calc(2px * var(--tw-divide-x-reverse));
+  border-left-width: calc(2px * calc(1 - var(--tw-divide-x-reverse)));
+}
+.divide-y-4 > :not([hidden]) ~ :not([hidden]) {
+  --tw-divide-y-reverse: 0;
+  border-top-width: calc(4px * calc(1 - var(--tw-divide-y-reverse)));
+  border-bottom-width: calc(4px * var(--tw-divide-y-reverse));
+}
+.divide-x-0 > :not([hidden]) ~ :not([hidden]) {
+  --tw-divide-x-reverse: 0;
+  border-right-width: calc(0px * var(--tw-divide-x-reverse));
+  border-left-width: calc(0px * calc(1 - var(--tw-divide-x-reverse)));
+}
+.divide-y-0 > :not([hidden]) ~ :not([hidden]) {
+  --tw-divide-y-reverse: 0;
+  border-top-width: calc(0px * calc(1 - var(--tw-divide-y-reverse)));
+  border-bottom-width: calc(0px * var(--tw-divide-y-reverse));
+}
+.divide-dotted > :not([hidden]) ~ :not([hidden]) {
+  border-style: dotted;
+}
+.divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
+  --tw-divide-opacity: 1;
+  border-color: rgba(229, 231, 235, var(--tw-divide-opacity));
+}
+.divide-opacity-50 > :not([hidden]) ~ :not([hidden]) {
+  --tw-divide-opacity: 0.5;
+}
+.place-self-center {
+  place-self: center;
+}
+.self-end {
+  align-self: flex-end;
+}
+.justify-self-start {
+  justify-self: start;
+}
+.overflow-hidden {
+  overflow: hidden;
+}
+.overscroll-contain {
+  overscroll-behavior: contain;
+}
+.truncate {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.overflow-ellipsis {
+  text-overflow: ellipsis;
+}
+.whitespace-nowrap {
+  white-space: nowrap;
+}
+.break-words {
+  overflow-wrap: break-word;
+}
+.rounded-md {
+  border-radius: 0.375rem;
+}
+.border {
+  border-width: 1px;
+}
+.border-2 {
+  border-width: 2px;
+}
+.border-solid {
+  border-style: solid;
+}
+.border-black {
+  --tw-border-opacity: 1;
+  border-color: rgba(0, 0, 0, var(--tw-border-opacity));
+}
+.border-opacity-10 {
+  --tw-border-opacity: 0.1;
+}
+.bg-green-500 {
+  --tw-bg-opacity: 1;
+  background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
+}
+.bg-opacity-20 {
+  --tw-bg-opacity: 0.2;
+}
+.bg-gradient-to-r {
+  background-image: linear-gradient(to right, var(--tw-gradient-stops));
+}
+.from-red-300 {
+  --tw-gradient-from: #fca5a5;
+  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(252, 165, 165, 0));
+}
+.via-purple-200 {
+  --tw-gradient-stops: var(--tw-gradient-from), #ddd6fe,
+    var(--tw-gradient-to, rgba(221, 214, 254, 0));
+}
+.to-blue-400 {
+  --tw-gradient-to: #60a5fa;
+}
+.decoration-slice {
+  box-decoration-break: slice;
+}
+.decoration-clone {
+  box-decoration-break: clone;
+}
+.bg-cover {
+  background-size: cover;
+}
+.bg-local {
+  background-attachment: local;
+}
+.bg-clip-border {
+  background-clip: border-box;
+}
+.bg-top {
+  background-position: top;
+}
+.bg-no-repeat {
+  background-repeat: no-repeat;
+}
+.bg-origin-border {
+  background-origin: border-box;
+}
+.bg-origin-padding {
+  background-origin: padding-box;
+}
+.bg-origin-content {
+  background-origin: content-box;
+}
+.fill-current {
+  fill: currentColor;
+}
+.stroke-current {
+  stroke: currentColor;
+}
+.stroke-2 {
+  stroke-width: 2;
+}
+.object-cover {
+  object-fit: cover;
+}
+.object-bottom {
+  object-position: bottom;
+}
+.p-4 {
+  padding: 1rem;
+}
+.py-2 {
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+}
+.px-3 {
+  padding-left: 0.75rem;
+  padding-right: 0.75rem;
+}
+.pt-1 {
+  padding-top: 0.25rem;
+}
+.pr-2 {
+  padding-right: 0.5rem;
+}
+.pb-3 {
+  padding-bottom: 0.75rem;
+}
+.pl-4 {
+  padding-left: 1rem;
+}
+.text-center {
+  text-align: center;
+}
+.align-middle {
+  vertical-align: middle;
+}
+.font-sans {
+  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
+    'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+    'Segoe UI Symbol', 'Noto Color Emoji';
+}
+.text-2xl {
+  font-size: 1.5rem;
+  line-height: 2rem;
+}
+.font-medium {
+  font-weight: 500;
+}
+.uppercase {
+  text-transform: uppercase;
+}
+.not-italic {
+  font-style: normal;
+}
+.ordinal,
+.slashed-zero,
+.lining-nums,
+.oldstyle-nums,
+.proportional-nums,
+.tabular-nums,
+.diagonal-fractions,
+.stacked-fractions {
+  --tw-ordinal: var(--tw-empty, /*!*/ /*!*/);
+  --tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/);
+  --tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/);
+  --tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/);
+  --tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/);
+  font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure)
+    var(--tw-numeric-spacing) var(--tw-numeric-fraction);
+}
+.ordinal {
+  --tw-ordinal: ordinal;
+}
+.tabular-nums {
+  --tw-numeric-spacing: tabular-nums;
+}
+.diagonal-fractions {
+  --tw-numeric-fraction: diagonal-fractions;
+}
+.leading-relaxed {
+  line-height: 1.625;
+}
+.leading-5 {
+  line-height: 1.25rem;
+}
+.tracking-tight {
+  letter-spacing: -0.025em;
+}
+.text-indigo-500 {
+  --tw-text-opacity: 1;
+  color: rgba(99, 102, 241, var(--tw-text-opacity));
+}
+.text-opacity-10 {
+  --tw-text-opacity: 0.1;
+}
+.underline {
+  text-decoration: underline;
+}
+.antialiased {
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+.placeholder-green-300::placeholder {
+  --tw-placeholder-opacity: 1;
+  color: rgba(110, 231, 183, var(--tw-placeholder-opacity));
+}
+.placeholder-opacity-60::placeholder {
+  --tw-placeholder-opacity: 0.6;
+}
+.opacity-90 {
+  opacity: 0.9;
+}
+.bg-blend-darken {
+  background-blend-mode: darken;
+}
+.bg-blend-difference {
+  background-blend-mode: difference;
+}
+.mix-blend-multiply {
+  mix-blend-mode: multiply;
+}
+.mix-blend-saturation {
+  mix-blend-mode: saturation;
+}
+.shadow {
+  --tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.shadow-md {
+  --tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.shadow-lg {
+  --tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.outline-none {
+  outline: 2px solid transparent;
+  outline-offset: 2px;
+}
+.outline-black {
+  outline: 2px dotted black;
+  outline-offset: 2px;
+}
+.ring {
+  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width)
+    var(--tw-ring-offset-color);
+  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width))
+    var(--tw-ring-color);
+  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+}
+.ring-4 {
+  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width)
+    var(--tw-ring-offset-color);
+  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width))
+    var(--tw-ring-color);
+  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+}
+.ring-white {
+  --tw-ring-opacity: 1;
+  --tw-ring-color: rgba(255, 255, 255, var(--tw-ring-opacity));
+}
+.ring-opacity-40 {
+  --tw-ring-opacity: 0.4;
+}
+.ring-offset-2 {
+  --tw-ring-offset-width: 2px;
+}
+.ring-offset-blue-300 {
+  --tw-ring-offset-color: #93c5fd;
+}
+.filter {
+  --tw-blur: var(--tw-empty, /*!*/ /*!*/);
+  --tw-brightness: var(--tw-empty, /*!*/ /*!*/);
+  --tw-contrast: var(--tw-empty, /*!*/ /*!*/);
+  --tw-grayscale: var(--tw-empty, /*!*/ /*!*/);
+  --tw-hue-rotate: var(--tw-empty, /*!*/ /*!*/);
+  --tw-invert: var(--tw-empty, /*!*/ /*!*/);
+  --tw-saturate: var(--tw-empty, /*!*/ /*!*/);
+  --tw-sepia: var(--tw-empty, /*!*/ /*!*/);
+  --tw-drop-shadow: var(--tw-empty, /*!*/ /*!*/);
+  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale)
+    var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
+}
+.filter-none {
+  filter: none;
+}
+.blur-md {
+  --tw-blur: blur(12px);
+}
+.brightness-150 {
+  --tw-brightness: brightness(1.5);
+}
+.contrast-50 {
+  --tw-contrast: contrast(0.5);
+}
+.drop-shadow-md {
+  --tw-drop-shadow: drop-shadow(0 4px 3px rgba(0, 0, 0, 0.07))
+    drop-shadow(0 2px 2px rgba(0, 0, 0, 0.06));
+}
+.grayscale {
+  --tw-grayscale: grayscale(100%);
+}
+.hue-rotate-60 {
+  --tw-hue-rotate: hue-rotate(60deg);
+}
+.invert {
+  --tw-invert: invert(100%);
+}
+.saturate-200 {
+  --tw-saturate: saturate(2);
+}
+.sepia {
+  --tw-sepia: sepia(100%);
+}
+.backdrop-filter {
+  --tw-backdrop-blur: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-brightness: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-contrast: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-grayscale: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-hue-rotate: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-invert: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-opacity: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-saturate: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-sepia: var(--tw-empty, /*!*/ /*!*/);
+  backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast)
+    var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert)
+    var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
+}
+.backdrop-filter-none {
+  backdrop-filter: none;
+}
+.backdrop-blur-lg {
+  --tw-backdrop-blur: blur(16px);
+}
+.backdrop-brightness-50 {
+  --tw-backdrop-brightness: brightness(0.5);
+}
+.backdrop-contrast-0 {
+  --tw-backdrop-contrast: contrast(0);
+}
+.backdrop-grayscale {
+  --tw-backdrop-grayscale: grayscale(100%);
+}
+.backdrop-hue-rotate-90 {
+  --tw-backdrop-hue-rotate: hue-rotate(90deg);
+}
+.backdrop-invert {
+  --tw-backdrop-invert: invert(100%);
+}
+.backdrop-opacity-75 {
+  --tw-backdrop-opacity: opacity(0.75);
+}
+.backdrop-saturate-150 {
+  --tw-backdrop-saturate: saturate(1.5);
+}
+.backdrop-sepia {
+  --tw-backdrop-sepia: sepia(100%);
+}
+.transition {
+  transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow,
+    transform, filter, backdrop-filter;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 150ms;
+}
+.transition-all {
+  transition-property: all;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 150ms;
+}
+.delay-300 {
+  transition-delay: 300ms;
+}
+.duration-200 {
+  transition-duration: 200ms;
+}
+.ease-in-out {
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+}
diff --git a/tests/jit/raw-content.test.html b/tests/jit/raw-content.test.html
new file mode 100644
--- /dev/null
+++ b/tests/jit/raw-content.test.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Title</title>
+    <link rel="stylesheet" href="./tailwind.css" />
+  </head>
+  <body>
+    <div class="sr-only"></div>
+    <div class="content-center"></div>
+    <div class="items-start"></div>
+    <div class="self-end"></div>
+    <div class="animate-none"></div>
+    <div class="animate-spin"></div>
+    <div class="appearance-none"></div>
+    <div class="bg-local"></div>
+    <div class="bg-clip-border"></div>
+    <div class="bg-green-500"></div>
+    <div class="bg-gradient-to-r"></div>
+    <div class="bg-opacity-20"></div>
+    <div class="bg-top"></div>
+    <div class="bg-no-repeat"></div>
+    <div class="bg-cover"></div>
+    <div class="bg-origin-border bg-origin-padding bg-origin-content"></div>
+    <div class="border-collapse"></div>
+    <div class="border-black"></div>
+    <div class="border-opacity-10"></div>
+    <div class="rounded-md"></div>
+    <div class="border-solid"></div>
+    <div class="border"></div>
+    <div class="border-2"></div>
+    <div class="shadow"></div>
+    <div class="shadow-md"></div>
+    <div class="shadow-lg"></div>
+    <div class="decoration-clone decoration-slice"></div>
+    <div class="box-border"></div>
+    <div class="clear-left"></div>
+    <div class="container"></div>
+    <div class="cursor-pointer"></div>
+    <div class="hidden inline-grid"></div>
+    <div class="divide-gray-200"></div>
+    <div class="divide-opacity-50"></div>
+    <div class="divide-dotted"></div>
+    <div class="divide-x-2 divide-y-4 divide-x-0 divide-y-0"></div>
+    <div class="fill-current"></div>
+    <div class="flex-1"></div>
+    <div class="flex-row-reverse"></div>
+    <div class="flex-grow"></div>
+    <div class="flex-grow-0"></div>
+    <div class="flex-shrink"></div>
+    <div class="flex-shrink-0"></div>
+    <div class="flex-wrap"></div>
+    <div class="float-right"></div>
+    <div class="font-sans"></div>
+    <div class="text-2xl"></div>
+    <div class="antialiased"></div>
+    <div class="not-italic"></div>
+    <div class="tabular-nums ordinal diagonal-fractions"></div>
+    <div class="font-medium"></div>
+    <div class="gap-x-2 gap-y-3 gap-4"></div>
+    <div class="from-red-300 via-purple-200 to-blue-400"></div>
+    <div class="auto-cols-min"></div>
+    <div class="grid-flow-row"></div>
+    <div class="auto-rows-max"></div>
+    <div class="col-span-3"></div>
+    <div class="col-start-1"></div>
+    <div class="col-end-4"></div>
+    <div class="row-span-2"></div>
+    <div class="row-start-3"></div>
+    <div class="row-end-5"></div>
+    <div class="grid-cols-4"></div>
+    <div class="grid-rows-3"></div>
+    <div class="h-16"></div>
+    <div class="inset-0 inset-y-4 inset-x-2 top-6 right-8 bottom-12 left-16"></div>
+    <div class="isolate isolation-auto"></div>
+    <div class="justify-center"></div>
+    <div class="justify-items-end"></div>
+    <div class="justify-self-start"></div>
+    <div class="tracking-tight"></div>
+    <div class="leading-relaxed leading-5"></div>
+    <div class="list-inside"></div>
+    <div class="list-disc"></div>
+    <div class="m-4 my-2 mx-auto mt-0 mr-1 mb-3 ml-4"></div>
+    <div class="max-h-screen"></div>
+    <div class="max-w-full"></div>
+    <div class="min-h-0"></div>
+    <div class="min-w-min"></div>
+    <div class="object-cover"></div>
+    <div class="object-bottom"></div>
+    <div class="opacity-90"></div>
+    <div class="bg-blend-darken bg-blend-difference"></div>
+    <div class="mix-blend-multiply mix-blend-saturation"></div>
+    <div class="order-last order-2"></div>
+    <div class="outline-none outline-black"></div>
+    <div class="overflow-hidden"></div>
+    <div class="overscroll-contain"></div>
+    <div class="p-4 py-2 px-3 pt-1 pr-2 pb-3 pl-4"></div>
+    <div class="place-content-start"></div>
+    <div class="placeholder-green-300"></div>
+    <div class="placeholder-opacity-60"></div>
+    <div class="place-items-end"></div>
+    <div class="place-self-center"></div>
+    <div class="pointer-events-none"></div>
+    <div class="absolute"></div>
+    <div class="resize-none"></div>
+    <div class="ring-white"></div>
+    <div class="ring-offset-blue-300"></div>
+    <div class="ring-offset-2"></div>
+    <div class="ring-opacity-40"></div>
+    <div class="ring ring-4"></div>
+    <div
+      class="filter filter-none blur-md brightness-150 contrast-50 drop-shadow-md grayscale hue-rotate-60 invert saturate-200 sepia"
+    ></div>
+    <div
+      class="backdrop-filter backdrop-filter-none backdrop-blur-lg backdrop-brightness-50 backdrop-contrast-0 backdrop-grayscale backdrop-hue-rotate-90 backdrop-invert backdrop-opacity-75 backdrop-saturate-150 backdrop-sepia"
+    ></div>
+    <div class="rotate-3"></div>
+    <div class="scale-95"></div>
+    <div class="skew-y-12 skew-x-12"></div>
+    <div class="space-x-4 space-y-3 space-x-reverse space-y-reverse"></div>
+    <div class="stroke-current"></div>
+    <div class="stroke-2"></div>
+    <div class="table-fixed"></div>
+    <div class="text-center"></div>
+    <div class="text-indigo-500"></div>
+    <div class="underline"></div>
+    <div class="text-opacity-10"></div>
+    <div class="overflow-ellipsis truncate"></div>
+    <div class="uppercase"></div>
+    <div class="transform transform-gpu"></div>
+    <div class="origin-top-right"></div>
+    <div class="delay-300"></div>
+    <div class="duration-200"></div>
+    <div class="transition transition-all"></div>
+    <div class="ease-in-out"></div>
+    <div class="translate-x-5 -translate-x-4 translate-y-6 -translate-x-3"></div>
+    <div class="select-none"></div>
+    <div class="align-middle"></div>
+    <div class="invisible"></div>
+    <div class="whitespace-nowrap"></div>
+    <div class="w-12"></div>
+    <div class="break-words"></div>
+    <div class="z-30"></div>
+  </body>
+</html>
diff --git a/tests/jit/raw-content.test.js b/tests/jit/raw-content.test.js
new file mode 100644
--- /dev/null
+++ b/tests/jit/raw-content.test.js
@@ -0,0 +1,85 @@
+import postcss from 'postcss'
+import fs from 'fs'
+import path from 'path'
+
+beforeEach(() => {
+  jest.resetModules()
+})
+
+function run(tailwind, input, config = {}) {
+  return postcss(tailwind(config)).process(input, {
+    from: path.resolve(__filename),
+  })
+}
+
+test('raw content', () => {
+  let tailwind = require('../../src/jit/index.js').default
+
+  let config = {
+    mode: 'jit',
+    purge: [{ raw: fs.readFileSync(path.resolve(__dirname, './raw-content.test.html'), 'utf8') }],
+    corePlugins: { preflight: false },
+    theme: {},
+    plugins: [],
+  }
+
+  let css = `
+    @tailwind base;
+    @tailwind components;
+    @tailwind utilities;
+  `
+
+  return run(tailwind, css, config).then((result) => {
+    let expectedPath = path.resolve(__dirname, './raw-content.test.css')
+    let expected = fs.readFileSync(expectedPath, 'utf8')
+
+    expect(result.css).toMatchFormattedCss(expected)
+  })
+})
+
+test('raw content with extension', () => {
+  let tailwind = require('../../src/jit/index.js').default
+
+  let config = {
+    mode: 'jit',
+    purge: {
+      content: [
+        {
+          raw: fs.readFileSync(path.resolve(__dirname, './raw-content.test.html'), 'utf8'),
+          extension: 'html',
+        },
+      ],
+      options: {
+        extractors: [
+          {
+            extractor: () => [],
+            extensions: ['html'],
+          },
+        ],
+      },
+    },
+    corePlugins: { preflight: false },
+    theme: {},
+    plugins: [],
+  }
+
+  let css = `
+    @tailwind base;
+    @tailwind components;
+    @tailwind utilities;
+  `
+
+  return run(tailwind, css, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(`
+      * {
+        --tw-shadow: 0 0 #0000;
+        --tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
+        --tw-ring-offset-width: 0px;
+        --tw-ring-offset-color: #fff;
+        --tw-ring-color: rgba(59, 130, 246, 0.5);
+        --tw-ring-offset-shadow: 0 0 #0000;
+        --tw-ring-shadow: 0 0 #0000;
+      }
+    `)
+  })
+})