-
-
Notifications
You must be signed in to change notification settings - Fork 5k
CSS codemod: inject @import in a more expected location
#14536
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ac5b8d4
42c0f1e
2b8bdcb
00b0cee
5f9943a
2126c38
a7e7b0f
a90e5c5
67ae8b4
866fefb
1c2d678
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,17 @@ | ||
| import { AtRule, type Plugin, type Root } from 'postcss' | ||
| import { AtRule, type ChildNode, type Plugin, type Root } from 'postcss' | ||
|
|
||
| const DEFAULT_LAYER_ORDER = ['theme', 'base', 'components', 'utilities'] | ||
|
|
||
| export function migrateTailwindDirectives(): Plugin { | ||
| function migrate(root: Root) { | ||
| let baseNode: AtRule | null = null | ||
| let utilitiesNode: AtRule | null = null | ||
| let baseNode = null as AtRule | null | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems really weird to me because the behavior between the declarations should be identical 🤔 |
||
| let utilitiesNode = null as AtRule | null | ||
| let orderedNodes: AtRule[] = [] | ||
|
|
||
| let defaultImportNode: AtRule | null = null | ||
| let utilitiesImportNode: AtRule | null = null | ||
| let preflightImportNode: AtRule | null = null | ||
| let themeImportNode: AtRule | null = null | ||
| let defaultImportNode = null as AtRule | null | ||
| let utilitiesImportNode = null as AtRule | null | ||
| let preflightImportNode = null as AtRule | null | ||
| let themeImportNode = null as AtRule | null | ||
|
|
||
| let layerOrder: string[] = [] | ||
|
|
||
|
|
@@ -26,15 +27,15 @@ export function migrateTailwindDirectives(): Plugin { | |
| (node.name === 'import' && node.params.match(/^["']tailwindcss\/base["']$/)) | ||
| ) { | ||
| layerOrder.push('base') | ||
| orderedNodes.push(node) | ||
| baseNode = node | ||
| node.remove() | ||
| } else if ( | ||
| (node.name === 'tailwind' && node.params === 'utilities') || | ||
| (node.name === 'import' && node.params.match(/^["']tailwindcss\/utilities["']$/)) | ||
| ) { | ||
| layerOrder.push('utilities') | ||
| orderedNodes.push(node) | ||
| utilitiesNode = node | ||
| node.remove() | ||
| } | ||
|
|
||
| // Remove directives that are not needed anymore | ||
|
|
@@ -51,24 +52,34 @@ export function migrateTailwindDirectives(): Plugin { | |
| // Insert default import if all directives are present | ||
| if (baseNode !== null && utilitiesNode !== null) { | ||
| if (!defaultImportNode) { | ||
| root.prepend(new AtRule({ name: 'import', params: "'tailwindcss'" })) | ||
| findTargetNode(orderedNodes).before(new AtRule({ name: 'import', params: "'tailwindcss'" })) | ||
| } | ||
| baseNode?.remove() | ||
| utilitiesNode?.remove() | ||
| } | ||
|
|
||
| // Insert individual imports if not all directives are present | ||
| else if (utilitiesNode !== null) { | ||
| if (!utilitiesImportNode) { | ||
| root.prepend( | ||
| findTargetNode(orderedNodes).before( | ||
| new AtRule({ name: 'import', params: "'tailwindcss/utilities' layer(utilities)" }), | ||
| ) | ||
| } | ||
| utilitiesNode?.remove() | ||
| } else if (baseNode !== null) { | ||
| if (!preflightImportNode) { | ||
| root.prepend(new AtRule({ name: 'import', params: "'tailwindcss/preflight' layer(base)" })) | ||
| } | ||
| if (!themeImportNode) { | ||
| root.prepend(new AtRule({ name: 'import', params: "'tailwindcss/theme' layer(theme)" })) | ||
| findTargetNode(orderedNodes).before( | ||
| new AtRule({ name: 'import', params: "'tailwindcss/theme' layer(theme)" }), | ||
| ) | ||
| } | ||
|
|
||
| if (!preflightImportNode) { | ||
| findTargetNode(orderedNodes).before( | ||
| new AtRule({ name: 'import', params: "'tailwindcss/preflight' layer(base)" }), | ||
| ) | ||
| } | ||
|
|
||
| baseNode?.remove() | ||
| } | ||
|
|
||
| // Insert `@layer …;` at the top when the order in the CSS was different | ||
|
|
@@ -94,3 +105,63 @@ export function migrateTailwindDirectives(): Plugin { | |
| OnceExit: migrate, | ||
| } | ||
| } | ||
|
|
||
| // Finds the location where we can inject the new `@import` at-rule. This | ||
| // guarantees that the `@import` is inserted at the most expected location. | ||
| // | ||
| // Ideally it's replacing the existing Tailwind directives, but we have to | ||
| // ensure that the `@import` is valid in this location or not. If not, we move | ||
| // the `@import` up until we find a valid location. | ||
| function findTargetNode(nodes: AtRule[]) { | ||
| // Start at the `base` or `utilities` node (whichever comes first), and find | ||
| // the spot where we can insert the new import. | ||
| let target: ChildNode = nodes.at(0)! | ||
|
|
||
| // Only allowed nodes before the `@import` are: | ||
| // | ||
| // - `@charset` at-rule. | ||
| // - `@layer foo, bar, baz;` at-rule to define the order of the layers. | ||
| // - `@import` at-rule to import other CSS files. | ||
| // - Comments. | ||
| // | ||
| // Nodes that cannot exist before the `@import` are: | ||
| // | ||
| // - Any other at-rule. | ||
| // - Any rule. | ||
| let previous = target.prev() | ||
| while (previous) { | ||
| // Rules are not allowed before the `@import`, so we have to at least inject | ||
| // the `@import` before this rule. | ||
| if (previous.type === 'rule') { | ||
| target = previous | ||
| } | ||
|
|
||
| // Some at-rules are allowed before the `@import`. | ||
| if (previous.type === 'atrule') { | ||
| // `@charset` and `@import` are allowed before the `@import`. | ||
| if (previous.name === 'charset' || previous.name === 'import') { | ||
| // Allowed | ||
| previous = previous.prev() | ||
| continue | ||
| } | ||
|
|
||
| // `@layer` without any nodes is allowed before the `@import`. | ||
| else if (previous.name === 'layer' && !previous.nodes) { | ||
| // Allowed | ||
| previous = previous.prev() | ||
| continue | ||
| } | ||
|
|
||
| // Anything other at-rule (`@media`, `@supports`, etc.) is not allowed | ||
| // before the `@import`. | ||
| else { | ||
| target = previous | ||
| } | ||
| } | ||
|
|
||
| // Keep checking the previous node. | ||
| previous = previous.prev() | ||
| } | ||
|
|
||
| return target | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't add
html {}before these imports because that would be an invalid input.