@maizzle/framework 4.4.0-beta.8 → 4.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maizzle/framework",
3
- "version": "4.4.0-beta.8",
3
+ "version": "4.4.0",
4
4
  "description": "Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.",
5
5
  "license": "MIT",
6
6
  "main": "src/index.js",
@@ -41,8 +41,8 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "@maizzle/cli": "^1.5.1",
44
- "autoprefixer": "^10.4.13",
45
- "browser-sync": "^2.27.11",
44
+ "autoprefixer": "^10.4.14",
45
+ "browser-sync": "^2.28.3",
46
46
  "color-shorthand-hex-to-six-digit": "^3.0.2",
47
47
  "email-comb": "^5.2.0",
48
48
  "front-matter": "^4.0.0",
@@ -50,7 +50,7 @@
50
50
  "glob-promise": "^4.1.0",
51
51
  "html-crush": "^4.0.0",
52
52
  "is-url-superb": "^5.0.0",
53
- "juice": "^8.0.0",
53
+ "juice": "^9.0.0",
54
54
  "lodash": "^4.17.20",
55
55
  "ora": "^5.1.0",
56
56
  "postcss": "^8.4.21",
@@ -58,27 +58,27 @@
58
58
  "postcss-merge-longhand": "^5.1.7",
59
59
  "posthtml": "^0.16.6",
60
60
  "posthtml-attrs-parser": "^0.1.1",
61
- "posthtml-base-url": "^1.0.1",
62
- "posthtml-component": "^1.0.0-beta.16",
61
+ "posthtml-base-url": "^2.0.0",
62
+ "posthtml-component": "^1.1.0",
63
63
  "posthtml-content": "^0.1.0",
64
64
  "posthtml-extend": "^0.6.0",
65
- "posthtml-extra-attributes": "^1.0.0",
66
- "posthtml-fetch": "^2.2.0",
65
+ "posthtml-extra-attributes": "^2.0.0",
66
+ "posthtml-fetch": "^3.0.0",
67
67
  "posthtml-markdownit": "^1.3.1",
68
68
  "posthtml-match-helper": "^1.0.3",
69
- "posthtml-mso": "^1.0.4",
70
- "posthtml-postcss-merge-longhand": "^1.0.2",
71
- "posthtml-safe-class-names": "^2.0.0",
72
- "posthtml-url-parameters": "^1.0.4",
69
+ "posthtml-mso": "^2.0.0",
70
+ "posthtml-postcss-merge-longhand": "^2.0.1",
71
+ "posthtml-safe-class-names": "^3.0.0",
72
+ "posthtml-url-parameters": "^2.0.0",
73
73
  "pretty": "^2.0.0",
74
74
  "query-string": "^7.1.3",
75
75
  "string-remove-widows": "^2.1.0",
76
76
  "string-strip-html": "^8.2.0",
77
- "tailwindcss": "^3.2.4"
77
+ "tailwindcss": "^3.2.7"
78
78
  },
79
79
  "devDependencies": {
80
- "ava": "^5.1.1",
81
- "c8": "^7.11.0",
80
+ "ava": "^5.2.0",
81
+ "c8": "^7.13.0",
82
82
  "np": "*",
83
83
  "xo": "0.39.1"
84
84
  },
