@maizzle/framework 4.8.7 → 5.0.0-beta.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 (80) hide show
  1. package/bin/maizzle +3 -1
  2. package/package.json +65 -58
  3. package/src/commands/build.js +244 -19
  4. package/src/commands/serve.js +2 -197
  5. package/src/generators/plaintext.js +192 -91
  6. package/src/generators/render.js +128 -0
  7. package/src/index.js +46 -14
  8. package/src/{generators/posthtml → posthtml}/defaultComponentsConfig.js +6 -4
  9. package/src/{generators/posthtml → posthtml}/defaultConfig.js +1 -1
  10. package/src/posthtml/index.js +74 -0
  11. package/src/posthtml/plugins/expandLinkTag.js +36 -0
  12. package/src/server/client.js +181 -0
  13. package/src/server/index.js +383 -0
  14. package/src/server/routes/hmr.js +24 -0
  15. package/src/server/routes/index.js +38 -0
  16. package/src/server/views/error.html +83 -0
  17. package/src/server/views/index.html +24 -0
  18. package/src/server/websockets.js +27 -0
  19. package/src/transformers/addAttributes.js +30 -0
  20. package/src/transformers/attributeToStyle.js +30 -36
  21. package/src/transformers/baseUrl.js +52 -23
  22. package/src/transformers/comb.js +51 -0
  23. package/src/transformers/core.js +20 -0
  24. package/src/transformers/filters/defaultFilters.js +90 -70
  25. package/src/transformers/filters/index.js +14 -78
  26. package/src/transformers/index.js +268 -63
  27. package/src/transformers/inline.js +240 -0
  28. package/src/transformers/markdown.js +13 -14
  29. package/src/transformers/minify.js +21 -16
  30. package/src/transformers/posthtmlMso.js +13 -8
  31. package/src/transformers/prettify.js +16 -15
  32. package/src/transformers/preventWidows.js +32 -26
  33. package/src/transformers/removeAttributes.js +17 -17
  34. package/src/transformers/replaceStrings.js +30 -9
  35. package/src/transformers/safeClassNames.js +24 -24
  36. package/src/transformers/shorthandCss.js +22 -0
  37. package/src/transformers/sixHex.js +15 -15
  38. package/src/transformers/urlParameters.js +18 -16
  39. package/src/transformers/useAttributeSizes.js +65 -0
  40. package/src/utils/getConfigByFilePath.js +124 -0
  41. package/src/utils/node.js +68 -0
  42. package/src/utils/string.js +117 -0
  43. package/types/build.d.ts +117 -57
  44. package/types/components.d.ts +130 -112
  45. package/types/config.d.ts +454 -242
  46. package/types/css/inline.d.ts +234 -0
  47. package/types/css/purge.d.ts +125 -0
  48. package/types/events.d.ts +5 -105
  49. package/types/index.d.ts +148 -116
  50. package/types/markdown.d.ts +20 -18
  51. package/types/minify.d.ts +122 -120
  52. package/types/plaintext.d.ts +46 -52
  53. package/types/posthtml.d.ts +103 -136
  54. package/types/render.d.ts +0 -117
  55. package/types/urlParameters.d.ts +21 -20
  56. package/types/widowWords.d.ts +9 -7
  57. package/src/functions/plaintext.js +0 -5
  58. package/src/functions/render.js +0 -5
  59. package/src/generators/config.js +0 -52
  60. package/src/generators/output/index.js +0 -4
  61. package/src/generators/output/to-disk.js +0 -254
  62. package/src/generators/output/to-string.js +0 -73
  63. package/src/generators/postcss.js +0 -23
  64. package/src/generators/posthtml/index.js +0 -75
  65. package/src/generators/tailwindcss.js +0 -157
  66. package/src/transformers/extraAttributes.js +0 -33
  67. package/src/transformers/inlineCss.js +0 -42
  68. package/src/transformers/removeInlineBackgroundColor.js +0 -56
  69. package/src/transformers/removeInlineSizes.js +0 -43
  70. package/src/transformers/removeInlinedSelectors.js +0 -100
  71. package/src/transformers/removeUnusedCss.js +0 -48
  72. package/src/transformers/shorthandInlineCSS.js +0 -26
  73. package/src/utils/helpers.js +0 -13
  74. package/types/baseUrl.d.ts +0 -79
  75. package/types/fetch.d.ts +0 -143
  76. package/types/inlineCss.d.ts +0 -207
  77. package/types/layouts.d.ts +0 -39
  78. package/types/removeUnusedCss.d.ts +0 -115
  79. package/types/tailwind.d.ts +0 -22
  80. package/types/templates.d.ts +0 -181
