Skip to content

Commit 1863d3f

Browse files
committed
Graph ordering test WIP
1 parent 8ac795b commit 1863d3f

2 files changed

Lines changed: 143 additions & 98 deletions

File tree

src/compiler/tsbuild.ts

Lines changed: 101 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
namespace ts {
2+
/**
3+
* Branded string for keeping track of when we've turned an ambiguous path
4+
* specified like "./blah" to an absolute path to an actual
5+
* tsconfig file, e.g. "/root/blah/tsconfig.json"
6+
*/
27
type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never };
38

49
const minimumDate = new Date(-8640000000000000);
510
const maximumDate = new Date(8640000000000000);
611

712
/**
813
* A BuildContext tracks what's going on during the course of a build.
9-
* The primary thing we track here is which files were written to,
10-
* but unchanged, because this enables fast downstream updates
14+
*
15+
* Callers may invoke any number of build requests within the same context;
16+
* until the context is reset, each project will only be built at most once.
17+
*
18+
* Example: In a standard setup where project B depends on project A, and both are out of date,
19+
* a failed build of A will result in A remaining out of date. When we try to build
20+
* B, we should immediately bail instead of recomputing A's up-to-date status again.
21+
*
22+
* This also matters for performing fast (i.e. fake) downstream builds of projects
23+
* when their upstream .d.ts files haven't changed content (but have newer timestamps)
1124
*/
1225
export interface BuildContext {
1326
options: BuildOptions;
@@ -21,6 +34,9 @@ namespace ts {
2134
*/
2235
projectStatus: FileMap<UpToDateStatus>;
2336

37+
/**
38+
* Issue a verbose diagnostic message. No-ops when options.verbose is false.
39+
*/
2440
verbose(diag: DiagnosticMessage, ...args: any[]): void;
2541
}
2642

@@ -74,86 +90,86 @@ namespace ts {
7490
}
7591

7692
type UpToDateStatus =
77-
| StatusUnbuildable
78-
| StatusUpToDate
79-
| StatusOutputMissing
80-
| StatusOutOfDateWithSelf
81-
| StatusOutOfDateWithUpstream
82-
| StatusUpstreamOutOfDate
83-
| StatusUpstreamBlocked;
84-
85-
/**
86-
* The project can't be built at all in its current state. For example,
87-
* its config file cannot be parsed, or it has a syntax error or missing file
88-
*/
89-
interface StatusUnbuildable {
90-
type: UpToDateStatusType.Unbuildable;
91-
reason: string;
92-
}
93+
| Status.Unbuildable
94+
| Status.UpToDate
95+
| Status.OutputMissing
96+
| Status.OutOfDateWithSelf
97+
| Status.OutOfDateWithUpstream
98+
| Status.UpstreamOutOfDate
99+
| Status.UpstreamBlocked;
100+
101+
namespace Status {
102+
/**
103+
* The project can't be built at all in its current state. For example,
104+
* its config file cannot be parsed, or it has a syntax error or missing file
105+
*/
106+
export interface Unbuildable {
107+
type: UpToDateStatusType.Unbuildable;
108+
reason: string;
109+
}
93110

94-
/**
95-
* The project is up to date with respect to its inputs.
96-
* We track what the newest input file is.
97-
*/
98-
interface StatusUpToDate {
99-
type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes;
100-
newestInputFileTime: Date;
101-
newestDeclarationFileContentChangedTime: Date;
102-
newestOutputFileTime: Date;
103-
}
111+
/**
112+
* The project is up to date with respect to its inputs.
113+
* We track what the newest input file is.
114+
*/
115+
export interface UpToDate {
116+
type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes;
117+
newestInputFileTime: Date;
118+
newestDeclarationFileContentChangedTime: Date;
119+
newestOutputFileTime: Date;
120+
}
104121

105-
/**
106-
* One or more of the outputs of the project does not exist.
107-
*/
108-
interface StatusOutputMissing {
109-
type: UpToDateStatusType.OutputMissing;
110122
/**
111-
* The name of the first output file that didn't exist
123+
* One or more of the outputs of the project does not exist.
112124
*/
113-
missingOutputFileName: string;
114-
}
125+
export interface OutputMissing {
126+
type: UpToDateStatusType.OutputMissing;
127+
/**
128+
* The name of the first output file that didn't exist
129+
*/
130+
missingOutputFileName: string;
131+
}
115132

116-
/**
117-
* One or more of the project's outputs is older than its newest input.
118-
*/
119-
interface StatusOutOfDateWithSelf {
120-
type: UpToDateStatusType.OutOfDateWithSelf;
121-
outOfDateOutputFileName: string;
122-
newerInputFileName: string;
123-
}
133+
/**
134+
* One or more of the project's outputs is older than its newest input.
135+
*/
136+
export interface OutOfDateWithSelf {
137+
type: UpToDateStatusType.OutOfDateWithSelf;
138+
outOfDateOutputFileName: string;
139+
newerInputFileName: string;
140+
}
124141

125-
/**
126-
* This project depends on an out-of-date project, so shouldn't be built yet
127-
*/
128-
interface StatusUpstreamOutOfDate {
129-
type: UpToDateStatusType.UpstreamOutOfDate;
130-
upstreamProjectName: string;
131-
}
142+
/**
143+
* This project depends on an out-of-date project, so shouldn't be built yet
144+
*/
145+
export interface UpstreamOutOfDate {
146+
type: UpToDateStatusType.UpstreamOutOfDate;
147+
upstreamProjectName: string;
148+
}
132149

133-
/**
134-
* This project depends an upstream project with build errors
135-
*/
136-
interface StatusUpstreamBlocked {
137-
type: UpToDateStatusType.UpstreamBlocked;
138-
upstreamProjectName: string;
139-
}
150+
/**
151+
* This project depends an upstream project with build errors
152+
*/
153+
export interface UpstreamBlocked {
154+
type: UpToDateStatusType.UpstreamBlocked;
155+
upstreamProjectName: string;
156+
}
140157

141-
/**
142-
* One or more of the project's outputs is older than the newest output of
143-
* an upstream project.
144-
*/
145-
interface StatusOutOfDateWithUpstream {
146-
type: UpToDateStatusType.OutOfDateWithUpstream;
147-
outOfDateOutputFileName: string;
148-
newerProjectName: string;
158+
/**
159+
* One or more of the project's outputs is older than the newest output of
160+
* an upstream project.
161+
*/
162+
export interface OutOfDateWithUpstream {
163+
type: UpToDateStatusType.OutOfDateWithUpstream;
164+
outOfDateOutputFileName: string;
165+
newerProjectName: string;
166+
}
149167
}
150168

151169
interface FileMap<T> {
152170
setValue(fileName: string, value: T): void;
153171
getValue(fileName: string): T | never;
154172
getValueOrUndefined(fileName: string): T | undefined;
155-
getValueOrDefault(fileName: string, defaultValue: T): T;
156-
tryGetValue(fileName: string): [false, undefined] | [true, T];
157173
}
158174

159175
/**
@@ -167,8 +183,6 @@ namespace ts {
167183
setValue,
168184
getValue,
169185
getValueOrUndefined,
170-
getValueOrDefault,
171-
tryGetValue
172186
};
173187

174188
function setValue(fileName: string, value: T) {
@@ -194,26 +208,6 @@ namespace ts {
194208
return undefined;
195209
}
196210
}
197-
198-
function getValueOrDefault(fileName: string, defaultValue: T): T {
199-
const f = normalizePath(fileName);
200-
if (f in lookup) {
201-
return lookup[f];
202-
}
203-
else {
204-
return defaultValue;
205-
}
206-
}
207-
208-
function tryGetValue(fileName: string): [false, undefined] | [true, T] {
209-
const f = normalizePath(fileName);
210-
if (f in lookup) {
211-
return [true as true, lookup[f]];
212-
}
213-
else {
214-
return [false as false, undefined];
215-
}
216-
}
217211
}
218212

219213
export function createDependencyMapper() {
@@ -402,13 +396,13 @@ namespace ts {
402396
}
403397
}
404398

405-
export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, options: BuildOptions) {
399+
export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions) {
406400
if (!host.getModifiedTime || !host.setModifiedTime) {
407401
throw new Error("Host must support timestamp APIs");
408402
}
409403

410404
const configFileCache = createConfigFileCache(host);
411-
let context = createBuildContext(options, reportDiagnostic);
405+
let context = createBuildContext(defaultOptions, reportDiagnostic);
412406

413407
return {
414408
getUpToDateStatus,
@@ -418,8 +412,8 @@ namespace ts {
418412
resetBuildContext
419413
};
420414

421-
function resetBuildContext() {
422-
context = createBuildContext(options, reportDiagnostic);
415+
function resetBuildContext(opts = defaultOptions) {
416+
context = createBuildContext(opts, reportDiagnostic);
423417
}
424418

425419
function getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus {
@@ -798,7 +792,7 @@ namespace ts {
798792
}
799793

800794
if (context.options.dry) {
801-
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")));
795+
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")));
802796
}
803797
else {
804798
if (!host.deleteFile) {
@@ -852,7 +846,7 @@ namespace ts {
852846
const projName = proj.options.configFilePath;
853847
if (status.type === UpToDateStatusType.UpToDate && !context.options.force) {
854848
// Up to date, skip
855-
if (options.dry) {
849+
if (defaultOptions.dry) {
856850
// In a dry build, inform the user of this fact
857851
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_0_is_up_to_date, projName));
858852
}
@@ -889,6 +883,9 @@ namespace ts {
889883
}
890884
}
891885

886+
/**
887+
* Report the build ordering inferred from the current project graph if we're in verbose mode
888+
*/
892889
function reportBuildQueue(graph: DependencyGraph) {
893890
if (!context.options.verbose) return;
894891

@@ -902,6 +899,9 @@ namespace ts {
902899
context.verbose(Diagnostics.Sorted_list_of_input_projects_Colon_0, names.map(s => "\r\n * " + s).join(""));
903900
}
904901

902+
/**
903+
* Report the up-to-date status of a project if we're in verbose mode
904+
*/
905905
function reportProjectStatus(configFileName: string, status: UpToDateStatus) {
906906
if (!context.options.verbose) return;
907907
switch (status.type) {
@@ -931,9 +931,12 @@ namespace ts {
931931
case UpToDateStatusType.UpstreamBlocked:
932932
context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName);
933933
return;
934-
934+
case UpToDateStatusType.Unbuildable:
935+
// TODO different error
936+
context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName);
937+
return;
935938
default:
936-
throw new Error(`Invalid build status - ${UpToDateStatusType[status.type]}`);
939+
assertTypeIsNever(status);
937940
}
938941
}
939942
}

src/harness/unittests/tsbuild.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,48 @@ namespace ts {
206206
});
207207
});
208208

