tailwindcss/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts
Philipp Spiess 545401469d
Postcss: Run plugin in Once hook (#15273)
Closes #15138

This PR changes the postcss client to run in the `Once` hook instead of
`OnceExit`. This makes sure the postcss order in v4 matches that of v3.

Conceptually this also makes more sense, since we expect tailwindcss to
be run as one of the first plugins in the pipeline (where `OnceExit`
would run it almost at the end).

To make sure it's still possible to use `postcss-import` before and have
it resolve to the right paths, we also needed to change the
`postcss-fix-relative-paths` plugin to run in the `Once` order
(`postcss-import` also uses `Once` order so the order).

## Test Plan

This issue had many ways in which it can manifest. I added a unit test
to ensure the plugin order works but here's a concrete example when
using the postcss plugin in Vite.

### Before

Image `url()`s were not properly handled since the postcss plugin to
transform these was run before Tailwind CSS could generate the class for
it:

<img width="2532" alt="Screenshot 2024-12-02 at 14 55 42"
src="https://github.com/user-attachments/assets/2f23b409-1576-441d-9ffe-6f24ad6e7436">

### After

<img width="2529" alt="Screenshot 2024-12-02 at 14 53 52"
src="https://github.com/user-attachments/assets/b754c3d8-1af1-4aeb-87da-0bfc3ffecdb7">

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2024-12-03 10:28:51 +01:00

74 lines
2 KiB
TypeScript

import { normalizePath } from '@tailwindcss/node'
import path from 'node:path'
import type { AtRule, Plugin } from 'postcss'
const SINGLE_QUOTE = "'"
const DOUBLE_QUOTE = '"'
export default function fixRelativePathsPlugin(): Plugin {
// Retain a list of touched at-rules to avoid infinite loops
let touched: WeakSet<AtRule> = new WeakSet()
function fixRelativePath(atRule: AtRule) {
let rootPath = atRule.root().source?.input.file
if (!rootPath) {
return
}
let inputFilePath = atRule.source?.input.file
if (!inputFilePath) {
return
}
if (touched.has(atRule)) {
return
}
let value = atRule.params[0]
let quote =
value[0] === DOUBLE_QUOTE && value[value.length - 1] === DOUBLE_QUOTE
? DOUBLE_QUOTE
: value[0] === SINGLE_QUOTE && value[value.length - 1] === SINGLE_QUOTE
? SINGLE_QUOTE
: null
if (!quote) {
return
}
let glob = atRule.params.slice(1, -1)
// Handle eventual negative rules. We only support one level of negation.
let negativePrefix = ''
if (glob.startsWith('!')) {
glob = glob.slice(1)
negativePrefix = '!'
}
// We only want to rewrite relative paths.
if (!glob.startsWith('./') && !glob.startsWith('../')) {
return
}
let absoluteGlob = path.posix.join(normalizePath(path.dirname(inputFilePath)), glob)
let absoluteRootPosixPath = path.posix.dirname(normalizePath(rootPath))
let relative = path.posix.relative(absoluteRootPosixPath, absoluteGlob)
// If the path points to a file in the same directory, `path.relative` will
// remove the leading `./` and we need to add it back in order to still
// consider the path relative
if (!relative.startsWith('.')) {
relative = './' + relative
}
atRule.params = quote + negativePrefix + relative + quote
touched.add(atRule)
}
return {
postcssPlugin: 'tailwindcss-postcss-fix-relative-paths',
Once(root) {
root.walkAtRules(/source|plugin|config/, fixRelativePath)
},
}
}