diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7d8ca4aa29..204dccc5d7d1 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 ebc251a66c92..32fcd1b83293 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -1385,9 +1385,7 @@ 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 ], } `, @@ -1396,20 +1394,28 @@ test( class="!flex sm:!block bg-gradient-to-t bg-[--my-red]" > `, - 'src/root.1.css': css` + 'src/root.1/index.css': css` /* Inject missing @config */ @tailwind base; @tailwind components; @tailwind utilities; `, - 'src/root.2.css': css` + '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"; + @config "../../tailwind.config.ts"; `, - 'src/root.3.css': css` + 'src/root.3/index.css': css` /* Inject missing @config above first @theme */ @tailwind base; @tailwind components; @@ -1425,18 +1431,35 @@ test( --color-blue-500: #00f; } `, - 'src/root.4.css': css` + '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 './root.4/base.css'; - @import './root.4/utilities.css'; + @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.css': css`@import './root.5/tailwind.css';`, + '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 */ - @import 'tailwindcss/tailwind.css'; + /* Should be located in the root: ../../ */ + @import 'tailwindcss'; `, }, }, @@ -1450,11 +1473,11 @@ test( class="flex! sm:block! bg-linear-to-t bg-(--my-red)" > - --- ./src/root.1.css --- + --- ./src/root.1/index.css --- /* Inject missing @config */ @import 'tailwindcss'; - @config '../tailwind.config.ts'; + @config './tailwind.config.ts'; /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, @@ -1474,11 +1497,11 @@ test( } } - --- ./src/root.2.css --- + --- ./src/root.2/index.css --- /* Already contains @config */ @import 'tailwindcss'; - @config "../tailwind.config.ts"; + @config "../../tailwind.config.ts"; /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, @@ -1498,11 +1521,11 @@ test( } } - --- ./src/root.3.css --- + --- ./src/root.3/index.css --- /* Inject missing @config above first @theme */ @import 'tailwindcss'; - @config '../tailwind.config.ts'; + @config './tailwind.config.ts'; @variant hocus (&:hover, &:focus); @@ -1532,15 +1555,12 @@ test( } } - --- ./src/root.4.css --- + --- ./src/root.4/index.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'; + @import './base.css'; + @import './utilities.css'; - --- ./src/root.5.css --- - @import './root.5/tailwind.css'; + @config './tailwind.config.ts'; --- ./src/root.4/base.css --- @import 'tailwindcss/theme' layer(theme); @@ -1567,8 +1587,12 @@ test( --- ./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'; @@ -1595,6 +1619,84 @@ test( }, ) +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 + ], + } + `, + 'src/index.html': html` +
+ `, + 'src/root.1.css': css` + /* Inject missing @config */ + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + 'src/root.2.css': css` + /* Already contains @config */ + @tailwind base; + @tailwind components; + @tailwind utilities; + @config "../tailwind.config.ts"; + `, + 'src/root.3.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.4.css': css` + /* Inject missing @config due to nested imports with tailwind imports */ + @import './root.4/base.css'; + @import './root.4/utilities.css'; + `, + 'src/root.4/base.css': css`@import 'tailwindcss/base';`, + 'src/root.4/utilities.css': css`@import 'tailwindcss/utilities';`, + + '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/tailwind.css'; + `, + }, + }, + async ({ exec }) => { + let output = await exec('npx @tailwindcss/upgrade --force', {}, { ignoreStdErr: true }).catch( + (e) => e.toString(), + ) + + expect(output).toMatch('Could not determine configuration file for:') + }, +) + test( 'injecting `@config` in the shared root, when a tailwind.config.{js,ts,…} is detected', { @@ -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 44ee3c6fc66d..7ac7a1147839 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 1e789a348575..3f57ef1987b1 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 4ac4fe8d367a..b69379df1f7d 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 1f4d6c46868b..1bfb2caa80f1 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 359b4888180d..7edf34f47a36 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 e01ba201defe..a3f2e344adda 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) + } + + 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.`, + ) } - // Link the `@config` directive to the root stylesheets - for (let sheet of withoutAtConfig) { - if (!sheet.file) continue + 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 5b04e8578cf0..a151b6ab46c5 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 +}