@maizzle/framework 5.0.0-beta.2 → 5.0.0-beta.20

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": "5.0.0-beta.2",
3
+ "version": "5.0.0-beta.20",
4
4
  "description": "Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,9 +23,10 @@
23
23
  },
24
24
  "scripts": {
25
25
  "dev": "vitest",
26
- "lint": "biome lint ./src ./test",
26
+ "release": "npx np",
27
27
  "pretest": "npm run lint",
28
- "test": "vitest run --coverage"
28
+ "test": "vitest run --coverage",
29
+ "lint": "biome lint ./src ./test"
29
30
  },
30
31
  "repository": {
31
32
  "type": "git",
@@ -47,7 +48,7 @@
47
48
  "html-emails"
48
49
  ],
49
50
  "dependencies": {
50
- "@csstools/css-calc": "^1.2.2",
51
+ "@csstools/css-calc": "^1.2.4",
51
52
  "@maizzle/cli": "next",
52
53
  "cheerio": "^1.0.0-rc.12",
53
54
  "chokidar": "^3.6.0",
@@ -63,35 +64,37 @@
63
64
  "istextorbinary": "^9.5.0",
64
65
  "juice": "^10.0.0",
65
66
  "lodash-es": "^4.17.21",
66
- "morphdom": "^2.7.2",
67
+ "morphdom": "^2.7.4",
67
68
  "ora": "^8.0.1",
68
69
  "pathe": "^1.1.2",
69
- "postcss": "^8.4.38",
70
- "postcss-custom-properties": "^13.3.10",
70
+ "postcss": "^8.4.39",
71
+ "postcss-custom-properties": "^13.3.12",
71
72
  "postcss-import": "^16.1.0",
72
73
  "postcss-safe-parser": "^7.0.0",
73
74
  "posthtml": "^0.16.6",
74
- "posthtml-attrs-parser": "^1.1.0",
75
- "posthtml-base-url": "^3.1.2",
76
- "posthtml-component": "^1.1.0",
77
- "posthtml-content": "^2.0.1",
78
- "posthtml-extra-attributes": "^3.0.0",
79
- "posthtml-markdownit": "^3.0.1",
80
- "posthtml-mso": "^3.0.0",
75
+ "posthtml-attrs-parser": "^1.1.1",
76
+ "posthtml-base-url": "^3.1.4",
77
+ "posthtml-component": "^2.0.0",
78
+ "posthtml-content": "^2.1.0",
79
+ "posthtml-expressions": "^1.11.4",
80
+ "posthtml-extra-attributes": "^3.1.0",
81
+ "posthtml-fetch": "^4.0.0",
82
+ "posthtml-markdownit": "^3.1.0",
83
+ "posthtml-mso": "^3.1.0",
81
84
  "posthtml-parser": "^0.12.0",
82
- "posthtml-postcss": "^1.0.0",
83
- "posthtml-postcss-merge-longhand": "^3.1.0",
85
+ "posthtml-postcss": "^1.0.2",
86
+ "posthtml-postcss-merge-longhand": "^3.1.2",
84
87
  "posthtml-render": "^3.0.0",
85
- "posthtml-safe-class-names": "^4.0.2",
86
- "posthtml-url-parameters": "^3.0.0",
88
+ "posthtml-safe-class-names": "^4.1.0",
89
+ "posthtml-url-parameters": "^3.1.0",
87
90
  "pretty": "^2.0.0",
88
91
  "string-remove-widows": "^4.0.22",
89
92
  "string-strip-html": "^13.4.8",
90
- "tailwindcss": "^3.4.4",
93
+ "tailwindcss": "^3.4.7",
91
94
  "ws": "^8.17.0"
92
95
  },
