-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
index.ts
206 lines (179 loc) · 6.24 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import { scanDir } 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'
import { compile } from 'tailwindcss'
/**
* 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
}
}
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 | ReturnType<typeof compile>,
css: '',
optimizedCss: '',
}
})
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
}
},
OnceExit(root, { result }) {
let inputFile = result.opts.from ?? ''
let context = cache.get(inputFile)
let inputBasePath = path.dirname(path.resolve(inputFile))
function createCompiler() {
return compile(root.toString(), {
loadPlugin: (pluginPath) => {
if (pluginPath[0] === '.') {
return require(path.resolve(inputBasePath, pluginPath))
}
return require(pluginPath)
},
})
}
// Setup the compiler if it doesn't exist yet. This way we can
// guarantee a `build()` function is available.
context.compiler ??= createCompiler()
let rebuildStrategy: 'full' | 'incremental' = 'incremental'
// Track file modification times to CSS files
{
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 scanDirResult = scanDir({
base, // Root directory, mainly used for auto content detection
sources: context.compiler.globs.map((pattern) => ({
base: inputBasePath, // Globs are relative to the input.css file
pattern,
})),
})
// Add all found files as direct dependencies
for (let file of scanDirResult.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 scanDirResult.globs) {
result.messages.push({
type: 'dir-dependency',
plugin: '@tailwindcss/postcss',
dir: base,
glob: pattern,
parent: result.opts.from,
})
}
if (rebuildStrategy === 'full') {
context.compiler = createCompiler()
css = context.compiler.build(hasTailwind ? scanDirResult.candidates : [])
} else if (rebuildStrategy === 'incremental') {
css = context.compiler.build!(scanDirResult.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>