methanol 0.0.1 → 0.0.2
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/README.md +58 -0
- package/package.json +10 -1
- package/src/config.js +59 -18
- package/src/dev-server.js +80 -7
- package/src/main.js +47 -1
- package/src/mdx.js +176 -24
- package/src/pagefind.js +10 -2
- package/src/pages.js +142 -9
- package/src/rehype-plugins/link-resolve.js +35 -8
- package/src/state.js +23 -3
- package/src/vite-plugins.js +2 -2
- package/themes/default/components/ThemeAccentSwitch.client.jsx +95 -0
- package/themes/default/components/ThemeAccentSwitch.static.jsx +23 -0
- package/themes/default/components/ThemeColorSwitch.client.jsx +1 -1
- package/themes/default/components/ThemeSearchBox.client.jsx +71 -34
- package/themes/default/components/ThemeSearchBox.static.jsx +0 -1
- package/themes/default/components/pre.client.jsx +1 -1
- package/themes/default/components/{pre.jsx → pre.static.jsx} +1 -1
- package/themes/default/index.js +4 -13
- package/themes/default/page.jsx +61 -7
- package/themes/default/pages/index.mdx +24 -2
- package/themes/default/public/favicon.png +0 -0
- package/themes/default/sources/prefetch.js +49 -0
- package/themes/default/{resources → sources}/style.css +600 -29
- package/.editorconfig +0 -19
- package/.prettierrc +0 -10
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Methanol
|
|
2
|
+
|
|
3
|
+
Opinionated MDX-first static site generator powered by rEFui + Vite.
|
|
4
|
+
|
|
5
|
+
For full documentation and examples, see the `methanol-docs` project.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# build
|
|
11
|
+
npx methanol build
|
|
12
|
+
|
|
13
|
+
# dev server
|
|
14
|
+
npx methanol dev
|
|
15
|
+
|
|
16
|
+
# preview the production build
|
|
17
|
+
npx methanol serve
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
From this repo, use `node bin/methanol.js [dev|build|serve]`.
|
|
21
|
+
|
|
22
|
+
## Project layout
|
|
23
|
+
|
|
24
|
+
Methanol expects a project like this:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pages/ # .mdx pages (file-based routing)
|
|
28
|
+
components/ # JSX/TSX components used by MDX
|
|
29
|
+
public/ # static assets copied/served as-is
|
|
30
|
+
dist/ # build output
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
Create `methanol.config.{js,mjs,cjs,ts,jsx,tsx,mts,cts}` and export a function:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
export default () => ({
|
|
39
|
+
// optional: search (Pagefind)
|
|
40
|
+
pagefind: {
|
|
41
|
+
enabled: true
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// optional: code highlighting (Starry Night)
|
|
45
|
+
starryNight: false,
|
|
46
|
+
|
|
47
|
+
// optional: theme sources
|
|
48
|
+
theme: {
|
|
49
|
+
sources: {
|
|
50
|
+
'/.my-theme': './sources'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## CLI notes
|
|
57
|
+
|
|
58
|
+
- `methanol preview` is an alias for `methanol serve`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "methanol",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Static site generator powered by rEFui and MDX",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,15 @@
|
|
|
16
16
|
"bin": {
|
|
17
17
|
"methanol": "./bin/methanol.js"
|
|
18
18
|
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin/",
|
|
21
|
+
"src/",
|
|
22
|
+
"themes/",
|
|
23
|
+
"index.js",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"README.md",
|
|
26
|
+
"banner.txt"
|
|
27
|
+
],
|
|
19
28
|
"dependencies": {
|
|
20
29
|
"@mdx-js/mdx": "^3.1.1",
|
|
21
30
|
"@sindresorhus/fnv1a": "^3.1.0",
|
package/src/config.js
CHANGED
|
@@ -34,8 +34,10 @@ const CONFIG_FILENAMES = [
|
|
|
34
34
|
'methanol.config.mjs',
|
|
35
35
|
'methanol.config.cjs',
|
|
36
36
|
'methanol.config.ts',
|
|
37
|
+
'methanol.config.jsx',
|
|
37
38
|
'methanol.config.mts',
|
|
38
|
-
'methanol.config.cts'
|
|
39
|
+
'methanol.config.cts',
|
|
40
|
+
'methanol.config.tsx'
|
|
39
41
|
]
|
|
40
42
|
|
|
41
43
|
const resolveRootPath = (value) => {
|
|
@@ -78,7 +80,7 @@ const resolveThemePublicDir = (root, value) => {
|
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key)
|
|
81
|
-
const
|
|
83
|
+
const normalizeSources = (value, root) => {
|
|
82
84
|
if (!value) return []
|
|
83
85
|
const entries = []
|
|
84
86
|
const addEntry = (find, replacement) => {
|
|
@@ -120,7 +122,7 @@ const resolvePagefindEnabled = (config) => {
|
|
|
120
122
|
const resolvePagefindOptions = (config) => {
|
|
121
123
|
const value = config?.pagefind
|
|
122
124
|
if (!value || typeof value !== 'object') return null
|
|
123
|
-
const { enabled, options, ...rest } = value
|
|
125
|
+
const { enabled, options, build, buildOptions, ...rest } = value
|
|
124
126
|
if (options && typeof options === 'object') {
|
|
125
127
|
return { ...options }
|
|
126
128
|
}
|
|
@@ -130,16 +132,42 @@ const resolvePagefindOptions = (config) => {
|
|
|
130
132
|
return null
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
const
|
|
135
|
+
const resolvePagefindBuild = (config) => {
|
|
134
136
|
const value = config?.pagefind
|
|
135
137
|
if (!value || typeof value !== 'object') return null
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
return { ...
|
|
138
|
+
const build = value.build
|
|
139
|
+
if (build && typeof build === 'object') {
|
|
140
|
+
return { ...build }
|
|
139
141
|
}
|
|
140
142
|
return null
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
const resolveStarryNightConfig = (value) => {
|
|
146
|
+
if (value == null) return { enabled: false, options: null }
|
|
147
|
+
if (typeof value === 'boolean') {
|
|
148
|
+
return { enabled: value, options: null }
|
|
149
|
+
}
|
|
150
|
+
if (typeof value !== 'object') {
|
|
151
|
+
return { enabled: false, options: null }
|
|
152
|
+
}
|
|
153
|
+
const { enabled, options, ...rest } = value
|
|
154
|
+
if (enabled === false) return { enabled: false, options: null }
|
|
155
|
+
if (options && typeof options === 'object') {
|
|
156
|
+
return { enabled: true, options: { ...options } }
|
|
157
|
+
}
|
|
158
|
+
if (Object.keys(rest).length) {
|
|
159
|
+
return { enabled: true, options: { ...rest } }
|
|
160
|
+
}
|
|
161
|
+
return { enabled: true, options: null }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const normalizeHooks = (value) => {
|
|
165
|
+
if (!value) return []
|
|
166
|
+
if (typeof value === 'function') return [value]
|
|
167
|
+
if (Array.isArray(value)) return value.filter((entry) => typeof entry === 'function')
|
|
168
|
+
return []
|
|
169
|
+
}
|
|
170
|
+
|
|
143
171
|
const loadConfigModule = async (filePath) => {
|
|
144
172
|
return import(`${pathToFileURL(filePath).href}?t=${Date.now()}`)
|
|
145
173
|
}
|
|
@@ -191,12 +219,12 @@ export const applyConfig = async (config, mode) => {
|
|
|
191
219
|
if (mode) {
|
|
192
220
|
state.CURRENT_MODE = mode
|
|
193
221
|
}
|
|
194
|
-
|
|
222
|
+
// config.paths / config.dirs are intentionally ignored (deprecated)
|
|
195
223
|
|
|
196
|
-
const pagesDirValue = cli.CLI_PAGES_DIR || config.pagesDir
|
|
197
|
-
const componentsDirValue = cli.CLI_COMPONENTS_DIR || config.componentsDir
|
|
198
|
-
const distDirValue = cli.CLI_OUTPUT_DIR || config.distDir
|
|
199
|
-
const publicDirValue = cli.CLI_ASSETS_DIR ?? config.publicDir
|
|
224
|
+
const pagesDirValue = cli.CLI_PAGES_DIR || config.pagesDir
|
|
225
|
+
const componentsDirValue = cli.CLI_COMPONENTS_DIR || config.componentsDir
|
|
226
|
+
const distDirValue = cli.CLI_OUTPUT_DIR || config.distDir
|
|
227
|
+
const publicDirValue = cli.CLI_ASSETS_DIR ?? config.publicDir
|
|
200
228
|
|
|
201
229
|
const resolvePagesFallback = () => {
|
|
202
230
|
const pagesPath = resolveFromRoot(root, 'pages', 'pages')
|
|
@@ -210,18 +238,18 @@ export const applyConfig = async (config, mode) => {
|
|
|
210
238
|
: resolvePagesFallback()
|
|
211
239
|
state.COMPONENTS_DIR = resolveFromRoot(root, componentsDirValue, 'components')
|
|
212
240
|
state.STATIC_DIR = resolveOptionalPath(root, publicDirValue, 'public')
|
|
213
|
-
state.BUILD_DIR = resolveFromRoot(root, config.buildDir
|
|
241
|
+
state.BUILD_DIR = resolveFromRoot(root, config.buildDir, 'build')
|
|
214
242
|
state.DIST_DIR = resolveFromRoot(root, distDirValue, 'dist')
|
|
215
243
|
|
|
216
|
-
const userSpecifiedPagesDir = cli.CLI_PAGES_DIR != null || hasOwn(config, 'pagesDir')
|
|
244
|
+
const userSpecifiedPagesDir = cli.CLI_PAGES_DIR != null || hasOwn(config, 'pagesDir')
|
|
217
245
|
if (userSpecifiedPagesDir && !existsSync(state.PAGES_DIR)) {
|
|
218
246
|
throw new Error(`Pages directory not found: ${state.PAGES_DIR}`)
|
|
219
247
|
}
|
|
220
|
-
const userSpecifiedComponentsDir = cli.CLI_COMPONENTS_DIR != null || hasOwn(config, 'componentsDir')
|
|
248
|
+
const userSpecifiedComponentsDir = cli.CLI_COMPONENTS_DIR != null || hasOwn(config, 'componentsDir')
|
|
221
249
|
if (userSpecifiedComponentsDir && !existsSync(state.COMPONENTS_DIR)) {
|
|
222
250
|
throw new Error(`Components directory not found: ${state.COMPONENTS_DIR}`)
|
|
223
251
|
}
|
|
224
|
-
const userSpecifiedPublicDir = cli.CLI_ASSETS_DIR != null || hasOwn(config, 'publicDir')
|
|
252
|
+
const userSpecifiedPublicDir = cli.CLI_ASSETS_DIR != null || hasOwn(config, 'publicDir')
|
|
225
253
|
if (userSpecifiedPublicDir && state.STATIC_DIR !== false && !existsSync(state.STATIC_DIR)) {
|
|
226
254
|
state.STATIC_DIR = resolveFromRoot(root, publicDirValue, 'public')
|
|
227
255
|
}
|
|
@@ -270,14 +298,27 @@ export const applyConfig = async (config, mode) => {
|
|
|
270
298
|
) {
|
|
271
299
|
state.STATIC_DIR = state.THEME_PUBLIC_DIR
|
|
272
300
|
}
|
|
273
|
-
state.
|
|
301
|
+
state.SOURCES = normalizeSources(state.USER_THEME.sources, themeRoot)
|
|
274
302
|
state.USER_VITE_CONFIG = config.vite || null
|
|
275
303
|
state.USER_MDX_CONFIG = config.mdx || null
|
|
276
304
|
state.RESOLVED_MDX_CONFIG = undefined
|
|
277
305
|
state.RESOLVED_VITE_CONFIG = undefined
|
|
278
306
|
state.PAGEFIND_ENABLED = resolvePagefindEnabled(config)
|
|
279
307
|
state.PAGEFIND_OPTIONS = resolvePagefindOptions(config)
|
|
280
|
-
state.
|
|
308
|
+
state.PAGEFIND_BUILD = resolvePagefindBuild(config)
|
|
309
|
+
state.USER_PRE_BUILD_HOOKS = normalizeHooks(config.preBuild)
|
|
310
|
+
state.USER_POST_BUILD_HOOKS = normalizeHooks(config.postBuild)
|
|
311
|
+
state.THEME_PRE_BUILD_HOOKS = normalizeHooks(state.USER_THEME?.preBuild)
|
|
312
|
+
state.THEME_POST_BUILD_HOOKS = normalizeHooks(state.USER_THEME?.postBuild)
|
|
313
|
+
const starryNight = resolveStarryNightConfig(config.starryNight)
|
|
314
|
+
const cliCodeHighlighting = cli.CLI_CODE_HIGHLIGHTING
|
|
315
|
+
if (cliCodeHighlighting != null) {
|
|
316
|
+
state.STARRY_NIGHT_ENABLED = cliCodeHighlighting === true
|
|
317
|
+
state.STARRY_NIGHT_OPTIONS = cliCodeHighlighting === true ? starryNight.options : null
|
|
318
|
+
} else {
|
|
319
|
+
state.STARRY_NIGHT_ENABLED = starryNight.enabled
|
|
320
|
+
state.STARRY_NIGHT_OPTIONS = starryNight.enabled ? starryNight.options : null
|
|
321
|
+
}
|
|
281
322
|
|
|
282
323
|
if (cli.CLI_INTERMEDIATE_DIR) {
|
|
283
324
|
state.INTERMEDIATE_DIR = resolveFromRoot(root, cli.CLI_INTERMEDIATE_DIR, 'build')
|
package/src/dev-server.js
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { existsSync } from 'fs'
|
|
22
|
-
import {
|
|
22
|
+
import { readFile } from 'fs/promises'
|
|
23
|
+
import { resolve, dirname, extname, join, basename, relative } from 'path'
|
|
23
24
|
import { fileURLToPath } from 'url'
|
|
24
25
|
import chokidar from 'chokidar'
|
|
25
26
|
import { createServer, mergeConfig } from 'vite'
|
|
@@ -151,6 +152,48 @@ export const runViteDev = async () => {
|
|
|
151
152
|
return mdxPath
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
const resolveHtmlCandidates = (pathname) => {
|
|
156
|
+
const candidates = []
|
|
157
|
+
if (pathname === '/' || pathname === '') {
|
|
158
|
+
candidates.push('/index.html')
|
|
159
|
+
} else if (pathname.endsWith('.html')) {
|
|
160
|
+
candidates.push(pathname)
|
|
161
|
+
} else {
|
|
162
|
+
candidates.push(`${pathname}.html`)
|
|
163
|
+
candidates.push(`${pathname}/index.html`)
|
|
164
|
+
}
|
|
165
|
+
return candidates.map((candidate) =>
|
|
166
|
+
resolve(state.PAGES_DIR, candidate.replace(/^\//, ''))
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const shouldServeHtml = (relativePath, requestedPath, hasMdx) => {
|
|
171
|
+
if (hasMdx) return false
|
|
172
|
+
const baseName = basename(relativePath, '.html')
|
|
173
|
+
if (baseName.startsWith('_') || baseName.startsWith('.')) return false
|
|
174
|
+
const excludedDirs = pagesContext?.excludedDirs
|
|
175
|
+
if (excludedDirs?.size) {
|
|
176
|
+
const dir = relativePath.split('/').slice(0, -1).join('/')
|
|
177
|
+
for (const excludedDir of excludedDirs) {
|
|
178
|
+
if (!excludedDir) return false
|
|
179
|
+
if (dir === excludedDir || dir.startsWith(`${excludedDir}/`)) {
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const excludedRoutes = pagesContext?.excludedRoutes
|
|
185
|
+
if (excludedRoutes?.has(requestedPath)) return false
|
|
186
|
+
const excludedDirPaths = pagesContext?.excludedDirPaths
|
|
187
|
+
if (excludedDirPaths?.size) {
|
|
188
|
+
for (const dirPath of excludedDirPaths) {
|
|
189
|
+
if (requestedPath === dirPath || requestedPath.startsWith(`${dirPath}/`)) {
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return true
|
|
195
|
+
}
|
|
196
|
+
|
|
154
197
|
const htmlMiddleware = async (req, res, next) => {
|
|
155
198
|
if (!req.url || req.method !== 'GET') {
|
|
156
199
|
return next()
|
|
@@ -206,9 +249,38 @@ export const runViteDev = async () => {
|
|
|
206
249
|
? (pagesContext?.pagesByRouteIndex?.get(requestedPath) ?? pagesContext?.pagesByRoute?.get(requestedPath) ?? null)
|
|
207
250
|
: (pagesContext?.pagesByRoute?.get(requestedPath) ?? null)
|
|
208
251
|
let filePath = pageMeta?.filePath || resolvePageFile(requestedPath)
|
|
252
|
+
const hasMdx = Boolean(pageMeta) || existsSync(filePath)
|
|
209
253
|
let status = 200
|
|
210
254
|
let renderRoutePath = requestedPath
|
|
211
255
|
|
|
256
|
+
if (!hasMdx) {
|
|
257
|
+
const candidates = resolveHtmlCandidates(pathname)
|
|
258
|
+
for (const candidate of candidates) {
|
|
259
|
+
if (!existsSync(candidate)) continue
|
|
260
|
+
const relativePath = relative(state.PAGES_DIR, candidate).replace(/\\/g, '/')
|
|
261
|
+
if (relativePath.startsWith('..')) {
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
if (!shouldServeHtml(relativePath, requestedPath, hasMdx)) {
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const html = await readFile(candidate, 'utf-8')
|
|
269
|
+
const candidateUrl = `/${relativePath}`
|
|
270
|
+
const transformed = await server.transformIndexHtml(candidateUrl, html)
|
|
271
|
+
res.statusCode = 200
|
|
272
|
+
res.setHeader('Content-Type', 'text/html')
|
|
273
|
+
res.end(transformed)
|
|
274
|
+
return
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error(err)
|
|
277
|
+
res.statusCode = 500
|
|
278
|
+
res.end('Internal Server Error')
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
212
284
|
if (isExcludedPath()) {
|
|
213
285
|
if (notFoundPage) {
|
|
214
286
|
filePath = notFoundPage.filePath
|
|
@@ -414,12 +486,13 @@ export const runViteDev = async () => {
|
|
|
414
486
|
prevEntry.toc = null
|
|
415
487
|
pagesContext.refreshPagesTree?.()
|
|
416
488
|
pagesContext.refreshLanguages?.()
|
|
417
|
-
if (prevEntry.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
489
|
+
if (prevEntry.content && prevEntry.content.trim().length) {
|
|
490
|
+
await compilePageMdx(prevEntry, pagesContext, {
|
|
491
|
+
lazyPagesTree: true,
|
|
492
|
+
refreshPagesTree: false
|
|
493
|
+
})
|
|
494
|
+
// Avoid caching a potentially stale render; recompile on request.
|
|
495
|
+
prevEntry.mdxComponent = null
|
|
423
496
|
}
|
|
424
497
|
return true
|
|
425
498
|
}
|
package/src/main.js
CHANGED
|
@@ -24,6 +24,7 @@ import { buildHtmlEntries, runViteBuild } from './build-system.js'
|
|
|
24
24
|
import { runPagefind } from './pagefind.js'
|
|
25
25
|
import { runVitePreview } from './preview-server.js'
|
|
26
26
|
import { cli, state } from './state.js'
|
|
27
|
+
import { HTMLRenderer } from './renderer.js'
|
|
27
28
|
import { readFile } from 'fs/promises'
|
|
28
29
|
|
|
29
30
|
const printBanner = async () => {
|
|
@@ -61,7 +62,39 @@ const main = async () => {
|
|
|
61
62
|
const mode = isDev ? 'development' : 'production'
|
|
62
63
|
const config = await loadUserConfig(mode, cli.CLI_CONFIG_PATH)
|
|
63
64
|
await applyConfig(config, mode)
|
|
65
|
+
const hookContext = {
|
|
66
|
+
mode,
|
|
67
|
+
root: state.ROOT_DIR,
|
|
68
|
+
command: normalizedCommand,
|
|
69
|
+
isDev,
|
|
70
|
+
isBuild,
|
|
71
|
+
isPreview,
|
|
72
|
+
HTMLRenderer,
|
|
73
|
+
site: {
|
|
74
|
+
name: state.SITE_NAME,
|
|
75
|
+
root: state.ROOT_DIR,
|
|
76
|
+
pagesDir: state.PAGES_DIR,
|
|
77
|
+
componentsDir: state.COMPONENTS_DIR,
|
|
78
|
+
publicDir: state.STATIC_DIR,
|
|
79
|
+
distDir: state.DIST_DIR,
|
|
80
|
+
mode: state.CURRENT_MODE,
|
|
81
|
+
pagefind: {
|
|
82
|
+
enabled: state.PAGEFIND_ENABLED,
|
|
83
|
+
options: state.PAGEFIND_OPTIONS || null,
|
|
84
|
+
build: state.PAGEFIND_BUILD || null
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
data: {}
|
|
88
|
+
}
|
|
89
|
+
const runHooks = async (hooks = [], extra = null) => {
|
|
90
|
+
const context = extra ? { ...hookContext, ...extra } : hookContext
|
|
91
|
+
for (const hook of hooks) {
|
|
92
|
+
await hook(context)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
64
95
|
if (isDev) {
|
|
96
|
+
await runHooks(state.USER_PRE_BUILD_HOOKS)
|
|
97
|
+
await runHooks(state.THEME_PRE_BUILD_HOOKS)
|
|
65
98
|
await runViteDev()
|
|
66
99
|
return
|
|
67
100
|
}
|
|
@@ -70,11 +103,24 @@ const main = async () => {
|
|
|
70
103
|
return
|
|
71
104
|
}
|
|
72
105
|
if (isBuild) {
|
|
73
|
-
|
|
106
|
+
await runHooks(state.USER_PRE_BUILD_HOOKS)
|
|
107
|
+
await runHooks(state.THEME_PRE_BUILD_HOOKS)
|
|
108
|
+
const { entry, htmlCache, pagesContext } = await buildHtmlEntries()
|
|
74
109
|
await runViteBuild(entry, htmlCache)
|
|
75
110
|
if (state.PAGEFIND_ENABLED) {
|
|
76
111
|
await runPagefind()
|
|
77
112
|
}
|
|
113
|
+
const postBuildContext = pagesContext
|
|
114
|
+
? {
|
|
115
|
+
pagesContext,
|
|
116
|
+
pages: pagesContext.pages,
|
|
117
|
+
pagesTree: pagesContext.pagesTree,
|
|
118
|
+
pagesByRoute: pagesContext.pagesByRoute,
|
|
119
|
+
site: pagesContext.site
|
|
120
|
+
}
|
|
121
|
+
: null
|
|
122
|
+
await runHooks(state.THEME_POST_BUILD_HOOKS, postBuildContext)
|
|
123
|
+
await runHooks(state.USER_POST_BUILD_HOOKS, postBuildContext)
|
|
78
124
|
return
|
|
79
125
|
}
|
|
80
126
|
cli.showHelp()
|
package/src/mdx.js
CHANGED
|
@@ -24,12 +24,13 @@ import * as JSXDevFactory from 'refui/jsx-dev-runtime'
|
|
|
24
24
|
import rehypeSlug from 'rehype-slug'
|
|
25
25
|
import extractToc from '@stefanprobst/rehype-extract-toc'
|
|
26
26
|
import withTocExport from '@stefanprobst/rehype-extract-toc/mdx'
|
|
27
|
+
import rehypeStarryNight from 'rehype-starry-night'
|
|
27
28
|
import { HTMLRenderer } from './renderer.js'
|
|
28
29
|
import { signal, computed, read, Suspense, nextTick } from 'refui'
|
|
29
30
|
import { createPortal } from 'refui/extras'
|
|
30
31
|
import { pathToFileURL } from 'url'
|
|
31
32
|
import { existsSync } from 'fs'
|
|
32
|
-
import { resolve } from 'path'
|
|
33
|
+
import { resolve, dirname, basename, relative } from 'path'
|
|
33
34
|
import { state } from './state.js'
|
|
34
35
|
import { resolveUserMdxConfig } from './config.js'
|
|
35
36
|
import { methanolCtx } from './rehype-plugins/methanol-ctx.js'
|
|
@@ -71,21 +72,113 @@ const resolveUserHeadAssets = () => {
|
|
|
71
72
|
return assets
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
const resolvePageAssetUrl = (page, filePath) => {
|
|
76
|
+
const root = page?.source === 'theme' && state.THEME_PAGES_DIR
|
|
77
|
+
? state.THEME_PAGES_DIR
|
|
78
|
+
: state.PAGES_DIR
|
|
79
|
+
if (!root) return null
|
|
80
|
+
const relPath = relative(root, filePath).replace(/\\/g, '/')
|
|
81
|
+
if (!relPath || relPath.startsWith('..')) return null
|
|
82
|
+
return `/${relPath}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const resolvePageHeadAssets = (page) => {
|
|
86
|
+
if (!page?.filePath) return []
|
|
87
|
+
const baseDir = dirname(page.filePath)
|
|
88
|
+
const baseName = basename(page.filePath).replace(/\.(mdx|md)$/, '')
|
|
89
|
+
const pagesRoot = state.PAGES_DIR ? resolve(state.PAGES_DIR) : null
|
|
90
|
+
const isRootIndex =
|
|
91
|
+
pagesRoot && baseName === 'index' && resolve(baseDir) === pagesRoot && page.source !== 'theme'
|
|
92
|
+
const isRootStylePage =
|
|
93
|
+
pagesRoot && baseName === 'style' && resolve(baseDir) === pagesRoot && page.source !== 'theme'
|
|
94
|
+
const assets = []
|
|
95
|
+
const cssPath = resolve(baseDir, `${baseName}.css`)
|
|
96
|
+
if (existsSync(cssPath)) {
|
|
97
|
+
if (isRootStylePage) {
|
|
98
|
+
const rootStyle = resolve(pagesRoot, 'style.css')
|
|
99
|
+
if (cssPath === rootStyle) {
|
|
100
|
+
return assets
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const href = resolvePageAssetUrl(page, cssPath)
|
|
104
|
+
if (href) {
|
|
105
|
+
assets.push(HTMLRenderer.c('link', { rel: 'stylesheet', href }))
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const scriptExtensions = ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts']
|
|
109
|
+
let scriptPath = null
|
|
110
|
+
for (const ext of scriptExtensions) {
|
|
111
|
+
const candidate = resolve(baseDir, `${baseName}${ext}`)
|
|
112
|
+
if (existsSync(candidate)) {
|
|
113
|
+
scriptPath = candidate
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (scriptPath) {
|
|
118
|
+
if (isRootIndex) {
|
|
119
|
+
const rootIndexJs = resolve(pagesRoot, 'index.js')
|
|
120
|
+
const rootIndexTs = resolve(pagesRoot, 'index.ts')
|
|
121
|
+
if (scriptPath === rootIndexJs || scriptPath === rootIndexTs) {
|
|
122
|
+
return assets
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const src = resolvePageAssetUrl(page, scriptPath)
|
|
126
|
+
if (src) {
|
|
127
|
+
assets.push(HTMLRenderer.c('script', { type: 'module', src }))
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return assets
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const buildPageContext = ({
|
|
134
|
+
routePath,
|
|
135
|
+
filePath,
|
|
136
|
+
pageMeta,
|
|
137
|
+
pagesContext,
|
|
138
|
+
lazyPagesTree = false
|
|
139
|
+
}) => {
|
|
75
140
|
const page = pageMeta
|
|
76
|
-
const pagesTree = pagesContext?.getPagesTree ? pagesContext.getPagesTree(routePath) : pagesContext?.pagesTree || []
|
|
77
141
|
const language = pagesContext?.getLanguageForRoute ? pagesContext.getLanguageForRoute(routePath) : null
|
|
78
|
-
|
|
142
|
+
const getSiblings = pagesContext?.getSiblings
|
|
143
|
+
? () => pagesContext.getSiblings(routePath, page?.filePath || filePath)
|
|
144
|
+
: null
|
|
145
|
+
if (page && getSiblings && page.getSiblings !== getSiblings) {
|
|
146
|
+
page.getSiblings = getSiblings
|
|
147
|
+
}
|
|
148
|
+
const ctx = {
|
|
79
149
|
routePath,
|
|
80
150
|
filePath,
|
|
81
151
|
page,
|
|
82
152
|
pages: pagesContext?.pages || [],
|
|
83
|
-
pagesTree,
|
|
84
153
|
pagesByRoute: pagesContext?.pagesByRoute || new Map(),
|
|
85
154
|
languages: pagesContext?.languages || [],
|
|
86
155
|
language,
|
|
87
|
-
site: pagesContext?.site || null
|
|
156
|
+
site: pagesContext?.site || null,
|
|
157
|
+
getSiblings
|
|
158
|
+
}
|
|
159
|
+
const resolvePagesTree = () =>
|
|
160
|
+
pagesContext?.getPagesTree ? pagesContext.getPagesTree(routePath) : pagesContext?.pagesTree || []
|
|
161
|
+
if (lazyPagesTree) {
|
|
162
|
+
let cachedTree = null
|
|
163
|
+
let hasTree = false
|
|
164
|
+
Object.defineProperty(ctx, 'pagesTree', {
|
|
165
|
+
enumerable: true,
|
|
166
|
+
get() {
|
|
167
|
+
if (!hasTree) {
|
|
168
|
+
cachedTree = resolvePagesTree()
|
|
169
|
+
hasTree = true
|
|
170
|
+
}
|
|
171
|
+
return cachedTree
|
|
172
|
+
},
|
|
173
|
+
set(value) {
|
|
174
|
+
cachedTree = value
|
|
175
|
+
hasTree = true
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
} else {
|
|
179
|
+
ctx.pagesTree = resolvePagesTree()
|
|
88
180
|
}
|
|
181
|
+
return ctx
|
|
89
182
|
}
|
|
90
183
|
|
|
91
184
|
const findTitleFromToc = (toc = []) => {
|
|
@@ -121,7 +214,39 @@ const findTitleFromToc = (toc = []) => {
|
|
|
121
214
|
|
|
122
215
|
let cachedMdxConfig = null
|
|
123
216
|
|
|
124
|
-
const
|
|
217
|
+
const normalizeStarryNightConfig = (value) => {
|
|
218
|
+
if (value == null) return null
|
|
219
|
+
if (typeof value === 'boolean') {
|
|
220
|
+
return { enabled: value, options: null }
|
|
221
|
+
}
|
|
222
|
+
if (typeof value !== 'object') return null
|
|
223
|
+
const { enabled, options, ...rest } = value
|
|
224
|
+
if (enabled === false) return { enabled: false, options: null }
|
|
225
|
+
if (options && typeof options === 'object') {
|
|
226
|
+
return { enabled: true, options: { ...options } }
|
|
227
|
+
}
|
|
228
|
+
if (Object.keys(rest).length) {
|
|
229
|
+
return { enabled: true, options: { ...rest } }
|
|
230
|
+
}
|
|
231
|
+
return { enabled: true, options: null }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const resolveStarryNightForPage = (frontmatter) => {
|
|
235
|
+
const base = {
|
|
236
|
+
enabled: state.STARRY_NIGHT_ENABLED === true,
|
|
237
|
+
options: state.STARRY_NIGHT_OPTIONS || null
|
|
238
|
+
}
|
|
239
|
+
if (!frontmatter || !Object.prototype.hasOwnProperty.call(frontmatter, 'starryNight')) {
|
|
240
|
+
return base
|
|
241
|
+
}
|
|
242
|
+
const override = normalizeStarryNightConfig(frontmatter.starryNight)
|
|
243
|
+
if (!override) return base
|
|
244
|
+
if (override.enabled === false) return { enabled: false, options: null }
|
|
245
|
+
const options = override.options != null ? override.options : base.options
|
|
246
|
+
return { enabled: true, options }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const resolveBaseMdxConfig = async () => {
|
|
125
250
|
const userMdxConfig = await resolveUserMdxConfig()
|
|
126
251
|
if (cachedMdxConfig) {
|
|
127
252
|
return cachedMdxConfig
|
|
@@ -135,16 +260,33 @@ const resolveMdxConfig = async () => {
|
|
|
135
260
|
rehypePlugins: [rehypeSlug, extractToc, [withTocExport, { name: 'toc' }]]
|
|
136
261
|
}
|
|
137
262
|
const mdxConfig = { ...baseMdxConfig, ...userMdxConfig }
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
263
|
+
const userRehypePlugins = Array.isArray(userMdxConfig.rehypePlugins) ? userMdxConfig.rehypePlugins : []
|
|
264
|
+
mdxConfig.rehypePlugins = [...baseMdxConfig.rehypePlugins, ...userRehypePlugins]
|
|
141
265
|
mdxConfig.rehypePlugins.push(linkResolve)
|
|
142
266
|
mdxConfig.rehypePlugins.push(methanolCtx)
|
|
143
267
|
return (cachedMdxConfig = mdxConfig)
|
|
144
268
|
}
|
|
145
269
|
|
|
270
|
+
const resolveMdxConfigForPage = async (frontmatter) => {
|
|
271
|
+
const baseConfig = await resolveBaseMdxConfig()
|
|
272
|
+
const mdxConfig = {
|
|
273
|
+
...baseConfig,
|
|
274
|
+
rehypePlugins: [...baseConfig.rehypePlugins]
|
|
275
|
+
}
|
|
276
|
+
const starryNightConfig = resolveStarryNightForPage(frontmatter)
|
|
277
|
+
if (!starryNightConfig.enabled) return mdxConfig
|
|
278
|
+
const plugin = starryNightConfig.options ? [rehypeStarryNight, starryNightConfig.options] : [rehypeStarryNight]
|
|
279
|
+
const insertIndex = mdxConfig.rehypePlugins.indexOf(linkResolve)
|
|
280
|
+
if (insertIndex >= 0) {
|
|
281
|
+
mdxConfig.rehypePlugins.splice(insertIndex, 0, plugin)
|
|
282
|
+
} else {
|
|
283
|
+
mdxConfig.rehypePlugins.push(plugin)
|
|
284
|
+
}
|
|
285
|
+
return mdxConfig
|
|
286
|
+
}
|
|
287
|
+
|
|
146
288
|
export const compileMdx = async ({ content, filePath, ctx }) => {
|
|
147
|
-
const mdxConfig = await
|
|
289
|
+
const mdxConfig = await resolveMdxConfigForPage(ctx?.page?.frontmatter)
|
|
148
290
|
const runtimeFactory = mdxConfig.development ? JSXDevFactory : JSXFactory
|
|
149
291
|
const compiled = await compile({ value: content, path: filePath }, mdxConfig)
|
|
150
292
|
|
|
@@ -156,17 +298,22 @@ export const compileMdx = async ({ content, filePath, ctx }) => {
|
|
|
156
298
|
})
|
|
157
299
|
}
|
|
158
300
|
|
|
159
|
-
export const compilePageMdx = async (page, pagesContext) => {
|
|
301
|
+
export const compilePageMdx = async (page, pagesContext, options = {}) => {
|
|
160
302
|
if (!page || page.content == null || page.mdxComponent) return
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
303
|
+
const { ctx = null, lazyPagesTree = false, refreshPagesTree = true } = options || {}
|
|
304
|
+
const activeCtx =
|
|
305
|
+
ctx ||
|
|
306
|
+
buildPageContext({
|
|
165
307
|
routePath: page.routePath,
|
|
166
308
|
filePath: page.filePath,
|
|
167
309
|
pageMeta: page,
|
|
168
|
-
pagesContext
|
|
310
|
+
pagesContext,
|
|
311
|
+
lazyPagesTree
|
|
169
312
|
})
|
|
313
|
+
const mdxModule = await compileMdx({
|
|
314
|
+
content: page.content,
|
|
315
|
+
filePath: page.filePath,
|
|
316
|
+
ctx: activeCtx
|
|
170
317
|
})
|
|
171
318
|
page.mdxComponent = mdxModule.default
|
|
172
319
|
page.toc = mdxModule.toc
|
|
@@ -181,11 +328,10 @@ export const compilePageMdx = async (page, pagesContext) => {
|
|
|
181
328
|
}
|
|
182
329
|
}
|
|
183
330
|
if (typeof pagesContext?.setDerivedTitle === 'function') {
|
|
184
|
-
pagesContext.setDerivedTitle(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
331
|
+
pagesContext.setDerivedTitle(page.filePath, shouldUseTocTitle ? page.title : null, page.toc)
|
|
332
|
+
}
|
|
333
|
+
if (ctx && refreshPagesTree && pagesContext?.getPagesTree) {
|
|
334
|
+
ctx.pagesTree = pagesContext.getPagesTree(activeCtx.routePath)
|
|
189
335
|
}
|
|
190
336
|
}
|
|
191
337
|
|
|
@@ -208,13 +354,19 @@ export const renderHtml = async ({
|
|
|
208
354
|
pageMeta,
|
|
209
355
|
pagesContext
|
|
210
356
|
})
|
|
357
|
+
await compilePageMdx(pageMeta, pagesContext, { ctx })
|
|
211
358
|
|
|
212
359
|
const [Head, Outlet] = createPortal()
|
|
213
360
|
const ExtraHead = () => {
|
|
214
|
-
return [
|
|
361
|
+
return [
|
|
362
|
+
RWND_INJECT,
|
|
363
|
+
...resolveUserHeadAssets(),
|
|
364
|
+
...resolvePageHeadAssets(pageMeta),
|
|
365
|
+
Outlet(),
|
|
366
|
+
RWND_FALLBACK
|
|
367
|
+
]
|
|
215
368
|
}
|
|
216
369
|
|
|
217
|
-
await compilePageMdx(pageMeta, pagesContext)
|
|
218
370
|
const mdxComponent = pageMeta?.mdxComponent
|
|
219
371
|
|
|
220
372
|
const Page = ({ components: extraComponents, ...props }, ...children) =>
|