mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Closes #15220 This PR fixes an issue where the upgrade tool would not be able to load some JavaScript config files across different drive letters on Windows. The issue in detail is that `path.relative(…)` tries to build a relative path but if the file is inside the same folder, it won't start the relative path with a `./` so we manually appended it in case that it isn't there. The issue on Windows specifically is that `file.relative(…)` can also return a legit absolute path, e.g. when the file is on a different drive. In this case we obviously don't want to prefix a path with `./`. ## Test plan To reproduce this issue, I checked out a Tailwind v3 project _on a different drive letter than my Windows installation_. In that case, I was adding a repo inside `D:` while `npm` was installed in `C:`. I then run `npx @tailwindcss/upgrade` to reproduce the issue. The fix was validated with a local `bun` run of the upgrade tool: 
720 lines
24 KiB
TypeScript
720 lines
24 KiB
TypeScript
import { normalizePath } from '@tailwindcss/node'
|
|
import { isGitIgnored } from 'globby'
|
|
import path from 'node:path'
|
|
import postcss, { type Result } from 'postcss'
|
|
import type { Config } from '../../tailwindcss/src/compat/plugin-api'
|
|
import type { DesignSystem } from '../../tailwindcss/src/design-system'
|
|
import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
|
|
import { segment } from '../../tailwindcss/src/utils/segment'
|
|
import { migrateAtApply } from './codemods/migrate-at-apply'
|
|
import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities'
|
|
import { migrateConfig } from './codemods/migrate-config'
|
|
import { migrateImport } from './codemods/migrate-import'
|
|
import { migrateMediaScreen } from './codemods/migrate-media-screen'
|
|
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
|
|
import { migratePreflight } from './codemods/migrate-preflight'
|
|
import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives'
|
|
import { migrateThemeToVar } from './codemods/migrate-theme-to-var'
|
|
import { migrateVariantsDirective } from './codemods/migrate-variants-directive'
|
|
import type { JSConfigMigration } from './migrate-js-config'
|
|
import { Stylesheet, type StylesheetConnection, type StylesheetId } from './stylesheet'
|
|
import { detectConfigPath } from './template/prepare-config'
|
|
import { error, highlight, relative, success } from './utils/renderer'
|
|
import { resolveCssId } from './utils/resolve'
|
|
import { walk, WalkAction } from './utils/walk'
|
|
|
|
export interface MigrateOptions {
|
|
newPrefix: string | null
|
|
designSystem: DesignSystem
|
|
userConfig: Config
|
|
configFilePath: string
|
|
jsConfigMigration: JSConfigMigration
|
|
}
|
|
|
|
export async function migrateContents(
|
|
stylesheet: Stylesheet | string,
|
|
options: MigrateOptions,
|
|
file?: string,
|
|
) {
|
|
if (typeof stylesheet === 'string') {
|
|
stylesheet = await Stylesheet.fromString(stylesheet)
|
|
stylesheet.file = file ?? null
|
|
}
|
|
|
|
return postcss()
|
|
.use(migrateImport())
|
|
.use(migrateAtApply(options))
|
|
.use(migrateMediaScreen(options))
|
|
.use(migrateVariantsDirective())
|
|
.use(migrateAtLayerUtilities(stylesheet))
|
|
.use(migrateMissingLayers())
|
|
.use(migrateTailwindDirectives(options))
|
|
.use(migrateConfig(stylesheet, options))
|
|
.use(migratePreflight(options))
|
|
.use(migrateThemeToVar(options))
|
|
.process(stylesheet.root, { from: stylesheet.file ?? undefined })
|
|
}
|
|
|
|
export async function migrate(stylesheet: Stylesheet, options: MigrateOptions) {
|
|
if (!stylesheet.file) {
|
|
throw new Error('Cannot migrate a stylesheet without a file path')
|
|
}
|
|
|
|
if (!stylesheet.canMigrate) return
|
|
|
|
await migrateContents(stylesheet, options)
|
|
}
|
|
|
|
export async function analyze(stylesheets: Stylesheet[]) {
|
|
let isIgnored = await isGitIgnored()
|
|
let processingQueue: (() => Promise<Result>)[] = []
|
|
let stylesheetsByFile = new DefaultMap<string, Stylesheet | null>((file) => {
|
|
// We don't want to process ignored files (like node_modules)
|
|
if (isIgnored(file)) {
|
|
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'))
|
|
}
|
|
}
|
|
|
|
export async function linkConfigs(
|
|
stylesheets: Stylesheet[],
|
|
{ configPath, base }: { configPath: string | null; base: string },
|
|
) {
|
|
let rootStylesheets = stylesheets.filter((sheet) => sheet.isTailwindRoot)
|
|
if (rootStylesheets.length === 0) {
|
|
throw new Error(
|
|
`Cannot find any CSS files that reference Tailwind CSS.\nBefore your project can be upgraded you need to create a CSS file that imports Tailwind CSS or uses ${highlight('@tailwind')}.`,
|
|
)
|
|
}
|
|
let withoutAtConfig = rootStylesheets.filter((sheet) => {
|
|
let hasConfig = false
|
|
sheet.root.walkAtRules('config', (node) => {
|
|
let configPath = path.resolve(path.dirname(sheet.file!), node.params.slice(1, -1))
|
|
sheet.linkedConfigPath = configPath
|
|
hasConfig = true
|
|
return false
|
|
})
|
|
return !hasConfig
|
|
})
|
|
|
|
// All stylesheets have a `@config` directives
|
|
if (withoutAtConfig.length === 0) return
|
|
|
|
// Find the config file path for each stylesheet
|
|
let configPathBySheet = new Map<Stylesheet, string>()
|
|
let sheetByConfigPath = new DefaultMap<string, Set<Stylesheet>>(() => new Set())
|
|
for (let sheet of withoutAtConfig) {
|
|
if (!sheet.file) continue
|
|
|
|
let localConfigPath = configPath as string
|
|
if (configPath === null) {
|
|
localConfigPath = await detectConfigPath(path.dirname(sheet.file), base)
|
|
} else if (!path.isAbsolute(localConfigPath)) {
|
|
localConfigPath = path.resolve(base, localConfigPath)
|
|
}
|
|
|
|
configPathBySheet.set(sheet, localConfigPath)
|
|
sheetByConfigPath.get(localConfigPath).add(sheet)
|
|
}
|
|
|
|
let problematicStylesheets = new Set<Stylesheet>()
|
|
for (let sheets of sheetByConfigPath.values()) {
|
|
if (sheets.size > 1) {
|
|
for (let sheet of sheets) {
|
|
problematicStylesheets.add(sheet)
|
|
}
|
|
}
|
|
}
|
|
|
|
// There are multiple "root" files without `@config` directives. Manual
|
|
// intervention is needed to link to the correct Tailwind config files.
|
|
if (problematicStylesheets.size > 1) {
|
|
for (let sheet of problematicStylesheets) {
|
|
error(
|
|
`Could not determine configuration file for: ${highlight(relative(sheet.file!, base))}\nUpdate your stylesheet to use ${highlight('@config')} to specify the correct configuration file explicitly and then run the upgrade tool again.`,
|
|
{ prefix: '↳ ' },
|
|
)
|
|
}
|
|
|
|
process.exit(1)
|
|
}
|
|
|
|
let relativePath = relative
|
|
for (let [sheet, configPath] of configPathBySheet) {
|
|
try {
|
|
if (!sheet || !sheet.file) return
|
|
success(
|
|
`Linked ${highlight(relativePath(configPath, base))} to ${highlight(relativePath(sheet.file, base))}`,
|
|
{ prefix: '↳ ' },
|
|
)
|
|
|
|
// Link the `@config` directive to the root stylesheets
|
|
|
|
// Track the config file path on the stylesheet itself for easy access
|
|
// without traversing the CSS ast and finding the corresponding
|
|
// `@config` later.
|
|
sheet.linkedConfigPath = configPath
|
|
|
|
// Create a relative path from the current file to the config file.
|
|
let relative = path.relative(path.dirname(sheet.file), configPath)
|
|
|
|
// 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('.') && !path.isAbsolute(relative)) {
|
|
relative = './' + relative
|
|
}
|
|
|
|
relative = normalizePath(relative)
|
|
|
|
// Add the `@config` directive to the root stylesheet.
|
|
{
|
|
let target = sheet.root as postcss.Root | postcss.AtRule
|
|
let atConfig = postcss.atRule({ name: 'config', params: `'${relative}'` })
|
|
|
|
sheet.root.walkAtRules((node) => {
|
|
if (node.name === 'tailwind' || node.name === 'import') {
|
|
target = node
|
|
}
|
|
})
|
|
|
|
if (target.type === 'root') {
|
|
sheet.root.prepend(atConfig)
|
|
} else if (target.type === 'atrule') {
|
|
target.after(atConfig)
|
|
}
|
|
}
|
|
} catch (e: any) {
|
|
error('Could not load the configuration file: ' + e.message, { prefix: '↳ ' })
|
|
process.exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function split(stylesheets: Stylesheet[]) {
|
|
let stylesheetsById = new Map<StylesheetId, Stylesheet>()
|
|
let stylesheetsByFile = new Map<string, Stylesheet>()
|
|
|
|
for (let sheet of stylesheets) {
|
|
stylesheetsById.set(sheet.id, sheet)
|
|
|
|
if (sheet.file) {
|
|
stylesheetsByFile.set(sheet.file, sheet)
|
|
}
|
|
}
|
|
|
|
// Keep track of sheets that contain `@utility` rules
|
|
let requiresSplit = new Set<Stylesheet>()
|
|
|
|
for (let sheet of stylesheets) {
|
|
// Root files don't need to be split
|
|
if (sheet.isTailwindRoot) continue
|
|
|
|
let containsUtility = false
|
|
let containsUnsafe = sheet.layers().size > 0
|
|
|
|
walk(sheet.root, (node) => {
|
|
if (node.type === 'atrule' && node.name === 'utility') {
|
|
containsUtility = true
|
|
}
|
|
|
|
// Safe to keep without splitting
|
|
else if (
|
|
// An `@import "…" layer(…)` is safe
|
|
(node.type === 'atrule' && node.name === 'import' && node.params.includes('layer(')) ||
|
|
// @layer blocks are safe
|
|
(node.type === 'atrule' && node.name === 'layer') ||
|
|
// Comments are safe
|
|
node.type === 'comment'
|
|
) {
|
|
return WalkAction.Skip
|
|
}
|
|
|
|
// Everything else is not safe, and requires a split
|
|
else {
|
|
containsUnsafe = true
|
|
}
|
|
|
|
// We already know we need to split this sheet
|
|
if (containsUtility && containsUnsafe) {
|
|
return WalkAction.Stop
|
|
}
|
|
|
|
return WalkAction.Skip
|
|
})
|
|
|
|
if (containsUtility && containsUnsafe) {
|
|
requiresSplit.add(sheet)
|
|
}
|
|
}
|
|
|
|
// Split every imported stylesheet into two parts
|
|
let utilitySheets = new Map<Stylesheet, Stylesheet>()
|
|
|
|
for (let sheet of stylesheets) {
|
|
// Ignore stylesheets that were not imported
|
|
if (!sheet.file) continue
|
|
if (sheet.parents.size === 0) continue
|
|
|
|
// Skip stylesheets that don't have utilities
|
|
// and don't have any children that have utilities
|
|
if (!requiresSplit.has(sheet)) {
|
|
if (!Array.from(sheet.descendants()).some((child) => requiresSplit.has(child))) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
let utilities = postcss.root()
|
|
|
|
walk(sheet.root, (node) => {
|
|
if (node.type !== 'atrule') return
|
|
if (node.name !== 'utility') return
|
|
|
|
// `append` will move this node from the original sheet
|
|
// to the new utilities sheet
|
|
utilities.append(node)
|
|
|
|
return WalkAction.Skip
|
|
})
|
|
|
|
let newFileName = sheet.file.replace(/\.css$/, '.utilities.css')
|
|
|
|
let counter = 0
|
|
|
|
// If we already have a utility sheet with this name, we need to rename it
|
|
while (stylesheetsByFile.has(newFileName)) {
|
|
counter += 1
|
|
newFileName = sheet.file.replace(/\.css$/, `.utilities.${counter}.css`)
|
|
}
|
|
|
|
let utilitySheet = await Stylesheet.fromRoot(utilities, newFileName)
|
|
|
|
utilitySheet.extension = counter > 0 ? `.utilities.${counter}.css` : `.utilities.css`
|
|
|
|
utilitySheets.set(sheet, utilitySheet)
|
|
stylesheetsById.set(utilitySheet.id, utilitySheet)
|
|
}
|
|
|
|
// Make sure the utility sheets are linked to one another
|
|
for (let [normalSheet, utilitySheet] of utilitySheets) {
|
|
for (let parent of normalSheet.parents) {
|
|
let utilityParent = utilitySheets.get(parent.item)
|
|
if (!utilityParent) continue
|
|
utilitySheet.parents.add({
|
|
item: utilityParent,
|
|
meta: parent.meta,
|
|
})
|
|
}
|
|
|
|
for (let child of normalSheet.children) {
|
|
let utilityChild = utilitySheets.get(child.item)
|
|
if (!utilityChild) continue
|
|
utilitySheet.children.add({
|
|
item: utilityChild,
|
|
meta: child.meta,
|
|
})
|
|
}
|
|
}
|
|
|
|
for (let sheet of stylesheets) {
|
|
let utilitySheet = utilitySheets.get(sheet)
|
|
let utilityImports: Set<postcss.AtRule> = new Set()
|
|
|
|
for (let node of sheet.importRules) {
|
|
let sheetId = node.raws.tailwind_destination_sheet_id as StylesheetId | undefined
|
|
|
|
// This import rule does not point to a stylesheet
|
|
// which likely means it points to `node_modules`
|
|
if (!sheetId) continue
|
|
|
|
let originalDestination = stylesheetsById.get(sheetId)
|
|
|
|
// This import points to a stylesheet that no longer exists which likely
|
|
// means it was removed by the optimizer this will be cleaned up later
|
|
if (!originalDestination) continue
|
|
|
|
let utilityDestination = utilitySheets.get(originalDestination)
|
|
|
|
// A utility sheet doesn't exist for this import so it doesn't need
|
|
// to be processed
|
|
if (!utilityDestination) continue
|
|
|
|
let match = node.params.match(/(['"])(.*)\1/)
|
|
if (!match) return
|
|
|
|
let quote = match[1]
|
|
let id = match[2]
|
|
|
|
let newFile = id.replace(/\.css$/, utilityDestination.extension!)
|
|
|
|
// The import will just point to the new file without any media queries,
|
|
// layers, or other conditions because `@utility` MUST be top-level.
|
|
let newImport = node.clone({
|
|
params: `${quote}${newFile}${quote}`,
|
|
raws: {
|
|
tailwind_injected_layer: node.raws.tailwind_injected_layer,
|
|
tailwind_original_params: `${quote}${id}${quote}`,
|
|
tailwind_destination_sheet_id: utilityDestination.id,
|
|
},
|
|
})
|
|
|
|
if (utilitySheet) {
|
|
// If this import is intended to go into the utility sheet
|
|
// we'll collect it into a list to add later. If we don't'
|
|
// we'll end up adding them in reverse order.
|
|
utilityImports.add(newImport)
|
|
} else {
|
|
// This import will go immediately after the original import
|
|
node.after(newImport)
|
|
}
|
|
}
|
|
|
|
// Add imports to the top of the utility sheet if necessary
|
|
if (utilitySheet && utilityImports.size > 0) {
|
|
utilitySheet.root.prepend(Array.from(utilityImports))
|
|
}
|
|
}
|
|
|
|
// Tracks the at rules that import a given stylesheet
|
|
let importNodes = new DefaultMap<Stylesheet, Set<postcss.AtRule>>(() => new Set())
|
|
|
|
for (let sheet of stylesheetsById.values()) {
|
|
for (let node of sheet.importRules) {
|
|
let sheetId = node.raws.tailwind_destination_sheet_id as StylesheetId | undefined
|
|
|
|
// This import rule does not point to a stylesheet
|
|
if (!sheetId) continue
|
|
|
|
let destination = stylesheetsById.get(sheetId)
|
|
|
|
// This import rule does not point to a stylesheet that exists
|
|
// We'll remove it later
|
|
if (!destination) continue
|
|
|
|
importNodes.get(destination).add(node)
|
|
}
|
|
}
|
|
|
|
// At this point we've created many `{name}.utilities.css` files.
|
|
// If the original file _becomes_ empty after splitting that means that
|
|
// dedicated utility file is not required and we can move the utilities
|
|
// back to the original file.
|
|
//
|
|
// This could be done in one step but separating them makes it easier to
|
|
// reason about since the stylesheets are in a consistent state before we
|
|
// perform any cleanup tasks.
|
|
let list: Stylesheet[] = []
|
|
|
|
for (let sheet of stylesheets.slice()) {
|
|
for (let child of sheet.descendants()) {
|
|
list.push(child)
|
|
}
|
|
|
|
list.push(sheet)
|
|
}
|
|
|
|
for (let sheet of list) {
|
|
let utilitySheet = utilitySheets.get(sheet)
|
|
|
|
// This sheet was not split so there's nothing to do
|
|
if (!utilitySheet) continue
|
|
|
|
// This sheet did not become empty
|
|
if (!sheet.isEmpty) continue
|
|
|
|
// We have a sheet that became empty after splitting
|
|
// 1. Replace the sheet with it's utility sheet content
|
|
sheet.root = utilitySheet.root
|
|
|
|
// 2. Rewrite imports in parent sheets to point to the original sheet
|
|
// Ideally this wouldn't need to be _undone_ but instead only done once at the end
|
|
for (let node of importNodes.get(utilitySheet)) {
|
|
node.params = node.raws.tailwind_original_params as any
|
|
}
|
|
|
|
// 3. Remove the original import from the non-utility sheet
|
|
for (let node of importNodes.get(sheet)) {
|
|
node.remove()
|
|
}
|
|
|
|
// 3. Mark the utility sheet for removal
|
|
utilitySheets.delete(sheet)
|
|
}
|
|
|
|
stylesheets.push(...utilitySheets.values())
|
|
}
|