Add simple JS config migration (#14639)

This PR implements the first version of JS config file migration to CSS.
It is based on the most simple config setups we are using in the
Tailwind UI templates Commit, Primer, Radiant, and Studio.

The example we use in the integration test is a config that looks like
this:

```js
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'

module.exports = {
  darkMode: 'selector',
  content: ['./src/**/*.{html,js}'],
  theme: {
    boxShadow: {
      sm: '0 2px 6px rgb(15 23 42 / 0.08)',
    },
    colors: {
      red: {
        500: '#ef4444',
      },
    },
    fontSize: {
      xs: ['0.75rem', { lineHeight: '1rem' }],
      sm: ['0.875rem', { lineHeight: '1.5rem' }],
      base: ['1rem', { lineHeight: '2rem' }],
    },
    extend: {
      colors: {
        red: {
          600: '#dc2626',
        },
      },
      fontFamily: {
        sans: 'Inter, system-ui, sans-serif',
        display: ['Cabinet Grotesk', ...defaultTheme.fontFamily.sans],
      },
      borderRadius: {
        '4xl': '2rem',
      },
    },
  },
  plugins: [],
} satisfies Config
```

As you can see, this file only has a `darkMode` selector, custom
`content` globs, a `theme` (with some theme keys being overwriting the
default theme and some others extending the defaults). Note that it does
not support `plugins` and/or `presets` yet.

In the case above, we will find the CSS file containing the existing
`@tailwind` directives and are migrating it to the following:

```css
@import 'tailwindcss';

@source './**/*.{html,js}';

@variant dark (&:where(.dark, .dark *));

@theme {
  --box-shadow-*: initial;
  --box-shadow-sm: 0 2px 6px rgb(15 23 42 / 0.08);

  --color-*: initial;
  --color-red-500: #ef4444;

  --font-size-*: initial;
  --font-size-xs: 0.75rem;
  --font-size-xs--line-height: 1rem;
  --font-size-sm: 0.875rem;
  --font-size-sm--line-height: 1.5rem;
  --font-size-base: 1rem;
  --font-size-base--line-height: 2rem;

  --color-red-600: #dc2626;

  --font-family-sans: Inter, system-ui, sans-serif;
  --font-family-display: Cabinet Grotesk, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";

  --border-radius-4xl: 2rem;
} 
```

This replicates all features of the JS config so we can even delete the
existing JS config in this case.

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
Philipp Spiess 2024-10-11 15:27:53 +02:00 committed by GitHub
parent bd3d6bc09b
commit 0cfb98484b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 528 additions and 136 deletions

View File

@ -14,6 +14,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)_: The upgrade tool now automatically discovers your JavaScript config ([#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)_: Fully convert simple JS configs to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639))
### Fixed

View File

@ -40,7 +40,8 @@ test(
--- ./src/input.css ---
@import 'tailwindcss';
@config '../tailwind.config.js';
@source './**/*.{html,js}';
"
`)
@ -71,8 +72,9 @@ test(
}
`,
'src/index.html': html`
<h1>🤠👋</h1>
<div class="!tw__flex sm:!tw__block tw__bg-gradient-to-t flex [color:red]"></div>
<div
class="!tw__flex sm:!tw__block tw__bg-gradient-to-t flex [color:red]"
></div>
`,
'src/input.css': css`
@tailwind base;
@ -91,13 +93,14 @@ test(
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
"
--- ./src/index.html ---
<h1>🤠👋</h1>
<div class="tw:flex! tw:sm:block! tw:bg-linear-to-t flex tw:[color:red]"></div>
<div
class="tw:flex! tw:sm:block! tw:bg-linear-to-t flex tw:[color:red]"
></div>
--- ./src/input.css ---
@import 'tailwindcss' prefix(tw);
@config '../tailwind.config.js';
@source './**/*.{html,js}';
.btn {
@apply tw:rounded-md! tw:px-2 tw:py-1 tw:bg-blue-500 tw:text-white;
@ -145,8 +148,6 @@ test(
--- ./src/index.css ---
@import 'tailwindcss';
@config '../tailwind.config.js';
.a {
@apply flex;
}
@ -201,8 +202,6 @@ test(
--- ./src/index.css ---
@import 'tailwindcss';
@config '../tailwind.config.js';
@layer base {
html {
color: #333;
@ -262,8 +261,6 @@ test(
--- ./src/index.css ---
@import 'tailwindcss';
@config '../tailwind.config.js';
@utility btn {
@apply rounded-md px-2 py-1 bg-blue-500 text-white;
}
@ -631,7 +628,6 @@ test(
--- ./src/index.css ---
@import 'tailwindcss';
@import './utilities.css';
@config '../tailwind.config.js';
--- ./src/utilities.css ---
@utility no-scrollbar {
@ -748,7 +744,6 @@ 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'
@ -882,17 +877,14 @@ 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);
@config '../tailwind.config.js';
"
`)
},
@ -912,11 +904,17 @@ test(
'tailwind.config.ts': js`
export default {
content: ['./src/**/*.{html,js}'],
plugins: [
() => {
// custom stuff which is too complicated to migrate to CSS
},
],
}
`,
'src/index.html': html`
<h1>🤠👋</h1>
<div class="!flex sm:!block bg-gradient-to-t bg-[--my-red]"></div>
<div
class="!flex sm:!block bg-gradient-to-t bg-[--my-red]"
></div>
`,
'src/root.1.css': css`
/* Inject missing @config */
@ -968,8 +966,9 @@ test(
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>
<div
class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)]"
></div>
--- ./src/root.1.css ---
/* Inject missing @config */

View File

@ -0,0 +1,152 @@
import { expect } from 'vitest'
import { css, json, test, ts } from '../utils'
test(
`upgrades a simple JS config file to CSS`,
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
module.exports = {
darkMode: 'selector',
content: ['./src/**/*.{html,js}', './my-app/**/*.{html,js}'],
theme: {
boxShadow: {
sm: '0 2px 6px rgb(15 23 42 / 0.08)',
},
colors: {
red: {
400: '#f87171',
500: 'red',
},
},
fontSize: {
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.5rem' }],
base: ['1rem', { lineHeight: '2rem' }],
},
extend: {
colors: {
red: {
500: '#ef4444',
600: '#dc2626',
},
},
fontFamily: {
sans: 'Inter, system-ui, sans-serif',
display: ['Cabinet Grotesk', ...defaultTheme.fontFamily.sans],
},
borderRadius: {
'4xl': '2rem',
},
},
},
plugins: [],
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@source './**/*.{html,js}';
@source '../my-app/**/*.{html,js}';
@variant dark (&:where(.dark, .dark *));
@theme {
--box-shadow-*: initial;
--box-shadow-sm: 0 2px 6px rgb(15 23 42 / 0.08);
--color-*: initial;
--color-red-400: #f87171;
--color-red-500: #ef4444;
--color-red-600: #dc2626;
--font-size-*: initial;
--font-size-xs: 0.75rem;
--font-size-xs--line-height: 1rem;
--font-size-sm: 0.875rem;
--font-size-sm--line-height: 1.5rem;
--font-size-base: 1rem;
--font-size-base--line-height: 2rem;
--font-family-sans: Inter, system-ui, sans-serif;
--font-family-display: Cabinet Grotesk, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--radius-4xl: 2rem;
}
"
`)
expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('')
},
)
test(
`does not upgrade a complex JS config file to CSS`,
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
export default {
plugins: [function complexConfig() {}],
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@config '../tailwind.config.ts';
"
`)
expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(`
"
--- tailwind.config.ts ---
import { type Config } from 'tailwindcss'
export default {
plugins: [function complexConfig() {}],
} satisfies Config
"
`)
},
)

View File

@ -75,7 +75,7 @@ export function test(
) {
return (only || (!process.env.CI && debug) ? defaultTest.only : defaultTest)(
name,
{ timeout: TEST_TIMEOUT, retry: debug || only ? 0 : 3 },
{ timeout: TEST_TIMEOUT, retry: process.env.CI ? 2 : 0 },
async (options) => {
let rootDir = debug ? path.join(REPO_ROOT, '.debug') : TMP_ROOT
await fs.mkdir(rootDir, { recursive: true })

View File

@ -1,101 +0,0 @@
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,
}
}

View File

@ -0,0 +1,140 @@
import path from 'node:path'
import postcss, { AtRule, type Plugin, Root } from 'postcss'
import { normalizePath } from '../../../@tailwindcss-node/src/normalize-path'
import type { JSConfigMigration } from '../migrate-js-config'
import type { Stylesheet } from '../stylesheet'
import { walk, WalkAction } from '../utils/walk'
const ALREADY_INJECTED = new WeakMap<Stylesheet, string[]>()
export function migrateConfig(
sheet: Stylesheet,
{
configFilePath,
jsConfigMigration,
}: { configFilePath: string; jsConfigMigration: JSConfigMigration },
): Plugin {
function injectInto(sheet: Stylesheet) {
let alreadyInjected = ALREADY_INJECTED.get(sheet)
if (alreadyInjected && alreadyInjected.includes(configFilePath)) {
return
} else if (alreadyInjected) {
alreadyInjected.push(configFilePath)
} else {
ALREADY_INJECTED.set(sheet, [configFilePath])
}
let root = sheet.root
// We don't have a sheet with a file path
if (!sheet.file) return
let cssConfig = new AtRule()
cssConfig.raws.tailwind_pretty = true
if (jsConfigMigration === null) {
// Skip if there is already a `@config` directive
{
let hasConfig = false
root.walkAtRules('config', () => {
hasConfig = true
return false
})
if (hasConfig) return
}
cssConfig.append(
new AtRule({
name: 'config',
params: `'${relativeToStylesheet(sheet, configFilePath)}'`,
}),
)
} else {
let css = '\n\n'
for (let source of jsConfigMigration.sources) {
let absolute = path.resolve(source.base, source.pattern)
css += `@source '${relativeToStylesheet(sheet, absolute)}';\n`
}
if (jsConfigMigration.sources.length > 0) {
css = css + '\n'
}
cssConfig.append(postcss.parse(css + jsConfigMigration.css))
}
// 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
})
if (!locationNode) {
root.prepend(cssConfig.nodes)
} else if (locationNode.name === 'import') {
locationNode.after(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, 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-config',
OnceExit: migrate,
}
}
function relativeToStylesheet(sheet: Stylesheet, absolute: string) {
if (!sheet.file) throw new Error('Can not find a path for the stylesheet')
let sheetPath = sheet.file
let relative = path.relative(path.dirname(sheetPath), absolute)
if (relative[0] !== '.') {
relative = `./${relative}`
}
// Ensure relative is a posix style path since we will merge it with the
// glob.
return normalizePath(relative)
}

View File

@ -20,6 +20,7 @@ let config = {
userConfig: {},
newPrefix: null,
configFilePath: path.resolve(__dirname, './tailwind.config.js'),
jsConfigMigration: null,
}
function migrate(input: string, config: any) {

View File

@ -11,6 +11,7 @@ import {
migrate as migrateStylesheet,
split as splitStylesheets,
} from './migrate'
import { migrateJsConfig } from './migrate-js-config'
import { migratePostCSSConfig } from './migrate-postcss'
import { Stylesheet } from './stylesheet'
import { migrate as migrateTemplate } from './template/migrate'
@ -37,6 +38,8 @@ if (flags['--help']) {
}
async function run() {
let base = process.cwd()
eprintln(header())
eprintln()
@ -50,7 +53,7 @@ async function run() {
}
}
let config = await prepareConfig(flags['--config'], { base: process.cwd() })
let config = await prepareConfig(flags['--config'], { base })
{
// Template migrations
@ -81,11 +84,16 @@ async function run() {
success('Template migration complete.')
}
// Migrate JS config
info('Migrating JavaScript configuration files using the provided configuration file.')
let jsConfigMigration = await migrateJsConfig(config.configFilePath, base)
{
// Stylesheet migrations
// Use provided files
let files = flags._.map((file) => path.resolve(process.cwd(), file))
let files = flags._.map((file) => path.resolve(base, file))
// Discover CSS files in case no files were provided
if (files.length === 0) {
@ -125,7 +133,7 @@ async function run() {
// Migrate each file
let migrateResults = await Promise.allSettled(
stylesheets.map((sheet) => migrateStylesheet(sheet, config)),
stylesheets.map((sheet) => migrateStylesheet(sheet, { ...config, jsConfigMigration })),
)
for (let result of migrateResults) {
@ -158,14 +166,19 @@ async function run() {
{
// PostCSS config migration
await migratePostCSSConfig(process.cwd())
await migratePostCSSConfig(base)
}
try {
// Upgrade Tailwind CSS
await pkg('add tailwindcss@next', process.cwd())
await pkg('add tailwindcss@next', base)
} catch {}
// Remove the JS config if it was fully migrated
if (jsConfigMigration !== null) {
await fs.rm(config.configFilePath)
}
// Figure out if we made any changes
if (isRepoDirty()) {
success('Verify the changes and commit them to your repository.')

View File

@ -0,0 +1,185 @@
import fs from 'node:fs/promises'
import { dirname } from 'path'
import type { Config } from 'tailwindcss'
import { fileURLToPath } from 'url'
import { loadModule } from '../../@tailwindcss-node/src/compile'
import {
keyPathToCssProperty,
themeableValues,
} from '../../tailwindcss/src/compat/apply-config-to-theme'
import { deepMerge } from '../../tailwindcss/src/compat/config/deep-merge'
import { mergeThemeExtension } from '../../tailwindcss/src/compat/config/resolve-config'
import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode'
import { info } from './utils/renderer'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
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 }[]
css: string
}
export async function migrateJsConfig(
fullConfigPath: string,
base: string,
): Promise<JSConfigMigration> {
let [unresolvedConfig, source] = await Promise.all([
loadModule(fullConfigPath, __dirname, () => {}).then((result) => result.module) as Config,
fs.readFile(fullConfigPath, 'utf-8'),
])
if (!isSimpleConfig(unresolvedConfig, source)) {
info(
'The configuration file is not a simple object. Please refer to the migration guide for how to migrate it fully to Tailwind CSS v4. For now, we will load the configuration file as-is.',
)
return null
}
let sources: { base: string; pattern: string }[] = []
let cssConfigs: string[] = []
if ('darkMode' in unresolvedConfig) {
cssConfigs.push(migrateDarkMode(unresolvedConfig as any))
}
if ('content' in unresolvedConfig) {
sources = migrateContent(unresolvedConfig as any, base)
}
if ('theme' in unresolvedConfig) {
cssConfigs.push(await migrateTheme(unresolvedConfig as any))
}
return {
sources,
css: cssConfigs.join('\n'),
}
}
async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise<string> {
let { extend: extendTheme, ...overwriteTheme } = unresolvedConfig.theme
let resetNamespaces = new Map<string, boolean>()
// Before we merge the resetting theme values with the `extend` values, we
// capture all namespaces that need to be reset
for (let [key, value] of themeableValues(overwriteTheme)) {
if (typeof value !== 'string' && typeof value !== 'number') {
continue
}
if (!resetNamespaces.has(key[0])) {
resetNamespaces.set(key[0], false)
}
}
let themeValues = deepMerge({}, [overwriteTheme, extendTheme], mergeThemeExtension)
let prevSectionKey = ''
let css = `@theme {`
for (let [key, value] of themeableValues(themeValues)) {
if (typeof value !== 'string' && typeof value !== 'number') {
continue
}
let sectionKey = createSectionKey(key)
if (sectionKey !== prevSectionKey) {
css += `\n`
prevSectionKey = sectionKey
}
if (resetNamespaces.has(key[0]) && resetNamespaces.get(key[0]) === false) {
resetNamespaces.set(key[0], true)
css += ` --${keyPathToCssProperty([key[0]])}-*: initial;\n`
}
css += ` --${keyPathToCssProperty(key)}: ${value};\n`
}
return css + '}\n'
}
function migrateDarkMode(unresolvedConfig: Config & { darkMode: any }): string {
let variant: string = ''
let addVariant = (_name: string, _variant: string) => (variant = _variant)
let config = () => unresolvedConfig.darkMode
darkModePlugin({ config, addVariant })
if (variant === '') {
return ''
}
return `@variant dark (${variant});\n`
}
// Returns a string identifier used to section theme declarations
function createSectionKey(key: string[]): string {
let sectionSegments = []
for (let i = 0; i < key.length - 1; i++) {
let segment = key[i]
// ignore tuples
if (key[i + 1][0] === '-') {
break
}
sectionSegments.push(segment)
}
return sectionSegments.join('-')
}
function migrateContent(
unresolvedConfig: Config & { content: any },
base: string,
): { base: string; pattern: string }[] {
let sources = []
for (let content of unresolvedConfig.content) {
if (typeof content !== 'string') {
throw new Error('Unsupported content value: ' + content)
}
sources.push({ base, pattern: content })
}
return sources
}
// Applies heuristics to determine if we can attempt to migrate the config
function isSimpleConfig(unresolvedConfig: Config, source: string): boolean {
// The file may not contain any functions
if (source.includes('function') || source.includes(' => ')) {
return false
}
// The file may not contain non-serializable values
function isSimpleValue(value: unknown): boolean {
if (typeof value === 'function') return false
if (Array.isArray(value)) return value.every(isSimpleValue)
if (typeof value === 'object' && value !== null) {
return Object.values(value).every(isSimpleValue)
}
return ['string', 'number', 'boolean', 'undefined'].includes(typeof value)
}
if (!isSimpleValue(unresolvedConfig)) {
return false
}
// The file may only contain known-migrateable top-level properties
let knownProperties = [
'darkMode',
'content',
'theme',
'plugins',
'presets',
'prefix', // Prefix is handled in the dedicated prefix migrator
]
if (Object.keys(unresolvedConfig).some((key) => !knownProperties.includes(key))) {
return false
}
if (unresolvedConfig.plugins && unresolvedConfig.plugins.length > 0) {
return false
}
if (unresolvedConfig.presets && unresolvedConfig.presets.length > 0) {
return false
}
return true
}

View File

@ -5,11 +5,12 @@ 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 { migrateConfig } from './codemods/migrate-config'
import { migrateMediaScreen } from './codemods/migrate-media-screen'
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives'
import type { JSConfigMigration } from './migrate-js-config'
import { Stylesheet, type StylesheetConnection, type StylesheetId } from './stylesheet'
import { resolveCssId } from './utils/resolve'
import { walk, WalkAction } from './utils/walk'
@ -19,6 +20,7 @@ export interface MigrateOptions {
designSystem: DesignSystem
userConfig: Config
configFilePath: string
jsConfigMigration: JSConfigMigration
}
export async function migrateContents(
@ -37,7 +39,7 @@ export async function migrateContents(
.use(migrateAtLayerUtilities(stylesheet))
.use(migrateMissingLayers())
.use(migrateTailwindDirectives(options))
.use(migrateAtConfig(stylesheet, options))
.use(migrateConfig(stylesheet, options))
.process(stylesheet.root, { from: stylesheet.file ?? undefined })
}

View File

@ -35,8 +35,7 @@ export async function prepareConfig(
// required so that the base for Tailwind CSS can bet inside the
// @tailwindcss-upgrade package and we can require `tailwindcss` properly.
let fullConfigPath = path.resolve(options.base, configPath)
let fullFilePath = path.resolve(__dirname)
let relative = path.relative(fullFilePath, fullConfigPath)
let relative = path.relative(__dirname, fullConfigPath)
// If the path points to a file in the same directory, `path.relative` will
// remove the leading `./` and we need to add it back in order to still

View File

@ -81,7 +81,7 @@ export function applyConfigToTheme(designSystem: DesignSystem, { theme }: Resolv
return theme
}
function themeableValues(config: ResolvedConfig['theme']): [string[], unknown][] {
export function themeableValues(config: ResolvedConfig['theme']): [string[], unknown][] {
let toAdd: [string[], unknown][] = []
walk(config as any, [], (value, path) => {
@ -110,9 +110,10 @@ function themeableValues(config: ResolvedConfig['theme']): [string[], unknown][]
return toAdd
}
function keyPathToCssProperty(path: string[]) {
export function keyPathToCssProperty(path: string[]) {
if (path[0] === 'colors') path[0] = 'color'
if (path[0] === 'screens') path[0] = 'breakpoint'
if (path[0] === 'borderRadius') path[0] = 'radius'
return (
path

View File

@ -87,7 +87,7 @@ export function resolveConfig(design: DesignSystem, files: ConfigFile[]): Resolv
}
}
function mergeThemeExtension(
export function mergeThemeExtension(
themeValue: ThemeValue | ThemeValue[],
extensionValue: ThemeValue | ThemeValue[],
) {

View File

@ -1,7 +1,7 @@
import type { ResolvedConfig } from './config/types'
import type { PluginAPI } from './plugin-api'
export function darkModePlugin({ addVariant, config }: PluginAPI) {
export function darkModePlugin({ addVariant, config }: Pick<PluginAPI, 'addVariant' | 'config'>) {
let darkMode = config('darkMode', null) as ResolvedConfig['darkMode']
let [mode, selector = '.dark'] = Array.isArray(darkMode) ? darkMode : [darkMode]