a
File size: 4,685 Bytes
f4d58d4
bc616c9
f4d58d4
bc616c9
f4d58d4
 
 
 
333faae
 
 
f4d58d4
76b8a0b
f4d58d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc616c9
f4d58d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3476d00
 
 
 
f4d58d4
3476d00
 
f4d58d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3476d00
f4d58d4
3476d00
 
 
bc616c9
3476d00
 
 
 
 
 
 
 
 
bc616c9
3476d00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4d58d4
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import { join, extname, basename } from 'node:path'
import { existsSync, statSync, writeFileSync, unlinkSync } from 'node:fs'
import type { Plugin } from 'vite'
import { IMAGE_EXTS, IMAGE_DIRS, listImages } from './vite-plugin-image-utils'
const WEBP_QUALITY = 80

/** Max widths at 2× retina for each image category on the landing page. */
const MAX_WIDTHS: Record<string, number> = {
  'size-backdrop-': 720, // displayed at 360px
  'size-poster-': 400,   // displayed at 200px
  'size-logo-': 400,     // displayed at 200px
  'backdrop-': 720,  // displayed at 360px
  'episode-': 520,   // displayed at 260px
  'logo-': 400,      // displayed at 200px
  'style-': 400,     // displayed at 200px
  'label-': 400,     // displayed at 200px
  'pos-': 320,       // displayed at 160px
}
const DEFAULT_MAX_WIDTH = 400 // posters displayed at 160–200px

function maxWidthFor(filename: string): number {
  for (const [prefix, width] of Object.entries(MAX_WIDTHS)) {
    if (filename.startsWith(prefix)) return width
  }
  return DEFAULT_MAX_WIDTH
}

/** Resize and convert a source image to WebP. */
async function toWebP(sourcePath: string): Promise<Buffer> {
  const sharp = (await import('sharp')).default
  return sharp(sourcePath)
    .resize({ width: maxWidthFor(basename(sourcePath)), withoutEnlargement: true })
    .webp({ quality: WEBP_QUALITY })
    .toBuffer()
}

/** Find the source jpg/png file for a .webp request. */
function findSource(dir: string, webpName: string): string | null {
  const base = webpName.replace(/\.webp$/, '')
  for (const ext of ['.jpg', '.jpeg', '.png']) {
    const candidate = join(dir, base + ext)
    try {
      statSync(candidate)
      return candidate
    } catch { /* not found */ }
  }
  return null
}

/**
 * Vite plugin that converts example images to WebP.
 * - Build: resizes, converts to WebP, removes originals in output.
 * - Dev: serves on-the-fly WebP conversion via middleware.
 */
export default function optimizeExamplesPlugin(): Plugin {
  let outDir: string
  let publicDir: string

  return {
    name: 'optimize-examples',

    configResolved(config) {
      outDir = config.build.outDir
      publicDir = config.publicDir
    },

    // Dev: serve .webp requests by converting source jpg/png on the fly (with in-memory cache)
    configureServer(server) {
      const cache = new Map<string, { mtime: number; buf: Buffer }>()

      server.middlewares.use(async (req, res, next) => {
        if (!req.url?.endsWith('.webp')) return next()

        const dir = IMAGE_DIRS.find(d => req.url!.startsWith(`/${d}/`))
        if (!dir) return next()

        const webpName = req.url!.slice(`/${dir}/`.length)
        const source = findSource(join(publicDir, dir), webpName)
        if (!source) return next()

        try {
          const mtime = statSync(source).mtimeMs
          const cached = cache.get(webpName)

          let buf: Buffer
          if (cached && cached.mtime === mtime) {
            buf = cached.buf
          } else {
            buf = await toWebP(source)
            cache.set(webpName, { mtime, buf })
          }

          res.setHeader('Content-Type', 'image/webp')
          res.setHeader('Cache-Control', 'no-cache')
          res.end(buf)
        } catch {
          next()
        }
      })
    },

    // Build: convert all images to WebP, resize, and remove originals
    async closeBundle() {
      for (const dir of IMAGE_DIRS) {
        const dirOut = join(outDir, dir)
        if (!existsSync(dirOut)) continue
        const files = listImages(dirOut)
        if (files.length === 0) continue

        let totalBefore = 0
        let totalAfter = 0

        await Promise.all(files.map(async (file) => {
          const filePath = join(dirOut, file)
          const origSize = statSync(filePath).size

          const webpName = file.replace(/\.[^.]+$/, '.webp')
          const webpPath = join(dirOut, webpName)

          const buf = await toWebP(filePath)

          writeFileSync(webpPath, buf)
          unlinkSync(filePath)

          totalBefore += origSize
          totalAfter += buf.length
          const pct = ((1 - buf.length / origSize) * 100).toFixed(0)
          console.log(`  ${file} → ${webpName}: ${formatBytes(origSize)} → ${formatBytes(buf.length)} (−${pct}%)`)
        }))

        if (totalBefore > 0) {
          const pct = ((1 - totalAfter / totalBefore) * 100).toFixed(0)
          console.log(`  ${dir} total: ${formatBytes(totalBefore)} → ${formatBytes(totalAfter)} (−${pct}%)`)
        }
      }
    },
  }
}

function formatBytes(bytes: number): string {
  if (bytes < 1024) return `${bytes}B`
  return `${(bytes / 1024).toFixed(0)}KB`
}