├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── LICENSE ├── README.md ├── icon.png ├── icon.svg ├── images └── usage.gif ├── package.json ├── src ├── extension.ts ├── fileitem.ts ├── index │ ├── referenceindex.ts │ └── referenceindexer.ts └── walk.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": [ "${workspaceRoot}/out/src/**/*.js" ], 14 | "preLaunchTask": "npm" 15 | }, 16 | { 17 | "name": "Launch Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], 22 | "stopOnEntry": false, 23 | "sourceMaps": true, 24 | "outFiles": [ "${workspaceRoot}/out/test/**/*.js" ], 25 | "preLaunchTask": "npm" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | } 9 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "0.1.0", 12 | 13 | // we want to run npm 14 | "command": "npm", 15 | 16 | // the command is a shell script 17 | "isShellCommand": true, 18 | 19 | // show the output window only if unrecognized errors occur. 20 | "showOutput": "silent", 21 | 22 | // we run the custom script "compile" as defined in package.json 23 | "args": ["run", "compile", "--loglevel", "silent"], 24 | 25 | // The tsc compiler is started in watching mode 26 | "isWatching": true, 27 | 28 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 29 | "problemMatcher": "$tsc-watch" 30 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.map 7 | .gitignore 8 | tsconfig.json 9 | vsc-extension-quickstart.md 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ryan Stringham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Move TS README 2 | 3 | Supports moving typescript files and updating relative imports within the workspace. 4 | 5 | ## Features 6 | Moves TypeScript files and folders containing TypeScript and updates their relative import paths. 7 | 8 | ## How to use 9 | 10 | ![demo](images/usage.gif) 11 | 12 | 13 | 14 | 15 | 16 | ## Release Notes 17 | 18 | ## 1.12.0 19 | 20 | Added the ability to remove the filename from index file imports. To disable set `movets.removeIndexSuffix` to `false` in User Settings. 21 | 22 | ## 1.11.3 23 | 24 | Add support for path mapping for Windows users. 25 | 26 | ## 1.11.2 27 | 28 | Add support for path mapping when mapping to multiple paths. 29 | 30 | ## 1.11.0 31 | 32 | Support multi select in the explorer for moving multiple items at the same time. Must be moving all items from the same folder. 33 | 34 | ## 1.10.0 35 | 36 | Added an option to make edits in vscode instead of changing the files on disk. This makes each file changed open in a new tab. To enable set `movets.openEditors` to `true` in User Settings. For large projects sometimes vscode struggles to open all of the files. 37 | 38 | ## 1.9.0 39 | 40 | Added the ability to resolve relative paths based on the location of `tsconfig.json`. To enable set `movets.relativeToTsconfig` to `true` in User Settings. 41 | 42 | ## 1.8.2 43 | 44 | Fix a bug when a moved file has two import statements using the same module specifier. 45 | 46 | ## 1.8.1 47 | 48 | Improve indexing performance using the TypeScript parser. 49 | 50 | ## 1.8.0 51 | 52 | Use the TypeScript parser instead of regular expressions to find and replace imports. 53 | 54 | ## 1.7.1 55 | 56 | Fix bug with indexing in Windows. 57 | 58 | ## 1.7.0 59 | 60 | Improve performance of indexing the workspace. 61 | 62 | ## 1.6.0 63 | 64 | Report progress with vscode's withProgress extension api when indexing the workspace. 65 | 66 | ## 1.5.0 67 | 68 | Added support for `tsconfig.json` CompilerOptions -> paths. 69 | 70 | ## 1.4.0 71 | 72 | Added support for `*.tsx` files. 73 | 74 | New configuration option that can limit which paths are scanned: `movets.filesToScan` should be an array of strings and defaults to `['**/*.ts', '**/*.tsx']` 75 | 76 | ### 1.3.1 77 | 78 | Allow initiating moving the current file with a hotkey. To use edit keybindings.json and add: 79 | 80 | ```json 81 | { 82 | "key": "ctrl+alt+m", 83 | "command": "move-ts.move", 84 | "when": "editorTextFocus" 85 | } 86 | ``` 87 | ### 1.3.0 88 | 89 | Support updating relative paths in export statements 90 | ### 1.2.0 91 | 92 | Support for Windows paths 93 | 94 | ### 1.1.0 95 | 96 | Add `movets.skipWarning` configuration option 97 | 98 | ### 1.0.0 99 | 100 | Initial release of Move TS 101 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringham/move-ts/d0ffb0f8de7701b4594db1040a1e3024e846345b/icon.png -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringham/move-ts/d0ffb0f8de7701b4594db1040a1e3024e846345b/images/usage.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "move-ts", 3 | "displayName": "Move TS - Move TypeScript files and update relative imports", 4 | "description": "extension for moving typescript files and folders and updating relative imports in your workspace", 5 | "version": "1.12.0", 6 | "publisher": "stringham", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/stringham/move-ts.git" 10 | }, 11 | "homepage": "https://github.com/stringham/move-ts", 12 | "icon": "icon.png", 13 | "keywords": [ 14 | "typescript", 15 | "import", 16 | "move", 17 | "refactor", 18 | "relative" 19 | ], 20 | "engines": { 21 | "vscode": "^1.12.0" 22 | }, 23 | "categories": [ 24 | "Other" 25 | ], 26 | "activationEvents": [ 27 | "onCommand:move-ts.move", 28 | "onCommand:move-ts.reindex" 29 | ], 30 | "main": "./out/src/extension", 31 | "contributes": { 32 | "menus": { 33 | "explorer/context": [ 34 | { 35 | "command": "move-ts.move", 36 | "group": "1_modification", 37 | "when": "explorerViewletVisible" 38 | } 39 | ] 40 | }, 41 | "commands": [ 42 | { 43 | "command": "move-ts.move", 44 | "title": "Move Typescript" 45 | }, 46 | { 47 | "command": "move-ts.reindex", 48 | "title": "Move TS: Reindex" 49 | } 50 | ], 51 | "configuration": { 52 | "type": "object", 53 | "title": "Move TS configuration", 54 | "properties": { 55 | "movets.relativeToTsconfig": { 56 | "type": "boolean", 57 | "default": false, 58 | "description": "Create relative paths relative to the tsconfig.json" 59 | }, 60 | "movets.skipWarning": { 61 | "type": "boolean", 62 | "default": false, 63 | "description": "Skip the warning when using the move typescript command" 64 | }, 65 | "movets.filesToScan": { 66 | "type": "array", 67 | "default": [ 68 | "**/*.ts", 69 | "**/*.tsx" 70 | ], 71 | "description": "Glob of files to scan and watch. Defaults to [**/*.ts,**/*.tsx]" 72 | }, 73 | "movets.openEditors": { 74 | "type": "boolean", 75 | "default": false, 76 | "description": "Make edits in vscode instead of saving the changes to disk." 77 | }, 78 | "movets.removeIndexSuffix": { 79 | "type": "boolean", 80 | "default": true, 81 | "description": "Removes index filename from imports" 82 | } 83 | } 84 | } 85 | }, 86 | "scripts": { 87 | "vscode:prepublish": "tsc -p ./", 88 | "compile": "tsc -watch -p ./", 89 | "postinstall": "node ./node_modules/vscode/bin/install", 90 | "test": "node ./node_modules/vscode/bin/test" 91 | }, 92 | "dependencies": { 93 | "typescript": "^3.7.5", 94 | "fs-extra-promise": "1.0.1", 95 | "@types/fs-extra-promise": "1.0.8", 96 | "minimatch": "3.0.4" 97 | }, 98 | "devDependencies": { 99 | "vscode": "^1.1.36", 100 | "mocha": "^7.0.1", 101 | "@types/node": "13.7.0", 102 | "@types/minimatch": "3.0.3", 103 | "@types/mocha": "^7.0.1" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | import * as path from 'path'; 4 | import * as fs from 'fs'; 5 | import {FileItem} from './fileitem'; 6 | import {ReferenceIndexer, isInDir} from './index/referenceindexer'; 7 | 8 | function warn(importer: ReferenceIndexer): Thenable { 9 | if (importer.conf('skipWarning', false) || importer.conf('openEditors', false)) { 10 | return Promise.resolve(true); 11 | } 12 | return vscode.window 13 | .showWarningMessage( 14 | 'This will save all open editors and all changes will immediately be saved. Do you want to continue?', 15 | 'Yes, I understand' 16 | ) 17 | .then((response: string|undefined): any => { 18 | if (response == 'Yes, I understand') { 19 | return true; 20 | } else { 21 | return false; 22 | } 23 | }); 24 | } 25 | 26 | function warnThenMove(importer: ReferenceIndexer, item: FileItem): Thenable { 27 | return warn(importer).then((success: boolean): any => { 28 | if (success) { 29 | return vscode.workspace.saveAll(false).then((): any => { 30 | importer.startNewMove(item.sourcePath, item.targetPath); 31 | const move = item.move(importer); 32 | move.catch(e => { 33 | console.log('error in extension.ts', e); 34 | }); 35 | if (!item.isDir) { 36 | return move 37 | .then(item => { 38 | return Promise.resolve( 39 | vscode.workspace.openTextDocument(item.targetPath) 40 | ).then((textDocument: vscode.TextDocument) => vscode.window.showTextDocument(textDocument)); 41 | }) 42 | .catch(e => { 43 | console.log('error in extension.ts', e); 44 | }); 45 | } 46 | return move; 47 | }); 48 | } 49 | return undefined; 50 | }); 51 | } 52 | 53 | function move(importer: ReferenceIndexer, fsPath: string) { 54 | const isDir = fs.statSync(fsPath).isDirectory(); 55 | return vscode.window.showInputBox({prompt: 'Where would you like to move?', value: fsPath}).then(value => { 56 | if (!value || value == fsPath) { 57 | return; 58 | } 59 | const item: FileItem = new FileItem(fsPath, value, isDir); 60 | if (item.exists()) { 61 | vscode.window.showErrorMessage(value + ' already exists.'); 62 | return; 63 | } 64 | if (item.isDir && isInDir(fsPath, value)) { 65 | vscode.window.showErrorMessage('Cannot move a folder within itself'); 66 | return; 67 | } 68 | return warnThenMove(importer, item); 69 | }); 70 | } 71 | 72 | function moveMultiple(importer: ReferenceIndexer, paths: string[]): Thenable { 73 | const dir = path.dirname(paths[0]); 74 | if (!paths.every(p => path.dirname(p) == dir)) { 75 | return Promise.resolve(); 76 | } 77 | 78 | return vscode.window.showInputBox( 79 | {prompt: 'Which directory would you like to move these to?', value: dir} 80 | ).then((value): any => { 81 | if (!value || path.extname(value) != '') { 82 | vscode.window.showErrorMessage('Must be moving to a directory'); 83 | return; 84 | } 85 | const newLocations = paths.map(p => { 86 | const newLocation = path.resolve(value, path.basename(p)); 87 | return new FileItem(p, newLocation, fs.statSync(p).isDirectory()); 88 | }); 89 | 90 | if (newLocations.some(l => l.exists())) { 91 | vscode.window.showErrorMessage('Not allowed to overwrite existing files'); 92 | return; 93 | } 94 | 95 | if (newLocations.some(l => l.isDir && isInDir(l.sourcePath, l.targetPath))) { 96 | vscode.window.showErrorMessage('Cannot move a folder within itself'); 97 | return; 98 | } 99 | 100 | return warn(importer).then((success: boolean): any => { 101 | if (success) { 102 | return vscode.workspace.saveAll(false).then(() => { 103 | importer.startNewMoves(newLocations); 104 | const move = FileItem.moveMultiple(newLocations, importer); 105 | move.catch(e => { 106 | console.log('error in extension.ts', e); 107 | }); 108 | return move; 109 | }); 110 | } 111 | }); 112 | }); 113 | } 114 | 115 | function getCurrentPath(): string { 116 | const activeEditor = vscode.window.activeTextEditor; 117 | const document = activeEditor && activeEditor.document; 118 | 119 | return (document && document.fileName) || ''; 120 | } 121 | 122 | export function activate(context: vscode.ExtensionContext) { 123 | const importer: ReferenceIndexer = new ReferenceIndexer(); 124 | 125 | function initWithProgress() { 126 | return vscode.window.withProgress( 127 | { 128 | location: vscode.ProgressLocation.Window, 129 | title: 'Move-ts indexing', 130 | }, 131 | async (progress) => { 132 | return importer.init(progress); 133 | } 134 | ); 135 | } 136 | 137 | const initialize = () => { 138 | if (importer.isInitialized) { 139 | return Promise.resolve(); 140 | } 141 | return initWithProgress(); 142 | }; 143 | 144 | const moveDisposable = vscode.commands.registerCommand('move-ts.move', (uri?: vscode.Uri, uris?: vscode.Uri[]) => { 145 | if (uris && uris.length > 1) { 146 | const dir = path.dirname(uris[0].fsPath); 147 | if (uris.every(u => path.dirname(u.fsPath) == dir)) { 148 | return initialize().then(() => { 149 | return moveMultiple(importer, uris.map(u => u.fsPath)); 150 | }); 151 | } 152 | } 153 | let filePath = uri ? uri.fsPath : getCurrentPath(); 154 | if (!filePath) { 155 | filePath = getCurrentPath(); 156 | } 157 | if (!filePath || filePath.length == 0) { 158 | vscode.window.showErrorMessage( 159 | 'Could not find target to move. Right click in explorer or open a file to move.' 160 | ); 161 | return; 162 | } 163 | const go = () => { 164 | return move(importer, filePath); 165 | }; 166 | return initialize().then(() => go()); 167 | }); 168 | context.subscriptions.push(moveDisposable); 169 | 170 | const reIndexDisposable = vscode.commands.registerCommand('move-ts.reindex', () => { 171 | return initWithProgress(); 172 | }); 173 | context.subscriptions.push(reIndexDisposable); 174 | } 175 | 176 | // this method is called when your extension is deactivated 177 | export function deactivate() { 178 | } -------------------------------------------------------------------------------- /src/fileitem.ts: -------------------------------------------------------------------------------- 1 | import * as Promise from 'bluebird'; 2 | import * as fs from 'fs-extra-promise'; 3 | import * as path from 'path'; 4 | 5 | import {ReferenceIndexer} from './index/referenceindexer'; 6 | 7 | export class FileItem { 8 | constructor( 9 | public sourcePath: string, 10 | public targetPath: string, 11 | public isDir: boolean, 12 | ) { 13 | } 14 | 15 | exists(): boolean { 16 | return fs.existsSync(this.targetPath); 17 | } 18 | 19 | static moveMultiple(items: FileItem[], index: ReferenceIndexer): Promise { 20 | return items[0].ensureDir().then(() => { 21 | 22 | const sourceDir = path.dirname(items[0].sourcePath); 23 | const targetDir = path.dirname(items[0].targetPath); 24 | const fileNames = items.map(i => path.basename(i.sourcePath)); 25 | return index.updateDirImports(sourceDir, targetDir, fileNames) 26 | .then(() => { 27 | const promises = items.map(i => fs.renameAsync(i.sourcePath, i.targetPath)); 28 | return Promise.all(promises); 29 | }) 30 | .then(() => { 31 | return index.updateMovedDir(sourceDir, targetDir, fileNames); 32 | }) 33 | .then(() => { 34 | return items; 35 | }); 36 | }); 37 | } 38 | 39 | public move(index: ReferenceIndexer): Promise { 40 | return this.ensureDir() 41 | .then(() => { 42 | if (this.isDir) { 43 | return index.updateDirImports(this.sourcePath, this.targetPath) 44 | .then(() => { 45 | return fs.renameAsync(this.sourcePath, this.targetPath); 46 | }) 47 | .then(() => { 48 | return index.updateMovedDir(this.sourcePath, this.targetPath); 49 | }) 50 | .then(() => { 51 | return this; 52 | }); 53 | } else { 54 | return index.updateImports(this.sourcePath, this.targetPath) 55 | .then(() => { 56 | return fs.renameAsync(this.sourcePath, this.targetPath); 57 | }) 58 | .then(() => { 59 | return index.updateMovedFile(this.sourcePath, this.targetPath); 60 | }) 61 | .then(() => { 62 | return this; 63 | }); 64 | } 65 | }) 66 | .then( 67 | (): 68 | any => { 69 | return this; 70 | } 71 | ) 72 | .catch(e => { 73 | console.log('error in move', e); 74 | }); 75 | } 76 | 77 | private ensureDir(): Promise { 78 | return fs.ensureDirAsync(path.dirname(this.targetPath)); 79 | } 80 | } -------------------------------------------------------------------------------- /src/index/referenceindex.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | export interface Reference { path: string; } 4 | 5 | export function isPathToAnotherDir(path: string) { 6 | return path.startsWith('../') || path.startsWith('..\\'); 7 | } 8 | 9 | export class ReferenceIndex { 10 | private referencedBy: {[key: string]: Reference[]} = {}; // path -> all of the files that reference it 11 | 12 | private references: {[key: string]: Reference[]} = {}; // path -> all of the files that it references 13 | 14 | // path references the reference 15 | public addReference(reference: string, path: string) { 16 | if (!this.referencedBy.hasOwnProperty(reference)) { 17 | this.referencedBy[reference] = []; 18 | } 19 | if (!this.references.hasOwnProperty(path)) { 20 | this.references[path] = []; 21 | } 22 | 23 | if (!this.references[path].some(ref => { 24 | return ref.path == reference; 25 | })) { 26 | this.references[path].push({path: reference}); 27 | } 28 | 29 | if (!this.referencedBy[reference].some(reference => { 30 | return reference.path == path; 31 | })) { 32 | this.referencedBy[reference].push({ 33 | path, 34 | }); 35 | } 36 | } 37 | 38 | public deleteByPath(path: string) { 39 | if (this.references.hasOwnProperty(path)) { 40 | this.references[path].forEach(p => { 41 | if (this.referencedBy.hasOwnProperty(p.path)) { 42 | this.referencedBy[p.path] = this.referencedBy[p.path].filter(reference => { 43 | return reference.path != path; 44 | }); 45 | } 46 | }); 47 | delete this.references[path]; 48 | } 49 | } 50 | 51 | // get a list of all of the files outside of this directory that reference files 52 | // inside of this directory. 53 | public getDirReferences(directory: string, fileNames: string[] = []): Reference[] { 54 | const result: Reference[] = []; 55 | 56 | const added = new Set(); 57 | const whiteList = new Set(fileNames); 58 | 59 | for (let p in this.referencedBy) { 60 | if (whiteList.size > 0) { 61 | const relative = path.relative(directory, p).split(path.sep)[0]; 62 | if (whiteList.has(relative)) { 63 | this.referencedBy[p].forEach(reference => { 64 | if (added.has(reference.path)) { 65 | return; 66 | } 67 | const relative2 = path.relative(directory, reference.path).split(path.sep)[0]; 68 | if (!whiteList.has(relative2)) { 69 | result.push(reference); 70 | added.add(reference.path); 71 | } 72 | }); 73 | } 74 | } else if (!isPathToAnotherDir(path.relative(directory, p))) { 75 | this.referencedBy[p].forEach(reference => { 76 | if (added.has(reference.path)) { 77 | return; 78 | } 79 | if (isPathToAnotherDir(path.relative(directory, reference.path))) { 80 | result.push(reference); 81 | added.add(reference.path); 82 | } 83 | }); 84 | } 85 | } 86 | return result; 87 | } 88 | 89 | // get a list of all of the files that reference path 90 | public getReferences(path: string): Reference[] { 91 | if (this.referencedBy.hasOwnProperty(path)) { 92 | return this.referencedBy[path]; 93 | } 94 | return []; 95 | } 96 | } -------------------------------------------------------------------------------- /src/index/referenceindexer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra-promise'; 2 | import * as path from 'path'; 3 | import * as ts from 'typescript'; 4 | import * as vscode from 'vscode'; 5 | 6 | import {FileItem} from '../fileitem'; 7 | 8 | import {isPathToAnotherDir, ReferenceIndex} from './referenceindex'; 9 | 10 | const minimatch = require('minimatch'); 11 | 12 | const BATCH_SIZE = 50; 13 | 14 | type Replacement = [string, string]; 15 | 16 | interface Edit { 17 | start: number; 18 | end: number; 19 | replacement: string; 20 | } 21 | 22 | interface Reference { 23 | specifier: string; 24 | location: {start: number, end: number}; 25 | } 26 | 27 | export function isInDir(dir: string, p: string) { 28 | const relative = path.relative(dir, p); 29 | return !isPathToAnotherDir(relative); 30 | } 31 | 32 | export function asUnix(fsPath: string) { 33 | return fsPath.replace(/\\/g, '/'); 34 | } 35 | 36 | export class ReferenceIndexer { 37 | changeDocumentEvent: vscode.Disposable; 38 | private tsconfigs: {[key: string]: any}; 39 | public index: ReferenceIndex = new ReferenceIndex(); 40 | 41 | private output: vscode.OutputChannel = vscode.window.createOutputChannel('move-ts'); 42 | 43 | private packageNames: {[key: string]: string} = {}; 44 | 45 | private extensions: string[] = ['.ts', '.tsx']; 46 | 47 | private paths: string[] = []; 48 | private filesToExclude: string[] = []; 49 | private fileWatcher: vscode.FileSystemWatcher; 50 | 51 | public isInitialized: boolean = false; 52 | 53 | public init(progress?: vscode.Progress<{message: string}>): Thenable { 54 | this.index = new ReferenceIndex(); 55 | 56 | return this.readPackageNames().then(() => { 57 | return this.scanAll(progress) 58 | .then(() => { 59 | return this.attachFileWatcher(); 60 | }) 61 | .then(() => { 62 | console.log('move-ts initialized'); 63 | this.isInitialized = true; 64 | }); 65 | }); 66 | } 67 | 68 | public conf(property: string, defaultValue: T): T { 69 | return vscode.workspace.getConfiguration('movets').get(property, defaultValue); 70 | } 71 | 72 | private readPackageNames(): Thenable { 73 | this.packageNames = {}; 74 | this.tsconfigs = {}; 75 | let seenPackageNames: {[key: string]: boolean} = {}; 76 | const packagePromise = vscode.workspace.findFiles('**/package.json', '**/node_modules/**', 1000).then(files => { 77 | const promises = files.map(file => { 78 | return fs.readFileAsync(file.fsPath, 'utf-8').then(content => { 79 | try { 80 | let json = JSON.parse(content); 81 | if (json.name) { 82 | if (seenPackageNames[json.name]) { 83 | delete this.packageNames[json.name]; 84 | return; 85 | } 86 | seenPackageNames[json.name] = true; 87 | this.packageNames[json.name] = path.dirname(file.fsPath); 88 | } 89 | } catch (e) { 90 | } 91 | }); 92 | }); 93 | return Promise.all(promises); 94 | }); 95 | const tsConfigPromise = 96 | vscode.workspace.findFiles('**/tsconfig{.json,.build.json}', '**/node_modules/**', 1000).then(files => { 97 | const promises = files.map(file => { 98 | return fs.readFileAsync(file.fsPath, 'utf-8').then(content => { 99 | try { 100 | const config = ts.parseConfigFileTextToJson(file.fsPath, content); 101 | if (config.config) { 102 | this.tsconfigs[file.fsPath] = config.config; 103 | } 104 | } catch (e) { 105 | } 106 | }); 107 | }); 108 | return Promise.all(promises); 109 | }); 110 | return Promise.all([packagePromise, tsConfigPromise]); 111 | } 112 | 113 | public startNewMoves(moves: FileItem[]) { 114 | this.output.appendLine('--------------------------------------------------'); 115 | this.output.appendLine(`Moving:`); 116 | for (let i = 0; i < moves.length; i++) { 117 | this.output.appendLine(` ${moves[i].sourcePath} -> ${moves[i].targetPath}`); 118 | } 119 | this.output.appendLine('--------------------------------------------------'); 120 | this.output.appendLine('Files changed:'); 121 | } 122 | 123 | public startNewMove(from: string, to: string) { 124 | this.output.appendLine('--------------------------------------------------'); 125 | this.output.appendLine(`Moving ${from} -> ${to}`); 126 | this.output.appendLine('--------------------------------------------------'); 127 | this.output.appendLine('Files changed:'); 128 | } 129 | 130 | private get filesToScanGlob(): string { 131 | const filesToScan = this.conf('filesToScan', ['**/*.ts', '**/*.tsx']); 132 | if (filesToScan.length == 0) { 133 | return ''; 134 | } 135 | return filesToScan.length == 1 ? filesToScan[0] : `{${filesToScan.join(',')}}`; 136 | } 137 | 138 | private scanAll(progress?: vscode.Progress<{message: string}>) { 139 | this.index = new ReferenceIndex(); 140 | const start = Date.now(); 141 | return vscode.workspace.findFiles(this.filesToScanGlob, '**/node_modules/**', 100000) 142 | .then(files => { 143 | return this.processWorkspaceFiles(files, false, progress); 144 | }) 145 | .then(() => { 146 | console.log('scan finished in ' + (Date.now() - start) + 'ms'); 147 | }); 148 | } 149 | 150 | private attachFileWatcher(): void { 151 | if (this.fileWatcher) { 152 | this.fileWatcher.dispose(); 153 | } 154 | if (this.changeDocumentEvent) { 155 | this.changeDocumentEvent.dispose(); 156 | } 157 | this.changeDocumentEvent = vscode.workspace.onDidChangeTextDocument(changeEvent => { 158 | addBatch(changeEvent.document.uri, changeEvent.document); 159 | }); 160 | this.fileWatcher = vscode.workspace.createFileSystemWatcher(this.filesToScanGlob); 161 | 162 | const watcher = this.fileWatcher; 163 | const batch: string[] = []; 164 | const documents: vscode.TextDocument[] = []; 165 | let batchTimeout: any = undefined; 166 | 167 | const batchHandler = () => { 168 | batchTimeout = undefined; 169 | 170 | vscode.workspace.findFiles(this.filesToScanGlob, '**/node_modules/**', 10000).then(files => { 171 | const b = new Set(batch.splice(0, batch.length)); 172 | if (b.size) { 173 | this.processWorkspaceFiles(files.filter(f => b.has(f.fsPath)), true); 174 | } 175 | const docs = documents.splice(0, documents.length); 176 | if (docs.length) { 177 | this.processDocuments(docs); 178 | } 179 | }); 180 | }; 181 | 182 | const addBatch = (file: vscode.Uri, doc?: vscode.TextDocument) => { 183 | if (doc) { 184 | documents.push(doc); 185 | } else { 186 | batch.push(file.fsPath); 187 | } 188 | if (batchTimeout) { 189 | clearTimeout(batchTimeout); 190 | batchTimeout = undefined; 191 | } 192 | batchTimeout = setTimeout(batchHandler, 250); 193 | }; 194 | 195 | watcher.onDidChange(addBatch); 196 | watcher.onDidCreate(addBatch); 197 | watcher.onDidDelete((file: vscode.Uri) => { 198 | this.index.deleteByPath(file.fsPath); 199 | }); 200 | } 201 | 202 | private getEdits(path: string, text: string, replacements: Replacement[], fromPath?: string): Edit[] { 203 | const edits: Edit[] = []; 204 | const relativeReferences = this.getRelativeReferences(text, fromPath || path); 205 | replacements.forEach(replacement => { 206 | const before = replacement[0]; 207 | const after = replacement[1]; 208 | if (before == after) { 209 | return; 210 | } 211 | const beforeReference = this.resolveRelativeReference(fromPath || path, before); 212 | const seen: any = {}; 213 | const beforeReplacements = relativeReferences.filter(ref => { 214 | return this.resolveRelativeReference(fromPath || path, ref.specifier) == beforeReference; 215 | }); 216 | beforeReplacements.forEach(beforeReplacement => { 217 | const edit = { 218 | start: beforeReplacement.location.start + 1, 219 | end: beforeReplacement.location.end - 1, 220 | replacement: after, 221 | }; 222 | edits.push(edit); 223 | }); 224 | }); 225 | 226 | return edits; 227 | } 228 | 229 | private applyEdits(text: string, edits: Edit[]): string { 230 | const replaceBetween = (str: string, start: number, end: number, replacement: string): string => { 231 | return str.substr(0, start) + replacement + str.substr(end); 232 | }; 233 | 234 | edits.sort((a, b) => { 235 | return a.start - b.start; 236 | }); 237 | 238 | let editOffset = 0; 239 | for (let i = 0; i < edits.length; i++) { 240 | const edit = edits[i]; 241 | text = replaceBetween(text, edit.start + editOffset, edit.end + editOffset, edit.replacement); 242 | editOffset += edit.replacement.length - (edit.end - edit.start); 243 | } 244 | return text; 245 | } 246 | 247 | private replaceReferences(filePath: string, getReplacements: (text: string) => Replacement[], fromPath?: string): 248 | Thenable { 249 | if (!this.conf('openEditors', false)) { 250 | return fs.readFileAsync(filePath, 'utf8').then(text => { 251 | const replacements = getReplacements(text); 252 | const edits = this.getEdits(filePath, text, replacements, fromPath); 253 | if (edits.length == 0) { 254 | return Promise.resolve(); 255 | } 256 | 257 | const newText = this.applyEdits(text, edits); 258 | 259 | this.output.show(); 260 | this.output.appendLine(filePath); 261 | 262 | return fs.writeFileAsync(filePath, newText, 'utf-8').then(() => { 263 | this.processFile(newText, filePath, true); 264 | }); 265 | }); 266 | } else { 267 | function attemptEdit(edit: vscode.WorkspaceEdit, attempts: number = 0): Thenable { 268 | return vscode.workspace.applyEdit(edit).then(success => { 269 | if (!success && attempts < 5) { 270 | console.log(attempts); 271 | return attemptEdit(edit, attempts + 1); 272 | } 273 | }); 274 | } 275 | 276 | return vscode.workspace.openTextDocument(filePath).then((doc: vscode.TextDocument): Thenable => { 277 | const text = doc.getText(); 278 | const replacements = getReplacements(text); 279 | 280 | const rawEdits = this.getEdits(filePath, text, replacements); 281 | const edits = rawEdits.map((edit: Edit) => { 282 | return vscode.TextEdit.replace( 283 | new vscode.Range(doc.positionAt(edit.start), doc.positionAt(edit.end)), edit.replacement 284 | ); 285 | }); 286 | if (edits.length > 0) { 287 | this.output.show(); 288 | this.output.appendLine(filePath); 289 | const edit = new vscode.WorkspaceEdit(); 290 | edit.set(doc.uri, edits); 291 | return attemptEdit(edit).then(() => { 292 | const newText = this.applyEdits(text, rawEdits); 293 | this.processFile(newText, filePath, true); 294 | }); 295 | } else { 296 | return Promise.resolve(); 297 | } 298 | }); 299 | } 300 | } 301 | 302 | public updateMovedFile(from: string, to: string): Thenable { 303 | return this 304 | .replaceReferences( 305 | to, 306 | (text: string): 307 | Replacement[] => { 308 | const references = Array.from(new Set(this.getRelativeImportSpecifiers(text, from))); 309 | 310 | const replacements = references.map((reference): [string, string] => { 311 | const absReference = this.resolveRelativeReference(from, reference); 312 | const newReference = this.getRelativePath(to, absReference); 313 | return [reference, newReference]; 314 | }); 315 | return replacements; 316 | }, 317 | from 318 | ) 319 | .then(() => { 320 | this.index.deleteByPath(from); 321 | }); 322 | } 323 | 324 | public updateMovedDir(from: string, to: string, fileNames: string[] = []): Thenable { 325 | const relative = vscode.workspace.asRelativePath(to); 326 | const glob = this.filesToScanGlob; 327 | const whiteList = new Set(fileNames); 328 | return vscode.workspace.findFiles(relative + '/**', undefined, 100000).then(files => { 329 | const promises = files 330 | .filter(file => { 331 | if (whiteList.size > 0) { 332 | return minimatch(file.fsPath, glob) && 333 | whiteList.has(path.relative(to, file.fsPath).split(path.sep)[0]); 334 | } 335 | return minimatch(file.fsPath, glob); 336 | }) 337 | .map(file => { 338 | const originalPath = path.resolve(from, path.relative(to, file.fsPath)); 339 | return this.replaceReferences(file.fsPath, (text: string): Replacement[] => { 340 | const references = this.getRelativeImportSpecifiers(text, file.fsPath); 341 | const change = 342 | references 343 | .filter(p => { 344 | const abs = this.resolveRelativeReference(originalPath, p); 345 | if (whiteList.size > 0) { 346 | const name = path.relative(from, abs).split(path.sep)[0]; 347 | if (whiteList.has(name)) { 348 | return false; 349 | } 350 | for (let i = 0; i < this.extensions.length; i++) { 351 | if (whiteList.has(name + this.extensions[i])) { 352 | return false; 353 | } 354 | } 355 | return true; 356 | } 357 | return isPathToAnotherDir(path.relative(from, abs)); 358 | }) 359 | .map((p): Replacement => { 360 | const abs = this.resolveRelativeReference(originalPath, p); 361 | const relative = this.getRelativePath(file.fsPath, abs); 362 | return [p, relative]; 363 | }); 364 | return change; 365 | }, originalPath); 366 | }); 367 | return Promise.all(promises); 368 | }); 369 | } 370 | 371 | public updateDirImports(from: string, to: string, fileNames: string[] = []): Thenable { 372 | const whiteList = new Set(fileNames); 373 | const affectedFiles = this.index.getDirReferences(from, fileNames); 374 | const promises = affectedFiles.map(reference => { 375 | return this.replaceReferences(reference.path, (text: string): Replacement[] => { 376 | const imports = this.getRelativeImportSpecifiers(text, reference.path); 377 | const change = imports 378 | .filter(p => { 379 | const abs = this.resolveRelativeReference(reference.path, p); 380 | if (fileNames.length > 0) { 381 | const name = path.relative(from, abs).split(path.sep)[0]; 382 | if (whiteList.has(name)) { 383 | return true; 384 | } 385 | for (let i = 0; i < this.extensions.length; i++) { 386 | if (whiteList.has(name + this.extensions[i])) { 387 | return true; 388 | } 389 | } 390 | return false; 391 | } 392 | return !isPathToAnotherDir(path.relative(from, abs)); 393 | }) 394 | .map((p): [string, string] => { 395 | const abs = this.resolveRelativeReference(reference.path, p); 396 | const relative = path.relative(from, abs); 397 | const newabs = path.resolve(to, relative); 398 | const changeTo = this.getRelativePath(reference.path, newabs); 399 | return [p, changeTo]; 400 | }); 401 | return change; 402 | }); 403 | }); 404 | return Promise.all(promises); 405 | } 406 | 407 | public removeExtension(filePath: string): string { 408 | let ext = path.extname(filePath); 409 | if (ext == '.ts' && filePath.endsWith('.d.ts')) { 410 | ext = '.d.ts'; 411 | } 412 | if (this.extensions.indexOf(ext) >= 0) { 413 | return filePath.slice(0, -ext.length); 414 | } 415 | return filePath; 416 | } 417 | 418 | public removeIndexSuffix(filePath: string): string { 419 | if (!this.conf('removeIndexSuffix', true)) { 420 | return filePath; 421 | } 422 | const indexSuffix = '/index'; 423 | if (filePath.endsWith(indexSuffix)) { 424 | return filePath.slice(0, -indexSuffix.length); 425 | } 426 | return filePath; 427 | } 428 | 429 | public updateImports(from: string, to: string): Promise { 430 | const affectedFiles = this.index.getReferences(from); 431 | const promises = affectedFiles.map(filePath => { 432 | return this.replaceReferences(filePath.path, (text: string): Replacement[] => { 433 | let relative = this.getRelativePath(filePath.path, from); 434 | relative = this.removeExtension(relative); 435 | 436 | let newRelative = this.getRelativePath(filePath.path, to); 437 | newRelative = this.removeExtension(newRelative); 438 | newRelative = this.removeIndexSuffix(newRelative); 439 | 440 | return [[relative, newRelative]]; 441 | }); 442 | }); 443 | return Promise.all(promises).catch(e => { 444 | console.log(e); 445 | }); 446 | } 447 | 448 | private processWorkspaceFiles(files: vscode.Uri[], deleteByFile: boolean = false, progress?: vscode.Progress<{ 449 | message: string 450 | }>): Promise { 451 | files = files.filter((f) => { 452 | return f.fsPath.indexOf('typings') === -1 && f.fsPath.indexOf('node_modules') === -1 && 453 | f.fsPath.indexOf('jspm_packages') === -1; 454 | }); 455 | 456 | return new Promise(resolve => { 457 | let index = 0; 458 | 459 | const next = () => { 460 | for (let i = 0; i < BATCH_SIZE && index < files.length; i++) { 461 | const file = files[index++]; 462 | try { 463 | const data = fs.readFileSync(file.fsPath, 'utf8'); 464 | this.processFile(data, file.fsPath, deleteByFile); 465 | } catch (e) { 466 | console.log('Failed to load file', e); 467 | } 468 | } 469 | 470 | if (progress) { 471 | progress.report({message: 'move-ts indexing... ' + index + '/' + files.length + ' indexed'}); 472 | } 473 | 474 | if (index < files.length) { 475 | setTimeout(next, 0); 476 | } else { 477 | resolve(); 478 | } 479 | }; 480 | next(); 481 | 482 | }); 483 | } 484 | 485 | private processDocuments(documents: vscode.TextDocument[]): Promise { 486 | documents = documents.filter((doc) => { 487 | return doc.uri.fsPath.indexOf('typings') === -1 && doc.uri.fsPath.indexOf('node_modules') === -1 && 488 | doc.uri.fsPath.indexOf('jspm_packages') === -1; 489 | }); 490 | 491 | return new Promise(resolve => { 492 | let index = 0; 493 | 494 | const next = () => { 495 | for (let i = 0; i < BATCH_SIZE && index < documents.length; i++) { 496 | const doc = documents[index++]; 497 | try { 498 | const data = doc.getText(); 499 | this.processFile(data, doc.uri.fsPath, false); 500 | } catch (e) { 501 | console.log('Failed to load file', e); 502 | } 503 | } 504 | if (index < documents.length) { 505 | setTimeout(next, 0); 506 | } else { 507 | resolve(); 508 | } 509 | }; 510 | next(); 511 | 512 | }); 513 | } 514 | 515 | private doesFileExist(filePath: string) { 516 | if (fs.existsSync(filePath)) { 517 | return true; 518 | } 519 | for (let i = 0; i < this.extensions.length; i++) { 520 | if (fs.existsSync(filePath + this.extensions[i])) { 521 | return true; 522 | } 523 | } 524 | return false; 525 | } 526 | 527 | private getRelativePath(from: string, to: string): string { 528 | const configInfo = this.getTsConfig(from); 529 | if (configInfo) { 530 | const config = configInfo.config; 531 | const configPath = configInfo.configPath; 532 | if (config.compilerOptions && config.compilerOptions.paths && config.compilerOptions.baseUrl) { 533 | const baseUrl = path.resolve(path.dirname(configPath), config.compilerOptions.baseUrl); 534 | for (let p in config.compilerOptions.paths) { 535 | const paths = config.compilerOptions.paths[p]; 536 | for (let i = 0; i < paths.length; i++) { 537 | const mapped = paths[i].slice(0, -1); 538 | const mappedDir = path.resolve(baseUrl, mapped); 539 | if (isInDir(mappedDir, to)) { 540 | return asUnix(p.slice(0, -1) + path.relative(mappedDir, to)); 541 | } 542 | } 543 | } 544 | } 545 | } 546 | for (let packageName in this.packageNames) { 547 | const packagePath = this.packageNames[packageName]; 548 | if (isInDir(packagePath, to) && !isInDir(packagePath, from)) { 549 | return asUnix(path.join(packageName, path.relative(packagePath, to))); 550 | } 551 | } 552 | const relativeToTsConfig = this.conf('relativeToTsconfig', false); 553 | if (relativeToTsConfig && configInfo) { 554 | const configDir = path.dirname(configInfo.configPath); 555 | if (isInDir(configDir, from) && isInDir(configDir, to)) { 556 | return asUnix(path.relative(configDir, to)); 557 | } 558 | } 559 | let relative = path.relative(path.dirname(from), to); 560 | if (!relative.startsWith('.')) { 561 | relative = './' + relative; 562 | } 563 | return asUnix(relative); 564 | } 565 | 566 | private resolveRelativeReference(fsPath: string, reference: string): string { 567 | if (reference.startsWith('.')) { 568 | return path.resolve(path.dirname(fsPath), reference); 569 | } else { 570 | const configInfo = this.getTsConfig(fsPath); 571 | if (configInfo) { 572 | const config = configInfo.config; 573 | const configPath = configInfo.configPath; 574 | const relativeToTsConfig = this.conf('relativeToTsconfig', false); 575 | if (relativeToTsConfig && configPath) { 576 | const check = path.resolve(path.dirname(configPath), reference); 577 | if (this.doesFileExist(check)) { 578 | return check; 579 | } 580 | } 581 | if (config.compilerOptions && config.compilerOptions.paths && config.compilerOptions.baseUrl) { 582 | const baseUrl = path.resolve(path.dirname(configPath), config.compilerOptions.baseUrl); 583 | for (let p in config.compilerOptions.paths) { 584 | if (p.endsWith('*') && reference.startsWith(p.slice(0, -1))) { 585 | const paths = config.compilerOptions.paths[p]; 586 | for (let i = 0; i < paths.length; i++) { 587 | const mapped = paths[i].slice(0, -1); 588 | const mappedDir = path.resolve(baseUrl, mapped); 589 | const potential = path.join(mappedDir, reference.substr(p.slice(0, -1).length)); 590 | if (this.doesFileExist(potential)) { 591 | return potential; 592 | } 593 | } 594 | if (config.compilerOptions.paths[p].length == 1) { 595 | const mapped = config.compilerOptions.paths[p][0].slice(0, -1); 596 | const mappedDir = path.resolve(path.dirname(configPath), mapped); 597 | return path.join(mappedDir, reference.substr(p.slice(0, -1).length)); 598 | } 599 | } 600 | } 601 | } 602 | } 603 | for (let packageName in this.packageNames) { 604 | if (reference.startsWith(packageName + '/')) { 605 | return path.resolve(this.packageNames[packageName], reference.substr(packageName.length + 1)); 606 | } 607 | } 608 | } 609 | return ''; 610 | } 611 | 612 | private getTsConfig(filePath: string): any { 613 | let prevDir = filePath; 614 | let dir = path.dirname(filePath); 615 | while (dir != prevDir) { 616 | const tsConfigPaths = [path.join(dir, 'tsconfig.json'), path.join(dir, 'tsconfig.build.json')]; 617 | const tsConfigPath = tsConfigPaths.find(p => this.tsconfigs.hasOwnProperty(p)); 618 | 619 | if (tsConfigPath) { 620 | return {config: this.tsconfigs[tsConfigPath], configPath: tsConfigPath}; 621 | } 622 | prevDir = dir; 623 | dir = path.dirname(dir); 624 | } 625 | return null; 626 | } 627 | 628 | private getRelativeImportSpecifiers(data: string, filePath: string): string[] { 629 | return this.getRelativeReferences(data, filePath).map(ref => ref.specifier); 630 | } 631 | 632 | private getReferences(fileName: string, data: string): Reference[] { 633 | const result: Reference[] = []; 634 | const file = ts.createSourceFile(fileName, data, ts.ScriptTarget.Latest); 635 | 636 | file.statements.forEach((node: ts.Node) => { 637 | if (ts.isImportDeclaration(node)) { 638 | if (ts.isStringLiteral(node.moduleSpecifier)) { 639 | result.push({ 640 | specifier: node.moduleSpecifier.text, 641 | location: { 642 | start: node.moduleSpecifier.getStart(file), 643 | end: node.moduleSpecifier.getEnd(), 644 | }, 645 | }); 646 | } 647 | } 648 | }); 649 | 650 | return result; 651 | } 652 | 653 | private getRelativeReferences(data: string, filePath: string): Reference[] { 654 | const references: Set = new Set(); 655 | let cachedConfig: any = undefined; 656 | const getConfig = () => { 657 | if (cachedConfig === undefined) { 658 | cachedConfig = this.getTsConfig(filePath); 659 | } 660 | return cachedConfig; 661 | }; 662 | const imports = this.getReferences(filePath, data); 663 | for (let i = 0; i < imports.length; i++) { 664 | const importModule = imports[i].specifier; 665 | if (importModule.startsWith('.')) { 666 | references.add(importModule); 667 | } else { 668 | const resolved = this.resolveRelativeReference(filePath, importModule); 669 | if (resolved.length > 0) { 670 | references.add(importModule); 671 | } 672 | } 673 | } 674 | return imports.filter(i => references.has(i.specifier)); 675 | } 676 | 677 | private processFile(data: string, filePath: string, deleteByFile: boolean = false) { 678 | if (deleteByFile) { 679 | this.index.deleteByPath(filePath); 680 | } 681 | 682 | const fsPath = this.removeExtension(filePath); 683 | 684 | const references = this.getRelativeImportSpecifiers(data, fsPath); 685 | 686 | for (let i = 0; i < references.length; i++) { 687 | let referenced = this.resolveRelativeReference(filePath, references[i]); 688 | for (let j = 0; j < this.extensions.length; j++) { 689 | const ext = this.extensions[j]; 690 | if (!referenced.endsWith(ext) && fs.existsSync(referenced + ext)) { 691 | referenced += ext; 692 | } 693 | } 694 | this.index.addReference(referenced, filePath); 695 | } 696 | } 697 | } 698 | -------------------------------------------------------------------------------- /src/walk.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | export function walk(node: ts.Node, fn: (node: ts.Node) => any): boolean { 4 | if (fn(node)) { 5 | return true; 6 | } 7 | const children = node.getChildren(); 8 | for (let i = 0; i < children.length; i++) { 9 | if (walk(children[i], fn)) { 10 | return true; 11 | } 12 | } 13 | return false; 14 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "strictNullChecks": true, 7 | "noImplicitAny": true, 8 | "lib": [ 9 | "es6" 10 | ], 11 | "sourceMap": true, 12 | "rootDir": "." 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | ".vscode-test" 17 | ] 18 | } --------------------------------------------------------------------------------