@maizzle/framework 5.0.9 → 5.2.0

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,12 @@ 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
+ ## [5.1.0] - 2025-06-13
8
+
9
+ This release adds support for using Tailwind CSS color opacity modifiers in Gmail by automatically converting modern rgb/a syntax to the legacy, comma-separated syntax.
10
+
11
+ - feat: use commas instead of slash for css color opacity modifiers f26774b
12
+
7
13
  ## [5.0.9] - 2025-06-12
8
14
 
9
15
  This release fixes an issue where builds failed because `preferUnitlessValues` (which is on by default) was throwing an error when encountering invalid inline CSS.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maizzle/framework",
3
- "version": "5.0.9",
3
+ "version": "5.2.0",
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",
@@ -70,6 +70,7 @@
70
70
  "pathe": "^2.0.0",
71
71
  "postcss": "^8.4.49",
72
72
  "postcss-calc": "^10.0.2",
73
+ "postcss-color-functional-notation": "^7.0.10",
73
74
  "postcss-css-variables": "^0.19.0",
74
75
  "postcss-import": "^16.1.0",
75
76
  "postcss-safe-parser": "^7.0.0",
@@ -96,7 +97,7 @@
96
97
  "ws": "^8.18.0"
97
98
  },
98
99
  "devDependencies": {
99
- "@biomejs/biome": "1.9.2",
100
+ "@biomejs/biome": "2.0.5",
100
101
  "@types/js-beautify": "^1.14.3",
101
102
  "@types/markdown-it": "^14.1.2",
102
103
  "@vitest/coverage-v8": "^3.0.4",
@@ -298,14 +298,19 @@ export default async (config = {}) => {
298
298
 
299
299
  /**
300
300
  * Copy static files
301
- *
302
- * TODO: support an array of objects with source and destination,
303
- * i.e. static: [{ source: 'src/assets', destination: 'assets' }, ...]
304
301
  */
305
- const staticSourcePaths = getRootDirectories([...new Set(get(config, 'build.static.source', []))])
302
+ let staticFiles = get(config, 'build.static', [])
303
+
304
+ if (!Array.isArray(staticFiles)) {
305
+ staticFiles = [staticFiles]
306
+ }
306
307
 
307
- for await (const rootDir of staticSourcePaths) {
308
- await cp(rootDir, path.join(buildOutputPath, get(config, 'build.static.destination')), { recursive: true })
308
+ for (const definition of staticFiles) {
309
+ const staticSourcePaths = getRootDirectories([...new Set(definition.source)])
310
+
311
+ for await (const rootDir of staticSourcePaths) {
312
+ await cp(rootDir, path.join(buildOutputPath, definition.destination), { recursive: true })
313
+ }
309
314
  }
310
315
 
311
316
  const allOutputFiles = await fg.glob(path.join(buildOutputPath, '**/*'))
@@ -17,6 +17,7 @@ import postcssCalc from 'postcss-calc'
17
17
  import postcssImport from 'postcss-import'
18
18
  import cssVariables from 'postcss-css-variables'
19
19
  import postcssSafeParser from 'postcss-safe-parser'
20
+ import postcssColorFunctionalNotation from 'postcss-color-functional-notation'
20
21
 
21
22
  import defaultComponentsConfig from './defaultComponentsConfig.js'
22
23
 
@@ -36,6 +37,7 @@ export async function process(html = '', config = {}) {
36
37
  tailwindcss(get(config, 'css.tailwind', {})),
37
38
  resolveCSSProps !== false && cssVariables(resolveCSSProps),
38
39
  resolveCalc !== false && postcssCalc(resolveCalc),
40
+ postcssColorFunctionalNotation(),
39
41
  ...get(config, 'postcss.plugins', []),
40
42
  ],
41
43
  merge(
@@ -104,11 +106,11 @@ export async function process(html = '', config = {}) {
104
106
  ...beforePlugins,
105
107
  envTags(config.env),
106
108
  envAttributes(config.env),
107
- expandLinkTag,
109
+ expandLinkTag(),
108
110
  postcssPlugin,
109
111
  fetchPlugin,
110
112
  components(componentsConfig),
111
- expandLinkTag,
113
+ expandLinkTag(),
112
114
  postcssPlugin,
113
115
  envTags(config.env),
114
116
  envAttributes(config.env),
@@ -1,37 +1,59 @@
1
- import fs from 'node:fs'
2
-
3
1
  const targets = new Set(['expand', 'inline'])
4
2
 
5
- // TODO: refactor to a Promise so we can use async readFile
6
- const plugin = (() => tree => {
7
- const process = node => {
8
- /**
9
- * Don't expand link tags that are not explicitly marked as such
10
- */
11
- if (node?.attrs && ![...targets].some(attr => attr in node.attrs)) {
12
- for (const attr of targets) {
13
- node.attrs[attr] = false
3
+ const expandLinkPlugin = () => tree => {
4
+ return new Promise((resolve, reject) => {
5
+ const isNode = typeof process !== 'undefined' && process.versions?.node
6
+
7
+ const loadFile = async href => {
8
+ if (isNode) {
9
+ const { readFile } = await import('node:fs/promises')
10
+ return await readFile(href, 'utf8')
14
11
  }
15
12
 
16
- return node
17
- }
13
+ const res = await fetch(href)
14
+
15
+ if (!res.ok) {
16
+ throw new Error(`Failed to fetch ${href}: ${res.statusText}`)
17
+ }
18
18
 
19
- if (
20
- node
21
- && node.tag === 'link'
22
- && node.attrs
23
- && node.attrs.href
24
- && node.attrs.rel === 'stylesheet'
25
- ) {
26
- node.content = [fs.readFileSync(node.attrs.href, 'utf8')]
27
- node.tag = 'style'
28
- node.attrs = {}
19
+ return await res.text()
29
20
  }
30
21
 
31
- return node
32
- }
22
+ const promises = []
33
23
 
34
- return tree.walk(process)
35
- })()
24
+ try {
25
+ tree.walk(node => {
26
+ if (node?.attrs && ![...targets].some(attr => attr in node.attrs)) {
27
+ for (const attr of targets) {
28
+ node.attrs[attr] = false
29
+ }
30
+ return node
31
+ }
32
+
33
+ if (
34
+ node?.tag === 'link' &&
35
+ node.attrs?.href &&
36
+ node.attrs.rel === 'stylesheet'
37
+ ) {
38
+ const promise = loadFile(node.attrs.href).then(content => {
39
+ node.tag = 'style'
40
+ node.attrs = {}
41
+ node.content = [content]
42
+ })
43
+
44
+ promises.push(promise)
45
+ }
46
+
47
+ return node
48
+ })
49
+
50
+ Promise.all(promises)
51
+ .then(() => resolve(tree))
52
+ .catch(reject)
53
+ } catch (err) {
54
+ reject(err)
55
+ }
56
+ })
57
+ }
36
58
 
37
- export default plugin
59
+ export default expandLinkPlugin
@@ -138,7 +138,7 @@ function connectWebSocket() {
138
138
  if (styleAttribute) {
139
139
  const urlPattern = /(url\(["']?)(.*?)(["']?\))/g
140
140
  // Replace URLs in style attribute with cache-busting parameter
141
- const updatedStyleAttribute = styleAttribute.replace(urlPattern, (match, p1, p2, p3) => {
141
+ const updatedStyleAttribute = styleAttribute.replace(urlPattern, (_match, p1, p2, p3) => {
142
142
  // Update the value of 'v' parameter if it already exists
143
143
  if (p2.includes('?')) {
144
144
  return `${p1}${p2.replace(/([?&])v=[^&]*/, `$1v=${randomNumber}`)}${p3}`
@@ -172,7 +172,7 @@ function connectWebSocket() {
172
172
  })
173
173
 
174
174
  // Handle connection opened
175
- socket.addEventListener('open', event => {
175
+ socket.addEventListener('open', _event => {
176
176
  console.log('WebSocket connection opened')
177
177
  })
178
178
 
@@ -53,7 +53,7 @@ async function getTemplatePaths(templateFolders) {
53
53
  return await fg.glob([...new Set(templateFolders)])
54
54
  }
55
55
 
56
- async function getUpdatedRoutes(app, config) {
56
+ async function getUpdatedRoutes(_app, config) {
57
57
  return getTemplatePaths(getTemplateFolders(config))
58
58
  }
59
59
 
@@ -252,6 +252,14 @@ export default async (config = {}) => {
252
252
  }
253
253
  })
254
254
 
255
+ let staticFiles = get(config, 'build.static', [])
256
+
257
+ if (!Array.isArray(staticFiles)) {
258
+ staticFiles = [staticFiles]
259
+ }
260
+
261
+ const staticFilesSourcePaths = staticFiles.flatMap((definition) => definition.source)
262
+
255
263
  /**
256
264
  * Global watcher
257
265
  *
@@ -263,7 +271,7 @@ export default async (config = {}) => {
263
271
  'maizzle.config*.{js,cjs,ts}',
264
272
  'tailwind*.config.{js,ts}',
265
273
  '**/*.css',
266
- ...get(config, 'build.static.source', []),
274
+ ...staticFilesSourcePaths,
267
275
  ...get(config, 'server.watch', []),
268
276
  ])
269
277
 
@@ -365,12 +373,12 @@ export default async (config = {}) => {
365
373
  const srcFoldersList = await fg.glob(
366
374
  [
367
375
  '**/*/',
368
- ...get(config, 'build.static.source', [])
376
+ ...staticFilesSourcePaths,
369
377
  ], {
370
378
  onlyFiles: false,
371
379
  ignore: [
372
380
  'node_modules',
373
- get(config, 'build.output.path', 'build_*'),
381
+ `${get(config, 'build.output.path', 'build_*')}/**`,
374
382
  ]
375
383
  })
376
384
 
@@ -8,7 +8,7 @@ const router = express.Router()
8
8
  const require = createRequire(import.meta.url)
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url))
10
10
 
11
- router.get('/hmr.js', async (req, res) => {
11
+ router.get('/hmr.js', async (_req, res) => {
12
12
  try {
13
13
  const morphdomPath = require.resolve('morphdom/dist/morphdom-umd.js')
14
14
  const morphdomScript = await fs.readFile(morphdomPath, 'utf8')
@@ -12,6 +12,7 @@ function groupFilesByDirectories(globs, files) {
12
12
  let current = {}
13
13
 
14
14
  globs.forEach(glob => {
15
+ // biome-ignore lint: needs to be escaped
15
16
  const rootPath = glob.split(/[\*\!\{\}]/)[0].replace(/\/+$/, '')
16
17
 
17
18
  files.forEach(file => {
@@ -71,10 +71,10 @@ export async function inline(html = '', options = {}) {
71
71
  })
72
72
 
73
73
  // Add a `data-embed` attribute to style tags that have the embed attribute
74
- $('style[embed]:not([data-embed])').each((i, el) => {
74
+ $('style[embed]:not([data-embed])').each((_i, el) => {
75
75
  $(el).attr('data-embed', '')
76
76
  })
77
- $('style[data-embed]:not([embed])').each((i, el) => {
77
+ $('style[data-embed]:not([embed])').each((_i, el) => {
78
78
  $(el).attr('embed', '')
79
79
  })
80
80
 
@@ -111,7 +111,7 @@ export async function inline(html = '', options = {}) {
111
111
  * Remove inlined selectors from the HTML
112
112
  */
113
113
  // For each style tag
114
- $('style:not([embed])').each((i, el) => {
114
+ $('style:not([embed])').each((_i, el) => {
115
115
  // Parse the CSS
116
116
  const { root } = postcss()
117
117
  .process(
@@ -243,7 +243,7 @@ export async function inline(html = '', options = {}) {
243
243
  })
244
244
  })
245
245
 
246
- $('style[embed]').each((i, el) => {
246
+ $('style[embed]').each((_i, el) => {
247
247
  $(el).removeAttr('embed')
248
248
  })
249
249
 
@@ -73,7 +73,7 @@ export async function readFileConfig(config) {
73
73
  baseConfig = merge(baseConfigFile, baseConfig)
74
74
  break
75
75
  }
76
- } catch (error) {
76
+ } catch (_error) {
77
77
  break
78
78
  }
79
79
 
@@ -107,7 +107,7 @@ export async function readFileConfig(config) {
107
107
  loaded = true
108
108
  break
109
109
  }
110
- } catch (error) {
110
+ } catch (_error) {
111
111
  break
112
112
  }
113
113
  }
@@ -128,7 +128,7 @@ export async function readFileConfig(config) {
128
128
  }
129
129
 
130
130
  return merge(envConfig, baseConfig)
131
- } catch (error) {
131
+ } catch (_error) {
132
132
  throw new Error('Could not compute config')
133
133
  }
134
134
  }
@@ -153,6 +153,7 @@ export function getRootDirectories(patterns = []) {
153
153
  * @returns
154
154
  */
155
155
  export function getFileExtensionsFromPattern(pattern) {
156
+ // biome-ignore lint: needs to be escaped
156
157
  const starExtPattern = /\.([^\*\{\}]+)$/ // Matches .ext but not .* or .{ext}
157
158
  const bracePattern = /\.{([^}]+)}$/ // Matches .{ext} or .{ext,ext}
158
159
  const wildcardPattern = /\.\*$/ // Matches .*
package/types/build.d.ts CHANGED
@@ -99,7 +99,10 @@ export default interface BuildConfig {
99
99
  * @default undefined
100
100
  */
101
101
  destination?: string;
102
- };
102
+ } | Array<{
103
+ source?: string[];
104
+ destination?: string;
105
+ }>
103
106
 
104
107
  /**
105
108
  * Type of spinner to show in the console.