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 CHANGED
@@ -20,5 +20,4 @@
20
20
  * under the License.
21
21
  */
22
22
 
23
- import '../src/register-loader.js'
24
- import('../src/main.js')
23
+ import('../src/entry.js')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "methanol",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "Static site generator powered by rEFui and MDX",
5
5
  "main": "./index.js",
6
6
  "type": "module",
@@ -29,21 +29,23 @@
29
29
  "@mdx-js/mdx": "^3.1.1",
30
30
  "@sindresorhus/fnv1a": "^3.1.0",
31
31
  "@stefanprobst/rehype-extract-toc": "^3.0.0",
32
- "@wooorm/starry-night": "^3.8.0",
32
+ "@wooorm/starry-night": "^3.9.0",
33
33
  "chokidar": "^5.0.0",
34
34
  "esbuild": "^0.27.2",
35
+ "fast-glob": "^3.3.3",
35
36
  "gray-matter": "^4.0.3",
36
37
  "hast-util-is-element": "^3.0.0",
38
+ "htmlparser2": "^10.1.0",
37
39
  "json5": "^2.2.3",
38
40
  "null-prototype-object": "^1.2.5",
41
+ "picomatch": "^4.0.3",
39
42
  "refui": "^0.17.1",
40
43
  "refurbish": "^0.1.8",
41
44
  "rehype-slug": "^6.0.0",
42
45
  "rehype-starry-night": "^2.2.0",
43
46
  "remark-gfm": "^4.0.1",
44
- "unist-util-visit": "^5.0.0",
47
+ "unist-util-visit": "^5.1.0",
45
48
  "vite": "^7.3.1",
46
- "vite-plugin-pwa": "^1.2.0",
47
49
  "workbox-core": "^7.4.0",
48
50
  "workbox-routing": "^7.4.0",
49
51
  "workbox-strategies": "^7.4.0",
package/src/base.js ADDED
@@ -0,0 +1,36 @@
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
+ export const normalizeBasePrefix = (value) => {
22
+ if (!value || value === '/' || value === './') return ''
23
+ if (typeof value !== 'string') return ''
24
+ let base = value.trim()
25
+ if (!base || base === '/' || base === './') return ''
26
+ if (base.startsWith('http://') || base.startsWith('https://')) {
27
+ try {
28
+ base = new URL(base).pathname
29
+ } catch {
30
+ return ''
31
+ }
32
+ }
33
+ if (!base.startsWith('/')) return ''
34
+ if (base.endsWith('/')) base = base.slice(0, -1)
35
+ return base
36
+ }
@@ -18,20 +18,22 @@
18
18
  * under the License.
19
19
  */
20
20
 
21
+ import { existsSync } from 'fs'
21
22
  import { writeFile, mkdir, rm, readFile, readdir, stat } from 'fs/promises'
22
- import { resolve, dirname, join } from 'path'
23
+ import { resolve, dirname, join, basename } from 'path'
24
+ import { createHash } from 'crypto'
23
25
  import { fileURLToPath } from 'url'
24
26
  import { build as viteBuild, mergeConfig, normalizePath } from 'vite'
25
- import { VitePWA } from 'vite-plugin-pwa'
26
27
  import { state, cli } from './state.js'
27
28
  import { resolveUserViteConfig } from './config.js'
28
29
  import { buildPagesContext } from './pages.js'
29
30
  import { selectFeedPages } from './feed.js'
30
31
  import { buildComponentRegistry } from './components.js'
31
32
  import { createBuildWorkers, runWorkerStage, terminateWorkers } from './workers/build-pool.js'
32
- import { methanolVirtualHtmlPlugin, methanolResolverPlugin } from './vite-plugins.js'
33
+ import { methanolResolverPlugin } from './vite-plugins.js'
33
34
  import { createStageLogger } from './stage-logger.js'
34
35
  import { preparePublicAssets } from './public-assets.js'
36
+ export { scanHtmlEntries, rewriteHtmlEntries } from './html/build-html.js'
35
37
 
36
38
  const __filename = fileURLToPath(import.meta.url)
37
39
  const __dirname = dirname(__filename)
