/
githubmirror
/
tailwindcss
Обзор
Документация
Войти
/
githubmirror
/
tailwindcss
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/@tailwindcss-upgrade/src/codemods/css/analyze.ts
303 строки
10 KB
Benoît Rouleau
Prevent `@tailwindcss/upgrade` from rewriting files inside `node_modules` when run from a workspace subpackage (#20329)
14 июл 2026, 19:39
Не верифицирован
14 июл 2026, 19:39
a854b35
Код
Авторство
О чём код?
import { isGitIgnored } from 'globby' import path from 'node:path' import postcss, { type Result } from 'postcss' import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' import { segment } from '../../../../tailwindcss/src/utils/segment' import { Stylesheet, type StylesheetConnection } from '../../stylesheet' import { gitRoot } from '../../utils/git' import { error, highlight, relative } from '../../utils/renderer' import { resolveCssId } from '../../utils/resolve' export async function analyze(stylesheets: Stylesheet[], { base }: { base: string }) { // Use the project's git root, instead of the `base` to ensure all // `.gitignore` rules are being used. let isIgnored = await isGitIgnored({ cwd: gitRoot(base) ?? base }) let processingQueue: (() => Promise<Result>)[] = [] let stylesheetsByFile = new DefaultMap<string, Stylesheet | null>((file) => { // We don't want to process ignored files (like node_modules) try { if (isIgnored(file)) { return null } } catch { // If the file is not part of the current working directory (which can // happen if you import `tailwindcss` and it's loading a shared file from // pnpm) then this will throw. return null } try { let sheet = Stylesheet.loadSync(file) // Mutate incoming stylesheets to include the newly discovered sheet stylesheets.push(sheet) // Queue up the processing of this stylesheet processingQueue.push(() => processor.process(sheet.root, { from: sheet.file! })) return sheet } catch { return null } }) // Step 1: Record which `@import` rules point to which stylesheets // and which stylesheets are parents/children of each other let processor = postcss([ { postcssPlugin: 'mark-import-nodes', AtRule: { import(node) { // Find what the import points to let id = node.params.match(/['"](.*?)['"]/)?.[1] if (!id) return let basePath = node.source?.input.file ? path.dirname(node.source.input.file) : process.cwd() // Resolve the import to a file path let resolvedPath: string | false = false try { // We first try to resolve the file as relative to the current file // to mimic the behavior of `postcss-import` since that's what was // used to resolve imports in Tailwind CSS v3. if (id[0] !== '.') { try { resolvedPath = resolveCssId(`./${id}`, basePath) } catch {} } if (!resolvedPath) { resolvedPath = resolveCssId(id, basePath) } } catch (err) { // Import is a URL, we don't want to process these, but also don't // want to show an error message for them. if (id.startsWith('http://') || id.startsWith('https://') || id.startsWith('//')) { return } // Something went wrong, we can't resolve the import. error( `Failed to resolve import: ${highlight(id)} in ${highlight(relative(node.source?.input.file!, basePath))}. Skipping.`, { prefix: '↳ ' }, ) return } if (!resolvedPath) return // Find the stylesheet pointing to the resolved path let stylesheet = stylesheetsByFile.get(resolvedPath) // If it _does not_ exist in stylesheets we don't care and skip it // this is likely because its in node_modules or a workspace package // that we don't want to modify if (!stylesheet) return // Mark the import node with the ID of the stylesheet it points to // We will use these later to build lookup tables and modify the AST node.raws.tailwind_destination_sheet_id = stylesheet.id let parent = node.source?.input.file ? stylesheetsByFile.get(node.source.input.file) : undefined let layers: string[] = [] for (let part of segment(node.params, ' ')) { if (!part.startsWith('layer(')) continue if (!part.endsWith(')')) continue layers.push(part.slice(6, -1).trim()) } // Connect sheets together in a dependency graph if (parent) { let meta = { layers } stylesheet.parents.add({ item: parent, meta }) parent.children.add({ item: stylesheet, meta }) } }, }, }, ]) // Seed the map with all the known stylesheets, and queue up the processing of // each incoming stylesheet. for (let sheet of stylesheets) { if (sheet.file) { stylesheetsByFile.set(sheet.file, sheet) processingQueue.push(() => processor.process(sheet.root, { from: sheet.file ?? undefined })) } } // Process all the stylesheets from step 1 while (processingQueue.length > 0) { let task = processingQueue.shift()! await task() } // --- let commonPath = process.cwd() function pathToString(path: StylesheetConnection[]) { let parts: string[] = [] for (let connection of path) { if (!connection.item.file) continue let filePath = connection.item.file.replace(commonPath, '') let layers = connection.meta.layers.join(', ') if (layers.length > 0) { parts.push(`${filePath} (layers: ${layers})`) } else { parts.push(filePath) } } return parts.join(' <- ') } let lines: string[] = [] for (let sheet of stylesheets) { if (!sheet.file) continue let { convertiblePaths, nonConvertiblePaths } = sheet.analyzeImportPaths() let isAmbiguous = convertiblePaths.length > 0 && nonConvertiblePaths.length > 0 if (!isAmbiguous) continue sheet.canMigrate = false let filePath = sheet.file.replace(commonPath, '') for (let path of convertiblePaths) { lines.push(`- ${filePath} <- ${pathToString(path)}`) } for (let path of nonConvertiblePaths) { lines.push(`- ${filePath} <- ${pathToString(path)}`) } } if (lines.length === 0) { let tailwindRootLeafs = new Set<Stylesheet>() for (let sheet of stylesheets) { // If the current file already contains `@config`, then we can assume it's // a Tailwind CSS root file. sheet.root.walkAtRules('config', () => { sheet.isTailwindRoot = true return false }) if (sheet.isTailwindRoot) continue // If an `@tailwind` at-rule, or `@import "tailwindcss"` is present, // then we can assume it's a file where Tailwind CSS might be configured. // // However, if 2 or more stylesheets exist with these rules that share a // common parent, then we want to mark the common parent as the root // stylesheet instead. sheet.root.walkAtRules((node) => { if ( node.name === 'tailwind' || (node.name === 'import' && node.params.match(/^["']tailwindcss["']/)) || (node.name === 'import' && node.params.match(/^["']tailwindcss\/.*?["']$/)) ) { sheet.isTailwindRoot = true tailwindRootLeafs.add(sheet) } }) } // Only a single Tailwind CSS root file exists, no need to do anything else. if (tailwindRootLeafs.size <= 1) { return } // Mark the common parent as the root file { // Group each sheet from tailwindRootLeafs by their common parent let commonParents = new DefaultMap<Stylesheet, Set<Stylesheet>>(() => new Set<Stylesheet>()) // Seed common parents with leafs for (let sheet of tailwindRootLeafs) { commonParents.get(sheet).add(sheet) } // If any 2 common parents come from the same tree, then all children of // parent A and parent B will be moved to the parent of parent A and // parent B. Parent A and parent B will be removed. let repeat = true repeat: while (repeat) { repeat = false for (let [sheetA, childrenA] of commonParents) { for (let [sheetB, childrenB] of commonParents) { if (sheetA === sheetB) continue // Ancestors from self to root. Reversed order so we find the // nearest common parent first // // Including self because if you compare a sheet with its parent, // then the parent is still the common sheet between the two. In // this case, the parent is the root file. let ancestorsA = [sheetA].concat(Array.from(sheetA.ancestors()).reverse()) let ancestorsB = [sheetB].concat(Array.from(sheetB.ancestors()).reverse()) for (let parentA of ancestorsA) { for (let parentB of ancestorsB) { if (parentA !== parentB) continue // Found the parent let parent = parentA commonParents.delete(sheetA) commonParents.delete(sheetB) for (let child of childrenA) { commonParents.get(parent).add(child) } for (let child of childrenB) { commonParents.get(parent).add(child) } // Found a common parent between sheet A and sheet B. We can // stop looking for more common parents between A and B, and // continue with the next sheet. repeat = true continue repeat } } } } } // Mark the common parent as the Tailwind CSS root file, and remove the // flag from each leaf. for (let [parent, children] of commonParents) { parent.isTailwindRoot = true for (let child of children) { if (parent === child) continue child.isTailwindRoot = false } } return } } { let error = `You have one or more stylesheets that are imported into a utility layer and non-utility layer.\n` error += `We cannot convert stylesheets under these conditions. Please look at the following stylesheets:\n` throw new Error(error + lines.join('\n')) } }