diff --git a/CHANGELOG.md b/CHANGELOG.md
index 53e856289..9ecdb4a1b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- _Upgrade (experimental)_: Migrate `grid-cols-[subgrid]` and `grid-rows-[subgrid]` to `grid-cols-subgrid` and `grid-rows-subgrid` ([#14840](https://github.com/tailwindlabs/tailwindcss/pull/14840))
+- _Upgrade (experimental)_: Support migrating projects with multiple config files ([#14863](https://github.com/tailwindlabs/tailwindcss/pull/14863))
### Fixed
diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts
index 4dd581b0c..cf7de5b8c 100644
--- a/integrations/upgrade/index.test.ts
+++ b/integrations/upgrade/index.test.ts
@@ -1,6 +1,51 @@
import { expect } from 'vitest'
import { candidate, css, html, js, json, test } from '../utils'
+test(
+ 'error when no CSS file with @tailwind is used',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "dependencies": {
+ "@tailwindcss/upgrade": "workspace:^"
+ },
+ "devDependencies": {
+ "@tailwindcss/cli": "workspace:^"
+ }
+ }
+ `,
+ 'tailwind.config.js': js`
+ /** @type {import('tailwindcss').Config} */
+ module.exports = {
+ content: ['./src/**/*.{html,js}'],
+ }
+ `,
+ 'src/index.html': html`
+
🤠👋
+
+ `,
+ 'src/fonts.css': css`/* Unrelated CSS file */`,
+ },
+ },
+ async ({ fs, exec }) => {
+ let output = await exec('npx @tailwindcss/upgrade')
+ expect(output).toContain('Cannot find any CSS files that reference Tailwind CSS.')
+
+ // Files should not be modified
+ expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
+ "
+ --- ./src/index.html ---
+ 🤠👋
+
+
+ --- ./src/fonts.css ---
+ /* Unrelated CSS file */
+ "
+ `)
+ },
+)
+
test(
`upgrades a v3 project to v4`,
{
@@ -858,6 +903,11 @@ test(
prefix: 'tw__',
}
`,
+ 'src/index.css': css`
+ @tailwind base;
+ @tailwind components;
+ @tailwind utilities;
+ `,
'src/index.html': html`
`,
@@ -1304,7 +1354,7 @@ test(
@tailwind base;
@tailwind components;
@tailwind utilities;
- @config "../tailwind.config.js";
+ @config "../tailwind.config.ts";
`,
'src/root.3.css': css`
/* Inject missing @config above first @theme */
@@ -1421,7 +1471,7 @@ test(
border-width: 0;
}
}
- @config "../tailwind.config.js";
+ @config "../tailwind.config.ts";
--- ./src/root.3.css ---
/* Inject missing @config above first @theme */
diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts
index 592ced85a..47214ae9e 100644
--- a/integrations/upgrade/js-config.test.ts
+++ b/integrations/upgrade/js-config.test.ts
@@ -783,6 +783,158 @@ test(
},
)
+test(
+ 'multi-root project',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "dependencies": {
+ "@tailwindcss/upgrade": "workspace:^"
+ }
+ }
+ `,
+
+ // Project A
+ 'project-a/tailwind.config.ts': ts`
+ export default {
+ content: {
+ relative: true,
+ files: ['./src/**/*.html'],
+ },
+ theme: {
+ extend: {
+ colors: {
+ primary: 'red',
+ },
+ },
+ },
+ }
+ `,
+ 'project-a/src/input.css': css`
+ @tailwind base;
+ @tailwind components;
+ @tailwind utilities;
+ @config "../tailwind.config.ts";
+ `,
+ 'project-a/src/index.html': html``,
+
+ // Project B
+ 'project-b/tailwind.config.ts': ts`
+ export default {
+ content: {
+ relative: true,
+ files: ['./src/**/*.html'],
+ },
+ theme: {
+ extend: {
+ colors: {
+ primary: 'blue',
+ },
+ },
+ },
+ }
+ `,
+ 'project-b/src/input.css': css`
+ @tailwind base;
+ @tailwind components;
+ @tailwind utilities;
+ @config "../tailwind.config.ts";
+ `,
+ 'project-b/src/index.html': html``,
+ },
+ },
+ async ({ exec, fs }) => {
+ await exec('npx @tailwindcss/upgrade')
+
+ expect(await fs.dumpFiles('project-{a,b}/**/*.{css,ts}')).toMatchInlineSnapshot(`
+ "
+ --- project-a/src/input.css ---
+ @import 'tailwindcss';
+
+ /*
+ 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);
+ }
+ }
+
+ /*
+ Form elements have a 1px border by default 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 \`border-0\` to
+ any form elements that shouldn't have a border.
+ */
+ @layer base {
+ input:where(:not([type='button'], [type='reset'], [type='submit'])),
+ select,
+ textarea {
+ border-width: 0;
+ }
+ }
+
+ @theme {
+ --color-primary: red;
+ }
+
+ --- project-b/src/input.css ---
+ @import 'tailwindcss';
+
+ /*
+ 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);
+ }
+ }
+
+ /*
+ Form elements have a 1px border by default 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 \`border-0\` to
+ any form elements that shouldn't have a border.
+ */
+ @layer base {
+ input:where(:not([type='button'], [type='reset'], [type='submit'])),
+ select,
+ textarea {
+ border-width: 0;
+ }
+ }
+
+ @theme {
+ --color-primary: blue;
+ }
+ "
+ `)
+ },
+)
+
describe('border compatibility', () => {
test(
'migrate border compatibility',
diff --git a/packages/@tailwindcss-cli/src/utils/renderer.test.ts b/packages/@tailwindcss-cli/src/utils/renderer.test.ts
index 0df4cde83..b9950655f 100644
--- a/packages/@tailwindcss-cli/src/utils/renderer.test.ts
+++ b/packages/@tailwindcss-cli/src/utils/renderer.test.ts
@@ -1,4 +1,4 @@
-import path from 'path'
+import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { relative, wordWrap } from './renderer'
import { normalizeWindowsSeperators } from './test-helpers'
diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts
index f040b611b..d9ff2d2db 100644
--- a/packages/@tailwindcss-postcss/src/index.ts
+++ b/packages/@tailwindcss-postcss/src/index.ts
@@ -2,9 +2,9 @@ import QuickLRU from '@alloc/quick-lru'
import { compile, env } from '@tailwindcss/node'
import { clearRequireCache } from '@tailwindcss/node/require-cache'
import { Scanner } from '@tailwindcss/oxide'
-import fs from 'fs'
import { Features, transform } from 'lightningcss'
-import path from 'path'
+import fs from 'node:fs'
+import path from 'node:path'
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
import fixRelativePathsPlugin from './postcss-fix-relative-paths'
diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts
index 99a1b7259..b6a43eec2 100644
--- a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts
+++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts
@@ -31,24 +31,13 @@ export function migrateConfig(
let cssConfig = new AtRule()
- if (jsConfigMigration === null) {
- // Skip if there is already a `@config` directive
- {
- let hasConfig = false
- root.walkAtRules('config', () => {
- hasConfig = true
- return false
- })
- if (hasConfig) return
- }
+ // Remove the `@config` directive if it exists and we couldn't migrate the
+ // config file.
+ if (jsConfigMigration !== null) {
+ root.walkAtRules('config', (node) => {
+ node.remove()
+ })
- cssConfig.append(
- new AtRule({
- name: 'config',
- params: `'${relativeToStylesheet(sheet, configFilePath)}'`,
- }),
- )
- } else {
let css = '\n\n'
for (let source of jsConfigMigration.sources) {
let absolute = path.resolve(source.base, source.pattern)
diff --git a/packages/@tailwindcss-upgrade/src/index.test.ts b/packages/@tailwindcss-upgrade/src/index.test.ts
index 28b5a36cf..f3c06d61a 100644
--- a/packages/@tailwindcss-upgrade/src/index.test.ts
+++ b/packages/@tailwindcss-upgrade/src/index.test.ts
@@ -95,8 +95,6 @@ it('should migrate a stylesheet', async () => {
).toMatchInlineSnapshot(`
"@import 'tailwindcss';
- @config './tailwind.config.js';
-
/*
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
@@ -216,8 +214,7 @@ it('should migrate a stylesheet (with imports)', async () => {
textarea {
border-width: 0;
}
- }
- @config './tailwind.config.js';"
+ }"
`)
})
@@ -242,7 +239,6 @@ it('should migrate a stylesheet (with preceding rules that should be wrapped in
@layer foo, bar, baz;
/**! My license comment */
@import 'tailwindcss';
- @config './tailwind.config.js';
/*
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts
index a0434b92f..a059c7461 100644
--- a/packages/@tailwindcss-upgrade/src/index.ts
+++ b/packages/@tailwindcss-upgrade/src/index.ts
@@ -8,6 +8,7 @@ import { formatNodes } from './codemods/format-nodes'
import { help } from './commands/help'
import {
analyze as analyzeStylesheets,
+ linkConfigs as linkConfigsToStylesheets,
migrate as migrateStylesheet,
split as splitStylesheets,
} from './migrate'
@@ -44,6 +45,8 @@ async function run() {
eprintln(header())
eprintln()
+ let cleanup: (() => void)[] = []
+
if (!flags['--force']) {
if (isRepoDirty()) {
error('Git directory is not clean. Please stash or commit your changes before migrating.')
@@ -54,42 +57,6 @@ async function run() {
}
}
- let config = await prepareConfig(flags['--config'], { base })
-
- {
- // Template migrations
-
- info('Migrating templates using the provided configuration file.')
-
- let set = new Set()
- for (let { pattern, base } of config.globs) {
- let files = await globby([pattern], {
- absolute: true,
- gitignore: true,
- cwd: base,
- })
-
- for (let file of files) {
- set.add(file)
- }
- }
-
- let files = Array.from(set)
- files.sort()
-
- // Migrate each file
- await Promise.allSettled(
- files.map((file) => migrateTemplate(config.designSystem, config.userConfig, file)),
- )
-
- success('Template migration complete.')
- }
-
- // Migrate JS config
-
- info('Migrating JavaScript configuration files using the provided configuration file.')
- let jsConfigMigration = await migrateJsConfig(config.designSystem, config.configFilePath, base)
-
{
// Stylesheet migrations
@@ -132,9 +99,91 @@ async function run() {
error(`${e}`)
}
- // Migrate each file
+ // Ensure stylesheets are linked to configs
+ try {
+ await linkConfigsToStylesheets(stylesheets, {
+ configPath: flags['--config'],
+ base,
+ })
+ } catch (e: unknown) {
+ error(`${e}`)
+ }
+
+ // Migrate js config files, linked to stylesheets
+ info('Migrating JavaScript configuration files using the provided configuration file.')
+ let configBySheet = new Map>>()
+ let jsConfigMigrationBySheet = new Map<
+ Stylesheet,
+ Awaited>
+ >()
+ for (let sheet of stylesheets) {
+ if (!sheet.isTailwindRoot) continue
+
+ let config = await prepareConfig(sheet.linkedConfigPath, { base })
+ configBySheet.set(sheet, config)
+
+ let jsConfigMigration = await migrateJsConfig(
+ config.designSystem,
+ config.configFilePath,
+ base,
+ )
+ jsConfigMigrationBySheet.set(sheet, jsConfigMigration)
+
+ if (jsConfigMigration !== null) {
+ // Remove the JS config if it was fully migrated
+ cleanup.push(() => fs.rm(config.configFilePath))
+ }
+ }
+
+ // Migrate source files, linked to config files
+ {
+ // Template migrations
+
+ info('Migrating templates using the provided configuration file.')
+ for (let config of configBySheet.values()) {
+ let set = new Set()
+ for (let { pattern, base } of config.globs) {
+ let files = await globby([pattern], {
+ absolute: true,
+ gitignore: true,
+ cwd: base,
+ })
+
+ for (let file of files) {
+ set.add(file)
+ }
+ }
+
+ let files = Array.from(set)
+ files.sort()
+
+ // Migrate each file
+ await Promise.allSettled(
+ files.map((file) => migrateTemplate(config.designSystem, config.userConfig, file)),
+ )
+ }
+
+ success('Template migration complete.')
+ }
+
+ // Migrate each CSS file
let migrateResults = await Promise.allSettled(
- stylesheets.map((sheet) => migrateStylesheet(sheet, { ...config, jsConfigMigration })),
+ stylesheets.map((sheet) => {
+ let config = configBySheet.get(sheet)!
+ let jsConfigMigration = jsConfigMigrationBySheet.get(sheet)!
+
+ if (!config) {
+ for (let parent of sheet.ancestors()) {
+ if (parent.isTailwindRoot) {
+ config ??= configBySheet.get(parent)!
+ jsConfigMigration ??= jsConfigMigrationBySheet.get(parent)!
+ break
+ }
+ }
+ }
+
+ return migrateStylesheet(sheet, { ...config, jsConfigMigration })
+ }),
)
for (let result of migrateResults) {
@@ -197,16 +246,14 @@ async function run() {
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'])
} catch {}
- // Remove the JS config if it was fully migrated
- if (jsConfigMigration !== null) {
- await fs.rm(config.configFilePath)
- }
-
// 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 60bcbcefd..047a6bfb6 100644
--- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts
+++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts
@@ -1,9 +1,9 @@
import { Scanner } from '@tailwindcss/oxide'
import fs from 'node:fs/promises'
-import { dirname } from 'path'
+import { dirname, resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
-import { fileURLToPath } from 'url'
import { loadModule } from '../../@tailwindcss-node/src/compile'
import { toCss, type AstNode } from '../../tailwindcss/src/ast'
import {
@@ -56,7 +56,7 @@ export async function migrateJsConfig(
}
if ('content' in unresolvedConfig) {
- sources = await migrateContent(unresolvedConfig as any, base)
+ sources = await migrateContent(unresolvedConfig as any, fullConfigPath, base)
}
if ('theme' in unresolvedConfig) {
@@ -173,13 +173,31 @@ function createSectionKey(key: string[]): string {
}
async function migrateContent(
- unresolvedConfig: Config & { content: any },
+ unresolvedConfig: Config,
+ configPath: string,
base: string,
): Promise<{ base: string; pattern: string }[]> {
let autoContentFiles = autodetectedSourceFiles(base)
let sources = []
- for (let content of unresolvedConfig.content) {
+ let contentIsRelative = (() => {
+ if (!unresolvedConfig.content) return false
+ if (Array.isArray(unresolvedConfig.content)) return false
+ if (unresolvedConfig.content.relative) return true
+ if (unresolvedConfig.future === 'all') return false
+ return unresolvedConfig.future?.relativeContentPathsByDefault ?? false
+ })()
+
+ let contentFiles = Array.isArray(unresolvedConfig.content)
+ ? unresolvedConfig.content
+ : (unresolvedConfig.content?.files ?? []).map((content) => {
+ if (typeof content === 'string' && contentIsRelative) {
+ return resolve(dirname(configPath), content)
+ }
+ return content
+ })
+
+ for (let content of contentFiles) {
if (typeof content !== 'string') {
throw new Error('Unsupported content value: ' + content)
}
diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts
index fbad6bc50..14c9ec158 100644
--- a/packages/@tailwindcss-upgrade/src/migrate.ts
+++ b/packages/@tailwindcss-upgrade/src/migrate.ts
@@ -1,3 +1,4 @@
+import { normalizePath } from '@tailwindcss/node'
import path from 'node:path'
import postcss from 'postcss'
import type { Config } from 'tailwindcss'
@@ -16,6 +17,8 @@ import { migrateThemeToVar } from './codemods/migrate-theme-to-var'
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 { resolveCssId } from './utils/resolve'
import { walk, WalkAction } from './utils/walk'
@@ -194,7 +197,99 @@ export async function analyze(stylesheets: Stylesheet[]) {
}
}
- if (lines.length === 0) return
+ if (lines.length === 0) {
+ let tailwindRootLeafs = new Set()
+
+ for (let sheet of stylesheets) {
+ // If the current file already contains `@config`, then we can assume it's
+ // a Tailwind CSS root file.
+ sheet.root.walkAtRules('config', () => {
+ sheet.isTailwindRoot = true
+ return false
+ })
+ if (sheet.isTailwindRoot) continue
+
+ // If an `@tailwind` at-rule, or `@import "tailwindcss"` is present,
+ // then we can assume it's a file where Tailwind CSS might be configured.
+ //
+ // However, if 2 or more stylesheets exist with these rules that share a
+ // common parent, then we want to mark the common parent as the root
+ // stylesheet instead.
+ sheet.root.walkAtRules((node) => {
+ if (
+ node.name === 'tailwind' ||
+ (node.name === 'import' && node.params.match(/^["']tailwindcss["']/)) ||
+ (node.name === 'import' && node.params.match(/^["']tailwindcss\/.*?["']$/))
+ ) {
+ sheet.isTailwindRoot = true
+ tailwindRootLeafs.add(sheet)
+ }
+ })
+ }
+
+ // Only a single Tailwind CSS root file exists, no need to do anything else.
+ if (tailwindRootLeafs.size <= 1) {
+ return
+ }
+
+ // Mark the common parent as the root file
+ {
+ // Group each sheet from tailwindRootLeafs by their common parent
+ let commonParents = new DefaultMap>(() => new Set())
+
+ // Seed common parents with leafs
+ for (let sheet of tailwindRootLeafs) {
+ commonParents.get(sheet).add(sheet)
+ }
+
+ // If any 2 common parents come from the same tree, then all children of
+ // parent A and parent B will be moved to the parent of parent A and
+ // parent B. Parent A and parent B will be removed.
+ let repeat = true
+ while (repeat) {
+ repeat = false
+
+ outer: for (let [sheetA, childrenA] of commonParents) {
+ for (let [sheetB, childrenB] of commonParents) {
+ if (sheetA === sheetB) continue
+
+ for (let parentA of sheetA.ancestors()) {
+ for (let parentB of sheetB.ancestors()) {
+ if (parentA !== parentB) continue
+
+ commonParents.delete(sheetA)
+ commonParents.delete(sheetB)
+
+ for (let child of childrenA) {
+ commonParents.get(parentA).add(child)
+ }
+
+ for (let child of childrenB) {
+ commonParents.get(parentA).add(child)
+ }
+
+ repeat = true
+ break outer
+ }
+ }
+ }
+ }
+ }
+
+ // Mark the common parent as the Tailwind CSS root file, and remove the
+ // flag from each leaf.
+ for (let [parent, children] of commonParents) {
+ parent.isTailwindRoot = true
+
+ for (let child of children) {
+ if (parent === child) continue
+
+ child.isTailwindRoot = false
+ }
+ }
+ return
+ }
+ }
let error = `You have one or more stylesheets that are imported into a utility layer and non-utility layer.\n`
error += `We cannot convert stylesheets under these conditions. Please look at the following stylesheets:\n`
@@ -202,6 +297,82 @@ export async function analyze(stylesheets: Stylesheet[]) {
throw new Error(error + lines.join('\n'))
}
+export async function linkConfigs(
+ stylesheets: Stylesheet[],
+ { configPath, base }: { configPath: string | null; base: string },
+) {
+ let rootStylesheets = stylesheets.filter((sheet) => sheet.isTailwindRoot)
+ if (rootStylesheets.length === 0) {
+ throw new Error(
+ 'Cannot find any CSS files that reference Tailwind CSS.\nBefore your project can be upgraded you need to create a CSS file that imports Tailwind CSS or uses `@tailwind`.',
+ )
+ }
+ let withoutAtConfig = rootStylesheets.filter((sheet) => {
+ let hasConfig = false
+ sheet.root.walkAtRules('config', (node) => {
+ let configPath = path.resolve(path.dirname(sheet.file!), node.params.slice(1, -1))
+ sheet.linkedConfigPath = configPath
+ hasConfig = true
+ return false
+ })
+ return !hasConfig
+ })
+
+ // All stylesheets have a `@config` directives
+ if (withoutAtConfig.length === 0) return
+
+ try {
+ if (configPath === null) {
+ configPath = await detectConfigPath(base)
+ } else if (!path.isAbsolute(configPath)) {
+ configPath = path.resolve(base, configPath)
+ }
+
+ // Link the `@config` directive to the root stylesheets
+ for (let sheet of withoutAtConfig) {
+ if (!sheet.file) continue
+
+ // Track the config file path on the stylesheet itself for easy access
+ // without traversing the CSS ast and finding the corresponding
+ // `@config` later.
+ sheet.linkedConfigPath = configPath
+
+ // Create a relative path from the current file to the config file.
+ let relative = path.relative(path.dirname(sheet.file), configPath)
+
+ // If the path points to a file in the same directory, `path.relative` will
+ // remove the leading `./` and we need to add it back in order to still
+ // consider the path relative
+ if (!relative.startsWith('.')) {
+ relative = './' + relative
+ }
+
+ relative = normalizePath(relative)
+
+ // Add the `@config` directive to the root stylesheet.
+ {
+ let target = sheet.root as postcss.Root | postcss.AtRule
+ let atConfig = postcss.atRule({ name: 'config', params: `'${relative}'` })
+
+ sheet.root.walkAtRules((node) => {
+ if (node.name === 'tailwind' || node.name === 'import') {
+ target = node
+ }
+ })
+
+ if (target.type === 'root') {
+ sheet.root.prepend(atConfig)
+ } else if (target.type === 'atrule') {
+ target.after(atConfig)
+ }
+ }
+ }
+ } catch (e: any) {
+ error('Could not load the configuration file: ' + e.message)
+ process.exit(1)
+ }
+}
+
export async function split(stylesheets: Stylesheet[]) {
let stylesheetsById = new Map()
let stylesheetsByFile = new Map()
diff --git a/packages/@tailwindcss-upgrade/src/stylesheet.ts b/packages/@tailwindcss-upgrade/src/stylesheet.ts
index 1bea7ad76..dc810903c 100644
--- a/packages/@tailwindcss-upgrade/src/stylesheet.ts
+++ b/packages/@tailwindcss-upgrade/src/stylesheet.ts
@@ -25,6 +25,17 @@ export class Stylesheet {
*/
root: postcss.Root
+ /**
+ * Whether or not this stylesheet is a Tailwind CSS root stylesheet.
+ */
+ isTailwindRoot = false
+
+ /**
+ * The Tailwind config path that is linked to this stylesheet. Essentially the
+ * contents of `@config`.
+ */
+ linkedConfigPath: string | null = null
+
/**
* The path to the file that this stylesheet was loaded from.
*
diff --git a/packages/@tailwindcss-upgrade/src/template/prepare-config.ts b/packages/@tailwindcss-upgrade/src/template/prepare-config.ts
index 174962ceb..e63e186d4 100644
--- a/packages/@tailwindcss-upgrade/src/template/prepare-config.ts
+++ b/packages/@tailwindcss-upgrade/src/template/prepare-config.ts
@@ -1,9 +1,8 @@
import { __unstable__loadDesignSystem, compile } from '@tailwindcss/node'
import fs from 'node:fs/promises'
-import path from 'node:path'
-import { dirname } from 'path'
+import path, { dirname } from 'node:path'
+import { fileURLToPath } from 'node:url'
import type { Config } from 'tailwindcss'
-import { fileURLToPath } from 'url'
import { loadModule } from '../../../@tailwindcss-node/src/compile'
import { resolveConfig } from '../../../tailwindcss/src/compat/config/resolve-config'
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
@@ -16,7 +15,7 @@ const __dirname = dirname(__filename)
const css = String.raw
export async function prepareConfig(
- configPath: string | null,
+ configFilePath: string | null,
options: { base: string },
): Promise<{
designSystem: DesignSystem
@@ -27,15 +26,16 @@ export async function prepareConfig(
newPrefix: string | null
}> {
try {
- if (configPath === null) {
- configPath = await detectConfigPath(options.base)
+ if (configFilePath === null) {
+ configFilePath = await detectConfigPath(options.base)
+ } else if (!path.isAbsolute(configFilePath)) {
+ configFilePath = path.resolve(options.base, configFilePath)
}
// We create a relative path from the current file to the config file. This is
// required so that the base for Tailwind CSS can bet inside the
// @tailwindcss-upgrade package and we can require `tailwindcss` properly.
- let fullConfigPath = path.resolve(options.base, configPath)
- let relative = path.relative(__dirname, fullConfigPath)
+ let relative = path.relative(__dirname, configFilePath)
// If the path points to a file in the same directory, `path.relative` will
// remove the leading `./` and we need to add it back in order to still
@@ -44,7 +44,7 @@ export async function prepareConfig(
relative = './' + relative
}
- let userConfig = await createResolvedUserConfig(fullConfigPath)
+ let userConfig = await createResolvedUserConfig(configFilePath)
let newPrefix = userConfig.prefix ? migratePrefix(userConfig.prefix) : null
let input = css`
@@ -62,7 +62,7 @@ export async function prepareConfig(
globs: compiler.globs,
userConfig,
newPrefix,
- configFilePath: fullConfigPath,
+ configFilePath,
}
} catch (e: any) {
error('Could not load the configuration file: ' + e.message)
@@ -94,7 +94,7 @@ const DEFAULT_CONFIG_FILES = [
'./tailwind.config.cts',
'./tailwind.config.mts',
]
-async function detectConfigPath(base: string) {
+export async function detectConfigPath(base: string) {
for (let file of DEFAULT_CONFIG_FILES) {
let fullPath = path.resolve(base, file)
try {
diff --git a/packages/@tailwindcss-upgrade/src/utils/renderer.ts b/packages/@tailwindcss-upgrade/src/utils/renderer.ts
index b9b9245f7..b902fe88d 100644
--- a/packages/@tailwindcss-upgrade/src/utils/renderer.ts
+++ b/packages/@tailwindcss-upgrade/src/utils/renderer.ts
@@ -40,7 +40,13 @@ export function relative(
/**
* Wrap `text` into multiple lines based on the `width`.
*/
-export function wordWrap(text: string, width: number) {
+export function wordWrap(text: string, width: number): string[] {
+ // Handle text with newlines by maintaining the newlines, then splitting
+ // each line separately.
+ if (text.includes('\n')) {
+ return text.split('\n').flatMap((line) => wordWrap(line, width))
+ }
+
let words = text.split(' ')
let lines = []
diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts
index 57eaf42af..644228891 100644
--- a/packages/@tailwindcss-vite/src/index.ts
+++ b/packages/@tailwindcss-vite/src/index.ts
@@ -3,7 +3,7 @@ import { clearRequireCache } from '@tailwindcss/node/require-cache'
import { Scanner } from '@tailwindcss/oxide'
import { Features, transform } from 'lightningcss'
import fs from 'node:fs/promises'
-import path from 'path'
+import path from 'node:path'
import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite'
const SPECIAL_QUERY_RE = /[?&](raw|url)\b/
diff --git a/packages/tailwindcss/src/compat/config/types.ts b/packages/tailwindcss/src/compat/config/types.ts
index 9d01052e0..dbcb30de3 100644
--- a/packages/tailwindcss/src/compat/config/types.ts
+++ b/packages/tailwindcss/src/compat/config/types.ts
@@ -25,7 +25,7 @@ export interface ResolvedConfig {
type ContentFile = string | { raw: string; extension?: string }
export interface UserConfig {
- content?: ContentFile[] | { files: ContentFile[] }
+ content?: ContentFile[] | { relative?: boolean; files: ContentFile[] }
}
type ResolvedContent = { base: string; pattern: string } | { raw: string; extension?: string }
diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts
index defd5d63e..0f916f593 100644
--- a/packages/tailwindcss/tests/ui.spec.ts
+++ b/packages/tailwindcss/tests/ui.spec.ts
@@ -1,7 +1,7 @@
import { expect, test, type Page } from '@playwright/test'
import { Scanner } from '@tailwindcss/oxide'
-import fs from 'fs'
-import path from 'path'
+import fs from 'node:fs'
+import path from 'node:path'
import { compile } from '../src'
import { optimizeCss } from '../src/test-utils/run'