93
96
  "devDependencies": {
94
- "@biomejs/biome": "^1.8.3",
97
+ "@biomejs/biome": "1.8.3",
95
98
  "@types/js-beautify": "^1.14.3",
96
99
  "@types/markdown-it": "^14.1.1",
97
100
  "@vitest/coverage-v8": "^2.0.1",
@@ -4,7 +4,8 @@ import {
4
4
  copyFile,
5
5
  lstat,
6
6
  mkdir,
7
- rm
7
+ rm,
8
+ cp,
8
9
  } from 'node:fs/promises'
9
10
  import path from 'pathe'
10
11
  import fg from 'fast-glob'
@@ -12,22 +13,50 @@ import { defu as merge } from 'defu'
12
13
 
13
14
  import get from 'lodash/get.js'
14
15
  import isEmpty from 'lodash-es/isEmpty.js'
15
- import { isBinary } from 'istextorbinary'
16
16
 
17
17
  import ora from 'ora'
18
18
  import pico from 'picocolors'
19
19
  import cliTable from 'cli-table3'
20
20
 
21
21
  import { render } from '../generators/render.js'
22
- import { formatTime } from '../utils/string.js'
22
+
23
+ import {
24
+ formatTime,
25
+ getRootDirectories,
26
+ getFileExtensionsFromPattern,
27
+ } from '../utils/string.js'
28
+
23
29
  import { getColorizedFileSize } from '../utils/node.js'
24
- import { readFileConfig } from '../utils/getConfigByFilePath.js'
30
+
25
31
  import {
26
32
  generatePlaintext,
27
33
  handlePlaintextTags,
28
34
  writePlaintextFile
29
35
  } from '../generators/plaintext.js'
30
36
 
37
+ import { readFileConfig } from '../utils/getConfigByFilePath.js'
38
+
39
+ /**
40
+ * Ensures that a directory exists, creating it if needed.
41
+ *
42
+ * @param {string} filePath - The path to the file to check.
43
+ */
44
+ async function ensureDirectoryExistence(filePath) {
45
+ const dirname = path.dirname(filePath)
46
+ await mkdir(dirname, { recursive: true })
47
+ }
48
+
49
+ /**
50
+ * Copy a file from source to target.
51
+ *
52
+ * @param {string} source - The source file path.
53
+ * @param {string} target - The target file path.
54
+ */
55
+ async function copyFileAsync(source, target) {
56
+ await ensureDirectoryExistence(target)
57
+ await copyFile(source, target)
58
+ }
59
+
31
60
  /**
32
61
  * Compile templates and output to the build directory.
33
62
  * Returns a promise containing an object with files output and the config object.
@@ -41,7 +70,10 @@ export default async (config = {}) => {
41
70
  try {
42
71
  const startTime = Date.now()
43
72
 
44
- // Compute config
73
+ /**
74
+ * Read the config file for this environment,
75
+ * merging it with the default config.
76
+ */
45
77
  config = await readFileConfig(config).catch(() => { throw new Error('Could not compute config') })
46
78
 
47
79
  /**
@@ -72,9 +104,12 @@ export default async (config = {}) => {
72
104
  head: ['File name', 'File size', 'Build time'].map(item => pico.bold(item)),
73
105
  })
74
106
 
75
- // Determine paths of templates to build
76
- const userFilePaths = get(config, 'build.content', 'src/templates/**/*.html')
77
- const templateFolders = Array.isArray(userFilePaths) ? userFilePaths : [userFilePaths]
107
+ /**
108
+ * Check that templates to be built, actually exist
109
+ */
110
+ const contentPaths = get(config, 'build.content', ['src/templates/**/*.html'])
111
+
112
+ const templateFolders = Array.isArray(contentPaths) ? contentPaths : [contentPaths]
78
113
  const templatePaths = await fg.glob([...new Set(templateFolders)])
79
114
 
80
115
  // If there are no templates to build, throw error
@@ -82,37 +117,84 @@ export default async (config = {}) => {
82
117
  throw new Error(`No templates found in ${pico.inverse(templateFolders)}`)
83
118
  }
84
119
 
85
- const baseDirs = templateFolders.filter(p => !p.startsWith('!')).map(p => {
86
- const parts = p.split('/')
87
- // remove the glob part (e.g., **/*.html):
88
- return parts.filter(part => !part.includes('*')).join('/')
120
+ /**
121
+ * Copy source directories to destination
122
+ *
123
+ * Copies each `build.content` path to the `build.output.path` directory.
124
+ */
125
+ let from = get(config, 'build.output.from', ['src/templates', 'src'])
126
+
127
+ const globPathsToCopy = contentPaths.map(glob => {
128
+ // Keep negated paths as they are
129
+ if (glob.startsWith('!')) {
130
+ return glob
131
+ }
132
+
133
+ // Keep single-file sources as they are
134
+ if (!/\*/.test(glob)) {
135
+ return glob
136
+ }
137
+
138
+ // Update non-negated paths to target all files, avoiding duplication
139
+ return glob.replace(/\/\*\*\/\*\.\{.*?\}$|\/\*\*\/\*\.[^/]*$|\/*\.[^/]*$/, '/**/*')
89
140
  })
90
141
 
142
+ try {
143
+ from = Array.isArray(from) ? from : [from]
144
+
145
+ /**
146
+ * Copy files from source to destination
147
+ *
148
+ * The array/set conversion is to remove duplicates
149
+ */
150
+ for (const file of await fg(Array.from(new Set(globPathsToCopy)))) {
151
+ let relativePath
152
+ for (const dir of from) {
153
+ if (file.startsWith(dir)) {
154
+ relativePath = path.relative(dir, file)
155
+ break
156
+ }
157
+ }
158
+ if (!relativePath) {
159
+ relativePath = path.relative('.', file)
160
+ }
161
+
162
+ const targetPath = path.join(config.build.output.path, relativePath)
163
+ await copyFileAsync(file, targetPath)
164
+ }
165
+ } catch (error) {
166
+ console.error(`Error while processing pattern ${pattern}: `, err);
167
+ }
168
+
91
169
  /**
92
- * Check for binary files
170
+ * Get a list of files to render, from the output directory
93
171
  *
94
- * We store paths to binary files in a separate array, because we don't want
95
- * to render them. These files will be treated as static files and will
96
- * be copied directly to the output directory, just like the
97
- * `build.static` folders.
172
+ * Uses all file extensions from non-negated glob paths in `build.content`
173
+ * to determine which files to render from the output directory.
98
174
  */
99
- const binaryPaths = await fg.glob([...new Set(baseDirs.map(base => `${base}/**/*.*`))])
100
- .then(paths => paths.filter(file => isBinary(file)))
175
+ const outputExtensions = new Set()
176
+
177
+ for (const pattern of contentPaths) {
178
+ outputExtensions.add(...getFileExtensionsFromPattern(pattern))
179
+ }
101
180
 
102
181
  /**
103
- * Render templates
104
- *
105
- * Render each template and write the output to the output directory,
106
- * preserving the relative path.
182
+ * Create a list of templates to compile
107
183
  */
108
- for await (const templatePath of templatePaths) {
109
- const templateBuildStartTime = Date.now()
184
+ const extensions = outputExtensions.size > 1 ? `{${[...outputExtensions].join(',')}}` : 'html'
110
185
 
111
- // Determine the base directory the template belongs to
112
- const baseDir = baseDirs.find(base => templatePath.startsWith(base))
186
+ const templatesToCompile = await fg.glob(
187
+ path.join(
188
+ buildOutputPath,
189
+ `**/*.${extensions}`
190
+ )
191
+ )
113
192
 
114
- // Compute the relative path
115
- const relativePath = path.relative(baseDir, templatePath)
193
+ /**
194
+ * Render templates
195
+ */
196
+ for await (const templatePath of templatesToCompile) {
197
+ const templateBuildStartTime = Date.now()
116
198
 
117
199
  /**
118
200
  * Add the current template path to the config
@@ -122,8 +204,6 @@ export default async (config = {}) => {
122
204
  */
123
205
  config.build.current = {
124
206
  path: path.parse(templatePath),
125
- baseDir,
126
- relativePath,
127
207
  }
128
208
 
129
209
  const html = await readFile(templatePath, 'utf8')
@@ -158,8 +238,9 @@ export default async (config = {}) => {
158
238
  * We do this before generating plaintext, so that
159
239
  * any paths will already have been created.
160
240
  */
161
- const outputPathFromConfig = get(rendered.config, 'permalink', path.join(buildOutputPath, relativePath))
241
+ const outputPathFromConfig = get(rendered.config, 'permalink', templatePath)
162
242
  const parsedOutputPath = path.parse(outputPathFromConfig)
243
+ // This keeps original file extension if no output extension is set
163
244
  const extension = get(rendered.config, 'build.output.extension', parsedOutputPath.ext.slice(1))
164
245
  const outputPath = `${parsedOutputPath.dir}/${parsedOutputPath.name}.${extension}`
165
246
 
@@ -175,62 +256,49 @@ export default async (config = {}) => {
175
256
  await writeFile(outputPath, rendered.html)
176
257
 
177
258
  /**
178
- * Add file to CLI table for build summary logging
259
+ * Remove original file if its path is different
260
+ * from the final destination path.
179
261
  */
180
- if (config.build.summary) {
181
- table.push([
182
- path.relative(get(rendered.config, 'build.output.path'), outputPath),
183
- getColorizedFileSize(rendered.html),
184
- formatTime(Date.now() - templateBuildStartTime)
185
- ])
262
+ if (outputPath !== templatePath) {
263
+ await rm(templatePath)
186
264
  }
265
+
266
+ /**
267
+ * Add file to CLI table for build summary logging
268
+ */
269
+ table.push([
270
+ path.relative(get(rendered.config, 'build.output.path'), outputPath),
271
+ getColorizedFileSize(rendered.html),
272
+ formatTime(Date.now() - templateBuildStartTime)
273
+ ])
187
274
  }
188
275
 
189
276
  /**
190
277
  * Copy static files
191
278
  *
192
- * Copy binary files that are alongside templates as well as
193
- * files from `build.static`, to the output directory.
194
- *
195
- * TODO: support an array of objects with source and destination, i.e. static: [{ source: 'src/assets', destination: 'assets' }, ...]
279
+ * TODO: support an array of objects with source and destination,
280
+ * i.e. static: [{ source: 'src/assets', destination: 'assets' }, ...]
196
281
  */
282
+ const staticSourcePaths = getRootDirectories([...new Set(get(config, 'build.static.source', []))])
197
283
 
198
- // Copy binary files that are alongside templates
199
- for await (const binaryPath of binaryPaths) {
200
- const relativePath = path.relative(get(config, 'build.current.baseDir'), binaryPath)
201
- const outputPath = path.join(get(config, 'build.output.path'), get(config, 'build.static.destination'), relativePath)
202
-
203
- await mkdir(path.dirname(outputPath), { recursive: true })
204
- await copyFile(binaryPath, outputPath)
284
+ for await (const rootDir of staticSourcePaths) {
285
+ await cp(rootDir, path.join(buildOutputPath, get(config, 'build.static.destination')), { recursive: true })
205
286
  }
206
287
 
207
- // Copy files from `build.static`
208
- const staticSourcePaths = await fg.glob([...new Set(get(config, 'build.static.source', []))])
209
- .then(paths => paths.filter(file => isBinary(file)))
210
-
211
- if (!isEmpty(staticSourcePaths)) {
212
- for await (const staticPath of staticSourcePaths) {
213
- const relativePath = path.relative(get(config, 'build.current.baseDir'), staticPath)
214
- const outputPath = path.join(get(config, 'build.output.path'), get(config, 'build.static.destination'), relativePath)
215
-
216
- await mkdir(path.dirname(outputPath), { recursive: true })
217
- await copyFile(staticPath, outputPath)
218
- }
219
- }
220
-
221
- const compiledFiles = await fg.glob(path.join(config.build.output.path, '**/*'))
288
+ const allOutputFiles = await fg.glob(path.join(buildOutputPath, '**/*'))
222
289
 
223
290
  /**
224
291
  * Run `afterBuild` event
225
292
  */
226
293
  if (typeof config.afterBuild === 'function') {
227
- await config.afterBuild({ files: compiledFiles, config, render })
294
+ await config.afterBuild({
295
+ config,
296
+ files: allOutputFiles,
297
+ })
228
298
  }
229
299
 
230
300
  /**
231
301
  * Log a build summary if enabled in the config
232
- *
233
- * Need to first clear the spinner
234
302
  */
235
303
 
236
304
  spinner.clear()
@@ -239,10 +307,10 @@ export default async (config = {}) => {
239
307
  console.log(table.toString() + '\n')
240
308
  }
241
309
 
242
- spinner.succeed(`Build completed in ${formatTime(Date.now() - startTime)}`)
310
+ spinner.succeed(`Built ${table.length} template${table.length > 1 ? 's' : ''} in ${formatTime(Date.now() - startTime)}`)
243
311
 
244
312
  return {
245
- files: compiledFiles,
313
+ files: allOutputFiles,
246
314
  config
247
315
  }
248
316
  } catch (error) {
@@ -134,7 +134,7 @@ export async function generatePlaintext(html = '', config = {}) {
134
134
  ).result
135
135
  }
136
136
 
137
- export async function writePlaintextFile(plaintext = '', templateConfig = {}) {
137
+ export async function writePlaintextFile(plaintext = '', config = {}) {
138
138
  if (!plaintext) {
139
139
  throw new Error('Missing plaintext content.')
140
140
  }
@@ -149,8 +149,8 @@ export async function writePlaintextFile(plaintext = '', templateConfig = {}) {
149
149
  * Fall back to template's build output path and extension, for example:
150
150
  * `config.build.output.path`
151
151
  */
152
- const plaintextConfig = get(templateConfig, 'plaintext')
153
- let plaintextOutputPath = get(plaintextConfig, 'output.path', get(templateConfig, 'build.output.path'))
152
+ const plaintextConfig = get(config, 'plaintext')
153
+ let plaintextOutputPath = get(plaintextConfig, 'output.path', get(config, 'build.output.path'))
154
154
  const plaintextExtension = get(plaintextConfig, 'output.extension', 'txt')
155
155
 
156
156
  /**
@@ -158,15 +158,12 @@ export async function writePlaintextFile(plaintext = '', templateConfig = {}) {
158
158
  */
159
159
  if (plaintextConfig === true) {
160
160
  // If the template has a `permalink` key set in the FM
161
- if (typeof templateConfig.permalink === 'string') {
161
+ if (typeof config.permalink === 'string') {
162
162
  // Output plaintext at the `permalink` path
163
- plaintextOutputPath = templateConfig.permalink
163
+ plaintextOutputPath = config.permalink
164
164
  } else {
165
165
  // Output plaintext at the same directory as the HTML file
166
- plaintextOutputPath = path.join(
167
- get(templateConfig, 'build.output.path'),
168
- get(templateConfig, 'build.current.relativePath')
169
- )
166
+ plaintextOutputPath = get(config, 'build.output.path')
170
167
  }
171
168
  }
172
169
 
@@ -186,8 +183,8 @@ export async function writePlaintextFile(plaintext = '', templateConfig = {}) {
186
183
  */
187
184
  if (path.extname(plaintextOutputPath)) {
188
185
  // Ensure the target directory exists
189
- await lstat(path.dirname(plaintextOutputPath)).catch(async () => {
190
- await mkdir(path.dirname(plaintextOutputPath), { recursive: true })
186
+ await lstat(plaintextOutputPath).catch(async () => {
187
+ await mkdir(plaintextOutputPath, { recursive: true })
191
188
  })
192
189
 
193
190
  // Ensure correct extension is used
@@ -196,17 +193,19 @@ export async function writePlaintextFile(plaintext = '', templateConfig = {}) {
196
193
  path.basename(plaintextOutputPath, path.extname(plaintextOutputPath)) + '.' + plaintextExtension
197
194
  )
198
195
 
196
+ console.log('plaintextOutputPath', plaintextOutputPath);
197
+
199
198
  return writeFile(plaintextOutputPath, plaintext)
200
199
  }
201
200
 
202
201
  /**
203
202
  * If `plaintextOutputPath` is a directory path, output file there, using the template's name
204
203
  */
205
- const templateFileName = get(templateConfig, 'build.current.path.name')
204
+ const templateFileName = get(config, 'build.current.path.name')
206
205
 
207
206
  plaintextOutputPath = path.join(
208
- plaintextOutputPath,
209
- get(templateConfig, 'build.current.path.dir'),
207
+ path.dirname(plaintextOutputPath),
208
+ get(config, 'build.current.path.dir'),
210
209
  templateFileName + '.' + plaintextExtension
211
210
  )
212
211
 
@@ -4,6 +4,7 @@ import { cwd } from 'node:process'
4
4
  import { defu as merge } from 'defu'
5
5
  import expressions from 'posthtml-expressions'
6
6
  import { parseFrontMatter } from '../utils/node.js'
7
+ import defaultConfig from '../posthtml/defaultConfig.js'
7
8
  import { process as compilePostHTML } from '../posthtml/index.js'
8
9
  import { run as useTransformers } from '../transformers/index.js'
9
10
 
@@ -40,7 +41,7 @@ export async function render(html = '', config = {}) {
40
41
  })
41
42
  ]
42
43
  )
43
- .process(matter)
44
+ .process(matter, defaultConfig)
44
45
  .then(({ html }) => parseFrontMatter(`---${html}\n---`))
45
46
 
46
47
  const templateConfig = merge(matterData, config)
@@ -57,15 +58,15 @@ export async function render(html = '', config = {}) {
57
58
  *
58
59
  * @param {Object} options
59
60
  * @param {string} options.html - The HTML to be transformed
61
+ * @param {Object} options.matter - The front matter data
60
62
  * @param {Object} options.config - The current template config
61
- * @param {function} options.render - The render function
62
63
  * @returns {string} - The transformed HTML, or the original one if nothing was returned
63
64
  */
64
65
  if (typeof templateConfig.beforeRender === 'function') {
65
66
  content = await templateConfig.beforeRender(({
66
67
  html: content,
68
+ matter: matterData,
67
69
  config: templateConfig,
68
- render
69
70
  })) ?? content
70
71
  }
71
72
 
@@ -77,15 +78,15 @@ export async function render(html = '', config = {}) {
77
78
  *
78
79
  * @param {Object} options
79
80
  * @param {string} options.html - The HTML to be transformed
81
+ * @param {Object} options.matter - The front matter data
80
82
  * @param {Object} options.config - The current template config
81
- * @param {function} options.render - The render function
82
83
  * @returns {string} - The transformed HTML, or the original one if nothing was returned
83
84
  */
84
85
  if (typeof templateConfig.afterRender === 'function') {
85
86
  compiled.html = await templateConfig.afterRender(({
86
87
  html: compiled.html,
88
+ matter: matterData,
87
89
  config: templateConfig,
88
- render
89
90
  })) ?? compiled.html
90
91
  }
91
92
 
@@ -109,15 +110,15 @@ export async function render(html = '', config = {}) {
109
110
  *
110
111
  * @param {Object} options
111
112
  * @param {string} options.html - The HTML to be transformed
113
+ * @param {Object} options.matter - The front matter data
112
114
  * @param {Object} options.config - The current template config
113
- * @param {function} options.render - The render function
114
115
  * @returns {string} - The transformed HTML, or the original one if nothing was returned
115
116
  */
116
117
  if (typeof templateConfig.afterTransformers === 'function') {
117
118
  compiled.html = await templateConfig.afterTransformers(({
118
119
  html: compiled.html,
120
+ matter: matterData,
119
121
  config: templateConfig,
120
- render
121
122
  })) ?? compiled.html
122
123
  }
123
124
 
@@ -3,10 +3,13 @@ import { defu as merge } from 'defu'
3
3
 
4
4
  // PostHTML
5
5
  import posthtml from 'posthtml'
6
+ import posthtmlFetch from 'posthtml-fetch'
7
+ import envTags from './plugins/envTags.js'
6
8
  import components from 'posthtml-component'
7
9
  import posthtmlPostcss from 'posthtml-postcss'
8
10
  import defaultPosthtmlConfig from './defaultConfig.js'
9
11
  import expandLinkTag from './plugins/expandLinkTag.js'
12
+ import envAttributes from './plugins/envAttributes.js'
10
13
 
11
14
  // PostCSS
12
15
  import tailwindcss from 'tailwindcss'
@@ -48,19 +51,45 @@ export async function process(html = '', config = {}) {
48
51
  { page: config },
49
52
  )
50
53
 
54
+ const fetchPlugin = posthtmlFetch(
55
+ merge(
56
+ {
57
+ expressions: merge(
58
+ { locals },
59
+ expressionsOptions,
60
+ {
61
+ missingLocal: '{local}',
62
+ strictMode: false,
63
+ },
64
+ ),
65
+ },
66
+ get(config, 'fetch', {})
67
+ )
68
+ )
69
+
51
70
  return posthtml([
52
71
  ...get(config, 'posthtml.plugins.before', []),
72
+ envTags(config.env),
73
+ envAttributes(config.env),
53
74
  expandLinkTag,
54
75
  postcssPlugin,
76
+ fetchPlugin,
55
77
  components(
56
- merge({
57
- expressions: {
58
- locals,
59
- }
60
- }, defaultComponentsConfig)
78
+ merge(
79
+ {
80
+ expressions: merge(
81
+ { locals },
82
+ expressionsOptions,
83
+ )
84
+ },
85
+ componentsUserOptions,
86
+ defaultComponentsConfig
87
+ )
61
88
  ),
62
89
  expandLinkTag,
63
90
  postcssPlugin,
91
+ envTags(config.env),
92
+ envAttributes(config.env),
64
93
  ...get(config, 'posthtml.plugins.after', get(config, 'posthtml.plugins', []))
65
94
  ])
66
95
  .process(html, posthtmlOptions)
@@ -0,0 +1,32 @@
1
+ const plugin = (env => tree => {
2
+ const process = node => {
3
+ // Return the original node if no environment is set
4
+ if (!env) {
5
+ return node
6
+ }
7
+
8
+ if (node.attrs) {
9
+ for (const attr in node.attrs) {
10
+ const suffix = `-${env}`
11
+
12
+ // Find attributes on this node that have this suffix
13
+ if (attr.endsWith(suffix)) {
14
+ const key = attr.slice(0, -suffix.length)
15
+ const value = node.attrs[attr]
16
+
17
+ // Change the attribute without the suffix to have the value of the suffixed attribute
18
+ node.attrs[key] = value
19
+
20
+ // Remove the attribute with the suffix
21
+ node.attrs[attr] = false
22
+ }
23
+ }
24
+ }
25
+
26
+ return node
27
+ }
28
+
29
+ return tree.walk(process)
30
+ })
31
+
32
+ export default plugin
@@ -0,0 +1,33 @@
1
+ const plugin = (env => tree => {
2
+ const process = node => {
3
+ env = env || 'local'
4
+
5
+ // Return the original node if it doesn't have a tag
6
+ if (!node.tag) {
7
+ return node
8
+ }
9
+
10
+ const tagEnv = node.tag.split(':').pop()
11
+
12
+ // Tag targets current env, remove it and return its content
13
+ if (node.tag === `env:${env}`) {
14
+ node.tag = false
15
+ }
16
+
17
+ // Tag doesn't target current env, remove it completely
18
+ if (
19
+ typeof node.tag === 'string'
20
+ && node.tag.startsWith('env:')
21
+ && tagEnv !== env
22
+ ) {
23
+ node.content = []
24
+ node.tag = false
25
+ }
26
+
27
+ return node
28
+ }
29
+
30
+ return tree.walk(process)
31
+ })
32
+
33
+ export default plugin