methanol 0.0.20 → 0.0.22
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/bin/methanol.js +1 -2
- package/package.json +6 -4
- package/src/base.js +36 -0
- package/src/build-system.js +337 -113
- package/src/client/sw.js +227 -180
- package/src/client/virtual-module/pwa-inject.js +25 -3
- package/src/config.js +19 -35
- package/src/dev-server.js +4 -2
- package/src/entry.js +22 -0
- package/src/feed.js +3 -12
- package/src/html/build-html.js +221 -0
- package/src/html/utils.js +125 -0
- package/src/html/worker-html.js +591 -0
- package/src/main.js +97 -6
- package/src/mdx.js +35 -2
- package/src/pages-index.js +11 -3
- package/src/pages.js +26 -11
- package/src/pwa.js +240 -0
- package/src/state.js +15 -3
- package/src/utils.js +1 -1
- package/src/vite-plugins.js +6 -2
- package/src/workers/build-pool.js +1 -1
- package/src/workers/build-worker.js +157 -18
- package/src/workers/entry-build-worker.js +22 -0
- package/src/workers/entry-mdx-compile-worker.js +22 -0
- package/src/workers/mdx-compile-worker.js +0 -1
- package/themes/benchmark/README.md +5 -0
- package/themes/benchmark/index.js +33 -0
- package/themes/benchmark/src/page.jsx +25 -0
- package/themes/blog/src/page.jsx +0 -2
- package/themes/default/src/nav-tree.jsx +3 -2
- package/themes/default/src/page.jsx +0 -2
package/src/feed.js
CHANGED
|
@@ -63,15 +63,6 @@ const wrapCdata = (value) => {
|
|
|
63
63
|
return `<![CDATA[${text.replace(/]]>/g, ']]]]><![CDATA[>')}]]>`
|
|
64
64
|
}
|
|
65
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
66
|
const resolveSiteUrl = (options, site) => {
|
|
76
67
|
if (options?.siteUrl) return options.siteUrl
|
|
77
68
|
if (state.SITE_BASE) return state.SITE_BASE
|
|
@@ -86,10 +77,10 @@ const buildItem = (page, siteUrl, htmlContent = null, isAtom = false, siteOwner
|
|
|
86
77
|
const link = new URL(href, siteUrl).href
|
|
87
78
|
const title = page.title || page.name || page.routePath || link
|
|
88
79
|
const description = extractExcerpt(page)
|
|
89
|
-
const contentSource = htmlContent
|
|
80
|
+
const contentSource = htmlContent ?? page.content ?? ''
|
|
90
81
|
const content = contentSource
|
|
91
82
|
? (isAtom
|
|
92
|
-
?
|
|
83
|
+
? contentSource
|
|
93
84
|
: HTMLRenderer.rawHTML(wrapCdata(contentSource)))
|
|
94
85
|
: null
|
|
95
86
|
const authorValue = page.frontmatter?.author
|
|
@@ -183,7 +174,7 @@ export const generateRssFeed = async (pagesContext, rssContent = null) => {
|
|
|
183
174
|
const items = pages
|
|
184
175
|
.map((page) => ({
|
|
185
176
|
page,
|
|
186
|
-
content: rssContent?.get(page.path)
|
|
177
|
+
content: rssContent?.get(page.path) ?? rssContent?.get(page.routePath) ?? null
|
|
187
178
|
}))
|
|
188
179
|
.map((entry) => buildItem(entry.page, siteUrl, entry.content, isAtom, siteOwner))
|
|
189
180
|
.filter(Boolean)
|
|
@@ -0,0 +1,221 @@
|
|
|
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 { writeFile, mkdir, rm } from 'fs/promises'
|
|
22
|
+
import { resolve, dirname, relative, posix } from 'path'
|
|
23
|
+
import { normalizePath } from 'vite'
|
|
24
|
+
import { state } from '../state.js'
|
|
25
|
+
import { hashMd5 } from './utils.js'
|
|
26
|
+
|
|
27
|
+
const ensureDir = async (dir) => {
|
|
28
|
+
await mkdir(dir, { recursive: true })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const METHANOL_DIR = '.methanol'
|
|
32
|
+
const ENTRY_DIR = 'entries'
|
|
33
|
+
|
|
34
|
+
const resolveMethanolDir = () => resolve(state.PAGES_DIR, METHANOL_DIR)
|
|
35
|
+
|
|
36
|
+
export async function scanHtmlEntries(entries, preScan = null, options = null) {
|
|
37
|
+
const methanolDir = resolveMethanolDir()
|
|
38
|
+
const entriesDir = resolve(methanolDir, ENTRY_DIR)
|
|
39
|
+
const assetsEntryPath = resolve(methanolDir, 'assets-entry.js')
|
|
40
|
+
await rm(entriesDir, { recursive: true, force: true })
|
|
41
|
+
await ensureDir(entriesDir)
|
|
42
|
+
const assetUrls = new Set()
|
|
43
|
+
const entryModules = []
|
|
44
|
+
const scriptCounts = new Map()
|
|
45
|
+
const scriptOrder = new Map()
|
|
46
|
+
let scriptIndex = 0
|
|
47
|
+
const stylePaths = new Set()
|
|
48
|
+
let commonScriptEntry = null
|
|
49
|
+
const commonScripts = new Set()
|
|
50
|
+
let pagesWithScripts = 0
|
|
51
|
+
|
|
52
|
+
const createEntryModule = async (kind, publicPath, contentOverride = null, extraImports = null) => {
|
|
53
|
+
const hash = hashMd5(`${kind}:${publicPath || contentOverride || ''}`)
|
|
54
|
+
const filename = `${kind}-${hash}.js`
|
|
55
|
+
const fsPath = resolve(entriesDir, filename)
|
|
56
|
+
const manifestKey = normalizePath(relative(state.PAGES_DIR, fsPath))
|
|
57
|
+
const lines = []
|
|
58
|
+
if (contentOverride) {
|
|
59
|
+
lines.push(contentOverride)
|
|
60
|
+
} else if (publicPath) {
|
|
61
|
+
lines.push(`import ${JSON.stringify(publicPath)}`)
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(extraImports) && extraImports.length) {
|
|
64
|
+
for (const entry of extraImports) {
|
|
65
|
+
if (!entry) continue
|
|
66
|
+
lines.push(`import ${JSON.stringify(entry)}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const content = lines.join('\n')
|
|
70
|
+
await writeFile(fsPath, content)
|
|
71
|
+
const entryInfo = {
|
|
72
|
+
kind,
|
|
73
|
+
publicPath,
|
|
74
|
+
fsPath,
|
|
75
|
+
manifestKey,
|
|
76
|
+
publicUrl: `/${METHANOL_DIR}/${ENTRY_DIR}/${filename}`
|
|
77
|
+
}
|
|
78
|
+
entryModules.push(entryInfo)
|
|
79
|
+
return entryInfo
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// assetUrls are collected from worker scan results
|
|
83
|
+
|
|
84
|
+
const parseSrcset = (value = '') =>
|
|
85
|
+
value
|
|
86
|
+
.split(',')
|
|
87
|
+
.map((entry) => entry.trim())
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.map((entry) => {
|
|
90
|
+
const [url, ...rest] = entry.split(/\s+/)
|
|
91
|
+
return { url, descriptor: rest.join(' ') }
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const reportProgress = typeof options?.onProgress === 'function'
|
|
95
|
+
? options.onProgress
|
|
96
|
+
: null
|
|
97
|
+
const totalEntries = entries.filter((entry) => entry.source !== 'static').length
|
|
98
|
+
let processedEntries = 0
|
|
99
|
+
|
|
100
|
+
const sortedEntries = [...entries].sort((a, b) => {
|
|
101
|
+
const left = a?.stagePath || a?.name || ''
|
|
102
|
+
const right = b?.stagePath || b?.name || ''
|
|
103
|
+
return left.localeCompare(right)
|
|
104
|
+
})
|
|
105
|
+
for (const entry of sortedEntries) {
|
|
106
|
+
if (entry.source === 'static') {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
if (preScan && preScan.has(entry.stagePath)) {
|
|
110
|
+
const scanned = preScan.get(entry.stagePath)
|
|
111
|
+
const scripts = Array.isArray(scanned?.scripts) ? scanned.scripts : []
|
|
112
|
+
const styles = Array.isArray(scanned?.styles) ? scanned.styles : []
|
|
113
|
+
const assets = Array.isArray(scanned?.assets) ? scanned.assets : []
|
|
114
|
+
if (scripts.length > 0) {
|
|
115
|
+
pagesWithScripts++
|
|
116
|
+
}
|
|
117
|
+
for (const script of scripts) {
|
|
118
|
+
if (!scriptOrder.has(script)) {
|
|
119
|
+
scriptOrder.set(script, scriptIndex++)
|
|
120
|
+
}
|
|
121
|
+
scriptCounts.set(script, (scriptCounts.get(script) || 0) + 1)
|
|
122
|
+
}
|
|
123
|
+
for (const style of styles) {
|
|
124
|
+
stylePaths.add(style)
|
|
125
|
+
}
|
|
126
|
+
for (const asset of assets) {
|
|
127
|
+
assetUrls.add(asset)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
processedEntries += 1
|
|
132
|
+
if (reportProgress) {
|
|
133
|
+
reportProgress(processedEntries, totalEntries)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (pagesWithScripts === 0) {
|
|
138
|
+
return { entryModules: [], assetsEntryPath: null, commonScriptEntry: null, commonScripts: [] }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const commonScriptCandidates = Array.from(scriptCounts.entries())
|
|
142
|
+
.filter(([, count]) => count === pagesWithScripts)
|
|
143
|
+
.map(([script]) => script)
|
|
144
|
+
.sort((a, b) => (scriptOrder.get(a) || 0) - (scriptOrder.get(b) || 0))
|
|
145
|
+
|
|
146
|
+
const assetsEntryPublicUrl = assetUrls.size ? `/${METHANOL_DIR}/assets-entry.js` : null
|
|
147
|
+
const extraImports = []
|
|
148
|
+
|
|
149
|
+
for (const style of stylePaths) {
|
|
150
|
+
const styleEntry = await createEntryModule('style', style)
|
|
151
|
+
extraImports.push(styleEntry.publicUrl)
|
|
152
|
+
}
|
|
153
|
+
if (assetsEntryPublicUrl) {
|
|
154
|
+
extraImports.push(assetsEntryPublicUrl)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (commonScriptCandidates.length) {
|
|
158
|
+
const commonImports = commonScriptCandidates
|
|
159
|
+
.map((script) => `import ${JSON.stringify(script)}`)
|
|
160
|
+
.join('\n')
|
|
161
|
+
commonScriptEntry = await createEntryModule('script-common', null, commonImports, extraImports)
|
|
162
|
+
for (const script of commonScriptCandidates) {
|
|
163
|
+
commonScripts.add(script)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const [script] of scriptCounts) {
|
|
168
|
+
if (commonScripts.has(script)) continue
|
|
169
|
+
if (!commonScriptEntry && extraImports.length) {
|
|
170
|
+
await createEntryModule('script', script, null, extraImports)
|
|
171
|
+
extraImports.length = 0
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
await createEntryModule('script', script)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await ensureDir(dirname(assetsEntryPath))
|
|
178
|
+
const assetLines = Array.from(assetUrls)
|
|
179
|
+
.sort()
|
|
180
|
+
.map((url) => `import ${JSON.stringify(url)};`)
|
|
181
|
+
if (assetLines.length) {
|
|
182
|
+
const assetEntry = `${assetLines.join('\n')}`
|
|
183
|
+
await writeFile(assetsEntryPath, assetEntry)
|
|
184
|
+
} else {
|
|
185
|
+
await rm(assetsEntryPath, { force: true })
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
entryModules,
|
|
190
|
+
commonScripts: Array.from(commonScripts),
|
|
191
|
+
commonScriptEntry,
|
|
192
|
+
assetsEntryPath: assetLines.length ? assetsEntryPath : null
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const resolveManifestEntry = (manifest, key) => {
|
|
197
|
+
if (!manifest || !key) return null
|
|
198
|
+
if (manifest[key]) return manifest[key]
|
|
199
|
+
if (manifest[`/${key}`]) return manifest[`/${key}`]
|
|
200
|
+
const normalized = posix.normalize(key)
|
|
201
|
+
if (manifest[normalized]) return manifest[normalized]
|
|
202
|
+
if (manifest[`/${normalized}`]) return manifest[`/${normalized}`]
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function rewriteHtmlEntries(entries, manifest, scanResult = null, options = null) {
|
|
207
|
+
const reportProgress = typeof options?.onProgress === 'function'
|
|
208
|
+
? options.onProgress
|
|
209
|
+
: null
|
|
210
|
+
const totalEntries = entries.filter((entry) => entry.source !== 'static').length
|
|
211
|
+
let processedEntries = 0
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
if (entry.source === 'static') {
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
processedEntries += 1
|
|
217
|
+
if (reportProgress) {
|
|
218
|
+
reportProgress(processedEntries, totalEntries)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
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 { createHash } from 'crypto'
|
|
22
|
+
|
|
23
|
+
export const hashMd5 = (value) =>
|
|
24
|
+
createHash('md5').update(value).digest('hex')
|
|
25
|
+
|
|
26
|
+
export const splitUrlParts = (value) => {
|
|
27
|
+
if (!value) return { path: '', suffix: '' }
|
|
28
|
+
const hashIndex = value.indexOf('#')
|
|
29
|
+
const queryIndex = value.indexOf('?')
|
|
30
|
+
let end = value.length
|
|
31
|
+
if (hashIndex >= 0) end = Math.min(end, hashIndex)
|
|
32
|
+
if (queryIndex >= 0) end = Math.min(end, queryIndex)
|
|
33
|
+
const path = value.slice(0, end)
|
|
34
|
+
const suffix = value.slice(end)
|
|
35
|
+
return { path, suffix }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const isExternalUrl = (value) => {
|
|
39
|
+
if (!value) return false
|
|
40
|
+
const trimmed = value.trim().toLowerCase()
|
|
41
|
+
if (!trimmed) return false
|
|
42
|
+
return (
|
|
43
|
+
trimmed.startsWith('http://') ||
|
|
44
|
+
trimmed.startsWith('https://') ||
|
|
45
|
+
trimmed.startsWith('//') ||
|
|
46
|
+
trimmed.startsWith('data:') ||
|
|
47
|
+
trimmed.startsWith('mailto:') ||
|
|
48
|
+
trimmed.startsWith('tel:') ||
|
|
49
|
+
trimmed.startsWith('javascript:')
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const resolvePageBase = (routePath) => {
|
|
54
|
+
if (!routePath || routePath === '/') return '/'
|
|
55
|
+
if (routePath.endsWith('/')) return routePath
|
|
56
|
+
const index = routePath.lastIndexOf('/')
|
|
57
|
+
if (index <= 0) return '/'
|
|
58
|
+
return `${routePath.slice(0, index)}/`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const stripBasePrefix = (value, basePrefix) => {
|
|
62
|
+
if (!value || !basePrefix || basePrefix === '/') return value
|
|
63
|
+
const trimmedBase = basePrefix.endsWith('/') ? basePrefix.slice(0, -1) : basePrefix
|
|
64
|
+
if (!trimmedBase) return value
|
|
65
|
+
if (value === trimmedBase) return '/'
|
|
66
|
+
if (value.startsWith(`${trimmedBase}/`)) {
|
|
67
|
+
const next = value.slice(trimmedBase.length)
|
|
68
|
+
return next.startsWith('/') ? next : `/${next}`
|
|
69
|
+
}
|
|
70
|
+
return value
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const joinBasePrefix = (basePrefix, value) => {
|
|
74
|
+
if (!value) return value
|
|
75
|
+
if (!basePrefix || basePrefix === '/') {
|
|
76
|
+
return value.startsWith('/') ? value : `/${value}`
|
|
77
|
+
}
|
|
78
|
+
const trimmedBase = basePrefix.endsWith('/') ? basePrefix.slice(0, -1) : basePrefix
|
|
79
|
+
const normalized = value.startsWith('/') ? value : `/${value}`
|
|
80
|
+
return `${trimmedBase}${normalized}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const resolveManifestKey = (value, basePrefix, pageRoutePath) => {
|
|
84
|
+
if (!value || isExternalUrl(value)) return null
|
|
85
|
+
const { path } = splitUrlParts(value)
|
|
86
|
+
if (!path) return null
|
|
87
|
+
const withoutBase = stripBasePrefix(path, basePrefix)
|
|
88
|
+
const resolvedPath = withoutBase.startsWith('/')
|
|
89
|
+
? withoutBase
|
|
90
|
+
: new URL(withoutBase, `http://methanol${resolvePageBase(pageRoutePath)}`).pathname
|
|
91
|
+
const key = resolvedPath.startsWith('/') ? resolvedPath.slice(1) : resolvedPath
|
|
92
|
+
return { key, resolvedPath }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const getAttr = (node, name) => {
|
|
96
|
+
const attrs = node.attrs || []
|
|
97
|
+
const attr = attrs.find((item) => item.name === name)
|
|
98
|
+
return attr ? attr.value : null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const setAttr = (node, name, value) => {
|
|
102
|
+
const attrs = node.attrs || []
|
|
103
|
+
const existing = attrs.find((item) => item.name === name)
|
|
104
|
+
if (existing) {
|
|
105
|
+
existing.value = value
|
|
106
|
+
} else {
|
|
107
|
+
attrs.push({ name, value })
|
|
108
|
+
}
|
|
109
|
+
node.attrs = attrs
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const getTextContent = (node) => {
|
|
113
|
+
if (!node.childNodes) return ''
|
|
114
|
+
return node.childNodes
|
|
115
|
+
.map((child) => (child.nodeName === '#text' ? child.value : ''))
|
|
116
|
+
.join('')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const walkNodes = async (node, visitor) => {
|
|
120
|
+
await visitor(node)
|
|
121
|
+
if (!node.childNodes) return
|
|
122
|
+
for (const child of node.childNodes) {
|
|
123
|
+
await walkNodes(child, visitor)
|
|
124
|
+
}
|
|
125
|
+
}
|