Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions packages/@d-zero/dom-scanner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# `@d-zero/dom-scanner`

指定ディレクトリ内のHTMLファイルとPugファイルから特定のCSSセレクタにマッチする要素を検索して報告するCLIツールです。

## 使い方

### 基本的な使い方

```sh
npx @d-zero/dom-scanner <selector> [options]
```

**引数**

- `selector` - CSSセレクタ(必須)

**オプション**

- `-d, --dir, --directory <directory>` - 検索対象のディレクトリパス(デフォルト: 現在のディレクトリ)
- `--ext, --extension <extensions>` - 検索対象の拡張子(カンマ区切り、デフォルト: `html`)
- 例: `--ext html,pug` でHTMLとPugファイルの両方を検索
- `-p, --processor <processor>` - 使用するプロセッサーを明示的に指定(`html` または `pug`)
- 例: `--processor pug` で全てのファイルをPugプロセッサーで処理
- 拡張子ごとのデフォルトプロセッサー: `html` → `html`, `pug` → `pug`
- `-x, --exclude-dirs <dirs>` - 除外するディレクトリ名(カンマ区切り)
- 例: `--exclude-dirs node_modules,dist` で特定のディレクトリを除外
- `--verbose` - 詳細なログを表示
- `--ignore <pattern>` - 無視するファイルパターン(複数指定可能)

### 使用例

```sh
# 現在のディレクトリでHTMLファイルのみを検索(デフォルト)
npx @d-zero/dom-scanner "button"

# 指定ディレクトリで検索
npx @d-zero/dom-scanner "button" --dir ./src

# HTMLとPugファイルの両方を検索
npx @d-zero/dom-scanner "button" --dir ./src --ext html,pug

# HTMLファイルをPugプロセッサーで処理(極端な例)
npx @d-zero/dom-scanner "button" --dir ./src --ext html --processor pug

# 除外ディレクトリをカスタマイズ
npx @d-zero/dom-scanner "button" --exclude-dirs node_modules,dist
```

## API

このパッケージはAPIとしても使用できます。

### 基本的な使い方

```typescript
import { scanDirectory } from '@d-zero/dom-scanner';

const results = await scanDirectory('./src', 'button', {
extensions: ['html', 'pug'],
});

for (const result of results) {
console.log(`${result.filePath}: ${result.count}件`);
}
```

## 動作環境

- Node.js 20.11以降
37 changes: 37 additions & 0 deletions packages/@d-zero/dom-scanner/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@d-zero/dom-scanner",
"version": "1.0.0",
"description": "Scan HTML and Pug files in a directory to find elements matching a CSS selector",
"author": "D-ZERO",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"bin": "./dist/cli.js",
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"clean": "tsc --build --clean"
},
"dependencies": {
"@d-zero/cli-core": "1.2.5",
"@d-zero/shared": "0.16.0",
"cheerio": "^1.0.0",
"pug": "^3.0.3"
},
"devDependencies": {
"@types/node": "24.10.1"
}
}
121 changes: 121 additions & 0 deletions packages/@d-zero/dom-scanner/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env node

import type { ProcessorType } from './types.js';
import type { BaseCLIOptions } from '@d-zero/cli-core';

import { cwd } from 'node:process';

import { createCLI, parseCommonOptions, parseList } from '@d-zero/cli-core';

import { scanDirectory } from './scanner.js';

interface DomScannerCLIOptions extends BaseCLIOptions {
dir?: string;
directory?: string;
ext?: string;
extension?: string;
processor?: string;
ignore?: string | string[];
'exclude-dirs'?: string;
excludeDirs?: string;
}

const { options: cliOptions, args } = createCLI<DomScannerCLIOptions>({
aliases: {
d: 'dir',
D: 'directory',
e: 'ext',
E: 'extension',
p: 'processor',
i: 'ignore',
x: 'exclude-dirs',
v: 'verbose',
},
usage: [
'Usage: dom-scanner <selector> [options]',
'',
'Arguments:',
'\t<selector> CSS selector (required)',
'',
'Options:',
'\t-d, --dir <directory> Directory to scan (default: current directory)',
'\t-D, --directory Alias for --dir',
'\t-e, --ext <extensions> File extensions to search (comma-separated, default: html)',
'\t-E, --extension Alias for --ext',
'\t-p, --processor <proc> Processor to use: html or pug (default: auto-detect by extension)',
'\t-i, --ignore <pattern> Ignore file patterns (can be specified multiple times)',
'\t-x, --exclude-dirs <dirs> Exclude directories (comma-separated)',
'\t-v, --verbose Enable verbose logging',
'',
'Examples:',
'\tdom-scanner "button"',
'\tdom-scanner "button" --dir ./src',
'\tdom-scanner "button" --ext html,pug',
'\tdom-scanner "button" --dir ./src --ext html --processor pug',
'\tdom-scanner "button" --exclude-dirs node_modules,dist',
],
parseArgs: (cli) => ({
...parseCommonOptions(cli),
dir: cli.dir ?? cli.directory,
ext: cli.ext ?? cli.extension,
processor: cli.processor,
ignore: cli.ignore,
'exclude-dirs': cli['exclude-dirs'] ?? cli.excludeDirs,
}),
validateArgs: (_options, cli) => {
return cli._.length > 0;
},
});

