@maizzle/framework 4.4.0-beta.9 → 4.4.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/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.1",
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",
@@ -36,13 +36,13 @@
36
36
  "release": "np"
37
37
  },
38
38
  "files": [
39
- "src/*",
39
+ "src/**/*",
40
40
  "bin/*"
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
  })
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ root: './',
3
+ folders: ['src/components', 'src/layouts', 'src/templates'],
4
+ fileExtension: 'html',
5
+ tag: 'component',
6
+ attribute: 'src',
7
+ yield: 'content',
8
+ propsAttribute: 'locals'
9
+ }
@@ -1,3 +1,4 @@
1
1
  module.exports = {
2
- recognizeNoValueAttribute: true
2
+ recognizeNoValueAttribute: true,
3
+ recognizeSelfClosing: true
3
4
  }
@@ -5,11 +5,18 @@ const fetch = require('posthtml-fetch')
5
5
  const layouts = require('posthtml-extend')
6
6
  const components = require('posthtml-component')
7
7
  const defaultConfig = require('./defaultConfig')
8
+ const defaultComponentsConfig = require('./defaultComponentsConfig')
8
9
 
9
10
  module.exports = async (html, config) => {
10
11
  const layoutsOptions = get(config, 'build.layouts', {})
11
12
  const componentsOptions = get(config, 'build.components', {})
12
- const expressionsOptions = merge({strictMode: false}, get(config, 'build.posthtml.expressions', {}))
13
+ const expressionsOptions = merge(
14
+ {
15
+ loopTags: ['each', 'for'],
16
+ strictMode: false
17
+ },
18
+ get(config, 'build.posthtml.expressions', {})
19
+ )
13
20
 
14
21
  const posthtmlOptions = merge(defaultConfig, get(config, 'build.posthtml.options', {}))
15
22
  const posthtmlPlugins = get(config, 'build.posthtml.plugins', [])
@@ -29,6 +36,20 @@ module.exports = async (html, config) => {
29
36
  )
30
37
  )
31
38
 
