@semba-ryuichiro/webpify 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 仙波 琉一朗 / Ryuichiro Semba
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # webpify
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D24.0.0-brightgreen)](https://nodejs.org/)
5
+
6
+ 画像ファイル(PNG/JPEG/GIF)を WebP 形式に変換する CLI ツール
7
+
8
+ ## 機能
9
+
10
+ - 単一ファイルまたはディレクトリ一括変換
11
+ - 再帰的なディレクトリ走査
12
+ - 品質パラメータの指定(1-100)
13
+ - 出力先ディレクトリの指定
14
+ - 既存ファイルの上書き制御
15
+ - WebP ファイル一覧表示
16
+
17
+ ## インストール
18
+
19
+ ```bash
20
+ npm install -g webpify
21
+ ```
22
+
23
+ ## 使い方
24
+
25
+ ### 基本
26
+
27
+ ```bash
28
+ # 単一ファイルを変換
29
+ webpify image.png
30
+
31
+ # ディレクトリ内の画像を一括変換
32
+ webpify ./images
33
+
34
+ # 再帰的に変換
35
+ webpify ./images -r
36
+ ```
37
+
38
+ ### オプション
39
+
40
+ | オプション | 説明 | デフォルト |
41
+ |-----------|------|-----------|
42
+ | `-o, --output <dir>` | 出力先ディレクトリ | 入力と同じ |
43
+ | `-q, --quality <n>` | 品質(1-100) | 100 |
44
+ | `-r, --recursive` | 再帰的に処理 | false |
45
+ | `-f, --force` | 既存ファイルを上書き | false |
46
+ | `--list` | WebP ファイル一覧表示 | - |
47
+ | `--quiet` | 統計情報を非表示 | false |
48
+ | `-v, --version` | バージョン表示 | - |
49
+ | `-h, --help` | ヘルプ表示 | - |
50
+
51
+ ### 例
52
+
53
+ ```bash
54
+ # 品質90で変換
55
+ webpify image.png -q 90
56
+
57
+ # 別ディレクトリに出力
58
+ webpify ./images -o ./webp-images
59
+
60
+ # 強制上書き + 再帰 + 静音
61
+ webpify ./images -r -f --quiet
62
+ ```
63
+
64
+ ## 要件
65
+
66
+ - Node.js 24.0.0 以上
67
+
68
+ ## ライセンス
69
+
70
+ [MIT](LICENSE)
71
+
72
+ ## 関連
73
+
74
+ - [コントリビューションガイド](CONTRIBUTING.md)
75
+ - [変更履歴](CHANGELOG.md)
76
+ - [セキュリティポリシー](SECURITY.md)
77
+ - [行動規範](CODE_OF_CONDUCT.md)
@@ -0,0 +1,6 @@
1
+ import type { FileSystemPort } from '../../ports/file-system.js';
2
+ /**
3
+ * Node.js fs モジュールを使用した FileSystemPort の実装
4
+ * ファイル操作とディレクトリ走査機能を提供する
5
+ */
6
+ export declare function createFsAdapter(): FileSystemPort;
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ /**
4
+ * Node.js fs モジュールを使用した FileSystemPort の実装
5
+ * ファイル操作とディレクトリ走査機能を提供する
6
+ */
7
+ export function createFsAdapter() {
8
+ return {
9
+ async exists(filePath) {
10
+ try {
11
+ await fs.access(filePath);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ },
18
+ async isDirectory(filePath) {
19
+ try {
20
+ const stat = await fs.stat(filePath);
21
+ return stat.isDirectory();
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ },
27
+ async mkdir(dirPath) {
28
+ await fs.mkdir(dirPath, { recursive: true });
29
+ },
30
+ async readDir(dirPath) {
31
+ return await fs.readdir(dirPath);
32
+ },
33
+ async readDirRecursive(dirPath) {
34
+ const result = [];
35
+ async function traverse(currentPath) {
36
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
37
+ for (const entry of entries) {
38
+ const fullPath = path.join(currentPath, entry.name);
39
+ if (entry.isDirectory()) {
40
+ await traverse(fullPath);
41
+ }
42
+ else {
43
+ result.push(fullPath);
44
+ }
45
+ }
46
+ }
47
+ await traverse(dirPath);
48
+ return result;
49
+ },
50
+ async stat(filePath) {
51
+ const stat = await fs.stat(filePath);
52
+ return { size: stat.size };
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,2 @@
1
+ export { createFsAdapter } from './fs-adapter/index.js';
2
+ export { createSharpAdapter } from './sharp-adapter/index.js';
@@ -0,0 +1,2 @@
1
+ export { createFsAdapter } from './fs-adapter/index.js';
2
+ export { createSharpAdapter } from './sharp-adapter/index.js';
@@ -0,0 +1,6 @@
1
+ import type { ImageProcessorPort } from '../../ports/image-processor.js';
2
+ /**
3
+ * sharp ライブラリを使用した ImageProcessorPort の実装
4
+ * WebP 変換と画像メタデータ取得機能を提供する
5
+ */
6
+ export declare function createSharpAdapter(): ImageProcessorPort;
@@ -0,0 +1,24 @@
1
+ import sharp from 'sharp';
2
+ /**
3
+ * sharp ライブラリを使用した ImageProcessorPort の実装
4
+ * WebP 変換と画像メタデータ取得機能を提供する
5
+ */
6
+ export function createSharpAdapter() {
7
+ return {
8
+ async convertToWebP(inputPath, outputPath, options) {
9
+ const result = await sharp(inputPath).webp({ quality: options.quality }).toFile(outputPath);
10
+ return { size: result.size };
11
+ },
12
+ async getMetadata(filePath) {
13
+ const metadata = await sharp(filePath).metadata();
14
+ if (!metadata.width || !metadata.height || !metadata.format) {
15
+ throw new Error(`Failed to get metadata for ${filePath}`);
16
+ }
17
+ return {
18
+ format: metadata.format,
19
+ height: metadata.height,
20
+ width: metadata.width,
21
+ };
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,25 @@
1
+ import type { ParsedOptions } from '../../types/index.js';
2
+ /**
3
+ * ArgumentParser サービスのインターフェース
4
+ */
5
+ export interface ArgumentParserService {
6
+ /**
7
+ * コマンドライン引数をパースする
8
+ * @param argv - process.argv 形式の配列
9
+ * @returns パースされたオプション
10
+ */
11
+ parse(argv: string[]): ParsedOptions;
12
+ /**
13
+ * ヘルプメッセージを表示する
14
+ */
15
+ showHelp(): void;
16
+ /**
17
+ * バージョン情報を表示する
18
+ */
19
+ showVersion(): void;
20
+ }
21
+ /**
22
+ * ArgumentParser のファクトリ関数
23
+ * @returns ArgumentParserService のインスタンス
24
+ */
25
+ export declare function createArgumentParser(): ArgumentParserService;
@@ -0,0 +1,108 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * デフォルトの品質値
4
+ */
5
+ const DEFAULT_QUALITY = 100;
6
+ /**
7
+ * 品質値の最小値
8
+ */
9
+ const MIN_QUALITY = 1;
10
+ /**
11
+ * 品質値の最大値
12
+ */
13
+ const MAX_QUALITY = 100;
14
+ /**
15
+ * パッケージのバージョン(package.json から取得できない場合のフォールバック)
16
+ */
17
+ const VERSION = '1.0.0';
18
+ /**
19
+ * 品質値をパースしてバリデーションするカスタムパーサー
20
+ * @param value - ユーザーが入力した品質値(文字列)
21
+ * @returns パースされた品質値
22
+ * @throws 無効な品質値の場合はエラーをスロー
23
+ */
24
+ function parseQuality(value) {
25
+ const quality = Number.parseInt(value, 10);
26
+ if (Number.isNaN(quality) || quality < MIN_QUALITY || quality > MAX_QUALITY) {
27
+ process.stderr.write(`error: Quality must be between ${MIN_QUALITY} and ${MAX_QUALITY}\n`);
28
+ process.exit(1);
29
+ }
30
+ return quality;
31
+ }
32
+ /**
33
+ * ArgumentParser のファクトリ関数
34
+ * @returns ArgumentParserService のインスタンス
35
+ */
36
+ export function createArgumentParser() {
37
+ const program = new Command();
38
+ program
39
+ .name('webpify')
40
+ .description('CLI tool to convert images to WebP format')
41
+ .version(VERSION, '-v, --version', 'Show version number')
42
+ .argument('[input]', 'Input file or directory path')
43
+ .option('-o, --output <path>', 'Output path')
44
+ .option('-q, --quality <number>', 'Quality level (1-100)', parseQuality, DEFAULT_QUALITY)
45
+ .option('-r, --recursive', 'Process directories recursively', false)
46
+ .option('-f, --force', 'Overwrite existing files', false)
47
+ .option('--quiet', 'Silent mode (no output)', false)
48
+ .option('--list', 'List WebP files with size information', false)
49
+ .configureOutput({
50
+ writeErr: (str) => process.stderr.write(str),
51
+ writeOut: (str) => process.stdout.write(str),
52
+ })
53
+ .exitOverride();
54
+ return {
55
+ parse(argv) {
56
+ // 引数なしの場合はヘルプを表示
57
+ if (argv.length <= 2) {
58
+ program.outputHelp();
59
+ return {
60
+ force: false,
61
+ input: '',
62
+ list: false,
63
+ quality: DEFAULT_QUALITY,
64
+ quiet: false,
65
+ recursive: false,
66
+ };
67
+ }
68
+ try {
69
+ program.parse(argv);
70
+ }
71
+ catch (err) {
72
+ // exitOverride()により、--help/--version時に例外がスローされる
73
+ // 例外をキャッチして正常終了を示す
74
+ if (err instanceof Error && 'code' in err) {
75
+ const code = err.code;
76
+ if (code === 'commander.helpDisplayed' || code === 'commander.version') {
77
+ return {
78
+ force: false,
79
+ input: '',
80
+ list: false,
81
+ quality: DEFAULT_QUALITY,
82
+ quiet: false,
83
+ recursive: false,
84
+ };
85
+ }
86
+ }
87
+ throw err;
88
+ }
89
+ const options = program.opts();
90
+ const args = program.args;
91
+ return {
92
+ force: options['force'],
93
+ input: args[0] || '',
94
+ list: options['list'],
95
+ output: options['output'],
96
+ quality: options['quality'],
97
+ quiet: options['quiet'],
98
+ recursive: options['recursive'],
99
+ };
100
+ },
101
+ showHelp() {
102
+ program.outputHelp();
103
+ },
104
+ showVersion() {
105
+ process.stdout.write(`${VERSION}\n`);
106
+ },
107
+ };
108
+ }
@@ -0,0 +1,3 @@
1
+ export { type ArgumentParserService, createArgumentParser } from './argument-parser/index.js';
2
+ export { createMain, type MainDependencies, type MainService } from './main/index.js';
3
+ export { createReporter, type ReporterService } from './reporter/index.js';
@@ -0,0 +1,3 @@
1
+ export { createArgumentParser } from './argument-parser/index.js';
2
+ export { createMain } from './main/index.js';
3
+ export { createReporter } from './reporter/index.js';
@@ -0,0 +1,30 @@
1
+ import type { ConverterService, FileScannerService, ImageInspectorService } from '../../core/index.js';
2
+ import type { ArgumentParserService } from '../argument-parser/index.js';
3
+ import type { ReporterService } from '../reporter/index.js';
4
+ /**
5
+ * Main サービスの依存性
6
+ */
7
+ export interface MainDependencies {
8
+ argumentParser: ArgumentParserService;
9
+ reporter: ReporterService;
10
+ converter: ConverterService;
11
+ fileScanner: FileScannerService;
12
+ imageInspector: ImageInspectorService;
13
+ }
14
+ /**
15
+ * Main サービスインターフェース
16
+ */
17
+ export interface MainService {
18
+ /**
19
+ * CLI のメイン処理を実行する
20
+ * @param argv コマンドライン引数(process.argv 形式)
21
+ * @returns 終了コード(0: 成功, 1: エラー)
22
+ */
23
+ run(argv: string[]): Promise<number>;
24
+ }
25
+ /**
26
+ * Main サービスを生成するファクトリ関数
27
+ * @param deps 依存性
28
+ * @returns MainService インスタンス
29
+ */
30
+ export declare function createMain(deps: MainDependencies): MainService;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * サポートされる変換対象の拡張子
3
+ */
4
+ const CONVERT_EXTENSIONS = ['png', 'jpeg', 'jpg', 'gif'];
5
+ /**
6
+ * WebP ファイルの拡張子
7
+ */
8
+ const WEBP_EXTENSIONS = ['webp'];
9
+ /**
10
+ * Main サービスを生成するファクトリ関数
11
+ * @param deps 依存性
12
+ * @returns MainService インスタンス
13
+ */
14
+ export function createMain(deps) {
15
+ const { argumentParser, reporter, converter, fileScanner, imageInspector } = deps;
16
+ /**
17
+ * 一覧表示モードを実行する
18
+ */
19
+ async function executeListMode(inputPath, recursive) {
20
+ const files = await fileScanner.scan(inputPath, {
21
+ extensions: WEBP_EXTENSIONS,
22
+ recursive,
23
+ });
24
+ const imageInfos = await imageInspector.getInfoBatch(files);
25
+ const items = imageInfos.map((info) => ({
26
+ height: info.height,
27
+ path: info.path,
28
+ size: info.size,
29
+ width: info.width,
30
+ }));
31
+ reporter.reportImageList(items);
32
+ return 0;
33
+ }
34
+ /**
35
+ * 単一ファイル変換モードを実行する
36
+ */
37
+ async function executeSingleFileMode(inputPath, quality, force, output, quiet) {
38
+ const result = await converter.convert(inputPath, {
39
+ force,
40
+ output,
41
+ quality,
42
+ });
43
+ reporter.reportConversion(result, quiet);
44
+ return result.error ? 1 : 0;
45
+ }
46
+ /**
47
+ * ディレクトリ変換モードを実行する
48
+ */
49
+ async function executeDirectoryMode(inputPath, quality, force, output, recursive, quiet) {
50
+ const files = await fileScanner.scan(inputPath, {
51
+ extensions: CONVERT_EXTENSIONS,
52
+ recursive,
53
+ });
54
+ // 変換対象ファイルがない場合は警告を表示(Requirement 4.4)
55
+ if (files.length === 0) {
56
+ process.stdout.write('Warning: No convertible files found\n');
57
+ return 0;
58
+ }
59
+ // 進捗コールバック
60
+ const onProgress = (result, index, total) => {
61
+ if (!quiet) {
62
+ reporter.reportProgress(index + 1, total, result.inputPath);
63
+ reporter.reportConversion(result, quiet);
64
+ }
65
+ };
66
+ const stats = await converter.convertBatch(files, { force, output, quality }, onProgress);
67
+ if (!quiet) {
68
+ reporter.reportStats(stats);
69
+ }
70
+ // エラーがあった場合は終了コード 1 を返す
71
+ return stats.errorCount > 0 ? 1 : 0;
72
+ }
73
+ return {
74
+ async run(argv) {
75
+ const options = argumentParser.parse(argv);
76
+ // 入力が空の場合は終了(ヘルプは既に表示済み)
77
+ if (!options.input) {
78
+ return 0;
79
+ }
80
+ // 入力パスの存在確認
81
+ const exists = await fileScanner.exists(options.input);
82
+ if (!exists) {
83
+ process.stderr.write(`Error: File not found: ${options.input}\n`);
84
+ return 1;
85
+ }
86
+ // 一覧表示モード(--list オプション)
87
+ if (options.list) {
88
+ const isDir = await fileScanner.isDirectory(options.input);
89
+ if (!isDir) {
90
+ // 単一ファイルの場合も一覧表示
91
+ const info = await imageInspector.getInfo(options.input);
92
+ reporter.reportImageList([
93
+ {
94
+ height: info.height,
95
+ path: info.path,
96
+ size: info.size,
97
+ width: info.width,
98
+ },
99
+ ]);
100
+ return 0;
101
+ }
102
+ return executeListMode(options.input, options.recursive);
103
+ }
104
+ // ファイル or ディレクトリの判定
105
+ const isDirectory = await fileScanner.isDirectory(options.input);
106
+ if (isDirectory) {
107
+ // ディレクトリ変換モード
108
+ return executeDirectoryMode(options.input, options.quality, options.force, options.output, options.recursive, options.quiet);
109
+ }
110
+ // 単一ファイル変換モード
111
+ return executeSingleFileMode(options.input, options.quality, options.force, options.output, options.quiet);
112
+ },
113
+ };
114
+ }
@@ -0,0 +1,34 @@
1
+ import type { ConversionResult, ConversionStats, ImageListItem } from '../../types/index.js';
2
+ /**
3
+ * Reporter サービスのインターフェース
4
+ */
5
+ export interface ReporterService {
6
+ /**
7
+ * 単一ファイルの変換結果を表示する
8
+ * @param result - 変換結果
9
+ * @param quiet - サイレントモードかどうか
10
+ */
11
+ reportConversion(result: ConversionResult, quiet: boolean): void;
12
+ /**
13
+ * 進捗を表示する
14
+ * @param current - 現在の処理番号
15
+ * @param total - 総ファイル数
16
+ * @param fileName - 処理中のファイル名
17
+ */
18
+ reportProgress(current: number, total: number, fileName: string): void;
19
+ /**
20
+ * バッチ変換の統計情報を表示する
21
+ * @param stats - 統計情報
22
+ */
23
+ reportStats(stats: ConversionStats): void;
24
+ /**
25
+ * WebP ファイル一覧を表示する
26
+ * @param items - 画像アイテムのリスト
27
+ */
28
+ reportImageList(items: ImageListItem[]): void;
29
+ }
30
+ /**
31
+ * Reporter のファクトリ関数
32
+ * @returns ReporterService のインスタンス
33
+ */
34
+ export declare function createReporter(): ReporterService;
@@ -0,0 +1,87 @@
1
+ import path from 'node:path';
2
+ /**
3
+ * バイト数を人間が読みやすい形式にフォーマットする
4
+ * @param bytes - バイト数
5
+ * @returns フォーマットされた文字列(例: "10.00 KB")
6
+ */
7
+ function formatSize(bytes) {
8
+ if (bytes < 1024) {
9
+ return `${bytes} B`;
10
+ }
11
+ if (bytes < 1024 * 1024) {
12
+ return `${(bytes / 1024).toFixed(2)} KB`;
13
+ }
14
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
15
+ }
16
+ /**
17
+ * サイズ削減率を計算してフォーマットする
18
+ * @param inputSize - 入力サイズ
19
+ * @param outputSize - 出力サイズ
20
+ * @returns フォーマットされた削減率(例: "50.0%" または "+20.0%")
21
+ */
22
+ function formatReduction(inputSize, outputSize) {
23
+ if (inputSize === 0) {
24
+ return '0.0%';
25
+ }
26
+ const reduction = ((inputSize - outputSize) / inputSize) * 100;
27
+ if (reduction < 0) {
28
+ return `+${Math.abs(reduction).toFixed(1)}%`;
29
+ }
30
+ return `${reduction.toFixed(1)}%`;
31
+ }
32
+ /**
33
+ * Reporter のファクトリ関数
34
+ * @returns ReporterService のインスタンス
35
+ */
36
+ export function createReporter() {
37
+ return {
38
+ reportConversion(result, quiet) {
39
+ if (quiet) {
40
+ return;
41
+ }
42
+ const fileName = path.basename(result.inputPath);
43
+ if (result.error) {
44
+ process.stdout.write(`Error: ${fileName} - ${result.error}\n`);
45
+ return;
46
+ }
47
+ if (result.skipped) {
48
+ process.stdout.write(`Skipped: ${fileName} (file already exists)\n`);
49
+ return;
50
+ }
51
+ const inputSizeStr = formatSize(result.inputSize);
52
+ const outputSizeStr = formatSize(result.outputSize);
53
+ const reduction = formatReduction(result.inputSize, result.outputSize);
54
+ process.stdout.write(`Converted: ${fileName} (${inputSizeStr} -> ${outputSizeStr}, ${reduction})\n`);
55
+ },
56
+ reportImageList(items) {
57
+ if (items.length === 0) {
58
+ process.stdout.write('WebP ファイルが見つかりません\n');
59
+ return;
60
+ }
61
+ process.stdout.write('\n--- WebP File List ---\n');
62
+ process.stdout.write(`${'File'.padEnd(40)} ${'Size'.padEnd(12)} ${'Width'.padEnd(8)} ${'Height'.padEnd(8)}\n`);
63
+ process.stdout.write(`${'-'.repeat(70)}\n`);
64
+ for (const item of items) {
65
+ const fileName = path.basename(item.path);
66
+ const sizeStr = formatSize(item.size);
67
+ process.stdout.write(`${fileName.padEnd(40)} ${sizeStr.padEnd(12)} ${String(item.width).padEnd(8)} ${String(item.height).padEnd(8)}\n`);
68
+ }
69
+ },
70
+ reportProgress(current, total, fileName) {
71
+ process.stdout.write(`[${current}/${total}] Processing: ${fileName}\n`);
72
+ },
73
+ reportStats(stats) {
74
+ process.stdout.write('\n--- Conversion Summary ---\n');
75
+ process.stdout.write(`Total files: ${stats.totalFiles}\n`);
76
+ process.stdout.write(`Converted: ${stats.successCount}\n`);
77
+ process.stdout.write(`Skipped: ${stats.skippedCount}\n`);
78
+ process.stdout.write(`Errors: ${stats.errorCount}\n`);
79
+ if (stats.successCount > 0 && stats.totalInputSize > 0) {
80
+ const inputSizeStr = formatSize(stats.totalInputSize);
81
+ const outputSizeStr = formatSize(stats.totalOutputSize);
82
+ const reduction = formatReduction(stats.totalInputSize, stats.totalOutputSize);
83
+ process.stdout.write(`Total size: ${inputSizeStr} -> ${outputSizeStr} (${reduction})\n`);
84
+ }
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,46 @@
1
+ import type { FileSystemPort } from '../../ports/file-system.js';
2
+ import type { ImageProcessorPort } from '../../ports/image-processor.js';
3
+ import type { ConversionResult, ConversionStats, ConvertOptions } from '../../types/index.js';
4
+ /**
5
+ * Converter の依存性
6
+ */
7
+ export interface ConverterDependencies {
8
+ fileSystem: FileSystemPort;
9
+ imageProcessor: ImageProcessorPort;
10
+ }
11
+ /**
12
+ * 進捗コールバック関数の型
13
+ */
14
+ export type ProgressCallback = (result: ConversionResult, index: number, total: number) => void;
15
+ /**
16
+ * Converter サービスインターフェース
17
+ */
18
+ export interface ConverterService {
19
+ /**
20
+ * ファイルがサポートされた形式かどうか判定する
21
+ * @param filePath ファイルパス
22
+ * @returns サポートされている場合は true
23
+ */
24
+ isSupportedFormat(filePath: string): boolean;
25
+ /**
26
+ * 単一ファイルを WebP に変換する
27
+ * @param inputPath 入力ファイルパス
28
+ * @param options 変換オプション
29
+ * @returns 変換結果
30
+ */
31
+ convert(inputPath: string, options: ConvertOptions): Promise<ConversionResult>;
32
+ /**
33
+ * 複数ファイルを一括で WebP に変換する
34
+ * @param inputPaths 入力ファイルパスの配列
35
+ * @param options 変換オプション
36
+ * @param onProgress 進捗コールバック(オプション)
37
+ * @returns 変換統計情報
38
+ */
39
+ convertBatch(inputPaths: string[], options: ConvertOptions, onProgress?: ProgressCallback): Promise<ConversionStats>;
40
+ }
41
+ /**
42
+ * Converter を生成するファクトリ関数
43
+ * @param deps 依存性
44
+ * @returns ConverterService インスタンス
45
+ */
46
+ export declare function createConverter(deps: ConverterDependencies): ConverterService;
@@ -0,0 +1,135 @@
1
+ import path from 'node:path';
2
+ /**
3
+ * サポートされる拡張子のリスト
4
+ */
5
+ const SUPPORTED_EXTENSIONS = ['png', 'jpeg', 'jpg', 'gif'];
6
+ /**
7
+ * Converter を生成するファクトリ関数
8
+ * @param deps 依存性
9
+ * @returns ConverterService インスタンス
10
+ */
11
+ export function createConverter(deps) {
12
+ const { fileSystem, imageProcessor } = deps;
13
+ /**
14
+ * ファイルの拡張子を取得する(小文字で返す)
15
+ */
16
+ function getExtension(filePath) {
17
+ return path.extname(filePath).slice(1).toLowerCase();
18
+ }
19
+ /**
20
+ * 出力パスを決定する
21
+ * @param inputPath 入力ファイルパス
22
+ * @param output 出力先オプション(ディレクトリ)
23
+ */
24
+ function resolveOutputPath(inputPath, output) {
25
+ const inputDir = path.dirname(inputPath);
26
+ const inputBasename = path.basename(inputPath, path.extname(inputPath));
27
+ const outputFilename = `${inputBasename}.webp`;
28
+ if (output) {
29
+ return path.join(output, outputFilename);
30
+ }
31
+ return path.join(inputDir, outputFilename);
32
+ }
33
+ /**
34
+ * エラー結果を生成するヘルパー関数
35
+ */
36
+ function createErrorResult(inputPath, outputPath, error, inputSize = 0) {
37
+ return {
38
+ error,
39
+ inputPath,
40
+ inputSize,
41
+ outputPath,
42
+ outputSize: 0,
43
+ skipped: false,
44
+ };
45
+ }
46
+ return {
47
+ async convert(inputPath, options) {
48
+ const outputPath = resolveOutputPath(inputPath, options.output);
49
+ // 入力ファイルの存在確認(Requirement 1.4)
50
+ const inputExists = await fileSystem.exists(inputPath);
51
+ if (!inputExists) {
52
+ return createErrorResult(inputPath, outputPath, `File not found: ${inputPath}`);
53
+ }
54
+ // サポート形式の検証(Requirement 1.5)
55
+ const ext = getExtension(inputPath);
56
+ if (!SUPPORTED_EXTENSIONS.includes(ext)) {
57
+ return createErrorResult(inputPath, outputPath, `Unsupported format: ${ext}. Supported: ${SUPPORTED_EXTENSIONS.join(', ')}`);
58
+ }
59
+ // 既存ファイルのスキップ処理(Requirement 5.1, 5.2)
60
+ const outputExists = await fileSystem.exists(outputPath);
61
+ if (outputExists && !options.force) {
62
+ const inputStat = await fileSystem.stat(inputPath);
63
+ return {
64
+ inputPath,
65
+ inputSize: inputStat.size,
66
+ outputPath,
67
+ outputSize: 0,
68
+ skipped: true,
69
+ };
70
+ }
71
+ // 出力ディレクトリが存在しない場合は作成
72
+ const outputDir = path.dirname(outputPath);
73
+ const outputDirExists = await fileSystem.exists(outputDir);
74
+ if (!outputDirExists) {
75
+ await fileSystem.mkdir(outputDir);
76
+ }
77
+ // 入力ファイルのサイズを取得
78
+ const inputStat = await fileSystem.stat(inputPath);
79
+ // WebP に変換
80
+ try {
81
+ const result = await imageProcessor.convertToWebP(inputPath, outputPath, {
82
+ quality: options.quality,
83
+ });
84
+ return {
85
+ inputPath,
86
+ inputSize: inputStat.size,
87
+ outputPath,
88
+ outputSize: result.size,
89
+ skipped: false,
90
+ };
91
+ }
92
+ catch (err) {
93
+ const errorMessage = err instanceof Error ? err.message : String(err);
94
+ return createErrorResult(inputPath, outputPath, `Image processing failed: ${errorMessage}`, inputStat.size);
95
+ }
96
+ },
97
+ async convertBatch(inputPaths, options, onProgress) {
98
+ // 統計情報の初期化
99
+ const stats = {
100
+ errorCount: 0,
101
+ skippedCount: 0,
102
+ successCount: 0,
103
+ totalFiles: inputPaths.length,
104
+ totalInputSize: 0,
105
+ totalOutputSize: 0,
106
+ };
107
+ const total = inputPaths.length;
108
+ // 各ファイルを順次変換
109
+ for (const [index, inputPath] of inputPaths.entries()) {
110
+ const result = await this.convert(inputPath, options);
111
+ // 統計情報を更新
112
+ stats.totalInputSize += result.inputSize;
113
+ stats.totalOutputSize += result.outputSize;
114
+ if (result.error) {
115
+ stats.errorCount++;
116
+ }
117
+ else if (result.skipped) {
118
+ stats.skippedCount++;
119
+ }
120
+ else {
121
+ stats.successCount++;
122
+ }
123
+ // 進捗コールバックを呼び出し
124
+ if (onProgress) {
125
+ onProgress(result, index, total);
126
+ }
127
+ }
128
+ return stats;
129
+ },
130
+ isSupportedFormat(filePath) {
131
+ const ext = getExtension(filePath);
132
+ return SUPPORTED_EXTENSIONS.includes(ext);
133
+ },
134
+ };
135
+ }
@@ -0,0 +1,36 @@
1
+ import type { FileSystemPort } from '../../ports/file-system.js';
2
+ import type { ScanOptions } from '../../types/index.js';
3
+ /**
4
+ * FileScanner の依存性
5
+ */
6
+ export interface FileScannerDependencies {
7
+ fileSystem: FileSystemPort;
8
+ }
9
+ /**
10
+ * FileScanner サービスインターフェース
11
+ */
12
+ export interface FileScannerService {
13
+ /**
14
+ * 指定されたディレクトリ内のファイルをスキャンする
15
+ * @param directoryPath ディレクトリパス
16
+ * @param options スキャンオプション
17
+ * @returns マッチしたファイルパスの配列
18
+ */
19
+ scan(directoryPath: string, options: ScanOptions): Promise<string[]>;
20
+ /**
21
+ * パスがディレクトリかどうか判定する
22
+ * @param filePath パス
23
+ */
24
+ isDirectory(filePath: string): Promise<boolean>;
25
+ /**
26
+ * パスが存在するか確認する
27
+ * @param filePath パス
28
+ */
29
+ exists(filePath: string): Promise<boolean>;
30
+ }
31
+ /**
32
+ * FileScanner を生成するファクトリ関数
33
+ * @param deps 依存性
34
+ * @returns FileScannerService インスタンス
35
+ */
36
+ export declare function createFileScanner(deps: FileScannerDependencies): FileScannerService;
@@ -0,0 +1,39 @@
1
+ import path from 'node:path';
2
+ /**
3
+ * FileScanner を生成するファクトリ関数
4
+ * @param deps 依存性
5
+ * @returns FileScannerService インスタンス
6
+ */
7
+ export function createFileScanner(deps) {
8
+ const { fileSystem } = deps;
9
+ /**
10
+ * 拡張子がマッチするかチェックする(大文字小文字を区別しない)
11
+ */
12
+ function matchesExtension(fileName, extensions) {
13
+ const ext = path.extname(fileName).slice(1).toLowerCase();
14
+ return extensions.map((e) => e.toLowerCase()).includes(ext);
15
+ }
16
+ return {
17
+ async exists(filePath) {
18
+ return fileSystem.exists(filePath);
19
+ },
20
+ async isDirectory(filePath) {
21
+ return fileSystem.isDirectory(filePath);
22
+ },
23
+ async scan(directoryPath, options) {
24
+ const { recursive, extensions } = options;
25
+ if (recursive) {
26
+ // 再帰モード: readDirRecursive で全ファイルを取得(フルパスが返る)
27
+ const allFiles = await fileSystem.readDirRecursive(directoryPath);
28
+ return allFiles.filter((filePath) => matchesExtension(filePath, extensions));
29
+ }
30
+ else {
31
+ // 非再帰モード: readDir でファイル名のみ取得
32
+ const fileNames = await fileSystem.readDir(directoryPath);
33
+ return fileNames
34
+ .filter((fileName) => matchesExtension(fileName, extensions))
35
+ .map((fileName) => path.join(directoryPath, fileName));
36
+ }
37
+ },
38
+ };
39
+ }
@@ -0,0 +1,33 @@
1
+ import type { FileSystemPort } from '../../ports/file-system.js';
2
+ import type { ImageProcessorPort } from '../../ports/image-processor.js';
3
+ import type { ImageInfo } from '../../types/index.js';
4
+ /**
5
+ * ImageInspector の依存性
6
+ */
7
+ export interface ImageInspectorDependencies {
8
+ fileSystem: FileSystemPort;
9
+ imageProcessor: ImageProcessorPort;
10
+ }
11
+ /**
12
+ * ImageInspector サービスインターフェース
13
+ */
14
+ export interface ImageInspectorService {
15
+ /**
16
+ * 画像ファイルのメタデータを取得する
17
+ * @param filePath ファイルパス
18
+ * @returns 画像情報
19
+ */
20
+ getInfo(filePath: string): Promise<ImageInfo>;
21
+ /**
22
+ * 複数の画像ファイルのメタデータを一括取得する
23
+ * @param filePaths ファイルパスの配列
24
+ * @returns 画像情報の配列
25
+ */
26
+ getInfoBatch(filePaths: string[]): Promise<ImageInfo[]>;
27
+ }
28
+ /**
29
+ * ImageInspector を生成するファクトリ関数
30
+ * @param deps 依存性
31
+ * @returns ImageInspectorService インスタンス
32
+ */
33
+ export declare function createImageInspector(deps: ImageInspectorDependencies): ImageInspectorService;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * ImageInspector を生成するファクトリ関数
3
+ * @param deps 依存性
4
+ * @returns ImageInspectorService インスタンス
5
+ */
6
+ export function createImageInspector(deps) {
7
+ const { fileSystem, imageProcessor } = deps;
8
+ return {
9
+ async getInfo(filePath) {
10
+ // ファイルサイズを取得
11
+ const stat = await fileSystem.stat(filePath);
12
+ // 画像のメタデータを取得
13
+ const metadata = await imageProcessor.getMetadata(filePath);
14
+ return {
15
+ format: metadata.format,
16
+ height: metadata.height,
17
+ path: filePath,
18
+ size: stat.size,
19
+ width: metadata.width,
20
+ };
21
+ },
22
+ async getInfoBatch(filePaths) {
23
+ const results = [];
24
+ for (const filePath of filePaths) {
25
+ const info = await this.getInfo(filePath);
26
+ results.push(info);
27
+ }
28
+ return results;
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,3 @@
1
+ export { type ConverterDependencies, type ConverterService, createConverter, type ProgressCallback, } from './converter/index.js';
2
+ export { createFileScanner, type FileScannerDependencies, type FileScannerService } from './file-scanner/index.js';
3
+ export { createImageInspector, type ImageInspectorDependencies, type ImageInspectorService, } from './image-inspector/index.js';
@@ -0,0 +1,3 @@
1
+ export { createConverter, } from './converter/index.js';
2
+ export { createFileScanner } from './file-scanner/index.js';
3
+ export { createImageInspector, } from './image-inspector/index.js';
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ import { createFsAdapter, createSharpAdapter } from './adapters/index.js';
3
+ import { createArgumentParser, createMain, createReporter } from './cli/index.js';
4
+ import { createConverter, createFileScanner, createImageInspector } from './core/index.js';
5
+ /**
6
+ * エントリポイント
7
+ * 依存性を組み立てて、メイン処理を実行する
8
+ */
9
+ async function main() {
10
+ // アダプターの生成
11
+ const fsAdapter = createFsAdapter();
12
+ const sharpAdapter = createSharpAdapter();
13
+ // コアコンポーネントの生成
14
+ const converter = createConverter({
15
+ fileSystem: fsAdapter,
16
+ imageProcessor: sharpAdapter,
17
+ });
18
+ const fileScanner = createFileScanner({
19
+ fileSystem: fsAdapter,
20
+ });
21
+ const imageInspector = createImageInspector({
22
+ fileSystem: fsAdapter,
23
+ imageProcessor: sharpAdapter,
24
+ });
25
+ // CLI コンポーネントの生成
26
+ const argumentParser = createArgumentParser();
27
+ const reporter = createReporter();
28
+ // メインサービスの生成と実行
29
+ const mainService = createMain({
30
+ argumentParser,
31
+ converter,
32
+ fileScanner,
33
+ imageInspector,
34
+ reporter,
35
+ });
36
+ const exitCode = await mainService.run(process.argv);
37
+ process.exit(exitCode);
38
+ }
39
+ main().catch((error) => {
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ process.stderr.write(`Fatal error: ${message}\n`);
42
+ process.exit(1);
43
+ });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * ファイルシステムポートインターフェース
3
+ * Node.js fs モジュールを抽象化し、テスト時にモック注入を可能にする
4
+ */
5
+ export interface FileSystemPort {
6
+ /**
7
+ * パスが存在するか確認する
8
+ * @param path ファイルまたはディレクトリのパス
9
+ */
10
+ exists(path: string): Promise<boolean>;
11
+ /**
12
+ * パスがディレクトリかどうか確認する
13
+ * @param path パス
14
+ */
15
+ isDirectory(path: string): Promise<boolean>;
16
+ /**
17
+ * ディレクトリ内のファイル一覧を取得する
18
+ * @param path ディレクトリパス
19
+ */
20
+ readDir(path: string): Promise<string[]>;
21
+ /**
22
+ * ディレクトリ内のファイルを再帰的に取得する
23
+ * @param path ディレクトリパス
24
+ */
25
+ readDirRecursive(path: string): Promise<string[]>;
26
+ /**
27
+ * ディレクトリを作成する(再帰的)
28
+ * @param path ディレクトリパス
29
+ */
30
+ mkdir(path: string): Promise<void>;
31
+ /**
32
+ * ファイルの統計情報を取得する
33
+ * @param path ファイルパス
34
+ */
35
+ stat(path: string): Promise<{
36
+ size: number;
37
+ }>;
38
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 画像処理ポートインターフェース
3
+ * sharp などの画像処理ライブラリを抽象化し、テスト時にモック注入を可能にする
4
+ */
5
+ export interface ImageProcessorPort {
6
+ /**
7
+ * 画像を WebP 形式に変換する
8
+ * @param inputPath 入力ファイルパス
9
+ * @param outputPath 出力ファイルパス
10
+ * @param options 変換オプション
11
+ * @returns 変換後のファイルサイズ情報
12
+ */
13
+ convertToWebP(inputPath: string, outputPath: string, options: {
14
+ quality: number;
15
+ }): Promise<{
16
+ size: number;
17
+ }>;
18
+ /**
19
+ * 画像のメタデータを取得する
20
+ * @param filePath ファイルパス
21
+ * @returns 画像の幅、高さ、フォーマット
22
+ */
23
+ getMetadata(filePath: string): Promise<{
24
+ width: number;
25
+ height: number;
26
+ format: string;
27
+ }>;
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export type { FileSystemPort } from './file-system.js';
2
+ export type { ImageProcessorPort } from './image-processor.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,105 @@
1
+ /**
2
+ * サポートされる画像フォーマット
3
+ */
4
+ export type SupportedFormat = 'png' | 'jpeg' | 'jpg' | 'gif';
5
+ /**
6
+ * 変換オプション
7
+ */
8
+ export interface ConvertOptions {
9
+ /** 出力先パス(省略時は入力ファイルと同じディレクトリ) */
10
+ output?: string | undefined;
11
+ /** 変換品質(1-100、デフォルト: 80) */
12
+ quality: number;
13
+ /** 既存ファイルを上書きするかどうか */
14
+ force: boolean;
15
+ }
16
+ /**
17
+ * 単一ファイルの変換結果
18
+ */
19
+ export interface ConversionResult {
20
+ /** 入力ファイルパス */
21
+ inputPath: string;
22
+ /** 出力ファイルパス */
23
+ outputPath: string;
24
+ /** 入力ファイルサイズ(バイト) */
25
+ inputSize: number;
26
+ /** 出力ファイルサイズ(バイト) */
27
+ outputSize: number;
28
+ /** スキップされたかどうか */
29
+ skipped: boolean;
30
+ /** エラーメッセージ(エラー発生時のみ) */
31
+ error?: string | undefined;
32
+ }
33
+ /**
34
+ * バッチ変換の統計情報
35
+ */
36
+ export interface ConversionStats {
37
+ /** 処理対象のファイル総数 */
38
+ totalFiles: number;
39
+ /** 変換成功数 */
40
+ successCount: number;
41
+ /** スキップ数 */
42
+ skippedCount: number;
43
+ /** エラー数 */
44
+ errorCount: number;
45
+ /** 入力ファイルの合計サイズ(バイト) */
46
+ totalInputSize: number;
47
+ /** 出力ファイルの合計サイズ(バイト) */
48
+ totalOutputSize: number;
49
+ }
50
+ /**
51
+ * 画像情報(一覧表示用)
52
+ */
53
+ export interface ImageInfo {
54
+ /** ファイルパス */
55
+ path: string;
56
+ /** ファイルサイズ(バイト) */
57
+ size: number;
58
+ /** 画像の幅(ピクセル) */
59
+ width: number;
60
+ /** 画像の高さ(ピクセル) */
61
+ height: number;
62
+ /** 画像フォーマット */
63
+ format: string;
64
+ }
65
+ /**
66
+ * ファイルスキャンオプション
67
+ */
68
+ export interface ScanOptions {
69
+ /** サブディレクトリも再帰的にスキャンするか */
70
+ recursive: boolean;
71
+ /** フィルタリングする拡張子リスト */
72
+ extensions: string[];
73
+ }
74
+ /**
75
+ * CLI パースオプション
76
+ */
77
+ export interface ParsedOptions {
78
+ /** 入力パス(ファイルまたはディレクトリ) */
79
+ input: string;
80
+ /** 出力先パス */
81
+ output?: string | undefined;
82
+ /** 変換品質(1-100) */
83
+ quality: number;
84
+ /** 再帰的に処理するか */
85
+ recursive: boolean;
86
+ /** 既存ファイルを上書きするか */
87
+ force: boolean;
88
+ /** サイレントモード */
89
+ quiet: boolean;
90
+ /** WebP ファイル一覧表示モード */
91
+ list: boolean;
92
+ }
93
+ /**
94
+ * 一覧表示用の画像アイテム
95
+ */
96
+ export interface ImageListItem {
97
+ /** ファイルパス */
98
+ path: string;
99
+ /** ファイルサイズ(バイト) */
100
+ size: number;
101
+ /** 画像の幅(ピクセル) */
102
+ width: number;
103
+ /** 画像の高さ(ピクセル) */
104
+ height: number;
105
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,115 @@
1
+ {
2
+ "author": "Ryuichiro Semba",
3
+ "bin": {
4
+ "webpify": "dist/index.js"
5
+ },
6
+ "bugs": {
7
+ "url": "https://github.com/semba-yui/webpify/issues"
8
+ },
9
+ "dependencies": {
10
+ "commander": "catalog:",
11
+ "sharp": "catalog:"
12
+ },
13
+ "description": "CLI tool to convert images to WebP format",
14
+ "devDependencies": {
15
+ "@biomejs/biome": "catalog:",
16
+ "@commitlint/cli": "catalog:",
17
+ "@commitlint/config-conventional": "catalog:",
18
+ "@fast-check/vitest": "catalog:",
19
+ "@secretlint/secretlint-rule-preset-recommend": "catalog:",
20
+ "@stryker-mutator/core": "catalog:",
21
+ "@stryker-mutator/typescript-checker": "catalog:",
22
+ "@stryker-mutator/vitest-runner": "catalog:",
23
+ "@types/node": "catalog:",
24
+ "@vitest/coverage-v8": "catalog:",
25
+ "dependency-cruiser": "catalog:",
26
+ "fast-check": "catalog:",
27
+ "lefthook": "catalog:",
28
+ "remark": "catalog:",
29
+ "remark-cli": "catalog:",
30
+ "remark-frontmatter": "catalog:",
31
+ "remark-gfm": "catalog:",
32
+ "remark-lint": "catalog:",
33
+ "remark-preset-lint-consistent": "catalog:",
34
+ "remark-preset-lint-recommended": "catalog:",
35
+ "remark-toc": "catalog:",
36
+ "secretlint": "catalog:",
37
+ "ts-node": "catalog:",
38
+ "typedoc": "catalog:",
39
+ "typedoc-plugin-merge-modules": "catalog:",
40
+ "typedoc-theme-hierarchy": "catalog:",
41
+ "typescript": "catalog:",
42
+ "vitest": "catalog:"
43
+ },
44
+ "devEngines": {
45
+ "runtime": {
46
+ "name": "node",
47
+ "onFail": "download",
48
+ "version": ">=24.11.0"
49
+ }
50
+ },
51
+ "engines": {
52
+ "runtime": {
53
+ "name": "node",
54
+ "onFail": "download",
55
+ "version": "^24.11.0"
56
+ }
57
+ },
58
+ "exports": {
59
+ ".": {
60
+ "import": "./dist/index.js",
61
+ "types": "./dist/index.d.ts"
62
+ }
63
+ },
64
+ "files": [
65
+ "dist"
66
+ ],
67
+ "homepage": "https://github.com/semba-yui/webpify#readme",
68
+ "keywords": [
69
+ "cli",
70
+ "converter",
71
+ "gif",
72
+ "image",
73
+ "image-processing",
74
+ "jpeg",
75
+ "jpg",
76
+ "optimization",
77
+ "png",
78
+ "sharp",
79
+ "webp"
80
+ ],
81
+ "license": "MIT",
82
+ "main": "./dist/index.js",
83
+ "name": "@semba-ryuichiro/webpify",
84
+ "packageManager": "pnpm@10.26.2",
85
+ "publishConfig": {
86
+ "access": "public",
87
+ "registry": "https://registry.npmjs.org/"
88
+ },
89
+ "repository": {
90
+ "type": "git",
91
+ "url": "git+https://github.com/semba-yui/webpify.git"
92
+ },
93
+ "scripts": {
94
+ "build": "tsc",
95
+ "deps": "dependency-cruiser src --config .dependency-cruiser.cjs",
96
+ "deps:graph": "dependency-cruiser src --config .dependency-cruiser.cjs --output-type dot | dot -T svg > dependency-graph.svg",
97
+ "dev": "tsc --watch",
98
+ "docs:gen": "typedoc",
99
+ "lint": "biome check .",
100
+ "lint:fix": "biome check --write .",
101
+ "lint:md": "remark . --quiet",
102
+ "lint:md:fix": "remark . --quiet --output",
103
+ "lint:secret": "secretlint \"**/*\"",
104
+ "prepublishOnly": "pnpm run lint && pnpm run test && pnpm run build",
105
+ "test": "vitest run",
106
+ "test:coverage": "vitest run --coverage",
107
+ "test:mutation": "stryker run",
108
+ "test:mutation:dry": "stryker run --dryRunOnly",
109
+ "test:watch": "vitest",
110
+ "typecheck": "tsc --noEmit"
111
+ },
112
+ "type": "module",
113
+ "types": "./dist/index.d.ts",
114
+ "version": "1.0.0"
115
+ }