const [selector] = args;

if (!selector) {
process.stderr.write('Error: selector is required\n');
process.exit(1);
}

const directory = cliOptions.dir ?? cwd();

const extensions = cliOptions.ext
? parseList(cliOptions.ext).map((ext) => ext.toLowerCase().trim())
: undefined;

const processor = cliOptions.processor as ProcessorType | undefined;
if (processor && processor !== 'html' && processor !== 'pug') {
process.stderr.write(`Error: processor must be 'html' or 'pug', got '${processor}'\n`);
process.exit(1);
}

const ignorePatterns = cliOptions.ignore
? Array.isArray(cliOptions.ignore)
? cliOptions.ignore
: [cliOptions.ignore]
: undefined;

const excludeDirs = cliOptions['exclude-dirs']
? parseList(cliOptions['exclude-dirs']).map((dir) => dir.trim())
: undefined;

const summary = await scanDirectory(directory, selector, {
extensions,
processor,
verbose: cliOptions.verbose,
ignore: ignorePatterns,
excludeDirs,
});

// 結果を表示
if (summary.results.length === 0) {
process.stdout.write('検索結果: 見つかりませんでした\n');
} else {
process.stdout.write('検索結果:\n');
for (const result of summary.results) {
process.stdout.write(` ${result.filePath}: ${result.count}件\n`);
}
process.stdout.write('\n');
process.stdout.write(
`合計: ${summary.totalFiles}ファイル, ${summary.totalMatches}件の要素が見つかりました\n`,
);
}

process.exit(0);
4 changes: 4 additions & 0 deletions packages/@d-zero/dom-scanner/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { scanDirectory } from './scanner.js';
export { parseFile, getDefaultProcessor } from './parser.js';
export type { ScanOptions, ScanResult, ScanSummary, ProcessorType } from './types.js';
export { DEFAULT_PROCESSOR_MAP, DEFAULT_EXTENSIONS } from './types.js';
75 changes: 75 additions & 0 deletions packages/@d-zero/dom-scanner/src/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { ProcessorType } from './types.js';

import { readFile } from 'node:fs/promises';
import path from 'node:path';

import * as cheerio from 'cheerio';
// @ts-expect-error - pug doesn't have type definitions
import pug from 'pug';

import { DEFAULT_PROCESSOR_MAP } from './types.js';

/**
* ファイル拡張子からデフォルトプロセッサーを取得
* @param filePath
*/
export function getDefaultProcessor(filePath: string): ProcessorType {
const ext = path.extname(filePath).slice(1).toLowerCase();
return DEFAULT_PROCESSOR_MAP[ext] ?? 'html';
}

/**
* HTMLファイルをパースして要素数をカウント
* @param html
* @param selector
*/
function parseHTML(html: string, selector: string): number {
const $ = cheerio.load(html);
return $(selector).length;
}

/**
* PugファイルをコンパイルしてHTMLに変換し、要素数をカウント
* @param pugContent
* @param selector
*/
function parsePug(pugContent: string, selector: string): number {
try {
const compileFunction = pug.compile(pugContent, {
basedir: process.cwd(),
});
const html = compileFunction();
return parseHTML(html, selector);
} catch (error) {
throw new Error(
`Pugコンパイルエラー: ${error instanceof Error ? error.message : String(error)}`,
);
}
}

/**
* ファイルを処理して要素数をカウント
* @param filePath
* @param selector
* @param processor
*/
export async function parseFile(
filePath: string,
selector: string,
processor?: ProcessorType,
): Promise<number> {
const content = await readFile(filePath, 'utf8');
const actualProcessor = processor ?? getDefaultProcessor(filePath);

switch (actualProcessor) {
case 'html': {
return parseHTML(content, selector);
}
case 'pug': {
return parsePug(content, selector);
}
default: {
throw new Error(`Unknown processor: ${actualProcessor}`);
}
}
}
Loading