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.
Files changed (45) hide show
  1. package/.editorconfig +19 -0
  2. package/.prettierrc +10 -0
  3. package/LICENSE +203 -0
  4. package/banner.txt +6 -0
  5. package/bin/methanol.js +24 -0
  6. package/index.js +22 -0
  7. package/package.json +42 -9
  8. package/src/assets.js +30 -0
  9. package/src/build-system.js +200 -0
  10. package/src/components.js +145 -0
  11. package/src/config.js +355 -0
  12. package/src/dev-server.js +559 -0
  13. package/src/main.js +87 -0
  14. package/src/mdx.js +254 -0
  15. package/src/node-loader.js +88 -0
  16. package/src/pagefind.js +99 -0
  17. package/src/pages.js +638 -0
  18. package/src/preview-server.js +58 -0
  19. package/src/public-assets.js +73 -0
  20. package/src/register-loader.js +29 -0
  21. package/src/rehype-plugins/link-resolve.js +89 -0
  22. package/src/rehype-plugins/methanol-ctx.js +89 -0
  23. package/src/renderer.js +25 -0
  24. package/src/rewind.js +117 -0
  25. package/src/stage-logger.js +59 -0
  26. package/src/state.js +159 -0
  27. package/src/virtual-module/inject.js +30 -0
  28. package/src/virtual-module/loader.js +116 -0
  29. package/src/virtual-module/pagefind.js +108 -0
  30. package/src/vite-plugins.js +173 -0
  31. package/themes/default/components/ThemeColorSwitch.client.jsx +95 -0
  32. package/themes/default/components/ThemeColorSwitch.static.jsx +23 -0
  33. package/themes/default/components/ThemeSearchBox.client.jsx +287 -0
  34. package/themes/default/components/ThemeSearchBox.static.jsx +41 -0
  35. package/themes/default/components/ThemeToCContainer.client.jsx +154 -0
  36. package/themes/default/components/ThemeToCContainer.static.jsx +61 -0
  37. package/themes/default/components/pre.client.jsx +84 -0
  38. package/themes/default/components/pre.jsx +27 -0
  39. package/themes/default/heading.jsx +35 -0
  40. package/themes/default/index.js +50 -0
  41. package/themes/default/page.jsx +249 -0
  42. package/themes/default/pages/404.mdx +8 -0
  43. package/themes/default/pages/index.mdx +9 -0
  44. package/themes/default/public/logo.png +0 -0
  45. 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
+ }