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" } + ] +}