pugkit 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # pugkit
2
+
3
+ ## About
4
+
5
+ pugkitは静的サイト制作に特化したビルドツールです。
6
+ 納品向きの綺麗なHTMLと自由度の高いアセットを出力します。
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install pugkit
12
+ ```
13
+
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
+ 最適化されたファイルを生成します。
31
+
32
+ ```bash
33
+ pugkit build
34
+ ```
35
+
36
+ ### SVG Sprite Generation
37
+
38
+ アイコン用のSVGスプライトを生成します。
39
+
40
+ ```bash
41
+ pugkit sprite
42
+ ```
43
+
44
+ ## Configuration
45
+
46
+ プロジェクトのルートに `pugkit.config.mjs` を配置することで、ビルド設定をカスタマイズできます。
47
+
48
+ ```js
49
+ // pugkit.config.mjs
50
+ export default {
51
+ siteUrl: 'https://example.com/',
52
+ subdir: 'subdirectory',
53
+ debug: false,
54
+ server: {
55
+ port: 5555,
56
+ host: 'localhost',
57
+ startPath: '/'
58
+ },
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
+ }
75
+ }
76
+ }
77
+ ```
78
+
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` | 画像最適化の詳細設定 | - |
90
+
91
+ ## Features
92
+
93
+ ### Pug Templates
94
+
95
+ Pugテンプレート内では、`Builder`オブジェクトと`imageSize()`関数が使用できます。
96
+
97
+ #### Builder Object
98
+
99
+ ```pug
100
+ //- 相対パスでリンク
101
+ a(href=`${Builder.dir}about/`)
102
+
103
+ //- 完全なURL
104
+ meta(property='og:url', content=Builder.url.href)
105
+ ```
106
+
107
+ | Property | Description | Example |
108
+ | ---------------------- | ---------------------------------- | ----------------------------------------- |
109
+ | `Builder.dir` | 現在のページからルートへの相対パス | `./` or `../` |
110
+ | `Builder.subdir` | サブディレクトリのパス | `/subdirectory` |
111
+ | `Builder.url.origin` | サイトのオリジン | `https://example.com` |
112
+ | `Builder.url.base` | サイトのベースURL | `https://example.com/subdirectory` |
113
+ | `Builder.url.pathname` | 現在のページのパス | `/about/` |
114
+ | `Builder.url.href` | 完全なURL | `https://example.com/subdirectory/about/` |
115
+
116
+ #### imageSize() Function
117
+
118
+ 画像ファイルのサイズを自動取得し、CLSを防ぎます。
119
+
120
+ ```pug
121
+ - const size = imageSize('/assets/img/photo.jpg')
122
+ img(src='/assets/img/photo.jpg', width=size.width, height=size.height, alt='')
123
+ ```
124
+
125
+ ### Image Optimization
126
+
127
+ ビルド時に自動的に画像を最適化します。
128
+
129
+ - `imageOptimization: 'webp'` - PNG/JPEGをWebP形式に変換
130
+ - `imageOptimization: 'compress'` - 元の形式を維持したまま圧縮
131
+ - `imageOptimization: false` - 最適化を無効化
132
+
133
+ ### File Naming Rules
134
+
135
+ - `_`(アンダースコア)で始まるファイルは部分テンプレートとして扱われます
136
+ - `_`で始まるディレクトリ内のファイルもビルド対象外です
137
+ - 通常のファイル名のみがビルドされます
138
+
139
+ ## Directory Structure
140
+
141
+ ```
142
+ project-root/
143
+ ├── src/ # ソースファイル
144
+ │ ├── *.pug
145
+ │ ├── *.scss
146
+ │ ├── *.ts
147
+ │ ├── *.js
148
+ │ ├── *.jpg
149
+ │ ├── *.png
150
+ │ └── *.svg
151
+ ├── public/ # 静的ファイル
152
+ │ ├── ogp.jpg
153
+ │ └── favicon.ico
154
+ ├── dist/ # ビルド出力先
155
+ └── pugkit.config.mjs # ビルド設定ファイル
156
+ ```
157
+
158
+ ## Tech Stack
159
+
160
+ - [Pug](https://pugjs.org/) - HTMLテンプレートエンジン
161
+ - [Sass](https://sass-lang.com/) - CSSプリプロセッサー
162
+ - [esbuild](https://esbuild.github.io/) - TypeScript/JavaScriptバンドラー
163
+ - [PostCSS](https://postcss.org/) - CSS後処理(Autoprefixer、cssnano)
164
+ - [Sharp](https://sharp.pixelplumbing.com/) - 画像最適化
165
+ - [SVGO](https://svgo.dev/) - SVG最適化
166
+ - [Chokidar](https://github.com/paulmillr/chokidar) - ファイル監視
167
+ - [BrowserSync](https://browsersync.io/) - 開発サーバー
168
+ - [Prettier](https://prettier.io/) - HTML整形
169
+
170
+ ## License
171
+
172
+ MIT
package/cli/build.mjs ADDED
@@ -0,0 +1,15 @@
1
+ import { createBuilder } from '../index.mjs'
2
+ import { logger } from './logger.mjs'
3
+
4
+ export async function build(options = {}) {
5
+ const { root = process.cwd() } = options
6
+
7
+ logger.info('pugkit', 'building...')
8
+
9
+ const startTime = Date.now()
10
+ const builder = await createBuilder(root, 'production')
11
+ await builder.build()
12
+
13
+ const elapsed = Date.now() - startTime
14
+ logger.success('pugkit', `built in ${elapsed}ms`)
15
+ }
@@ -0,0 +1,16 @@
1
+ import { createBuilder } from '../index.mjs'
2
+ import { logger } from './logger.mjs'
3
+
4
+ export async function develop(options = {}) {
5
+ const { root = process.cwd(), port, host, open } = options
6
+
7
+ logger.info('pugkit', 'starting dev server...')
8
+
9
+ const builder = await createBuilder(root, 'development')
10
+
11
+ if (port) builder.context.config.server.port = port
12
+ if (host) builder.context.config.server.host = host
13
+ if (open) builder.context.config.server.open = open
14
+
15
+ await builder.watch()
16
+ }
package/cli/index.mjs ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'node:fs'
4
+ import { fileURLToPath } from 'node:url'
5
+ import path from 'node:path'
6
+ import { cac } from 'cac'
7
+ import { develop } from './develop.mjs'
8
+ import { build } from './build.mjs'
9
+ import { sprite } from './sprite.mjs'
10
+
11
+ const __filename = fileURLToPath(import.meta.url)
12
+ const __dirname = path.dirname(__filename)
13
+
14
+ function pkgVersion() {
15
+ const pkgPath = path.join(__dirname, '../package.json')
16
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
17
+ return pkg.version
18
+ }
19
+
20
+ const cli = cac('pugkit')
21
+
22
+ cli
23
+ .command('[root]', 'Start development mode with file watching')
24
+ .alias('dev')
25
+ .alias('watch')
26
+ .action(async (root, options) => {
27
+ try {
28
+ await develop({ root: root || process.cwd() })
29
+ } catch (err) {
30
+ console.error(err)
31
+ process.exit(1)
32
+ }
33
+ })
34
+
35
+ cli.command('build [root]', 'Production build').action(async root => {
36
+ try {
37
+ await build({ root: root || process.cwd() })
38
+ } catch (err) {
39
+ console.error(err)
40
+ process.exit(1)
41
+ }
42
+ })
43
+
44
+ cli.command('sprite [root]', 'Generate SVG sprite').action(async root => {
45
+ try {
46
+ await sprite({ root: root || process.cwd() })
47
+ } catch (err) {
48
+ console.error(err)
49
+ process.exit(1)
50
+ }
51
+ })
52
+
53
+ cli.help()
54
+ cli.version(pkgVersion())
55
+ cli.parse()
package/cli/logger.mjs ADDED
@@ -0,0 +1,31 @@
1
+ import pc from 'picocolors'
2
+ import { readFileSync } from 'node:fs'
3
+ import { fileURLToPath } from 'node:url'
4
+ import path from 'node:path'
5
+
6
+ const __filename = fileURLToPath(import.meta.url)
7
+ const __dirname = path.dirname(__filename)
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
+ }
14
+
15
+ export const logger = {
16
+ info(name, message) {
17
+ console.log(`${pc.cyan(pc.bold(name.toUpperCase()))} ${pc.dim(`v${getVersion()}`)} ${message}`)
18
+ },
19
+
20
+ success(name, message) {
21
+ console.log(`${pc.cyan(pc.bold(name.toUpperCase()))} ${pc.dim(`v${getVersion()}`)} ${message}`)
22
+ },
23
+
24
+ warn(name, message) {
25
+ console.log(`${pc.cyan(pc.bold(name.toUpperCase()))} ${pc.dim(`v${getVersion()}`)} ${message}`)
26
+ },
27
+
28
+ error(name, message) {
29
+ console.error(`${pc.cyan(pc.bold(name.toUpperCase()))} ${pc.dim(`v${getVersion()}`)} ${message}`)
30
+ }
31
+ }
package/cli/sprite.mjs ADDED
@@ -0,0 +1,13 @@
1
+ import { createBuilder } from '../index.mjs'
2
+ import { logger } from './logger.mjs'
3
+
4
+ export async function sprite(options = {}) {
5
+ const { root = process.cwd() } = options
6
+
7
+ logger.info('pugkit', 'generating sprite...')
8
+
9
+ const builder = await createBuilder(root, 'production')
10
+ await builder.runTask('sprite')
11
+
12
+ logger.success('pugkit', 'sprite generated')
13
+ }
@@ -0,0 +1,36 @@
1
+ export const defaultConfig = {
2
+ siteUrl: '',
3
+ subdir: '',
4
+ debug: false,
5
+ server: {
6
+ port: 5555,
7
+ host: 'localhost',
8
+ startPath: '/',
9
+ open: false
10
+ },
11
+ build: {
12
+ imageOptimization: 'webp',
13
+ imageOptions: {
14
+ webp: {
15
+ quality: 90,
16
+ effort: 6,
17
+ smartSubsample: true,
18
+ method: 6,
19
+ reductionEffort: 6,
20
+ alphaQuality: 100,
21
+ lossless: false
22
+ },
23
+ jpeg: {
24
+ quality: 75,
25
+ progressive: true,
26
+ mozjpeg: false
27
+ },
28
+ png: {
29
+ quality: 85,
30
+ compressionLevel: 6,
31
+ adaptiveFiltering: true,
32
+ palette: true
33
+ }
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,3 @@
1
+ export function defineConfig(config) {
2
+ return config
3
+ }
@@ -0,0 +1,4 @@
1
+ export { loadConfig, resolveConfig } from './main.mjs'
2
+ export { createGlobPatterns } from './patterns.mjs'
3
+ export { defineConfig } from './define.mjs'
4
+ export { defaultConfig } from './defaults.mjs'
@@ -0,0 +1,53 @@
1
+ import { resolve } from 'node:path'
2
+ import { existsSync } from 'node:fs'
3
+ import { defaultConfig } from './defaults.mjs'
4
+
5
+ async function loadUserConfig(root) {
6
+ const configPath = resolve(root, 'pugkit.config.mjs')
7
+
8
+ if (!existsSync(configPath)) return {}
9
+
10
+ try {
11
+ const module = await import(configPath)
12
+ return module.default || {}
13
+ } catch (error) {
14
+ console.warn(`Failed to load pugkit.config.mjs: ${error.message}`)
15
+ return {}
16
+ }
17
+ }
18
+
19
+ function mergeConfig(defaults, user) {
20
+ return {
21
+ siteUrl: user.siteUrl || defaults.siteUrl,
22
+ subdir: user.subdir || defaults.subdir,
23
+ debug: user.debug !== undefined ? user.debug : defaults.debug,
24
+ server: { ...defaults.server, ...(user.server || {}) },
25
+ build: {
26
+ ...defaults.build,
27
+ ...(user.build || {}),
28
+ imageOptions: {
29
+ webp: { ...defaults.build.imageOptions.webp, ...(user.build?.imageOptions?.webp || {}) },
30
+ jpeg: { ...defaults.build.imageOptions.jpeg, ...(user.build?.imageOptions?.jpeg || {}) },
31
+ png: { ...defaults.build.imageOptions.png, ...(user.build?.imageOptions?.png || {}) }
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ export async function loadConfig(root = process.cwd()) {
38
+ const userConfig = await loadUserConfig(root)
39
+ const config = mergeConfig(defaultConfig, userConfig)
40
+ config.root = root
41
+ return config
42
+ }
43
+
44
+ export async function resolveConfig(inlineConfig = {}) {
45
+ const root = inlineConfig.root || process.cwd()
46
+ const config = await loadConfig(root)
47
+
48
+ if (inlineConfig.server) {
49
+ Object.assign(config.server, inlineConfig.server)
50
+ }
51
+
52
+ return config
53
+ }
@@ -0,0 +1,30 @@
1
+ export function createGlobPatterns(srcPath) {
2
+ return {
3
+ pug: {
4
+ src: [`${srcPath}/**/[^_]*.pug`, `!${srcPath}/_*/**/*.pug`]
5
+ },
6
+ sass: {
7
+ src: [`${srcPath}/**/[^_]*.scss`, `!${srcPath}/**/_*.scss`]
8
+ },
9
+ script: {
10
+ src: './**/[^_]*.{ts,js}',
11
+ ignore: ['**/*.d.ts', '**/node_modules/**']
12
+ },
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
+ }
24
+ },
25
+ svg: {
26
+ src: [`${srcPath}/**/*.svg`],
27
+ ignore: [`!${srcPath}/**/sprites_*/*`, `!${srcPath}/**/_inline*/*`, `!${srcPath}/**/icons*/*`]
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,139 @@
1
+ import { BuildContext } from './context.mjs'
2
+ import { logger } from '../utils/logger.mjs'
3
+ import { cleanDir } from '../utils/file.mjs'
4
+
5
+ /**
6
+ * メインビルダー
7
+ */
8
+ export class Builder {
9
+ constructor(config, mode = 'development') {
10
+ this.context = new BuildContext(config, mode)
11
+ this.tasks = {}
12
+ }
13
+
14
+ /**
15
+ * タスクを登録
16
+ */
17
+ registerTask(name, fn) {
18
+ this.tasks[name] = fn
19
+ }
20
+
21
+ /**
22
+ * 複数タスクを登録
23
+ */
24
+ registerTasks(tasks) {
25
+ Object.entries(tasks).forEach(([name, fn]) => {
26
+ this.registerTask(name, fn)
27
+ })
28
+ }
29
+
30
+ /**
31
+ * 本番ビルド
32
+ */
33
+ async build() {
34
+ const { context } = this
35
+ const startTime = Date.now()
36
+
37
+ logger.info('build', `Building in ${context.mode} mode`)
38
+
39
+ try {
40
+ // 1. クリーンアップ
41
+ await this.clean()
42
+
43
+ // 2. 並列ビルド(軽量タスク + スプライト)
44
+ const parallelTasks = []
45
+
46
+ if (this.tasks.sass) {
47
+ parallelTasks.push({ name: 'sass', fn: this.tasks.sass })
48
+ }
49
+ if (this.tasks.script) {
50
+ parallelTasks.push({ name: 'script', fn: this.tasks.script })
51
+ }
52
+ if (this.tasks.sprite) {
53
+ parallelTasks.push({ name: 'sprite', fn: this.tasks.sprite })
54
+ }
55
+
56
+ if (parallelTasks.length > 0) {
57
+ await context.runParallel(parallelTasks)
58
+ }
59
+
60
+ // 3. Pug(Sass/Scriptの出力を参照するため後)
61
+ if (this.tasks.pug) {
62
+ await context.runTask('pug', this.tasks.pug)
63
+ }
64
+
65
+ // 4. 最終処理(並列)
66
+ const finalTasks = []
67
+
68
+ if (this.tasks.image) {
69
+ finalTasks.push({ name: 'image', fn: this.tasks.image })
70
+ }
71
+ if (this.tasks.svg) {
72
+ finalTasks.push({ name: 'svg', fn: this.tasks.svg })
73
+ }
74
+ if (this.tasks.copy) {
75
+ finalTasks.push({ name: 'copy', fn: this.tasks.copy })
76
+ }
77
+
78
+ if (finalTasks.length > 0) {
79
+ await context.runParallel(finalTasks)
80
+ }
81
+
82
+ const elapsed = Date.now() - startTime
83
+ logger.success('build', `Completed in ${elapsed}ms`)
84
+ } catch (error) {
85
+ logger.error('build', error.message)
86
+ throw error
87
+ }
88
+ }
89
+
90
+ /**
91
+ * 監視モード(開発)
92
+ */
93
+ async watch() {
94
+ const { context } = this
95
+
96
+ try {
97
+ // ファイル監視開始
98
+ if (this.tasks.watch) {
99
+ await context.runTask('watch', this.tasks.watch)
100
+ }
101
+
102
+ // 開発サーバー起動
103
+ if (this.tasks.server) {
104
+ await context.runTask('server', this.tasks.server)
105
+ }
106
+ } catch (error) {
107
+ logger.error('watch', error.message)
108
+ throw error
109
+ }
110
+ }
111
+
112
+ /**
113
+ * 個別タスク実行
114
+ */
115
+ async runTask(taskName, options = {}) {
116
+ const task = this.tasks[taskName]
117
+
118
+ if (!task) {
119
+ throw new Error(`Task not found: ${taskName}`)
120
+ }
121
+
122
+ await this.context.runTask(taskName, task, options)
123
+ }
124
+
125
+ /**
126
+ * クリーンアップ
127
+ */
128
+ async clean() {
129
+ const distPath = this.context.paths.dist
130
+ logger.info('clean', 'Cleaning dist directory')
131
+
132
+ await cleanDir(distPath)
133
+
134
+ this.context.cache.clear()
135
+ this.context.graph.clear()
136
+
137
+ logger.success('clean', 'Completed')
138
+ }
139
+ }
package/core/cache.mjs ADDED
@@ -0,0 +1,98 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { stat, readFile } from 'node:fs/promises'
3
+ import { existsSync } from 'node:fs'
4
+
5
+ /**
6
+ * 統合キャッシュマネージャー
7
+ */
8
+ export class CacheManager {
9
+ constructor(mode) {
10
+ this.mode = mode
11
+ this.fileHashes = new Map() // ファイルパス -> ハッシュ
12
+ this.compiledCache = new Map() // Pugコンパイル済みテンプレート
13
+ this.isDevelopment = mode === 'development'
14
+ }
15
+
16
+ /**
17
+ * ファイルが変更されたかチェック
18
+ */
19
+ async isFileChanged(filePath) {
20
+ if (!existsSync(filePath)) {
21
+ return false
22
+ }
23
+
24
+ const currentHash = await this.computeHash(filePath)
25
+ const cachedHash = this.fileHashes.get(filePath)
26
+
27
+ // キャッシュが存在しない場合は変更ありとして扱う
28
+ if (cachedHash === undefined) {
29
+ this.fileHashes.set(filePath, currentHash)
30
+ return true
31
+ }
32
+
33
+ if (cachedHash === currentHash) {
34
+ return false
35
+ }
36
+
37
+ this.fileHashes.set(filePath, currentHash)
38
+ return true
39
+ }
40
+
41
+ /**
42
+ * 複数ファイルの変更をバッチチェック
43
+ */
44
+ async getChangedFiles(filePaths) {
45
+ const checks = await Promise.all(
46
+ filePaths.map(async path => ({
47
+ path,
48
+ changed: await this.isFileChanged(path)
49
+ }))
50
+ )
51
+ return checks.filter(c => c.changed).map(c => c.path)
52
+ }
53
+
54
+ /**
55
+ * Pugテンプレートのキャッシュ取得
56
+ */
57
+ getPugTemplate(filePath) {
58
+ return this.compiledCache.get(filePath)
59
+ }
60
+
61
+ /**
62
+ * Pugテンプレートをキャッシュに保存
63
+ */
64
+ setPugTemplate(filePath, template) {
65
+ if (this.isDevelopment) {
66
+ this.compiledCache.set(filePath, template)
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Pugテンプレートのキャッシュを無効化
72
+ */
73
+ invalidatePugTemplate(filePath) {
74
+ this.compiledCache.delete(filePath)
75
+ this.fileHashes.delete(filePath)
76
+ }
77
+
78
+ /**
79
+ * ハッシュ計算(ファイル内容 + 更新日時)
80
+ */
81
+ async computeHash(filePath) {
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
88
+ }
89
+ }
90
+
91
+ /**
92
+ * すべてのキャッシュをクリア
93
+ */
94
+ clear() {
95
+ this.fileHashes.clear()
96
+ this.compiledCache.clear()
97
+ }
98
+ }