mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2026-01-25 16:44:12 +00:00
While testing the latest alpha release across Tailwind v3 projects, we
noticed one regression in relation to the default color of `<button>`
elements. In v3, the reset would change the default to `inherit` but in
v4 we would _not include it in the reset snippet inserted by the upgrade
too_.
This PR changes the upgrade snippet to include it:
```diff
/*
In Tailwind CSS v4, basic styles are applied to form elements by default. To
maintain compatibility with v3, the following resets have been added:
*/
@layer base {
input,
textarea,
select,
button {
border: 0px solid;
border-radius: 0;
padding: 0;
+ color: inherit;
background-color: transparent;
}
}
```
This PR also ensures that there's a newline between the two code
snippets.
## Test Plan
### Before

### After
<img width="1354" alt="Screenshot 2024-11-21 at 15 42 58"
src="https://github.com/user-attachments/assets/9a4503fe-683f-4d08-abf2-7dd111ed5428">
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('.')) {
|
|
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())
|
|
}
|