pugkit 1.1.0 → 1.3.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
@@ -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/` 配下のディレクトリ構成を維持したまま `outDir`(デフォルト: `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`を配置することで、ビルド設定をカスタマイズできます。
@@ -50,6 +73,7 @@ import { defineConfig } from 'pugkit'
50
73
  export default defineConfig({
51
74
  siteUrl: 'https://example.com/',
52
75
  subdir: '',
76
+ outDir: 'dist',
53
77
  debug: false,
54
78
  server: {
55
79
  port: 5555,
@@ -62,19 +86,20 @@ export default defineConfig({
62
86
  })
63
87
  ```
64
88
 
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` | - |
89
+ | Option | Description | Type / Values | Default |
90
+ | ------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------- | ------------- |
91
+ | `siteUrl` | サイトのベースURL(`Builder.url` に使用) | `string` | `''` |
92
+ | `subdir` | サブディレクトリのパス | `string` | `''` |
93
+ | `outDir` | ビルド出力先ディレクトリ。相対・絶対パス・ネスト(`htdocs/v2`)・上位(`../htdocs`)も指定可 | `string` | `'dist'` |
94
+ | `debug` | デバッグモード(開発時のみ有効) | `boolean` | `false` |
95
+ | `server.port` | 開発サーバーのポート番号 | `number` | `5555` |
96
+ | `server.host` | 開発サーバーのホスト | `string` | `'localhost'` |
97
+ | `server.startPath` | サーバー起動時に開くパス | `string` | `'/'` |
98
+ | `build.clean` | ビルド前に `outDir` をクリーンするか(`false` にすると他リソースと共存可能) | `boolean` | `true` |
99
+ | `build.imageOptimization` | 画像最適化の方式 | `'webp'` \| `'compress'` \| `false` | `'webp'` |
100
+ | `build.imageOptions.webp` | WebP変換オプション([Sharp WebP options](https://sharp.pixelplumbing.com/api-output#webp)) | `object` | - |
101
+ | `build.imageOptions.jpeg` | JPEG圧縮オプション([Sharp JPEG options](https://sharp.pixelplumbing.com/api-output#jpeg)) | `object` | - |
102
+ | `build.imageOptions.png` | PNG圧縮オプション([Sharp PNG options](https://sharp.pixelplumbing.com/api-output#png)) | `object` | - |
78
103
 
79
104
  ## Features
80
105
 
@@ -176,12 +201,16 @@ npm install --save-dev typescript
176
201
  - `'compress'` - 元の形式を維持したまま圧縮
177
202
  - `false` - 最適化を無効化
178
203
 
204
+ ### SVG Optimization
205
+
206
+ `icons/`以外に配置した SVG ファイルはSVGOで自動最適化されて出力されます。
207
+
179
208
  ### SVG Sprite
180
209
 
181
- `src/` 配下の `icons/` ディレクトリに配置したSVGを1つのスプライトファイルにまとめます。
210
+ `src/`配下の`icons/`ディレクトリに配置したSVGを1つのスプライトファイルにまとめます。
182
211
 
183
212
  ```
184
- src/assets/icons/arrow.svg → dist/assets/icons.svg#arrow
213
+ `src/assets/icons/arrow.svg → <outDir>/assets/icons.svg#arrow`
185
214
  ```
186
215
 
187
216
  ```html
@@ -191,13 +220,9 @@ src/assets/icons/arrow.svg → dist/assets/icons.svg#arrow
191
220
  - SVG ファイル名がそのまま `<symbol id>` になります
192
221
  - `fill` / `stroke` は自動的に `currentColor` に変換されます
193
222
 
194
- ### SVG Optimization
195
-
196
- `icons/` 以外に配置した SVG ファイルは SVGO で自動最適化されて出力されます。
197
-
198
223
  ### Public Directory
199
224
 
200
- `public/` に置いたファイルはそのまま `dist/` のルートにコピーされます。faviconやOGP画像など最適化不要なファイルの置き場として使用します。
225
+ `public/` に置いたファイルはそのまま `outDir` のルートにコピーされます。faviconやOGP画像など最適化不要なファイルの置き場として使用します。
201
226
 
202
227
  ### Debug Mode
203
228
 
@@ -208,34 +233,6 @@ src/assets/icons/arrow.svg → dist/assets/icons.svg#arrow
208
233
  | CSS | minify済み | expanded + ソースマップ |
209
234
  | JS | minify済み・`console.*` 削除 | ソースマップ・`console.*` 保持 |
210
235
 
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
236
  ## Tech Stack
240
237
 
