diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d9b5f637148..de378c91af76 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700))
+### Changed
+
+- _Upgrade (experimental)_: Don't create `@source` rules for `content` paths that are already covered by automatic source detection ([#14714](https://github.com/tailwindlabs/tailwindcss/pull/14714))
+
## [4.0.0-alpha.28] - 2024-10-17
### Added
diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts
index 97409d53d4e5..948c0a9e9fd6 100644
--- a/integrations/upgrade/index.test.ts
+++ b/integrations/upgrade/index.test.ts
@@ -40,8 +40,6 @@ test(
--- ./src/input.css ---
@import 'tailwindcss';
-
- @source './**/*.{html,js}';
"
`)
@@ -100,8 +98,6 @@ test(
--- ./src/input.css ---
@import 'tailwindcss' prefix(tw);
- @source './**/*.{html,js}';
-
.btn {
@apply tw:rounded-md! tw:px-2 tw:py-1 tw:bg-blue-500 tw:text-white;
}
diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts
index aa4088d00832..ef7a188e133a 100644
--- a/integrations/upgrade/js-config.test.ts
+++ b/integrations/upgrade/js-config.test.ts
@@ -1,5 +1,5 @@
import { expect } from 'vitest'
-import { css, json, test, ts } from '../utils'
+import { css, html, json, test, ts } from '../utils'
test(
`upgrade JS config files with flat theme values, darkMode, and content fields`,
@@ -18,7 +18,7 @@ test(
module.exports = {
darkMode: 'selector',
- content: ['./src/**/*.{html,js}', './my-app/**/*.{html,js}'],
+ content: ['./src/**/*.{html,js}', './node_modules/my-external-lib/**/*.{html}'],
theme: {
boxShadow: {
sm: '0 2px 6px rgb(15 23 42 / 0.08)',
@@ -72,6 +72,11 @@ test(
@tailwind components;
@tailwind utilities;
`,
+ 'node_modules/my-external-lib/src/template.html': html`
+
+ Hello world!
+
+ `,
},
},
async ({ exec, fs }) => {
@@ -82,8 +87,7 @@ test(
--- src/input.css ---
@import 'tailwindcss';
- @source './**/*.{html,js}';
- @source '../my-app/**/*.{html,js}';
+ @source '../node_modules/my-external-lib/**/*.{html}';
@variant dark (&:where(.dark, .dark *));
diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts
index 19e0305560bb..3c7818902953 100644
--- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts
+++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts
@@ -1,6 +1,7 @@
+import { Scanner } from '@tailwindcss/oxide'
import fs from 'node:fs/promises'
import { dirname } from 'path'
-import type { Config } from 'tailwindcss'
+import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
import { fileURLToPath } from 'url'
import { loadModule } from '../../@tailwindcss-node/src/compile'
@@ -54,7 +55,7 @@ export async function migrateJsConfig(
}
if ('content' in unresolvedConfig) {
- sources = migrateContent(unresolvedConfig as any, base)
+ sources = await migrateContent(unresolvedConfig as any, base)
}
if ('theme' in unresolvedConfig) {
@@ -158,16 +159,31 @@ function createSectionKey(key: string[]): string {
return sectionSegments.join('-')
}
-function migrateContent(
+async function migrateContent(
unresolvedConfig: Config & { content: any },
base: string,
-): { base: string; pattern: string }[] {
+): Promise<{ base: string; pattern: string }[]> {
+ let autoContentFiles = autodetectedSourceFiles(base)
+
let sources = []
for (let content of unresolvedConfig.content) {
if (typeof content !== 'string') {
throw new Error('Unsupported content value: ' + content)
}
- sources.push({ base, pattern: content })
+
+ let sourceFiles = patternSourceFiles({ base, pattern: content })
+
+ let autoContentContainsAllSourceFiles = true
+ for (let sourceFile of sourceFiles) {
+ if (!autoContentFiles.includes(sourceFile)) {
+ autoContentContainsAllSourceFiles = false
+ break
+ }
+ }
+
+ if (!autoContentContainsAllSourceFiles) {
+ sources.push({ base, pattern: content })
+ }
}
return sources
}
@@ -253,3 +269,15 @@ function keyframesToCss(keyframes: Record): string {
let ast: AstNode[] = keyframesToRules({ theme: { keyframes } })
return toCss(ast).trim() + '\n'
}
+
+function autodetectedSourceFiles(base: string) {
+ let scanner = new Scanner({ detectSources: { base } })
+ scanner.scan()
+ return scanner.files
+}
+
+function patternSourceFiles(source: { base: string; pattern: string }): string[] {
+ let scanner = new Scanner({ sources: [source] })
+ scanner.scan()
+ return scanner.files
+}