pugkit 1.0.1 → 1.1.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 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
9
-
10
- ```bash
11
- npm install pugkit
12
- ```
13
-
14
- ## Usage
15
-
16
- ### Development Mode
17
-
18
- ファイルの変更を監視し、ブラウザの自動リロードを行います。
17
+ ## How To Use
19
18
 
20
- ```bash
21
- pugkit
22
- # or
23
- pugkit dev
24
- # or
25
- pugkit watch
19
+ ```sh
20
+ $ npm install --save-dev pugkit
21
+ $ touch ./src/index.pug
26
22
  ```
27
23
 
28
- ### Production Build
24
+ `package.json` にスクリプトを追加します。
29
25
 
30
- 製品用ファイルを生成します。
31
-
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
34
+ ## Commands
37
35
 
38
- アイコン用のSVGスプライトを生成します。
39
-
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,53 +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/` 配下の画像は非対応です。
138
124
 
139
- ### SVG Sprite
125
+ ### Sass
126
+
127
+ `src/` 配下の `.scss` ファイルをコンパイルして出力します。ベンダープレフィックスの自動付与と圧縮も行われます。
128
+
129
+ > ブラウザターゲットを指定する場合は、プロジェクトルートに `.browserslistrc` を配置してください。
140
130
 
141
- `src/` 配下の `icons/` ディレクトリに配置した SVG ファイルを1つのスプライトにまとめます。
131
+ ### JavaScript / TypeScript
142
132
 
143
- #### ディレクトリ構成
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
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
+ }
145
159
  ```
146
- src/
147
- └── assets/
148
- └── icons/ # ここに SVG を配置
149
- ├── arrow.svg
150
- └── close.svg
160
+
161
+ `package.json` にスクリプトを追加します。
162
+
163
+ ```json
164
+ "scripts": {
165
+ "start": "pugkit",
166
+ "build": "tsc --noEmit && pugkit build",
167
+ "sprite": "pugkit sprite",
168
+ }
151
169
  ```
152
170
 
153
- #### 出力
171
+ ### Image Optimization
172
+
173
+ ビルド時に `src/` 配下の画像(JPEG・PNG)を自動的に最適化します。
174
+
175
+ - `'webp'` - PNG/JPEGをWebPに変換
176
+ - `'compress'` - 元の形式を維持したまま圧縮
177
+ - `false` - 最適化を無効化
178
+
179
+ ### SVG Sprite
180
+
181
+ `src/` 配下の `icons/` ディレクトリに配置したSVGを1つのスプライトファイルにまとめます。
154
182
 
155
183
  ```
156
- dist/
157
- └── assets/
158
- └── icons.svg # 自動生成されるスプライト
184
+ src/assets/icons/arrow.svg → dist/assets/icons.svg#arrow
159
185
  ```
160
186
 
161
- #### 使い方
162
-
163
187
  ```html
164
188
  <svg><use href="assets/icons.svg#arrow"></use></svg>
165
189
  ```
166
190
 
167
- - `icons/` ディレクトリは `src/` 配下の任意の場所に配置できます
168
191
  - SVG ファイル名がそのまま `<symbol id>` になります
169
- - `fill` / `stroke` は自動的に `currentColor` に変換されます(SVGの色をCSSで制御可能)
192
+ - `fill` / `stroke` は自動的に `currentColor` に変換されます
170
193
 
171
- ### Image Optimization
194
+ ### SVG Optimization
195
+
196
+ `icons/` 以外に配置した SVG ファイルは SVGO で自動最適化されて出力されます。
197
+
198
+ ### Public Directory
199
+
200
+ `public/` に置いたファイルはそのまま `dist/` のルートにコピーされます。faviconやOGP画像など最適化不要なファイルの置き場として使用します。
201
+
202
+ ### Debug Mode
172
203
 
173
- ビルド時に自動的に画像を最適化します。
204
+ `debug: true` のとき、開発モードでのみ以下の出力に切り替わります。
174
205
 
175
- - `imageOptimization: 'webp'` - PNG/JPEGをWebP形式に変換
176
- - `imageOptimization: 'compress'` - 元の形式を維持したまま圧縮
177
- - `imageOptimization: false` - 最適化を無効化
206
+ | 対象 | 通常 | debug: true |
207
+ | ---- | ---------------------------- | ------------------------------ |
208
+ | CSS | minify済み | expanded + ソースマップ |
209
+ | JS | minify済み・`console.*` 削除 | ソースマップ・`console.*` 保持 |
178
210
 
179
211
  ### File Naming Rules
180
212
 
181
- - `_`(アンダースコア)で始まるファイルはテンプレートとして扱われます
182
- - `_`で始まるディレクトリ内のファイルもビルド対象外です
183
- - 通常のファイル名のみがビルドされます
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
+ ```
184
219
 
