@maizzle/framework 6.0.0-1 → 6.0.0-3

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 CHANGED
@@ -4,6 +4,13 @@ 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-2] - 2025-07-11
8
+
9
+ ### Fixed
10
+
11
+ - fixed an issue with duplicate CSS selectors for utilities that cannot be disabled in Tailwind CSS v4, like `text-decoration`
12
+ - fixed an issue where some Tailwind directives like `@layer` or `@property` were still present in the final build, even though they were not used
13
+
7
14
  ## [6.0.0-1] - 2025-07-11
8
15
 
9
16
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maizzle/framework",
3
- "version": "6.0.0-1",
3
+ "version": "6.0.0-3",
4
4
  "description": "Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,6 +18,8 @@ import tailwindcss from '@tailwindcss/postcss'
18
18
  import postcssCalc from 'postcss-calc'
19
19
  import cssVariables from 'postcss-css-variables'
20
20
  import postcssSafeParser from 'postcss-safe-parser'
21
+ import removeDuplicateSelectors from './plugins/postcss/removeDuplicateSelectors.js'
22
+ import cleanupTailwindArtifacts from './plugins/postcss/cleanupTailwindArtifacts.js'
21
23
 
22
24
  import defaultComponentsConfig from './defaultComponentsConfig.js'
23
25
 
@@ -36,6 +38,8 @@ export async function process(html = '', config = {}) {
36
38
  tailwindcss(get(config, 'css.tailwind', {})),
37
39
  resolveCSSProps !== false && cssVariables(resolveCSSProps),
38
40
  resolveCalc !== false && postcssCalc(resolveCalc),
41
+ removeDuplicateSelectors(),
42
+ cleanupTailwindArtifacts(get(config, 'css.cleanup', {})),
39
43
  ...get(config, 'postcss.plugins', []),
40
44
  ],
41
45
  merge(
@@ -0,0 +1,78 @@
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
@@ -0,0 +1,35 @@
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
@@ -32,21 +32,26 @@ export async function inline(html = '', options = {}) {
32
32
  options.safelist = new Set([
33
33
  ...get(options, 'safelist', []),
34
34
  ...[
35
- '.body', // Gmail
36
- '.gmail', // Gmail
37
- '.apple', // Apple Mail
38
- '.ios', // Mail on iOS
39
- '.ox-', // Open-Xchange
40
- '.outlook', // Outlook.com
35
+ 'body', // Gmail
36
+ 'gmail', // Gmail
37
+ 'apple', // Apple Mail
38
+ 'ios', // Mail on iOS
39
+ 'ox-', // Open-Xchange
40
+ 'yahoo', // Yahoo! Mail
41
+ 'outlook', // Outlook Mac and Android
41
42
  '[data-ogs', // Outlook.com
42
- '.bloop_container', // Airmail
43
- '.Singleton', // Apple Mail 10
44
- '.unused', // Notes 8
45
- '.moz-text-html', // Thunderbird
46
- '.mail-detail-content', // Comcast, Libero webmail
43
+ 'bloop_container', // Airmail
44
+ 'Singleton', // Apple Mail 10
45
+ 'unused', // Notes 8
46
+ 'moz-text-html', // Thunderbird
47
+ 'mail-detail-content', // Comcast, Libero webmail
48
+ 'mail-content', // Notion
47
49
  'edo', // Edison (all)
48
50
  '#msgBody', // Freenet uses #msgBody
49
- '.lang' // Fenced code blocks
51
+ 'lang', // Fenced code blocks
52
+ 'ShadowHTML', // Superhuman
53
+ 'spark', // Spark
54
+ 'at-', // Safe class names for container queries
50
55
  ],
51
56
  ])
52
57
 
@@ -9,20 +9,24 @@ import { getPosthtmlOptions } from '../posthtml/defaultConfig.js'
9
9
  const posthtmlPlugin = options => tree => {
10
10
  const defaultSafelist = [
11
11
  '*body*', // Gmail
12
- '.gmail*', // Gmail
13
- '.apple*', // Apple Mail
14
- '.ios*', // Mail on iOS
15
- '.ox-*', // Open-Xchange
16
- '.outlook*', // Outlook.com
12
+ '*gmail*', // Gmail
13
+ '*apple*', // Apple Mail
14
+ '*ios*', // Mail on iOS
15
+ '*ox-*', // Open-Xchange
16
+ '*outlook*', // Outlook.com
17
17
  '[data-ogs*', // Outlook.com
18
- '.bloop_container', // Airmail
19
- '.Singleton', // Apple Mail 10
20
- '.unused', // Notes 8
21
- '.moz-text-html', // Thunderbird
22
- '.mail-detail-content', // Comcast, Libero webmail
18
+ '*bloop_container*', // Airmail
19
+ '*Singleton*', // Apple Mail 10
20
+ '*unused', // Notes 8
21
+ '*moz-text-html*', // Thunderbird
22
+ '*mail-detail-content*', // Comcast, Libero webmail
23
+ '*mail-content-*', // Notion
23
24
  '*edo*', // Edison (all)
24
25
  '#*', // Freenet uses #msgBody
25
- '.lang*' // Fenced code blocks
26
+ '*lang*', // Fenced code blocks
27
+ '*ShadowHTML*', // Superhuman
28
+ '*spark*', // Spark
29
+ '*at-*', // Safe class names for container queries
26
30
  ]
27
31
 
28
32
  const defaultOptions = {
@@ -12,7 +12,8 @@ export default function posthtmlPlugin(options = {}) {
12
12
  options = merge({
13
13
  replacements: {
14
14
  '{': '{',
15
- '}': '}'
15
+ '}': '}',
16
+ '&': '&',
16
17
  }
17
18
  }, options)
18
19