Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Upgrade (experimental)_: Minify arbitrary values when printing candidates ([#14720](https://github.com/tailwindlabs/tailwindcss/pull/14720))
- _Upgrade (experimental)_: Ensure legacy theme values ending in `1` (like `theme(spacing.1)`) are correctly migrated to custom properties ([#14724](https://github.com/tailwindlabs/tailwindcss/pull/14724))
- _Upgrade (experimental)_: Migrate arbitrary values to bare values for the `from-*`, `via-*`, and `to-*` utilities ([#14725](https://github.com/tailwindlabs/tailwindcss/pull/14725))
- _Upgrade (experimental)_: Ensure `layer(utilities)` is removed from `@import` to keep `@utility` top-level ([#14738](https://github.com/tailwindlabs/tailwindcss/pull/14738))

### Changed

Expand Down
56 changes: 55 additions & 1 deletion integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,60 @@ test(
},
)

test(
'migrate utilities in an imported file and keep @utility top-level',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.js': js`module.exports = {}`,
'src/index.css': css`
@import 'tailwindcss/utilities';
@import './utilities.css';
@import 'tailwindcss/components';
`,
'src/utilities.css': css`
@layer utilities {
.no-scrollbar::-webkit-scrollbar {
display: none;
}

.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
}
`,
},
},
async ({ fs, exec }) => {
await exec('npx @tailwindcss/upgrade --force')

expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
"
--- ./src/index.css ---
@import 'tailwindcss/utilities' layer(utilities);
@import './utilities.css';

--- ./src/utilities.css ---
@utility no-scrollbar {
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none;
}
"
`)
},
)

test(
'migrate utilities in deep import trees',
{
Expand Down Expand Up @@ -737,7 +791,7 @@ test(
@import './a.1.css' layer(utilities);
@import './a.1.utilities.1.css';
@import './b.1.css';
@import './c.1.css' layer(utilities);
@import './c.1.css';
@import './c.1.utilities.css';
@import './d.1.css';

Expand Down
41 changes: 41 additions & 0 deletions packages/@tailwindcss-upgrade/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,47 @@ async function run() {
error(`${e}`)
}

// Cleanup `@import "…" layer(utilities)`
for (let sheet of stylesheets) {
// If the `@import` contains an injected `layer(…)` we need to remove it
if (!Array.from(sheet.importRules).some((node) => node.raws.tailwind_injected_layer)) {
continue
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can also skip stylesheets with parent stylesheets yeah?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah wait not if the tailwind stuff is in a non-root stylesheet so maybe not.

let hasAtUtility = false

// Only remove the `layer(…)` next to the import, if any of the children
// contains an `@utility`. Otherwise the `@utility` will not be top-level.
{
sheet.root.walkAtRules('utility', () => {
hasAtUtility = true
return false
})

if (!hasAtUtility) {
for (let child of sheet.descendants()) {
child.root.walkAtRules('utility', () => {
hasAtUtility = true
return false
})

if (hasAtUtility) {
break
}
}
}
}

// No `@utility` found, we can keep the `layer(…)` next to the import
if (!hasAtUtility) continue

for (let importNode of sheet.importRules) {
if (importNode.raws.tailwind_injected_layer) {
importNode.params = importNode.params.replace(/ layer\([^)]+\)/, '').trim()
}
}
}

// Format nodes
for (let sheet of stylesheets) {
await postcss([formatNodes()]).process(sheet.root!, { from: sheet.file! })
Expand Down
11 changes: 6 additions & 5 deletions packages/@tailwindcss-upgrade/src/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,20 +159,20 @@ export async function analyze(stylesheets: Stylesheet[]) {
for (let sheet of stylesheets) {
if (!sheet.file) continue

let { convertablePaths, nonConvertablePaths } = sheet.analyzeImportPaths()
let isAmbiguous = convertablePaths.length > 0 && nonConvertablePaths.length > 0
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 convertablePaths) {
for (let path of convertiblePaths) {
lines.push(`- ${filePath} <- ${pathToString(path)}`)
}

for (let path of nonConvertablePaths) {
for (let path of nonConvertiblePaths) {
lines.push(`- ${filePath} <- ${pathToString(path)}`)
}
}
Expand All @@ -197,7 +197,7 @@ export async function split(stylesheets: Stylesheet[]) {
}
}

// Keep track of sheets that contain `@utillity` rules
// Keep track of sheets that contain `@utility` rules
let containsUtilities = new Set<Stylesheet>()

for (let sheet of stylesheets) {
Expand Down Expand Up @@ -324,6 +324,7 @@ export async function split(stylesheets: Stylesheet[]) {
params: `${quote}${newFile}${quote}`,
raws: {
after: '\n\n',
tailwind_injected_layer: node.raws.tailwind_injected_layer,
tailwind_original_params: `${quote}${id}${quote}`,
tailwind_destination_sheet_id: utilityDestination.id,
},
Expand Down
16 changes: 8 additions & 8 deletions packages/@tailwindcss-upgrade/src/stylesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,26 +197,26 @@ export class Stylesheet {
* adjusting imports which is a non-trivial task.
*/
analyzeImportPaths() {
let convertablePaths: StylesheetConnection[][] = []
let nonConvertablePaths: StylesheetConnection[][] = []
let convertiblePaths: StylesheetConnection[][] = []
let nonConvertiblePaths: StylesheetConnection[][] = []

for (let path of this.pathsToRoot()) {
let isConvertable = false
let isConvertible = false

for (let { meta } of path) {
for (let layer of meta.layers) {
isConvertable ||= layer === 'utilities' || layer === 'components'
isConvertible ||= layer === 'utilities' || layer === 'components'
}
}

if (isConvertable) {
convertablePaths.push(path)
if (isConvertible) {
convertiblePaths.push(path)
} else {
nonConvertablePaths.push(path)
nonConvertiblePaths.push(path)
}
}

return { convertablePaths, nonConvertablePaths }
return { convertiblePaths, nonConvertiblePaths }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol what a typo 🤣

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be fair, convertable makes more sense to my non-native-English brain lol

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean same — it does make more sense. I guess I just wasn't thinking when I wrote it originally haha

}

[util.inspect.custom]() {
Expand Down