typescriptでファイル名をcamel caseからsnake caseに変換する必要があったので調べたところ、ts-morphがとても良かった
ts-morph の概要
ts-morph は、TypeScript コードの静的解析、操作、生成をプログラム的に行うためのライブラリである。
AST (Abstract Syntax Tree) レベルでの操作を可能にすることで、単純なテキスト置換ベースのリファクタリングと比較して、構文的な安全性を確保しつつ高度なコード変換を実現する。
主な機能として、変数や関数の識別子変更、未使用 import の除去、コードパターンの検索・置換、そして本稿の主題であるファイルリネームとそれに伴う import/export 宣言の自動更新などが挙げられる.。
ts-morph による変換スクリプト
① 依存関係の追加ts-morph をプロジェクトの devDependencies に追加する。
npm install ts-morph --save-dev # or yarn add ts-morph --dev # or pnpm add ts-morph --save-dev
② 変換スクリプトの作成プロジェクト内に、ファイル名変換ロジックを含む TypeScript スクリプト (例: renameFiles.ts) を配置する。
import { Project, SourceFile } from 'ts-morph'; import * as path from 'path'; // camelCase または PascalCase を snake_case に変換するヘルパー関数 // 例: myFile -> my_file, MyComponent -> my_component, AnotherFile -> another_file function camelToSnake(str: string): string { // 最初の文字を小文字にする (PascalCase 対応) const initialLower = str.charAt(0).toLowerCase() + str.slice(1); // 大文字の前にアンダースコアを挿入し、全体を小文字にする return initialLower .replace(/([A-Z])/g, '_$1') .toLowerCase(); } // ファイル名が camelCase または PascalCase かどうかを判定する簡易的な関数 // (先頭が小文字で大文字を含むか、先頭が大文字) function isCamelOrPascalCase(fileName: string): boolean { // 拡張子を除いたファイル名を取得 const nameWithoutExt = path.parse(fileName).name; // 数字や特殊文字で始まる場合は対象外とする (任意) if (!/^[a-zA-Z]/.test(nameWithoutExt)) { return false; } // アンダースコアが含まれていたら snake_case とみなし対象外 if (nameWithoutExt.includes('_')) { return false; } // 全て小文字または全て大文字の場合は対象外 (任意) if (nameWithoutExt.toLowerCase() === nameWithoutExt || nameWithoutExt.toUpperCase() === nameWithoutExt) { return false; } // 先頭が小文字で、途中に大文字が含まれる (camelCase) // または先頭が大文字 (PascalCase) return (nameWithoutExt.charAt(0) === nameWithoutExt.charAt(0).toLowerCase() && /[A-Z]/.test(nameWithoutExt)) || (nameWithoutExt.charAt(0) === nameWithoutExt.charAt(0).toUpperCase()); } async function renameFilesToSnakeCase() { // tsconfig.json を読み込んでプロジェクトを初期化 // プロジェクトのルートディレクトリに合わせてパスを調整してください const project = new Project({ tsConfigFilePath: 'tsconfig.json', // tsconfig.json のパス // 必要に応じて skipAddingFilesFromTsConfig: true を設定し、 // project.addSourceFilesAtPaths() で対象ファイルを明示的に指定することもできます }); console.log('プロジェクトのソースファイルを解析中...'); // 対象ディレクトリ配下のすべての TypeScript ファイル (.ts, .tsx) を取得 // 必要に応じてパスパターンを調整してください (例: 'src/**/*.ts') const sourceFiles = project.getSourceFiles('src/**/*.{ts,tsx}'); // 'src' ディレクトリ配下を対象とする例 console.log(`${sourceFiles.length} 個のソースファイルが見つかりました。`); let renamedCount = 0; for (const sourceFile of sourceFiles) { const filePath = sourceFile.getFilePath(); const dirPath = sourceFile.getDirectoryPath(); const baseName = sourceFile.getBaseName(); // ファイル名 + 拡張子 (例: myComponent.tsx) const baseNameWithoutExt = sourceFile.getBaseNameWithoutExtension(); // ファイル名のみ (例: myComponent) const extension = sourceFile.getExtension(); // 拡張子 (例: .tsx) try { // ファイル名が camelCase または PascalCase かどうかをチェック if (isCamelOrPascalCase(baseNameWithoutExt)) { const newBaseNameWithoutExt = camelToSnake(baseNameWithoutExt); const newBaseName = `${newBaseNameWithoutExt}${extension}`; const newFilePath = path.join(dirPath, newBaseName); // 新しいファイル名が現在のファイル名と異なる場合のみリネームを実行 if (newFilePath !== filePath) { console.log(`リネーム: ${filePath} -> ${newFilePath}`); // ファイルを移動 (リネーム) // この操作により、他のファイル内の関連する import/export 宣言も更新されます await sourceFile.move(newFilePath); renamedCount++; } } } catch (error) { console.error(`ファイルのリネーム中にエラーが発生しました: ${filePath}`, error); // エラーが発生しても処理を続行する場合は、ここに continue 文を追加 } } // 変更をファイルシステムに保存 if (renamedCount > 0) { console.log('\nファイルシステムの変更を保存中...'); await project.save(); console.log(`${renamedCount} 個のファイルがリネームされました。`); } else { console.log('\nリネーム対象のファイルはありませんでした。'); } console.log('処理が完了しました。'); } // スクリプトを実行 renameFilesToSnakeCase().catch(err => { console.error('スクリプトの実行中に予期せぬエラーが発生しました:', err); process.exit(1); });
③ スクリプトの実行作成したスクリプトを tsx 等を用いて実行する。
// 失敗したときに備えて、gitで戻せるようにしておくこと npx tsx renameFiles.ts
問題点
tsconfig.json の paths オプション等で定義されたパスエイリアス (例: @/components/some_component) を含む import 宣言は、ts-morph による自動更新が正しく機能しない。