Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure that CSS inside Svelte `<style>` blocks always run the expected Svelte processors when using the Vite extension ([#14981](https://github.com/tailwindlabs/tailwindcss/pull/14981))
- _Upgrade (experimental)_: Ensure it's safe to migrate `blur`, `rounded`, or `shadow` ([#14979](https://github.com/tailwindlabs/tailwindcss/pull/14979))
- _Upgrade (experimental)_: Do not rename classes using custom defined theme values ([#14976](https://github.com/tailwindlabs/tailwindcss/pull/14976))
- _Upgrade (experimental)_: Ensure `@config` is injected in nearest common ancestor stylesheet ([#14989](https://github.com/tailwindlabs/tailwindcss/pull/14989))

## [4.0.0-alpha.33] - 2024-11-11

Expand Down
270 changes: 270 additions & 0 deletions integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,276 @@ test(
},
)

test(
'injecting `@config` in the shared root, when a tailwind.config.{js,ts,…} is detected',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': js`
export default {
content: ['./src/**/*.{html,js}'],
plugins: [
() => {
// custom stuff which is too complicated to migrate to CSS
},
],
Comment on lines +1484 to +1488
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
plugins: [
() => {
// custom stuff which is too complicated to migrate to CSS
},
],
theme: {
extend: {
colors: {
'my-red': 'red',
},
},
},

When we update the config to something that we can convert to, we noticed that the @theme now ends up in src/index.css and that the @config is still inside src/tailwind-setup.css.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a failing test for this: a8a66a4

Then fixed it: dc5cd8d

Bonus points: basically deleted most of the code in the migrate-config file

}
`,
'src/index.html': html`
<div
class="!flex sm:!block bg-gradient-to-t bg-[--my-red]"
></div>
`,
'src/index.css': css`@import './tailwind-setup.css';`,
'src/tailwind-setup.css': css`
@import './base.css';
@import './components.css';
@import './utilities.css';
`,
'src/base.css': css`
html {
color: red;
}
@tailwind base;
`,
'src/components.css': css`
@import './typography.css';
@layer components {
.foo {
color: red;
}
}
@tailwind components;
`,
'src/typography.css': css`
.typography {
color: red;
}
`,
'src/utilities.css': css`
@layer utilities {
.bar {
color: red;
}
}
@tailwind utilities;
`,
},
},
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-[var(--my-red)]"
></div>

--- ./src/index.css ---
@import './tailwind-setup.css';

--- ./src/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);
}
}

@layer base {
html {
color: red;
}
}

--- ./src/components.css ---
@import './typography.css';

@utility foo {
color: red;
}

--- ./src/tailwind-setup.css ---
@import './base.css';
@import './components.css';
@import './utilities.css';

@config '../tailwind.config.ts';

--- ./src/typography.css ---
.typography {
color: red;
}

--- ./src/utilities.css ---
@import 'tailwindcss/utilities' layer(utilities);

@utility bar {
color: red;
}
"
`)
},
)

test(
'injecting `@config` in the shared root (+ migrating), when a tailwind.config.{js,ts,…} is detected',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': js`
export default {
content: ['./src/**/*.{html,js}'],
theme: {
extend: {
colors: {
'my-red': 'red',
},
},
},
}
`,
'src/index.html': html`
<div
class="!flex sm:!block bg-gradient-to-t bg-[--my-red]"
></div>
`,
'src/index.css': css`@import './tailwind-setup.css';`,
'src/tailwind-setup.css': css`
@import './base.css';
@import './components.css';
@import './utilities.css';
`,
'src/base.css': css`
html {
color: red;
}
@tailwind base;
`,
'src/components.css': css`
@import './typography.css';
@layer components {
.foo {
color: red;
}
}
@tailwind components;
`,
'src/typography.css': css`
.typography {
color: red;
}
`,
'src/utilities.css': css`
@layer utilities {
.bar {
color: red;
}
}
@tailwind utilities;
`,
},
},
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-[var(--my-red)]"
></div>

--- ./src/index.css ---
@import './tailwind-setup.css';

--- ./src/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);
}
}

@layer base {
html {
color: red;
}
}

--- ./src/components.css ---
@import './typography.css';

@utility foo {
color: red;
}

--- ./src/tailwind-setup.css ---
@import './base.css';
@import './components.css';
@import './utilities.css';

@theme {
--color-my-red: red;
}

--- ./src/typography.css ---
.typography {
color: red;
}

--- ./src/utilities.css ---
@import 'tailwindcss/utilities' layer(utilities);

@utility bar {
color: red;
}
"
`)
},
)

test(
'relative imports without a relative path prefix are migrated to include a relative path prefix',
{
Expand Down
41 changes: 4 additions & 37 deletions packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from 'node:path'
import postcss, { AtRule, type Plugin, Root } from 'postcss'
import postcss, { AtRule, type Plugin } from 'postcss'
import { normalizePath } from '../../../@tailwindcss-node/src/normalize-path'
import type { JSConfigMigration } from '../migrate-js-config'
import type { Stylesheet } from '../stylesheet'
Expand All @@ -13,7 +13,9 @@ export function migrateConfig(
jsConfigMigration,
}: { configFilePath: string; jsConfigMigration: JSConfigMigration },
): Plugin {
function injectInto(sheet: Stylesheet) {
function migrate() {
if (!sheet.isTailwindRoot) return

let alreadyInjected = ALREADY_INJECTED.get(sheet)
if (alreadyInjected && alreadyInjected.includes(configFilePath)) {
return
Expand Down Expand Up @@ -82,41 +84,6 @@ export function migrateConfig(
root.append(cssConfig.nodes)
}

function migrate(root: Root) {
// We can only migrate if there is an `@import "tailwindcss"` (or sub-import)
let hasTailwindImport = false
let hasFullTailwindImport = false
root.walkAtRules('import', (node) => {
if (node.params.match(/['"]tailwindcss['"]/)) {
hasTailwindImport = true
hasFullTailwindImport = true
return false
} else if (node.params.match(/['"]tailwindcss\/.*?['"]/)) {
hasTailwindImport = true
}
})

if (!hasTailwindImport) return

// If a full `@import "tailwindcss"` is present or this is the root
// stylesheet, we can inject the `@config` directive directly into this
// file.
if (hasFullTailwindImport || sheet.parents.size <= 0) {
injectInto(sheet)
return
}

// Otherwise, if we are not the root file, we need to inject the `@config`
// into the root file.
if (sheet.parents.size > 0) {
for (let parent of sheet.ancestors()) {
if (parent.parents.size === 0) {
injectInto(parent)
}
}
}
}

return {
postcssPlugin: '@tailwindcss/upgrade/migrate-config',
OnceExit: migrate,
Expand Down
26 changes: 21 additions & 5 deletions packages/@tailwindcss-upgrade/src/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,22 +253,38 @@ export async function analyze(stylesheets: Stylesheet[]) {
for (let [sheetB, childrenB] of commonParents) {
if (sheetA === sheetB) continue

for (let parentA of sheetA.ancestors()) {
for (let parentB of sheetB.ancestors()) {
// Ancestors from self to root. Reversed order so we find the
// nearest common parent first
//
// Including self because if you compare a sheet with its parent,
// then the parent is still the common sheet between the two. In
// this case, the parent is the root file.
let ancestorsA = [sheetA].concat(Array.from(sheetA.ancestors()).reverse())
let ancestorsB = [sheetB].concat(Array.from(sheetB.ancestors()).reverse())

for (let parentA of ancestorsA) {
for (let parentB of ancestorsB) {
if (parentA !== parentB) continue

// Found the parent
let parent = parentA

commonParents.delete(sheetA)
commonParents.delete(sheetB)

for (let child of childrenA) {
commonParents.get(parentA).add(child)
commonParents.get(parent).add(child)
}

for (let child of childrenB) {
commonParents.get(parentA).add(child)
commonParents.get(parent).add(child)
}

repeat = true
repeat = parent !== sheetA && parent !== sheetB

// Found a common parent between sheet A and sheet B. We can
// stop looking for more common parents between A and B, and
// continue with the next sheet.
break outer
}
}
Expand Down
3 changes: 1 addition & 2 deletions packages/tailwindcss/src/compat/plugin-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2973,8 +2973,7 @@ describe('matchUtilities()', () => {
return compiled.build(candidates)
}

expect(optimizeCss(await run(['@w-1','hover:@w-1'])).trim())
.toMatchInlineSnapshot(`
expect(optimizeCss(await run(['@w-1', 'hover:@w-1'])).trim()).toMatchInlineSnapshot(`
".\\@w-1 {
width: 1px;
}
Expand Down