From f65d58ff2427e4c5778deef4c98ddf9ef34a96c7 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 15 Nov 2024 13:51:43 +0100 Subject: [PATCH 1/8] remove "No input stylesheets provided." When reading this, it almost looks like you made an error, but that's not the case. We will just find CSS files for you instead. --- packages/@tailwindcss-upgrade/src/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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, From 2c33aade9ef16f96e363dd400ed9371426dc0d84 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 15 Nov 2024 15:26:22 +0100 Subject: [PATCH 2/8] add failing test --- integrations/upgrade/index.test.ts | 122 +++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 3b2c7e9d317e..7029ec339134 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -2236,3 +2236,125 @@ test( `) }, ) + +test( + 'passing in a single CSS file should resolve all imports and migrate them', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3.4.14", + "@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; + } + } + " + `) + }, +) From 927c6a043aa79cb28ab175b238feeb40acdd3b2b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 15 Nov 2024 15:27:05 +0100 Subject: [PATCH 3/8] load a stylesheet and all of its imports as separate stylesheets --- packages/@tailwindcss-upgrade/src/index.ts | 46 +++++++++++++++++----- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 1e789a348575..0e2d0b2f5b40 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -1,9 +1,10 @@ #!/usr/bin/env node -import { globby } from 'globby' +import { globby, isGitIgnored } from 'globby' import fs from 'node:fs/promises' import path from 'node:path' import postcss from 'postcss' +import atImport from 'postcss-import' import { formatNodes } from './codemods/format-nodes' import { sortBuckets } from './codemods/sort-buckets' import { help } from './commands/help' @@ -78,19 +79,44 @@ async function run() { // Ensure we are only dealing with CSS files files = files.filter((file) => file.endsWith('.css')) - // Analyze the stylesheets - let loadResults = await Promise.allSettled(files.map((filepath) => Stylesheet.load(filepath))) + // Load the stylesheets and their imports + let sheetsByFile = new Map() + let isIgnored = await isGitIgnored() + let queue = files.slice() + while (queue.length > 0) { + let file = queue.shift()! - // Load and parse all stylesheets - for (let result of loadResults) { - if (result.status === 'rejected') { - error(`${result.reason}`) + // Already handled + if (sheetsByFile.has(file)) continue + + // We don't want to process ignored files (like node_modules) + if (isIgnored(file)) continue + + let sheet = await Stylesheet.load(file).catch((e) => { + error(`${e}`) + return null + }) + if (!sheet) continue + + // Track the sheet by its file + sheetsByFile.set(file, sheet) + + // We process the stylesheet which will also process its imports and + // inline everything. We still want to handle the imports separately, so + // we just use the postcss-import messages to find the imported files. + // + // We can't use the `sheet.root` directly because this will mutate the + // `sheet.root` + let processed = await postcss().use(atImport()).process(sheet.root.toString(), { from: file }) + + for (let msg of processed.messages) { + if (msg.type === 'dependency' && msg.plugin === 'postcss-import') { + queue.push(msg.file) + } } } - let stylesheets = loadResults - .filter((result) => result.status === 'fulfilled') - .map((result) => result.value) + let stylesheets = Array.from(sheetsByFile.values()) // Analyze the stylesheets try { From c577fd219faee08046173ad5d557764b1c257ece Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 15 Nov 2024 16:00:58 +0100 Subject: [PATCH 4/8] rely on Tailwind CSS v3 when upgrading --- integrations/upgrade/index.test.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 7029ec339134..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": { @@ -2244,7 +2245,7 @@ test( 'package.json': json` { "dependencies": { - "tailwindcss": "^3.4.14", + "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" } } From ecca617abf5be1ef2c3fb7900f878f32c5800f75 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 15 Nov 2024 17:36:56 +0100 Subject: [PATCH 5/8] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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 From b293ac5b589c6b87ea6634af6fc8c7c1c9b4480a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 15 Nov 2024 20:59:58 +0100 Subject: [PATCH 6/8] revert changes to index --- packages/@tailwindcss-upgrade/src/index.ts | 46 +++++----------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 0e2d0b2f5b40..1e789a348575 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -1,10 +1,9 @@ #!/usr/bin/env node -import { globby, isGitIgnored } from 'globby' +import { globby } from 'globby' import fs from 'node:fs/promises' import path from 'node:path' import postcss from 'postcss' -import atImport from 'postcss-import' import { formatNodes } from './codemods/format-nodes' import { sortBuckets } from './codemods/sort-buckets' import { help } from './commands/help' @@ -79,44 +78,19 @@ async function run() { // Ensure we are only dealing with CSS files files = files.filter((file) => file.endsWith('.css')) - // Load the stylesheets and their imports - let sheetsByFile = new Map() - let isIgnored = await isGitIgnored() - let queue = files.slice() - while (queue.length > 0) { - let file = queue.shift()! - - // Already handled - if (sheetsByFile.has(file)) continue - - // We don't want to process ignored files (like node_modules) - if (isIgnored(file)) continue + // Analyze the stylesheets + let loadResults = await Promise.allSettled(files.map((filepath) => Stylesheet.load(filepath))) - let sheet = await Stylesheet.load(file).catch((e) => { - error(`${e}`) - return null - }) - if (!sheet) continue - - // Track the sheet by its file - sheetsByFile.set(file, sheet) - - // We process the stylesheet which will also process its imports and - // inline everything. We still want to handle the imports separately, so - // we just use the postcss-import messages to find the imported files. - // - // We can't use the `sheet.root` directly because this will mutate the - // `sheet.root` - let processed = await postcss().use(atImport()).process(sheet.root.toString(), { from: file }) - - for (let msg of processed.messages) { - if (msg.type === 'dependency' && msg.plugin === 'postcss-import') { - queue.push(msg.file) - } + // Load and parse all stylesheets + for (let result of loadResults) { + if (result.status === 'rejected') { + error(`${result.reason}`) } } - let stylesheets = Array.from(sheetsByFile.values()) + let stylesheets = loadResults + .filter((result) => result.status === 'fulfilled') + .map((result) => result.value) // Analyze the stylesheets try { From 0001eb2bbfead87390092e54f52249ca3d213eb0 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 15 Nov 2024 21:00:21 +0100 Subject: [PATCH 7/8] add `Stylesheet.loadSync` This allows us to use a sync version in places where we can't use async code. --- packages/@tailwindcss-upgrade/src/stylesheet.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) 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) From bfc08aad7fab5350256ece6560e5dc0d674eba52 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 15 Nov 2024 21:02:10 +0100 Subject: [PATCH 8/8] create stylesheets from imports --- packages/@tailwindcss-upgrade/src/migrate.ts | 43 ++++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) 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[]) {