Skip to main content
Glama
SiroSuzume

MCP ts-morph Refactoring Tools

by SiroSuzume
remove-path-alias.test.ts9.19 kB
import { Project } from "ts-morph"; import { describe, it, expect } from "vitest"; import * as path from "node:path"; import { removePathAlias } from "./remove-path-alias"; const TEST_TSCONFIG_PATH = "/tsconfig.json"; const TEST_BASE_URL = "/src"; const TEST_PATHS = { "@/*": ["*"], "@components/*": ["components/*"], "@utils/helpers": ["utils/helpers.ts"], }; const setupProject = () => { const project = new Project({ useInMemoryFileSystem: true, compilerOptions: { baseUrl: path.relative(path.dirname(TEST_TSCONFIG_PATH), TEST_BASE_URL), paths: TEST_PATHS, allowJs: true, }, }); project.createSourceFile( TEST_TSCONFIG_PATH, JSON.stringify({ compilerOptions: { baseUrl: "./src", paths: TEST_PATHS }, }), ); return project; }; describe("removePathAlias", () => { it("単純なワイルドカードエイリアス (@/*) を相対パスに変換できること", async () => { const project = setupProject(); const importerPath = "/src/features/featureA/index.ts"; const componentPath = "/src/components/Button.ts"; project.createSourceFile(componentPath, "export const Button = {};"); const importerContent = `import { Button } from '@/components/Button';`; project.createSourceFile(importerPath, importerContent); const result = await removePathAlias({ project, targetPath: importerPath, baseUrl: TEST_BASE_URL, paths: TEST_PATHS, dryRun: false, }); const sourceFile = project.getSourceFileOrThrow(importerPath); const importDeclaration = sourceFile.getImportDeclarations()[0]; expect(importDeclaration?.getModuleSpecifierValue()).toBe( "../../components/Button", ); expect(result.changedFiles).toEqual([importerPath]); }); it("特定のパスエイリアス (@components/*) を相対パスに変換できること", async () => { const project = setupProject(); const importerPath = "/src/index.ts"; const componentPath = "/src/components/Input/index.ts"; project.createSourceFile(componentPath, "export const Input = {};"); const importerContent = `import { Input } from '@components/Input';`; project.createSourceFile(importerPath, importerContent); const result = await removePathAlias({ project, targetPath: importerPath, baseUrl: TEST_BASE_URL, paths: TEST_PATHS, dryRun: false, }); const sourceFile = project.getSourceFileOrThrow(importerPath); expect( sourceFile.getImportDeclarations()[0]?.getModuleSpecifierValue(), ).toBe("./components/Input/index"); expect(result.changedFiles).toEqual([importerPath]); }); it("ファイルへの直接エイリアス (@utils/helpers) を相対パスに変換できること", async () => { const project = setupProject(); const importerPath = "/src/features/featureB/utils.ts"; const helperPath = "/src/utils/helpers.ts"; project.createSourceFile(helperPath, "export const helperFunc = () => {};"); const importerContent = `import { helperFunc } from '@utils/helpers';`; project.createSourceFile(importerPath, importerContent); const result = await removePathAlias({ project, targetPath: importerPath, baseUrl: TEST_BASE_URL, paths: TEST_PATHS, dryRun: false, }); const sourceFile = project.getSourceFileOrThrow(importerPath); expect( sourceFile.getImportDeclarations()[0]?.getModuleSpecifierValue(), ).toBe("../../utils/helpers"); expect(result.changedFiles).toEqual([importerPath]); }); it("エイリアスでない通常の相対パスは変更しないこと", async () => { const project = setupProject(); const importerPath = "/src/features/featureA/index.ts"; const servicePath = "/src/features/featureA/service.ts"; project.createSourceFile(servicePath, "export class Service {}"); const importerContent = `import { Service } from './service';`; const sourceFile = project.createSourceFile(importerPath, importerContent); const originalContent = sourceFile.getFullText(); const result = await removePathAlias({ project, targetPath: importerPath, baseUrl: TEST_BASE_URL, paths: TEST_PATHS, dryRun: false, }); expect(sourceFile.getFullText()).toBe(originalContent); expect(result.changedFiles).toEqual([]); }); it("エイリアスでない node_modules パスは変更しないこと", async () => { const project = setupProject(); const importerPath = "/src/index.ts"; const importerContent = `import * as fs from 'fs';`; const sourceFile = project.createSourceFile(importerPath, importerContent); const originalContent = sourceFile.getFullText(); const result = await removePathAlias({ project, targetPath: importerPath, baseUrl: TEST_BASE_URL, paths: TEST_PATHS, dryRun: false, }); expect(sourceFile.getFullText()).toBe(originalContent); expect(result.changedFiles).toEqual([]); }); it("dryRun モードではファイルを変更せず、変更予定リストを返すこと", async () => { const project = setupProject(); const importerPath = "/src/features/featureA/index.ts"; const componentPath = "/src/components/Button.ts"; project.createSourceFile(componentPath, "export const Button = {};"); const importerContent = `import { Button } from '@/components/Button';`; const sourceFile = project.createSourceFile(importerPath, importerContent); const originalContent = sourceFile.getFullText(); const result = await removePathAlias({ project, targetPath: importerPath, baseUrl: TEST_BASE_URL, paths: TEST_PATHS, dryRun: true, }); expect(sourceFile.getFullText()).toBe(originalContent); expect(result.changedFiles).toEqual([importerPath]); }); it("ディレクトリを対象とした場合に、内部の複数ファイルのエイリアスを変換できること", async () => { const project = setupProject(); const dirPath = "/src/features/multi"; const file1Path = path.join(dirPath, "file1.ts"); const file2Path = path.join(dirPath, "sub/file2.ts"); const buttonPath = "/src/components/Button.ts"; const inputPath = "/src/components/Input.ts"; project.createSourceFile(buttonPath, "export const Button = {};"); project.createSourceFile(inputPath, "export const Input = {};"); project.createSourceFile( file1Path, "import { Button } from '@/components/Button';", ); project.createSourceFile( file2Path, "import { Input } from '@components/Input';", ); const result = await removePathAlias({ project, targetPath: dirPath, baseUrl: TEST_BASE_URL, paths: TEST_PATHS, dryRun: false, }); const file1 = project.getSourceFileOrThrow(file1Path); const file2 = project.getSourceFileOrThrow(file2Path); expect(file1.getImportDeclarations()[0]?.getModuleSpecifierValue()).toBe( "../../components/Button", ); expect(file2.getImportDeclarations()[0]?.getModuleSpecifierValue()).toBe( "../../../components/Input", ); expect(result.changedFiles.sort()).toEqual([file1Path, file2Path].sort()); }); it("解決できないエイリアスパスを変更しないこと", async () => { const project = setupProject(); const importerPath = "/src/index.ts"; const importerContent = `import { Something } from '@unknown/package';`; const sourceFile = project.createSourceFile(importerPath, importerContent); const originalContent = sourceFile.getFullText(); const result = await removePathAlias({ project, targetPath: importerPath, baseUrl: TEST_BASE_URL, paths: TEST_PATHS, dryRun: false, }); expect(sourceFile.getFullText()).toBe(originalContent); expect(result.changedFiles).toEqual([]); }); it("エイリアスが index.ts を指す場合、結果は /index で終わる (省略されない)", async () => { const project = setupProject(); const importerPath = "/src/features/featureA/component.ts"; const indexPath = "/src/components/index.ts"; project.createSourceFile(indexPath, "export const CompIndex = 1;"); project.createSourceFile( importerPath, "import { CompIndex } from '@/components';", ); const result = await removePathAlias({ project, targetPath: importerPath, baseUrl: "/", paths: { "@/*": ["src/*"] }, dryRun: false, }); const sourceFile = project.getSourceFileOrThrow(importerPath); expect( sourceFile.getImportDeclarations()[0]?.getModuleSpecifierValue(), ).toBe("../../components/index"); expect(result.changedFiles).toEqual([importerPath]); }); it("エイリアスが .js ファイルを指す場合、結果から拡張子は削除される", async () => { const project = setupProject(); const importerPath = "/src/app.ts"; const jsPath = "/src/utils/legacy.js"; project.createSourceFile(jsPath, "export const legacyFunc = () => {};"); project.createSourceFile( importerPath, "import { legacyFunc } from '@/utils/legacy.js';", ); const result = await removePathAlias({ project, targetPath: importerPath, baseUrl: "/", paths: { "@/*": ["src/*"] }, dryRun: false, }); const sourceFile = project.getSourceFileOrThrow(importerPath); expect( sourceFile.getImportDeclarations()[0]?.getModuleSpecifierValue(), ).toBe("./utils/legacy"); expect(result.changedFiles).toEqual([importerPath]); }); });

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