@@ -40,11 +42,29 @@ const ensureDir = async (dir) => {
40
42
  await mkdir(dir, { recursive: true })
41
43
  }
42
44
 
45
+ const ensureSwEntry = async () => {
46
+ const entriesDir = resolve(resolveMethanolDir(), ENTRY_DIR)
47
+ await ensureDir(entriesDir)
48
+ const swEntryPath = resolve(entriesDir, 'sw-entry.js')
49
+ const swSource = normalizePath(resolve(__dirname, 'client', 'sw.js'))
50
+ await writeFile(swEntryPath, `import ${JSON.stringify(swSource)}\n`)
51
+ return swEntryPath
52
+ }
53
+
54
+ const INLINE_DIR = 'inline'
55
+ const ENTRY_DIR = 'entries'
56
+ const WRITE_CONCURRENCY_LIMIT = 32
57
+
58
+ const resolveMethanolDir = () => resolve(state.PAGES_DIR, '.methanol')
59
+
43
60
  const isHtmlFile = (name) => name.endsWith('.html')
44
61
  const collectHtmlFiles = async (dir, basePath = '') => {
45
62
  const entries = await readdir(dir)
46
63
  const files = []
47
64
  for (const entry of entries.sort()) {
65
+ if (entry.startsWith('.') || entry.startsWith('_')) {
66
+ continue
67
+ }
48
68
  const fullPath = resolve(dir, entry)
49
69
  const stats = await stat(fullPath)
50
70
  if (stats.isDirectory()) {
@@ -65,11 +85,18 @@ const collectHtmlFiles = async (dir, basePath = '') => {
65
85
  return files
66
86
  }
67
87
 
68
- export const buildHtmlEntries = async () => {
88
+ const hashKey = (value) =>
89
+ createHash('md5').update(value).digest('hex')
90
+
91
+ const makeInputKey = (prefix, value) => `${prefix}-${hashKey(value).slice(0, 12)}`
92
+
93
+ export const buildHtmlEntries = async (options = {}) => {
94
+ const keepWorkers = Boolean(options.keepWorkers)
69
95
  await resolveUserViteConfig('build') // Prepare `base`
70
- if (state.INTERMEDIATE_DIR) {
71
- await rm(state.INTERMEDIATE_DIR, { recursive: true, force: true })
72
- await ensureDir(state.INTERMEDIATE_DIR)
96
+ const htmlStageDir = state.INTERMEDIATE_DIR || resolve(state.PAGES_DIR, '.methanol/html')
97
+ if (htmlStageDir) {
98
+ await rm(htmlStageDir, { recursive: true, force: true })
99
+ await ensureDir(htmlStageDir)
73
100
  }
74
101
 
75
102
  const logEnabled = state.CURRENT_MODE === 'production' && cli.command === 'build' && !cli.CLI_VERBOSE
@@ -84,8 +111,13 @@ export const buildHtmlEntries = async () => {
84
111
  }
85
112
  await buildComponentRegistry()
86
113
  const pagesContext = await buildPagesContext({ compileAll: false })
87
- const entry = {}
88
- const htmlCache = new Map()
114
+ const htmlEntries = []
115
+ const htmlEntryNames = new Set()
116
+ const inlineDir = resolve(resolveMethanolDir(), INLINE_DIR)
117
+ await rm(inlineDir, { recursive: true, force: true })
118
+ await ensureDir(inlineDir)
119
+ const renderScans = new Map()
120
+ const renderScansById = new Map()
89
121
  const resolveOutputName = (page) => {
90
122
  if (page.routePath === '/') return 'index'
91
123
  if (page.isIndex && page.dir) {
@@ -94,13 +126,16 @@ export const buildHtmlEntries = async () => {
94
126
  return page.routePath.slice(1)
95
127
  }
96
128
 
97
- const pages = pagesContext.pages || []
129
+ const pages = pagesContext.pagesAll || pagesContext.pages || []
98
130
  const totalPages = pages.length
99
131
  const { workers, assignments } = createBuildWorkers(totalPages)
132
+ const writeConcurrency = Math.max(1, Math.floor(WRITE_CONCURRENCY_LIMIT / workers.length))
100
133
  const excludedRoutes = Array.from(pagesContext.excludedRoutes || [])
101
134
  const excludedDirs = Array.from(pagesContext.excludedDirs || [])
102
135
  const rssContent = new Map()
103
- const intermediateOutputs = []
136
+ let feedIds = []
137
+ let feedAssignments = null
138
+ let completedRun = false
104
139
  try {
105
140
  await runWorkerStage({
106
141
  workers,
@@ -168,6 +203,18 @@ export const buildHtmlEntries = async () => {
168
203
  }
169
204
  }))
170
205
  })
206
+ if (state.RSS_ENABLED) {
207
+ const feedPages = selectFeedPages(pages, state.RSS_OPTIONS || {})
208
+ const pageIndex = new Map(pages.map((page, index) => [page, index]))
209
+ feedIds = feedPages.map((page) => pageIndex.get(page)).filter((id) => id != null)
210
+ if (feedIds.length) {
211
+ feedAssignments = Array.from({ length: workers.length }, () => [])
212
+ for (const id of feedIds) {
213
+ feedAssignments[id % workers.length].push(id)
214
+ }
215
+ }
216
+ }
217
+
171
218
  const renderToken = stageLogger.start('Rendering pages')
172
219
  completed = 0
173
220
  await runWorkerStage({
@@ -178,7 +225,10 @@ export const buildHtmlEntries = async () => {
178
225
  message: {
179
226
  type: 'render',
180
227
  stage: 'render',
181
- ids: assignments[index]
228
+ ids: assignments[index],
229
+ feedIds: feedAssignments ? feedAssignments[index] : [],
230
+ htmlStageDir,
231
+ writeConcurrency
182
232
  }
183
233
  })),
184
234
  onProgress: (count) => {
@@ -190,74 +240,36 @@ export const buildHtmlEntries = async () => {
190
240
  if (!result || typeof result.id !== 'number') return
191
241
  const page = pages[result.id]
192
242
  if (!page) return
193
- const html = result.html
243
+ if (result.scan) {
244
+ renderScansById.set(result.id, result.scan)
245
+ }
194
246
  const name = resolveOutputName(page)
195
- const id = normalizePath(resolve(state.VIRTUAL_HTML_OUTPUT_ROOT, `${name}.html`))
196
- entry[name] = id
197
- htmlCache.set(id, html)
198
- if (state.INTERMEDIATE_DIR) {
199
- intermediateOutputs.push({ name, id })
247
+ const outPath = htmlStageDir
248
+ ? (result.stagePath || resolve(htmlStageDir, `${name}.html`))
249
+ : `${name}.html`
250
+ htmlEntryNames.add(name)
251
+ htmlEntries.push({ name, routePath: page.routePath, stagePath: outPath, source: 'rendered' })
252
+ if (result.feedContent != null) {
253
+ const key = page.path || page.routePath
254
+ if (key) {
255
+ rssContent.set(key, result.feedContent || '')
256
+ }
200
257
  }
201
258
  }
202
259
  })
203
260
  stageLogger.end(renderToken)
204
261
 
205
- if (state.RSS_ENABLED) {
206
- const feedPages = selectFeedPages(pages, state.RSS_OPTIONS || {})
207
- const feedIds = []
208
- const pageIndex = new Map(pages.map((page, index) => [page, index]))
209
- for (const page of feedPages) {
210
- const id = pageIndex.get(page)
211
- if (id != null) {
212
- feedIds.push(id)
213
- }
214
- }
215
- if (feedIds.length) {
216
- const rssToken = stageLogger.start('Rendering feed')
217
- completed = 0
218
- const rssAssignments = Array.from({ length: workers.length }, () => [])
219
- for (let i = 0; i < feedIds.length; i += 1) {
220
- rssAssignments[i % workers.length].push(feedIds[i])
221
- }
222
- await runWorkerStage({
223
- workers,
224
- stage: 'rss',
225
- messages: workers.map((worker, index) => ({
226
- worker,
227
- message: {
228
- type: 'rss',
229
- stage: 'rss',
230
- ids: rssAssignments[index]
231
- }
232
- })),
233
- onProgress: (count) => {
234
- if (!logEnabled) return
235
- completed = count
236
- stageLogger.update(rssToken, `Rendering feed [${completed}/${feedIds.length}]`)
237
- },
238
- onResult: (result) => {
239
- if (!result || typeof result.id !== 'number') return
240
- const page = pages[result.id]
241
- if (!page) return
242
- const key = page.path || page.routePath
243
- if (key) {
244
- rssContent.set(key, result.content || '')
245
- }
246
- }
247
- })
248
- stageLogger.end(rssToken)
249
- }
262
+ for (const [id, scan] of renderScansById.entries()) {
263
+ const page = pages[id]
264
+ if (!page || !scan) continue
265
+ const name = resolveOutputName(page)
266
+ const stagePath = htmlStageDir ? resolve(htmlStageDir, `${name}.html`) : `${name}.html`
267
+ renderScans.set(stagePath, scan)
250
268
  }
269
+ completedRun = true
251
270
  } finally {
252
- await terminateWorkers(workers)
253
- }
254
- if (state.INTERMEDIATE_DIR) {
255
- for (const output of intermediateOutputs) {
256
- const html = htmlCache.get(output.id)
257
- if (typeof html !== 'string') continue
258
- const outPath = resolve(state.INTERMEDIATE_DIR, `${output.name}.html`)
259
- await ensureDir(dirname(outPath))
260
- await writeFile(outPath, html)
271
+ if (!keepWorkers || !completedRun) {
272
+ await terminateWorkers(workers)
261
273
  }
262
274
  }
263
275
 
@@ -281,27 +293,133 @@ export const buildHtmlEntries = async () => {
281
293
  }
282
294
  const name = file.relativePath.replace(/\.html$/, '')
283
295
  const outputName = name === 'index' ? 'index' : name
284
- if (entry[outputName]) {
296
+ if (htmlEntryNames.has(outputName)) {
285
297
  continue
286
298
  }
287
299
  const html = await readFile(file.fullPath, 'utf-8')
288
- const id = normalizePath(resolve(state.VIRTUAL_HTML_OUTPUT_ROOT, `${outputName}.html`))
289
- entry[outputName] = id
290
- htmlCache.set(id, html)
291
- if (state.INTERMEDIATE_DIR) {
292
- const outPath = resolve(state.INTERMEDIATE_DIR, file.relativePath)
300
+ const outPath = htmlStageDir ? resolve(htmlStageDir, file.relativePath) : null
301
+ if (outPath) {
293
302
  await ensureDir(dirname(outPath))
294
303
  await writeFile(outPath, html)
295
304
  }
305
+ htmlEntryNames.add(outputName)
306
+ htmlEntries.push({
307
+ name: outputName,
308
+ routePath: outputName === 'index'
309
+ ? '/'
310
+ : outputName.endsWith('/index')
311
+ ? `/${outputName.slice(0, -'/index'.length)}/`
312
+ : `/${outputName}`,
313
+ stagePath: outPath,
314
+ inputPath: file.fullPath,
315
+ source: 'static'
316
+ })
296
317
  }
297
318
 
298
- return { entry, htmlCache, pagesContext, rssContent }
319
+ return {
320
+ htmlEntries,
321
+ htmlStageDir,
322
+ pagesContext,
323
+ rssContent,
324
+ renderScans,
325
+ renderScansById,
326
+ workers: keepWorkers ? workers : null,
327
+ assignments: keepWorkers ? assignments : null
328
+ }
329
+ }
330
+
331
+ export const rewriteHtmlEntriesInWorkers = async ({
332
+ pages = [],
333
+ htmlStageDir,
334
+ manifest,
335
+ scanResult,
336
+ renderScansById,
337
+ onProgress,
338
+ workers: existingWorkers = null,
339
+ assignments: existingAssignments = null
340
+ }) => {
341
+ const totalPages = pages.length
342
+ if (!totalPages) return
343
+ const useExisting = Array.isArray(existingWorkers) && Array.isArray(existingAssignments)
344
+ const { workers, assignments } = useExisting
345
+ ? { workers: existingWorkers, assignments: existingAssignments }
346
+ : createBuildWorkers(totalPages)
347
+ try {
348
+ if (!useExisting) {
349
+ await runWorkerStage({
350
+ workers,
351
+ stage: 'setPagesLite',
352
+ messages: workers.map((worker) => ({
353
+ worker,
354
+ message: {
355
+ type: 'setPagesLite',
356
+ stage: 'setPagesLite',
357
+ pages
358
+ }
359
+ }))
360
+ })
361
+ }
362
+
363
+ const entryModules = Array.isArray(scanResult?.entryModules) ? scanResult.entryModules : []
364
+ const commonScripts = Array.isArray(scanResult?.commonScripts) ? scanResult.commonScripts : []
365
+ const commonEntry = scanResult?.commonScriptEntry?.manifestKey
366
+ ? manifest?.[scanResult.commonScriptEntry.manifestKey] || manifest?.[`/${scanResult.commonScriptEntry.manifestKey}`]
367
+ : null
368
+
369
+ await runWorkerStage({
370
+ workers,
371
+ stage: 'rewrite',
372
+ messages: workers.map((worker, index) => {
373
+ const ids = assignments[index] || []
374
+ const scans = {}
375
+ if (renderScansById) {
376
+ for (const id of ids) {
377
+ const scan = renderScansById.get(id)
378
+ if (scan) scans[id] = scan
379
+ }
380
+ }
381
+ return {
382
+ worker,
383
+ message: {
384
+ type: 'rewrite',
385
+ stage: 'rewrite',
386
+ ids,
387
+ htmlStageDir,
388
+ manifest,
389
+ entryModules,
390
+ commonScripts,
391
+ commonEntry,
392
+ scans
393
+ }
394
+ }
395
+ }),
396
+ onProgress: (count) => {
397
+ if (typeof onProgress === 'function') {
398
+ onProgress(count, totalPages)
399
+ }
400
+ }
401
+ })
402
+ } finally {
403
+ if (!useExisting) {
404
+ await terminateWorkers(workers)
405
+ }
406
+ }
299
407
  }
300
408
 
301
- export const runViteBuild = async (entry, htmlCache) => {
409
+ export const runViteBuild = async (inputs) => {
302
410
  const logEnabled = state.CURRENT_MODE === 'production' && cli.command === 'build' && !cli.CLI_VERBOSE
303
411
  const stageLogger = createStageLogger(logEnabled)
304
412
  const token = stageLogger.start('Building bundle')
413
+ const rewriteOptions = inputs.rewrite || null
414
+ const preWrite = typeof inputs.preWrite === 'function' ? inputs.preWrite : null
415
+ const postWrite = typeof inputs.postWrite === 'function' ? inputs.postWrite : null
416
+ let manifestData = null
417
+ let bundleEnded = false
418
+ const endBundleStage = () => {
419
+ if (bundleEnded) return
420
+ bundleEnded = true
421
+ stageLogger.end(token)
422
+ }
305
423
 
306
424
  if (state.STATIC_DIR !== false && state.MERGED_ASSETS_DIR) {
307
425
  await preparePublicAssets({
@@ -311,20 +429,53 @@ export const runViteBuild = async (entry, htmlCache) => {
311
429
  })
312
430
  }
313
431
  const copyPublicDirEnabled = state.STATIC_DIR !== false
314
- const manifestConfig = state.PWA_OPTIONS?.manifest || {}
315
- const resolvedManifest = { name: state.SITE_NAME, short_name: state.SITE_NAME, ...manifestConfig }
432
+ const entryModules = Array.isArray(inputs.entryModules) ? inputs.entryModules : []
433
+ const entryInputs = entryModules
434
+ .filter((entry) => entry && entry.kind !== 'style')
435
+ .map((entry) => entry.fsPath)
436
+ .filter(Boolean)
437
+ .sort()
438
+ const htmlEntries = Array.isArray(inputs.htmlEntries) ? inputs.htmlEntries : []
439
+ const htmlInputs = htmlEntries
440
+ .filter((entry) => entry?.source === 'static' && entry.inputPath)
441
+ .map((entry) => entry.inputPath)
442
+ .sort()
443
+ if (cli.CLI_VERBOSE && entryInputs.length === 0) {
444
+ console.log('Vite pipeline: no wrapper entries detected (no module scripts/stylesheets found)')
445
+ }
446
+ const rollupInput = {}
447
+ for (const entryPath of entryInputs) {
448
+ const normalized = normalizePath(entryPath)
449
+ rollupInput[makeInputKey('chunk', normalized)] = normalized
450
+ }
451
+ for (const htmlPath of htmlInputs) {
452
+ const normalized = normalizePath(htmlPath)
453
+ rollupInput[makeInputKey('html', normalized)] = normalized
454
+ }
455
+ let swEntryPath = null
456
+ if (state.PWA_ENABLED) {
457
+ swEntryPath = await ensureSwEntry()
458
+ if (swEntryPath) {
459
+ const normalized = normalizePath(swEntryPath)
460
+ rollupInput['sw'] = normalized
461
+ }
462
+ }
316
463
  const baseConfig = {
317
464
  configFile: false,
318
465
  root: state.PAGES_DIR,
319
- appType: 'mpa',
466
+ appType: 'custom',
320
467
  publicDir: state.STATIC_DIR === false ? false : state.STATIC_DIR,
321
468
  logLevel: cli.CLI_VERBOSE ? 'info' : 'silent',
322
469
  build: {
323
470
  outDir: state.DIST_DIR,
324
471
  emptyOutDir: true,
325
472
  rollupOptions: {
326
- input: entry
473
+ input: rollupInput,
474
+ output: {
475
+ entryFileNames: (chunk) => (chunk.name === 'sw' ? 'sw.js' : 'assets/[name]-[hash].js')
476
+ }
327
477
  },
478
+ manifest: true,
328
479
  copyPublicDir: copyPublicDirEnabled,
329
480
  minify: true
330
481
  },
@@ -335,42 +486,115 @@ export const runViteBuild = async (entry, htmlCache) => {
335
486
  resolve: {
336
487
  dedupe: ['refui', 'methanol']
337
488
  },
338
- plugins: [
339
- methanolVirtualHtmlPlugin(htmlCache),
340
- methanolResolverPlugin(),
341
- state.PWA_ENABLED
342
- ? VitePWA({
343
- injectRegister: 'auto',
344
- registerType: 'autoUpdate',
345
- strategies: 'injectManifest',
346
- srcDir: resolve(__dirname, 'client'),
347
- filename: 'sw.js',
348
- manifest: resolvedManifest,
349
- injectManifest: {
350
- globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
351
- ...(state.PWA_OPTIONS?.injectManifest || {})
352
- }
353
- })
354
- : null
355
- ]
489
+ plugins: [methanolResolverPlugin()]
356
490
  }
357
491
  const userConfig = await resolveUserViteConfig('build')
358
492
  const finalConfig = userConfig ? mergeConfig(baseConfig, userConfig) : baseConfig
359
493
 
360
- const originalLog = console.log
361
- const originalWarn = console.warn
362
- if (!cli.CLI_VERBOSE) {
363
- console.log = () => {}
364
- console.warn = () => {}
494
+ // Keep the pipeline deterministic: do not let user configs override the build root/output/inputs.
495
+ finalConfig.root = state.PAGES_DIR
496
+ finalConfig.appType = 'custom'
497
+ finalConfig.publicDir = state.STATIC_DIR === false ? false : state.STATIC_DIR
498
+ finalConfig.build = {
499
+ ...(finalConfig.build || {}),
500
+ outDir: state.DIST_DIR,
501
+ emptyOutDir: true,
502
+ manifest: finalConfig.build?.manifest === undefined ? true : finalConfig.build.manifest,
503
+ copyPublicDir: copyPublicDirEnabled,
504
+ rollupOptions: {
505
+ ...((finalConfig.build && finalConfig.build.rollupOptions) || {}),
506
+ input: rollupInput,
507
+ output: (() => {
508
+ const existing = finalConfig.build?.rollupOptions?.output
509
+ const outputConfig = Array.isArray(existing) ? existing[0] || {} : existing || {}
510
+ return {
511
+ ...outputConfig,
512
+ entryFileNames: (chunk) => (chunk.name === 'sw' ? 'sw.js' : 'assets/[name]-[hash].js')
513
+ }
514
+ })()
515
+ }
516
+ }
517
+
518
+ const manifestFileName = typeof finalConfig.build?.manifest === 'string'
519
+ ? finalConfig.build.manifest
520
+ : '.vite/manifest.json'
521
+ const manifestPath = resolve(state.DIST_DIR, manifestFileName.replace(/^\//, ''))
522
+ const manifestEnabled = finalConfig.build?.manifest !== false
523
+ let rewriteDone = false
524
+ let preWriteDone = false
525
+ let postWriteDone = false
526
+ const loadManifest = async () => {
527
+ if (!manifestEnabled) return null
528
+ if (!existsSync(manifestPath)) return null
529
+ const raw = await readFile(manifestPath, 'utf-8')
530
+ return JSON.parse(raw)
531
+ }
532
+ const runPreWrite = async () => {
533
+ if (rewriteOptions) {
534
+ endBundleStage()
535
+ }
536
+ if (preWrite) {
537
+ await preWrite({ manifest: manifestData })
538
+ }
539
+ }
540
+ const runPostWrite = async () => {
541
+ if (postWrite) {
542
+ await postWrite({ manifest: manifestData })
543
+ }
544
+ }
545
+ const runRewrite = async () => {
546
+ if (!rewriteOptions || rewriteDone || !manifestData) return
547
+ const rewriteToken = logEnabled ? stageLogger.start('Rewriting HTML') : null
548
+ try {
549
+ await rewriteHtmlEntriesInWorkers({
550
+ ...rewriteOptions,
551
+ manifest: manifestData,
552
+ onProgress: (done, total) => {
553
+ if (!rewriteToken) return
554
+ stageLogger.update(rewriteToken, `Rewriting HTML [${done}/${total}]`)
555
+ }
556
+ })
557
+ } finally {
558
+ rewriteDone = true
559
+ if (rewriteToken) {
560
+ stageLogger.end(rewriteToken)
561
+ }
562
+ }
563
+ }
564
+
565
+ const postBundlePlugin = {
566
+ name: 'methanol:post-bundle',
567
+ apply: 'build',
568
+ enforce: 'post',
569
+ async writeBundle() {
570
+ manifestData = await loadManifest()
571
+ await runPreWrite()
572
+ await runRewrite()
573
+ await runPostWrite()
574
+ }
365
575
  }
366
576
 
577
+ finalConfig.plugins = [...(finalConfig.plugins || []), postBundlePlugin]
578
+
579
+ await viteBuild(finalConfig)
580
+ endBundleStage()
581
+ const methanolManifestDir = resolve(state.PAGES_DIR, '.methanol')
582
+ const methanolManifestPath = resolve(methanolManifestDir, 'manifest.json')
367
583
  try {
368
- await viteBuild(finalConfig)
369
- } finally {
370
- if (!cli.CLI_VERBOSE) {
371
- console.log = originalLog
372
- console.warn = originalWarn
584
+ const parsed = manifestData || (manifestEnabled ? JSON.parse(await readFile(manifestPath, 'utf-8')) : null)
585
+ if (!parsed) return {}
586
+ await ensureDir(methanolManifestDir)
587
+ await writeFile(methanolManifestPath, JSON.stringify(parsed, null, 2))
588
+ await rm(manifestPath, { force: true })
589
+ const manifestDir = dirname(manifestPath)
590
+ if (basename(manifestDir) === '.vite') {
591
+ await rm(manifestDir, { recursive: true, force: true })
592
+ }
593
+ return parsed
594
+ } catch (error) {
595
+ if (cli.CLI_VERBOSE) {
596
+ console.log(`Vite pipeline: failed to read manifest at ${manifestPath}`)
373
597
  }
598
+ return {}
374
599
  }
375
- stageLogger.end(token)
376
600
  }