Repository: tailwindlabs/tailwindcss

Problem statement:
Not all classes are being pulled in from Blade files, some only after re-saving the files.
### What version of @tailwindcss/jit are you using?

0.1.3

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

v14.16.0

### What browser are you using?

Chrome

### What operating system are you using?

macOS

### Reproduction repository

https://github.com/foof/tailwind-jit-test

The linked repo is a fresh Laravel install but where all the build configs are located in `resources/myfolder/` instead of the root folder.

Reproduction steps:

1. Clone repo
2. `cd resources/myfolder`
3. `npm install`
4. `npm run watch`

The purge option is configured to look at blade files in the `resources/views/myfolder` directory.

Classes from `resources/views/myfolder/welcome.blade.php` are not being put into the generated CSS file. If I simply re-save the blade file while watch is running, then some of the classes gets added, but not all. For example `sm:p-6` is never included in the generated css.


Hints:
> You don't have package.json or package-lock.json in your repo. `npm install` won't work.

Perhaps it was unclear, but those files are also located in the subfolder `resources/myfolder`. In our project we have multiple frontends which means we have multiple package.json etc that are located in other subfolders like `resources/myfolder`, `resources/myfolder2`, `resources/myfolder3` etc.
Hey! Thank you for your bug report!
Much appreciated! 🙏

The reason `sm:p-6` never gets included is because you removed all the screens in your config, which results in the fact that you can't use `responsive` utilities:

```js
module.exports = {
    purge: [
        "./**/*.{js,vue}",
        "./../views/myfolder/**/*.php", // Classes from these files are only included after re-saving the files when watcher is running
    ],
    theme: {
        screens: {},
        extend: {},
    },
    plugins: [],
};
```

You can fix that by dropping that reset:

```diff
 module.exports = {
     purge: [
         "./**/*.{js,vue}",
         "./../views/myfolder/**/*.php", // Classes from these files are only included after re-saving the files when watcher is running
     ],
     theme: {
-        screens: {},
         extend: {},
     },
     plugins: [],
 };
```

In addition, the classes for `dark` mode are not included either, this is because you didn't  enable dark mode. You can do so by choosing the `class` or `media` option. https://tailwindcss.com/docs/dark-mode

```diff
 module.exports = {
+    darkMode: "class",
     purge: [
         "./**/*.{js,vue}",
         "./../views/myfolder/**/*.php", // Classes from these files are only included after re-saving the files when watcher is running
     ],
     theme: {
-        screens: {},
         extend: {},
     },
     plugins: [],
 };
```

The last class that is missing is the `items-top` one, this is because it doesn't exist in default Tailwind: https://tailwindcss.com/docs/align-items


Class | Properties
-- | --
items-start | align-items: flex-start;
items-end | align-items: flex-end;
items-center | align-items: center;
items-baseline | align-items: baseline;
items-stretch | align-items: stretch;



@RobinMalfait That explains some of the classes :) However `w-96` is not included when I first start `npm run watch` after making your suggested changes. But if I simply go into the blade file and hit Cmd+S without changing anything it then recompiles and `w-96` is included
Very odd, I am going to re-open this issue. It seems that when you drop the `"./**/*.{js,vue}"` line it does create the `w-96` consistently on the first run. I do have to mention that I see multiple "runs" in the webpack output.
When you include that line, initially the `w-96` does not get created. 

The good part is that I can reproduce it consistently.
Once I find the solution / actual issue I'll update this issue!

Thanks for opening the issue!
I also get multiple runs which I forgot to mention and I wasn't sure if it was intended or not. Normally we also build JS but I removed all non-CSS stuff from the repo for simplicity's sake.

If it helps, an interesting thing I found is that if I copy the entire `views` directory into `resources/myfolder` and change the purge config to `'./views/myfolder/**/*.php'`, then it works as expected. (Although still with multiple runs)
I'll open another issue but I just want to add that I'm running into the same thing, specifically multiple compilations and classes from Blade files missing after the first save.

I'm using a pretty standard Laravel Mix setup, and with JIT _every_ Webpack compilation runs twice now. It may be because both Mix and JIT are watching a bunch of the same files, but I need them to since I have Tailwind classes in my Vue components.

