forked from microsoft/TypeScript
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathbuilder.ts
370 lines (314 loc) · 15.2 KB
/
builder.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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
/// <reference path="..\compiler\commandLineParser.ts" />
/// <reference path="..\services\services.ts" />
/// <reference path="session.ts" />
/// <reference types="node" />
namespace ts.server {
export function shouldEmitFile(scriptInfo: ScriptInfo) {
return !scriptInfo.hasMixedContent;
}
/**
* An abstract file info that maintains a shape signature.
*/
export class BuilderFileInfo {
private lastCheckedShapeSignature: string;
constructor(public readonly scriptInfo: ScriptInfo, public readonly project: Project) {
}
public isExternalModuleOrHasOnlyAmbientExternalModules() {
const sourceFile = this.getSourceFile();
return isExternalModule(sourceFile) || this.containsOnlyAmbientModules(sourceFile);
}
/**
* For script files that contains only ambient external modules, although they are not actually external module files,
* they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore,
* there are no point to rebuild all script files if these special files have changed. However, if any statement
* in the file is not ambient external module, we treat it as a regular script file.
*/
private containsOnlyAmbientModules(sourceFile: SourceFile) {
for (const statement of sourceFile.statements) {
if (statement.kind !== SyntaxKind.ModuleDeclaration || (<ModuleDeclaration>statement).name.kind !== SyntaxKind.StringLiteral) {
return false;
}
}
return true;
}
private computeHash(text: string): string {
return this.project.projectService.host.createHash(text);
}
private getSourceFile(): SourceFile {
return this.project.getSourceFile(this.scriptInfo.path);
}
/**
* @return {boolean} indicates if the shape signature has changed since last update.
*/
public updateShapeSignature() {
const sourceFile = this.getSourceFile();
if (!sourceFile) {
return true;
}
const lastSignature = this.lastCheckedShapeSignature;
if (sourceFile.isDeclarationFile) {
this.lastCheckedShapeSignature = this.computeHash(sourceFile.text);
}
else {
const emitOutput = this.project.getFileEmitOutput(this.scriptInfo, /*emitOnlyDtsFiles*/ true);
if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) {
this.lastCheckedShapeSignature = this.computeHash(emitOutput.outputFiles[0].text);
}
}
return !lastSignature || this.lastCheckedShapeSignature !== lastSignature;
}
}
export interface Builder {
readonly project: Project;
getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
onProjectUpdateGraph(): void;
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean;
}
abstract class AbstractBuilder<T extends BuilderFileInfo> implements Builder {
private fileInfos = createFileMap<T>();
constructor(public readonly project: Project, private ctor: { new (scriptInfo: ScriptInfo, project: Project): T }) {
}
protected getFileInfo(path: Path): T {
return this.fileInfos.get(path);
}
protected getOrCreateFileInfo(path: Path): T {
let fileInfo = this.getFileInfo(path);
if (!fileInfo) {
const scriptInfo = this.project.getScriptInfo(path);
fileInfo = new this.ctor(scriptInfo, this.project);
this.setFileInfo(path, fileInfo);
}
return fileInfo;
}
protected getFileInfoPaths(): Path[] {
return this.fileInfos.getKeys();
}
protected setFileInfo(path: Path, info: T) {
this.fileInfos.set(path, info);
}
protected removeFileInfo(path: Path) {
this.fileInfos.remove(path);
}
protected forEachFileInfo(action: (fileInfo: T) => any) {
this.fileInfos.forEachValue((_path, value) => action(value));
}
abstract getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
abstract onProjectUpdateGraph(): void;
/**
* @returns {boolean} whether the emit was conducted or not
*/
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean {
const fileInfo = this.getFileInfo(scriptInfo.path);
if (!fileInfo) {
return false;
}
const { emitSkipped, outputFiles } = this.project.getFileEmitOutput(fileInfo.scriptInfo, /*emitOnlyDtsFiles*/ false);
if (!emitSkipped) {
const projectRootPath = this.project.getProjectRootPath();
for (const outputFile of outputFiles) {
const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName));
writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark);
}
}
return !emitSkipped;
}
}
class NonModuleBuilder extends AbstractBuilder<BuilderFileInfo> {
constructor(public readonly project: Project) {
super(project, BuilderFileInfo);
}
onProjectUpdateGraph() {
}
/**
* Note: didn't use path as parameter because the returned file names will be directly
* consumed by the API user, which will use it to interact with file systems. Path
* should only be used internally, because the case sensitivity is not trustable.
*/
getFilesAffectedBy(scriptInfo: ScriptInfo): string[] {
const info = this.getOrCreateFileInfo(scriptInfo.path);
const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName];
if (info.updateShapeSignature()) {
const options = this.project.getCompilerOptions();
// If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project,
// so returning the file itself is good enough.
if (options && (options.out || options.outFile)) {
return singleFileResult;
}
return this.project.getAllEmittableFiles();
}
return singleFileResult;
}
}
class ModuleBuilderFileInfo extends BuilderFileInfo {
references: ModuleBuilderFileInfo[] = [];
referencedBy: ModuleBuilderFileInfo[] = [];
scriptVersionForReferences: string;
static compareFileInfos(lf: ModuleBuilderFileInfo, rf: ModuleBuilderFileInfo): number {
const l = lf.scriptInfo.fileName;
const r = rf.scriptInfo.fileName;
return (l < r ? -1 : (l > r ? 1 : 0));
};
static addToReferenceList(array: ModuleBuilderFileInfo[], fileInfo: ModuleBuilderFileInfo) {
if (array.length === 0) {
array.push(fileInfo);
return;
}
const insertIndex = binarySearch(array, fileInfo, ModuleBuilderFileInfo.compareFileInfos);
if (insertIndex < 0) {
array.splice(~insertIndex, 0, fileInfo);
}
}
static removeFromReferenceList(array: ModuleBuilderFileInfo[], fileInfo: ModuleBuilderFileInfo) {
if (!array || array.length === 0) {
return;
}
if (array[0] === fileInfo) {
array.splice(0, 1);
return;
}
const removeIndex = binarySearch(array, fileInfo, ModuleBuilderFileInfo.compareFileInfos);
if (removeIndex >= 0) {
array.splice(removeIndex, 1);
}
}
addReferencedBy(fileInfo: ModuleBuilderFileInfo): void {
ModuleBuilderFileInfo.addToReferenceList(this.referencedBy, fileInfo);
}
removeReferencedBy(fileInfo: ModuleBuilderFileInfo): void {
ModuleBuilderFileInfo.removeFromReferenceList(this.referencedBy, fileInfo);
}
removeFileReferences() {
for (const reference of this.references) {
reference.removeReferencedBy(this);
}
this.references = [];
}
}
class ModuleBuilder extends AbstractBuilder<ModuleBuilderFileInfo> {
constructor(public readonly project: Project) {
super(project, ModuleBuilderFileInfo);
}
private projectVersionForDependencyGraph: string;
private getReferencedFileInfos(fileInfo: ModuleBuilderFileInfo): ModuleBuilderFileInfo[] {
if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) {
return [];
}
const referencedFilePaths = this.project.getReferencedFiles(fileInfo.scriptInfo.path);
if (referencedFilePaths.length > 0) {
return map(referencedFilePaths, f => this.getOrCreateFileInfo(f)).sort(ModuleBuilderFileInfo.compareFileInfos);
}
return [];
}
onProjectUpdateGraph() {
this.ensureProjectDependencyGraphUpToDate();
}
private ensureProjectDependencyGraphUpToDate() {
if (!this.projectVersionForDependencyGraph || this.project.getProjectVersion() !== this.projectVersionForDependencyGraph) {
const currentScriptInfos = this.project.getScriptInfos();
for (const scriptInfo of currentScriptInfos) {
const fileInfo = this.getOrCreateFileInfo(scriptInfo.path);
this.updateFileReferences(fileInfo);
}
this.forEachFileInfo(fileInfo => {
if (!this.project.containsScriptInfo(fileInfo.scriptInfo)) {
// This file was deleted from this project
fileInfo.removeFileReferences();
this.removeFileInfo(fileInfo.scriptInfo.path);
}
});
this.projectVersionForDependencyGraph = this.project.getProjectVersion();
}
}
private updateFileReferences(fileInfo: ModuleBuilderFileInfo) {
// Only need to update if the content of the file changed.
if (fileInfo.scriptVersionForReferences === fileInfo.scriptInfo.getLatestVersion()) {
return;
}
const newReferences = this.getReferencedFileInfos(fileInfo);
const oldReferences = fileInfo.references;
let oldIndex = 0;
let newIndex = 0;
while (oldIndex < oldReferences.length && newIndex < newReferences.length) {
const oldReference = oldReferences[oldIndex];
const newReference = newReferences[newIndex];
const compare = ModuleBuilderFileInfo.compareFileInfos(oldReference, newReference);
if (compare < 0) {
// New reference is greater then current reference. That means
// the current reference doesn't exist anymore after parsing. So delete
// references.
oldReference.removeReferencedBy(fileInfo);
oldIndex++;
}
else if (compare > 0) {
// A new reference info. Add it.
newReference.addReferencedBy(fileInfo);
newIndex++;
}
else {
// Equal. Go to next
oldIndex++;
newIndex++;
}
}
// Clean old references
for (let i = oldIndex; i < oldReferences.length; i++) {
oldReferences[i].removeReferencedBy(fileInfo);
}
// Update new references
for (let i = newIndex; i < newReferences.length; i++) {
newReferences[i].addReferencedBy(fileInfo);
}
fileInfo.references = newReferences;
fileInfo.scriptVersionForReferences = fileInfo.scriptInfo.getLatestVersion();
}
getFilesAffectedBy(scriptInfo: ScriptInfo): string[] {
this.ensureProjectDependencyGraphUpToDate();
const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName];
const fileInfo = this.getFileInfo(scriptInfo.path);
if (!fileInfo || !fileInfo.updateShapeSignature()) {
return singleFileResult;
}
if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) {
return this.project.getAllEmittableFiles();
}
const options = this.project.getCompilerOptions();
if (options && (options.isolatedModules || options.out || options.outFile)) {
return singleFileResult;
}
// Now we need to if each file in the referencedBy list has a shape change as well.
// Because if so, its own referencedBy files need to be saved as well to make the
// emitting result consistent with files on disk.
// Use slice to clone the array to avoid manipulating in place
const queue = fileInfo.referencedBy.slice(0);
const fileNameSet = createMap<ScriptInfo>();
fileNameSet[scriptInfo.fileName] = scriptInfo;
while (queue.length > 0) {
const processingFileInfo = queue.pop();
if (processingFileInfo.updateShapeSignature() && processingFileInfo.referencedBy.length > 0) {
for (const potentialFileInfo of processingFileInfo.referencedBy) {
if (!fileNameSet[potentialFileInfo.scriptInfo.fileName]) {
queue.push(potentialFileInfo);
}
}
}
fileNameSet[processingFileInfo.scriptInfo.fileName] = processingFileInfo.scriptInfo;
}
const result: string[] = [];
for (const fileName in fileNameSet) {
if (shouldEmitFile(fileNameSet[fileName])) {
result.push(fileName);
}
}
return result;
}
}
export function createBuilder(project: Project): Builder {
const moduleKind = project.getCompilerOptions().module;
switch (moduleKind) {
case ModuleKind.None:
return new NonModuleBuilder(project);
default:
return new ModuleBuilder(project);
}
}
}