mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
This PR is an internal refactor of the codemods package structure that will make a few follow-up PRs easier. Essentially what happens is: 1. Moved `./src/template/` into `src/codemods/template/` 2. Moved `./src/codemods` into `./src/codemods/css` (because the CSS related codemods already) 3. Moved the migration files for the JS config, PostCSS config and Prettier config into `./src/codemods/config/`. 4. Made filenames with actual migrations consistent by prefixing them with `migrate-`. 5. Made sure that all the migration functions also use `migrate…` When looking at this PR, go commit by commit, it will be easier. In a lot of cases, it's just moving files around but those commits also come with changes to the code just to update the imports. [ci-all]
162 lines
4.6 KiB
TypeScript
162 lines
4.6 KiB
TypeScript
import postcss, { type AtRule, type ChildNode, type Comment, type Plugin, type Root } from 'postcss'
|
|
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
|
|
import { walk, WalkAction } from '../../utils/walk'
|
|
|
|
const BUCKET_ORDER = [
|
|
// Imports
|
|
'import', // @import
|
|
|
|
// Configuration
|
|
'config', // @config
|
|
'plugin', // @plugin
|
|
'source', // @source
|
|
'custom-variant', // @custom-variant
|
|
'theme', // @theme
|
|
|
|
// Styles
|
|
'compatibility', // @layer base with compatibility CSS
|
|
'utility', // @utility
|
|
|
|
// User CSS
|
|
'user',
|
|
]
|
|
|
|
export function sortBuckets(): Plugin {
|
|
async function migrate(root: Root) {
|
|
// 1. Move items that are not in a bucket, into a bucket
|
|
{
|
|
let comments: Comment[] = []
|
|
|
|
let buckets = new DefaultMap<string, AtRule>((name) => {
|
|
let bucket = postcss.atRule({ name: 'tw-bucket', params: name, nodes: [] })
|
|
root.append(bucket)
|
|
return bucket
|
|
})
|
|
|
|
// Seed the buckets with existing buckets
|
|
root.walkAtRules('tw-bucket', (node) => {
|
|
buckets.set(node.params, node)
|
|
})
|
|
|
|
let lastLayer = 'user'
|
|
function injectInto(name: string, ...nodes: ChildNode[]) {
|
|
lastLayer = name
|
|
buckets.get(name).nodes?.push(...comments.splice(0), ...nodes)
|
|
}
|
|
|
|
walk(root, (node) => {
|
|
// Already in a bucket, skip it
|
|
if (node.type === 'atrule' && node.name === 'tw-bucket') {
|
|
return WalkAction.Skip
|
|
}
|
|
|
|
// Comments belong to the bucket of the nearest node, which is typically
|
|
// in the "next" bucket.
|
|
if (node.type === 'comment') {
|
|
// We already have comments, which means that we already have nodes
|
|
// that belong in the next bucket, so we should move the current
|
|
// comment into the next bucket as well.
|
|
if (comments.length > 0) {
|
|
comments.push(node)
|
|
return
|
|
}
|
|
|
|
// Figure out the closest node to the comment
|
|
let prevDistance = distance(node.prev(), node) ?? Infinity
|
|
let nextDistance = distance(node, node.next()) ?? Infinity
|
|
|
|
if (prevDistance < nextDistance) {
|
|
buckets.get(lastLayer).nodes?.push(node)
|
|
} else {
|
|
comments.push(node)
|
|
}
|
|
}
|
|
|
|
// Known at-rules
|
|
else if (
|
|
node.type === 'atrule' &&
|
|
['config', 'plugin', 'source', 'theme', 'utility', 'custom-variant'].includes(node.name)
|
|
) {
|
|
injectInto(node.name, node)
|
|
}
|
|
|
|
// Imports bucket, which also contains the `@charset` and body-less `@layer`
|
|
else if (
|
|
(node.type === 'atrule' && node.name === 'layer' && !node.nodes) || // @layer foo, bar;
|
|
(node.type === 'atrule' && node.name === 'import') ||
|
|
(node.type === 'atrule' && node.name === 'charset') || // @charset "UTF-8";
|
|
(node.type === 'atrule' && node.name === 'tailwind')
|
|
) {
|
|
injectInto('import', node)
|
|
}
|
|
|
|
// User CSS
|
|
else if (node.type === 'rule' || node.type === 'atrule') {
|
|
injectInto('user', node)
|
|
}
|
|
|
|
// Fallback
|
|
else {
|
|
injectInto('user', node)
|
|
}
|
|
|
|
return WalkAction.Skip
|
|
})
|
|
|
|
if (comments.length > 0) {
|
|
injectInto(lastLayer)
|
|
}
|
|
}
|
|
|
|
// 2. Merge `@tw-bucket` with the same name together
|
|
let firstBuckets = new Map<string, AtRule>()
|
|
root.walkAtRules('tw-bucket', (node) => {
|
|
let firstBucket = firstBuckets.get(node.params)
|
|
if (!firstBucket) {
|
|
firstBuckets.set(node.params, node)
|
|
return
|
|
}
|
|
|
|
if (node.nodes) {
|
|
firstBucket.append(...node.nodes)
|
|
}
|
|
})
|
|
|
|
// 3. Remove empty `@tw-bucket`
|
|
root.walkAtRules('tw-bucket', (node) => {
|
|
if (!node.nodes?.length) {
|
|
node.remove()
|
|
}
|
|
})
|
|
|
|
// 4. Sort the `@tw-bucket` themselves
|
|
{
|
|
let sorted = Array.from(firstBuckets.values()).sort((a, z) => {
|
|
let aIndex = BUCKET_ORDER.indexOf(a.params)
|
|
let zIndex = BUCKET_ORDER.indexOf(z.params)
|
|
return aIndex - zIndex
|
|
})
|
|
|
|
// Re-inject the sorted buckets
|
|
root.removeAll()
|
|
root.append(sorted)
|
|
}
|
|
}
|
|
|
|
return {
|
|
postcssPlugin: '@tailwindcss/upgrade/sort-buckets',
|
|
OnceExit: migrate,
|
|
}
|
|
}
|
|
|
|
function distance(before?: ChildNode, after?: ChildNode): number | null {
|
|
if (!before || !after) return null
|
|
if (!before.source || !after.source) return null
|
|
if (!before.source.start || !after.source.start) return null
|
|
if (!before.source.end || !after.source.end) return null
|
|
|
|
// Compare end of Before, to start of After
|
|
let d = Math.abs(before.source.end.line - after.source.start.line)
|
|
return d
|
|
}
|