pugkit 1.0.0-beta.1

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/tasks/copy.mjs ADDED
@@ -0,0 +1,40 @@
1
+ import { glob } from 'glob'
2
+ import { readFile, writeFile, copyFile } from 'node:fs/promises'
3
+ import { relative, resolve, basename } from 'node:path'
4
+ import { logger } from '../utils/logger.mjs'
5
+ import { ensureFileDir } from '../utils/file.mjs'
6
+
7
+ /**
8
+ * ファイルコピータスク
9
+ */
10
+ export async function copyTask(context, options = {}) {
11
+ const { paths } = context
12
+
13
+ const files = await glob('**/*', {
14
+ cwd: paths.public,
15
+ absolute: true,
16
+ nodir: true,
17
+ dot: true
18
+ })
19
+
20
+ if (files.length === 0) {
21
+ logger.skip('copy', 'No files to copy')
22
+ return
23
+ }
24
+
25
+ logger.info('copy', `Copying ${files.length} file(s)`)
26
+
27
+ await Promise.all(
28
+ files.map(async file => {
29
+ const relativePath = relative(paths.public, file)
30
+ const outputPath = resolve(paths.dist, relativePath)
31
+
32
+ await ensureFileDir(outputPath)
33
+ await copyFile(file, outputPath)
34
+ })
35
+ )
36
+
37
+ logger.success('copy', `Copied ${files.length} file(s)`)
38
+ }
39
+
40
+ export default copyTask
@@ -0,0 +1,93 @@
1
+ import { glob } from 'glob'
2
+ import { readFile, writeFile } from 'node:fs/promises'
3
+ import { relative, resolve, dirname, extname } from 'node:path'
4
+ import sharp from 'sharp'
5
+ import { logger } from '../utils/logger.mjs'
6
+ import { ensureFileDir } from '../utils/file.mjs'
7
+
8
+ /**
9
+ * 画像最適化タスク
10
+ */
11
+ export async function imageTask(context, options = {}) {
12
+ const { paths, config, isProduction, cache } = context
13
+
14
+ const optimization = config.build.imageOptimization
15
+
16
+ if (!optimization || optimization === false) {
17
+ logger.skip('image', 'Image optimization disabled')
18
+ return
19
+ }
20
+
21
+ // 特定のファイルが指定されている場合(watch時)
22
+ if (options.files && Array.isArray(options.files)) {
23
+ logger.info('image', `Processing ${options.files.length} image(s)`)
24
+ await Promise.all(options.files.map(file => processImage(file, context, optimization, isProduction)))
25
+ logger.success('image', `Processed ${options.files.length} image(s)`)
26
+ return
27
+ }
28
+
29
+ // 対象画像を取得
30
+ const images = await glob('**/*.{jpg,jpeg,png,gif}', {
31
+ cwd: paths.src,
32
+ absolute: true,
33
+ ignore: ['**/_*/**']
34
+ })
35
+
36
+ if (images.length === 0) {
37
+ logger.skip('image', 'No images found')
38
+ return
39
+ }
40
+
41
+ logger.info('image', `Processing ${images.length} image(s)`)
42
+
43
+ // 並列処理
44
+ await Promise.all(images.map(file => processImage(file, context, optimization, isProduction)))
45
+
46
+ logger.success('image', `Processed ${images.length} image(s)`)
47
+ }
48
+
49
+ /**
50
+ * 画像を処理(最適化)
51
+ */
52
+ async function processImage(filePath, context, optimization, isProduction) {
53
+ const { paths, config } = context
54
+ const ext = extname(filePath).toLowerCase()
55
+ const relativePath = relative(paths.src, filePath)
56
+
57
+ try {
58
+ const image = sharp(filePath)
59
+ const metadata = await image.metadata()
60
+
61
+ let outputPath
62
+ let outputImage
63
+
64
+ if (optimization === 'webp') {
65
+ // WebP変換
66
+ outputPath = resolve(paths.dist, relativePath.replace(/\.(jpg|jpeg|png|gif)$/i, '.webp'))
67
+ outputImage = image.webp(config.build.imageOptions.webp)
68
+ } else {
69
+ // 元の形式で圧縮
70
+ outputPath = resolve(paths.dist, relativePath)
71
+
72
+ if (ext === '.jpg' || ext === '.jpeg') {
73
+ outputImage = image.jpeg(config.build.imageOptions.jpeg)
74
+ } else if (ext === '.png') {
75
+ outputImage = image.png(config.build.imageOptions.png)
76
+ } else {
77
+ // GIFなどはそのままコピー
78
+ const buffer = await readFile(filePath)
79
+ await ensureFileDir(outputPath)
80
+ await writeFile(outputPath, buffer)
81
+ return
82
+ }
83
+ }
84
+
85
+ // 保存
86
+ await ensureFileDir(outputPath)
87
+ await outputImage.toFile(outputPath)
88
+ } catch (error) {
89
+ logger.error('image', `Failed to process ${relativePath}: ${error.message}`)
90
+ }
91
+ }
92
+
93
+ export default imageTask
package/tasks/pug.mjs ADDED
@@ -0,0 +1,81 @@
1
+ import { glob } from 'glob'
2
+ import { basename } from 'node:path'
3
+ import { compilePugFile } from '../transform/pug.mjs'
4
+ import { formatHtml } from '../transform/html.mjs'
5
+ import { createBuilderVars } from '../transform/builder-vars.mjs'
6
+ import { createImageSizeHelper } from '../transform/image-size.mjs'
7
+ import { generatePage } from '../generate/page.mjs'
8
+ import { logger } from '../utils/logger.mjs'
9
+
10
+ export async function pugTask(context, options = {}) {
11
+ const { paths, cache, isProduction } = context
12
+ const { files: targetFiles } = options
13
+
14
+ const filesToBuild = await resolveFiles(paths, targetFiles)
15
+ if (filesToBuild.length === 0) {
16
+ logger.skip('pug', 'No files to build')
17
+ return
18
+ }
19
+
20
+ const changed = await resolveChangedFiles(filesToBuild, targetFiles, cache, isProduction)
21
+ if (changed.length === 0) {
22
+ logger.skip('pug', 'No changes detected')
23
+ return
24
+ }
25
+
26
+ logger.info('pug', `Building ${changed.length} file(s)`)
27
+ await Promise.all(changed.map(file => processFile(file, context)))
28
+ logger.success('pug', `Built ${changed.length} file(s)`)
29
+ }
30
+
31
+ async function resolveFiles(paths, targetFiles) {
32
+ if (targetFiles?.length > 0) {
33
+ return targetFiles.filter(file => !basename(file).startsWith('_'))
34
+ }
35
+
36
+ const allFiles = await glob('**/*.pug', {
37
+ cwd: paths.src,
38
+ absolute: true,
39
+ ignore: ['**/_*/**', '**/_*.pug']
40
+ })
41
+
42
+ return allFiles.filter(file => !basename(file).startsWith('_'))
43
+ }
44
+
45
+ async function resolveChangedFiles(files, targetFiles, cache, isProduction) {
46
+ if (targetFiles?.length > 0) return files
47
+ if (!isProduction) return cache.getChangedFiles(files)
48
+ return files
49
+ }
50
+
51
+ async function processFile(filePath, context) {
52
+ const { paths, config, cache, graph } = context
53
+
54
+ try {
55
+ let template = cache.getPugTemplate(filePath)
56
+
57
+ if (!template) {
58
+ const result = await compilePugFile(filePath, { basedir: paths.src })
59
+
60
+ if (result.dependencies.length > 0) {
61
+ graph.clearDependencies(filePath)
62
+ result.dependencies.forEach(dep => graph.addDependency(filePath, dep))
63
+ }
64
+
65
+ template = result.template
66
+ cache.setPugTemplate(filePath, template)
67
+ }
68
+
69
+ const builderVars = createBuilderVars(filePath, paths, config)
70
+ const imageSize = createImageSizeHelper(filePath, paths, logger)
71
+
72
+ const html = template({ Builder: builderVars, imageSize })
73
+ const formatted = await formatHtml(html)
74
+ await generatePage(filePath, formatted, paths)
75
+ } catch (error) {
76
+ logger.error('pug', `Failed: ${basename(filePath)} - ${error.message}`)
77
+ throw error
78
+ }
79
+ }
80
+
81
+ export default pugTask
package/tasks/sass.mjs ADDED
@@ -0,0 +1,107 @@
1
+ import { glob } from 'glob'
2
+ import { readFile, writeFile } from 'node:fs/promises'
3
+ import { relative, resolve, basename, extname } from 'node:path'
4
+ import * as sass from 'sass'
5
+ import postcss from 'postcss'
6
+ import autoprefixer from 'autoprefixer'
7
+ import cssnano from 'cssnano'
8
+ import { logger } from '../utils/logger.mjs'
9
+ import { ensureFileDir } from '../utils/file.mjs'
10
+
11
+ /**
12
+ * Sassビルドタスク
13
+ */
14
+ export async function sassTask(context, options = {}) {
15
+ const { paths, config, isProduction } = context
16
+
17
+ // debugモードはdevモード時のみ有効
18
+ const isDebugMode = !isProduction && config.debug
19
+
20
+ // 1. ビルド対象ファイルの取得
21
+ const scssFiles = await glob('**/[^_]*.scss', {
22
+ cwd: paths.src,
23
+ absolute: true,
24
+ ignore: ['**/_*.scss']
25
+ })
26
+
27
+ if (scssFiles.length === 0) {
28
+ logger.skip('sass', 'No files to build')
29
+ return
30
+ }
31
+
32
+ logger.info('sass', `Building ${scssFiles.length} file(s)`)
33
+
34
+ // 2. 並列コンパイル
35
+ await Promise.all(scssFiles.map(file => compileSassFile(file, context, isDebugMode)))
36
+
37
+ logger.success('sass', `Built ${scssFiles.length} file(s)`)
38
+ }
39
+
40
+ /**
41
+ * 個別Sassファイルのコンパイル
42
+ */
43
+ async function compileSassFile(filePath, context, isDebugMode) {
44
+ const { paths, config, isProduction } = context
45
+
46
+ try {
47
+ // Sassコンパイル
48
+ const result = sass.compile(filePath, {
49
+ silenceDeprecations: ['legacy-js-api'],
50
+ style: isDebugMode ? 'expanded' : 'compressed',
51
+ loadPaths: [resolve(paths.root, 'node_modules')],
52
+ charset: false,
53
+ quietDeps: true,
54
+ sourceMap: isDebugMode,
55
+ sourceMapIncludeSources: isDebugMode
56
+ })
57
+
58
+ let css = result.css
59
+
60
+ // PostCSS処理
61
+ const postcssPlugins = [autoprefixer()]
62
+
63
+ // debugモード以外は常にminify
64
+ if (!isDebugMode) {
65
+ postcssPlugins.push(
66
+ cssnano({
67
+ preset: [
68
+ 'default',
69
+ {
70
+ discardComments: { removeAll: true },
71
+ normalizeWhitespace: true,
72
+ colormin: true,
73
+ minifySelectors: true,
74
+ calc: false
75
+ }
76
+ ]
77
+ })
78
+ )
79
+ }
80
+
81
+ const outputRelativePath = relative(paths.src, filePath).replace('.scss', '.css')
82
+ const outputPath = resolve(paths.dist, outputRelativePath)
83
+
84
+ const postcssResult = await postcss(postcssPlugins).process(css, {
85
+ from: filePath,
86
+ to: outputPath,
87
+ map: isDebugMode ? { inline: false, annotation: true } : false
88
+ })
89
+
90
+ css = postcssResult.css
91
+
92
+ // 出力
93
+ await ensureFileDir(outputPath)
94
+ await writeFile(outputPath, css, 'utf8')
95
+
96
+ // debugモード時はソースマップを出力
97
+ if (isDebugMode && postcssResult.map) {
98
+ const mapPath = `${outputPath}.map`
99
+ await writeFile(mapPath, postcssResult.map.toString(), 'utf8')
100
+ }
101
+ } catch (error) {
102
+ logger.error('sass', `Failed to compile ${basename(filePath)}: ${error.message}`)
103
+ throw error
104
+ }
105
+ }
106
+
107
+ export default sassTask
@@ -0,0 +1,75 @@
1
+ import { glob } from 'glob'
2
+ import { resolve, relative, dirname } from 'node:path'
3
+ import * as esbuild from 'esbuild'
4
+ import { logger } from '../utils/logger.mjs'
5
+ import { ensureDir } from '../utils/file.mjs'
6
+
7
+ /**
8
+ * esbuild(TypeScript/JavaScript)ビルドタスク
9
+ */
10
+ export async function scriptTask(context, options = {}) {
11
+ const { paths, isProduction, config } = context
12
+
13
+ // 1. ビルド対象ファイルの取得
14
+ const scriptFiles = await glob('**/[^_]*.{ts,js}', {
15
+ cwd: paths.src,
16
+ absolute: true,
17
+ ignore: ['**/*.d.ts', '**/node_modules/**']
18
+ })
19
+
20
+ if (scriptFiles.length === 0) {
21
+ logger.skip('script', 'No files to build')
22
+ return
23
+ }
24
+
25
+ logger.info('script', `Building ${scriptFiles.length} file(s)`)
26
+
27
+ // debugモードはdevモード時のみ有効
28
+ const isDebugMode = !isProduction && config.debug
29
+
30
+ try {
31
+ // 2. esbuild設定
32
+ const config = {
33
+ entryPoints: scriptFiles,
34
+ outdir: paths.dist,
35
+ outbase: paths.src,
36
+ bundle: true,
37
+ format: 'esm',
38
+ target: 'es2022',
39
+ platform: 'browser',
40
+ splitting: false,
41
+ write: true,
42
+ sourcemap: isDebugMode,
43
+ minify: false,
44
+ metafile: false,
45
+ logLevel: 'error',
46
+ keepNames: false,
47
+ external: [],
48
+ plugins: [],
49
+ legalComments: 'none',
50
+ treeShaking: true,
51
+ minifyWhitespace: !isDebugMode,
52
+ minifySyntax: !isDebugMode
53
+ }
54
+
55
+ // debugモードでない場合はconsole/debuggerを削除
56
+ if (!isDebugMode) {
57
+ config.drop = ['console', 'debugger']
58
+ }
59
+
60
+ // 3. ビルド実行
61
+ await ensureDir(paths.dist)
62
+ const result = await esbuild.build(config)
63
+
64
+ if (result.errors && result.errors.length > 0) {
65
+ throw new Error(`esbuild errors: ${result.errors.length}`)
66
+ }
67
+
68
+ logger.success('script', `Built ${scriptFiles.length} file(s)`)
69
+ } catch (error) {
70
+ logger.error('script', error.message)
71
+ throw error
72
+ }
73
+ }
74
+
75
+ export default scriptTask
@@ -0,0 +1,125 @@
1
+ import { glob } from 'glob'
2
+ import { readFile, writeFile } from 'node:fs/promises'
3
+ import { resolve, dirname, basename, relative } from 'node:path'
4
+ import { optimize } from 'svgo'
5
+ import { logger } from '../utils/logger.mjs'
6
+ import { ensureFileDir } from '../utils/file.mjs'
7
+
8
+ /**
9
+ * SVGO設定(スプライト用)
10
+ */
11
+ const SPRITE_SVGO_CONFIG = {
12
+ plugins: [
13
+ {
14
+ name: 'preset-default',
15
+ params: {
16
+ overrides: {
17
+ cleanupIds: {
18
+ minify: false,
19
+ preserve: [],
20
+ preservePrefixes: []
21
+ }
22
+ }
23
+ }
24
+ },
25
+ 'removeDimensions',
26
+ {
27
+ name: 'removeViewBox',
28
+ active: false
29
+ },
30
+ {
31
+ name: 'addAttributesToSVGElement',
32
+ params: {
33
+ attributes: [{ xmlns: 'http://www.w3.org/2000/svg' }]
34
+ }
35
+ }
36
+ ]
37
+ }
38
+
39
+ /**
40
+ * SVGスプライト生成タスク
41
+ */
42
+ export async function spriteTask(context, options = {}) {
43
+ const { paths, isProduction } = context
44
+
45
+ // iconsディレクトリを検索
46
+ const iconDirs = await glob('**/icons', {
47
+ cwd: paths.src,
48
+ absolute: false
49
+ })
50
+
51
+ if (iconDirs.length === 0) {
52
+ logger.skip('sprite', 'No icons directories found')
53
+ return
54
+ }
55
+
56
+ let totalIcons = 0
57
+
58
+ // 各iconsディレクトリでスプライト生成
59
+ for (const iconDir of iconDirs) {
60
+ const inputDir = resolve(paths.src, iconDir)
61
+ const outputDir = resolve(paths.dist, dirname(iconDir))
62
+ const outputPath = resolve(outputDir, 'icons.svg')
63
+
64
+ const count = await generateSprite(inputDir, outputPath)
65
+ if (count) {
66
+ totalIcons += count
67
+ }
68
+ }
69
+
70
+ if (totalIcons > 0) {
71
+ logger.success('sprite', `Generated sprite with ${totalIcons} icon(s)`)
72
+ } else {
73
+ logger.skip('sprite', 'No icons to process')
74
+ }
75
+ }
76
+
77
+ /**
78
+ * SVGスプライト生成
79
+ */
80
+ async function generateSprite(iconDir, outputPath) {
81
+ const svgFiles = await glob('*.svg', {
82
+ cwd: iconDir,
83
+ absolute: true
84
+ })
85
+
86
+ if (svgFiles.length === 0) {
87
+ return 0
88
+ }
89
+
90
+ const symbols = []
91
+
92
+ for (const svgFile of svgFiles) {
93
+ const fileName = basename(svgFile, '.svg')
94
+ const svgContent = await readFile(svgFile, 'utf-8')
95
+
96
+ // SVGを最適化
97
+ const optimized = optimize(svgContent, SPRITE_SVGO_CONFIG)
98
+ let svg = optimized.data
99
+
100
+ // viewBoxを抽出
101
+ const viewBoxMatch = svg.match(/viewBox="([^"]+)"/)
102
+ const viewBox = viewBoxMatch ? viewBoxMatch[1] : '0 0 24 24'
103
+
104
+ // <svg>タグを<symbol>に変換
105
+ svg = svg.replace(/<svg[^>]*>/, `<symbol id="${fileName}" viewBox="${viewBox}">`).replace(/<\/svg>/, '</symbol>')
106
+
107
+ // fill/strokeをcurrentColorに統一
108
+ svg = svg
109
+ .replace(/fill="(?!none)[^"]*"/g, 'fill="currentColor"')
110
+ .replace(/stroke="(?!none)[^"]*"/g, 'stroke="currentColor"')
111
+
112
+ symbols.push(svg)
113
+ }
114
+
115
+ const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
116
+ ${symbols.join('\n')}
117
+ </svg>`
118
+
119
+ await ensureFileDir(outputPath)
120
+ await writeFile(outputPath, sprite, 'utf-8')
121
+
122
+ return svgFiles.length
123
+ }
124
+
125
+ export default spriteTask
package/tasks/svg.mjs ADDED
@@ -0,0 +1,73 @@
1
+ import { glob } from 'glob'
2
+ import { readFile, writeFile } from 'node:fs/promises'
3
+ import { relative, resolve } from 'node:path'
4
+ import { optimize } from 'svgo'
5
+ import { logger } from '../utils/logger.mjs'
6
+ import { ensureFileDir } from '../utils/file.mjs'
7
+
8
+ /**
9
+ * SVG最適化タスク
10
+ */
11
+ export async function svgTask(context, options = {}) {
12
+ const { paths } = context
13
+
14
+ // 特定のファイルが指定されている場合(watch時)
15
+ if (options.files && Array.isArray(options.files)) {
16
+ logger.info('svg', `Optimizing ${options.files.length} SVG file(s)`)
17
+ await Promise.all(options.files.map(file => optimizeSvg(file, context)))
18
+ logger.success('svg', `Optimized ${options.files.length} SVG file(s)`)
19
+ return
20
+ }
21
+
22
+ // 対象SVGを取得
23
+ const svgs = await glob('**/*.svg', {
24
+ cwd: paths.src,
25
+ absolute: true,
26
+ ignore: ['**/_*/**', '**/icons/**'] // iconsはスプライト用なので除外
27
+ })
28
+
29
+ if (svgs.length === 0) {
30
+ logger.skip('svg', 'No SVG files found')
31
+ return
32
+ }
33
+
34
+ logger.info('svg', `Optimizing ${svgs.length} SVG file(s)`)
35
+
36
+ // 並列処理
37
+ await Promise.all(svgs.map(file => optimizeSvg(file, context)))
38
+
39
+ logger.success('svg', `Optimized ${svgs.length} SVG file(s)`)
40
+ }
41
+
42
+ /**
43
+ * SVGを最適化
44
+ */
45
+ async function optimizeSvg(filePath, context) {
46
+ const { paths } = context
47
+ const relativePath = relative(paths.src, filePath)
48
+
49
+ try {
50
+ const content = await readFile(filePath, 'utf8')
51
+
52
+ // SVGOで最適化
53
+ const result = optimize(content, {
54
+ path: filePath,
55
+ plugins: [
56
+ 'preset-default',
57
+ {
58
+ name: 'removeViewBox',
59
+ active: false
60
+ }
61
+ ]
62
+ })
63
+
64
+ // 出力
65
+ const outputPath = resolve(paths.dist, relativePath)
66
+ await ensureFileDir(outputPath)
67
+ await writeFile(outputPath, result.data, 'utf8')
68
+ } catch (error) {
69
+ logger.error('svg', `Failed to optimize ${relativePath}: ${error.message}`)
70
+ }
71
+ }
72
+
73
+ export default svgTask
@@ -0,0 +1,33 @@
1
+ import { relative } from 'node:path'
2
+
3
+ export function createBuilderVars(filePath, paths, config) {
4
+ const relativePath = relative(paths.src, filePath)
5
+ const depth = relativePath.split('/').length - 1
6
+ const autoDir = depth === 0 ? './' : '../'.repeat(depth)
7
+
8
+ let autoPageUrl = relativePath.replace(/\\/g, '/')
9
+
10
+ if (autoPageUrl.endsWith('index.pug')) {
11
+ autoPageUrl = autoPageUrl.replace(/index\.pug$/, '')
12
+ } else {
13
+ autoPageUrl = autoPageUrl.replace(/\.pug$/, '.html')
14
+ }
15
+
16
+ const siteUrl = config.siteUrl || ''
17
+ const subdir = config.subdir ? '/' + config.subdir.replace(/^\/|\/$/g, '') : ''
18
+ const origin = siteUrl.replace(/\/$/, '')
19
+ const base = origin + subdir
20
+ const pathname = autoPageUrl ? (autoPageUrl.startsWith('/') ? autoPageUrl : '/' + autoPageUrl) : '/'
21
+ const href = base + pathname
22
+
23
+ return {
24
+ dir: autoDir,
25
+ subdir,
26
+ url: {
27
+ origin,
28
+ base,
29
+ pathname,
30
+ href
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,14 @@
1
+ import prettier from 'prettier'
2
+
3
+ export async function formatHtml(html, options = {}) {
4
+ const cleaned = html.replace(/[\u200B-\u200D\uFEFF]/g, '')
5
+
6
+ return prettier.format(cleaned, {
7
+ parser: 'html',
8
+ printWidth: options.printWidth || 100_000,
9
+ tabWidth: options.tabWidth || 2,
10
+ useTabs: options.useTabs || false,
11
+ htmlWhitespaceSensitivity: 'ignore',
12
+ singleAttributePerLine: false
13
+ })
14
+ }
@@ -0,0 +1,38 @@
1
+ import { readFileSync, existsSync } from 'node:fs'
2
+ import { resolve, dirname, basename } from 'node:path'
3
+ import sizeOf from 'image-size'
4
+
5
+ export function createImageSizeHelper(filePath, paths, logger) {
6
+ return src => {
7
+ const resolveImagePath = (imageSrc, baseDir) => {
8
+ if (imageSrc.startsWith('/')) {
9
+ return resolve(paths.src, imageSrc.slice(1))
10
+ }
11
+ return resolve(baseDir, imageSrc)
12
+ }
13
+
14
+ const findImageFile = resolvedPath => {
15
+ if (existsSync(resolvedPath)) return resolvedPath
16
+ const publicPath = resolvedPath.replace(paths.src, paths.public)
17
+ if (existsSync(publicPath)) return publicPath
18
+ return null
19
+ }
20
+
21
+ try {
22
+ const pageDir = dirname(filePath)
23
+ const resolvedPath = resolveImagePath(src, pageDir)
24
+ const foundPath = findImageFile(resolvedPath)
25
+
26
+ if (foundPath) {
27
+ const buffer = readFileSync(foundPath)
28
+ return sizeOf(buffer)
29
+ }
30
+
31
+ logger?.warn('pug', `Image not found: ${basename(resolvedPath)}`)
32
+ return { width: undefined, height: undefined }
33
+ } catch {
34
+ logger?.warn('pug', `Failed to get image size: ${basename(src)}`)
35
+ return { width: undefined, height: undefined }
36
+ }
37
+ }
38
+ }