mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
This PR fixes an issue where globs in you `content` configuration escape
the current "root" of the project.
This can happen if you have a folder, and you need to look up in the
tree (e.g.: when looking at another package in a monorepo, or in case of
a Laravel project where you want to look at mail templates).
This applies a similar strategy we already implement on the Rust side.
1. Expand braces in the globs
2. Move static parts of the `pattern` to the `base` of the glob entry
object
---
Given a project setup like this:
```
.
├── admin
│ ├── my-tailwind.config.ts
│ └── src
│ ├── abc.jpg
│ ├── index.html
│ ├── index.js
│ └── styles
│ └── input.css
├── dashboard
│ ├── src
│ │ ├── index.html
│ │ ├── index.js
│ │ ├── input.css
│ │ └── pickaday.css
│ └── tailwind.config.ts
├── package-lock.json
├── package.json
├── postcss.config.js
└── unrelated
└── index.html
7 directories, 14 files
```
If you then have this config:
```ts
// admin/my-tailwind.config.ts
export default {
content: {
relative: true,
files: ['./src/**/*.html', '../dashboard/src/**/*.html'],
// ^^ this is the important part, which escapes
// the current root of the project.
},
theme: {
extend: {
colors: {
primary: 'red',
},
},
},
}
```
Then before this change, running the command looks like this:
<img width="1760" alt="image"
src="https://github.com/user-attachments/assets/60e2dfc7-3751-4432-80e3-8b4b8f1083d4">
After this change, running the command looks like this:
<img width="1452" alt="image"
src="https://github.com/user-attachments/assets/5c47182c-119c-4732-a253-2dace7086049">
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
273 lines
7.8 KiB
JavaScript
273 lines
7.8 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { globby } from 'globby'
|
|
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import postcss from 'postcss'
|
|
import { formatNodes } from './codemods/format-nodes'
|
|
import { sortBuckets } from './codemods/sort-buckets'
|
|
import { help } from './commands/help'
|
|
import {
|
|
analyze as analyzeStylesheets,
|
|
linkConfigs as linkConfigsToStylesheets,
|
|
migrate as migrateStylesheet,
|
|
split as splitStylesheets,
|
|
} from './migrate'
|
|
import { migrateJsConfig } from './migrate-js-config'
|
|
import { migratePostCSSConfig } from './migrate-postcss'
|
|
import { migratePrettierPlugin } from './migrate-prettier'
|
|
import { Stylesheet } from './stylesheet'
|
|
import { migrate as migrateTemplate } from './template/migrate'
|
|
import { prepareConfig } from './template/prepare-config'
|
|
import { args, type Arg } from './utils/args'
|
|
import { isRepoDirty } from './utils/git'
|
|
import { hoistStaticGlobParts } from './utils/hoist-static-glob-parts'
|
|
import { pkg } from './utils/packages'
|
|
import { eprintln, error, header, highlight, info, success } from './utils/renderer'
|
|
|
|
const options = {
|
|
'--config': { type: 'string', description: 'Path to the configuration file', alias: '-c' },
|
|
'--help': { type: 'boolean', description: 'Display usage information', alias: '-h' },
|
|
'--force': { type: 'boolean', description: 'Force the migration', alias: '-f' },
|
|
'--version': { type: 'boolean', description: 'Display the version number', alias: '-v' },
|
|
} satisfies Arg
|
|
const flags = args(options)
|
|
|
|
if (flags['--help']) {
|
|
help({
|
|
usage: ['npx @tailwindcss/upgrade'],
|
|
options,
|
|
})
|
|
process.exit(0)
|
|
}
|
|
|
|
async function run() {
|
|
let base = process.cwd()
|
|
|
|
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.')
|
|
info(
|
|
`You may use the ${highlight('--force')} flag to silence this warning and perform the migration.`,
|
|
)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
{
|
|
// Stylesheet migrations
|
|
|
|
// Use provided files
|
|
let files = flags._.map((file) => path.resolve(base, file))
|
|
|
|
// Discover CSS files in case no files were provided
|
|
if (files.length === 0) {
|
|
info(
|
|
'No input stylesheets provided. Searching for CSS files in the current directory and its subdirectories…',
|
|
)
|
|
|
|
files = await globby(['**/*.css'], {
|
|
absolute: true,
|
|
gitignore: true,
|
|
})
|
|
}
|
|
|
|
// Ensure we are only dealing with CSS files
|
|
files = files.filter((file) => file.endsWith('.css'))
|
|
|
|
// Analyze the stylesheets
|
|
let loadResults = await Promise.allSettled(files.map((filepath) => Stylesheet.load(filepath)))
|
|
|
|
// Load and parse all stylesheets
|
|
for (let result of loadResults) {
|
|
if (result.status === 'rejected') {
|
|
error(`${result.reason}`)
|
|
}
|
|
}
|
|
|
|
let stylesheets = loadResults
|
|
.filter((result) => result.status === 'fulfilled')
|
|
.map((result) => result.value)
|
|
|
|
// Analyze the stylesheets
|
|
try {
|
|
await analyzeStylesheets(stylesheets)
|
|
} catch (e: unknown) {
|
|
error(`${e}`)
|
|
}
|
|
|
|
// 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 globEntry of config.globs.flatMap((entry) => hoistStaticGlobParts(entry))) {
|
|
let files = await globby([globEntry.pattern], {
|
|
absolute: true,
|
|
gitignore: true,
|
|
cwd: globEntry.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) => {
|
|
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) {
|
|
if (result.status === 'rejected') {
|
|
error(`${result.reason}`)
|
|
}
|
|
}
|
|
|
|
// Split up stylesheets (as needed)
|
|
try {
|
|
await splitStylesheets(stylesheets)
|
|
} catch (e: unknown) {
|
|
error(`${e}`)
|
|
}
|
|
|
|
// Cleanup `@import "…" layer(utilities)`
|
|
for (let sheet of stylesheets) {
|
|
for (let importRule of sheet.importRules) {
|
|
if (!importRule.raws.tailwind_injected_layer) continue
|
|
let importedSheet = stylesheets.find(
|
|
(sheet) => sheet.id === importRule.raws.tailwind_destination_sheet_id,
|
|
)
|
|
if (!importedSheet) continue
|
|
|
|
// Only remove the `layer(…)` next to the import if any of the children
|
|
// contain `@utility`. Otherwise `@utility` will not be top-level.
|
|
if (
|
|
!importedSheet.containsRule((node) => node.type === 'atrule' && node.name === 'utility')
|
|
) {
|
|
continue
|
|
}
|
|
|
|
// Make sure to remove the `layer(…)` from the `@import` at-rule
|
|
importRule.params = importRule.params.replace(/ layer\([^)]+\)/, '').trim()
|
|
}
|
|
}
|
|
|
|
// Format nodes
|
|
for (let sheet of stylesheets) {
|
|
await postcss([sortBuckets(), formatNodes()]).process(sheet.root!, { from: sheet.file! })
|
|
}
|
|
|
|
// Write all files to disk
|
|
for (let sheet of stylesheets) {
|
|
if (!sheet.file) continue
|
|
|
|
await fs.writeFile(sheet.file, sheet.root.toString())
|
|
}
|
|
|
|
success('Stylesheet migration complete.')
|
|
}
|
|
|
|
{
|
|
// PostCSS config migration
|
|
await migratePostCSSConfig(base)
|
|
}
|
|
|
|
{
|
|
// Migrate the prettier plugin to the latest version
|
|
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 {}
|
|
|
|
// Figure out if we made any changes
|
|
if (isRepoDirty()) {
|
|
success('Verify the changes and commit them to your repository.')
|
|
} else {
|
|
success('No changes were made to your repository.')
|
|
}
|
|
}
|
|
|
|
run()
|
|
.then(() => process.exit(0))
|
|
.catch((err) => {
|
|
console.error(err)
|
|
process.exit(1)
|
|
})
|