@maizzle/framework 5.0.0-beta.8 → 5.0.0

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.
Files changed (46) hide show
  1. package/package.json +33 -31
  2. package/src/commands/build.js +106 -30
  3. package/src/generators/plaintext.js +22 -18
  4. package/src/generators/render.js +16 -18
  5. package/src/index.js +3 -3
  6. package/src/posthtml/defaultComponentsConfig.js +10 -2
  7. package/src/posthtml/defaultConfig.js +13 -3
  8. package/src/posthtml/index.js +59 -14
  9. package/src/server/index.js +171 -98
  10. package/src/server/routes/index.js +51 -13
  11. package/src/server/views/404.html +59 -0
  12. package/src/server/views/index.html +162 -14
  13. package/src/transformers/addAttributes.js +2 -3
  14. package/src/transformers/attributeToStyle.js +1 -3
  15. package/src/transformers/baseUrl.js +6 -6
  16. package/src/transformers/filters/index.js +1 -2
  17. package/src/transformers/index.js +57 -95
  18. package/src/transformers/inline.js +44 -48
  19. package/src/transformers/markdown.js +14 -7
  20. package/src/transformers/minify.js +4 -3
  21. package/src/transformers/posthtmlMso.js +1 -3
  22. package/src/transformers/prettify.js +4 -3
  23. package/src/transformers/preventWidows.js +15 -65
  24. package/src/transformers/{comb.js → purge.js} +9 -8
  25. package/src/transformers/removeAttributes.js +3 -4
  26. package/src/transformers/replaceStrings.js +7 -5
  27. package/src/transformers/safeClassNames.js +1 -2
  28. package/src/transformers/shorthandCss.js +1 -3
  29. package/src/transformers/sixHex.js +1 -3
  30. package/src/transformers/template.js +9 -9
  31. package/src/transformers/urlParameters.js +1 -3
  32. package/src/transformers/useAttributeSizes.js +1 -3
  33. package/src/utils/getConfigByFilePath.js +10 -0
  34. package/src/utils/node.js +0 -23
  35. package/src/utils/string.js +34 -12
  36. package/types/build.d.ts +51 -28
  37. package/types/config.d.ts +127 -64
  38. package/types/css/inline.d.ts +10 -26
  39. package/types/css/purge.d.ts +3 -3
  40. package/types/events.d.ts +153 -5
  41. package/types/index.d.ts +4 -3
  42. package/types/posthtml.d.ts +3 -3
  43. package/types/urlParameters.d.ts +1 -1
  44. package/types/widowWords.d.ts +16 -36
  45. package/types/components.d.ts +0 -195
  46. package/types/expressions.d.ts +0 -100
@@ -3,27 +3,39 @@ 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'
10
11
  import envAttributes from './plugins/envAttributes.js'
11
- import envTags from './plugins/envTags.js'
12
+ import { getPosthtmlOptions } from './defaultConfig.js'
12
13
 
13
14
  // PostCSS
14
15
  import tailwindcss from 'tailwindcss'
16
+ import postcssCalc from 'postcss-calc'
15
17
  import postcssImport from 'postcss-import'
18
+ import cssVariables from 'postcss-css-variables'
16
19
  import postcssSafeParser from 'postcss-safe-parser'
17
- import customProperties from 'postcss-custom-properties'
18
20
 
19
21
  import defaultComponentsConfig from './defaultComponentsConfig.js'
20
22
 
