2024-09-18 16:45:43 +02:00
|
|
|
import path from 'node:path'
|
|
|
|
|
import postcss from 'postcss'
|
2024-10-03 15:35:28 +02:00
|
|
|
import type { Config } from 'tailwindcss'
|
|
|
|
|
import type { DesignSystem } from '../../tailwindcss/src/design-system'
|
2024-10-10 09:44:04 -04:00
|
|
|
import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
|
|
|
|
|
import { segment } from '../../tailwindcss/src/utils/segment'
|
2024-09-18 16:45:43 +02:00
|
|
|
import { migrateAtApply } from './codemods/migrate-at-apply'
|
Add CSS codemods for migrating `@layer utilities` (#14455)
This PR adds CSS codemods for migrating existing `@layer utilities` to
`@utility` directives.
This PR has the ability to migrate the following cases:
---
The most basic case is when you want to migrate a simple class to a
utility directive.
Input:
```css
@layer utilities {
.foo {
color: red;
}
.bar {
color: blue;
}
}
```
Output:
```css
@utility foo {
color: red;
}
@utility bar {
color: blue;
}
```
You'll notice that the class `foo` will be used as the utility name, the
declarations (and the rest of the body of the rule) will become the body
of the `@utility` definition.
---
In v3, every class in a selector will become a utility. To correctly
migrate this to `@utility` directives, we have to register each class in
the selector and generate `n` utilities.
We can use nesting syntax, and replace the current class with `&` to
ensure that the final result behaves the same.
Input:
```css
@layer utilities {
.foo .bar .baz {
color: red;
}
}
```
Output:
```css
@utility foo {
& .bar .baz {
color: red;
}
}
@utility bar {
.foo & .baz {
color: red;
}
}
@utility .baz {
.foo .bar & {
color: red;
}
}
```
In this case, it could be that you know that some of them will never be
used as a utility (e.g.: `hover:bar`), but then you can safely remove
them.
---
Even classes inside of `:has(…)` will become a utility. The only
exception to the rule is that we don't do it for `:not(…)`.
Input:
```css
@layer utilities {
.foo .bar:not(.qux):has(.baz) {
display: none;
}
}
```
Output:
```css
@utility foo {
& .bar:not(.qux):has(.baz) {
display: none;
}
}
@utility bar {
.foo &:not(.qux):has(.baz) {
display: none;
}
}
@utility baz {
.foo .bar:not(.qux):has(&) {
display: none;
}
}
```
Notice that there is no `@utility qux` because it was used inside of
`:not(…)`.
---
When classes are nested inside at-rules, then these classes will also
become utilities. However, the `@utility <name>` will be at the top and
the at-rules will live inside of it. If there are multiple classes
inside a shared at-rule, then the at-rule will be duplicated for each
class.
Let's look at an example to make it more clear:
Input:
```css
@layer utilities {
@media (min-width: 640px) {
.foo {
color: red;
}
.bar {
color: blue;
}
@media (min-width: 1024px) {
.baz {
color: green;
}
@media (min-width: 1280px) {
.qux {
color: yellow;
}
}
}
}
}
```
Output:
```css
@utility foo {
@media (min-width: 640px) {
color: red;
}
}
@utility bar {
@media (min-width: 640px) {
color: blue;
}
}
@utility baz {
@media (min-width: 640px) {
@media (min-width: 1024px) {
color: green;
}
}
}
@utility qux {
@media (min-width: 640px) {
@media (min-width: 1024px) {
@media (min-width: 1280px) {
color: yellow;
}
}
}
}
```
---
When classes result in multiple `@utility` directives with the same
name, then the definitions will be merged together.
Input:
```css
@layer utilities {
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
}
```
Intermediate representation:
```css
@utility no-scrollbar {
&::-webkit-scrollbar {
display: none;
}
}
@utility no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
```
Output:
```css
@utility no-scrollbar {
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none
}
```
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2024-09-24 18:17:09 +02:00
|
|
|
import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities'
|
2024-10-10 15:06:53 +02:00
|
|
|
import { migrateMediaScreen } from './codemods/migrate-media-screen'
|
2024-09-24 18:32:50 +02:00
|
|
|
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
|
2024-09-18 22:40:23 +02:00
|
|
|
import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives'
|
2024-10-10 09:44:04 -04:00
|
|
|
import { Stylesheet, type StylesheetConnection, type StylesheetId } from './stylesheet'
|
|
|
|
|
import { resolveCssId } from './utils/resolve'
|
|
|
|
|
import { walk, WalkAction } from './utils/walk'
|
2024-09-18 16:45:43 +02:00
|
|
|
|
Template migrations: Migrate v3 prefixes to v4 (#14557)
This PR adds a new migration that can migrate Tailwind CSS v3 style
prefixes into Tailwind CSS v4.
The migration is split into three separate pieces of work:
1. Firstly, we need to read the full JavaScript config to get the _old_
prefix option. This is necessary because in v4, we will not allow things
like custom-separators for the prefix. From this option we will then try
and compute a new prefix (in 90% of the cases this is going to just
remove the trailing `-` but it can also work in more complex cases).
2. Then we migrate all Candidates. The important thing here is that we
need to operate on the raw candidate string because by relying on
`parseCandidate` (which we do for all other migrations) would not work,
as the candidates are not valid in v4 syntax. More on that in a bit.
3. Lastly we also make sure to update the CSS config to include the new
prefix. This is done by prepending the prefix option like so:
```css
@import "tailwindcss" prefix(tw);
```
### Migrating candidates
The main difference between v3 prefixes and v4 prefixes is that in v3,
the prefix was _part of the utility_ where as in v4 it is _always in
front of the CSS class.
So, for example, this candidate in v3:
```
hover:-tw-mr-4
```
Would be converted to the following in v4:
```
tw:hover:-mr-4
```
Since the first example _won't parse as a valid Candidate in v4, as the
`tw-mr` utility does not exist, we have to operate on the raw candidate
string first. To do this I created a fork of the `parseCandidate`
function _without any validation of utilities or variants_. This is used
to identify part of the candidate that is the `base` and then ensuring
the `base` starts with the old prefix. We then remove this to create an
"unprefixed" candidate that we validate against a version of the
DesignSystem _with no prefixes configured_. If the variant is valid this
way, we can then print it again with the `DesignSystem` that has the new
prefix to get the migrated version.
Since we set up the `DesignSystem` to include the new prefix, we can
also be certain that migrations that happen afterwards would still
disqualify candidates that aren't valid according to the new prefix
policy. This does mean we need to have the prefix fixup be the first
step in our pipeline.
One interesting bit is that in v3, arbitrary properties did not require
prefixes where as in v4 they do. So the following candidate:
```
[color:red]
```
Will be converted to:
```
tw:[color:red]
```
2024-10-01 18:04:08 +02:00
|
|
|
export interface MigrateOptions {
|
2024-10-07 18:02:28 +02:00
|
|
|
newPrefix: string | null
|
|
|
|
|
designSystem: DesignSystem
|
|
|
|
|
userConfig: Config
|
Template migrations: Migrate v3 prefixes to v4 (#14557)
This PR adds a new migration that can migrate Tailwind CSS v3 style
prefixes into Tailwind CSS v4.
The migration is split into three separate pieces of work:
1. Firstly, we need to read the full JavaScript config to get the _old_
prefix option. This is necessary because in v4, we will not allow things
like custom-separators for the prefix. From this option we will then try
and compute a new prefix (in 90% of the cases this is going to just
remove the trailing `-` but it can also work in more complex cases).
2. Then we migrate all Candidates. The important thing here is that we
need to operate on the raw candidate string because by relying on
`parseCandidate` (which we do for all other migrations) would not work,
as the candidates are not valid in v4 syntax. More on that in a bit.
3. Lastly we also make sure to update the CSS config to include the new
prefix. This is done by prepending the prefix option like so:
```css
@import "tailwindcss" prefix(tw);
```
### Migrating candidates
The main difference between v3 prefixes and v4 prefixes is that in v3,
the prefix was _part of the utility_ where as in v4 it is _always in
front of the CSS class.
So, for example, this candidate in v3:
```
hover:-tw-mr-4
```
Would be converted to the following in v4:
```
tw:hover:-mr-4
```
Since the first example _won't parse as a valid Candidate in v4, as the
`tw-mr` utility does not exist, we have to operate on the raw candidate
string first. To do this I created a fork of the `parseCandidate`
function _without any validation of utilities or variants_. This is used
to identify part of the candidate that is the `base` and then ensuring
the `base` starts with the old prefix. We then remove this to create an
"unprefixed" candidate that we validate against a version of the
DesignSystem _with no prefixes configured_. If the variant is valid this
way, we can then print it again with the `DesignSystem` that has the new
prefix to get the migrated version.
Since we set up the `DesignSystem` to include the new prefix, we can
also be certain that migrations that happen afterwards would still
disqualify candidates that aren't valid according to the new prefix
policy. This does mean we need to have the prefix fixup be the first
step in our pipeline.
One interesting bit is that in v3, arbitrary properties did not require
prefixes where as in v4 they do. So the following candidate:
```
[color:red]
```
Will be converted to:
```
tw:[color:red]
```
2024-10-01 18:04:08 +02:00
|
|
|
}
|
|
|
|
|
|
2024-10-10 09:44:04 -04:00
|
|
|
export async function migrateContents(
|
|
|
|
|
stylesheet: Stylesheet | string,
|
|
|
|
|
options: MigrateOptions,
|
|
|
|
|
file?: string,
|
|
|
|
|
) {
|
|
|
|
|
if (typeof stylesheet === 'string') {
|
|
|
|
|
stylesheet = await Stylesheet.fromString(stylesheet)
|
|
|
|
|
stylesheet.file = file ?? null
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-18 16:45:43 +02:00
|
|
|
return postcss()
|
2024-10-03 15:35:28 +02:00
|
|
|
.use(migrateAtApply(options))
|
2024-10-10 15:06:53 +02:00
|
|
|
.use(migrateMediaScreen(options))
|
2024-10-10 09:44:04 -04:00
|
|
|
.use(migrateAtLayerUtilities(stylesheet))
|
2024-09-24 18:32:50 +02:00
|
|
|
.use(migrateMissingLayers())
|
Template migrations: Migrate v3 prefixes to v4 (#14557)
This PR adds a new migration that can migrate Tailwind CSS v3 style
prefixes into Tailwind CSS v4.
The migration is split into three separate pieces of work:
1. Firstly, we need to read the full JavaScript config to get the _old_
prefix option. This is necessary because in v4, we will not allow things
like custom-separators for the prefix. From this option we will then try
and compute a new prefix (in 90% of the cases this is going to just
remove the trailing `-` but it can also work in more complex cases).
2. Then we migrate all Candidates. The important thing here is that we
need to operate on the raw candidate string because by relying on
`parseCandidate` (which we do for all other migrations) would not work,
as the candidates are not valid in v4 syntax. More on that in a bit.
3. Lastly we also make sure to update the CSS config to include the new
prefix. This is done by prepending the prefix option like so:
```css
@import "tailwindcss" prefix(tw);
```
### Migrating candidates
The main difference between v3 prefixes and v4 prefixes is that in v3,
the prefix was _part of the utility_ where as in v4 it is _always in
front of the CSS class.
So, for example, this candidate in v3:
```
hover:-tw-mr-4
```
Would be converted to the following in v4:
```
tw:hover:-mr-4
```
Since the first example _won't parse as a valid Candidate in v4, as the
`tw-mr` utility does not exist, we have to operate on the raw candidate
string first. To do this I created a fork of the `parseCandidate`
function _without any validation of utilities or variants_. This is used
to identify part of the candidate that is the `base` and then ensuring
the `base` starts with the old prefix. We then remove this to create an
"unprefixed" candidate that we validate against a version of the
DesignSystem _with no prefixes configured_. If the variant is valid this
way, we can then print it again with the `DesignSystem` that has the new
prefix to get the migrated version.
Since we set up the `DesignSystem` to include the new prefix, we can
also be certain that migrations that happen afterwards would still
disqualify candidates that aren't valid according to the new prefix
policy. This does mean we need to have the prefix fixup be the first
step in our pipeline.
One interesting bit is that in v3, arbitrary properties did not require
prefixes where as in v4 they do. So the following candidate:
```
[color:red]
```
Will be converted to:
```
tw:[color:red]
```
2024-10-01 18:04:08 +02:00
|
|
|
.use(migrateTailwindDirectives(options))
|
2024-10-10 09:44:04 -04:00
|
|
|
.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)
|
2024-09-18 16:45:43 +02:00
|
|
|
}
|
|
|
|
|
|
2024-10-10 09:44:04 -04:00
|
|
|
export async function analyze(stylesheets: Stylesheet[]) {
|
|
|
|
|
let stylesheetsByFile = new Map<string, Stylesheet>()
|
|
|
|
|
|
|
|
|
|
for (let sheet of stylesheets) {
|
|
|
|
|
if (sheet.file) {
|
|
|
|
|
stylesheetsByFile.set(sheet.file, sheet)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
try {
|
|
|
|
|
resolvedPath = resolveCssId(id, basePath)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn(`Failed to resolve import: ${id}. Skipping.`)
|
|
|
|
|
console.error(err)
|
|
|
|
|
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 })
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
for (let sheet of stylesheets) {
|
|
|
|
|
if (!sheet.file) continue
|
|
|
|
|
|
|
|
|
|
await processor.process(sheet.root, { from: sheet.file })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 { convertablePaths, nonConvertablePaths } = sheet.analyzeImportPaths()
|
|
|
|
|
let isAmbiguous = convertablePaths.length > 0 && nonConvertablePaths.length > 0
|
|
|
|
|
|
|
|
|
|
if (!isAmbiguous) continue
|
|
|
|
|
|
|
|
|
|
sheet.canMigrate = false
|
|
|
|
|
|
|
|
|
|
let filePath = sheet.file.replace(commonPath, '')
|
|
|
|
|
|
|
|
|
|
for (let path of convertablePaths) {
|
|
|
|
|
lines.push(`- ${filePath} <- ${pathToString(path)}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let path of nonConvertablePaths) {
|
|
|
|
|
lines.push(`- ${filePath} <- ${pathToString(path)}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lines.length === 0) 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 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 `@utillity` rules
|
|
|
|
|
let containsUtilities = new Set<Stylesheet>()
|
|
|
|
|
|
|
|
|
|
for (let sheet of stylesheets) {
|
|
|
|
|
let layers = sheet.layers()
|
|
|
|
|
let isLayered = layers.has('utilities') || layers.has('components')
|
|
|
|
|
if (!isLayered) continue
|
|
|
|
|
|
|
|
|
|
walk(sheet.root, (node) => {
|
|
|
|
|
if (node.type !== 'atrule') return
|
|
|
|
|
if (node.name !== 'utility') return
|
|
|
|
|
|
|
|
|
|
containsUtilities.add(sheet)
|
|
|
|
|
|
|
|
|
|
return WalkAction.Stop
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 (!containsUtilities.has(sheet)) {
|
|
|
|
|
if (!Array.from(sheet.descendants()).some((child) => containsUtilities.has(child))) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let utilities = postcss.root({
|
|
|
|
|
raws: {
|
|
|
|
|
tailwind_pretty: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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: {
|
|
|
|
|
after: '\n\n',
|
|
|
|
|
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)
|
|
|
|
|
}
|
2024-09-18 16:45:43 +02:00
|
|
|
|
2024-10-10 09:44:04 -04:00
|
|
|
stylesheets.push(...utilitySheets.values())
|
2024-09-18 16:45:43 +02:00
|
|
|
}
|