@maizzle/framework 5.0.0-beta.3 → 5.0.0-beta.30
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 +32 -30
- package/src/commands/build.js +146 -73
- package/src/generators/plaintext.js +26 -23
- package/src/generators/render.js +15 -14
- package/src/posthtml/defaultComponentsConfig.js +2 -2
- package/src/posthtml/defaultConfig.js +13 -3
- package/src/posthtml/index.js +38 -9
- package/src/posthtml/plugins/envAttributes.js +32 -0
- package/src/posthtml/plugins/envTags.js +33 -0
- package/src/server/index.js +159 -96
- package/src/server/routes/index.js +51 -13
- package/src/server/views/404.html +59 -0
- package/src/server/views/index.html +162 -14
- package/src/transformers/addAttributes.js +2 -3
- package/src/transformers/attributeToStyle.js +1 -3
- package/src/transformers/baseUrl.js +6 -6
- package/src/transformers/comb.js +7 -6
- package/src/transformers/core.js +12 -0
- package/src/transformers/filters/index.js +1 -2
- package/src/transformers/index.js +56 -67
- package/src/transformers/inline.js +53 -16
- package/src/transformers/markdown.js +14 -7
- package/src/transformers/minify.js +4 -3
- package/src/transformers/posthtmlMso.js +1 -3
- package/src/transformers/prettify.js +4 -3
- package/src/transformers/preventWidows.js +15 -65
- package/src/transformers/removeAttributes.js +3 -4
- package/src/transformers/replaceStrings.js +7 -5
- package/src/transformers/safeClassNames.js +1 -2
- package/src/transformers/shorthandCss.js +1 -3
- package/src/transformers/sixHex.js +1 -3
- package/src/transformers/template.js +26 -0
- package/src/transformers/urlParameters.js +1 -3
- package/src/transformers/useAttributeSizes.js +1 -3
- package/src/utils/string.js +89 -0
- package/types/build.d.ts +53 -24
- package/types/config.d.ts +60 -49
- package/types/css/inline.d.ts +2 -0
- package/types/css/purge.d.ts +1 -1
- package/types/events.d.ts +153 -5
- package/types/index.d.ts +4 -3
- package/types/posthtml.d.ts +3 -3
- package/types/urlParameters.d.ts +1 -1
- package/types/widowWords.d.ts +16 -36
- package/types/components.d.ts +0 -195
- package/types/expressions.d.ts +0 -100
package/src/posthtml/index.js
CHANGED
|
@@ -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
|
-
import defaultPosthtmlConfig from './defaultConfig.js'
|
|
9
10
|
import expandLinkTag from './plugins/expandLinkTag.js'
|
|
11
|
+
import envAttributes from './plugins/envAttributes.js'
|
|
12
|
+
import { getPosthtmlOptions } from './defaultConfig.js'
|
|
10
13
|
|
|
11
14
|
// PostCSS
|
|
12
15
|
import tailwindcss from 'tailwindcss'
|
|
@@ -33,12 +36,12 @@ export async function process(html = '', config = {}) {
|
|
|
33
36
|
)
|
|
34
37
|
)
|
|
35
38
|
|
|
36
|
-
const posthtmlOptions =
|
|
39
|
+
const posthtmlOptions = getPosthtmlOptions(get(config, 'posthtml.options', {}))
|
|
37
40
|
|
|
38
41
|
const componentsUserOptions = get(config, 'components', {})
|
|
39
42
|
|
|
40
43
|
const expressionsOptions = merge(
|
|
41
|
-
get(config, '
|
|
44
|
+
get(config, 'expressions', get(config, 'posthtml.expressions', {})),
|
|
42
45
|
get(componentsUserOptions, 'expressions', {}),
|
|
43
46
|
)
|
|
44
47
|
|
|
@@ -48,24 +51,50 @@ 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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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)
|
|
67
96
|
.then(result => ({
|
|
68
|
-
config,
|
|
97
|
+
config: merge(config, { page: config }),
|
|
69
98
|
html: result.html,
|
|
70
99
|
}))
|
|
71
100
|
.catch(error => {
|
|
@@ -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/index.js
CHANGED
|
@@ -40,6 +40,71 @@ app.use(hmrRoute)
|
|
|
40
40
|
|
|
41
41
|
let viewing = ''
|
|
42
42
|
const spinner = ora()
|
|
43
|
+
let templatePaths = []
|
|
44
|
+
|
|
45
|
+
function getTemplateFolders(config) {
|
|
46
|
+
return Array.isArray(get(config, 'build.content'))
|
|
47
|
+
? config.build.content
|
|
48
|
+
: [config.build.content]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function getTemplatePaths(templateFolders) {
|
|
52
|
+
return await fg.glob([...new Set(templateFolders)])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function getUpdatedRoutes(app, config) {
|
|
56
|
+
return getTemplatePaths(getTemplateFolders(config))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function renderUpdatedFile(file, config) {
|
|
60
|
+
try {
|
|
61
|
+
const startTime = Date.now()
|
|
62
|
+
spinner.start('Building...')
|
|
63
|
+
|
|
64
|
+
// beforeCreate event
|
|
65
|
+
if (typeof config.beforeCreate === 'function') {
|
|
66
|
+
await config.beforeCreate({ config })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Read the file
|
|
70
|
+
const fileContent = await fs.readFile(file, 'utf8')
|
|
71
|
+
|
|
72
|
+
// Set a `dev` flag on the config
|
|
73
|
+
config._dev = true
|
|
74
|
+
|
|
75
|
+
// Render the file with PostHTML
|
|
76
|
+
let { html } = await render(fileContent, config)
|
|
77
|
+
|
|
78
|
+
// Update console message
|
|
79
|
+
const shouldReportFileSize = get(config, 'server.reportFileSize', false)
|
|
80
|
+
|
|
81
|
+
spinner.succeed(
|
|
82
|
+
`Done in ${formatTime(Date.now() - startTime)}`
|
|
83
|
+
+ `${pico.gray(` [${path.relative(cwd(), file)}]`)}`
|
|
84
|
+
+ `${shouldReportFileSize ? ' · ' + getColorizedFileSize(html) : ''}`
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Inject HMR script
|
|
89
|
+
*/
|
|
90
|
+
html = injectScript(html, '<script src="/hmr.js"></script>')
|
|
91
|
+
|
|
92
|
+
// Notify connected websocket clients about the change
|
|
93
|
+
wss.clients.forEach(client => {
|
|
94
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
95
|
+
client.send(JSON.stringify({
|
|
96
|
+
type: 'change',
|
|
97
|
+
content: html,
|
|
98
|
+
scrollSync: get(config, 'server.scrollSync', false),
|
|
99
|
+
hmr: get(config, 'server.hmr', true),
|
|
100
|
+
}))
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
} catch (error) {
|
|
104
|
+
spinner.fail('Failed to render template.')
|
|
105
|
+
throw error
|
|
106
|
+
}
|
|
107
|
+
}
|
|
43
108
|
|
|
44
109
|
export default async (config = {}) => {
|
|
45
110
|
// Read the Maizzle config file
|
|
@@ -47,7 +112,10 @@ export default async (config = {}) => {
|
|
|
47
112
|
|
|
48
113
|
/**
|
|
49
114
|
* Dev server settings
|
|
50
|
-
|
|
115
|
+
*/
|
|
116
|
+
spinner.spinner = get(config, 'server.spinner', 'circleHalves')
|
|
117
|
+
spinner.start('Starting server...')
|
|
118
|
+
|
|
51
119
|
const shouldScroll = get(config, 'server.scrollSync', false)
|
|
52
120
|
const useHmr = get(config, 'server.hmr', true)
|
|
53
121
|
|
|
@@ -60,18 +128,20 @@ export default async (config = {}) => {
|
|
|
60
128
|
* Initialize WebSocket server
|
|
61
129
|
* Used to send messages between the server and the browser
|
|
62
130
|
*/
|
|
63
|
-
initWebSockets(wss, {
|
|
131
|
+
initWebSockets(wss, { shouldScroll, useHmr })
|
|
64
132
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
? config.build.content
|
|
68
|
-
: [config.build.content]
|
|
133
|
+
// Register routes
|
|
134
|
+
templatePaths = await getUpdatedRoutes(app, config)
|
|
69
135
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Store template paths on the request object
|
|
138
|
+
*
|
|
139
|
+
* We use it in the index view to list all templates.
|
|
140
|
+
* */
|
|
73
141
|
app.request.templatePaths = templatePaths
|
|
74
142
|
|
|
143
|
+
app.request.maizzleConfig = config
|
|
144
|
+
|
|
75
145
|
/**
|
|
76
146
|
* Create route pattern
|
|
77
147
|
* Only allow files with the following extensions
|
|
@@ -83,7 +153,7 @@ export default async (config = {}) => {
|
|
|
83
153
|
)
|
|
84
154
|
].join('|')
|
|
85
155
|
|
|
86
|
-
const routePattern = Array.isArray(
|
|
156
|
+
const routePattern = Array.isArray(getTemplateFolders(config))
|
|
87
157
|
? `*/:file.(${extensions})`
|
|
88
158
|
: `:file.(${extensions})`
|
|
89
159
|
|
|
@@ -94,11 +164,11 @@ export default async (config = {}) => {
|
|
|
94
164
|
app.get(routePattern, async (req, res, next) => {
|
|
95
165
|
// Run beforeCreate event
|
|
96
166
|
if (typeof config.beforeCreate === 'function') {
|
|
97
|
-
config.beforeCreate(config)
|
|
167
|
+
await config.beforeCreate({ config })
|
|
98
168
|
}
|
|
99
169
|
|
|
100
170
|
try {
|
|
101
|
-
const filePath = templatePaths.find(t => t.endsWith(req.url.slice(1)))
|
|
171
|
+
const filePath = templatePaths.find(t => t.endsWith(decodeURI(req.url.slice(1))))
|
|
102
172
|
|
|
103
173
|
// Set the file being viewed
|
|
104
174
|
viewing = filePath
|
|
@@ -125,99 +195,82 @@ export default async (config = {}) => {
|
|
|
125
195
|
})
|
|
126
196
|
})
|
|
127
197
|
|
|
128
|
-
// Error-handling middleware
|
|
129
|
-
app.use(async (error, req, res, next) => { // eslint-disable-line
|
|
130
|
-
console.error(error)
|
|
131
|
-
|
|
132
|
-
const view = await fs.readFile(path.join(__dirname, 'views', 'error.html'), 'utf8')
|
|
133
|
-
const { html } = await render(view, {
|
|
134
|
-
method: req.method,
|
|
135
|
-
url: req.url,
|
|
136
|
-
error
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
res.status(500).send(html)
|
|
140
|
-
})
|
|
141
|
-
|
|
142
198
|
/**
|
|
143
199
|
* Components watcher
|
|
144
200
|
*
|
|
145
201
|
* Watches for changes in the configured Templates and Components paths
|
|
146
202
|
*/
|
|
203
|
+
let isWatcherReady = false
|
|
147
204
|
chokidar
|
|
148
|
-
.watch(
|
|
205
|
+
.watch(
|
|
206
|
+
[
|
|
207
|
+
...templatePaths,
|
|
208
|
+
...get(config, 'components.folders', defaultComponentsConfig.folders)
|
|
209
|
+
],
|
|
210
|
+
{
|
|
211
|
+
ignoreInitial: true,
|
|
212
|
+
awaitWriteFinish: {
|
|
213
|
+
stabilityThreshold: 150,
|
|
214
|
+
pollInterval: 25,
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
)
|
|
149
218
|
.on('change', async () => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return
|
|
219
|
+
if (viewing) {
|
|
220
|
+
await renderUpdatedFile(viewing, config)
|
|
153
221
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// Update console message
|
|
174
|
-
const shouldReportFileSize = get(config, 'server.reportFileSize', false)
|
|
175
|
-
|
|
176
|
-
spinner.succeed(
|
|
177
|
-
`Done in ${formatTime(Date.now() - startTime)}`
|
|
178
|
-
+ `${pico.gray(` [${path.relative(cwd(), viewing)}]`)}`
|
|
179
|
-
+ `${ shouldReportFileSize ? ' · ' + getColorizedFileSize(html) : ''}`
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Inject HMR script
|
|
184
|
-
*/
|
|
185
|
-
html = injectScript(html, '<script src="/hmr.js"></script>')
|
|
186
|
-
|
|
187
|
-
// Notify connected websocket clients about the change
|
|
188
|
-
wss.clients.forEach(client => {
|
|
189
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
190
|
-
client.send(JSON.stringify({
|
|
191
|
-
type: 'change',
|
|
192
|
-
content: html,
|
|
193
|
-
scrollSync: get(config, 'server.scrollSync', false),
|
|
194
|
-
hmr: get(config, 'server.hmr', true),
|
|
195
|
-
}))
|
|
196
|
-
}
|
|
197
|
-
})
|
|
198
|
-
} catch (error) {
|
|
199
|
-
spinner.fail('Failed to render template.')
|
|
200
|
-
throw error
|
|
222
|
+
})
|
|
223
|
+
.on('ready', () => {
|
|
224
|
+
/**
|
|
225
|
+
* `add` fires immediately when the watcher is created,
|
|
226
|
+
* so we use this trick to detect new files added
|
|
227
|
+
* after it has started.
|
|
228
|
+
*/
|
|
229
|
+
isWatcherReady = true
|
|
230
|
+
})
|
|
231
|
+
.on('add', async () => {
|
|
232
|
+
if (isWatcherReady) {
|
|
233
|
+
templatePaths = await getUpdatedRoutes(app, config)
|
|
234
|
+
app.request.templatePaths = templatePaths
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
.on('unlink', async () => {
|
|
238
|
+
if (isWatcherReady) {
|
|
239
|
+
templatePaths = await getUpdatedRoutes(app, config)
|
|
240
|
+
app.request.templatePaths = templatePaths
|
|
201
241
|
}
|
|
202
242
|
})
|
|
203
243
|
|
|
204
244
|
/**
|
|
205
245
|
* Global watcher
|
|
206
246
|
*
|
|
207
|
-
* Watch for changes in the config
|
|
247
|
+
* Watch for changes in the config files, Tailwind CSS config, CSS files,
|
|
248
|
+
* configured static assets, and user-defined watch paths.
|
|
208
249
|
*/
|
|
209
250
|
const globalWatchedPaths = new Set([
|
|
210
|
-
'config*.js',
|
|
211
|
-
'maizzle.config*.js',
|
|
212
|
-
'tailwind*.config.js',
|
|
251
|
+
'config*.{js,cjs,ts}',
|
|
252
|
+
'maizzle.config*.{js,cjs,ts}',
|
|
253
|
+
'tailwind*.config.{js,ts}',
|
|
213
254
|
'**/*.css',
|
|
214
|
-
...get(config, '
|
|
255
|
+
...get(config, 'build.static.source', []),
|
|
256
|
+
...get(config, 'server.watch', []),
|
|
215
257
|
])
|
|
216
258
|
|
|
217
259
|
async function globalPathsHandler(file, eventType) {
|
|
260
|
+
// Update express.static to serve new files
|
|
261
|
+
if (eventType === 'add') {
|
|
262
|
+
app.use(express.static(path.dirname(file)))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Stop serving deleted files
|
|
266
|
+
if (eventType === 'unlink') {
|
|
267
|
+
app._router.stack = app._router.stack.filter(
|
|
268
|
+
layer => layer.regexp.source !== path.dirname(file).replace(/\\/g, '/')
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
218
272
|
// Not viewing a component in the browser, no need to rebuild
|
|
219
273
|
if (!viewing) {
|
|
220
|
-
spinner.info(`file ${eventType}: ${file}`)
|
|
221
274
|
return
|
|
222
275
|
}
|
|
223
276
|
|
|
@@ -235,7 +288,7 @@ export default async (config = {}) => {
|
|
|
235
288
|
|
|
236
289
|
// Run beforeCreate event
|
|
237
290
|
if (typeof config.beforeCreate === 'function') {
|
|
238
|
-
await config.beforeCreate(config)
|
|
291
|
+
await config.beforeCreate({ config })
|
|
239
292
|
}
|
|
240
293
|
|
|
241
294
|
// Read the file
|
|
@@ -254,7 +307,7 @@ export default async (config = {}) => {
|
|
|
254
307
|
spinner.succeed(
|
|
255
308
|
`Done in ${formatTime(Date.now() - startTime)}`
|
|
256
309
|
+ `${pico.gray(` [${path.relative(cwd(), filePath)}]`)}`
|
|
257
|
-
+ `${
|
|
310
|
+
+ `${shouldReportFileSize ? ' · ' + getColorizedFileSize(html) : ''}`
|
|
258
311
|
)
|
|
259
312
|
|
|
260
313
|
/**
|
|
@@ -274,7 +327,7 @@ export default async (config = {}) => {
|
|
|
274
327
|
}
|
|
275
328
|
})
|
|
276
329
|
} catch (error) {
|
|
277
|
-
spinner.fail(
|
|
330
|
+
spinner.fail(`Failed to render template: ${file}`)
|
|
278
331
|
throw error
|
|
279
332
|
}
|
|
280
333
|
}
|
|
@@ -286,6 +339,10 @@ export default async (config = {}) => {
|
|
|
286
339
|
get(config, 'build.output.path', 'build_production'),
|
|
287
340
|
],
|
|
288
341
|
ignoreInitial: true,
|
|
342
|
+
awaitWriteFinish: {
|
|
343
|
+
stabilityThreshold: 150,
|
|
344
|
+
pollInterval: 25,
|
|
345
|
+
},
|
|
289
346
|
})
|
|
290
347
|
.on('change', async file => await globalPathsHandler(file, 'change'))
|
|
291
348
|
.on('add', async file => await globalPathsHandler(file, 'add'))
|
|
@@ -293,25 +350,33 @@ export default async (config = {}) => {
|
|
|
293
350
|
|
|
294
351
|
/**
|
|
295
352
|
* Serve all folders in the cwd as static files
|
|
296
|
-
*
|
|
297
|
-
* TODO: change to include build.assets or build.static, which may be outside cwd
|
|
298
353
|
*/
|
|
299
354
|
const srcFoldersList = await fg.glob(
|
|
300
355
|
[
|
|
301
356
|
'**/*/',
|
|
302
357
|
...get(config, 'build.static.source', [])
|
|
303
358
|
], {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
359
|
+
onlyFiles: false,
|
|
360
|
+
ignore: [
|
|
361
|
+
'node_modules',
|
|
362
|
+
get(config, 'build.output.path', 'build_*'),
|
|
363
|
+
]
|
|
364
|
+
})
|
|
310
365
|
|
|
311
366
|
srcFoldersList.forEach(folder => {
|
|
312
367
|
app.use(express.static(path.join(config.cwd, folder)))
|
|
313
368
|
})
|
|
314
369
|
|
|
370
|
+
// Error-handling middleware
|
|
371
|
+
app.use(async (req, res) => {
|
|
372
|
+
const view = await fs.readFile(path.join(__dirname, 'views', '404.html'), 'utf8')
|
|
373
|
+
const { html } = await render(view, {
|
|
374
|
+
url: req.url,
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
res.status(404).send(html)
|
|
378
|
+
})
|
|
379
|
+
|
|
315
380
|
/**
|
|
316
381
|
* Start the server
|
|
317
382
|
*/
|
|
@@ -321,8 +386,6 @@ export default async (config = {}) => {
|
|
|
321
386
|
|
|
322
387
|
function startServer(port) {
|
|
323
388
|
const serverStartTime = Date.now()
|
|
324
|
-
spinner.start('Starting server...')
|
|
325
|
-
|
|
326
389
|
const server = createServer(app)
|
|
327
390
|
|
|
328
391
|
/**
|
|
@@ -7,27 +7,65 @@ import { fileURLToPath } from 'node:url'
|
|
|
7
7
|
import expressions from 'posthtml-expressions'
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
const
|
|
10
|
+
function groupFilesByDirectories(globs, files) {
|
|
11
|
+
const result = {}
|
|
12
|
+
let current = {}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
globs.forEach(glob => {
|
|
15
|
+
const rootPath = glob.split(/[\*\!\{\}]/)[0].replace(/\/+$/, '')
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
files.forEach(file => {
|
|
18
|
+
if (file.startsWith(rootPath)) {
|
|
19
|
+
const relativePath = file.slice(rootPath.length + 1)
|
|
20
|
+
const parts = relativePath.split('/')
|
|
21
|
+
current = result[rootPath] = result[rootPath] || {}
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
24
|
+
current = current[parts[i]] = current[parts[i]] || {}
|
|
25
|
+
}
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
const fileName = parts[parts.length - 1]
|
|
28
|
+
current[fileName] = {
|
|
29
|
+
name: fileName,
|
|
30
|
+
href: encodeURI(file),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
25
33
|
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function flattenPaths(paths, parentPath = '', currentDepth = 0) {
|
|
40
|
+
const flatArray = []
|
|
41
|
+
|
|
42
|
+
for (const [key, value] of Object.entries(paths)) {
|
|
43
|
+
const fullPath = parentPath ? `${parentPath}/${key}` : key
|
|
44
|
+
|
|
45
|
+
if (value && typeof value === 'object' && !value.name) {
|
|
46
|
+
// If it's a folder, add it with the current depth and recurse into its contents
|
|
47
|
+
flatArray.push({ name: key, path: fullPath, depth: currentDepth, type: 'folder' })
|
|
48
|
+
flatArray.push(...flattenPaths(value, fullPath, currentDepth + 1))
|
|
49
|
+
} else if (value && typeof value === 'object' && value.name) {
|
|
50
|
+
// If it's a file, add it with the current depth
|
|
51
|
+
flatArray.push({ name: value.name, href: value.href, path: fullPath, depth: currentDepth, type: 'file' })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return flatArray
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
route.get(['/', '/index.html'], async (req, res) => {
|
|
59
|
+
const view = await fs.readFile(path.join(__dirname, '../views', 'index.html'), 'utf8')
|
|
60
|
+
|
|
61
|
+
const content = new Set(req.maizzleConfig.build.content)
|
|
62
|
+
|
|
63
|
+
const groupedByDir = groupFilesByDirectories(content, req.templatePaths)
|
|
26
64
|
|
|
27
65
|
const { html } = await posthtml()
|
|
28
66
|
.use(expressions({
|
|
29
67
|
locals: {
|
|
30
|
-
|
|
68
|
+
paths: flattenPaths(groupedByDir)
|
|
31
69
|
}
|
|
32
70
|
}))
|
|
33
71
|
.process(view)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>404 - Template not found</title>
|
|
7
|
+
<style>
|
|
8
|
+
html, body {
|
|
9
|
+
font-family: Helvetica, Arial, sans-serif;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
height: 100%;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.container {
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
height: 100vh;
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
position: relative;
|
|
23
|
+
z-index: 1;
|
|
24
|
+
padding: 24px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.error-code {
|
|
28
|
+
font-size: 25rem;
|
|
29
|
+
font-weight: 700;
|
|
30
|
+
color: #f1f5f9;
|
|
31
|
+
position: fixed;
|
|
32
|
+
top: -1.5rem;
|
|
33
|
+
left: -3rem;
|
|
34
|
+
user-select: none;
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
<span class="error-code">404</span>
|
|
40
|
+
|
|
41
|
+
<div class="container">
|
|
42
|
+
<div style="text-align: center;">
|
|
43
|
+
<h1 style="font-size: 3rem; color: #0F172A; margin: 2.25rem 0">
|
|
44
|
+
Template Not Found
|
|
45
|
+
</h1>
|
|
46
|
+
<p style="margin: 0 0 2.25rem; font-size: 1.25rem; line-height: 1.5; color: #64748B;">
|
|
47
|
+
The Template you are looking for was not found:
|
|
48
|
+
</p>
|
|
49
|
+
<p style="margin: 1rem 0 0; font-size: 1rem; line-height: 1.5; font-weight: 600; color: #334155;">
|
|
50
|
+
{{ page.url }}
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div style="position: fixed; bottom: 0; right: 0; pointer-events: none; user-select: none;">
|
|
56
|
+
<svg width="883" height="536" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="1100" height="536"><path fill="#D9D9D9" d="M0 .955h1100V536H0z"/></mask><g mask="url(#a)" stroke="#94A3B8" stroke-miterlimit="10"><path d="M1056.93 92.587c0-50.03-43.95-90.587-98.168-90.587-54.22 0-98.174 40.557-98.174 90.587v483.125c0 50.029 43.954 90.586 98.174 90.586 54.218 0 98.168-40.557 98.168-90.586V92.587ZM646.241 92.587C646.241 42.556 602.287 2 548.067 2c-54.219 0-98.173 40.557-98.173 90.587v483.125c0 50.029 43.954 90.586 98.173 90.586 54.22 0 98.174-40.557 98.174-90.586V92.587Z"/><path d="M1036.18 148.383c33.41-39.402 25.88-96.336-16.82-127.164C976.657-9.61 914.955-2.66 881.544 36.742L471.586 520.215c-33.411 39.402-25.879 96.336 16.824 127.164 42.702 30.829 104.404 23.879 137.815-15.523l409.955-483.473ZM625.093 148.396c33.411-39.403 25.878-96.336-16.824-127.164C565.567-9.597 503.865-2.647 470.454 36.755L60.495 520.228c-33.41 39.402-25.878 96.336 16.825 127.164 42.702 30.829 104.404 23.879 137.815-15.523l409.958-483.473Z"/></g></svg>
|
|
57
|
+
</div>
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|