pugkit 1.4.0 → 1.6.0

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/cli/build.mjs CHANGED
@@ -2,12 +2,12 @@ import { createBuilder } from '../index.mjs'
2
2
  import { logger } from './logger.mjs'
3
3
 
4
4
  export async function build(options = {}) {
5
- const { root = process.cwd() } = options
5
+ const { root = process.cwd(), siteUrl } = options
6
6
 
7
7
  logger.info('pugkit', 'building...')
8
8
 
9
9
  const startTime = Date.now()
10
- const builder = await createBuilder(root, 'production')
10
+ const builder = await createBuilder(root, 'production', { siteUrl })
11
11
  await builder.build()
12
12
 
13
13
  const elapsed = Date.now() - startTime
package/cli/index.mjs CHANGED
@@ -33,14 +33,17 @@ cli
33
33
  }
34
34
  })
35
35
 
36
- cli.command('build [root]', 'Production build').action(async root => {
37
- try {
38
- await build({ root: root || process.cwd() })
39
- } catch (err) {
40
- console.error(err)
41
- process.exit(1)
42
- }
43
- })
36
+ cli
37
+ .command('build [root]', 'Production build')
38
+ .option('--site-url <url>', 'Override siteUrl in config')
39
+ .action(async (root, options) => {
40
+ try {
41
+ await build({ root: root || process.cwd(), siteUrl: options.siteUrl })
42
+ } catch (err) {
43
+ console.error(err)
44
+ process.exit(1)
45
+ }
46
+ })
44
47
 
