8000 feat: add get-dependent-fils utils · mikebellcoder/angular-cli@6590743 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6590743

Browse files
committed
feat: add get-dependent-fils utils
utility file contains a function getDependentFiles provides a way for files access, AST parse for ModuleSpecifier, map for dependent files.
1 parent c85b14f commit 6590743

File tree

4 files changed

+283
-1
lines changed

4 files changed

+283
-1
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
'use strict';
2+
3+
import * as fs from 'fs';
4+
import * as ts from 'typescript';
5+
import * as glob from 'glob';
6+
import * as path from 'path';
7+
import * as denodeify from 'denodeify';
8+
9+
import { Promise } from 'es6-promise';
10+
11+
/**
12+
* Interface that represents a module specifier and its position in the source file.
13+
* Use for storing a string literal, start position and end posittion of ImportClause node kinds.
14+
*/
15+
export interface ModuleImport {
16+
specifierText: string;
17+
pos: number;
18+
end: number;
19+
};
20+
21+
export interface ModuleMap {
22+
[key: string]: ModuleImport[];
23+
}
24+
25+
/**
26+
* Create a SourceFile as defined by Typescript Compiler API.
27+
* Generate a AST structure from a source file.
28+
*
29+
* @param fileName source file for which AST is to be extracted
30+
*/
31+
export function createTsSourceFile(fileName: string): Promise<ts.SourceFile> {
32+
const readFile = denodeify(fs.readFile);
33+
return readFile(fileName, 'utf8')
34+
.then((contents: string) => {
35+
return ts.createSourceFile(fileName, contents, ts.ScriptTarget.ES6, true);
36+
});
37+
}
38+
39+
/**
40+
* Traverses through AST of a given file of kind 'ts.SourceFile', filters out child
41+
* nodes of the kind 'ts.SyntaxKind.ImportDeclaration' and returns import clauses as
42+
* ModuleImport[]
43+
*
44+
* @param {ts.SourceFile} node: Typescript Node of whose AST is being traversed
45+
*
46+
* @return {ModuleImport[]} traverses through ts.Node and returns an array of moduleSpecifiers.
47+
*/
48+
export function getImportClauses(node: ts.SourceFile): ModuleImport[] {
49+
return node.statements
50+
.filter(node => node.kind === ts.SyntaxKind.ImportDeclaration) // Only Imports.
51+
.map((node: ts.ImportDeclaration) => {
52+
let moduleSpecifier = node.moduleSpecifier;
53+
return {
54+
specifierText: moduleSpecifier.getText().slice(1, -1),
55+
pos: moduleSpecifier.pos,
56+
end: moduleSpecifier.end
57+
};
58+
});
59+
}
60+
61+
/**
62+
* Find the file, 'index.ts' given the directory name and return boolean value
63+
* based on its findings.
64+
*
65+
* @param dirPath
66+
*
67+
* @return a boolean value after it searches for a barrel (index.ts by convention) in a given path
68+
*/
69+
export function hasIndexFile(dirPath: string): Promise<Boolean> {
70+
const globSearch = denodeify(glob);
71+
return globSearch(path.join(dirPath, 'index.ts'), { nodir: true })
72+
.then((indexFile: string[]) => {
73+
return indexFile.length > 0;
74+
});
75+
}
76+
77+
/**
78+
* Returns a map of all dependent file/s' path with their moduleSpecifier object
79+
* (specifierText, pos, end)
80+
*
81+
* @param fileName file upon which other files depend
82+
* @param rootPath root of the project
83+
*
84+
* @return {Promise<ModuleMap>} ModuleMap of all dependent file/s (specifierText, pos, end)
85+
*
86+
*/
87+
export function getDependentFiles(fileName: string, rootPath: string): Promise<ModuleMap> {
88+
const globSearch = denodeify(glob);
89+
return globSearch(path.join(rootPath, '**/*.*.ts'), { nodir: true })
90+
.then((files: string[]) => Promise.all(files.map(file => createTsSourceFile(file)))
91+
.then((tsFiles: ts.SourceFile[]) => tsFiles.map(file => getImportClauses(file)))
92+
.then((moduleSpecifiers: ModuleImport[][]) => {
93+
let allFiles: ModuleMap = {};
94+
files.forEach((file, index) => {
95+
let sourcePath = path.normalize(file);
96+
allFiles[sourcePath] = moduleSpecifiers[index];
97+
});
98+
return allFiles;
99+
})
100+
.then((allFiles: ModuleMap) => {
101+
let relevantFiles: ModuleMap = {};
102+
Object.keys(allFiles).forEach(filePath => {
103+
const tempModuleSpecifiers: ModuleImport[] = allFiles[filePath]
104+
.filter(importClause => {
105+
// Filter only relative imports
106+
let singleSlash = importClause.specifierText.charAt(0) === '/';
107+
let currentDirSyntax = importClause.specifierText.slice(0, 2) === './';
108+
let parentDirSyntax = importClause.specifierText.slice(0, 3) === '../';
109+
return singleSlash || currentDirSyntax || parentDirSyntax;
110+
})
111+
.filter(importClause => {
112+
let modulePath = path.resolve(path.dirname(filePath), importClause.specifierText);
113+
let resolvedFileName = path.resolve(fileName);
114+
let fileBaseName = path.basename(resolvedFileName, '.ts');
115+
let parsedFilePath = path.join(path.dirname(resolvedFileName), fileBaseName);
116+
return (parsedFilePath === modulePath) || (resolvedFileName === modulePath);
117+
});
118+
if (tempModuleSpecifiers.length > 0) {
119+
relevantFiles[filePath] = tempModuleSpecifiers;
120+
};
121+
});
122+
return relevantFiles;
123+
}));
124+
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@
4040
"broccoli-uglify-js": "^0.1.3",
4141
"broccoli-writer": "^0.1.1",
4242
"chalk": "^1.1.3",
43+
"denodeify": "^1.2.1",
4344
"ember-cli": "2.5.0",
4445
"ember-cli-string-utils": "^1.0.0",
46+
"es6-promise": "^3.2.1",
4547
"exit": "^0.1.2",
4648
"fs-extra": "^0.30.0",
4749
"glob": "^7.0.3",
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
'use strict';
2+
3+
// This needs to be first so fs module can be mocked correctly.
4+
let mockFs = require('mock-fs');
5+
import { expect, assert } from 'chai';
6+
import * as path from 'path';
7+
import * as ts from 'typescript';
8+
import * as dependentFilesUtils from '../../addon/ng2/utilities/get-dependent-files';
9+
10+
describe('Get Dependent Files: ', () => {
11+
let rootPath = 'src/app';
12+
13+
beforeEach(() => {
14+
let mockDrive = {
15+
'src/app': {
16+
'foo': {
17+
'foo.component.ts': `import * from '../bar/baz/baz.component'
18+
import * from '../bar/bar.component'`,
19+
'index.ts': `export * from './foo.component'`
20+
},
21+
'bar': {
22+
'baz': {
23+
'baz.component.ts': 'import * from "../bar.component"',
24+
'baz.html': '<h1> Hello </h1>'
25+
},
26+
'bar.component.ts': `import * from './baz/baz.component'
27+
import * from '../foo'`
28+
},
29+
'foo-baz': {
30+
'no-module.component.ts': ''
31+
},
32+
'empty-dir': {}
33+
}
34+
};
35+
mockFs(mockDrive);
36+
});
37+
afterEach(() => {
38+
mockFs.restore();
39+
});
40+
41+
describe('getImportClauses', () => {
42+
it('returns import specifiers when there is a single import statement', () => {
43+
let sourceFile = path.join(rootPath, 'bar/baz/baz.component.ts');
44+
return dependentFilesUtils.createTsSourceFile(sourceFile)
45+
.then((tsFile: ts.SourceFile) => {
46+
let contents = dependentFilesUtils.getImportClauses(tsFile);
47+
let expectedContents = [{
48+
specifierText: '../bar.component',
49+
pos: 13,
50+
end: 32
51+
}];
52+
assert.deepEqual(contents, expectedContents);
53+
});
54+
});
55+
it('returns imports specifiers when there are multiple import statements', () => {
56+
let sourceFile = path.join(rootPath, 'foo/foo.component.ts');
57+
return dependentFilesUtils.createTsSourceFile(sourceFile)
58+
.then((tsFile: ts.SourceFile) => {
59+
let contents = dependentFilesUtils.getImportClauses(tsFile);
60+
let expectedContents = [
61+
{
62+
specifierText: '../bar/baz/baz.component',
63+
pos: 13,
64+
end: 40
65+
},
66+
{
67+
specifierText: '../bar/bar.component',
68+
pos: 85,
69+
end: 108
70+
}
71+
];
72+
assert.deepEqual(contents, expectedContents);
73+
});
74+
});
75+
});
76+
77+
describe('createTsSourceFile', () => {
78+
it('creates ts.SourceFile give a file path', () => {
79+
let sourceFile = path.join(rootPath, 'foo/foo.component.ts');
80+
return dependentFilesUtils.createTsSourceFile(sourceFile)
81+
.then((tsFile: ts.SourceFile) => {
82+
let isTsSourceFile = (tsFile.kind === ts.SyntaxKind.SourceFile);
83+
expect(isTsSourceFile).to.be.true;
84+
});
85+
});
86+
});
87+
88+
describe('hasIndexFile', () => {
89+
it('returns true when there is a index file', () => {
90+
let sourceFile = path.join(rootPath, 'foo');
91+
dependentFilesUtils.hasIndexFile(sourceFile)
92+
.then((booleanValue: boolean) => {
93+
expect(booleanValue).to.be.true;
94+
});
95+
});
96+
it('returns false when there is no index file', () => {
97+
let sourceFile = path.join(rootPath, 'bar');
98+
dependentFilesUtils.hasIndexFile(sourceFile)
99+
.then((booleanValue: boolean) => {
100+
expect(booleanValue).to.be.false;
101+
});
102+
});
103+
});
104+
105+
describe('returns a map of all files which depend on a given file ', () => {
106+
it('when the given component unit has no index file', () => {
107+
let sourceFile = path.join(rootPath, 'bar/bar.component.ts');
108+
return dependentFilesUtils.getDependentFiles(sourceFile, rootPath)
109+
.then((contents: dependentFilesUtils.ModuleMap) => {
110+
let bazFile = path.join(rootPath, 'bar/baz/baz.component.ts');
111+
let fooFile = path.join(rootPath, 'foo/foo.component.ts');
112+
let expectedContents: dependentFilesUtils.ModuleMap = {};
113+
expectedContents[bazFile] = [{
114+
specifierText: '../bar.component',
115+
pos: 13,
< 10000 code>116+
end: 32
117+
}];
118+
expectedContents[fooFile] = [{
119+
specifierText: '../bar/bar.component',
120+
pos: 85,
121+
end: 108
122+
}];
123+
assert.deepEqual(contents, expectedContents);
124+
});
125+
});
126+
it('when the given component unit has no index file [More Test]', () => {
127+
let sourceFile = path.join(rootPath, 'bar/baz/baz.component.ts');
128+
return dependentFilesUtils.getDependentFiles(sourceFile, rootPath)
129+
.then((contents: dependentFilesUtils.ModuleMap) => {
130+
let expectedContents: dependentFilesUtils.ModuleMap = {};
131+
let barFile = path.join(rootPath, 'bar/bar.component.ts');
132+
let fooFile = path.join(rootPath, 'foo/foo.component.ts');
133+
expectedContents[barFile] = [{
134+
specifierText: './baz/baz.component',
135+
pos: 13,
136+
end: 35
137+
}];
138+
expectedContents[fooFile] = [{
139+
specifierText: '../bar/baz/baz.component',
140+
pos: 13,
141+
end: 40
142+
}];
143+
assert.deepEqual(contents, expectedContents);
144+
});
145+
});
146+
it('when there are no dependent files', () => {
147+
let sourceFile = path.join(rootPath, 'foo-baz/no-module.component.ts');
148+
return dependentFilesUtils.getDependentFiles(sourceFile, rootPath)
149+
.then((contents: dependentFilesUtils.ModuleMap) => {
150+
assert.deepEqual(contents, {});
151+
});
152+
});
153+
});
154+
});

typings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
2-
"dependencies": {},
2+
"dependencies": {
3+
"es6-promise": "registry:npm/es6-promise#3.0.0+20160211003958"
4+
},
35
"devDependencies": {
46
"chalk": "github:typings/typed-chalk#a7e422c5455e70292e5675a727d43a7b05fc3e58"
57
},

0 commit comments

Comments
 (0)
0