39
+ const defaultComponentsOptions = merge(
40
+ defaultComponentsConfig,
41
+ {
42
+ folders: [
43
+ ...defaultComponentsConfig.folders,
44
+ ...get(componentsOptions, 'folders', [])
45
+ ]
46
+ },
47
+ {
48
+ root: componentsOptions.root || './',
49
+ expressions: {...expressionsOptions, locals}
50
+ }
51
+ )
52
+
32
53
  return posthtml([
33
54
  fetchPlugin,
34
55
  layouts(
@@ -42,15 +63,7 @@ module.exports = async (html, config) => {
42
63
  ),
43
64
  components(
44
65
  merge(
45
- {
46
- root: componentsOptions.root || './',
47
- folders: ['src/components', 'src/layouts', 'src/templates'],
48
- tag: 'component',
49
- attribute: 'src',
50
- yield: 'content',
51
- propsAttribute: 'locals',
52
- expressions: {...expressionsOptions, locals}
53
- },
66
+ defaultComponentsOptions,
54
67
  componentsOptions
55
68
  )
56
69
  ),
@@ -7,6 +7,23 @@ const postcssNested = require('tailwindcss/nesting')
7
7
  const {requireUncached} = require('../utils/helpers')
8
8
  const mergeLonghand = require('postcss-merge-longhand')
9
9
  const {get, isObject, isEmpty, merge} = require('lodash')
10
+ const defaultComponentsConfig = require('./posthtml/defaultComponentsConfig')
11
+
12
+ const addImportantPlugin = () => {
13
+ return {
14
+ postcssPlugin: 'add-important',
15
+ Rule(rule) {
16
+ const shouldAddImportant = get(rule, 'raws.tailwind.layer') === 'variants'
17
+ || get(rule, 'parent.type') === 'atrule'
18
+
19
+ if (shouldAddImportant) {
20
+ rule.walkDecls(decl => {
21
+ decl.important = true
22
+ })
23
+ }
24
+ }
25
+ }
26
+ }
10
27
 
11
28
  module.exports = {
12
29
  compile: async ({css = '', html = '', config = {}}) => {
@@ -32,73 +49,86 @@ module.exports = {
32
49
 
33
50
  // Merge user's Tailwind config on top of a 'base' config
34
51
  const layoutsRoot = get(config, 'build.layouts.root')
35
- const componentsRoot = get(config, 'build.components.root')
52
+ const componentsRoot = get(config, 'build.components.root', defaultComponentsConfig.root)
53
+
54
+ const layoutsPath = typeof layoutsRoot === 'string' && layoutsRoot ?
55
+ `${layoutsRoot}/**/*.*`.replace(/\/\//g, '/') :
56
+ 'src/layouts/**/*.*'
57
+
58
+ const componentsPath = defaultComponentsConfig.folders.map(folder => {
59
+ return path
60
+ .join(componentsRoot, folder, `**/*.${defaultComponentsConfig.fileExtension}`)
61
+ .replace(/\\/g, '/')
62
+ .replace(/\/\//g, '/')
63
+ })
36
64
 
37
65
  const tailwindConfig = merge({
38
- important: true,
39
66
  content: {
40
67
  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',
47
- {raw: html, extension: 'html'}
68
+ ...componentsPath,
69
+ layoutsPath
48
70
  ]
49
71
  }
50
72
  }, userConfig(config))
51
73
 
52
- // Add back the `{raw: html}` option if user provided own config
74
+ // If `content` is an array, add it to `content.files`
53
75
  if (Array.isArray(tailwindConfig.content)) {
54
76
  tailwindConfig.content = {
55
77
  files: [
56
- ...tailwindConfig.content,
57
- {raw: html, extension: 'html'}
78
+ ...componentsPath,
79
+ layoutsPath,
80
+ ...tailwindConfig.content
58
81
  ]
59
82
  }
60
83
  }
61
84
 
85
+ // Add raw HTML if using API
86
+ if (html) {
87
+ tailwindConfig.content.files.push({raw: html, extension: 'html'})
88
+ }
89
+
62
90
  // Include all `build.templates.source` paths when scanning for selectors to preserve
63
91
  const buildTemplates = get(config, 'build.templates')
64
92
 
65
93
  if (buildTemplates) {
66
94
  const templateObjects = Array.isArray(buildTemplates) ? buildTemplates : [buildTemplates]
67
- const templateSources = templateObjects.map(template => {
95
+ const fileTypes = get(buildTemplates, 'filetypes', 'html')
96
+
97
+ templateObjects.forEach(template => {
68
98
  const source = get(template, 'source')
69
99
 
70
100
  if (typeof source === 'function') {
71
101
  const sources = source(config)
72
102
 
73
103
  if (Array.isArray(sources)) {
74
- sources.map(s => tailwindConfig.content.files.push(s))
104
+ sources.map(s => tailwindConfig.content.files.push(`${s}/**/*.${fileTypes}`))
75
105
  } else if (typeof sources === 'string') {
76
106
  tailwindConfig.content.files.push(sources)
77
107
  }
78
-
79
- // Must return a valid `content` entry
80
- return {raw: '', extension: 'html'}
81
108
  }
82
109
 
83
110
  // Support single-file sources i.e. src/templates/index.html
84
- if (typeof source === 'string' && Boolean(path.extname(source))) {
111
+ else if (typeof source === 'string' && Boolean(path.extname(source))) {
85
112
  tailwindConfig.content.files.push(source)
86
-
87
- return {raw: '', extension: 'html'}
88
113
  }
89
114
 
90
- return `${source}/**/*.*`
115
+ // Default behavior - directory sources as a string
116
+ else {
117
+ tailwindConfig.content.files.push(`${source}/**/*.${fileTypes}`)
118
+ }
91
119
  })
92
-
93
- tailwindConfig.content.files.push(...templateSources)
94
120
  }
95
121
 
122
+ // Filter out any duplicate content paths
123
+ tailwindConfig.content.files = [...new Set(tailwindConfig.content.files)]
124
+
96
125
  const userFilePath = get(config, 'build.tailwind.css', path.join(process.cwd(), 'src/css/tailwind.css'))
97
126
  const userFileExists = await fs.pathExists(userFilePath)
98
127
 
99
128
  const toProcess = [
100
129
  postcssNested(),
101
130
  tailwindcss(tailwindConfig),
131
+ get(tailwindConfig, 'important') === false ? () => {} : addImportantPlugin(),
102
132
  get(config, 'shorthandCSS', get(config, 'shorthandInlineCSS')) === true ?
103
133
  mergeLonghand() :
104
134
  () => {},
@@ -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
  }