Skip to content

Commit

Permalink
feat: add module-resolver utils
Browse files Browse the repository at this point in the history
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
ashoktamang committed Aug 1, 2016
1 parent 1945e85 commit b8ddeec
Show file tree
Hide file tree
Showing 2 changed files with 269 additions and 0 deletions.
102 changes: 102 additions & 0 deletions addon/ng2/utilities/module-resolver.ts
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;
});
}
}
167 changes: 167 additions & 0 deletions tests/acceptance/module-resolver.spec.ts
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);
});
});
});
});

0 comments on commit b8ddeec

Please sign in to comment.