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 +1 -1
- package/src/build-system.js +76 -19
- package/src/components.js +2 -0
- package/src/config.js +41 -0
- package/src/dev-server.js +6 -4
- package/src/feed.js +198 -0
- package/src/main.js +6 -1
- package/src/mdx.js +48 -24
- package/src/pages.js +105 -111
- package/src/reframe.js +19 -2
- package/src/state.js +22 -0
- package/src/templates/atom-feed.jsx +61 -0
- package/src/{error-page.jsx → templates/error-page.jsx} +2 -2
- package/src/templates/rss-feed.jsx +60 -0
- package/src/workers/build-pool.js +10 -1
- package/src/workers/build-worker.js +51 -21
- package/themes/blog/sources/style.css +44 -4
- package/themes/blog/src/layout-categories.jsx +2 -2
- package/themes/blog/src/layout-collections.jsx +2 -2
- package/themes/blog/src/layout-home.jsx +2 -2
- package/themes/blog/src/page.jsx +55 -6
- package/themes/blog/src/post-utils.js +13 -3
- package/themes/default/sources/style.css +27 -1
- package/themes/default/src/nav-tree.jsx +70 -55
- package/themes/default/src/page.jsx +23 -0
package/package.json
CHANGED
package/src/build-system.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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, '&')
|
|
71
|
+
.replace(/</g, '<')
|
|
72
|
+
.replace(/>/g, '>')
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|