Skip to content

Commit

Permalink
Detect and migrate static plugin usages (#14648)
Browse files Browse the repository at this point in the history
This PR builds on top of the new [JS config to CSS
migration](#14651) and
extends it to support migrating _static_ plugins.

What are _static_ plugins you might ask? Static plugins are plugins
where we can statically determine that these are coming from a different
file (so there is nothing inside the JS config that creates them). An
example for this is this config file:

```js
import typographyPlugin from '@tailwindcss/typography'
import { type Config } from 'tailwindcss'

export default {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  darkMode: 'selector',
  plugins: [typographyPlugin],
} satisfies Config
```

Here, the `plugins` array only has one element and it is a static import
from the `@tailwindcss/typography` module. In this PR we attempt to
parse the config file via Tree-sitter to extract the following
information from this file:

- What are the contents of the `plugins` array
- What are statically imported resources from the file

We then check if _all_ entries in the `plugins` array are either static
resources or _strings_ (something I saw working in some tests but I’m
not sure it still does). We migrate the JS config file to CSS if all
plugins are static and we can migrate them to CSS `@plugin` calls.

## Todo

This will need to be rebased after the updated tests in #14648
  • Loading branch information
philipp-spiess authored Oct 14, 2024
1 parent 4b19de3 commit 468cb5e
Show file tree
Hide file tree
Showing 9 changed files with 521 additions and 57 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Upgrade (experimental)_: Migrate v3 PostCSS setups to v4 in some cases ([#14612](https://github.com/tailwindlabs/tailwindcss/pull/14612))
- _Upgrade (experimental)_: Automatically discover JavaScript config files ([#14597](https://github.com/tailwindlabs/tailwindcss/pull/14597))
- _Upgrade (experimental)_: Migrate legacy classes to the v4 alternative ([#14643](https://github.com/tailwindlabs/tailwindcss/pull/14643))
- _Upgrade (experimental)_: Migrate static JS configurations to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639), [#14650](https://github.com/tailwindlabs/tailwindcss/pull/14650))
- _Upgrade (experimental)_: Migrate static JS configurations to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639), [#14650](https://github.com/tailwindlabs/tailwindcss/pull/14650), [#14648](https://github.com/tailwindlabs/tailwindcss/pull/14648))
- _Upgrade (experimental)_: Migrate `@media screen(…)` when running codemods ([#14603](https://github.com/tailwindlabs/tailwindcss/pull/14603))
- _Upgrade (experimental)_: Inject `@config "…"` when a `tailwind.config.{js,ts,…}` is detected ([#14635](https://github.com/tailwindlabs/tailwindcss/pull/14635))
- _Upgrade (experimental)_: Migrate `aria-*`, `data-*`, and `supports-*` variants from arbitrary values to bare values ([#14644](https://github.com/tailwindlabs/tailwindcss/pull/14644))
Expand Down
99 changes: 47 additions & 52 deletions integrations/upgrade/js-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,29 +103,32 @@ test(
)

test(
'does not upgrade JS config files with functions in the theme config',
'upgrades JS config files with plugins',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
import typography from '@tailwindcss/typography'
import customPlugin from './custom-plugin'
export default {
theme: {
extend: {
colors: ({ colors }) => ({
gray: colors.neutral,
}),
},
},
plugins: [typography, customPlugin],
} satisfies Config
`,
'custom-plugin.js': ts`
export default function ({ addVariant }) {
addVariant('inverted', '@media (inverted-colors: inverted)')
addVariant('hocus', ['&:focus', '&:hover'])
}
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
Expand All @@ -136,35 +139,26 @@ test(
async ({ exec, fs }) => {
await exec('npx @tailwindcss/upgrade')

expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(`
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@config '../tailwind.config.ts';
@plugin '@tailwindcss/typography';
@plugin '../custom-plugin';
"
`)

expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(`
"
--- tailwind.config.ts ---
import { type Config } from 'tailwindcss'
export default {
theme: {
extend: {
colors: ({ colors }) => ({
gray: colors.neutral,
}),
},
},
} satisfies Config
"
`)
},
)

test(
'does not upgrade JS config files with theme keys contributed to by plugins in the theme config',
'does not upgrade JS config files with functions in the theme config',
{
fs: {
'package.json': json`
Expand All @@ -179,13 +173,10 @@ test(
export default {
theme: {
typography: {
DEFAULT: {
css: {
'--tw-prose-body': 'red',
color: 'var(--tw-prose-body)',
},
},
extend: {
colors: ({ colors }) => ({
gray: colors.neutral,
}),
},
},
} satisfies Config
Expand All @@ -194,14 +185,13 @@ test(
@tailwind base;
@tailwind components;
@tailwind utilities;
@config '../tailwind.config.ts';
`,
},
},
async ({ exec, fs }) => {
await exec('npx @tailwindcss/upgrade')

expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
Expand All @@ -216,13 +206,10 @@ test(
export default {
theme: {
typography: {
DEFAULT: {
css: {
'--tw-prose-body': 'red',
color: 'var(--tw-prose-body)',
},
},
extend: {
colors: ({ colors }) => ({
gray: colors.neutral,
}),
},
},
} satisfies Config
Expand All @@ -232,36 +219,37 @@ test(
)

test(
'does not upgrade JS config files with plugins',
'does not upgrade JS config files with theme keys contributed to by plugins in the theme config',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
import typography from '@tailwindcss/typography'
import customPlugin from './custom-plugin'
export default {
plugins: [typography, customPlugin],
theme: {
typography: {
DEFAULT: {
css: {
'--tw-prose-body': 'red',
color: 'var(--tw-prose-body)',
},
},
},
},
} satisfies Config
`,
'custom-plugin.js': ts`
export default function ({ addVariant }) {
addVariant('inverted', '@media (inverted-colors: inverted)')
addVariant('hocus', ['&:focus', '&:hover'])
}
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@config '../tailwind.config.ts';
`,
},
},
Expand All @@ -280,11 +268,18 @@ test(
"
--- tailwind.config.ts ---
import { type Config } from 'tailwindcss'
import typography from '@tailwindcss/typography'
import customPlugin from './custom-plugin'
export default {
plugins: [typography, customPlugin],
theme: {
typography: {
DEFAULT: {
css: {
'--tw-prose-body': 'red',
color: 'var(--tw-prose-body)',
},
},
},
},
} satisfies Config
"
`)
Expand Down
4 changes: 3 additions & 1 deletion packages/@tailwindcss-upgrade/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
"postcss-selector-parser": "^6.1.2",
"prettier": "^3.3.3",
"string-byte-slice": "^3.0.0",
"tailwindcss": "workspace:^"
"tailwindcss": "workspace:^",
"tree-sitter": "^0.21.1",
"tree-sitter-typescript": "^0.23.0"
},
"devDependencies": {
"@types/node": "catalog:",
Expand Down
12 changes: 11 additions & 1 deletion packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,21 @@ export function migrateConfig(
let absolute = path.resolve(source.base, source.pattern)
css += `@source '${relativeToStylesheet(sheet, absolute)}';\n`
}

if (jsConfigMigration.sources.length > 0) {
css = css + '\n'
}

for (let plugin of jsConfigMigration.plugins) {
let relative =
plugin.path[0] === '.'
? relativeToStylesheet(sheet, path.resolve(plugin.base, plugin.path))
: plugin.path
css += `@plugin '${relative}';\n`
}
if (jsConfigMigration.plugins.length > 0) {
css = css + '\n'
}

cssConfig.append(postcss.parse(css + jsConfigMigration.css))
}

Expand Down
17 changes: 15 additions & 2 deletions packages/@tailwindcss-upgrade/src/migrate-js-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { deepMerge } from '../../tailwindcss/src/compat/config/deep-merge'
import { mergeThemeExtension } from '../../tailwindcss/src/compat/config/resolve-config'
import type { ThemeConfig } from '../../tailwindcss/src/compat/config/types'
import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode'
import { findStaticPlugins } from './utils/extract-static-plugins'
import { info } from './utils/renderer'

const __filename = fileURLToPath(import.meta.url)
Expand All @@ -21,6 +22,7 @@ export type JSConfigMigration =
// Could not convert the config file, need to inject it as-is in a @config directive
null | {
sources: { base: string; pattern: string }[]
plugins: { base: string; path: string }[]
css: string
}

Expand All @@ -41,6 +43,7 @@ export async function migrateJsConfig(
}

let sources: { base: string; pattern: string }[] = []
let plugins: { base: string; path: string }[] = []
let cssConfigs: string[] = []

if ('darkMode' in unresolvedConfig) {
Expand All @@ -56,8 +59,16 @@ export async function migrateJsConfig(
if (themeConfig) cssConfigs.push(themeConfig)
}

let simplePlugins = findStaticPlugins(source)
if (simplePlugins !== null) {
for (let plugin of simplePlugins) {
plugins.push({ base, path: plugin })
}
}

return {
sources,
plugins,
css: cssConfigs.join('\n'),
}
}
Expand Down Expand Up @@ -168,7 +179,9 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean {
return ['string', 'number', 'boolean', 'undefined'].includes(typeof value)
}

if (!isSimpleValue(unresolvedConfig)) {
// Plugins are more complex, so we have a special heuristics for them.
let { plugins, ...remainder } = unresolvedConfig
if (!isSimpleValue(remainder)) {
return false
}

Expand All @@ -186,7 +199,7 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean {
return false
}

if (unresolvedConfig.plugins && unresolvedConfig.plugins.length > 0) {
if (findStaticPlugins(source) === null) {
return false
}

Expand Down
Loading

0 comments on commit 468cb5e

Please sign in to comment.