From 894bf9f5ef3e72e7e2d5bf4b56e90eae24be2350 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 4 Nov 2024 17:52:11 +0100 Subject: [PATCH] Support migrating projects with multiple config files (#14863) When migrating a project from Tailwind CSS v3 to Tailwind CSS v4, then we started the migration process in the following order: 1. Migrate the JS/TS config file 2. Migrate the source files (found via the `content` option) 3. Migrate the CSS files However, if you have a setup where you have multiple CSS root files (e.g.: `frontend` and `admin` are separated), then that typically means that you have an `@config` directive in your CSS files. These point to the Tailwind CSS config file. This PR changes the migration order to do the following: 1. Build a tree of all the CSS files 2. For each `@config` directive, migrate the JS/TS config file 3. For each JS/TS config file, migrate the source files If a CSS file does not contain any `@config` directives, then we start by filling in the `@config` directive with the default Tailwind CSS config file (if found, or the one passed in). If no default config file or passed in config file can be found, then we will error out (just like we do now) --------- Co-authored-by: Adam Wathan --- CHANGELOG.md | 1 + integrations/upgrade/index.test.ts | 54 +++++- integrations/upgrade/js-config.test.ts | 152 +++++++++++++++ .../src/utils/renderer.test.ts | 2 +- packages/@tailwindcss-postcss/src/index.ts | 4 +- .../src/codemods/migrate-config.ts | 23 +-- .../@tailwindcss-upgrade/src/index.test.ts | 6 +- packages/@tailwindcss-upgrade/src/index.ts | 133 +++++++++----- .../src/migrate-js-config.ts | 28 ++- packages/@tailwindcss-upgrade/src/migrate.ts | 173 +++++++++++++++++- .../@tailwindcss-upgrade/src/stylesheet.ts | 11 ++ .../src/template/prepare-config.ts | 22 +-- .../src/utils/renderer.ts | 8 +- packages/@tailwindcss-vite/src/index.ts | 2 +- .../tailwindcss/src/compat/config/types.ts | 2 +- packages/tailwindcss/tests/ui.spec.ts | 4 +- 16 files changed, 533 insertions(+), 92 deletions(-) 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'