Skip to content

Commit

Permalink
feat: add get-dependent-fils utils
Browse files Browse the repository at this point in the history
utility file contains a function getDependentFiles provides a way for
files access, AST parse for ModuleSpecifier, map for dependent files.
  • Loading branch information
ashoktamang committed Jul 18, 2016
1 parent c85b14f commit 6590743
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 1 deletion.
124 changes: 124 additions & 0 deletions addon/ng2/utilities/get-dependent-files.ts
@@ -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;
}));
}
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -40,8 +40,10 @@
"broccoli-uglify-js": "^0.1.3",
"broccoli-writer": "^0.1.1",
"chalk": "^1.1.3",
"denodeify": "^1.2.1",
"ember-cli": "2.5.0",
"ember-cli-string-utils": "^1.0.0",
"es6-promise": "^3.2.1",
"exit": "^0.1.2",
"fs-extra": "^0.30.0",
"glob": "^7.0.3",
Expand Down
154 changes: 154 additions & 0 deletions tests/acceptance/get-dependent-files.spec.ts
@@ -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, {});
});
});
});
});
4 changes: 3 additions & 1 deletion typings.json
@@ -1,5 +1,7 @@
{
"dependencies": {},
"dependencies": {
"es6-promise": "registry:npm/es6-promise#3.0.0+20160211003958"
},
"devDependencies": {
"chalk": "github:typings/typed-chalk#a7e422c5455e70292e5675a727d43a7b05fc3e58"
},
Expand Down

0 comments on commit 6590743

Please sign in to comment.