mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2026-01-18 16:17:36 +00:00
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 <adam.wathan@gmail.com>
182 lines
5.3 KiB
JavaScript
182 lines
5.3 KiB
JavaScript
let { rm, existsSync } = require('fs')
|
|
let path = require('path')
|
|
let fs = require('fs/promises')
|
|
|
|
let chokidar = require('chokidar')
|
|
|
|
let resolveToolRoot = require('./resolve-tool-root')
|
|
|
|
function getWatcherOptions() {
|
|
return {
|
|
usePolling: true,
|
|
interval: 200,
|
|
awaitWriteFinish: {
|
|
stabilityThreshold: 1500,
|
|
pollInterval: 50,
|
|
},
|
|
}
|
|
}
|
|
|
|
module.exports = function ({
|
|
/** Output directory, relative to the tool. */
|
|
output = 'dist',
|
|
|
|
/** Input directory, relative to the tool. */
|
|
input = 'src',
|
|
|
|
/** Whether or not you want to cleanup the output directory. */
|
|
cleanup = true,
|
|
} = {}) {
|
|
let toolRoot = resolveToolRoot()
|
|
let fileCache = {}
|
|
|
|
let absoluteOutputFolder = path.resolve(toolRoot, output)
|
|
let absoluteInputFolder = path.resolve(toolRoot, input)
|
|
|
|
if (cleanup) {
|
|
beforeAll((done) => rm(absoluteOutputFolder, { recursive: true, force: true }, done))
|
|
afterEach((done) => rm(absoluteOutputFolder, { recursive: true, force: true }, done))
|
|
}
|
|
|
|
// Restore all written files
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
Object.entries(fileCache).map(async ([file, content]) => {
|
|
try {
|
|
if (content === null) {
|
|
return await fs.unlink(file)
|
|
} else {
|
|
return await fs.writeFile(file, content, 'utf8')
|
|
}
|
|
} catch {}
|
|
})
|
|
)
|
|
})
|
|
|
|
async function readdir(start, parent = []) {
|
|
let files = await fs.readdir(start, { withFileTypes: true })
|
|
let resolvedFiles = await Promise.all(
|
|
files.map((file) => {
|
|
if (file.isDirectory()) {
|
|
return readdir(path.resolve(start, file.name), [...parent, file.name])
|
|
}
|
|
return parent.concat(file.name).join(path.sep)
|
|
})
|
|
)
|
|
return resolvedFiles.flat(Infinity)
|
|
}
|
|
|
|
async function resolveFile(fileOrRegex, directory) {
|
|
if (fileOrRegex instanceof RegExp) {
|
|
let files = await readdir(directory)
|
|
if (files.length === 0) {
|
|
throw new Error(`No files exists in "${directory}"`)
|
|
}
|
|
|
|
let filtered = files.filter((file) => fileOrRegex.test(file))
|
|
if (filtered.length === 0) {
|
|
throw new Error(`Not a single file matched: ${fileOrRegex}`)
|
|
} else if (filtered.length > 1) {
|
|
throw new Error(`Multiple files matched: ${fileOrRegex}`)
|
|
}
|
|
|
|
return filtered[0]
|
|
}
|
|
|
|
return fileOrRegex
|
|
}
|
|
|
|
return {
|
|
cleanupFile(file) {
|
|
let filePath = path.resolve(toolRoot, file)
|
|
fileCache[filePath] = null
|
|
},
|
|
async fileExists(file) {
|
|
let filePath = path.resolve(toolRoot, file)
|
|
return existsSync(filePath)
|
|
},
|
|
async removeFile(file) {
|
|
let filePath = path.resolve(toolRoot, file)
|
|
|
|
if (!fileCache[filePath]) {
|
|
fileCache[filePath] = await fs.readFile(filePath, 'utf8').catch(() => null)
|
|
}
|
|
|
|
await fs.unlink(filePath).catch(() => null)
|
|
},
|
|
async readOutputFile(file) {
|
|
file = await resolveFile(file, absoluteOutputFolder)
|
|
return fs.readFile(path.resolve(absoluteOutputFolder, file), 'utf8')
|
|
},
|
|
async readInputFile(file) {
|
|
file = await resolveFile(file, absoluteInputFolder)
|
|
return fs.readFile(path.resolve(absoluteInputFolder, file), 'utf8')
|
|
},
|
|
async appendToInputFile(file, contents) {
|
|
let filePath = path.resolve(absoluteInputFolder, file)
|
|
if (!fileCache[filePath]) {
|
|
fileCache[filePath] = await fs.readFile(filePath, 'utf8')
|
|
}
|
|
|
|
return fs.appendFile(filePath, contents, 'utf8')
|
|
},
|
|
async writeInputFile(file, contents) {
|
|
let filePath = path.resolve(absoluteInputFolder, file)
|
|
if (!fileCache[filePath]) {
|
|
try {
|
|
fileCache[filePath] = await fs.readFile(filePath, 'utf8')
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT') {
|
|
fileCache[filePath] = null // Sentinel value to `delete` the file afterwards. This also means that we are writing to a `new` file inside the test.
|
|
} else {
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
return fs.writeFile(filePath, contents, 'utf8')
|
|
},
|
|
async waitForOutputFileCreation(file) {
|
|
if (file instanceof RegExp) {
|
|
let r = file
|
|
let watcher = chokidar.watch(absoluteOutputFolder, getWatcherOptions())
|
|
|
|
return new Promise((resolve) => {
|
|
watcher.on('add', (file) => {
|
|
if (r.test(file)) {
|
|
watcher.close().then(() => resolve())
|
|
}
|
|
})
|
|
})
|
|
} else {
|
|
let filePath = path.resolve(absoluteOutputFolder, file)
|
|
|
|
return new Promise((resolve) => {
|
|
let watcher = chokidar.watch(absoluteOutputFolder, getWatcherOptions())
|
|
|
|
watcher.on('add', (addedFile) => {
|
|
if (addedFile !== filePath) return
|
|
return watcher.close().finally(resolve)
|
|
})
|
|
})
|
|
}
|
|
},
|
|
async waitForOutputFileChange(file, cb = () => {}) {
|
|
file = await resolveFile(file, absoluteOutputFolder)
|
|
let filePath = path.resolve(absoluteOutputFolder, file)
|
|
|
|
return new Promise((resolve) => {
|
|
let watcher = chokidar.watch(absoluteOutputFolder, getWatcherOptions())
|
|
|
|
watcher
|
|
.on('change', (changedFile) => {
|
|
if (changedFile !== filePath) return
|
|
return watcher.close().finally(resolve)
|
|
})
|
|
.on('ready', cb)
|
|
})
|
|
},
|
|
}
|
|
}
|