With some Blade files I'm now noticing the above issue too, with dynamic classes like `hover:text-[#3b5998]`. After saving, Webpack recompiles (twice) and the generated class isn't present in my CSS, then after saving again, and Webpack recompiling twice again, it is.
Okay, inexplicably, it's now working fine. I set `DEBUG=true` to see the JIT output, everything suddenly worked normally, and I removed it again and it now works as expected, even the double builds are gone. Gonna leave my comment in case it helps someone else, but sorry to muddy the waters!
I was having this problem after I added the `TAILWIND_MODE=watch` variable, not sure why but after I removed it the classes get generated normally.
I'm not having exactly this problem, but related.  I have a `purge` config that looks at changes to certain markdown files and yaml files that are outside of the directory structure (found using `../../`)  and this worked up until recently:

```js
module.exports = {
  mode: 'jit',
  purge: [
    '../../config/**/*.yaml',
    '../../pages/**/*.md',
    './blueprints/**/*.yaml',
    './js/**/*.js',
    './templates/**/*.twig',
    './typhoon.yaml',
    './typhoon.php',
    './available-classes.md',
  ],
...
```

However, now with JIT, the `../../` paths are not being picked up and recognized. This means that all the classes defined in those files are not being found, so not included in the CSS.

Makes no difference if i save the file, restart the `--watch` command, those first two paths are never included.  

This worked prior to JIT.
Having the same issue as above, doesn't seem to be able to pick up folders above the tailwind config. Same level folders or deeper work fine but going above no longer seems to work.