21
23
  export async function process(html = '', config = {}) {
24
+ /**
25
+ * Configure PostCSS pipeline. Plugins defined and added here
26
+ * will apply to all `<style>` tags in the HTML.
27
+ */
28
+ const resolveCSSProps = get(config, 'css.resolveProps')
29
+ const resolveCalc = get(config, 'css.resolveCalc') !== false
30
+ ? get(config, 'css.resolveCalc', { precision: 2 }) // it's true by default, use default precision 2
31
+ : false
32
+
22
33
  const postcssPlugin = posthtmlPostcss(
23
34
  [
24
35
  postcssImport(),
25
36
  tailwindcss(get(config, 'css.tailwind', {})),
26
- get(config, 'css.inline.resolveCSSVariables', true) && customProperties(),
37
+ resolveCSSProps !== false && cssVariables(resolveCSSProps),
38
+ resolveCalc !== false && postcssCalc(resolveCalc),
27
39
  ...get(config, 'postcss.plugins', []),
28
40
  ],
29
41
  merge(
@@ -35,12 +47,16 @@ export async function process(html = '', config = {}) {
35
47
  )
36
48
  )
37
49
 
38
- const posthtmlOptions = merge(get(config, 'posthtml.options', {}), defaultPosthtmlConfig)
50
+ /**
51
+ * Define PostHTML options by merging user-provided ones
52
+ * on top of a default configuration.
53
+ */
54
+ const posthtmlOptions = getPosthtmlOptions(get(config, 'posthtml.options', {}))
39
55
 
40
56
  const componentsUserOptions = get(config, 'components', {})
41
57
 
42
58
  const expressionsOptions = merge(
43
- get(config, 'build.expressions', get(config, 'posthtml.expressions', {})),
59
+ get(config, 'expressions', get(config, 'posthtml.expressions', {})),
44
60
  get(componentsUserOptions, 'expressions', {}),
45
61
  )
46
62
 
@@ -50,26 +66,55 @@ export async function process(html = '', config = {}) {
50
66
  { page: config },
51
67
  )
52
68
 
69
+ const fetchPlugin = posthtmlFetch(
70
+ merge(
71
+ {
72
+ expressions: merge(
73
+ { locals },
74
+ expressionsOptions,
75
+ {
76
+ missingLocal: '{local}',
77
+ strictMode: false,
78
+ },
79
+ ),
80
+ },
81
+ get(config, 'fetch', {})
82
+ )
83
+ )
84
+
85
+ const componentsConfig = merge(
86
+ {
87
+ expressions: merge(
88
+ { locals },
89
+ expressionsOptions,
90
+ )
91
+ },
92
+ componentsUserOptions,
93
+ defaultComponentsConfig
94
+ )
95
+
96
+ // Ensure `fileExtension` is array and has no duplicates
97
+ componentsConfig.fileExtension = Array.from(new Set(
98
+ [].concat(componentsConfig.fileExtension)
99
+ ))
100
+
53
101
  return posthtml([
54
102
  ...get(config, 'posthtml.plugins.before', []),
55
103
  envTags(config.env),
56
104
  envAttributes(config.env),
57
105
  expandLinkTag,
58
106
  postcssPlugin,
59
- components(
60
- merge({
61
- expressions: {
62
- locals,
63
- }
64
- }, defaultComponentsConfig)
65
- ),
107
+ fetchPlugin,
108
+ components(componentsConfig),
66
109
  expandLinkTag,
67
110
  postcssPlugin,
111
+ envTags(config.env),
112
+ envAttributes(config.env),
68
113
  ...get(config, 'posthtml.plugins.after', get(config, 'posthtml.plugins', []))
69
114
  ])
70
115
  .process(html, posthtmlOptions)
71
116
  .then(result => ({
72
- config,
117
+ config: merge(config, { page: config }),
73
118
  html: result.html,
74
119
  }))
75
120
  .catch(error => {
@@ -40,6 +40,77 @@ app.use(hmrRoute)
40
40
 
41
41
  let viewing = ''
42
42
  const spinner = ora()
43
+ let templatePaths = []
44
+ const serverStartTime = Date.now()
45
+
46
+ function getTemplateFolders(config) {
47
+ return Array.isArray(get(config, 'build.content'))
48
+ ? config.build.content
49
+ : [config.build.content]
50
+ }
51
+
52
+ async function getTemplatePaths(templateFolders) {
53
+ return await fg.glob([...new Set(templateFolders)])
54
+ }
55
+
56
+ async function getUpdatedRoutes(app, config) {
57
+ return getTemplatePaths(getTemplateFolders(config))
58
+ }
59
+
60
+ async function renderUpdatedFile(file, config) {
61
+ try {
62
+ const startTime = Date.now()
63
+ spinner.start('Building...')
64
+
65
+ /**
66
+ * Add current template path info to the config
67
+ *
68
+ * Can be used in events like `beforeRender` to determine
69
+ * which template file is being rendered.
70
+ */
71
+ config.build.current = {
72
+ path: path.parse(file),
73
+ }
74
+
75
+ // Read the file
76
+ const fileContent = await fs.readFile(file, 'utf8')
77
+
78
+ // Set a `dev` flag on the config
79
+ config._dev = true
80
+
81
+ // Render the file with PostHTML
82
+ let { html } = await render(fileContent, config)
83
+
84
+ // Update console message
85
+ const shouldReportFileSize = get(config, 'server.reportFileSize', false)
86
+
87
+ spinner.succeed(
88
+ `Done in ${formatTime(Date.now() - startTime)}`
89
+ + `${pico.gray(` [${path.relative(cwd(), file)}]`)}`
90
+ + `${shouldReportFileSize ? ' · ' + getColorizedFileSize(html) : ''}`
91
+ )
92
+
93
+ /**
94
+ * Inject HMR script
95
+ */
96
+ html = injectScript(html, '<script src="/hmr.js"></script>')
97
+
98
+ // Notify connected websocket clients about the change
99
+ wss.clients.forEach(client => {
100
+ if (client.readyState === WebSocket.OPEN) {
101
+ client.send(JSON.stringify({
102
+ type: 'change',
103
+ content: html,
104
+ scrollSync: get(config, 'server.scrollSync', false),
105
+ hmr: get(config, 'server.hmr', true),
106
+ }))
107
+ }
108
+ })
109
+ } catch (error) {
110
+ spinner.fail('Failed to render template.')
111
+ throw error
112
+ }
113
+ }
43
114
 
44
115
  export default async (config = {}) => {
45
116
  // Read the Maizzle config file
@@ -47,7 +118,10 @@ export default async (config = {}) => {
47
118
 
48
119
  /**
49
120
  * Dev server settings
50
- */
121
+ */
122
+ spinner.spinner = get(config, 'server.spinner', 'circleHalves')
123
+ spinner.start('Starting server...')
124
+
51
125
  const shouldScroll = get(config, 'server.scrollSync', false)
52
126
  const useHmr = get(config, 'server.hmr', true)
53
127
 
@@ -60,18 +134,20 @@ export default async (config = {}) => {
60
134
  * Initialize WebSocket server
61
135
  * Used to send messages between the server and the browser
62
136
  */
63
- initWebSockets(wss, { scrollSync: shouldScroll, hmr: useHmr })
64
-
65
- // Get a list of all template paths
66
- const templateFolders = Array.isArray(get(config, 'build.content'))
67
- ? config.build.content
68
- : [config.build.content]
137
+ initWebSockets(wss, { shouldScroll, useHmr })
69
138
 
70
- const templatePaths = await fg.glob([...new Set(templateFolders)])
139
+ // Register routes
140
+ templatePaths = await getUpdatedRoutes(app, config)
71
141
 
72
- // Set the template paths on the app, we use them in the index view
142
+ /**
143
+ * Store template paths on the request object
144
+ *
145
+ * We use it in the index view to list all templates.
146
+ * */
73
147
  app.request.templatePaths = templatePaths
74
148
 
149
+ app.request.maizzleConfig = config
150
+
75
151
  /**
76
152
  * Create route pattern
77
153
  * Only allow files with the following extensions
@@ -83,7 +159,7 @@ export default async (config = {}) => {
83
159
  )
84
160
  ].join('|')
85
161
 
86
- const routePattern = Array.isArray(templateFolders)
162
+ const routePattern = Array.isArray(getTemplateFolders(config))
87
163
  ? `*/:file.(${extensions})`
88
164
  : `:file.(${extensions})`
89
165
 
@@ -94,15 +170,20 @@ export default async (config = {}) => {
94
170
  app.get(routePattern, async (req, res, next) => {
95
171
  // Run beforeCreate event
96
172
  if (typeof config.beforeCreate === 'function') {
97
- config.beforeCreate(config)
173
+ await config.beforeCreate({ config })
98
174
  }
99
175
 
100
176
  try {
101
- const filePath = templatePaths.find(t => t.endsWith(req.url.slice(1)))
177
+ const filePath = templatePaths.find(t => t.endsWith(decodeURI(req.url.slice(1))))
102
178
 
103
179
  // Set the file being viewed
104
180
  viewing = filePath
105
181
 
182
+ // Add current template path info to the config
183
+ config.build.current = {
184
+ path: path.parse(filePath),
185
+ }
186
+
106
187
  // Read the file
107
188
  const fileContent = await fs.readFile(filePath, 'utf8')
108
189
 
@@ -125,99 +206,82 @@ export default async (config = {}) => {
125
206
  })
126
207
  })
127
208
 
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
209
  /**
143
210
  * Components watcher
144
211
  *
145
212
  * Watches for changes in the configured Templates and Components paths
146
213
  */
214
+ let isWatcherReady = false
147
215
  chokidar
148
- .watch([...templatePaths, ...get(config, 'components.folders', defaultComponentsConfig.folders) ])
216
+ .watch(
217
+ [
218
+ ...templatePaths,
219
+ ...get(config, 'components.folders', defaultComponentsConfig.folders)
220
+ ],
221
+ {
222
+ ignoreInitial: true,
223
+ awaitWriteFinish: {
224
+ stabilityThreshold: 150,
225
+ pollInterval: 25,
226
+ },
227
+ }
228
+ )
149
229
  .on('change', async () => {
150
- // Not viewing a component in the browser, no need to rebuild
151
- if (!viewing) {
152
- return
230
+ if (viewing) {
231
+ await renderUpdatedFile(viewing, config)
153
232
  }
154
-
155
- try {
156
- const startTime = Date.now()
157
- spinner.start('Building...')
158
-
159
- // beforeCreate event
160
- if (typeof config.beforeCreate === 'function') {
161
- await config.beforeCreate(config)
162
- }
163
-
164
- // Read the file
165
- const fileContent = await fs.readFile(viewing, 'utf8')
166
-
167
- // Set a `dev` flag on the config
168
- config._dev = true
169
-
170
- // Render the file with PostHTML
171
- let { html } = await render(fileContent, config)
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
233
+ })
234
+ .on('ready', () => {
235
+ /**
236
+ * `add` fires immediately when the watcher is created,
237
+ * so we use this trick to detect new files added
238
+ * after it has started.
239
+ */
240
+ isWatcherReady = true
241
+ })
242
+ .on('add', async () => {
243
+ if (isWatcherReady) {
244
+ templatePaths = await getUpdatedRoutes(app, config)
245
+ app.request.templatePaths = templatePaths
246
+ }
247
+ })
248
+ .on('unlink', async () => {
249
+ if (isWatcherReady) {
250
+ templatePaths = await getUpdatedRoutes(app, config)
251
+ app.request.templatePaths = templatePaths
201
252
  }
202
253
  })
203
254
 
204
255
  /**
205
256
  * Global watcher
206
257
  *
207
- * Watch for changes in the config file, Tailwind CSS config, and CSS files
258
+ * Watch for changes in the config files, Tailwind CSS config, CSS files,
259
+ * configured static assets, and user-defined watch paths.
208
260
  */
209
261
  const globalWatchedPaths = new Set([
210
- 'config*.js',
211
- 'maizzle.config*.js',
212
- 'tailwind*.config.js',
262
+ 'config*.{js,cjs,ts}',
263
+ 'maizzle.config*.{js,cjs,ts}',
264
+ 'tailwind*.config.{js,ts}',
213
265
  '**/*.css',
214
- ...get(config, 'server.watch', [])
266
+ ...get(config, 'build.static.source', []),
267
+ ...get(config, 'server.watch', []),
215
268
  ])
216
269
 
217
270
  async function globalPathsHandler(file, eventType) {
271
+ // Update express.static to serve new files
272
+ if (eventType === 'add') {
273
+ app.use(express.static(path.dirname(file)))
274
+ }
275
+
276
+ // Stop serving deleted files
277
+ if (eventType === 'unlink') {
278
+ app._router.stack = app._router.stack.filter(
279
+ layer => layer.regexp.source !== path.dirname(file).replace(/\\/g, '/')
280
+ )
281
+ }
282
+
218
283
  // Not viewing a component in the browser, no need to rebuild
219
284
  if (!viewing) {
220
- spinner.info(`file ${eventType}: ${file}`)
221
285
  return
222
286
  }
223
287
 
@@ -235,7 +299,7 @@ export default async (config = {}) => {
235
299
 
236
300
  // Run beforeCreate event
237
301
  if (typeof config.beforeCreate === 'function') {
238
- await config.beforeCreate(config)
302
+ await config.beforeCreate({ config })
239
303
  }
240
304
 
241
305
  // Read the file
@@ -254,7 +318,7 @@ export default async (config = {}) => {
254
318
  spinner.succeed(
255
319
  `Done in ${formatTime(Date.now() - startTime)}`
256
320
  + `${pico.gray(` [${path.relative(cwd(), filePath)}]`)}`
257
- + `${ shouldReportFileSize ? ' · ' + getColorizedFileSize(html) : ''}`
321
+ + `${shouldReportFileSize ? ' · ' + getColorizedFileSize(html) : ''}`
258
322
  )
259
323
 
260
324
  /**
@@ -274,7 +338,7 @@ export default async (config = {}) => {
274
338
  }
275
339
  })
276
340
  } catch (error) {
277
- spinner.fail('Failed to render template.')
341
+ spinner.fail(`Failed to render template: ${file}`)
278
342
  throw error
279
343
  }
280
344
  }
@@ -286,6 +350,10 @@ export default async (config = {}) => {
286
350
  get(config, 'build.output.path', 'build_production'),
287
351
  ],
288
352
  ignoreInitial: true,
353
+ awaitWriteFinish: {
354
+ stabilityThreshold: 150,
355
+ pollInterval: 25,
356
+ },
289
357
  })
290
358
  .on('change', async file => await globalPathsHandler(file, 'change'))
291
359
  .on('add', async file => await globalPathsHandler(file, 'add'))
@@ -293,25 +361,33 @@ export default async (config = {}) => {
293
361
 
294
362
  /**
295
363
  * 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
364
  */
299
365
  const srcFoldersList = await fg.glob(
300
366
  [
301
367
  '**/*/',
302
368
  ...get(config, 'build.static.source', [])
303
369
  ], {
304
- onlyFiles: false,
305
- ignore: [
306
- 'node_modules',
307
- get(config, 'build.output.path', 'build_*'),
308
- ]
309
- })
370
+ onlyFiles: false,
371
+ ignore: [
372
+ 'node_modules',
373
+ get(config, 'build.output.path', 'build_*'),
374
+ ]
375
+ })
310
376
 
311
377
  srcFoldersList.forEach(folder => {
312
378
  app.use(express.static(path.join(config.cwd, folder)))
313
379
  })
314
380
 
381
+ // Error-handling middleware
382
+ app.use(async (req, res) => {
383
+ const view = await fs.readFile(path.join(__dirname, 'views', '404.html'), 'utf8')
384
+ const { html } = await render(view, {
385
+ url: req.url,
386
+ })
387
+
388
+ res.status(404).send(html)
389
+ })
390
+
315
391
  /**
316
392
  * Start the server
317
393
  */
@@ -320,9 +396,6 @@ export default async (config = {}) => {
320
396
  const maxRetries = get(config, 'server.maxRetries', 10)
321
397
 
322
398
  function startServer(port) {
323
- const serverStartTime = Date.now()
324
- spinner.start('Starting server...')
325
-
326
399
  const server = createServer(app)
327
400
 
328
401
  /**
@@ -343,7 +416,7 @@ export default async (config = {}) => {
343
416
  )
344
417
 
345
418
  spinner.stopAndPersist({
346
- text: `${pico.bgBlue(` Maizzle v${version} `)} ready in ${pico.bold(Date.now() - serverStartTime)} ms`
419
+ text: `\n${pico.bgBlue(` Maizzle v${version} `)} ready in ${pico.bold(formatTime(Date.now() - serverStartTime))}`
347
420
  + '\n\n'
348
421
  + ` → Local: http://localhost:${port}`
349
422
  + '\n'
@@ -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
- route.get(['/', '/index.html'], async (req, res) => {
11
- const view = await fs.readFile(path.join(__dirname, '../views', 'index.html'), 'utf8')
10
+ function groupFilesByDirectories(globs, files) {
11
+ const result = {}
12
+ let current = {}
12
13
 
13
- // Group by `dir`
14
- const groupedByDir = {}
14
+ globs.forEach(glob => {
15
+ const rootPath = glob.split(/[\*\!\{\}]/)[0].replace(/\/+$/, '')
15
16
 
16
- req.templatePaths
17
- .map(t => path.parse(t))
18
- .forEach(file => {
19
- if (!groupedByDir[file.dir]) {
20
- groupedByDir[file.dir] = []
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
- file.href = [file.dir.replace(file.root, ''), file.base].join('/')
24
- groupedByDir[file.dir].push(file)
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
- templates: groupedByDir
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>