Move to file の動作イメージはこちら(公式ドキュメント)
https://code.visualstudio.com/updates/v1_82#_move-to-file-refactoring
VSCode で TypeScript のリファクタ機能「Move to file」をVueプロジェクトで実行すると、
クラッシュして動作しないという問題があります。
TypeScript Server Error (5.9.3) Debug Failure.
File xxx.vue has unknown extension.
Error: Debug Failure.
Move to file を実行すると VSCode 右下にこのようなエラーが表示されます。
この記事では、TypeScript 本体に 1行パッチ を当てることで Move to file を動作させる方法を紹介します。
node_modules 配下の編集は本来避けるべきですが、
影響範囲が非常に限定的に修正可能であり、この機能が使えるかどうかでリファクタコストが大幅に変わると思うので記事にしてみました。
本記事で紹介する内容は TypeScript に公式修正が入るまでの暫定的な回避策です。
影響範囲は限定的ですが、パッチの内容を理解したうえで適用することを推奨します。
結論(最短で知りたい人向け)
node_modules/typescript/lib/typescript.js を次のように修正します。
typescript5.9.3では 153916 行目あたりです。
// 153908行目(typescript 5.9.3の場合)
function getMoveToRefactoringFileSuggestions(fileName, positionOrRange, preferences = emptyOptions) {
// ...
const files = mapDefined(allFiles, (file) => {
- const fileNameExtension = extensionFromPath(file.fileName);
+ const fileNameExtension = tryGetExtensionFromPath2(file.fileName);
+ if (fileNameExtension === void 0) return;
const isValidSourceFile = !(program == null ? void 0 : program.isSourceFileFromExternalLibrary(sourceFile)) && !(sourceFile === getValidSourceFile(file.fileName) || extension === ".ts" /* Ts */ && fileNameExtension === ".d.ts" /* Dts */ || extension === ".d.ts" /* Dts */ && startsWith(getBaseFileName(file.fileName), "lib.") && fileNameExtension === ".d.ts" /* Dts */);
return isValidSourceFile && (extension === fileNameExtension || (extension === ".tsx" /* Tsx */ && fileNameExtension === ".ts" /* Ts */ || extension === ".jsx" /* Jsx */ && fileNameExtension === ".js" /* Js */) && !toMoveContainsJsx) ? file.fileName : void 0;
});
return { newFileName: createNewFileName(sourceFile, program, host, toMove), files };
}
これで
-
.ts→.tsの Move は正常動作 -
.vueの import 自動書き換えも成功
という期待通りの動作をしてくれるようになります。
Move to file とは
VSCode + TypeScript で利用できるリファクタリング機能です。
変数名などにカーソルを置き、右クリック→リファクター→ Move to file から利用できます。
次のような機能を提供します。
- 変数 / 関数 / 型などを他ファイルに安全に移動
- 元ファイルは自動的に import に置き換え
- 参照しているすべてのファイルの import 文を書き換える
詳しくは https://code.visualstudio.com/updates/v1_82#_move-to-file-refactoring を参照してください。
Move to file を利用したことがない方はぜひ利用してみてください!
リファクタのコストが大きく下がります。
該当Issue
- Vue Official
https://github.com/vuejs/language-tools/issues/4638 - TypeScript 本体
https://github.com/microsoft/TypeScript/issues/56749
TypeScript 側の修正で解決するはずですが執筆時点では未修正の状態です。
動作確認環境
- TypeScript: 5.9.3
- VSCode の Vue Official 拡張機能: 3.1.4
解決方法(詳しく)
1. Move to file が落ちることを確認する
適当な .ts の変数を右クリック → リファクター → Move to file
Vue プロジェクトではここで落ちます。
VSCode 右下のエラーメッセージ → 詳細表示 で、次のようなスタックトレースが見えるはずです。
File <project root>/ComponentA.vue has unknown extension.
at extensionFromPath (<project root>/node_modules/typescript/lib/typescript.js:22738:39)
at <project root>/node_modules/typescript/lib/typescript.js:153916:33
at mapDefined (<project root>/node_modules/typescript/lib/typescript.js:2606:22)
このメッセージから node_modules の TypeScript が利用されているか、VSCode標準の TypeScript が利用されているかがわかるはずです。
2. VSCode が「ワークスペースの TypeScript」を使っているか確認
VSCode 下側のステータスバーで TypeScript バージョンを確認し、
- ワークスペースのバージョンを使用
を選びます。
もしくは .vscode/settings.json に直接設定してもOKです。
{
"typescript.tsdk": "node_modules/typescript/lib"
}
これをしないとパッチした TypeScript が使われず効果がでません。
3. TypeScript 本体(typescript.js)を patch
node_modules/typescript/lib/typescript.js を開き次のように修正します。
(結論(最短で知りたい人向け)のdiffと同様の内容です)
// 153908行目(typescript 5.9.3の場合)
function getMoveToRefactoringFileSuggestions(fileName, positionOrRange, preferences = emptyOptions) {
// ...
const files = mapDefined(allFiles, (file) => {
- const fileNameExtension = extensionFromPath(file.fileName);
+ const fileNameExtension = tryGetExtensionFromPath2(file.fileName);
+ if (fileNameExtension === void 0) return;
const isValidSourceFile = !(program == null ? void 0 : program.isSourceFileFromExternalLibrary(sourceFile)) && !(sourceFile === getValidSourceFile(file.fileName) || extension === ".ts" /* Ts */ && fileNameExtension === ".d.ts" /* Dts */ || extension === ".d.ts" /* Dts */ && startsWith(getBaseFileName(file.fileName), "lib.") && fileNameExtension === ".d.ts" /* Dts */);
return isValidSourceFile && (extension === fileNameExtension || (extension === ".tsx" /* Tsx */ && fileNameExtension === ".ts" /* Ts */ || extension === ".jsx" /* Jsx */ && fileNameExtension === ".js" /* Js */) && !toMoveContainsJsx) ? file.fileName : void 0;
});
return { newFileName: createNewFileName(sourceFile, program, host, toMove), files };
}
ポイントは、
-
extensionFromPathをやめて -
extensionFromPathが内部で利用しているtryGetExtensionFromPathを直接呼ぶことでDebug.fail()が実行されないようにする - 未知拡張子(
.vue)の場合はreturnでスキップ
という処理にすることです。
4. VSCode を再起動
TypeScript サービスがパッチ版を読み込みます。
忘れず再起動してください。
5. Move to file が正常動作するか確認
次を確認し、問題なければパッチ成功です!
-
a.tsに定義した変数をb.tsへ Move to file すると問題なく移動できる - その変数を参照している
.vueファイルの import も自動で書き換えられる
(任意だが推奨)patch-package で差分を管理する
本編は以上ですが、TypeScript を更新しパッチが消えてしまうことに備えての補足もしておきます。
node_modules/typescript/lib/typescript.js に加えた修正は patch-package で管理しておくと安全です。
# パッチファイル作成 (プロジェクトに `patches/typescript+5.9.3.patch` のようなファイルが作成されます)
npx patch-package typescript
# パッチを適用 (作成されたパッチが `node_modules/typescript/lib/typescript.js` に適用される)
npx patch-package
パッチファイルが作成されるので、どのような変更をしたか git で管理できます。
個人でパッチするだけなら stash するか、.git/info/exclude に追記するなどして、パッチファイルが git で共有されないようにしておけばいいと思います。
また、patch-package 公式では postinstall 時にパッチを自動実行するようにしていますが、Move to file 用パッチだけであればそこまでしなくていいと思います。
まとめ
- Vue + TypeScript で Move To File が落ちるのは TS 本体の未修正バグ
- TypeScript の
getMoveToRefactoringFileSuggestionsに1行パッチで回避可能 -
.vue側の import 書き換えも Volar が自動対応 - patch-package で安定運用できる
- TypeScript の公式修正が入るまでの一時的な回避策