@maizzle/framework 5.0.0-beta.0 → 5.0.0-beta.10
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 +10 -10
- package/src/commands/build.js +84 -71
- package/src/generators/plaintext.js +13 -14
- package/src/generators/render.js +7 -4
- package/src/posthtml/index.js +4 -0
- package/src/posthtml/plugins/envAttributes.js +32 -0
- package/src/posthtml/plugins/envTags.js +33 -0
- package/src/server/client.js +2 -1
- package/src/server/routes/hmr.js +3 -2
- package/src/transformers/core.js +12 -0
- package/src/transformers/index.js +31 -3
- package/src/transformers/inline.js +4 -3
- package/src/transformers/template.js +26 -0
- package/src/utils/node.js +6 -0
- package/src/utils/string.js +79 -0
- package/types/build.d.ts +9 -3
- package/types/config.d.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maizzle/framework",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.10",
|
|
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,10 +23,10 @@
|
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"dev": "vitest",
|
|
26
|
-
"
|
|
26
|
+
"release": "npx np",
|
|
27
27
|
"pretest": "npm run lint",
|
|
28
28
|
"test": "vitest run --coverage",
|
|
29
|
-
"
|
|
29
|
+
"lint": "biome lint ./src ./test"
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"html-emails"
|
|
49
49
|
],
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@csstools/css-calc": "^1.2.
|
|
51
|
+
"@csstools/css-calc": "^1.2.4",
|
|
52
52
|
"@maizzle/cli": "next",
|
|
53
53
|
"cheerio": "^1.0.0-rc.12",
|
|
54
54
|
"chokidar": "^3.6.0",
|
|
@@ -64,16 +64,16 @@
|
|
|
64
64
|
"istextorbinary": "^9.5.0",
|
|
65
65
|
"juice": "^10.0.0",
|
|
66
66
|
"lodash-es": "^4.17.21",
|
|
67
|
-
"morphdom": "^2.7.
|
|
67
|
+
"morphdom": "^2.7.4",
|
|
68
68
|
"ora": "^8.0.1",
|
|
69
69
|
"pathe": "^1.1.2",
|
|
70
|
-
"postcss": "^8.4.
|
|
71
|
-
"postcss-custom-properties": "^13.3.
|
|
70
|
+
"postcss": "^8.4.39",
|
|
71
|
+
"postcss-custom-properties": "^13.3.12",
|
|
72
72
|
"postcss-import": "^16.1.0",
|
|
73
73
|
"postcss-safe-parser": "^7.0.0",
|
|
74
74
|
"posthtml": "^0.16.6",
|
|
75
75
|
"posthtml-attrs-parser": "^1.1.0",
|
|
76
|
-
"posthtml-base-url": "^3.1.
|
|
76
|
+
"posthtml-base-url": "^3.1.4",
|
|
77
77
|
"posthtml-component": "^1.1.0",
|
|
78
78
|
"posthtml-content": "^2.0.1",
|
|
79
79
|
"posthtml-extra-attributes": "^3.0.0",
|
|
@@ -88,11 +88,11 @@
|
|
|
88
88
|
"pretty": "^2.0.0",
|
|
89
89
|
"string-remove-widows": "^4.0.22",
|
|
90
90
|
"string-strip-html": "^13.4.8",
|
|
91
|
-
"tailwindcss": "^3.4.
|
|
91
|
+
"tailwindcss": "^3.4.5",
|
|
92
92
|
"ws": "^8.17.0"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@biomejs/biome": "
|
|
95
|
+
"@biomejs/biome": "1.8.3",
|
|
96
96
|
"@types/js-beautify": "^1.14.3",
|
|
97
97
|
"@types/markdown-it": "^14.1.1",
|
|
98
98
|
"@vitest/coverage-v8": "^2.0.1",
|
package/src/commands/build.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
readFile,
|
|
3
3
|
writeFile,
|
|
4
|
-
copyFile,
|
|
5
4
|
lstat,
|
|
6
5
|
mkdir,
|
|
7
|
-
rm
|
|
6
|
+
rm,
|
|
7
|
+
cp,
|
|
8
8
|
} from 'node:fs/promises'
|
|
9
9
|
import path from 'pathe'
|
|
10
10
|
import fg from 'fast-glob'
|
|
@@ -12,22 +12,31 @@ import { defu as merge } from 'defu'
|
|
|
12
12
|
|
|
13
13
|
import get from 'lodash/get.js'
|
|
14
14
|
import isEmpty from 'lodash-es/isEmpty.js'
|
|
15
|
-
import { isBinary } from 'istextorbinary'
|
|
16
15
|
|
|
17
16
|
import ora from 'ora'
|
|
18
17
|
import pico from 'picocolors'
|
|
19
18
|
import cliTable from 'cli-table3'
|
|
20
19
|
|
|
21
20
|
import { render } from '../generators/render.js'
|
|
22
|
-
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
formatTime,
|
|
24
|
+
getRootDirectories,
|
|
25
|
+
getFileExtensionsFromPattern,
|
|
26
|
+
} from '../utils/string.js'
|
|
27
|
+
|
|
23
28
|
import { getColorizedFileSize } from '../utils/node.js'
|
|
24
|
-
|
|
29
|
+
|
|
25
30
|
import {
|
|
26
31
|
generatePlaintext,
|
|
27
32
|
handlePlaintextTags,
|
|
28
33
|
writePlaintextFile
|
|
29
34
|
} from '../generators/plaintext.js'
|
|
30
35
|
|
|
36
|
+
import { readFileConfig } from '../utils/getConfigByFilePath.js'
|
|
37
|
+
|
|
38
|
+
import { transformers } from '../transformers/index.js'
|
|
39
|
+
|
|
31
40
|
/**
|
|
32
41
|
* Compile templates and output to the build directory.
|
|
33
42
|
* Returns a promise containing an object with files output and the config object.
|
|
@@ -72,9 +81,19 @@ export default async (config = {}) => {
|
|
|
72
81
|
head: ['File name', 'File size', 'Build time'].map(item => pico.bold(item)),
|
|
73
82
|
})
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Determine paths to handle
|
|
86
|
+
*
|
|
87
|
+
* 1. Resolve globs in `build.content` to folders that should be copied over to `build.output.path`
|
|
88
|
+
* 2. Check that templates to be built, actually exist
|
|
89
|
+
*/
|
|
90
|
+
const contentPaths = get(config, 'build.content', 'src/templates/**/*.html')
|
|
91
|
+
|
|
92
|
+
// 1. Resolve globs in `build.content` to folders that should be copied over to `build.output.path`
|
|
93
|
+
const rootDirs = await getRootDirectories(contentPaths)
|
|
94
|
+
|
|
95
|
+
// 2. Check that templates to be built, actually exist
|
|
96
|
+
const templateFolders = Array.isArray(contentPaths) ? contentPaths : [contentPaths]
|
|
78
97
|
const templatePaths = await fg.glob([...new Set(templateFolders)])
|
|
79
98
|
|
|
80
99
|
// If there are no templates to build, throw error
|
|
@@ -82,37 +101,44 @@ export default async (config = {}) => {
|
|
|
82
101
|
throw new Error(`No templates found in ${pico.inverse(templateFolders)}`)
|
|
83
102
|
}
|
|
84
103
|
|
|
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('/')
|
|
89
|
-
})
|
|
90
|
-
|
|
91
104
|
/**
|
|
92
|
-
*
|
|
105
|
+
* Copy source directories to destination
|
|
93
106
|
*
|
|
94
|
-
*
|
|
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.
|
|
107
|
+
* Copies each `build.content` path to the `build.output.path` directory.
|
|
98
108
|
*/
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
for await (const rootDir of rootDirs) {
|
|
110
|
+
await cp(rootDir, buildOutputPath, { recursive: true })
|
|
111
|
+
}
|
|
101
112
|
|
|
102
113
|
/**
|
|
103
|
-
*
|
|
114
|
+
* Get a list of files to render, from the output directory
|
|
104
115
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
116
|
+
* Uses all file extensions from non-negated glob paths in `build.content`
|
|
117
|
+
* to determine which files to render from the output directory.
|
|
107
118
|
*/
|
|
108
|
-
|
|
109
|
-
const templateBuildStartTime = Date.now()
|
|
119
|
+
const outputExtensions = new Set()
|
|
110
120
|
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
for (const pattern of contentPaths) {
|
|
122
|
+
outputExtensions.add(...getFileExtensionsFromPattern(pattern))
|
|
123
|
+
}
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Create a list of templates to compile
|
|
127
|
+
*/
|
|
128
|
+
const extensions = outputExtensions.size > 1 ? `{${[...outputExtensions].join(',')}}` : 'html'
|
|
129
|
+
|
|
130
|
+
const templatesToCompile = await fg.glob(
|
|
131
|
+
path.join(
|
|
132
|
+
buildOutputPath,
|
|
133
|
+
`**/*.${extensions}`
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Render templates
|
|
139
|
+
*/
|
|
140
|
+
for await (const templatePath of templatesToCompile) {
|
|
141
|
+
const templateBuildStartTime = Date.now()
|
|
116
142
|
|
|
117
143
|
/**
|
|
118
144
|
* Add the current template path to the config
|
|
@@ -122,8 +148,6 @@ export default async (config = {}) => {
|
|
|
122
148
|
*/
|
|
123
149
|
config.build.current = {
|
|
124
150
|
path: path.parse(templatePath),
|
|
125
|
-
baseDir,
|
|
126
|
-
relativePath,
|
|
127
151
|
}
|
|
128
152
|
|
|
129
153
|
const html = await readFile(templatePath, 'utf8')
|
|
@@ -158,8 +182,9 @@ export default async (config = {}) => {
|
|
|
158
182
|
* We do this before generating plaintext, so that
|
|
159
183
|
* any paths will already have been created.
|
|
160
184
|
*/
|
|
161
|
-
const outputPathFromConfig = get(rendered.config, 'permalink',
|
|
185
|
+
const outputPathFromConfig = get(rendered.config, 'permalink', templatePath)
|
|
162
186
|
const parsedOutputPath = path.parse(outputPathFromConfig)
|
|
187
|
+
// This keeps original file extension if no output extension is set
|
|
163
188
|
const extension = get(rendered.config, 'build.output.extension', parsedOutputPath.ext.slice(1))
|
|
164
189
|
const outputPath = `${parsedOutputPath.dir}/${parsedOutputPath.name}.${extension}`
|
|
165
190
|
|
|
@@ -175,62 +200,50 @@ export default async (config = {}) => {
|
|
|
175
200
|
await writeFile(outputPath, rendered.html)
|
|
176
201
|
|
|
177
202
|
/**
|
|
178
|
-
*
|
|
203
|
+
* Remove original file if its path is different
|
|
204
|
+
* from the final destination path.
|
|
179
205
|
*/
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
path.relative(get(rendered.config, 'build.output.path'), outputPath),
|
|
183
|
-
getColorizedFileSize(rendered.html),
|
|
184
|
-
formatTime(Date.now() - templateBuildStartTime)
|
|
185
|
-
])
|
|
206
|
+
if (outputPath !== templatePath) {
|
|
207
|
+
await rm(templatePath)
|
|
186
208
|
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Add file to CLI table for build summary logging
|
|
212
|
+
*/
|
|
213
|
+
table.push([
|
|
214
|
+
path.relative(get(rendered.config, 'build.output.path'), outputPath),
|
|
215
|
+
getColorizedFileSize(rendered.html),
|
|
216
|
+
formatTime(Date.now() - templateBuildStartTime)
|
|
217
|
+
])
|
|
187
218
|
}
|
|
188
219
|
|
|
189
220
|
/**
|
|
190
221
|
* Copy static files
|
|
191
222
|
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* TODO: support an array of objects with source and destination, i.e. static: [{ source: 'src/assets', destination: 'assets' }, ...]
|
|
223
|
+
* TODO: support an array of objects with source and destination,
|
|
224
|
+
* i.e. static: [{ source: 'src/assets', destination: 'assets' }, ...]
|
|
196
225
|
*/
|
|
226
|
+
const staticSourcePaths = getRootDirectories([...new Set(get(config, 'build.static.source', []))])
|
|
197
227
|
|
|
198
|
-
|
|
199
|
-
|
|
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)
|
|
205
|
-
}
|
|
206
|
-
|
|
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
|
-
}
|
|
228
|
+
for await (const rootDir of staticSourcePaths) {
|
|
229
|
+
await cp(rootDir, path.join(buildOutputPath, get(config, 'build.static.destination')), { recursive: true })
|
|
219
230
|
}
|
|
220
231
|
|
|
221
|
-
const
|
|
232
|
+
const allOutputFiles = await fg.glob(path.join(buildOutputPath, '**/*'))
|
|
222
233
|
|
|
223
234
|
/**
|
|
224
235
|
* Run `afterBuild` event
|
|
225
236
|
*/
|
|
226
237
|
if (typeof config.afterBuild === 'function') {
|
|
227
|
-
await config.afterBuild({
|
|
238
|
+
await config.afterBuild({
|
|
239
|
+
config,
|
|
240
|
+
files: allOutputFiles,
|
|
241
|
+
transform: transformers,
|
|
242
|
+
})
|
|
228
243
|
}
|
|
229
244
|
|
|
230
245
|
/**
|
|
231
246
|
* Log a build summary if enabled in the config
|
|
232
|
-
*
|
|
233
|
-
* Need to first clear the spinner
|
|
234
247
|
*/
|
|
235
248
|
|
|
236
249
|
spinner.clear()
|
|
@@ -239,10 +252,10 @@ export default async (config = {}) => {
|
|
|
239
252
|
console.log(table.toString() + '\n')
|
|
240
253
|
}
|
|
241
254
|
|
|
242
|
-
spinner.succeed(`
|
|
255
|
+
spinner.succeed(`Built ${table.length} template${table.length > 1 ? 's' : ''} in ${formatTime(Date.now() - startTime)}`)
|
|
243
256
|
|
|
244
257
|
return {
|
|
245
|
-
files:
|
|
258
|
+
files: allOutputFiles,
|
|
246
259
|
config
|
|
247
260
|
}
|
|
248
261
|
} 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 = '',
|
|
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(
|
|
153
|
-
let plaintextOutputPath = get(plaintextConfig, 'output.path', get(
|
|
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
|
|
161
|
+
if (typeof config.permalink === 'string') {
|
|
162
162
|
// Output plaintext at the `permalink` path
|
|
163
|
-
plaintextOutputPath =
|
|
163
|
+
plaintextOutputPath = config.permalink
|
|
164
164
|
} else {
|
|
165
165
|
// Output plaintext at the same directory as the HTML file
|
|
166
|
-
plaintextOutputPath = path
|
|
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(
|
|
190
|
-
await mkdir(
|
|
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(
|
|
204
|
+
const templateFileName = get(config, 'build.current.path.name')
|
|
206
205
|
|
|
207
206
|
plaintextOutputPath = path.join(
|
|
208
|
-
plaintextOutputPath,
|
|
209
|
-
get(
|
|
207
|
+
path.dirname(plaintextOutputPath),
|
|
208
|
+
get(config, 'build.current.path.dir'),
|
|
210
209
|
templateFileName + '.' + plaintextExtension
|
|
211
210
|
)
|
|
212
211
|
|
package/src/generators/render.js
CHANGED
|
@@ -5,7 +5,7 @@ import { defu as merge } from 'defu'
|
|
|
5
5
|
import expressions from 'posthtml-expressions'
|
|
6
6
|
import { parseFrontMatter } from '../utils/node.js'
|
|
7
7
|
import { process as compilePostHTML } from '../posthtml/index.js'
|
|
8
|
-
import { run as useTransformers } from '../transformers/index.js'
|
|
8
|
+
import { run as useTransformers, transformers } from '../transformers/index.js'
|
|
9
9
|
|
|
10
10
|
export async function render(html = '', config = {}) {
|
|
11
11
|
if (typeof html !== 'string') {
|
|
@@ -65,7 +65,8 @@ export async function render(html = '', config = {}) {
|
|
|
65
65
|
content = await templateConfig.beforeRender(({
|
|
66
66
|
html: content,
|
|
67
67
|
config: templateConfig,
|
|
68
|
-
|
|
68
|
+
posthtml: compilePostHTML,
|
|
69
|
+
transform: transformers,
|
|
69
70
|
})) ?? content
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -85,7 +86,8 @@ export async function render(html = '', config = {}) {
|
|
|
85
86
|
compiled.html = await templateConfig.afterRender(({
|
|
86
87
|
html: compiled.html,
|
|
87
88
|
config: templateConfig,
|
|
88
|
-
|
|
89
|
+
posthtml: compilePostHTML,
|
|
90
|
+
transform: transformers,
|
|
89
91
|
})) ?? compiled.html
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -117,7 +119,8 @@ export async function render(html = '', config = {}) {
|
|
|
117
119
|
compiled.html = await templateConfig.afterTransformers(({
|
|
118
120
|
html: compiled.html,
|
|
119
121
|
config: templateConfig,
|
|
120
|
-
|
|
122
|
+
posthtml: compilePostHTML,
|
|
123
|
+
transform: transformers,
|
|
121
124
|
})) ?? compiled.html
|
|
122
125
|
}
|
|
123
126
|
|
package/src/posthtml/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import components from 'posthtml-component'
|
|
|
7
7
|
import posthtmlPostcss from 'posthtml-postcss'
|
|
8
8
|
import defaultPosthtmlConfig from './defaultConfig.js'
|
|
9
9
|
import expandLinkTag from './plugins/expandLinkTag.js'
|
|
10
|
+
import envAttributes from './plugins/envAttributes.js'
|
|
11
|
+
import envTags from './plugins/envTags.js'
|
|
10
12
|
|
|
11
13
|
// PostCSS
|
|
12
14
|
import tailwindcss from 'tailwindcss'
|
|
@@ -50,6 +52,8 @@ export async function process(html = '', config = {}) {
|
|
|
50
52
|
|
|
51
53
|
return posthtml([
|
|
52
54
|
...get(config, 'posthtml.plugins.before', []),
|
|
55
|
+
envTags(config.env),
|
|
56
|
+
envAttributes(config.env),
|
|
53
57
|
expandLinkTag,
|
|
54
58
|
postcssPlugin,
|
|
55
59
|
components(
|
|
@@ -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
|
package/src/server/client.js
CHANGED
package/src/server/routes/hmr.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import express from 'express'
|
|
2
2
|
const router = express.Router()
|
|
3
3
|
import fs from 'node:fs/promises'
|
|
4
|
-
import {
|
|
4
|
+
import { cwd } from 'node:process'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { dirname, join, resolve } from 'pathe'
|
|
6
7
|
|
|
7
8
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
9
|
|
|
9
10
|
router.get('/hmr.js', async (req, res) => {
|
|
10
11
|
const morphdomScript = await fs.readFile(
|
|
11
|
-
|
|
12
|
+
resolve(cwd(), 'node_modules/morphdom/dist/morphdom-umd.js'),
|
|
12
13
|
'utf8'
|
|
13
14
|
)
|
|
14
15
|
|
package/src/transformers/core.js
CHANGED
|
@@ -11,6 +11,18 @@ const posthtmlPlugin = (config = {}) => tree => {
|
|
|
11
11
|
node.content = ['']
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Custom attributes to prevent inlining CSS from <style> tags
|
|
16
|
+
*/
|
|
17
|
+
if (
|
|
18
|
+
node.tag === 'style'
|
|
19
|
+
&& (node.attrs?.['no-inline'] || node.attrs?.embed)
|
|
20
|
+
) {
|
|
21
|
+
node.attrs['no-inline'] = false
|
|
22
|
+
node.attrs.embed = false
|
|
23
|
+
node.attrs['data-embed'] = true
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
return node
|
|
15
27
|
}
|
|
16
28
|
|
|
@@ -8,6 +8,7 @@ import minify from './minify.js'
|
|
|
8
8
|
import baseUrl from './baseUrl.js'
|
|
9
9
|
import inlineCSS from './inline.js'
|
|
10
10
|
import prettify from './prettify.js'
|
|
11
|
+
import templateTag from './template.js'
|
|
11
12
|
import filters from './filters/index.js'
|
|
12
13
|
import markdown from 'posthtml-markdownit'
|
|
13
14
|
import posthtmlMso from './posthtmlMso.js'
|
|
@@ -56,9 +57,9 @@ export async function run(html = '', config = {}) {
|
|
|
56
57
|
* Rewrite Tailwind CSS class names to email-safe alternatives,
|
|
57
58
|
* unless explicitly disabled
|
|
58
59
|
*/
|
|
59
|
-
if (get(config, 'css.
|
|
60
|
+
if (get(config, 'css.safe') !== false) {
|
|
60
61
|
posthtmlPlugins.push(
|
|
61
|
-
safeClassNames(get(config, 'css.
|
|
62
|
+
safeClassNames(get(config, 'css.safe', {}))
|
|
62
63
|
)
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -251,7 +252,14 @@ export async function run(html = '', config = {}) {
|
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
/**
|
|
254
|
-
* 18.
|
|
255
|
+
* 18. <template> tags
|
|
256
|
+
*
|
|
257
|
+
* Replace <template> tags with their content
|
|
258
|
+
*/
|
|
259
|
+
posthtmlPlugins.push(templateTag())
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 19. Replace strings
|
|
255
263
|
*
|
|
256
264
|
* Replace strings through regular expressions
|
|
257
265
|
*/
|
|
@@ -267,3 +275,23 @@ export async function run(html = '', config = {}) {
|
|
|
267
275
|
html: result.html,
|
|
268
276
|
}))
|
|
269
277
|
}
|
|
278
|
+
|
|
279
|
+
export const transformers = {
|
|
280
|
+
comb,
|
|
281
|
+
sixHex,
|
|
282
|
+
minify,
|
|
283
|
+
baseUrl,
|
|
284
|
+
inlineCSS,
|
|
285
|
+
prettify,
|
|
286
|
+
filters,
|
|
287
|
+
markdown,
|
|
288
|
+
posthtmlMso,
|
|
289
|
+
shorthandCss,
|
|
290
|
+
preventWidows,
|
|
291
|
+
addAttributes,
|
|
292
|
+
urlParameters,
|
|
293
|
+
safeClassNames,
|
|
294
|
+
replaceStrings,
|
|
295
|
+
attributeToStyle,
|
|
296
|
+
removeAttributes,
|
|
297
|
+
}
|
|
@@ -10,6 +10,7 @@ import * as cheerio from 'cheerio/lib/slim'
|
|
|
10
10
|
import safeParser from 'postcss-safe-parser'
|
|
11
11
|
import isObject from 'lodash-es/isObject.js'
|
|
12
12
|
import { parser as parse } from 'posthtml-parser'
|
|
13
|
+
import { parseCSSRule } from '../utils/string.js'
|
|
13
14
|
import { useAttributeSizes } from './useAttributeSizes.js'
|
|
14
15
|
|
|
15
16
|
const posthtmlPlugin = (options = {}) => tree => {
|
|
@@ -116,7 +117,7 @@ export async function inline(html = '', options = {}) {
|
|
|
116
117
|
rule.walkDecls(decl => {
|
|
117
118
|
// Resolve calc() values to static values
|
|
118
119
|
if (options.resolveCalc) {
|
|
119
|
-
decl.value = decl.value.includes('calc(') ? calc(decl.value) : decl.value
|
|
120
|
+
decl.value = decl.value.includes('calc(') ? calc(decl.value, {precision: 2}) : decl.value
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
declarations.add(decl)
|
|
@@ -179,10 +180,10 @@ export async function inline(html = '', options = {}) {
|
|
|
179
180
|
|
|
180
181
|
if (styleAttr) {
|
|
181
182
|
inlineStyles = styleAttr.split(';').reduce((acc, i) => {
|
|
182
|
-
let
|
|
183
|
+
let { property, value } = parseCSSRule(i)
|
|
183
184
|
|
|
184
185
|
if (value && options.resolveCalc) {
|
|
185
|
-
value = value.includes('calc') ? calc(value) : value
|
|
186
|
+
value = value.includes('calc') ? calc(value, {precision: 2}) : value
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
if (value && options.preferUnitlessValues) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const posthtmlPlugin = (() => tree => {
|
|
2
|
+
const process = node => {
|
|
3
|
+
// Return the original node if it doesn't have a tag
|
|
4
|
+
if (!node.tag) {
|
|
5
|
+
return node
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Preserve <template> tags marked as such
|
|
9
|
+
if (node.tag === 'template' && node.attrs?.preserve) {
|
|
10
|
+
node.attrs.preserve = false
|
|
11
|
+
|
|
12
|
+
return node
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Replace <template> tags with their content
|
|
16
|
+
if (node.tag === 'template') {
|
|
17
|
+
node.tag = false
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return node
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return tree.walk(process)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export default posthtmlPlugin
|
package/src/utils/node.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import path from 'pathe'
|
|
1
2
|
import os from 'node:os'
|
|
2
3
|
import gm from 'gray-matter'
|
|
3
4
|
import pico from 'picocolors'
|
|
4
5
|
import { humanFileSize } from './string.js'
|
|
6
|
+
import {
|
|
7
|
+
copyFile,
|
|
8
|
+
mkdir,
|
|
9
|
+
readdir
|
|
10
|
+
} from 'node:fs/promises'
|
|
5
11
|
|
|
6
12
|
// Return a local IP address
|
|
7
13
|
export function getLocalIP() {
|
package/src/utils/string.js
CHANGED
|
@@ -115,3 +115,82 @@ export function humanFileSize(bytes, si=false, dp=2) {
|
|
|
115
115
|
|
|
116
116
|
return bytes.toFixed(dp) + ' ' + units[u]
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get the root directories from a list of glob patterns.
|
|
121
|
+
*
|
|
122
|
+
* @param {array} patterns List of glob patterns.
|
|
123
|
+
* @returns {array} List of root directories.
|
|
124
|
+
*/
|
|
125
|
+
export function getRootDirectories(patterns = []) {
|
|
126
|
+
if (!Array.isArray(patterns)) {
|
|
127
|
+
return []
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (patterns.length === 0) {
|
|
131
|
+
return []
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return [...new Set(
|
|
135
|
+
patterns
|
|
136
|
+
.filter(pattern => !pattern.startsWith('!'))
|
|
137
|
+
.map(pattern => {
|
|
138
|
+
// If the pattern doesn't include wildcards, use it as is
|
|
139
|
+
if (!pattern.includes('*')) {
|
|
140
|
+
return pattern.replace(/\/$/, '') // Remove trailing slash if present
|
|
141
|
+
}
|
|
142
|
+
// For patterns with wildcards, get the part before the first wildcard
|
|
143
|
+
const parts = pattern.split(/[*{]/)[0].split('/')
|
|
144
|
+
return parts.slice(0, -1).join('/')
|
|
145
|
+
})
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
)]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get the file extensions from a glob pattern.
|
|
152
|
+
* @param {*} pattern
|
|
153
|
+
* @returns
|
|
154
|
+
*/
|
|
155
|
+
export function getFileExtensionsFromPattern(pattern) {
|
|
156
|
+
const starExtPattern = /\.([^\*\{\}]+)$/ // Matches .ext but not .* or .{ext}
|
|
157
|
+
const bracePattern = /\.{([^}]+)}$/ // Matches .{ext} or .{ext,ext}
|
|
158
|
+
const wildcardPattern = /\.\*$/ // Matches .*
|
|
159
|
+
|
|
160
|
+
if (wildcardPattern.test(pattern)) {
|
|
161
|
+
return ['html'] // We default to 'html' if the pattern is a wildcard
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const braceMatch = pattern.match(bracePattern);
|
|
165
|
+
if (braceMatch) {
|
|
166
|
+
return braceMatch[1].split(',') // Split and return extensions inside braces
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const starExtMatch = pattern.match(starExtPattern)
|
|
170
|
+
if (starExtMatch) {
|
|
171
|
+
return [starExtMatch[1]] // Return single extension
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return ['html'] // No recognizable extension pattern, default to 'html'
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function parseCSSRule(rule) {
|
|
178
|
+
// Step 1: Trim the input string
|
|
179
|
+
rule = rule.trim()
|
|
180
|
+
|
|
181
|
+
// Step 2: Find the index of the first colon
|
|
182
|
+
const colonIndex = rule.indexOf(':')
|
|
183
|
+
|
|
184
|
+
// Step 3: Extract property and value parts
|
|
185
|
+
if (colonIndex === -1) {
|
|
186
|
+
return {
|
|
187
|
+
property: '',
|
|
188
|
+
value: ''
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const property = rule.slice(0, colonIndex).trim()
|
|
193
|
+
const value = rule.slice(colonIndex + 1).trim()
|
|
194
|
+
|
|
195
|
+
return { property, value }
|
|
196
|
+
}
|
package/types/build.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ComponentsConfig from './components';
|
|
2
2
|
import type { SpinnerName } from 'cli-spinners';
|
|
3
|
+
import type ExpressionsConfig from './expressions';
|
|
3
4
|
|
|
4
5
|
export default interface BuildConfig {
|
|
5
6
|
/**
|
|
@@ -8,7 +9,7 @@ export default interface BuildConfig {
|
|
|
8
9
|
components?: ComponentsConfig;
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
+
* Paths where Maizzle should look for Templates to compile.
|
|
12
13
|
*
|
|
13
14
|
* @default ['src/templates/**\/*.html']
|
|
14
15
|
*
|
|
@@ -16,12 +17,17 @@ export default interface BuildConfig {
|
|
|
16
17
|
* ```
|
|
17
18
|
* export default {
|
|
18
19
|
* build: {
|
|
19
|
-
*
|
|
20
|
+
* content: ['src/templates/**\/*.html']
|
|
20
21
|
* }
|
|
21
22
|
* }
|
|
22
23
|
* ```
|
|
23
24
|
*/
|
|
24
|
-
|
|
25
|
+
content?: string | string[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
Configure expressions.
|
|
29
|
+
*/
|
|
30
|
+
expressions?: ExpressionsConfig;
|
|
25
31
|
|
|
26
32
|
/**
|
|
27
33
|
* Define the output path for compiled Templates, and what file extension they should use.
|
package/types/config.d.ts
CHANGED
|
@@ -136,7 +136,7 @@ export default interface Config {
|
|
|
136
136
|
* ```
|
|
137
137
|
* export default {
|
|
138
138
|
* css: {
|
|
139
|
-
*
|
|
139
|
+
* safe: {
|
|
140
140
|
* ':': '__',
|
|
141
141
|
* '!': 'i-',
|
|
142
142
|
* }
|
|
@@ -144,7 +144,7 @@ export default interface Config {
|
|
|
144
144
|
* }
|
|
145
145
|
* ```
|
|
146
146
|
*/
|
|
147
|
-
|
|
147
|
+
safe?: boolean | Record<string, string>;
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
150
|
* Ensure that all your HEX colors inside `bgcolor` and `color` attributes are defined with six digits.
|