@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.
- package/bin/maizzle +3 -1
- package/package.json +65 -58
- package/src/commands/build.js +244 -19
- package/src/commands/serve.js +2 -197
- package/src/generators/plaintext.js +192 -91
- package/src/generators/render.js +128 -0
- package/src/index.js +46 -14
- package/src/{generators/posthtml → posthtml}/defaultComponentsConfig.js +6 -4
- package/src/{generators/posthtml → posthtml}/defaultConfig.js +1 -1
- package/src/posthtml/index.js +74 -0
- package/src/posthtml/plugins/expandLinkTag.js +36 -0
- package/src/server/client.js +181 -0
- package/src/server/index.js +383 -0
- package/src/server/routes/hmr.js +24 -0
- package/src/server/routes/index.js +38 -0
- package/src/server/views/error.html +83 -0
- package/src/server/views/index.html +24 -0
- package/src/server/websockets.js +27 -0
- package/src/transformers/addAttributes.js +30 -0
- package/src/transformers/attributeToStyle.js +30 -36
- package/src/transformers/baseUrl.js +52 -23
- package/src/transformers/comb.js +51 -0
- package/src/transformers/core.js +20 -0
- package/src/transformers/filters/defaultFilters.js +90 -70
- package/src/transformers/filters/index.js +14 -78
- package/src/transformers/index.js +268 -63
- package/src/transformers/inline.js +240 -0
- package/src/transformers/markdown.js +13 -14
- package/src/transformers/minify.js +21 -16
- package/src/transformers/posthtmlMso.js +13 -8
- package/src/transformers/prettify.js +16 -15
- package/src/transformers/preventWidows.js +32 -26
- package/src/transformers/removeAttributes.js +17 -17
- package/src/transformers/replaceStrings.js +30 -9
- package/src/transformers/safeClassNames.js +24 -24
- package/src/transformers/shorthandCss.js +22 -0
- package/src/transformers/sixHex.js +15 -15
- package/src/transformers/urlParameters.js +18 -16
- package/src/transformers/useAttributeSizes.js +65 -0
- package/src/utils/getConfigByFilePath.js +124 -0
- package/src/utils/node.js +68 -0
- package/src/utils/string.js +117 -0
- package/types/build.d.ts +117 -57
- package/types/components.d.ts +130 -112
- package/types/config.d.ts +454 -242
- package/types/css/inline.d.ts +234 -0
- package/types/css/purge.d.ts +125 -0
- package/types/events.d.ts +5 -105
- package/types/index.d.ts +148 -116
- package/types/markdown.d.ts +20 -18
- package/types/minify.d.ts +122 -120
- package/types/plaintext.d.ts +46 -52
- package/types/posthtml.d.ts +103 -136
- package/types/render.d.ts +0 -117
- package/types/urlParameters.d.ts +21 -20
- package/types/widowWords.d.ts +9 -7
- package/src/functions/plaintext.js +0 -5
- package/src/functions/render.js +0 -5
- package/src/generators/config.js +0 -52
- package/src/generators/output/index.js +0 -4
- package/src/generators/output/to-disk.js +0 -254
- package/src/generators/output/to-string.js +0 -73
- package/src/generators/postcss.js +0 -23
- package/src/generators/posthtml/index.js +0 -75
- package/src/generators/tailwindcss.js +0 -157
- package/src/transformers/extraAttributes.js +0 -33
- package/src/transformers/inlineCss.js +0 -42
- package/src/transformers/removeInlineBackgroundColor.js +0 -56
- package/src/transformers/removeInlineSizes.js +0 -43
- package/src/transformers/removeInlinedSelectors.js +0 -100
- package/src/transformers/removeUnusedCss.js +0 -48
- package/src/transformers/shorthandInlineCSS.js +0 -26
- package/src/utils/helpers.js +0 -13
- package/types/baseUrl.d.ts +0 -79
- package/types/fetch.d.ts +0 -143
- package/types/inlineCss.d.ts +0 -207
- package/types/layouts.d.ts +0 -39
- package/types/removeUnusedCss.d.ts +0 -115
- package/types/tailwind.d.ts +0 -22
- 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>
|