Resolve imports from CSS file (#15010)

This PR adds an improvement to the upgrade tool to make sure that if you
pass a single CSS file, that the upgrade tool resolves all the imports
in that file and processes them as well.


Test plan:
---

Created a project where `index.css` imports `other.css`. Another
`leave-me-alone.css` is created to proof that this file is _not_
changed. Running the upgrade guide using `index.css` also migrates
`other.css` but not `leave-me-alone.css`.

Here is a video so you don't have to manually create it:



https://github.com/user-attachments/assets/20decf77-77d2-4a7c-8ff1-accb1c77f8c1
This commit is contained in:
Robin Malfait 2024-11-15 22:06:52 +01:00 • committed by GitHub
parent 3fb6902f08
commit 57f87be440
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 184 additions and 25 deletions

View file

@ -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

View file

@ -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;
}
}
"
`)
},
)

View file

@ -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,

View file

@ -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<string, Stylesheet>()
for (let sheet of stylesheets) {
if (sheet.file) {
stylesheetsByFile.set(sheet.file, sheet)
let isIgnored = await isGitIgnored()
let processingQueue: (() => Promise<Result>)[] = []
let stylesheetsByFile = new DefaultMap<string, Stylesheet | null>((file) => {
// We don't want to process ignored files (like node_modules)
if (isIgnored(file)) {
return null
}
}
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
await processor.process(sheet.root, { from: sheet.file })
if (sheet.file) {
stylesheetsByFile.set(sheet.file, sheet)
processingQueue.push(() => processor.process(sheet.root, { from: sheet.file ?? undefined }))
}
}
// 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[]) {

View file

@ -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)