pugkit 1.0.0 → 1.0.2

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 CHANGED
@@ -1,55 +1,55 @@
1
1
  # pugkit
2
2
 
3
+ <p>
4
+ <a aria-label="NPM version" href="https://www.npmjs.com/package/pugkit">
5
+ <img alt="" src="https://img.shields.io/npm/v/pugkit.svg?style=for-the-badge&labelColor=212121">
6
+ </a>
7
+ <a aria-label="License" href="https://github.com/mfxgu2i/pugkit/blob/main/LICENSE">
8
+ <img alt="" src="https://img.shields.io/npm/l/pugkit.svg?style=for-the-badge&labelColor=212121">
9
+ </a>
10
+ </p>
11
+
3
12
  ## About
4
13
 
5
14
  pugkitは静的サイト制作に特化したビルドツールです。
6
15
  納品向きの綺麗なHTMLと、ファイル構成に制約のないアセットファイルを出力可能です。
7
16
 
8
- ## Installation
17
+ ## How To Use
9
18
 
10
- ```bash
11
- npm install pugkit
19
+ ```sh
20
+ $ npm install --save-dev pugkit
21
+ $ touch ./src/index.pug
12
22
  ```
13
23
 
14
- ## Usage
15
-
16
- ### Development Mode
17
-
18
- ファイルの変更を監視し、ブラウザの自動リロードを行います。
19
-
20
- ```bash
21
- pugkit
22
- # or
23
- pugkit dev
24
- # or
25
- pugkit watch
26
- ```
27
-
28
- ### Production Build
29
-
30
- 製品用ファイルを生成します。
24
+ `package.json` にスクリプトを追加します。
31
25
 
32
- ```bash
33
- pugkit build
26
+ ```json
27
+ "scripts": {
28
+ "start": "pugkit",
29
+ "build": "pugkit build",
30
+ "sprite": "pugkit sprite"
31
+ }
34
32
  ```
35
33
 
36
- ### SVG Sprite Generation
37
-
38
- アイコン用のSVGスプライトを生成します。
34
+ ## Commands
39
35
 
40
- ```bash
41
- pugkit sprite
42
- ```
36
+ | コマンド | 内容 |
37
+ | --------------- | ----------------------------- |
38
+ | `pugkit` | 開発モード(Ctrl + C で停止) |
39
+ | `pugkit build` | 本番ビルド |
40
+ | `pugkit sprite` | SVGスプライト生成 |
43
41
 
44
42
  ## Configuration
45
43
 
46
- プロジェクトのルートに `pugkit.config.mjs` を配置することで、ビルド設定をカスタマイズできます。
44
+ プロジェクトルートに`pugkit.config.mjs`を配置することで、ビルド設定をカスタマイズできます。
47
45
 
48
46
  ```js
49
47
  // pugkit.config.mjs
50
- export default {
48
+ import { defineConfig } from 'pugkit'
49
+
50
+ export default defineConfig({
51
51
  siteUrl: 'https://example.com/',
52
- subdir: 'subdirectory',
52
+ subdir: '',
53
53
  debug: false,
54
54
  server: {
55
55
  port: 5555,
@@ -57,42 +57,30 @@ export default {
57
57
  startPath: '/'
58
58
  },
59
59
  build: {
60
- imageOptimization: 'webp', // 'webp' | 'compress' | false
61
- imageOptions: {
62
- webp: {
63
- quality: 90,
64
- effort: 6
65
- },
66
- jpeg: {
67
- quality: 75,
68
- progressive: true
69
- },
70
- png: {
71
- quality: 85,
72
- compressionLevel: 6
73
- }
74
- }
60
+ imageOptimization: 'webp'
75
61
  }
76
- }
62
+ })
77
63
  ```
78
64
 
