Skip to main content
Glama
SiroSuzume

MCP ts-morph Refactoring Tools

by SiroSuzume
rename-file-system-entry.test.ts10.8 kB
import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { renameFileSystemEntry } from "./rename-file-system-entry"; import { initializeProject } from "../_utils/ts-morph-project"; import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; /** * テスト用の一時ディレクトリを作成 */ function createTempDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), "rename-file-system-test-")); } /** * ディレクトリを再帰的に削除 */ function removeTempDir(dir: string): void { if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); } } describe("renameFileSystemEntry 統合テスト", () => { let tempDir: string; let tsconfigPath: string; let srcDir: string; beforeEach(() => { tempDir = createTempDir(); tsconfigPath = path.join(tempDir, "tsconfig.json"); srcDir = path.join(tempDir, "src"); fs.mkdirSync(srcDir, { recursive: true }); // tsconfig.json を作成 fs.writeFileSync( tsconfigPath, JSON.stringify( { compilerOptions: { rootDir: "./src", outDir: "./dist", module: "commonjs", target: "es2020", strict: true, baseUrl: ".", paths: { "@/*": ["src/*"], }, }, include: ["src/**/*"], }, null, 2, ), ); }); afterEach(() => { removeTempDir(tempDir); }); it("単一ファイルのリネームとインポート更新", async () => { // テストファイルを作成 const oldUtilsPath = path.join(srcDir, "utils.ts"); const newUtilsPath = path.join(srcDir, "helpers.ts"); const mainPath = path.join(srcDir, "main.ts"); fs.writeFileSync( oldUtilsPath, `export function formatDate(date: Date): string { return date.toISOString(); } export const VERSION = "1.0.0"; `, ); fs.writeFileSync( mainPath, `import { formatDate, VERSION } from "./utils"; const now = new Date(); console.log(formatDate(now)); console.log("Version:", VERSION); `, ); // プロジェクトを作成してリネーム実行 const project = initializeProject(tsconfigPath); const result = await renameFileSystemEntry({ project, renames: [ { oldPath: oldUtilsPath, newPath: newUtilsPath, }, ], dryRun: false, }); // リネーム後のファイルが存在することを確認 expect(fs.existsSync(newUtilsPath)).toBe(true); expect(fs.existsSync(oldUtilsPath)).toBe(false); // インポート文が更新されていることを確認 const updatedMainContent = fs.readFileSync(mainPath, "utf-8"); expect(updatedMainContent).toContain('from "./helpers"'); expect(updatedMainContent).not.toContain('from "./utils"'); // 変更されたファイルのリストを確認 expect(result.changedFiles).toContain(mainPath); expect(result.changedFiles).toContain(newUtilsPath); }); it("フォルダのリネームと複数ファイルの参照更新", async () => { // フォルダ構造を作成 const oldFolderPath = path.join(srcDir, "components"); const newFolderPath = path.join(srcDir, "widgets"); fs.mkdirSync(oldFolderPath, { recursive: true }); const buttonPath = path.join(oldFolderPath, "Button.ts"); const modalPath = path.join(oldFolderPath, "Modal.ts"); const appPath = path.join(srcDir, "app.ts"); fs.writeFileSync( buttonPath, `export class Button { constructor(public label: string) {} render() { return \`<button>\${this.label}</button>\`; } } `, ); fs.writeFileSync( modalPath, `import { Button } from "./Button"; export class Modal { private closeButton = new Button("Close"); render() { return \`<div class="modal">\${this.closeButton.render()}</div>\`; } } `, ); fs.writeFileSync( appPath, `import { Button } from "./components/Button"; import { Modal } from "./components/Modal"; const button = new Button("Click me"); const modal = new Modal(); console.log(button.render()); console.log(modal.render()); `, ); // プロジェクトを作成してリネーム実行 const project = initializeProject(tsconfigPath); await renameFileSystemEntry({ project, renames: [ { oldPath: oldFolderPath, newPath: newFolderPath, }, ], dryRun: false, }); // フォルダがリネームされていることを確認 expect(fs.existsSync(newFolderPath)).toBe(true); // ts-morphはファイルを移動するが、空のフォルダは残ることがある // 重要なのはファイルが正しく移動されていること // ファイルが新しいフォルダに移動していることを確認 expect(fs.existsSync(path.join(newFolderPath, "Button.ts"))).toBe(true); expect(fs.existsSync(path.join(newFolderPath, "Modal.ts"))).toBe(true); // 元のフォルダにファイルが残っていないことを確認 expect(fs.existsSync(path.join(oldFolderPath, "Button.ts"))).toBe(false); expect(fs.existsSync(path.join(oldFolderPath, "Modal.ts"))).toBe(false); // インポート文が更新されていることを確認 const updatedAppContent = fs.readFileSync(appPath, "utf-8"); expect(updatedAppContent).toContain('from "./widgets/Button"'); expect(updatedAppContent).toContain('from "./widgets/Modal"'); expect(updatedAppContent).not.toContain('from "./components/'); // Modal.ts内の相対インポートも更新されていることを確認 const updatedModalContent = fs.readFileSync( path.join(newFolderPath, "Modal.ts"), "utf-8", ); expect(updatedModalContent).toContain('from "./Button"'); // 相対パスは変更なし }); it("dryRunモードでファイルシステムを変更しない", async () => { const oldPath = path.join(srcDir, "old-file.ts"); const newPath = path.join(srcDir, "new-file.ts"); const importerPath = path.join(srcDir, "importer.ts"); fs.writeFileSync(oldPath, "export const value = 42;"); fs.writeFileSync( importerPath, `import { value } from "./old-file"; console.log(value); `, ); const project = initializeProject(tsconfigPath); const result = await renameFileSystemEntry({ project, renames: [ { oldPath, newPath, }, ], dryRun: true, // dryRunモードを有効化 }); // ファイルシステムが変更されていないことを確認 expect(fs.existsSync(oldPath)).toBe(true); expect(fs.existsSync(newPath)).toBe(false); // 元のインポート文が変更されていないことを確認 const importerContent = fs.readFileSync(importerPath, "utf-8"); expect(importerContent).toContain('from "./old-file"'); // 変更予定のファイルリストは返される expect(result.changedFiles.length).toBeGreaterThan(0); }); it("パスエイリアスを使用したインポートの更新", async () => { const utilsDir = path.join(srcDir, "utils"); const helpersDir = path.join(srcDir, "helpers"); fs.mkdirSync(utilsDir, { recursive: true }); const mathPath = path.join(utilsDir, "math.ts"); const appPath = path.join(srcDir, "app.ts"); fs.writeFileSync( mathPath, `export function add(a: number, b: number): number { return a + b; } export function multiply(a: number, b: number): number { return a * b; } `, ); fs.writeFileSync( appPath, `import { add, multiply } from "@/utils/math"; console.log(add(2, 3)); console.log(multiply(4, 5)); `, ); const project = initializeProject(tsconfigPath); await renameFileSystemEntry({ project, renames: [ { oldPath: utilsDir, newPath: helpersDir, }, ], dryRun: false, }); // フォルダがリネームされていることを確認 expect(fs.existsSync(helpersDir)).toBe(true); // ts-morphはファイルを移動するが、空のフォルダは残ることがある // 重要なのはファイルが正しく移動されていること // ファイルが新しいフォルダに移動していることを確認 expect(fs.existsSync(path.join(helpersDir, "math.ts"))).toBe(true); expect(fs.existsSync(path.join(utilsDir, "math.ts"))).toBe(false); // パスエイリアスを使用したインポートが更新されていることを確認 // ts-morphのリネーム処理ではパスエイリアスが相対パスに変換されることがある const updatedAppContent = fs.readFileSync(appPath, "utf-8"); expect(updatedAppContent).toContain('from "./helpers/math"'); expect(updatedAppContent).not.toContain('from "@/utils/math"'); }); it("複数ファイルの同時リネーム", async () => { const file1OldPath = path.join(srcDir, "file1.ts"); const file1NewPath = path.join(srcDir, "renamed1.ts"); const file2OldPath = path.join(srcDir, "file2.ts"); const file2NewPath = path.join(srcDir, "renamed2.ts"); const mainPath = path.join(srcDir, "main.ts"); fs.writeFileSync(file1OldPath, `export const value1 = "first";`); fs.writeFileSync(file2OldPath, `export const value2 = "second";`); fs.writeFileSync( mainPath, `import { value1 } from "./file1"; import { value2 } from "./file2"; console.log(value1, value2); `, ); const project = initializeProject(tsconfigPath); await renameFileSystemEntry({ project, renames: [ { oldPath: file1OldPath, newPath: file1NewPath }, { oldPath: file2OldPath, newPath: file2NewPath }, ], dryRun: false, }); // 両方のファイルがリネームされていることを確認 expect(fs.existsSync(file1NewPath)).toBe(true); expect(fs.existsSync(file2NewPath)).toBe(true); expect(fs.existsSync(file1OldPath)).toBe(false); expect(fs.existsSync(file2OldPath)).toBe(false); // インポート文が両方とも更新されていることを確認 const updatedMainContent = fs.readFileSync(mainPath, "utf-8"); expect(updatedMainContent).toContain('from "./renamed1"'); expect(updatedMainContent).toContain('from "./renamed2"'); expect(updatedMainContent).not.toContain('from "./file1"'); expect(updatedMainContent).not.toContain('from "./file2"'); }); it("AbortSignalによる処理のキャンセル", async () => { const oldPath = path.join(srcDir, "cancelable.ts"); const newPath = path.join(srcDir, "renamed.ts"); fs.writeFileSync(oldPath, `export const data = "test";`); const project = initializeProject(tsconfigPath); const abortController = new AbortController(); // 即座にキャンセル abortController.abort(); await expect( renameFileSystemEntry({ project, renames: [{ oldPath, newPath }], dryRun: false, signal: abortController.signal, }), ).rejects.toThrow(); // ファイルが変更されていないことを確認 expect(fs.existsSync(oldPath)).toBe(true); expect(fs.existsSync(newPath)).toBe(false); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SiroSuzume/mcp-ts-morph'

If you have feedback or need assistance with the MCP directory API, please join our Discord server