diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts
index 6993a3b438c..6b9c0e8710c 100644
--- a/src/compiler/sys.ts
+++ b/src/compiler/sys.ts
@@ -1073,6 +1073,7 @@ namespace ts {
_fs.utimesSync(path, time, time);
}
catch (e) {
+ return;
}
}
@@ -1081,6 +1082,7 @@ namespace ts {
return _fs.unlinkSync(path);
}
catch (e) {
+ return;
}
}
diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts
index b59e6ee4e95..d9c3eb4212d 100644
--- a/src/compiler/tsbuild.ts
+++ b/src/compiler/tsbuild.ts
@@ -7,7 +7,7 @@ namespace ts {
* The primary thing we track here is which files were written to,
* but unchanged, because this enables fast downstream updates
*/
- interface BuildContext {
+ export interface BuildContext {
options: BuildOptions;
/**
* Map from output file name to its pre-build timestamp
@@ -299,6 +299,9 @@ namespace ts {
function parseConfigFile(configFilePath: string) {
const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile;
+ if (sourceFile === undefined) {
+ return undefined;
+ }
const parsed = parseJsonSourceFileConfigFileContent(sourceFile, configParseHost, getDirectoryPath(configFilePath));
parsed.options.configFilePath = configFilePath;
cache.setValue(configFilePath, parsed);
@@ -322,7 +325,7 @@ namespace ts {
return fileExtensionIs(fileName, ".d.ts");
}
- function createBuildContext(options: BuildOptions): BuildContext {
+ export function createBuildContext(options: BuildOptions): BuildContext {
const verboseDiag = options.verbose && createDiagnosticReporter(sys, /*pretty*/ false);
return {
options,
@@ -334,18 +337,15 @@ namespace ts {
};
}
- export function performBuild(args: string[]) {
- const diagReporter = createDiagnosticReporter(sys, /*pretty*/true);
- const host = createCompilerHost({});
-
+ export function performBuild(host: CompilerHost, reportDiagnostic: DiagnosticReporter, args: string[]) {
let verbose = false;
let dry = false;
let force = false;
let clean = false;
const projects: string[] = [];
- for (let i = 0; i < args.length; i++) {
- switch (args[i].toLowerCase()) {
+ for (const arg of args) {
+ switch (arg.toLowerCase()) {
case "-v":
case "--verbose":
verbose = true;
@@ -363,7 +363,7 @@ namespace ts {
continue;
}
// Not a flag, parse as filename
- addProject(args[i]);
+ addProject(arg);
}
if (projects.length === 0) {
@@ -372,7 +372,7 @@ namespace ts {
}
const context = createBuildContext({ verbose, dry, force });
- const builder = createSolutionBuilder(host, context);
+ const builder = createSolutionBuilder(host, reportDiagnostic, context);
if (clean) {
builder.cleanProjects(projects);
}
@@ -384,15 +384,14 @@ namespace ts {
const fileName = resolvePath(host.getCurrentDirectory(), projectSpecification);
const refPath = resolveProjectReferencePath(host, { path: fileName });
if (!host.fileExists(refPath)) {
- diagReporter(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName));
+ reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName));
}
projects.push(refPath);
}
}
- export function createSolutionBuilder(host: CompilerHost, context: BuildContext) {
- const diagReporter = createDiagnosticReporter(sys, /*pretty*/true);
+ export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, context: BuildContext) {
const configFileCache = createConfigFileCache(host);
return {
@@ -418,7 +417,7 @@ namespace ts {
else {
const outputs: string[] = [];
for (const inputFile of project.fileNames) {
- (outputs as string[]).push(...getOutputFileNames(inputFile, project));
+ outputs.push(...getOutputFileNames(inputFile, project));
}
return outputs;
}
@@ -553,7 +552,8 @@ namespace ts {
for (const root of roots) {
const config = configFileCache.parseConfigFile(root);
if (config === undefined) {
- throw new Error(`Could not parse ${root}`);
+ reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, root));
+ continue;
}
enumerateReferences(normalizePath(root), config);
}
@@ -564,7 +564,7 @@ namespace ts {
dependencyMap
};
- function enumerateReferences(fileName: string, root: ts.ParsedCommandLine): void {
+ function enumerateReferences(fileName: string, root: ParsedCommandLine): void {
const myBuildLevel = buildQueue[buildQueuePosition] = buildQueue[buildQueuePosition] || [];
if (myBuildLevel.indexOf(fileName) < 0) {
myBuildLevel.push(fileName);
@@ -605,7 +605,7 @@ namespace ts {
// TODO Accept parsedCommandLine
function buildSingleProject(proj: string) {
if (context.options.dry) {
- diagReporter(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj));
+ reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj));
}
context.verbose(Diagnostics.Building_project_0, proj);
@@ -627,18 +627,18 @@ namespace ts {
const programOptions: CreateProgramOptions = {
projectReferences: configFile.projectReferences,
- host: host,
+ host,
rootNames: configFile.fileNames,
options: configFile.options
};
- const program = ts.createProgram(programOptions);
+ const program = createProgram(programOptions);
// Don't emit anything in the presence of syntactic errors or options diagnostics
const syntaxDiagnostics = [...program.getOptionsDiagnostics(), ...program.getSyntacticDiagnostics()];
if (syntaxDiagnostics.length) {
resultFlags |= BuildResultFlags.SyntaxErrors;
for (const diag of syntaxDiagnostics) {
- diagReporter(diag);
+ reportDiagnostic(diag);
}
return resultFlags;
}
@@ -649,7 +649,7 @@ namespace ts {
if (declDiagnostics.length) {
resultFlags |= BuildResultFlags.DeclarationEmitErrors;
for (const diag of declDiagnostics) {
- diagReporter(diag);
+ reportDiagnostic(diag);
}
return resultFlags;
}
@@ -659,13 +659,13 @@ namespace ts {
if (semanticDiagnostics.length) {
resultFlags |= BuildResultFlags.TypeErrors;
for (const diag of semanticDiagnostics) {
- diagReporter(diag);
+ reportDiagnostic(diag);
}
return resultFlags;
}
let newestDeclarationFileContentChangedTime = minimumDate;
- program.emit(undefined, (fileName, content, writeBom, onError) => {
+ program.emit(/*targetSourceFile*/ undefined, (fileName, content, writeBom, onError) => {
let priorChangeTime: Date | undefined;
if (isDeclarationFile(fileName) && host.fileExists(fileName)) {
@@ -690,7 +690,7 @@ namespace ts {
function updateOutputTimestamps(proj: ParsedCommandLine) {
if (context.options.dry) {
- diagReporter(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj.options.configFilePath));
+ reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj.options.configFilePath));
return;
}
@@ -731,13 +731,29 @@ namespace ts {
}
if (context.options.dry) {
- diagReporter(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, fileReport.map(f => `\r\n * ${f}`).join("")));
+ reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, fileReport.map(f => `\r\n * ${f}`).join("")));
}
}
function buildProjects(configFileNames: string[]) {
+ const resolvedNames: string[] = [];
+ for (const name of configFileNames) {
+ let fullPath = resolvePath(host.getCurrentDirectory(), name);
+ if (host.fileExists(fullPath)) {
+ resolvedNames.push(fullPath);
+ continue;
+ }
+ fullPath = combinePaths(fullPath, "tsconfig.json");
+ if (host.fileExists(fullPath)) {
+ resolvedNames.push(fullPath);
+ continue;
+ }
+ reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath));
+ return;
+ }
+
// Establish what needs to be built
- const graph = createDependencyGraph(configFileNames);
+ const graph = createDependencyGraph(resolvedNames);
const queue = graph.buildQueue;
reportBuildQueue(graph);
diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts
index 51f3db7d616..ba05092c11b 100644
--- a/src/compiler/tsc.ts
+++ b/src/compiler/tsc.ts
@@ -48,9 +48,9 @@ namespace ts {
export function executeCommandLine(args: string[]): void {
if ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b")) {
- return performBuild(args.slice(1));
+ return performBuild(createCompilerHost({}), createDiagnosticReporter(sys), args.slice(1));
}
-
+
const commandLine = parseCommandLine(args);
// Configuration file name (if any)
diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts
index 4fdd30c940e..f5a2861d385 100644
--- a/src/harness/fakes.ts
+++ b/src/harness/fakes.ts
@@ -131,6 +131,10 @@ namespace fakes {
return stats ? stats.mtime : undefined;
}
+ public setModifiedTime(path: string, time: Date) {
+ this.vfs.utimesSync(path, time, time);
+ }
+
public createHash(data: string): string {
return data;
}
@@ -252,6 +256,14 @@ namespace fakes {
return this.sys.directoryExists(directoryName);
}
+ public getModifiedTime(fileName: string) {
+ return this.sys.getModifiedTime(fileName);
+ }
+
+ public setModifiedTime(fileName: string, time: Date) {
+ return this.sys.setModifiedTime(fileName, time);
+ }
+
public getDirectories(path: string): string[] {
return this.sys.getDirectories(path);
}
diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json
index 4a95cd44928..ab857eb520d 100644
--- a/src/harness/tsconfig.json
+++ b/src/harness/tsconfig.json
@@ -52,6 +52,7 @@
"../compiler/builder.ts",
"../compiler/resolutionCache.ts",
"../compiler/watch.ts",
+ "../compiler/tsbuild.ts",
"../compiler/commandLineParser.ts",
"../services/types.ts",
diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts
new file mode 100644
index 00000000000..5bd713f377f
--- /dev/null
+++ b/src/harness/unittests/tsbuild.ts
@@ -0,0 +1,71 @@
+///
+
+namespace ts {
+ let currentTime = 100;
+ const bfs = new vfs.FileSystem(/*ignoreCase*/ false, { time });
+ const lastDiagnostics: Diagnostic[] = [];
+ const reportDiagnostic: DiagnosticReporter = diagnostic => lastDiagnostics.push(diagnostic);
+
+ const sampleRoot = resolvePath(__dirname, "../../tests/projects/sample1");
+ loadFsMirror(bfs, sampleRoot, "/src");
+ bfs.mkdirpSync("/lib");
+ bfs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts")));
+ bfs.meta.set("defaultLibLocation", "/lib");
+ bfs.makeReadonly();
+
+ describe("tsbuild tests", () => {
+ it("builds the referenced project", () => {
+ const fs = bfs.shadow();
+ const host = new fakes.CompilerHost(fs);
+ const builder = createSolutionBuilder(host, reportDiagnostic, createBuildContext({ dry: false, force: false, verbose: false }));
+
+ fs.chdir("/src/tests");
+ fs.debugPrint();
+ builder.buildProjects(["."]);
+ printDiagnostics();
+ fs.debugPrint();
+ assertDiagnosticMessages(Diagnostics.File_0_does_not_exist);
+
+ tick();
+ });
+ });
+
+ function assertDiagnosticMessages(...expected: DiagnosticMessage[]) {
+ const actual = lastDiagnostics.slice();
+ actual.sort((a, b) => b.code - a.code);
+ expected.sort((a, b) => b.code - a.code);
+ if (actual.length !== expected.length) {
+ assert.fail(actual, expected, `Diagnostic arrays did not match - expected ${actual.join(",")}, got ${expected.join(",")}`);
+ }
+ for (let i = 0; i < actual.length; i++) {
+ if (actual[i].code !== expected[i].code) {
+ assert.fail(actual[i].messageText, expected[i].message, "Mismatched error code");
+ }
+ }
+ }
+
+ export function printDiagnostics() {
+ const out = createDiagnosticReporter(sys);
+ for (const d of lastDiagnostics) {
+ out(d);
+ }
+ }
+
+ function tick() {
+ currentTime += 10;
+ }
+ function time() {
+ return currentTime;
+ }
+
+ function loadFsMirror(vfs: vfs.FileSystem, localRoot: string, virtualRoot: string) {
+ vfs.mkdirpSync(virtualRoot);
+ for (const path of Harness.IO.readDirectory(localRoot)) {
+ const file = getBaseFileName(path);
+ vfs.writeFileSync(virtualRoot + "/" + file, Harness.IO.readFile(localRoot + "/" + file));
+ }
+ for (const dir of Harness.IO.getDirectories(localRoot)){
+ loadFsMirror(vfs, localRoot + "/" + dir, virtualRoot + "/" + dir);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts
index 34a48f59091..f05a47ad64c 100644
--- a/src/harness/vfs.ts
+++ b/src/harness/vfs.ts
@@ -5,6 +5,11 @@ namespace vfs {
*/
export const builtFolder = "/.ts";
+ /**
+ * Posix-style path to additional mountable folders (./tests/projects in this repo)
+ */
+ export const projectsFolder = "/.projects";
+
/**
* Posix-style path to additional test libraries
*/
@@ -404,7 +409,18 @@ namespace vfs {
}
/**
- * Get file status.
+ * Change file access times
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public utimesSync(path: string, atime: Date, mtime: Date) {
+ const entry = this._walk(this._resolve(path));
+ entry.node.atimeMs = +atime;
+ entry.node.mtimeMs = +mtime;
+ }
+
+ /**
+ * Get file status. If `path` is a symbolic link, it is dereferenced.
*
* @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/lstat.html
*
@@ -414,6 +430,7 @@ namespace vfs {
return this._stat(this._walk(this._resolve(path), /*noFollow*/ true));
}
+
private _stat(entry: WalkResult) {
const node = entry.node;
if (!node) throw createIOError("ENOENT");
@@ -1282,6 +1299,7 @@ namespace vfs {
files: {
[builtFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "built/local"), resolver),
[testLibFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/lib"), resolver),
+ [projectsFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/projects"), resolver),
[srcFolder]: {}
},
cwd: srcFolder,
diff --git a/tests/projects/sample1/core/index.ts b/tests/projects/sample1/core/index.ts
new file mode 100644
index 00000000000..9ade19f5e2e
--- /dev/null
+++ b/tests/projects/sample1/core/index.ts
@@ -0,0 +1,2 @@
+export function leftPad(s: string, n: number) { return s + n; }
+export function multiply(a: number, b: number) { return a * b; }
diff --git a/tests/projects/sample1/core/tsconfig.json b/tests/projects/sample1/core/tsconfig.json
new file mode 100644
index 00000000000..a514dfe8a03
--- /dev/null
+++ b/tests/projects/sample1/core/tsconfig.json
@@ -0,0 +1,3 @@
+{
+
+}
\ No newline at end of file
diff --git a/tests/projects/sample1/logic/index.ts b/tests/projects/sample1/logic/index.ts
new file mode 100644
index 00000000000..cccadd1718b
--- /dev/null
+++ b/tests/projects/sample1/logic/index.ts
@@ -0,0 +1,4 @@
+import * as c from '../core';
+export function getSecondsInDay() {
+ return c.multiply(10, 15);
+}
diff --git a/tests/projects/sample1/logic/tsconfig.json b/tests/projects/sample1/logic/tsconfig.json
new file mode 100644
index 00000000000..3c2056eec88
--- /dev/null
+++ b/tests/projects/sample1/logic/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "references": [
+ { "path": "../core" }
+ ]
+}
diff --git a/tests/projects/sample1/tests/index.ts b/tests/projects/sample1/tests/index.ts
new file mode 100644
index 00000000000..e9a1ebde3e1
--- /dev/null
+++ b/tests/projects/sample1/tests/index.ts
@@ -0,0 +1,5 @@
+import * as c from '../core';
+import * as logic from '../logic';
+
+c.leftPad("", 10);
+logic.getSecondsInDay();
diff --git a/tests/projects/sample1/tests/tsconfig.json b/tests/projects/sample1/tests/tsconfig.json
new file mode 100644
index 00000000000..dd1bf3bae6b
--- /dev/null
+++ b/tests/projects/sample1/tests/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "references": [
+ { "path": "../core" },
+ { "path": "../logic" }
+ ]
+}
\ No newline at end of file
diff --git a/tests/projects/sample1/ui/index.ts b/tests/projects/sample1/ui/index.ts
new file mode 100644
index 00000000000..9d7e7e3a89e
--- /dev/null
+++ b/tests/projects/sample1/ui/index.ts
@@ -0,0 +1,5 @@
+import * as logic from '../logic';
+
+export function run() {
+ console.log(logic.getSecondsInDay());
+}
diff --git a/tests/projects/sample1/ui/tsconfig.json b/tests/projects/sample1/ui/tsconfig.json
new file mode 100644
index 00000000000..45eff16d4d9
--- /dev/null
+++ b/tests/projects/sample1/ui/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "references": [
+ { "path": "../logic" }
+ ]
+}