Skip to main content
Glama
SiroSuzume

MCP ts-morph Refactoring Tools

by SiroSuzume
rename-file-system-entry.special.test.ts14.2 kB
import { describe, it, expect } from "vitest"; import { Project } from "ts-morph"; import { renameFileSystemEntry } from "./rename-file-system-entry"; // --- Test Setup Helper --- const setupProject = () => { const project = new Project({ useInMemoryFileSystem: true, compilerOptions: { baseUrl: ".", paths: { "@/*": ["src/*"], }, esModuleInterop: true, allowJs: true, }, }); project.createDirectory("/src"); project.createDirectory("/src/utils"); project.createDirectory("/src/components"); return project; }; describe("renameFileSystemEntry Special Cases", () => { it("dryRun: true の場合、ファイルシステム(メモリ上)の変更を行わず、変更予定リストを返す", async () => { const project = setupProject(); const oldUtilPath = "/src/utils/old-util.ts"; const newUtilPath = "/src/utils/new-util.ts"; const componentPath = "/src/components/MyComponent.ts"; project.createSourceFile( oldUtilPath, 'export const oldUtil = () => "old";', ); project.createSourceFile( componentPath, `import { oldUtil } from '../utils/old-util';`, ); const result = await renameFileSystemEntry({ project, renames: [{ oldPath: oldUtilPath, newPath: newUtilPath }], dryRun: true, }); expect(project.getSourceFile(oldUtilPath)).toBeUndefined(); expect(project.getSourceFile(newUtilPath)).toBeDefined(); expect(result.changedFiles).toContain(newUtilPath); expect(result.changedFiles).toContain(componentPath); expect(result.changedFiles).not.toContain(oldUtilPath); }); it("どのファイルからも参照されていないファイルをリネームする", async () => { const project = setupProject(); const oldPath = "/src/utils/unreferenced.ts"; const newPath = "/src/utils/renamed-unreferenced.ts"; project.createSourceFile(oldPath, "export const lonely = true;"); const result = await renameFileSystemEntry({ project, renames: [{ oldPath, newPath }], dryRun: false, }); expect(project.getSourceFile(oldPath)).toBeUndefined(); expect(project.getSourceFile(newPath)).toBeDefined(); expect(project.getSourceFileOrThrow(newPath).getFullText()).toContain( "export const lonely = true;", ); expect(result.changedFiles).toEqual([newPath]); }); it("デフォルトインポートのパスが正しく更新される", async () => { const project = setupProject(); const oldDefaultPath = "/src/utils/defaultExport.ts"; const newDefaultPath = "/src/utils/renamedDefaultExport.ts"; const importerPath = "/src/importer.ts"; project.createSourceFile( oldDefaultPath, "export default function myDefaultFunction() { return 'default'; }", ); project.createSourceFile( importerPath, "import MyDefaultImport from './utils/defaultExport';\nconsole.log(MyDefaultImport());", ); await renameFileSystemEntry({ project, renames: [{ oldPath: oldDefaultPath, newPath: newDefaultPath }], dryRun: false, }); const updatedImporterContent = project .getSourceFileOrThrow(importerPath) .getFullText(); expect(project.getSourceFile(oldDefaultPath)).toBeUndefined(); expect(project.getSourceFile(newDefaultPath)).toBeDefined(); expect(updatedImporterContent).toContain( "import MyDefaultImport from './utils/renamedDefaultExport';", ); }); it("デフォルトエクスポートされた変数 (export default variableName) のパスが正しく更新される", async () => { const project = setupProject(); const oldVarDefaultPath = "/src/utils/variableDefaultExport.ts"; const newVarDefaultPath = "/src/utils/renamedVariableDefaultExport.ts"; const importerPath = "/src/importerVar.ts"; project.createSourceFile( oldVarDefaultPath, "const myVar = { value: 'default var' };\nexport default myVar;", ); project.createSourceFile( importerPath, "import MyVarImport from './utils/variableDefaultExport';\nconsole.log(MyVarImport.value);", ); await renameFileSystemEntry({ project, renames: [{ oldPath: oldVarDefaultPath, newPath: newVarDefaultPath }], dryRun: false, }); const updatedImporterContent = project .getSourceFileOrThrow(importerPath) .getFullText(); expect(project.getSourceFile(oldVarDefaultPath)).toBeUndefined(); expect(project.getSourceFile(newVarDefaultPath)).toBeDefined(); expect(updatedImporterContent).toContain( "import MyVarImport from './utils/renamedVariableDefaultExport';", ); }); }); describe("renameFileSystemEntry Extension Preservation", () => { it("import文のパスに .js 拡張子が含まれている場合、リネーム後も維持される", async () => { const project = setupProject(); const oldJsPath = "/src/utils/legacy-util.js"; const newJsPath = "/src/utils/modern-util.js"; const importerPath = "/src/components/MyComponent.ts"; const otherTsPath = "/src/utils/helper.ts"; const newOtherTsPath = "/src/utils/renamed-helper.ts"; project.createSourceFile(oldJsPath, "export const legacyValue = 1;"); project.createSourceFile(otherTsPath, "export const helperValue = 2;"); project.createSourceFile( importerPath, `import { legacyValue } from '../utils/legacy-util.js'; import { helperValue } from '../utils/helper'; console.log(legacyValue, helperValue); `, ); await renameFileSystemEntry({ project, renames: [ { oldPath: oldJsPath, newPath: newJsPath }, { oldPath: otherTsPath, newPath: newOtherTsPath }, ], dryRun: false, }); const updatedImporterContent = project .getSourceFileOrThrow(importerPath) .getFullText(); expect(updatedImporterContent).toContain( "import { legacyValue } from '../utils/modern-util.js';", ); expect(updatedImporterContent).toContain( "import { helperValue } from '../utils/renamed-helper';", ); expect(project.getSourceFile(oldJsPath)).toBeUndefined(); expect(project.getSourceFile(newJsPath)).toBeDefined(); expect(project.getSourceFile(otherTsPath)).toBeUndefined(); expect(project.getSourceFile(newOtherTsPath)).toBeDefined(); }); }); describe("renameFileSystemEntry with index.ts re-exports", () => { it("index.ts が 'export * from \"./moduleB\"' 形式で moduleB.ts を再エクスポートし、moduleB.ts をリネームした場合", async () => { const project = setupProject(); const utilsDir = "/src/utils"; const moduleBOriginalPath = `${utilsDir}/moduleB.ts`; const moduleBRenamedPath = `${utilsDir}/moduleBRenamed.ts`; const indexTsPath = `${utilsDir}/index.ts`; const componentPath = "/src/components/MyComponent.ts"; project.createSourceFile( moduleBOriginalPath, "export const importantValue = 'Hello from B';", ); project.createSourceFile(indexTsPath, 'export * from "./moduleB";'); project.createSourceFile( componentPath, "import { importantValue } from '@/utils';\\nconsole.log(importantValue);", ); const result = await renameFileSystemEntry({ project, renames: [{ oldPath: moduleBOriginalPath, newPath: moduleBRenamedPath }], dryRun: false, }); expect(project.getSourceFile(moduleBOriginalPath)).toBeUndefined(); expect(project.getSourceFile(moduleBRenamedPath)).toBeDefined(); expect(project.getSourceFileOrThrow(moduleBRenamedPath).getFullText()).toBe( "export const importantValue = 'Hello from B';", ); const indexTsContent = project .getSourceFileOrThrow(indexTsPath) .getFullText(); expect(indexTsContent).toContain('export * from "./moduleBRenamed";'); expect(indexTsContent).not.toContain('export * from "./moduleB";'); const componentContent = project .getSourceFileOrThrow(componentPath) .getFullText(); expect(componentContent).toContain( "import { importantValue } from '@/utils';", ); expect(result.changedFiles).toHaveLength(3); expect(result.changedFiles).toEqual( expect.arrayContaining([moduleBRenamedPath, indexTsPath, componentPath]), ); }); it("index.ts が 'export { specificExport } from \"./moduleC\"' 形式で moduleC.ts を再エクスポートし、moduleC.ts をリネームした場合", async () => { const project = setupProject(); const utilsDir = "/src/utils"; const moduleCOriginalPath = `${utilsDir}/moduleC.ts`; const moduleCRenamedPath = `${utilsDir}/moduleCRenamed.ts`; const indexTsPath = `${utilsDir}/index.ts`; const componentPath = "/src/components/MyComponentForC.ts"; project.createSourceFile( moduleCOriginalPath, "export const specificExport = 'Hello from C';", ); project.createSourceFile( indexTsPath, 'export { specificExport } from "./moduleC";', ); project.createSourceFile( componentPath, "import { specificExport } from '@/utils';\\nconsole.log(specificExport);", ); const result = await renameFileSystemEntry({ project, renames: [{ oldPath: moduleCOriginalPath, newPath: moduleCRenamedPath }], dryRun: false, }); expect(project.getSourceFile(moduleCOriginalPath)).toBeUndefined(); expect(project.getSourceFile(moduleCRenamedPath)).toBeDefined(); expect(project.getSourceFileOrThrow(moduleCRenamedPath).getFullText()).toBe( "export const specificExport = 'Hello from C';", ); const indexTsContent = project .getSourceFileOrThrow(indexTsPath) .getFullText(); expect(indexTsContent).toContain( 'export { specificExport } from "./moduleCRenamed";', ); expect(indexTsContent).not.toContain( 'export { specificExport } from "./moduleC";', ); const componentContent = project .getSourceFileOrThrow(componentPath) .getFullText(); expect(componentContent).toContain( "import { specificExport } from '@/utils';", ); expect(result.changedFiles).toHaveLength(3); expect(result.changedFiles).toEqual( expect.arrayContaining([moduleCRenamedPath, indexTsPath, componentPath]), ); }); it("index.ts が再エクスポートを行い、その utils ディレクトリ全体をリネームした場合", async () => { const project = setupProject(); const oldUtilsDir = "/src/utils"; const newUtilsDir = "/src/newUtils"; const moduleDOriginalPath = `${oldUtilsDir}/moduleD.ts`; const indexTsOriginalPath = `${oldUtilsDir}/index.ts`; const componentPath = "/src/components/MyComponentForD.ts"; project.createSourceFile( moduleDOriginalPath, "export const valueFromD = 'Hello from D';", ); project.createSourceFile(indexTsOriginalPath, 'export * from "./moduleD";'); project.createSourceFile( componentPath, "import { valueFromD } from '@/utils';\\nconsole.log(valueFromD);", ); const result = await renameFileSystemEntry({ project, renames: [{ oldPath: oldUtilsDir, newPath: newUtilsDir }], dryRun: false, }); const moduleDRenamedPath = `${newUtilsDir}/moduleD.ts`; const indexTsRenamedPath = `${newUtilsDir}/index.ts`; expect(project.getSourceFile(moduleDOriginalPath)).toBeUndefined(); expect(project.getSourceFile(indexTsOriginalPath)).toBeUndefined(); // expect(project.getDirectory(oldUtilsDir)).toBeUndefined(); // ユーザーの指示によりコメントアウト expect(project.getDirectory(newUtilsDir)).toBeDefined(); expect(project.getSourceFile(moduleDRenamedPath)).toBeDefined(); expect(project.getSourceFile(indexTsRenamedPath)).toBeDefined(); expect(project.getSourceFileOrThrow(moduleDRenamedPath).getFullText()).toBe( "export const valueFromD = 'Hello from D';", ); expect(project.getSourceFileOrThrow(indexTsRenamedPath).getFullText()).toBe( 'export * from "./moduleD";', ); const componentContent = project .getSourceFileOrThrow(componentPath) .getFullText(); expect(componentContent).toContain( "import { valueFromD } from '../newUtils/index';", ); expect(result.changedFiles).toHaveLength(3); expect(result.changedFiles).toEqual( expect.arrayContaining([ moduleDRenamedPath, indexTsRenamedPath, componentPath, ]), ); }); }); describe("renameFileSystemEntry with index.ts re-exports (actual bug reproduction)", () => { it("index.tsが複数のモジュールを再エクスポートし、そのうちの1つをリネームした際、インポート元のパスがindex.tsを指し続けること", async () => { const project = setupProject(); const utilsDir = "/src/utils"; const moduleAOriginalPath = `${utilsDir}/moduleA.ts`; const moduleARenamedPath = `${utilsDir}/moduleARenamed.ts`; const moduleBPath = `${utilsDir}/moduleB.ts`; const indexTsPath = `${utilsDir}/index.ts`; const componentPath = "/src/components/MyComponent.ts"; project.createSourceFile( moduleAOriginalPath, "export const funcA = () => 'original_A';", ); project.createSourceFile(moduleBPath, "export const funcB = () => 'B';"); project.createSourceFile( indexTsPath, 'export * from "./moduleA";\nexport * from "./moduleB";', ); project.createSourceFile( componentPath, "import { funcA, funcB } from '@/utils';\nconsole.log(funcA(), funcB());", ); const originalComponentContent = project .getSourceFileOrThrow(componentPath) .getFullText(); await renameFileSystemEntry({ project, renames: [{ oldPath: moduleAOriginalPath, newPath: moduleARenamedPath }], dryRun: false, }); // 1. moduleA.ts がリネームされていること expect(project.getSourceFile(moduleAOriginalPath)).toBeUndefined(); expect(project.getSourceFile(moduleARenamedPath)).toBeDefined(); expect(project.getSourceFileOrThrow(moduleARenamedPath).getFullText()).toBe( "export const funcA = () => 'original_A';", ); // 2. index.ts が正しく更新されていること const indexTsContent = project .getSourceFileOrThrow(indexTsPath) .getFullText(); expect(indexTsContent).toContain('export * from "./moduleARenamed";'); expect(indexTsContent).toContain('export * from "./moduleB";'); expect(indexTsContent).not.toContain('export * from "./moduleA";'); // 3. MyComponent.ts のインポートパスが変更されていないこと const updatedComponentContent = project .getSourceFileOrThrow(componentPath) .getFullText(); expect(updatedComponentContent).toBe(originalComponentContent); // さらに具体的に確認 expect(updatedComponentContent).toContain( "import { funcA, funcB } from '@/utils';", ); }); });

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