79
- ### Configuration Options
80
-
81
- | Option | Description | Default |
82
- | ------------------------- | -------------------------------- | ------------- |
83
- | `debug` | デバッグモード(開発時のみ有効) | `false` |
84
- | `server.port` | 開発サーバーのポート番号 | `5555` |
85
- | `server.host` | 開発サーバーのホスト | `'localhost'` |
86
- | `server.startPath` | サーバー起動時に開くパス | `'/'` |
87
- | `server.open` | サーバー起動時にブラウザを開く | `false` |
88
- | `build.imageOptimization` | 画像最適化の方式 | `'webp'` |
89
- | `build.imageOptions` | 画像最適化の詳細設定 | - |
65
+ | Option | Description | Type / Values | Default |
66
+ | ------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------- | ------------- |
67
+ | `siteUrl` | サイトのベースURL(`Builder.url` に使用) | `string` | `''` |
68
+ | `subdir` | サブディレクトリのパス | `string` | `''` |
69
+ | `debug` | デバッグモード(開発時のみ有効) | `boolean` | `false` |
70
+ | `server.port` | 開発サーバーのポート番号 | `number` | `5555` |
71
+ | `server.host` | 開発サーバーのホスト | `string` | `'localhost'` |
72
+ | `server.startPath` | サーバー起動時に開くパス | `string` | `'/'` |
73
+ | `server.open` | サーバー起動時にブラウザを開く | `boolean` | `false` |
74
+ | `build.imageOptimization` | 画像最適化の方式 | `'webp'` \| `'compress'` \| `false` | `'webp'` |
75
+ | `build.imageOptions.webp` | WebP変換オプション([Sharp WebP options](https://sharp.pixelplumbing.com/api-output#webp)) | `object` | - |
76
+ | `build.imageOptions.jpeg` | JPEG圧縮オプション([Sharp JPEG options](https://sharp.pixelplumbing.com/api-output#jpeg)) | `object` | - |
77
+ | `build.imageOptions.png` | PNG圧縮オプション([Sharp PNG options](https://sharp.pixelplumbing.com/api-output#png)) | `object` | - |
90
78
 
91
79
  ## Features
92
80
 
93
81
  ### Pug Templates
94
82
 
95
- Pugテンプレート内では、`Builder` オブジェクトと `imageInfo()` 関数が使用できます。
83
+ Pugテンプレート内では `Builder` オブジェクトと `imageInfo()` 関数が使用できます。
96
84
 
97
85
  #### Builder Object
98
86
 
@@ -122,8 +110,6 @@ meta(property='og:url', content=Builder.url.href)
122
110
  img(src=info.src width=info.width height=info.height alt='')
123
111
  ```
124
112
 
125
- **返り値**
126
-
127
113
  | Property | Type | Description |
128
114
  | -------- | -------------------------------- | ----------------------------------------------------- |
129
115
  | `src` | `string` | 最適化設定に応じたパス(webpモード時は `.webp` パス) |
@@ -134,21 +120,102 @@ img(src=info.src width=info.width height=info.height alt='')
134
120
  | `retina` | `{ src: string } \| null` | `@2x` 画像が存在する場合に自動検出 |
135
121
  | `sp` | `{ src, width, height } \| null` | `_sp` 画像が存在する場合に自動検出 |
136
122
 
137
- > **Note:** `imageInfo()`は`src/`配下の画像のみ対応しています。`public/`配下の画像は非対応です。
123
+ > `imageInfo()` は `src/` 配下の画像のみ対応しています。`public/` 配下の画像は非対応です。
124
+
125
+ ### Sass
126
+
127
+ `src/` 配下の `.scss` ファイルをコンパイルして出力します。ベンダープレフィックスの自動付与と圧縮も行われます。
128
+
129
+ > ブラウザターゲットを指定する場合は、プロジェクトルートに `.browserslistrc` を配置してください。
130
+
131
+ ### JavaScript / TypeScript
132
+
133
+ `src/` 配下の `.js` / `.ts` ファイルをバンドルして出力します。
134
+
135
+ esbuild がTypeScriptをネイティブ処理するため、`tsconfig.json` は不要です。ただし型チェックは行わずトランスパイルのみ行います。
136
+
137
+ #### TypeScript 型チェックを追加する(オプション)
138
+
139
+ 型チェックが必要な場合は`typescript`を追加し、`tsc --noEmit`を組み合わせて使用します。
140
+
141
+ ```sh
142
+ npm install --save-dev typescript
143
+ ```
144
+
145
+ `tsconfig.json`をプロジェクトルートに作成します。
146
+
147
+ ```json
148
+ {
149
+ "compilerOptions": {
150
+ "target": "ES2022",
151
+ "module": "ESNext",
152
+ "moduleResolution": "bundler",
153
+ "strict": true,
154
+ "noEmit": true,
155
+ "skipLibCheck": true
156
+ },
157
+ "include": ["src/**/*.ts"]
158
+ }
159
+ ```
160
+
161
+ `package.json` にスクリプトを追加します。
162
+
163
+ ```json
164
+ "scripts": {
165
+ "start": "pugkit",
166
+ "build": "tsc --noEmit && pugkit build",
167
+ "sprite": "pugkit sprite",
168
+ }
169
+ ```
138
170
 
139
171
  ### Image Optimization
140
172
 
141
- ビルド時に自動的に画像を最適化します。
173
+ ビルド時に `src/` 配下の画像(JPEG・PNG)を自動的に最適化します。
174
+
175
+ - `'webp'` - PNG/JPEGをWebPに変換
176
+ - `'compress'` - 元の形式を維持したまま圧縮
177
+ - `false` - 最適化を無効化
178
+
179
+ ### SVG Sprite
180
+
181
+ `src/` 配下の `icons/` ディレクトリに配置したSVGを1つのスプライトファイルにまとめます。
182
+
183
+ ```
184
+ src/assets/icons/arrow.svg → dist/assets/icons.svg#arrow
185
+ ```
186
+
187
+ ```html
188
+ <svg><use href="assets/icons.svg#arrow"></use></svg>
189
+ ```
190
+
191
+ - SVG ファイル名がそのまま `<symbol id>` になります
192
+ - `fill` / `stroke` は自動的に `currentColor` に変換されます
193
+
194
+ ### SVG Optimization
195
+
196
+ `icons/` 以外に配置した SVG ファイルは SVGO で自動最適化されて出力されます。
197
+
198
+ ### Public Directory
199
+
200
+ `public/` に置いたファイルはそのまま `dist/` のルートにコピーされます。faviconやOGP画像など最適化不要なファイルの置き場として使用します。
201
+
202
+ ### Debug Mode
142
203
 
143
- - `imageOptimization: 'webp'` - PNG/JPEGをWebP形式に変換
144
- - `imageOptimization: 'compress'` - 元の形式を維持したまま圧縮
145
- - `imageOptimization: false` - 最適化を無効化
204
+ `debug: true` のとき、開発モードでのみ以下の出力に切り替わります。
205
+
206
+ | 対象 | 通常 | debug: true |
207
+ | ---- | ---------------------------- | ------------------------------ |
208
+ | CSS | minify済み | expanded + ソースマップ |
209
+ | JS | minify済み・`console.*` 削除 | ソースマップ・`console.*` 保持 |
146
210
 
147
211
  ### File Naming Rules
148
212
 
149
- - `_`(アンダースコア)で始まるファイルはテンプレートとして扱われます
150
- - `_`で始まるディレクトリ内のファイルもビルド対象外です
151
- - 通常のファイル名のみがビルドされます
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
+ ```
152
219
 
153
220
  ## Directory Structure
154
221
 
@@ -178,10 +245,4 @@ project-root/
178
245
  - [Sharp](https://sharp.pixelplumbing.com/) - 画像最適化
179
246
  - [SVGO](https://svgo.dev/) - SVG最適化
180
247
  - [Chokidar](https://github.com/paulmillr/chokidar) - ファイル監視
181
- - [sirv](https://github.com/lukeed/sirv) - 開発サーバー
182
- - SSE(Server-Sent Events) - ライブリロード
183
- - [Prettier](https://prettier.io/) - HTML整形
184
-
185
- ## License
186
-
187
- MIT
248
+ - [sirv](https://github.com/lukeed/sirv) + SSE(Server-Sent Events) - 開発サーバー
package/core/cache.mjs CHANGED
@@ -83,8 +83,8 @@ export class CacheManager {
83
83
  const [stats, content] = await Promise.all([stat(filePath), readFile(filePath)])
84
84
 
85
85
  return createHash('md5').update(content).update(stats.mtime.toISOString()).digest('hex')
86
- } catch (error) {
87
- return null
86
+ } catch {
87
+ return `error-${Math.random()}`
88
88
  }
89
89
  }
90
90
 
package/core/context.mjs CHANGED
@@ -23,12 +23,7 @@ export class BuildContext {
23
23
  }
24
24
 
25
25
  async runTask(taskName, taskFn, options = {}) {
26
- try {
27
- await taskFn(this, options)
28
- } catch (error) {
29
- console.error(`Task failed: ${taskName}`)
30
- throw error
31
- }
26
+ await taskFn(this, options)
32
27
  }
33
28
 
34
29
  async runParallel(tasks) {
package/core/server.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import http from 'node:http'
2
2
  import path from 'node:path'
3
- import { existsSync, readFileSync } from 'node:fs'
4
- import { mkdir } from 'node:fs/promises'
3
+ import { existsSync } from 'node:fs'
4
+ import { mkdir, readFile } from 'node:fs/promises'
5
5
  import sirv from 'sirv'
6
6
  import { logger } from '../utils/logger.mjs'
7
7
 
@@ -17,7 +17,6 @@ const liveReloadScript = `<script>
17
17
  location.reload();
18
18
  });
19
19
  es.addEventListener('css-update', function() {
20
- // 同一オリジンの <link rel="stylesheet"> のみ再読み込み(外部フォント等は除外)
21
20
  document.querySelectorAll('link[rel="stylesheet"]').forEach(function(link) {
22
21
  var url = new URL(link.href);
23
22
  if (url.origin !== location.origin) return;
@@ -29,6 +28,9 @@ const liveReloadScript = `<script>
29
28
  es.close();
30
29
  setTimeout(function() { location.reload(); }, 1000);
31
30
  };
31
+ window.addEventListener('beforeunload', function() {
32
+ es.close();
33
+ });
32
34
  })();
33
35
  </script>`
34
36
 
@@ -42,15 +44,17 @@ export async function serverTask(context, options = {}) {
42
44
  await mkdir(paths.dist, { recursive: true })
43
45
  }
44
46
 
45
- const port = config.server?.port ?? 3000
47
+ const port = config.server?.port ?? 5555
46
48
  const host = config.server?.host ?? 'localhost'
47
49
  const subdir = config.subdir ? '/' + config.subdir.replace(/^\/|\/$/g, '') : ''
48
50
  const startPath = (config.server?.startPath || '/').replace(/^\//, '')
49
51
  const fullStartPath = subdir ? `${subdir}/${startPath}` : `/${startPath}`
50
52
 
53
+ const serveRoot = path.resolve(config.root, 'dist')
54
+
51
55
  const clients = new Set()
52
56
 
53
- const staticServe = sirv(paths.dist, {
57
+ const staticServe = sirv(serveRoot, {
54
58
  dev: true,
55
59
  extensions: ['html'],
56
60
  setHeaders(res, filePath) {
@@ -73,36 +77,44 @@ export async function serverTask(context, options = {}) {
73
77
  })
74
78
  res.write('retry: 1000\n\n')
75
79
  clients.add(res)
76
- req.on('close', () => clients.delete(res))
80
+
81
+ const cleanup = () => clients.delete(res)
82
+ req.on('close', cleanup)
83
+ req.socket.on('close', cleanup)
84
+ res.on('error', cleanup)
77
85
  return
78
86
  }
79
87
 
80
88
  // ── HTML へのライブリロードスクリプト注入 ───────────
81
89
  const decoded = decodeURIComponent(urlPath)
82
90
  const candidates = [
83
- path.join(paths.dist, decoded === '/' ? 'index.html' : decoded.replace(/\/$/, '') + '/index.html'),
84
- path.join(paths.dist, decoded === '/' ? 'index.html' : decoded + '.html'),
85
- path.join(paths.dist, decoded)
91
+ path.join(serveRoot, decoded === '/' ? 'index.html' : decoded.replace(/\/$/, '') + '/index.html'),
92
+ path.join(serveRoot, decoded === '/' ? 'index.html' : decoded + '.html'),
93
+ path.join(serveRoot, decoded)
86
94
  ]
87
95
  const htmlFile = candidates.find(p => p.endsWith('.html') && existsSync(p))
88
96
 
89
97
  if (htmlFile) {
90
- try {
91
- let html = readFileSync(htmlFile, 'utf-8')
92
- html = html.includes('</body>')
93
- ? html.replace('</body>', liveReloadScript + '</body>')
94
- : html + liveReloadScript
95
- const buf = Buffer.from(html, 'utf-8')
96
- res.writeHead(200, {
97
- 'Content-Type': 'text/html; charset=utf-8',
98
- 'Content-Length': buf.length,
99
- 'Cache-Control': 'no-cache'
98
+ readFile(htmlFile, 'utf-8')
99
+ .then(html => {
100
+ html = html.includes('</body>')
101
+ ? html.replace('</body>', liveReloadScript + '</body>')
102
+ : html + liveReloadScript
103
+ const buf = Buffer.from(html, 'utf-8')
104
+ res.writeHead(200, {
105
+ 'Content-Type': 'text/html; charset=utf-8',
106
+ 'Content-Length': buf.length,
107
+ 'Cache-Control': 'no-cache'
108
+ })
109
+ res.end(buf)
100
110
  })
101
- res.end(buf)
102
- return
103
- } catch {
104
- // 読み込み失敗時は sirv にフォールスルー
105
- }
111
+ .catch(() => {
112
+ staticServe(req, res, () => {
113
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
114
+ res.end('404 Not Found')
115
+ })
116
+ })
117
+ return
106
118
  }
107
119
 
108
120
  // ── sirv で静的ファイルを配信 ───────────────────────
@@ -115,7 +127,11 @@ export async function serverTask(context, options = {}) {
115
127
  function broadcast(event, data = '') {
116
128
  const msg = `event: ${event}\ndata: ${data}\n\n`
117
129
  for (const res of clients) {
118
- res.write(msg)
130
+ try {
131
+ res.write(msg)
132
+ } catch {
133
+ clients.delete(res)
134
+ }
119
135
  }
120
136
  }
121
137
 
package/core/watcher.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import chokidar from 'chokidar'
2
- import { relative } from 'node:path'
2
+ import { rm } from 'node:fs/promises'
3
+ import { relative, resolve, basename, extname } from 'node:path'
3
4
  import { logger } from '../utils/logger.mjs'
4
5
 
5
6
  /**
@@ -100,6 +101,22 @@ class FileWatcher {
100
101
  }
101
102
  })
102
103
 
104
+ watcher.on('unlink', async path => {
105
+ if (!path.endsWith('.pug')) return
106
+ const relPath = relative(basePath, path)
107
+
108
+ // キャッシュとグラフをクリア
109
+ this.context.cache.invalidatePugTemplate(path)
110
+ this.context.graph.clearDependencies(path)
111
+
112
+ if (basename(path).startsWith('_')) {
113
+ logger.info('unlink', relPath)
114
+ return
115
+ }
116
+ const distPath = resolve(this.context.paths.dist, relPath.replace(/\.pug$/, '.html'))
117
+ await this.deleteDistFile(distPath, relPath)
118
+ })
119
+
103
120
  this.watchers.push(watcher)
104
121
  }
105
122
 
@@ -111,7 +128,7 @@ class FileWatcher {
111
128
  ignoreInitial: true,
112
129
  ignored: [/(^|[\/\\])\../, /node_modules/, /\.git/],
113
130
  persistent: true,
114
- awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
131
+ awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 }
115
132
  })
116
133
 
117
134
  watcher.on('change', async path => {
@@ -128,14 +145,23 @@ class FileWatcher {
128
145
  await this.context.taskRegistry.sass(this.context)
129
146
  }
130
147
 
131
- // CSSはインジェクション
132
- const cssUrlPath = '/' + relPath.replace(/\\/g, '/').replace(/\.scss$/, '.css')
133
- this.injectCSS(cssUrlPath)
148
+ this.injectCSS()
134
149
  } catch (error) {
135
150
  logger.error('watch', `Sass build failed: ${error.message}`)
136
151
  }
137
152
  })
138
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
+
139
165
  this.watchers.push(watcher)
140
166
  }
141
167
 
@@ -170,6 +196,17 @@ class FileWatcher {
170
196
  }
171
197
  })
172
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
+ })
209
+
173
210
  this.watchers.push(watcher)
174
211
  }
175
212
 
@@ -184,7 +221,7 @@ class FileWatcher {
184
221
  awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
185
222
  })
186
223
 
187
- const handleSvgChange = async path => {
224
+ const handleSvgChange = async (path, event) => {
188
225
  // .svgファイルのみ処理(iconsディレクトリは除外)
189
226
  // Windows対応: パスセパレータを正規化
190
227
  const normalizedPath = path.replace(/\\/g, '/')
@@ -192,7 +229,7 @@ class FileWatcher {
192
229
  return
193
230
  }
194
231
  const relPath = relative(basePath, path)
195
- logger.info('change', `svg: ${relPath}`)
232
+ logger.info(event, `svg: ${relPath}`)
196
233
 
197
234
  try {
198
235
  // SVGタスクを実行(変更されたファイルのみ)
@@ -208,8 +245,16 @@ class FileWatcher {
208
245
  }
209
246
  }
210
247
 
211
- watcher.on('change', handleSvgChange)
212
- watcher.on('add', handleSvgChange)
248
+ watcher.on('change', path => handleSvgChange(path, 'change'))
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
+ })
213
258
 
214
259
  this.watchers.push(watcher)
215
260
  }
@@ -225,13 +270,13 @@ class FileWatcher {
225
270
  awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
226
271
  })
227
272
 
228
- const handleImageChange = async path => {
273
+ const handleImageChange = async (path, event) => {
229
274
  // 画像ファイルのみ処理
230
275
  if (!/\.(jpg|jpeg|png|gif)$/i.test(path)) {
231
276
  return
232
277
  }
233
278
  const relPath = relative(basePath, path)
234
- logger.info('change', `image: ${relPath}`)
279
+ logger.info(event, `image: ${relPath}`)
235
280
 
236
281
  try {
237
282
  // 追加・変更時: 画像を処理
@@ -247,8 +292,18 @@ class FileWatcher {
247
292
  }
248
293
  }
249
294
 
250
- watcher.on('change', handleImageChange)
251
- watcher.on('add', handleImageChange)
295
+ watcher.on('change', path => handleImageChange(path, 'change'))
296
+ watcher.on('add', path => handleImageChange(path, 'add'))
297
+
298
+ watcher.on('unlink', async path => {
299
+ if (!/\.(jpg|jpeg|png|gif)$/i.test(path)) return
300
+ const relPath = relative(basePath, path)
301
+ const useWebp = this.context.config.build.imageOptimization === 'webp'
302
+ const ext = extname(path)
303
+ const destRelPath = useWebp ? relPath.replace(new RegExp(`\\${ext}$`, 'i'), '.webp') : relPath
304
+ const distPath = resolve(this.context.paths.dist, destRelPath)
305
+ await this.deleteDistFile(distPath, relPath)
306
+ })
252
307
 
253
308
  this.watchers.push(watcher)
254
309
  }
@@ -264,9 +319,9 @@ class FileWatcher {
264
319
  awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
265
320
  })
266
321
 
267
- watcher.on('change', async path => {
322
+ const handlePublicChange = async (path, event) => {
268
323
  const relPath = relative(basePath, path)
269
- logger.info('change', `public: ${relPath}`)
324
+ logger.info(event, `public: ${relPath}`)
270
325
 
271
326
  try {
272
327
  // Copyタスクを実行
@@ -278,6 +333,15 @@ class FileWatcher {
278
333
  } catch (error) {
279
334
  logger.error('watch', `Copy failed: ${error.message}`)
280
335
  }
336
+ }
337
+
338
+ watcher.on('change', path => handlePublicChange(path, 'change'))
339
+ watcher.on('add', path => handlePublicChange(path, 'add'))
340
+
341
+ watcher.on('unlink', async path => {
342
+ const relPath = relative(basePath, path)
343
+ const distPath = resolve(this.context.paths.dist, relPath)
344
+ await this.deleteDistFile(distPath, relPath)
281
345
  })
282
346
 
283
347
  this.watchers.push(watcher)
@@ -294,14 +358,25 @@ class FileWatcher {
294
358
  }
295
359
  }
296
360
 
361
+ /**
362
+ * dist内のファイルを削除してブラウザをリロード
363
+ */
364
+ async deleteDistFile(distPath, relPath) {
365
+ try {
366
+ await rm(distPath, { force: true })
367
+ logger.info('unlink', relPath)
368
+ this.reload()
369
+ } catch (error) {
370
+ logger.error('watch', `Failed to delete ${relPath}: ${error.message}`)
371
+ }
372
+ }
373
+
297
374
  /**
298
375
  * CSSインジェクション
299
376
  */
300
377
  injectCSS() {
301
378
  if (this.context.server) {
302
- setTimeout(() => {
303
- this.context.server.reloadCSS()
304
- }, 100)
379
+ this.context.server.reloadCSS()
305
380
  }
306
381
  }
307
382
 
@@ -7,6 +7,8 @@ export async function generateAsset(outputPath, data) {
7
7
 
8
8
  if (typeof data === 'string' || Buffer.isBuffer(data)) {
9
9
  await writeFile(outputPath, data)
10
+ } else {
11
+ throw new TypeError(`generateAsset: data must be a string or Buffer, got ${typeof data}`)
10
12
  }
11
13
 
12
14
  return outputPath
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pugkit",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A build tool for Pug-based projects",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/tasks/pug.mjs CHANGED
@@ -57,10 +57,8 @@ async function processFile(filePath, context) {
57
57
  if (!template) {
58
58
  const result = await compilePugFile(filePath, { basedir: paths.src })
59
59
 
60
- if (result.dependencies.length > 0) {
61
- graph.clearDependencies(filePath)
62
- result.dependencies.forEach(dep => graph.addDependency(filePath, dep))
63
- }
60
+ graph.clearDependencies(filePath)
61
+ result.dependencies.forEach(dep => graph.addDependency(filePath, dep))
64
62
 
65
63
  template = result.template
66
64
  cache.setPugTemplate(filePath, template)
package/tasks/script.mjs CHANGED
@@ -29,7 +29,7 @@ export async function scriptTask(context, options = {}) {
29
29
 
30
30
  try {
31
31
  // 2. esbuild設定
32
- const config = {
32
+ const esbuildConfig = {
33
33
  entryPoints: scriptFiles,
34
34
  outdir: paths.dist,
35
35
  outbase: paths.src,
@@ -54,12 +54,12 @@ export async function scriptTask(context, options = {}) {
54
54
 
55
55
  // debugモードでない場合はconsole/debuggerを削除
56
56
  if (!isDebugMode) {
57
- config.drop = ['console', 'debugger']
57
+ esbuildConfig.drop = ['console', 'debugger']
58
58
  }
59
59
 
60
60
  // 3. ビルド実行
61
61
  await ensureDir(paths.dist)
62
- const result = await esbuild.build(config)
62
+ const result = await esbuild.build(esbuildConfig)
63
63
 
64
64
  if (result.errors && result.errors.length > 0) {
65
65
  throw new Error(`esbuild errors: ${result.errors.length}`)
@@ -23,10 +23,6 @@ const SPRITE_SVGO_CONFIG = {
23
23
  }
24
24
  },
25
25
  'removeDimensions',
26
- {
27
- name: 'removeViewBox',
28
- active: false
29
- },
30
26
  {
31
27
  name: 'addAttributesToSVGElement',
32
28
  params: {
package/tasks/svg.mjs CHANGED
@@ -52,13 +52,7 @@ async function optimizeSvg(filePath, context) {
52
52
  // SVGOで最適化
53
53
  const result = optimize(content, {
54
54
  path: filePath,
55
- plugins: [
56
- 'preset-default',
57
- {
58
- name: 'removeViewBox',
59
- active: false
60
- }
61
- ]
55
+ plugins: ['preset-default']
62
56
  })
63
57
 
64
58
  // 出力
package/utils/file.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  import { mkdir, rm } from 'node:fs/promises'
2
- import { existsSync } from 'node:fs'
3
2
  import { dirname } from 'node:path'
4
3
 
5
4
  /**
@@ -10,9 +9,7 @@ import { dirname } from 'node:path'
10
9
  * ディレクトリを作成(再帰的)
11
10
  */
12
11
  export async function ensureDir(dirPath) {
13
- if (!existsSync(dirPath)) {
14
- await mkdir(dirPath, { recursive: true })
15
- }
12
+ await mkdir(dirPath, { recursive: true })
16
13
  }
17
14
 
18
15
  /**
@@ -26,9 +23,7 @@ export async function ensureFileDir(filePath) {
26
23
  * ディレクトリをクリーンアップ
27
24
  */
28
25
  export async function cleanDir(dirPath) {
29
- if (existsSync(dirPath)) {
30
- await rm(dirPath, { recursive: true, force: true })
31
- }
26
+ await rm(dirPath, { recursive: true, force: true })
32
27
  await mkdir(dirPath, { recursive: true })
33
28
  }
34
29