@@ -36,132 +36,154 @@ const serve = async (env = 'local', config = {}) => {
36
36
 
37
37
  const spinner = ora()
38
38
 
39
- try {
40
- await buildToFile(env, config)
41
-
42
- let templates = get(config, 'build.templates')
43
- templates = Array.isArray(templates) ? templates : [templates]
44
-
45
- const templatePaths = [...new Set(templates.map(config => `${get(config, 'source', 'src')}/**`))]
46
- const tailwindConfig = get(config, 'build.tailwind.config', 'tailwind.config.js')
47
- const globalPaths = [
48
- 'src/**',
49
- ...new Set(get(config, 'build.browsersync.watch', []))
50
- ]
51
-
52
- if (typeof tailwindConfig === 'string') {
53
- globalPaths.push(tailwindConfig)
54
- }
55
-
56
- // Watch for Template file changes
57
- browsersync()
58
- .watch(templatePaths)
59
- .on('change', async file => {
60
- config = await getConfig(env, config)
61
-
62
- if (config.events && typeof config.events.beforeCreate === 'function') {
63
- await config.events.beforeCreate(config)
64
- }
65
-
66
- // Don't render if file type is not configured
67
- // eslint-disable-next-line
68
- const filetypes = templates.reduce((acc, template) => {
69
- return [...acc, ...get(template, 'filetypes', ['html'])]
70
- }, [])
71
-
72
- if (!filetypes.includes(path.extname(file).slice(1))) {
73
- return
74
- }
75
-
76
- if (get(config, 'build.console.clear')) {
77
- clearConsole()
78
- }
79
-
80
- const start = new Date()
81
-
82
- spinner.start('Building email...')
83
-
84
- file = file.replace(/\\/g, '/')
39
+ // Build all emails first
40
+ await buildToFile(env, config)
41
+
42
+ // Set some paths to watch
43
+ let templates = get(config, 'build.templates')
44
+ templates = Array.isArray(templates) ? templates : [templates]
45
+
46
+ const templatePaths = [...new Set(templates.map(config => `${get(config, 'source', 'src')}/**`))]
47
+ const tailwindConfig = get(config, 'build.tailwind.config', 'tailwind.config.js')
48
+ const globalPaths = [
49
+ 'src/**',
50
+ ...new Set(get(config, 'build.browsersync.watch', []))
51
+ ]
52
+
53
+ if (typeof tailwindConfig === 'string') {
54
+ globalPaths.push(tailwindConfig)
55
+ }
85
56
 
86
- const renderOptions = {
87
- maizzle: config,
57
+ // Watch for Template file changes
58
+ browsersync()
59
+ .watch(templatePaths)
60
+ .on('change', async file => {
61
+ config = await getConfig(env, config)
62
+
63
+ if (config.events && typeof config.events.beforeCreate === 'function') {
64
+ await config.events.beforeCreate(config)
65
+ }
66
+
67
+ // Don't render if file type is not configured
68
+ // eslint-disable-next-line
69
+ const filetypes = templates.reduce((acc, template) => {
70
+ return [...acc, ...get(template, 'filetypes', ['html'])]
71
+ }, [])
72
+
73
+ if (!filetypes.includes(path.extname(file).slice(1))) {
74
+ return
75
+ }
76
+
77
+ // Clear console if enabled
78
+ if (get(config, 'build.console.clear')) {
79
+ clearConsole()
80
+ }
81
+
82
+ // Start the spinner
83
+ const start = new Date()
84
+ spinner.start('Building email...')
85
+
86
+ // Render the template
87
+ renderToString(
88
+ await fs.readFile(file.replace(/\\/g, '/'), 'utf8'),
89
+ {
90
+ maizzle: merge(
91
+ config,
92
+ {
93
+ build: {
94
+ current: {
95
+ path: path.parse(file)
96
+ }
97
+ }
98
+ }
99
+ ),
88
100
  ...config.events
89
101
  }
90
-
91
- renderToString(
92
- await fs.readFile(file, 'utf8'),
93
- renderOptions
94
- )
95
- .then(async ({html, config}) => {
96
- let source = ''
97
- let dest = ''
98
- let ext = ''
99
-
100
- if (Array.isArray(config.build.templates)) {
101
- const match = config.build.templates.find(template => template.source === path.parse(file).dir)
102
- source = get(match, 'source')
103
- dest = get(match, 'destination.path', 'build_local')
104
- ext = get(match, 'destination.ext', 'html')
105
- } else if (isObject(config.build.templates)) {
106
- source = get(config, 'build.templates.source')
107
- dest = get(config, 'build.templates.destination.path', 'build_local')
108
- ext = get(config, 'build.templates.destination.ext', 'html')
109
- }
110
-
111
- const fileDir = path.parse(file).dir.replace(source, '')
112
- const finalDestination = path.join(dest, fileDir, `${path.parse(file).name}.${ext}`)
113
-
114
- await fs.outputFile(config.permalink || finalDestination, html)
115
- })
116
- .then(() => {
117
- browsersync().reload()
118
- spinner.succeed(`Compiled in ${(Date.now() - start) / 1000}s [${file}]`)
119
- })
120
- .catch(() => spinner.warn(`Received empty HTML, please save your file again [${file}]`))
102
+ )
103
+ .then(async ({html, config}) => {
104
+ // Write the file to disk
105
+ let source = ''
106
+ let dest = ''
107
+ let ext = ''
108
+
109
+ if (Array.isArray(config.build.templates)) {
110
+ const match = config.build.templates.find(template => template.source === path.parse(file).dir)
111
+ source = path.normalize(get(match, 'source'))
112
+ dest = path.normalize(get(match, 'destination.path', 'build_local'))
113
+ ext = get(match, 'destination.ext', 'html')
114
+ } else if (isObject(config.build.templates)) {
115
+ source = path.normalize(get(config, 'build.templates.source'))
116
+ dest = path.normalize(get(config, 'build.templates.destination.path', 'build_local'))
117
+ ext = get(config, 'build.templates.destination.ext', 'html')
118
+ }
119
+
120
+ const fileDir = path.parse(file).dir.replace(source, '')
121
+ const finalDestination = path.join(dest, fileDir, `${path.parse(file).name}.${ext}`)
122
+
123
+ await fs.outputFile(config.permalink || finalDestination, html)
124
+ })
125
+ .then(() => {
126
+ browsersync().reload()
127
+ spinner.succeed(`Compiled in ${(Date.now() - start) / 1000}s [${file}]`)
128
+ })
129
+ .catch(error => {
130
+ throw error
131
+ })
132
+ })
133
+
134
+ // Watch for changes in all other files
135
+ browsersync()
136
+ .watch(globalPaths, {ignored: templatePaths})
137
+ .on('change', () => buildToFile(env, config)
138
+ .then(() => browsersync().reload())
139
+ .catch(error => {
140
+ throw error
121
141
  })
122
-
123
- // Watch for changes in all other files
124
- browsersync()
125
- .watch(globalPaths, {ignored: templatePaths})
126
- .on('change', () => buildToFile(env, config).then(() => browsersync().reload()))
127
- .on('unlink', () => buildToFile(env, config).then(() => browsersync().reload()))
128
-
129
- // Watch for changes in config files
130
- browsersync()
131
- .watch('config*.js')
132
- .on('change', async file => {
133
- const parsedEnv = path.parse(file).name.split('.')[1] || 'local'
134
-
135
- Config
136
- .getMerged(parsedEnv)
137
- .then(config => buildToFile(parsedEnv, config).then(() => browsersync().reload()))
142
+ )
143
+ .on('unlink', () => buildToFile(env, config)
144
+ .then(() => browsersync().reload())
145
+ .catch(error => {
146
+ throw error
138
147
  })
139
-
140
- // Browsersync options
141
- const baseDir = templates.map(t => t.destination.path)
142
-
143
- // Initialize Browsersync
144
- browsersync()
145
- .init(
146
- merge(
147
- {
148
- notify: false,
149
- open: false,
150
- port: 3000,
151
- server: {
152
- baseDir,
153
- directory: true
154
- },
155
- tunnel: false,
156
- ui: {port: 3001},
157
- logFileChanges: false
148
+ )
149
+
150
+ // Watch for changes in config files
151
+ browsersync()
152
+ .watch('config*.js')
153
+ .on('change', async file => {
154
+ const parsedEnv = path.parse(file).name.split('.')[1] || 'local'
155
+
156
+ Config
157
+ .getMerged(parsedEnv)
158
+ .then(config => buildToFile(parsedEnv, config)
159
+ .then(() => browsersync().reload())
160
+ .catch(error => {
161
+ throw error
162
+ })
163
+ )
164
+ })
165
+
166
+ // Browsersync options
167
+ const baseDir = templates.map(t => t.destination.path)
168
+
169
+ // Initialize Browsersync
170
+ browsersync()
171
+ .init(
172
+ merge(
173
+ {
174
+ notify: false,
175
+ open: false,
176
+ port: 3000,
177
+ server: {
178
+ baseDir,
179
+ directory: true
158
180
  },
159
- get(config, 'build.browsersync', {})
160
- ), () => {})
161
- } catch (error) {
162
- spinner.fail(error)
163
- throw error
164
- }
181
+ tunnel: false,
182
+ ui: {port: 3001},
183
+ logFileChanges: false
184
+ },
185
+ get(config, 'build.browsersync', {})
186
+ ), () => {})
165
187
  }
166
188
 
167
189
  module.exports = serve
@@ -2,7 +2,6 @@ const path = require('path')
2
2
  const fs = require('fs-extra')
3
3
  const glob = require('glob-promise')
4
4
  const {get, isEmpty, merge} = require('lodash')
5
- const {asyncForEach} = require('../../utils/helpers')
6
5
 
7
6
  const Config = require('../config')
8
7
  const Tailwind = require('../tailwindcss')
@@ -31,7 +30,7 @@ module.exports = async (env, spinner, config) => {
31
30
  : await Tailwind.compile({config})
32
31
 
33
32
  // Parse each template config object
34
- await asyncForEach(templatesConfig, async templateConfig => {
33
+ for await (const templateConfig of templatesConfig) {
35
34
  if (!templateConfig) {
36
35
  const configFileName = env === 'local' ? 'config.js' : `config.${env}.js`
37
36
  throw new Error(`No template sources defined in \`build.templates\`, check your ${configFileName} file`)
@@ -79,18 +78,20 @@ module.exports = async (env, spinner, config) => {
79
78
  : templateConfig.filetypes || get(templateConfig, 'filetypes', 'html')
80
79
 
81
80
  // List of files that won't be copied to the output directory
82
- const omitted = Array.isArray(templateConfig.omit) ?
83
- templateConfig.omit :
84
- [get(templateConfig, 'omit', '')]
81
+ const omitted = Array.isArray(templateConfig.omit)
82
+ ? templateConfig.omit
83
+ : [get(templateConfig, 'omit', '')]
85
84
 
86
85
  // Parse each template source
87
- await asyncForEach(templateSource, async source => {
86
+ for await (const source of templateSource) {
88
87
  /**
89
88
  * Copy single-file sources correctly
90
89
  * If `src` is a file, `dest` cannot be a directory
91
90
  * https://github.com/jprichardson/node-fs-extra/issues/323
92
91
  */
93
- const out = fs.lstatSync(source).isFile() ? `${outputDir}/${path.basename(source)}` : outputDir
92
+ const out = fs.lstatSync(source).isFile()
93
+ ? `${outputDir}/${path.basename(source)}`
94
+ : outputDir
94
95
 
95
96
  await fs
96
97
  .copy(source, out, {filter: file => {
@@ -119,7 +120,7 @@ module.exports = async (env, spinner, config) => {
119
120
  await config.events.beforeCreate(config)
120
121
  }
121
122
 
122
- await asyncForEach(templates, async file => {
123
+ for await (const file of templates) {
123
124
  config.build.current = {
124
125
  path: path.parse(file)
125
126
  }
@@ -202,18 +203,18 @@ module.exports = async (env, spinner, config) => {
202
203
  throw error
203
204
  }
204
205
  }
205
- })
206
+ }
206
207
 
207
208
  const assets = {source: '', destination: 'assets', ...get(templateConfig, 'assets')}
208
209
 
209
210
  if (Array.isArray(assets.source)) {
210
- await asyncForEach(assets.source, async source => {
211
+ for await (const source of assets.source) {
211
212
  if (fs.existsSync(source)) {
212
213
  await fs
213
214
  .copy(source, path.join(templateConfig.destination.path, assets.destination))
214
215
  .catch(error => spinner.warn(error.message))
215
216
  }
216
- })
217
+ }
217
218
  } else {
218
219
  if (fs.existsSync(assets.source)) {
219
220
  await fs
@@ -228,8 +229,8 @@ module.exports = async (env, spinner, config) => {
228
229
  })
229
230
  })
230
231
  .catch(error => spinner.warn(error.message))
231
- })
232
- })
232
+ }
233
+ }
233
234
 
234
235
  if (config.events && typeof config.events.afterBuild === 'function') {
235
236
  await config.events.afterBuild(files)
@@ -37,7 +37,7 @@ module.exports = async (html, options) => {
37
37
  config: merge(config, {
38
38
  build: {
39
39
  tailwind: {
40
- config: get(options, 'tailwind.config', {})
40
+ config: get(options, 'tailwind.config')
41
41
  }
42
42
  }
43
43
  })
@@ -1,3 +1,4 @@
1
1
  module.exports = {
2
- recognizeNoValueAttribute: true
2
+ recognizeNoValueAttribute: true,
3
+ recognizeSelfClosing: true
3
4
  }
@@ -9,7 +9,13 @@ const defaultConfig = require('./defaultConfig')
9
9
  module.exports = async (html, config) => {
10
10
  const layoutsOptions = get(config, 'build.layouts', {})
11
11
  const componentsOptions = get(config, 'build.components', {})
12
- const expressionsOptions = merge({strictMode: false}, get(config, 'build.posthtml.expressions', {}))
12
+ const expressionsOptions = merge(
13
+ {
14
+ loopTags: ['each', 'for'],
15
+ strictMode: false
16
+ },
17
+ get(config, 'build.posthtml.expressions', {})
18
+ )
13
19
 
14
20
  const posthtmlOptions = merge(defaultConfig, get(config, 'build.posthtml.options', {}))
15
21
  const posthtmlPlugins = get(config, 'build.posthtml.plugins', [])
@@ -8,6 +8,22 @@ const {requireUncached} = require('../utils/helpers')
8
8
  const mergeLonghand = require('postcss-merge-longhand')
9
9
  const {get, isObject, isEmpty, merge} = require('lodash')
10
10
 
11
+ const addImportantPlugin = () => {
12
+ return {
13
+ postcssPlugin: 'add-important',
14
+ Rule(rule) {
15
+ const shouldAddImportant = get(rule, 'raws.tailwind.layer') === 'variants'
16
+ || get(rule, 'parent.type') === 'atrule'
17
+
18
+ if (shouldAddImportant) {
19
+ rule.walkDecls(decl => {
20
+ decl.important = true
21
+ })
22
+ }
23
+ }
24
+ }
25
+ }
26
+
11
27
  module.exports = {
12
28
  compile: async ({css = '', html = '', config = {}}) => {
13
29
  // Compute the Tailwind config to use
@@ -34,25 +50,30 @@ module.exports = {
34
50
  const layoutsRoot = get(config, 'build.layouts.root')
35
51
  const componentsRoot = get(config, 'build.components.root')
36
52
 
53
+ const layoutsPath = typeof layoutsRoot === 'string' && layoutsRoot ?
54
+ `${layoutsRoot}/**/*.html`.replace(/\/\//g, '/') :
55
+ './src/layouts/**/*.html'
56
+
57
+ const componentsPath = typeof componentsRoot === 'string' && componentsRoot ?
58
+ `${componentsRoot}/**/*.html`.replace(/\/\//g, '/') :
59
+ './src/components/**/*.html'
60
+
37
61
  const tailwindConfig = merge({
38
- important: true,
39
62
  content: {
40
63
  files: [
41
- typeof layoutsRoot === 'string' && layoutsRoot ?
42
- `${layoutsRoot}/**/*.html`.replace(/\/\//g, '/') :
43
- './src/layouts/**/*.html',
44
- typeof componentsRoot === 'string' && componentsRoot ?
45
- `${componentsRoot}/**/*.html`.replace(/\/\//g, '/') :
46
- './src/components/**/*.html',
64
+ layoutsPath,
65
+ componentsPath,
47
66
  {raw: html, extension: 'html'}
48
67
  ]
49
68
  }
50
69
  }, userConfig(config))
51
70
 
52
- // Add back the `{raw: html}` option if user provided own config
71
+ // If `content` is an array, add it to `content.files`
53
72
  if (Array.isArray(tailwindConfig.content)) {
54
73
  tailwindConfig.content = {
55
74
  files: [
75
+ layoutsPath,
76
+ componentsPath,
56
77
  ...tailwindConfig.content,
57
78
  {raw: html, extension: 'html'}
58
79
  ]
@@ -99,6 +120,7 @@ module.exports = {
99
120
  const toProcess = [
100
121
  postcssNested(),
101
122
  tailwindcss(tailwindConfig),
123
+ get(tailwindConfig, 'important') === false ? () => {} : addImportantPlugin(),
102
124
  get(config, 'shorthandCSS', get(config, 'shorthandInlineCSS')) === true ?
103
125
  mergeLonghand() :
104
126
  () => {},
@@ -53,7 +53,7 @@ exports.addURLParams = (html, config) => addURLParams(html, config, true)
53
53
  exports.preventWidows = (html, config) => preventWidows(html, config)
54
54
  exports.replaceStrings = (html, config) => replaceStrings(html, config, true)
55
55
  exports.safeClassNames = (html, config) => safeClassNames(html, config, true)
56
- exports.removeUnusedCSS = (html, config) => removeUnusedCSS(html, config)
56
+ exports.removeUnusedCSS = (html, config) => removeUnusedCSS(html, config, true)
57
57
  exports.removeAttributes = (html, config) => removeAttributes(html, config, true)
58
58
  exports.attributeToStyle = (html, config) => attributeToStyle(html, config, true)
59
59
  exports.removeInlineSizes = (html, config) => removeInlineSizes(html, config, true)
@@ -14,7 +14,7 @@ module.exports = async (html, config = {}, direct = false) => {
14
14
 
15
15
  if (get(config, 'inlineCSS') === true || !isEmpty(options)) {
16
16
  options.applyAttributesTableElements = true
17
- juice.styleToAttribute = get(options, 'styleToAttribute', {'vertical-align': 'valign'})
17
+ juice.styleToAttribute = get(options, 'styleToAttribute', {})
18
18
 
19
19
  juice.widthElements = get(options, 'applyWidthAttributes', []).map(i => i.toUpperCase())
20
20
  juice.heightElements = get(options, 'applyHeightAttributes', []).map(i => i.toUpperCase())
@@ -1,10 +1,12 @@
1
1
  const {comb} = require('email-comb')
2
- const {get, merge} = require('lodash')
2
+ const {get, merge, isEmpty, isObject} = require('lodash')
3
3
  const removeInlinedClasses = require('./removeInlinedSelectors')
4
4
 
5
- module.exports = async (html, config = {}) => {
6
- // If it's explicitly disabled, return the HTML
7
- if (get(config, 'removeUnusedCSS') === false) {
5
+ module.exports = async (html, config = {}, direct = false) => {
6
+ config = direct ? config : get(config, 'removeUnusedCSS')
7
+
8
+ // Don't purge CSS if `removeUnusedCSS` is not set
9
+ if (!config || (isObject(config) && isEmpty(config))) {
8
10
  return html
9
11
  }
10
12
 
@@ -34,9 +36,13 @@ module.exports = async (html, config = {}) => {
34
36
  whitelist: [...get(config, 'whitelist', []), ...safelist]
35
37
  }
36
38
 
37
- const options = merge(defaultOptions, get(config, 'removeUnusedCSS', config))
39
+ const options = merge(defaultOptions, get(config, 'removeUnusedCSS', {}))
38
40
 
39
- html = await removeInlinedClasses(html, options)
41
+ /**
42
+ * Remove possibly inlined selectors, as long as we're not calling
43
+ * this function directly, i.e. Maizzle.removeUnusedCSS()
44
+ * */
45
+ html = direct ? html : await removeInlinedClasses(html, options)
40
46
 
41
47
  return comb(html, options).result
42
48
  }
@@ -1,9 +1,4 @@
1
1
  module.exports = {
2
- asyncForEach: async (array, callback) => {
3
- for (let index = 0; index < array.length; index++) {
4
- await callback(array[index], index, array) // eslint-disable-line
5
- }
6
- },
7
2
  requireUncached: module => {
8
3
  try {
9
4
  delete require.cache[require.resolve(module)]