45
48
  cli.command('sprite [root]', 'Generate SVG sprite').action(async root => {
46
49
  try {
package/config/main.mjs CHANGED
@@ -75,10 +75,13 @@ function validateConfig(config) {
75
75
  return config
76
76
  }
77
77
 
78
- export async function loadConfig(root = process.cwd()) {
78
+ export async function loadConfig(root = process.cwd(), inlineConfig = {}) {
79
79
  const userConfig = await loadUserConfig(root)
80
80
  const config = mergeConfig(defaultConfig, userConfig)
81
81
  config.root = root
82
+ if (inlineConfig.siteUrl !== undefined && inlineConfig.siteUrl !== null) {
83
+ config.siteUrl = inlineConfig.siteUrl
84
+ }
82
85
  validateConfig(config)
83
86
  return config
84
87
  }
package/core/context.mjs CHANGED
@@ -11,6 +11,7 @@ export class BuildContext {
11
11
  this.graph = new DependencyGraph()
12
12
  this.sassGraph = new DependencyGraph()
13
13
  this.scriptGraph = new DependencyGraph()
14
+ this.imageGraph = new DependencyGraph() // Pug -> 画像ファイルの依存グラフ(dev時に構築)
14
15
 
15
16
  const outDir = config.outDir ?? 'dist'
16
17
  const resolvedOutDir = isAbsolute(outDir) ? outDir : resolve(config.root, outDir)
package/core/watcher.mjs CHANGED
@@ -2,6 +2,7 @@ import chokidar from 'chokidar'
2
2
  import { rm } from 'node:fs/promises'
3
3
  import { relative, resolve, basename, extname } from 'node:path'
4
4
  import { logger } from '../utils/logger.mjs'
5
+ import { clearImageSizeCache } from '../transform/image-size.mjs'
5
6
 
6
7
  /**
7
8
  * ファイル監視タスク
@@ -43,7 +44,7 @@ class FileWatcher {
43
44
  ignoreInitial: true,
44
45
  ignored: [/(^|[\/\\])\./, /node_modules/, /\.git/],
45
46
  persistent: true,
46
- awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
47
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 100 }
47
48
  })
48
49
  .on('change', filePath => this.handleChange(filePath))
49
50
  .on('add', filePath => this.handleAdd(filePath))
@@ -116,10 +117,11 @@ class FileWatcher {
116
117
  }
117
118
 
118
119
  async onPugUnlink(filePath) {
119
- const { paths, cache, graph } = this.context
120
+ const { paths, cache, graph, imageGraph } = this.context
120
121
  const relPath = relative(paths.src, filePath)
121
122
  cache.invalidatePugTemplate(filePath)
122
123
  graph.clearDependencies(filePath)
124
+ imageGraph.clearDependencies(filePath)
123
125
  if (basename(filePath).startsWith('_')) {
124
126
  logger.info('unlink', relPath)
125
127
  return
@@ -202,12 +204,19 @@ class FileWatcher {
202
204
  // ---- Image ----
203
205
 
204
206
  async onImageChange(filePath, event) {
207
+ clearImageSizeCache()
205
208
  const relPath = relative(this.context.paths.src, filePath)
206
209
  logger.info(event, `image: ${relPath}`)
207
210
  try {
208
211
  if (this.context.taskRegistry?.image) {
209
212
  await this.context.taskRegistry.image(this.context, { files: [filePath] })
210
213
  }
214
+ // imageGraph から影響を受ける Pug ファイルのみ再ビルド
215
+ // グラフ未構築(初回 dev 起動直後など)の場合は全 Pug を再ビルド
216
+ if (this.context.taskRegistry?.pug) {
217
+ const affected = this.context.imageGraph.getAffectedParents(filePath)
218
+ await this.context.taskRegistry.pug(this.context, { files: affected.length > 0 ? affected : undefined })
219
+ }
211
220
  this.reload()
212
221
  } catch (error) {
213
222
  logger.error('watch', `Image processing failed: ${error.message}`)
@@ -215,6 +224,7 @@ class FileWatcher {
215
224
  }
216
225
 
217
226
  async onImageUnlink(filePath) {
227
+ clearImageSizeCache()
218
228
  const { paths, config } = this.context
219
229
  const relPath = relative(paths.src, filePath)
220
230
  const optimization = config.build.imageOptimization
@@ -223,6 +233,15 @@ class FileWatcher {
223
233
  const destRelPath = relPath.replace(new RegExp(`\\${ext}$`, 'i'), newExt)
224
234
  const distPath = resolve(paths.dist, destRelPath)
225
235
  await this.deleteDistFile(distPath, relPath)
236
+ // imageGraph から影響を受ける Pug ファイルのみ再ビルド
237
+ if (this.context.taskRegistry?.pug) {
238
+ const affected = this.context.imageGraph.getAffectedParents(filePath)
239
+ this.context.imageGraph.clearDependencies(filePath)
240
+ if (affected.length > 0) {
241
+ await this.context.taskRegistry.pug(this.context, { files: affected })
242
+ this.reload()
243
+ }
244
+ }
226
245
  }
227
246
 
228
247
  // ---- Public ----
package/index.mjs CHANGED
@@ -10,8 +10,8 @@ import spriteTask from './tasks/svg-sprite.mjs'
10
10
  import serverTask from './core/server.mjs'
11
11
  import watcherTask from './core/watcher.mjs'
12
12
 
13
- export async function createBuilder(root = process.cwd(), mode = 'development') {
14
- const config = await loadConfig(root)
13
+ export async function createBuilder(root = process.cwd(), mode = 'development', inlineConfig = {}) {
14
+ const config = await loadConfig(root, inlineConfig)
15
15
  const builder = new Builder(config, mode)
16
16
 
17
17
  builder.registerTasks({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pugkit",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "A build tool for Pug-based projects",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/tasks/image.mjs CHANGED
@@ -5,6 +5,10 @@ import sharp from 'sharp'
5
5
  import { logger } from '../utils/logger.mjs'
6
6
  import { ensureFileDir } from '../utils/file.mjs'
7
7
 
8
+ // libvips の内部キャッシュを制限してメモリ消費を抑える
9
+ sharp.cache({ memory: 50, files: 20, items: 200 })
10
+ sharp.concurrency(1)
11
+
8
12
  /**
9
13
  * 画像最適化タスク
10
14
  */
@@ -62,7 +66,7 @@ export async function imageTask(context, options = {}) {
62
66
  /**
63
67
  * 画像を処理(最適化)
64
68
  */
65
- async function processImage(filePath, context, optimization, isProduction) {
69
+ async function processImage(filePath, context, optimization, isProduction, retries = 3, retryDelay = 200) {
66
70
  const { paths, config } = context
67
71
  const ext = extname(filePath).toLowerCase()
68
72
  const relativePath = relative(paths.src, filePath)
@@ -104,6 +108,10 @@ async function processImage(filePath, context, optimization, isProduction) {
104
108
  await ensureFileDir(outputPath)
105
109
  await outputImage.toFile(outputPath)
106
110
  } catch (error) {
111
+ if (retries > 0 && error.message.includes('unsupported image format')) {
112
+ await new Promise(resolve => setTimeout(resolve, retryDelay))
113
+ return processImage(filePath, context, optimization, isProduction, retries - 1, retryDelay * 2)
114
+ }
107
115
  logger.error('image', `Failed to process ${relativePath}: ${error.message}`)
108
116
  }
109
117
  }
package/tasks/pug.mjs CHANGED
@@ -24,7 +24,7 @@ export async function pugTask(context, options = {}) {
24
24
  }
25
25
 
26
26
  logger.info('pug', `Building ${changed.length} file(s)`)
27
- await Promise.all(changed.map(file => processFile(file, context)))
27
+ await runWithConcurrency(changed, 8, file => processFile(file, context))
28
28
  logger.success('pug', `Built ${changed.length} file(s)`)
29
29
  }
30
30
 
@@ -48,8 +48,18 @@ async function resolveChangedFiles(files, targetFiles, cache, isProduction) {
48
48
  return files
49
49
  }
50
50
 
51
+ async function runWithConcurrency(items, concurrency, fn) {
52
+ let i = 0
53
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
54
+ while (i < items.length) {
55
+ await fn(items[i++])
56
+ }
57
+ })
58
+ await Promise.all(workers)
59
+ }
60
+
51
61
  async function processFile(filePath, context) {
52
- const { paths, config, cache, graph } = context
62
+ const { paths, config, cache, graph, imageGraph } = context
53
63
 
54
64
  try {
55
65
  let template = cache.getPugTemplate(filePath)
@@ -65,10 +75,20 @@ async function processFile(filePath, context) {
65
75
  }
66
76
 
67
77
  const builderVars = createBuilderVars(filePath, paths, config)
68
- const imageSize = createImageSizeHelper(filePath, paths, logger)
69
- const imageInfo = createImageInfoHelper(filePath, paths, logger, config)
78
+
79
+ // dev 時のみ: imageGraph Pug->画像 の依存を記録して画像変更時の最小再ビルドに使う
80
+ const accessedImages = new Set()
81
+ const onAccess = context.isDevelopment ? imgPath => accessedImages.add(imgPath) : undefined
82
+ const imageSize = createImageSizeHelper(filePath, paths, logger, { onAccess })
83
+ const imageInfo = createImageInfoHelper(filePath, paths, logger, config, { onAccess })
70
84
 
71
85
  const html = template({ Builder: builderVars, imageSize, imageInfo })
86
+
87
+ if (context.isDevelopment && imageGraph) {
88
+ imageGraph.clearDependencies(filePath)
89
+ accessedImages.forEach(imgPath => imageGraph.addDependency(filePath, imgPath))
90
+ }
91
+
72
92
  const formatted = formatHtml(html, config.build.html)
73
93
  await generatePage(filePath, formatted, paths)
74
94
  } catch (error) {
@@ -2,7 +2,21 @@ import { readFileSync, existsSync } from 'node:fs'
2
2
  import { resolve, relative, dirname, basename, extname } from 'node:path'
3
3
  import sizeOf from 'image-size'
4
4
 
5
- export function createImageSizeHelper(filePath, paths, logger) {
5
+ // ビルドセッション内で画像ファイルの内容をキャッシュし、同じファイルの重複読み込みを防ぐ
6
+ const _imageBufferCache = new Map()
7
+
8
+ function readImageCached(filePath) {
9
+ if (_imageBufferCache.has(filePath)) return _imageBufferCache.get(filePath)
10
+ const buf = readFileSync(filePath)
11
+ _imageBufferCache.set(filePath, buf)
12
+ return buf
13
+ }
14
+
15
+ export function clearImageSizeCache() {
16
+ _imageBufferCache.clear()
17
+ }
18
+
19
+ export function createImageSizeHelper(filePath, paths, logger, { onAccess } = {}) {
6
20
  return src => {
7
21
  const resolveImagePath = (imageSrc, baseDir) => {
8
22
  if (imageSrc.startsWith('/')) {
@@ -24,7 +38,8 @@ export function createImageSizeHelper(filePath, paths, logger) {
24
38
  const foundPath = findImageFile(resolvedPath)
25
39
 
26
40
  if (foundPath) {
27
- const buffer = readFileSync(foundPath)
41
+ onAccess?.(foundPath)
42
+ const buffer = readImageCached(foundPath)
28
43
  return sizeOf(buffer)
29
44
  }
30
45
 
@@ -37,7 +52,7 @@ export function createImageSizeHelper(filePath, paths, logger) {
37
52
  }
38
53
  }
39
54
 
40
- export function createImageInfoHelper(filePath, paths, logger, config) {
55
+ export function createImageInfoHelper(filePath, paths, logger, config, { onAccess } = {}) {
41
56
  const optimization = config?.build?.imageOptimization
42
57
  const newExt = optimization === 'avif' || optimization === 'webp' ? `.${optimization}` : null
43
58
  const artDirectionSuffix = config?.build?.imageInfo?.artDirectionSuffix ?? '_sp'
@@ -75,7 +90,8 @@ export function createImageInfoHelper(filePath, paths, logger, config) {
75
90
  return fallback
76
91
  }
77
92
 
78
- const buffer = readFileSync(foundPath)
93
+ const buffer = readImageCached(foundPath)
94
+ onAccess?.(foundPath)
79
95
  const { width, height, type: format } = sizeOf(buffer)
80
96
 
81
97
  const ext = extname(src)
@@ -91,7 +107,8 @@ export function createImageInfoHelper(filePath, paths, logger, config) {
91
107
  const retinaResolvedPath = resolveImagePath(retinaSrc, pageDir)
92
108
  const retinaFoundPath = findImageFile(retinaResolvedPath)
93
109
  if (retinaFoundPath) {
94
- const retinaBuffer = readFileSync(retinaFoundPath)
110
+ const retinaBuffer = readImageCached(retinaFoundPath)
111
+ onAccess?.(retinaFoundPath)
95
112
  const { width: rWidth, height: rHeight } = sizeOf(retinaBuffer)
96
113
  retina = {
97
114
  src: newExt ? `${base}@2x${newExt}` : retinaSrc,
@@ -108,7 +125,8 @@ export function createImageInfoHelper(filePath, paths, logger, config) {
108
125
  const variantResolvedPath = resolveImagePath(variantSrc, pageDir)
109
126
  const variantFoundPath = findImageFile(variantResolvedPath)
110
127
  if (variantFoundPath) {
111
- const variantBuffer = readFileSync(variantFoundPath)
128
+ const variantBuffer = readImageCached(variantFoundPath)
129
+ onAccess?.(variantFoundPath)
112
130
  const { width: vWidth, height: vHeight } = sizeOf(variantBuffer)
113
131
  variant = {
114
132
  src: newExt ? `${base}${artDirectionSuffix}${newExt}` : variantSrc,