mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
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 <adam.wathan@gmail.com>
This commit is contained in:
parent
df6dfb012c
commit
894bf9f5ef
@ -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
|
||||
|
||||
|
||||
@ -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`
|
||||
<h1>🤠👋</h1>
|
||||
<div class="!flex"></div>
|
||||
`,
|
||||
'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 ---
|
||||
<h1>🤠👋</h1>
|
||||
<div class="!flex"></div>
|
||||
|
||||
--- ./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`
|
||||
<div class="tw__bg-gradient-to-t"></div>
|
||||
`,
|
||||
@ -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 */
|
||||
|
||||
@ -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`<div class="!text-primary"></div>`,
|
||||
|
||||
// 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`<div class="!text-primary"></div>`,
|
||||
},
|
||||
},
|
||||
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',
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<string>()
|
||||
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<Stylesheet, Awaited<ReturnType<typeof prepareConfig>>>()
|
||||
let jsConfigMigrationBySheet = new Map<
|
||||
Stylesheet,
|
||||
Awaited<ReturnType<typeof migrateJsConfig>>
|
||||
>()
|
||||
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<string>()
|
||||
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.')
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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<Stylesheet>()
|
||||
|
||||
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<Stylesheet, Set<Stylesheet>>(() => new Set<Stylesheet>())
|
||||
|
||||
// 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<StylesheetId, Stylesheet>()
|
||||
let stylesheetsByFile = new Map<string, Stylesheet>()
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 = []
|
||||
|
||||
|
||||
@ -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/
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user