209+
describe("tsbuild - graph-ordering", () => {
210+
it("orders the graph correctly", () => {
211+
const fs = new vfs.FileSystem(false);
212+
const host = new fakes.CompilerHost(fs);
213+
const deps: [string, string][] = [
214+
["A", "B"],
215+
["B", "C"],
216+
["A", "C"],
217+
["B", "D"],
218+
["C", "D"],
219+
["C", "E"],
220+
["F", "E"]
221+
];
222+
223+
writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps);
224+
const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false });
225+
builder.buildProjects(["/project/A", "/project/G"]);
226+
printDiagnostics();
227+
});
228+
229+
function writeProjects(fileSystem: vfs.FileSystem, projectNames: string[], deps: [string, string][]): string[] {
230+
const projFileNames: string[] = [];
231+
for (const dep of deps) {
232+
if (projectNames.indexOf(dep[0]) < 0) throw new Error(`Invalid dependency - project ${dep[0]} does not exist`);
233+
if (projectNames.indexOf(dep[1]) < 0) throw new Error(`Invalid dependency - project ${dep[1]} does not exist`);
234+
}
235+
for (const proj of projectNames) {
236+
fileSystem.mkdirpSync(`/project/${proj}`);
237+
fileSystem.writeFileSync(`/project/${proj}/${proj}.ts`, "export {}");
238+
const configFileName = `/project/${proj}/tsconfig.json`;
239+
const configContent = JSON.stringify({
240+
compilerOptions: { composite: true },
241+
files: [`./${proj}.ts`],
242+
references: deps.filter(d => d[0] === proj).map(d => ({ path: `../${d[1]}` }))
243+
}, undefined, 2);
244+
fileSystem.writeFileSync(configFileName, configContent);
245+
projFileNames.push(configFileName);
246+
}
247+
return projFileNames;
248+
}
249+
});
250+
209251
function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) {
210252
if (!fs.statSync(path).isFile()) {
211253
throw new Error(`File ${path} does not exist`);

0 commit comments

Comments
 (0)