Blog
How Tailwind CSS detects circular dependancy.

How Tailwind CSS detects circular dependancy.

In this article, we analyze error thrown in substituteAtApply. This error is about circular dependency detected.

walk(rule.nodes, (child) => {
  if (child !== node) return
  throw new Error(
   `You cannot \`@apply\` the \`${candidate}\` utility here because it creates a circular dependency.`,
   )
})

This is a high level overview of the code around this error.

walk — recursive function:

Let’s begin with walk:

export function walk(
   ast: AstNode[],
   visit: (
     node: AstNode,
     utils: {
     parent: AstNode | null
     replaceWith(newNode: AstNode | AstNode[]): void
     context: Record<string, string>
   },
 ) => void | WalkAction,
 parent: AstNode | null = null,
 context: Record<string, string> = {},
) {
 for (let i = 0; i < ast.length; i++) {
   let node = ast[i]
  // We want context nodes to be transparent in walks. This means that
   // whenever we encounter one, we immediately walk through its children and
   // furthermore we also don't update the parent.
 if (node.kind === 'context') {
   walk(node.nodes, visit, parent, { …context, …node.context })
   continue
 }
let status = visit(node, {
   parent,
   replaceWith(newNode) {
   ast.splice(i, 1, …(Array.isArray(newNode) ? newNode : [newNode]))
   // We want to visit the newly replaced node(s), which start at the
   // current index (i). By decrementing the index here, the next loop
   // will process this position (containing the replaced node) again.
   i - 
 },
 context,
 }) ?? WalkAction.Continue
  // Stop the walk entirely
   if (status === WalkAction.Stop) return
  // Skip visiting the children of this node
   if (status === WalkAction.Skip) continue
  if (node.kind === 'rule') {
   walk(node.nodes, visit, node, context)
   }
 }
}

walk is a recursive function located in ast.ts.

It calls itself recursively when node.kind === ‘context’ or when node.kind === ‘rule’, breaking condition is based on status

// Stop the walk entirely
if (status === WalkAction.Stop) return
// Skip visiting the children of this node
if (status === WalkAction.Skip) continue

Now let’s zoom out a bit and study the code in the vicinity of walk function in apply.ts

// Verify that we don't have any circular dependencies by verifying that
// the current node does not appear in the new nodes.
walk(newNodes, (child) => {
 if (child !== node) return
  // At this point we already know that we have a circular dependency.
  //
  // Figure out which candidate caused the circular dependency. This will
 // help to create a useful error message for the end user.
 for (let candidate of candidates) {
   let selector = `.${escape(candidate)}`
    for (let rule of candidateAst) {
     if (rule.kind !== 'rule') continue
     if (rule.selector !== selector) continue
      walk(rule.nodes, (child) => {
     if (child !== node) return
      throw new Error(
       `You cannot \`@apply\` the \`${candidate}\` utility here because it creates a circular dependency.`,
       )
     })
    }
 }
})

TailwindCSS authors have added explaining comments across the codebase where required or it makes sense to provide additional context
with comments.

About me:

Hey, my name is Ramu Narasinga. I study large open-source projects and create content about their codebase architecture and best practices, sharing it through articles, videos.

I am open to work on interesting projects. Send me an email at ramu.narasinga@gmail.com

My Github — https://github.com/ramu-narasinga

My website — https://ramunarasinga.com

My Youtube channel — https://www.youtube.com/@thinkthroo

Learning platform — https://thinkthroo.com

Codebase Architecture — https://app.thinkthroo.com/architecture

Best practices — https://app.thinkthroo.com/best-practices

Production-grade projects — https://app.thinkthroo.com/production-grade-projects

References:

  1. https://github.com/tailwindlabs/tailwindcss/blob/next/packages/tailwindcss/src/ast.ts#L70

  2. https://github.com/tailwindlabs/tailwindcss/blob/c01b8254e822d4f328674357347ca0532f1283a0/packages/tailwindcss/src/apply.ts

  3. https://stackoverflow.com/questions/71669246/need-help-using-apply-directive-in-tailwind-css

  4. https://github.com/tailwindlabs/tailwindcss/issues/2807