@@ -0,0 +1,181 @@
1
+ // biome-ignore lint: need it globally
2
+ var lastKnownScrollPosition = 0
3
+
4
+ function connectWebSocket() {
5
+ if (!('WebSocket' in window)) {
6
+ return
7
+ }
8
+
9
+ const { hostname, port } = window.location
10
+ const socket = new WebSocket(`ws://${hostname}:${port}`)
11
+
12
+ /**
13
+ * Synchronized scrolling
14
+ * Sends the scroll position to the server
15
+ */
16
+ function handleScroll() {
17
+ socket.send(JSON.stringify({
18
+ type: 'scroll',
19
+ position: window.scrollY
20
+ }))
21
+ }
22
+
23
+ function scrollHandler() {
24
+ lastKnownScrollPosition = window.scrollY
25
+ requestAnimationFrame(handleScroll)
26
+ }
27
+
28
+ window.addEventListener('scroll', scrollHandler)
29
+
30
+ socket.addEventListener('message', async event => {
31
+ const data = JSON.parse(event.data)
32
+
33
+ if (data.type === 'scroll' && data.scrollSync === true) {
34
+ window.scrollTo(0, data.position)
35
+ }
36
+
37
+ if (data.type === 'change') {
38
+ if (data.hmr === true) {
39
+ // Use morphdom to update the existing DOM with the new content
40
+ morphdom(document.documentElement, data.content, {
41
+ childrenOnly: true,
42
+ onBeforeElUpdated(fromEl, toEl) {
43
+ // Speed-up trick from morphdom docs - https://dom.spec.whatwg.org/#concept-node-equals
44
+ if (fromEl.isEqualNode(toEl)) {
45
+ return false
46
+ }
47
+
48
+ return true
49
+ },
50
+ onElUpdated(el) {
51
+ // Handle broken images updates, like incorrect file paths
52
+ if (el.tagName === 'IMG' && !el.complete) {
53
+ const img = new Image()
54
+ img.src = el.src
55
+ el.src = ''
56
+
57
+ img.onload = () => {
58
+ el.src = img.src
59
+ }
60
+ }
61
+ },
62
+ })
63
+ } else {
64
+ // Reload the page
65
+ window.location.reload()
66
+ }
67
+
68
+ /**
69
+ * Fix for attributes not being updated on <html> tag
70
+ * Borrowed from https://github.com/11ty/eleventy-dev-server/
71
+ */
72
+ const parser = new DOMParser()
73
+ const parsed = parser.parseFromString(data.content, 'text/html')
74
+ const parsedDoc = parsed.documentElement
75
+ const newAttrs = parsedDoc.getAttributeNames()
76
+ const docEl = document.documentElement
77
+
78
+ // Remove old attributes
79
+ const removedAttrs = docEl.getAttributeNames().filter(name => !newAttrs.includes(name))
80
+ for (const attr of removedAttrs) {
81
+ docEl.removeAttribute(attr)
82
+ }
83
+
84
+ // Add new attributes
85
+ for (const attr of newAttrs) {
86
+ docEl.setAttribute(attr, parsedDoc.getAttribute(attr))
87
+ }
88
+ }
89
+
90
+ if (['add', 'unlink'].includes(data.type)) {
91
+ if (data.hmr === true) {
92
+ const randomNumber = Math.floor(Math.random() * 10 ** 16).toString().padStart(16, '0')
93
+
94
+ /**
95
+ * Cache busting for images
96
+ *
97
+ * Appends a `?v=` cache-busting parameter to image sources
98
+ * every time a file is added or removed. This forces the
99
+ * browser to re-download the image and immediately
100
+ * reflect the changes through HMR.
101
+ */
102
+
103
+ // For all elements with `src` attributes
104
+ const srcElements = document.querySelectorAll('[src]')
105
+
106
+ srcElements.forEach(el => {
107
+ // Update the value of 'v' parameter if it already exists
108
+ if (el.src.includes('?')) {
109
+ el.src = el.src.replace(/([?&])v=[^&]*/, `$1v=${randomNumber}`)
110
+ } else {
111
+ // Add 'v' parameter
112
+ el.src += `?v=${randomNumber}`
113
+ }
114
+ })
115
+
116
+ // For `background` attributes
117
+ const htmlBgElements = document.querySelectorAll('[background]')
118
+
119
+ htmlBgElements.forEach(el => {
120
+ const bgValue = el.getAttribute('background')
121
+ if (bgValue) {
122
+ // Update the value of 'v' parameter if it already exists
123
+ if (bgValue.includes('?')) {
124
+ el.setAttribute('background', bgValue.replace(/([?&])v=[^&]*/, `$1v=${randomNumber}`))
125
+ } else {
126
+ // Add 'v' parameter
127
+ el.setAttribute('background', `${bgValue}?v=${randomNumber}`)
128
+ }
129
+ }
130
+ })
131
+
132
+ // For inline CSS `background` properties
133
+ const styleElements = document.querySelectorAll('[style]')
134
+
135
+ styleElements.forEach(el => {
136
+ const styleAttribute = el.getAttribute('style')
137
+ if (styleAttribute) {
138
+ const urlPattern = /(url\(["']?)(.*?)(["']?\))/g
139
+ // Replace URLs in style attribute with cache-busting parameter
140
+ const updatedStyleAttribute = styleAttribute.replace(urlPattern, (match, p1, p2, p3) => {
141
+ // Update the value of 'v' parameter if it already exists
142
+ if (p2.includes('?')) {
143
+ return `${p1}${p2.replace(/([?&])v=[^&]*/, `$1v=${randomNumber}`)}${p3}`
144
+ }
145
+
146
+ // Add 'v' parameter
147
+ return `${p1}${p2}?v=${randomNumber}${p3}`
148
+ })
149
+
150
+ // Update style attribute
151
+ el.setAttribute('style', updatedStyleAttribute)
152
+ }
153
+ })
154
+ } else {
155
+ // Reload the page
156
+ window.location.reload()
157
+ }
158
+ }
159
+ })
160
+
161
+ socket.addEventListener('close', () => {
162
+ window.removeEventListener('scroll', scrollHandler)
163
+
164
+ // debug only:
165
+ console.log('WebSocket connection closed. Reconnecting...')
166
+
167
+ // Reconnect after a short delay
168
+ setTimeout(() => {
169
+ connectWebSocket()
170
+ }, 1000)
171
+ })
172
+
173
+ // Handle connection opened
174
+ socket.addEventListener('open', event => {
175
+ console.log('WebSocket connection opened')
176
+ })
177
+
178
+ return socket
179
+ }
180
+
181
+ connectWebSocket()
@@ -0,0 +1,383 @@
1
+ import path from 'pathe'
2
+ import fs from 'node:fs/promises'
3
+ import { createServer } from 'node:http'
4
+ import { cwd, exit } from 'node:process'
5
+
6
+ import { fileURLToPath } from 'node:url'
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
+
9
+ import ora from 'ora'
10
+ import fg from 'fast-glob'
11
+ import express from 'express'
12
+ import pico from 'picocolors'
13
+ import get from 'lodash-es/get.js'
14
+ import * as chokidar from 'chokidar'
15
+ import { isBinary } from 'istextorbinary'
16
+
17
+ import WebSocket, { WebSocketServer } from 'ws'
18
+ import { initWebSockets } from './websockets.js'
19
+
20
+ import {
21
+ getLocalIP,
22
+ getColorizedFileSize,
23
+ } from '../utils/node.js'
24
+ import { injectScript, formatTime } from '../utils/string.js'
25
+
26
+ import { render } from '../generators/render.js'
27
+ import { readFileConfig } from '../utils/getConfigByFilePath.js'
28
+ import defaultComponentsConfig from '../posthtml/defaultComponentsConfig.js'
29
+
30
+ // Routes
31
+ import hmrRoute from './routes/hmr.js'
32
+ import indexRoute from './routes/index.js'
33
+
34
+ const app = express()
35
+ const wss = new WebSocketServer({ noServer: true })
36
+
37
+ // Register routes
38
+ app.use(indexRoute)
39
+ app.use(hmrRoute)
40
+
41
+ let viewing = ''
42
+ const spinner = ora()
43
+
44
+ export default async (config = {}) => {
45
+ // Read the Maizzle config file
46
+ config = await readFileConfig(config).catch(() => { throw new Error('Could not compute config') })
47
+
48
+ /**
49
+ * Dev server settings
50
+ */
51
+ const shouldScroll = get(config, 'server.scrollSync', false)
52
+ const useHmr = get(config, 'server.hmr', true)
53
+
54
+ // Add static assets root prefix so user doesn't have to
55
+ if (!config.baseURL) {
56
+ config.baseURL = '/'
57
+ }
58
+
59
+ /**
60
+ * Initialize WebSocket server
61
+ * Used to send messages between the server and the browser
62
+ */
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]
69
+
70
+ const templatePaths = await fg.glob([...new Set(templateFolders)])
71
+
72
+ // Set the template paths on the app, we use them in the index view
73
+ app.request.templatePaths = templatePaths
74
+
75
+ /**
76
+ * Create route pattern
77
+ * Only allow files with the following extensions
78
+ */
79
+ const extensions = [
80
+ ...new Set(templatePaths
81
+ .filter(p => !isBinary(p)) // exclude binary files from routes
82
+ .map(p => path.extname(p).slice(1).toLowerCase())
83
+ )
84
+ ].join('|')
85
+
86
+ const routePattern = Array.isArray(templateFolders)
87
+ ? `*/:file.(${extensions})`
88
+ : `:file.(${extensions})`
89
+
90
+ /**
91
+ * Loop over the source folders and create route for each file
92
+ */
93
+ templatePaths.forEach(() => {
94
+ app.get(routePattern, async (req, res, next) => {
95
+ // Run beforeCreate event
96
+ if (typeof config.beforeCreate === 'function') {
97
+ config.beforeCreate(config)
98
+ }
99
+
100
+ try {
101
+ const filePath = templatePaths.find(t => t.endsWith(req.url.slice(1)))
102
+
103
+ // Set the file being viewed
104
+ viewing = filePath
105
+
106
+ // Read the file
107
+ const fileContent = await fs.readFile(filePath, 'utf8')
108
+
109
+ // Set a `dev` flag on the config
110
+ config._dev = true
111
+
112
+ // Render the file with PostHTML
113
+ let { html } = await render(fileContent, config)
114
+
115
+ /**
116
+ * Inject HMR script
117
+ */
118
+ html = injectScript(html, '<script src="/hmr.js"></script>')
119
+
120
+ res.send(html)
121
+ } catch (error) {
122
+ spinner.fail(`Failed to render template: ${req.url}\n`)
123
+ next(error)
124
+ }
125
+ })
126
+ })
127
+
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
+ /**
143
+ * Components watcher
144
+ *
145
+ * Watches for changes in the configured Templates and Components paths
146
+ */
147
+ chokidar
148
+ .watch([...templatePaths, ...get(config, 'components.folders', defaultComponentsConfig.folders) ])
149
+ .on('change', async () => {
150
+ // Not viewing a component in the browser, no need to rebuild
151
+ if (!viewing) {
152
+ return
153
+ }
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
201
+ }
202
+ })
203
+
204
+ /**
205
+ * Global watcher
206
+ *
207
+ * Watch for changes in the config file, Tailwind CSS config, and CSS files
208
+ */
209
+ const globalWatchedPaths = new Set([
210
+ 'config*.js',
211
+ 'maizzle.config*.js',
212
+ 'tailwind*.config.js',
213
+ '**/*.css',
214
+ ...get(config, 'server.watch', [])
215
+ ])
216
+
217
+ async function globalPathsHandler(file, eventType) {
218
+ // Not viewing a component in the browser, no need to rebuild
219
+ if (!viewing) {
220
+ spinner.info(`file ${eventType}: ${file}`)
221
+ return
222
+ }
223
+
224
+ try {
225
+ const startTime = Date.now()
226
+ spinner.start('Building...')
227
+
228
+ // Read the Maizzle config file
229
+ config = await readFileConfig()
230
+
231
+ // Add static assets root prefix so user doesn't have to
232
+ if (!config.baseURL) {
233
+ config.baseURL = '/'
234
+ }
235
+
236
+ // Run beforeCreate event
237
+ if (typeof config.beforeCreate === 'function') {
238
+ await config.beforeCreate(config)
239
+ }
240
+
241
+ // Read the file
242
+ const filePath = templatePaths.find(t => t.endsWith(viewing))
243
+ const fileContent = await fs.readFile(path.normalize(filePath), 'utf8')
244
+
245
+ // Set a `dev` flag on the config
246
+ config._dev = true
247
+
248
+ // Render the file with PostHTML
249
+ let { html } = await render(fileContent, config)
250
+
251
+ // Update console message
252
+ const shouldReportFileSize = get(config, 'server.reportFileSize', false)
253
+
254
+ spinner.succeed(
255
+ `Done in ${formatTime(Date.now() - startTime)}`
256
+ + `${pico.gray(` [${path.relative(cwd(), filePath)}]`)}`
257
+ + `${ shouldReportFileSize ? ' · ' + getColorizedFileSize(html) : ''}`
258
+ )
259
+
260
+ /**
261
+ * Inject HMR script
262
+ */
263
+ html = injectScript(html, '<script src="/hmr.js"></script>')
264
+
265
+ // Notify connected websocket clients about the change
266
+ wss.clients.forEach(client => {
267
+ if (client.readyState === WebSocket.OPEN) {
268
+ client.send(JSON.stringify({
269
+ type: eventType,
270
+ content: html,
271
+ scrollSync: get(config, 'server.scrollSync', false),
272
+ hmr: get(config, 'server.hmr', true),
273
+ }))
274
+ }
275
+ })
276
+ } catch (error) {
277
+ spinner.fail('Failed to render template.')
278
+ throw error
279
+ }
280
+ }
281
+
282
+ chokidar
283
+ .watch([...globalWatchedPaths], {
284
+ ignored: [
285
+ 'node_modules',
286
+ get(config, 'build.output.path', 'build_production'),
287
+ ],
288
+ ignoreInitial: true,
289
+ })
290
+ .on('change', async file => await globalPathsHandler(file, 'change'))
291
+ .on('add', async file => await globalPathsHandler(file, 'add'))
292
+ .on('unlink', async file => await globalPathsHandler(file, 'unlink'))
293
+
294
+ /**
295
+ * 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
+ */
299
+ const srcFoldersList = await fg.glob(
300
+ [
301
+ '**/*/',
302
+ ...get(config, 'build.static.source', [])
303
+ ], {
304
+ onlyFiles: false,
305
+ ignore: [
306
+ 'node_modules',
307
+ get(config, 'build.output.path', 'build_*'),
308
+ ]
309
+ })
310
+
311
+ srcFoldersList.forEach(folder => {
312
+ app.use(express.static(path.join(config.cwd, folder)))
313
+ })
314
+
315
+ /**
316
+ * Start the server
317
+ */
318
+ let retryCount = 0
319
+ const port = get(config, 'server.port', 3000)
320
+ const maxRetries = get(config, 'server.maxRetries', 10)
321
+
322
+ function startServer(port) {
323
+ const serverStartTime = Date.now()
324
+ spinner.start('Starting server...')
325
+
326
+ const server = createServer(app)
327
+
328
+ /**
329
+ * Handle WebSocket upgrades
330
+ * Attaches the WebSocket server to the Express server.
331
+ */
332
+ server.on('upgrade', (request, socket, head) => {
333
+ wss.handleUpgrade(request, socket, head, ws => {
334
+ wss.emit('connection', ws, request)
335
+ })
336
+ })
337
+
338
+ server.listen(port, async () => {
339
+ const { version } = JSON.parse(
340
+ await fs.readFile(
341
+ new URL('../../package.json', import.meta.url)
342
+ )
343
+ )
344
+
345
+ spinner.stopAndPersist({
346
+ text: `${pico.bgBlue(` Maizzle v${version} `)} ready in ${pico.bold(Date.now() - serverStartTime)} ms`
347
+ + '\n\n'
348
+ + ` → Local: http://localhost:${port}`
349
+ + '\n'
350
+ + ` → Network: http://${getLocalIP()}:${port}\n`
351
+ })
352
+ })
353
+
354
+ server.on('error', error => {
355
+ try {
356
+ if (error.code === 'EADDRINUSE') {
357
+ server.close()
358
+ retryPort()
359
+ }
360
+ } catch (error) {
361
+ spinner.fail(error.message)
362
+ exit(1)
363
+ }
364
+ })
365
+
366
+ return server
367
+ }
368
+
369
+ function retryPort() {
370
+ retryCount++
371
+
372
+ if (retryCount <= maxRetries) {
373
+ const nextPort = port + retryCount
374
+ startServer(nextPort)
375
+ } else {
376
+ spinner.fail(`Exceeded maximum number of retries (${maxRetries}). Unable to find a free port.`)
377
+
378
+ exit(1)
379
+ }
380
+ }
381
+
382
+ startServer(port)
383
+ }
@@ -0,0 +1,24 @@
1
+ import express from 'express'
2
+ const router = express.Router()
3
+ import fs from 'node:fs/promises'
4
+ import { dirname, join } from 'pathe'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url))
8
+
9
+ router.get('/hmr.js', async (req, res) => {
10
+ const morphdomScript = await fs.readFile(
11
+ join(__dirname, '../../../node_modules/morphdom/dist/morphdom-umd.js'),
12
+ 'utf8'
13
+ )
14
+
15
+ const clientScript = await fs.readFile(
16
+ join(__dirname, '../client.js'),
17
+ 'utf8'
18
+ )
19
+
20
+ res.setHeader('Content-Type', 'application/javascript')
21
+ res.send(morphdomScript + clientScript)
22
+ })
23
+
24
+ export default router
@@ -0,0 +1,38 @@
1
+ import path from 'pathe'
2
+ import express from 'express'
3
+ const route = express.Router()
4
+ import posthtml from 'posthtml'
5
+ import fs from 'node:fs/promises'
6
+ import { fileURLToPath } from 'node:url'
7
+ import expressions from 'posthtml-expressions'
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+
10
+ route.get(['/', '/index.html'], async (req, res) => {
11
+ const view = await fs.readFile(path.join(__dirname, '../views', 'index.html'), 'utf8')
12
+
13
+ // Group by `dir`
14
+ const groupedByDir = {}
15
+
16
+ req.templatePaths
17
+ .map(t => path.parse(t))
18
+ .forEach(file => {
19
+ if (!groupedByDir[file.dir]) {
20
+ groupedByDir[file.dir] = []
21
+ }
22
+
23
+ file.href = [file.dir.replace(file.root, ''), file.base].join('/')
24
+ groupedByDir[file.dir].push(file)
25
+ })
26
+
27
+ const { html } = await posthtml()
28
+ .use(expressions({
29
+ locals: {
30
+ templates: groupedByDir
31
+ }
32
+ }))
33
+ .process(view)
34
+
35
+ res.send(html)
36
+ })
37
+
38
+ export default route
@@ -0,0 +1,83 @@
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>Error</title>
7
+ <style>
8
+ body {
9
+ font-family: Helvetica, Arial, sans-serif;
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ .container {
16
+ display: flex;
17
+ flex-direction: column;
18
+ align-items: center;
19
+ text-align: center;
20
+ position: relative;
21
+ z-index: 1;
22
+ padding: 24px;
23
+ }
24
+
25
+ .stack-trace-wrapper {
26
+ width: 100%;
27
+ max-width: 90ch;
28
+ margin-top: 2.25rem;
29
+ }
30
+
31
+ .stack-trace {
32
+ overflow-x: auto;
33
+ padding: 24px;
34
+ border-radius: 4px;
35
+ text-align: left;
36
+ font-size: 1rem;
37
+ box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.1);
38
+ border-left: 4px solid #FB7185;
39
+ background-color: rgba(248, 250, 252, 0.7);
40
+ backdrop-filter: blur(4px);
41
+ font-family: 'Courier New', Courier, monospace;
42
+ }
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <div class="container">
47
+ <h1 style="font-size: 3rem; color: #0F172A; margin: 2.25rem 0">
48
+ <span style="color: #4f46e5">Oops!</span>
49
+ Something went wrong.
50
+ </h1>
51
+
52
+ <p style="margin: 0 0 2.25rem; font-size: 1.25rem; line-height: 1.5; color: #64748B;">
53
+ {{ page.error.message }}
54
+ </p>
55
+
56
+ <span style="padding: 2px 12px; font-size: 1rem; line-height: 1.5; color: #fff; background-color: #64748B; border-radius: 8px;">
57
+ {{ page.method }}
58
+ </span>
59
+
60
+ <p style="margin: 1rem 0 0; font-size: 1rem; line-height: 1.5; font-weight: 600; color: #334155;">
61
+ {{ page.url }}
62
+ </p>
63
+
64
+ <if condition="page.error.code">
65
+ <p style="margin: 1rem 0 0; font-size: 1rem; line-height: 1.5; font-weight: 600; color: #334155;">
66
+ {{ page.error.code }}
67
+ </p>
68
+ </if>
69
+
70
+ <div class="stack-trace-wrapper">
71
+ <div class="stack-trace">
72
+ <each loop="line, index in page.error.stack.split('\n')">
73
+ <p>{{{ line }}}</p>
74
+ </each>
75
+ </div>
76
+ </div>
77
+ </div>
78
+
79
+ <div style="position: fixed; bottom: 0; right: 0; pointer-events: none; user-select: none;">
80
+ <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>
81
+ </div>
82
+ </body>
83
+ </html>