├── .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 | 
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 | }
--------------------------------------------------------------------------------