@maizzle/framework 6.0.0-6 → 6.0.0-8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/posthtml/plugins/postcss/compileCss.js +2 -6
- package/src/posthtml/plugins/removeRawStyleAttributes.js +8 -0
- package/src/transformers/inline.js +159 -111
- package/src/utils/getConfigByFilePath.js +16 -7
- package/src/posthtml/plugins/postcss/cleanupTailwindArtifacts.js +0 -78
- package/src/posthtml/plugins/postcss/removeDuplicateSelectors.js +0 -35
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [6.0.0-8] - 2025-07-16
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- refactor: css inlining d443c31
|
|
12
|
+
|
|
13
|
+
## [6.0.0-7] - 2025-07-15
|
|
14
|
+
|
|
15
|
+
This release integrates the `v5.2.2` patch release, which fixes an issue where both local and production `build.content` paths were used when building for production. When specified in a `config.{env}.js` file, build.content must not be merged with the one in the base `config.js`.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- fix: ensure content paths are unique to each build environment 46c292b
|
|
20
|
+
|
|
7
21
|
## [6.0.0-6] - 2025-07-14
|
|
8
22
|
|
|
9
23
|
### Fixed
|
package/package.json
CHANGED
|
@@ -6,8 +6,6 @@ import { transform } from 'lightningcss'
|
|
|
6
6
|
import tailwindcss from '@tailwindcss/postcss'
|
|
7
7
|
import cssVariables from 'postcss-css-variables'
|
|
8
8
|
import postcssSafeParser from 'postcss-safe-parser'
|
|
9
|
-
import removeDuplicateSelectors from './removeDuplicateSelectors.js'
|
|
10
|
-
import cleanupTailwindArtifacts from './cleanupTailwindArtifacts.js'
|
|
11
9
|
|
|
12
10
|
const attributes = new Set([
|
|
13
11
|
'raw',
|
|
@@ -24,8 +22,8 @@ export const validAttributeNames = attributes
|
|
|
24
22
|
* PostHTML plugin to process Tailwind CSS within style tags.
|
|
25
23
|
*
|
|
26
24
|
* This plugin processes CSS content in `<style>` tags and
|
|
27
|
-
* compiles it with PostCSS.
|
|
28
|
-
* `
|
|
25
|
+
* compiles it with PostCSS. Tags marked with attribute
|
|
26
|
+
* names found in `attributes` will be skipped.
|
|
29
27
|
*/
|
|
30
28
|
export function compileCss(config = {}) {
|
|
31
29
|
return tree => {
|
|
@@ -88,8 +86,6 @@ async function processCss(css, config) {
|
|
|
88
86
|
tailwindcss(get(config, 'css.tailwind', {})),
|
|
89
87
|
resolveCSSProps !== false && cssVariables(resolveCSSProps),
|
|
90
88
|
resolveCalc !== false && postcssCalc(resolveCalc),
|
|
91
|
-
removeDuplicateSelectors(),
|
|
92
|
-
cleanupTailwindArtifacts(get(config, 'css.cleanup', {})),
|
|
93
89
|
...get(config, 'postcss.plugins', []),
|
|
94
90
|
].filter(Boolean))
|
|
95
91
|
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { validAttributeNames } from './postcss/compileCss.js'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Remove specific attributes from `<style>` tags in PostHTML.
|
|
5
|
+
*
|
|
6
|
+
* Use to clean up <style> tag attributes after proccessing
|
|
7
|
+
* has taken place. A "raw" attribute is used to indicate
|
|
8
|
+
* that the content should not be compiled.
|
|
9
|
+
* @returns {Function} PostHTML plugin
|
|
10
|
+
*/
|
|
3
11
|
const plugin = () => tree => {
|
|
4
12
|
const process = node => {
|
|
5
13
|
if (node.tag === 'style') {
|
|
@@ -6,18 +6,29 @@ import * as cheerio from 'cheerio/slim'
|
|
|
6
6
|
import remove from 'lodash-es/remove.js'
|
|
7
7
|
import { render } from 'posthtml-render'
|
|
8
8
|
import isEmpty from 'lodash-es/isEmpty.js'
|
|
9
|
+
import { match } from 'posthtml/lib/api.js'
|
|
9
10
|
import safeParser from 'postcss-safe-parser'
|
|
10
11
|
import isObject from 'lodash-es/isObject.js'
|
|
11
12
|
import { parser as parse } from 'posthtml-parser'
|
|
12
13
|
import { useAttributeSizes } from './useAttributeSizes.js'
|
|
13
14
|
import { getPosthtmlOptions } from '../posthtml/defaultConfig.js'
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* PostHTML plugin to inline CSS
|
|
18
|
+
* @param {*} options `css.inline` object from config
|
|
19
|
+
* @returns {Function} PostHTML tree
|
|
20
|
+
*/
|
|
21
|
+
export default (options = {}) => tree => {
|
|
22
|
+
return inline(render(tree), options)
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Function to inline CSS styles with Posthtml and Juice.
|
|
27
|
+
*
|
|
28
|
+
* @param {*} html HTML string to process
|
|
29
|
+
* @param {*} options Options for Juice
|
|
30
|
+
* @returns {string} HTML with inlined styles
|
|
31
|
+
*/
|
|
21
32
|
export async function inline(html = '', options = {}) {
|
|
22
33
|
// Exit early if no HTML is passed
|
|
23
34
|
if (typeof html !== 'string' || html === '') {
|
|
@@ -68,32 +79,34 @@ export async function inline(html = '', options = {}) {
|
|
|
68
79
|
})
|
|
69
80
|
}
|
|
70
81
|
|
|
71
|
-
const $ = cheerio.load(html, {
|
|
72
|
-
xml: {
|
|
73
|
-
decodeEntities: false,
|
|
74
|
-
xmlMode: false,
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
// Add a `data-embed` attribute to style tags that have the embed attribute
|
|
79
|
-
$('style[embed]:not([data-embed])').each((_i, el) => {
|
|
80
|
-
$(el).attr('data-embed', '')
|
|
81
|
-
})
|
|
82
|
-
$('style[data-embed]:not([embed])').each((_i, el) => {
|
|
83
|
-
$(el).attr('embed', '')
|
|
84
|
-
})
|
|
85
|
-
|
|
86
82
|
/**
|
|
87
83
|
* Inline the CSS
|
|
88
84
|
*
|
|
89
85
|
* If customCSS is passed, inline that CSS specifically
|
|
90
86
|
* Otherwise, use Juice's default inlining
|
|
91
87
|
*/
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
const tree = parse(html, getPosthtmlOptions())
|
|
89
|
+
tree.match = match
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Add a `data-embed` attribute to style tags that have the `embed`
|
|
93
|
+
* attribute, so that Juice can skip them.
|
|
94
|
+
*/
|
|
95
|
+
tree.match({ tag: 'style' }, (node) => {
|
|
96
|
+
if (node.attrs && node.attrs.embed !== undefined) {
|
|
97
|
+
node.attrs['data-embed'] = true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (node.attrs && node.attrs['data-embed'] !== undefined) {
|
|
101
|
+
node.attrs.embed = true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return node
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
let inlined_html = css
|
|
108
|
+
? juice.inlineContent(render(tree), css, { removeStyleTags, ...options })
|
|
109
|
+
: juice(render(tree), { removeStyleTags, ...options })
|
|
97
110
|
|
|
98
111
|
/**
|
|
99
112
|
* Prefer attribute sizes
|
|
@@ -104,23 +117,34 @@ export async function inline(html = '', options = {}) {
|
|
|
104
117
|
* natural size.
|
|
105
118
|
*/
|
|
106
119
|
if (options.useAttributeSizes) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
})
|
|
112
|
-
)
|
|
120
|
+
inlined_html = await useAttributeSizes(inlined_html, {
|
|
121
|
+
width: juice.widthElements,
|
|
122
|
+
height: juice.heightElements,
|
|
123
|
+
}, getPosthtmlOptions())
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
/**
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
* Remove inlined selectors from the HTML
|
|
128
|
+
*
|
|
129
|
+
*/
|
|
130
|
+
const inlined_tree = parse(inlined_html, getPosthtmlOptions())
|
|
131
|
+
inlined_tree.match = match
|
|
132
|
+
|
|
133
|
+
const preservedAtRules = get(options, 'preservedAtRules', ['media'])
|
|
134
|
+
const selectors = new Set()
|
|
135
|
+
|
|
136
|
+
inlined_tree.match({ tag: 'style' }, node => {
|
|
137
|
+
// If this is an embedded style tag, exit early
|
|
138
|
+
if (node.attrs && ('data-embed' in node.attrs || 'embed' in node.attrs)) {
|
|
139
|
+
return node
|
|
140
|
+
}
|
|
141
|
+
|
|
120
142
|
// Parse the CSS
|
|
121
143
|
const { root } = postcss()
|
|
122
144
|
.process(
|
|
123
|
-
|
|
145
|
+
Array.isArray(node.content)
|
|
146
|
+
? node.content.join('')
|
|
147
|
+
: node.content,
|
|
124
148
|
{
|
|
125
149
|
from: undefined,
|
|
126
150
|
parser: safeParser
|
|
@@ -134,14 +158,15 @@ export async function inline(html = '', options = {}) {
|
|
|
134
158
|
|
|
135
159
|
const combinedRegex = new RegExp(combinedPattern)
|
|
136
160
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// Preserve selectors in at rules
|
|
161
|
+
// Preserve selectors in predefined at-rules
|
|
140
162
|
root.walkAtRules(rule => {
|
|
141
|
-
if (
|
|
163
|
+
if (preservedAtRules.includes(rule.name)) {
|
|
142
164
|
rule.walkRules(rule => {
|
|
143
165
|
options.safelist.add(rule.selector)
|
|
144
166
|
})
|
|
167
|
+
} else {
|
|
168
|
+
// Remove the at rule if it's not predefined
|
|
169
|
+
rule.remove()
|
|
145
170
|
}
|
|
146
171
|
})
|
|
147
172
|
|
|
@@ -157,11 +182,12 @@ export async function inline(html = '', options = {}) {
|
|
|
157
182
|
prop: get(rule.nodes[0], 'prop')
|
|
158
183
|
})
|
|
159
184
|
}
|
|
160
|
-
// Preserve pseudo selectors
|
|
161
185
|
else {
|
|
186
|
+
// Preserve pseudo selectors
|
|
162
187
|
options.safelist.add(selector)
|
|
163
188
|
}
|
|
164
189
|
|
|
190
|
+
|
|
165
191
|
if (options.removeInlinedSelectors) {
|
|
166
192
|
// Remove the rule in the <style> tag as long as it's not a preserved class
|
|
167
193
|
if (!options.safelist.has(selector) && !combinedRegex.test(selector)) {
|
|
@@ -169,90 +195,112 @@ export async function inline(html = '', options = {}) {
|
|
|
169
195
|
}
|
|
170
196
|
|
|
171
197
|
// Update the <style> tag contents
|
|
172
|
-
|
|
198
|
+
node.content = root.toString()
|
|
173
199
|
}
|
|
174
200
|
})
|
|
175
201
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
* 1. `preferUnitlessValues` - Replace unit values with `0` where possible
|
|
180
|
-
* 2. `removeInlinedSelectors` - Remove inlined selectors from the HTML
|
|
181
|
-
*/
|
|
182
|
-
|
|
183
|
-
// Loop over selectors that we found in the <style> tags
|
|
184
|
-
selectors.forEach(({ name, prop }) => {
|
|
185
|
-
try {
|
|
186
|
-
const elements = $(name).get()
|
|
187
|
-
|
|
188
|
-
// If the property is excluded from inlining, skip
|
|
189
|
-
if (!juice.excludedProperties.includes(prop)) {
|
|
190
|
-
// Find the selector in the HTML
|
|
191
|
-
elements.forEach((el) => {
|
|
192
|
-
// Get a `property|value` list from the inline style attribute
|
|
193
|
-
const styleAttr = $(el).attr('style')
|
|
194
|
-
const inlineStyles = {}
|
|
195
|
-
|
|
196
|
-
// 1. `preferUnitlessValues`
|
|
197
|
-
if (styleAttr) {
|
|
198
|
-
try {
|
|
199
|
-
const root = postcss.parse(`* { ${styleAttr} }`)
|
|
200
|
-
|
|
201
|
-
root.first.each((decl) => {
|
|
202
|
-
const property = decl.prop
|
|
203
|
-
let value = decl.value
|
|
204
|
-
|
|
205
|
-
if (value && options.preferUnitlessValues) {
|
|
206
|
-
value = value.replace(
|
|
207
|
-
/\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\b/g,
|
|
208
|
-
'0'
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (property) {
|
|
213
|
-
inlineStyles[property] = value
|
|
214
|
-
}
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
// Update the element's style attribute with the new value
|
|
218
|
-
$(el).attr(
|
|
219
|
-
'style',
|
|
220
|
-
Object.entries(inlineStyles).map(([property, value]) => `${property}: ${value}`).join('; ')
|
|
221
|
-
)
|
|
222
|
-
} catch {}
|
|
223
|
-
}
|
|
202
|
+
// This is inlined_tree
|
|
203
|
+
return node
|
|
204
|
+
})
|
|
224
205
|
|
|
225
|
-
|
|
226
|
-
|
|
206
|
+
/**
|
|
207
|
+
* CSS optimizations
|
|
208
|
+
*
|
|
209
|
+
* `preferUnitlessValues` - Replace unit values with `0` where possible
|
|
210
|
+
* `removeInlinedSelectors` - Remove inlined selectors from the HTML
|
|
211
|
+
*/
|
|
227
212
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
213
|
+
const $ = cheerio.load(render(inlined_tree), {
|
|
214
|
+
xml: {
|
|
215
|
+
decodeEntities: false,
|
|
216
|
+
xmlMode: false,
|
|
217
|
+
}
|
|
218
|
+
})
|
|
231
219
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
220
|
+
// Loop over selectors that we found in the <style> tags
|
|
221
|
+
selectors.forEach(({ name, prop }) => {
|
|
222
|
+
try {
|
|
223
|
+
const elements = $(name).get()
|
|
224
|
+
// If the property is excluded from inlining, skip
|
|
225
|
+
if (!juice.excludedProperties.includes(prop)) {
|
|
226
|
+
// Find the selector in the HTML
|
|
227
|
+
elements.forEach((el) => {
|
|
228
|
+
// Get a `property|value` list from the inline style attribute
|
|
229
|
+
const styleAttr = $(el).attr('style')
|
|
230
|
+
// Store the element's inline styles
|
|
231
|
+
const inlineStyles = {}
|
|
232
|
+
|
|
233
|
+
// `preferUnitlessValues`
|
|
234
|
+
if (styleAttr) {
|
|
235
|
+
try {
|
|
236
|
+
const root = postcss.parse(`* { ${styleAttr} }`)
|
|
237
|
+
|
|
238
|
+
root.first.each((decl) => {
|
|
239
|
+
const property = decl.prop
|
|
240
|
+
let value = decl.value
|
|
241
|
+
|
|
242
|
+
if (value && options.preferUnitlessValues) {
|
|
243
|
+
value = value.replace(
|
|
244
|
+
/\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\b/g,
|
|
245
|
+
'0'
|
|
246
|
+
)
|
|
237
247
|
}
|
|
238
248
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
$(el).attr('class', classList.join(' '))
|
|
242
|
-
} else {
|
|
243
|
-
$(el).removeAttr('class')
|
|
249
|
+
if (property) {
|
|
250
|
+
inlineStyles[property] = value
|
|
244
251
|
}
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Update the element's style attribute with the new value
|
|
255
|
+
$(el).attr(
|
|
256
|
+
'style',
|
|
257
|
+
Object.entries(inlineStyles).map(([property, value]) => `${property}: ${value}`).join('; ')
|
|
258
|
+
)
|
|
259
|
+
} catch { }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Get the classes from the element's class attribute
|
|
263
|
+
const classes = $(el).attr('class')
|
|
264
|
+
|
|
265
|
+
// `removeInlinedSelectors`
|
|
266
|
+
if (options.removeInlinedSelectors && classes) {
|
|
267
|
+
const classList = classes.split(' ')
|
|
268
|
+
|
|
269
|
+
// If the class has been inlined in the style attribute...
|
|
270
|
+
if (has(inlineStyles, prop)) {
|
|
271
|
+
// Try to remove the classes that have been inlined
|
|
272
|
+
if (![...options.safelist].some(item => item.includes(name))) {
|
|
273
|
+
remove(classList, classToRemove => name.includes(classToRemove))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Update the class list on the element with the new classes
|
|
277
|
+
if (classList.length > 0) {
|
|
278
|
+
$(el).attr('class', classList.join(' '))
|
|
279
|
+
} else {
|
|
280
|
+
$(el).removeAttr('class')
|
|
245
281
|
}
|
|
246
282
|
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
} catch { }
|
|
251
287
|
})
|
|
252
288
|
|
|
253
|
-
|
|
254
|
-
|
|
289
|
+
const optimized_tree = parse($.html(), getPosthtmlOptions())
|
|
290
|
+
optimized_tree.match = match
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Remove the `embed` attribute from style tags
|
|
294
|
+
*/
|
|
295
|
+
optimized_tree.match({ tag: 'style' }, (node) => {
|
|
296
|
+
// If this is an embedded style tag, exit early
|
|
297
|
+
if (node.attrs && node.attrs.embed !== undefined) {
|
|
298
|
+
delete node.attrs.embed
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return node
|
|
255
302
|
})
|
|
256
303
|
|
|
257
|
-
return
|
|
304
|
+
// Finally, return the inlined html
|
|
305
|
+
return render(optimized_tree)
|
|
258
306
|
}
|
|
@@ -118,16 +118,25 @@ export async function readFileConfig(config) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
* Override the `content` key in `baseConfig` with the one in `envConfig
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* order to avoid compiling unnecessary files.
|
|
121
|
+
* Override the `build.content` key in `baseConfig` with the one in `envConfig`
|
|
122
|
+
* if present. We do this so that each build uses its own `content` paths,
|
|
123
|
+
* in order to avoid compiling unnecessary files.
|
|
125
124
|
*/
|
|
126
|
-
if (Array.isArray(envConfig.content)) {
|
|
127
|
-
baseConfig.
|
|
125
|
+
if (envConfig.build && Array.isArray(envConfig.build.content)) {
|
|
126
|
+
baseConfig.build = baseConfig.build || {}
|
|
127
|
+
baseConfig.build.content = envConfig.build.content
|
|
128
|
+
// Remove build.content from envConfig to prevent merging duplicates
|
|
129
|
+
envConfig = { ...envConfig, build: { ...envConfig.build, content: undefined } }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Merge envConfig and baseConfig, but ensure build.content is not duplicated
|
|
133
|
+
const merged = merge(envConfig, baseConfig)
|
|
134
|
+
|
|
135
|
+
if (baseConfig.build && Array.isArray(baseConfig.build.content)) {
|
|
136
|
+
merged.build.content = baseConfig.build.content
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
return
|
|
139
|
+
return merged
|
|
131
140
|
} catch (_error) {
|
|
132
141
|
throw new Error('Could not compute config')
|
|
133
142
|
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PostCSS plugin to clean up Tailwind CSS artifacts and leftovers.
|
|
3
|
-
*
|
|
4
|
-
* This plugin safely removes unused Tailwind-specific CSS that
|
|
5
|
-
* may be left behind after processing, while preserving
|
|
6
|
-
* any CSS that might be intentionally used.
|
|
7
|
-
*/
|
|
8
|
-
export default function cleanupTailwindArtifacts(options = {}) {
|
|
9
|
-
const opts = {
|
|
10
|
-
removeEmptyLayers: true,
|
|
11
|
-
removeUnusedTwProperties: true,
|
|
12
|
-
removeEmptyRules: false,
|
|
13
|
-
preserveCustomProperties: [], // Array of custom property names to preserve
|
|
14
|
-
...options
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
postcssPlugin: 'cleanup-tailwind-artifacts',
|
|
19
|
-
OnceExit(root) {
|
|
20
|
-
const usedCustomProperties = new Set()
|
|
21
|
-
const rulesToRemove = []
|
|
22
|
-
|
|
23
|
-
// First pass: collect all custom properties usage
|
|
24
|
-
root.walkDecls(decl => {
|
|
25
|
-
// Check if any declaration uses custom properties
|
|
26
|
-
if (decl.value.includes('var(--')) {
|
|
27
|
-
const matches = decl.value.match(/var\(--[\w-]+\)/g)
|
|
28
|
-
if (matches) {
|
|
29
|
-
matches.forEach(match => {
|
|
30
|
-
const propName = match.replace(/var\(|-|\)/g, '')
|
|
31
|
-
usedCustomProperties.add(propName)
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
// Second pass: find unused @property declarations and empty @layer rules
|
|
38
|
-
root.walkAtRules(rule => {
|
|
39
|
-
// Handle @property declarations
|
|
40
|
-
if (rule.name === 'property' && opts.removeUnusedTwProperties) {
|
|
41
|
-
const propertyName = rule.params.replace(/^--/, '')
|
|
42
|
-
|
|
43
|
-
// Only remove Tailwind-specific custom properties that aren't used
|
|
44
|
-
if (propertyName.startsWith('tw-') && !usedCustomProperties.has(propertyName)) {
|
|
45
|
-
// Check if it's in the preserve list
|
|
46
|
-
if (!opts.preserveCustomProperties.includes(propertyName)) {
|
|
47
|
-
rulesToRemove.push(rule)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Handle @layer rules
|
|
53
|
-
if (rule.name === 'layer' && opts.removeEmptyLayers) {
|
|
54
|
-
// Only remove @layer rules that have no nodes
|
|
55
|
-
if (!rule.nodes || rule.nodes.length === 0) {
|
|
56
|
-
rulesToRemove.push(rule)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
// Third pass: remove empty rules (optional, off by default)
|
|
62
|
-
if (opts.removeEmptyRules) {
|
|
63
|
-
root.walkRules(rule => {
|
|
64
|
-
if (!rule.nodes || rule.nodes.length === 0) {
|
|
65
|
-
rulesToRemove.push(rule)
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Remove all identified artifacts
|
|
71
|
-
rulesToRemove.forEach(rule => {
|
|
72
|
-
rule.remove()
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
cleanupTailwindArtifacts.postcss = true
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PostCSS plugin to remove duplicate selectors, keeping only the last occurrence.
|
|
3
|
-
* This is useful when CSS contains multiple rules with the same selector,
|
|
4
|
-
* and we want to keep only the most recent one.
|
|
5
|
-
*/
|
|
6
|
-
export default function removeDuplicateSelectors() {
|
|
7
|
-
return {
|
|
8
|
-
postcssPlugin: 'remove-duplicate-selectors',
|
|
9
|
-
OnceExit(root) {
|
|
10
|
-
const selectorMap = new Map()
|
|
11
|
-
const rulesToRemove = []
|
|
12
|
-
|
|
13
|
-
// First pass: collect all rules and their selectors
|
|
14
|
-
root.walkRules(rule => {
|
|
15
|
-
const selector = rule.selector
|
|
16
|
-
|
|
17
|
-
// If we've seen this selector before, mark the previous one for removal
|
|
18
|
-
if (selectorMap.has(selector)) {
|
|
19
|
-
const previousRule = selectorMap.get(selector)
|
|
20
|
-
rulesToRemove.push(previousRule)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Update the map with the current rule (latest occurrence)
|
|
24
|
-
selectorMap.set(selector, rule)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
// Second pass: remove all the duplicate rules
|
|
28
|
-
rulesToRemove.forEach(rule => {
|
|
29
|
-
rule.remove()
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
removeDuplicateSelectors.postcss = true
|