methanol 0.0.0 → 0.0.1
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/.editorconfig +19 -0
- package/.prettierrc +10 -0
- package/LICENSE +203 -0
- package/banner.txt +6 -0
- package/bin/methanol.js +24 -0
- package/index.js +22 -0
- package/package.json +42 -9
- package/src/assets.js +30 -0
- package/src/build-system.js +200 -0
- package/src/components.js +145 -0
- package/src/config.js +355 -0
- package/src/dev-server.js +559 -0
- package/src/main.js +87 -0
- package/src/mdx.js +254 -0
- package/src/node-loader.js +88 -0
- package/src/pagefind.js +99 -0
- package/src/pages.js +638 -0
- package/src/preview-server.js +58 -0
- package/src/public-assets.js +73 -0
- package/src/register-loader.js +29 -0
- package/src/rehype-plugins/link-resolve.js +89 -0
- package/src/rehype-plugins/methanol-ctx.js +89 -0
- package/src/renderer.js +25 -0
- package/src/rewind.js +117 -0
- package/src/stage-logger.js +59 -0
- package/src/state.js +159 -0
- package/src/virtual-module/inject.js +30 -0
- package/src/virtual-module/loader.js +116 -0
- package/src/virtual-module/pagefind.js +108 -0
- package/src/vite-plugins.js +173 -0
- package/themes/default/components/ThemeColorSwitch.client.jsx +95 -0
- package/themes/default/components/ThemeColorSwitch.static.jsx +23 -0
- package/themes/default/components/ThemeSearchBox.client.jsx +287 -0
- package/themes/default/components/ThemeSearchBox.static.jsx +41 -0
- package/themes/default/components/ThemeToCContainer.client.jsx +154 -0
- package/themes/default/components/ThemeToCContainer.static.jsx +61 -0
- package/themes/default/components/pre.client.jsx +84 -0
- package/themes/default/components/pre.jsx +27 -0
- package/themes/default/heading.jsx +35 -0
- package/themes/default/index.js +50 -0
- package/themes/default/page.jsx +249 -0
- package/themes/default/pages/404.mdx +8 -0
- package/themes/default/pages/index.mdx +9 -0
- package/themes/default/public/logo.png +0 -0
- package/themes/default/resources/style.css +1089 -0
package/src/pages.js
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
/* Copyright Yukino Song, SudoMaker Ltd.
|
|
2
|
+
*
|
|
3
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
* or more contributor license agreements. See the NOTICE file
|
|
5
|
+
* distributed with this work for additional information
|
|
6
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
* to you under the Apache License, Version 2.0 (the
|
|
8
|
+
* "License"); you may not use this file except in compliance
|
|
9
|
+
* with the License. You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing,
|
|
14
|
+
* software distributed under the License is distributed on an
|
|
15
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
* KIND, either express or implied. See the License for the
|
|
17
|
+
* specific language governing permissions and limitations
|
|
18
|
+
* under the License.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import matter from 'gray-matter'
|
|
22
|
+
import { readdir, readFile, stat } from 'fs/promises'
|
|
23
|
+
import { existsSync } from 'fs'
|
|
24
|
+
import { resolve, join, relative } from 'path'
|
|
25
|
+
import { state, cli } from './state.js'
|
|
26
|
+
import { compilePageMdx } from './mdx.js'
|
|
27
|
+
import { createStageLogger } from './stage-logger.js'
|
|
28
|
+
|
|
29
|
+
const isPageFile = (name) => name.endsWith('.mdx') || name.endsWith('.md')
|
|
30
|
+
const isInternalPage = (name) => name.startsWith('_') || name.startsWith('.')
|
|
31
|
+
const isIgnoredEntry = (name) => name.startsWith('.')
|
|
32
|
+
|
|
33
|
+
const pageMetadataCache = new Map()
|
|
34
|
+
const pageDerivedCache = new Map()
|
|
35
|
+
|
|
36
|
+
const collectLanguagesFromPages = (pages = []) => {
|
|
37
|
+
const languages = new Map()
|
|
38
|
+
for (const page of pages) {
|
|
39
|
+
if (!page?.isIndex) continue
|
|
40
|
+
const label = page?.frontmatter?.lang
|
|
41
|
+
if (label == null || label === '') continue
|
|
42
|
+
const routePath = page.routePath || '/'
|
|
43
|
+
const href = page.routeHref || routePath || '/'
|
|
44
|
+
languages.set(routePath, {
|
|
45
|
+
routePath,
|
|
46
|
+
href,
|
|
47
|
+
label: String(label)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
return Array.from(languages.values()).sort((a, b) => a.href.localeCompare(b.href))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const normalizeRoutePath = (value) => {
|
|
54
|
+
if (!value || value === '/') return '/'
|
|
55
|
+
return value.replace(/\/+$/, '')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const resolveLanguageForRoute = (languages = [], routePath = '/') => {
|
|
59
|
+
if (!languages.length) return null
|
|
60
|
+
const normalizedRoute = normalizeRoutePath(routePath)
|
|
61
|
+
let best = null
|
|
62
|
+
let bestLength = -1
|
|
63
|
+
let rootLanguage = null
|
|
64
|
+
for (const lang of languages) {
|
|
65
|
+
const base = normalizeRoutePath(lang?.routePath || lang?.href)
|
|
66
|
+
if (!base) continue
|
|
67
|
+
if (base === '/') {
|
|
68
|
+
rootLanguage = lang
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
if (normalizedRoute === base || normalizedRoute.startsWith(`${base}/`)) {
|
|
72
|
+
if (base.length > bestLength) {
|
|
73
|
+
best = lang
|
|
74
|
+
bestLength = base.length
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return best || rootLanguage
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const routePathFromFile = (filePath, pagesDir = state.PAGES_DIR) => {
|
|
82
|
+
if (!filePath.endsWith('.mdx') && !filePath.endsWith('.md')) {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
const relPath = relative(pagesDir, filePath)
|
|
86
|
+
if (relPath.startsWith('..')) {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
const name = relPath.replace(/\.(mdx|md)$/, '')
|
|
90
|
+
const baseName = name.split(/[\\/]/).pop()
|
|
91
|
+
if (isInternalPage(baseName)) {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
const normalized = name.replace(/\\/g, '/')
|
|
95
|
+
if (normalized === 'index') {
|
|
96
|
+
return '/'
|
|
97
|
+
}
|
|
98
|
+
if (normalized.endsWith('/index')) {
|
|
99
|
+
return `/${normalized.slice(0, -'/index'.length)}`
|
|
100
|
+
}
|
|
101
|
+
return `/${normalized}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const parseFrontmatter = (raw) => {
|
|
105
|
+
const parsed = matter(raw)
|
|
106
|
+
const data = parsed.data || {}
|
|
107
|
+
const content = parsed.content ?? ''
|
|
108
|
+
return {
|
|
109
|
+
data,
|
|
110
|
+
content,
|
|
111
|
+
matter: parsed.matter ?? null
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const parsePageMetadata = async (filePath) => {
|
|
116
|
+
const raw = await readFile(filePath, 'utf-8')
|
|
117
|
+
const { data: frontmatter, content, matter } = parseFrontmatter(raw)
|
|
118
|
+
let title = frontmatter.title
|
|
119
|
+
return {
|
|
120
|
+
raw,
|
|
121
|
+
content,
|
|
122
|
+
frontmatter,
|
|
123
|
+
matter,
|
|
124
|
+
title
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const parseWeight = (value) => {
|
|
129
|
+
if (value == null || value === '') return null
|
|
130
|
+
const parsed = Number(value)
|
|
131
|
+
return Number.isFinite(parsed) ? parsed : null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const parseDate = (value) => {
|
|
135
|
+
if (!value) return null
|
|
136
|
+
const date = new Date(value)
|
|
137
|
+
return Number.isNaN(date.valueOf()) ? null : date.toISOString()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const stripRootPrefix = (value, rootDir) => {
|
|
141
|
+
if (!rootDir || !value) return value
|
|
142
|
+
if (value === rootDir) return ''
|
|
143
|
+
if (value.startsWith(`${rootDir}/`)) {
|
|
144
|
+
return value.slice(rootDir.length + 1)
|
|
145
|
+
}
|
|
146
|
+
return value
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const buildPagesTree = (pages, options = {}) => {
|
|
150
|
+
const rootPath = normalizeRoutePath(options.rootPath || '/')
|
|
151
|
+
const rootDir = rootPath === '/' ? '' : rootPath.replace(/^\/+/, '')
|
|
152
|
+
const includeHiddenRoot = Boolean(options.includeHiddenRoot)
|
|
153
|
+
const rootSegments = rootDir ? rootDir.split('/') : []
|
|
154
|
+
const isUnderRoot = (page) => {
|
|
155
|
+
if (!rootDir) return true
|
|
156
|
+
return page.routePath === rootPath || page.routePath.startsWith(`${rootPath}/`)
|
|
157
|
+
}
|
|
158
|
+
const treePages = pages
|
|
159
|
+
.filter((page) => !page.isInternal)
|
|
160
|
+
.filter((page) => isUnderRoot(page))
|
|
161
|
+
.map((page) => {
|
|
162
|
+
if (!rootDir) return page
|
|
163
|
+
const relativePath = stripRootPrefix(page.relativePath, rootDir)
|
|
164
|
+
const dir = stripRootPrefix(page.dir, rootDir)
|
|
165
|
+
const segments = page.segments.slice(rootSegments.length)
|
|
166
|
+
const depth = segments.length
|
|
167
|
+
return {
|
|
168
|
+
...page,
|
|
169
|
+
relativePath,
|
|
170
|
+
dir,
|
|
171
|
+
segments,
|
|
172
|
+
depth
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
const root = []
|
|
176
|
+
const dirs = new Map()
|
|
177
|
+
const hiddenDirs = new Set(
|
|
178
|
+
treePages
|
|
179
|
+
.filter((page) => page.isIndex && page.dir && page.hidden && !(includeHiddenRoot && page.routePath === rootPath))
|
|
180
|
+
.map((page) => page.dir)
|
|
181
|
+
)
|
|
182
|
+
if (includeHiddenRoot && rootDir) {
|
|
183
|
+
for (const hiddenDir of Array.from(hiddenDirs)) {
|
|
184
|
+
if (rootDir === hiddenDir || rootDir.startsWith(`${hiddenDir}/`)) {
|
|
185
|
+
hiddenDirs.delete(hiddenDir)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const isUnderHiddenDir = (dir) => {
|
|
190
|
+
if (!dir) return false
|
|
191
|
+
const parts = dir.split('/')
|
|
192
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
193
|
+
const candidate = parts.slice(0, i).join('/')
|
|
194
|
+
if (hiddenDirs.has(candidate)) {
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
const getDirNode = (path, name, depth) => {
|
|
201
|
+
if (dirs.has(path)) return dirs.get(path)
|
|
202
|
+
const dir = {
|
|
203
|
+
type: 'directory',
|
|
204
|
+
name,
|
|
205
|
+
path: `/${path}`,
|
|
206
|
+
children: [],
|
|
207
|
+
depth,
|
|
208
|
+
routePath: null,
|
|
209
|
+
title: null,
|
|
210
|
+
weight: null,
|
|
211
|
+
date: null,
|
|
212
|
+
routeHref: null,
|
|
213
|
+
isRoot: false
|
|
214
|
+
}
|
|
215
|
+
dirs.set(path, dir)
|
|
216
|
+
return dir
|
|
217
|
+
}
|
|
218
|
+
for (const page of treePages) {
|
|
219
|
+
if (page.hidden && !(includeHiddenRoot && page.routePath === rootPath)) {
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
if (isUnderHiddenDir(page.dir)) {
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
const parts = page.relativePath.split('/')
|
|
226
|
+
parts.pop()
|
|
227
|
+
let cursor = root
|
|
228
|
+
let currentPath = ''
|
|
229
|
+
for (const part of parts) {
|
|
230
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part
|
|
231
|
+
if (hiddenDirs.has(currentPath)) {
|
|
232
|
+
cursor = null
|
|
233
|
+
break
|
|
234
|
+
}
|
|
235
|
+
const dir = getDirNode(currentPath, part, currentPath.split('/').length)
|
|
236
|
+
if (!cursor.includes(dir)) {
|
|
237
|
+
cursor.push(dir)
|
|
238
|
+
}
|
|
239
|
+
cursor = dir.children
|
|
240
|
+
}
|
|
241
|
+
if (!cursor) {
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
if (page.isIndex && page.dir) {
|
|
245
|
+
const dir = getDirNode(page.dir, page.dir.split('/').pop(), page.depth)
|
|
246
|
+
dir.routePath = page.routePath
|
|
247
|
+
dir.routeHref = page.routeHref || page.routePath
|
|
248
|
+
dir.title = page.title
|
|
249
|
+
dir.weight = page.weight ?? null
|
|
250
|
+
dir.date = page.date ?? null
|
|
251
|
+
dir.isRoot = page.isRoot || false
|
|
252
|
+
dir.page = page
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
cursor.push({
|
|
256
|
+
type: 'page',
|
|
257
|
+
...page
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
const compareNodes = (a, b, level = 0) => {
|
|
261
|
+
if (a?.routePath === '/' && b?.routePath !== '/') return -1
|
|
262
|
+
if (b?.routePath === '/' && a?.routePath !== '/') return 1
|
|
263
|
+
if (a?.isIndex && !b?.isIndex) return -1
|
|
264
|
+
if (b?.isIndex && !a?.isIndex) return 1
|
|
265
|
+
if (level === 0 && a?.type !== b?.type) {
|
|
266
|
+
return a.type === 'page' ? -1 : 1
|
|
267
|
+
}
|
|
268
|
+
const weightA = Number.isFinite(a.weight) ? a.weight : null
|
|
269
|
+
const weightB = Number.isFinite(b.weight) ? b.weight : null
|
|
270
|
+
if (weightA != null || weightB != null) {
|
|
271
|
+
if (weightA == null) return 1
|
|
272
|
+
if (weightB == null) return -1
|
|
273
|
+
if (weightA !== weightB) return weightA - weightB
|
|
274
|
+
}
|
|
275
|
+
const dateA = a.date ? new Date(a.date).valueOf() : null
|
|
276
|
+
const dateB = b.date ? new Date(b.date).valueOf() : null
|
|
277
|
+
if (dateA != null || dateB != null) {
|
|
278
|
+
if (dateA == null) return 1
|
|
279
|
+
if (dateB == null) return -1
|
|
280
|
+
if (dateA !== dateB) return dateB - dateA
|
|
281
|
+
}
|
|
282
|
+
const labelA = (a.title || a.name || '').toLowerCase()
|
|
283
|
+
const labelB = (b.title || b.name || '').toLowerCase()
|
|
284
|
+
if (labelA < labelB) return -1
|
|
285
|
+
if (labelA > labelB) return 1
|
|
286
|
+
return 0
|
|
287
|
+
}
|
|
288
|
+
const sortTree = (nodes, level = 0) => {
|
|
289
|
+
nodes.sort((a, b) => compareNodes(a, b, level))
|
|
290
|
+
for (const node of nodes) {
|
|
291
|
+
if (node.type === 'directory') {
|
|
292
|
+
sortTree(node.children, level + 1)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
sortTree(root)
|
|
297
|
+
return root
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const walkPages = async function* (dir, basePath = '') {
|
|
301
|
+
const entries = await readdir(dir)
|
|
302
|
+
const files = []
|
|
303
|
+
const dirs = []
|
|
304
|
+
|
|
305
|
+
for (const entry of entries.sort()) {
|
|
306
|
+
if (isIgnoredEntry(entry)) {
|
|
307
|
+
continue
|
|
308
|
+
}
|
|
309
|
+
const fullPath = join(dir, entry)
|
|
310
|
+
const stats = await stat(fullPath)
|
|
311
|
+
if (stats.isDirectory()) {
|
|
312
|
+
dirs.push({ entry, fullPath })
|
|
313
|
+
} else if (isPageFile(entry)) {
|
|
314
|
+
files.push({ entry, fullPath })
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const { entry, fullPath } of files) {
|
|
319
|
+
const name = entry.replace(/\.(mdx|md)$/, '')
|
|
320
|
+
const relativePath = join(basePath, name).replace(/\\/g, '/')
|
|
321
|
+
const isIndex = name === 'index'
|
|
322
|
+
const routePath = isIndex ? (basePath ? `/${basePath}` : '/') : `/${relativePath}`
|
|
323
|
+
const routeHref = isIndex && basePath ? `/${basePath}/` : routePath
|
|
324
|
+
yield { routePath, routeHref, filePath: fullPath, isIndex }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for (const { entry, fullPath } of dirs) {
|
|
328
|
+
yield* walkPages(fullPath, join(basePath, entry))
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
|
|
333
|
+
const routePath = routePathFromFile(filePath, pagesDir)
|
|
334
|
+
if (!routePath) return null
|
|
335
|
+
const relPath = relative(pagesDir, filePath).replace(/\\/g, '/')
|
|
336
|
+
const name = relPath.replace(/\.(mdx|md)$/, '')
|
|
337
|
+
const baseName = name.split('/').pop()
|
|
338
|
+
const dir = name.split('/').slice(0, -1).join('/')
|
|
339
|
+
const dirName = dir ? dir.split('/').pop() : ''
|
|
340
|
+
const isIndex = baseName === 'index'
|
|
341
|
+
const routeHref = isIndex && dir ? `/${dir}/` : routePath
|
|
342
|
+
const segments = routePath.split('/').filter(Boolean)
|
|
343
|
+
const stats = await stat(filePath)
|
|
344
|
+
const cached = pageMetadataCache.get(filePath)
|
|
345
|
+
let metadata = null
|
|
346
|
+
if (cached && cached.mtimeMs === stats.mtimeMs) {
|
|
347
|
+
metadata = cached.metadata
|
|
348
|
+
} else {
|
|
349
|
+
metadata = await parsePageMetadata(filePath)
|
|
350
|
+
pageMetadataCache.set(filePath, { mtimeMs: stats.mtimeMs, metadata })
|
|
351
|
+
}
|
|
352
|
+
const derived = pageDerivedCache.get(filePath)
|
|
353
|
+
const exclude = Boolean(metadata.frontmatter?.exclude)
|
|
354
|
+
const frontmatterHidden = metadata.frontmatter?.hidden
|
|
355
|
+
const isNotFoundPage = routePath === '/404'
|
|
356
|
+
const hidden = frontmatterHidden === false
|
|
357
|
+
? false
|
|
358
|
+
: frontmatterHidden === true
|
|
359
|
+
? true
|
|
360
|
+
: isNotFoundPage || Boolean(metadata.frontmatter?.isRoot)
|
|
361
|
+
return {
|
|
362
|
+
routePath,
|
|
363
|
+
routeHref,
|
|
364
|
+
filePath,
|
|
365
|
+
source,
|
|
366
|
+
relativePath: relPath,
|
|
367
|
+
name: baseName,
|
|
368
|
+
dir,
|
|
369
|
+
segments,
|
|
370
|
+
depth: segments.length,
|
|
371
|
+
isIndex,
|
|
372
|
+
isInternal: isInternalPage(baseName),
|
|
373
|
+
title: metadata.title || derived?.title || (baseName === 'index' ? (dirName || 'Home') : baseName),
|
|
374
|
+
weight: parseWeight(metadata.frontmatter?.weight),
|
|
375
|
+
date: parseDate(metadata.frontmatter?.date) || parseDate(stats.mtime),
|
|
376
|
+
isRoot: Boolean(metadata.frontmatter?.isRoot),
|
|
377
|
+
hidden,
|
|
378
|
+
exclude,
|
|
379
|
+
content: metadata.content,
|
|
380
|
+
frontmatter: metadata.frontmatter,
|
|
381
|
+
toc: derived?.toc || null,
|
|
382
|
+
matter: metadata.matter ?? null,
|
|
383
|
+
stats: {
|
|
384
|
+
size: stats.size,
|
|
385
|
+
createdAt: stats.birthtime?.toISOString?.() || null,
|
|
386
|
+
updatedAt: stats.mtime?.toISOString?.() || null
|
|
387
|
+
},
|
|
388
|
+
createdAt: stats.birthtime?.toISOString?.() || null,
|
|
389
|
+
updatedAt: stats.mtime?.toISOString?.() || null
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const collectPagesFromDir = async (pagesDir, source) => {
|
|
394
|
+
if (!pagesDir || !existsSync(pagesDir)) {
|
|
395
|
+
return []
|
|
396
|
+
}
|
|
397
|
+
const pages = []
|
|
398
|
+
for await (const page of walkPages(pagesDir)) {
|
|
399
|
+
const entry = await buildPageEntry({
|
|
400
|
+
filePath: page.filePath,
|
|
401
|
+
pagesDir,
|
|
402
|
+
source
|
|
403
|
+
})
|
|
404
|
+
if (entry) {
|
|
405
|
+
entry.routeHref = page.routeHref || entry.routeHref
|
|
406
|
+
entry.isIndex = page.isIndex || entry.isIndex
|
|
407
|
+
pages.push(entry)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return pages
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const collectPages = async () => {
|
|
414
|
+
const userPages = await collectPagesFromDir(state.PAGES_DIR, 'user')
|
|
415
|
+
const themePages = state.THEME_PAGES_DIR
|
|
416
|
+
? await collectPagesFromDir(state.THEME_PAGES_DIR, 'theme')
|
|
417
|
+
: []
|
|
418
|
+
const userRoutes = new Set(userPages.map((page) => page.routePath))
|
|
419
|
+
const pages = [...userPages, ...themePages.filter((page) => !userRoutes.has(page.routePath))]
|
|
420
|
+
const excludedDirs = new Set(pages.filter((page) => page.exclude && page.isIndex && page.dir).map((page) => page.dir))
|
|
421
|
+
const isUnderExcludedDir = (dir) => {
|
|
422
|
+
if (!dir) return false
|
|
423
|
+
const parts = dir.split('/')
|
|
424
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
425
|
+
const candidate = parts.slice(0, i).join('/')
|
|
426
|
+
if (excludedDirs.has(candidate)) {
|
|
427
|
+
return true
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return false
|
|
431
|
+
}
|
|
432
|
+
const excludedRoutes = new Set(pages.filter((page) => page.exclude).map((page) => page.routePath))
|
|
433
|
+
const filteredPages = pages.filter((page) => {
|
|
434
|
+
if (page.exclude) return false
|
|
435
|
+
if (isUnderExcludedDir(page.dir)) return false
|
|
436
|
+
return true
|
|
437
|
+
})
|
|
438
|
+
return { pages: filteredPages, excludedRoutes, excludedDirs }
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const buildIndexFallback = (pages, siteName) => {
|
|
442
|
+
const visiblePages = pages
|
|
443
|
+
.filter((page) => !page.isInternal && page.routePath !== '/')
|
|
444
|
+
.sort((a, b) => a.routePath.localeCompare(b.routePath))
|
|
445
|
+
|
|
446
|
+
const lines = [
|
|
447
|
+
`# ${siteName || 'Methanol Site'}`,
|
|
448
|
+
'',
|
|
449
|
+
'No `index.md` or `index.mdx` found in your pages directory.',
|
|
450
|
+
'',
|
|
451
|
+
'## Pages'
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
if (!visiblePages.length) {
|
|
455
|
+
lines.push('', 'No pages found yet.')
|
|
456
|
+
return lines.join('\n')
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
lines.push('')
|
|
460
|
+
for (const page of visiblePages) {
|
|
461
|
+
const label = page.title || page.routePath
|
|
462
|
+
lines.push(`- [${label}](${encodeURI(page.routePath)})`)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return lines.join('\n')
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const resolveRootPath = (routePath, pagesByRoute, pagesByRouteIndex = null) => {
|
|
469
|
+
const normalized = normalizeRoutePath(routePath || '/')
|
|
470
|
+
const segments = normalized.split('/').filter(Boolean)
|
|
471
|
+
const lookup = pagesByRouteIndex || pagesByRoute
|
|
472
|
+
for (let i = segments.length; i >= 1; i--) {
|
|
473
|
+
const candidate = `/${segments.slice(0, i).join('/')}`
|
|
474
|
+
const page = lookup.get(candidate)
|
|
475
|
+
if (page?.isIndex && page?.isRoot) {
|
|
476
|
+
return candidate
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return '/'
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export const buildPagesContext = async ({ compileAll = true } = {}) => {
|
|
483
|
+
const logEnabled = state.CURRENT_MODE === 'production' && cli.command === 'build'
|
|
484
|
+
const stageLogger = createStageLogger(logEnabled)
|
|
485
|
+
const collectToken = stageLogger.start('Collecting pages')
|
|
486
|
+
const collected = await collectPages()
|
|
487
|
+
stageLogger.end(collectToken)
|
|
488
|
+
let pages = collected.pages
|
|
489
|
+
const excludedRoutes = collected.excludedRoutes
|
|
490
|
+
const excludedDirs = collected.excludedDirs
|
|
491
|
+
const hasIndex = pages.some((page) => page.routePath === '/')
|
|
492
|
+
if (!hasIndex) {
|
|
493
|
+
const content = buildIndexFallback(pages, state.SITE_NAME)
|
|
494
|
+
pages = [
|
|
495
|
+
{
|
|
496
|
+
routePath: '/',
|
|
497
|
+
routeHref: '/',
|
|
498
|
+
filePath: resolve(state.PAGES_DIR, 'index.md'),
|
|
499
|
+
relativePath: 'index.md',
|
|
500
|
+
name: 'index',
|
|
501
|
+
dir: '',
|
|
502
|
+
segments: [],
|
|
503
|
+
depth: 0,
|
|
504
|
+
isIndex: true,
|
|
505
|
+
isInternal: false,
|
|
506
|
+
title: state.SITE_NAME || 'Methanol Site',
|
|
507
|
+
weight: null,
|
|
508
|
+
date: null,
|
|
509
|
+
isRoot: false,
|
|
510
|
+
hidden: false,
|
|
511
|
+
content,
|
|
512
|
+
frontmatter: {},
|
|
513
|
+
matter: null,
|
|
514
|
+
stats: { size: content.length, createdAt: null, updatedAt: null },
|
|
515
|
+
createdAt: null,
|
|
516
|
+
updatedAt: null
|
|
517
|
+
},
|
|
518
|
+
...pages
|
|
519
|
+
]
|
|
520
|
+
if (excludedRoutes?.has('/')) {
|
|
521
|
+
excludedRoutes.delete('/')
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const pagesByRoute = new Map()
|
|
526
|
+
const pagesByRouteIndex = new Map()
|
|
527
|
+
for (const page of pages) {
|
|
528
|
+
if (page.isIndex) {
|
|
529
|
+
pagesByRouteIndex.set(page.routePath, page)
|
|
530
|
+
if (!pagesByRoute.has(page.routePath)) {
|
|
531
|
+
pagesByRoute.set(page.routePath, page)
|
|
532
|
+
}
|
|
533
|
+
continue
|
|
534
|
+
}
|
|
535
|
+
const existing = pagesByRoute.get(page.routePath)
|
|
536
|
+
if (!existing || existing.isIndex) {
|
|
537
|
+
pagesByRoute.set(page.routePath, page)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const getPageByRoute = (routePath, options = {}) => {
|
|
541
|
+
const { filePath, preferIndex } = options || {}
|
|
542
|
+
if (filePath) {
|
|
543
|
+
for (const page of pages) {
|
|
544
|
+
if (page.routePath === routePath && page.filePath === filePath) {
|
|
545
|
+
return page
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (preferIndex === true) {
|
|
550
|
+
return pagesByRouteIndex.get(routePath) || pagesByRoute.get(routePath) || null
|
|
551
|
+
}
|
|
552
|
+
if (preferIndex === false) {
|
|
553
|
+
return pagesByRoute.get(routePath) || pagesByRouteIndex.get(routePath) || null
|
|
554
|
+
}
|
|
555
|
+
return pagesByRoute.get(routePath) || pagesByRouteIndex.get(routePath) || null
|
|
556
|
+
}
|
|
557
|
+
const pagesTreeCache = new Map()
|
|
558
|
+
const getPagesTree = (routePath) => {
|
|
559
|
+
const rootPath = resolveRootPath(routePath, pagesByRoute, pagesByRouteIndex)
|
|
560
|
+
if (pagesTreeCache.has(rootPath)) {
|
|
561
|
+
return pagesTreeCache.get(rootPath)
|
|
562
|
+
}
|
|
563
|
+
const tree = buildPagesTree(pages, {
|
|
564
|
+
rootPath,
|
|
565
|
+
includeHiddenRoot: rootPath !== '/'
|
|
566
|
+
})
|
|
567
|
+
pagesTreeCache.set(rootPath, tree)
|
|
568
|
+
return tree
|
|
569
|
+
}
|
|
570
|
+
let pagesTree = getPagesTree('/')
|
|
571
|
+
const notFound = pagesByRoute.get('/404') || null
|
|
572
|
+
const languages = collectLanguagesFromPages(pages)
|
|
573
|
+
const site = {
|
|
574
|
+
name: state.SITE_NAME,
|
|
575
|
+
root: state.ROOT_DIR,
|
|
576
|
+
pagesDir: state.PAGES_DIR,
|
|
577
|
+
componentsDir: state.COMPONENTS_DIR,
|
|
578
|
+
publicDir: state.STATIC_DIR,
|
|
579
|
+
distDir: state.DIST_DIR,
|
|
580
|
+
mode: state.CURRENT_MODE,
|
|
581
|
+
pagefind: {
|
|
582
|
+
enabled: state.PAGEFIND_ENABLED,
|
|
583
|
+
options: state.PAGEFIND_OPTIONS || null,
|
|
584
|
+
buildOptions: state.PAGEFIND_BUILD_OPTIONS || null
|
|
585
|
+
},
|
|
586
|
+
generatedAt: new Date().toISOString()
|
|
587
|
+
}
|
|
588
|
+
const excludedDirPaths = new Set(Array.from(excludedDirs).map((dir) => `/${dir}`))
|
|
589
|
+
const pagesContext = {
|
|
590
|
+
pages,
|
|
591
|
+
pagesByRoute,
|
|
592
|
+
pagesByRouteIndex,
|
|
593
|
+
getPageByRoute,
|
|
594
|
+
pagesTree,
|
|
595
|
+
getPagesTree,
|
|
596
|
+
derivedTitleCache: pageDerivedCache,
|
|
597
|
+
setDerivedTitle: (filePath, title, toc) => {
|
|
598
|
+
if (!filePath) return
|
|
599
|
+
pageDerivedCache.set(filePath, { title, toc })
|
|
600
|
+
},
|
|
601
|
+
clearDerivedTitle: (filePath) => {
|
|
602
|
+
if (!filePath) return
|
|
603
|
+
pageDerivedCache.delete(filePath)
|
|
604
|
+
},
|
|
605
|
+
refreshPagesTree: () => {
|
|
606
|
+
pagesTreeCache.clear()
|
|
607
|
+
pagesContext.pagesTree = getPagesTree('/')
|
|
608
|
+
},
|
|
609
|
+
refreshLanguages: () => {
|
|
610
|
+
pagesContext.languages = collectLanguagesFromPages(pages)
|
|
611
|
+
pagesContext.getLanguageForRoute = (routePath) =>
|
|
612
|
+
resolveLanguageForRoute(pagesContext.languages, routePath)
|
|
613
|
+
},
|
|
614
|
+
excludedRoutes,
|
|
615
|
+
excludedDirs,
|
|
616
|
+
excludedDirPaths,
|
|
617
|
+
notFound,
|
|
618
|
+
languages,
|
|
619
|
+
getLanguageForRoute: (routePath) => resolveLanguageForRoute(languages, routePath),
|
|
620
|
+
site
|
|
621
|
+
}
|
|
622
|
+
if (compileAll) {
|
|
623
|
+
const compileToken = stageLogger.start('Compiling MDX')
|
|
624
|
+
const totalPages = pages.length
|
|
625
|
+
for (let i = 0; i < pages.length; i++) {
|
|
626
|
+
const page = pages[i]
|
|
627
|
+
if (logEnabled) {
|
|
628
|
+
stageLogger.update(compileToken, `Compiling MDX [${i + 1}/${totalPages}] ${page.filePath}`)
|
|
629
|
+
}
|
|
630
|
+
await compilePageMdx(page, pagesContext)
|
|
631
|
+
}
|
|
632
|
+
stageLogger.end(compileToken)
|
|
633
|
+
pagesTreeCache.clear()
|
|
634
|
+
pagesTree = getPagesTree('/')
|
|
635
|
+
pagesContext.pagesTree = pagesTree
|
|
636
|
+
}
|
|
637
|
+
return pagesContext
|
|
638
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* Copyright Yukino Song, SudoMaker Ltd.
|
|
2
|
+
*
|
|
3
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
* or more contributor license agreements. See the NOTICE file
|
|
5
|
+
* distributed with this work for additional information
|
|
6
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
* to you under the Apache License, Version 2.0 (the
|
|
8
|
+
* "License"); you may not use this file except in compliance
|
|
9
|
+
* with the License. You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing,
|
|
14
|
+
* software distributed under the License is distributed on an
|
|
15
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
* KIND, either express or implied. See the License for the
|
|
17
|
+
* specific language governing permissions and limitations
|
|
18
|
+
* under the License.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync } from 'fs'
|
|
22
|
+
import { resolve } from 'path'
|
|
23
|
+
import { mergeConfig, preview as vitePreview } from 'vite'
|
|
24
|
+
import { state, cli } from './state.js'
|
|
25
|
+
import { resolveUserViteConfig } from './config.js'
|
|
26
|
+
import { methanolPreviewRoutingPlugin } from './vite-plugins.js'
|
|
27
|
+
|
|
28
|
+
export const runVitePreview = async () => {
|
|
29
|
+
const baseConfig = {
|
|
30
|
+
configFile: false,
|
|
31
|
+
root: state.PAGES_DIR,
|
|
32
|
+
base: './',
|
|
33
|
+
build: {
|
|
34
|
+
outDir: state.DIST_DIR
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const userConfig = await resolveUserViteConfig('preview')
|
|
38
|
+
const finalConfig = userConfig ? mergeConfig(baseConfig, userConfig) : baseConfig
|
|
39
|
+
if (cli.CLI_PORT != null) {
|
|
40
|
+
finalConfig.preview = { ...(finalConfig.preview || {}), port: cli.CLI_PORT }
|
|
41
|
+
}
|
|
42
|
+
if (cli.CLI_HOST !== null) {
|
|
43
|
+
finalConfig.preview = { ...(finalConfig.preview || {}), host: cli.CLI_HOST }
|
|
44
|
+
}
|
|
45
|
+
const outDir = finalConfig.build?.outDir || state.DIST_DIR
|
|
46
|
+
const distDir = resolve(state.ROOT_DIR, outDir)
|
|
47
|
+
const notFoundPath = resolve(distDir, '404.html')
|
|
48
|
+
const previewPlugins = Array.isArray(finalConfig.plugins) ? [...finalConfig.plugins] : []
|
|
49
|
+
previewPlugins.push(methanolPreviewRoutingPlugin(distDir, notFoundPath))
|
|
50
|
+
finalConfig.plugins = previewPlugins
|
|
51
|
+
if (!existsSync(distDir)) {
|
|
52
|
+
console.error(`Dist directory not found: ${distDir}`)
|
|
53
|
+
console.error('Run a production build before previewing.')
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
56
|
+
const server = await vitePreview(finalConfig)
|
|
57
|
+
server.printUrls()
|
|
58
|
+
}
|