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'