-
Notifications
You must be signed in to change notification settings - Fork 12k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
1945e85
commit b8ddeec
Showing
2 changed files
with
269 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
'use strict'; | ||
|
||
import * as path from 'path'; | ||
import * as ts from 'typescript'; | ||
import * as dependentFilesUtils from './get-dependent-files'; | ||
|
||
import { Promise } from 'es6-promise'; | ||
import { Change, ReplaceChange } from './change'; | ||
|
||
// The root directory of Angular Project. | ||
const ROOT_PATH = path.resolve('src/app'); | ||
|
||
/** | ||
* Rewrites import module of dependent files when the file is moved. | ||
* Also, rewrites export module of related index file of the given file. | ||
*/ | ||
export class ModuleResolver { | ||
|
||
constructor(public oldFilePath: string, public newFilePath: string) {} | ||
|
||
/** | ||
* Changes are applied from the bottom of a file to the top. | ||
* An array of Change instances are sorted based upon the order, | ||
* then apply() method is called sequentially. | ||
* | ||
* @param changes {Change []} | ||
* @return Promise after all apply() method of Change class is called | ||
* to all Change instances sequentially. | ||
*/ | ||
applySortedChangePromise(changes: Change[]): Promise<void> { | ||
return changes | ||
.sort((currentChange, nextChange) => nextChange.order - currentChange.order) | ||
.reduce((newChange, change) => newChange.then(() => change.apply()), Promise.resolve()); | ||
} | ||
|
||
/** | ||
* Assesses the import specifier and determines if it is a relative import. | ||
* | ||
* @return {boolean} boolean value if the import specifier is a relative import. | ||
*/ | ||
isRelativeImport(importClause: dependentFilesUtils.ModuleImport): boolean { | ||
let singleSlash = importClause.specifierText.charAt(0) === '/'; | ||
let currentDirSyntax = importClause.specifierText.slice(0, 2) === './'; | ||
let parentDirSyntax = importClause.specifierText.slice(0, 3) === '../'; | ||
return singleSlash || currentDirSyntax || parentDirSyntax; | ||
} | ||
|
||
/** | ||
* Rewrites the import specifiers of all the dependent files (cases for no index file). | ||
* | ||
* @todo Implement the logic for rewriting imports of the dependent files when the file | ||
* being moved has index file in its old path and/or in its new path. | ||
* | ||
* @return {Promise<Change[]>} | ||
*/ | ||
resolveDependentFiles(): Promise<Change[]> { | ||
return dependentFilesUtils.getDependentFiles(this.oldFilePath, ROOT_PATH) | ||
.then((files: dependentFilesUtils.ModuleMap) => { | ||
let changes: Change[] = []; | ||
Object.keys(files).forEach(file => { | ||
let tempChanges: ReplaceChange[] = files[file] | ||
.map(specifier => { | ||
let componentName = path.basename(this.oldFilePath, '.ts'); | ||
let fileDir = path.dirname(file); | ||
let changeText = path.relative(fileDir, path.join(this.newFilePath, componentName)); | ||
if (changeText.length > 0 && changeText.charAt(0) !== '.') { | ||
changeText = `.${path.sep}${changeText}`; | ||
}; | ||
let position = specifier.end - specifier.specifierText.length; | ||
return new ReplaceChange(file, position - 1, specifier.specifierText, changeText); | ||
}); | ||
changes = changes.concat(tempChanges); | ||
}); | ||
return changes; | ||
}); | ||
} | ||
|
||
/** | ||
* Rewrites the file's own relative imports after it has been moved to new path. | ||
* | ||
* @return {Promise<Change[]>} | ||
*/ | ||
resolveOwnImports(): Promise<Change[]> { | ||
return dependentFilesUtils.createTsSourceFile(this.oldFilePath) | ||
.then((tsFile: ts.SourceFile) => dependentFilesUtils.getImportClauses(tsFile)) | ||
.then(moduleSpecifiers => { | ||
let changes: Change[] = moduleSpecifiers | ||
.filter(importClause => this.isRelativeImport(importClause)) | ||
.map(specifier => { | ||
let specifierText = specifier.specifierText; | ||
let moduleAbsolutePath = path.resolve(path.dirname(this.oldFilePath), specifierText); | ||
let changeText = path.relative(this.newFilePath, moduleAbsolutePath); | ||
if (changeText.length > 0 && changeText.charAt(0) !== '.') { | ||
changeText = `.${path.sep}${changeText}`; | ||
} | ||
let position = specifier.end - specifier.specifierText.length; | ||
return new ReplaceChange(this.oldFilePath, position - 1, specifierText, changeText); | ||
}); | ||
return changes; | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
'use strict'; | ||
|
||
const mockFs = require('mock-fs'); | ||
|
||
import * as ts from 'typescript'; | ||
import * as path from 'path'; | ||
import * as dependentFilesUtils from '../../addon/ng2/utilities/get-dependent-files'; | ||
|
||
import { expect } from 'chai'; | ||
import { ModuleResolver } from '../../addon/ng2/utilities/module-resolver'; | ||
|
||
describe('ModuleResolver', () => { | ||
let rootPath = 'src/app'; | ||
|
||
beforeEach(() => { | ||
let mockDrive = { | ||
'src/app': { | ||
'foo': { | ||
'foo.component.ts': `import * from "../bar/baz/baz.component";`, | ||
}, | ||
'bar': { | ||
'baz': { | ||
'baz.component.ts': `import * from "../bar.component" | ||
import * from '../../foo-baz/qux/quux/foobar/foobar.component' | ||
` | ||
}, | ||
'bar.component.ts': `import * from './baz/baz.component' | ||
import * from '../foo/foo.component'`, | ||
}, | ||
'foo-baz': { | ||
'qux': { | ||
'quux': { | ||
'foobar': { | ||
'foobar.component.ts': `import * from "../../../../foo/foo.component" | ||
import * from '../fooqux.component' | ||
`, | ||
}, | ||
'fooqux': { | ||
'fooqux.component.ts': 'import * from "../foobar/foobar.component"' | ||
} | ||
} | ||
}, | ||
'no-module.component.ts': '', | ||
'foo-baz.component.ts': 'import * from \n"../foo/foo.component"\n' | ||
}, | ||
'empty-dir': {} | ||
} | ||
}; | ||
mockFs(mockDrive); | ||
}); | ||
afterEach(() => { | ||
mockFs.restore(); | ||
}); | ||
|
||
describe('Rewrite imports', () => { | ||
// Normalize paths for platform specific delimeter. | ||
let barFile = path.join(rootPath, 'bar/bar.component.ts'); | ||
let fooFile = path.join(rootPath, 'foo/foo.component.ts'); | ||
let bazFile = path.join(rootPath, 'bar/baz/baz.component.ts'); | ||
let fooBazFile = path.join(rootPath, 'foo-baz/foo-baz.component.ts'); | ||
let fooBarFile = path.join(rootPath, 'foo-baz/qux/quux/foobar/foobar.component.ts'); | ||
let fooQuxFile = path.join(rootPath, 'foo-baz/qux/quux/fooqux/fooqux.component.ts'); | ||
|
||
it('when there is no index.ts in oldPath', () => { | ||
let oldFilePath = path.join(rootPath, 'bar/baz/baz.component.ts'); | ||
let newFilePath = path.join(rootPath, 'foo'); | ||
let resolver = new ModuleResolver(oldFilePath, newFilePath); | ||
return resolver.resolveDependentFiles() | ||
.then((changes) => resolver.applySortedChangePromise(changes)) | ||
.then(() => dependentFilesUtils.createTsSourceFile(barFile)) | ||
.then((tsFileBar: ts.SourceFile) => { | ||
let contentsBar = dependentFilesUtils.getImportClauses(tsFileBar); | ||
let bazExpectedContent = path.normalize('../foo/baz.component'); | ||
expect(contentsBar[0].specifierText).to.equal(bazExpectedContent); | ||
}) | ||
.then(() => dependentFilesUtils.createTsSourceFile(fooFile)) | ||
.then((tsFileFoo: ts.SourceFile) => { | ||
let contentsFoo = dependentFilesUtils.getImportClauses(tsFileFoo); | ||
let bazExpectedContent = './baz.component'.replace('/', path.sep); | ||
expect(contentsFoo[0].specifierText).to.equal(bazExpectedContent); | ||
}) | ||
.then(() => resolver.resolveOwnImports()) | ||
.then((changes) => resolver.applySortedChangePromise(changes)) | ||
.then(() => dependentFilesUtils.createTsSourceFile(bazFile)) | ||
.then((tsFileBaz: ts.SourceFile) => { | ||
let contentsBaz = dependentFilesUtils.getImportClauses(tsFileBaz); | ||
let barExpectedContent = path.normalize('../bar/bar.component'); | ||
let fooBarExpectedContent = path.normalize('../foo-baz/qux/quux/foobar/foobar.component'); | ||
expect(contentsBaz[0].specifierText).to.equal(barExpectedContent); | ||
expect(contentsBaz[1].specifierText).to.equal(fooBarExpectedContent); | ||
}); | ||
}); | ||
it('when no files are importing the given file', () => { | ||
let oldFilePath = path.join(rootPath, 'foo-baz/foo-baz.component.ts'); | ||
let newFilePath = path.join(rootPath, 'bar'); | ||
let resolver = new ModuleResolver(oldFilePath, newFilePath); | ||
return resolver.resolveDependentFiles() | ||
.then((changes) => resolver.applySortedChangePromise(changes)) | ||
.then(() => resolver.resolveOwnImports()) | ||
.then((changes) => resolver.applySortedChangePromise(changes)) | ||
.then(() => dependentFilesUtils.createTsSourceFile(fooBazFile)) | ||
.then((tsFile: ts.SourceFile) => { | ||
let contents = dependentFilesUtils.getImportClauses(tsFile); | ||
let fooExpectedContent = path.normalize('../foo/foo.component'); | ||
expect(contents[0].specifierText).to.equal(fooExpectedContent); | ||
}); | ||
}); | ||
it('when oldPath and newPath both do not have index.ts', () => { | ||
let oldFilePath = path.join(rootPath, 'bar/baz/baz.component.ts'); | ||
let newFilePath = path.join(rootPath, 'foo-baz'); | ||
let resolver = new ModuleResolver(oldFilePath, newFilePath); | ||
return resolver.resolveDependentFiles() | ||
.then((changes) => resolver.applySortedChangePromise(changes)) | ||
.then(() => dependentFilesUtils.createTsSourceFile(barFile)) | ||
.then((tsFileBar: ts.SourceFile) => { | ||
let contentsBar = dependentFilesUtils.getImportClauses(tsFileBar); | ||
let bazExpectedContent = path.normalize('../foo-baz/baz.component'); | ||
expect(contentsBar[0].specifierText).to.equal(bazExpectedContent); | ||
}) | ||
.then(() => dependentFilesUtils.createTsSourceFile(fooFile)) | ||
.then((tsFileFoo: ts.SourceFile) => { | ||
let contentsFoo = dependentFilesUtils.getImportClauses(tsFileFoo); | ||
let bazExpectedContent = path.normalize('../foo-baz/baz.component'); | ||
expect(contentsFoo[0].specifierText).to.equal(bazExpectedContent); | ||
}) | ||
.then(() => resolver.resolveOwnImports()) | ||
.then((changes) => resolver.applySortedChangePromise(changes)) | ||
.then(() => dependentFilesUtils.createTsSourceFile(bazFile)) | ||
.then((tsFile: ts.SourceFile) => { | ||
let contentsBaz = dependentFilesUtils.getImportClauses(tsFile); | ||
let barExpectedContent = path.normalize('../bar/bar.component'); | ||
let fooBarExpectedContent = `.${path.sep}qux${path.sep}quux${path.sep}foobar${path.sep}foobar.component`; | ||
expect(contentsBaz[0].specifierText).to.equal(barExpectedContent); | ||
expect(contentsBaz[1].specifierText).to.equal(fooBarExpectedContent); | ||
}); | ||
}); | ||
it('when there are multiple spaces between symbols and specifier', () => { | ||
let oldFilePath = path.join(rootPath, 'foo-baz/qux/quux/foobar/foobar.component.ts'); | ||
let newFilePath = path.join(rootPath, 'foo'); | ||
let resolver = new ModuleResolver(oldFilePath, newFilePath); | ||
return resolver.resolveDependentFiles() | ||
.then((changes) => resolver.applySortedChangePromise(changes)) | ||
.then(() => dependentFilesUtils.createTsSourceFile(fooQuxFile)) | ||
.then((tsFileFooQux: ts.SourceFile) => { | ||
let contentsFooQux = dependentFilesUtils.getImportClauses(tsFileFooQux); | ||
let fooQuxExpectedContent = path.normalize('../../../../foo/foobar.component'); | ||
expect(contentsFooQux[0].specifierText).to.equal(fooQuxExpectedContent); | ||
}) | ||
.then(() => dependentFilesUtils.createTsSourceFile(bazFile)) | ||
.then((tsFileBaz: ts.SourceFile) => { | ||
let contentsBaz = dependentFilesUtils.getImportClauses(tsFileBaz); | ||
let bazExpectedContent = path.normalize('../../foo/foobar.component'); | ||
expect(contentsBaz[1].specifierText).to.equal(bazExpectedContent); | ||
}) | ||
.then(() => resolver.resolveOwnImports()) | ||
.then((changes) => resolver.applySortedChangePromise(changes)) | ||
.then(() => dependentFilesUtils.createTsSourceFile(fooBarFile)) | ||
.then((tsFileFooBar: ts.SourceFile) => { | ||
let contentsFooBar = dependentFilesUtils.getImportClauses(tsFileFooBar); | ||
let fooExpectedContent = `.${path.sep}foo.component`; | ||
let fooQuxExpectedContent = path.normalize('../foo-baz/qux/quux/fooqux.component'); | ||
expect(contentsFooBar[0].specifierText).to.equal(fooExpectedContent); | ||
expect(contentsFooBar[1].specifierText).to.equal(fooQuxExpectedContent); | ||
}); | ||
}); | ||
}); | ||
}); |