methanol 0.0.8 → 0.0.10
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/README.md +3 -0
- package/index.js +7 -2
- package/package.json +9 -3
- package/src/build-system.js +50 -8
- package/src/client/sw.js +751 -0
- package/src/{assets.js → client/virtual-module/assets.js} +7 -3
- package/src/{virtual-module → client/virtual-module}/inject.js +1 -0
- package/src/{virtual-module → client/virtual-module}/loader.js +5 -5
- package/src/{virtual-module → client/virtual-module}/pagefind-loader.js +1 -1
- package/src/client/virtual-module/pwa-inject.js +25 -0
- package/src/components.js +5 -5
- package/src/config.js +38 -7
- package/src/dev-server.js +64 -66
- package/src/logger.js +84 -0
- package/src/main.js +11 -0
- package/src/mdx.js +33 -38
- package/src/pagefind.js +16 -5
- package/src/pages.js +61 -51
- package/src/public-assets.js +1 -1
- package/src/{rewind.js → reframe.js} +1 -1
- package/src/rehype-plugins/link-resolve.js +2 -2
- package/src/stage-logger.js +10 -6
- package/src/state.js +31 -4
- package/src/utils.js +23 -1
- package/src/vite-plugins.js +17 -4
- package/themes/default/components/ThemeSearchBox.client.jsx +120 -11
- package/themes/default/index.js +2 -2
- package/themes/default/pages/404.mdx +4 -0
- package/themes/default/pages/index.mdx +7 -9
- package/themes/default/pages/offline.mdx +11 -0
- package/themes/default/sources/style.css +248 -169
- package/themes/default/src/nav-tree.jsx +112 -0
- package/themes/default/{page.jsx → src/page.jsx} +10 -55
- /package/themes/default/{heading.jsx → src/heading.jsx} +0 -0
package/src/state.js
CHANGED
|
@@ -101,12 +101,11 @@ const withCommonOptions = (y) =>
|
|
|
101
101
|
type: 'boolean',
|
|
102
102
|
default: false
|
|
103
103
|
})
|
|
104
|
-
.option('
|
|
104
|
+
.option('highlight', {
|
|
105
105
|
describe: 'Enable or disable code highlighting',
|
|
106
|
-
type: '
|
|
106
|
+
type: 'boolean',
|
|
107
107
|
coerce: (value) => {
|
|
108
108
|
if (value == null) return null
|
|
109
|
-
if (value === true || value === '') return true
|
|
110
109
|
if (typeof value === 'boolean') return value
|
|
111
110
|
const normalized = String(value).trim().toLowerCase()
|
|
112
111
|
if (normalized === 'true') return true
|
|
@@ -114,6 +113,26 @@ const withCommonOptions = (y) =>
|
|
|
114
113
|
return null
|
|
115
114
|
}
|
|
116
115
|
})
|
|
116
|
+
.option('verbose', {
|
|
117
|
+
alias: 'v',
|
|
118
|
+
describe: 'Enable verbose output',
|
|
119
|
+
type: 'boolean',
|
|
120
|
+
default: false
|
|
121
|
+
})
|
|
122
|
+
.option('base', {
|
|
123
|
+
describe: 'Base URL override',
|
|
124
|
+
type: 'string',
|
|
125
|
+
requiresArg: true,
|
|
126
|
+
nargs: 1
|
|
127
|
+
})
|
|
128
|
+
.option('search', {
|
|
129
|
+
describe: 'Enable search indexing (pagefind)',
|
|
130
|
+
type: 'boolean'
|
|
131
|
+
})
|
|
132
|
+
.option('pwa', {
|
|
133
|
+
describe: 'Enable PWA support',
|
|
134
|
+
type: 'boolean'
|
|
135
|
+
})
|
|
117
136
|
|
|
118
137
|
const parser = yargs(hideBin(process.argv))
|
|
119
138
|
.scriptName('methanol')
|
|
@@ -141,7 +160,11 @@ export const cli = {
|
|
|
141
160
|
CLI_OUTPUT_DIR: argv.output || null,
|
|
142
161
|
CLI_CONFIG_PATH: argv.config || null,
|
|
143
162
|
CLI_SITE_NAME: argv['site-name'] || null,
|
|
144
|
-
CLI_CODE_HIGHLIGHTING: typeof argv
|
|
163
|
+
CLI_CODE_HIGHLIGHTING: typeof argv.highlight === 'boolean' ? argv.highlight : null,
|
|
164
|
+
CLI_VERBOSE: Boolean(argv.verbose),
|
|
165
|
+
CLI_BASE: argv.base || null,
|
|
166
|
+
CLI_SEARCH: argv.search,
|
|
167
|
+
CLI_PWA: argv.pwa
|
|
145
168
|
}
|
|
146
169
|
|
|
147
170
|
export const state = {
|
|
@@ -170,6 +193,7 @@ export const state = {
|
|
|
170
193
|
PAGEFIND_ENABLED: false,
|
|
171
194
|
PAGEFIND_OPTIONS: null,
|
|
172
195
|
PAGEFIND_BUILD: null,
|
|
196
|
+
PWA_ENABLED: false,
|
|
173
197
|
USER_PRE_BUILD_HOOKS: [],
|
|
174
198
|
USER_POST_BUILD_HOOKS: [],
|
|
175
199
|
USER_PRE_BUNDLE_HOOKS: [],
|
|
@@ -180,6 +204,9 @@ export const state = {
|
|
|
180
204
|
THEME_POST_BUNDLE_HOOKS: [],
|
|
181
205
|
STARRY_NIGHT_ENABLED: false,
|
|
182
206
|
STARRY_NIGHT_OPTIONS: null,
|
|
207
|
+
GFM_ENABLED: true,
|
|
208
|
+
PWA_ENABLED: false,
|
|
209
|
+
PWA_OPTIONS: null,
|
|
183
210
|
CURRENT_MODE: 'production',
|
|
184
211
|
RESOLVED_MDX_CONFIG: undefined,
|
|
185
212
|
RESOLVED_VITE_CONFIG: undefined
|
package/src/utils.js
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
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 NullProtoObj from 'null-prototype-object'
|
|
22
|
+
|
|
1
23
|
export const cached = (fn) => {
|
|
2
24
|
let cache = null
|
|
3
25
|
return () => (cache ?? (cache = fn()))
|
|
4
26
|
}
|
|
5
27
|
|
|
6
28
|
export const cachedStr = (fn) => {
|
|
7
|
-
const cache =
|
|
29
|
+
const cache = new NullProtoObj()
|
|
8
30
|
return (key) => (cache[key] ?? (cache[key] = fn(key)))
|
|
9
31
|
}
|
package/src/vite-plugins.js
CHANGED
|
@@ -26,7 +26,7 @@ import { normalizePath } from 'vite'
|
|
|
26
26
|
import { state } from './state.js'
|
|
27
27
|
import { resolveBasePrefix } from './config.js'
|
|
28
28
|
import { genRegistryScript } from './components.js'
|
|
29
|
-
import { INJECT_SCRIPT, LOADER_SCRIPT, PAGEFIND_LOADER_SCRIPT } from './assets.js'
|
|
29
|
+
import { INJECT_SCRIPT, LOADER_SCRIPT, PAGEFIND_LOADER_SCRIPT, PWA_INJECT_SCRIPT } from './client/virtual-module/assets.js'
|
|
30
30
|
import { projectRequire } from './node-loader.js'
|
|
31
31
|
|
|
32
32
|
const require = createRequire(import.meta.url)
|
|
@@ -129,9 +129,22 @@ const virtualModuleMap = {
|
|
|
129
129
|
get 'registry.js'() {
|
|
130
130
|
return `export const registry = ${genRegistryScript()}`
|
|
131
131
|
},
|
|
132
|
-
loader
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
get loader() {
|
|
133
|
+
return LOADER_SCRIPT()
|
|
134
|
+
},
|
|
135
|
+
get 'inject.js'() {
|
|
136
|
+
return INJECT_SCRIPT()
|
|
137
|
+
},
|
|
138
|
+
get 'pagefind-loader'() {
|
|
139
|
+
return PAGEFIND_LOADER_SCRIPT()
|
|
140
|
+
},
|
|
141
|
+
get 'pwa-inject'() {
|
|
142
|
+
if (state.PWA_ENABLED) {
|
|
143
|
+
return PWA_INJECT_SCRIPT()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return ''
|
|
147
|
+
}
|
|
135
148
|
}
|
|
136
149
|
|
|
137
150
|
const getModuleIdSegment = (id, start) => {
|
|
@@ -30,6 +30,7 @@ const loadPagefindModule = async () => {
|
|
|
30
30
|
|
|
31
31
|
let keybindReady = false
|
|
32
32
|
let cachedPagefind = null
|
|
33
|
+
const PAGE_SIZE = 10
|
|
33
34
|
|
|
34
35
|
const resolveShortcutLabel = () => {
|
|
35
36
|
if (typeof navigator === 'undefined') return 'Ctrl+K'
|
|
@@ -60,48 +61,107 @@ export default function ({ options } = {}) {
|
|
|
60
61
|
const query = signal('')
|
|
61
62
|
const results = signal([])
|
|
62
63
|
const isLoading = signal(false)
|
|
64
|
+
const isLoadingMore = signal(false)
|
|
65
|
+
const hasMore = signal(false)
|
|
63
66
|
const activeIndex = signal(-1)
|
|
67
|
+
const loadError = signal('')
|
|
64
68
|
|
|
65
69
|
const buttonRef = signal()
|
|
66
70
|
const inputRef = signal()
|
|
71
|
+
const resultsRef = signal()
|
|
72
|
+
const loadingMoreRef = signal()
|
|
67
73
|
const resultIdPrefix = `search-result-${Math.random().toString(36).slice(2)}`
|
|
68
74
|
const activeMatch = onCondition(activeIndex)
|
|
69
75
|
|
|
70
76
|
let debounceTimer = null
|
|
77
|
+
let resultHandles = []
|
|
78
|
+
let resultOffset = 0
|
|
79
|
+
let latestSearchId = 0
|
|
71
80
|
const shortcutLabel = resolveShortcutLabel()
|
|
72
81
|
const [Inlet, Outlet] = createPortal()
|
|
73
82
|
|
|
83
|
+
const resetSearchState = () => {
|
|
84
|
+
resultHandles = []
|
|
85
|
+
resultOffset = 0
|
|
86
|
+
hasMore.value = false
|
|
87
|
+
isLoadingMore.value = false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const loadMore = async (initial = false) => {
|
|
91
|
+
const searchId = latestSearchId
|
|
92
|
+
if (!initial) {
|
|
93
|
+
if (isLoadingMore.value || !hasMore.value) return
|
|
94
|
+
isLoadingMore.value = true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const slice = resultHandles.slice(resultOffset, resultOffset + PAGE_SIZE)
|
|
98
|
+
if (!slice.length) {
|
|
99
|
+
hasMore.value = false
|
|
100
|
+
if (!initial) isLoadingMore.value = false
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const data = await Promise.all(slice.map((r) => r.data()))
|
|
106
|
+
if (searchId !== latestSearchId) return
|
|
107
|
+
|
|
108
|
+
results.value = results.value.concat(data.map((value) => ({ value, el: signal() })))
|
|
109
|
+
resultOffset += slice.length
|
|
110
|
+
hasMore.value = resultOffset < resultHandles.length
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (searchId !== latestSearchId) return
|
|
113
|
+
loadError.value = 'Search is unavailable. Please refresh and try again.'
|
|
114
|
+
console.error('Search error:', err)
|
|
115
|
+
} finally {
|
|
116
|
+
if (!initial && searchId === latestSearchId) isLoadingMore.value = false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
74
120
|
const search = async (q) => {
|
|
121
|
+
const searchId = ++latestSearchId
|
|
75
122
|
isLoading.value = true
|
|
76
123
|
results.value = []
|
|
77
124
|
activeIndex.value = -1
|
|
125
|
+
resetSearchState()
|
|
78
126
|
|
|
79
127
|
const pagefind = await ensurePagefind(options)
|
|
128
|
+
if (searchId !== latestSearchId) return
|
|
129
|
+
|
|
80
130
|
if (!pagefind) {
|
|
81
131
|
isLoading.value = false
|
|
132
|
+
loadError.value = 'Search is unavailable. Please refresh and try again.'
|
|
82
133
|
return
|
|
83
134
|
}
|
|
135
|
+
loadError.value = ''
|
|
84
136
|
|
|
85
137
|
try {
|
|
86
138
|
const searchResult = await pagefind.search(q)
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
139
|
+
if (searchId !== latestSearchId) return
|
|
140
|
+
|
|
141
|
+
resultHandles = searchResult?.results || []
|
|
142
|
+
resultOffset = 0
|
|
143
|
+
hasMore.value = resultHandles.length > 0
|
|
144
|
+
await loadMore(true)
|
|
91
145
|
} catch (err) {
|
|
146
|
+
if (searchId !== latestSearchId) return
|
|
147
|
+
loadError.value = 'Search is unavailable. Please refresh and try again.'
|
|
92
148
|
console.error('Search error:', err)
|
|
93
149
|
} finally {
|
|
94
|
-
isLoading.value = false
|
|
150
|
+
if (searchId === latestSearchId) isLoading.value = false
|
|
95
151
|
}
|
|
96
152
|
}
|
|
97
153
|
|
|
98
154
|
const onInput = (event) => {
|
|
99
155
|
const value = event.target.value
|
|
100
156
|
query.value = value
|
|
157
|
+
loadError.value = ''
|
|
101
158
|
if (debounceTimer) clearTimeout(debounceTimer)
|
|
102
159
|
|
|
103
160
|
if (!value.trim()) {
|
|
161
|
+
latestSearchId++
|
|
162
|
+
isLoading.value = false
|
|
104
163
|
results.value = []
|
|
164
|
+
resetSearchState()
|
|
105
165
|
activeIndex.value = -1
|
|
106
166
|
return
|
|
107
167
|
}
|
|
@@ -118,13 +178,18 @@ export default function ({ options } = {}) {
|
|
|
118
178
|
const open = async () => {
|
|
119
179
|
isOpen.value = true
|
|
120
180
|
setTimeout(focusInput, 50)
|
|
121
|
-
await ensurePagefind(options)
|
|
181
|
+
const pagefind = await ensurePagefind(options)
|
|
182
|
+
if (!pagefind) {
|
|
183
|
+
loadError.value = 'Search is unavailable. Please refresh and try again.'
|
|
184
|
+
}
|
|
122
185
|
}
|
|
123
186
|
|
|
124
187
|
const close = () => {
|
|
125
188
|
isOpen.value = false
|
|
126
189
|
query.value = ''
|
|
127
190
|
results.value = []
|
|
191
|
+
loadError.value = ''
|
|
192
|
+
resetSearchState()
|
|
128
193
|
activeIndex.value = -1
|
|
129
194
|
if (debounceTimer) clearTimeout(debounceTimer)
|
|
130
195
|
if (inputRef.value) inputRef.value.blur()
|
|
@@ -150,6 +215,13 @@ export default function ({ options } = {}) {
|
|
|
150
215
|
if (event.key === 'ArrowDown') {
|
|
151
216
|
event.preventDefault()
|
|
152
217
|
if (results.value.length > 0) {
|
|
218
|
+
if (hasMore.value && activeIndex.value === results.value.length - 1) {
|
|
219
|
+
loadMore(false)
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
loadingMoreRef.value?.scrollIntoView({ block: 'nearest' })
|
|
222
|
+
}, 10)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
153
225
|
const nextIndex = activeIndex.value >= 0 ? (activeIndex.value + 1) % results.value.length : 0
|
|
154
226
|
activeIndex.value = nextIndex
|
|
155
227
|
scrollActiveIntoView()
|
|
@@ -181,6 +253,13 @@ export default function ({ options } = {}) {
|
|
|
181
253
|
}
|
|
182
254
|
if (event.key === 'ArrowDown') {
|
|
183
255
|
event.preventDefault()
|
|
256
|
+
if (hasMore.value && indexValue === results.value.length - 1) {
|
|
257
|
+
loadMore(false)
|
|
258
|
+
setTimeout(() => {
|
|
259
|
+
loadingMoreRef.value?.scrollIntoView({ block: 'nearest' })
|
|
260
|
+
}, 10)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
184
263
|
const nextIndex = (indexValue + 1) % results.value.length
|
|
185
264
|
activeIndex.value = nextIndex
|
|
186
265
|
scrollActiveIntoView()
|
|
@@ -197,6 +276,13 @@ export default function ({ options } = {}) {
|
|
|
197
276
|
}
|
|
198
277
|
}
|
|
199
278
|
|
|
279
|
+
const onResultsScroll = (event) => {
|
|
280
|
+
const el = event.currentTarget
|
|
281
|
+
if (!el) return
|
|
282
|
+
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 24
|
|
283
|
+
if (nearBottom) loadMore(false)
|
|
284
|
+
}
|
|
285
|
+
|
|
200
286
|
const showEmpty = $(() => !query.value)
|
|
201
287
|
const showNoResults = $(() => {
|
|
202
288
|
const _query = query.value
|
|
@@ -204,6 +290,9 @@ export default function ({ options } = {}) {
|
|
|
204
290
|
const _length = results.value.length
|
|
205
291
|
return _query && !_isLoading && _length === 0
|
|
206
292
|
})
|
|
293
|
+
const showError = $(() => loadError.value)
|
|
294
|
+
const showStatus = $(() => !loadError.value)
|
|
295
|
+
const showLoadingMore = $(() => isLoadingMore.value && !isLoading.value)
|
|
207
296
|
|
|
208
297
|
if (typeof window !== 'undefined') {
|
|
209
298
|
window.__methanolSearchOpen = open
|
|
@@ -280,12 +369,19 @@ export default function ({ options } = {}) {
|
|
|
280
369
|
$ref={inputRef}
|
|
281
370
|
/>
|
|
282
371
|
</div>
|
|
283
|
-
<div class="search-results">
|
|
284
|
-
<If condition={
|
|
285
|
-
<If condition={
|
|
286
|
-
{() =>
|
|
372
|
+
<div class="search-results" on:scroll={onResultsScroll} $ref={resultsRef}>
|
|
373
|
+
<If condition={showError}>{() => <div class="search-status">{loadError}</div>}</If>
|
|
374
|
+
<If condition={showStatus}>
|
|
375
|
+
{() => (
|
|
376
|
+
<>
|
|
377
|
+
<If condition={showEmpty}>{() => <div class="search-status">Type to start searching...</div>}</If>
|
|
378
|
+
<If condition={showNoResults}>
|
|
379
|
+
{() => <div class="search-status">No results found for "{query}"</div>}
|
|
380
|
+
</If>
|
|
381
|
+
<If condition={isLoading}>{() => <div class="search-status">Searching...</div>}</If>
|
|
382
|
+
</>
|
|
383
|
+
)}
|
|
287
384
|
</If>
|
|
288
|
-
<If condition={isLoading}>{() => <div class="search-status">Searching...</div>}</If>
|
|
289
385
|
<For entries={results} indexed>
|
|
290
386
|
{({ item: { value, el }, index }) => (
|
|
291
387
|
<a
|
|
@@ -321,6 +417,19 @@ export default function ({ options } = {}) {
|
|
|
321
417
|
</a>
|
|
322
418
|
)}
|
|
323
419
|
</For>
|
|
420
|
+
<If condition={showLoadingMore}>
|
|
421
|
+
{() => (
|
|
422
|
+
<div
|
|
423
|
+
class="search-status"
|
|
424
|
+
$ref={(el) => {
|
|
425
|
+
loadingMoreRef.value = el
|
|
426
|
+
el.scrollIntoView({ block: 'nearest' })
|
|
427
|
+
}}
|
|
428
|
+
>
|
|
429
|
+
Loading more results...
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
</If>{' '}
|
|
324
433
|
</div>
|
|
325
434
|
</div>
|
|
326
435
|
</div>
|
package/themes/default/index.js
CHANGED
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
import { readFileSync } from 'fs'
|
|
22
22
|
import { fileURLToPath } from 'url'
|
|
23
23
|
import { dirname, resolve } from 'path'
|
|
24
|
-
import PAGE_TEMPLATE from './page.jsx'
|
|
25
|
-
import { createHeadings } from './heading.jsx'
|
|
24
|
+
import PAGE_TEMPLATE from './src/page.jsx'
|
|
25
|
+
import { createHeadings } from './src/heading.jsx'
|
|
26
26
|
|
|
27
27
|
const __filename = fileURLToPath(import.meta.url)
|
|
28
28
|
const __dirname = dirname(__filename)
|
|
@@ -6,19 +6,13 @@ title: Welcome
|
|
|
6
6
|
|
|
7
7
|
Methanol turns your content folder into a static site with rEFui and MDX, file-based routing, and a fast dev server.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npx methanol dev
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Open `http://localhost:5173`.
|
|
9
|
+
If you're seeing this page, that means you haven't created your `index.mdx` yet.
|
|
16
10
|
|
|
17
|
-
##
|
|
11
|
+
## Quick Start
|
|
18
12
|
|
|
19
13
|
Pages are currently read from the configured `pagesDir`:
|
|
20
14
|
|
|
21
|
-
<p><code>{ctx.site
|
|
15
|
+
<p><code>{ctx.site.pagesDir || 'pages/'}</code></p>
|
|
22
16
|
|
|
23
17
|
You can change this with `pagesDir` in your config or `--input` on the CLI. Create an `index.mdx` in that folder to replace this page.
|
|
24
18
|
|
|
@@ -29,3 +23,7 @@ components/ # JSX/TSX components for MDX
|
|
|
29
23
|
public/ # static assets copied as-is
|
|
30
24
|
dist/ # production output
|
|
31
25
|
```
|
|
26
|
+
|
|
27
|
+
## Further Reading
|
|
28
|
+
|
|
29
|
+
Visit [Methanol Docs](https://methanol.sudomaker.com) for details.
|