@maizzle/framework 4.4.0-beta.9 → 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.9",
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.6"
77
+ "tailwindcss": "^3.2.7"
78
78
  },
79
79
  "devDependencies": {
80
80
  "ava": "^5.2.0",
81
- "c8": "^7.11.0",
81
+ "c8": "^7.13.0",
82
82
  "np": "*",
83
83
  "xo": "0.39.1"
84
84
  },
@@ -36,137 +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...')
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
+ }
83
56
 
84
- renderToString(
85
- await fs.readFile(file.replace(/\\/g, '/'), 'utf8'),
86
- {
87
- maizzle: merge(
88
- config,
89
- {
90
- build: {
91
- current: {
92
- path: path.parse(file)
93
- }
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)
94
96
  }
95
97
  }
96
- ),
97
- ...config.events
98
- }
99
- )
100
- .then(async ({html, config}) => {
101
- let source = ''
102
- let dest = ''
103
- let ext = ''
104
-
105
- if (Array.isArray(config.build.templates)) {
106
- const match = config.build.templates.find(template => template.source === path.parse(file).dir)
107
- source = get(match, 'source')
108
- dest = get(match, 'destination.path', 'build_local')
109
- ext = get(match, 'destination.ext', 'html')
110
- } else if (isObject(config.build.templates)) {
111
- source = get(config, 'build.templates.source')
112
- dest = get(config, 'build.templates.destination.path', 'build_local')
113
- ext = get(config, 'build.templates.destination.ext', 'html')
114
98
  }
99
+ ),
100
+ ...config.events
101
+ }
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
+ }
115
119
 
116
- const fileDir = path.parse(file).dir.replace(source, '')
117
- const finalDestination = path.join(dest, fileDir, `${path.parse(file).name}.${ext}`)
118
-
119
- await fs.outputFile(config.permalink || finalDestination, html)
120
- })
121
- .then(() => {
122
- browsersync().reload()
123
- spinner.succeed(`Compiled in ${(Date.now() - start) / 1000}s [${file}]`)
124
- })
125
- .catch(() => spinner.warn(`Received empty HTML, please save your file again [${file}]`))
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
126
141
  })
127
-
128
- // Watch for changes in all other files
129
- browsersync()
130
- .watch(globalPaths, {ignored: templatePaths})
131
- .on('change', () => buildToFile(env, config).then(() => browsersync().reload()))
132
- .on('unlink', () => buildToFile(env, config).then(() => browsersync().reload()))
133
-
134
- // Watch for changes in config files
135
- browsersync()
136
- .watch('config*.js')
137
- .on('change', async file => {
138
- const parsedEnv = path.parse(file).name.split('.')[1] || 'local'
139
-
140
- Config
141
- .getMerged(parsedEnv)
142
- .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
143
147
  })
144
-
145
- // Browsersync options
146
- const baseDir = templates.map(t => t.destination.path)
147
-
148
- // Initialize Browsersync
149
- browsersync()
150
- .init(
151
- merge(
152
- {
153
- notify: false,
154
- open: false,
155
- port: 3000,
156
- server: {
157
- baseDir,
158
- directory: true
159
- },
160
- tunnel: false,
161
- ui: {port: 3001},
162
- 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
163
180
  },
164
- get(config, 'build.browsersync', {})
165
- ), () => {})
166
- } catch (error) {
167
- spinner.fail(error)
168
- throw error
169
- }
181
+ tunnel: false,
182
+ ui: {port: 3001},
183
+ logFileChanges: false
184
+ },
185
+ get(config, 'build.browsersync', {})
186
+ ), () => {})
170
187
  }
171
188
 
172
189
  module.exports = serve
@@ -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
  }