@maizzle/framework 4.4.0-beta.1 → 4.4.0-beta.11

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/README.md CHANGED
@@ -34,9 +34,9 @@ The Maizzle framework is open-sourced software licensed under the [MIT license](
34
34
 
35
35
  [npm]: https://www.npmjs.com/package/@maizzle/framework
36
36
  [npm-stats]: https://npm-stat.com/charts.html?package=%40maizzle%2Fframework&from=2019-03-27
37
- [npm-version-shield]: https://img.shields.io/npm/v/@maizzle/framework.svg?style=flat-square
38
- [npm-stats-shield]: https://img.shields.io/npm/dt/@maizzle/framework.svg?style=flat-square&color=4f46e5
37
+ [npm-version-shield]: https://img.shields.io/npm/v/@maizzle/framework.svg
38
+ [npm-stats-shield]: https://img.shields.io/npm/dt/@maizzle/framework.svg?color=4f46e5
39
39
  [github-ci]: https://github.com/maizzle/framework/actions
40
- [github-ci-shield]: https://img.shields.io/github/workflow/status/maizzle/framework/Node.js%20CI?style=flat-square
40
+ [github-ci-shield]: https://github.com/maizzle/framework/actions/workflows/nodejs.yml/badge.svg
41
41
  [license]: ./LICENSE
42
- [license-shield]: https://img.shields.io/npm/l/@maizzle/framework.svg?style=flat-square&color=0e9f6e
42
+ [license-shield]: https://img.shields.io/npm/l/@maizzle/framework.svg?color=0e9f6e
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maizzle/framework",
3
- "version": "4.4.0-beta.1",
3
+ "version": "4.4.0-beta.11",
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",
@@ -42,7 +42,7 @@
42
42
  "dependencies": {
43
43
  "@maizzle/cli": "^1.5.1",
44
44
  "autoprefixer": "^10.4.13",
45
- "browser-sync": "^2.26.13",
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",
@@ -53,34 +53,32 @@
53
53
  "juice": "^8.0.0",
54
54
  "lodash": "^4.17.20",
55
55
  "ora": "^5.1.0",
56
- "postcss": "^8.4.19",
56
+ "postcss": "^8.4.21",
57
57
  "postcss-import": "^15.0.0",
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.0.0-RC.1",
63
63
  "posthtml-content": "^0.1.0",
64
- "posthtml-expressions": "^1.8.1",
65
64
  "posthtml-extend": "^0.6.0",
66
- "posthtml-extra-attributes": "^1.0.0",
67
- "posthtml-fetch": "^2.2.0",
68
- "posthtml-markdownit": "^1.3.0",
65
+ "posthtml-extra-attributes": "^2.0.0",
66
+ "posthtml-fetch": "^3.0.0",
67
+ "posthtml-markdownit": "^1.3.1",
69
68
  "posthtml-match-helper": "^1.0.3",
70
- "posthtml-modules": "^0.9.0",
71
- "posthtml-mso": "^1.0.4",
72
- "posthtml-postcss-merge-longhand": "^1.0.2",
73
- "posthtml-safe-class-names": "^2.0.0",
74
- "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",
75
73
  "pretty": "^2.0.0",
76
- "query-string": "^7.1.0",
74
+ "query-string": "^7.1.3",
77
75
  "string-remove-widows": "^2.1.0",
78
76
  "string-strip-html": "^8.2.0",
79
- "tailwindcss": "^3.2.4"
77
+ "tailwindcss": "^3.2.7"
80
78
  },
81
79
  "devDependencies": {
82
- "ava": "^5.1.0",
83
- "c8": "^7.11.0",
80
+ "ava": "^5.2.0",
81
+ "c8": "^7.13.0",
84
82
  "np": "*",
85
83
  "xo": "0.39.1"
86
84
  },
@@ -9,6 +9,10 @@ const renderToString = require('../functions/render')
9
9
  const {get, merge, isObject} = require('lodash')
10
10
  const {clearConsole} = require('../utils/helpers')
11
11
 
12
+ /**
13
+ * Initialize Browsersync on-demand
14
+ * https://github.com/maizzle/framework/issues/605
15
+ */
12
16
  const browsersync = () => {
13
17
  if (!global.cachedBrowserSync) {
14
18
  const bs = require('browser-sync')
@@ -18,142 +22,168 @@ const browsersync = () => {
18
22
  return global.cachedBrowserSync
19
23
  }
20
24
 
25
+ const getConfig = async (env = 'local', config = {}) => merge(
26
+ config,
27
+ await Config.getMerged(env)
28
+ )
29
+
21
30
  const serve = async (env = 'local', config = {}) => {
22
- config = merge(
23
- config,
24
- await Config.getMerged(env),
25
- {
26
- build: {
27
- command: 'serve'
28
- }
31
+ config = await getConfig(env, merge(config, {
32
+ build: {
33
+ command: 'serve'
29
34
  }
30
- )
35
+ }))
31
36
 
32
37
  const spinner = ora()
33
38
 
34
- try {
35
- await buildToFile(env, config)
36
-
37
- let templates = get(config, 'build.templates')
38
- templates = Array.isArray(templates) ? templates : [templates]
39
+ // Build all emails first
40
+ await buildToFile(env, config)
39
41
 
40
- const templatePaths = [...new Set(templates.map(config => `${get(config, 'source', 'src')}/**`))]
41
- const tailwindConfig = get(config, 'build.tailwind.config', 'tailwind.config.js')
42
- const globalPaths = [
43
- 'src/**',
44
- ...new Set(get(config, 'build.browsersync.watch', []))
45
- ]
46
- if (typeof tailwindConfig === 'string') {
47
- globalPaths.push(tailwindConfig);
48
- }
42
+ // Set some paths to watch
43
+ let templates = get(config, 'build.templates')
44
+ templates = Array.isArray(templates) ? templates : [templates]
49
45
 
50
- // Watch for Template file changes
51
- browsersync()
52
- .watch(templatePaths)
53
- .on('change', async file => {
54
- if (config.events && typeof config.events.beforeCreate === 'function') {
55
- await config.events.beforeCreate(config)
56
- }
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
+ ]
57
52
 
58
- // Don't render if file type is not configured
59
- // eslint-disable-next-line
60
- const filetypes = templates.reduce((acc, template) => {
61
- return [...acc, ...get(template, 'filetypes', ['html'])]
62
- }, [])
53
+ if (typeof tailwindConfig === 'string') {
54
+ globalPaths.push(tailwindConfig)
55
+ }
63
56
 
64
- if (!filetypes.includes(path.extname(file).slice(1))) {
65
- return
66
- }
57
+ // Watch for Template file changes
58
+ browsersync()
59
+ .watch(templatePaths)
60
+ .on('change', async file => {
61
+ config = await getConfig(env, config)
67
62
 
68
- if (get(config, 'build.console.clear')) {
69
- clearConsole()
70
- }
63
+ if (config.events && typeof config.events.beforeCreate === 'function') {
64
+ await config.events.beforeCreate(config)
65
+ }
71
66
 
72
- const start = new Date()
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
+ }, [])
73
72
 
74
- spinner.start('Building email...')
73
+ if (!filetypes.includes(path.extname(file).slice(1))) {
74
+ return
75
+ }
75
76
 
76
- file = file.replace(/\\/g, '/')
77
+ // Clear console if enabled
78
+ if (get(config, 'build.console.clear')) {
79
+ clearConsole()
80
+ }
77
81
 
78
- const renderOptions = {
79
- maizzle: config,
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
+ ),
80
100
  ...config.events
81
101
  }
82
-
83
- renderToString(
84
- await fs.readFile(file, 'utf8'),
85
- renderOptions
86
- )
87
- .then(async ({html, config}) => {
88
- let source = ''
89
- let dest = ''
90
- let ext = ''
91
-
92
- if (Array.isArray(config.build.templates)) {
93
- const match = config.build.templates.find(template => template.source === path.parse(file).dir)
94
- source = get(match, 'source')
95
- dest = get(match, 'destination.path', 'build_local')
96
- ext = get(match, 'destination.ext', 'html')
97
- } else if (isObject(config.build.templates)) {
98
- source = get(config, 'build.templates.source')
99
- dest = get(config, 'build.templates.destination.path', 'build_local')
100
- ext = get(config, 'build.templates.destination.ext', 'html')
101
- }
102
-
103
- const fileDir = path.parse(file).dir.replace(source, '')
104
- const finalDestination = path.join(dest, fileDir, `${path.parse(file).name}.${ext}`)
105
-
106
- await fs.outputFile(config.permalink || finalDestination, html)
107
- })
108
- .then(() => {
109
- browsersync().reload()
110
- spinner.succeed(`Compiled in ${(Date.now() - start) / 1000}s [${file}]`)
111
- })
112
- .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
113
141
  })
114
-
115
- // Watch for changes in all other files
116
- browsersync()
117
- .watch(globalPaths, {ignored: templatePaths})
118
- .on('change', () => buildToFile(env, config).then(() => browsersync().reload()))
119
- .on('unlink', () => buildToFile(env, config).then(() => browsersync().reload()))
120
-
121
- // Watch for changes in config files
122
- browsersync()
123
- .watch('config*.js')
124
- .on('change', async file => {
125
- const parsedEnv = path.parse(file).name.split('.')[1] || 'local'
126
-
127
- Config
128
- .getMerged(parsedEnv)
129
- .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
130
147
  })
131
-
132
- // Browsersync options
133
- const baseDir = templates.map(t => t.destination.path)
134
-
135
- // Initialize Browsersync
136
- browsersync()
137
- .init(
138
- merge(
139
- {
140
- notify: false,
141
- open: false,
142
- port: 3000,
143
- server: {
144
- baseDir,
145
- directory: true
146
- },
147
- tunnel: false,
148
- ui: {port: 3001},
149
- 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
150
180
  },
151
- get(config, 'build.browsersync', {})
152
- ), () => {})
153
- } catch (error) {
154
- spinner.fail(error)
155
- throw error
156
- }
181
+ tunnel: false,
182
+ ui: {port: 3001},
183
+ logFileChanges: false
184
+ },
185
+ get(config, 'build.browsersync', {})
186
+ ), () => {})
157
187
  }
