@maizzle/framework 6.0.0-0 → 6.0.0-1

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,74 @@ 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-1] - 2025-07-11
8
+
9
+ ### Added
10
+
11
+ - types for `css.combineMediaQueries` and `css.lightningcss` options in the config
12
+
13
+ ### Changed
14
+
15
+ - made `combineMediaQueries` configurable through the `css.combineMediaQueries` option, which can be set to an object with options for `postcss-sort-media-queries` or to `false` to disable it
16
+ - made `lightningcss` configurable through the `css.lightningcss` option
17
+
18
+ ### Fixed
19
+
20
+ - fixed an issue with CSS syntax lowering, we now use `lightningcss` directly instead of `postcss-lightningcss` which was it to trip on CSS inside `style=""` attributes
21
+
22
+ ## [6.0.0-0] - 2025-07-10
23
+
24
+ This release adds initial support for Tailwind CSS v4 in Maizzle.
25
+
26
+ To jump right in, simply use the `next` branch of the Starter:
27
+
28
+ ```sh
29
+ git clone -b next https://github.com/maizzle/maizzle.git
30
+ ```
31
+
32
+ Or, if you have a project:
33
+
34
+ 1. Update `package.json`:
35
+
36
+ ```json
37
+ {
38
+ "private": true,
39
+ "type": "module",
40
+ "scripts": {
41
+ "dev": "maizzle serve",
42
+ "build": "maizzle build"
43
+ },
44
+ "dependencies": {
45
+ "@maizzle/framework": "next",
46
+ "@maizzle/tailwindcss": "latest"
47
+ }
48
+ }
49
+ ```
50
+
51
+ 2. Update the `style` tag in `layouts/main.html`:
52
+
53
+ ```html
54
+ <style>
55
+ @import "@maizzle/tailwindcss";
56
+
57
+ img {
58
+ @apply max-w-full align-middle;
59
+ }
60
+ </style>
61
+ ```
62
+
63
+ There are still things missign and/or broken:
64
+
65
+ - inline CSS like the `line-height` on spacers is broken/missing
66
+ - `tailwindcss-email-variants` and `tailwindcss-mso` are not ported yet
67
+ - some CSS duplication and artifacts still present in the final build
68
+
69
+ ---
70
+
71
+ - feat: add tailwindcss v4 support 8b5596e
72
+
73
+ https://github.com/maizzle/framework/compare/v5.2.1...v6.0.0-0
74
+
7
75
  ## [5.2.1] - 2025-06-25
8
76
 
9
77
  This is just a maintenance release to update dependencies.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maizzle/framework",
3
- "version": "6.0.0-0",
3
+ "version": "6.0.0-1",
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",
@@ -65,6 +65,7 @@
65
65
  "is-url-superb": "^6.1.0",
66
66
  "istextorbinary": "^9.5.0",
67
67
  "juice": "^11.0.0",
68
+ "lightningcss": "^1.30.1",
68
69
  "lodash-es": "^4.17.21",
69
70
  "morphdom": "^2.7.4",
70
71
  "ora": "^8.1.0",
@@ -72,7 +73,6 @@
72
73
  "postcss": "^8.4.49",
73
74
  "postcss-calc": "^10.0.2",
74
75
  "postcss-css-variables": "^0.19.0",
75
- "postcss-lightningcss": "^1.0.1",
76
76
  "postcss-safe-parser": "^7.0.0",
77
77
  "postcss-sort-media-queries": "^5.2.0",
78
78
  "posthtml": "^0.16.6",
@@ -10,6 +10,7 @@ import posthtmlPostcss from 'posthtml-postcss'
10
10
  import expandLinkTag from './plugins/expandLinkTag.js'
11
11
  import envAttributes from './plugins/envAttributes.js'
12
12
  import { getPosthtmlOptions } from './defaultConfig.js'
13
+ import lowerCssSyntax from './plugins/lowerCssSyntax.js'
13
14
  import combineMediaQueries from './plugins/combineMediaQueries.js'
14
15
 
15
16
  // PostCSS
@@ -17,7 +18,6 @@ import tailwindcss from '@tailwindcss/postcss'
17
18
  import postcssCalc from 'postcss-calc'
