@maizzle/framework 6.0.0-7 → 6.0.0-9

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,26 @@ 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-9] - 2025-07-16
8
+
9
+ ### Changed
10
+
11
+ - refactor: resolving css props acacc14
12
+
13
+ ## [6.0.0-8] - 2025-07-16
14
+
15
+ ### Changed
16
+
17
+ - refactor: css inlining d443c31
18
+
19
+ ## [6.0.0-7] - 2025-07-15
20
+
21
+ 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`.
22
+
23
+ ### Fixed
24
+
25
+ - fix: ensure content paths are unique to each build environment 46c292b
26
+
7
27
  ## [6.0.0-6] - 2025-07-14
8
28
 
9
29
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maizzle/framework",
3
- "version": "6.0.0-7",
3
+ "version": "6.0.0-9",
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",
@@ -71,8 +71,7 @@
71
71
  "ora": "^8.1.0",
72
72
  "pathe": "^2.0.0",
73
73
  "postcss": "^8.4.49",
74
- "postcss-calc": "^10.0.2",
75
- "postcss-css-variables": "^0.19.0",
74
+ "postcss-custom-properties": "^14.0.6",
76
75
  "postcss-safe-parser": "^7.0.0",
77
76
  "postcss-sort-media-queries": "^5.2.0",
78
77
  "posthtml": "^0.16.6",
@@ -4,7 +4,7 @@ import sortMediaQueries from 'postcss-sort-media-queries'
4
4
  const plugin = (options = {}) => tree => {
5
5
  const process = node => {
6
6
  // Check if this is a style tag with content
7
- if (node.tag === 'style' && node.content && Array.isArray(node.content)) {
7
+ if (node && node.tag === 'style' && node.content && Array.isArray(node.content)) {
8
8
  // Get the CSS content from the style tag
9
9
  const cssContent = node.content.join('')
10
10
 
@@ -1,13 +1,10 @@
1
1
  import postcss from 'postcss'
2
2
  import get from 'lodash-es/get.js'
3
3
  import { defu as merge } from 'defu'
4
- import postcssCalc from 'postcss-calc'
5
4
  import { transform } from 'lightningcss'
6
5
  import tailwindcss from '@tailwindcss/postcss'
7
- import cssVariables from 'postcss-css-variables'
8
6
  import postcssSafeParser from 'postcss-safe-parser'
9
- import removeDuplicateSelectors from './removeDuplicateSelectors.js'
10
- import cleanupTailwindArtifacts from './cleanupTailwindArtifacts.js'
7
+ import customProperties from 'postcss-custom-properties'
11
8
 
12
9
  const attributes = new Set([
13
10
  'raw',
@@ -24,8 +21,8 @@ export const validAttributeNames = attributes
24
21
  * PostHTML plugin to process Tailwind CSS within style tags.
25
22
  *
26
23
  * This plugin processes CSS content in `<style>` tags and
27
- * compiles it with PostCSS. `<style>` tags marked as
28
- * `no-process` will be skipped.
24
+ * compiles it with PostCSS. Tags marked with attribute
25
+ * names found in `attributes` will be skipped.
29
26
  */
30
27
  export function compileCss(config = {}) {
31
28
  return tree => {
@@ -33,7 +30,7 @@ export function compileCss(config = {}) {
33
30
  const stylePromises = []
34
31
 
35
32
  tree.walk(node => {
36
- if (node.tag === 'style' && node.content) {
33
+ if (node && node.tag === 'style' && node.content) {
37
34
  if (node.attrs && Object.keys(node.attrs).some(attr => attributes.has(attr))) {
38
35
  return node
39
36
  }
@@ -69,31 +66,26 @@ async function processCss(css, config) {
69
66
  * will apply to all `<style>` tags in the HTML,
70
67
  * unless marked to be excluded.
71
68
  */
72
- const resolveCSSProps = get(config, 'css.resolveProps')
73
- const resolveCalc = get(config, 'css.resolveCalc') !== false
74
- ? get(config, 'css.resolveCalc', { precision: 2 })
75
- : false
76
-
77
- const lightningCssOptions = merge(
78
- get(config, 'css.lightning', {}),
79
- {
80
- targets: {
81
- ie: 1,
82
- },
83
- }
84
- )
69
+ const resolveCSSProps = merge(get(config, 'css.resolveProps', {}), {preseve: false})
70
+
71
+ let lightningCssOptions = get(config, 'css.lightning')
72
+ if (lightningCssOptions !== false) {
73
+ lightningCssOptions = merge(
74
+ lightningCssOptions,
75
+ {
76
+ targets: {
77
+ ie: 1,
78
+ },
79
+ }
80
+ )
81
+ }
85
82
 
86
83
  try {
87
- const processor = postcss([
84
+ const result = await postcss([
88
85
  tailwindcss(get(config, 'css.tailwind', {})),
89
- resolveCSSProps !== false && cssVariables(resolveCSSProps),
90
- resolveCalc !== false && postcssCalc(resolveCalc),
91
- removeDuplicateSelectors(),
92
- cleanupTailwindArtifacts(get(config, 'css.cleanup', {})),
86
+ customProperties(resolveCSSProps),
93
87
  ...get(config, 'postcss.plugins', []),
94
- ].filter(Boolean))
95
-
96
- const result = await processor.process(css, merge(
88
+ ]).process(css, merge(
97
89
  get(config, 'postcss.options', {}),
98
90
  {
99
91
  from: config.cwd || './',
@@ -108,7 +100,7 @@ async function processCss(css, config) {
108
100
  * to be more email-friendly.
109
101
  */
110
102
 
111
- if (result.css?.trim()) {
103
+ if (result.css?.trim() && lightningCssOptions !== false) {
112
104
  try {
113
105
  const { code } = transform(
114
106
  merge(
@@ -1,8 +1,16 @@
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
- if (node.tag === 'style') {
13
+ if (node && node.tag === 'style') {
6
14
  if (node.attrs && Object.keys(node.attrs).some(attr => validAttributeNames.has(attr))) {
7
15
  // Remove the attribute
8
16
  for (const attr of Object.keys(node.attrs)) {
@@ -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
- const posthtmlPlugin = (options = {}) => tree => {
16
- return inline(render(tree), options).then(html => parse(html, getPosthtmlOptions()))
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
- export default posthtmlPlugin
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
- $.root().html(
93
- css
94
- ? juice.inlineContent($.html(), css, { removeStyleTags, ...options })
95
- : juice($.html(), { removeStyleTags, ...options })
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
- $.root().html(
108
- await useAttributeSizes(html, {
109
- width: juice.widthElements,
110
- height: juice.heightElements,
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
- * Remove inlined selectors from the HTML
117
- */
118
- // For each style tag
119
- $('style:not([embed])').each((_i, el) => {
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
- $(el).html(),
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
- const selectors = new Set()
138
-
139
- // Preserve selectors in at rules
161
+ // Preserve selectors in predefined at-rules
140
162
  root.walkAtRules(rule => {
141
- if (['media', 'supports'].includes(rule.name)) {
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
- $(el).html(root.toString())
198
+ node.content = root.toString()
173
199
  }
174
200
  })
175
201
 
176
- /**
177
- * CSS optimizations
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
- // Get the classes from the element's class attribute
226
- const classes = $(el).attr('class')
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
- // 2. `removeInlinedSelectors`
229
- if (options.removeInlinedSelectors && classes) {
230
- const classList = classes.split(' ')
213
+ const $ = cheerio.load(render(inlined_tree), {
214
+ xml: {
215
+ decodeEntities: false,
216
+ xmlMode: false,
217
+ }
218
+ })
231
219
 
232
- // If the class has been inlined in the style attribute...
233
- if (has(inlineStyles, prop)) {
234
- // Try to remove the classes that have been inlined
235
- if (![...options.safelist].some(item => item.includes(name))) {
236
- remove(classList, classToRemove => name.includes(classToRemove))
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
- // Update the class list on the element with the new classes
240
- if (classList.length > 0) {
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
- } catch {}
250
- })
283
+ }
284
+ })
285
+ }
286
+ } catch { }
251
287
  })
252
288
 
253
- $('style[embed]').each((_i, el) => {
254
- $(el).removeAttr('embed')
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 $.html()
304
+ // Finally, return the inlined html
305
+ return render(optimized_tree)
258
306
  }
@@ -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