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/README.md +172 -0
- package/cli/build.mjs +15 -0
- package/cli/develop.mjs +16 -0
- package/cli/index.mjs +55 -0
- package/cli/logger.mjs +31 -0
- package/cli/sprite.mjs +13 -0
- package/config/defaults.mjs +36 -0
- package/config/define.mjs +3 -0
- package/config/index.mjs +4 -0
- package/config/main.mjs +53 -0
- package/config/patterns.mjs +30 -0
- package/core/builder.mjs +139 -0
- package/core/cache.mjs +98 -0
- package/core/context.mjs +51 -0
- package/core/graph.mjs +97 -0
- package/core/server.mjs +78 -0
- package/core/watcher.mjs +314 -0
- package/generate/asset.mjs +19 -0
- package/generate/page.mjs +13 -0
- package/index.mjs +60 -0
- package/package.json +61 -0
- package/tasks/copy.mjs +40 -0
- package/tasks/image.mjs +93 -0
- package/tasks/pug.mjs +81 -0
- package/tasks/sass.mjs +107 -0
- package/tasks/script.mjs +75 -0
- package/tasks/svg-sprite.mjs +125 -0
- package/tasks/svg.mjs +73 -0
- package/transform/builder-vars.mjs +33 -0
- package/transform/html.mjs +14 -0
- package/transform/image-size.mjs +38 -0
- package/transform/pug.mjs +26 -0
- package/utils/file.mjs +44 -0
- package/utils/logger.mjs +40 -0
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
|
package/tasks/image.mjs
ADDED
|
@@ -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
|
package/tasks/script.mjs
ADDED
|
@@ -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
|
+
}
|