Skip to content

Commit

Permalink
Resolve imports from CSS file (#15010)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
RobinMalfait authored Nov 15, 2024
1 parent 3fb6902 commit 57f87be
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 23 deletions.
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;
}
}
"
`)
},
)
4 changes: 1 addition & 3 deletions packages/@tailwindcss-upgrade/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
43 changes: 35 additions & 8 deletions packages/@tailwindcss-upgrade/src/migrate.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -65,13 +66,28 @@ export async function migrate(stylesheet: Stylesheet, options: MigrateOptions) {
}

export async function analyze(stylesheets: Stylesheet[]) {
let stylesheetsByFile = new Map<string, Stylesheet>()
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
}

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
Expand Down Expand Up @@ -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[]) {
Expand Down
10 changes: 10 additions & 0 deletions packages/@tailwindcss-upgrade/src/stylesheet.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit 57f87be

Please sign in to comment.