pugkit 1.0.2 → 1.2.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/README.md +34 -39
- package/cli/develop.mjs +1 -2
- package/cli/logger.mjs +5 -9
- package/config/defaults.mjs +2 -2
- package/config/main.mjs +1 -0
- package/config/patterns.mjs +3 -12
- package/core/builder.mjs +7 -1
- package/core/cache.mjs +4 -5
- package/core/context.mjs +2 -0
- package/core/graph.mjs +1 -1
- package/core/watcher.mjs +183 -297
- package/generate/page.mjs +1 -1
- package/package.json +2 -2
- package/tasks/copy.mjs +15 -0
- package/tasks/pug.mjs +1 -1
- package/tasks/sass.mjs +52 -10
- package/tasks/script.mjs +51 -10
- package/transform/html.mjs +13 -9
package/README.md
CHANGED
|
@@ -9,11 +9,6 @@
|
|
|
9
9
|
</a>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
|
-
## About
|
|
13
|
-
|
|
14
|
-
pugkitは静的サイト制作に特化したビルドツールです。
|
|
15
|
-
納品向きの綺麗なHTMLと、ファイル構成に制約のないアセットファイルを出力可能です。
|
|
16
|
-
|
|
17
12
|
## How To Use
|
|
18
13
|
|
|
19
14
|
```sh
|
|
@@ -39,6 +34,34 @@ $ touch ./src/index.pug
|
|
|
39
34
|
| `pugkit build` | 本番ビルド |
|
|
40
35
|
| `pugkit sprite` | SVGスプライト生成 |
|
|
41
36
|
|
|
37
|
+
## Directory Structure
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
project-root/
|
|
41
|
+
├── src/ # ソースファイル
|
|
42
|
+
│ ├── *.pug
|
|
43
|
+
│ ├── *.scss
|
|
44
|
+
│ ├── *.ts
|
|
45
|
+
│ ├── *.js
|
|
46
|
+
│ ├── *.jpg
|
|
47
|
+
│ ├── *.png
|
|
48
|
+
│ └── *.svg
|
|
49
|
+
├── public/ # 静的ファイル
|
|
50
|
+
│ ├── ogp.jpg
|
|
51
|
+
│ └── favicon.ico
|
|
52
|
+
├── dist/ # ビルド出力先
|
|
53
|
+
└── pugkit.config.mjs # ビルド設定ファイル
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### File Naming Rules
|
|
57
|
+
|
|
58
|
+
`_`(アンダースコア)で始まるファイル・ディレクトリはビルド対象外です。それ以外のファイルは `src/` 配下のディレクトリ構成を維持したまま `dist/` に出力されます。
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
src/foo/style.scss → dist/foo/style.css
|
|
62
|
+
src/foo/bar/script.js → dist/foo/bar/script.js
|
|
63
|
+
```
|
|
64
|
+
|
|
42
65
|
## Configuration
|
|
43
66
|
|
|
44
67
|
プロジェクトルートに`pugkit.config.mjs`を配置することで、ビルド設定をカスタマイズできます。
|
|
@@ -70,7 +93,7 @@ export default defineConfig({
|
|
|
70
93
|
| `server.port` | 開発サーバーのポート番号 | `number` | `5555` |
|
|
71
94
|
| `server.host` | 開発サーバーのホスト | `string` | `'localhost'` |
|
|
72
95
|
| `server.startPath` | サーバー起動時に開くパス | `string` | `'/'` |
|
|
73
|
-
| `
|
|
96
|
+
| `build.clean` | ビルド前に `dist/` をクリーンするか(`false` にすると他リソースと共存可能) | `boolean` | `true` |
|
|
74
97
|
| `build.imageOptimization` | 画像最適化の方式 | `'webp'` \| `'compress'` \| `false` | `'webp'` |
|
|
75
98
|
| `build.imageOptions.webp` | WebP変換オプション([Sharp WebP options](https://sharp.pixelplumbing.com/api-output#webp)) | `object` | - |
|
|
76
99
|
| `build.imageOptions.jpeg` | JPEG圧縮オプション([Sharp JPEG options](https://sharp.pixelplumbing.com/api-output#jpeg)) | `object` | - |
|
|
@@ -176,9 +199,13 @@ npm install --save-dev typescript
|
|
|
176
199
|
- `'compress'` - 元の形式を維持したまま圧縮
|
|
177
200
|
- `false` - 最適化を無効化
|
|
178
201
|
|
|
202
|
+
### SVG Optimization
|
|
203
|
+
|
|
204
|
+
`icons/`以外に配置した SVG ファイルはSVGOで自動最適化されて出力されます。
|
|
205
|
+
|
|
179
206
|
### SVG Sprite
|
|
180
207
|
|
|
181
|
-
`src
|
|
208
|
+
`src/`配下の`icons/`ディレクトリに配置したSVGを1つのスプライトファイルにまとめます。
|
|
182
209
|
|
|
183
210
|
```
|
|
184
211
|
src/assets/icons/arrow.svg → dist/assets/icons.svg#arrow
|
|
@@ -191,10 +218,6 @@ src/assets/icons/arrow.svg → dist/assets/icons.svg#arrow
|
|
|
191
218
|
- SVG ファイル名がそのまま `<symbol id>` になります
|
|
192
219
|
- `fill` / `stroke` は自動的に `currentColor` に変換されます
|
|
193
220
|
|
|
194
|
-
### SVG Optimization
|
|
195
|
-
|
|
196
|
-
`icons/` 以外に配置した SVG ファイルは SVGO で自動最適化されて出力されます。
|
|
197
|
-
|
|
198
221
|
### Public Directory
|
|
199
222
|
|
|
200
223
|
`public/` に置いたファイルはそのまま `dist/` のルートにコピーされます。faviconやOGP画像など最適化不要なファイルの置き場として使用します。
|
|
@@ -208,34 +231,6 @@ src/assets/icons/arrow.svg → dist/assets/icons.svg#arrow
|
|
|
208
231
|
| CSS | minify済み | expanded + ソースマップ |
|
|
209
232
|
| JS | minify済み・`console.*` 削除 | ソースマップ・`console.*` 保持 |
|
|
210
233
|
|
|
211
|
-
### File Naming Rules
|
|
212
|
-
|
|
213
|
-
`_`(アンダースコア)で始まるファイル・ディレクトリはビルド対象外です。それ以外のファイルは `src/` 配下のディレクトリ構成を維持したまま `dist/` に出力されます。
|
|
214
|
-
|
|
215
|
-
```
|
|
216
|
-
src/foo/style.scss → dist/foo/style.css
|
|
217
|
-
src/foo/bar/script.js → dist/foo/bar/script.js
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
## Directory Structure
|
|
221
|
-
|
|
222
|
-
```
|
|
223
|
-
project-root/
|
|
224
|
-
├── src/ # ソースファイル
|
|
225
|
-
│ ├── *.pug
|
|
226
|
-
│ ├── *.scss
|
|
227
|
-
│ ├── *.ts
|
|
228
|
-
│ ├── *.js
|
|
229
|
-
│ ├── *.jpg
|
|
230
|
-
│ ├── *.png
|
|
231
|
-
│ └── *.svg
|
|
232
|
-
├── public/ # 静的ファイル
|
|
233
|
-
│ ├── ogp.jpg
|
|
234
|
-
│ └── favicon.ico
|
|
235
|
-
├── dist/ # ビルド出力先
|
|
236
|
-
└── pugkit.config.mjs # ビルド設定ファイル
|
|
237
|
-
```
|
|
238
|
-
|
|
239
234
|
## Tech Stack
|
|
240
235
|
|
|
241
236
|
- [Pug](https://pugjs.org/) - HTMLテンプレートエンジン
|
package/cli/develop.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { createBuilder } from '../index.mjs'
|
|
|
2
2
|
import { logger } from './logger.mjs'
|
|
3
3
|
|
|
4
4
|
export async function develop(options = {}) {
|
|
5
|
-
const { root = process.cwd(), port, host
|
|
5
|
+
const { root = process.cwd(), port, host } = options
|
|
6
6
|
|
|
7
7
|
logger.info('pugkit', 'starting dev server...')
|
|
8
8
|
|
|
@@ -10,7 +10,6 @@ export async function develop(options = {}) {
|
|
|
10
10
|
|
|
11
11
|
if (port) builder.context.config.server.port = port
|
|
12
12
|
if (host) builder.context.config.server.host = host
|
|
13
|
-
if (open) builder.context.config.server.open = open
|
|
14
13
|
|
|
15
14
|
await builder.watch()
|
|
16
15
|
}
|
package/cli/logger.mjs
CHANGED
|
@@ -6,26 +6,22 @@ import path from 'node:path'
|
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url)
|
|
7
7
|
const __dirname = path.dirname(__filename)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const pkgPath = path.join(__dirname, '../package.json')
|
|
11
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
12
|
-
return pkg.version
|
|
13
|
-
}
|
|
9
|
+
const version = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8')).version
|
|
14
10
|
|
|
15
11
|
export const logger = {
|
|
16
12
|
info(name, message) {
|
|
17
|
-
console.log(`${pc.cyan(pc.bold(name.toUpperCase()))} ${pc.dim(`v${
|
|
13
|
+
console.log(`${pc.cyan(pc.bold(name.toUpperCase()))} ${pc.dim(`v${version}`)} ${message}`)
|
|
18
14
|
},
|
|
19
15
|
|
|
20
16
|
success(name, message) {
|
|
21
|
-
console.log(`${pc.cyan(pc.bold(name.toUpperCase()))} ${pc.dim(`v${
|
|
17
|
+
console.log(`${pc.cyan(pc.bold(name.toUpperCase()))} ${pc.dim(`v${version}`)} ${message}`)
|
|
22
18
|
},
|
|
23
19
|
|
|
24
20
|
warn(name, message) {
|
|
25
|
-
console.log(`${pc.cyan(pc.bold(name.toUpperCase()))} ${pc.dim(`v${
|
|
21
|
+
console.log(`${pc.cyan(pc.bold(name.toUpperCase()))} ${pc.dim(`v${version}`)} ${message}`)
|
|
26
22
|
},
|
|
27
23
|
|
|
28
24
|
error(name, message) {
|
|
29
|
-
console.error(`${pc.cyan(pc.bold(name.toUpperCase()))} ${pc.dim(`v${
|
|
25
|
+
console.error(`${pc.cyan(pc.bold(name.toUpperCase()))} ${pc.dim(`v${version}`)} ${message}`)
|
|
30
26
|
}
|
|
31
27
|
}
|
package/config/defaults.mjs
CHANGED
package/config/main.mjs
CHANGED
|
@@ -25,6 +25,7 @@ function mergeConfig(defaults, user) {
|
|
|
25
25
|
build: {
|
|
26
26
|
...defaults.build,
|
|
27
27
|
...(user.build || {}),
|
|
28
|
+
clean: user.build?.clean !== undefined ? user.build.clean : defaults.build.clean,
|
|
28
29
|
imageOptions: {
|
|
29
30
|
webp: { ...defaults.build.imageOptions.webp, ...(user.build?.imageOptions?.webp || {}) },
|
|
30
31
|
jpeg: { ...defaults.build.imageOptions.jpeg, ...(user.build?.imageOptions?.jpeg || {}) },
|
package/config/patterns.mjs
CHANGED
|
@@ -11,20 +11,11 @@ export function createGlobPatterns(srcPath) {
|
|
|
11
11
|
ignore: ['**/*.d.ts', '**/node_modules/**']
|
|
12
12
|
},
|
|
13
13
|
images: {
|
|
14
|
-
optimize: [
|
|
15
|
-
|
|
16
|
-
`!${srcPath}/**/sprites_*/*`,
|
|
17
|
-
`!${srcPath}/**/_inline*/*`,
|
|
18
|
-
`!${srcPath}/**/icons*/*`
|
|
19
|
-
],
|
|
20
|
-
webp: {
|
|
21
|
-
src: [`${srcPath}/**/*.{png,jpg,jpeg,gif}`],
|
|
22
|
-
ignore: [`!${srcPath}/**/favicons/*`, `!${srcPath}/**/ogp.{png,jpg}`]
|
|
23
|
-
}
|
|
14
|
+
optimize: [`${srcPath}/**/*.{png,jpg,jpeg}`, `!${srcPath}/**/icons/*`],
|
|
15
|
+
webp: [`${srcPath}/**/*.{png,jpg,jpeg,gif}`, `!${srcPath}/**/icons/*`]
|
|
24
16
|
},
|
|
25
17
|
svg: {
|
|
26
|
-
src: [`${srcPath}/**/*.svg
|
|
27
|
-
ignore: [`!${srcPath}/**/sprites_*/*`, `!${srcPath}/**/_inline*/*`, `!${srcPath}/**/icons*/*`]
|
|
18
|
+
src: [`${srcPath}/**/*.svg`, `!${srcPath}/**/icons/*`]
|
|
28
19
|
}
|
|
29
20
|
}
|
|
30
21
|
}
|
package/core/builder.mjs
CHANGED
|
@@ -34,11 +34,17 @@ export class Builder {
|
|
|
34
34
|
const { context } = this
|
|
35
35
|
const startTime = Date.now()
|
|
36
36
|
|
|
37
|
+
const shouldClean = context.config.build.clean
|
|
38
|
+
|
|
37
39
|
logger.info('build', `Building in ${context.mode} mode`)
|
|
38
40
|
|
|
39
41
|
try {
|
|
40
42
|
// 1. クリーンアップ
|
|
41
|
-
|
|
43
|
+
if (shouldClean) {
|
|
44
|
+
await this.clean()
|
|
45
|
+
} else {
|
|
46
|
+
logger.info('build', 'Skipping clean (clean: false)')
|
|
47
|
+
}
|
|
42
48
|
|
|
43
49
|
// 2. 並列ビルド(軽量タスク + スプライト)
|
|
44
50
|
const parallelTasks = []
|
package/core/cache.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import {
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -76,13 +76,12 @@ export class CacheManager {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
79
|
+
* ハッシュ計算(ファイル内容のみ)
|
|
80
80
|
*/
|
|
81
81
|
async computeHash(filePath) {
|
|
82
82
|
try {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
return createHash('md5').update(content).update(stats.mtime.toISOString()).digest('hex')
|
|
83
|
+
const content = await readFile(filePath)
|
|
84
|
+
return createHash('md5').update(content).digest('hex')
|
|
86
85
|
} catch {
|
|
87
86
|
return `error-${Math.random()}`
|
|
88
87
|
}
|
package/core/context.mjs
CHANGED
package/core/graph.mjs
CHANGED
package/core/watcher.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export async function watcherTask(context, options = {}) {
|
|
|
17
17
|
class FileWatcher {
|
|
18
18
|
constructor(context) {
|
|
19
19
|
this.context = context
|
|
20
|
-
this.
|
|
20
|
+
this.watcher = null
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
async start() {
|
|
@@ -25,331 +25,226 @@ class FileWatcher {
|
|
|
25
25
|
|
|
26
26
|
// 初回ビルド(依存関係グラフ構築のため)
|
|
27
27
|
logger.info('watch', 'Building initial dependency graph...')
|
|
28
|
+
|
|
29
|
+
const initialTasks = []
|
|
28
30
|
if (this.context.taskRegistry?.pug) {
|
|
29
|
-
|
|
31
|
+
initialTasks.push(this.context.taskRegistry.pug(this.context))
|
|
30
32
|
}
|
|
33
|
+
if (this.context.taskRegistry?.sass) {
|
|
34
|
+
initialTasks.push(this.context.taskRegistry.sass(this.context))
|
|
35
|
+
}
|
|
36
|
+
if (this.context.taskRegistry?.script) {
|
|
37
|
+
initialTasks.push(this.context.taskRegistry.script(this.context))
|
|
38
|
+
}
|
|
39
|
+
await Promise.all(initialTasks)
|
|
40
|
+
|
|
41
|
+
this.watcher = chokidar
|
|
42
|
+
.watch([paths.src, paths.public], {
|
|
43
|
+
ignoreInitial: true,
|
|
44
|
+
ignored: [/(^|[\/\\])\./, /node_modules/, /\.git/],
|
|
45
|
+
persistent: true,
|
|
46
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
|
|
47
|
+
})
|
|
48
|
+
.on('change', filePath => this.handleChange(filePath))
|
|
49
|
+
.on('add', filePath => this.handleAdd(filePath))
|
|
50
|
+
.on('unlink', filePath => this.handleUnlink(filePath))
|
|
31
51
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Sass監視
|
|
36
|
-
this.watchSass(paths.src)
|
|
52
|
+
logger.info('watch', 'File watching started')
|
|
53
|
+
}
|
|
37
54
|
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
// ---- ルーティング ----
|
|
56
|
+
handleChange(filePath) {
|
|
57
|
+
if (this.isPublic(filePath)) return this.onPublicChange(filePath, 'change')
|
|
58
|
+
if (filePath.endsWith('.pug')) return this.onPugChange(filePath)
|
|
59
|
+
if (filePath.endsWith('.scss')) return this.onSassChange(filePath)
|
|
60
|
+
if (this.isScript(filePath)) return this.onScriptChange(filePath)
|
|
61
|
+
if (filePath.endsWith('.svg') && !this.isIcons(filePath)) return this.onSvgChange(filePath, 'change')
|
|
62
|
+
if (/\.(jpg|jpeg|png|gif)$/i.test(filePath)) return this.onImageChange(filePath, 'change')
|
|
63
|
+
}
|
|
40
64
|
|
|
41
|
-
|
|
42
|
-
this.
|
|
65
|
+
handleAdd(filePath) {
|
|
66
|
+
if (this.isPublic(filePath)) return this.onPublicChange(filePath, 'add')
|
|
67
|
+
if (filePath.endsWith('.pug')) return this.onPugChange(filePath)
|
|
68
|
+
if (filePath.endsWith('.scss')) return this.onSassChange(filePath)
|
|
69
|
+
if (this.isScript(filePath)) return this.onScriptChange(filePath)
|
|
70
|
+
if (filePath.endsWith('.svg') && !this.isIcons(filePath)) return this.onSvgChange(filePath, 'add')
|
|
71
|
+
if (/\.(jpg|jpeg|png|gif)$/i.test(filePath)) return this.onImageChange(filePath, 'add')
|
|
72
|
+
}
|
|
43
73
|
|
|
44
|
-
|
|
45
|
-
this.
|
|
74
|
+
handleUnlink(filePath) {
|
|
75
|
+
if (this.isPublic(filePath)) return this.onPublicUnlink(filePath)
|
|
76
|
+
if (filePath.endsWith('.pug')) return this.onPugUnlink(filePath)
|
|
77
|
+
if (filePath.endsWith('.scss')) return this.onSassUnlink(filePath)
|
|
78
|
+
if (this.isScript(filePath)) return this.onScriptUnlink(filePath)
|
|
79
|
+
if (filePath.endsWith('.svg') && !this.isIcons(filePath)) return this.onSvgUnlink(filePath)
|
|
80
|
+
if (/\.(jpg|jpeg|png|gif)$/i.test(filePath)) return this.onImageUnlink(filePath)
|
|
81
|
+
}
|
|
46
82
|
|
|
47
|
-
|
|
48
|
-
this.watchPublic(paths.public)
|
|
83
|
+
// ---- 判定ヘルパー ----
|
|
49
84
|
|
|
50
|
-
|
|
85
|
+
isPublic(filePath) {
|
|
86
|
+
return filePath.startsWith(this.context.paths.public)
|
|
51
87
|
}
|
|
52
88
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
watchPug(basePath) {
|
|
57
|
-
const watcher = chokidar.watch(basePath, {
|
|
58
|
-
ignoreInitial: true,
|
|
59
|
-
ignored: [
|
|
60
|
-
/(^|[\/\\])\../, // 隠しファイル
|
|
61
|
-
/node_modules/,
|
|
62
|
-
/\.git/
|
|
63
|
-
],
|
|
64
|
-
persistent: true,
|
|
65
|
-
awaitWriteFinish: {
|
|
66
|
-
stabilityThreshold: 100,
|
|
67
|
-
pollInterval: 50
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
watcher.on('change', async path => {
|
|
72
|
-
// .pugファイルのみ処理
|
|
73
|
-
if (!path.endsWith('.pug')) {
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
const relPath = relative(basePath, path)
|
|
77
|
-
logger.info('change', `pug: ${relPath}`)
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
// パーシャルの場合、依存する親ファイルも再ビルド
|
|
81
|
-
const affectedFiles = this.context.graph.getAffectedParents(path)
|
|
82
|
-
|
|
83
|
-
if (affectedFiles.length > 0) {
|
|
84
|
-
logger.info('pug', `Rebuilding ${affectedFiles.length} affected file(s)`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// キャッシュ無効化
|
|
88
|
-
this.context.cache.invalidatePugTemplate(path)
|
|
89
|
-
affectedFiles.forEach(f => this.context.cache.invalidatePugTemplate(f))
|
|
90
|
-
|
|
91
|
-
// Pugタスクを実行(変更されたファイルのみ)
|
|
92
|
-
if (this.context.taskRegistry?.pug) {
|
|
93
|
-
await this.context.taskRegistry.pug(this.context, {
|
|
94
|
-
files: [path, ...affectedFiles]
|
|
95
|
-
})
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
this.reload()
|
|
99
|
-
} catch (error) {
|
|
100
|
-
logger.error('watch', `Pug build failed: ${error.message}`)
|
|
101
|
-
}
|
|
102
|
-
})
|
|
89
|
+
isScript(filePath) {
|
|
90
|
+
return (filePath.endsWith('.ts') || filePath.endsWith('.js')) && !filePath.endsWith('.d.ts')
|
|
91
|
+
}
|
|
103
92
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
93
|
+
isIcons(filePath) {
|
|
94
|
+
// Windows 対応: セパレータを正規化
|
|
95
|
+
return filePath.replace(/\\/g, '/').includes('/icons/')
|
|
96
|
+
}
|
|
107
97
|
|
|
108
|
-
|
|
109
|
-
this.context.cache.invalidatePugTemplate(path)
|
|
110
|
-
this.context.graph.clearDependencies(path)
|
|
98
|
+
// ---- Pug ----
|
|
111
99
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
100
|
+
async onPugChange(filePath) {
|
|
101
|
+
const { paths, graph, cache, taskRegistry } = this.context
|
|
102
|
+
const relPath = relative(paths.src, filePath)
|
|
103
|
+
logger.info('change', `pug: ${relPath}`)
|
|
104
|
+
try {
|
|
105
|
+
const affectedFiles = graph.getAffectedParents(filePath)
|
|
106
|
+
if (affectedFiles.length > 0) logger.info('pug', `Rebuilding ${affectedFiles.length} affected file(s)`)
|
|
107
|
+
cache.invalidatePugTemplate(filePath)
|
|
108
|
+
affectedFiles.forEach(f => cache.invalidatePugTemplate(f))
|
|
109
|
+
if (taskRegistry?.pug) {
|
|
110
|
+
await taskRegistry.pug(this.context, { files: [filePath, ...affectedFiles] })
|
|
115
111
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
this.watchers.push(watcher)
|
|
112
|
+
this.reload()
|
|
113
|
+
} catch (error) {
|
|
114
|
+
logger.error('watch', `Pug build failed: ${error.message}`)
|
|
115
|
+
}
|
|
121
116
|
}
|
|
122
117
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
watcher.on('change', async path => {
|
|
135
|
-
// .scssファイルのみ処理
|
|
136
|
-
if (!path.endsWith('.scss')) {
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
const relPath = relative(basePath, path)
|
|
140
|
-
logger.info('change', `sass: ${relPath}`)
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
// Sassタスクを実行
|
|
144
|
-
if (this.context.taskRegistry?.sass) {
|
|
145
|
-
await this.context.taskRegistry.sass(this.context)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
this.injectCSS()
|
|
149
|
-
} catch (error) {
|
|
150
|
-
logger.error('watch', `Sass build failed: ${error.message}`)
|
|
151
|
-
}
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
watcher.on('unlink', async path => {
|
|
155
|
-
if (!path.endsWith('.scss')) return
|
|
156
|
-
const relPath = relative(basePath, path)
|
|
157
|
-
if (basename(path).startsWith('_')) {
|
|
158
|
-
logger.info('unlink', relPath)
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
const distPath = resolve(this.context.paths.dist, relPath.replace(/\.scss$/, '.css'))
|
|
162
|
-
await this.deleteDistFile(distPath, relPath)
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
this.watchers.push(watcher)
|
|
118
|
+
async onPugUnlink(filePath) {
|
|
119
|
+
const { paths, cache, graph } = this.context
|
|
120
|
+
const relPath = relative(paths.src, filePath)
|
|
121
|
+
cache.invalidatePugTemplate(filePath)
|
|
122
|
+
graph.clearDependencies(filePath)
|
|
123
|
+
if (basename(filePath).startsWith('_')) {
|
|
124
|
+
logger.info('unlink', relPath)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
const distPath = resolve(paths.dist, relPath.replace(/\.pug$/, '.html'))
|
|
128
|
+
await this.deleteDistFile(distPath, relPath)
|
|
166
129
|
}
|
|
167
130
|
|
|
168
|
-
|
|
169
|
-
* Script監視
|
|
170
|
-
*/
|
|
171
|
-
watchScript(basePath) {
|
|
172
|
-
const watcher = chokidar.watch(basePath, {
|
|
173
|
-
ignoreInitial: true,
|
|
174
|
-
ignored: [/(^|[\/\\])\../, /node_modules/, /\.git/, /\.d\.ts$/],
|
|
175
|
-
persistent: true,
|
|
176
|
-
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
watcher.on('change', async path => {
|
|
180
|
-
// .ts/.jsファイルのみ処理(.d.tsを除く)
|
|
181
|
-
if (!(path.endsWith('.ts') || path.endsWith('.js')) || path.endsWith('.d.ts')) {
|
|
182
|
-
return
|
|
183
|
-
}
|
|
184
|
-
const relPath = relative(basePath, path)
|
|
185
|
-
logger.info('change', `script: ${relPath}`)
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
// Scriptタスクを実行
|
|
189
|
-
if (this.context.taskRegistry?.script) {
|
|
190
|
-
await this.context.taskRegistry.script(this.context)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
this.reload()
|
|
194
|
-
} catch (error) {
|
|
195
|
-
logger.error('watch', `Script build failed: ${error.message}`)
|
|
196
|
-
}
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
watcher.on('unlink', async path => {
|
|
200
|
-
if (!(path.endsWith('.ts') || path.endsWith('.js')) || path.endsWith('.d.ts')) return
|
|
201
|
-
const relPath = relative(basePath, path)
|
|
202
|
-
if (basename(path).startsWith('_')) {
|
|
203
|
-
logger.info('unlink', relPath)
|
|
204
|
-
return
|
|
205
|
-
}
|
|
206
|
-
const distPath = resolve(this.context.paths.dist, relPath.replace(/\.ts$/, '.js'))
|
|
207
|
-
await this.deleteDistFile(distPath, relPath)
|
|
208
|
-
})
|
|
131
|
+
// ---- Sass ----
|
|
209
132
|
|
|
210
|
-
|
|
133
|
+
async onSassChange(filePath) {
|
|
134
|
+
const relPath = relative(this.context.paths.src, filePath)
|
|
135
|
+
logger.info('change', `sass: ${relPath}`)
|
|
136
|
+
try {
|
|
137
|
+
if (this.context.taskRegistry?.sass) await this.context.taskRegistry.sass(this.context, { files: [filePath] })
|
|
138
|
+
this.injectCSS()
|
|
139
|
+
} catch (error) {
|
|
140
|
+
logger.error('watch', `Sass build failed: ${error.message}`)
|
|
141
|
+
}
|
|
211
142
|
}
|
|
212
143
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
persistent: true,
|
|
221
|
-
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
const handleSvgChange = async (path, event) => {
|
|
225
|
-
// .svgファイルのみ処理(iconsディレクトリは除外)
|
|
226
|
-
// Windows対応: パスセパレータを正規化
|
|
227
|
-
const normalizedPath = path.replace(/\\/g, '/')
|
|
228
|
-
if (!path.endsWith('.svg') || normalizedPath.includes('/icons/')) {
|
|
229
|
-
return
|
|
230
|
-
}
|
|
231
|
-
const relPath = relative(basePath, path)
|
|
232
|
-
logger.info(event, `svg: ${relPath}`)
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
// SVGタスクを実行(変更されたファイルのみ)
|
|
236
|
-
if (this.context.taskRegistry?.svg) {
|
|
237
|
-
await this.context.taskRegistry.svg(this.context, {
|
|
238
|
-
files: [path]
|
|
239
|
-
})
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
this.reload()
|
|
243
|
-
} catch (error) {
|
|
244
|
-
logger.error('watch', `SVG processing failed: ${error.message}`)
|
|
245
|
-
}
|
|
144
|
+
async onSassUnlink(filePath) {
|
|
145
|
+
const { paths, sassGraph } = this.context
|
|
146
|
+
const relPath = relative(paths.src, filePath)
|
|
147
|
+
sassGraph.clearDependencies(filePath)
|
|
148
|
+
if (basename(filePath).startsWith('_')) {
|
|
149
|
+
logger.info('unlink', relPath)
|
|
150
|
+
return
|
|
246
151
|
}
|
|
152
|
+
const distPath = resolve(paths.dist, relPath.replace(/\.scss$/, '.css'))
|
|
153
|
+
await this.deleteDistFile(distPath, relPath)
|
|
154
|
+
}
|
|
247
155
|
|
|
248
|
-
|
|
249
|
-
watcher.on('add', path => handleSvgChange(path, 'add'))
|
|
250
|
-
|
|
251
|
-
watcher.on('unlink', async path => {
|
|
252
|
-
const normalizedPath = path.replace(/\\/g, '/')
|
|
253
|
-
if (!path.endsWith('.svg') || normalizedPath.includes('/icons/')) return
|
|
254
|
-
const relPath = relative(basePath, path)
|
|
255
|
-
const distPath = resolve(this.context.paths.dist, relPath)
|
|
256
|
-
await this.deleteDistFile(distPath, relPath)
|
|
257
|
-
})
|
|
156
|
+
// ---- Script ----
|
|
258
157
|
|
|
259
|
-
|
|
158
|
+
async onScriptChange(filePath) {
|
|
159
|
+
const relPath = relative(this.context.paths.src, filePath)
|
|
160
|
+
logger.info('change', `script: ${relPath}`)
|
|
161
|
+
try {
|
|
162
|
+
if (this.context.taskRegistry?.script) await this.context.taskRegistry.script(this.context, { files: [filePath] })
|
|
163
|
+
this.reload()
|
|
164
|
+
} catch (error) {
|
|
165
|
+
logger.error('watch', `Script build failed: ${error.message}`)
|
|
166
|
+
}
|
|
260
167
|
}
|
|
261
168
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
persistent: true,
|
|
270
|
-
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
const handleImageChange = async (path, event) => {
|
|
274
|
-
// 画像ファイルのみ処理
|
|
275
|
-
if (!/\.(jpg|jpeg|png|gif)$/i.test(path)) {
|
|
276
|
-
return
|
|
277
|
-
}
|
|
278
|
-
const relPath = relative(basePath, path)
|
|
279
|
-
logger.info(event, `image: ${relPath}`)
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
// 追加・変更時: 画像を処理
|
|
283
|
-
if (this.context.taskRegistry?.image) {
|
|
284
|
-
await this.context.taskRegistry.image(this.context, {
|
|
285
|
-
files: [path]
|
|
286
|
-
})
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
this.reload()
|
|
290
|
-
} catch (error) {
|
|
291
|
-
logger.error('watch', `Image processing failed: ${error.message}`)
|
|
292
|
-
}
|
|
169
|
+
async onScriptUnlink(filePath) {
|
|
170
|
+
const { paths, scriptGraph } = this.context
|
|
171
|
+
const relPath = relative(paths.src, filePath)
|
|
172
|
+
scriptGraph.clearDependencies(filePath)
|
|
173
|
+
if (basename(filePath).startsWith('_')) {
|
|
174
|
+
logger.info('unlink', relPath)
|
|
175
|
+
return
|
|
293
176
|
}
|
|
177
|
+
const distPath = resolve(paths.dist, relPath.replace(/\.ts$/, '.js'))
|
|
178
|
+
await this.deleteDistFile(distPath, relPath)
|
|
179
|
+
}
|
|
294
180
|
|
|
295
|
-
|
|
296
|
-
watcher.on('add', path => handleImageChange(path, 'add'))
|
|
181
|
+
// ---- SVG ----
|
|
297
182
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
})
|
|
183
|
+
async onSvgChange(filePath, event) {
|
|
184
|
+
const relPath = relative(this.context.paths.src, filePath)
|
|
185
|
+
logger.info(event, `svg: ${relPath}`)
|
|
186
|
+
try {
|
|
187
|
+
if (this.context.taskRegistry?.svg) {
|
|
188
|
+
await this.context.taskRegistry.svg(this.context, { files: [filePath] })
|
|
189
|
+
}
|
|
190
|
+
this.reload()
|
|
191
|
+
} catch (error) {
|
|
192
|
+
logger.error('watch', `SVG processing failed: ${error.message}`)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
307
195
|
|
|
308
|
-
|
|
196
|
+
async onSvgUnlink(filePath) {
|
|
197
|
+
const relPath = relative(this.context.paths.src, filePath)
|
|
198
|
+
const distPath = resolve(this.context.paths.dist, relPath)
|
|
199
|
+
await this.deleteDistFile(distPath, relPath)
|
|
309
200
|
}
|
|
310
201
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
const handlePublicChange = async (path, event) => {
|
|
323
|
-
const relPath = relative(basePath, path)
|
|
324
|
-
logger.info(event, `public: ${relPath}`)
|
|
325
|
-
|
|
326
|
-
try {
|
|
327
|
-
// Copyタスクを実行
|
|
328
|
-
if (this.context.taskRegistry?.copy) {
|
|
329
|
-
await this.context.taskRegistry.copy(this.context)
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
this.reload()
|
|
333
|
-
} catch (error) {
|
|
334
|
-
logger.error('watch', `Copy failed: ${error.message}`)
|
|
202
|
+
// ---- Image ----
|
|
203
|
+
|
|
204
|
+
async onImageChange(filePath, event) {
|
|
205
|
+
const relPath = relative(this.context.paths.src, filePath)
|
|
206
|
+
logger.info(event, `image: ${relPath}`)
|
|
207
|
+
try {
|
|
208
|
+
if (this.context.taskRegistry?.image) {
|
|
209
|
+
await this.context.taskRegistry.image(this.context, { files: [filePath] })
|
|
335
210
|
}
|
|
211
|
+
this.reload()
|
|
212
|
+
} catch (error) {
|
|
213
|
+
logger.error('watch', `Image processing failed: ${error.message}`)
|
|
336
214
|
}
|
|
215
|
+
}
|
|
337
216
|
|
|
338
|
-
|
|
339
|
-
|
|
217
|
+
async onImageUnlink(filePath) {
|
|
218
|
+
const { paths, config } = this.context
|
|
219
|
+
const relPath = relative(paths.src, filePath)
|
|
220
|
+
const useWebp = config.build.imageOptimization === 'webp'
|
|
221
|
+
const ext = extname(filePath)
|
|
222
|
+
const destRelPath = useWebp ? relPath.replace(new RegExp(`\\${ext}$`, 'i'), '.webp') : relPath
|
|
223
|
+
const distPath = resolve(paths.dist, destRelPath)
|
|
224
|
+
await this.deleteDistFile(distPath, relPath)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---- Public ----
|
|
340
228
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
229
|
+
async onPublicChange(filePath, event) {
|
|
230
|
+
const relPath = relative(this.context.paths.public, filePath)
|
|
231
|
+
logger.info(event, `public: ${relPath}`)
|
|
232
|
+
try {
|
|
233
|
+
if (this.context.taskRegistry?.copy) await this.context.taskRegistry.copy(this.context, { files: [filePath] })
|
|
234
|
+
this.reload()
|
|
235
|
+
} catch (error) {
|
|
236
|
+
logger.error('watch', `Copy failed: ${error.message}`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
346
239
|
|
|
347
|
-
|
|
240
|
+
async onPublicUnlink(filePath) {
|
|
241
|
+
const relPath = relative(this.context.paths.public, filePath)
|
|
242
|
+
const distPath = resolve(this.context.paths.dist, relPath)
|
|
243
|
+
await this.deleteDistFile(distPath, relPath)
|
|
348
244
|
}
|
|
349
245
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
*/
|
|
246
|
+
// ---- 共通ヘルパー ----
|
|
247
|
+
|
|
353
248
|
reload() {
|
|
354
249
|
if (this.context.server) {
|
|
355
250
|
setTimeout(() => {
|
|
@@ -358,9 +253,6 @@ class FileWatcher {
|
|
|
358
253
|
}
|
|
359
254
|
}
|
|
360
255
|
|
|
361
|
-
/**
|
|
362
|
-
* dist内のファイルを削除してブラウザをリロード
|
|
363
|
-
*/
|
|
364
256
|
async deleteDistFile(distPath, relPath) {
|
|
365
257
|
try {
|
|
366
258
|
await rm(distPath, { force: true })
|
|
@@ -371,20 +263,14 @@ class FileWatcher {
|
|
|
371
263
|
}
|
|
372
264
|
}
|
|
373
265
|
|
|
374
|
-
/**
|
|
375
|
-
* CSSインジェクション
|
|
376
|
-
*/
|
|
377
266
|
injectCSS() {
|
|
378
267
|
if (this.context.server) {
|
|
379
268
|
this.context.server.reloadCSS()
|
|
380
269
|
}
|
|
381
270
|
}
|
|
382
271
|
|
|
383
|
-
/**
|
|
384
|
-
* 監視停止
|
|
385
|
-
*/
|
|
386
272
|
async stop() {
|
|
387
|
-
|
|
273
|
+
if (this.watcher) await this.watcher.close()
|
|
388
274
|
logger.info('watch', 'File watching stopped')
|
|
389
275
|
}
|
|
390
276
|
}
|
package/generate/page.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { ensureFileDir } from '../utils/file.mjs'
|
|
|
4
4
|
|
|
5
5
|
export async function generatePage(filePath, html, paths) {
|
|
6
6
|
const relativePath = relative(paths.src, filePath)
|
|
7
|
-
const outputPath = resolve(paths.dist, relativePath.replace(
|
|
7
|
+
const outputPath = resolve(paths.dist, relativePath.replace(/\.pug$/, '.html'))
|
|
8
8
|
|
|
9
9
|
await ensureFileDir(outputPath)
|
|
10
10
|
await writeFile(outputPath, html, 'utf8')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pugkit",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A build tool for Pug-based projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"esbuild": "^0.25.5",
|
|
50
50
|
"glob": "^13.0.6",
|
|
51
51
|
"image-size": "^2.0.2",
|
|
52
|
+
"js-beautify": "^1.15.4",
|
|
52
53
|
"picocolors": "^1.1.1",
|
|
53
54
|
"postcss": "^8.4.49",
|
|
54
|
-
"prettier": "^3.8.0",
|
|
55
55
|
"pug": "^3.0.3",
|
|
56
56
|
"sass": "^1.89.2",
|
|
57
57
|
"sharp": "^0.34.2",
|
package/tasks/copy.mjs
CHANGED
|
@@ -10,6 +10,21 @@ import { ensureFileDir } from '../utils/file.mjs'
|
|
|
10
10
|
export async function copyTask(context, options = {}) {
|
|
11
11
|
const { paths } = context
|
|
12
12
|
|
|
13
|
+
// 特定のファイルが指定されている場合(watch時)
|
|
14
|
+
if (options.files && Array.isArray(options.files)) {
|
|
15
|
+
logger.info('copy', `Copying ${options.files.length} file(s)`)
|
|
16
|
+
await Promise.all(
|
|
17
|
+
options.files.map(async file => {
|
|
18
|
+
const relativePath = relative(paths.public, file)
|
|
19
|
+
const outputPath = resolve(paths.dist, relativePath)
|
|
20
|
+
await ensureFileDir(outputPath)
|
|
21
|
+
await copyFile(file, outputPath)
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
logger.success('copy', `Copied ${options.files.length} file(s)`)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
const files = await glob('**/*', {
|
|
14
29
|
cwd: paths.public,
|
|
15
30
|
absolute: true,
|
package/tasks/pug.mjs
CHANGED
|
@@ -69,7 +69,7 @@ async function processFile(filePath, context) {
|
|
|
69
69
|
const imageInfo = createImageInfoHelper(filePath, paths, logger, config)
|
|
70
70
|
|
|
71
71
|
const html = template({ Builder: builderVars, imageSize, imageInfo })
|
|
72
|
-
const formatted =
|
|
72
|
+
const formatted = formatHtml(html)
|
|
73
73
|
await generatePage(filePath, formatted, paths)
|
|
74
74
|
} catch (error) {
|
|
75
75
|
logger.error('pug', `Failed: ${basename(filePath)} - ${error.message}`)
|
package/tasks/sass.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { glob } from 'glob'
|
|
2
2
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { relative, resolve, basename, extname } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
4
5
|
import * as sass from 'sass'
|
|
5
6
|
import postcss from 'postcss'
|
|
6
7
|
import autoprefixer from 'autoprefixer'
|
|
@@ -12,36 +13,64 @@ import { ensureFileDir } from '../utils/file.mjs'
|
|
|
12
13
|
* Sassビルドタスク
|
|
13
14
|
*/
|
|
14
15
|
export async function sassTask(context, options = {}) {
|
|
15
|
-
const { paths, config, isProduction } = context
|
|
16
|
+
const { paths, config, isProduction, isDevelopment, sassGraph, cache } = context
|
|
16
17
|
|
|
17
18
|
// debugモードはdevモード時のみ有効
|
|
18
19
|
const isDebugMode = !isProduction && config.debug
|
|
19
20
|
|
|
20
|
-
// 1.
|
|
21
|
-
const
|
|
21
|
+
// 1. ビルド対象ファイルの取得(非パーシャル)
|
|
22
|
+
const allEntryFiles = await glob('**/[^_]*.scss', {
|
|
22
23
|
cwd: paths.src,
|
|
23
24
|
absolute: true,
|
|
24
25
|
ignore: ['**/_*.scss']
|
|
25
26
|
})
|
|
26
27
|
|
|
27
|
-
if (
|
|
28
|
+
if (allEntryFiles.length === 0) {
|
|
28
29
|
logger.skip('sass', 'No files to build')
|
|
29
30
|
return
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
// 2. dev モードでのインクリメンタルビルド
|
|
34
|
+
let filesToBuild = allEntryFiles
|
|
35
|
+
|
|
36
|
+
if (isDevelopment && options.files?.length > 0) {
|
|
37
|
+
const changedFile = options.files[0]
|
|
38
|
+
const isPartial = basename(changedFile).startsWith('_')
|
|
39
|
+
|
|
40
|
+
if (isPartial) {
|
|
41
|
+
// パーシャル変更 → 依存グラフから影響を受けるエントリファイルを特定
|
|
42
|
+
const affected = sassGraph.getAffectedParents(changedFile)
|
|
43
|
+
filesToBuild = affected.filter(f => allEntryFiles.includes(f))
|
|
44
|
+
|
|
45
|
+
if (filesToBuild.length === 0) {
|
|
46
|
+
// グラフにまだ情報がない場合はフルビルド
|
|
47
|
+
filesToBuild = allEntryFiles
|
|
48
|
+
} else {
|
|
49
|
+
logger.info('sass', `Partial changed, rebuilding ${filesToBuild.length} affected file(s)`)
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// 非パーシャル変更 → そのファイルだけリビルド
|
|
53
|
+
filesToBuild = allEntryFiles.filter(f => f === changedFile)
|
|
54
|
+
if (filesToBuild.length === 0) {
|
|
55
|
+
logger.skip('sass', 'Changed file is not a build target')
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
logger.info('sass', `Building ${filesToBuild.length} file(s)`)
|
|
33
62
|
|
|
34
|
-
//
|
|
35
|
-
await Promise.all(
|
|
63
|
+
// 3. 並列コンパイル
|
|
64
|
+
await Promise.all(filesToBuild.map(file => compileSassFile(file, context, isDebugMode)))
|
|
36
65
|
|
|
37
|
-
logger.success('sass', `Built ${
|
|
66
|
+
logger.success('sass', `Built ${filesToBuild.length} file(s)`)
|
|
38
67
|
}
|
|
39
68
|
|
|
40
69
|
/**
|
|
41
70
|
* 個別Sassファイルのコンパイル
|
|
42
71
|
*/
|
|
43
72
|
async function compileSassFile(filePath, context, isDebugMode) {
|
|
44
|
-
const { paths, config, isProduction } = context
|
|
73
|
+
const { paths, config, isProduction, sassGraph } = context
|
|
45
74
|
|
|
46
75
|
try {
|
|
47
76
|
// Sassコンパイル
|
|
@@ -55,6 +84,19 @@ async function compileSassFile(filePath, context, isDebugMode) {
|
|
|
55
84
|
sourceMapIncludeSources: isDebugMode
|
|
56
85
|
})
|
|
57
86
|
|
|
87
|
+
// 依存グラフを更新(loadedUrls からパーシャルの依存関係を構築)
|
|
88
|
+
sassGraph.clearDependencies(filePath)
|
|
89
|
+
if (result.loadedUrls) {
|
|
90
|
+
for (const url of result.loadedUrls) {
|
|
91
|
+
if (url.protocol === 'file:') {
|
|
92
|
+
const depPath = fileURLToPath(url)
|
|
93
|
+
if (depPath !== filePath) {
|
|
94
|
+
sassGraph.addDependency(filePath, depPath)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
58
100
|
let css = result.css
|
|
59
101
|
|
|
60
102
|
// PostCSS処理
|
|
@@ -78,7 +120,7 @@ async function compileSassFile(filePath, context, isDebugMode) {
|
|
|
78
120
|
)
|
|
79
121
|
}
|
|
80
122
|
|
|
81
|
-
const outputRelativePath = relative(paths.src, filePath).replace(
|
|
123
|
+
const outputRelativePath = relative(paths.src, filePath).replace(/\.scss$/, '.css')
|
|
82
124
|
const outputPath = resolve(paths.dist, outputRelativePath)
|
|
83
125
|
|
|
84
126
|
const postcssResult = await postcss(postcssPlugins).process(css, {
|
package/tasks/script.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { glob } from 'glob'
|
|
2
|
-
import { resolve, relative, dirname } from 'node:path'
|
|
2
|
+
import { resolve, relative, dirname, basename } from 'node:path'
|
|
3
3
|
import * as esbuild from 'esbuild'
|
|
4
4
|
import { logger } from '../utils/logger.mjs'
|
|
5
5
|
import { ensureDir } from '../utils/file.mjs'
|
|
@@ -8,29 +8,53 @@ import { ensureDir } from '../utils/file.mjs'
|
|
|
8
8
|
* esbuild(TypeScript/JavaScript)ビルドタスク
|
|
9
9
|
*/
|
|
10
10
|
export async function scriptTask(context, options = {}) {
|
|
11
|
-
const { paths, isProduction, config } = context
|
|
11
|
+
const { paths, isProduction, isDevelopment, config, scriptGraph } = context
|
|
12
12
|
|
|
13
13
|
// 1. ビルド対象ファイルの取得
|
|
14
|
-
const
|
|
14
|
+
const allEntryFiles = await glob('**/[^_]*.{ts,js}', {
|
|
15
15
|
cwd: paths.src,
|
|
16
16
|
absolute: true,
|
|
17
17
|
ignore: ['**/*.d.ts', '**/node_modules/**']
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
if (
|
|
20
|
+
if (allEntryFiles.length === 0) {
|
|
21
21
|
logger.skip('script', 'No files to build')
|
|
22
22
|
return
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// 2. dev モードでのインクリメンタルビルド
|
|
26
|
+
let filesToBuild = allEntryFiles
|
|
27
|
+
|
|
28
|
+
if (isDevelopment && options.files?.length > 0) {
|
|
29
|
+
const changedFile = options.files[0]
|
|
30
|
+
const isPartial = basename(changedFile).startsWith('_')
|
|
31
|
+
|
|
32
|
+
if (isPartial) {
|
|
33
|
+
// パーシャル変更 → 依存グラフから影響を受けるエントリファイルを特定
|
|
34
|
+
const affected = scriptGraph.getAffectedParents(changedFile)
|
|
35
|
+
filesToBuild = affected.filter(f => allEntryFiles.includes(f))
|
|
36
|
+
|
|
37
|
+
if (filesToBuild.length === 0) {
|
|
38
|
+
// グラフにまだ情報がない場合はフルビルド
|
|
39
|
+
filesToBuild = allEntryFiles
|
|
40
|
+
} else {
|
|
41
|
+
logger.info('script', `Partial changed, rebuilding ${filesToBuild.length} affected file(s)`)
|
|
42
|
+
}
|
|
43
|
+
} else if (allEntryFiles.includes(changedFile)) {
|
|
44
|
+
// 非パーシャルのエントリファイル → そのファイルだけリビルド
|
|
45
|
+
filesToBuild = [changedFile]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logger.info('script', `Building ${filesToBuild.length} file(s)`)
|
|
26
50
|
|
|
27
51
|
// debugモードはdevモード時のみ有効
|
|
28
52
|
const isDebugMode = !isProduction && config.debug
|
|
29
53
|
|
|
30
54
|
try {
|
|
31
|
-
//
|
|
55
|
+
// 3. esbuild設定
|
|
32
56
|
const esbuildConfig = {
|
|
33
|
-
entryPoints:
|
|
57
|
+
entryPoints: filesToBuild,
|
|
34
58
|
outdir: paths.dist,
|
|
35
59
|
outbase: paths.src,
|
|
36
60
|
bundle: true,
|
|
@@ -41,7 +65,7 @@ export async function scriptTask(context, options = {}) {
|
|
|
41
65
|
write: true,
|
|
42
66
|
sourcemap: isDebugMode,
|
|
43
67
|
minify: false,
|
|
44
|
-
metafile:
|
|
68
|
+
metafile: true,
|
|
45
69
|
logLevel: 'error',
|
|
46
70
|
keepNames: false,
|
|
47
71
|
external: [],
|
|
@@ -57,7 +81,7 @@ export async function scriptTask(context, options = {}) {
|
|
|
57
81
|
esbuildConfig.drop = ['console', 'debugger']
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
//
|
|
84
|
+
// 4. ビルド実行
|
|
61
85
|
await ensureDir(paths.dist)
|
|
62
86
|
const result = await esbuild.build(esbuildConfig)
|
|
63
87
|
|
|
@@ -65,7 +89,24 @@ export async function scriptTask(context, options = {}) {
|
|
|
65
89
|
throw new Error(`esbuild errors: ${result.errors.length}`)
|
|
66
90
|
}
|
|
67
91
|
|
|
68
|
-
|
|
92
|
+
// 5. 依存グラフを更新(metafile から依存関係を構築)
|
|
93
|
+
if (result.metafile) {
|
|
94
|
+
for (const [output, meta] of Object.entries(result.metafile.outputs)) {
|
|
95
|
+
if (!meta.entryPoint) continue
|
|
96
|
+
const entryPath = resolve(paths.root, meta.entryPoint)
|
|
97
|
+
|
|
98
|
+
scriptGraph.clearDependencies(entryPath)
|
|
99
|
+
|
|
100
|
+
for (const inputPath of Object.keys(meta.inputs)) {
|
|
101
|
+
const absInputPath = resolve(paths.root, inputPath)
|
|
102
|
+
if (absInputPath !== entryPath) {
|
|
103
|
+
scriptGraph.addDependency(entryPath, absInputPath)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
logger.success('script', `Built ${filesToBuild.length} file(s)`)
|
|
69
110
|
} catch (error) {
|
|
70
111
|
logger.error('script', error.message)
|
|
71
112
|
throw error
|
package/transform/html.mjs
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import pkg from 'js-beautify'
|
|
2
|
+
const { html: beautifyHtml } = pkg
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
+
export function formatHtml(html, options = {}) {
|
|
4
5
|
const cleaned = html.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
5
6
|
|
|
6
|
-
return
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
return beautifyHtml(cleaned, {
|
|
8
|
+
indent_size: options.tabWidth || 2,
|
|
9
|
+
indent_with_tabs: options.useTabs || false,
|
|
10
|
+
max_preserve_newlines: 1,
|
|
11
|
+
preserve_newlines: false,
|
|
12
|
+
end_with_newline: true,
|
|
13
|
+
extra_liners: [],
|
|
14
|
+
wrap_line_length: 0,
|
|
15
|
+
inline: [],
|
|
16
|
+
content_unformatted: ['script', 'style', 'pre']
|
|
13
17
|
})
|
|
14
18
|
}
|