Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 @@ -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

Expand Down
147 changes: 135 additions & 12 deletions integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ test(
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -1069,7 +1069,7 @@ test(
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
Expand Down Expand Up @@ -1123,7 +1123,7 @@ test(
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"tailwindcss": "^3",
"@tailwindcss/cli": "workspace:^",
"@tailwindcss/upgrade": "workspace:^"
}
Expand Down Expand Up @@ -1310,7 +1310,7 @@ test(
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"tailwindcss": "^3",
"@tailwindcss/cli": "workspace:^",
"@tailwindcss/upgrade": "workspace:^"
}
Expand Down Expand Up @@ -1376,6 +1376,7 @@ test(
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
Expand Down Expand Up @@ -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';
`,
},
},
Expand Down Expand Up @@ -1871,7 +1872,7 @@ test(
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
Expand Down Expand Up @@ -1933,7 +1934,7 @@ test(
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
Expand Down Expand Up @@ -2017,7 +2018,7 @@ test(
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
},
"devDependencies": {
Expand Down Expand Up @@ -2047,7 +2048,7 @@ test(
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3.4.14",
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
},
"devDependencies": {
Expand Down Expand Up @@ -2152,7 +2153,7 @@ test(
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3.4.14",
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
},
"devDependencies": {
Expand Down Expand Up @@ -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;
}
}
"
`)
},
)
50 changes: 37 additions & 13 deletions packages/@tailwindcss-upgrade/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -67,9 +68,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,
Expand All @@ -80,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<string, Stylesheet>()
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
Copy link
Member Author

Choose a reason for hiding this comment

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

When you don't pass any files, then we used globby with the gitignore: true option, so I wanted to add that here as well.

I added an explicitly ignored CSS file to the integration tests to make sure we don't touch it.


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 })
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a chance this would fail and we'd want to just skip it? Or should it hard crash or w/e?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also shouldn't we be able to do some of this stuff in analyze() instead of using postcss-import or nah?

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, hmm, let me try to move it. Might work indeed 🤔


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 {
Expand Down