-
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.
utility file contains a function getDependentFiles provides a way for files access, AST parse for ModuleSpecifier, map for dependent files.
- Loading branch information
1 parent
c85b14f
commit 6590743
Showing
4 changed files
with
283 additions
and
1 deletion.
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,124 @@ | ||
'use strict'; | ||
|
||
import * as fs from 'fs'; | ||
import * as ts from 'typescript'; | ||
import * as glob from 'glob'; | ||
import * as path from 'path'; | ||
import * as denodeify from 'denodeify'; | ||
|
||
import { Promise } from 'es6-promise'; | ||
|
||
/** | ||
* Interface that represents a module specifier and its position in the source file. | ||
* Use for storing a string literal, start position and end posittion of ImportClause node kinds. | ||
*/ | ||
export interface ModuleImport { | ||
specifierText: string; | ||
pos: number; | ||
end: number; | ||
}; | ||
|
||
export interface ModuleMap { | ||
[key: string]: ModuleImport[]; | ||
} | ||
|
||
/** | ||
* Create a SourceFile as defined by Typescript Compiler API. | ||
* Generate a AST structure from a source file. | ||
* | ||
* @param fileName source file for which AST is to be extracted | ||
*/ | ||
export function createTsSourceFile(fileName: string): Promise<ts.SourceFile> { | ||
const readFile = denodeify(fs.readFile); | ||
return readFile(fileName, 'utf8') | ||
.then((contents: string) => { | ||
return ts.createSourceFile(fileName, contents, ts.ScriptTarget.ES6, true); | ||
}); | ||
} | ||
|
||
/** | ||
* Traverses through AST of a given file of kind 'ts.SourceFile', filters out child | ||
* nodes of the kind 'ts.SyntaxKind.ImportDeclaration' and returns import clauses as | ||
* ModuleImport[] | ||
* | ||
* @param {ts.SourceFile} node: Typescript Node of whose AST is being traversed | ||
* | ||
* @return {ModuleImport[]} traverses through ts.Node and returns an array of moduleSpecifiers. | ||
*/ | ||
export function getImportClauses(node: ts.SourceFile): ModuleImport[] { | ||
return node.statements | ||
.filter(node => node.kind === ts.SyntaxKind.ImportDeclaration) // Only Imports. | ||
.map((node: ts.ImportDeclaration) => { | ||
let moduleSpecifier = node.moduleSpecifier; | ||
return { | ||
specifierText: moduleSpecifier.getText().slice(1, -1), | ||
pos: moduleSpecifier.pos, | ||
end: moduleSpecifier.end | ||
}; | ||
}); | ||
} | ||
|
||
/** | ||
* Find the file, 'index.ts' given the directory name and return boolean value | ||
* based on its findings. | ||
* | ||
* @param dirPath | ||
* | ||
* @return a boolean value after it searches for a barrel (index.ts by convention) in a given path | ||
*/ | ||
export function hasIndexFile(dirPath: string): Promise<Boolean> { | ||
const globSearch = denodeify(glob); | ||
return globSearch(path.join(dirPath, 'index.ts'), { nodir: true }) | ||
.then((indexFile: string[]) => { | ||
return indexFile.length > 0; | ||
}); | ||
} | ||
|
||
/** | ||
* Returns a map of all dependent file/s' path with their moduleSpecifier object | ||
* (specifierText, pos, end) | ||
* | ||
* @param fileName file upon which other files depend | ||
* @param rootPath root of the project | ||
* | ||
* @return {Promise<ModuleMap>} ModuleMap of all dependent file/s (specifierText, pos, end) | ||
* | ||
*/ | ||
export function getDependentFiles(fileName: string, rootPath: string): Promise<ModuleMap> { | ||
const globSearch = denodeify(glob); | ||
return globSearch(path.join(rootPath, '**/*.*.ts'), { nodir: true }) | ||
.then((files: string[]) => Promise.all(files.map(file => createTsSourceFile(file))) | ||
.then((tsFiles: ts.SourceFile[]) => tsFiles.map(file => getImportClauses(file))) | ||
.then((moduleSpecifiers: ModuleImport[][]) => { | ||
let allFiles: ModuleMap = {}; | ||
files.forEach((file, index) => { | ||
let sourcePath = path.normalize(file); | ||
allFiles[sourcePath] = moduleSpecifiers[index]; | ||
}); | ||
return allFiles; | ||
}) | ||
.then((allFiles: ModuleMap) => { | ||
let relevantFiles: ModuleMap = {}; | ||
Object.keys(allFiles).forEach(filePath => { | ||
const tempModuleSpecifiers: ModuleImport[] = allFiles[filePath] | ||
.filter(importClause => { | ||
// Filter only relative imports | ||
let singleSlash = importClause.specifierText.charAt(0) === '/'; | ||
let currentDirSyntax = importClause.specifierText.slice(0, 2) === './'; | ||
let parentDirSyntax = importClause.specifierText.slice(0, 3) === '../'; | ||
return singleSlash || currentDirSyntax || parentDirSyntax; | ||
}) | ||
.filter(importClause => { | ||
let modulePath = path.resolve(path.dirname(filePath), importClause.specifierText); | ||
let resolvedFileName = path.resolve(fileName); | ||
let fileBaseName = path.basename(resolvedFileName, '.ts'); | ||
let parsedFilePath = path.join(path.dirname(resolvedFileName), fileBaseName); | ||
return (parsedFilePath === modulePath) || (resolvedFileName === modulePath); | ||
}); | ||
if (tempModuleSpecifiers.length > 0) { | ||
relevantFiles[filePath] = tempModuleSpecifiers; | ||
}; | ||
}); | ||
return relevantFiles; | ||
})); | ||
} |
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
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,154 @@ | ||
'use strict'; | ||
|
||
// This needs to be first so fs module can be mocked correctly. | ||
let mockFs = require('mock-fs'); | ||
import { expect, assert } from 'chai'; | ||
import * as path from 'path'; | ||
import * as ts from 'typescript'; | ||
import * as dependentFilesUtils from '../../addon/ng2/utilities/get-dependent-files'; | ||
|
||
describe('Get Dependent Files: ', () => { | ||
let rootPath = 'src/app'; | ||
|
||
beforeEach(() => { | ||
let mockDrive = { | ||
'src/app': { | ||
'foo': { | ||
'foo.component.ts': `import * from '../bar/baz/baz.component' | ||
import * from '../bar/bar.component'`, | ||
'index.ts': `export * from './foo.component'` | ||
}, | ||
'bar': { | ||
'baz': { | ||
'baz.component.ts': 'import * from "../bar.component"', | ||
'baz.html': '<h1> Hello </h1>' | ||
}, | ||
'bar.component.ts': `import * from './baz/baz.component' | ||
import * from '../foo'` | ||
}, | ||
'foo-baz': { | ||
'no-module.component.ts': '' | ||
}, | ||
'empty-dir': {} | ||
} | ||
}; | ||
mockFs(mockDrive); | ||
}); | ||
afterEach(() => { | ||
mockFs.restore(); | ||
}); | ||
|
||
describe('getImportClauses', () => { | ||
it('returns import specifiers when there is a single import statement', () => { | ||
let sourceFile = path.join(rootPath, 'bar/baz/baz.component.ts'); | ||
return dependentFilesUtils.createTsSourceFile(sourceFile) | ||
.then((tsFile: ts.SourceFile) => { | ||
let contents = dependentFilesUtils.getImportClauses(tsFile); | ||
let expectedContents = [{ | ||
specifierText: '../bar.component', | ||
pos: 13, | ||
end: 32 | ||
}]; | ||
assert.deepEqual(contents, expectedContents); | ||
}); | ||
}); | ||
it('returns imports specifiers when there are multiple import statements', () => { | ||
let sourceFile = path.join(rootPath, 'foo/foo.component.ts'); | ||
return dependentFilesUtils.createTsSourceFile(sourceFile) | ||
.then((tsFile: ts.SourceFile) => { | ||
let contents = dependentFilesUtils.getImportClauses(tsFile); | ||
let expectedContents = [ | ||
{ | ||
specifierText: '../bar/baz/baz.component', | ||
pos: 13, | ||
end: 40 | ||
}, | ||
{ | ||
specifierText: '../bar/bar.component', | ||
pos: 85, | ||
end: 108 | ||
} | ||
]; | ||
assert.deepEqual(contents, expectedContents); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('createTsSourceFile', () => { | ||
it('creates ts.SourceFile give a file path', () => { | ||
let sourceFile = path.join(rootPath, 'foo/foo.component.ts'); | ||
return dependentFilesUtils.createTsSourceFile(sourceFile) | ||
.then((tsFile: ts.SourceFile) => { | ||
let isTsSourceFile = (tsFile.kind === ts.SyntaxKind.SourceFile); | ||
expect(isTsSourceFile).to.be.true; | ||
}); | ||
}); | ||
}); | ||
|
||
describe('hasIndexFile', () => { | ||
it('returns true when there is a index file', () => { | ||
let sourceFile = path.join(rootPath, 'foo'); | ||
dependentFilesUtils.hasIndexFile(sourceFile) | ||
.then((booleanValue: boolean) => { | ||
expect(booleanValue).to.be.true; | ||
}); | ||
}); | ||
it('returns false when there is no index file', () => { | ||
let sourceFile = path.join(rootPath, 'bar'); | ||
dependentFilesUtils.hasIndexFile(sourceFile) | ||
.then((booleanValue: boolean) => { | ||
expect(booleanValue).to.be.false; | ||
}); | ||
}); | ||
}); | ||
|
||
describe('returns a map of all files which depend on a given file ', () => { | ||
it('when the given component unit has no index file', () => { | ||
let sourceFile = path.join(rootPath, 'bar/bar.component.ts'); | ||
return dependentFilesUtils.getDependentFiles(sourceFile, rootPath) | ||
.then((contents: dependentFilesUtils.ModuleMap) => { | ||
let bazFile = path.join(rootPath, 'bar/baz/baz.component.ts'); | ||
let fooFile = path.join(rootPath, 'foo/foo.component.ts'); | ||
let expectedContents: dependentFilesUtils.ModuleMap = {}; | ||
expectedContents[bazFile] = [{ | ||
specifierText: '../bar.component', | ||
pos: 13, | ||
end: 32 | ||
}]; | ||
expectedContents[fooFile] = [{ | ||
specifierText: '../bar/bar.component', | ||
pos: 85, | ||
end: 108 | ||
}]; | ||
assert.deepEqual(contents, expectedContents); | ||
}); | ||
}); | ||
it('when the given component unit has no index file [More Test]', () => { | ||
let sourceFile = path.join(rootPath, 'bar/baz/baz.component.ts'); | ||
return dependentFilesUtils.getDependentFiles(sourceFile, rootPath) | ||
.then((contents: dependentFilesUtils.ModuleMap) => { | ||
let expectedContents: dependentFilesUtils.ModuleMap = {}; | ||
let barFile = path.join(rootPath, 'bar/bar.component.ts'); | ||
let fooFile = path.join(rootPath, 'foo/foo.component.ts'); | ||
expectedContents[barFile] = [{ | ||
specifierText: './baz/baz.component', | ||
pos: 13, | ||
end: 35 | ||
}]; | ||
expectedContents[fooFile] = [{ | ||
specifierText: '../bar/baz/baz.component', | ||
pos: 13, | ||
end: 40 | ||
}]; | ||
assert.deepEqual(contents, expectedContents); | ||
}); | ||
}); | ||
it('when there are no dependent files', () => { | ||
let sourceFile = path.join(rootPath, 'foo-baz/no-module.component.ts'); | ||
return dependentFilesUtils.getDependentFiles(sourceFile, rootPath) | ||
.then((contents: dependentFilesUtils.ModuleMap) => { | ||
assert.deepEqual(contents, {}); | ||
}); | ||
}); | ||
}); | ||
}); |
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