mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2026-01-25 16:44:12 +00:00
This PR fixes an issue where an `@config` was injected in a strange location if you have multiple CSS files with Tailwind directives. Let's say you have this setup: ```css /* ./src/index.css */ @import "./tailwind-setup.css"; /* ./src/tailwind-setup.css */ @import "./base.css"; @import "./components.css"; @import "./utilities.css"; /* ./src/base.css */ @tailwind base; /* ./src/components.css */ @tailwind components; /* ./src/utilities.css */ @tailwind utilities; ``` In this case, `base.css`, `components.css`, and `utilities.css` are all considered Tailwind roots because they contain Tailwind directives or imports. Since there are multiple roots, the nearest common ancestor should become the tailwind root (where `@config` is injected). In this case, the nearest common ancestor is `tailwind-setup.css` (not `index.css` because that's further away). Before this change, we find the common ancestor between `base.css` and `components.css` which would be `index.css` instead of `tailwind-setup.css`. In a next iteration, we compare `index.css` with `utilities.css` and find that there is no common ancestor (because the `index.css` file has no parents). This resulted in the `@config` being injected in `index.css` and in `utilities.css`. Continuing with the rest of the migrations, we migrate the `index.css`'s `@config` away, but we didn't migrate the `@config` from `utilities.css`. With this PR, we don't even have the `@config` in the `utilities.css` file anymore. Test plan --- 1. Added an integration test with a non-migrateable config file to ensure that the `@config` is injected in the correct file. 2. Added an integration test with a migrateable config file to ensure that the CSS config is injected in the correct file. h/t @philipp-spiess 3. Ran the upgrade on the https://commit.tailwindui.com project and ensured that 1. The `@config` does not exist in the `utilities.css` file (this was the first bug we solved) 2. The `@config` is replaced in the `tailwind.css` file correctly. <img width="592" alt="image" src="https://github.com/user-attachments/assets/02e3f6ea-a85d-46c2-ac93-09f34ac4a4b8"> <img width="573" alt="image" src="https://github.com/user-attachments/assets/e372eb5f-5732-4052-ab39-096ba7970ff6">
110 lines
3.2 KiB
TypeScript
110 lines
3.2 KiB
TypeScript
import path from 'node:path'
|
|
import postcss, { AtRule, type Plugin } from 'postcss'
|
|
import { normalizePath } from '../../../@tailwindcss-node/src/normalize-path'
|
|
import type { JSConfigMigration } from '../migrate-js-config'
|
|
import type { Stylesheet } from '../stylesheet'
|
|
|
|
const ALREADY_INJECTED = new WeakMap<Stylesheet, string[]>()
|
|
|
|
export function migrateConfig(
|
|
sheet: Stylesheet,
|
|
{
|
|
configFilePath,
|
|
jsConfigMigration,
|
|
}: { configFilePath: string; jsConfigMigration: JSConfigMigration },
|
|
): Plugin {
|
|
function migrate() {
|
|
if (!sheet.isTailwindRoot) return
|
|
|
|
let alreadyInjected = ALREADY_INJECTED.get(sheet)
|
|
if (alreadyInjected && alreadyInjected.includes(configFilePath)) {
|
|
return
|
|
} else if (alreadyInjected) {
|
|
alreadyInjected.push(configFilePath)
|
|
} else {
|
|
ALREADY_INJECTED.set(sheet, [configFilePath])
|
|
}
|
|
|
|
let root = sheet.root
|
|
|
|
// We don't have a sheet with a file path
|
|
if (!sheet.file) return
|
|
|
|
let cssConfig = new AtRule()
|
|
|
|
// Remove the `@config` directive if it exists and we couldn't migrate the
|
|
// config file.
|
|
if (jsConfigMigration !== null) {
|
|
root.walkAtRules('config', (node) => {
|
|
node.remove()
|
|
})
|
|
|
|
let css = '\n\n'
|
|
css += '\n@tw-bucket source {'
|
|
for (let source of jsConfigMigration.sources) {
|
|
let absolute = path.resolve(source.base, source.pattern)
|
|
css += `@source '${relativeToStylesheet(sheet, absolute)}';\n`
|
|
}
|
|
css += '}\n'
|
|
|
|
css += '\n@tw-bucket plugin {\n'
|
|
for (let plugin of jsConfigMigration.plugins) {
|
|
let relative =
|
|
plugin.path[0] === '.'
|
|
? relativeToStylesheet(sheet, path.resolve(plugin.base, plugin.path))
|
|
: plugin.path
|
|
|
|
if (plugin.options === null) {
|
|
css += `@plugin '${relative}';\n`
|
|
} else {
|
|
css += `@plugin '${relative}' {\n`
|
|
for (let [property, value] of Object.entries(plugin.options)) {
|
|
let cssValue = ''
|
|
if (typeof value === 'string') {
|
|
cssValue = quoteString(value)
|
|
} else if (Array.isArray(value)) {
|
|
cssValue = value
|
|
.map((v) => (typeof v === 'string' ? quoteString(v) : '' + v))
|
|
.join(', ')
|
|
} else {
|
|
cssValue = '' + value
|
|
}
|
|
|
|
css += ` ${property}: ${cssValue};\n`
|
|
}
|
|
css += '}\n' // @plugin
|
|
}
|
|
}
|
|
css += '}\n' // @tw-bucket
|
|
|
|
cssConfig.append(postcss.parse(css + jsConfigMigration.css))
|
|
}
|
|
|
|
// Inject the `@config` directive
|
|
root.append(cssConfig.nodes)
|
|
}
|
|
|
|
return {
|
|
postcssPlugin: '@tailwindcss/upgrade/migrate-config',
|
|
OnceExit: migrate,
|
|
}
|
|
}
|
|
|
|
function relativeToStylesheet(sheet: Stylesheet, absolute: string) {
|
|
if (!sheet.file) throw new Error('Can not find a path for the stylesheet')
|
|
|
|
let sheetPath = sheet.file
|
|
|
|
let relative = path.relative(path.dirname(sheetPath), absolute)
|
|
if (relative[0] !== '.') {
|
|
relative = `./${relative}`
|
|
}
|
|
// Ensure relative is a POSIX style path since we will merge it with the
|
|
// glob.
|
|
return normalizePath(relative)
|
|
}
|
|
|
|
function quoteString(value: string): string {
|
|
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
}
|