mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
When you configure custom content globs inside an `@config` file, we want to tread these globs as being relative to that config file and not the CSS file that requires the content file. A config can be used by multiple CSS configs. --------- Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
225 lines
6.8 KiB
TypeScript
225 lines
6.8 KiB
TypeScript
import { compile } from '@tailwindcss/node'
|
|
import { clearRequireCache } from '@tailwindcss/node/require-cache'
|
|
import { Scanner } from '@tailwindcss/oxide'
|
|
import fs from 'fs'
|
|
import fixRelativePathsPlugin from 'internal-postcss-fix-relative-paths'
|
|
import { Features, transform } from 'lightningcss'
|
|
import path from 'path'
|
|
import postcss, { AtRule, type AcceptedPlugin, type PluginCreator } from 'postcss'
|
|
import postcssImport from 'postcss-import'
|
|
|
|
/**
|
|
* A Map that can generate default values for keys that don't exist.
|
|
* Generated default values are added to the map to avoid recomputation.
|
|
*/
|
|
class DefaultMap<T = string, V = any> extends Map<T, V> {
|
|
constructor(private factory: (key: T, self: DefaultMap<T, V>) => V) {
|
|
super()
|
|
}
|
|
|
|
get(key: T): V {
|
|
let value = super.get(key)
|
|
|
|
if (value === undefined) {
|
|
value = this.factory(key, this)
|
|
this.set(key, value)
|
|
}
|
|
|
|
return value
|
|
}
|
|
}
|
|
|
|
export type PluginOptions = {
|
|
// The base directory to scan for class candidates.
|
|
base?: string
|
|
|
|
// Optimize and minify the output CSS.
|
|
optimize?: boolean | { minify?: boolean }
|
|
}
|
|
|
|
function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
|
let base = opts.base ?? process.cwd()
|
|
let optimize = opts.optimize ?? process.env.NODE_ENV === 'production'
|
|
|
|
let cache = new DefaultMap(() => {
|
|
return {
|
|
mtimes: new Map<string, number>(),
|
|
compiler: null as null | Awaited<ReturnType<typeof compile>>,
|
|
css: '',
|
|
optimizedCss: '',
|
|
fullRebuildPaths: [] as string[],
|
|
}
|
|
})
|
|
|
|
let hasApply: boolean, hasTailwind: boolean
|
|
|
|
return {
|
|
postcssPlugin: '@tailwindcss/postcss',
|
|
plugins: [
|
|
// We need to run `postcss-import` first to handle `@import` rules.
|
|
postcssImport(),
|
|
fixRelativePathsPlugin(),
|
|
|
|
{
|
|
postcssPlugin: 'tailwindcss',
|
|
Once() {
|
|
// Reset some state between builds
|
|
hasApply = false
|
|
hasTailwind = false
|
|
},
|
|
AtRule(rule: AtRule) {
|
|
if (rule.name === 'apply') {
|
|
hasApply = true
|
|
} else if (rule.name === 'tailwind') {
|
|
hasApply = true
|
|
hasTailwind = true
|
|
}
|
|
},
|
|
async OnceExit(root, { result }) {
|
|
let inputFile = result.opts.from ?? ''
|
|
let context = cache.get(inputFile)
|
|
let inputBasePath = path.dirname(path.resolve(inputFile))
|
|
|
|
async function createCompiler() {
|
|
clearRequireCache(context.fullRebuildPaths)
|
|
|
|
context.fullRebuildPaths = []
|
|
|
|
return compile(root.toString(), {
|
|
base: inputBasePath,
|
|
onDependency: (path) => {
|
|
context.fullRebuildPaths.push(path)
|
|
},
|
|
})
|
|
}
|
|
|
|
// Setup the compiler if it doesn't exist yet. This way we can
|
|
// guarantee a `build()` function is available.
|
|
context.compiler ??= await createCompiler()
|
|
|
|
let rebuildStrategy: 'full' | 'incremental' = 'incremental'
|
|
|
|
// Track file modification times to CSS files
|
|
{
|
|
for (let file of context.fullRebuildPaths) {
|
|
result.messages.push({
|
|
type: 'dependency',
|
|
plugin: '@tailwindcss/postcss',
|
|
file,
|
|
parent: result.opts.from,
|
|
})
|
|
}
|
|
|
|
let files = result.messages.flatMap((message) => {
|
|
if (message.type !== 'dependency') return []
|
|
return message.file
|
|
})
|
|
files.push(inputFile)
|
|
|
|
for (let file of files) {
|
|
let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null
|
|
if (changedTime === null) {
|
|
if (file === inputFile) {
|
|
rebuildStrategy = 'full'
|
|
}
|
|
continue
|
|
}
|
|
|
|
let prevTime = context.mtimes.get(file)
|
|
if (prevTime === changedTime) continue
|
|
|
|
rebuildStrategy = 'full'
|
|
context.mtimes.set(file, changedTime)
|
|
}
|
|
}
|
|
|
|
// Do nothing if neither `@tailwind` nor `@apply` is used
|
|
if (!hasTailwind && !hasApply) return
|
|
|
|
let css = ''
|
|
|
|
// Look for candidates used to generate the CSS
|
|
let scanner = new Scanner({
|
|
detectSources: { base },
|
|
sources: context.compiler.globs.map(({ origin, pattern }) => ({
|
|
// Ensure the glob is relative to the input CSS file or the config
|
|
// file where it is specified.
|
|
base: origin ? path.dirname(path.resolve(inputBasePath, origin)) : inputBasePath,
|
|
pattern,
|
|
})),
|
|
})
|
|
|
|
//
|
|
let candidates = scanner.scan()
|
|
|
|
// Add all found files as direct dependencies
|
|
for (let file of scanner.files) {
|
|
result.messages.push({
|
|
type: 'dependency',
|
|
plugin: '@tailwindcss/postcss',
|
|
file,
|
|
parent: result.opts.from,
|
|
})
|
|
}
|
|
|
|
// Register dependencies so changes in `base` cause a rebuild while
|
|
// giving tools like Vite or Parcel a glob that can be used to limit
|
|
// the files that cause a rebuild to only those that match it.
|
|
for (let { base, pattern } of scanner.globs) {
|
|
result.messages.push({
|
|
type: 'dir-dependency',
|
|
plugin: '@tailwindcss/postcss',
|
|
dir: base,
|
|
glob: pattern,
|
|
parent: result.opts.from,
|
|
})
|
|
}
|
|
|
|
if (rebuildStrategy === 'full') {
|
|
context.compiler = await createCompiler()
|
|
css = context.compiler.build(hasTailwind ? candidates : [])
|
|
} else if (rebuildStrategy === 'incremental') {
|
|
css = context.compiler.build!(candidates)
|
|
}
|
|
|
|
// Replace CSS
|
|
if (css !== context.css && optimize) {
|
|
context.optimizedCss = optimizeCss(css, {
|
|
minify: typeof optimize === 'object' ? optimize.minify : true,
|
|
})
|
|
}
|
|
context.css = css
|
|
root.removeAll()
|
|
root.append(postcss.parse(optimize ? context.optimizedCss : context.css, result.opts))
|
|
},
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
function optimizeCss(
|
|
input: string,
|
|
{ file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {},
|
|
) {
|
|
return transform({
|
|
filename: file,
|
|
code: Buffer.from(input),
|
|
minify,
|
|
sourceMap: false,
|
|
drafts: {
|
|
customMedia: true,
|
|
},
|
|
nonStandard: {
|
|
deepSelectorCombinator: true,
|
|
},
|
|
include: Features.Nesting,
|
|
exclude: Features.LogicalProperties,
|
|
targets: {
|
|
safari: (16 << 16) | (4 << 8),
|
|
},
|
|
errorRecovery: true,
|
|
}).code.toString()
|
|
}
|
|
|
|
export default Object.assign(tailwindcss, { postcss: true }) as PluginCreator<PluginOptions>
|