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 +2 -2
- package/cli/index.mjs +11 -8
- package/config/main.mjs +4 -1
- package/core/context.mjs +1 -0
- package/core/watcher.mjs +21 -2
- package/index.mjs +2 -2
- package/package.json +1 -1
- package/tasks/image.mjs +9 -1
- package/tasks/pug.mjs +24 -4
- package/transform/image-size.mjs +24 -6
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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:
|
|
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
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
|
|
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
|
-
|
|
69
|
-
|
|
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) {
|
package/transform/image-size.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|