Skip to content

Commit 858696a

Browse files
Warn when broad glob patterns are used in the content configuration (#14140)
When you use a glob pattern in your `content` configuration that is too broad, it could be that you are accidentally including files that you didn't intend to include. E.g.: all of `node_modules` This has been documented in the [Tailwind CSS documentation](https://tailwindcss.com/docs/content-configuration#pattern-recommendations), but it's still something that a lot of people run into. This PR will try to detect those patterns and show a big warning to let you know if you may have done something wrong. We will show a warning if all of these conditions are true: 1. We detect `**` in the glob pattern 2. _and_ you didn't explicitly use `node_modules` in the glob pattern 3. _and_ we found files that include `node_modules` in the file path 4. _and_ no other globs exist that explicitly match the found file With these rules in place, the DX has nice trade-offs: 1. Very simple projects (that don't even have a `node_modules` folder), can simply use `./**/*` because while resolving actual files we won't see files from `node_modules` and thus won't warn. 2. If you use `./src/**` and you do have a `node_modules`, then we also won't complain (unless you have a `node_modules` folder in the `./src` folder). 3. If you work with a 3rd party library that you want to make changes to. Using an explicit match like `./node_modules/my-package/**/*` is allowed because `node_modules` is explicitly mentioned. Note: this only shows a warning, it does not stop the process entirely. The warning will be show when the very first file in the `node_modules` is detected. <!-- 👋 Hey, thanks for your interest in contributing to Tailwind! **Please ask first before starting work on any significant new features.** It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create an issue to first discuss any significant new features. This includes things like adding new utilities, creating new at-rules, or adding new component examples to the documentation. https://github.com/tailwindcss/tailwindcss/blob/master/.github/CONTRIBUTING.md --> --------- Co-authored-by: Adam Wathan <[email protected]>
1 parent 1f23c2e commit 858696a

File tree

6 files changed

+300
-9
lines changed

6 files changed

+300
-9
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

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

1215
## [3.4.7] - 2024-07-25
1316

integrations/content-resolution/tests/content.test.js

+212-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
let fs = require('fs')
22
let path = require('path')
3+
let { stripVTControlCharacters } = require('util')
34
let { cwd } = require('./cwd.js')
45
let { writeConfigs, destroyConfigs } = require('./config.js')
56

67
let $ = require('../../execute')
78
let { css } = require('../../syntax')
89