158
188
 
159
189
  module.exports = serve
@@ -13,16 +13,23 @@ module.exports = {
13
13
 
14
14
  const cwd = env === 'maizzle-ci' ? './test/stubs/config' : process.cwd()
15
15
 
16
- for (const module of ['./config', './config.local']) {
16
+ for (const module of ['./config', './config.cjs', './config.local', './config.local.cjs']) {
17
17
  try {
18
18
  baseConfig = merge(baseConfig, requireUncached(path.resolve(cwd, module)))
19
19
  } catch {}
20
20
  }
21
21
 
22
22
  if (typeof env === 'string' && env !== 'local') {
23
- try {
24
- envConfig = merge(envConfig, requireUncached(path.resolve(cwd, `./config.${env}`)))
25
- } catch {
23
+ let loaded = false
24
+ for (const module of [`./config.${env}`, `./config.${env}.cjs`]) {
25
+ try {
26
+ envConfig = merge(envConfig, requireUncached(path.resolve(cwd, module)))
27
+ loaded = true
28
+ break
29
+ } catch {}
30
+ }
31
+
32
+ if (!loaded) {
26
33
  throw new Error(`could not load config.${env}.js`)
27
34
  }
28
35
  }
@@ -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')
@@ -28,10 +27,15 @@ module.exports = async (env, spinner, config) => {
28
27
 
29
28
  const css = (typeof get(config, 'build.tailwind.compiled') === 'string')
30
29
  ? config.build.tailwind.compiled
31
- : await Tailwind.compile('', '', {}, config)
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) {
34
+ if (!templateConfig) {
35
+ const configFileName = env === 'local' ? 'config.js' : `config.${env}.js`
36
+ throw new Error(`No template sources defined in \`build.templates\`, check your ${configFileName} file`)
37
+ }
38
+
35
39
  const outputDir = get(templateConfig, 'destination.path', `build_${env}`)
36
40
 
37
41
  await fs.remove(outputDir)
@@ -67,23 +71,45 @@ module.exports = async (env, spinner, config) => {
67
71
  }
68
72
  }
69
73
 
74
+ // Create a pipe-delimited list of allowed extensions
75
+ // We only compile these, the rest are copied as-is
76
+ const extensions = Array.isArray(templateConfig.filetypes)
77
+ ? templateConfig.filetypes.join('|')
78
+ : templateConfig.filetypes || get(templateConfig, 'filetypes', 'html')
79
+
80
+ // List of files that won't be copied to the output directory
81
+ const omitted = Array.isArray(templateConfig.omit)
82
+ ? templateConfig.omit
83
+ : [get(templateConfig, 'omit', '')]
84
+
70
85
  // Parse each template source
71
- await asyncForEach(templateSource, async source => {
86
+ for await (const source of templateSource) {
72
87
  /**
73
88
  * Copy single-file sources correctly
74
89
  * If `src` is a file, `dest` cannot be a directory
75
90
  * https://github.com/jprichardson/node-fs-extra/issues/323
76
91
  */
77
- 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
78
95
 
79
96
  await fs
80
- .copy(source, out)
97
+ .copy(source, out, {filter: file => {
98
+ // Do not copy omitted files
99
+ return !omitted
100
+ .filter(Boolean)
101
+ .some(omit => path.normalize(file).includes(path.normalize(omit)))
102
+ }})
81
103
  .then(async () => {
82
- const extensions = Array.isArray(templateConfig.filetypes)
83
- ? templateConfig.filetypes.join('|')
84
- : templateConfig.filetypes || get(templateConfig, 'filetypes', 'html')
104
+ const allSourceFiles = await glob(`${outputDir}/**/*.+(${extensions})`)
85
105
 
86
- const templates = await glob(`${outputDir}/**/*.+(${extensions})`)
106
+ const skipped = Array.isArray(templateConfig.skip) ?
107
+ templateConfig.skip :
108
+ [get(templateConfig, 'skip', '')]
109
+
110
+ const templates = allSourceFiles.filter(template => {
111
+ return !skipped.includes(template.replace(`${outputDir}/`, ''))
112
+ })
87
113
 
88
114
  if (templates.length === 0) {
89
115
  spinner.warn(`Error: no files with the .${extensions} extension found in ${templateConfig.source}`)
@@ -94,7 +120,7 @@ module.exports = async (env, spinner, config) => {
94
120
  await config.events.beforeCreate(config)
95
121
  }
96
122
 
97
- await asyncForEach(templates, async file => {
123
+ for await (const file of templates) {
98
124
  config.build.current = {
99
125
  path: path.parse(file)
100
126
  }
@@ -114,7 +140,7 @@ module.exports = async (env, spinner, config) => {
114
140
  ...config.events
115
141
  })
116
142
 
117
- const destination = config.permalink || file
143
+ const destination = get(compiled, 'config.permalink', file)
118
144
 
119
145
  /**
120
146
  * Generate plaintext
@@ -125,7 +151,7 @@ module.exports = async (env, spinner, config) => {
125
151
 
126
152
  // Check if plaintext: true globally, fallback to template's front matter
127
153
  const plaintextConfig = get(templateConfig, 'plaintext', get(compiled.config, 'plaintext', false))
128
- const plaintextPath = get(plaintextConfig, 'destination.path', config.permalink || file)
154
+ const plaintextPath = get(plaintextConfig, 'destination.path', destination)
129
155
 
130
156
  if (Boolean(plaintextConfig) || !isEmpty(plaintextConfig)) {
131
157
  await Plaintext
@@ -177,19 +203,23 @@ module.exports = async (env, spinner, config) => {
177
203
  throw error
178
204
  }
179
205
  }
180
- })
206
+ }
181
207
 
182
208
  const assets = {source: '', destination: 'assets', ...get(templateConfig, 'assets')}
183
209
 
184
210
  if (Array.isArray(assets.source)) {
185
- await asyncForEach(assets.source, async source => {
211
+ for await (const source of assets.source) {
186
212
  if (fs.existsSync(source)) {
187
- await fs.copy(source, path.join(templateConfig.destination.path, assets.destination)).catch(error => spinner.warn(error.message))
213
+ await fs
214
+ .copy(source, path.join(templateConfig.destination.path, assets.destination))
215
+ .catch(error => spinner.warn(error.message))
188
216
  }
189
- })
217
+ }
190
218
  } else {
191
219
  if (fs.existsSync(assets.source)) {
192
- await fs.copy(assets.source, path.join(templateConfig.destination.path, assets.destination)).catch(error => spinner.warn(error.message))
220
+ await fs
221
+ .copy(assets.source, path.join(templateConfig.destination.path, assets.destination))
222
+ .catch(error => spinner.warn(error.message))
193
223
  }
194
224
  }
195
225
 
@@ -199,8 +229,8 @@ module.exports = async (env, spinner, config) => {
199
229
  })
200
230
  })
201
231
  .catch(error => spinner.warn(error.message))
202
- })
203
- })
232
+ }
233
+ }
204
234
 
205
235
  if (config.events && typeof config.events.afterBuild === 'function') {
206
236
  await config.events.afterBuild(files)
@@ -22,9 +22,6 @@ module.exports = async (html, options) => {
22
22
 
23
23
  let config = merge(fileConfig, get(options, 'maizzle', {}))
24
24
 
25
- const tailwindConfig = get(options, 'tailwind.config', {})
26
- const cssString = get(options, 'tailwind.css', '')
27
-
28
25
  const {frontmatter} = fm(html)
29
26
 
30
27
  html = `---\n${frontmatter}\n---\n\n${fm(html).body}`
@@ -34,7 +31,17 @@ module.exports = async (html, options) => {
34
31
  if (typeof get(options, 'tailwind.compiled') === 'string') {
35
32
  config.css = options.tailwind.compiled
36
33
  } else {
37
- config.css = await Tailwind.compile(cssString, html, tailwindConfig, config)
34
+ config.css = await Tailwind.compile({
35
+ css: get(options, 'tailwind.css', ''),
36
+ html,
37
+ config: merge(config, {
38
+ build: {
39
+ tailwind: {
40
+ config: get(options, 'tailwind.config')
41
+ }
42
+ }
43
+ })
44
+ })
38
45
  }
39
46
 
40
47
  if (options && typeof options.beforeRender === 'function') {
@@ -9,7 +9,9 @@ module.exports = {
9
9
  return postcss([
10
10
  postcssImport(),
11
11
  postcssNested(),
12
- maizzleConfig.env === 'local' ? () => {} : mergeLonghand(),
12
+ get(maizzleConfig, 'shorthandCSS', get(maizzleConfig, 'shorthandInlineCSS')) === true ?
13
+ mergeLonghand() :
14
+ () => {},
13
15
  ...get(maizzleConfig, 'build.postcss.plugins', [])
14
16
  ])
15
17
  .process(css, {from: undefined})
@@ -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', [])
@@ -9,14 +9,14 @@ const mergeLonghand = require('postcss-merge-longhand')
9
9
  const {get, isObject, isEmpty, merge} = require('lodash')
10
10
 
11
11
  module.exports = {
12
- compile: async (css = '', html = '', tailwindConfig = {}, maizzleConfig = {}) => {
13
- tailwindConfig = (isObject(tailwindConfig) && !isEmpty(tailwindConfig)) ? tailwindConfig : get(maizzleConfig, 'build.tailwind.config', 'tailwind.config.js')
14
-
12
+ compile: async ({css = '', html = '', config = {}}) => {
15
13
  // Compute the Tailwind config to use
16
- const userConfig = () => {
14
+ const userConfig = config => {
15
+ const tailwindUserConfig = get(config, 'build.tailwind.config', 'tailwind.config.js')
16
+
17
17
  // If a custom config object was passed, use that
18
- if (isObject(tailwindConfig) && !isEmpty(tailwindConfig)) {
19
- return tailwindConfig
18
+ if (isObject(tailwindUserConfig) && !isEmpty(tailwindUserConfig)) {
19
+ return tailwindUserConfig
20
20
  }
21
21
 
22
22
  /**
@@ -24,17 +24,17 @@ module.exports = {
24
24
  * This will use the default Tailwind config (with rem units etc)
25
25
  */
26
26
  try {
27
- return requireUncached(path.resolve(process.cwd(), tailwindConfig))
27
+ return requireUncached(path.resolve(process.cwd(), tailwindUserConfig))
28
28
  } catch {
29
29
  return {}
30
30
  }
31
31
  }
32
32
 
33
33
  // Merge user's Tailwind config on top of a 'base' config
34
- const layoutsRoot = get(maizzleConfig, 'build.layouts.root')
35
- const componentsRoot = get(maizzleConfig, 'build.components.root')
34
+ const layoutsRoot = get(config, 'build.layouts.root')
35
+ const componentsRoot = get(config, 'build.components.root')
36
36
 
37
- const config = merge({
37
+ const tailwindConfig = merge({
38
38
  important: true,
39
39
  content: {
40
40
  files: [
@@ -47,20 +47,20 @@ module.exports = {
47
47
  {raw: html, extension: 'html'}
48
48
  ]
49
49
  }
50
- }, userConfig())
50
+ }, userConfig(config))
51
51
 
52
52
  // Add back the `{raw: html}` option if user provided own config
53
- if (Array.isArray(config.content)) {
54
- config.content = {
53
+ if (Array.isArray(tailwindConfig.content)) {
54
+ tailwindConfig.content = {
55
55
  files: [
56
- ...config.content,
56
+ ...tailwindConfig.content,
57
57
  {raw: html, extension: 'html'}
58
58
  ]
59
59
  }
60
60
  }
61
61
 
62
62
  // Include all `build.templates.source` paths when scanning for selectors to preserve
63
- const buildTemplates = get(maizzleConfig, 'build.templates')
63
+ const buildTemplates = get(config, 'build.templates')
64
64
 
65
65
  if (buildTemplates) {
66
66
  const templateObjects = Array.isArray(buildTemplates) ? buildTemplates : [buildTemplates]
@@ -68,12 +68,12 @@ module.exports = {
68
68
  const source = get(template, 'source')
69
69
 
70
70
  if (typeof source === 'function') {
71
- const sources = source(maizzleConfig)
71
+ const sources = source(config)
72
72
 
73
73
  if (Array.isArray(sources)) {
74
- sources.map(s => config.content.files.push(s))
74
+ sources.map(s => tailwindConfig.content.files.push(s))
75
75
  } else if (typeof sources === 'string') {
76
- config.content.files.push(sources)
76
+ tailwindConfig.content.files.push(sources)
77
77
  }
78
78
 
79
79
  // Must return a valid `content` entry
@@ -82,7 +82,7 @@ module.exports = {
82
82
 
83
83
  // Support single-file sources i.e. src/templates/index.html
84
84
  if (typeof source === 'string' && Boolean(path.extname(source))) {
85
- config.content.files.push(source)
85
+ tailwindConfig.content.files.push(source)
86
86
 
87
87
  return {raw: '', extension: 'html'}
88
88
  }
@@ -90,21 +90,24 @@ module.exports = {
90
90
  return `${source}/**/*.*`
91
91
  })
92
92
 
93
- config.content.files.push(...templateSources)
93
+ tailwindConfig.content.files.push(...templateSources)
94
94
  }
95
95
 
96
- const userFilePath = get(maizzleConfig, 'build.tailwind.css', path.join(process.cwd(), 'src/css/tailwind.css'))
96
+ const userFilePath = get(config, 'build.tailwind.css', path.join(process.cwd(), 'src/css/tailwind.css'))
97
97
  const userFileExists = await fs.pathExists(userFilePath)
98
98
 
99
99
  const toProcess = [
100
100
  postcssNested(),
101
- tailwindcss(config),
102
- maizzleConfig.env === 'local' ? () => {} : mergeLonghand(),
103
- ...get(maizzleConfig, 'build.postcss.plugins', [])
101
+ tailwindcss(tailwindConfig),
102
+ get(config, 'shorthandCSS', get(config, 'shorthandInlineCSS')) === true ?
103
+ mergeLonghand() :
104
+ () => {},
105
+ ...get(config, 'build.postcss.plugins', [])
104
106
  ]
105
107
 
106
108
  if (userFileExists) {
107
109
  css = await fs.readFile(path.resolve(userFilePath), 'utf8') + css
110
+
108
111
  toProcess.unshift(
109
112
  postcssImport({path: path.dirname(userFilePath)})
110
113
  )
@@ -5,7 +5,7 @@ const {get, merge, isObject, isEmpty} = require('lodash')
5
5
  const defaultConfig = require('../generators/posthtml/defaultConfig')
6
6
 
7
7
  module.exports = async (html, config = {}, direct = false) => {
8
- const url = direct ? config : get(config, 'baseURL')
8
+ const url = direct ? config : get(config, 'baseURL', get(config, 'baseUrl'))
9
9
  const posthtmlOptions = merge(defaultConfig, get(config, 'build.posthtml.options', {}))
10
10
 
11
11
  // Handle `baseUrl` as a string
@@ -19,11 +19,15 @@ module.exports = async (html, config = {}, direct = false) => {
19
19
  .then(result => result.html)
20
20
  }
21
21
 
22
- // Handle `baseUrl` as an object
22
+ // Handle `baseURL` as an object
23
23
  if (isObject(url) && !isEmpty(url)) {
24
- html = rewriteVMLs(html, url.url)
24
+ html = rewriteVMLs(html, get(url, 'url', ''))
25
25
 
26
- return posthtml([baseUrl(url)]).process(html, posthtmlOptions).then(result => result.html)
26
+ return posthtml([
27
+ baseUrl(merge({styleTag: true, inlineCss: true}, url))
28
+ ])
29
+ .process(html, posthtmlOptions)
30
+ .then(result => result.html)
27
31
  }
28
32
 
29
33
  return html
@@ -22,10 +22,19 @@ module.exports = async (html, config = {}, direct = false) => {
22
22
  * Compile CSS in <style {post|tailwind}css> tags
23
23
  */
24
24
  const maizzleConfig = omit(config, ['build.tailwind.css', 'css'])
25
- const tailwindConfig = get(config, 'build.tailwind.config', 'tailwind.config.js')
26
25
 
27
26
  filters.postcss = css => PostCSS.process(css, maizzleConfig)
28
- filters.tailwindcss = css => Tailwind.compile(css, html, tailwindConfig, maizzleConfig)
27
+ filters.tailwindcss = css => Tailwind.compile({
28
+ css,
29
+ html,
30
+ config: merge({
31
+ build: {
32
+ tailwind: {
33
+ config: get(config, 'build.tailwind.config', {})
34
+ }
35
+ }
36
+ }, maizzleConfig)
37
+ })
29
38
 
30
39
  const posthtmlPlugins = [
31
40
  styleDataEmbed(),
@@ -26,9 +26,8 @@ exports.process = async (html, config) => {
26
26
  html = await preventWidows(html, config)
27
27
  html = await attributeToStyle(html, config)
28
28
  html = await inline(html, config)
29
- html = await shorthandInlineCSS(html, config)
30
29
  html = await removeUnusedCSS(html, config)
31
- html = await removeInlinedClasses(html, config)
30
+ html = await shorthandInlineCSS(html, config)
32
31
  html = await removeInlineSizes(html, config)
33
32
  html = await removeInlineBgColor(html, config)
34
33
  html = await removeAttributes(html, config)
@@ -54,7 +53,7 @@ exports.addURLParams = (html, config) => addURLParams(html, config, true)
54
53
  exports.preventWidows = (html, config) => preventWidows(html, config)
55
54
  exports.replaceStrings = (html, config) => replaceStrings(html, config, true)
56
55
  exports.safeClassNames = (html, config) => safeClassNames(html, config, true)
57
- exports.removeUnusedCSS = (html, config) => removeUnusedCSS(html, config, true)
56
+ exports.removeUnusedCSS = (html, config) => removeUnusedCSS(html, config)
58
57
  exports.removeAttributes = (html, config) => removeAttributes(html, config, true)
59
58
  exports.attributeToStyle = (html, config) => attributeToStyle(html, config, true)
60
59
  exports.removeInlineSizes = (html, config) => removeInlineSizes(html, config, true)
@@ -20,16 +20,26 @@ const plugin = posthtmlOptions => tree => {
20
20
  // For each style tag...
21
21
  if (node.tag === 'style') {
22
22
  const {root} = postcss().process(node.content)
23
+ const preservedClasses = []
23
24
 
24
- root.walkRules(rule => {
25
- // Skip media queries and such...
26
- if (rule.parent.type === 'atrule') {
27
- return
25
+ // Preserve selectors in at rules
26
+ root.walkAtRules(rule => {
27
+ if (['media', 'supports'].includes(rule.name)) {
28
+ rule.walkRules(rule => {
29
+ preservedClasses.push(rule.selector)
30
+ })
28
31
  }
32
+ })
29
33
 
34
+ root.walkRules(rule => {
30
35
  const {selector} = rule
31
36
  const prop = get(rule.nodes[0], 'prop')
32
37
 
38
+ // Preserve pseudo selectors
39
+ if (selector.includes(':')) {
40
+ preservedClasses.push(selector)
41
+ }
42
+
33
43
  try {
34
44
  // If we find the selector in the HTML...
35
45
  tree.match(matchHelper(selector), n => {
@@ -39,23 +49,16 @@ const plugin = posthtmlOptions => tree => {
39
49
 
40
50
  // If the class is in the style attribute (inlined), remove it
41
51
  if (has(styleAttr, prop)) {
42
- // Remove the class attribute
43
- remove(classAttr, s => selector.includes(s))
52
+ // Remove the class as long as it's not a preserved class
53
+ if (!preservedClasses.some(item => item.endsWith(selector) || item.startsWith(selector))) {
54
+ remove(classAttr, classToRemove => selector.includes(classToRemove))
55
+ }
44
56
 
45
57
  // Remove the rule in the <style> tag
46
- rule.remove()
47
- }
48
-
49
- /**
50
- * Remove from <style> selectors that were used to create shorthand declarations
51
- * like `margin: 0 0 0 16px` (transformed with mergeLonghand when inlining).
52
- */
53
- Object.keys(styleAttr).forEach(key => {
54
- if (prop && prop.includes(key)) {
58
+ if (rule.parent.type !== 'atrule') {
55
59
  rule.remove()
56
- remove(classAttr, s => selector.includes(s))
57
60
  }
58
- })
61
+ }
59
62
 
60
63
  n.attrs = parsedAttrs.compose()
61
64
 
@@ -1,15 +1,13 @@
1
1
  const {comb} = require('email-comb')
2
2
  const {get, merge} = require('lodash')
3
+ const removeInlinedClasses = require('./removeInlinedSelectors')
3
4
 
4
- module.exports = async (html, config = {}, direct = false) => {
5
+ module.exports = async (html, config = {}) => {
6
+ // If it's explicitly disabled, return the HTML
5
7
  if (get(config, 'removeUnusedCSS') === false) {
6
8
  return html
7
9
  }
8
10
 
9
- if (!direct && !get(config, 'removeUnusedCSS')) {
10
- return html
11
- }
12
-
13
11
  const safelist = [
14
12
  '*body*', // Gmail
15
13
  '.gmail*', // Gmail
@@ -36,9 +34,9 @@ module.exports = async (html, config = {}, direct = false) => {
36
34
  whitelist: [...get(config, 'whitelist', []), ...safelist]
37
35
  }
38
36
 
39
- const options = typeof config === 'boolean' && config ?
40
- defaultOptions :
41
- merge(defaultOptions, get(config, 'removeUnusedCSS', config))
37
+ const options = merge(defaultOptions, get(config, 'removeUnusedCSS', config))
38
+
39
+ html = await removeInlinedClasses(html, options)
42
40
 
43
41
  return comb(html, options).result
44
42
  }
@@ -1,19 +1,25 @@
1
1
  const posthtml = require('posthtml')
2
2
  const {get, merge, isObject, isEmpty} = require('lodash')
3
- const mergeLonghand = require('posthtml-postcss-merge-longhand')
3
+ const mergeInlineLonghand = require('posthtml-postcss-merge-longhand')
4
4
  const defaultConfig = require('../generators/posthtml/defaultConfig')
5
5
 
6
6
  module.exports = async (html, config, direct = false) => {
7
- config = direct ? (isObject(config) ? config : true) : get(config, 'shorthandInlineCSS', [])
7
+ config = direct ?
8
+ (isObject(config) ? config : true) :
9
+ get(config, 'shorthandCSS', get(config, 'shorthandInlineCSS', []))
8
10
 
9
11
  const posthtmlOptions = merge(defaultConfig, get(config, 'build.posthtml.options', {}))
10
12
 
11
13
  if (typeof config === 'boolean' && config) {
12
- html = await posthtml([mergeLonghand()]).process(html, posthtmlOptions).then(result => result.html)
14
+ html = await posthtml([mergeInlineLonghand()])
15
+ .process(html, posthtmlOptions)
16
+ .then(result => result.html)
13
17
  }
14
18
 
15
19
  if (isObject(config) && !isEmpty(config)) {
16
- html = await posthtml([mergeLonghand(config)]).process(html, posthtmlOptions).then(result => result.html)
20
+ html = await posthtml([mergeInlineLonghand(config)])
21
+ .process(html, posthtmlOptions)
22
+ .then(result => result.html)
17
23
  }
18
24
 
19
25
  return html
@@ -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)]