18
19
  import cssVariables from 'postcss-css-variables'
19
20
  import postcssSafeParser from 'postcss-safe-parser'
20
- import postcssLightningCss from 'postcss-lightningcss'
21
21
 
22
22
  import defaultComponentsConfig from './defaultComponentsConfig.js'
23
23
 
@@ -36,15 +36,6 @@ export async function process(html = '', config = {}) {
36
36
  tailwindcss(get(config, 'css.tailwind', {})),
37
37
  resolveCSSProps !== false && cssVariables(resolveCSSProps),
38
38
  resolveCalc !== false && postcssCalc(resolveCalc),
39
- postcssLightningCss({
40
- lightningcssOptions: {
41
- errorRecovery: true,
42
- minify: false,
43
- targets: {
44
- ie: 1,
45
- }
46
- }
47
- }),
48
39
  ...get(config, 'postcss.plugins', []),
49
40
  ],
50
41
  merge(
@@ -121,7 +112,8 @@ export async function process(html = '', config = {}) {
121
112
  postcssPlugin,
122
113
  envTags(config.env),
123
114
  envAttributes(config.env),
124
- combineMediaQueries(get(config, 'css.combineMediaQueries', { sort: 'mobile-first' })),
115
+ lowerCssSyntax(get(config, 'css.lightningcss', {})),
116
+ get(config, 'css.combineMediaQueries') !== false && combineMediaQueries(get(config, 'css.combineMediaQueries', { sort: 'mobile-first' })),
125
117
  ...get(
126
118
  config,
127
119
  'posthtml.plugins.after',
@@ -129,7 +121,7 @@ export async function process(html = '', config = {}) {
129
121
  ? []
130
122
  : get(config, 'posthtml.plugins', [])
131
123
  ),
132
- ])
124
+ ].filter(Boolean))
133
125
  .process(html, posthtmlOptions)
