Support migrating projects with multiple config files (#14863)

When migrating a project from Tailwind CSS v3 to Tailwind CSS v4, then
we started the migration process in the following order:

1. Migrate the JS/TS config file
2. Migrate the source files (found via the `content` option)
3. Migrate the CSS files

However, if you have a setup where you have multiple CSS root files
(e.g.: `frontend` and `admin` are separated), then that typically means
that you have an `@config` directive in your CSS files. These point to
the Tailwind CSS config file.

This PR changes the migration order to do the following:

1. Build a tree of all the CSS files
2. For each `@config` directive, migrate the JS/TS config file
3. For each JS/TS config file, migrate the source files

If a CSS file does not contain any `@config` directives, then we start
by filling in the `@config` directive with the default Tailwind CSS
config file (if found, or the one passed in). If no default config file
or passed in config file can be found, then we will error out (just like
we do now)

---------

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This commit is contained in:
Robin Malfait 2024-11-04 17:52:11 +01:00 committed by GitHub
parent df6dfb012c
commit 894bf9f5ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 533 additions and 92 deletions

View File

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- _Upgrade (experimental)_: Migrate `grid-cols-[subgrid]` and `grid-rows-[subgrid]` to `grid-cols-subgrid` and `grid-rows-subgrid` ([#14840](https://github.com/tailwindlabs/tailwindcss/pull/14840))
- _Upgrade (experimental)_: Support migrating projects with multiple config files ([#14863](https://github.com/tailwindlabs/tailwindcss/pull/14863))
### Fixed

View File

@ -1,6 +1,51 @@
import { expect } from 'vitest'
import { candidate, css, html, js, json, test } from '../utils'
test(
'error when no CSS file with @tailwind is used',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/upgrade": "workspace:^"
},
"devDependencies": {
"@tailwindcss/cli": "workspace:^"
}
}
`,
'tailwind.config.js': js`
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{html,js}'],
}
`,
'src/index.html': html`
<h1>🤠👋</h1>
<div class="!flex"></div>
`,
'src/fonts.css': css`/* Unrelated CSS file */`,
},
},
async ({ fs, exec }) => {
let output = await exec('npx @tailwindcss/upgrade')
expect(output).toContain('Cannot find any CSS files that reference Tailwind CSS.')
// Files should not be modified
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
"
--- ./src/index.html ---
<h1>🤠👋</h1>
<div class="!flex"></div>
--- ./src/fonts.css ---
/* Unrelated CSS file */
"
`)
},
)
test(
`upgrades a v3 project to v4`,
{
@ -858,6 +903,11 @@ test(
prefix: 'tw__',
}
`,
'src/index.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
'src/index.html': html`
<div class="tw__bg-gradient-to-t"></div>
`,
@ -1304,7 +1354,7 @@ test(
@tailwind base;
@tailwind components;
@tailwind utilities;
@config "../tailwind.config.js";
@config "../tailwind.config.ts";
`,
'src/root.3.css': css`
/* Inject missing @config above first @theme */
@ -1421,7 +1471,7 @@ test(
border-width: 0;
}
}
@config "../tailwind.config.js";
@config "../tailwind.config.ts";
--- ./src/root.3.css ---
/* Inject missing @config above first @theme */

View File

@ -783,6 +783,158 @@ test(
},
)
test(
'multi-root project',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
// Project A
'project-a/tailwind.config.ts': ts`
export default {
content: {
relative: true,
files: ['./src/**/*.html'],
},
theme: {
extend: {
colors: {
primary: 'red',
},
},
},
}
`,
'project-a/src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@config "../tailwind.config.ts";
`,
'project-a/src/index.html': html`<div class="!text-primary"></div>`,
// Project B
'project-b/tailwind.config.ts': ts`
export default {
content: {
relative: true,
files: ['./src/**/*.html'],
},
theme: {
extend: {
colors: {
primary: 'blue',
},
},
},
}
`,
'project-b/src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@config "../tailwind.config.ts";
`,
'project-b/src/index.html': html`<div class="!text-primary"></div>`,
},
},
async ({ exec, fs }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('project-{a,b}/**/*.{css,ts}')).toMatchInlineSnapshot(`
"
--- project-a/src/input.css ---
@import 'tailwindcss';
/*
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);
}
}
/*
Form elements have a 1px border by default 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 \`border-0\` to
any form elements that shouldn't have a border.
*/
@layer base {
input:where(:not([type='button'], [type='reset'], [type='submit'])),
select,
textarea {
border-width: 0;
}
}
@theme {
--color-primary: red;
}
--- project-b/src/input.css ---
@import 'tailwindcss';
/*
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);
}
}
/*
Form elements have a 1px border by default 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 \`border-0\` to
any form elements that shouldn't have a border.
*/
@layer base {
input:where(:not([type='button'], [type='reset'], [type='submit'])),
select,
textarea {
border-width: 0;
}
}
@theme {
--color-primary: blue;
}
"
`)
},
)
describe('border compatibility', () => {
test(
'migrate border compatibility',

View File

@ -1,4 +1,4 @@
import path from 'path'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { relative, wordWrap } from './renderer'
import { normalizeWindowsSeperators } from './test-helpers'

View File

@ -2,9 +2,9 @@ import QuickLRU from '@alloc/quick-lru'
import { compile, env } from '@tailwindcss/node'
import { clearRequireCache } from '@tailwindcss/node/require-cache'
import { Scanner } from '@tailwindcss/oxide'
import fs from 'fs'
import { Features, transform } from 'lightningcss'
import path from 'path'
import fs from 'node:fs'
import path from 'node:path'
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
import fixRelativePathsPlugin from './postcss-fix-relative-paths'

View File

@ -31,24 +31,13 @@ export function migrateConfig(
let cssConfig = new AtRule()
if (jsConfigMigration === null) {
// Skip if there is already a `@config` directive
{
let hasConfig = false
root.walkAtRules('config', () => {
hasConfig = true
return false
})
if (hasConfig) return
}
// Remove the `@config` directive if it exists and we couldn't migrate the
// config file.
if (jsConfigMigration !== null) {
root.walkAtRules('config', (node) => {
node.remove()
})
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)

View File

@ -95,8 +95,6 @@ it('should migrate a stylesheet', async () => {
).toMatchInlineSnapshot(`
"@import 'tailwindcss';
@config './tailwind.config.js';
/*
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
@ -216,8 +214,7 @@ it('should migrate a stylesheet (with imports)', async () => {
textarea {
border-width: 0;
}
}
@config './tailwind.config.js';"
}"
`)
})
@ -242,7 +239,6 @@ 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';
/*
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still

View File

@ -8,6 +8,7 @@ import { formatNodes } from './codemods/format-nodes'
import { help } from './commands/help'
import {
analyze as analyzeStylesheets,
linkConfigs as linkConfigsToStylesheets,
migrate as migrateStylesheet,
split as splitStylesheets,
} from './migrate'
@ -44,6 +45,8 @@ async function run() {
eprintln(header())
eprintln()
let cleanup: (() => void)[] = []
if (!flags['--force']) {
if (isRepoDirty()) {
error('Git directory is not clean. Please stash or commit your changes before migrating.')
@ -54,42 +57,6 @@ async function run() {
}
}
let config = await prepareConfig(flags['--config'], { base })
{
// Template migrations
info('Migrating templates using the provided configuration file.')
let set = new Set<string>()
for (let { pattern, base } of config.globs) {
let files = await globby([pattern], {
absolute: true,
gitignore: true,
cwd: base,
})
for (let file of files) {
set.add(file)
}
}
let files = Array.from(set)
files.sort()
// Migrate each file
await Promise.allSettled(
files.map((file) => migrateTemplate(config.designSystem, config.userConfig, file)),
)
success('Template migration complete.')
}
// Migrate JS config
info('Migrating JavaScript configuration files using the provided configuration file.')
let jsConfigMigration = await migrateJsConfig(config.designSystem, config.configFilePath, base)
{
// Stylesheet migrations
@ -132,9 +99,91 @@ async function run() {
error(`${e}`)
}
// Migrate each file
// Ensure stylesheets are linked to configs
try {
await linkConfigsToStylesheets(stylesheets, {
configPath: flags['--config'],
base,
})
} catch (e: unknown) {
error(`${e}`)
}
// Migrate js config files, linked to stylesheets
info('Migrating JavaScript configuration files using the provided configuration file.')
let configBySheet = new Map<Stylesheet, Awaited<ReturnType<typeof prepareConfig>>>()
let jsConfigMigrationBySheet = new Map<
Stylesheet,
Awaited<ReturnType<typeof migrateJsConfig>>
>()
for (let sheet of stylesheets) {
if (!sheet.isTailwindRoot) continue
let config = await prepareConfig(sheet.linkedConfigPath, { base })
configBySheet.set(sheet, config)
let jsConfigMigration = await migrateJsConfig(
config.designSystem,
config.configFilePath,
base,
)
jsConfigMigrationBySheet.set(sheet, jsConfigMigration)
if (jsConfigMigration !== null) {
// Remove the JS config if it was fully migrated
cleanup.push(() => fs.rm(config.configFilePath))
}
}
// Migrate source files, linked to config files
{
// Template migrations
info('Migrating templates using the provided configuration file.')
for (let config of configBySheet.values()) {
let set = new Set<string>()
for (let { pattern, base } of config.globs) {
let files = await globby([pattern], {
absolute: true,
gitignore: true,
cwd: base,
})
for (let file of files) {
set.add(file)
}
}
let files = Array.from(set)
files.sort()
// Migrate each file
await Promise.allSettled(
files.map((file) => migrateTemplate(config.designSystem, config.userConfig, file)),
)
}
success('Template migration complete.')
}
// Migrate each CSS file
let migrateResults = await Promise.allSettled(
stylesheets.map((sheet) => migrateStylesheet(sheet, { ...config, jsConfigMigration })),
stylesheets.map((sheet) => {
let config = configBySheet.get(sheet)!
let jsConfigMigration = jsConfigMigrationBySheet.get(sheet)!
if (!config) {
for (let parent of sheet.ancestors()) {
if (parent.isTailwindRoot) {
config ??= configBySheet.get(parent)!
jsConfigMigration ??= jsConfigMigrationBySheet.get(parent)!
break
}
}
}
return migrateStylesheet(sheet, { ...config, jsConfigMigration })
}),
)
for (let result of migrateResults) {
@ -197,16 +246,14 @@ async function run() {
await migratePrettierPlugin(base)
}
// Run all cleanup functions because we completed the migration
await Promise.allSettled(cleanup.map((fn) => fn()))
try {
// Upgrade Tailwind CSS
await pkg(base).add(['tailwindcss@next'])
} 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

@ -1,9 +1,9 @@
import { Scanner } from '@tailwindcss/oxide'
import fs from 'node:fs/promises'
import { dirname } from 'path'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
import { fileURLToPath } from 'url'
import { loadModule } from '../../@tailwindcss-node/src/compile'
import { toCss, type AstNode } from '../../tailwindcss/src/ast'
import {
@ -56,7 +56,7 @@ export async function migrateJsConfig(
}
if ('content' in unresolvedConfig) {
sources = await migrateContent(unresolvedConfig as any, base)
sources = await migrateContent(unresolvedConfig as any, fullConfigPath, base)
}
if ('theme' in unresolvedConfig) {
@ -173,13 +173,31 @@ function createSectionKey(key: string[]): string {
}
async function migrateContent(
unresolvedConfig: Config & { content: any },
unresolvedConfig: Config,
configPath: string,
base: string,
): Promise<{ base: string; pattern: string }[]> {
let autoContentFiles = autodetectedSourceFiles(base)
let sources = []
for (let content of unresolvedConfig.content) {
let contentIsRelative = (() => {
if (!unresolvedConfig.content) return false
if (Array.isArray(unresolvedConfig.content)) return false
if (unresolvedConfig.content.relative) return true
if (unresolvedConfig.future === 'all') return false
return unresolvedConfig.future?.relativeContentPathsByDefault ?? false
})()
let contentFiles = Array.isArray(unresolvedConfig.content)
? unresolvedConfig.content
: (unresolvedConfig.content?.files ?? []).map((content) => {
if (typeof content === 'string' && contentIsRelative) {
return resolve(dirname(configPath), content)
}
return content
})
for (let content of contentFiles) {
if (typeof content !== 'string') {
throw new Error('Unsupported content value: ' + content)
}

View File

@ -1,3 +1,4 @@
import { normalizePath } from '@tailwindcss/node'
import path from 'node:path'
import postcss from 'postcss'
import type { Config } from 'tailwindcss'
@ -16,6 +17,8 @@ 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 { detectConfigPath } from './template/prepare-config'
import { error } from './utils/renderer'
import { resolveCssId } from './utils/resolve'
import { walk, WalkAction } from './utils/walk'
@ -194,7 +197,99 @@ export async function analyze(stylesheets: Stylesheet[]) {
}
}
if (lines.length === 0) return
if (lines.length === 0) {
let tailwindRootLeafs = new Set<Stylesheet>()
for (let sheet of stylesheets) {
// If the current file already contains `@config`, then we can assume it's
// a Tailwind CSS root file.
sheet.root.walkAtRules('config', () => {
sheet.isTailwindRoot = true
return false
})
if (sheet.isTailwindRoot) continue
// If an `@tailwind` at-rule, or `@import "tailwindcss"` is present,
// then we can assume it's a file where Tailwind CSS might be configured.
//
// However, if 2 or more stylesheets exist with these rules that share a
// common parent, then we want to mark the common parent as the root
// stylesheet instead.
sheet.root.walkAtRules((node) => {
if (
node.name === 'tailwind' ||
(node.name === 'import' && node.params.match(/^["']tailwindcss["']/)) ||
(node.name === 'import' && node.params.match(/^["']tailwindcss\/.*?["']$/))
) {
sheet.isTailwindRoot = true
tailwindRootLeafs.add(sheet)
}
})
}
// Only a single Tailwind CSS root file exists, no need to do anything else.
if (tailwindRootLeafs.size <= 1) {
return
}
// Mark the common parent as the root file
{
// Group each sheet from tailwindRootLeafs by their common parent
let commonParents = new DefaultMap<Stylesheet, Set<Stylesheet>>(() => new Set<Stylesheet>())
// Seed common parents with leafs
for (let sheet of tailwindRootLeafs) {
commonParents.get(sheet).add(sheet)
}
// If any 2 common parents come from the same tree, then all children of
// parent A and parent B will be moved to the parent of parent A and
// parent B. Parent A and parent B will be removed.
let repeat = true
while (repeat) {
repeat = false
outer: for (let [sheetA, childrenA] of commonParents) {
for (let [sheetB, childrenB] of commonParents) {
if (sheetA === sheetB) continue
for (let parentA of sheetA.ancestors()) {
for (let parentB of sheetB.ancestors()) {
if (parentA !== parentB) continue
commonParents.delete(sheetA)
commonParents.delete(sheetB)
for (let child of childrenA) {
commonParents.get(parentA).add(child)
}
for (let child of childrenB) {
commonParents.get(parentA).add(child)
}
repeat = true
break outer
}
}
}
}
}
// Mark the common parent as the Tailwind CSS root file, and remove the
// flag from each leaf.
for (let [parent, children] of commonParents) {
parent.isTailwindRoot = true
for (let child of children) {
if (parent === child) continue
child.isTailwindRoot = false
}
}
return
}
}
let error = `You have one or more stylesheets that are imported into a utility layer and non-utility layer.\n`
error += `We cannot convert stylesheets under these conditions. Please look at the following stylesheets:\n`
@ -202,6 +297,82 @@ export async function analyze(stylesheets: Stylesheet[]) {
throw new Error(error + lines.join('\n'))
}
export async function linkConfigs(
stylesheets: Stylesheet[],
{ configPath, base }: { configPath: string | null; base: string },
) {
let rootStylesheets = stylesheets.filter((sheet) => sheet.isTailwindRoot)
if (rootStylesheets.length === 0) {
throw new Error(
'Cannot find any CSS files that reference Tailwind CSS.\nBefore your project can be upgraded you need to create a CSS file that imports Tailwind CSS or uses `@tailwind`.',
)
}
let withoutAtConfig = rootStylesheets.filter((sheet) => {
let hasConfig = false
sheet.root.walkAtRules('config', (node) => {
let configPath = path.resolve(path.dirname(sheet.file!), node.params.slice(1, -1))
sheet.linkedConfigPath = configPath
hasConfig = true
return false
})
return !hasConfig
})
// All stylesheets have a `@config` directives
if (withoutAtConfig.length === 0) return
try {
if (configPath === null) {
configPath = await detectConfigPath(base)
} else if (!path.isAbsolute(configPath)) {
configPath = path.resolve(base, configPath)
}
// Link the `@config` directive to the root stylesheets
for (let sheet of withoutAtConfig) {
if (!sheet.file) continue
// Track the config file path on the stylesheet itself for easy access
// without traversing the CSS ast and finding the corresponding
// `@config` later.
sheet.linkedConfigPath = configPath
// Create a relative path from the current file to the config file.
let relative = path.relative(path.dirname(sheet.file), configPath)
// 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
// consider the path relative
if (!relative.startsWith('.')) {
relative = './' + relative
}
relative = normalizePath(relative)
// Add the `@config` directive to the root stylesheet.
{
let target = sheet.root as postcss.Root | postcss.AtRule
let atConfig = postcss.atRule({ name: 'config', params: `'${relative}'` })
sheet.root.walkAtRules((node) => {
if (node.name === 'tailwind' || node.name === 'import') {
target = node
}
})
if (target.type === 'root') {
sheet.root.prepend(atConfig)
} else if (target.type === 'atrule') {
target.after(atConfig)
}
}
}
} catch (e: any) {
error('Could not load the configuration file: ' + e.message)
process.exit(1)
}
}
export async function split(stylesheets: Stylesheet[]) {
let stylesheetsById = new Map<StylesheetId, Stylesheet>()
let stylesheetsByFile = new Map<string, Stylesheet>()

View File

@ -25,6 +25,17 @@ export class Stylesheet {
*/
root: postcss.Root
/**
* Whether or not this stylesheet is a Tailwind CSS root stylesheet.
*/
isTailwindRoot = false
/**
* The Tailwind config path that is linked to this stylesheet. Essentially the
* contents of `@config`.
*/
linkedConfigPath: string | null = null
/**
* The path to the file that this stylesheet was loaded from.
*

View File

@ -1,9 +1,8 @@
import { __unstable__loadDesignSystem, compile } from '@tailwindcss/node'
import fs from 'node:fs/promises'
import path from 'node:path'
import { dirname } from 'path'
import path, { dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Config } from 'tailwindcss'
import { fileURLToPath } from 'url'
import { loadModule } from '../../../@tailwindcss-node/src/compile'
import { resolveConfig } from '../../../tailwindcss/src/compat/config/resolve-config'
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
@ -16,7 +15,7 @@ const __dirname = dirname(__filename)
const css = String.raw
export async function prepareConfig(
configPath: string | null,
configFilePath: string | null,
options: { base: string },
): Promise<{
designSystem: DesignSystem
@ -27,15 +26,16 @@ export async function prepareConfig(
newPrefix: string | null
}> {
try {
if (configPath === null) {
configPath = await detectConfigPath(options.base)
if (configFilePath === null) {
configFilePath = await detectConfigPath(options.base)
} else if (!path.isAbsolute(configFilePath)) {
configFilePath = path.resolve(options.base, configFilePath)
}
// We create a relative path from the current file to the config file. This is
// 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 relative = path.relative(__dirname, fullConfigPath)
let relative = path.relative(__dirname, configFilePath)
// 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
@ -44,7 +44,7 @@ export async function prepareConfig(
relative = './' + relative
}
let userConfig = await createResolvedUserConfig(fullConfigPath)
let userConfig = await createResolvedUserConfig(configFilePath)
let newPrefix = userConfig.prefix ? migratePrefix(userConfig.prefix) : null
let input = css`
@ -62,7 +62,7 @@ export async function prepareConfig(
globs: compiler.globs,
userConfig,
newPrefix,
configFilePath: fullConfigPath,
configFilePath,
}
} catch (e: any) {
error('Could not load the configuration file: ' + e.message)
@ -94,7 +94,7 @@ const DEFAULT_CONFIG_FILES = [
'./tailwind.config.cts',
'./tailwind.config.mts',
]
async function detectConfigPath(base: string) {
export async function detectConfigPath(base: string) {
for (let file of DEFAULT_CONFIG_FILES) {
let fullPath = path.resolve(base, file)
try {

View File

@ -40,7 +40,13 @@ export function relative(
/**
* Wrap `text` into multiple lines based on the `width`.
*/
export function wordWrap(text: string, width: number) {
export function wordWrap(text: string, width: number): string[] {
// Handle text with newlines by maintaining the newlines, then splitting
// each line separately.
if (text.includes('\n')) {
return text.split('\n').flatMap((line) => wordWrap(line, width))
}
let words = text.split(' ')
let lines = []

View File

@ -3,7 +3,7 @@ import { clearRequireCache } from '@tailwindcss/node/require-cache'
import { Scanner } from '@tailwindcss/oxide'
import { Features, transform } from 'lightningcss'
import fs from 'node:fs/promises'
import path from 'path'
import path from 'node:path'
import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite'
const SPECIAL_QUERY_RE = /[?&](raw|url)\b/

View File

@ -25,7 +25,7 @@ export interface ResolvedConfig {
type ContentFile = string | { raw: string; extension?: string }
export interface UserConfig {
content?: ContentFile[] | { files: ContentFile[] }
content?: ContentFile[] | { relative?: boolean; files: ContentFile[] }
}
type ResolvedContent = { base: string; pattern: string } | { raw: string; extension?: string }

View File

@ -1,7 +1,7 @@
import { expect, test, type Page } from '@playwright/test'
import { Scanner } from '@tailwindcss/oxide'
import fs from 'fs'
import path from 'path'
import fs from 'node:fs'
import path from 'node:path'
import { compile } from '../src'
import { optimizeCss } from '../src/test-utils/run'