methanol 0.0.1 → 0.0.3

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
@@ -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
- export const buildPageContext = ({ routePath, filePath, pageMeta, pagesContext }) => {
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
- return {
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 resolveMdxConfig = async () => {
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
- if (userMdxConfig.rehypePlugins.length) {
139
- mdxConfig.rehypePlugins = [...baseMdxConfig.rehypePlugins, ...userMdxConfig.rehypePlugins]
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 resolveMdxConfig()
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 mdxModule = await compileMdx({
162
- content: page.content,
163
- filePath: page.filePath,
164
- ctx: buildPageContext({
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
- page.filePath,
186
- shouldUseTocTitle ? page.title : null,
187
- page.toc
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 [RWND_INJECT, ...resolveUserHeadAssets(), Outlet(), RWND_FALLBACK]
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) =>
package/src/pagefind.js CHANGED
@@ -20,7 +20,7 @@
20
20
 
21
21
  import { access } from 'fs/promises'
22
22
  import { constants } from 'fs'
23
- import { join } from 'path'
23
+ import { join, delimiter } from 'path'
24
24
  import { spawn } from 'child_process'
25
25
  import { state } from './state.js'
26
26
 
@@ -36,6 +36,14 @@ const resolvePagefindBin = async () => {
36
36
  return candidate
37
37
  } catch {}
38
38
  }
39
+ const pathEntries = (process.env.PATH || '').split(delimiter).filter(Boolean)
40
+ for (const entry of pathEntries) {
41
+ const candidate = join(entry, binName)
42
+ try {
43
+ await access(candidate, constants.X_OK)
44
+ return candidate
45
+ } catch {}
46
+ }
39
47
  return null
40
48
  }
41
49
 
@@ -88,7 +96,7 @@ export const runPagefind = async () => {
88
96
  return false
89
97
  }
90
98
  console.log('Running Pagefind search indexing...')
91
- const extraArgs = buildArgsFromOptions(state.PAGEFIND_BUILD_OPTIONS)
99
+ const extraArgs = buildArgsFromOptions(state.PAGEFIND_BUILD)
92
100
  const ok = await runCommand(bin, ['--site', state.DIST_DIR, ...extraArgs], {
93
101
  cwd: state.PROJECT_ROOT
94
102
  })
package/src/pages.js CHANGED
@@ -28,7 +28,7 @@ import { createStageLogger } from './stage-logger.js'
28
28
 
29
29
  const isPageFile = (name) => name.endsWith('.mdx') || name.endsWith('.md')
30
30
  const isInternalPage = (name) => name.startsWith('_') || name.startsWith('.')
31
- const isIgnoredEntry = (name) => name.startsWith('.')
31
+ const isIgnoredEntry = (name) => name.startsWith('.') || name.startsWith('_')
32
32
 
33
33
  const pageMetadataCache = new Map()
34
34
  const pageDerivedCache = new Map()
@@ -41,10 +41,18 @@ const collectLanguagesFromPages = (pages = []) => {
41
41
  if (label == null || label === '') continue
42
42
  const routePath = page.routePath || '/'
43
43
  const href = page.routeHref || routePath || '/'
44
+ const frontmatterCode = page?.frontmatter?.langCode
45
+ const code =
46
+ typeof frontmatterCode === 'string' && frontmatterCode.trim()
47
+ ? frontmatterCode.trim()
48
+ : routePath === '/'
49
+ ? null
50
+ : routePath.replace(/^\/+/, '')
44
51
  languages.set(routePath, {
45
52
  routePath,
46
53
  href,
47
- label: String(label)
54
+ label: String(label),
55
+ code: code || null
48
56
  })
49
57
  }
50
58
  return Array.from(languages.values()).sort((a, b) => a.href.localeCompare(b.href))
@@ -103,7 +111,13 @@ export const routePathFromFile = (filePath, pagesDir = state.PAGES_DIR) => {
103
111
 
104
112
  const parseFrontmatter = (raw) => {
105
113
  const parsed = matter(raw)
106
- const data = parsed.data || {}
114
+ const data = { ...(parsed.data || {}) }
115
+ if (data.excerpt == null && data.description != null) {
116
+ data.excerpt = data.description
117
+ }
118
+ if (data.description == null && data.excerpt != null) {
119
+ data.description = data.excerpt
120
+ }
107
121
  const content = parsed.content ?? ''
108
122
  return {
109
123
  data,
@@ -150,7 +164,19 @@ const buildPagesTree = (pages, options = {}) => {
150
164
  const rootPath = normalizeRoutePath(options.rootPath || '/')
151
165
  const rootDir = rootPath === '/' ? '' : rootPath.replace(/^\/+/, '')
152
166
  const includeHiddenRoot = Boolean(options.includeHiddenRoot)
167
+ const currentRoutePath = normalizeRoutePath(options.currentRoutePath || '/')
153
168
  const rootSegments = rootDir ? rootDir.split('/') : []
169
+ const resolveRouteWithinRoot = (routePath) => {
170
+ if (!routePath) return '/'
171
+ if (!rootDir) return routePath
172
+ if (routePath === rootPath) return '/'
173
+ if (routePath.startsWith(`${rootPath}/`)) {
174
+ const stripped = routePath.slice(rootPath.length)
175
+ return stripped.startsWith('/') ? stripped : `/${stripped}`
176
+ }
177
+ return routePath
178
+ }
179
+ const currentRouteWithinRoot = resolveRouteWithinRoot(currentRoutePath)
154
180
  const isUnderRoot = (page) => {
155
181
  if (!rootDir) return true
156
182
  return page.routePath === rootPath || page.routePath.startsWith(`${rootPath}/`)
@@ -179,6 +205,18 @@ const buildPagesTree = (pages, options = {}) => {
179
205
  .filter((page) => page.isIndex && page.dir && page.hidden && !(includeHiddenRoot && page.routePath === rootPath))
180
206
  .map((page) => page.dir)
181
207
  )
208
+ const exposedHiddenDirs = new Set()
209
+ if (currentRoutePath && currentRoutePath !== '/' && hiddenDirs.size) {
210
+ for (const hiddenDir of hiddenDirs) {
211
+ const hiddenRoute = `/${hiddenDir}`
212
+ if (
213
+ currentRouteWithinRoot === hiddenRoute ||
214
+ currentRouteWithinRoot.startsWith(`${hiddenRoute}/`)
215
+ ) {
216
+ exposedHiddenDirs.add(hiddenDir)
217
+ }
218
+ }
219
+ }
182
220
  if (includeHiddenRoot && rootDir) {
183
221
  for (const hiddenDir of Array.from(hiddenDirs)) {
184
222
  if (rootDir === hiddenDir || rootDir.startsWith(`${hiddenDir}/`)) {
@@ -186,6 +224,11 @@ const buildPagesTree = (pages, options = {}) => {
186
224
  }
187
225
  }
188
226
  }
227
+ if (exposedHiddenDirs.size) {
228
+ for (const hiddenDir of exposedHiddenDirs) {
229
+ hiddenDirs.delete(hiddenDir)
230
+ }
231
+ }
189
232
  const isUnderHiddenDir = (dir) => {
190
233
  if (!dir) return false
191
234
  const parts = dir.split('/')
@@ -215,9 +258,25 @@ const buildPagesTree = (pages, options = {}) => {
215
258
  dirs.set(path, dir)
216
259
  return dir
217
260
  }
261
+ const isUnderExposedHiddenDir = (dir) => {
262
+ if (!dir || !exposedHiddenDirs.size) return false
263
+ for (const hiddenDir of exposedHiddenDirs) {
264
+ if (dir === hiddenDir || dir.startsWith(`${hiddenDir}/`)) {
265
+ return true
266
+ }
267
+ }
268
+ return false
269
+ }
218
270
  for (const page of treePages) {
219
271
  if (page.hidden && !(includeHiddenRoot && page.routePath === rootPath)) {
220
- continue
272
+ const isHidden404 = page.routePath === '/404'
273
+ const shouldExposeHidden =
274
+ !isHidden404 &&
275
+ page.hiddenByFrontmatter === true &&
276
+ (page.routePath === currentRoutePath || isUnderExposedHiddenDir(page.dir))
277
+ if (!shouldExposeHidden) {
278
+ continue
279
+ }
221
280
  }
222
281
  if (isUnderHiddenDir(page.dir)) {
223
282
  continue
@@ -352,6 +411,7 @@ export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
352
411
  const derived = pageDerivedCache.get(filePath)
353
412
  const exclude = Boolean(metadata.frontmatter?.exclude)
354
413
  const frontmatterHidden = metadata.frontmatter?.hidden
414
+ const hiddenByFrontmatter = frontmatterHidden === true
355
415
  const isNotFoundPage = routePath === '/404'
356
416
  const hidden = frontmatterHidden === false
357
417
  ? false
@@ -375,6 +435,7 @@ export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
375
435
  date: parseDate(metadata.frontmatter?.date) || parseDate(stats.mtime),
376
436
  isRoot: Boolean(metadata.frontmatter?.isRoot),
377
437
  hidden,
438
+ hiddenByFrontmatter,
378
439
  exclude,
379
440
  content: metadata.content,
380
441
  frontmatter: metadata.frontmatter,
@@ -479,6 +540,38 @@ const resolveRootPath = (routePath, pagesByRoute, pagesByRouteIndex = null) => {
479
540
  return '/'
480
541
  }
481
542
 
543
+ const buildNavSequence = (nodes, pagesByRoute) => {
544
+ const result = []
545
+ const seen = new Set()
546
+ const addEntry = (entry) => {
547
+ if (!entry?.routePath) return
548
+ const key = entry.filePath || entry.routePath
549
+ if (seen.has(key)) return
550
+ seen.add(key)
551
+ result.push(entry)
552
+ }
553
+ const walk = (items = []) => {
554
+ for (const node of items) {
555
+ if (node.type === 'directory') {
556
+ if (node.routePath) {
557
+ const page = pagesByRoute.get(node.routePath) || node.page || null
558
+ if (page) addEntry(page)
559
+ }
560
+ if (node.children?.length) {
561
+ walk(node.children)
562
+ }
563
+ continue
564
+ }
565
+ if (node.type === 'page') {
566
+ const page = pagesByRoute.get(node.routePath) || node
567
+ addEntry(page)
568
+ }
569
+ }
570
+ }
571
+ walk(nodes)
572
+ return result
573
+ }
574
+
482
575
  export const buildPagesContext = async ({ compileAll = true } = {}) => {
483
576
  const logEnabled = state.CURRENT_MODE === 'production' && cli.command === 'build'
484
577
  const stageLogger = createStageLogger(logEnabled)
@@ -555,18 +648,34 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
555
648
  return pagesByRoute.get(routePath) || pagesByRouteIndex.get(routePath) || null
556
649
  }
557
650
  const pagesTreeCache = new Map()
651
+ const navSequenceCache = new Map()
558
652
  const getPagesTree = (routePath) => {
559
653
  const rootPath = resolveRootPath(routePath, pagesByRoute, pagesByRouteIndex)
560
- if (pagesTreeCache.has(rootPath)) {
561
- return pagesTreeCache.get(rootPath)
654
+ const normalizedRoute = normalizeRoutePath(routePath || '/')
655
+ const cacheKey = `${rootPath}::${normalizedRoute}`
656
+ if (pagesTreeCache.has(cacheKey)) {
657
+ return pagesTreeCache.get(cacheKey)
562
658
  }
563
659
  const tree = buildPagesTree(pages, {
564
660
  rootPath,
565
- includeHiddenRoot: rootPath !== '/'
661
+ includeHiddenRoot: rootPath !== '/',
662
+ currentRoutePath: normalizedRoute
566
663
  })
567
- pagesTreeCache.set(rootPath, tree)
664
+ pagesTreeCache.set(cacheKey, tree)
568
665
  return tree
569
666
  }
667
+ const getNavSequence = (routePath) => {
668
+ const rootPath = resolveRootPath(routePath, pagesByRoute, pagesByRouteIndex)
669
+ const normalizedRoute = normalizeRoutePath(routePath || '/')
670
+ const cacheKey = `${rootPath}::${normalizedRoute}`
671
+ if (navSequenceCache.has(cacheKey)) {
672
+ return navSequenceCache.get(cacheKey)
673
+ }
674
+ const tree = getPagesTree(routePath)
675
+ const sequence = buildNavSequence(tree, pagesByRoute)
676
+ navSequenceCache.set(cacheKey, sequence)
677
+ return sequence
678
+ }
570
679
  let pagesTree = getPagesTree('/')
571
680
  const notFound = pagesByRoute.get('/404') || null
572
681
  const languages = collectLanguagesFromPages(pages)
@@ -581,7 +690,7 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
581
690
  pagefind: {
582
691
  enabled: state.PAGEFIND_ENABLED,
583
692
  options: state.PAGEFIND_OPTIONS || null,
584
- buildOptions: state.PAGEFIND_BUILD_OPTIONS || null
693
+ build: state.PAGEFIND_BUILD || null
585
694
  },
586
695
  generatedAt: new Date().toISOString()
587
696
  }
@@ -604,8 +713,35 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
604
713
  },
605
714
  refreshPagesTree: () => {
606
715
  pagesTreeCache.clear()
716
+ navSequenceCache.clear()
607
717
  pagesContext.pagesTree = getPagesTree('/')
608
718
  },
719
+ getSiblings: (routePath, filePath = null) => {
720
+ if (!routePath) return { prev: null, next: null }
721
+ const sequence = getNavSequence(routePath)
722
+ if (!sequence.length) return { prev: null, next: null }
723
+ let index = -1
724
+ if (filePath) {
725
+ index = sequence.findIndex((entry) => entry.filePath === filePath)
726
+ }
727
+ if (index < 0) {
728
+ index = sequence.findIndex((entry) => entry.routePath === routePath)
729
+ }
730
+ if (index < 0) return { prev: null, next: null }
731
+ const toNavEntry = (entry) => {
732
+ if (!entry) return null
733
+ return {
734
+ routePath: entry.routePath,
735
+ routeHref: entry.routeHref || entry.routePath,
736
+ title: entry.title || entry.name || entry.routePath,
737
+ filePath: entry.filePath || null
738
+ }
739
+ }
740
+ return {
741
+ prev: toNavEntry(sequence[index - 1] || null),
742
+ next: toNavEntry(sequence[index + 1] || null)
743
+ }
744
+ },
609
745
  refreshLanguages: () => {
610
746
  pagesContext.languages = collectLanguagesFromPages(pages)
611
747
  pagesContext.getLanguageForRoute = (routePath) =>
@@ -627,7 +763,10 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
627
763
  if (logEnabled) {
628
764
  stageLogger.update(compileToken, `Compiling MDX [${i + 1}/${totalPages}] ${page.filePath}`)
629
765
  }
630
- await compilePageMdx(page, pagesContext)
766
+ await compilePageMdx(page, pagesContext, {
767
+ lazyPagesTree: true,
768
+ refreshPagesTree: false
769
+ })
631
770
  }
632
771
  stageLogger.end(compileToken)
633
772
  pagesTreeCache.clear()
@@ -18,12 +18,7 @@
18
18
  * under the License.
19
19
  */
20
20
 
21
- import { fileURLToPath } from 'node:url'
22
- import { dirname, resolve } from 'node:path'
23
21
  import { register } from 'node:module'
24
22
 
25
- const __filename = fileURLToPath(import.meta.url)
26
- const __dirname = dirname(__filename)
27
- const loaderPath = resolve(__dirname, '../src/node-loader.js')
28
-
29
- register(loaderPath, import.meta.url)
23
+ const loaderUrl = new URL('./node-loader.js', import.meta.url)
24
+ register(loaderUrl.href, import.meta.url)
@@ -19,9 +19,10 @@
19
19
  */
20
20
 
21
21
  import { existsSync } from 'fs'
22
- import { dirname, resolve } from 'path'
22
+ import { dirname, resolve, relative, isAbsolute } from 'path'
23
23
  import { isElement } from 'hast-util-is-element'
24
24
  import { visit } from 'unist-util-visit'
25
+ import { state } from '../state.js'
25
26
 
26
27
  const extensionRegex = /\.(mdx?|html)$/i
27
28
 
@@ -44,14 +45,40 @@ const splitHref = (href) => {
44
45
 
45
46
  const resolveCandidate = (baseDir, targetPath) => resolve(baseDir, targetPath)
46
47
 
47
- const hasExistingSource = (baseDir, pathWithoutSuffix, extension) => {
48
- const targetPath = `${pathWithoutSuffix}${extension}`
49
- return existsSync(resolveCandidate(baseDir, targetPath))
48
+ const isWithinRoot = (root, targetPath) => {
49
+ if (!root) return false
50
+ const relPath = relative(root, targetPath)
51
+ if (relPath === '') return true
52
+ if (relPath.startsWith('..') || relPath.startsWith('..\\')) return false
53
+ if (isAbsolute(relPath)) return false
54
+ return true
55
+ }
56
+
57
+ const hasExistingSource = (baseDir, pathWithoutSuffix, extension, root) => {
58
+ const targetPath = resolveCandidate(baseDir, `${pathWithoutSuffix}${extension}`)
59
+ if (root && !isWithinRoot(root, targetPath)) {
60
+ return false
61
+ }
62
+ return existsSync(targetPath)
63
+ }
64
+
65
+ const resolvePagesRoot = (filePath) => {
66
+ const roots = [state.PAGES_DIR, state.THEME_PAGES_DIR].filter(Boolean).map((dir) => resolve(dir))
67
+ if (!roots.length) return null
68
+ if (!filePath) return roots[0]
69
+ const resolvedFile = resolve(filePath)
70
+ for (const root of roots) {
71
+ if (isWithinRoot(root, resolvedFile)) {
72
+ return root
73
+ }
74
+ }
75
+ return roots[0]
50
76
  }
51
77
 
52
78
  export const linkResolve = () => {
53
79
  return (tree, file) => {
54
80
  const baseDir = file?.path ? dirname(file.path) : file?.cwd || process.cwd()
81
+ const pagesRoot = resolvePagesRoot(file?.path || null)
55
82
  visit(tree, (node) => {
56
83
  if (!isElement(node) || node.tagName !== 'a') {
57
84
  return
@@ -71,12 +98,12 @@ export const linkResolve = () => {
71
98
 
72
99
  let shouldStrip = false
73
100
  if (/\.mdx?$/i.test(extension)) {
74
- shouldStrip = hasExistingSource(baseDir, withoutExtension, extension)
101
+ shouldStrip = hasExistingSource(baseDir, withoutExtension, extension, pagesRoot)
75
102
  } else if (/\.html$/i.test(extension)) {
76
103
  shouldStrip =
77
- hasExistingSource(baseDir, withoutExtension, extension) ||
78
- hasExistingSource(baseDir, withoutExtension, '.md') ||
79
- hasExistingSource(baseDir, withoutExtension, '.mdx')
104
+ hasExistingSource(baseDir, withoutExtension, extension, pagesRoot) ||
105
+ hasExistingSource(baseDir, withoutExtension, '.md', pagesRoot) ||
106
+ hasExistingSource(baseDir, withoutExtension, '.mdx', pagesRoot)
80
107
  }
81
108
 
82
109
  if (!shouldStrip) {