diff --git a/CHANGELOG.md b/CHANGELOG.md index 7940c36fd811..e8a13a9ded19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 948c0a9e9fd6..80005c088256 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -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', { @@ -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'; diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index f82717413306..17a1041c8d5b 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -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 + } + + 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! }) diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index b6524d1b0451..1b72afc6f780 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -159,8 +159,8 @@ 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 @@ -168,11 +168,11 @@ export async function analyze(stylesheets: Stylesheet[]) { 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)}`) } } @@ -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() for (let sheet of stylesheets) { @@ -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, }, diff --git a/packages/@tailwindcss-upgrade/src/stylesheet.ts b/packages/@tailwindcss-upgrade/src/stylesheet.ts index c3a91fe4ba88..1b8fa841e7b5 100644 --- a/packages/@tailwindcss-upgrade/src/stylesheet.ts +++ b/packages/@tailwindcss-upgrade/src/stylesheet.ts @@ -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 } } [util.inspect.custom]() {