134
126
  .then(result => ({
135
127
  config: merge(config, { page: config }),
@@ -0,0 +1,43 @@
1
+ import { defu as merge } from 'defu'
2
+ import { transform } from 'lightningcss'
3
+
4
+ const plugin = (options = {}) => tree => {
5
+ options = merge(options, {
6
+ targets: options.targets ? {} : {
7
+ ie: 1,
8
+ },
9
+ })
10
+
11
+ const process = node => {
12
+ // Check if this is a style tag with content
13
+ if (node.tag === 'style' && node.content && Array.isArray(node.content)) {
14
+ // Get the CSS content from the style tag
15
+ const cssContent = node.content.join('')
16
+
17
+ if (cssContent.trim()) {
18
+ try {
19
+ const { code } = transform(
20
+ merge(
21
+ options,
22
+ {
23
+ code: Buffer.from(cssContent)
24
+ }
25
+ )
26
+ )
27
+
28
+ // Replace the content with processed CSS
29
+ node.content = [code.toString()]
30
+ } catch (error) {
31
+ // If processing fails, leave the content unchanged
32
+ console.warn('Failed to process media queries:', error.message)
33
+ }
34
+ }
35
+ }
36
+
37
+ return node
38
+ }
39
+
40
+ return tree.walk(process)
41
+ }
42
+
43
+ export default plugin
@@ -177,69 +177,71 @@ export async function inline(html = '', options = {}) {
177
177
 
178
178
  // Loop over selectors that we found in the <style> tags
179
179
  selectors.forEach(({ name, prop }) => {
180
- const elements = $(name).get()
181
-
182
- // If the property is excluded from inlining, skip
183
- if (!juice.excludedProperties.includes(prop)) {
184
- // Find the selector in the HTML
185
- elements.forEach((el) => {
186
- // Get a `property|value` list from the inline style attribute
187
- const styleAttr = $(el).attr('style')
188
- const inlineStyles = {}
189
-
190
- // 1. `preferUnitlessValues`
191
- if (styleAttr) {
192
- try {
193
- const root = postcss.parse(`* { ${styleAttr} }`)
194
-
195
- root.first.each((decl) => {
196
- const property = decl.prop
197
- let value = decl.value
198
-
199
- if (value && options.preferUnitlessValues) {
200
- value = value.replace(
201
- /\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\b/g,
202
- '0'
203
- )
204
- }
180
+ try {
181
+ const elements = $(name).get()
182
+
183
+ // If the property is excluded from inlining, skip
184
+ if (!juice.excludedProperties.includes(prop)) {
185
+ // Find the selector in the HTML
186
+ elements.forEach((el) => {
187
+ // Get a `property|value` list from the inline style attribute
188
+ const styleAttr = $(el).attr('style')
189
+ const inlineStyles = {}
190
+
191
+ // 1. `preferUnitlessValues`
192
+ if (styleAttr) {
193
+ try {
194
+ const root = postcss.parse(`* { ${styleAttr} }`)
195
+
196
+ root.first.each((decl) => {
197
+ const property = decl.prop
198
+ let value = decl.value
199
+
200
+ if (value && options.preferUnitlessValues) {
201
+ value = value.replace(
202
+ /\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\b/g,
203
+ '0'
204
+ )
205
+ }
206
+
207
+ if (property) {
208
+ inlineStyles[property] = value
209
+ }
210
+ })
211
+
212
+ // Update the element's style attribute with the new value
213
+ $(el).attr(
214
+ 'style',
215
+ Object.entries(inlineStyles).map(([property, value]) => `${property}: ${value}`).join('; ')
216
+ )
217
+ } catch {}
218
+ }
219
+
220
+ // Get the classes from the element's class attribute
221
+ const classes = $(el).attr('class')
205
222
 
206
- if (property) {
207
- inlineStyles[property] = value
223
+ // 2. `removeInlinedSelectors`
224
+ if (options.removeInlinedSelectors && classes) {
225
+ const classList = classes.split(' ')
226
+
227
+ // If the class has been inlined in the style attribute...
228
+ if (has(inlineStyles, prop)) {
229
+ // Try to remove the classes that have been inlined
230
+ if (![...options.safelist].some(item => item.includes(name))) {
231
+ remove(classList, classToRemove => name.includes(classToRemove))
208
232
  }
209
- })
210
-
211
- // Update the element's style attribute with the new value
212
- $(el).attr(
213
- 'style',
214
- Object.entries(inlineStyles).map(([property, value]) => `${property}: ${value}`).join('; ')
215
- )
216
- } catch {}
217
- }
218
-
219
- // Get the classes from the element's class attribute
220
- const classes = $(el).attr('class')
221
-
222
- // 2. `removeInlinedSelectors`
223
- if (options.removeInlinedSelectors && classes) {
224
- const classList = classes.split(' ')
225
-
226
- // If the class has been inlined in the style attribute...
227
- if (has(inlineStyles, prop)) {
228
- // Try to remove the classes that have been inlined
229
- if (![...options.safelist].some(item => item.includes(name))) {
230
- remove(classList, classToRemove => name.includes(classToRemove))
231
- }
232
233
 
233
- // Update the class list on the element with the new classes
234
- if (classList.length > 0) {
235
- $(el).attr('class', classList.join(' '))
236
- } else {
237
- $(el).removeAttr('class')
234
+ // Update the class list on the element with the new classes
235
+ if (classList.length > 0) {
236
+ $(el).attr('class', classList.join(' '))
237
+ } else {
238
+ $(el).removeAttr('class')
239
+ }
238
240
  }
239
241
  }
240
- }
241
- })
242
- }
242
+ })
243
+ }
244
+ } catch {}
243
245
  })
244
246
  })
245
247
 
package/types/config.d.ts CHANGED
@@ -14,9 +14,10 @@ import type { BaseURLConfig } from 'posthtml-base-url';
14
14
  import type URLParametersConfig from './urlParameters';
15
15
  import type { PostCssCalcOptions } from 'postcss-calc';
16
16
  import type { PostHTMLFetchConfig } from 'posthtml-fetch';
17
- import type { Config as TailwindConfig } from 'tailwindcss';
18
17
  import type { PostHTMLComponents } from 'posthtml-component';
19
18
  import type { PostHTMLExpressions } from 'posthtml-expressions';
19
+ import type { TransformOptions, CustomAtRules } from 'lightningcss';
20
+ import type { PostCSSSortMediaQueriesOptions } from './css/combineMediaQueries';
20
21
 
21
22
  export default interface Config {
22
23
  /**
@@ -217,9 +218,30 @@ export default interface Config {
217
218
  sixHex?: boolean;
218
219
 
219
220
  /**
220
- * Use a custom Tailwind CSS configuration object.
221
+ * Configure Lightning CSS.
222
+ *
223
+ * This is used mainly for lowering CSS syntax to be more compatible with email clients.
224
+ */
225
+ lightningcss?: TransformOptions<CustomAtRules>;
226
+
227
+ /**
228
+ * Combine media queries in your CSS.
229
+ *
230
+ * @default { sort: 'mobile-first' }
231
+ *
232
+ * @example
233
+ * ```
234
+ * export default {
235
+ * css: {
236
+ * combineMediaQueries: {
237
+ * sort: 'desktop-first',
238
+ * onlyTopLevel: true,
239
+ * }
240
+ * }
241
+ * }
242
+ * ```
221
243
  */
222
- tailwind?: TailwindConfig;
244
+ combineMediaQueries?: PostCSSSortMediaQueriesOptions;
223
245
  }
224
246
 
225
247
  /**
@@ -0,0 +1,90 @@
1
+ import type { Plugin } from 'postcss';
2
+
3
+ /**
4
+ * Sort function for media queries.
5
+ * Takes two media query strings and returns a number indicating their relative order.
6
+ */
7
+ export type SortFunction = (a: string, b: string) => number;
8
+
9
+ /**
10
+ * Configuration object for sort-css-media-queries library.
11
+ */
12
+ export interface SortConfiguration {
13
+ [key: string]: any;
14
+ }
15
+
16
+ /**
17
+ * Options for the postcss-sort-media-queries plugin.
18
+ */
19
+ export interface PostCSSSortMediaQueriesOptions {
20
+ /**
21
+ * Sorting method for media queries.
22
+ *
23
+ * @default 'mobile-first'
24
+ *
25
+ * @example
26
+ * ```
27
+ * // Use built-in mobile-first sorting
28
+ * sort: 'mobile-first'
29
+ *
30
+ * // Use built-in desktop-first sorting
31
+ * sort: 'desktop-first'
32
+ *
33
+ * // Use custom sorting function
34
+ * sort: (a, b) => a.localeCompare(b)
35
+ * ```
36
+ */
37
+ sort?: 'mobile-first' | 'desktop-first' | SortFunction;
38
+
39
+ /**
40
+ * Custom configuration object for the sort-css-media-queries library.
41
+ * When provided, it will be used to create a custom sort function.
42
+ *
43
+ * @default false
44
+ *
45
+ * @example
46
+ * ```
47
+ * configuration: {
48
+ * unitlessMqAlwaysFirst: true,
49
+ * sort: 'mobile-first'
50
+ * }
51
+ * ```
52
+ */
53
+ configuration?: false | SortConfiguration;
54
+
55
+ /**
56
+ * Whether to only process media queries at the top level (direct children of root).
57
+ * When true, nested media queries will be ignored.
58
+ *
59
+ * @default false
60
+ *
61
+ * @example
62
+ * ```
63
+ * onlyTopLevel: true
64
+ * ```
65
+ */
66
+ onlyTopLevel?: boolean;
67
+ }
68
+
69
+ /**
70
+ * PostCSS plugin that sorts CSS media queries.
71
+ *
72
+ * @param options - Plugin configuration options
73
+ * @returns PostCSS plugin instance
74
+ *
75
+ * @example
76
+ * ```
77
+ * import postcss from 'postcss';
78
+ * import sortMediaQueries from 'postcss-sort-media-queries';
79
+ *
80
+ * postcss([
81
+ * sortMediaQueries({
82
+ * sort: 'mobile-first',
83
+ * onlyTopLevel: false
84
+ * })
85
+ * ])
86
+ * ```
87
+ */
88
+ declare function postcssSortMediaQueries(options?: PostCSSSortMediaQueriesOptions): Plugin;
89
+
90
+ export default postcssSortMediaQueries;