Skip to content

Commit b8ddeec

Browse files
committed
feat: add module-resolver utils
add class that rewrites the imports of a given file and its dependent files based on where the file has been moved inside the project.
1 parent 1945e85 commit b8ddeec

File tree

2 files changed

+269
-0
lines changed

2 files changed

+269
-0
lines changed
+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use strict';
2+
3+
import * as path from 'path';
4+
import * as ts from 'typescript';
5+
import * as dependentFilesUtils from './get-dependent-files';
6+
7+
import { Promise } from 'es6-promise';
8+
import { Change, ReplaceChange } from './change';
9+
10+
// The root directory of Angular Project.
11+
const ROOT_PATH = path.resolve('src/app');
12+
13+
/**
14+
* Rewrites import module of dependent files when the file is moved.
15+
* Also, rewrites export module of related index file of the given file.
16+
*/
17+
export class ModuleResolver {
18+
19+
constructor(public oldFilePath: string, public newFilePath: string) {}
20+
21+
/**
22+
* Changes are applied from the bottom of a file to the top.
23+
* An array of Change instances are sorted based upon the order,
24+
* then apply() method is called sequentially.
25+
*
26+
* @param changes {Change []}
27+
* @return Promise after all apply() method of Change class is called
28+
* to all Change instances sequentially.
29+
*/
30+
applySortedChangePromise(changes: Change[]): Promise<void> {
31+
return changes
32+
.sort((currentChange, nextChange) => nextChange.order - currentChange.order)
33+
.reduce((newChange, change) => newChange.then(() => change.apply()), Promise.resolve());
34+
}
35+
36+
/**
37+
* Assesses the import specifier and determines if it is a relative import.
38+
*
39+
* @return {boolean} boolean value if the import specifier is a relative import.
40+
*/
41+
isRelativeImport(importClause: dependentFilesUtils.ModuleImport): boolean {
42+
let singleSlash = importClause.specifierText.charAt(0) === '/';
43+
let currentDirSyntax = importClause.specifierText.slice(0, 2) === './';
44+
let parentDirSyntax = importClause.specifierText.slice(0, 3) === '../';
45+
return singleSlash || currentDirSyntax || parentDirSyntax;
46+
}
47+
48+
/**
49+
* Rewrites the import specifiers of all the dependent files (cases for no index file).
50+
*
51+
* @todo Implement the logic for rewriting imports of the dependent files when the file
52+
* being moved has index file in its old path and/or in its new path.
53+
*
54+
* @return {Promise<Change[]>}
55+
*/
56+
resolveDependentFiles(): Promise<Change[]> {
57+
return dependentFilesUtils.getDependentFiles(this.oldFilePath, ROOT_PATH)
58+
.then((files: dependentFilesUtils.ModuleMap) => {
59+
let changes: Change[] = [];
60+
Object.keys(files).forEach(file => {
61+
let tempChanges: ReplaceChange[] = files[file]
62+
.map(specifier => {
63+
let componentName = path.basename(this.oldFilePath, '.ts');
64+
let fileDir = path.dirname(file);
65+
let changeText = path.relative(fileDir, path.join(this.newFilePath, componentName));
66+
if (changeText.length > 0 && changeText.charAt(0) !== '.') {
67+
changeText = `.${path.sep}${changeText}`;
68+
};
69+
let position = specifier.end - specifier.specifierText.length;
70+
return new ReplaceChange(file, position - 1, specifier.specifierText, changeText);
71+
});
72+
changes = changes.concat(tempChanges);
73+
});
74+
return changes;
75+
});
76+
}
77+
78+
/**
79+
* Rewrites the file's own relative imports after it has been moved to new path.
80+
*
81+
* @return {Promise<Change[]>}
82+
*/
83+
resolveOwnImports(): Promise<Change[]> {
84+
return dependentFilesUtils.createTsSourceFile(this.oldFilePath)
85+
.then((tsFile: ts.SourceFile) => dependentFilesUtils.getImportClauses(tsFile))
86+
.then(moduleSpecifiers => {
87+
let changes: Change[] = moduleSpecifiers
88+
.filter(importClause => this.isRelativeImport(importClause))
89+
.map(specifier => {
90+
let specifierText = specifier.specifierText;
91+
let moduleAbsolutePath = path.resolve(path.dirname(this.oldFilePath), specifierText);
92+
let changeText = path.relative(this.newFilePath, moduleAbsolutePath);
93+
if (changeText.length > 0 && changeText.charAt(0) !== '.') {
94+
changeText = `.${path.sep}${changeText}`;
95+
}
96+
let position = specifier.end - specifier.specifierText.length;
97+
return new ReplaceChange(this.oldFilePath, position - 1, specifierText, changeText);
98+
});
99+
return changes;
100+
});
101+
}
102+
}
+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
'use strict';
2+
3+
const mockFs = require('mock-fs');
4+
5+
import * as ts from 'typescript';
6+
import * as path from 'path';
7+
import * as dependentFilesUtils from '../../addon/ng2/utilities/get-dependent-files';
8+
9+
import { expect } from 'chai';
10+
import { ModuleResolver } from '../../addon/ng2/utilities/module-resolver';
11+
12+
describe('ModuleResolver', () => {
13+
let rootPath = 'src/app';
14+
15+
beforeEach(() => {
16+
let mockDrive = {
17+
'src/app': {
18+
'foo': {
19+
'foo.component.ts': `import * from "../bar/baz/baz.component";`,
20+
},
21+
'bar': {
22+
'baz': {
23+
'baz.component.ts': `import * from "../bar.component"
24+
import * from '../../foo-baz/qux/quux/foobar/foobar.component'
25+
`
26+
},
27+
'bar.component.ts': `import * from './baz/baz.component'
28+
import * from '../foo/foo.component'`,
29+
},
30+
'foo-baz': {
31+
'qux': {
32+
'quux': {
33+
'foobar': {
34+
'foobar.component.ts': `import * from "../../../../foo/foo.component"
35+
import * from '../fooqux.component'
36+
`,
37+
},
38+
'fooqux': {
39+
'fooqux.component.ts': 'import * from "../foobar/foobar.component"'
40+
}
41+
}
42+
},
43+
'no-module.component.ts': '',
44+
'foo-baz.component.ts': 'import * from \n"../foo/foo.component"\n'
45+
},
46+
'empty-dir': {}
47+
}
48+
};
49+
mockFs(mockDrive);
50+
});
51+
afterEach(() => {
52+
mockFs.restore();
53+
});
54+
55+
describe('Rewrite imports', () => {
56+
// Normalize paths for platform specific delimeter.
57+
let barFile = path.join(rootPath, 'bar/bar.component.ts');
58+
let fooFile = path.join(rootPath, 'foo/foo.component.ts');
59+
let bazFile = path.join(rootPath, 'bar/baz/baz.component.ts');
60+
let fooBazFile = path.join(rootPath, 'foo-baz/foo-baz.component.ts');
61+
let fooBarFile = path.join(rootPath, 'foo-baz/qux/quux/foobar/foobar.component.ts');
62+
let fooQuxFile = path.join(rootPath, 'foo-baz/qux/quux/fooqux/fooqux.component.ts');
63+
64+
it('when there is no index.ts in oldPath', () => {
65+
let oldFilePath = path.join(rootPath, 'bar/baz/baz.component.ts');
66+
let newFilePath = path.join(rootPath, 'foo');
67+
let resolver = new ModuleResolver(oldFilePath, newFilePath);
68+
return resolver.resolveDependentFiles()
69+
.then((changes) => resolver.applySortedChangePromise(changes))
70+
.then(() => dependentFilesUtils.createTsSourceFile(barFile))
71+
.then((tsFileBar: ts.SourceFile) => {
72+
let contentsBar = dependentFilesUtils.getImportClauses(tsFileBar);
73+
let bazExpectedContent = path.normalize('../foo/baz.component');
74+
expect(contentsBar[0].specifierText).to.equal(bazExpectedContent);
75+
})
76+
.then(() => dependentFilesUtils.createTsSourceFile(fooFile))
77+
.then((tsFileFoo: ts.SourceFile) => {
78+
let contentsFoo = dependentFilesUtils.getImportClauses(tsFileFoo);
79+
let bazExpectedContent = './baz.component'.replace('/', path.sep);
80+
expect(contentsFoo[0].specifierText).to.equal(bazExpectedContent);
81+
})
82+
.then(() => resolver.resolveOwnImports())
83+
.then((changes) => resolver.applySortedChangePromise(changes))
84+
.then(() => dependentFilesUtils.createTsSourceFile(bazFile))
85+
.then((tsFileBaz: ts.SourceFile) => {
86+
let contentsBaz = dependentFilesUtils.getImportClauses(tsFileBaz);
87+
let barExpectedContent = path.normalize('../bar/bar.component');
88+
let fooBarExpectedContent = path.normalize('../foo-baz/qux/quux/foobar/foobar.component');
89+
expect(contentsBaz[0].specifierText).to.equal(barExpectedContent);
90+
expect(contentsBaz[1].specifierText).to.equal(fooBarExpectedContent);
91+
});
92+
});
93+
it('when no files are importing the given file', () => {
94+
let oldFilePath = path.join(rootPath, 'foo-baz/foo-baz.component.ts');
95+
let newFilePath = path.join(rootPath, 'bar');
96+
let resolver = new ModuleResolver(oldFilePath, newFilePath);
97+
return resolver.resolveDependentFiles()
98+
.then((changes) => resolver.applySortedChangePromise(changes))
99+
.then(() => resolver.resolveOwnImports())
100+
.then((changes) => resolver.applySortedChangePromise(changes))
101+
.then(() => dependentFilesUtils.createTsSourceFile(fooBazFile))
102+
.then((tsFile: ts.SourceFile) => {
103+
let contents = dependentFilesUtils.getImportClauses(tsFile);
104+
let fooExpectedContent = path.normalize('../foo/foo.component');
105+
expect(contents[0].specifierText).to.equal(fooExpectedContent);
106+
});
107+
});
108+
it('when oldPath and newPath both do not have index.ts', () => {
109+
let oldFilePath = path.join(rootPath, 'bar/baz/baz.component.ts');
110+
let newFilePath = path.join(rootPath, 'foo-baz');
111+
let resolver = new ModuleResolver(oldFilePath, newFilePath);
112+
return resolver.resolveDependentFiles()
113+
.then((changes) => resolver.applySortedChangePromise(changes))
114+
.then(() => dependentFilesUtils.createTsSourceFile(barFile))
115+
.then((tsFileBar: ts.SourceFile) => {
116+
let contentsBar = dependentFilesUtils.getImportClauses(tsFileBar);
117+
let bazExpectedContent = path.normalize('../foo-baz/baz.component');
118+
expect(contentsBar[0].specifierText).to.equal(bazExpectedContent);
119+
})
120+
.then(() => dependentFilesUtils.createTsSourceFile(fooFile))
121+
.then((tsFileFoo: ts.SourceFile) => {
122+
let contentsFoo = dependentFilesUtils.getImportClauses(tsFileFoo);
123+
let bazExpectedContent = path.normalize('../foo-baz/baz.component');
124+
expect(contentsFoo[0].specifierText).to.equal(bazExpectedContent);
125+
})
126+
.then(() => resolver.resolveOwnImports())
127+
.then((changes) => resolver.applySortedChangePromise(changes))
128+
.then(() => dependentFilesUtils.createTsSourceFile(bazFile))
129+
.then((tsFile: ts.SourceFile) => {
130+
let contentsBaz = dependentFilesUtils.getImportClauses(tsFile);
131+
let barExpectedContent = path.normalize('../bar/bar.component');
132+
let fooBarExpectedContent = `.${path.sep}qux${path.sep}quux${path.sep}foobar${path.sep}foobar.component`;
133+
expect(contentsBaz[0].specifierText).to.equal(barExpectedContent);
134+
expect(contentsBaz[1].specifierText).to.equal(fooBarExpectedContent);
135+
});
136+
});
137+
it('when there are multiple spaces between symbols and specifier', () => {
138+
let oldFilePath = path.join(rootPath, 'foo-baz/qux/quux/foobar/foobar.component.ts');
139+
let newFilePath = path.join(rootPath, 'foo');
140+
let resolver = new ModuleResolver(oldFilePath, newFilePath);
141+
return resolver.resolveDependentFiles()
142+
.then((changes) => resolver.applySortedChangePromise(changes))
143+
.then(() => dependentFilesUtils.createTsSourceFile(fooQuxFile))
144+
.then((tsFileFooQux: ts.SourceFile) => {
145+
let contentsFooQux = dependentFilesUtils.getImportClauses(tsFileFooQux);
146+
let fooQuxExpectedContent = path.normalize('../../../../foo/foobar.component');
147+
expect(contentsFooQux[0].specifierText).to.equal(fooQuxExpectedContent);
148+
})
149+
.then(() => dependentFilesUtils.createTsSourceFile(bazFile))
150+
.then((tsFileBaz: ts.SourceFile) => {
151+
let contentsBaz = dependentFilesUtils.getImportClauses(tsFileBaz);
152+
let bazExpectedContent = path.normalize('../../foo/foobar.component');
153+
expect(contentsBaz[1].specifierText).to.equal(bazExpectedContent);
154+
})
155+
.then(() => resolver.resolveOwnImports())
156+
.then((changes) => resolver.applySortedChangePromise(changes))
157+
.then(() => dependentFilesUtils.createTsSourceFile(fooBarFile))
158+
.then((tsFileFooBar: ts.SourceFile) => {
159+
let contentsFooBar = dependentFilesUtils.getImportClauses(tsFileFooBar);
160+
let fooExpectedContent = `.${path.sep}foo.component`;
161+
let fooQuxExpectedContent = path.normalize('../foo-baz/qux/quux/fooqux.component');
162+
expect(contentsFooBar[0].specifierText).to.equal(fooExpectedContent);
163+
expect(contentsFooBar[1].specifierText).to.equal(fooQuxExpectedContent);
164+
});
165+
});
166+
});
167+
});

0 commit comments

Comments
 (0)