Patch:
diff --git a/src/index.js b/src/index.js
--- a/src/index.js
+++ b/src/index.js
@@ -97,7 +97,7 @@ module.exports = function (config) {
 
   return {
     postcssPlugin: 'tailwindcss',
-    plugins: [...plugins, processTailwindFeatures(getConfig), formatCSS],
+    plugins: [...plugins, processTailwindFeatures(getConfig, resolvedConfigPath), formatCSS],
   }
 }
 
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
@@ -787,7 +787,14 @@ export default function setupContext(configOrPath) {
       configDependencies: new Set(),
       candidateFiles: purgeContent
         .filter((item) => typeof item === 'string')
-        .map((path) => normalizePath(path)),
+        .map((purgePath) =>
+          normalizePath(
+            path.resolve(
+              userConfigPath === null ? process.cwd() : path.dirname(userConfigPath),
+              purgePath
+            )
+          )
+        ),
       rawContent: purgeContent
         .filter((item) => typeof item.raw === 'string')
         .map(({ raw, extension }) => ({ content: raw, extension })),
diff --git a/src/lib/purgeUnusedStyles.js b/src/lib/purgeUnusedStyles.js
--- a/src/lib/purgeUnusedStyles.js
+++ b/src/lib/purgeUnusedStyles.js
@@ -3,6 +3,7 @@ import postcss from 'postcss'
 import purgecss from '@fullhuman/postcss-purgecss'
 import log from '../util/log'
 import htmlTags from 'html-tags'
+import path from 'path'
 
 function removeTailwindMarkers(css) {
   css.walkAtRules('tailwind', (rule) => rule.remove())
@@ -33,7 +34,7 @@ export function tailwindExtractor(content) {
   return broadMatches.concat(broadMatchesWithoutTrailingSlash).concat(innerMatches)
 }
 
-export default function purgeUnusedUtilities(config, configChanged) {
+export default function purgeUnusedUtilities(config, configChanged, resolvedConfigPath) {
   const purgeEnabled = _.get(
     config,
     'purge.enabled',
@@ -104,7 +105,6 @@ export default function purgeUnusedUtilities(config, configChanged) {
     },
     removeTailwindMarkers,
     purgecss({
-      content: Array.isArray(config.purge) ? config.purge : config.purge.content,
       defaultExtractor: (content) => {
         const extractor = defaultExtractor || tailwindExtractor
         const preserved = [...extractor(content)]
@@ -116,6 +116,18 @@ export default function purgeUnusedUtilities(config, configChanged) {
         return preserved
       },
       ...purgeOptions,
+      content: (Array.isArray(config.purge)
+        ? config.purge
+        : config.purge.content || purgeOptions.content || []
+      ).map((item) => {
+        if (typeof item === 'string') {
+          return path.resolve(
+            resolvedConfigPath ? path.dirname(resolvedConfigPath) : process.cwd(),
+            item
+          )
+        }
+        return item
+      }),
     }),
   ])
 }
diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js
--- a/src/processTailwindFeatures.js
+++ b/src/processTailwindFeatures.js
@@ -24,7 +24,7 @@ let previousConfig = null
 let processedPlugins = null
 let getProcessedPlugins = null
 
-export default function (getConfig) {
+export default function (getConfig, resolvedConfigPath) {
   return function (css) {
     const config = getConfig()
     const configChanged = hash(previousConfig) !== hash(config)
@@ -65,7 +65,7 @@ export default function (getConfig) {
       substituteScreenAtRules(config),
       substituteClassApplyAtRules(config, getProcessedPlugins, configChanged),
       applyImportantConfiguration(config),
-      purgeUnusedStyles(config, configChanged),
+      purgeUnusedStyles(config, configChanged, resolvedConfigPath),
     ]).process(css, { from: _.get(css, 'source.input.file') })
   }
 }


Test patch:
diff --git a/tests/fixtures/custom-purge-config.js b/tests/fixtures/custom-purge-config.js
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/custom-purge-config.js
@@ -0,0 +1,20 @@
+module.exports = {
+  purge: ['./*.html'],
+  theme: {
+    extend: {
+      colors: {
+        'black!': '#000',
+      },
+      spacing: {
+        1.5: '0.375rem',
+        '(1/2+8)': 'calc(50% + 2rem)',
+      },
+      minHeight: {
+        '(screen-4)': 'calc(100vh - 1rem)',
+      },
+      fontFamily: {
+        '%#$@': 'Comic Sans',
+      },
+    },
+  },
+}
diff --git a/tests/jit/relative-purge-paths.config.js b/tests/jit/relative-purge-paths.config.js
new file mode 100644
--- /dev/null
+++ b/tests/jit/relative-purge-paths.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+  mode: 'jit',
+  purge: ['./relative-purge-paths.test.html'],
+  corePlugins: { preflight: false },
+  theme: {},
+  plugins: [],
+}
diff --git a/tests/jit/relative-purge-paths.test.css b/tests/jit/relative-purge-paths.test.css
new file mode 100644
--- /dev/null
+++ b/tests/jit/relative-purge-paths.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/relative-purge-paths.test.html b/tests/jit/relative-purge-paths.test.html
new file mode 100644
--- /dev/null
+++ b/tests/jit/relative-purge-paths.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/relative-purge-paths.test.js b/tests/jit/relative-purge-paths.test.js
new file mode 100644
--- /dev/null
+++ b/tests/jit/relative-purge-paths.test.js
@@ -0,0 +1,25 @@
+import postcss from 'postcss'
+import fs from 'fs'
+import path from 'path'
+import tailwind from '../../src/jit/index.js'
+
+function run(input, config = {}) {
+  return postcss(tailwind(config)).process(input, {
+    from: path.resolve(__filename),
+  })
+}
+
+test('relative purge paths', () => {
+  let css = `
+    @tailwind base;
+    @tailwind components;
+    @tailwind utilities;
+  `
+
+  return run(css, path.resolve(__dirname, './relative-purge-paths.config.js')).then((result) => {
+    let expectedPath = path.resolve(__dirname, './relative-purge-paths.test.css')
+    let expected = fs.readFileSync(expectedPath, 'utf8')
+
+    expect(result.css).toMatchFormattedCss(expected)
+  })
+})
diff --git a/tests/purgeUnusedStyles.test.js b/tests/purgeUnusedStyles.test.js
--- a/tests/purgeUnusedStyles.test.js
+++ b/tests/purgeUnusedStyles.test.js
@@ -4,6 +4,7 @@ import postcss from 'postcss'
 import tailwind from '../src/index'
 import { tailwindExtractor } from '../src/lib/purgeUnusedStyles'
 import defaultConfig from '../stubs/defaultConfig.stub.js'
+import customConfig from './fixtures/custom-purge-config.js'
 
 function suppressConsoleLogs(cb, type = 'warn') {
   return () => {
@@ -38,25 +39,13 @@ async function inProduction(callback) {
   return result
 }
 
+function withTestName(path) {
+  return `${path}?test=${expect.getState().currentTestName}`
+}
+
 const config = {
   ...defaultConfig,
-  theme: {
-    extend: {
-      colors: {
-        'black!': '#000',
-      },
-      spacing: {
-        1.5: '0.375rem',
-        '(1/2+8)': 'calc(50% + 2rem)',
-      },
-      minHeight: {
-        '(screen-4)': 'calc(100vh - 1rem)',
-      },
-      fontFamily: {
-        '%#$@': 'Comic Sans',
-      },
-    },
-  },
+  ...customConfig,
 }
 
 delete config.presets
@@ -108,7 +97,22 @@ test('purges unused classes', () => {
           purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
         }),
       ])
-        .process(input, { from: inputPath })
+        .process(input, { from: withTestName(inputPath) })
+        .then((result) => {
+          assertPurged(result)
+        })
+    })
+  )
+})
+
+test('purge patterns are resolved relative to the config file', () => {
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([tailwind(path.resolve(`${__dirname}/fixtures/custom-purge-config.js`))])
+        .process(input, { from: withTestName(inputPath) })
         .then((result) => {
           assertPurged(result)
         })
@@ -275,7 +279,7 @@ test('purges unused classes with important string', () => {
           purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
         }),
       ])
