tailwindlabs / tailwindcss

A utility-first CSS framework for rapid UI development.
https://tailwindcss.com/
MIT License
80.38k stars 4.04k forks source link

Circular dependencies when using `@apply` cause infinite loops/hanging #2807

Closed unformatt closed 2 years ago

unformatt commented 3 years ago

Describe the problem:

After upgrading to Tailwind v2, my webpack build started hanging during the building phase

28% building 1/3 entries 22/27 dependencies 5/14 modules

It seems to be that a compound selector is causing an infinite loop and is not triggering RangeError: Maximum call stack size exceeded. Also not triggering Tailwind's circular dependency detection.

.my-border { @apply border-2; }
/* Remove the following line to see Tailwind report a "circular dependency". Leave it to see build hang.
*/
.foo { @apply my-border; }
.bar.input-border { @apply my-border; }

Secondary issue

Why is this a circular dependency? https://play.tailwindcss.com/lS1DnjwS6g?file=css

Link to a minimal reproduction:

https://play.tailwindcss.com/UO2eaaZ0AD?file=css

tailwind.zip

adamwathan commented 3 years ago

Will look into this in more detail when I can but wanted to quickly answer this part:

Why is this a circular dependency?

.my-border {
  @apply border-2;
}
.bar.my-border {
  @apply my-border;
}

The part that is a circular dependency here is just this part:

.bar.my-border {
  @apply my-border;
}

@apply in Tailwind 2 will apply all instance of a selector, so this is trying to apply my-border within another selector that already contains .my-border triggering a circular dependency.

Consider with different names:

.foo {
  color: blue;
}
.bar.foo {
  color: red;
}

.potato {
  @apply foo;
}

The output of this is:

.foo {
  color: blue;
}
.bar.foo {
  color: red;
}

.potato {
  color: blue;
}
.bar.potato {
  color: red;
}
unformatt commented 3 years ago

In regards to the last example, I don't think it's intuitive behavior that @apply foo would pick up styles from .bar.foo.

adamwathan commented 3 years ago

I don't think it's intuitive behavior that @apply foo would pick up styles from .bar.foo.

You say that now but if you had this HTML:

<div class="group">
  <div class="group-hover:bg-black text-red-500 font-medium">...</div>
</div>

...you'd expect this refactoring to work right?

<style>
  .custom-class {
    @apply group-hover:bg-black text-red-500 font-medium
  }
</style>

<div class="group">
  <div class="custom-class">...</div>
</div>

The only way for that to work is for @apply to consider every use of a class name in the stylesheet. The guiding principal for what the correct behavior of @apply should be is "if you extract a list of classes from your HTML into a custom class using @apply, your code should work the same way it did before".

SachinAgarwal1337 commented 3 years ago

I have a use case for this, where I don't want @apply to pickup styles from every css class

I am using element UI as my framework for frontend. Now I would want to apply w-full to say upload element but it won't work because the width is set to some child div tag. Now to solve this I am doing this

.w-full {
  .el-upload {
    @apply w-full;

    .el-upload-dragger {
      @apply w-full ;
    }
  }
}

Now I can use a different out class, but the reason I want it to be called w-full is to keep my class names similar to tailwindcss.

The possible solution would be

.w-full {
  .el-upload {
    width: 100%;

    .el-upload-dragger {
      width: 100%;
    }
  }
}

But again w-full is simple behaviour. It would be a prob if I want to apply color or margins etc

LeoniePhiline commented 2 years ago

@adamwathan

I have a similar issue where tailwind 2 does not allow me to apply a generated class, even though I do not see how this is an actual circular dependency:

@layer base {
   .bg-magenta.text-white .rte a[href]:not(.cta) {
     @apply text-white;
   }
}

I am using .rte ("real text editor") like .prose is used in @tailwindcss/typography. All CMS output (which will not contain tailwind classes) is wrapped in .rte containers.

So this says:

Now tailwindcss answers with:

You cannot @apply the text-white utility here because it creates a circular dependency.

I am not wanting to apply anything to .bg-magenta.text-white (which could cause a circular dependency), but, actually, to its decendant elements .bg-magenta.text-white .rte a[href]:not(.cta).

So I fail to understand wht this case is considered a circular dependency.

What am I missing? Is the heuristic for circular dependencies correct?

What can I do? Is reverting to color: theme('colors.white'); my only chance?

jaimeiniesta commented 2 years ago

I'm having a similar issue on a Phoenix app, it works fine with Tailwind 2 and esbuild as in this Phoenix PR and this tutorial but if I try to use the @layer directive, I get a RangeError: Maximum call stack size exceeded error.

Here are the details on my CSS and config:

https://gist.github.com/jaimeiniesta/79dd351e77a7537b7b5c6e908d128ff2

It works fine if I put my classes outside the @layer components block.

jaimeiniesta commented 2 years ago

Update: I found where the circular dependency was in my CSS:

