diff --git a/CHANGELOG.md b/CHANGELOG.md index e5dbf0cd925a..26cbb8bf6c48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Ensure `flex` is suggested ([#15014](https://github.com/tailwindlabs/tailwindcss/pull/15014)) +- _Upgrade (experimental)_: Resolve imports from passed CSS file(s) ([#15010](https://github.com/tailwindlabs/tailwindcss/pull/15010)) ### Changed diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 3b2c7e9d317e..5db4487b4803 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -239,7 +239,7 @@ test( 'package.json': json` { "dependencies": { - "tailwindcss": "workspace:^", + "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" } } @@ -1000,14 +1000,14 @@ test( 'package.json': json` { "dependencies": { - "tailwindcss": "workspace:^", + "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" } } `, 'tailwind.config.js': js`module.exports = {}`, 'src/index.css': css` - @import 'tailwindcss'; + @import 'tailwindcss/tailwind.css'; @import './utilities.css' layer(utilities); `, 'src/utilities.css': css` @@ -1069,7 +1069,7 @@ test( 'package.json': json` { "dependencies": { - "tailwindcss": "workspace:^", + "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" } } @@ -1123,7 +1123,7 @@ test( 'package.json': json` { "dependencies": { - "tailwindcss": "workspace:^", + "tailwindcss": "^3", "@tailwindcss/cli": "workspace:^", "@tailwindcss/upgrade": "workspace:^" } @@ -1310,7 +1310,7 @@ test( 'package.json': json` { "dependencies": { - "tailwindcss": "workspace:^", + "tailwindcss": "^3", "@tailwindcss/cli": "workspace:^", "@tailwindcss/upgrade": "workspace:^" } @@ -1376,6 +1376,7 @@ test( 'package.json': json` { "dependencies": { + "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" } } @@ -1435,7 +1436,7 @@ test( 'src/root.5.css': css`@import './root.5/tailwind.css';`, 'src/root.5/tailwind.css': css` /* Inject missing @config in this file, due to full import */ - @import 'tailwindcss'; + @import 'tailwindcss/tailwind.css'; `, }, }, @@ -1871,7 +1872,7 @@ test( 'package.json': json` { "dependencies": { - "tailwindcss": "workspace:^", + "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" } } @@ -1933,7 +1934,7 @@ test( 'package.json': json` { "dependencies": { - "tailwindcss": "workspace:^", + "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" } } @@ -2017,7 +2018,7 @@ test( 'package.json': json` { "dependencies": { - "tailwindcss": "workspace:^", + "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" }, "devDependencies": { @@ -2047,7 +2048,7 @@ test( 'package.json': json` { "dependencies": { - "tailwindcss": "^3.4.14", + "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" }, "devDependencies": { @@ -2152,7 +2153,7 @@ test( 'package.json': json` { "dependencies": { - "tailwindcss": "^3.4.14", + "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" }, "devDependencies": { @@ -2236,3 +2237,125 @@ test( `) }, ) + +test( + 'passing in a single CSS file should resolve all imports and migrate them', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.js': js`module.exports = {}`, + 'src/index.css': css` + @import './base.css'; + @import './components.css'; + @import './utilities.css'; + @import './generated/ignore-me.css'; + `, + 'src/generated/.gitignore': ` + * + !.gitignore + `, + 'src/generated/ignore-me.css': css` + /* This should not be converted */ + @layer utilities { + .ignore-me { + color: red; + } + } + `, + 'src/base.css': css`@import 'tailwindcss/base';`, + 'src/components.css': css` + @import './typography.css'; + @layer components { + .foo { + color: red; + } + } + @tailwind components; + `, + 'src/utilities.css': css` + @layer utilities { + .bar { + color: blue; + } + } + @tailwind utilities; + `, + 'src/typography.css': css` + @layer components { + .typography { + color: red; + } + } + `, + }, + }, + async ({ exec, fs }) => { + await exec('npx @tailwindcss/upgrade ./src/index.css') + + expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(` + " + --- ./src/index.css --- + @import './base.css'; + @import './components.css'; + @import './utilities.css'; + @import './generated/ignore-me.css'; + + --- ./src/base.css --- + @import 'tailwindcss/theme' layer(theme); + @import 'tailwindcss/preflight' layer(base); + + /* + The default border color has changed to \`currentColor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentColor); + } + } + + --- ./src/components.css --- + @import './typography.css'; + + @utility foo { + color: red; + } + + --- ./src/typography.css --- + @utility typography { + color: red; + } + + --- ./src/utilities.css --- + @import 'tailwindcss/utilities' layer(utilities); + + @utility bar { + color: blue; + } + + --- ./src/generated/ignore-me.css --- + /* This should not be converted */ + @layer utilities { + .ignore-me { + color: red; + } + } + " + `) + }, +) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 616ce65bb1a1..1e789a348575 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -67,9 +67,7 @@ async function run() { // Discover CSS files in case no files were provided if (files.length === 0) { - info( - 'No input stylesheets provided. Searching for CSS files in the current directory and its subdirectories…', - ) + info('Searching for CSS files in the current directory and its subdirectories…') files = await globby(['**/*.css'], { absolute: true, diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index bbde4a717ae6..e01ba201defe 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -1,6 +1,7 @@ import { normalizePath } from '@tailwindcss/node' +import { isGitIgnored } from 'globby' import path from 'node:path' -import postcss from 'postcss' +import postcss, { type Result } from 'postcss' import type { Config } from '../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../tailwindcss/src/design-system' import { DefaultMap } from '../../tailwindcss/src/utils/default-map' @@ -65,13 +66,28 @@ export async function migrate(stylesheet: Stylesheet, options: MigrateOptions) { } export async function analyze(stylesheets: Stylesheet[]) { - let stylesheetsByFile = new Map() + let isIgnored = await isGitIgnored() + let processingQueue: (() => Promise)[] = [] + let stylesheetsByFile = new DefaultMap((file) => { + // We don't want to process ignored files (like node_modules) + if (isIgnored(file)) { + return null + } - for (let sheet of stylesheets) { - if (sheet.file) { - stylesheetsByFile.set(sheet.file, sheet) + try { + let sheet = Stylesheet.loadSync(file) + + // Mutate incoming stylesheets to include the newly discovered sheet + stylesheets.push(sheet) + + // Queue up the processing of this stylesheet + processingQueue.push(() => processor.process(sheet.root, { from: sheet.file! })) + + return sheet + } catch { + return null } - } + }) // Step 1: Record which `@import` rules point to which stylesheets // and which stylesheets are parents/children of each other @@ -147,12 +163,23 @@ export async function analyze(stylesheets: Stylesheet[]) { }, ]) + // Seed the map with all the known stylesheets, and queue up the processing of + // each incoming stylesheet. for (let sheet of stylesheets) { - if (!sheet.file) continue + if (sheet.file) { + stylesheetsByFile.set(sheet.file, sheet) + processingQueue.push(() => processor.process(sheet.root, { from: sheet.file ?? undefined })) + } + } - await processor.process(sheet.root, { from: sheet.file }) + // Process all the stylesheets from step 1 + while (processingQueue.length > 0) { + let task = processingQueue.shift()! + await task() } + // --- + let commonPath = process.cwd() function pathToString(path: StylesheetConnection[]) { diff --git a/packages/@tailwindcss-upgrade/src/stylesheet.ts b/packages/@tailwindcss-upgrade/src/stylesheet.ts index dc810903c6ce..092d9414b569 100644 --- a/packages/@tailwindcss-upgrade/src/stylesheet.ts +++ b/packages/@tailwindcss-upgrade/src/stylesheet.ts @@ -1,3 +1,4 @@ +import * as fsSync from 'node:fs' import * as fs from 'node:fs/promises' import * as path from 'node:path' import * as util from 'node:util' @@ -72,6 +73,15 @@ export class Stylesheet { return new Stylesheet(root, filepath) } + static loadSync(filepath: string) { + filepath = path.resolve(process.cwd(), filepath) + + let css = fsSync.readFileSync(filepath, 'utf-8') + let root = postcss.parse(css, { from: filepath }) + + return new Stylesheet(root, filepath) + } + static async fromString(css: string) { let root = postcss.parse(css)