-        .process(input, { from: inputPath })
+        .process(input, { from: withTestName(inputPath) })
         .then((result) => {
           assertPurged(result)
         })
@@ -298,7 +302,7 @@ test('mode must be a valid value', () => {
               content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
             },
           }),
-        ]).process(input, { from: inputPath })
+        ]).process(input, { from: withTestName(inputPath) })
       ).rejects.toThrow()
     })
   )
@@ -319,7 +323,7 @@ test('components are purged by default in layers mode', () => {
           purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
         }),
       ])
-        .process(input, { from: inputPath })
+        .process(input, { from: withTestName(inputPath) })
         .then((result) => {
           expect(result.css).not.toContain('.container')
           assertPurged(result)
@@ -347,7 +351,7 @@ test('you can specify which layers to purge', () => {
           },
         }),
       ])
-        .process(input, { from: inputPath })
+        .process(input, { from: withTestName(inputPath) })
         .then((result) => {
           const rules = extractRules(result.root)
           expect(rules).toContain('optgroup')
@@ -377,7 +381,7 @@ test('you can purge just base and component layers (but why)', () => {
           },
         }),
       ])
-        .process(input, { from: inputPath })
+        .process(input, { from: withTestName(inputPath) })
         .then((result) => {
           const rules = extractRules(result.root)
           expect(rules).not.toContain('[type="checkbox"]')
@@ -439,7 +443,7 @@ test(
         purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
       }),
     ])
-      .process(input, { from: inputPath })
+      .process(input, { from: withTestName(inputPath) })
       .then((result) => {
         const expected = fs.readFileSync(
           path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
@@ -465,7 +469,7 @@ test('does not purge if the array is empty', () => {
           purge: [],
         }),
       ])
-        .process(input, { from: inputPath })
+        .process(input, { from: withTestName(inputPath) })
         .then((result) => {
           process.env.NODE_ENV = OLD_NODE_ENV
           const expected = fs.readFileSync(
@@ -491,7 +495,7 @@ test('does not purge if explicitly disabled', () => {
           purge: { enabled: false },
         }),
       ])
-        .process(input, { from: inputPath })
+        .process(input, { from: withTestName(inputPath) })
         .then((result) => {
           const expected = fs.readFileSync(
             path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
@@ -516,7 +520,7 @@ test('does not purge if purge is simply false', () => {
           purge: false,
         }),
       ])
-        .process(input, { from: inputPath })
+        .process(input, { from: withTestName(inputPath) })
         .then((result) => {
           const expected = fs.readFileSync(
             path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
@@ -541,7 +545,7 @@ test('purges outside of production if explicitly enabled', () => {
           purge: { enabled: true, content: [path.resolve(`${__dirname}/fixtures/**/*.html`)] },
         }),
       ])
-        .process(input, { from: inputPath })
+        .process(input, { from: withTestName(inputPath) })
         .then((result) => {
           assertPurged(result)
         })
@@ -567,7 +571,7 @@ test(
         },
       }),
     ])
-      .process(input, { from: inputPath })
+      .process(input, { from: withTestName(inputPath) })
       .then((result) => {
         expect(result.css).toContain('.md\\:bg-green-500')
         assertPurged(result)
@@ -596,7 +600,7 @@ test(
         css.walkComments((c) => c.remove())
       },
     ])
-      .process(input, { from: inputPath })
+      .process(input, { from: withTestName(inputPath) })
       .then((result) => {
         expect(result.css).toContain('html')
         expect(result.css).toContain('body')
@@ -624,7 +628,7 @@ test('element selectors are preserved by default', () => {
           },
         }),
       ])
-        .process(input, { from: inputPath })
+        .process(input, { from: withTestName(inputPath) })
         .then((result) => {
           const rules = extractRules(result.root)
           ;[
@@ -678,7 +682,7 @@ test('element selectors are preserved even when defaultExtractor is overridden',
           },
         }),
       ])
-        .process(input, { from: inputPath })
+        .process(input, { from: withTestName(inputPath) })
         .then((result) => {
           const rules = extractRules(result.root)
           ;[
@@ -729,7 +733,7 @@ test('preserving element selectors can be disabled', () => {
           },
         }),
       ])
-        .process(input, { from: inputPath })
+        .process(input, { from: withTestName(inputPath) })
         .then((result) => {
           const rules = extractRules(result.root)