forked from angular/angular-cli
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathget-dependent-files.ts
149 lines (139 loc) · 5.43 KB
/
get-dependent-files.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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';
const readFile = <any>denodeify(fs.readFile);
const globSearch = <any>denodeify(glob);
/**
* Interface that represents a module specifier and its position in the source file.
* Use for storing a string literal, start position and end position 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> {
return readFile(fileName, 'utf8')
.then((contents: string) => {
return ts.createSourceFile(fileName, contents, ts.ScriptTarget.Latest, 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> {
return globSearch(path.join(dirPath, 'index.ts'), { nodir: true })
.then((indexFile: string[]) => {
return indexFile.length > 0;
});
}
/**
* Function to get all the templates, stylesheets, and spec files of a given component unit
* Assumption: When any component/service/pipe unit is generated, Angular CLI has a blueprint for
* creating associated files with the name of the generated unit. So, there are two
* assumptions made:
* a. the function only returns associated files that have names matching to the given unit.
* b. the function only looks for the associated files in the directory where the given unit
* exists.
*
* @todo read the metadata to look for the associated files of a given file.
*
* @param fileName
*
* @return absolute paths of '.html/.css/.sass/.spec.ts' files associated with the given file.
*
*/
export function getAllAssociatedFiles(fileName: string): Promise<string[]> {
let fileDirName = path.dirname(fileName);
let componentName = path.basename(fileName, '.ts');
return globSearch(path.join(fileDirName, `${componentName}.*`), { nodir: true })
.then((files: string[]) => {
return files.filter((file) => {
return (path.basename(file) !== 'index.ts');
});
});
}
/**
* 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> {
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;
}));
}