Repository: tailwindlabs/tailwindcss-jit
Pull request #125 (2021-03-20 13:35:20)

feat: allow custom extractors
I'm building a ui library using Tailwind which relies heavily on a custom [purge extractor](https://purgecss.com/extractors.html), which means I wouldn't be able to leverage `tailwindcss-jit` 😿 .

This PR aims to support custom extractors as defined in the purge config options if any, falling back to the default one from `tailwindcss-jit`.

I tried to make as few modifications as I could to the code

I'm hoping you're on board with the general idea 🙏 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a38190f..77e2b1d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Nothing yet!
 
+## [0.1.7] - 2021-03-22
+
+### Fixed
+
+- Clone nodes to prevent bugs related to rule cache mutation ([#141](https://github.com/tailwindlabs/tailwindcss-jit/pull/141))
+
+
+## [0.1.6] - 2021-03-22
+
+### Fixed
+
+- Add initial support for recursive `@apply` ([#136](https://github.com/tailwindlabs/tailwindcss-jit/pull/136))
+
 ## [0.1.5] - 2021-03-20
 
 ### Fixed
@@ -57,10 +70,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Everything!
 
-[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v0.1.5...HEAD
-[0.1.5]: https://github.com/tailwindlabs/tailwindcss/compare/v0.1.4...v0.1.5
-[0.1.4]: https://github.com/tailwindlabs/tailwindcss/compare/v0.1.3...v0.1.4
-[0.1.3]: https://github.com/tailwindlabs/tailwindcss/compare/v0.1.2...v0.1.3
-[0.1.2]: https://github.com/tailwindlabs/tailwindcss/compare/v0.1.1...v0.1.2
-[0.1.1]: https://github.com/tailwindlabs/tailwindcss/compare/v0.1.0...v0.1.1
+[unreleased]: https://github.com/tailwindlabs/tailwindcss-jit/compare/v0.1.7...HEAD
+[0.1.7]: https://github.com/tailwindlabs/tailwindcss-jit/compare/v0.1.6...v0.1.7
+[0.1.6]: https://github.com/tailwindlabs/tailwindcss-jit/compare/v0.1.5...v0.1.6
+[0.1.5]: https://github.com/tailwindlabs/tailwindcss-jit/compare/v0.1.4...v0.1.5
+[0.1.4]: https://github.com/tailwindlabs/tailwindcss-jit/compare/v0.1.3...v0.1.4
+[0.1.3]: https://github.com/tailwindlabs/tailwindcss-jit/compare/v0.1.2...v0.1.3
+[0.1.2]: https://github.com/tailwindlabs/tailwindcss-jit/compare/v0.1.1...v0.1.2
+[0.1.1]: https://github.com/tailwindlabs/tailwindcss-jit/compare/v0.1.0...v0.1.1
 [0.1.0]: https://github.com/tailwindlabs/tailwindcss-jit/releases/tag/v0.1.0
diff --git a/package-lock.json b/package-lock.json
index 8dcb479..9a69ac1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@tailwindcss/jit",
-  "version": "0.1.5",
+  "version": "0.1.7",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "@tailwindcss/jit",
-      "version": "0.1.5",
+      "version": "0.1.7",
       "license": "MIT",
       "dependencies": {
         "chokidar": "^3.5.1",
diff --git a/package.json b/package.json
index 57dbbd9..4a4c5a1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tailwindcss/jit",
-  "version": "0.1.5",
+  "version": "0.1.7",
   "license": "MIT",
   "main": "src/index.js",
   "repository": "https://github.com/tailwindlabs/tailwindcss-jit",
diff --git a/src/index.js b/src/index.js
index 9863d63..a86dc09 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,6 +3,7 @@ const postcss = require('postcss')
 const evaluateTailwindFunctions = require('tailwindcss/lib/lib/evaluateTailwindFunctions').default
 const substituteScreenAtRules = require('tailwindcss/lib/lib/substituteScreenAtRules').default
 
+const rewriteTailwindImports = require('./lib/rewriteTailwindImports')
 const setupContext = require('./lib/setupContext')
 const removeLayerAtRules = require('./lib/removeLayerAtRules')
 const expandTailwindAtRules = require('./lib/expandTailwindAtRules')
@@ -31,6 +32,8 @@ module.exports = (configOrPath = {}) => {
           })
         }
 
+        rewriteTailwindImports(root)
+
         let context = setupContext(configOrPath)(result, root)
 
         if (context.configPath !== null) {
diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js
index c3bad3e..d0a46fe 100644
--- a/src/lib/expandApplyAtRules.js
+++ b/src/lib/expandApplyAtRules.js
@@ -9,7 +9,10 @@ function buildApplyCache(applyCandidates, context) {
     }
 
     if (context.classCache.has(candidate)) {
-      context.applyClassCache.set(candidate, context.classCache.get(candidate))
+      context.applyClassCache.set(
+        candidate,
+        context.classCache.get(candidate).map(([meta, rule]) => [meta, rule.clone()])
+      )
       continue
     }
 
@@ -38,7 +41,7 @@ function extractApplyCandidates(params) {
 }
 
 function expandApplyAtRules(context) {
-  return (root) => {
+  return function processApply(root) {
     let applyCandidates = new Set()
 
     // Collect all @apply rules and candidates
@@ -79,12 +82,16 @@ function expandApplyAtRules(context) {
        */
       // TODO: Should we use postcss-selector-parser for this instead?
       function replaceSelector(selector, utilitySelectors, candidate) {
+        let needle = `.${escapeClassName(candidate)}`
+        let utilitySelectorsList = utilitySelectors.split(/\s*,\s*/g)
+
         return selector
           .split(/\s*,\s*/g)
           .map((s) => {
             let replaced = []
-            for (let utilitySelector of utilitySelectors.split(/\s*,\s*/g)) {
-              let replacedSelector = utilitySelector.replace(`.${escapeClassName(candidate)}`, s)
+
+            for (let utilitySelector of utilitySelectorsList) {
+              let replacedSelector = utilitySelector.replace(needle, s)
               if (replacedSelector === utilitySelector) {
                 continue
               }
@@ -136,6 +143,9 @@ function expandApplyAtRules(context) {
           apply.parent.remove()
         }
       }
+
+      // Do it again, in case we have other `@apply` rules
+      processApply(root)
     }
   }
 }
diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js
index 0d6ed2a..dfcaf70 100644
--- a/src/lib/expandTailwindAtRules.js
+++ b/src/lib/expandTailwindAtRules.js
@@ -1,16 +1,46 @@
 const fs = require('fs')
+const path = require('path')
 const fastGlob = require('fast-glob')
 const sharedState = require('./sharedState')
 const { generateRules } = require('./generateRules')
-const { bigSign } = require('./utils')
+const { bigSign, cloneNodes } = require('./utils')
 
 let env = sharedState.env
 let contentMatchCache = sharedState.contentMatchCache
 
+const BROAD_MATCH_GLOBAL_REGEXP = /[^<>"'`\s]*[^<>"'`\s:]/g
+const INNER_MATCH_GLOBAL_REGEXP = /[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g
+
+function defaultJitExtractor(content) {
+  let broadMatches = content.match(BROAD_MATCH_GLOBAL_REGEXP) || []
+  let innerMatches = content.match(INNER_MATCH_GLOBAL_REGEXP) || []
+
+  return [...broadMatches, ...innerMatches]
+}
+
+function getExtractor(fileName, tailwindConfig) {
+  const purgeOptions = tailwindConfig && tailwindConfig.purge && tailwindConfig.purge.options
+
+  if (!purgeOptions) {
+    return defaultJitExtractor
+  }
+
+  const fileExtension = path.extname(fileName).slice(1)
+  const fileSpecificExtractor = (purgeOptions.extractors || []).find((extractor) =>
+    extractor.extensions.includes(fileExtension)
+  )
+
+  if (fileSpecificExtractor) {
+    return fileSpecificExtractor.extractor
+  }
+
+  return purgeOptions.defaultExtractor || defaultJitExtractor
+}
+
 // Scans template contents for possible classes. This is a hot path on initial build but
 // not too important for subsequent builds. The faster the better though — if we can speed
 // up these regexes by 50% that could cut initial build time by like 20%.
-function getClassCandidates(content, contentMatchCache, candidates, seen) {
+function getClassCandidates(content, extractor, contentMatchCache, candidates, seen) {
   for (let line of content.split('\n')) {
     line = line.trim()
 
@@ -24,20 +54,14 @@ function getClassCandidates(content, contentMatchCache, candidates, seen) {
         candidates.add(match)
       }
     } else {
-      let allMatches = new Set()
-      let broadMatches = line.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || []
-      let innerMatches = line.match(/[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g) || []
+      let extractorMatches = extractor(line)
+      let lineMatchesSet = new Set(extractorMatches)
 
-      for (let match of broadMatches) {
-        allMatches.add(match)
-        candidates.add(match)
-      }
-      for (let match of innerMatches) {
-        allMatches.add(match)
+      for (let match of lineMatchesSet) {
         candidates.add(match)
       }
 
-      contentMatchCache.set(line, allMatches)
+      contentMatchCache.set(line, lineMatchesSet)
     }
   }
 }
@@ -143,7 +167,8 @@ function expandTailwindAtRules(context, registerDependency) {
     env.DEBUG && console.time('Reading changed files')
     for (let file of context.changedFiles) {
       let content = fs.readFileSync(file, 'utf8')
-      getClassCandidates(content, contentMatchCache, candidates, seen)
+      let extractor = getExtractor(file, context.tailwindConfig)
+      getClassCandidates(content, extractor, contentMatchCache, candidates, seen)
     }
     env.DEBUG && console.timeEnd('Reading changed files')
 
@@ -179,25 +204,25 @@ function expandTailwindAtRules(context, registerDependency) {
     // Replace any Tailwind directives with generated CSS
 
     if (layerNodes.base) {
-      layerNodes.base.before([...baseNodes])
+      layerNodes.base.before(cloneNodes([...baseNodes]))
       layerNodes.base.remove()
     }
 
     if (layerNodes.components) {
-      layerNodes.components.before([...componentNodes])
+      layerNodes.components.before(cloneNodes([...componentNodes]))
       layerNodes.components.remove()
     }
 
     if (layerNodes.utilities) {
-      layerNodes.utilities.before([...utilityNodes])
+      layerNodes.utilities.before(cloneNodes([...utilityNodes]))
       layerNodes.utilities.remove()
     }
 
     if (layerNodes.screens) {
-      layerNodes.screens.before([...screenNodes])
+      layerNodes.screens.before(cloneNodes([...screenNodes]))
       layerNodes.screens.remove()
     } else {
-      root.append([...screenNodes])
+      root.append(cloneNodes([...screenNodes]))
     }
 
     // ---
diff --git a/src/lib/rewriteTailwindImports.js b/src/lib/rewriteTailwindImports.js
new file mode 100644
index 0000000..31a39c9
--- /dev/null
+++ b/src/lib/rewriteTailwindImports.js
@@ -0,0 +1,28 @@
+function rewriteTailwindImports(root) {
+  root.walkAtRules('import', (atRule) => {
+    if (atRule.params === '"tailwindcss/base"' || atRule.params === "'tailwindcss/base'") {
+      atRule.name = 'tailwind'
+      atRule.params = 'base'
+    } else if (
+      atRule.params === '"tailwindcss/components"' ||
+      atRule.params === "'tailwindcss/components'"
+    ) {
+      atRule.name = 'tailwind'
+      atRule.params = 'components'
+    } else if (
+      atRule.params === '"tailwindcss/utilities"' ||
+      atRule.params === "'tailwindcss/utilities'"
+    ) {
+      atRule.name = 'tailwind'
+      atRule.params = 'utilities'
+    } else if (
+      atRule.params === '"tailwindcss/screens"' ||
+      atRule.params === "'tailwindcss/screens'"
+    ) {
+      atRule.name = 'tailwind'
+      atRule.params = 'screens'
+    }
+  })
+}
+
+module.exports = rewriteTailwindImports
diff --git a/src/lib/utils.js b/src/lib/utils.js
index 0d07913..8fcbf06 100644
--- a/src/lib/utils.js
+++ b/src/lib/utils.js
@@ -84,6 +84,16 @@ function nameClass(...args) {
   return escapeCommas(tailwindUtils.nameClass(...args))
 }
 
+/**
+ * Clone generated and/or cached nodes to ensure no future
+ * postcss plugins can mutate the rules and mess up our cache
+ *
+ * @param {import('postcss').Node[]} nodes
+ * */
+function cloneNodes(nodes) {
+  return nodes.map((node) => node.clone())
+}
+
 module.exports = {
   toPostCssNode,
   bigSign,
@@ -91,4 +101,5 @@ module.exports = {
   escapeClassName,
   escapeCommas,
   nameClass,
+  cloneNodes,
 }


Test patch:
diff --git a/tests/10-apply.test.css b/tests/10-apply.test.css
index ceca6f3..ddf73cf 100644
--- a/tests/10-apply.test.css
+++ b/tests/10-apply.test.css
@@ -178,3 +178,90 @@
     font-weight: 400;
   }
 }
+.use-base-only-a {
+  font-weight: 700;
+}
+.use-dependant-only-b {
+  font-weight: 700;
+  font-weight: 400;
+}
+.btn {
+  border-radius: 0.25rem;
+  padding-left: 1rem;
+  padding-right: 1rem;
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+  font-weight: 700;
+}
+.btn-blue {
+  border-radius: 0.25rem;
+  padding-left: 1rem;
+  padding-right: 1rem;
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+  font-weight: 700;
+  --tw-bg-opacity: 1;
+  background-color: rgba(59, 130, 246, var(--tw-bg-opacity));
+  --tw-text-opacity: 1;
+  color: rgba(255, 255, 255, var(--tw-text-opacity));
+}
+.btn-blue:hover {
+  --tw-bg-opacity: 1;
+  background-color: rgba(29, 78, 216, var(--tw-bg-opacity));
+}
+.recursive-apply-a {
+  font-weight: 900;
+}
+@media (min-width: 640px) {
+  .recursive-apply-a {
+    font-weight: 100;
+  }
+}
+.recursive-apply-b {
+  font-weight: 900;
+}
+@media (min-width: 640px) {
+  .recursive-apply-b {
+    font-weight: 100;
+  }
+}
+.recursive-apply-b {
+  font-weight: 600;
+}
+@media (min-width: 768px) {
+  .recursive-apply-b {
+    font-weight: 200;
+  }
+}
+.recursive-apply-c {
+  font-weight: 900;
+}
+@media (min-width: 640px) {
+  .recursive-apply-c {
+    font-weight: 100;
+  }
+}
+.recursive-apply-c {
+  font-weight: 600;
+}
+@media (min-width: 768px) {
+  .recursive-apply-c {
+    font-weight: 200;
+  }
+}
+.recursive-apply-c {
+  font-weight: 700;
+}
+@media (min-width: 1024px) {
+  .recursive-apply-c {
+    font-weight: 300;
+  }
+}
+.use-with-other-properties-base {
+  color: green;
+  font-weight: 700;
+}
+.use-with-other-properties-component {
+  color: green;
+  font-weight: 700;
+}
diff --git a/tests/10-apply.test.html b/tests/10-apply.test.html
index 7f9c472..d355412 100644
--- a/tests/10-apply.test.html
+++ b/tests/10-apply.test.html
@@ -23,5 +23,14 @@
     <div class="basic-nesting-parent">
       <div class="basic-nesting-child"></div>
     </div>
+    <div class="use-base-only-a"></div>
+    <div class="use-dependant-only-b"></div>
+    <div class="btn"></div>
+    <div class="btn-blue"></div>
+    <div class="recursive-apply-a"></div>
+    <div class="recursive-apply-b"></div>
+    <div class="recursive-apply-c"></div>
+    <div class="use-with-other-properties-base use-with-other-properties-component"></div>
+    <div class="a b"></div>
   </body>
 </html>
diff --git a/tests/10-apply.test.js b/tests/10-apply.test.js
index 173f190..732ed03 100644
--- a/tests/10-apply.test.js
+++ b/tests/10-apply.test.js
@@ -65,6 +65,40 @@ test('@apply', () => {
         @apply font-bold hover:font-normal;
       }
     }
+    .use-base-only-a {
+      @apply font-bold;
+    }
+    .use-base-only-b {
+      @apply use-base-only-a font-normal;
+    }
+    .use-dependant-only-a {
+      @apply font-bold;
+    }
+    .use-dependant-only-b {
+      @apply use-dependant-only-a font-normal;
+    }
+    .btn {
+      @apply font-bold py-2 px-4 rounded; 
+    }
+    .btn-blue {
+      @apply btn bg-blue-500 hover:bg-blue-700 text-white;
+    }
+    .recursive-apply-a {
+      @apply font-black sm:font-thin;
+    }
+    .recursive-apply-b {
+      @apply recursive-apply-a font-semibold md:font-extralight;
+    }
+    .recursive-apply-c {
+      @apply recursive-apply-b font-bold lg:font-light;
+    }
+    .use-with-other-properties-base {
+      color: green;
+      @apply font-bold;
+    }
+    .use-with-other-properties-component {
+      @apply use-with-other-properties-base;
+    }
   }
 
   @layer utilities {
diff --git a/tests/11-custom-extractors.test.css b/tests/11-custom-extractors.test.css
new file mode 100644
index 0000000..423463d
--- /dev/null
+++ b/tests/11-custom-extractors.test.css
@@ -0,0 +1,17 @@
+* {
+  --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;
+}
+.bg-white {
+  --tw-bg-opacity: 1;
+  background-color: rgba(255, 255, 255, var(--tw-bg-opacity));
+}
+.text-indigo-500 {
+  --tw-text-opacity: 1;
+  color: rgba(99, 102, 241, var(--tw-text-opacity));
+}
diff --git a/tests/11-custom-extractors.test.html b/tests/11-custom-extractors.test.html
new file mode 100644
index 0000000..871b099
--- /dev/null
+++ b/tests/11-custom-extractors.test.html
@@ -0,0 +1,14 @@
+<!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="text-indigo-500 bg-white">hello world</div>
+    <span>text-red-500 shouldn't appear in the output</span>
+  </body>
+</html>
diff --git a/tests/11-custom-extractors.test.js b/tests/11-custom-extractors.test.js
new file mode 100644
index 0000000..07e475f
--- /dev/null
+++ b/tests/11-custom-extractors.test.js
@@ -0,0 +1,63 @@
+const postcss = require('postcss')
+const fs = require('fs')
+const path = require('path')
+
+function run(input, config = {}) {
+  jest.resetModules()
+  const tailwind = require('../src/index.js')
+  return postcss([tailwind(config)]).process(input, { from: path.resolve(__filename) })
+}
+
+function customExtractor(content) {
+  const matches = content.match(/class="([^"]+)"/)
+  return matches ? matches[1].split(/\s+/) : []
+}
+
+const css = `
+  @tailwind base;
+  @tailwind components;
+  @tailwind utilities;
+`
+const expectedPath = path.resolve(__dirname, './11-custom-extractors.test.css')
+const expected = fs.readFileSync(expectedPath, 'utf8')
+
+test('defaultExtractor', () => {
+  let config = {
+    purge: {
+      content: [path.resolve(__dirname, './11-custom-extractors.test.html')],
+      options: {
+        defaultExtractor: customExtractor,
+      },
+    },
+    corePlugins: { preflight: false },
+    theme: {},
+    plugins: [],
+  }
+
+  return run(css, config).then((result) => {
+    expect(result.css).toMatchCss(expected)
+  })
+})
+
+test('extractors array', () => {
+  let config = {
+    purge: {
+      content: [path.resolve(__dirname, './11-custom-extractors.test.html')],
+      options: {
+        extractors: [
+          {
+            extractor: customExtractor,
+            extensions: ['html'],
+          },
+        ],
+      },
+    },
+    corePlugins: { preflight: false },
+    theme: {},
+    plugins: [],
+  }
+
+  return run(css, config).then((result) => {
+    expect(result.css).toMatchCss(expected)
+  })
+})
diff --git a/tests/11-mutable.test.css b/tests/11-mutable.test.css
new file mode 100644
index 0000000..1b3c524
--- /dev/null
+++ b/tests/11-mutable.test.css
@@ -0,0 +1,3 @@
+.bg-foo {
+  background-image: url("./foo.png");
+}
diff --git a/tests/11-mutable.test.html b/tests/11-mutable.test.html
new file mode 100644
index 0000000..b9e487a
--- /dev/null
+++ b/tests/11-mutable.test.html
@@ -0,0 +1,13 @@
+<!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="bg-foo"></div>
+  </body>
+</html>
diff --git a/tests/11-mutable.test.js b/tests/11-mutable.test.js
new file mode 100644
index 0000000..13d4f40
--- /dev/null
+++ b/tests/11-mutable.test.js
@@ -0,0 +1,53 @@
+const postcss = require('postcss')
+const tailwind = require('../src/index.js')
+const fs = require('fs')
+const path = require('path')
+
+function pluginThatMutatesRules() {
+  return (root) => {
+    root.walkRules((rule) => {
+      rule.nodes.filter(node => node.prop === 'background-image').forEach(node => {
+        node.value = 'url("./bar.png")'
+      })
+
+      return rule
+    })
+  }
+}
+
+function run(input, config = {}) {
+  return postcss([tailwind(config)]).process(input, { from: path.resolve(__filename) })
+}
+
+test.only('plugins mutating rules after tailwind doesnt break it', async () => {
+  let config = {
+    purge: [path.resolve(__dirname, './11-mutable.test.html')],
+    theme: {
+      backgroundImage: {
+        'foo': 'url("./foo.png")',
+      }
+    },
+    plugins: [],
+  }
+
+  let css = `@tailwind utilities;`
+
+  function checkResult(result) {
+    let expectedPath = path.resolve(__dirname, './11-mutable.test.css')
+    let expected = fs.readFileSync(expectedPath, 'utf8')
+
+    expect(result.css).toMatchCss(expected)
+  }
+
+  // Verify the first run produces the expected result
+  let firstRun = await run(css, config)
+  checkResult(firstRun)
+
+  // Outside of the context of tailwind jit more postcss plugins may operate on the AST:
+  // In this case we have a plugin that mutates rules directly
+  await postcss([ pluginThatMutatesRules() ]).process(firstRun, { from: path.resolve(__filename) })
+
+  // Verify subsequent runs don't produce mutated rules
+  let secondRun = await run(css, config)
+  checkResult(secondRun)
+})
diff --git a/tests/12-import-syntax.test.css b/tests/12-import-syntax.test.css
new file mode 100644
index 0000000..030ec8a
--- /dev/null
+++ b/tests/12-import-syntax.test.css
@@ -0,0 +1,52 @@
+* {
+  --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;
+}
+h1 {
+  font-size: 32px;
+}
+.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;
+  }
+}
+.mt-6 {
+  margin-top: 1.5rem;
+}
+.bg-black {
+  --tw-bg-opacity: 1;
+  background-color: rgba(0, 0, 0, var(--tw-bg-opacity));
+}
+@media (min-width: 768px) {
+  .md\:hover\:text-center:hover {
+    text-align: center;
+  }
+}
diff --git a/tests/12-import-syntax.test.html b/tests/12-import-syntax.test.html
new file mode 100644
index 0000000..1971996
--- /dev/null
+++ b/tests/12-import-syntax.test.html
@@ -0,0 +1,5 @@
+<h1>Hello world!</h1>
+<div class="container"></div>
+<div class="mt-6"></div>
+<div class="bg-black"></div>
+<div class="md:hover:text-center"></div>
diff --git a/tests/12-import-syntax.test.js b/tests/12-import-syntax.test.js
new file mode 100644
index 0000000..a0f80c3
--- /dev/null
+++ b/tests/12-import-syntax.test.js
@@ -0,0 +1,38 @@
+const postcss = require('postcss')
+const tailwind = require('../src/index.js')
+const fs = require('fs')
+const path = require('path')
+
+function run(input, config = {}) {
+  return postcss([tailwind(config)]).process(input, { from: path.resolve(__filename) })
+}
+
+test('using @import instead of @tailwind', () => {
+  let config = {
+    purge: [path.resolve(__dirname, './12-import-syntax.test.html')],
+    corePlugins: { preflight: false },
+    theme: {},
+    plugins: [
+      function ({ addBase }) {
+        addBase({
+          h1: {
+            fontSize: '32px',
+          },
+        })
+      },
+    ],
+  }
+
+  let css = `
+    @import "tailwindcss/base";
+    @import "tailwindcss/components";
+    @import "tailwindcss/utilities";
+  `
+
+  return run(css, config).then((result) => {
+    let expectedPath = path.resolve(__dirname, './12-import-syntax.test.css')
+    let expected = fs.readFileSync(expectedPath, 'utf8')
+
+    expect(result.css).toMatchCss(expected)
+  })
+})