From 08c6c96f02b8ac9a57964727d2a7192e19473d41 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 18 Nov 2024 17:43:44 +0100 Subject: [PATCH] Improve multi-root `@config` linking (#15001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR improves the discoverability of Tailwind config files when we are trying to link them to your CSS files. When you have multiple "root" CSS files in your project, and if they don't include an `@config` directive, then we tried to find the Tailwind config file in your current working directory. This means that if you run the upgrade command from the root of your project, and you have a nested folder with a separate Tailwind setup, then the nested CSS file would link to the root Tailwind config file. Visually, you can think of it like this: ``` . ├── admin │   ├── src │   │   └── styles │   │   └── index.css <-- This will be linked to (1) │   └── tailwind.config.js (2) ├── src │   └── styles │   └── index.css <-- This will be linked to (1) └── tailwind.config.js (1) ``` If you run the upgrade command from the root of your project, then the `/src/styles/index.css` will be linked to `/tailwind.config.js` which is what we expect. But `/admin/src/styles/index.css` will _also_ be linked to `/tailwind.config.js` With this PR we improve this behavior by looking at the CSS file, and crawling up the parent tree. This mens that the new behavior looks like this: ``` . ├── admin │   ├── src │   │   └── styles │   │   └── index.css <-- This will be linked to (2) │   └── tailwind.config.js (2) ├── src │   └── styles │   └── index.css <-- This will be linked to (1) └── tailwind.config.js (1) ``` Now `/src/styles/index.css` will be linked to `/tailwind.config.js`, and `/admin/src/styles/index.css` will be linked to `/admin/tailwind.config.js`. When we discover the Tailwind config file, we will also print a message to the user to let them know which CSS file is linked to which Tailwind config file. This should be a safe improvement because if your Tailwind config file had a different name, or if it lived in a sibling folder then Tailwind wouldn't find it either and you already required a `@config "…";` directive in your CSS file to point to the correct file. In the unlikely event that it turns out that 2 (or more) CSS files resolve to the same to the same Tailwind config file, then an upgrade might not be safe and some manual intervention might be needed. In this case, we will show a warning about this. image Test plan: --- - Added an integration test that properly links the nearest Tailwind config file by looking up the tree - Added an integration test that resolves 2 or more CSS files to the same config file, resulting in an error where manual intervention is needed - Ran it on the Tailwind UI codebase Running this on Tailwind UI's codebase it looks like this: image --------- Co-authored-by: Jordan Pittman --- CHANGELOG.md | 1 + integrations/upgrade/index.test.ts | 444 +++++++++++------- integrations/utils.ts | 12 +- packages/@tailwindcss-upgrade/src/index.ts | 34 +- .../src/migrate-js-config.ts | 4 +- .../src/migrate-postcss.ts | 48 +- .../src/migrate-prettier.ts | 4 +- packages/@tailwindcss-upgrade/src/migrate.ts | 59 ++- .../src/template/prepare-config.ts | 43 +- 9 files changed, 416 insertions(+), 233 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7d8ca4a..204dccc5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure `flex` is suggested ([#15014](https://github.com/tailwindlabs/tailwindcss/pull/15014)) - _Upgrade (experimental)_: Resolve imports when specifying a CSS entry point on the command-line ([#15010](https://github.com/tailwindlabs/tailwindcss/pull/15010)) +- _Upgrade (experimental)_: Resolve nearest Tailwind config file when CSS file does not contain `@config` ([#15001](https://github.com/tailwindlabs/tailwindcss/pull/15001)) ### Changed diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index ebc251a66..32fcd1b83 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -1385,9 +1385,257 @@ test( export default { content: ['./src/**/*.{html,js}'], plugins: [ - () => { - // custom stuff which is too complicated to migrate to CSS - }, + () => {}, // custom stuff which is too complicated to migrate to CSS + ], + } + `, + 'src/index.html': html` +
+ `, + 'src/root.1/index.css': css` + /* Inject missing @config */ + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + 'src/root.1/tailwind.config.ts': js` + export default { + content: ['./src/**/*.{html,js}'], + plugins: [ + () => {}, // custom stuff which is too complicated to migrate to CSS + ], + } + `, + 'src/root.2/index.css': css` + /* Already contains @config */ + @tailwind base; + @tailwind components; + @tailwind utilities; + @config "../../tailwind.config.ts"; + `, + 'src/root.3/index.css': css` + /* Inject missing @config above first @theme */ + @tailwind base; + @tailwind components; + @tailwind utilities; + + @variant hocus (&:hover, &:focus); + + @theme { + --color-red-500: #f00; + } + + @theme { + --color-blue-500: #00f; + } + `, + 'src/root.3/tailwind.config.ts': js` + export default { + content: ['./src/**/*.{html,js}'], + plugins: [ + () => {}, // custom stuff which is too complicated to migrate to CSS + ], + } + `, + 'src/root.4/index.css': css` + /* Inject missing @config due to nested imports with tailwind imports */ + @import './base.css'; + @import './utilities.css'; + `, + 'src/root.4/tailwind.config.ts': js` + export default { + content: ['./src/**/*.{html,js}'], + plugins: [ + () => {}, // custom stuff which is too complicated to migrate to CSS + ], + } + `, + 'src/root.4/base.css': css`@import 'tailwindcss/base';`, + 'src/root.4/utilities.css': css`@import 'tailwindcss/utilities';`, + + 'src/root.5/index.css': css`@import './tailwind.css';`, + 'src/root.5/tailwind.css': css` + /* Inject missing @config in this file, due to full import */ + /* Should be located in the root: ../../ */ + @import 'tailwindcss'; + `, + }, + }, + async ({ exec, fs }) => { + await exec('npx @tailwindcss/upgrade --force') + + expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(` + " + --- ./src/index.html --- +
+ + --- ./src/root.1/index.css --- + /* Inject missing @config */ + @import 'tailwindcss'; + + @config './tailwind.config.ts'; + + /* + 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/root.2/index.css --- + /* Already contains @config */ + @import 'tailwindcss'; + + @config "../../tailwind.config.ts"; + + /* + 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/root.3/index.css --- + /* Inject missing @config above first @theme */ + @import 'tailwindcss'; + + @config './tailwind.config.ts'; + + @variant hocus (&:hover, &:focus); + + @theme { + --color-red-500: #f00; + } + + @theme { + --color-blue-500: #00f; + } + + /* + 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/root.4/index.css --- + /* Inject missing @config due to nested imports with tailwind imports */ + @import './base.css'; + @import './utilities.css'; + + @config './tailwind.config.ts'; + + --- ./src/root.4/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/root.4/utilities.css --- + @import 'tailwindcss/utilities' layer(utilities); + + --- ./src/root.5/index.css --- + @import './tailwind.css'; + + --- ./src/root.5/tailwind.css --- + /* Inject missing @config in this file, due to full import */ + /* Should be located in the root: ../../ */ + @import 'tailwindcss'; + + @config '../../tailwind.config.ts'; + + /* + 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); + } + } + " + `) + }, +) + +test( + 'multiple CSS roots that resolve to the same Tailwind config file requires manual intervention', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': js` + export default { + content: ['./src/**/*.{html,js}'], + plugins: [ + () => {}, // custom stuff which is too complicated to migrate to CSS ], } `, @@ -1440,158 +1688,12 @@ test( `, }, }, - async ({ exec, fs }) => { - await exec('npx @tailwindcss/upgrade --force') + async ({ exec }) => { + let output = await exec('npx @tailwindcss/upgrade --force', {}, { ignoreStdErr: true }).catch( + (e) => e.toString(), + ) - expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(` - " - --- ./src/index.html --- -
- - --- ./src/root.1.css --- - /* Inject missing @config */ - @import 'tailwindcss'; - - @config '../tailwind.config.ts'; - - /* - 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/root.2.css --- - /* Already contains @config */ - @import 'tailwindcss'; - - @config "../tailwind.config.ts"; - - /* - 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/root.3.css --- - /* Inject missing @config above first @theme */ - @import 'tailwindcss'; - - @config '../tailwind.config.ts'; - - @variant hocus (&:hover, &:focus); - - @theme { - --color-red-500: #f00; - } - - @theme { - --color-blue-500: #00f; - } - - /* - 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/root.4.css --- - /* Inject missing @config due to nested imports with tailwind imports */ - @import './root.4/base.css'; - @import './root.4/utilities.css'; - - @config '../tailwind.config.ts'; - - --- ./src/root.5.css --- - @import './root.5/tailwind.css'; - - --- ./src/root.4/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/root.4/utilities.css --- - @import 'tailwindcss/utilities' layer(utilities); - - --- ./src/root.5/tailwind.css --- - /* Inject missing @config in this file, due to full import */ - @import 'tailwindcss'; - - @config '../../tailwind.config.ts'; - - /* - 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); - } - } - " - `) + expect(output).toMatch('Could not determine configuration file for:') }, ) @@ -1602,6 +1704,7 @@ test( 'package.json': json` { "dependencies": { + "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" } } @@ -1662,14 +1765,14 @@ test( expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(` " + --- ./src/index.css --- + @import './tailwind-setup.css'; + --- ./src/index.html ---
- --- ./src/index.css --- - @import './tailwind-setup.css'; - --- ./src/base.css --- @import 'tailwindcss/theme' layer(theme); @import 'tailwindcss/preflight' layer(base); @@ -1735,6 +1838,7 @@ test( 'package.json': json` { "dependencies": { + "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" } } @@ -1797,14 +1901,14 @@ test( expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(` " + --- ./src/index.css --- + @import './tailwind-setup.css'; + --- ./src/index.html ---
- --- ./src/index.css --- - @import './tailwind-setup.css'; - --- ./src/base.css --- @import 'tailwindcss/theme' layer(theme); @import 'tailwindcss/preflight' layer(base); @@ -2105,13 +2209,6 @@ test( // Files should not be modified expect(await fs.dumpFiles('./*.{js,css,html}')).toMatchInlineSnapshot(` " - --- index.html --- -
-
-
-
-
- --- index.css --- @import 'tailwindcss'; @@ -2141,6 +2238,13 @@ test( border-color: var(--color-gray-200, currentColor); } } + + --- index.html --- +
+
+
+
+
" `) }, @@ -2196,9 +2300,6 @@ test( // Files should not be modified expect(await fs.dumpFiles('./*.{js,css,html,tsx}')).toMatchInlineSnapshot(` " - --- index.html --- -
- --- index.css --- @import 'tailwindcss'; @@ -2220,6 +2321,9 @@ test( } } + --- index.html --- +
+ --- example-component.tsx --- type Star = [ x: number, diff --git a/integrations/utils.ts b/integrations/utils.ts index 44ee3c6fc..7ac7a1147 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -346,15 +346,21 @@ export function test( let zParts = z[0].split('/') let aFile = aParts.at(-1) - let zFile = aParts.at(-1) + let zFile = zParts.at(-1) // Sort by depth, shallow first if (aParts.length < zParts.length) return -1 if (aParts.length > zParts.length) return 1 + // Sort by folder names, alphabetically + for (let i = 0; i < aParts.length - 1; i++) { + let diff = aParts[i].localeCompare(zParts[i]) + if (diff !== 0) return diff + } + // Sort by filename, sort files named `index` before others - if (aFile?.startsWith('index')) return -1 - if (zFile?.startsWith('index')) return 1 + if (aFile?.startsWith('index') && !zFile?.startsWith('index')) return -1 + if (zFile?.startsWith('index') && !aFile?.startsWith('index')) return 1 // Sort by filename, alphabetically return a[0].localeCompare(z[0]) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 1e789a348..3f57ef198 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -23,7 +23,7 @@ import { args, type Arg } from './utils/args' import { isRepoDirty } from './utils/git' import { hoistStaticGlobParts } from './utils/hoist-static-glob-parts' import { pkg } from './utils/packages' -import { eprintln, error, header, highlight, info, success } from './utils/renderer' +import { eprintln, error, header, highlight, info, relative, success } from './utils/renderer' const options = { '--config': { type: 'string', description: 'Path to the configuration file', alias: '-c' }, @@ -110,7 +110,7 @@ async function run() { } // Migrate js config files, linked to stylesheets - info('Migrating JavaScript configuration files using the provided configuration file.') + info('Migrating JavaScript configuration files…') let configBySheet = new Map>>() let jsConfigMigrationBySheet = new Map< Stylesheet, @@ -133,13 +133,18 @@ async function run() { // Remove the JS config if it was fully migrated cleanup.push(() => fs.rm(config.configFilePath)) } + + if (jsConfigMigration !== null) { + success( + `↳ Migrated configuration file: ${highlight(relative(config.configFilePath, base))}`, + ) + } } // Migrate source files, linked to config files + info('Migrating templates…') { // Template migrations - - info('Migrating templates using the provided configuration file.') for (let config of configBySheet.values()) { let set = new Set() for (let globEntry of config.globs.flatMap((entry) => hoistStaticGlobParts(entry))) { @@ -161,12 +166,15 @@ async function run() { await Promise.allSettled( files.map((file) => migrateTemplate(config.designSystem, config.userConfig, file)), ) - } - success('Template migration complete.') + success( + `↳ Migrated templates for configuration file: ${highlight(relative(config.configFilePath, base))}`, + ) + } } // Migrate each CSS file + info('Migrating stylesheets…') let migrateResults = await Promise.allSettled( stylesheets.map((sheet) => { let config = configBySheet.get(sheet)! @@ -231,9 +239,11 @@ async function run() { if (!sheet.file) continue await fs.writeFile(sheet.file, sheet.root.toString()) - } - success('Stylesheet migration complete.') + if (sheet.isTailwindRoot) { + success(`↳ Migrated stylesheet: ${highlight(relative(sheet.file, base))}`) + } + } } { @@ -241,19 +251,21 @@ async function run() { await migratePostCSSConfig(base) } + info('Updating dependencies…') { // Migrate the prettier plugin to the latest version await migratePrettierPlugin(base) } - // Run all cleanup functions because we completed the migration - await Promise.allSettled(cleanup.map((fn) => fn())) - try { // Upgrade Tailwind CSS await pkg(base).add(['tailwindcss@next']) + success(`↳ Updated package: ${highlight('tailwindcss')}`) } catch {} + // Run all cleanup functions because we completed the migration + await Promise.allSettled(cleanup.map((fn) => fn())) + // Figure out if we made any changes if (isRepoDirty()) { success('Verify the changes and commit them to your repository.') diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 4ac4fe8d3..b69379df1 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -19,7 +19,7 @@ import type { DesignSystem } from '../../tailwindcss/src/design-system' import { escape } from '../../tailwindcss/src/utils/escape' import { isValidSpacingMultiplier } from '../../tailwindcss/src/utils/infer-data-type' import { findStaticPlugins, type StaticPluginOptions } from './utils/extract-static-plugins' -import { info } from './utils/renderer' +import { highlight, info, relative } from './utils/renderer' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -44,7 +44,7 @@ export async function migrateJsConfig( if (!canMigrateConfig(unresolvedConfig, source)) { info( - 'Your configuration file could not be automatically migrated to the new CSS configuration format, so your CSS has been updated to load your existing configuration file.', + `↳ The configuration file at ${highlight(relative(fullConfigPath, base))} could not be automatically migrated to the new CSS configuration format, so your CSS has been updated to load your existing configuration file.`, ) return null } diff --git a/packages/@tailwindcss-upgrade/src/migrate-postcss.ts b/packages/@tailwindcss-upgrade/src/migrate-postcss.ts index 1f4d6c468..1bfb2caa8 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-postcss.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-postcss.ts @@ -1,7 +1,7 @@ import fs from 'node:fs/promises' import path from 'node:path' import { pkg } from './utils/packages' -import { info, success, warn } from './utils/renderer' +import { highlight, info, relative, success, warn } from './utils/renderer' // Migrates simple PostCSS setups. This is to cover non-dynamic config files // similar to the ones we have all over our docs: @@ -24,7 +24,7 @@ export async function migratePostCSSConfig(base: string) { // Priority 1: Handle JS config files let jsConfigPath = await detectJSConfigPath(base) if (jsConfigPath) { - let result = await migratePostCSSJSConfig(base, jsConfigPath) + let result = await migratePostCSSJSConfig(jsConfigPath) if (result) { didMigrate = true @@ -41,7 +41,7 @@ export async function migratePostCSSConfig(base: string) { packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8')) } catch {} if (!didMigrate && packageJson && 'postcss' in packageJson) { - let result = await migratePostCSSJsonConfig(base, packageJson.postcss) + let result = await migratePostCSSJsonConfig(packageJson.postcss) if (result) { await fs.writeFile( @@ -64,7 +64,7 @@ export async function migratePostCSSConfig(base: string) { jsonConfig = JSON.parse(await fs.readFile(jsonConfigPath, 'utf-8')) } catch {} if (jsonConfig) { - let result = await migratePostCSSJsonConfig(base, jsonConfig) + let result = await migratePostCSSJsonConfig(jsonConfig) if (result) { await fs.writeFile(jsonConfigPath, JSON.stringify(result.json, null, 2)) @@ -78,7 +78,7 @@ export async function migratePostCSSConfig(base: string) { } if (!didMigrate) { - info(`No PostCSS config found, skipping migration.`) + info('No PostCSS config found, skipping migration.') return } @@ -92,6 +92,7 @@ export async function migratePostCSSConfig(base: string) { if (location !== null) { try { await pkg(base).add(['@tailwindcss/postcss@next'], location) + success(`↳ Installed package: ${highlight('@tailwindcss/postcss')}`) } catch {} } } @@ -102,16 +103,18 @@ export async function migratePostCSSConfig(base: string) { didRemovePostCSSImport ? 'postcss-import' : null, ].filter(Boolean) as string[] await pkg(base).remove(packagesToRemove) + for (let pkg of packagesToRemove) { + success(`↳ Removed package: ${highlight(pkg)}`) + } } catch {} } - success(`PostCSS config has been upgraded.`) + if (didMigrate && jsConfigPath) { + success(`↳ Migrated PostCSS configuration: ${highlight(relative(jsConfigPath, base))}`) + } } -async function migratePostCSSJSConfig( - base: string, - configPath: string, -): Promise<{ +async function migratePostCSSJSConfig(configPath: string): Promise<{ didAddPostcssClient: boolean didRemoveAutoprefixer: boolean didRemovePostCSSImport: boolean @@ -129,11 +132,11 @@ async function migratePostCSSJSConfig( return /['"]tailwindcss\/nesting['"]\: ?(\{\}|['"]postcss-nesting['"])/.test(line) } - info(`Attempt to upgrade the PostCSS config in file: ${configPath}`) + info('Migrating PostCSS configuration…') - let isSimpleConfig = await isSimplePostCSSConfig(base, configPath) + let isSimpleConfig = await isSimplePostCSSConfig(configPath) if (!isSimpleConfig) { - warn(`The PostCSS config contains dynamic JavaScript and can not be automatically migrated.`) + warn('The PostCSS config contains dynamic JavaScript and can not be automatically migrated.') return null } @@ -141,8 +144,7 @@ async function migratePostCSSJSConfig( let didRemoveAutoprefixer = false let didRemovePostCSSImport = false - let fullPath = path.resolve(base, configPath) - let content = await fs.readFile(fullPath, 'utf-8') + let content = await fs.readFile(configPath, 'utf-8') let lines = content.split('\n') let newLines: string[] = [] for (let i = 0; i < lines.length; i++) { @@ -186,15 +188,12 @@ async function migratePostCSSJSConfig( newLines.push(line) } } - await fs.writeFile(fullPath, newLines.join('\n')) + await fs.writeFile(configPath, newLines.join('\n')) return { didAddPostcssClient, didRemoveAutoprefixer, didRemovePostCSSImport } } -async function migratePostCSSJsonConfig( - base: string, - json: any, -): Promise<{ +async function migratePostCSSJsonConfig(json: any): Promise<{ json: any didAddPostcssClient: boolean didRemoveAutoprefixer: boolean @@ -291,7 +290,7 @@ async function detectJSConfigPath(base: string): Promise { let fullPath = path.resolve(base, file) try { await fs.access(fullPath) - return file + return fullPath } catch {} } return null @@ -308,15 +307,14 @@ async function detectJSONConfigPath(base: string): Promise { let fullPath = path.resolve(base, file) try { await fs.access(fullPath) - return file + return fullPath } catch {} } return null } -async function isSimplePostCSSConfig(base: string, configPath: string): Promise { - let fullPath = path.resolve(base, configPath) - let content = await fs.readFile(fullPath, 'utf-8') +async function isSimplePostCSSConfig(configPath: string): Promise { + let content = await fs.readFile(configPath, 'utf-8') return ( content.includes('tailwindcss:') && !( diff --git a/packages/@tailwindcss-upgrade/src/migrate-prettier.ts b/packages/@tailwindcss-upgrade/src/migrate-prettier.ts index 359b48881..7edf34f47 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-prettier.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-prettier.ts @@ -1,7 +1,7 @@ import fs from 'node:fs/promises' import path from 'node:path' import { pkg } from './utils/packages' -import { success } from './utils/renderer' +import { highlight, success } from './utils/renderer' export async function migratePrettierPlugin(base: string) { let packageJsonPath = path.resolve(base, 'package.json') @@ -9,7 +9,7 @@ export async function migratePrettierPlugin(base: string) { let packageJson = await fs.readFile(packageJsonPath, 'utf-8') if (packageJson.includes('prettier-plugin-tailwindcss')) { await pkg(base).add(['prettier-plugin-tailwindcss@latest']) - success(`Prettier plugin migrated to latest version.`) + success(`↳ Updated package: ${highlight('prettier-plugin-tailwindcss')}`) } } catch {} } diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index e01ba201d..a3f2e344a 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -19,7 +19,7 @@ import { migrateVariantsDirective } from './codemods/migrate-variants-directive' import type { JSConfigMigration } from './migrate-js-config' import { Stylesheet, type StylesheetConnection, type StylesheetId } from './stylesheet' import { detectConfigPath } from './template/prepare-config' -import { error } from './utils/renderer' +import { error, highlight, relative, success } from './utils/renderer' import { resolveCssId } from './utils/resolve' import { walk, WalkAction } from './utils/walk' @@ -364,16 +364,53 @@ export async function linkConfigs( // All stylesheets have a `@config` directives if (withoutAtConfig.length === 0) return - try { + // Find the config file path for each stylesheet + let configPathBySheet = new Map() + let sheetByConfigPath = new DefaultMap>(() => new Set()) + for (let sheet of withoutAtConfig) { + if (!sheet.file) continue + + let localConfigPath = configPath as string if (configPath === null) { - configPath = await detectConfigPath(base) - } else if (!path.isAbsolute(configPath)) { - configPath = path.resolve(base, configPath) + localConfigPath = await detectConfigPath(path.dirname(sheet.file), base) + } else if (!path.isAbsolute(localConfigPath)) { + localConfigPath = path.resolve(base, localConfigPath) } - // Link the `@config` directive to the root stylesheets - for (let sheet of withoutAtConfig) { - if (!sheet.file) continue + configPathBySheet.set(sheet, localConfigPath) + sheetByConfigPath.get(localConfigPath).add(sheet) + } + + let problematicStylesheets = new Set() + for (let sheets of sheetByConfigPath.values()) { + if (sheets.size > 1) { + for (let sheet of sheets) { + problematicStylesheets.add(sheet) + } + } + } + + // There are multiple "root" files without `@config` directives. Manual + // intervention is needed to link to the correct Tailwind config files. + if (problematicStylesheets.size > 1) { + for (let sheet of problematicStylesheets) { + error( + `Could not determine configuration file for: ${highlight(relative(sheet.file!, base))}\nUpdate your stylesheet to use ${highlight('@config')} to specify the correct configuration file explicitly and then run the upgrade tool again.`, + ) + } + + process.exit(1) + } + + let relativePath = relative + for (let [sheet, configPath] of configPathBySheet) { + try { + if (!sheet || !sheet.file) return + success( + `↳ Linked ${highlight(relativePath(configPath, base))} to ${highlight(relativePath(sheet.file, base))}`, + ) + + // Link the `@config` directive to the root stylesheets // Track the config file path on the stylesheet itself for easy access // without traversing the CSS ast and finding the corresponding @@ -409,10 +446,10 @@ export async function linkConfigs( target.after(atConfig) } } + } catch (e: any) { + error('Could not load the configuration file: ' + e.message) + process.exit(1) } - } catch (e: any) { - error('Could not load the configuration file: ' + e.message) - process.exit(1) } } diff --git a/packages/@tailwindcss-upgrade/src/template/prepare-config.ts b/packages/@tailwindcss-upgrade/src/template/prepare-config.ts index 5b04e8578..a151b6ab4 100644 --- a/packages/@tailwindcss-upgrade/src/template/prepare-config.ts +++ b/packages/@tailwindcss-upgrade/src/template/prepare-config.ts @@ -6,7 +6,7 @@ import { loadModule } from '../../../@tailwindcss-node/src/compile' import { resolveConfig } from '../../../tailwindcss/src/compat/config/resolve-config' import type { Config } from '../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../tailwindcss/src/design-system' -import { error } from '../utils/renderer' +import { error, highlight, relative } from '../utils/renderer' import { migratePrefix } from './codemods/prefix' const __filename = fileURLToPath(import.meta.url) @@ -94,15 +94,40 @@ const DEFAULT_CONFIG_FILES = [ './tailwind.config.cts', './tailwind.config.mts', ] -export async function detectConfigPath(base: string) { - for (let file of DEFAULT_CONFIG_FILES) { - let fullPath = path.resolve(base, file) - try { - await fs.access(fullPath) - return file - } catch {} +export async function detectConfigPath(start: string, end: string = start) { + for (let base of parentPaths(start, end)) { + for (let file of DEFAULT_CONFIG_FILES) { + let fullPath = path.resolve(base, file) + try { + await fs.access(fullPath) + return fullPath + } catch {} + } } + throw new Error( - 'No configuration file found. Please provide a path to the Tailwind CSS v3 config file via the `--config` option.', + `No configuration file found for ${highlight(relative(start))}. Please provide a path to the Tailwind CSS v3 config file via the ${highlight('--config')} option.`, ) } + +// Yields all parent paths from `from` to `to` (inclusive on both ends) +function* parentPaths(from: string, to: string = from) { + let fromAbsolute = path.resolve(from) + let toAbsolute = path.resolve(to) + + if (!fromAbsolute.startsWith(toAbsolute)) { + throw new Error(`The path ${from} is not a parent of ${to}`) + } + + if (fromAbsolute === toAbsolute) { + yield fromAbsolute + return + } + + let current = fromAbsolute + do { + yield current + current = path.dirname(current) + } while (current !== toAbsolute) + yield toAbsolute +}