Improve multi-root @config linking (#15001)
This PR improves the discoverability of Tailwind config files when we are trying to link them to your CSS files. When you have multiple "root" CSS files in your project, and if they don't include an `@config` directive, then we tried to find the Tailwind config file in your current working directory. This means that if you run the upgrade command from the root of your project, and you have a nested folder with a separate Tailwind setup, then the nested CSS file would link to the root Tailwind config file. Visually, you can think of it like this: ``` . ├── admin │ ├── src │ │ └── styles │ │ └── index.css <-- This will be linked to (1) │ └── tailwind.config.js (2) ├── src │ └── styles │ └── index.css <-- This will be linked to (1) └── tailwind.config.js (1) ``` If you run the upgrade command from the root of your project, then the `/src/styles/index.css` will be linked to `/tailwind.config.js` which is what we expect. But `/admin/src/styles/index.css` will _also_ be linked to `/tailwind.config.js` With this PR we improve this behavior by looking at the CSS file, and crawling up the parent tree. This mens that the new behavior looks like this: ``` . ├── admin │ ├── src │ │ └── styles │ │ └── index.css <-- This will be linked to (2) │ └── tailwind.config.js (2) ├── src │ └── styles │ └── index.css <-- This will be linked to (1) └── tailwind.config.js (1) ``` Now `/src/styles/index.css` will be linked to `/tailwind.config.js`, and `/admin/src/styles/index.css` will be linked to `/admin/tailwind.config.js`. When we discover the Tailwind config file, we will also print a message to the user to let them know which CSS file is linked to which Tailwind config file. This should be a safe improvement because if your Tailwind config file had a different name, or if it lived in a sibling folder then Tailwind wouldn't find it either and you already required a `@config "…";` directive in your CSS file to point to the correct file. In the unlikely event that it turns out that 2 (or more) CSS files resolve to the same to the same Tailwind config file, then an upgrade might not be safe and some manual intervention might be needed. In this case, we will show a warning about this. <img width="1552" alt="image" src="https://github.com/user-attachments/assets/7a1ad11d-18c5-4b7d-9a02-14f0116ae955"> Test plan: --- - Added an integration test that properly links the nearest Tailwind config file by looking up the tree - Added an integration test that resolves 2 or more CSS files to the same config file, resulting in an error where manual intervention is needed - Ran it on the Tailwind UI codebase Running this on Tailwind UI's codebase it looks like this: <img width="1552" alt="image" src="https://github.com/user-attachments/assets/21785428-5e0d-47f7-80ec-dab497f58784"> --------- Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This commit is contained in:
parent
dd3441bf82
commit
08c6c96f02
9 changed files with 416 additions and 233 deletions
|
|
@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- Ensure `flex` is suggested ([#15014](https://github.com/tailwindlabs/tailwindcss/pull/15014))
|
||||
- _Upgrade (experimental)_: Resolve imports when specifying a CSS entry point on the command-line ([#15010](https://github.com/tailwindlabs/tailwindcss/pull/15010))
|
||||
- _Upgrade (experimental)_: Resolve nearest Tailwind config file when CSS file does not contain `@config` ([#15001](https://github.com/tailwindlabs/tailwindcss/pull/15001))
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -1385,9 +1385,257 @@ test(
|
|||
export default {
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
plugins: [
|
||||
() => {
|
||||
// custom stuff which is too complicated to migrate to CSS
|
||||
},
|
||||
() => {}, // custom stuff which is too complicated to migrate to CSS
|
||||
],
|
||||
}
|
||||
`,
|
||||
'src/index.html': html`
|
||||
<div
|
||||
class="!flex sm:!block bg-gradient-to-t bg-[--my-red]"
|
||||
></div>
|
||||
`,
|
||||
'src/root.1/index.css': css`
|
||||
/* Inject missing @config */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
'src/root.1/tailwind.config.ts': js`
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
plugins: [
|
||||
() => {}, // custom stuff which is too complicated to migrate to CSS
|
||||
],
|
||||
}
|
||||
`,
|
||||
'src/root.2/index.css': css`
|
||||
/* Already contains @config */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@config "../../tailwind.config.ts";
|
||||
`,
|
||||
'src/root.3/index.css': css`
|
||||
/* Inject missing @config above first @theme */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@variant hocus (&:hover, &:focus);
|
||||
|
||||
@theme {
|
||||
--color-red-500: #f00;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-blue-500: #00f;
|
||||
}
|
||||
`,
|
||||
'src/root.3/tailwind.config.ts': js`
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
plugins: [
|
||||
() => {}, // custom stuff which is too complicated to migrate to CSS
|
||||
],
|
||||
}
|
||||
`,
|
||||
'src/root.4/index.css': css`
|
||||
/* Inject missing @config due to nested imports with tailwind imports */
|
||||
@import './base.css';
|
||||
@import './utilities.css';
|
||||
`,
|
||||
'src/root.4/tailwind.config.ts': js`
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
plugins: [
|
||||
() => {}, // custom stuff which is too complicated to migrate to CSS
|
||||
],
|
||||
}
|
||||
`,
|
||||
'src/root.4/base.css': css`@import 'tailwindcss/base';`,
|
||||
'src/root.4/utilities.css': css`@import 'tailwindcss/utilities';`,
|
||||
|
||||
'src/root.5/index.css': css`@import './tailwind.css';`,
|
||||
'src/root.5/tailwind.css': css`
|
||||
/* Inject missing @config in this file, due to full import */
|
||||
/* Should be located in the root: ../../ */
|
||||
@import 'tailwindcss';
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./src/index.html ---
|
||||
<div
|
||||
class="flex! sm:block! bg-linear-to-t bg-(--my-red)"
|
||||
></div>
|
||||
|
||||
--- ./src/root.1/index.css ---
|
||||
/* Inject missing @config */
|
||||
@import 'tailwindcss';
|
||||
|
||||
@config './tailwind.config.ts';
|
||||
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
--- ./src/root.2/index.css ---
|
||||
/* Already contains @config */
|
||||
@import 'tailwindcss';
|
||||
|
||||
@config "../../tailwind.config.ts";
|
||||
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
--- ./src/root.3/index.css ---
|
||||
/* Inject missing @config above first @theme */
|
||||
@import 'tailwindcss';
|
||||
|
||||
@config './tailwind.config.ts';
|
||||
|
||||
@variant hocus (&:hover, &:focus);
|
||||
|
||||
@theme {
|
||||
--color-red-500: #f00;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-blue-500: #00f;
|
||||
}
|
||||
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
--- ./src/root.4/index.css ---
|
||||
/* Inject missing @config due to nested imports with tailwind imports */
|
||||
@import './base.css';
|
||||
@import './utilities.css';
|
||||
|
||||
@config './tailwind.config.ts';
|
||||
|
||||
--- ./src/root.4/base.css ---
|
||||
@import 'tailwindcss/theme' layer(theme);
|
||||
@import 'tailwindcss/preflight' layer(base);
|
||||
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
--- ./src/root.4/utilities.css ---
|
||||
@import 'tailwindcss/utilities' layer(utilities);
|
||||
|
||||
--- ./src/root.5/index.css ---
|
||||
@import './tailwind.css';
|
||||
|
||||
--- ./src/root.5/tailwind.css ---
|
||||
/* Inject missing @config in this file, due to full import */
|
||||
/* Should be located in the root: ../../ */
|
||||
@import 'tailwindcss';
|
||||
|
||||
@config '../../tailwind.config.ts';
|
||||
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'multiple CSS roots that resolve to the same Tailwind config file requires manual intervention',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.ts': js`
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
plugins: [
|
||||
() => {}, // custom stuff which is too complicated to migrate to CSS
|
||||
],
|
||||
}
|
||||
`,
|
||||
|
|
@ -1440,158 +1688,12 @@ test(
|
|||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
async ({ exec }) => {
|
||||
let output = await exec('npx @tailwindcss/upgrade --force', {}, { ignoreStdErr: true }).catch(
|
||||
(e) => e.toString(),
|
||||
)
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./src/index.html ---
|
||||
<div
|
||||
class="flex! sm:block! bg-linear-to-t bg-(--my-red)"
|
||||
></div>
|
||||
|
||||
--- ./src/root.1.css ---
|
||||
/* Inject missing @config */
|
||||
@import 'tailwindcss';
|
||||
|
||||
@config '../tailwind.config.ts';
|
||||
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
--- ./src/root.2.css ---
|
||||
/* Already contains @config */
|
||||
@import 'tailwindcss';
|
||||
|
||||
@config "../tailwind.config.ts";
|
||||
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
--- ./src/root.3.css ---
|
||||
/* Inject missing @config above first @theme */
|
||||
@import 'tailwindcss';
|
||||
|
||||
@config '../tailwind.config.ts';
|
||||
|
||||
@variant hocus (&:hover, &:focus);
|
||||
|
||||
@theme {
|
||||
--color-red-500: #f00;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-blue-500: #00f;
|
||||
}
|
||||
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
--- ./src/root.4.css ---
|
||||
/* Inject missing @config due to nested imports with tailwind imports */
|
||||
@import './root.4/base.css';
|
||||
@import './root.4/utilities.css';
|
||||
|
||||
@config '../tailwind.config.ts';
|
||||
|
||||
--- ./src/root.5.css ---
|
||||
@import './root.5/tailwind.css';
|
||||
|
||||
--- ./src/root.4/base.css ---
|
||||
@import 'tailwindcss/theme' layer(theme);
|
||||
@import 'tailwindcss/preflight' layer(base);
|
||||
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
--- ./src/root.4/utilities.css ---
|
||||
@import 'tailwindcss/utilities' layer(utilities);
|
||||
|
||||
--- ./src/root.5/tailwind.css ---
|
||||
/* Inject missing @config in this file, due to full import */
|
||||
@import 'tailwindcss';
|
||||
|
||||
@config '../../tailwind.config.ts';
|
||||
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
expect(output).toMatch('Could not determine configuration file for:')
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -1602,6 +1704,7 @@ test(
|
|||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
|
@ -1662,14 +1765,14 @@ test(
|
|||
|
||||
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./src/index.css ---
|
||||
@import './tailwind-setup.css';
|
||||
|
||||
--- ./src/index.html ---
|
||||
<div
|
||||
class="flex! sm:block! bg-linear-to-t bg-(--my-red)"
|
||||
></div>
|
||||
|
||||
--- ./src/index.css ---
|
||||
@import './tailwind-setup.css';
|
||||
|
||||
--- ./src/base.css ---
|
||||
@import 'tailwindcss/theme' layer(theme);
|
||||
@import 'tailwindcss/preflight' layer(base);
|
||||
|
|
@ -1735,6 +1838,7 @@ test(
|
|||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
|
@ -1797,14 +1901,14 @@ test(
|
|||
|
||||
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./src/index.css ---
|
||||
@import './tailwind-setup.css';
|
||||
|
||||
--- ./src/index.html ---
|
||||
<div
|
||||
class="flex! sm:block! bg-linear-to-t bg-(--my-red)"
|
||||
></div>
|
||||
|
||||
--- ./src/index.css ---
|
||||
@import './tailwind-setup.css';
|
||||
|
||||
--- ./src/base.css ---
|
||||
@import 'tailwindcss/theme' layer(theme);
|
||||
@import 'tailwindcss/preflight' layer(base);
|
||||
|
|
@ -2105,13 +2209,6 @@ test(
|
|||
// Files should not be modified
|
||||
expect(await fs.dumpFiles('./*.{js,css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- index.html ---
|
||||
<div>
|
||||
<div class="shadow shadow-sm shadow-xs"></div>
|
||||
<div class="blur blur-xs"></div>
|
||||
<div class="rounded rounded-sm"></div>
|
||||
</div>
|
||||
|
||||
--- index.css ---
|
||||
@import 'tailwindcss';
|
||||
|
||||
|
|
@ -2141,6 +2238,13 @@ test(
|
|||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
--- index.html ---
|
||||
<div>
|
||||
<div class="shadow shadow-sm shadow-xs"></div>
|
||||
<div class="blur blur-xs"></div>
|
||||
<div class="rounded rounded-sm"></div>
|
||||
</div>
|
||||
"
|
||||
`)
|
||||
},
|
||||
|
|
@ -2196,9 +2300,6 @@ test(
|
|||
// Files should not be modified
|
||||
expect(await fs.dumpFiles('./*.{js,css,html,tsx}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- index.html ---
|
||||
<div class="rounded-sm blur-sm shadow-sm"></div>
|
||||
|
||||
--- index.css ---
|
||||
@import 'tailwindcss';
|
||||
|
||||
|
|
@ -2220,6 +2321,9 @@ test(
|
|||
}
|
||||
}
|
||||
|
||||
--- index.html ---
|
||||
<div class="rounded-sm blur-sm shadow-sm"></div>
|
||||
|
||||
--- example-component.tsx ---
|
||||
type Star = [
|
||||
x: number,
|
||||
|
|
|
|||
|
|
@ -346,15 +346,21 @@ export function test(
|
|||
let zParts = z[0].split('/')
|
||||
|
||||
let aFile = aParts.at(-1)
|
||||
let zFile = aParts.at(-1)
|
||||
let zFile = zParts.at(-1)
|
||||
|
||||
// Sort by depth, shallow first
|
||||
if (aParts.length < zParts.length) return -1
|
||||
if (aParts.length > zParts.length) return 1
|
||||
|
||||
// Sort by folder names, alphabetically
|
||||
for (let i = 0; i < aParts.length - 1; i++) {
|
||||
let diff = aParts[i].localeCompare(zParts[i])
|
||||
if (diff !== 0) return diff
|
||||
}
|
||||
|
||||
// Sort by filename, sort files named `index` before others
|
||||
if (aFile?.startsWith('index')) return -1
|
||||
if (zFile?.startsWith('index')) return 1
|
||||
if (aFile?.startsWith('index') && !zFile?.startsWith('index')) return -1
|
||||
if (zFile?.startsWith('index') && !aFile?.startsWith('index')) return 1
|
||||
|
||||
// Sort by filename, alphabetically
|
||||
return a[0].localeCompare(z[0])
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { args, type Arg } from './utils/args'
|
|||
import { isRepoDirty } from './utils/git'
|
||||
import { hoistStaticGlobParts } from './utils/hoist-static-glob-parts'
|
||||
import { pkg } from './utils/packages'
|
||||
import { eprintln, error, header, highlight, info, success } from './utils/renderer'
|
||||
import { eprintln, error, header, highlight, info, relative, success } from './utils/renderer'
|
||||
|
||||
const options = {
|
||||
'--config': { type: 'string', description: 'Path to the configuration file', alias: '-c' },
|
||||
|
|
@ -110,7 +110,7 @@ async function run() {
|
|||
}
|
||||
|
||||
// Migrate js config files, linked to stylesheets
|
||||
info('Migrating JavaScript configuration files using the provided configuration file.')
|
||||
info('Migrating JavaScript configuration files…')
|
||||
let configBySheet = new Map<Stylesheet, Awaited<ReturnType<typeof prepareConfig>>>()
|
||||
let jsConfigMigrationBySheet = new Map<
|
||||
Stylesheet,
|
||||
|
|
@ -133,13 +133,18 @@ async function run() {
|
|||
// Remove the JS config if it was fully migrated
|
||||
cleanup.push(() => fs.rm(config.configFilePath))
|
||||
}
|
||||
|
||||
if (jsConfigMigration !== null) {
|
||||
success(
|
||||
`↳ Migrated configuration file: ${highlight(relative(config.configFilePath, base))}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate source files, linked to config files
|
||||
info('Migrating templates…')
|
||||
{
|
||||
// Template migrations
|
||||
|
||||
info('Migrating templates using the provided configuration file.')
|
||||
for (let config of configBySheet.values()) {
|
||||
let set = new Set<string>()
|
||||
for (let globEntry of config.globs.flatMap((entry) => hoistStaticGlobParts(entry))) {
|
||||
|
|
@ -161,12 +166,15 @@ async function run() {
|
|||
await Promise.allSettled(
|
||||
files.map((file) => migrateTemplate(config.designSystem, config.userConfig, file)),
|
||||
)
|
||||
}
|
||||
|
||||
success('Template migration complete.')
|
||||
success(
|
||||
`↳ Migrated templates for configuration file: ${highlight(relative(config.configFilePath, base))}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate each CSS file
|
||||
info('Migrating stylesheets…')
|
||||
let migrateResults = await Promise.allSettled(
|
||||
stylesheets.map((sheet) => {
|
||||
let config = configBySheet.get(sheet)!
|
||||
|
|
@ -231,9 +239,11 @@ async function run() {
|
|||
if (!sheet.file) continue
|
||||
|
||||
await fs.writeFile(sheet.file, sheet.root.toString())
|
||||
}
|
||||
|
||||
success('Stylesheet migration complete.')
|
||||
if (sheet.isTailwindRoot) {
|
||||
success(`↳ Migrated stylesheet: ${highlight(relative(sheet.file, base))}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
|
@ -241,19 +251,21 @@ async function run() {
|
|||
await migratePostCSSConfig(base)
|
||||
}
|
||||
|
||||
info('Updating dependencies…')
|
||||
{
|
||||
// Migrate the prettier plugin to the latest version
|
||||
await migratePrettierPlugin(base)
|
||||
}
|
||||
|
||||
// Run all cleanup functions because we completed the migration
|
||||
await Promise.allSettled(cleanup.map((fn) => fn()))
|
||||
|
||||
try {
|
||||
// Upgrade Tailwind CSS
|
||||
await pkg(base).add(['tailwindcss@next'])
|
||||
success(`↳ Updated package: ${highlight('tailwindcss')}`)
|
||||
} catch {}
|
||||
|
||||
// Run all cleanup functions because we completed the migration
|
||||
await Promise.allSettled(cleanup.map((fn) => fn()))
|
||||
|
||||
// Figure out if we made any changes
|
||||
if (isRepoDirty()) {
|
||||
success('Verify the changes and commit them to your repository.')
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import type { DesignSystem } from '../../tailwindcss/src/design-system'
|
|||
import { escape } from '../../tailwindcss/src/utils/escape'
|
||||
import { isValidSpacingMultiplier } from '../../tailwindcss/src/utils/infer-data-type'
|
||||
import { findStaticPlugins, type StaticPluginOptions } from './utils/extract-static-plugins'
|
||||
import { info } from './utils/renderer'
|
||||
import { highlight, info, relative } from './utils/renderer'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
|
@ -44,7 +44,7 @@ export async function migrateJsConfig(
|
|||
|
||||
if (!canMigrateConfig(unresolvedConfig, source)) {
|
||||
info(
|
||||
'Your configuration file could not be automatically migrated to the new CSS configuration format, so your CSS has been updated to load your existing configuration file.',
|
||||
`↳ The configuration file at ${highlight(relative(fullConfigPath, base))} could not be automatically migrated to the new CSS configuration format, so your CSS has been updated to load your existing configuration file.`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { pkg } from './utils/packages'
|
||||
import { info, success, warn } from './utils/renderer'
|
||||
import { highlight, info, relative, success, warn } from './utils/renderer'
|
||||
|
||||
// Migrates simple PostCSS setups. This is to cover non-dynamic config files
|
||||
// similar to the ones we have all over our docs:
|
||||
|
|
@ -24,7 +24,7 @@ export async function migratePostCSSConfig(base: string) {
|
|||
// Priority 1: Handle JS config files
|
||||
let jsConfigPath = await detectJSConfigPath(base)
|
||||
if (jsConfigPath) {
|
||||
let result = await migratePostCSSJSConfig(base, jsConfigPath)
|
||||
let result = await migratePostCSSJSConfig(jsConfigPath)
|
||||
|
||||
if (result) {
|
||||
didMigrate = true
|
||||
|
|
@ -41,7 +41,7 @@ export async function migratePostCSSConfig(base: string) {
|
|||
packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'))
|
||||
} catch {}
|
||||
if (!didMigrate && packageJson && 'postcss' in packageJson) {
|
||||
let result = await migratePostCSSJsonConfig(base, packageJson.postcss)
|
||||
let result = await migratePostCSSJsonConfig(packageJson.postcss)
|
||||
|
||||
if (result) {
|
||||
await fs.writeFile(
|
||||
|
|
@ -64,7 +64,7 @@ export async function migratePostCSSConfig(base: string) {
|
|||
jsonConfig = JSON.parse(await fs.readFile(jsonConfigPath, 'utf-8'))
|
||||
} catch {}
|
||||
if (jsonConfig) {
|
||||
let result = await migratePostCSSJsonConfig(base, jsonConfig)
|
||||
let result = await migratePostCSSJsonConfig(jsonConfig)
|
||||
|
||||
if (result) {
|
||||
await fs.writeFile(jsonConfigPath, JSON.stringify(result.json, null, 2))
|
||||
|
|
@ -78,7 +78,7 @@ export async function migratePostCSSConfig(base: string) {
|
|||
}
|
||||
|
||||
if (!didMigrate) {
|
||||
info(`No PostCSS config found, skipping migration.`)
|
||||
info('No PostCSS config found, skipping migration.')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +92,7 @@ export async function migratePostCSSConfig(base: string) {
|
|||
if (location !== null) {
|
||||
try {
|
||||
await pkg(base).add(['@tailwindcss/postcss@next'], location)
|
||||
success(`↳ Installed package: ${highlight('@tailwindcss/postcss')}`)
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
|
@ -102,16 +103,18 @@ export async function migratePostCSSConfig(base: string) {
|
|||
didRemovePostCSSImport ? 'postcss-import' : null,
|
||||
].filter(Boolean) as string[]
|
||||
await pkg(base).remove(packagesToRemove)
|
||||
for (let pkg of packagesToRemove) {
|
||||
success(`↳ Removed package: ${highlight(pkg)}`)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
success(`PostCSS config has been upgraded.`)
|
||||
if (didMigrate && jsConfigPath) {
|
||||
success(`↳ Migrated PostCSS configuration: ${highlight(relative(jsConfigPath, base))}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function migratePostCSSJSConfig(
|
||||
base: string,
|
||||
configPath: string,
|
||||
): Promise<{
|
||||
async function migratePostCSSJSConfig(configPath: string): Promise<{
|
||||
didAddPostcssClient: boolean
|
||||
didRemoveAutoprefixer: boolean
|
||||
didRemovePostCSSImport: boolean
|
||||
|
|
@ -129,11 +132,11 @@ async function migratePostCSSJSConfig(
|
|||
return /['"]tailwindcss\/nesting['"]\: ?(\{\}|['"]postcss-nesting['"])/.test(line)
|
||||
}
|
||||
|
||||
info(`Attempt to upgrade the PostCSS config in file: ${configPath}`)
|
||||
info('Migrating PostCSS configuration…')
|
||||
|
||||
let isSimpleConfig = await isSimplePostCSSConfig(base, configPath)
|
||||
let isSimpleConfig = await isSimplePostCSSConfig(configPath)
|
||||
if (!isSimpleConfig) {
|
||||
warn(`The PostCSS config contains dynamic JavaScript and can not be automatically migrated.`)
|
||||
warn('The PostCSS config contains dynamic JavaScript and can not be automatically migrated.')
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -141,8 +144,7 @@ async function migratePostCSSJSConfig(
|
|||
let didRemoveAutoprefixer = false
|
||||
let didRemovePostCSSImport = false
|
||||
|
||||
let fullPath = path.resolve(base, configPath)
|
||||
let content = await fs.readFile(fullPath, 'utf-8')
|
||||
let content = await fs.readFile(configPath, 'utf-8')
|
||||
let lines = content.split('\n')
|
||||
let newLines: string[] = []
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
|
|
@ -186,15 +188,12 @@ async function migratePostCSSJSConfig(
|
|||
newLines.push(line)
|
||||
}
|
||||
}
|
||||
await fs.writeFile(fullPath, newLines.join('\n'))
|
||||
await fs.writeFile(configPath, newLines.join('\n'))
|
||||
|
||||
return { didAddPostcssClient, didRemoveAutoprefixer, didRemovePostCSSImport }
|
||||
}
|
||||
|
||||
async function migratePostCSSJsonConfig(
|
||||
base: string,
|
||||
json: any,
|
||||
): Promise<{
|
||||
async function migratePostCSSJsonConfig(json: any): Promise<{
|
||||
json: any
|
||||
didAddPostcssClient: boolean
|
||||
didRemoveAutoprefixer: boolean
|
||||
|
|
@ -291,7 +290,7 @@ async function detectJSConfigPath(base: string): Promise<null | string> {
|
|||
let fullPath = path.resolve(base, file)
|
||||
try {
|
||||
await fs.access(fullPath)
|
||||
return file
|
||||
return fullPath
|
||||
} catch {}
|
||||
}
|
||||
return null
|
||||
|
|
@ -308,15 +307,14 @@ async function detectJSONConfigPath(base: string): Promise<null | string> {
|
|||
let fullPath = path.resolve(base, file)
|
||||
try {
|
||||
await fs.access(fullPath)
|
||||
return file
|
||||
return fullPath
|
||||
} catch {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function isSimplePostCSSConfig(base: string, configPath: string): Promise<boolean> {
|
||||
let fullPath = path.resolve(base, configPath)
|
||||
let content = await fs.readFile(fullPath, 'utf-8')
|
||||
async function isSimplePostCSSConfig(configPath: string): Promise<boolean> {
|
||||
let content = await fs.readFile(configPath, 'utf-8')
|
||||
return (
|
||||
content.includes('tailwindcss:') &&
|
||||
!(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { pkg } from './utils/packages'
|
||||
import { success } from './utils/renderer'
|
||||
import { highlight, success } from './utils/renderer'
|
||||
|
||||
export async function migratePrettierPlugin(base: string) {
|
||||
let packageJsonPath = path.resolve(base, 'package.json')
|
||||
|
|
@ -9,7 +9,7 @@ export async function migratePrettierPlugin(base: string) {
|
|||
let packageJson = await fs.readFile(packageJsonPath, 'utf-8')
|
||||
if (packageJson.includes('prettier-plugin-tailwindcss')) {
|
||||
await pkg(base).add(['prettier-plugin-tailwindcss@latest'])
|
||||
success(`Prettier plugin migrated to latest version.`)
|
||||
success(`↳ Updated package: ${highlight('prettier-plugin-tailwindcss')}`)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { migrateVariantsDirective } from './codemods/migrate-variants-directive'
|
|||
import type { JSConfigMigration } from './migrate-js-config'
|
||||
import { Stylesheet, type StylesheetConnection, type StylesheetId } from './stylesheet'
|
||||
import { detectConfigPath } from './template/prepare-config'
|
||||
import { error } from './utils/renderer'
|
||||
import { error, highlight, relative, success } from './utils/renderer'
|
||||
import { resolveCssId } from './utils/resolve'
|
||||
import { walk, WalkAction } from './utils/walk'
|
||||
|
||||
|
|
@ -364,16 +364,53 @@ export async function linkConfigs(
|
|||
// All stylesheets have a `@config` directives
|
||||
if (withoutAtConfig.length === 0) return
|
||||
|
||||
try {
|
||||
// Find the config file path for each stylesheet
|
||||
let configPathBySheet = new Map<Stylesheet, string>()
|
||||
let sheetByConfigPath = new DefaultMap<string, Set<Stylesheet>>(() => new Set())
|
||||
for (let sheet of withoutAtConfig) {
|
||||
if (!sheet.file) continue
|
||||
|
||||
let localConfigPath = configPath as string
|
||||
if (configPath === null) {
|
||||
configPath = await detectConfigPath(base)
|
||||
} else if (!path.isAbsolute(configPath)) {
|
||||
configPath = path.resolve(base, configPath)
|
||||
localConfigPath = await detectConfigPath(path.dirname(sheet.file), base)
|
||||
} else if (!path.isAbsolute(localConfigPath)) {
|
||||
localConfigPath = path.resolve(base, localConfigPath)
|
||||
}
|
||||
|
||||
// Link the `@config` directive to the root stylesheets
|
||||
for (let sheet of withoutAtConfig) {
|
||||
if (!sheet.file) continue
|
||||
configPathBySheet.set(sheet, localConfigPath)
|
||||
sheetByConfigPath.get(localConfigPath).add(sheet)
|
||||
}
|
||||
|
||||
let problematicStylesheets = new Set<Stylesheet>()
|
||||
for (let sheets of sheetByConfigPath.values()) {
|
||||
if (sheets.size > 1) {
|
||||
for (let sheet of sheets) {
|
||||
problematicStylesheets.add(sheet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There are multiple "root" files without `@config` directives. Manual
|
||||
// intervention is needed to link to the correct Tailwind config files.
|
||||
if (problematicStylesheets.size > 1) {
|
||||
for (let sheet of problematicStylesheets) {
|
||||
error(
|
||||
`Could not determine configuration file for: ${highlight(relative(sheet.file!, base))}\nUpdate your stylesheet to use ${highlight('@config')} to specify the correct configuration file explicitly and then run the upgrade tool again.`,
|
||||
)
|
||||
}
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
let relativePath = relative
|
||||
for (let [sheet, configPath] of configPathBySheet) {
|
||||
try {
|
||||
if (!sheet || !sheet.file) return
|
||||
success(
|
||||
`↳ Linked ${highlight(relativePath(configPath, base))} to ${highlight(relativePath(sheet.file, base))}`,
|
||||
)
|
||||
|
||||
// Link the `@config` directive to the root stylesheets
|
||||
|
||||
// Track the config file path on the stylesheet itself for easy access
|
||||
// without traversing the CSS ast and finding the corresponding
|
||||
|
|
@ -409,10 +446,10 @@ export async function linkConfigs(
|
|||
target.after(atConfig)
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
error('Could not load the configuration file: ' + e.message)
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (e: any) {
|
||||
error('Could not load the configuration file: ' + e.message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { loadModule } from '../../../@tailwindcss-node/src/compile'
|
|||
import { resolveConfig } from '../../../tailwindcss/src/compat/config/resolve-config'
|
||||
import type { Config } from '../../../tailwindcss/src/compat/plugin-api'
|
||||
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
|
||||
import { error } from '../utils/renderer'
|
||||
import { error, highlight, relative } from '../utils/renderer'
|
||||
import { migratePrefix } from './codemods/prefix'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
|
|
@ -94,15 +94,40 @@ const DEFAULT_CONFIG_FILES = [
|
|||
'./tailwind.config.cts',
|
||||
'./tailwind.config.mts',
|
||||
]
|
||||
export async function detectConfigPath(base: string) {
|
||||
for (let file of DEFAULT_CONFIG_FILES) {
|
||||
let fullPath = path.resolve(base, file)
|
||||
try {
|
||||
await fs.access(fullPath)
|
||||
return file
|
||||
} catch {}
|
||||
export async function detectConfigPath(start: string, end: string = start) {
|
||||
for (let base of parentPaths(start, end)) {
|
||||
for (let file of DEFAULT_CONFIG_FILES) {
|
||||
let fullPath = path.resolve(base, file)
|
||||
try {
|
||||
await fs.access(fullPath)
|
||||
return fullPath
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'No configuration file found. Please provide a path to the Tailwind CSS v3 config file via the `--config` option.',
|
||||
`No configuration file found for ${highlight(relative(start))}. Please provide a path to the Tailwind CSS v3 config file via the ${highlight('--config')} option.`,
|
||||
)
|
||||
}
|
||||
|
||||
// Yields all parent paths from `from` to `to` (inclusive on both ends)
|
||||
function* parentPaths(from: string, to: string = from) {
|
||||
let fromAbsolute = path.resolve(from)
|
||||
let toAbsolute = path.resolve(to)
|
||||
|
||||
if (!fromAbsolute.startsWith(toAbsolute)) {
|
||||
throw new Error(`The path ${from} is not a parent of ${to}`)
|
||||
}
|
||||
|
||||
if (fromAbsolute === toAbsolute) {
|
||||
yield fromAbsolute
|
||||
return
|
||||
}
|
||||
|
||||
let current = fromAbsolute
|
||||
do {
|
||||
yield current
|
||||
current = path.dirname(current)
|
||||
} while (current !== toAbsolute)
|
||||
yield toAbsolute
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue