Skip to content

Commit aeb5625

Browse files
committed
WIP
1 parent d98a9a0 commit aeb5625

1 file changed

Lines changed: 320 additions & 3 deletions

File tree

src/compiler/tsbuild.ts

Lines changed: 320 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,125 @@
11
namespace ts {
2-
/*
2+
const MinimumDate = new Date(-8640000000000000);
3+
const MaximumDate = new Date(8640000000000000);
4+
5+
/**
6+
* A BuildContext tracks what's going on during the course of a build.
7+
* The primary thing we track here is which files were written to,
8+
* but unchanged, because this enables fast downstream updates
9+
*/
310
interface BuildContext {
4-
unchangedOutputs: FileMap<number>;
11+
/**
12+
* Map from output file name to its pre-build timestamp
13+
*/
14+
unchangedOutputs: FileMap<Date>;
15+
16+
/**
17+
* Map from config file name to up-to-date status
18+
*/
19+
projectStatus: FileMap<UpToDateStatus>;
20+
}
21+
22+
enum BuildResultFlags {
23+
None = 0,
24+
25+
/**
26+
* No errors of any kind occurred during build
27+
*/
28+
Success = 1 << 0,
29+
/**
30+
* None of the .d.ts files emitted by this build were
31+
* different from the existing files on disk
32+
*/
33+
DeclarationOutputUnchanged = 1 << 1,
34+
35+
ConfigFileErrors = 1 << 2,
36+
SyntaxErrors = 1 << 3,
37+
TypeErrors = 1 << 4,
38+
DeclarationEmitErrors = 1 << 5,
39+
40+
AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors
41+
}
42+
43+
enum UpToDateStatusType {
44+
Unbuildable,
45+
UpToDate,
46+
/**
47+
* The project appears out of date because its upstream inputs are newer than its outputs,
48+
* but all of its outputs are actually newer than the previous identical outputs of its inputs.
49+
* This means we can Pseudo-build (just touch timestamps), as if we had actually built this project.
50+
*/
51+
UpToDateWithUpstreamTypes,
52+
OutputMissing,
53+
OutOfDateWithSelf,
54+
OutOfDateWithUpstream,
55+
UpstreamOutOfDate
56+
}
57+
58+
type UpToDateStatus =
59+
| StatusUnbuildable
60+
| StatusUpToDate
61+
| StatusOutputMissing
62+
| StatusOutOfDateWithSelf
63+
| StatusOutOfDateWithUpstream
64+
| StatusUpstreamOutOfDate;
65+
66+
/**
67+
* The project can't be built at all in its current state. For example,
68+
* its config file cannot be parsed, or it has a syntax error or missing file
69+
*/
70+
interface StatusUnbuildable {
71+
type: UpToDateStatusType.Unbuildable;
72+
reason: string;
73+
}
74+
75+
/**
76+
* The project is up to date with respect to its inputs.
77+
* We track what the newest input file is.
78+
*/
79+
interface StatusUpToDate {
80+
type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes;
81+
newestInputFileTime: Date;
82+
newestDeclarationFileContentChangedTime: Date;
83+
newestOutputFileTime: Date;
84+
}
85+
86+
/**
87+
* One or more of the outputs of the project does not exist.
88+
*/
89+
interface StatusOutputMissing {
90+
type: UpToDateStatusType.OutputMissing;
91+
/**
92+
* The name of the first output file that didn't exist
93+
*/
94+
missingOutputFileName: string;
95+
}
96+
97+
/**
98+
* One or more of the project's outputs is older than its newest input.
99+
*/
100+
interface StatusOutOfDateWithSelf {
101+
type: UpToDateStatusType.OutOfDateWithSelf;
102+
outOfDateOutputFileName: string;
103+
newerInputFileName: string;
5104
}
6105

106+
/**
107+
* This project depends on an out-of-date project, so shouldn't be built yet
108+
*/
109+
interface StatusUpstreamOutOfDate {
110+
type: UpToDateStatusType.UpstreamOutOfDate;
111+
upstreamProjectName: string;
112+
}
7113

114+
/**
115+
* One or more of the project's outputs is older than the newest output of
116+
* an upstream project.
117+
*/
118+
interface StatusOutOfDateWithUpstream {
119+
type: UpToDateStatusType.OutOfDateWithUpstream;
120+
outOfDateOutputFileName: string;
121+
newerProjectName: string;
122+
}
8123

9124
interface FileMap<T> {
10125
setValue(fileName: string, value: T): void;
@@ -14,6 +129,9 @@ namespace ts {
14129
tryGetValue(fileName: string): [false, undefined] | [true, T];
15130
}
16131

132+
/**
133+
* A FileMap maintains a normalized-key to value relationship
134+
*/
17135
function createFileMap<T>(): FileMap<T> {
18136
const lookup: { [key: string]: T } = Object.create(null);
19137

@@ -65,5 +183,204 @@ namespace ts {
65183
}
66184
}
67185
}
68-
*/
186+
187+
function getOutputDeclarationFileName(inputFileName: string, configFile: ts.ParsedCommandLine) {
188+
const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, true);
189+
const outputPath = resolvePath(configFile.options.declarationDir || configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath);
190+
return changeExtension(outputPath, ".d.ts");
191+
}
192+
193+
function getOutputJavaScriptFileName(inputFileName: string, configFile: ts.ParsedCommandLine) {
194+
// TODO handle JSX: Preserve
195+
const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, true);
196+
const outputPath = resolvePath(configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath);
197+
return changeExtension(outputPath, (fileExtensionIs(inputFileName, ".tsx") && configFile.options.jsx === JsxEmit.Preserve) ? ".jsx" : ".js");
198+
}
199+
200+
function getOutputFileNames(inputFileName: string, configFile: ts.ParsedCommandLine): ReadonlyArray<string> {
201+
if (configFile.options.outFile) {
202+
return emptyArray;
203+
}
204+
205+
const outputs: string[] = [];
206+
outputs.push(getOutputJavaScriptFileName(inputFileName, configFile));
207+
if (configFile.options.declaration) {
208+
const dts = outputs.push(getOutputDeclarationFileName(inputFileName, configFile));
209+
if (configFile.options.declarationMap) {
210+
outputs.push(dts + ".map");
211+
}
212+
}
213+
return outputs;
214+
}
215+
216+
function getOutFileOutputs(project: ts.ParsedCommandLine): ReadonlyArray<string> {
217+
Debug.assert(!!project.options.outFile, "outFile must be set");
218+
const outputs: string[] = [];
219+
outputs.push(project.options.outFile);
220+
if (project.options.declaration) {
221+
const dts = outputs.push(changeExtension(project.options.outFile, ".d.ts"));
222+
if (project.options.declarationMap) {
223+
outputs.push(dts + ".map");
224+
}
225+
}
226+
return outputs;
227+
}
228+
229+
function rootDirOfOptions(opts: ts.CompilerOptions, configFileName: string) {
230+
return opts.rootDir || path.dirname(configFileName);
231+
}
232+
233+
function createConfigFileCache(host: CompilerHost) {
234+
const cache = createFileMap<ParsedCommandLine>();
235+
const configParseHost = parseConfigHostFromCompilerHost(host);
236+
237+
// TODO: Cache invalidation!
238+
239+
function parseConfigFile(configFilePath: string) {
240+
const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile;
241+
const parsed = parseJsonSourceFileConfigFileContent(sourceFile, configParseHost, configFilePath);
242+
cache.setValue(configFilePath, parsed);
243+
return parsed;
244+
}
245+
246+
return {
247+
parseConfigFile
248+
}
249+
}
250+
251+
function newer(date1: Date, date2: Date): Date {
252+
return date2 > date1 ? date2 : date1;
253+
}
254+
255+
function older(date1: Date, date2: Date): Date {
256+
return date2 < date1 ? date2 : date1;
257+
}
258+
259+
function createSolutionBuilder(host: CompilerHost) {
260+
const configFileCache = createConfigFileCache(host);
261+
262+
function getUpToDateStatus(project: ParsedCommandLine, context: BuildContext): UpToDateStatus {
263+
let newestInputFileName: string = '???';
264+
let newestInputFileTime = MinimumDate;
265+
// Get timestamps of input files
266+
for (const inputFile of project.fileNames) {
267+
if (!host.fileExists(inputFile)) {
268+
return {
269+
type: UpToDateStatusType.Unbuildable,
270+
reason: `${inputFile} does not exist`
271+
};
272+
}
273+
274+
const inputTime = sys.getModifiedTime(inputFile);
275+
if (inputTime > newestInputFileTime) {
276+
newestInputFileName = inputFile;
277+
newestInputFileTime = inputTime;
278+
}
279+
}
280+
281+
// Collect the expected outputs of this project
282+
let outputs: ReadonlyArray<string>;
283+
if (project.options.outFile) {
284+
outputs = getOutFileOutputs(project);
285+
}
286+
else {
287+
outputs = [];
288+
for (const inputFile of project.fileNames) {
289+
(outputs as string[]).push(...getOutputFileNames(inputFile, project));
290+
}
291+
}
292+
293+
// Now see if all outputs are newer than the newest input
294+
let oldestOutputFileName: string = "n/a";
295+
let oldestOutputFileTime: Date = MinimumDate;
296+
let newestOutputFileTime: Date = MaximumDate;
297+
let newestDeclarationFileContentChangedTime: Date = MinimumDate;
298+
for (const output of outputs) {
299+
// Output is missing
300+
if (!host.fileExists(output)) {
301+
return {
302+
type: UpToDateStatusType.OutputMissing,
303+
missingOutputFileName: output
304+
};
305+
}
306+
307+
const outputTime = sys.getModifiedTime(output);
308+
// If an output is older than the newest input, we can stop checking
309+
if (outputTime < newestInputFileTime) {
310+
return {
311+
type: UpToDateStatusType.OutOfDateWithSelf,
312+
outOfDateOutputFileName: output,
313+
newerInputFileName: newestInputFileName
314+
};
315+
}
316+
317+
if (outputTime < oldestOutputFileTime) {
318+
oldestOutputFileTime = outputTime;
319+
oldestOutputFileName = output;
320+
}
321+
newestOutputFileTime = older(newestOutputFileTime, outputTime);
322+
323+
// Keep track of when the most recent time a .d.ts file was changed.
324+
// In addition to file timestamps, we also keep track of when a .d.ts file
325+
// had its file touched but not had its contents changed - this allows us
326+
// to skip a downstream typecheck
327+
if (fileExtensionIs(output, ".d.ts")) {
328+
const unchangedTime = context.unchangedOutputs.getValueOrUndefined(output);
329+
if (unchangedTime !== undefined) {
330+
newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime);
331+
}
332+
else {
333+
newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, sys.getModifiedTime(output));
334+
}
335+
}
336+
}
337+
338+
let pseudoUpToDate = false;
339+
// By here, we know the project is at least up-to-date with its own inputs.
340+
// See if any of its upstream projects are newer than it
341+
for (const ref of project.projectReferences) {
342+
const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(ref.path), context);
343+
344+
// If the upstream project is out of date, then so are we (someone shouldn't have asked, though?)
345+
if (refStatus.type !== UpToDateStatusType.UpToDate) {
346+
return {
347+
type: UpToDateStatusType.UpstreamOutOfDate,
348+
upstreamProjectName: ref.path
349+
}
350+
}
351+
352+
// If the upstream project's newest file is older than our oldest output, we
353+
// can't be out of date because of it
354+
if (refStatus.newestInputFileTime < oldestOutputFileTime) {
355+
continue;
356+
}
357+
358+
// If the upstream project has only change .d.ts files, and we've built
359+
// *after* those files, then we're "psuedo up to date" and eligible for a fast rebuild
360+
if (refStatus.newestDeclarationFileContentChangedTime < oldestOutputFileTime) {
361+
pseudoUpToDate = true;
362+
continue;
363+
}
364+
365+
// We have an output older than an upstream output - we are out of date
366+
return {
367+
type: UpToDateStatusType.OutOfDateWithUpstream,
368+
outOfDateOutputFileName: oldestOutputFileName,
369+
newerProjectName: ref.path
370+
};
371+
}
372+
373+
// Up to date
374+
return {
375+
type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate,
376+
newestDeclarationFileContentChangedTime,
377+
newestInputFileTime,
378+
newestOutputFileTime
379+
};
380+
}
381+
382+
return {
383+
getUpToDateStatus
384+
}
385+
}
69386
}

0 commit comments

Comments
 (0)