241
238
  - [Pug](https://pugjs.org/) - HTMLテンプレートエンジン
@@ -1,6 +1,7 @@
1
1
  export const defaultConfig = {
2
2
  siteUrl: '',
3
3
  subdir: '',
4
+ outDir: 'dist',
4
5
  debug: false,
5
6
  server: {
6
7
  port: 5555,
@@ -8,6 +9,7 @@ export const defaultConfig = {
8
9
  startPath: '/'
9
10
  },
10
11
  build: {
12
+ clean: true,
11
13
  imageOptimization: 'webp',
12
14
  imageOptions: {
13
15
  webp: {
package/config/main.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { resolve } from 'node:path'
1
+ import { resolve, isAbsolute, relative } from 'node:path'
2
2
  import { existsSync } from 'node:fs'
3
3
  import { defaultConfig } from './defaults.mjs'
4
4
 
@@ -20,11 +20,13 @@ function mergeConfig(defaults, user) {
20
20
  return {
21
21
  siteUrl: user.siteUrl || defaults.siteUrl,
22
22
  subdir: user.subdir || defaults.subdir,
23
+ outDir: user.outDir !== undefined ? user.outDir : defaults.outDir,
23
24
  debug: user.debug !== undefined ? user.debug : defaults.debug,
24
25
  server: { ...defaults.server, ...(user.server || {}) },
25
26
  build: {
26
27
  ...defaults.build,
27
28
  ...(user.build || {}),
29
+ clean: user.build?.clean !== undefined ? user.build.clean : defaults.build.clean,
28
30
  imageOptions: {
29
31
  webp: { ...defaults.build.imageOptions.webp, ...(user.build?.imageOptions?.webp || {}) },
30
32
  jpeg: { ...defaults.build.imageOptions.jpeg, ...(user.build?.imageOptions?.jpeg || {}) },
@@ -34,10 +36,32 @@ function mergeConfig(defaults, user) {
34
36
  }
35
37
  }
36
38
 
39
+ function validateConfig(config) {
40
+ const root = config.root
41
+ const outDir = config.outDir
42
+ const resolvedOutDir = isAbsolute(outDir) ? outDir : resolve(root, outDir)
43
+
44
+ // Vite同様、outDirがrootと同一またはrootの親ディレクトリの場合に警告
45
+ // relative() を使うことでWindows(バックスラッシュ)でも正しく動作する
46
+ const isSameAsRoot = resolvedOutDir === root
47
+ const relToRoot = relative(resolvedOutDir, root)
48
+ const isParentOfRoot = relToRoot !== '' && !relToRoot.startsWith('..')
49
+
50
+ if (isSameAsRoot || isParentOfRoot) {
51
+ console.warn(
52
+ `[pugkit] outDir "${outDir}" はプロジェクトルートと同じか親ディレクトリです。` +
53
+ `ソースファイルが上書きされる可能性があるため、別のディレクトリを指定してください。`
54
+ )
55
+ }
56
+
57
+ return config
58
+ }
59
+
37
60
  export async function loadConfig(root = process.cwd()) {
38
61
  const userConfig = await loadUserConfig(root)
39
62
  const config = mergeConfig(defaultConfig, userConfig)
40
63
  config.root = root
64
+ validateConfig(config)
41
65
  return config
42
66
  }
43
67
 
@@ -11,20 +11,11 @@ export function createGlobPatterns(srcPath) {
11
11
  ignore: ['**/*.d.ts', '**/node_modules/**']
12
12
  },
13
13
  images: {
14
- optimize: [
15
- `${srcPath}/**/*.{png,jpg,jpeg}`,
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
- await this.clean()
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/context.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { resolve } from 'node:path'
1
+ import { resolve, isAbsolute } from 'node:path'
2
2
  import { CacheManager } from './cache.mjs'
3
3
  import { DependencyGraph } from './graph.mjs'
4
4
  import { createGlobPatterns } from '../config/index.mjs'
@@ -9,11 +9,17 @@ export class BuildContext {
9
9
  this.mode = mode
10
10
  this.cache = new CacheManager(mode)
11
11
  this.graph = new DependencyGraph()
12
+ this.sassGraph = new DependencyGraph()
13
+ this.scriptGraph = new DependencyGraph()
14
+
15
+ const outDir = config.outDir ?? 'dist'
16
+ const resolvedOutDir = isAbsolute(outDir) ? outDir : resolve(config.root, outDir)
12
17
 
13
18
  this.paths = {
14
19
  root: config.root,
15
20
  src: resolve(config.root, 'src'),
16
- dist: resolve(config.root, 'dist', config.subdir || ''),
21
+ outDir: resolvedOutDir,
22
+ dist: config.subdir ? resolve(resolvedOutDir, config.subdir) : resolvedOutDir,
17
23
  public: resolve(config.root, 'public')
18
24
  }
19
25
 
package/core/graph.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 依存関係グラフ
3
- * Pugのパーシャル依存を管理
3
+ * Pug・Sass・Script のパーシャル依存を管理
4
4
  */
5
5
  export class DependencyGraph {
6
6
  constructor() {
package/core/server.mjs CHANGED
@@ -50,7 +50,7 @@ export async function serverTask(context, options = {}) {
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')
53
+ const serveRoot = paths.outDir
54
54
 
55
55
  const clients = new Set()
56
56
 
package/core/watcher.mjs CHANGED
@@ -25,9 +25,18 @@ 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
- await this.context.taskRegistry.pug(this.context)
31
+ initialTasks.push(this.context.taskRegistry.pug(this.context))
32
+ }
33
+ if (this.context.taskRegistry?.sass) {
34
+ initialTasks.push(this.context.taskRegistry.sass(this.context))
30
35
  }
36
+ if (this.context.taskRegistry?.script) {
37
+ initialTasks.push(this.context.taskRegistry.script(this.context))
38
+ }
39
+ await Promise.all(initialTasks)
31
40
 
32
41
  this.watcher = chokidar
33
42
  .watch([paths.src, paths.public], {
@@ -55,6 +64,9 @@ class FileWatcher {
55
64
 
56
65
  handleAdd(filePath) {
57
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)
58
70
  if (filePath.endsWith('.svg') && !this.isIcons(filePath)) return this.onSvgChange(filePath, 'add')
59
71
  if (/\.(jpg|jpeg|png|gif)$/i.test(filePath)) return this.onImageChange(filePath, 'add')
60
72
  }
@@ -122,7 +134,7 @@ class FileWatcher {
122
134
  const relPath = relative(this.context.paths.src, filePath)
123
135
  logger.info('change', `sass: ${relPath}`)
124
136
  try {
125
- if (this.context.taskRegistry?.sass) await this.context.taskRegistry.sass(this.context)
137
+ if (this.context.taskRegistry?.sass) await this.context.taskRegistry.sass(this.context, { files: [filePath] })
126
138
  this.injectCSS()
127
139
  } catch (error) {
128
140
  logger.error('watch', `Sass build failed: ${error.message}`)
@@ -130,8 +142,9 @@ class FileWatcher {
130
142
  }
131
143
 
132
144
  async onSassUnlink(filePath) {
133
- const { paths } = this.context
145
+ const { paths, sassGraph } = this.context
134
146
  const relPath = relative(paths.src, filePath)
147
+ sassGraph.clearDependencies(filePath)
135
148
  if (basename(filePath).startsWith('_')) {
136
149
  logger.info('unlink', relPath)
137
150
  return
@@ -146,7 +159,7 @@ class FileWatcher {
146
159
  const relPath = relative(this.context.paths.src, filePath)
147
160
  logger.info('change', `script: ${relPath}`)
148
161
  try {
149
- if (this.context.taskRegistry?.script) await this.context.taskRegistry.script(this.context)
162
+ if (this.context.taskRegistry?.script) await this.context.taskRegistry.script(this.context, { files: [filePath] })
150
163
  this.reload()
151
164
  } catch (error) {
152
165
  logger.error('watch', `Script build failed: ${error.message}`)
@@ -154,8 +167,9 @@ class FileWatcher {
154
167
  }
155
168
 
156
169
  async onScriptUnlink(filePath) {
157
- const { paths } = this.context
170
+ const { paths, scriptGraph } = this.context
158
171
  const relPath = relative(paths.src, filePath)
172
+ scriptGraph.clearDependencies(filePath)
159
173
  if (basename(filePath).startsWith('_')) {
160
174
  logger.info('unlink', relPath)
161
175
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pugkit",
3
- "version": "1.1.0",
3
+ "version": "1.3.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/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 = await formatHtml(html)
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 scssFiles = await glob('**/[^_]*.scss', {
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 (scssFiles.length === 0) {
28
+ if (allEntryFiles.length === 0) {
28
29
  logger.skip('sass', 'No files to build')
29
30
  return
30
31
  }
31
32
 
32
- logger.info('sass', `Building ${scssFiles.length} file(s)`)
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
- // 2. 並列コンパイル
35
- await Promise.all(scssFiles.map(file => compileSassFile(file, context, isDebugMode)))
63
+ // 3. 並列コンパイル
64
+ await Promise.all(filesToBuild.map(file => compileSassFile(file, context, isDebugMode)))
36
65
 
37
- logger.success('sass', `Built ${scssFiles.length} file(s)`)
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処理
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 scriptFiles = await glob('**/[^_]*.{ts,js}', {
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 (scriptFiles.length === 0) {
20
+ if (allEntryFiles.length === 0) {
21
21
  logger.skip('script', 'No files to build')
22
22
  return
23
23
  }
24
24
 
25
- logger.info('script', `Building ${scriptFiles.length} file(s)`)
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
- // 2. esbuild設定
55
+ // 3. esbuild設定
32
56
  const esbuildConfig = {
33
- entryPoints: scriptFiles,
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: false,
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
- // 3. ビルド実行
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
- logger.success('script', `Built ${scriptFiles.length} file(s)`)
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
@@ -1,14 +1,18 @@
1
- import prettier from 'prettier'
1
+ import pkg from 'js-beautify'
2
+ const { html: beautifyHtml } = pkg
2
3
 
3
- export async function formatHtml(html, options = {}) {
4
+ export function formatHtml(html, options = {}) {
4
5
  const cleaned = html.replace(/[\u200B-\u200D\uFEFF]/g, '')
5
6
 
6
- return prettier.format(cleaned, {
7
- parser: 'html',
8
- printWidth: options.printWidth || 100_000,
9
- tabWidth: options.tabWidth || 2,
10
- useTabs: options.useTabs || false,
11
- htmlWhitespaceSensitivity: 'ignore',
12
- singleAttributePerLine: false
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
  }