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/main.js
CHANGED
|
@@ -20,14 +20,19 @@
|
|
|
20
20
|
|
|
21
21
|
import { loadUserConfig, applyConfig } from './config.js'
|
|
22
22
|
import { runViteDev } from './dev-server.js'
|
|
23
|
-
import { buildHtmlEntries, runViteBuild } from './build-system.js'
|
|
23
|
+
import { buildHtmlEntries, runViteBuild, scanHtmlEntries } from './build-system.js'
|
|
24
|
+
import { buildPrecacheManifest, patchServiceWorker, writeWebManifest } from './pwa.js'
|
|
25
|
+
import { terminateWorkers } from './workers/build-pool.js'
|
|
24
26
|
import { runPagefind } from './pagefind.js'
|
|
25
27
|
import { generateRssFeed } from './feed.js'
|
|
26
28
|
import { runVitePreview } from './preview-server.js'
|
|
27
29
|
import { cli, state } from './state.js'
|
|
28
30
|
import { HTMLRenderer } from './renderer.js'
|
|
29
|
-
import { readFile } from 'fs/promises'
|
|
31
|
+
import { readFile, rm, mkdir, copyFile, cp } from 'fs/promises'
|
|
32
|
+
import { resolve, dirname } from 'path'
|
|
30
33
|
import { style, logger } from './logger.js'
|
|
34
|
+
import { createStageLogger } from './stage-logger.js'
|
|
35
|
+
import { preparePublicAssets } from './public-assets.js'
|
|
31
36
|
|
|
32
37
|
const printBanner = async () => {
|
|
33
38
|
try {
|
|
@@ -111,9 +116,27 @@ const main = async () => {
|
|
|
111
116
|
}
|
|
112
117
|
if (isBuild) {
|
|
113
118
|
const startTime = performance.now()
|
|
119
|
+
const logEnabled = state.CURRENT_MODE === 'production' && cli.command === 'build' && !cli.CLI_VERBOSE
|
|
120
|
+
const stageLogger = createStageLogger(logEnabled)
|
|
114
121
|
await runHooks(state.USER_PRE_BUILD_HOOKS)
|
|
115
122
|
await runHooks(state.THEME_PRE_BUILD_HOOKS)
|
|
116
|
-
const {
|
|
123
|
+
const {
|
|
124
|
+
htmlEntries,
|
|
125
|
+
pagesContext,
|
|
126
|
+
rssContent,
|
|
127
|
+
renderScans,
|
|
128
|
+
renderScansById,
|
|
129
|
+
htmlStageDir,
|
|
130
|
+
workers,
|
|
131
|
+
assignments
|
|
132
|
+
} = await buildHtmlEntries({ keepWorkers: true })
|
|
133
|
+
const scanResult = await scanHtmlEntries(htmlEntries, renderScans)
|
|
134
|
+
const hasEntryModules = Array.isArray(scanResult.entryModules) && scanResult.entryModules.length > 0
|
|
135
|
+
const hasCommonScripts = Array.isArray(scanResult.commonScripts) && scanResult.commonScripts.length > 0
|
|
136
|
+
const hasCommonEntry = Boolean(scanResult.commonScriptEntry)
|
|
137
|
+
const hasAssetsEntry = Boolean(scanResult.assetsEntryPath)
|
|
138
|
+
const hasStaticHtmlInputs = htmlEntries.some((entry) => entry?.source === 'static' && entry.inputPath)
|
|
139
|
+
const shouldBundle = state.PWA_ENABLED || hasEntryModules || hasCommonScripts || hasCommonEntry || hasAssetsEntry || hasStaticHtmlInputs
|
|
117
140
|
const buildContext = pagesContext
|
|
118
141
|
? {
|
|
119
142
|
pagesContext,
|
|
@@ -125,15 +148,83 @@ const main = async () => {
|
|
|
125
148
|
: null
|
|
126
149
|
await runHooks(state.USER_PRE_BUNDLE_HOOKS, buildContext)
|
|
127
150
|
await runHooks(state.THEME_PRE_BUNDLE_HOOKS, buildContext)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
|
|
152
|
+
let finalizeToken = null
|
|
153
|
+
try {
|
|
154
|
+
if (shouldBundle) {
|
|
155
|
+
await runViteBuild({
|
|
156
|
+
...scanResult,
|
|
157
|
+
htmlEntries,
|
|
158
|
+
preWrite: async () => {
|
|
159
|
+
await runHooks(state.THEME_POST_BUNDLE_HOOKS, buildContext)
|
|
160
|
+
await runHooks(state.USER_POST_BUNDLE_HOOKS, buildContext)
|
|
161
|
+
await runHooks(state.USER_PRE_WRITE_HOOKS, buildContext)
|
|
162
|
+
await runHooks(state.THEME_PRE_WRITE_HOOKS, buildContext)
|
|
163
|
+
},
|
|
164
|
+
postWrite: async () => {
|
|
165
|
+
await runHooks(state.THEME_POST_WRITE_HOOKS, buildContext)
|
|
166
|
+
await runHooks(state.USER_POST_WRITE_HOOKS, buildContext)
|
|
167
|
+
finalizeToken = stageLogger.start('Finalizing build')
|
|
168
|
+
},
|
|
169
|
+
rewrite: {
|
|
170
|
+
pages: pagesContext?.pagesAll || pagesContext?.pages || [],
|
|
171
|
+
htmlStageDir,
|
|
172
|
+
scanResult,
|
|
173
|
+
renderScansById,
|
|
174
|
+
workers,
|
|
175
|
+
assignments
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
} else {
|
|
179
|
+
await runHooks(state.THEME_POST_BUNDLE_HOOKS, buildContext)
|
|
180
|
+
await runHooks(state.USER_POST_BUNDLE_HOOKS, buildContext)
|
|
181
|
+
await runHooks(state.USER_PRE_WRITE_HOOKS, buildContext)
|
|
182
|
+
await runHooks(state.THEME_PRE_WRITE_HOOKS, buildContext)
|
|
183
|
+
if (state.STATIC_DIR !== false && state.MERGED_ASSETS_DIR) {
|
|
184
|
+
await preparePublicAssets({
|
|
185
|
+
themeDir: state.THEME_ASSETS_DIR,
|
|
186
|
+
userDir: state.USER_ASSETS_DIR,
|
|
187
|
+
targetDir: state.MERGED_ASSETS_DIR
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
await rm(state.DIST_DIR, { recursive: true, force: true })
|
|
191
|
+
await mkdir(state.DIST_DIR, { recursive: true })
|
|
192
|
+
if (state.STATIC_DIR !== false && state.STATIC_DIR) {
|
|
193
|
+
await cp(state.STATIC_DIR, state.DIST_DIR, { recursive: true, dereference: true })
|
|
194
|
+
}
|
|
195
|
+
for (const entry of htmlEntries) {
|
|
196
|
+
const name = entry?.name
|
|
197
|
+
const stagePath = entry?.stagePath || entry?.inputPath
|
|
198
|
+
if (!name || !stagePath) continue
|
|
199
|
+
const distPath = resolve(state.DIST_DIR, `${name}.html`)
|
|
200
|
+
await mkdir(dirname(distPath), { recursive: true })
|
|
201
|
+
await copyFile(stagePath, distPath)
|
|
202
|
+
}
|
|
203
|
+
await runHooks(state.THEME_POST_WRITE_HOOKS, buildContext)
|
|
204
|
+
await runHooks(state.USER_POST_WRITE_HOOKS, buildContext)
|
|
205
|
+
finalizeToken = stageLogger.start('Finalizing build')
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
if (workers) {
|
|
209
|
+
await terminateWorkers(workers)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
await runHooks(state.THEME_FINALIZE_HOOKS, buildContext)
|
|
213
|
+
await runHooks(state.USER_FINALIZE_HOOKS, buildContext)
|
|
214
|
+
stageLogger.end(finalizeToken)
|
|
131
215
|
if (state.PAGEFIND_ENABLED) {
|
|
132
216
|
await runPagefind()
|
|
133
217
|
}
|
|
134
218
|
if (state.RSS_ENABLED) {
|
|
135
219
|
await generateRssFeed(pagesContext, rssContent)
|
|
136
220
|
}
|
|
221
|
+
if (state.PWA_ENABLED) {
|
|
222
|
+
await writeWebManifest({ distDir: state.DIST_DIR, options: state.PWA_OPTIONS })
|
|
223
|
+
const precache = await buildPrecacheManifest({ distDir: state.DIST_DIR, options: state.PWA_OPTIONS })
|
|
224
|
+
if (precache?.manifestHash) {
|
|
225
|
+
await patchServiceWorker({ distDir: state.DIST_DIR, manifestHash: precache.manifestHash })
|
|
226
|
+
}
|
|
227
|
+
}
|
|
137
228
|
await runHooks(state.THEME_POST_BUILD_HOOKS, buildContext)
|
|
138
229
|
await runHooks(state.USER_POST_BUILD_HOOKS, buildContext)
|
|
139
230
|
const endTime = performance.now()
|
package/src/mdx.js
CHANGED
|
@@ -49,6 +49,7 @@ const RWND_FALLBACK = HTMLRenderer.rawHTML(
|
|
|
49
49
|
)
|
|
50
50
|
|
|
51
51
|
let cachedHeadAssets = null
|
|
52
|
+
let cachedSiteHeadAssets = null
|
|
52
53
|
|
|
53
54
|
const resolveUserHeadAssets = () => {
|
|
54
55
|
if (cachedHeadAssets) {
|
|
@@ -125,6 +126,36 @@ const resolvePageHeadAssets = (page) => {
|
|
|
125
126
|
return assets
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
const resolveSiteHeadAssets = (ctx) => {
|
|
130
|
+
if (cachedSiteHeadAssets) {
|
|
131
|
+
return cachedSiteHeadAssets
|
|
132
|
+
}
|
|
133
|
+
const assets = []
|
|
134
|
+
const site = ctx?.site || {}
|
|
135
|
+
const feed = site.feed
|
|
136
|
+
if (feed?.enabled && feed.href) {
|
|
137
|
+
const label = feed.atom ? 'Atom' : 'RSS'
|
|
138
|
+
const name = typeof site.name === 'string' && site.name.trim() ? site.name : 'Site'
|
|
139
|
+
const type = feed.atom ? 'application/atom+xml' : 'application/rss+xml'
|
|
140
|
+
assets.push(
|
|
141
|
+
HTMLRenderer.c('link', {
|
|
142
|
+
rel: 'alternate',
|
|
143
|
+
type,
|
|
144
|
+
title: `${name} ${label}`,
|
|
145
|
+
href: feed.href
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
const pwa = site.pwa
|
|
150
|
+
if (state.CURRENT_MODE === 'production' && pwa?.enabled && pwa.manifestHref) {
|
|
151
|
+
assets.push(HTMLRenderer.c('link', { rel: 'manifest', href: pwa.manifestHref }))
|
|
152
|
+
}
|
|
153
|
+
if (state.CURRENT_MODE === 'production') {
|
|
154
|
+
cachedSiteHeadAssets = assets
|
|
155
|
+
}
|
|
156
|
+
return assets
|
|
157
|
+
}
|
|
158
|
+
|
|
128
159
|
export const buildPageContext = ({ routePath, path, pageMeta, pagesContext, lazyPagesTree = false }) => {
|
|
129
160
|
const page = pageMeta
|
|
130
161
|
const language = pagesContext.getLanguageForRoute ? pagesContext.getLanguageForRoute(routePath) : null
|
|
@@ -583,6 +614,7 @@ export const renderHtml = async ({ routePath, path, components, pagesContext, pa
|
|
|
583
614
|
resolveRewindInject(),
|
|
584
615
|
...resolveUserHeadAssets(),
|
|
585
616
|
...resolvePageHeadAssets(pageMeta),
|
|
617
|
+
...resolveSiteHeadAssets(ctx),
|
|
586
618
|
Outlet(),
|
|
587
619
|
RWND_FALLBACK
|
|
588
620
|
]
|
|
@@ -636,7 +668,8 @@ export const renderPageContent = async ({ routePath, path, components, pagesCont
|
|
|
636
668
|
routePath,
|
|
637
669
|
path,
|
|
638
670
|
pageMeta,
|
|
639
|
-
pagesContext
|
|
671
|
+
pagesContext,
|
|
672
|
+
lazyPagesTree: true
|
|
640
673
|
})
|
|
641
674
|
|
|
642
675
|
await compilePageMdx(pageMeta, pagesContext, { ctx })
|
|
@@ -646,7 +679,7 @@ export const renderPageContent = async ({ routePath, path, components, pagesCont
|
|
|
646
679
|
setReframeHydrationEnabled(false)
|
|
647
680
|
state.THEME_ENV?.setHydrationEnabled?.(false)
|
|
648
681
|
const mdxComponent = pageMeta.mdxComponent
|
|
649
|
-
const PageContent = () => mdxComponent()
|
|
682
|
+
const PageContent = () => mdxComponent({ components })
|
|
650
683
|
|
|
651
684
|
try {
|
|
652
685
|
const renderResult = await new Promise((resolve, reject) => {
|
package/src/pages-index.js
CHANGED
|
@@ -18,10 +18,18 @@
|
|
|
18
18
|
* under the License.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import JSON5 from 'json5'
|
|
22
21
|
import { extractExcerpt } from './text-utils.js'
|
|
23
22
|
|
|
24
|
-
const OMIT_KEYS = new Set([
|
|
23
|
+
const OMIT_KEYS = new Set([
|
|
24
|
+
'content',
|
|
25
|
+
'mdxComponent',
|
|
26
|
+
'mdxCtx',
|
|
27
|
+
'getSiblings',
|
|
28
|
+
'matter',
|
|
29
|
+
'path',
|
|
30
|
+
'exclude',
|
|
31
|
+
'segments'
|
|
32
|
+
])
|
|
25
33
|
|
|
26
34
|
const sanitizePage = (page) => {
|
|
27
35
|
const result = {}
|
|
@@ -38,5 +46,5 @@ export const serializePagesIndex = (pages) => {
|
|
|
38
46
|
const list = Array.isArray(pages)
|
|
39
47
|
? pages.filter((page) => !page?.hidden).map(sanitizePage)
|
|
40
48
|
: []
|
|
41
|
-
return
|
|
49
|
+
return JSON.stringify(JSON.stringify(list))
|
|
42
50
|
}
|
package/src/pages.js
CHANGED
|
@@ -34,7 +34,7 @@ const isIgnoredEntry = (name) => name.startsWith('.') || name.startsWith('_')
|
|
|
34
34
|
|
|
35
35
|
const pageMetadataCache = new Map()
|
|
36
36
|
const pageDerivedCache = new Map()
|
|
37
|
-
const MDX_WORKER_URL = new URL('./workers/mdx-compile-worker.js', import.meta.url)
|
|
37
|
+
const MDX_WORKER_URL = new URL('./workers/entry-mdx-compile-worker.js', import.meta.url)
|
|
38
38
|
const cliOverrides = {
|
|
39
39
|
CLI_INTERMEDIATE_DIR: cli.CLI_INTERMEDIATE_DIR,
|
|
40
40
|
CLI_EMIT_INTERMEDIATE: cli.CLI_EMIT_INTERMEDIATE,
|
|
@@ -523,13 +523,14 @@ export const buildPageEntry = async ({ path, pagesDir, source }) => {
|
|
|
523
523
|
const isSpecialPage = isNotFoundPage || isOfflinePage
|
|
524
524
|
const isSiteRoot = routePath === '/'
|
|
525
525
|
const frontmatterIsRoot = Boolean(metadata.frontmatter?.isRoot)
|
|
526
|
-
const hidden =
|
|
527
|
-
|
|
528
|
-
: frontmatterHidden === false
|
|
526
|
+
const hidden =
|
|
527
|
+
frontmatterHidden === false
|
|
529
528
|
? false
|
|
530
529
|
: frontmatterHidden === true
|
|
531
530
|
? true
|
|
532
|
-
:
|
|
531
|
+
: isSpecialPage
|
|
532
|
+
? true
|
|
533
|
+
: frontmatterIsRoot
|
|
533
534
|
return {
|
|
534
535
|
routePath,
|
|
535
536
|
routeHref: withBase(routePath),
|
|
@@ -608,12 +609,12 @@ const collectPages = async () => {
|
|
|
608
609
|
}
|
|
609
610
|
|
|
610
611
|
const buildIndexFallback = (pages, siteName) => {
|
|
612
|
+
const isSpecialPage = (page) => page?.routePath === '/404' || page?.routePath === '/offline'
|
|
611
613
|
const visiblePages = pages
|
|
612
614
|
.filter(
|
|
613
615
|
(page) =>
|
|
614
616
|
page.routePath !== '/' &&
|
|
615
|
-
page.
|
|
616
|
-
page.routePath !== '/offline'
|
|
617
|
+
(!(isSpecialPage(page)) || page.hidden === false)
|
|
617
618
|
)
|
|
618
619
|
.sort((a, b) => a.routePath.localeCompare(b.routePath))
|
|
619
620
|
|
|
@@ -694,6 +695,11 @@ const buildNavSequence = (nodes, pagesByRoute) => {
|
|
|
694
695
|
|
|
695
696
|
export const createPagesContextFromPages = ({ pages, excludedRoutes, excludedDirs } = {}) => {
|
|
696
697
|
const pageList = Array.isArray(pages) ? pages : []
|
|
698
|
+
const pagesAll = pageList
|
|
699
|
+
const isSpecialPage = (page) => page?.routePath === '/404' || page?.routePath === '/offline'
|
|
700
|
+
const listForNavigation = pageList.filter(
|
|
701
|
+
(page) => !(isSpecialPage(page) && page.hidden !== false)
|
|
702
|
+
)
|
|
697
703
|
const routeExcludes = excludedRoutes || new Set()
|
|
698
704
|
const dirExcludes = excludedDirs || new Set()
|
|
699
705
|
const pagesByRoute = new Map()
|
|
@@ -705,7 +711,7 @@ export const createPagesContextFromPages = ({ pages, excludedRoutes, excludedDir
|
|
|
705
711
|
const getPageByRoute = (routePath, options = {}) => {
|
|
706
712
|
const { path } = options || {}
|
|
707
713
|
if (path) {
|
|
708
|
-
for (const page of
|
|
714
|
+
for (const page of pagesAll) {
|
|
709
715
|
if (page.routePath === routePath && page.path === path) {
|
|
710
716
|
return page
|
|
711
717
|
}
|
|
@@ -715,7 +721,7 @@ export const createPagesContextFromPages = ({ pages, excludedRoutes, excludedDir
|
|
|
715
721
|
}
|
|
716
722
|
const filterPagesForRoot = (rootPath) => {
|
|
717
723
|
const normalizedRoot = normalizeRoutePath(rootPath || '/')
|
|
718
|
-
return
|
|
724
|
+
return listForNavigation.filter((page) => {
|
|
719
725
|
const resolvedRoot = resolveRootPath(page.routePath, pagesByRoute)
|
|
720
726
|
if (normalizedRoot === '/') {
|
|
721
727
|
return resolvedRoot === '/' || page.routePath === resolvedRoot
|
|
@@ -783,6 +789,13 @@ export const createPagesContextFromPages = ({ pages, excludedRoutes, excludedDir
|
|
|
783
789
|
href: withBase(feedPath)
|
|
784
790
|
}
|
|
785
791
|
: { enabled: false }
|
|
792
|
+
const pwa = state.PWA_ENABLED
|
|
793
|
+
? {
|
|
794
|
+
enabled: true,
|
|
795
|
+
manifestPath: '/manifest.webmanifest',
|
|
796
|
+
manifestHref: withBase('/manifest.webmanifest')
|
|
797
|
+
}
|
|
798
|
+
: { enabled: false }
|
|
786
799
|
const site = {
|
|
787
800
|
...userSite,
|
|
788
801
|
base: siteBase,
|
|
@@ -800,11 +813,13 @@ export const createPagesContextFromPages = ({ pages, excludedRoutes, excludedDir
|
|
|
800
813
|
build: state.PAGEFIND_BUILD || null
|
|
801
814
|
},
|
|
802
815
|
feed,
|
|
816
|
+
pwa,
|
|
803
817
|
generatedAt: new Date().toISOString()
|
|
804
818
|
}
|
|
805
819
|
const excludedDirPaths = new Set(Array.from(dirExcludes).map((dir) => `/${dir}`))
|
|
806
820
|
const pagesContext = {
|
|
807
|
-
pages:
|
|
821
|
+
pages: listForNavigation,
|
|
822
|
+
pagesAll,
|
|
808
823
|
pagesByRoute,
|
|
809
824
|
getPageByRoute,
|
|
810
825
|
pagesTree: pagesTreeGlobal,
|
|
@@ -864,7 +879,7 @@ export const createPagesContextFromPages = ({ pages, excludedRoutes, excludedDir
|
|
|
864
879
|
}
|
|
865
880
|
},
|
|
866
881
|
refreshLanguages: () => {
|
|
867
|
-
pagesContext.languages = collectLanguagesFromPages(
|
|
882
|
+
pagesContext.languages = collectLanguagesFromPages(pagesAll)
|
|
868
883
|
pagesContext.getLanguageForRoute = (routePath) =>
|
|
869
884
|
resolveLanguageForRoute(pagesContext.languages, routePath)
|
|
870
885
|
},
|
package/src/pwa.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
import { existsSync } from 'fs'
|
|
23
|
+
import { readFile, writeFile } from 'fs/promises'
|
|
24
|
+
import { resolve } from 'path'
|
|
25
|
+
import fg from 'fast-glob'
|
|
26
|
+
import picomatch from 'picomatch'
|
|
27
|
+
import { normalizePath } from 'vite'
|
|
28
|
+
import { state } from './state.js'
|
|
29
|
+
import { resolveBasePrefix } from './config.js'
|
|
30
|
+
|
|
31
|
+
const DEFAULT_PRECACHE = {
|
|
32
|
+
include: ['**/*.{html,js,css,ico,png,svg,webp,jpg,jpeg,gif,woff,woff2,ttf}'],
|
|
33
|
+
exclude: ['**/*.map', '**/pagefind/**'],
|
|
34
|
+
priority: null,
|
|
35
|
+
limit: null,
|
|
36
|
+
batchSize: null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULT_INSTALL_PRIORITY_MAX = 2
|
|
40
|
+
const TEXT_EXTS = new Set([
|
|
41
|
+
'.css',
|
|
42
|
+
'.js',
|
|
43
|
+
'.mjs',
|
|
44
|
+
'.json',
|
|
45
|
+
'.txt',
|
|
46
|
+
'.xml',
|
|
47
|
+
'.webmanifest'
|
|
48
|
+
])
|
|
49
|
+
const BINARY_EXTS = new Set([
|
|
50
|
+
'.png',
|
|
51
|
+
'.jpg',
|
|
52
|
+
'.jpeg',
|
|
53
|
+
'.gif',
|
|
54
|
+
'.webp',
|
|
55
|
+
'.avif',
|
|
56
|
+
'.svg',
|
|
57
|
+
'.ico',
|
|
58
|
+
'.bmp',
|
|
59
|
+
'.woff',
|
|
60
|
+
'.woff2',
|
|
61
|
+
'.ttf',
|
|
62
|
+
'.otf',
|
|
63
|
+
'.eot',
|
|
64
|
+
'.mp3',
|
|
65
|
+
'.wav',
|
|
66
|
+
'.ogg',
|
|
67
|
+
'.mp4',
|
|
68
|
+
'.webm',
|
|
69
|
+
'.pdf'
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
const normalizeList = (value, fallback) => {
|
|
73
|
+
if (Array.isArray(value)) return value.filter(Boolean)
|
|
74
|
+
if (typeof value === 'string') return [value]
|
|
75
|
+
return fallback
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const resolvePwaOptions = (input) => {
|
|
79
|
+
if (input === true) {
|
|
80
|
+
return { enabled: true, options: { precache: { ...DEFAULT_PRECACHE } } }
|
|
81
|
+
}
|
|
82
|
+
if (input && typeof input === 'object') {
|
|
83
|
+
const precache = resolvePrecacheOptions(input.precache)
|
|
84
|
+
return { enabled: true, options: { ...input, precache } }
|
|
85
|
+
}
|
|
86
|
+
if (input === false) {
|
|
87
|
+
return { enabled: false, options: null }
|
|
88
|
+
}
|
|
89
|
+
return { enabled: false, options: null }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const resolvePrecacheOptions = (input) => {
|
|
93
|
+
const precache = input && typeof input === 'object' ? input : {}
|
|
94
|
+
return {
|
|
95
|
+
include: normalizeList(precache.include, DEFAULT_PRECACHE.include),
|
|
96
|
+
exclude: normalizeList(precache.exclude, DEFAULT_PRECACHE.exclude),
|
|
97
|
+
priority: Array.isArray(precache.priority) ? precache.priority : DEFAULT_PRECACHE.priority,
|
|
98
|
+
limit: Number.isFinite(precache.limit) && precache.limit >= 0 ? precache.limit : null,
|
|
99
|
+
batchSize: Number.isFinite(precache.batchSize) ? precache.batchSize : null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const hashMd5 = (value) => createHash('md5').update(value).digest('hex')
|
|
104
|
+
|
|
105
|
+
const joinBase = (prefix, value) => {
|
|
106
|
+
if (!prefix) return value
|
|
107
|
+
if (value.startsWith(prefix)) return value
|
|
108
|
+
return `${prefix}${value}`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const isRootOrAssets = (relativePath) => {
|
|
112
|
+
if (relativePath.startsWith('assets/')) return true
|
|
113
|
+
return !relativePath.includes('/')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const getExtension = (relativePath) => {
|
|
117
|
+
const index = relativePath.lastIndexOf('.')
|
|
118
|
+
if (index === -1) return ''
|
|
119
|
+
return relativePath.slice(index).toLowerCase()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const isTextAsset = (relativePath) => TEXT_EXTS.has(getExtension(relativePath))
|
|
123
|
+
const isBinaryAsset = (relativePath) => BINARY_EXTS.has(getExtension(relativePath))
|
|
124
|
+
|
|
125
|
+
const resolveDefaultPriority = (relativePath) => {
|
|
126
|
+
const lower = relativePath.toLowerCase()
|
|
127
|
+
if (lower === 'offline.html' || lower === '404.html') return 0
|
|
128
|
+
if (relativePath.startsWith('assets/') && isTextAsset(relativePath)) return 0
|
|
129
|
+
if (lower.endsWith('.html')) return 1
|
|
130
|
+
if (isBinaryAsset(relativePath)) return 3
|
|
131
|
+
if (isTextAsset(relativePath)) return 2
|
|
132
|
+
if (isRootOrAssets(relativePath)) return 2
|
|
133
|
+
return 3
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const resolvePriorityBuckets = (priority) => {
|
|
137
|
+
if (!Array.isArray(priority) || priority.length === 0) return null
|
|
138
|
+
const buckets = []
|
|
139
|
+
for (const entry of priority) {
|
|
140
|
+
if (typeof entry === 'function') {
|
|
141
|
+
buckets.push(entry)
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (typeof entry === 'string' || Array.isArray(entry)) {
|
|
145
|
+
const matcher = picomatch(entry)
|
|
146
|
+
buckets.push((item) => matcher(item.path))
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
if (entry && typeof entry === 'object') {
|
|
150
|
+
if (typeof entry.test === 'function') {
|
|
151
|
+
buckets.push(entry.test)
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
const match = entry.match || entry.include
|
|
155
|
+
if (typeof match === 'string' || Array.isArray(match)) {
|
|
156
|
+
const matcher = picomatch(match)
|
|
157
|
+
buckets.push((item) => matcher(item.path))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return buckets.length ? buckets : null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const resolvePriority = (relativePath, buckets) => {
|
|
165
|
+
if (!buckets || !buckets.length) return resolveDefaultPriority(relativePath)
|
|
166
|
+
const item = { path: relativePath }
|
|
167
|
+
for (let i = 0; i < buckets.length; i += 1) {
|
|
168
|
+
try {
|
|
169
|
+
if (buckets[i](item)) return i
|
|
170
|
+
} catch {}
|
|
171
|
+
}
|
|
172
|
+
return buckets.length
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const writeWebManifest = async ({ distDir, options }) => {
|
|
176
|
+
if (!options) return null
|
|
177
|
+
const manifest = {
|
|
178
|
+
name: state.SITE_NAME,
|
|
179
|
+
short_name: state.SITE_NAME,
|
|
180
|
+
...(options.manifest || {})
|
|
181
|
+
}
|
|
182
|
+
const outPath = resolve(distDir, 'manifest.webmanifest')
|
|
183
|
+
await writeFile(outPath, JSON.stringify(manifest, null, 2))
|
|
184
|
+
return outPath
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const buildPrecacheManifest = async ({ distDir, options }) => {
|
|
188
|
+
if (!options) return null
|
|
189
|
+
const precache = resolvePrecacheOptions(options.precache)
|
|
190
|
+
const basePrefix = resolveBasePrefix()
|
|
191
|
+
const buckets = resolvePriorityBuckets(precache.priority)
|
|
192
|
+
const files = (await fg(precache.include, {
|
|
193
|
+
cwd: distDir,
|
|
194
|
+
onlyFiles: true,
|
|
195
|
+
dot: false,
|
|
196
|
+
ignore: precache.exclude
|
|
197
|
+
})).filter((file) => normalizePath(file) !== 'precache-manifest.json')
|
|
198
|
+
const entries = []
|
|
199
|
+
for (const file of files.sort()) {
|
|
200
|
+
const normalized = normalizePath(file)
|
|
201
|
+
const fsPath = resolve(distDir, file)
|
|
202
|
+
if (!existsSync(fsPath)) continue
|
|
203
|
+
const content = await readFile(fsPath)
|
|
204
|
+
const revision = hashMd5(content)
|
|
205
|
+
const url = joinBase(basePrefix, `/${normalized}`)
|
|
206
|
+
const priority = resolvePriority(normalized, buckets)
|
|
207
|
+
entries.push({ url, revision, priority })
|
|
208
|
+
}
|
|
209
|
+
entries.sort((a, b) => {
|
|
210
|
+
if (a.priority !== b.priority) return a.priority - b.priority
|
|
211
|
+
return a.url.localeCompare(b.url)
|
|
212
|
+
})
|
|
213
|
+
if (precache.limit && entries.length > precache.limit) {
|
|
214
|
+
entries.length = precache.limit
|
|
215
|
+
}
|
|
216
|
+
const installCount = entries.filter((entry) => entry.priority <= DEFAULT_INSTALL_PRIORITY_MAX).length
|
|
217
|
+
const manifestBody = {
|
|
218
|
+
entries: entries.map(({ url, revision }) => ({ url, revision })),
|
|
219
|
+
installCount,
|
|
220
|
+
batchSize: precache.batchSize
|
|
221
|
+
}
|
|
222
|
+
const manifestHash = hashMd5(JSON.stringify(manifestBody))
|
|
223
|
+
const manifest = { ...manifestBody, hash: manifestHash }
|
|
224
|
+
const manifestPath = resolve(distDir, 'precache-manifest.json')
|
|
225
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2))
|
|
226
|
+
return { manifestPath, manifestHash }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const patchServiceWorker = async ({ distDir, manifestHash }) => {
|
|
230
|
+
if (!manifestHash) return false
|
|
231
|
+
const swPath = resolve(distDir, 'sw.js')
|
|
232
|
+
if (!existsSync(swPath)) return false
|
|
233
|
+
const raw = await readFile(swPath, 'utf-8')
|
|
234
|
+
if (!raw.includes('__METHANOL_MANIFEST_HASH__')) return false
|
|
235
|
+
const next = raw.replace(/__METHANOL_MANIFEST_HASH__/g, manifestHash)
|
|
236
|
+
if (next !== raw) {
|
|
237
|
+
await writeFile(swPath, next)
|
|
238
|
+
}
|
|
239
|
+
return true
|
|
240
|
+
}
|
package/src/state.js
CHANGED
|
@@ -169,19 +169,25 @@ const parser = yargs(hideBin(process.argv))
|
|
|
169
169
|
.wrap(null)
|
|
170
170
|
|
|
171
171
|
const argv = parser.parseSync()
|
|
172
|
+
const parsedCommand = argv._[0] ? String(argv._[0]) : null
|
|
173
|
+
const isServeCommand = parsedCommand === 'serve' || parsedCommand === 'preview'
|
|
174
|
+
const rawInput = argv.input || null
|
|
175
|
+
const rawOutput = argv.output || null
|
|
176
|
+
const normalizedInput = isServeCommand && rawInput && !rawOutput ? null : rawInput
|
|
177
|
+
const normalizedOutput = isServeCommand && rawInput && !rawOutput ? rawInput : rawOutput
|
|
172
178
|
|
|
173
179
|
export const cli = {
|
|
174
180
|
argv,
|
|
175
|
-
command:
|
|
181
|
+
command: parsedCommand,
|
|
176
182
|
showHelp: () => parser.showHelp(),
|
|
177
183
|
CLI_INTERMEDIATE_DIR: argv['intermediate-dir'] || null,
|
|
178
184
|
CLI_EMIT_INTERMEDIATE: Boolean(argv['emit-intermediate']),
|
|
179
185
|
CLI_HOST: argv.host ?? null,
|
|
180
186
|
CLI_PORT: typeof argv.port === 'number' ? argv.port : null,
|
|
181
|
-
CLI_PAGES_DIR:
|
|
187
|
+
CLI_PAGES_DIR: normalizedInput,
|
|
182
188
|
CLI_COMPONENTS_DIR: argv.components || null,
|
|
183
189
|
CLI_ASSETS_DIR: argv.assets || null,
|
|
184
|
-
CLI_OUTPUT_DIR:
|
|
190
|
+
CLI_OUTPUT_DIR: normalizedOutput,
|
|
185
191
|
CLI_CONFIG_PATH: argv.config || null,
|
|
186
192
|
CLI_SITE_NAME: argv['site-name'] || null,
|
|
187
193
|
CLI_OWNER: argv.owner || null,
|
|
@@ -231,10 +237,16 @@ export const state = {
|
|
|
231
237
|
USER_POST_BUILD_HOOKS: [],
|
|
232
238
|
USER_PRE_BUNDLE_HOOKS: [],
|
|
233
239
|
USER_POST_BUNDLE_HOOKS: [],
|
|
240
|
+
USER_PRE_WRITE_HOOKS: [],
|
|
241
|
+
USER_POST_WRITE_HOOKS: [],
|
|
242
|
+
USER_FINALIZE_HOOKS: [],
|
|
234
243
|
THEME_PRE_BUILD_HOOKS: [],
|
|
235
244
|
THEME_POST_BUILD_HOOKS: [],
|
|
236
245
|
THEME_PRE_BUNDLE_HOOKS: [],
|
|
237
246
|
THEME_POST_BUNDLE_HOOKS: [],
|
|
247
|
+
THEME_PRE_WRITE_HOOKS: [],
|
|
248
|
+
THEME_POST_WRITE_HOOKS: [],
|
|
249
|
+
THEME_FINALIZE_HOOKS: [],
|
|
238
250
|
STARRY_NIGHT_ENABLED: false,
|
|
239
251
|
STARRY_NIGHT_OPTIONS: null,
|
|
240
252
|
WORKER_JOBS: 0,
|
package/src/utils.js
CHANGED
package/src/vite-plugins.js
CHANGED
|
@@ -137,7 +137,7 @@ const virtualModuleMap = {
|
|
|
137
137
|
return PAGEFIND_LOADER_SCRIPT()
|
|
138
138
|
},
|
|
139
139
|
get 'pwa-inject'() {
|
|
140
|
-
if (state.PWA_ENABLED) {
|
|
140
|
+
if (state.PWA_ENABLED && state.CURRENT_MODE === 'production') {
|
|
141
141
|
return PWA_INJECT_SCRIPT()
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -145,7 +145,7 @@ const virtualModuleMap = {
|
|
|
145
145
|
},
|
|
146
146
|
get pages() {
|
|
147
147
|
const pages = state.PAGES_CONTEXT?.pages || []
|
|
148
|
-
return `export const pages = ${serializePagesIndex(pages)}\nexport default pages`
|
|
148
|
+
return `export const pages = JSON.parse(${serializePagesIndex(pages)})\nexport default pages`
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -162,6 +162,10 @@ export const methanolResolverPlugin = () => {
|
|
|
162
162
|
return {
|
|
163
163
|
name: 'methanol-resolver',
|
|
164
164
|
resolveId(id) {
|
|
165
|
+
if (id.startsWith('/.methanol/')) {
|
|
166
|
+
return resolve(state.PAGES_DIR, '.methanol', id.slice('/.methanol/'.length))
|
|
167
|
+
}
|
|
168
|
+
|
|
165
169
|
if (id === 'refui' || id.startsWith('refui/')) {
|
|
166
170
|
try {
|
|
167
171
|
return projectRequire.resolve(id)
|
|
@@ -22,7 +22,7 @@ import { cpus } from 'os'
|
|
|
22
22
|
import { Worker } from 'worker_threads'
|
|
23
23
|
import { state, cli } from '../state.js'
|
|
24
24
|
|
|
25
|
-
const BUILD_WORKER_URL = new URL('./build-worker.js', import.meta.url)
|
|
25
|
+
const BUILD_WORKER_URL = new URL('./entry-build-worker.js', import.meta.url)
|
|
26
26
|
const cliOverrides = {
|
|
27
27
|
CLI_INTERMEDIATE_DIR: cli.CLI_INTERMEDIATE_DIR,
|
|
28
28
|
CLI_EMIT_INTERMEDIATE: cli.CLI_EMIT_INTERMEDIATE,
|