9-
let { readOutputFile } = require('../../io')({
10+
let { writeInputFile, readOutputFile } = require('../../io')({
1011
output: 'dist',
1112
input: '.',
1213
})
@@ -37,14 +38,22 @@ async function build({ cwd: cwdPath } = {}) {
3738

3839
await cwd.switch(cwdPath)
3940

41+
// Hide console.log and console.error output
42+
let consoleLogMock = jest.spyOn(console, 'log').mockImplementation(() => {})
43+
let consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => {})
44+
4045
// Note that ./tailwind.config.js is hardcoded on purpose here
4146
// It represents a config but one that could be in different places
42-
await $(`postcss ${inputPath} -o ${outputPath}`, {
43-
env: { NODE_ENV: 'production' },
47+
let result = await $(`postcss ${inputPath} -o ${outputPath}`, {
48+
env: { NODE_ENV: 'production', JEST_WORKER_ID: undefined },
4449
cwd: cwdPath,
4550
})
4651

52+
consoleLogMock.mockRestore()
53+
consoleErrorMock.mockRestore()
54+
4755
return {
56+
...result,
4857
css: await readOutputFile('main.css'),
4958
}
5059
}
@@ -160,6 +169,206 @@ it('it handles ignored globs correctly when not relative to the config', async (
160169
expect(result.css).toMatchCss(``)
161170
})
162171

172+
it('warns when globs are too broad and match node_modules', async () => {
173+
await writeConfigs({
174+
both: {
175+
content: {
176+
files: ['./**/*.html'],
177+
},
178+
},
179+
})
180+
181+
let result = await build({ cwd: path.resolve(__dirname, '..') })
182+
183+
// No issues yet, because we don't have a file that resolves inside `node_modules`
184+
expect(result.stderr).toEqual('')
185+
186+
// We didn't scan any node_modules files yet
187+
expect(result.css).not.toIncludeCss(
188+
css`
189+
.content-\[\'node\\_modules\/bad\.html\'\] {
190+
--tw-content: 'node_modules/bad.html';
191+
content: var(--tw-content);
192+
}
193+
`
194+
)
195+
196+
// Write a file that resolves inside `node_modules`
197+
await writeInputFile(
198+
'node_modules/bad.html',
199+
String.raw`<div class="content-['node\_modules/bad.html']">Bad</div>`
200+
)
201+
202+
result = await build({ cwd: path.resolve(__dirname, '..') })
203+
204+
// We still expect the node_modules file to be processed
205+
expect(result.css).toIncludeCss(
206+
css`
207+
.content-\[\'node\\_modules\/bad\.html\'\] {
208+
--tw-content: 'node_modules/bad.html';
209+
content: var(--tw-content);
210+
}
211+
`
212+
)
213+
214+
// We didn't list `node_modules` in the glob explicitly, so we should see a
215+
// warning.
216+
expect(stripVTControlCharacters(result.stderr)).toMatchInlineSnapshot(`
217+
"
218+
warn - Your \`content\` configuration includes a pattern which looks like it's accidentally matching all of \`node_modules\` and can cause serious performance issues.
219+
warn - Pattern: \`./**/*.html\`
220+
warn - See our documentation for recommendations:
221+
warn - https://tailwindcss.com/docs/content-configuration#pattern-recommendations
222+
"
223+
`)
224+
})
225+
226+
it('should not warn when glob contains node_modules explicitly', async () => {
227+
await writeConfigs({
228+
both: {
229+
content: {
230+
files: ['./node_modules/**/*.html'],
231+
},
232+
},
233+
})
234+
235+
let result = await build({ cwd: path.resolve(__dirname, '..') })
236+
237+
// Write a file that resolves inside `node_modules`
238+
await writeInputFile(
239+
'node_modules/bad.html',
240+
String.raw`<div class="content-['node\_modules/bad.html']">Bad</div>`
241+
)
242+
243+
result = await build({ cwd: path.resolve(__dirname, '..') })
244+
245+
// We still expect the node_modules file to be processed
246+
expect(result.css).toIncludeCss(
247+
css`
248+
.content-\[\'node\\_modules\/bad\.html\'\] {
249+
--tw-content: 'node_modules/bad.html';
250+
content: var(--tw-content);
251+
}
252+
`
253+
)
254+
255+
// We explicitly listed `node_modules` in the glob, so we shouldn't see a
256+
// warning.
257+
expect(result.stderr).toEqual('')
258+
})
259+
260+
it('should not warn when globs are too broad if other glob match node_modules explicitly', async () => {
261+
await writeConfigs({
262+
both: {
263+
content: {
264+
files: ['./**/*.html', './node_modules/bad.html'],
265+
},
266+
},
267+
})
268+
269+
let result = await build({ cwd: path.resolve(__dirname, '..') })
270+
271+
// No issues yet, because we don't have a file that resolves inside `node_modules`
272+
expect(result.stderr).toEqual('')
273+
274+
// We didn't scan any node_modules files yet
275+
expect(result.css).not.toIncludeCss(
276+
css`
277+
.content-\[\'node\\_modules\/bad\.html\'\] {
278+
--tw-content: 'node_modules/bad.html';
279+
content: var(--tw-content);
280+
}
281+
`
282+
)
283+
284+
// Write a file that resolves inside `node_modules`
285+
await writeInputFile(
286+
'node_modules/bad.html',
287+
String.raw`<div class="content-['node\_modules/bad.html']">Bad</div>`
288+
)
289+
290+
result = await build({ cwd: path.resolve(__dirname, '..') })
291+
292+
// We still expect the node_modules file to be processed
293+
expect(result.css).toIncludeCss(
294+
css`
295+
.content-\[\'node\\_modules\/bad\.html\'\] {
296+
--tw-content: 'node_modules/bad.html';
297+
content: var(--tw-content);
298+
}
299+
`
300+
)
301+
302+
// We explicitly listed `node_modules` in the glob, so we shouldn't see a
303+
// warning.
304+
expect(result.stderr).toEqual('')
305+
306+
// Write a file that resolves inside `node_modules` but is not covered by the
307+
// explicit glob patterns.
308+
await writeInputFile(
309+
'node_modules/very-very-bad.html',
310+
String.raw`<div class="content-['node\_modules/very-very-bad.html']">Bad</div>`
311+
)
312+
313+
result = await build({ cwd: path.resolve(__dirname, '..') })
314+
315+
// We still expect the node_modules file to be processed
316+
expect(result.css).toIncludeCss(
317+
css`
318+
.content-\[\'node\\_modules\/very-very-bad\.html\'\] {
319+
--tw-content: 'node_modules/very-very-bad.html';
320+
content: var(--tw-content);
321+
}
322+
`
323+
)
324+
325+
// The very-very-bad.html file is not covered by the explicit glob patterns,
326+
// so we should see a warning.
327+
expect(stripVTControlCharacters(result.stderr)).toMatchInlineSnapshot(`
328+
"
329+
warn - Your \`content\` configuration includes a pattern which looks like it's accidentally matching all of \`node_modules\` and can cause serious performance issues.
330+
warn - Pattern: \`./**/*.html\`
331+
warn - See our documentation for recommendations:
332+
warn - https://tailwindcss.com/docs/content-configuration#pattern-recommendations
333+
"
334+
`)
335+
})
336+
337+
it('should not warn when a negative glob is used', async () => {
338+
await writeConfigs({
339+
both: {
340+
content: {
341+
files: ['./**/*.html', '!./node_modules/**/*.html'],
342+
},
343+
},
344+
})
345+
346+
// Write a file that resolves inside `node_modules`
347+
await writeInputFile(
348+
'node_modules/bad.html',
349+
String.raw`<div class="content-['node\_modules/bad.html']">Bad</div>`
350+
)
351+
352+
let result = await build({ cwd: path.resolve(__dirname, '..') })
353+
354+
// The initial glob resolving shouldn't use the node_modules file
355+
// in the first place.
356+
357+
// We still expect the node_modules file to be processed
358+
expect(result.css).not.toIncludeCss(
359+
css`
360+
.content-\[\'node\\_modules\/bad\.html\'\] {
361+
--tw-content: 'node_modules/bad.html';
362+
content: var(--tw-content);
363+
}
364+
`
365+
)
366+
367+
// The node_modules file shouldn't have been processed at all because it was
368+
// ignored by the negative glob.
369+
expect(result.stderr).toEqual('')
370+
})
371+
163372
it('it handles ignored globs correctly when relative to the config', async () => {
164373
await writeConfigs({
165374
both: {

integrations/execute.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ let resolveToolRoot = require('./resolve-tool-root')
55

66
let SHOW_OUTPUT = false
77

8-
let runningProcessess = []
8+
let runningProcesses = []
99

1010
afterEach(() => {
11-
runningProcessess.splice(0).forEach((runningProcess) => runningProcess.stop())
11+
runningProcesses.splice(0).forEach((runningProcess) => runningProcess.stop())
1212
})
1313

1414
function debounce(fn, ms) {
@@ -129,7 +129,7 @@ module.exports = function $(command, options = {}) {
129129
})
130130
})
131131

132-
runningProcessess.push(runningProcess)
132+
runningProcesses.push(runningProcess)
133133

134134
return Object.assign(runningProcess, {
135135
stop() {

integrations/io.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ module.exports = function ({
134134
}
135135
}
136136

137-
return fs.writeFile(path.resolve(absoluteInputFolder, file), contents, 'utf8')
137+
await fs.mkdir(path.dirname(filePath), { recursive: true })
138+
return fs.writeFile(filePath, contents, 'utf8')
138139
},
139140
async waitForOutputFileCreation(file) {
140141
if (file instanceof RegExp) {

src/cli/build/plugin.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { loadAutoprefixer, loadCssNano, loadPostcss, loadPostcssImport } from '.
1212
import { formatNodes, drainStdin, outputFile } from './utils'
1313
import { env } from '../../lib/sharedState'
1414
import resolveConfig from '../../../resolveConfig.js'
15-
import { parseCandidateFiles } from '../../lib/content.js'
15+
import { createBroadPatternCheck, parseCandidateFiles } from '../../lib/content.js'
1616
import { createWatcher } from './watching.js'
1717
import fastGlob from 'fast-glob'
1818
import { findAtConfigPath } from '../../lib/findAtConfigPath.js'
@@ -184,7 +184,11 @@ let state = {
184184
// TODO: When we make the postcss plugin async-capable this can become async
185185
let files = fastGlob.sync(this.contentPatterns.all)
186186

187+
let checkBroadPattern = createBroadPatternCheck(this.contentPatterns.all)
188+
187189
for (let file of files) {
190+
checkBroadPattern(file)
191+
188192
content.push({
189193
content: fs.readFileSync(path.resolve(file), 'utf8'),
190194
extension: path.extname(file).slice(1),
@@ -318,7 +322,7 @@ export async function createProcessor(args, cliConfigPath) {
318322
return fs.promises.readFile(path.resolve(input), 'utf8')
319323
}
320324

321-
// No input file provided, fallback to default atrules
325+
// No input file provided, fallback to default at-rules
322326
return '@tailwind base; @tailwind components; @tailwind utilities'
323327
}
324328

0 commit comments

Comments
 (0)