185
220
  ## Directory Structure
186
221
 
@@ -210,10 +245,4 @@ project-root/
210
245
  - [Sharp](https://sharp.pixelplumbing.com/) - 画像最適化
211
246
  - [SVGO](https://svgo.dev/) - SVG最適化
212
247
  - [Chokidar](https://github.com/paulmillr/chokidar) - ファイル監視
213
- - [sirv](https://github.com/lukeed/sirv) - 開発サーバー
214
- - SSE(Server-Sent Events) - ライブリロード
215
- - [Prettier](https://prettier.io/) - HTML整形
216
-
217
- ## License
218
-
219
- MIT
248
+ - [sirv](https://github.com/lukeed/sirv) + SSE(Server-Sent Events) - 開発サーバー
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, open } = options
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
- function getVersion() {
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${getVersion()}`)} ${message}`)
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${getVersion()}`)} ${message}`)
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${getVersion()}`)} ${message}`)
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${getVersion()}`)} ${message}`)
25
+ console.error(`${pc.cyan(pc.bold(name.toUpperCase()))} ${pc.dim(`v${version}`)} ${message}`)
30
26
  }
31
27
  }
@@ -5,8 +5,7 @@ export const defaultConfig = {
5
5
  server: {
6
6
  port: 5555,
7
7
  host: 'localhost',
8
- startPath: '/',
9
- open: false
8
+ startPath: '/'
10
9
  },
11
10
  build: {
12
11
  imageOptimization: 'webp',
package/core/cache.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto'
2
- import { stat, readFile } from 'node:fs/promises'
2
+ import { readFile } from 'node:fs/promises'
3
3
  import { existsSync } from 'node:fs'
4
4
 
5
5
  /**
@@ -76,15 +76,14 @@ export class CacheManager {
76
76
  }
77
77
 
78
78
  /**
79
- * ハッシュ計算(ファイル内容 + 更新日時)
79
+ * ハッシュ計算(ファイル内容のみ)
80
80
  */
81
81
  async computeHash(filePath) {
82
82
  try {
83
- const [stats, content] = await Promise.all([stat(filePath), readFile(filePath)])
84
-
85
- return createHash('md5').update(content).update(stats.mtime.toISOString()).digest('hex')
86
- } catch (error) {
87
- return null
83
+ const content = await readFile(filePath)
84
+ return createHash('md5').update(content).digest('hex')
85
+ } catch {
86
+ return `error-${Math.random()}`
88
87
  }
89
88
  }
90
89
 
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
@@ -44,15 +44,17 @@ export async function serverTask(context, options = {}) {
44
44
  await mkdir(paths.dist, { recursive: true })
45
45
  }
46
46
 
47
- const port = config.server?.port ?? 3000
47
+ const port = config.server?.port ?? 5555
48
48
  const host = config.server?.host ?? 'localhost'
49
49
  const subdir = config.subdir ? '/' + config.subdir.replace(/^\/|\/$/g, '') : ''
50
50
  const startPath = (config.server?.startPath || '/').replace(/^\//, '')
51
51
  const fullStartPath = subdir ? `${subdir}/${startPath}` : `/${startPath}`
52
52
 
53
+ const serveRoot = path.resolve(config.root, 'dist')
54
+
53
55
  const clients = new Set()
54
56
 
55
- const staticServe = sirv(paths.dist, {
57
+ const staticServe = sirv(serveRoot, {
56
58
  dev: true,
57
59
  extensions: ['html'],
58
60
  setHeaders(res, filePath) {
@@ -86,9 +88,9 @@ export async function serverTask(context, options = {}) {
86
88
  // ── HTML へのライブリロードスクリプト注入 ───────────
87
89
  const decoded = decodeURIComponent(urlPath)
88
90
  const candidates = [
89
- path.join(paths.dist, decoded === '/' ? 'index.html' : decoded.replace(/\/$/, '') + '/index.html'),
90
- path.join(paths.dist, decoded === '/' ? 'index.html' : decoded + '.html'),
91
- 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)
92
94
  ]
93
95
  const htmlFile = candidates.find(p => p.endsWith('.html') && existsSync(p))
94
96
 
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
  /**
@@ -16,7 +17,7 @@ export async function watcherTask(context, options = {}) {
16
17
  class FileWatcher {
17
18
  constructor(context) {
18
19
  this.context = context
19
- this.watchers = []
20
+ this.watcher = null
20
21
  }
21
22
 
22
23
  async start() {
@@ -28,264 +29,208 @@ class FileWatcher {
28
29
  await this.context.taskRegistry.pug(this.context)
29
30
  }
30
31
 
31
- // Pug監視
32
- this.watchPug(paths.src)
32
+ this.watcher = chokidar
33
+ .watch([paths.src, paths.public], {
34
+ ignoreInitial: true,
35
+ ignored: [/(^|[\/\\])\./, /node_modules/, /\.git/],
36
+ persistent: true,
37
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
38
+ })
39
+ .on('change', filePath => this.handleChange(filePath))
40
+ .on('add', filePath => this.handleAdd(filePath))
41
+ .on('unlink', filePath => this.handleUnlink(filePath))
33
42
 
34
- // Sass監視
35
- this.watchSass(paths.src)
43
+ logger.info('watch', 'File watching started')
44
+ }
36
45
 
37
- // Script監視
38
- this.watchScript(paths.src)
46
+ // ---- ルーティング ----
47
+ handleChange(filePath) {
48
+ if (this.isPublic(filePath)) return this.onPublicChange(filePath, 'change')
49
+ if (filePath.endsWith('.pug')) return this.onPugChange(filePath)
50
+ if (filePath.endsWith('.scss')) return this.onSassChange(filePath)
51
+ if (this.isScript(filePath)) return this.onScriptChange(filePath)
52
+ if (filePath.endsWith('.svg') && !this.isIcons(filePath)) return this.onSvgChange(filePath, 'change')
53
+ if (/\.(jpg|jpeg|png|gif)$/i.test(filePath)) return this.onImageChange(filePath, 'change')
54
+ }
39
55
 
40
- // SVG監視
41
- this.watchSvg(paths.src)
56
+ handleAdd(filePath) {
57
+ if (this.isPublic(filePath)) return this.onPublicChange(filePath, 'add')
58
+ if (filePath.endsWith('.svg') && !this.isIcons(filePath)) return this.onSvgChange(filePath, 'add')
59
+ if (/\.(jpg|jpeg|png|gif)$/i.test(filePath)) return this.onImageChange(filePath, 'add')
60
+ }
42
61
 
43
- // 画像監視
44
- this.watchImages(paths.src)
62
+ handleUnlink(filePath) {
63
+ if (this.isPublic(filePath)) return this.onPublicUnlink(filePath)
64
+ if (filePath.endsWith('.pug')) return this.onPugUnlink(filePath)
65
+ if (filePath.endsWith('.scss')) return this.onSassUnlink(filePath)
66
+ if (this.isScript(filePath)) return this.onScriptUnlink(filePath)
67
+ if (filePath.endsWith('.svg') && !this.isIcons(filePath)) return this.onSvgUnlink(filePath)
68
+ if (/\.(jpg|jpeg|png|gif)$/i.test(filePath)) return this.onImageUnlink(filePath)
69
+ }
45
70
 
46
- // Public監視
47
- this.watchPublic(paths.public)
71
+ // ---- 判定ヘルパー ----
48
72
 
49
- logger.info('watch', 'File watching started')
73
+ isPublic(filePath) {
74
+ return filePath.startsWith(this.context.paths.public)
50
75
  }
51
76
 
52
- /**
53
- * Pug監視(依存関係を考慮)
54
- */
55
- watchPug(basePath) {
56
- const watcher = chokidar.watch(basePath, {
57
- ignoreInitial: true,
58
- ignored: [
59
- /(^|[\/\\])\../, // 隠しファイル
60
- /node_modules/,
61
- /\.git/
62
- ],
63
- persistent: true,
64
- awaitWriteFinish: {
65
- stabilityThreshold: 100,
66
- pollInterval: 50
67
- }
68
- })
69
-
70
- watcher.on('change', async path => {
71
- // .pugファイルのみ処理
72
- if (!path.endsWith('.pug')) {
73
- return
74
- }
75
- const relPath = relative(basePath, path)
76
- logger.info('change', `pug: ${relPath}`)
77
-
78
- try {
79
- // パーシャルの場合、依存する親ファイルも再ビルド
80
- const affectedFiles = this.context.graph.getAffectedParents(path)
81
-
82
- if (affectedFiles.length > 0) {
83
- logger.info('pug', `Rebuilding ${affectedFiles.length} affected file(s)`)
84
- }
85
-
86
- // キャッシュ無効化
87
- this.context.cache.invalidatePugTemplate(path)
88
- affectedFiles.forEach(f => this.context.cache.invalidatePugTemplate(f))
89
-
90
- // Pugタスクを実行(変更されたファイルのみ)
91
- if (this.context.taskRegistry?.pug) {
92
- await this.context.taskRegistry.pug(this.context, {
93
- files: [path, ...affectedFiles]
94
- })
95
- }
96
-
97
- this.reload()
98
- } catch (error) {
99
- logger.error('watch', `Pug build failed: ${error.message}`)
100
- }
101
- })
77
+ isScript(filePath) {
78
+ return (filePath.endsWith('.ts') || filePath.endsWith('.js')) && !filePath.endsWith('.d.ts')
79
+ }
102
80
 
103
- this.watchers.push(watcher)
81
+ isIcons(filePath) {
82
+ // Windows 対応: セパレータを正規化
83
+ return filePath.replace(/\\/g, '/').includes('/icons/')
104
84
  }
105
85
 
106
- /**
107
- * Sass監視
108
- */
109
- watchSass(basePath) {
110
- const watcher = chokidar.watch(basePath, {
111
- ignoreInitial: true,
112
- ignored: [/(^|[\/\\])\../, /node_modules/, /\.git/],
113
- persistent: true,
114
- awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 }
115
- })
116
-
117
- watcher.on('change', async path => {
118
- // .scssファイルのみ処理
119
- if (!path.endsWith('.scss')) {
120
- return
86
+ // ---- Pug ----
87
+
88
+ async onPugChange(filePath) {
89
+ const { paths, graph, cache, taskRegistry } = this.context
90
+ const relPath = relative(paths.src, filePath)
91
+ logger.info('change', `pug: ${relPath}`)
92
+ try {
93
+ const affectedFiles = graph.getAffectedParents(filePath)
94
+ if (affectedFiles.length > 0) logger.info('pug', `Rebuilding ${affectedFiles.length} affected file(s)`)
95
+ cache.invalidatePugTemplate(filePath)
96
+ affectedFiles.forEach(f => cache.invalidatePugTemplate(f))
97
+ if (taskRegistry?.pug) {
98
+ await taskRegistry.pug(this.context, { files: [filePath, ...affectedFiles] })
121
99
  }
122
- const relPath = relative(basePath, path)
123
- logger.info('change', `sass: ${relPath}`)
124
-
125
- try {
126
- // Sassタスクを実行
127
- if (this.context.taskRegistry?.sass) {
128
- await this.context.taskRegistry.sass(this.context)
129
- }
130
-
131
- // CSSはインジェクション
132
- const cssUrlPath = '/' + relPath.replace(/\\/g, '/').replace(/\.scss$/, '.css')
133
- this.injectCSS(cssUrlPath)
134
- } catch (error) {
135
- logger.error('watch', `Sass build failed: ${error.message}`)
136
- }
137
- })
100
+ this.reload()
101
+ } catch (error) {
102
+ logger.error('watch', `Pug build failed: ${error.message}`)
103
+ }
104
+ }
138
105
 
139
- this.watchers.push(watcher)
106
+ async onPugUnlink(filePath) {
107
+ const { paths, cache, graph } = this.context
108
+ const relPath = relative(paths.src, filePath)
109
+ cache.invalidatePugTemplate(filePath)
110
+ graph.clearDependencies(filePath)
111
+ if (basename(filePath).startsWith('_')) {
112
+ logger.info('unlink', relPath)
113
+ return
114
+ }
115
+ const distPath = resolve(paths.dist, relPath.replace(/\.pug$/, '.html'))
116
+ await this.deleteDistFile(distPath, relPath)
140
117
  }
141
118
 
142
- /**
143
- * Script監視
144
- */
145
- watchScript(basePath) {
146
- const watcher = chokidar.watch(basePath, {
147
- ignoreInitial: true,
148
- ignored: [/(^|[\/\\])\../, /node_modules/, /\.git/, /\.d\.ts$/],
149
- persistent: true,
150
- awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
151
- })
152
-
153
- watcher.on('change', async path => {
154
- // .ts/.jsファイルのみ処理(.d.tsを除く)
155
- if (!(path.endsWith('.ts') || path.endsWith('.js')) || path.endsWith('.d.ts')) {
156
- return
157
- }
158
- const relPath = relative(basePath, path)
159
- logger.info('change', `script: ${relPath}`)
160
-
161
- try {
162
- // Scriptタスクを実行
163
- if (this.context.taskRegistry?.script) {
164
- await this.context.taskRegistry.script(this.context)
165
- }
166
-
167
- this.reload()
168
- } catch (error) {
169
- logger.error('watch', `Script build failed: ${error.message}`)
170
- }
171
- })
119
+ // ---- Sass ----
172
120
 
173
- this.watchers.push(watcher)
121
+ async onSassChange(filePath) {
122
+ const relPath = relative(this.context.paths.src, filePath)
123
+ logger.info('change', `sass: ${relPath}`)
124
+ try {
125
+ if (this.context.taskRegistry?.sass) await this.context.taskRegistry.sass(this.context)
126
+ this.injectCSS()
127
+ } catch (error) {
128
+ logger.error('watch', `Sass build failed: ${error.message}`)
129
+ }
174
130
  }
175
131
 
176
- /**
177
- * SVG監視
178
- */
179
- watchSvg(basePath) {
180
- const watcher = chokidar.watch(basePath, {
181
- ignoreInitial: true,
182
- ignored: [/(^|[\/\\])\../, /node_modules/, /\.git/, /icons/], // iconsはスプライト用なので除外
183
- persistent: true,
184
- awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
185
- })
186
-
187
- const handleSvgChange = async path => {
188
- // .svgファイルのみ処理(iconsディレクトリは除外)
189
- // Windows対応: パスセパレータを正規化
190
- const normalizedPath = path.replace(/\\/g, '/')
191
- if (!path.endsWith('.svg') || normalizedPath.includes('/icons/')) {
192
- return
193
- }
194
- const relPath = relative(basePath, path)
195
- logger.info('change', `svg: ${relPath}`)
196
-
197
- try {
198
- // SVGタスクを実行(変更されたファイルのみ)
199
- if (this.context.taskRegistry?.svg) {
200
- await this.context.taskRegistry.svg(this.context, {
201
- files: [path]
202
- })
203
- }
204
-
205
- this.reload()
206
- } catch (error) {
207
- logger.error('watch', `SVG processing failed: ${error.message}`)
208
- }
132
+ async onSassUnlink(filePath) {
133
+ const { paths } = this.context
134
+ const relPath = relative(paths.src, filePath)
135
+ if (basename(filePath).startsWith('_')) {
136
+ logger.info('unlink', relPath)
137
+ return
209
138
  }
139
+ const distPath = resolve(paths.dist, relPath.replace(/\.scss$/, '.css'))
140
+ await this.deleteDistFile(distPath, relPath)
141
+ }
210
142
 
211
- watcher.on('change', handleSvgChange)
212
- watcher.on('add', handleSvgChange)
143
+ // ---- Script ----
213
144
 
214
- this.watchers.push(watcher)
145
+ async onScriptChange(filePath) {
146
+ const relPath = relative(this.context.paths.src, filePath)
147
+ logger.info('change', `script: ${relPath}`)
148
+ try {
149
+ if (this.context.taskRegistry?.script) await this.context.taskRegistry.script(this.context)
150
+ this.reload()
151
+ } catch (error) {
152
+ logger.error('watch', `Script build failed: ${error.message}`)
153
+ }
215
154
  }
216
155
 
217
- /**
218
- * 画像監視
219
- */
220
- watchImages(basePath) {
221
- const watcher = chokidar.watch(basePath, {
222
- ignoreInitial: true,
223
- ignored: [/(^|[\/\\])\../, /node_modules/, /\.git/],
224
- persistent: true,
225
- awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
226
- })
227
-
228
- const handleImageChange = async path => {
229
- // 画像ファイルのみ処理
230
- if (!/\.(jpg|jpeg|png|gif)$/i.test(path)) {
231
- return
232
- }
233
- const relPath = relative(basePath, path)
234
- logger.info('change', `image: ${relPath}`)
235
-
236
- try {
237
- // 追加・変更時: 画像を処理
238
- if (this.context.taskRegistry?.image) {
239
- await this.context.taskRegistry.image(this.context, {
240
- files: [path]
241
- })
242
- }
243
-
244
- this.reload()
245
- } catch (error) {
246
- logger.error('watch', `Image processing failed: ${error.message}`)
247
- }
156
+ async onScriptUnlink(filePath) {
157
+ const { paths } = this.context
158
+ const relPath = relative(paths.src, filePath)
159
+ if (basename(filePath).startsWith('_')) {
160
+ logger.info('unlink', relPath)
161
+ return
248
162
  }
163
+ const distPath = resolve(paths.dist, relPath.replace(/\.ts$/, '.js'))
164
+ await this.deleteDistFile(distPath, relPath)
165
+ }
249
166
 
250
- watcher.on('change', handleImageChange)
251
- watcher.on('add', handleImageChange)
167
+ // ---- SVG ----
252
168
 
253
- this.watchers.push(watcher)
169
+ async onSvgChange(filePath, event) {
170
+ const relPath = relative(this.context.paths.src, filePath)
171
+ logger.info(event, `svg: ${relPath}`)
172
+ try {
173
+ if (this.context.taskRegistry?.svg) {
174
+ await this.context.taskRegistry.svg(this.context, { files: [filePath] })
175
+ }
176
+ this.reload()
177
+ } catch (error) {
178
+ logger.error('watch', `SVG processing failed: ${error.message}`)
179
+ }
180
+ }
181
+
182
+ async onSvgUnlink(filePath) {
183
+ const relPath = relative(this.context.paths.src, filePath)
184
+ const distPath = resolve(this.context.paths.dist, relPath)
185
+ await this.deleteDistFile(distPath, relPath)
254
186
  }
255
187
 
256
- /**
257
- * Public監視
258
- */
259
- watchPublic(basePath) {
260
- const watcher = chokidar.watch(basePath, {
261
- ignoreInitial: true,
262
- ignored: [/(^|[\/\\])\../, /node_modules/, /\.git/],
263
- persistent: true,
264
- awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
265
- })
266
-
267
- watcher.on('change', async path => {
268
- const relPath = relative(basePath, path)
269
- logger.info('change', `public: ${relPath}`)
270
-
271
- try {
272
- // Copyタスクを実行
273
- if (this.context.taskRegistry?.copy) {
274
- await this.context.taskRegistry.copy(this.context)
275
- }
276
-
277
- this.reload()
278
- } catch (error) {
279
- logger.error('watch', `Copy failed: ${error.message}`)
188
+ // ---- Image ----
189
+
190
+ async onImageChange(filePath, event) {
191
+ const relPath = relative(this.context.paths.src, filePath)
192
+ logger.info(event, `image: ${relPath}`)
193
+ try {
194
+ if (this.context.taskRegistry?.image) {
195
+ await this.context.taskRegistry.image(this.context, { files: [filePath] })
280
196
  }
281
- })
197
+ this.reload()
198
+ } catch (error) {
199
+ logger.error('watch', `Image processing failed: ${error.message}`)
200
+ }
201
+ }
202
+
203
+ async onImageUnlink(filePath) {
204
+ const { paths, config } = this.context
205
+ const relPath = relative(paths.src, filePath)
206
+ const useWebp = config.build.imageOptimization === 'webp'
207
+ const ext = extname(filePath)
208
+ const destRelPath = useWebp ? relPath.replace(new RegExp(`\\${ext}$`, 'i'), '.webp') : relPath
209
+ const distPath = resolve(paths.dist, destRelPath)
210
+ await this.deleteDistFile(distPath, relPath)
211
+ }
282
212
 
283
- this.watchers.push(watcher)
213
+ // ---- Public ----
214
+
215
+ async onPublicChange(filePath, event) {
216
+ const relPath = relative(this.context.paths.public, filePath)
217
+ logger.info(event, `public: ${relPath}`)
218
+ try {
219
+ if (this.context.taskRegistry?.copy) await this.context.taskRegistry.copy(this.context, { files: [filePath] })
220
+ this.reload()
221
+ } catch (error) {
222
+ logger.error('watch', `Copy failed: ${error.message}`)
223
+ }
284
224
  }
285
225
 
286
- /**
287
- * ブラウザリロード
288
- */
226
+ async onPublicUnlink(filePath) {
227
+ const relPath = relative(this.context.paths.public, filePath)
228
+ const distPath = resolve(this.context.paths.dist, relPath)
229
+ await this.deleteDistFile(distPath, relPath)
230
+ }
231
+
232
+ // ---- 共通ヘルパー ----
233
+
289
234
  reload() {
290
235
  if (this.context.server) {
291
236
  setTimeout(() => {
@@ -294,20 +239,24 @@ class FileWatcher {
294
239
  }
295
240
  }
296
241
 
297
- /**
298
- * CSSインジェクション
299
- */
242
+ async deleteDistFile(distPath, relPath) {
243
+ try {
244
+ await rm(distPath, { force: true })
245
+ logger.info('unlink', relPath)
246
+ this.reload()
247
+ } catch (error) {
248
+ logger.error('watch', `Failed to delete ${relPath}: ${error.message}`)
249
+ }
250
+ }
251
+
300
252
  injectCSS() {
301
253
  if (this.context.server) {
302
254
  this.context.server.reloadCSS()
303
255
  }
304
256
  }
305
257
 
306
- /**
307
- * 監視停止
308
- */
309
258
  async stop() {
310
- await Promise.all(this.watchers.map(w => w.close()))
259
+ if (this.watcher) await this.watcher.close()
311
260
  logger.info('watch', 'File watching stopped')
312
261
  }
313
262
  }
@@ -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/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('.pug', '.html'))
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.1",
3
+ "version": "1.1.0",
4
4
  "description": "A build tool for Pug-based projects",
5
5
  "type": "module",
6
6
  "license": "MIT",
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
@@ -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/sass.mjs CHANGED
@@ -78,7 +78,7 @@ async function compileSassFile(filePath, context, isDebugMode) {
78
78
  )
79
79
  }
80
80
 
81
- const outputRelativePath = relative(paths.src, filePath).replace('.scss', '.css')
81
+ const outputRelativePath = relative(paths.src, filePath).replace(/\.scss$/, '.css')
82
82
  const outputPath = resolve(paths.dist, outputRelativePath)
83
83
 
84
84
  const postcssResult = await postcss(postcssPlugins).process(css, {
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