methanol 0.0.19 → 0.0.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "methanol",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "description": "Static site generator powered by rEFui and MDX",
5
5
  "main": "./index.js",
6
6
  "type": "module",
@@ -26,6 +26,7 @@ import { VitePWA } from 'vite-plugin-pwa'
26
26
  import { state, cli } from './state.js'
27
27
  import { resolveUserViteConfig } from './config.js'
28
28
  import { buildPagesContext } from './pages.js'
29
+ import { selectFeedPages } from './feed.js'
29
30
  import { buildComponentRegistry } from './components.js'
30
31
  import { createBuildWorkers, runWorkerStage, terminateWorkers } from './workers/build-pool.js'
31
32
  import { methanolVirtualHtmlPlugin, methanolResolverPlugin } from './vite-plugins.js'
@@ -98,6 +99,8 @@ export const buildHtmlEntries = async () => {
98
99
  const { workers, assignments } = createBuildWorkers(totalPages)
99
100
  const excludedRoutes = Array.from(pagesContext.excludedRoutes || [])
100
101
  const excludedDirs = Array.from(pagesContext.excludedDirs || [])
102
+ const rssContent = new Map()
103
+ const intermediateOutputs = []
101
104
  try {
102
105
  await runWorkerStage({
103
106
  workers,
@@ -136,6 +139,10 @@ export const buildHtmlEntries = async () => {
136
139
  })
137
140
  stageLogger.end(compileToken)
138
141
 
142
+ const titleUpdates = updates
143
+ .filter((update) => update && update.title !== undefined)
144
+ .map((update) => ({ id: update.id, title: update.title }))
145
+
139
146
  for (const update of updates) {
140
147
  const page = pages[update.id]
141
148
  if (!page) continue
@@ -149,7 +156,6 @@ export const buildHtmlEntries = async () => {
149
156
  pagesContext.refreshPagesTree?.()
150
157
  state.PAGES_CONTEXT = pagesContext
151
158
 
152
- const titleSnapshot = pages.map((page) => page.title)
153
159
  await runWorkerStage({
154
160
  workers,
155
161
  stage: 'sync',
@@ -158,15 +164,13 @@ export const buildHtmlEntries = async () => {
158
164
  message: {
159
165
  type: 'sync',
160
166
  stage: 'sync',
161
- updates,
162
- titles: titleSnapshot
167
+ updates: titleUpdates
163
168
  }
164
169
  }))
165
170
  })
166
-
167
171
  const renderToken = stageLogger.start('Rendering pages')
168
172
  completed = 0
169
- const rendered = await runWorkerStage({
173
+ await runWorkerStage({
170
174
  workers,
171
175
  stage: 'render',
172
176
  messages: workers.map((worker, index) => ({
@@ -182,27 +186,80 @@ export const buildHtmlEntries = async () => {
182
186
  completed = count
183
187
  stageLogger.update(renderToken, `Rendering pages [${completed}/${totalPages}]`)
184
188
  },
185
- collect: (message) => message.results || []
189
+ onResult: (result) => {
190
+ if (!result || typeof result.id !== 'number') return
191
+ const page = pages[result.id]
192
+ if (!page) return
193
+ const html = result.html
194
+ const name = resolveOutputName(page)
195
+ const id = normalizePath(resolve(state.VIRTUAL_HTML_OUTPUT_ROOT, `${name}.html`))
196
+ entry[name] = id
197
+ htmlCache.set(id, html)
198
+ if (state.INTERMEDIATE_DIR) {
199
+ intermediateOutputs.push({ name, id })
200
+ }
201
+ }
186
202
  })
187
203
  stageLogger.end(renderToken)
188
204
 
189
- for (const item of rendered) {
190
- const page = pages[item.id]
191
- if (!page) continue
192
- const html = item.html
193
- const name = resolveOutputName(page)
194
- const id = normalizePath(resolve(state.VIRTUAL_HTML_OUTPUT_ROOT, `${name}.html`))
195
- entry[name] = id
196
- htmlCache.set(id, html)
197
- if (state.INTERMEDIATE_DIR) {
198
- const outPath = resolve(state.INTERMEDIATE_DIR, `${name}.html`)
199
- await ensureDir(dirname(outPath))
200
- await writeFile(outPath, html)
205
+ if (state.RSS_ENABLED) {
206
+ const feedPages = selectFeedPages(pages, state.RSS_OPTIONS || {})
207
+ const feedIds = []
208
+ const pageIndex = new Map(pages.map((page, index) => [page, index]))
209
+ for (const page of feedPages) {
210
+ const id = pageIndex.get(page)
211
+ if (id != null) {
212
+ feedIds.push(id)
213
+ }
214
+ }
215
+ if (feedIds.length) {
216
+ const rssToken = stageLogger.start('Rendering feed')
217
+ completed = 0
218
+ const rssAssignments = Array.from({ length: workers.length }, () => [])
219
+ for (let i = 0; i < feedIds.length; i += 1) {
220
+ rssAssignments[i % workers.length].push(feedIds[i])
221
+ }
222
+ await runWorkerStage({
223
+ workers,
224
+ stage: 'rss',
225
+ messages: workers.map((worker, index) => ({
226
+ worker,
227
+ message: {
228
+ type: 'rss',
229
+ stage: 'rss',
230
+ ids: rssAssignments[index]
231
+ }
232
+ })),
233
+ onProgress: (count) => {
234
+ if (!logEnabled) return
235
+ completed = count
236
+ stageLogger.update(rssToken, `Rendering feed [${completed}/${feedIds.length}]`)
237
+ },
238
+ onResult: (result) => {
239
+ if (!result || typeof result.id !== 'number') return
240
+ const page = pages[result.id]
241
+ if (!page) return
242
+ const key = page.path || page.routePath
243
+ if (key) {
244
+ rssContent.set(key, result.content || '')
245
+ }
246
+ }
247
+ })
248
+ stageLogger.end(rssToken)
201
249
  }
202
250
  }
203
251
  } finally {
204
252
  await terminateWorkers(workers)
205
253
  }
254
+ if (state.INTERMEDIATE_DIR) {
255
+ for (const output of intermediateOutputs) {
256
+ const html = htmlCache.get(output.id)
257
+ if (typeof html !== 'string') continue
258
+ const outPath = resolve(state.INTERMEDIATE_DIR, `${output.name}.html`)
259
+ await ensureDir(dirname(outPath))
260
+ await writeFile(outPath, html)
261
+ }
262
+ }
206
263
 
207
264
  const htmlFiles = await collectHtmlFiles(state.PAGES_DIR)
208
265
  const htmlExcludedDirs = pagesContext.excludedDirs || new Set()
@@ -238,7 +295,7 @@ export const buildHtmlEntries = async () => {
238
295
  }
239
296
  }
240
297
 
241
- return { entry, htmlCache, pagesContext }
298
+ return { entry, htmlCache, pagesContext, rssContent }
242
299
  }
243
300
 
244
301
  export const runViteBuild = async (entry, htmlCache) => {
package/src/components.js CHANGED
@@ -52,6 +52,8 @@ export const register = reframeEnv.register
52
52
  export const invalidateRegistryEntry = reframeEnv.invalidate
53
53
  export const genRegistryScript = reframeEnv.genRegistryScript
54
54
  export const resetReframeRenderCount = () => reframeEnv.resetRenderCount()
55
+ export const setReframeHydrationEnabled = (value) => reframeEnv.setHydrationEnabled(value)
56
+ export const getReframeHydrationEnabled = () => reframeEnv.getHydrationEnabled()
55
57
 
56
58
  const resolveComponentExport = (componentPath, exportName, ext) => {
57
59
  const staticCandidate = `${componentPath}.static${ext}`
package/src/config.js CHANGED
@@ -23,6 +23,7 @@ import { existsSync } from 'fs'
23
23
  import { resolve, isAbsolute, extname, basename, dirname } from 'path'
24
24
  import { pathToFileURL, fileURLToPath } from 'url'
25
25
  import { mergeConfig } from 'vite'
26
+ import { isMainThread } from 'worker_threads'
26
27
  import { projectRequire } from './node-loader.js'
27
28
  import { cli, state } from './state.js'
28
29
  import { logger } from './logger.js'
@@ -122,6 +123,7 @@ const normalizeViteBase = (value) => {
122
123
  }
123
124
 
124
125
  const warnDevBase = (value) => {
126
+ if (!isMainThread) return
125
127
  if (devBaseWarningShown) return
126
128
  devBaseWarningShown = true
127
129
  const label = value ? ` (received "${value}")` : ''
@@ -191,6 +193,28 @@ const resolvePagefindBuild = (config) => {
191
193
  return null
192
194
  }
193
195
 
196
+ const resolveFeedEnabled = (config) => {
197
+ if (config?.feed == null) return false
198
+ if (typeof config.feed === 'boolean') return config.feed
199
+ if (typeof config.feed === 'object') {
200
+ if (hasOwn(config.feed, 'enabled')) {
201
+ return config.feed.enabled !== false
202
+ }
203
+ return true
204
+ }
205
+ return false
206
+ }
207
+
208
+ const resolveFeedOptions = (config) => {
209
+ const value = config?.feed
210
+ if (!value || typeof value !== 'object') return null
211
+ const { enabled, ...rest } = value
212
+ if (Object.keys(rest).length) {
213
+ return { ...rest }
214
+ }
215
+ return null
216
+ }
217
+
194
218
  const resolveStarryNightConfig = (value) => {
195
219
  if (value == null) return { enabled: true, options: null }
196
220
  if (typeof value === 'boolean') {
@@ -301,10 +325,15 @@ export const applyConfig = async (config, mode) => {
301
325
  state.ROOT_DIR = root
302
326
  const configSiteName = cli.CLI_SITE_NAME ?? config.site?.name ?? null
303
327
  state.SITE_NAME = configSiteName || basename(root) || 'Methanol Site'
328
+ const configOwner = cli.CLI_OWNER ?? config.site?.owner ?? null
329
+ state.SITE_OWNER = configOwner ? String(configOwner) : null
304
330
  const userSite = config.site && typeof config.site === 'object' ? { ...config.site } : null
305
331
  const siteBase = normalizeSiteBase(cli.CLI_BASE || userSite?.base)
306
332
  state.SITE_BASE = siteBase
307
333
  if (userSite) {
334
+ if (configOwner != null) {
335
+ userSite.owner = String(configOwner)
336
+ }
308
337
  if (siteBase == null) {
309
338
  delete userSite.base
310
339
  } else {
@@ -426,6 +455,18 @@ export const applyConfig = async (config, mode) => {
426
455
  }
427
456
  state.PAGEFIND_OPTIONS = resolvePagefindOptions(config)
428
457
  state.PAGEFIND_BUILD = resolvePagefindBuild(config)
458
+
459
+ state.RSS_ENABLED = resolveFeedEnabled(config)
460
+ if (cli.CLI_RSS !== undefined) {
461
+ state.RSS_ENABLED = cli.CLI_RSS
462
+ }
463
+ state.RSS_OPTIONS = resolveFeedOptions(config)
464
+ if (cli.CLI_ATOM !== undefined) {
465
+ if (cli.CLI_ATOM === true) {
466
+ state.RSS_ENABLED = true
467
+ }
468
+ state.RSS_OPTIONS = { ...(state.RSS_OPTIONS || {}), atom: cli.CLI_ATOM }
469
+ }
429
470
 
430
471
  if (hasOwn(config, 'pwa')) {
431
472
  if (config.pwa === true) {
package/src/dev-server.js CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  } from './components.js'
39
39
  import { buildPagesContext, buildPageEntry, routePathFromFile } from './pages.js'
40
40
  import { compilePageMdx, renderHtml } from './mdx.js'
41
- import { DevErrorPage } from './error-page.jsx'
41
+ import { DevErrorPage } from './templates/error-page.jsx'
42
42
  import { HTMLRenderer } from './renderer.js'
43
43
  import { methanolResolverPlugin } from './vite-plugins.js'
44
44
  import { preparePublicAssets, updateAsset } from './public-assets.js'
@@ -251,6 +251,10 @@ export const runViteDev = async () => {
251
251
  })
252
252
  if (token !== pagesContextToken) return
253
253
 
254
+ const titleUpdates = updates
255
+ .filter((update) => update && update.title !== undefined)
256
+ .map((update) => ({ id: update.id, title: update.title }))
257
+
254
258
  for (const update of updates) {
255
259
  const page = pages[update.id]
256
260
  if (!page) continue
@@ -266,7 +270,6 @@ export const runViteDev = async () => {
266
270
  invalidateHtmlCache()
267
271
  const renderEpoch = htmlCacheEpoch
268
272
 
269
- const titleSnapshot = pages.map((page) => page.title)
270
273
  await runWorkerStage({
271
274
  workers,
272
275
  stage: 'sync',
@@ -275,8 +278,7 @@ export const runViteDev = async () => {
275
278
  message: {
276
279
  type: 'sync',
277
280
  stage: 'sync',
278
- updates,
279
- titles: titleSnapshot
281
+ updates: titleUpdates
280
282
  }
281
283
  }))
282
284
  })
package/src/feed.js ADDED
@@ -0,0 +1,198 @@
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 { dirname, resolve } from 'path'
22
+ import { mkdir, writeFile } from 'fs/promises'
23
+ import { state } from './state.js'
24
+ import { HTMLRenderer } from './renderer.js'
25
+ import { extractExcerpt } from './text-utils.js'
26
+ import { withBase } from './config.js'
27
+ import { logger } from './logger.js'
28
+ import { setReframeHydrationEnabled, getReframeHydrationEnabled } from './components.js'
29
+ import RssFeed from './templates/rss-feed.jsx'
30
+ import AtomFeed from './templates/atom-feed.jsx'
31
+
32
+ const DEFAULT_RSS_PATH = '/rss.xml'
33
+ const DEFAULT_ATOM_PATH = '/atom.xml'
34
+ const DEFAULT_RSS_LIMIT = 10
35
+
36
+ const isAbsoluteUrl = (value) =>
37
+ typeof value === 'string' && /^https?:\/\//i.test(value.trim())
38
+
39
+ const normalizeFeedPath = (value, isAtom) => {
40
+ if (!value || typeof value !== 'string') return isAtom ? DEFAULT_ATOM_PATH : DEFAULT_RSS_PATH
41
+ const trimmed = value.trim()
42
+ if (!trimmed) return isAtom ? DEFAULT_ATOM_PATH : DEFAULT_RSS_PATH
43
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`
44
+ }
45
+
46
+ const ensureDir = async (dir) => {
47
+ await mkdir(dir, { recursive: true })
48
+ }
49
+
50
+ const resolveFeedLimit = (options) => {
51
+ return typeof options?.limit === 'number' && Number.isFinite(options.limit)
52
+ ? Math.max(0, Math.floor(options.limit))
53
+ : DEFAULT_RSS_LIMIT
54
+ }
55
+
56
+ const wrapCdata = (value) => {
57
+ const text = value == null ? '' : String(value)
58
+ if (!text) return ''
59
+ const trimmed = text.trim()
60
+ if (trimmed.startsWith('<![CDATA[') && trimmed.endsWith(']]>')) {
61
+ return trimmed
62
+ }
63
+ return `<![CDATA[${text.replace(/]]>/g, ']]]]><![CDATA[>')}]]>`
64
+ }
65
+
66
+ const escapeXml = (value) => {
67
+ const text = value == null ? '' : String(value)
68
+ if (!text) return ''
69
+ return text
70
+ .replace(/&/g, '&amp;')
71
+ .replace(/</g, '&lt;')
72
+ .replace(/>/g, '&gt;')
73
+ }
74
+
75
+ const resolveSiteUrl = (options, site) => {
76
+ if (options?.siteUrl) return options.siteUrl
77
+ if (state.SITE_BASE) return state.SITE_BASE
78
+ if (site?.base) return site.base
79
+ return null
80
+ }
81
+
82
+ const buildItem = (page, siteUrl, htmlContent = null, isAtom = false, siteOwner = null) => {
83
+ if (!page) return null
84
+ const href = page.routeHref || withBase(page.routePath)
85
+ if (!href) return null
86
+ const link = new URL(href, siteUrl).href
87
+ const title = page.title || page.name || page.routePath || link
88
+ const description = extractExcerpt(page)
89
+ const contentSource = htmlContent || page.content || ''
90
+ const content = contentSource
91
+ ? (isAtom
92
+ ? HTMLRenderer.rawHTML(escapeXml(contentSource))
93
+ : HTMLRenderer.rawHTML(wrapCdata(contentSource)))
94
+ : null
95
+ const authorValue = page.frontmatter?.author
96
+ const author = Array.isArray(authorValue)
97
+ ? authorValue.filter(Boolean).join(', ')
98
+ : authorValue || siteOwner || null
99
+ const pubDate = page.date ? new Date(page.date).toUTCString() : null
100
+ const updated = page.date ? new Date(page.date).toISOString() : null
101
+ return {
102
+ title,
103
+ link,
104
+ description,
105
+ content,
106
+ author,
107
+ pubDate,
108
+ updated
109
+ }
110
+ }
111
+
112
+ const getSortTime = (page) => {
113
+ const value = page?.date
114
+ if (!value) return 0
115
+ const time = Date.parse(value)
116
+ return Number.isNaN(time) ? 0 : time
117
+ }
118
+
119
+ export const selectFeedPages = (pages, options) => {
120
+ const limit = resolveFeedLimit(options)
121
+ if (!limit) return []
122
+ return (Array.isArray(pages) ? pages : [])
123
+ .filter((page) => page && !page.hidden)
124
+ .sort((a, b) => getSortTime(b) - getSortTime(a))
125
+ .slice(0, limit)
126
+ }
127
+
128
+ export const renderRssFeed = ({ site, items }) => {
129
+ const prevHydration = getReframeHydrationEnabled()
130
+ const prevThemeHydration = state.THEME_ENV?.getHydrationEnabled?.()
131
+ setReframeHydrationEnabled(false)
132
+ state.THEME_ENV?.setHydrationEnabled?.(false)
133
+ try {
134
+ return HTMLRenderer.serialize(HTMLRenderer.c(RssFeed, { site, items }))
135
+ } finally {
136
+ setReframeHydrationEnabled(prevHydration)
137
+ if (prevThemeHydration != null) {
138
+ state.THEME_ENV?.setHydrationEnabled?.(prevThemeHydration)
139
+ }
140
+ }
141
+ }
142
+
143
+ export const renderAtomFeed = ({ site, items }) => {
144
+ const prevHydration = getReframeHydrationEnabled()
145
+ const prevThemeHydration = state.THEME_ENV?.getHydrationEnabled?.()
146
+ setReframeHydrationEnabled(false)
147
+ state.THEME_ENV?.setHydrationEnabled?.(false)
148
+ try {
149
+ return HTMLRenderer.serialize(HTMLRenderer.c(AtomFeed, { site, items }))
150
+ } finally {
151
+ setReframeHydrationEnabled(prevHydration)
152
+ if (prevThemeHydration != null) {
153
+ state.THEME_ENV?.setHydrationEnabled?.(prevThemeHydration)
154
+ }
155
+ }
156
+ }
157
+
158
+ export const generateRssFeed = async (pagesContext, rssContent = null) => {
159
+ if (!state.RSS_ENABLED) return null
160
+ const options = state.RSS_OPTIONS || {}
161
+ const site = pagesContext?.site || state.USER_SITE || {}
162
+ const siteUrl = resolveSiteUrl(options, site)
163
+ if (!isAbsoluteUrl(siteUrl)) {
164
+ logger.warn('Feed skipped: site.base must be an absolute URL (e.g. https://example.com/).')
165
+ return null
166
+ }
167
+ const isAtom = options.atom === true
168
+ const path = normalizeFeedPath(options.path, isAtom)
169
+ const now = new Date()
170
+ const finalSite = {
171
+ name: site.name,
172
+ title: Object.prototype.hasOwnProperty.call(options, 'title') ? options.title : site.name,
173
+ description: Object.prototype.hasOwnProperty.call(options, 'description') ? options.description : site.description,
174
+ language: Object.prototype.hasOwnProperty.call(options, 'language') ? options.language : (site.language || site.lang),
175
+ url: siteUrl,
176
+ feedUrl: new URL(path, siteUrl).href,
177
+ generator: 'Methanol',
178
+ lastBuildDate: now.toUTCString(),
179
+ updated: now.toISOString()
180
+ }
181
+ const pages = selectFeedPages(pagesContext?.pages || [], options)
182
+ const siteOwner = site.owner || null
183
+ const items = pages
184
+ .map((page) => ({
185
+ page,
186
+ content: rssContent?.get(page.path) || rssContent?.get(page.routePath) || null
187
+ }))
188
+ .map((entry) => buildItem(entry.page, siteUrl, entry.content, isAtom, siteOwner))
189
+ .filter(Boolean)
190
+ const xml = isAtom
191
+ ? renderAtomFeed({ site: finalSite, items })
192
+ : renderRssFeed({ site: finalSite, items })
193
+ const outPath = resolve(state.DIST_DIR, path.slice(1))
194
+ await ensureDir(dirname(outPath))
195
+ await writeFile(outPath, xml)
196
+ logger.success(`${isAtom ? 'Atom' : 'RSS'} feed generated: ${path} (${items.length} ${items.length === 1 ? 'item' : 'items'})`)
197
+ return { path, count: items.length }
198
+ }
package/src/main.js CHANGED
@@ -22,6 +22,7 @@ import { loadUserConfig, applyConfig } from './config.js'
22
22
  import { runViteDev } from './dev-server.js'
23
23
  import { buildHtmlEntries, runViteBuild } from './build-system.js'
24
24
  import { runPagefind } from './pagefind.js'
25
+ import { generateRssFeed } from './feed.js'
25
26
  import { runVitePreview } from './preview-server.js'
26
27
  import { cli, state } from './state.js'
27
28
  import { HTMLRenderer } from './renderer.js'
@@ -77,6 +78,7 @@ const main = async () => {
77
78
  ...userSite,
78
79
  base: siteBase,
79
80
  name: state.SITE_NAME,
81
+ owner: state.SITE_OWNER,
80
82
  root: state.ROOT_DIR,
81
83
  pagesDir: state.PAGES_DIR,
82
84
  componentsDir: state.COMPONENTS_DIR,
@@ -111,7 +113,7 @@ const main = async () => {
111
113
  const startTime = performance.now()
112
114
  await runHooks(state.USER_PRE_BUILD_HOOKS)
113
115
  await runHooks(state.THEME_PRE_BUILD_HOOKS)
114
- const { entry, htmlCache, pagesContext } = await buildHtmlEntries()
116
+ const { entry, htmlCache, pagesContext, rssContent } = await buildHtmlEntries()
115
117
  const buildContext = pagesContext
116
118
  ? {
117
119
  pagesContext,
@@ -129,6 +131,9 @@ const main = async () => {
129
131
  if (state.PAGEFIND_ENABLED) {
130
132
  await runPagefind()
131
133
  }
134
+ if (state.RSS_ENABLED) {
135
+ await generateRssFeed(pagesContext, rssContent)
136
+ }
132
137
  await runHooks(state.THEME_POST_BUILD_HOOKS, buildContext)
133
138
  await runHooks(state.USER_POST_BUILD_HOOKS, buildContext)
134
139
  const endTime = performance.now()
package/src/mdx.js CHANGED
@@ -28,7 +28,7 @@ import rehypeStarryNight from 'rehype-starry-night'
28
28
  import { createStarryNight } from '@wooorm/starry-night'
29
29
  import remarkGfm from 'remark-gfm'
30
30
  import { HTMLRenderer } from './renderer.js'
31
- import { signal, computed, read, Suspense, nextTick } from 'refui'
31
+ import { Suspense, nextTick } from 'refui'
32
32
  import { createPortal } from 'refui/extras'
33
33
  import { pathToFileURL } from 'url'
34
34
  import { existsSync } from 'fs'
@@ -38,7 +38,7 @@ import { resolveUserMdxConfig, withBase } from './config.js'
38
38
  import { methanolCtx } from './rehype-plugins/methanol-ctx.js'
39
39
  import { linkResolve } from './rehype-plugins/link-resolve.js'
40
40
  import { cached } from './utils.js'
41
- import { resetReframeRenderCount } from './components.js'
41
+ import { resetReframeRenderCount, setReframeHydrationEnabled, getReframeHydrationEnabled } from './components.js'
42
42
 
43
43
  // Workaround for Vite: it doesn't support resolving module/virtual modules in script src in dev mode
44
44
  const resolveRewindInject = cached(() =>
@@ -145,27 +145,8 @@ export const buildPageContext = ({ routePath, path, pageMeta, pagesContext, lazy
145
145
  getSiblings,
146
146
  withBase
147
147
  }
148
- const resolvePagesTree = () =>
149
- pagesContext.getPagesTree ? pagesContext.getPagesTree(routePath) : pagesContext.pagesTree || []
150
- if (lazyPagesTree) {
151
- let cachedTree = null
152
- let hasTree = false
153
- Object.defineProperty(ctx, 'pagesTree', {
154
- enumerable: true,
155
- get() {
156
- if (!hasTree) {
157
- cachedTree = resolvePagesTree()
158
- hasTree = true
159
- }
160
- return cachedTree
161
- },
162
- set(value) {
163
- cachedTree = value
164
- hasTree = true
165
- }
166
- })
167
- } else {
168
- ctx.pagesTree = resolvePagesTree()
148
+ if (!lazyPagesTree) {
149
+ ctx.pagesTree = pagesContext.getPagesTree ? pagesContext.getPagesTree(routePath) : pagesContext.pagesTree || []
169
150
  }
170
151
  return ctx
171
152
  }
@@ -645,5 +626,48 @@ export const renderHtml = async ({ routePath, path, components, pagesContext, pa
645
626
  )
646
627
  })
647
628
 
648
- return HTMLRenderer.serialize(renderResult)
629
+ const result = HTMLRenderer.serialize(renderResult)
630
+
631
+ return result
632
+ }
633
+
634
+ export const renderPageContent = async ({ routePath, path, components, pagesContext, pageMeta }) => {
635
+ const ctx = buildPageContext({
636
+ routePath,
637
+ path,
638
+ pageMeta,
639
+ pagesContext
640
+ })
641
+
642
+ await compilePageMdx(pageMeta, pagesContext, { ctx })
643
+
644
+ const prevHydration = getReframeHydrationEnabled()
645
+ const prevThemeHydration = state.THEME_ENV?.getHydrationEnabled?.()
646
+ setReframeHydrationEnabled(false)
647
+ state.THEME_ENV?.setHydrationEnabled?.(false)
648
+ const mdxComponent = pageMeta.mdxComponent
649
+ const PageContent = () => mdxComponent()
650
+
651
+ try {
652
+ const renderResult = await new Promise((resolve, reject) => {
653
+ const result = HTMLRenderer.c(
654
+ Suspense,
655
+ {
656
+ onLoad() {
657
+ nextTick(() => resolve(result))
658
+ },
659
+ catch({ error }) {
660
+ reject(error)
661
+ }
662
+ },
663
+ PageContent
664
+ )
665
+ })
666
+ return HTMLRenderer.serialize(renderResult)
667
+ } finally {
668
+ setReframeHydrationEnabled(prevHydration)
669
+ if (prevThemeHydration != null) {
670
+ state.THEME_ENV?.setHydrationEnabled?.(prevThemeHydration)
671
+ }
672
+ }
649
673
  }