methanol 0.0.7 → 0.0.9

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/src/mdx.js CHANGED
@@ -25,6 +25,7 @@ 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
27
  import rehypeStarryNight from 'rehype-starry-night'
28
+ import remarkGfm from 'remark-gfm'
28
29
  import { HTMLRenderer } from './renderer.js'
29
30
  import { signal, computed, read, Suspense, nextTick } from 'refui'
30
31
  import { createPortal } from 'refui/extras'
@@ -32,12 +33,13 @@ import { pathToFileURL } from 'url'
32
33
  import { existsSync } from 'fs'
33
34
  import { resolve, dirname, basename, relative } from 'path'
34
35
  import { state } from './state.js'
35
- import { resolveUserMdxConfig } from './config.js'
36
+ import { resolveUserMdxConfig, withBase } from './config.js'
36
37
  import { methanolCtx } from './rehype-plugins/methanol-ctx.js'
37
38
  import { linkResolve } from './rehype-plugins/link-resolve.js'
38
39
 
39
40
  // Workaround for Vite: it doesn't support resolving module/virtual modules in script src in dev mode
40
- const RWND_INJECT = HTMLRenderer.rawHTML`<script type="module" src="/.methanol_virtual_module/inject.js"></script>`
41
+ const resolveRewindInject = () =>
42
+ HTMLRenderer.rawHTML(`<script type="module" src="${withBase('/.methanol_virtual_module/inject.js')}"></script>`)
41
43
  const RWND_FALLBACK = HTMLRenderer.rawHTML`<script>
42
44
  if (!window.$$rwnd) {
43
45
  const l = []
@@ -59,12 +61,12 @@ const resolveUserHeadAssets = () => {
59
61
  const pagesDir = state.PAGES_DIR
60
62
  if (!pagesDir) return assets
61
63
  if (existsSync(resolve(pagesDir, 'style.css'))) {
62
- assets.push(HTMLRenderer.c('link', { rel: 'stylesheet', href: '/style.css' }))
64
+ assets.push(HTMLRenderer.c('link', { rel: 'stylesheet', href: withBase('/style.css') }))
63
65
  }
64
66
  if (existsSync(resolve(pagesDir, 'index.js'))) {
65
- assets.push(HTMLRenderer.c('script', { type: 'module', src: '/index.js' }))
67
+ assets.push(HTMLRenderer.c('script', { type: 'module', src: withBase('/index.js') }))
66
68
  } else if (existsSync(resolve(pagesDir, 'index.ts'))) {
67
- assets.push(HTMLRenderer.c('script', { type: 'module', src: '/index.ts' }))
69
+ assets.push(HTMLRenderer.c('script', { type: 'module', src: withBase('/index.ts') }))
68
70
  }
69
71
  if (state.CURRENT_MODE === 'production') {
70
72
  cachedHeadAssets = assets
@@ -73,17 +75,17 @@ const resolveUserHeadAssets = () => {
73
75
  }
74
76
 
75
77
  const resolvePageAssetUrl = (page, filePath) => {
76
- const root = page?.source === 'theme' && state.THEME_PAGES_DIR
78
+ const root = page.source === 'theme' && state.THEME_PAGES_DIR
77
79
  ? state.THEME_PAGES_DIR
78
80
  : state.PAGES_DIR
79
81
  if (!root) return null
80
82
  const relPath = relative(root, filePath).replace(/\\/g, '/')
81
83
  if (!relPath || relPath.startsWith('..')) return null
82
- return `/${relPath}`
84
+ return withBase(`/${relPath}`)
83
85
  }
84
86
 
85
87
  const resolvePageHeadAssets = (page) => {
86
- if (!page?.filePath) return []
88
+ if (!page.filePath) return []
87
89
  const baseDir = dirname(page.filePath)
88
90
  const baseName = basename(page.filePath).replace(/\.(mdx|md)$/, '')
89
91
  const pagesRoot = state.PAGES_DIR ? resolve(state.PAGES_DIR) : null
@@ -138,26 +140,28 @@ export const buildPageContext = ({
138
140
  lazyPagesTree = false
139
141
  }) => {
140
142
  const page = pageMeta
141
- const language = pagesContext?.getLanguageForRoute ? pagesContext.getLanguageForRoute(routePath) : null
142
- const getSiblings = pagesContext?.getSiblings
143
- ? () => pagesContext.getSiblings(routePath, page?.filePath || filePath)
143
+ const language = pagesContext.getLanguageForRoute ? pagesContext.getLanguageForRoute(routePath) : null
144
+ const getSiblings = pagesContext.getSiblings
145
+ ? () => pagesContext.getSiblings(routePath, page.filePath || filePath)
144
146
  : null
145
147
  if (page && getSiblings && page.getSiblings !== getSiblings) {
146
148
  page.getSiblings = getSiblings
147
149
  }
148
150
  const ctx = {
149
151
  routePath,
152
+ routeHref: withBase(routePath),
150
153
  filePath,
151
154
  page,
152
- pages: pagesContext?.pages || [],
153
- pagesByRoute: pagesContext?.pagesByRoute || new Map(),
154
- languages: pagesContext?.languages || [],
155
+ pages: pagesContext.pages || [],
156
+ pagesByRoute: pagesContext.pagesByRoute || new Map(),
157
+ languages: pagesContext.languages || [],
155
158
  language,
156
- site: pagesContext?.site || null,
157
- getSiblings
159
+ site: pagesContext.site || null,
160
+ getSiblings,
161
+ withBase
158
162
  }
159
163
  const resolvePagesTree = () =>
160
- pagesContext?.getPagesTree ? pagesContext.getPagesTree(routePath) : pagesContext?.pagesTree || []
164
+ pagesContext.getPagesTree ? pagesContext.getPagesTree(routePath) : pagesContext.pagesTree || []
161
165
  if (lazyPagesTree) {
162
166
  let cachedTree = null
163
167
  let hasTree = false
@@ -257,11 +261,21 @@ const resolveBaseMdxConfig = async () => {
257
261
  jsxImportSource: 'refui',
258
262
  development: state.CURRENT_MODE !== 'production',
259
263
  elementAttributeNameCase: 'html',
260
- rehypePlugins: [rehypeSlug, extractToc, [withTocExport, { name: 'toc' }]]
264
+ rehypePlugins: [rehypeSlug, extractToc, [withTocExport, { name: 'toc' }]],
265
+ remarkPlugins: []
261
266
  }
267
+
268
+ if (state.GFM_ENABLED) {
269
+ baseMdxConfig.remarkPlugins.push(remarkGfm)
270
+ }
271
+
262
272
  const mdxConfig = { ...baseMdxConfig, ...userMdxConfig }
263
273
  const userRehypePlugins = Array.isArray(userMdxConfig.rehypePlugins) ? userMdxConfig.rehypePlugins : []
264
274
  mdxConfig.rehypePlugins = [...baseMdxConfig.rehypePlugins, ...userRehypePlugins]
275
+
276
+ const userRemarkPlugins = Array.isArray(userMdxConfig.remarkPlugins) ? userMdxConfig.remarkPlugins : []
277
+ mdxConfig.remarkPlugins = [...baseMdxConfig.remarkPlugins, ...userRemarkPlugins]
278
+
265
279
  mdxConfig.rehypePlugins.push(linkResolve)
266
280
  mdxConfig.rehypePlugins.push(methanolCtx)
267
281
  return (cachedMdxConfig = mdxConfig)
@@ -286,7 +300,7 @@ const resolveMdxConfigForPage = async (frontmatter) => {
286
300
  }
287
301
 
288
302
  export const compileMdx = async ({ content, filePath, ctx }) => {
289
- const mdxConfig = await resolveMdxConfigForPage(ctx?.page?.frontmatter)
303
+ const mdxConfig = await resolveMdxConfigForPage(ctx.page.frontmatter)
290
304
  const runtimeFactory = mdxConfig.development ? JSXDevFactory : JSXFactory
291
305
  const compiled = await compile({ value: content, path: filePath }, mdxConfig)
292
306
 
@@ -322,15 +336,15 @@ export const compilePageMdx = async (page, pagesContext, options = {}) => {
322
336
  const nextTitle = findTitleFromToc(page.toc) || page.title
323
337
  if (nextTitle !== page.title) {
324
338
  page.title = nextTitle
325
- if (typeof pagesContext?.refreshPagesTree === 'function') {
339
+ if (typeof pagesContext.refreshPagesTree === 'function') {
326
340
  pagesContext.refreshPagesTree()
327
341
  }
328
342
  }
329
343
  }
330
- if (typeof pagesContext?.setDerivedTitle === 'function') {
344
+ if (typeof pagesContext.setDerivedTitle === 'function') {
331
345
  pagesContext.setDerivedTitle(page.filePath, shouldUseTocTitle ? page.title : null, page.toc)
332
346
  }
333
- if (ctx && refreshPagesTree && pagesContext?.getPagesTree) {
347
+ if (ctx && refreshPagesTree && pagesContext.getPagesTree) {
334
348
  ctx.pagesTree = pagesContext.getPagesTree(activeCtx.routePath)
335
349
  }
336
350
  }
@@ -340,14 +354,8 @@ export const renderHtml = async ({
340
354
  filePath,
341
355
  components,
342
356
  pagesContext,
343
- pageMeta: explicitPageMeta = null
357
+ pageMeta
344
358
  }) => {
345
- const pageMeta =
346
- explicitPageMeta ||
347
- (pagesContext.getPageByRoute
348
- ? pagesContext.getPageByRoute(routePath, { filePath })
349
- : pagesContext.pagesByRoute.get(routePath))
350
-
351
359
  const ctx = buildPageContext({
352
360
  routePath,
353
361
  filePath,
@@ -359,7 +367,7 @@ export const renderHtml = async ({
359
367
  const [Head, Outlet] = createPortal()
360
368
  const ExtraHead = () => {
361
369
  return [
362
- RWND_INJECT,
370
+ resolveRewindInject(),
363
371
  ...resolveUserHeadAssets(),
364
372
  ...resolvePageHeadAssets(pageMeta),
365
373
  Outlet(),
@@ -367,9 +375,9 @@ export const renderHtml = async ({
367
375
  ]
368
376
  }
369
377
 
370
- const mdxComponent = pageMeta?.mdxComponent
378
+ const mdxComponent = pageMeta.mdxComponent
371
379
 
372
- const Page = ({ components: extraComponents, ...props }, ...children) =>
380
+ const PageContent = ({ components: extraComponents, ...props }, ...children) =>
373
381
  mdxComponent({
374
382
  children,
375
383
  ...props,
@@ -394,7 +402,9 @@ export const renderHtml = async ({
394
402
  () =>
395
403
  template({
396
404
  ctx,
397
- Page,
405
+ page: ctx.page,
406
+ withBase,
407
+ PageContent,
398
408
  ExtraHead,
399
409
  HTMLRenderer,
400
410
  components
package/src/pagefind.js CHANGED
@@ -22,7 +22,9 @@ import { access } from 'fs/promises'
22
22
  import { constants } from 'fs'
23
23
  import { join, delimiter } from 'path'
24
24
  import { spawn } from 'child_process'
25
- import { state } from './state.js'
25
+ import { state, cli } from './state.js'
26
+ import { createStageLogger } from './stage-logger.js'
27
+ import { logger } from './logger.js'
26
28
 
27
29
  const resolvePagefindBin = async () => {
28
30
  const binName = process.platform === 'win32' ? 'pagefind.cmd' : 'pagefind'
@@ -92,16 +94,25 @@ const runCommand = (command, args, options) =>
92
94
  export const runPagefind = async () => {
93
95
  const bin = await resolvePagefindBin()
94
96
  if (!bin) {
95
- console.log('Pagefind not found; skipping search indexing.')
97
+ logger.warn('Pagefind not found; skipping search indexing.')
96
98
  return false
97
99
  }
98
- console.log('Running Pagefind search indexing...')
100
+ const logEnabled = state.CURRENT_MODE === 'production' && cli.command === 'build' && !cli.CLI_VERBOSE
101
+ const stageLogger = createStageLogger(logEnabled)
102
+ const token = stageLogger.start('Indexing search')
103
+
104
+ if (cli.CLI_VERBOSE) {
105
+ logger.info('Running Pagefind search indexing...')
106
+ }
107
+
99
108
  const extraArgs = buildArgsFromOptions(state.PAGEFIND_BUILD)
100
109
  const ok = await runCommand(bin, ['--site', state.DIST_DIR, ...extraArgs], {
101
- cwd: state.PROJECT_ROOT
110
+ cwd: state.PROJECT_ROOT,
111
+ stdio: cli.CLI_VERBOSE ? 'inherit' : 'ignore'
102
112
  })
103
113
  if (!ok) {
104
- console.warn('Pagefind failed to build search index.')
114
+ logger.warn('Pagefind failed to build search index.')
105
115
  }
116
+ stageLogger.end(token)
106
117
  return ok
107
118
  }
package/src/pages.js CHANGED
@@ -23,6 +23,7 @@ import { readdir, readFile, stat } from 'fs/promises'
23
23
  import { existsSync } from 'fs'
24
24
  import { resolve, join, relative } from 'path'
25
25
  import { state, cli } from './state.js'
26
+ import { withBase } from './config.js'
26
27
  import { compilePageMdx } from './mdx.js'
27
28
  import { createStageLogger } from './stage-logger.js'
28
29
 
@@ -36,12 +37,11 @@ const pageDerivedCache = new Map()
36
37
  const collectLanguagesFromPages = (pages = []) => {
37
38
  const languages = new Map()
38
39
  for (const page of pages) {
39
- if (!page?.isIndex) continue
40
- const label = page?.frontmatter?.lang
40
+ if (!page.isIndex) continue
41
+ const label = page.frontmatter?.lang
41
42
  if (label == null || label === '') continue
42
43
  const routePath = page.routePath || '/'
43
- const href = page.routeHref || routePath || '/'
44
- const frontmatterCode = page?.frontmatter?.langCode
44
+ const frontmatterCode = page.frontmatter?.langCode
45
45
  const code =
46
46
  typeof frontmatterCode === 'string' && frontmatterCode.trim()
47
47
  ? frontmatterCode.trim()
@@ -50,19 +50,39 @@ const collectLanguagesFromPages = (pages = []) => {
50
50
  : routePath.replace(/^\/+/, '')
51
51
  languages.set(routePath, {
52
52
  routePath,
53
- href,
53
+ routeHref: withBase(routePath),
54
54
  label: String(label),
55
55
  code: code || null
56
56
  })
57
57
  }
58
- return Array.from(languages.values()).sort((a, b) => a.href.localeCompare(b.href))
58
+ return Array.from(languages.values()).sort((a, b) => a.routePath.localeCompare(b.routePath))
59
59
  }
60
60
 
61
61
  const normalizeRoutePath = (value) => {
62
+ if (!value) return '/'
63
+ let normalized = value
64
+ if (!normalized.startsWith('/')) {
65
+ normalized = `/${normalized}`
66
+ }
67
+ normalized = normalized.replace(/\/{2,}/g, '/')
68
+ if (normalized === '/') return '/'
69
+ if (normalized.endsWith('/')) {
70
+ return normalized.replace(/\/+$/, '/')
71
+ }
72
+ return normalized
73
+ }
74
+
75
+ const stripTrailingSlash = (value) => {
62
76
  if (!value || value === '/') return '/'
63
77
  return value.replace(/\/+$/, '')
64
78
  }
65
79
 
80
+ const toRoutePrefix = (value) => {
81
+ const normalized = normalizeRoutePath(value)
82
+ const stripped = stripTrailingSlash(normalized)
83
+ return stripped === '/' ? '' : stripped
84
+ }
85
+
66
86
  const resolveLanguageForRoute = (languages = [], routePath = '/') => {
67
87
  if (!languages.length) return null
68
88
  const normalizedRoute = normalizeRoutePath(routePath)
@@ -70,16 +90,17 @@ const resolveLanguageForRoute = (languages = [], routePath = '/') => {
70
90
  let bestLength = -1
71
91
  let rootLanguage = null
72
92
  for (const lang of languages) {
73
- const base = normalizeRoutePath(lang?.routePath || lang?.href)
93
+ const base = normalizeRoutePath(lang.routePath)
94
+ const basePrefix = toRoutePrefix(base)
74
95
  if (!base) continue
75
96
  if (base === '/') {
76
97
  rootLanguage = lang
77
98
  continue
78
99
  }
79
- if (normalizedRoute === base || normalizedRoute.startsWith(`${base}/`)) {
80
- if (base.length > bestLength) {
100
+ if (normalizedRoute === base || (basePrefix && normalizedRoute.startsWith(`${basePrefix}/`))) {
101
+ if (basePrefix.length > bestLength) {
81
102
  best = lang
82
- bestLength = base.length
103
+ bestLength = basePrefix.length
83
104
  }
84
105
  }
85
106
  }
@@ -104,7 +125,7 @@ export const routePathFromFile = (filePath, pagesDir = state.PAGES_DIR) => {
104
125
  return '/'
105
126
  }
106
127
  if (normalized.endsWith('/index')) {
107
- return `/${normalized.slice(0, -'/index'.length)}`
128
+ return `/${normalized.slice(0, -'/index'.length)}/`
108
129
  }
109
130
  return `/${normalized}`
110
131
  }
@@ -162,7 +183,8 @@ const stripRootPrefix = (value, rootDir) => {
162
183
 
163
184
  const buildPagesTree = (pages, options = {}) => {
164
185
  const rootPath = normalizeRoutePath(options.rootPath || '/')
165
- const rootDir = rootPath === '/' ? '' : rootPath.replace(/^\/+/, '')
186
+ const rootPrefix = toRoutePrefix(rootPath)
187
+ const rootDir = rootPrefix ? rootPrefix.replace(/^\/+/, '') : ''
166
188
  const includeHiddenRoot = Boolean(options.includeHiddenRoot)
167
189
  const currentRoutePath = normalizeRoutePath(options.currentRoutePath || '/')
168
190
  const rootSegments = rootDir ? rootDir.split('/') : []
@@ -170,8 +192,8 @@ const buildPagesTree = (pages, options = {}) => {
170
192
  if (!routePath) return '/'
171
193
  if (!rootDir) return routePath
172
194
  if (routePath === rootPath) return '/'
173
- if (routePath.startsWith(`${rootPath}/`)) {
174
- const stripped = routePath.slice(rootPath.length)
195
+ if (rootPrefix && routePath.startsWith(`${rootPrefix}/`)) {
196
+ const stripped = routePath.slice(rootPrefix.length)
175
197
  return stripped.startsWith('/') ? stripped : `/${stripped}`
176
198
  }
177
199
  return routePath
@@ -179,7 +201,7 @@ const buildPagesTree = (pages, options = {}) => {
179
201
  const currentRouteWithinRoot = resolveRouteWithinRoot(currentRoutePath)
180
202
  const isUnderRoot = (page) => {
181
203
  if (!rootDir) return true
182
- return page.routePath === rootPath || page.routePath.startsWith(`${rootPath}/`)
204
+ return page.routePath === rootPath || (rootPrefix && page.routePath.startsWith(`${rootPrefix}/`))
183
205
  }
184
206
  const treePages = pages
185
207
  .filter((page) => !page.isInternal)
@@ -249,10 +271,10 @@ const buildPagesTree = (pages, options = {}) => {
249
271
  children: [],
250
272
  depth,
251
273
  routePath: null,
274
+ routeHref: null,
252
275
  title: null,
253
276
  weight: null,
254
277
  date: null,
255
- routeHref: null,
256
278
  isRoot: false
257
279
  }
258
280
  dirs.set(path, dir)
@@ -306,7 +328,7 @@ const buildPagesTree = (pages, options = {}) => {
306
328
  if (page.isIndex && page.dir) {
307
329
  const dir = getDirNode(page.dir, page.dir.split('/').pop(), page.depth)
308
330
  dir.routePath = page.routePath
309
- dir.routeHref = page.routeHref || page.routePath
331
+ dir.routeHref = page.routeHref
310
332
  dir.title = page.title
311
333
  dir.weight = page.weight ?? null
312
334
  dir.date = page.date ?? null
@@ -381,9 +403,8 @@ const walkPages = async function* (dir, basePath = '') {
381
403
  const name = entry.replace(/\.(mdx|md)$/, '')
382
404
  const relativePath = join(basePath, name).replace(/\\/g, '/')
383
405
  const isIndex = name === 'index'
384
- const routePath = isIndex ? (basePath ? `/${basePath}` : '/') : `/${relativePath}`
385
- const routeHref = isIndex && basePath ? `/${basePath}/` : routePath
386
- yield { routePath, routeHref, filePath: fullPath, isIndex }
406
+ const routePath = isIndex ? (basePath ? `/${basePath}/` : '/') : `/${relativePath}`
407
+ yield { routePath, filePath: fullPath, isIndex }
387
408
  }
388
409
 
389
410
  for (const { entry, fullPath } of dirs) {
@@ -400,7 +421,6 @@ export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
400
421
  const dir = name.split('/').slice(0, -1).join('/')
401
422
  const dirName = dir ? dir.split('/').pop() : ''
402
423
  const isIndex = baseName === 'index'
403
- const routeHref = isIndex && dir ? `/${dir}/` : routePath
404
424
  const segments = routePath.split('/').filter(Boolean)
405
425
  const stats = await stat(filePath)
406
426
  const cached = pageMetadataCache.get(filePath)
@@ -423,7 +443,7 @@ export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
423
443
  : isNotFoundPage || Boolean(metadata.frontmatter?.isRoot)
424
444
  return {
425
445
  routePath,
426
- routeHref,
446
+ routeHref: withBase(routePath),
427
447
  filePath,
428
448
  source,
429
449
  relativePath: relPath,
@@ -466,7 +486,6 @@ const collectPagesFromDir = async (pagesDir, source) => {
466
486
  source
467
487
  })
468
488
  if (entry) {
469
- entry.routeHref = page.routeHref || entry.routeHref
470
489
  entry.isIndex = page.isIndex || entry.isIndex
471
490
  pages.push(entry)
472
491
  }
@@ -529,15 +548,22 @@ const buildIndexFallback = (pages, siteName) => {
529
548
  return lines.join('\n')
530
549
  }
531
550
 
532
- const resolveRootPath = (routePath, pagesByRoute, pagesByRouteIndex = null) => {
551
+ const resolveRootPath = (routePath, pagesByRoute) => {
533
552
  const normalized = normalizeRoutePath(routePath || '/')
534
553
  const segments = normalized.split('/').filter(Boolean)
535
- const lookup = pagesByRouteIndex || pagesByRoute
536
554
  for (let i = segments.length; i >= 1; i--) {
537
555
  const candidate = `/${segments.slice(0, i).join('/')}`
538
- const page = lookup.get(candidate)
556
+ const page = pagesByRoute.get(candidate)
539
557
  if (page?.isIndex && page?.isRoot) {
540
- return candidate
558
+ return page.routePath
559
+ }
560
+ const isExact = normalized === candidate
561
+ if (!isExact) {
562
+ const indexCandidate = candidate === '/' ? '/' : `${candidate}/`
563
+ const indexPage = pagesByRoute.get(indexCandidate)
564
+ if (indexPage?.isIndex && indexPage?.isRoot) {
565
+ return indexPage.routePath
566
+ }
541
567
  }
542
568
  }
543
569
  return '/'
@@ -547,7 +573,7 @@ const buildNavSequence = (nodes, pagesByRoute) => {
547
573
  const result = []
548
574
  const seen = new Set()
549
575
  const addEntry = (entry) => {
550
- if (!entry?.routePath) return
576
+ if (!entry.routePath) return
551
577
  const key = entry.filePath || entry.routePath
552
578
  if (seen.has(key)) return
553
579
  seen.add(key)
@@ -587,54 +613,42 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
587
613
  const hasIndex = pages.some((page) => page.routePath === '/')
588
614
  if (!hasIndex) {
589
615
  const content = buildIndexFallback(pages, state.SITE_NAME)
590
- pages = [
591
- {
592
- routePath: '/',
593
- routeHref: '/',
594
- filePath: resolve(state.PAGES_DIR, 'index.md'),
595
- relativePath: 'index.md',
596
- name: 'index',
597
- dir: '',
598
- segments: [],
599
- depth: 0,
600
- isIndex: true,
601
- isInternal: false,
602
- title: state.SITE_NAME || 'Methanol Site',
603
- weight: null,
604
- date: null,
605
- isRoot: false,
606
- hidden: false,
607
- content,
608
- frontmatter: {},
609
- matter: null,
610
- stats: { size: content.length, createdAt: null, updatedAt: null },
611
- createdAt: null,
612
- updatedAt: null
613
- },
614
- ...pages
615
- ]
616
+ pages.unshift({
617
+ routePath: '/',
618
+ routeHref: withBase('/'),
619
+ filePath: resolve(state.PAGES_DIR, 'index.md'),
620
+ relativePath: 'index.md',
621
+ name: 'index',
622
+ dir: '',
623
+ segments: [],
624
+ depth: 0,
625
+ isIndex: true,
626
+ isInternal: false,
627
+ title: state.SITE_NAME || 'Methanol Site',
628
+ weight: null,
629
+ date: null,
630
+ isRoot: false,
631
+ hidden: false,
632
+ content,
633
+ frontmatter: {},
634
+ matter: null,
635
+ stats: { size: content.length, createdAt: null, updatedAt: null },
636
+ createdAt: null,
637
+ updatedAt: null
638
+ })
616
639
  if (excludedRoutes?.has('/')) {
617
640
  excludedRoutes.delete('/')
618
641
  }
619
642
  }
620
643
 
621
644
  const pagesByRoute = new Map()
622
- const pagesByRouteIndex = new Map()
623
645
  for (const page of pages) {
624
- if (page.isIndex) {
625
- pagesByRouteIndex.set(page.routePath, page)
626
- if (!pagesByRoute.has(page.routePath)) {
627
- pagesByRoute.set(page.routePath, page)
628
- }
629
- continue
630
- }
631
- const existing = pagesByRoute.get(page.routePath)
632
- if (!existing || existing.isIndex) {
646
+ if (!pagesByRoute.has(page.routePath)) {
633
647
  pagesByRoute.set(page.routePath, page)
634
648
  }
635
649
  }
636
650
  const getPageByRoute = (routePath, options = {}) => {
637
- const { filePath, preferIndex } = options || {}
651
+ const { filePath } = options || {}
638
652
  if (filePath) {
639
653
  for (const page of pages) {
640
654
  if (page.routePath === routePath && page.filePath === filePath) {
@@ -642,18 +656,12 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
642
656
  }
643
657
  }
644
658
  }
645
- if (preferIndex === true) {
646
- return pagesByRouteIndex.get(routePath) || pagesByRoute.get(routePath) || null
647
- }
648
- if (preferIndex === false) {
649
- return pagesByRoute.get(routePath) || pagesByRouteIndex.get(routePath) || null
650
- }
651
- return pagesByRoute.get(routePath) || pagesByRouteIndex.get(routePath) || null
659
+ return pagesByRoute.get(routePath) || null
652
660
  }
653
661
  const pagesTreeCache = new Map()
654
662
  const navSequenceCache = new Map()
655
663
  const getPagesTree = (routePath) => {
656
- const rootPath = resolveRootPath(routePath, pagesByRoute, pagesByRouteIndex)
664
+ const rootPath = resolveRootPath(routePath, pagesByRoute)
657
665
  const normalizedRoute = normalizeRoutePath(routePath || '/')
658
666
  const cacheKey = `${rootPath}::${normalizedRoute}`
659
667
  if (pagesTreeCache.has(cacheKey)) {
@@ -668,7 +676,7 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
668
676
  return tree
669
677
  }
670
678
  const getNavSequence = (routePath) => {
671
- const rootPath = resolveRootPath(routePath, pagesByRoute, pagesByRouteIndex)
679
+ const rootPath = resolveRootPath(routePath, pagesByRoute)
672
680
  const normalizedRoute = normalizeRoutePath(routePath || '/')
673
681
  const cacheKey = `${rootPath}::${normalizedRoute}`
674
682
  if (navSequenceCache.has(cacheKey)) {
@@ -683,8 +691,10 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
683
691
  const notFound = pagesByRoute.get('/404') || null
684
692
  const languages = collectLanguagesFromPages(pages)
685
693
  const userSite = state.USER_SITE || {}
694
+ const siteBase = state.VITE_BASE ?? userSite.base ?? null
686
695
  const site = {
687
696
  ...userSite,
697
+ base: siteBase,
688
698
  name: state.SITE_NAME,
689
699
  root: state.ROOT_DIR,
690
700
  pagesDir: state.PAGES_DIR,
@@ -703,7 +713,6 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
703
713
  const pagesContext = {
704
714
  pages,
705
715
  pagesByRoute,
706
- pagesByRouteIndex,
707
716
  getPageByRoute,
708
717
  pagesTree,
709
718
  getPagesTree,
@@ -737,7 +746,7 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
737
746
  if (!entry) return null
738
747
  return {
739
748
  routePath: entry.routePath,
740
- routeHref: entry.routeHref || entry.routePath,
749
+ routeHref: entry.routeHref,
741
750
  title: entry.title || entry.name || entry.routePath,
742
751
  filePath: entry.filePath || null
743
752
  }
@@ -28,8 +28,8 @@ import { methanolPreviewRoutingPlugin } from './vite-plugins.js'
28
28
  export const runVitePreview = async () => {
29
29
  const baseConfig = {
30
30
  configFile: false,
31
+ appType: 'mpa',
31
32
  root: state.PAGES_DIR,
32
- base: '/',
33
33
  build: {
34
34
  outDir: state.DIST_DIR
35
35
  }
@@ -27,7 +27,6 @@ import { state } from '../state.js'
27
27
  const extensionRegex = /\.(mdx?|html)$/i
28
28
 
29
29
  const isRelativeHref = (href) => {
30
- if (typeof href !== 'string') return false
31
30
  if (!href) return false
32
31
  if (href.startsWith('#') || href.startsWith('?') || href.startsWith('/')) return false
33
32
  if (href.startsWith('//')) return false
@@ -18,6 +18,8 @@
18
18
  * under the License.
19
19
  */
20
20
 
21
+ import { style } from './logger.js'
22
+
21
23
  const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now())
22
24
 
23
25
  export const createStageLogger = (enabled) => {
@@ -43,17 +45,18 @@ export const createStageLogger = (enabled) => {
43
45
  }
44
46
  const start = (label) => {
45
47
  if (!enabled) return null
46
- writeLine(`${label}...`, false)
48
+ writeLine(`${style.cyan('◼')} ${label}...`, false)
47
49
  return { label, start: now() }
48
50
  }
49
51
  const update = (token, message) => {
50
52
  if (!enabled || !token || !message) return
51
- writeLine(message, false)
53
+ writeLine(`${style.cyan('◼')} ${message}`, false)
52
54
  }
53
55
  const end = (token) => {
54
56
  if (!enabled || !token) return
55
- const duration = Math.round(now() - token.start)
56
- writeLine(`${token.label}...\t${duration}ms`, true)
57
+ const duration = now() - token.start
58
+ const timeString = duration > 1000 ? `${(duration / 1000).toFixed(2)}s` : `${Math.round(duration)}ms`
59
+ writeLine(`${style.green('✔')} ${token.label}\t${style.dim(timeString)}`, true)
57
60
  }
58
61
  return { start, update, end }
59
62
  }