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
+}