mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Inject @config "..." when a tailwind.config.{js,ts,...} is detected (#14635)
This PR injects a `@config "…"` in the CSS file if a JS based config has been found. We will try to inject the `@config` in a sensible place: 1. Above the very first `@theme` 2. If that doesn't work, below the last `@import` 3. If that doesn't work, at the top of the file (as a last resort)
This commit is contained in:
parent
4d1becd2f9
commit
fb1731a2de
@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- _Upgrade (experimental)_: Ensure CSS before a layer stays unlayered when running codemods ([#14596](https://github.com/tailwindlabs/tailwindcss/pull/14596))
|
||||
- _Upgrade (experimental)_: Resolve issues where some prefixed candidates were not properly migrated ([#14600](https://github.com/tailwindlabs/tailwindcss/pull/14600))
|
||||
- _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))
|
||||
|
||||
## [4.0.0-alpha.26] - 2024-10-03
|
||||
|
||||
|
||||
@ -39,7 +39,9 @@ test(
|
||||
<div class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)]"></div>
|
||||
|
||||
--- ./src/input.css ---
|
||||
@import 'tailwindcss';"
|
||||
@import 'tailwindcss';
|
||||
@config "../tailwind.config.js";
|
||||
"
|
||||
`)
|
||||
|
||||
let packageJsonContent = await fs.read('package.json')
|
||||
@ -95,9 +97,12 @@ test(
|
||||
--- ./src/input.css ---
|
||||
@import 'tailwindcss' prefix(tw);
|
||||
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
.btn {
|
||||
@apply tw:rounded-md! tw:px-2 tw:py-1 tw:bg-blue-500 tw:text-white;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
@ -140,6 +145,8 @@ test(
|
||||
--- ./src/index.css ---
|
||||
@import 'tailwindcss';
|
||||
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
.a {
|
||||
@apply flex;
|
||||
}
|
||||
@ -150,7 +157,8 @@ test(
|
||||
|
||||
.c {
|
||||
@apply flex! flex-col! items-center!;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
@ -193,6 +201,8 @@ test(
|
||||
--- ./src/index.css ---
|
||||
@import 'tailwindcss';
|
||||
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
color: #333;
|
||||
@ -203,7 +213,8 @@ test(
|
||||
.btn {
|
||||
color: red;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
@ -251,6 +262,8 @@ test(
|
||||
--- ./src/index.css ---
|
||||
@import 'tailwindcss';
|
||||
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
@utility btn {
|
||||
@apply rounded-md px-2 py-1 bg-blue-500 text-white;
|
||||
}
|
||||
@ -261,7 +274,8 @@ test(
|
||||
}
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
@ -533,7 +547,8 @@ test(
|
||||
<div class="flex"></div>
|
||||
|
||||
--- ./src/other.html ---
|
||||
<div class="tw:flex"></div>"
|
||||
<div class="tw:flex"></div>
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
@ -573,7 +588,8 @@ test(
|
||||
<div class="tw:bg-linear-to-t"></div>
|
||||
|
||||
--- ./src/other.html ---
|
||||
<div class="bg-gradient-to-t"></div>"
|
||||
<div class="bg-gradient-to-t"></div>
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
@ -615,6 +631,7 @@ test(
|
||||
--- ./src/index.css ---
|
||||
@import 'tailwindcss';
|
||||
@import './utilities.css';
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
--- ./src/utilities.css ---
|
||||
@utility no-scrollbar {
|
||||
@ -623,7 +640,8 @@ test(
|
||||
}
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
@ -730,6 +748,7 @@ test(
|
||||
@import './c.1.css' layer(utilities);
|
||||
@import './c.1.utilities.css';
|
||||
@import './d.1.css';
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
--- ./src/a.1.css ---
|
||||
@import './a.1.utilities.css'
|
||||
@ -804,7 +823,8 @@ test(
|
||||
--- ./src/d.4.css ---
|
||||
@utility from-a-4 {
|
||||
color: blue;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
@ -862,14 +882,141 @@ test(
|
||||
--- ./src/root.1.css ---
|
||||
@import 'tailwindcss/utilities' layer(utilities);
|
||||
@import './a.1.css' layer(utilities);
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
--- ./src/root.2.css ---
|
||||
@import 'tailwindcss/utilities' layer(utilities);
|
||||
@import './a.1.css' layer(components);
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
--- ./src/root.3.css ---
|
||||
@import 'tailwindcss/utilities' layer(utilities);
|
||||
@import './a.1.css' layer(utilities);"
|
||||
@import './a.1.css' layer(utilities);
|
||||
@config "../tailwind.config.js";
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'injecting `@config` 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}'],
|
||||
}
|
||||
`,
|
||||
'src/index.html': html`
|
||||
<h1>🤠👋</h1>
|
||||
<div class="!flex sm:!block bg-gradient-to-t bg-[--my-red]"></div>
|
||||
`,
|
||||
'src/root.1.css': css`
|
||||
/* Inject missing @config */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
'src/root.2.css': css`
|
||||
/* Already contains @config */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@config "../tailwind.config.js";
|
||||
`,
|
||||
'src/root.3.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.4.css': css`
|
||||
/* Inject missing @config due to nested imports with tailwind imports */
|
||||
@import './root.4/base.css';
|
||||
@import './root.4/utilities.css';
|
||||
`,
|
||||
'src/root.4/base.css': css`@import 'tailwindcss/base';`,
|
||||
'src/root.4/utilities.css': css`@import 'tailwindcss/utilities';`,
|
||||
|
||||
'src/root.5.css': css`@import './root.5/tailwind.css';`,
|
||||
'src/root.5/tailwind.css': css`
|
||||
/* Inject missing @config in this file, due to full import */
|
||||
@import 'tailwindcss';
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./src/index.html ---
|
||||
<h1>🤠👋</h1>
|
||||
<div class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)]"></div>
|
||||
|
||||
--- ./src/root.1.css ---
|
||||
/* Inject missing @config */
|
||||
@import 'tailwindcss';
|
||||
@config "../tailwind.config.ts";
|
||||
|
||||
--- ./src/root.2.css ---
|
||||
/* Already contains @config */
|
||||
@import 'tailwindcss';
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
--- ./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;
|
||||
}
|
||||
|
||||
--- ./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);
|
||||
|
||||
--- ./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";
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
@ -330,7 +330,8 @@ export function test(
|
||||
return a[0].localeCompare(z[0])
|
||||
})
|
||||
.map(([file, content]) => `--- ${file} ---\n${content || '<EMPTY>'}`)
|
||||
.join('\n\n')}`
|
||||
.join('\n\n')
|
||||
.trim()}\n`
|
||||
},
|
||||
async expectFileToContain(filePath, contents) {
|
||||
return retryAssertion(async () => {
|
||||
|
||||
101
packages/@tailwindcss-upgrade/src/codemods/migrate-at-config.ts
Normal file
101
packages/@tailwindcss-upgrade/src/codemods/migrate-at-config.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import path from 'node:path'
|
||||
import { AtRule, type Plugin, type Root } from 'postcss'
|
||||
import { normalizePath } from '../../../@tailwindcss-node/src/normalize-path'
|
||||
import type { Stylesheet } from '../stylesheet'
|
||||
import { walk, WalkAction } from '../utils/walk'
|
||||
|
||||
export function migrateAtConfig(
|
||||
sheet: Stylesheet,
|
||||
{ configFilePath }: { configFilePath: string },
|
||||
): Plugin {
|
||||
function injectInto(sheet: Stylesheet) {
|
||||
let root = sheet.root
|
||||
|
||||
// We don't have a sheet with a file path
|
||||
if (!sheet.file) return
|
||||
|
||||
// Skip if there is already a `@config` directive
|
||||
{
|
||||
let hasConfig = false
|
||||
root.walkAtRules('config', () => {
|
||||
hasConfig = true
|
||||
return false
|
||||
})
|
||||
if (hasConfig) return
|
||||
}
|
||||
|
||||
// Figure out the path to the config file
|
||||
let sheetPath = sheet.file
|
||||
let configPath = configFilePath
|
||||
|
||||
let relative = path.relative(path.dirname(sheetPath), configPath)
|
||||
if (relative[0] !== '.') {
|
||||
relative = `./${relative}`
|
||||
}
|
||||
// Ensure relative is a posix style path since we will merge it with the
|
||||
// glob.
|
||||
relative = normalizePath(relative)
|
||||
|
||||
// Inject the `@config` in a sensible place
|
||||
// 1. Below the last `@import`
|
||||
// 2. At the top of the file
|
||||
let locationNode = null as AtRule | null
|
||||
|
||||
walk(root, (node) => {
|
||||
if (node.type === 'atrule' && node.name === 'import') {
|
||||
locationNode = node
|
||||
}
|
||||
|
||||
return WalkAction.Skip
|
||||
})
|
||||
|
||||
let configNode = new AtRule({ name: 'config', params: `"${relative}"` })
|
||||
|
||||
if (!locationNode) {
|
||||
root.prepend(configNode)
|
||||
} else if (locationNode.name === 'import') {
|
||||
locationNode.after(configNode)
|
||||
}
|
||||
}
|
||||
|
||||
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, we can inject the
|
||||
// `@config` directive directly into this stylesheet.
|
||||
// - If we are the root file (no parents), then we can inject the `@config`
|
||||
// directive directly into this file as well.
|
||||
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-at-config',
|
||||
OnceExit: migrate,
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,13 @@ export function migrateMissingLayers(): Plugin {
|
||||
root.each((node) => {
|
||||
if (node.type === 'atrule') {
|
||||
// Known Tailwind directives that should not be inside a layer.
|
||||
if (node.name === 'theme' || node.name === 'utility') {
|
||||
if (
|
||||
node.name === 'config' ||
|
||||
node.name === 'source' ||
|
||||
node.name === 'theme' ||
|
||||
node.name === 'utility' ||
|
||||
node.name === 'variant'
|
||||
) {
|
||||
if (bucket.length > 0) {
|
||||
buckets.push([lastLayer, bucket.splice(0)])
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
|
||||
import dedent from 'dedent'
|
||||
import path from 'node:path'
|
||||
import postcss from 'postcss'
|
||||
import { expect, it } from 'vitest'
|
||||
import { formatNodes } from './codemods/format-nodes'
|
||||
@ -13,7 +14,13 @@ let designSystem = await __unstable__loadDesignSystem(
|
||||
`,
|
||||
{ base: __dirname },
|
||||
)
|
||||
let config = { designSystem, userConfig: {}, newPrefix: null }
|
||||
|
||||
let config = {
|
||||
designSystem,
|
||||
userConfig: {},
|
||||
newPrefix: null,
|
||||
configFilePath: path.resolve(__dirname, './tailwind.config.js'),
|
||||
}
|
||||
|
||||
function migrate(input: string, config: any) {
|
||||
return migrateContents(input, config, expect.getState().testPath)
|
||||
@ -87,6 +94,8 @@ it('should migrate a stylesheet', async () => {
|
||||
).toMatchInlineSnapshot(`
|
||||
"@import 'tailwindcss';
|
||||
|
||||
@config "./tailwind.config.js";
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
overflow: hidden;
|
||||
@ -138,7 +147,8 @@ it('should migrate a stylesheet (with imports)', async () => {
|
||||
"@import 'tailwindcss';
|
||||
@import './my-base.css' layer(base);
|
||||
@import './my-components.css' layer(components);
|
||||
@import './my-utilities.css' layer(utilities);"
|
||||
@import './my-utilities.css' layer(utilities);
|
||||
@config "./tailwind.config.js";"
|
||||
`)
|
||||
})
|
||||
|
||||
@ -163,6 +173,7 @@ 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";
|
||||
@layer base {
|
||||
html {
|
||||
color: red;
|
||||
|
||||
@ -5,6 +5,7 @@ import type { DesignSystem } from '../../tailwindcss/src/design-system'
|
||||
import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
|
||||
import { segment } from '../../tailwindcss/src/utils/segment'
|
||||
import { migrateAtApply } from './codemods/migrate-at-apply'
|
||||
import { migrateAtConfig } from './codemods/migrate-at-config'
|
||||
import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities'
|
||||
import { migrateMediaScreen } from './codemods/migrate-media-screen'
|
||||
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
|
||||
@ -17,6 +18,7 @@ export interface MigrateOptions {
|
||||
newPrefix: string | null
|
||||
designSystem: DesignSystem
|
||||
userConfig: Config
|
||||
configFilePath: string
|
||||
}
|
||||
|
||||
export async function migrateContents(
|
||||
@ -35,6 +37,7 @@ export async function migrateContents(
|
||||
.use(migrateAtLayerUtilities(stylesheet))
|
||||
.use(migrateMissingLayers())
|
||||
.use(migrateTailwindDirectives(options))
|
||||
.use(migrateAtConfig(stylesheet, options))
|
||||
.process(stylesheet.root, { from: stylesheet.file ?? undefined })
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ export async function prepareConfig(
|
||||
designSystem: DesignSystem
|
||||
globs: { base: string; pattern: string }[]
|
||||
userConfig: Config
|
||||
configFilePath: string
|
||||
|
||||
newPrefix: string | null
|
||||
}> {
|
||||
@ -57,7 +58,13 @@ export async function prepareConfig(
|
||||
__unstable__loadDesignSystem(input, { base: __dirname }),
|
||||
])
|
||||
|
||||
return { designSystem, globs: compiler.globs, userConfig, newPrefix }
|
||||
return {
|
||||
designSystem,
|
||||
globs: compiler.globs,
|
||||
userConfig,
|
||||
newPrefix,
|
||||
configFilePath: fullConfigPath,
|
||||
}
|
||||
} catch (e: any) {
|
||||
error('Could not load the configuration file: ' + e.message)
|
||||
process.exit(1)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user