methanol 0.0.21 → 0.0.23

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/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 { entry, htmlCache, pagesContext, rssContent } = await buildHtmlEntries()
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
- await runViteBuild(entry, htmlCache)
129
- await runHooks(state.THEME_POST_BUNDLE_HOOKS, buildContext)
130
- await runHooks(state.USER_POST_BUNDLE_HOOKS, buildContext)
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
  ]
@@ -647,7 +679,7 @@ export const renderPageContent = async ({ routePath, path, components, pagesCont
647
679
  setReframeHydrationEnabled(false)
648
680
  state.THEME_ENV?.setHydrationEnabled?.(false)
649
681
  const mdxComponent = pageMeta.mdxComponent
650
- const PageContent = () => mdxComponent()
682
+ const PageContent = () => mdxComponent({ components })
651
683
 
652
684
  try {
653
685
  const renderResult = await new Promise((resolve, reject) => {
@@ -18,7 +18,6 @@
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
23
  const OMIT_KEYS = new Set([
@@ -47,5 +46,5 @@ export const serializePagesIndex = (pages) => {
47
46
  const list = Array.isArray(pages)
48
47
  ? pages.filter((page) => !page?.hidden).map(sanitizePage)
49
48
  : []
50
- return JSON5.stringify(list)
49
+ return JSON.stringify(JSON.stringify(list))
51
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 = isSpecialPage
527
- ? true
528
- : frontmatterHidden === false
526
+ const hidden =
527
+ frontmatterHidden === false
529
528
  ? false
530
529
  : frontmatterHidden === true
531
530
  ? true
532
- : frontmatterIsRoot
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.routePath !== '/404' &&
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 pages) {
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 pages.filter((page) => {
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: pageList,
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(pages)
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
@@ -237,10 +237,16 @@ export const state = {
237
237
  USER_POST_BUILD_HOOKS: [],
238
238
  USER_PRE_BUNDLE_HOOKS: [],
239
239
  USER_POST_BUNDLE_HOOKS: [],
240
+ USER_PRE_WRITE_HOOKS: [],
241
+ USER_POST_WRITE_HOOKS: [],
242
+ USER_FINALIZE_HOOKS: [],
240
243
  THEME_PRE_BUILD_HOOKS: [],
241
244
  THEME_POST_BUILD_HOOKS: [],
242
245
  THEME_PRE_BUNDLE_HOOKS: [],
243
246
  THEME_POST_BUNDLE_HOOKS: [],
247
+ THEME_PRE_WRITE_HOOKS: [],
248
+ THEME_POST_WRITE_HOOKS: [],
249
+ THEME_FINALIZE_HOOKS: [],
244
250
  STARRY_NIGHT_ENABLED: false,
245
251
  STARRY_NIGHT_OPTIONS: null,
246
252
  WORKER_JOBS: 0,
package/src/utils.js CHANGED
@@ -22,7 +22,7 @@ import NullProtoObj from 'null-prototype-object'
22
22
 
23
23
  export const cached = (fn) => {
24
24
  let cache = null
25
- return () => (cache ?? (cache = fn()))
25
+ return (...args) => (cache ?? (cache = fn(...args)))
26
26
  }
27
27
 
28
28
  export const cachedStr = (fn) => {
@@ -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,