Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Warn when broad glob patterns are used in the content configuration #14140

Merged
merged 12 commits into from
Aug 7, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Fix minification when using nested CSS ([#14105](https://github.com/tailwindlabs/tailwindcss/pull/14105))
- Warn when broad glob patterns are used in the content configuration ([#14140](https://github.com/tailwindlabs/tailwindcss/pull/14140))

## [3.4.7] - 2024-07-25
Expand Down
12 changes: 8 additions & 4 deletions integrations/content-resolution/tests/content.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,11 @@ it('warns when globs are too broad and match node_modules', async () => {
// warning.
expect(stripVTControlCharacters(result.stderr)).toMatchInlineSnapshot(`
"
warn - You are using a glob pattern that includes \`node_modules\` without explicitly specifying \`node_modules\` in the glob.
warn - Your \`content\` configuration uses a glob pattern that includes \`node_modules\` without explicitly specifying \`node_modules\` in the glob.
warn - This can lead to performance issues and is not recommended.
warn - Please consider using a more specific pattern.
warn - Glob: \`./**/*.html\`
warn - File: \`./node_modules/bad.html\`
warn - Please consider using a more specific pattern or use \`node_modules\` explicitly.
warn - https://tailwindcss.com/docs/content-configuration#pattern-recommendations
"
`)
Expand Down Expand Up @@ -326,9 +328,11 @@ it('should not warn when globs are too broad if other glob match node_modules ex
// so we should see a warning.
expect(stripVTControlCharacters(result.stderr)).toMatchInlineSnapshot(`
"
warn - You are using a glob pattern that includes \`node_modules\` without explicitly specifying \`node_modules\` in the glob.
warn - Your \`content\` configuration uses a glob pattern that includes \`node_modules\` without explicitly specifying \`node_modules\` in the glob.
warn - This can lead to performance issues and is not recommended.
warn - Please consider using a more specific pattern.
warn - Glob: \`./**/*.html\`
warn - File: \`./node_modules/very-very-bad.html\`
warn - Please consider using a more specific pattern or use \`node_modules\` explicitly.
warn - https://tailwindcss.com/docs/content-configuration#pattern-recommendations
"
`)
Expand Down
45 changes: 4 additions & 41 deletions src/cli/build/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import path from 'path'
import fs from 'fs'
import postcssrc from 'postcss-load-config'
import { lilconfig } from 'lilconfig'
import micromatch from 'micromatch'
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API

Expand All @@ -13,25 +12,14 @@ import { loadAutoprefixer, loadCssNano, loadPostcss, loadPostcssImport } from '.
import { formatNodes, drainStdin, outputFile } from './utils'
import { env } from '../../lib/sharedState'
import resolveConfig from '../../../resolveConfig.js'
import { parseCandidateFiles } from '../../lib/content.js'
import { createBroadPatternCheck, parseCandidateFiles } from '../../lib/content.js'
import { createWatcher } from './watching.js'
import fastGlob from 'fast-glob'
import { findAtConfigPath } from '../../lib/findAtConfigPath.js'
import log from '../../util/log'
import { loadConfig } from '../../lib/load-config'
import getModuleDependencies from '../../lib/getModuleDependencies'

const LARGE_DIRECTORIES = [
'node_modules', // Node
'vendor', // PHP
]

// Ensures that `node_modules` has to match as-is, otherwise `mynode_modules`
// would match as well, but that is not a known large directory.
const LARGE_DIRECTORIES_REGEX = new RegExp(
`(${LARGE_DIRECTORIES.map((dir) => String.raw`\b${dir}\b`).join('|')})`
)

/**
*
* @param {string} [customPostCssPath ]
Expand Down Expand Up @@ -196,36 +184,11 @@ let state = {
// TODO: When we make the postcss plugin async-capable this can become async
let files = fastGlob.sync(this.contentPatterns.all)

// Detect whether a glob pattern might be too broad. This means that it:
// - Includes `**`
// - Does not include any of the known large directories (e.g.: node_modules)
let maybeBroadPattern = this.contentPatterns.all.some(
(path) => path.includes('**') && !LARGE_DIRECTORIES_REGEX.test(path)
)

// All globs that explicitly contain any of the known large directories (e.g.:
// node_modules)
let explicitGlobs = this.contentPatterns.all.filter((path) =>
LARGE_DIRECTORIES_REGEX.test(path)
)
let checkBroadPattern = createBroadPatternCheck(this.contentPatterns.all)
RobinMalfait marked this conversation as resolved.
Show resolved Hide resolved

for (let file of files) {
if (
maybeBroadPattern &&
// When a broad pattern is used, we have to double check that the file was
// not explicitly included in the globs.
!micromatch.isMatch(file, explicitGlobs)
) {
let largeDirectory = LARGE_DIRECTORIES.find((directory) => file.includes(directory))
if (largeDirectory) {
log.warn('broad-content-glob-pattern', [
`You are using a glob pattern that includes \`${largeDirectory}\` without explicitly specifying \`${largeDirectory}\` in the glob.`,
'This can lead to performance issues and is not recommended.',
'Please consider using a more specific pattern.',
'https://tailwindcss.com/docs/content-configuration#pattern-recommendations',
])
}
}
checkBroadPattern(file)

content.push({
content: fs.readFileSync(path.resolve(file), 'utf8'),
extension: path.extname(file).slice(1),
Expand Down
89 changes: 64 additions & 25 deletions src/lib/content.js
RobinMalfait marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -195,46 +195,85 @@ const LARGE_DIRECTORIES_REGEX = new RegExp(
)

/**
*
* @param {ContentPath[]} candidateFiles
* @param {Map<string, number>} fileModifiedMap
* @returns {[Set<string>, Map<string, number>]}
* @param {string[]} paths
*/
function resolveChangedFiles(candidateFiles, fileModifiedMap) {
let paths = candidateFiles.map((contentPath) => contentPath.pattern)
let mTimesToCommit = new Map()

export function createBroadPatternCheck(paths) {
// Detect whether a glob pattern might be too broad. This means that it:
// - Includes `**`
// - Does not include any of the known large directories (e.g.: node_modules)
let maybeBroadPattern = paths.some(
(path) => path.includes('**') && !LARGE_DIRECTORIES_REGEX.test(path)
)

// Didn't detect any potentially broad patterns, so we can skip further
// checks.
if (!maybeBroadPattern) {
return () => {}
}

// All globs that explicitly contain any of the known large directories (e.g.:
// node_modules)
// node_modules).
let explicitGlobs = paths.filter((path) => LARGE_DIRECTORIES_REGEX.test(path))

// Keep track of whether we already warned about the broad pattern issue or
// not. The `log.warn` function already does something similar where we only
// output the log once. However, with this we can also skip the other checks
// when we already warned about the broad pattern.
let warned = false
RobinMalfait marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param {string} file
*/
return (file) => {
if (warned) return // Already warned about the broad pattern
if (micromatch.isMatch(file, explicitGlobs)) return // Explicitly included, so we can skip further checks

// When a broad pattern is used, we have to double check that the file was
// not explicitly included in the globs.
let matchingGlob = paths.find((path) => micromatch.isMatch(file, path))
if (!matchingGlob) return // This should never happen

// Create relative paths to make the output a bit more readable.
let relativeMatchingGlob = path.relative(process.cwd(), matchingGlob)
if (relativeMatchingGlob[0] !== '.') relativeMatchingGlob = `./${relativeMatchingGlob}`

let relativeFile = path.relative(process.cwd(), file)
if (relativeFile[0] !== '.') relativeFile = `./${relativeFile}`

let largeDirectory = LARGE_DIRECTORIES.find((directory) => file.includes(directory))
if (largeDirectory) {
warned = true

log.warn('broad-content-glob-pattern', [
`Your \`content\` configuration uses a glob pattern that includes \`${largeDirectory}\` without explicitly specifying \`${largeDirectory}\` in the glob.`,
'This can lead to performance issues and is not recommended.',
`Glob: \`${relativeMatchingGlob}\``,
`File: \`${relativeFile}\``,
`Please consider using a more specific pattern or use \`${largeDirectory}\` explicitly.`,
'https://tailwindcss.com/docs/content-configuration#pattern-recommendations',
RobinMalfait marked this conversation as resolved.
Show resolved Hide resolved
])
}
}
}

/**
*
* @param {ContentPath[]} candidateFiles
* @param {Map<string, number>} fileModifiedMap
* @returns {[Set<string>, Map<string, number>]}
*/
function resolveChangedFiles(candidateFiles, fileModifiedMap) {
let paths = candidateFiles.map((contentPath) => contentPath.pattern)
let mTimesToCommit = new Map()

let checkBroadPattern = createBroadPatternCheck(paths)

let changedFiles = new Set()
env.DEBUG && console.time('Finding changed files')
let files = fastGlob.sync(paths, { absolute: true })
for (let file of files) {
if (
maybeBroadPattern &&
// When a broad pattern is used, we have to double check that the file was
// not explicitly included in the globs.
!micromatch.isMatch(file, explicitGlobs)
) {
let largeDirectory = LARGE_DIRECTORIES.find((directory) => file.includes(directory))
if (largeDirectory) {
log.warn('broad-content-glob-pattern', [
`You are using a glob pattern that includes \`${largeDirectory}\` without explicitly specifying \`${largeDirectory}\` in the glob.`,
'This can lead to performance issues and is not recommended.',
'Please consider using a more specific pattern.',
'https://tailwindcss.com/docs/content-configuration#pattern-recommendations',
])
}
}
checkBroadPattern(file)

let prevModified = fileModifiedMap.get(file) || -Infinity
let modified = fs.statSync(file).mtimeMs

Expand Down
Loading