div.leading-relaxed p {
    @apply leading-relaxed mb-4;
}

I will fix that on my side, but I wanted to report that this doesn't fail unless it's inside a @layer directive. If my main app.css file is like this:

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@layer components {}

div.leading-relaxed p {
    @apply leading-relaxed mb-4;
}

Then it works fine and generates:

.leading-relaxed {
  line-height: 1.625;
}

div.leading-relaxed p {
  margin-bottom: 1rem;
  line-height: 1.625;
}

But, if I move that inside @layer components like this, it fails with a maximum call stack size exceeded:

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@layer components {
    div.leading-relaxed p {
        @apply leading-relaxed mb-4;
    }
}
dorianmariecom commented 2 years ago

I was doing something like:

textarea {
  &.hidden {
    @apply hidden
  }
}

makes sense that there is a circular dependency, it just got detected in tailwindcss 3, solution is just to do:

textarea {
  &.hidden {
    display: none;
  }
}
RobinMalfait commented 2 years ago

Hey! Thank you for your bug report! Much appreciated! 🙏

In the latest version we detect circular dependencies and notify you instead of causing an infinite loop 👍

unformatt commented 2 years ago

@RobinMalfait are you referring to the last comment about an actual circular dependency or the original bug report here which about a non-circular dependency being reported as a circular dependency?

jstrangfeld commented 2 years ago

What is the proper solution with Tailwind 3 to avoid circular dependencies but still explicitly define styles and re-use class names?

Wrong:

#cms-output .clear-left {
  @apply clear-left;
}

Right: ?

RobinMalfait commented 2 years ago

@unformatt multiple things are happening here:

This is still a circular dependency:

.my-border {
  @apply border-2;
}
.bar.my-border {
  @apply my-border;
}

Because you are applying my-border in function of my-border.

The goal of @apply, is to make sure that you can substitute the new selector with all the things you are applying. Therefore we have to apply all known definitions. I wrote more about that here: https://github.com/tailwindlabs/tailwindcss/issues/6451#issuecomment-997034450


@jstrangfeld the short answer is, don't use utilities in the selector and in the @apply, a longer answer can be found here: https://github.com/tailwindlabs/tailwindcss/issues/6451#issuecomment-997034450

For your specific use case, update:

#cms-output .clear-left {
  @apply clear-left;
}

To:

#cms-output .clear-left {
  clear: left;
}
LeoniePhiline commented 2 years ago

@RobinMalfait

I have found a false positive.

The following worked in TailwindCSS 2, but is broken in TailwindCSS 3:

@layer base {
   .rte a[href]:not(.cta) {
     @apply text-magenta; /* Note: text-magenta, not bg-magenta. */
   }

    /* 
     * False error in next selector below: 
     * You cannot @apply the bg-magenta utility here because it creates a circular dependency. 
     */

   .bg-magenta.text-white .rte a[href]:not(.cta) { /* line 11 */
       color: theme('colors.white');
   }
}

I get the following error reported:

@parcel/transformer-postcss:

/src/tailwind/custom/base/rte/hyperlinks.css:11:4: You cannot `@apply` the `bg-magenta` utility here because it creates a circular dependency.

This must be a false positive, because in line 11 I do not @apply bg-magenta at all.

In line 11, tailwindcss might dereference .rte a[href]:not(.cta), which does apply the foreground color @apply text-magenta. But it does not @apply the background color bg-magenta. .bg-magenta.text-white here is just a selector. (Flipping/inverting from a common color on white scheme to a special white on color scheme.) No @apply bg-magenta and no @apply text-white either.

Side note: The entire section is wrapped in @layer base, which, I thought, might protect the selector from being dereferenced. But in TailwindCSS 3, this does not help. The selector is read as if it was an @apply anyway.

robocub commented 2 years ago

Am I getting a false positive here?

.input {
  > div {
    @apply bg-dark;

    .bg-dark & {
      @apply bg-body-light;
    }
  }
}

This gives me this error message:

  You cannot `@apply` the `bg-dark` utility here because it creates a circular dependency.

  316 |   @apply bg-dark;
  317 | }
> 318 | .bg-dark .input > div {
      | ^
  319 |   @apply bg-body-light;
  320 | }
dorianmariecom commented 2 years ago

@robocub you should open a new issue, also yes it's an infinite loop, what did you expect the generated CSS to be?

In general it's better not to extend tailwind classes and create one own's classes

jaylaw81 commented 1 year ago

I've found a way to get around this as well

This should work

*:has(.bg-dark) .input > div {
  background-color: theme(colors.body-light);
}
yaroslav-drach commented 7 months ago

Just interesting fact that such code causes error during styles compilation, because CSS class flex is reserved by Tailwind

.field {
    .control {
        &.flex {
            background: red;
        }
    }
}

But like workaround you can just replace .flex with [class="flex"] and compilation works without errors.

.field {
    .control {
        &[class*="flex"] {
            background: red;
        }
    }
}