From d59f1b3e5d8c1489f0295e8b3ff18e7058ff2f8f Mon Sep 17 00:00:00 2001
From: philipp-spiess <458591+philipp-spiess@users.noreply.github.com>
Date: Tue, 22 Oct 2024 16:03:07 +0000
Subject: [PATCH 1/5] Vite: Fix issues when loading files via static asset
queries (#14716)
Fixes: #14558
This PR fixes an issue where our Vite plugin would crash when trying to load stylesheets via certain static asset query parameters:
```ts
import raw from './style.css?raw'
import url from './style.css?url'
```
The proper behavior for our extension is to _not touch these file at all_. The `?raw` identifier should never transform anything and the `?url` one will emit a module which points to the asset URL. However, if that URL is loaded as a stylesheet, another transform hook is called and the file is properly transformed. I verified this in the Vite setup and have added an integration test ensuring these two features work as expected.
I've also greatly reduced the complexity of the Vite playground to make it easier to set up examples like this in the future.
---
CHANGELOG.md | 1 +
integrations/vite/index.test.ts | 61 +++++++++++++++++++++
packages/@tailwindcss-vite/src/index.ts | 11 +++-
playgrounds/vite/package.json | 3 +-
playgrounds/vite/src/animate.js | 1 -
playgrounds/vite/src/app.tsx | 4 --
playgrounds/vite/src/bar.tsx | 7 ---
playgrounds/vite/src/foo.tsx | 10 ----
playgrounds/vite/src/forms.js | 1 -
playgrounds/vite/src/{app.css => index.css} | 1 -
playgrounds/vite/src/index.html | 1 -
playgrounds/vite/src/main.tsx | 2 +
playgrounds/vite/src/plugin.js | 4 --
playgrounds/vite/src/typography.js | 1 -
pnpm-lock.yaml | 55 +------------------
15 files changed, 75 insertions(+), 88 deletions(-)
delete mode 100644 playgrounds/vite/src/animate.js
delete mode 100644 playgrounds/vite/src/bar.tsx
delete mode 100644 playgrounds/vite/src/foo.tsx
delete mode 100644 playgrounds/vite/src/forms.js
rename playgrounds/vite/src/{app.css => index.css} (50%)
delete mode 100644 playgrounds/vite/src/plugin.js
delete mode 100644 playgrounds/vite/src/typography.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13acdc318..437259146 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure color opacity modifiers work with OKLCH colors ([#14741](https://github.com/tailwindlabs/tailwindcss/pull/14741))
- Ensure changes to the input CSS file result in a full rebuild ([#14744](https://github.com/tailwindlabs/tailwindcss/pull/14744))
- Add `postcss` as a dependency of `@tailwindcss/postcss` ([#14750](https://github.com/tailwindlabs/tailwindcss/pull/14750))
+- Ensure loading stylesheets via the `?raw` and `?url` static asset query works when using the Vite plugin ([#14716](https://github.com/tailwindlabs/tailwindcss/pull/14716))
- _Upgrade (experimental)_: Migrate `flex-grow` to `grow` and `flex-shrink` to `shrink` ([#14721](https://github.com/tailwindlabs/tailwindcss/pull/14721))
- _Upgrade (experimental)_: Minify arbitrary values when printing candidates ([#14720](https://github.com/tailwindlabs/tailwindcss/pull/14720))
- _Upgrade (experimental)_: Ensure legacy theme values ending in `1` (like `theme(spacing.1)`) are correctly migrated to custom properties ([#14724](https://github.com/tailwindlabs/tailwindcss/pull/14724))
diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts
index f56575c1a..89814ccc6 100644
--- a/integrations/vite/index.test.ts
+++ b/integrations/vite/index.test.ts
@@ -504,3 +504,64 @@ test(
})
},
)
+
+test(
+ `does not interfere with ?raw and ?url static asset handling`,
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ "vite": "^5.3.5"
+ }
+ }
+ `,
+ 'vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
+
+ export default defineConfig({
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ })
+ `,
+ 'index.html': html`
+
+
+
+ `,
+ 'src/index.js': js`
+ import url from './index.css?url'
+ import raw from './index.css?raw'
+ `,
+ 'src/index.css': css`@import 'tailwindcss';`,
+ },
+ },
+ async ({ spawn, getFreePort }) => {
+ let port = await getFreePort()
+ await spawn(`pnpm vite dev --port ${port}`)
+
+ await retryAssertion(async () => {
+ // We have to load the .js file first so that the static assets are
+ // resolved
+ await fetch(`http://localhost:${port}/src/index.js`).then((r) => r.text())
+
+ let [raw, url] = await Promise.all([
+ fetch(`http://localhost:${port}/src/index.css?raw`).then((r) => r.text()),
+ fetch(`http://localhost:${port}/src/index.css?url`).then((r) => r.text()),
+ ])
+
+ expect(firstLine(raw)).toBe(`export default "@import 'tailwindcss';"`)
+ expect(firstLine(url)).toBe(`export default "/src/index.css"`)
+ })
+ },
+)
+
+function firstLine(str: string) {
+ return str.split('\n')[0]
+}
diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts
index abb13a613..9571342cd 100644
--- a/packages/@tailwindcss-vite/src/index.ts
+++ b/packages/@tailwindcss-vite/src/index.ts
@@ -5,6 +5,8 @@ import { Features, transform } from 'lightningcss'
import path from 'path'
import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite'
+const SPECIAL_QUERY_RE = /[?&](raw|url)\b/
+
export default function tailwindcss(): Plugin[] {
let servers: ViteDevServer[] = []
let config: ResolvedConfig | null = null
@@ -261,9 +263,12 @@ function getExtension(id: string) {
function isPotentialCssRootFile(id: string) {
let extension = getExtension(id)
let isCssFile =
- extension === 'css' ||
- (extension === 'vue' && id.includes('&lang.css')) ||
- (extension === 'astro' && id.includes('&lang.css'))
+ (extension === 'css' ||
+ (extension === 'vue' && id.includes('&lang.css')) ||
+ (extension === 'astro' && id.includes('&lang.css'))) &&
+ // Don't intercept special static asset resources
+ !SPECIAL_QUERY_RE.test(id)
+
return isCssFile
}
diff --git a/playgrounds/vite/package.json b/playgrounds/vite/package.json
index 846b96252..a5a953a14 100644
--- a/playgrounds/vite/package.json
+++ b/playgrounds/vite/package.json
@@ -19,7 +19,6 @@
"@types/react": "^18.3.9",
"@types/react-dom": "^18.3.1",
"bun": "^1.1.29",
- "vite": "catalog:",
- "vite-plugin-handlebars": "^2.0.0"
+ "vite": "catalog:"
}
}
diff --git a/playgrounds/vite/src/animate.js b/playgrounds/vite/src/animate.js
deleted file mode 100644
index 0a5617399..000000000
--- a/playgrounds/vite/src/animate.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = require('tailwindcss-animate')
diff --git a/playgrounds/vite/src/app.tsx b/playgrounds/vite/src/app.tsx
index 285bd4143..8ec502989 100644
--- a/playgrounds/vite/src/app.tsx
+++ b/playgrounds/vite/src/app.tsx
@@ -1,11 +1,7 @@
-import { Foo } from './foo'
-
export function App() {
return (
diff --git a/playgrounds/vite/src/plugin.js b/playgrounds/vite/src/plugin.js
deleted file mode 100644
index 085212671..000000000
--- a/playgrounds/vite/src/plugin.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = function ({ addVariant }) {
- addVariant('inverted', '@media (inverted-colors: inverted)')
- addVariant('hocus', ['&:focus', '&:hover'])
-}
diff --git a/playgrounds/vite/src/typography.js b/playgrounds/vite/src/typography.js
deleted file mode 100644
index 711af27f7..000000000
--- a/playgrounds/vite/src/typography.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = require('@tailwindcss/typography')
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a13e85711..ba225832c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -435,9 +435,6 @@ importers:
vite:
specifier: 'catalog:'
version: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
- vite-plugin-handlebars:
- specifier: ^2.0.0
- version: 2.0.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
packages:
@@ -1941,11 +1938,6 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
- handlebars@4.7.8:
- resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
- engines: {node: '>=0.4.7'}
- hasBin: true
-
has-bigints@1.0.2:
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
@@ -2352,9 +2344,6 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
- neo-async@2.6.2:
- resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
-
next@14.1.0:
resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==}
engines: {node: '>=18.17.0'}
@@ -3007,11 +2996,6 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
- uglify-js@3.19.1:
- resolution: {integrity: sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==}
- engines: {node: '>=0.8.0'}
- hasBin: true
-
unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
@@ -3039,9 +3023,6 @@ packages:
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
- vite-plugin-handlebars@2.0.0:
- resolution: {integrity: sha512-+J3It0nyhPzx4nT1I+fnWH+jShTEXzm6X0Tgsggdm9IYFD7/eJ6a3ROI13HTe0CVoyaxm/fPxH5HDAKyfz7T0g==}
-
vite@5.4.0:
resolution: {integrity: sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -3133,9 +3114,6 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
- wordwrap@1.0.0:
- resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
-
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@@ -4739,15 +4717,6 @@ snapshots:
graceful-fs@4.2.11: {}
- handlebars@4.7.8:
- dependencies:
- minimist: 1.2.8
- neo-async: 2.6.2
- source-map: 0.6.1
- wordwrap: 1.0.0
- optionalDependencies:
- uglify-js: 3.19.1
-
has-bigints@1.0.2: {}
has-flag@3.0.0: {}
@@ -5088,8 +5057,6 @@ snapshots:
natural-compare@1.4.0: {}
- neo-async@2.6.2: {}
-
next@14.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@next/env': 14.1.0
@@ -5486,7 +5453,8 @@ snapshots:
source-map: 0.6.1
optional: true
- source-map@0.6.1: {}
+ source-map@0.6.1:
+ optional: true
source-map@0.8.0-beta.0:
dependencies:
@@ -5761,9 +5729,6 @@ snapshots:
typescript@5.5.4: {}
- uglify-js@3.19.1:
- optional: true
-
unbox-primitive@1.0.2:
dependencies:
call-bind: 1.0.7
@@ -5805,20 +5770,6 @@ snapshots:
- supports-color
- terser
- vite-plugin-handlebars@2.0.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6):
- dependencies:
- handlebars: 4.7.8
- vite: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
- transitivePeerDependencies:
- - '@types/node'
- - less
- - lightningcss
- - sass
- - sass-embedded
- - stylus
- - sugarss
- - terser
-
vite@5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6):
dependencies:
esbuild: 0.21.5
@@ -5920,8 +5871,6 @@ snapshots:
word-wrap@1.2.5: {}
- wordwrap@1.0.0: {}
-
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
From 5bf2efb52170f7123ae85b9b114414833d82d0eb Mon Sep 17 00:00:00 2001
From: RobinMalfait <1834413+RobinMalfait@users.noreply.github.com>
Date: Tue, 22 Oct 2024 16:09:53 +0000
Subject: [PATCH 2/5] Add codemod for migrating `@variants` and `@responsive`
directives (#14748)
This PR migrates the `@variants` and `@responsive` directives.
In Tailwind CSS v2, these were used to generate certain variants of responsive variants for the give classes. In Tailwind CSS v3, these still worked but were implemented as a no-op such that these directives don't end up in your final CSS.
In Tailwind CSS v4, these don't exist at all anymore, so we can safely get rid of them by replacing them with their contents.
Input:
```css
@variants hover, focus {
.foo {
color: red;
}
}
@responsive {
.bar {
color: blue;
}
}
```
Output:
```css
.foo {
color: red;
}
.bar {
color: blue;
}
```
---
CHANGELOG.md | 1 +
integrations/upgrade/index.test.ts | 10 ++++++
.../migrate-tailwind-directives.test.ts | 16 +++++++++
.../codemods/migrate-tailwind-directives.ts | 12 +++++++
.../migrate-variants-directive.test.ts | 33 +++++++++++++++++
.../codemods/migrate-variants-directive.ts | 35 +++++++++++++++++++
packages/@tailwindcss-upgrade/src/migrate.ts | 2 ++
7 files changed, 109 insertions(+)
create mode 100644 packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.test.ts
create mode 100644 packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 437259146..1d0b4831f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700))
- _Upgrade (experimental)_: Allow JS configuration files with `corePlugins` options to be migrated to CSS ([#14742](https://github.com/tailwindlabs/tailwindcss/pull/14742))
+- _Upgrade (experimental)_: Migrate `@variants` and `@responsive` directives ([#14748](https://github.com/tailwindlabs/tailwindcss/pull/14748))
### Fixed
diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts
index 80005c088..ceec310ae 100644
--- a/integrations/upgrade/index.test.ts
+++ b/integrations/upgrade/index.test.ts
@@ -26,6 +26,12 @@ test(
@tailwind base;
@tailwind components;
@tailwind utilities;
+
+ @variants hover, focus {
+ .foo {
+ color: red;
+ }
+ }
`,
},
},
@@ -40,6 +46,10 @@ test(
--- ./src/input.css ---
@import 'tailwindcss';
+
+ @utility foo {
+ color: red;
+ }
"
`)
diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts
index ba2d9ea9d..7c74b1ed6 100644
--- a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts
+++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts
@@ -400,3 +400,19 @@ it('should drop `@tailwind variants;`', async () => {
`),
).toEqual('')
})
+
+it('should replace `@responsive` with its children', async () => {
+ expect(
+ await migrate(css`
+ @responsive {
+ .foo {
+ color: red;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ ".foo {
+ color: red;
+ }"
+ `)
+})
diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts
index 0f7143017..d3d74a65e 100644
--- a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts
+++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts
@@ -54,6 +54,18 @@ export function migrateTailwindDirectives(options: { newPrefix: string | null })
) {
node.remove()
}
+
+ // Replace Tailwind CSS v2 directives that still worked in v3.
+ else if (node.name === 'responsive') {
+ if (node.nodes) {
+ for (let child of node.nodes) {
+ child.raws.tailwind_pretty = true
+ }
+ node.replaceWith(node.nodes)
+ } else {
+ node.remove()
+ }
+ }
})
// Insert default import if all directives are present
diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.test.ts
new file mode 100644
index 000000000..e18a161e6
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.test.ts
@@ -0,0 +1,33 @@
+import dedent from 'dedent'
+import postcss from 'postcss'
+import { expect, it } from 'vitest'
+import { formatNodes } from './format-nodes'
+import { migrateVariantsDirective } from './migrate-variants-directive'
+
+const css = dedent
+
+function migrate(input: string) {
+ return postcss()
+ .use(migrateVariantsDirective())
+ .use(formatNodes())
+ .process(input, { from: expect.getState().testPath })
+ .then((result) => result.css)
+}
+
+it('should replace `@variants` with `@layer utilities`', async () => {
+ expect(
+ await migrate(css`
+ @variants hover, focus {
+ .foo {
+ color: red;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "@layer utilities {
+ .foo {
+ color: red;
+ }
+ }"
+ `)
+})
diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.ts
new file mode 100644
index 000000000..1592c0059
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.ts
@@ -0,0 +1,35 @@
+import { type Plugin, type Root } from 'postcss'
+
+export function migrateVariantsDirective(): Plugin {
+ function migrate(root: Root) {
+ root.walkAtRules('variants', (node) => {
+ // Migrate `@variants` to `@utility` because `@variants` make the classes
+ // an actual utility.
+ // ```css
+ // @variants hover {
+ // .foo {}
+ // }
+ // ```
+ //
+ // Means that you can do this in your HTML:
+ // ```html
+ //
+ // ```
+ //
+ // Notice the `focus:`, even though we _only_ configured the `hover`
+ // variant.
+ //
+ // This means that we can convert it to an `@layer utilities` rule. Later,
+ // this will get converted to an `@utility` rule.
+ if (node.name === 'variants') {
+ node.name = 'layer'
+ node.params = 'utilities'
+ }
+ })
+ }
+
+ return {
+ postcssPlugin: '@tailwindcss/upgrade/migrate-variants-directive',
+ OnceExit: migrate,
+ }
+}
diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts
index 1b72afc6f..4220904c9 100644
--- a/packages/@tailwindcss-upgrade/src/migrate.ts
+++ b/packages/@tailwindcss-upgrade/src/migrate.ts
@@ -11,6 +11,7 @@ import { migrateMediaScreen } from './codemods/migrate-media-screen'
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives'
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 { resolveCssId } from './utils/resolve'
@@ -38,6 +39,7 @@ export async function migrateContents(
.use(migrateAtApply(options))
.use(migrateThemeToVar(options))
.use(migrateMediaScreen(options))
+ .use(migrateVariantsDirective())
.use(migrateAtLayerUtilities(stylesheet))
.use(migrateMissingLayers())
.use(migrateTailwindDirectives(options))
From 02cb52ad38ce540862aac8e26efde51f5d4a3c15 Mon Sep 17 00:00:00 2001
From: RobinMalfait <1834413+RobinMalfait@users.noreply.github.com>
Date: Tue, 22 Oct 2024 16:09:55 +0000
Subject: [PATCH 3/5] Add codemod for migrating the `@screen` directive
(#14749)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR adds a codemod for migrating the old `@screen` directive from Tailwind
CSS v2 that also worked in Tailwind CSS v3 but wasn't documented anymore.
Internally, this first migrates `@screen md` to `@media screen(md)`, then we rely on the existing migration that migrates the `screen(…)` function.
Input:
```css
@screen md {
.foo {
color: red;
}
}
```
Output (IR):
```css
@media screen(md) {
.foo {
color: red;
}
}
```
Output:
```css
@media theme(--breakpoint-md) {
.foo {
color: red;
}
}
```
---
CHANGELOG.md | 1 +
.../src/codemods/migrate-media-screen.test.ts | 18 ++++++++++++++++++
.../src/codemods/migrate-media-screen.ts | 7 +++++++
3 files changed, 26 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d0b4831f..81c92200c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700))
- _Upgrade (experimental)_: Allow JS configuration files with `corePlugins` options to be migrated to CSS ([#14742](https://github.com/tailwindlabs/tailwindcss/pull/14742))
- _Upgrade (experimental)_: Migrate `@variants` and `@responsive` directives ([#14748](https://github.com/tailwindlabs/tailwindcss/pull/14748))
+- _Upgrade (experimental)_: Migrate `@screen` directive ([#14749](https://github.com/tailwindlabs/tailwindcss/pull/14749))
### Fixed
diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts
index b7370d4d1..d0d231207 100644
--- a/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts
+++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts
@@ -41,6 +41,24 @@ it('should migrate a built-in breakpoint', async () => {
`)
})
+it('should migrate `@screen` with a built-in breakpoint', async () => {
+ expect(
+ await migrate(css`
+ @screen md {
+ .foo {
+ color: red;
+ }
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ "@media (width >= theme(--breakpoint-md)) {
+ .foo {
+ color: red;
+ }
+ }"
+ `)
+})
+
it('should migrate a custom min-width screen (string)', async () => {
expect(
await migrate(
diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.ts
index 02bd73dda..538e482c5 100644
--- a/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.ts
+++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.ts
@@ -24,6 +24,13 @@ export function migrateMediaScreen({
return value ? buildMediaQuery(value) : null
})
+ // First migrate `@screen md` to `@media screen(md)`
+ root.walkAtRules('screen', (node) => {
+ node.name = 'media'
+ node.params = `screen(${node.params})`
+ })
+
+ // Then migrate the `screen(…)` function
root.walkAtRules((rule) => {
if (rule.name !== 'media') return
From c7b190f136f327732a8238dcfe36e4d5e667d82b Mon Sep 17 00:00:00 2001
From: RobinMalfait <1834413+RobinMalfait@users.noreply.github.com>
Date: Tue, 22 Oct 2024 16:18:13 +0000
Subject: [PATCH 4/5] Prepare preflight for border compatibility (#14745)
This PR prepares the `preflight.css` so that we can introduce border style compatibility in a future PR.
---
.../@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap | 2 +-
packages/tailwindcss/preflight.css | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap
index 507e72d5f..14a1fc61a 100644
--- a/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap
+++ b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap
@@ -514,7 +514,7 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = `
}
input:where(:not([type="button"], [type="reset"], [type="submit"])), select, textarea {
- border: 1px solid;
+ border-width: 1px;
}
button, input:where([type="button"], [type="reset"], [type="submit"]) {
diff --git a/packages/tailwindcss/preflight.css b/packages/tailwindcss/preflight.css
index c1857b1bf..6e57b9faa 100644
--- a/packages/tailwindcss/preflight.css
+++ b/packages/tailwindcss/preflight.css
@@ -200,7 +200,7 @@ textarea,
input:where(:not([type='button'], [type='reset'], [type='submit'])),
select,
textarea {
- border: 1px solid;
+ border-width: 1px;
}
/*
From 338a78050a78ee1d3f0f12824d9fb04614c93700 Mon Sep 17 00:00:00 2001
From: philipp-spiess <458591+philipp-spiess@users.noreply.github.com>
Date: Tue, 22 Oct 2024 16:24:30 +0000
Subject: [PATCH 5/5] Upgrade: Reduce number of false-positive migrations of
the important modifier (#14737)
The important candidate migration is one of the most broad we have since it matches for any utility that are prefixed with an exclamation mark.
When running the codemodes on our example projects, we noticed that this was instead creating false-positives with candidates used in code positions, e.g:
```ts
export default {
shouldNotUse: !border.shouldUse,
}
```
To prevent false-positives, this PR adds a heuristics to detect wether or not a candidate is used in a non-code position. We do this by checking the character before and after the modifier and only allow quotes or spaces.
This can cause candidates to not migrate that are valid Tailwind CSS classes, e.g.:
```ts
let classNames = `!underline${isHovered ? ' font-bold' : ''}`
```
This, however, is not a big issue since v4 can parse the v3 important prefix too.
---
CHANGELOG.md | 1 +
integrations/upgrade/js-config.test.ts | 12 ++++-
.../src/template/codemods/important.test.ts | 36 ++++++++++++-
.../src/template/codemods/important.ts | 52 +++++++++++++++++++
.../src/template/migrate.ts | 19 ++++++-
5 files changed, 116 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 81c92200c..515473f78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Upgrade (experimental)_: Ensure legacy theme values ending in `1` (like `theme(spacing.1)`) are correctly migrated to custom properties ([#14724](https://github.com/tailwindlabs/tailwindcss/pull/14724))
- _Upgrade (experimental)_: Migrate arbitrary values to bare values for the `from-*`, `via-*`, and `to-*` utilities ([#14725](https://github.com/tailwindlabs/tailwindcss/pull/14725))
- _Upgrade (experimental)_: Ensure `layer(utilities)` is removed from `@import` to keep `@utility` top-level ([#14738](https://github.com/tailwindlabs/tailwindcss/pull/14738))
+- _Upgrade (experimental)_: Don't migrate important modifiers that are actually logical negations (e.g. `let foo = !border` to `let foo = border!`) ([#14737](https://github.com/tailwindlabs/tailwindcss/pull/14737))
### Changed
diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts
index cf068b6e6..22e4ace9c 100644
--- a/integrations/upgrade/js-config.test.ts
+++ b/integrations/upgrade/js-config.test.ts
@@ -72,6 +72,11 @@ test(
@tailwind components;
@tailwind utilities;
`,
+ 'src/test.js': ts`
+ export default {
+ shouldNotUse: !border.shouldUse,
+ }
+ `,
'node_modules/my-external-lib/src/template.html': html`
Hello world!
@@ -82,7 +87,7 @@ test(
async ({ exec, fs }) => {
await exec('npx @tailwindcss/upgrade')
- expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
+ expect(await fs.dumpFiles('src/**/*.{css,js}')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@@ -134,6 +139,11 @@ test(
}
}
}
+
+ --- src/test.js ---
+ export default {
+ shouldNotUse: !border.shouldUse,
+ }
"
`)
diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts
index 4a53f8d4a..64f098b9b 100644
--- a/packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts
+++ b/packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts
@@ -15,5 +15,39 @@ test.each([
base: __dirname,
})
- expect(important(designSystem, {}, candidate)).toEqual(result)
+ expect(
+ important(designSystem, {}, candidate, {
+ contents: `"${candidate}"`,
+ start: 1,
+ end: candidate.length + 1,
+ }),
+ ).toEqual(result)
+})
+
+test('does not match false positives', async () => {
+ let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
+ base: __dirname,
+ })
+
+ expect(
+ important(designSystem, {}, '!border', {
+ contents: `let notBorder = !border\n`,
+ start: 16,
+ end: 16 + '!border'.length,
+ }),
+ ).toEqual('!border')
+})
+
+test('does not match false positives with spaces at the end of the line', async () => {
+ let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
+ base: __dirname,
+ })
+
+ expect(
+ important(designSystem, {}, '!border', {
+ contents: `let notBorder = !border \n`,
+ start: 16,
+ end: 16 + '!border'.length,
+ }),
+ ).toEqual('!border')
})
diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/important.ts b/packages/@tailwindcss-upgrade/src/template/codemods/important.ts
index 69dbdba85..454790529 100644
--- a/packages/@tailwindcss-upgrade/src/template/codemods/important.ts
+++ b/packages/@tailwindcss-upgrade/src/template/codemods/important.ts
@@ -19,9 +19,50 @@ export function important(
designSystem: DesignSystem,
_userConfig: Config,
rawCandidate: string,
+ location?: {
+ contents: string
+ start: number
+ end: number
+ },
): string {
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
if (candidate.important && candidate.raw[candidate.raw.length - 1] !== '!') {
+ // The important migration is one of the most broad migrations with a high
+ // potential of matching false positives since `!` is a valid character in
+ // most programming languages. Since v4 is technically backward compatible
+ // with v3 in that it can read `!` in the front of the utility too, we err
+ // on the side of caution and only migrate candidates that we are certain
+ // are inside of a string.
+ if (location) {
+ let isQuoteBeforeCandidate = false
+ for (let i = location.start - 1; i >= 0; i--) {
+ let char = location.contents.at(i)!
+ if (char === '\n') {
+ break
+ }
+ if (isQuote(char)) {
+ isQuoteBeforeCandidate = true
+ break
+ }
+ }
+
+ let isQuoteAfterCandidate = false
+ for (let i = location.end; i < location.contents.length; i++) {
+ let char = location.contents.at(i)!
+ if (char === '\n') {
+ break
+ }
+ if (isQuote(char)) {
+ isQuoteAfterCandidate = true
+ break
+ }
+ }
+
+ if (!isQuoteBeforeCandidate || !isQuoteAfterCandidate) {
+ continue
+ }
+ }
+
// The printCandidate function will already put the exclamation mark in
// the right place, so we just need to mark this candidate as requiring a
// migration.
@@ -31,3 +72,14 @@ export function important(
return rawCandidate
}
+
+function isQuote(char: string) {
+ switch (char) {
+ case '"':
+ case "'":
+ case '`':
+ return true
+ default:
+ return false
+ }
+}
diff --git a/packages/@tailwindcss-upgrade/src/template/migrate.ts b/packages/@tailwindcss-upgrade/src/template/migrate.ts
index c6aa0a11c..5e2d02eab 100644
--- a/packages/@tailwindcss-upgrade/src/template/migrate.ts
+++ b/packages/@tailwindcss-upgrade/src/template/migrate.ts
@@ -17,6 +17,11 @@ export type Migration = (
designSystem: DesignSystem,
userConfig: Config,
rawCandidate: string,
+ location?: {
+ contents: string
+ start: number
+ end: number
+ },
) => string
export const DEFAULT_MIGRATIONS: Migration[] = [
@@ -34,9 +39,15 @@ export function migrateCandidate(
designSystem: DesignSystem,
userConfig: Config,
rawCandidate: string,
+ // Location is only set when migrating a candidate from a source file
+ location?: {
+ contents: string
+ start: number
+ end: number
+ },
): string {
for (let migration of DEFAULT_MIGRATIONS) {
- rawCandidate = migration(designSystem, userConfig, rawCandidate)
+ rawCandidate = migration(designSystem, userConfig, rawCandidate, location)
}
return rawCandidate
}
@@ -52,7 +63,11 @@ export default async function migrateContents(
let changes: StringChange[] = []
for (let { rawCandidate, start, end } of candidates) {
- let migratedCandidate = migrateCandidate(designSystem, userConfig, rawCandidate)
+ let migratedCandidate = migrateCandidate(designSystem, userConfig, rawCandidate, {
+ contents,
+ start,
+ end,
+ })
if (migratedCandidate === rawCandidate) {
continue