diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index b9149d76d7a..d7760d1f75f 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -109,6 +109,14 @@ namespace ts { paramType: Diagnostics.FILE_OR_DIRECTORY, description: Diagnostics.Compile_the_project_given_the_path_to_its_configuration_file_or_to_a_folder_with_a_tsconfig_json, }, + { + name: "build", + type: "boolean", + shortName: "b", + showInSimplifiedHelpView: true, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Build_one_or_more_projects_and_their_dependencies_if_out_of_date + }, { name: "pretty", type: "boolean", @@ -968,6 +976,125 @@ namespace ts { } + function getDiagnosticText(_message: DiagnosticMessage, ..._args: any[]): string { + const diagnostic = createCompilerDiagnostic.apply(undefined, arguments); + return diagnostic.messageText; + } + + /* @internal */ + export function printVersion() { + sys.write(getDiagnosticText(Diagnostics.Version_0, version) + sys.newLine); + } + + /* @internal */ + export function printHelp(optionsList: CommandLineOption[], syntaxPrefix = "") { + const output: string[] = []; + + // We want to align our "syntax" and "examples" commands to a certain margin. + const syntaxLength = getDiagnosticText(Diagnostics.Syntax_Colon_0, "").length; + const examplesLength = getDiagnosticText(Diagnostics.Examples_Colon_0, "").length; + let marginLength = Math.max(syntaxLength, examplesLength); + + // Build up the syntactic skeleton. + let syntax = makePadding(marginLength - syntaxLength); + syntax += `tsc ${syntaxPrefix}[${getDiagnosticText(Diagnostics.options)}] [${getDiagnosticText(Diagnostics.file)}...]`; + + output.push(getDiagnosticText(Diagnostics.Syntax_Colon_0, syntax)); + output.push(sys.newLine + sys.newLine); + + // Build up the list of examples. + const padding = makePadding(marginLength); + output.push(getDiagnosticText(Diagnostics.Examples_Colon_0, makePadding(marginLength - examplesLength) + "tsc hello.ts") + sys.newLine); + output.push(padding + "tsc --outFile file.js file.ts" + sys.newLine); + output.push(padding + "tsc @args.txt" + sys.newLine); + output.push(padding + "tsc --build tsconfig.json" + sys.newLine); + output.push(sys.newLine); + + output.push(getDiagnosticText(Diagnostics.Options_Colon) + sys.newLine); + + // We want our descriptions to align at the same column in our output, + // so we keep track of the longest option usage string. + marginLength = 0; + const usageColumn: string[] = []; // Things like "-d, --declaration" go in here. + const descriptionColumn: string[] = []; + + const optionsDescriptionMap = createMap(); // Map between option.description and list of option.type if it is a kind + + for (const option of optionsList) { + // If an option lacks a description, + // it is not officially supported. + if (!option.description) { + continue; + } + + let usageText = " "; + if (option.shortName) { + usageText += "-" + option.shortName; + usageText += getParamType(option); + usageText += ", "; + } + + usageText += "--" + option.name; + usageText += getParamType(option); + + usageColumn.push(usageText); + let description: string; + + if (option.name === "lib") { + description = getDiagnosticText(option.description); + const element = (option).element; + const typeMap = >element.type; + optionsDescriptionMap.set(description, arrayFrom(typeMap.keys()).map(key => `'${key}'`)); + } + else { + description = getDiagnosticText(option.description); + } + + descriptionColumn.push(description); + + // Set the new margin for the description column if necessary. + marginLength = Math.max(usageText.length, marginLength); + } + + // Special case that can't fit in the loop. + const usageText = " @<" + getDiagnosticText(Diagnostics.file) + ">"; + usageColumn.push(usageText); + descriptionColumn.push(getDiagnosticText(Diagnostics.Insert_command_line_options_and_files_from_a_file)); + marginLength = Math.max(usageText.length, marginLength); + + // Print out each row, aligning all the descriptions on the same column. + for (let i = 0; i < usageColumn.length; i++) { + const usage = usageColumn[i]; + const description = descriptionColumn[i]; + const kindsList = optionsDescriptionMap.get(description); + output.push(usage + makePadding(marginLength - usage.length + 2) + description + sys.newLine); + + if (kindsList) { + output.push(makePadding(marginLength + 4)); + for (const kind of kindsList) { + output.push(kind + " "); + } + output.push(sys.newLine); + } + } + + for (const line of output) { + sys.write(line); + } + return; + + function getParamType(option: CommandLineOption) { + if (option.paramType !== undefined) { + return " " + getDiagnosticText(option.paramType); + } + return ""; + } + + function makePadding(paddingLength: number): string { + return Array(paddingLength + 1).join(" "); + } + } + export type DiagnosticReporter = (diagnostic: Diagnostic) => void; /** * Reports config file diagnostics diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index f2a10a08fde..668f3b5a6f0 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3620,6 +3620,95 @@ "category": "Error", "code": 6309 }, + "Project '{0}' is out of date because oldest output '{1}' is older than newest input '{2}'": { + "category": "Message", + "code": 6350 + }, + "Project '{0}' is up to date because newest input '{1}' is older than oldest output '{2}'": { + "category": "Message", + "code": 6351 + }, + "Project '{0}' is out of date because output file '{1}' does not exist": { + "category": "Message", + "code": 6352 + }, + "Project '{0}' is out of date because its dependency '{1}' is out of date": { + "category": "Message", + "code": 6353 + }, + + "Project '{0}' is up to date with .d.ts files from its dependencies": { + "category": "Message", + "code": 6354 + }, + "Projects in this build: {0}": { + "category": "Message", + "code": 6355 + }, + "A non-dry build would delete the following files: {0}": { + "category": "Message", + "code": 6356 + }, + "A non-dry build would build project '{0}'": { + "category": "Message", + "code": 6357 + }, + "Building project '{0}'...": { + "category": "Message", + "code": 6358 + }, + "Updating output timestamps of project '{0}'...": { + "category": "Message", + "code": 6359 + }, + "delete this - Project '{0}' is up to date because it was previously built": { + "category": "Message", + "code": 6360 + }, + "Project '{0}' is up to date": { + "category": "Message", + "code": 6361 + }, + "Skipping build of project '{0}' because its dependency '{1}' has errors": { + "category": "Message", + "code": 6362 + }, + "Project '{0}' can't be built because its dependency '{1}' has errors": { + "category": "Message", + "code": 6363 + }, + "Build one or more projects and their dependencies, if out of date": { + "category": "Message", + "code": 6364 + }, + "Delete the outputs of all projects": { + "category": "Message", + "code": 6365 + }, + "Enable verbose logging": { + "category": "Message", + "code": 6366 + }, + "Show what would be built (or deleted, if specified with '--clean')": { + "category": "Message", + "code": 6367 + }, + "Build all projects, including those that appear to be up to date": { + "category": "Message", + "code": 6368 + }, + "Option '--build' must be the first command line argument.": { + "category": "Error", + "code": 6369 + }, + "Options '{0}' and '{1}' cannot be combined.": { + "category": "Error", + "code": 6370 + }, + "Skipping clean because not all projects could be located": { + "category": "Error", + "code": 6371 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 90ec1c793a3..7269462578e 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1026,7 +1026,7 @@ namespace ts { // SyntaxKind.UnparsedSource function emitUnparsedSource(unparsed: UnparsedSource) { - write(unparsed.text); + writer.rawWrite(unparsed.text); } // diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 2e776134a72..1152e409e75 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -2587,16 +2587,19 @@ namespace ts { return node; } - export function createUnparsedSourceFile(text: string): UnparsedSource { + export function createUnparsedSourceFile(text: string, map?: string): UnparsedSource { const node = createNode(SyntaxKind.UnparsedSource); node.text = text; + node.sourceMapText = map; return node; } - export function createInputFiles(javascript: string, declaration: string): InputFiles { + export function createInputFiles(javascript: string, declaration: string, javascriptMapText?: string, declarationMapText?: string): InputFiles { const node = createNode(SyntaxKind.InputFiles); node.javascriptText = javascript; + node.javascriptMapText = javascriptMapText; node.declarationText = declaration; + node.declarationMapText = declarationMapText; return node; } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index cb6e1347868..b08f966864a 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -189,7 +189,10 @@ namespace ts { getEnvironmentVariable: name => sys.getEnvironmentVariable ? sys.getEnvironmentVariable(name) : "", getDirectories: (path: string) => sys.getDirectories(path), realpath, - readDirectory: (path, extensions, include, exclude, depth) => sys.readDirectory(path, extensions, include, exclude, depth) + readDirectory: (path, extensions, include, exclude, depth) => sys.readDirectory(path, extensions, include, exclude, depth), + getModifiedTime: sys.getModifiedTime && (path => sys.getModifiedTime!(path)), + setModifiedTime: sys.setModifiedTime && ((path, date) => sys.setModifiedTime!(path, date)), + deleteFile: sys.deleteFile && (path => sys.deleteFile!(path)) }; } @@ -615,25 +618,27 @@ namespace ts { // A parallel array to projectReferences storing the results of reading in the referenced tsconfig files const resolvedProjectReferences: (ResolvedProjectReference | undefined)[] | undefined = projectReferences ? [] : undefined; const projectReferenceRedirects: Map = createMap(); - if (projectReferences) { - for (const ref of projectReferences) { - const parsedRef = parseProjectReferenceConfigFile(ref); - resolvedProjectReferences!.push(parsedRef); - if (parsedRef) { - if (parsedRef.commandLine.options.outFile) { - const dtsOutfile = changeExtension(parsedRef.commandLine.options.outFile, ".d.ts"); - processSourceFile(dtsOutfile, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); - } - addProjectReferenceRedirects(parsedRef.commandLine, projectReferenceRedirects); - } - } - } const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); const structuralIsReused = tryReuseStructureFromOldProgram(); if (structuralIsReused !== StructureIsReused.Completely) { processingDefaultLibFiles = []; processingOtherFiles = []; + + if (projectReferences) { + for (const ref of projectReferences) { + const parsedRef = parseProjectReferenceConfigFile(ref); + resolvedProjectReferences!.push(parsedRef); + if (parsedRef) { + if (parsedRef.commandLine.options.outFile) { + const dtsOutfile = changeExtension(parsedRef.commandLine.options.outFile, ".d.ts"); + processSourceFile(dtsOutfile, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + } + addProjectReferenceRedirects(parsedRef.commandLine, projectReferenceRedirects); + } + } + } + forEach(rootNames, name => processRootFile(name, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false)); // load type declarations specified via 'types' argument or implicitly from types/ and node_modules/@types folders @@ -1021,7 +1026,7 @@ namespace ts { for (const oldSourceFile of oldSourceFiles) { let newSourceFile = host.getSourceFileByPath - ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile) + ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.resolvedPath || oldSourceFile.path, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile) : host.getSourceFile(oldSourceFile.fileName, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile); // TODO: GH#18217 if (!newSourceFile) { @@ -1234,8 +1239,10 @@ namespace ts { const dtsFilename = changeExtension(resolvedRefOpts.options.outFile, ".d.ts"); const js = host.readFile(resolvedRefOpts.options.outFile) || `/* Input file ${resolvedRefOpts.options.outFile} was missing */\r\n`; + const jsMap = host.readFile(resolvedRefOpts.options.outFile + ".map"); // TODO: try to read sourceMappingUrl comment from the js file const dts = host.readFile(dtsFilename) || `/* Input file ${dtsFilename} was missing */\r\n`; - const node = createInputFiles(js, dts); + const dtsMap = host.readFile(dtsFilename + ".map"); + const node = createInputFiles(js, dts, jsMap, dtsMap); nodes.push(node); } } @@ -2047,6 +2054,7 @@ namespace ts { if (file) { sourceFilesFoundSearchingNodeModules.set(path, currentNodeModulesDepth > 0); file.path = path; + file.resolvedPath = toPath(fileName); if (host.useCaseSensitiveFileNames()) { const pathLowerCase = path.toLowerCase(); @@ -2781,7 +2789,7 @@ namespace ts { /** * Returns the target config filename of a project reference */ - function resolveProjectReferencePath(host: CompilerHost, ref: ProjectReference): string | undefined { + export function resolveProjectReferencePath(host: CompilerHost, ref: ProjectReference): string | undefined { if (!host.fileExists(ref.path)) { return combinePaths(ref.path, "tsconfig.json"); } diff --git a/src/compiler/sourcemap.ts b/src/compiler/sourcemap.ts index 9229e2daa0e..8ef71ecf955 100644 --- a/src/compiler/sourcemap.ts +++ b/src/compiler/sourcemap.ts @@ -99,6 +99,10 @@ namespace ts { let sourceMapDataList: SourceMapData[] | undefined; let disabled: boolean = !(compilerOptions.sourceMap || compilerOptions.inlineSourceMap); + let completedSections: SourceMapSectionDefinition[]; + let sectionStartLine: number; + let sectionStartColumn: number; + return { initialize, reset, @@ -146,6 +150,9 @@ namespace ts { lastEncodedNameIndex = 0; // Initialize source map data + completedSections = []; + sectionStartLine = 1; + sectionStartColumn = 1; sourceMapData = { sourceMapFilePath, jsSourceMappingURL: !compilerOptions.inlineSourceMap ? getBaseFileName(normalizeSlashes(sourceMapFilePath)) : undefined!, // TODO: GH#18217 @@ -214,6 +221,65 @@ namespace ts { lastEncodedNameIndex = undefined; sourceMapData = undefined!; sourceMapDataList = undefined!; + completedSections = undefined!; + sectionStartLine = undefined!; + sectionStartColumn = undefined!; + } + + interface SourceMapSection { + version: 3; + file: string; + sourceRoot?: string; + sources: string[]; + names?: string[]; + mappings: string; + sourcesContent?: string[]; + sections?: undefined; + } + + type SourceMapSectionDefinition = + | { offset: { line: number, column: number }, url: string } // Included for completeness + | { offset: { line: number, column: number }, map: SourceMap }; + + interface SectionalSourceMap { + version: 3; + file: string; + sections: SourceMapSectionDefinition[]; + } + + type SourceMap = SectionalSourceMap | SourceMapSection; + + function captureSection(): SourceMapSection { + return { + version: 3, + file: sourceMapData.sourceMapFile, + sourceRoot: sourceMapData.sourceMapSourceRoot, + sources: sourceMapData.sourceMapSources, + names: sourceMapData.sourceMapNames, + mappings: sourceMapData.sourceMapMappings, + sourcesContent: sourceMapData.sourceMapSourcesContent, + }; + } + + function resetSectionalData(): void { + sourceMapData.sourceMapSources = []; + sourceMapData.sourceMapNames = []; + sourceMapData.sourceMapMappings = ""; + sourceMapData.sourceMapSourcesContent = compilerOptions.inlineSources ? [] : undefined; + } + + function generateMap(): SourceMap { + if (completedSections.length) { + captureSectionalSpanIfNeeded(/*reset*/ false); + return { + version: 3, + file: sourceMapData.sourceMapFile, + sections: completedSections + }; + } + else { + return captureSection(); + } } // Encoding for sourcemap span @@ -284,8 +350,8 @@ namespace ts { sourceLinePos.line++; sourceLinePos.character++; - const emittedLine = writer.getLine(); - const emittedColumn = writer.getColumn(); + const emittedLine = writer.getLine() - sectionStartLine + 1; + const emittedColumn = emittedLine === 0 ? (writer.getColumn() - sectionStartColumn + 1) : writer.getColumn(); // If this location wasn't recorded or the location in source is going backwards, record the span if (!lastRecordedSourceMapSpan || @@ -320,6 +386,15 @@ namespace ts { } } + function captureSectionalSpanIfNeeded(reset: boolean) { + if (lastRecordedSourceMapSpan && lastRecordedSourceMapSpan === lastEncodedSourceMapSpan) { // If we've recorded some spans, save them + completedSections.push({ offset: { line: sectionStartLine - 1, column: sectionStartColumn - 1 }, map: captureSection() }); + if (reset) { + resetSectionalData(); + } + } + } + /** * Emits a node with possible leading and trailing source maps. * @@ -333,6 +408,35 @@ namespace ts { } if (node) { + if (isUnparsedSource(node) && node.sourceMapText !== undefined) { + captureSectionalSpanIfNeeded(/*reset*/ true); + const text = node.sourceMapText; + let parsed: {} | undefined; + try { + parsed = JSON.parse(text); + } + catch { + // empty + } + const offset = { line: writer.getLine() - 1, column: writer.getColumn() - 1 }; + completedSections.push(parsed + ? { + offset, + map: parsed as SourceMap + } + : { + offset, + // This is just passes the buck on sourcemaps we don't really understand, instead of issuing an error (which would be difficult this late) + url: `data:application/json;charset=utf-8;base64,${base64encode(sys, text)}` + } + ); + const emitResult = emitCallback(hint, node); + sectionStartLine = writer.getLine(); + sectionStartColumn = writer.getColumn(); + lastRecordedSourceMapSpan = undefined!; + lastEncodedSourceMapSpan = defaultLastEncodedSourceMapSpan; + return emitResult; + } const emitNode = node.emitNode; const emitFlags = emitNode && emitNode.flags || EmitFlags.None; const range = emitNode && emitNode.sourceMapRange; @@ -460,15 +564,7 @@ namespace ts { encodeLastRecordedSourceMapSpan(); - return JSON.stringify({ - version: 3, - file: sourceMapData.sourceMapFile, - sourceRoot: sourceMapData.sourceMapSourceRoot, - sources: sourceMapData.sourceMapSources, - names: sourceMapData.sourceMapNames, - mappings: sourceMapData.sourceMapMappings, - sourcesContent: sourceMapData.sourceMapSourcesContent, - }); + return JSON.stringify(generateMap()); } /** diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 91e5a48dc05..ee6c9edc3bd 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -433,6 +433,7 @@ namespace ts { readFile(path: string, encoding?: string): string | undefined; getFileSize?(path: string): number; writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; + /** * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that * use native OS file watching @@ -448,6 +449,8 @@ namespace ts { getDirectories(path: string): string[]; readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; getModifiedTime?(path: string): Date; + setModifiedTime?(path: string, time: Date): void; + deleteFile?(path: string): void; /** * A good implementation is node.js' `crypto.createHash`. (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) */ @@ -592,6 +595,8 @@ namespace ts { }, readDirectory, getModifiedTime, + setModifiedTime, + deleteFile, createHash: _crypto ? createMD5HashUsingNativeCrypto : generateDjb2Hash, createSHA256Hash: _crypto ? createSHA256Hash : undefined, getMemoryUsage() { @@ -1069,6 +1074,24 @@ namespace ts { } } + function setModifiedTime(path: string, time: Date) { + try { + _fs.utimesSync(path, time, time); + } + catch (e) { + return; + } + } + + function deleteFile(path: string) { + try { + return _fs.unlinkSync(path); + } + catch (e) { + return; + } + } + /** * djb2 hashing algorithm * http://www.cse.yorku.ca/~oz/hash.html diff --git a/src/compiler/transformers/declarations.ts b/src/compiler/transformers/declarations.ts index 007162ff13c..38ddd0067f0 100644 --- a/src/compiler/transformers/declarations.ts +++ b/src/compiler/transformers/declarations.ts @@ -180,7 +180,7 @@ namespace ts { } ), mapDefined(node.prepends, prepend => { if (prepend.kind === SyntaxKind.InputFiles) { - return createUnparsedSourceFile(prepend.declarationText); + return createUnparsedSourceFile(prepend.declarationText, prepend.declarationMapText); } })); bundle.syntheticFileReferences = []; diff --git a/src/compiler/transformers/ts.ts b/src/compiler/transformers/ts.ts index 0d29ac43ecf..b4421506357 100644 --- a/src/compiler/transformers/ts.ts +++ b/src/compiler/transformers/ts.ts @@ -100,7 +100,7 @@ namespace ts { function transformBundle(node: Bundle) { return createBundle(node.sourceFiles.map(transformSourceFile), mapDefined(node.prepends, prepend => { if (prepend.kind === SyntaxKind.InputFiles) { - return createUnparsedSourceFile(prepend.javascriptText); + return createUnparsedSourceFile(prepend.javascriptText, prepend.javascriptMapText); } return prepend; })); diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts new file mode 100644 index 00000000000..79306cc5d06 --- /dev/null +++ b/src/compiler/tsbuild.ts @@ -0,0 +1,1212 @@ +namespace ts { + /** + * Branded string for keeping track of when we've turned an ambiguous path + * specified like "./blah" to an absolute path to an actual + * tsconfig file, e.g. "/root/blah/tsconfig.json" + */ + export type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never }; + + const minimumDate = new Date(-8640000000000000); + const maximumDate = new Date(8640000000000000); + + export interface BuildHost { + verbose(diag: DiagnosticMessage, ...args: string[]): void; + error(diag: DiagnosticMessage, ...args: string[]): void; + errorDiagnostic(diag: Diagnostic): void; + message(diag: DiagnosticMessage, ...args: string[]): void; + } + + /** + * A BuildContext tracks what's going on during the course of a build. + * + * Callers may invoke any number of build requests within the same context; + * until the context is reset, each project will only be built at most once. + * + * Example: In a standard setup where project B depends on project A, and both are out of date, + * a failed build of A will result in A remaining out of date. When we try to build + * B, we should immediately bail instead of recomputing A's up-to-date status again. + * + * This also matters for performing fast (i.e. fake) downstream builds of projects + * when their upstream .d.ts files haven't changed content (but have newer timestamps) + */ + export interface BuildContext { + options: BuildOptions; + /** + * Map from output file name to its pre-build timestamp + */ + unchangedOutputs: FileMap; + + /** + * Map from config file name to up-to-date status + */ + projectStatus: FileMap; + + invalidatedProjects: FileMap; + queuedProjects: FileMap; + missingRoots: Map; + } + + type Mapper = ReturnType; + interface DependencyGraph { + buildQueue: ResolvedConfigFileName[]; + dependencyMap: Mapper; + } + + interface BuildOptions { + dry: boolean; + force: boolean; + verbose: boolean; + } + + enum BuildResultFlags { + None = 0, + + /** + * No errors of any kind occurred during build + */ + Success = 1 << 0, + /** + * None of the .d.ts files emitted by this build were + * different from the existing files on disk + */ + DeclarationOutputUnchanged = 1 << 1, + + ConfigFileErrors = 1 << 2, + SyntaxErrors = 1 << 3, + TypeErrors = 1 << 4, + DeclarationEmitErrors = 1 << 5, + + AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors + } + + export enum UpToDateStatusType { + Unbuildable, + UpToDate, + /** + * The project appears out of date because its upstream inputs are newer than its outputs, + * but all of its outputs are actually newer than the previous identical outputs of its (.d.ts) inputs. + * This means we can Pseudo-build (just touch timestamps), as if we had actually built this project. + */ + UpToDateWithUpstreamTypes, + OutputMissing, + OutOfDateWithSelf, + OutOfDateWithUpstream, + UpstreamOutOfDate, + UpstreamBlocked, + + /** + * Projects with no outputs (i.e. "solution" files) + */ + ContainerOnly + } + + export type UpToDateStatus = + | Status.Unbuildable + | Status.UpToDate + | Status.OutputMissing + | Status.OutOfDateWithSelf + | Status.OutOfDateWithUpstream + | Status.UpstreamOutOfDate + | Status.UpstreamBlocked + | Status.ContainerOnly; + + export namespace Status { + /** + * The project can't be built at all in its current state. For example, + * its config file cannot be parsed, or it has a syntax error or missing file + */ + export interface Unbuildable { + type: UpToDateStatusType.Unbuildable; + reason: string; + } + + /** + * This project doesn't have any outputs, so "is it up to date" is a meaningless question. + */ + export interface ContainerOnly { + type: UpToDateStatusType.ContainerOnly; + } + + /** + * The project is up to date with respect to its inputs. + * We track what the newest input file is. + */ + export interface UpToDate { + type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes; + newestInputFileTime: Date; + newestInputFileName: string; + newestDeclarationFileContentChangedTime: Date; + newestOutputFileTime: Date; + newestOutputFileName: string; + oldestOutputFileName: string; + } + + /** + * One or more of the outputs of the project does not exist. + */ + export interface OutputMissing { + type: UpToDateStatusType.OutputMissing; + /** + * The name of the first output file that didn't exist + */ + missingOutputFileName: string; + } + + /** + * One or more of the project's outputs is older than its newest input. + */ + export interface OutOfDateWithSelf { + type: UpToDateStatusType.OutOfDateWithSelf; + outOfDateOutputFileName: string; + newerInputFileName: string; + } + + /** + * This project depends on an out-of-date project, so shouldn't be built yet + */ + export interface UpstreamOutOfDate { + type: UpToDateStatusType.UpstreamOutOfDate; + upstreamProjectName: string; + } + + /** + * This project depends an upstream project with build errors + */ + export interface UpstreamBlocked { + type: UpToDateStatusType.UpstreamBlocked; + upstreamProjectName: string; + } + + /** + * One or more of the project's outputs is older than the newest output of + * an upstream project. + */ + export interface OutOfDateWithUpstream { + type: UpToDateStatusType.OutOfDateWithUpstream; + outOfDateOutputFileName: string; + newerProjectName: string; + } + } + + interface FileMap { + setValue(fileName: string, value: T): void; + getValue(fileName: string): T | never; + getValueOrUndefined(fileName: string): T | undefined; + hasKey(fileName: string): boolean; + removeKey(fileName: string): void; + getKeys(): string[]; + } + + /** + * A FileMap maintains a normalized-key to value relationship + */ + function createFileMap(): FileMap { + // tslint:disable-next-line:no-null-keyword + const lookup = createMap(); + + return { + setValue, + getValue, + getValueOrUndefined, + removeKey, + getKeys, + hasKey + }; + + function getKeys(): string[] { + return Object.keys(lookup); + } + + function hasKey(fileName: string) { + return lookup.has(normalizePath(fileName)); + } + + function removeKey(fileName: string) { + lookup.delete(normalizePath(fileName)); + } + + function setValue(fileName: string, value: T) { + lookup.set(normalizePath(fileName), value); + } + + function getValue(fileName: string): T | never { + const f = normalizePath(fileName); + if (lookup.has(f)) { + return lookup.get(f)!; + } + else { + throw new Error(`No value corresponding to ${fileName} exists in this map`); + } + } + + function getValueOrUndefined(fileName: string): T | undefined { + const f = normalizePath(fileName); + return lookup.get(f); + } + } + + export function createDependencyMapper() { + const childToParents = createFileMap(); + const parentToChildren = createFileMap(); + const allKeys = createFileMap(); + + function addReference(childConfigFileName: ResolvedConfigFileName, parentConfigFileName: ResolvedConfigFileName): void { + addEntry(childToParents, childConfigFileName, parentConfigFileName); + addEntry(parentToChildren, parentConfigFileName, childConfigFileName); + } + + function getReferencesTo(parentConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] { + return parentToChildren.getValueOrUndefined(parentConfigFileName) || []; + } + + function getReferencesOf(childConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] { + return childToParents.getValueOrUndefined(childConfigFileName) || []; + } + + function getKeys(): ReadonlyArray { + return allKeys.getKeys() as ResolvedConfigFileName[]; + } + + function addEntry(mapToAddTo: typeof childToParents | typeof parentToChildren, key: ResolvedConfigFileName, element: ResolvedConfigFileName) { + key = normalizePath(key) as ResolvedConfigFileName; + element = normalizePath(element) as ResolvedConfigFileName; + let arr = mapToAddTo.getValueOrUndefined(key); + if (arr === undefined) { + mapToAddTo.setValue(key, arr = []); + } + if (arr.indexOf(element) < 0) { + arr.push(element); + } + allKeys.setValue(key, true); + allKeys.setValue(element, true); + } + + return { + addReference, + getReferencesTo, + getReferencesOf, + getKeys + }; + } + + function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine) { + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath!), inputFileName, /*ignoreCase*/ true); + const outputPath = resolvePath(configFile.options.declarationDir || configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath!), relativePath); + return changeExtension(outputPath, Extension.Dts); + } + + function getOutputJavaScriptFileName(inputFileName: string, configFile: ParsedCommandLine) { + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath!), inputFileName, /*ignoreCase*/ true); + const outputPath = resolvePath(configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath!), relativePath); + return changeExtension(outputPath, (fileExtensionIs(inputFileName, Extension.Tsx) && configFile.options.jsx === JsxEmit.Preserve) ? Extension.Jsx : Extension.Js); + } + + function getOutputFileNames(inputFileName: string, configFile: ParsedCommandLine): ReadonlyArray { + if (configFile.options.outFile) { + return emptyArray; + } + + const outputs: string[] = []; + outputs.push(getOutputJavaScriptFileName(inputFileName, configFile)); + if (configFile.options.declaration) { + const dts = outputs.push(getOutputDeclarationFileName(inputFileName, configFile)); + if (configFile.options.declarationMap) { + outputs.push(dts + ".map"); + } + } + return outputs; + } + + function getOutFileOutputs(project: ParsedCommandLine): ReadonlyArray { + if (!project.options.outFile) { + return Debug.fail("outFile must be set"); + } + const outputs: string[] = []; + outputs.push(project.options.outFile); + if (project.options.declaration) { + const dts = changeExtension(project.options.outFile, Extension.Dts); + outputs.push(dts); + if (project.options.declarationMap) { + outputs.push(dts + ".map"); + } + } + return outputs; + } + + function rootDirOfOptions(opts: CompilerOptions, configFileName: string) { + return opts.rootDir || getDirectoryPath(configFileName); + } + + function createConfigFileCache(host: CompilerHost) { + const cache = createFileMap(); + const configParseHost = parseConfigHostFromCompilerHost(host); + + function parseConfigFile(configFilePath: ResolvedConfigFileName) { + 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); + return parsed; + } + + function removeKey(configFilePath: ResolvedConfigFileName) { + cache.removeKey(configFilePath); + } + + return { + parseConfigFile, + removeKey + }; + } + + function newer(date1: Date, date2: Date): Date { + return date2 > date1 ? date2 : date1; + } + + function isDeclarationFile(fileName: string) { + return fileExtensionIs(fileName, Extension.Dts); + } + + export function createBuildContext(options: BuildOptions): BuildContext { + const invalidatedProjects = createFileMap(); + const queuedProjects = createFileMap(); + const missingRoots = createMap(); + + return { + options, + projectStatus: createFileMap(), + unchangedOutputs: createFileMap(), + invalidatedProjects, + missingRoots, + queuedProjects + }; + } + + const buildOpts: CommandLineOption[] = [ + { + name: "verbose", + shortName: "v", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Enable_verbose_logging, + type: "boolean" + }, + { + name: "dry", + shortName: "d", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Show_what_would_be_built_or_deleted_if_specified_with_clean, + type: "boolean" + }, + { + name: "force", + shortName: "f", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Build_all_projects_including_those_that_appear_to_be_up_to_date, + type: "boolean" + }, + { + name: "clean", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Delete_the_outputs_of_all_projects, + type: "boolean" + }, + { + name: "watch", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Watch_input_files, + type: "boolean" + } + ]; + + export function performBuild(args: string[], compilerHost: CompilerHost, buildHost: BuildHost, system?: System) { + let verbose = false; + let dry = false; + let force = false; + let clean = false; + let watch = false; + + const projects: string[] = []; + for (const arg of args) { + switch (arg.toLowerCase()) { + case "-v": + case "--verbose": + verbose = true; + continue; + case "-d": + case "--dry": + dry = true; + continue; + case "-f": + case "--force": + force = true; + continue; + case "--clean": + clean = true; + continue; + case "--watch": + case "-w": + watch = true; + continue; + + case "--?": + case "-?": + case "--help": + return printHelp(buildOpts, "--build "); + } + // Not a flag, parse as filename + addProject(arg); + } + + // Nonsensical combinations + if (clean && force) { + return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force"); + } + if (clean && verbose) { + return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose"); + } + if (clean && watch) { + return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch"); + } + if (watch && dry) { + return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry"); + } + + if (projects.length === 0) { + // tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ." + addProject("."); + } + + const builder = createSolutionBuilder(compilerHost, buildHost, projects, { dry, force, verbose }, system); + if (clean) { + builder.cleanAllProjects(); + } + else { + builder.buildAllProjects(); + } + + if (watch) { + return builder.startWatching(); + } + + function addProject(projectSpecification: string) { + const fileName = resolvePath(compilerHost.getCurrentDirectory(), projectSpecification); + const refPath = resolveProjectReferencePath(compilerHost, { path: fileName }); + if (!refPath) { + return buildHost.error(Diagnostics.File_0_does_not_exist, projectSpecification); + } + + if (!compilerHost.fileExists(refPath)) { + return buildHost.error(Diagnostics.File_0_does_not_exist, fileName); + } + projects.push(refPath); + + } + } + + /** + * A SolutionBuilder has an immutable set of rootNames that are the "entry point" projects, but + * can dynamically add/remove other projects based on changes on the rootNames' references + */ + export function createSolutionBuilder(compilerHost: CompilerHost, buildHost: BuildHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions, system?: System) { + if (!compilerHost.getModifiedTime || !compilerHost.setModifiedTime) { + throw new Error("Host must support timestamp APIs"); + } + + const configFileCache = createConfigFileCache(compilerHost); + let context = createBuildContext(defaultOptions); + + const existingWatchersForWildcards = createMap(); + + return { + buildAllProjects, + getUpToDateStatus, + getUpToDateStatusOfFile, + cleanAllProjects, + resetBuildContext, + getBuildGraph, + + invalidateProject, + buildInvalidatedProjects, + buildDependentInvalidatedProjects, + + resolveProjectName, + + startWatching + }; + + function startWatching() { + if (!system) throw new Error("System host must be provided if using --watch"); + if (!system.watchFile || !system.watchDirectory || !system.setTimeout) throw new Error("System host must support watchFile / watchDirectory / setTimeout if using --watch"); + + const graph = getGlobalDependencyGraph()!; + if (!graph.buildQueue) { + // Everything is broken - we don't even know what to watch. Give up. + return; + } + + for (const resolved of graph.buildQueue) { + const cfg = configFileCache.parseConfigFile(resolved); + if (cfg) { + // Watch this file + system.watchFile(resolved, () => { + configFileCache.removeKey(resolved); + invalidateProjectAndScheduleBuilds(resolved); + }); + + // Update watchers for wildcard directories + if (cfg.configFileSpecs) { + updateWatchingWildcardDirectories(existingWatchersForWildcards, createMapFromTemplate(cfg.configFileSpecs.wildcardDirectories), (dir, flags) => { + return system.watchDirectory!(dir, () => { + invalidateProjectAndScheduleBuilds(resolved); + }, !!(flags & WatchDirectoryFlags.Recursive)); + }); + } + + // Watch input files + for (const input of cfg.fileNames) { + system.watchFile(input, () => { + invalidateProjectAndScheduleBuilds(resolved); + }); + } + } + } + + function invalidateProjectAndScheduleBuilds(resolved: ResolvedConfigFileName) { + invalidateProject(resolved); + system!.setTimeout!(buildInvalidatedProjects, 100); + system!.setTimeout!(buildDependentInvalidatedProjects, 3000); + } + } + + function resetBuildContext(opts = defaultOptions) { + context = createBuildContext(opts); + } + + function getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus { + return getUpToDateStatus(configFileCache.parseConfigFile(configFileName)); + } + + function getBuildGraph(configFileNames: ReadonlyArray) { + const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); + if (resolvedNames === undefined) return undefined; + + return createDependencyGraph(resolvedNames); + } + + function getGlobalDependencyGraph() { + return getBuildGraph(rootNames); + } + + function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus { + if (project === undefined) { + return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; + } + + const prior = context.projectStatus.getValueOrUndefined(project.options.configFilePath!); + if (prior !== undefined) { + return prior; + } + const actual = getUpToDateStatusWorker(project); + context.projectStatus.setValue(project.options.configFilePath!, actual); + return actual; + } + + function invalidateProject(configFileName: string) { + const resolved = resolveProjectName(configFileName); + if (resolved === undefined) { + // If this was a rootName, we need to track it as missing. + // Otherwise we can just ignore it and have it possibly surface as an error in any downstream projects, + // if they exist + + // TODO: do those things + return; + } + + configFileCache.removeKey(resolved); + context.invalidatedProjects.setValue(resolved, true); + context.projectStatus.removeKey(resolved); + + const graph = getGlobalDependencyGraph()!; + if (graph) { + queueBuildForDownstreamReferences(resolved); + } + + // Mark all downstream projects of this one needing to be built "later" + function queueBuildForDownstreamReferences(root: ResolvedConfigFileName) { + debugger; + const deps = graph.dependencyMap.getReferencesTo(root); + for (const ref of deps) { + // Can skip circular references + if (!context.queuedProjects.hasKey(ref)) { + context.queuedProjects.setValue(ref, true); + queueBuildForDownstreamReferences(ref); + } + } + } + } + + function buildInvalidatedProjects() { + buildSomeProjects(p => context.invalidatedProjects.hasKey(p)); + } + + function buildDependentInvalidatedProjects() { + buildSomeProjects(p => context.queuedProjects.hasKey(p)); + } + + function buildSomeProjects(predicate: (projName: ResolvedConfigFileName) => boolean) { + const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(rootNames); + if (resolvedNames === undefined) return; + + const graph = createDependencyGraph(resolvedNames)!; + for (const next of graph.buildQueue) { + if (!predicate(next)) continue; + + const resolved = resolveProjectName(next); + if (!resolved) continue; // ?? + const proj = configFileCache.parseConfigFile(resolved); + if (!proj) continue; // ? + + const status = getUpToDateStatus(proj); + verboseReportProjectStatus(next, status); + + if (status.type === UpToDateStatusType.UpstreamBlocked) { + if (context.options.verbose) buildHost.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName); + continue; + } + + buildSingleProject(next); + } + } + + function getAllProjectOutputs(project: ParsedCommandLine): ReadonlyArray { + if (project.options.outFile) { + return getOutFileOutputs(project); + } + else { + const outputs: string[] = []; + for (const inputFile of project.fileNames) { + outputs.push(...getOutputFileNames(inputFile, project)); + } + return outputs; + } + } + + function getUpToDateStatusWorker(project: ParsedCommandLine): UpToDateStatus { + let newestInputFileName: string = undefined!; + let newestInputFileTime = minimumDate; + // Get timestamps of input files + for (const inputFile of project.fileNames) { + if (!compilerHost.fileExists(inputFile)) { + return { + type: UpToDateStatusType.Unbuildable, + reason: `${inputFile} does not exist` + }; + } + + const inputTime = compilerHost.getModifiedTime!(inputFile); + if (inputTime > newestInputFileTime) { + newestInputFileName = inputFile; + newestInputFileTime = inputTime; + } + } + + // Collect the expected outputs of this project + const outputs = getAllProjectOutputs(project); + + if (outputs.length === 0) { + return { + type: UpToDateStatusType.ContainerOnly + }; + } + + // Now see if all outputs are newer than the newest input + let oldestOutputFileName = "(none)"; + let oldestOutputFileTime = maximumDate; + let newestOutputFileName = "(none)"; + let newestOutputFileTime = minimumDate; + let missingOutputFileName: string | undefined; + let newestDeclarationFileContentChangedTime = minimumDate; + let isOutOfDateWithInputs = false; + for (const output of outputs) { + // Output is missing; can stop checking + // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status + if (!compilerHost.fileExists(output)) { + missingOutputFileName = output; + break; + } + + const outputTime = compilerHost.getModifiedTime!(output); + if (outputTime < oldestOutputFileTime) { + oldestOutputFileTime = outputTime; + oldestOutputFileName = output; + } + + // If an output is older than the newest input, we can stop checking + // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status + if (outputTime < newestInputFileTime) { + isOutOfDateWithInputs = true; + break; + } + + if (outputTime > newestOutputFileTime) { + newestOutputFileTime = outputTime; + newestOutputFileName = output; + } + + // Keep track of when the most recent time a .d.ts file was changed. + // In addition to file timestamps, we also keep track of when a .d.ts file + // had its file touched but not had its contents changed - this allows us + // to skip a downstream typecheck + if (isDeclarationFile(output)) { + const unchangedTime = context.unchangedOutputs.getValueOrUndefined(output); + if (unchangedTime !== undefined) { + newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime); + } + else { + newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, compilerHost.getModifiedTime!(output)); + } + } + } + + let pseudoUpToDate = false; + if (project.projectReferences) { + for (const ref of project.projectReferences) { + const resolvedRef = resolveProjectReferencePath(compilerHost, ref) as ResolvedConfigFileName; + const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef)); + + // An upstream project is blocked + if (refStatus.type === UpToDateStatusType.Unbuildable) { + return { + type: UpToDateStatusType.UpstreamBlocked, + upstreamProjectName: ref.path + }; + } + + // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) + if (refStatus.type !== UpToDateStatusType.UpToDate) { + return { + type: UpToDateStatusType.UpstreamOutOfDate, + upstreamProjectName: ref.path + }; + } + + // If the upstream project's newest file is older than our oldest output, we + // can't be out of date because of it + if (refStatus.newestInputFileTime <= oldestOutputFileTime) { + continue; + } + + // If the upstream project has only change .d.ts files, and we've built + // *after* those files, then we're "psuedo up to date" and eligible for a fast rebuild + if (refStatus.newestDeclarationFileContentChangedTime <= oldestOutputFileTime) { + pseudoUpToDate = true; + continue; + } + + // We have an output older than an upstream output - we are out of date + Debug.assert(oldestOutputFileName !== undefined, "Should have an oldest output filename here"); + return { + type: UpToDateStatusType.OutOfDateWithUpstream, + outOfDateOutputFileName: oldestOutputFileName, + newerProjectName: ref.path + }; + } + } + + if (missingOutputFileName !== undefined) { + return { + type: UpToDateStatusType.OutputMissing, + missingOutputFileName + }; + } + + if (isOutOfDateWithInputs) { + return { + type: UpToDateStatusType.OutOfDateWithSelf, + outOfDateOutputFileName: oldestOutputFileName, + newerInputFileName: newestInputFileName + }; + } + + // Up to date + return { + type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime, + newestInputFileTime, + newestOutputFileTime, + newestInputFileName, + newestOutputFileName, + oldestOutputFileName + }; + } + + function createDependencyGraph(roots: ResolvedConfigFileName[]): DependencyGraph | undefined { + const temporaryMarks: { [path: string]: true } = {}; + const permanentMarks: { [path: string]: true } = {}; + const circularityReportStack: string[] = []; + const buildOrder: ResolvedConfigFileName[] = []; + const graph = createDependencyMapper(); + + let hadError = false; + + for (const root of roots) { + visit(root); + } + + if (hadError) { + return undefined; + } + + return { + buildQueue: buildOrder, + dependencyMap: graph + }; + + function visit(projPath: ResolvedConfigFileName, inCircularContext = false) { + // Already visited + if (permanentMarks[projPath]) return; + // Circular + if (temporaryMarks[projPath]) { + if (!inCircularContext) { + hadError = true; + buildHost.error(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n")); + return; + } + } + + temporaryMarks[projPath] = true; + circularityReportStack.push(projPath); + const parsed = configFileCache.parseConfigFile(projPath); + if (parsed === undefined) { + hadError = true; + return; + } + if (parsed.projectReferences) { + for (const ref of parsed.projectReferences) { + const resolvedRefPath = resolveProjectName(ref.path); + if (resolvedRefPath === undefined) { + hadError = true; + break; + } + visit(resolvedRefPath, inCircularContext || ref.circular); + graph.addReference(projPath, resolvedRefPath); + } + } + + circularityReportStack.pop(); + permanentMarks[projPath] = true; + buildOrder.push(projPath); + } + } + + function buildSingleProject(proj: ResolvedConfigFileName): BuildResultFlags { + if (context.options.dry) { + buildHost.message(Diagnostics.A_non_dry_build_would_build_project_0, proj); + return BuildResultFlags.Success; + } + + if (context.options.verbose) buildHost.verbose(Diagnostics.Building_project_0, proj); + + let resultFlags = BuildResultFlags.None; + resultFlags |= BuildResultFlags.DeclarationOutputUnchanged; + + const configFile = configFileCache.parseConfigFile(proj); + if (!configFile) { + // Failed to read the config file + resultFlags |= BuildResultFlags.ConfigFileErrors; + context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" }); + return resultFlags; + } + + if (configFile.fileNames.length === 0) { + // Nothing to build - must be a solution file, basically + return BuildResultFlags.None; + } + + const programOptions: CreateProgramOptions = { + projectReferences: configFile.projectReferences, + host: compilerHost, + rootNames: configFile.fileNames, + options: configFile.options + }; + 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) { + buildHost.errorDiagnostic(diag); + } + context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Syntactic errors" }); + return resultFlags; + } + + // Don't emit .d.ts if there are decl file errors + if (program.getCompilerOptions().declaration) { + const declDiagnostics = program.getDeclarationDiagnostics(); + if (declDiagnostics.length) { + resultFlags |= BuildResultFlags.DeclarationEmitErrors; + for (const diag of declDiagnostics) { + buildHost.errorDiagnostic(diag); + } + context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Declaration file errors" }); + return resultFlags; + } + } + + // Same as above but now for semantic diagnostics + const semanticDiagnostics = program.getSemanticDiagnostics(); + if (semanticDiagnostics.length) { + resultFlags |= BuildResultFlags.TypeErrors; + for (const diag of semanticDiagnostics) { + buildHost.errorDiagnostic(diag); + } + context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Semantic errors" }); + return resultFlags; + } + + let newestDeclarationFileContentChangedTime = minimumDate; + program.emit(/*targetSourceFile*/ undefined, (fileName, content, writeBom, onError) => { + let priorChangeTime: Date | undefined; + + if (isDeclarationFile(fileName) && compilerHost.fileExists(fileName)) { + if (compilerHost.readFile(fileName) === content) { + // Check for unchanged .d.ts files + resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; + priorChangeTime = compilerHost.getModifiedTime && compilerHost.getModifiedTime(fileName); + } + } + + compilerHost.writeFile(fileName, content, writeBom, onError, emptyArray); + if (priorChangeTime !== undefined) { + newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); + context.unchangedOutputs.setValue(fileName, priorChangeTime); + } + }); + + context.projectStatus.setValue(proj, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime } as UpToDateStatus); + return resultFlags; + } + + function updateOutputTimestamps(proj: ParsedCommandLine) { + if (context.options.dry) { + return buildHost.message(Diagnostics.A_non_dry_build_would_build_project_0, proj.options.configFilePath!); + } + + if (context.options.verbose) buildHost.verbose(Diagnostics.Updating_output_timestamps_of_project_0, proj.options.configFilePath!); + const now = new Date(); + const outputs = getAllProjectOutputs(proj); + let priorNewestUpdateTime = minimumDate; + for (const file of outputs) { + if (isDeclarationFile(file)) { + priorNewestUpdateTime = newer(priorNewestUpdateTime, compilerHost.getModifiedTime!(file)); + } + compilerHost.setModifiedTime!(file, now); + } + + context.projectStatus.setValue(proj.options.configFilePath!, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); + } + + function getFilesToClean(configFileNames: ReadonlyArray): string[] | undefined { + const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); + if (resolvedNames === undefined) return undefined; + + // Get the same graph for cleaning we'd use for building + const graph = createDependencyGraph(resolvedNames); + if (graph === undefined) return undefined; + + const filesToDelete: string[] = []; + for (const proj of graph.buildQueue) { + const parsed = configFileCache.parseConfigFile(proj); + if (parsed === undefined) { + // File has gone missing; fine to ignore here + continue; + } + const outputs = getAllProjectOutputs(parsed); + for (const output of outputs) { + if (compilerHost.fileExists(output)) { + filesToDelete.push(output); + } + } + } + return filesToDelete; + } + + function getAllProjectsInScope(): ReadonlyArray | undefined { + const resolvedNames = resolveProjectNames(rootNames); + if (resolvedNames === undefined) return undefined; + const graph = createDependencyGraph(resolvedNames); + if (graph === undefined) return undefined; + return graph.buildQueue; + } + + function cleanAllProjects() { + const resolvedNames: ReadonlyArray | undefined = getAllProjectsInScope(); + if (resolvedNames === undefined) { + return buildHost.message(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located); + } + + const filesToDelete = getFilesToClean(resolvedNames); + if (filesToDelete === undefined) { + return buildHost.message(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located); + } + + if (context.options.dry) { + return buildHost.message(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")); + } + + // Do this check later to allow --clean --dry to function even if the host can't delete files + if (!compilerHost.deleteFile) { + throw new Error("Host does not support deleting files"); + } + + for (const output of filesToDelete) { + compilerHost.deleteFile(output); + } + } + + function resolveProjectName(name: string): ResolvedConfigFileName | undefined { + const fullPath = resolvePath(compilerHost.getCurrentDirectory(), name); + if (compilerHost.fileExists(fullPath)) { + return fullPath as ResolvedConfigFileName; + } + const fullPathWithTsconfig = combinePaths(fullPath, "tsconfig.json"); + if (compilerHost.fileExists(fullPathWithTsconfig)) { + return fullPathWithTsconfig as ResolvedConfigFileName; + } + buildHost.error(Diagnostics.File_0_not_found, relName(fullPath)); + return undefined; + } + + function resolveProjectNames(configFileNames: ReadonlyArray): ResolvedConfigFileName[] | undefined { + const resolvedNames: ResolvedConfigFileName[] = []; + for (const name of configFileNames) { + const resolved = resolveProjectName(name); + if (resolved === undefined) { + return undefined; + } + resolvedNames.push(resolved); + } + return resolvedNames; + } + + function buildAllProjects() { + const graph = getGlobalDependencyGraph(); + if (graph === undefined) return; + + const queue = graph.buildQueue; + reportBuildQueue(graph); + + for (const next of queue) { + const proj = configFileCache.parseConfigFile(next); + if (proj === undefined) { + break; + } + const status = getUpToDateStatus(proj); + verboseReportProjectStatus(next, status); + + const projName = proj.options.configFilePath!; + if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { + // Up to date, skip + if (defaultOptions.dry) { + // In a dry build, inform the user of this fact + buildHost.message(Diagnostics.Project_0_is_up_to_date, projName); + } + continue; + } + + if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes && !context.options.force) { + // Fake build + updateOutputTimestamps(proj); + continue; + } + + if (status.type === UpToDateStatusType.UpstreamBlocked) { + if (context.options.verbose) buildHost.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, projName, status.upstreamProjectName); + continue; + } + + if (status.type === UpToDateStatusType.ContainerOnly) { + // Do nothing + continue; + } + + buildSingleProject(next); + } + } + + /** + * Report the build ordering inferred from the current project graph if we're in verbose mode + */ + function reportBuildQueue(graph: DependencyGraph) { + if (!context.options.verbose) return; + + const names: string[] = []; + for (const name of graph.buildQueue) { + names.push(name); + } + if (context.options.verbose) buildHost.verbose(Diagnostics.Projects_in_this_build_Colon_0, names.map(s => "\r\n * " + relName(s)).join("")); + } + + function relName(path: string): string { + return convertToRelativePath(path, compilerHost.getCurrentDirectory(), f => compilerHost.getCanonicalFileName(f)); + } + + /** + * Report the up-to-date status of a project if we're in verbose mode + */ + function verboseReportProjectStatus(configFileName: string, status: UpToDateStatus) { + if (!context.options.verbose) return; + switch (status.type) { + case UpToDateStatusType.OutOfDateWithSelf: + return buildHost.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + relName(configFileName), + relName(status.outOfDateOutputFileName), + relName(status.newerInputFileName)); + case UpToDateStatusType.OutOfDateWithUpstream: + return buildHost.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + relName(configFileName), + relName(status.outOfDateOutputFileName), + relName(status.newerProjectName)); + case UpToDateStatusType.OutputMissing: + return buildHost.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + relName(configFileName), + relName(status.missingOutputFileName)); + case UpToDateStatusType.UpToDate: + if (status.newestInputFileTime !== undefined) { + return buildHost.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + relName(configFileName), + relName(status.newestInputFileName), + relName(status.oldestOutputFileName)); + } + // Don't report anything for "up to date because it was already built" -- too verbose + break; + case UpToDateStatusType.UpToDateWithUpstreamTypes: + return buildHost.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, + relName(configFileName)); + case UpToDateStatusType.UpstreamOutOfDate: + return buildHost.verbose(Diagnostics.Project_0_is_out_of_date_because_its_dependency_1_is_out_of_date, + relName(configFileName), + relName(status.upstreamProjectName)); + case UpToDateStatusType.UpstreamBlocked: + return buildHost.verbose(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, + relName(configFileName), + relName(status.upstreamProjectName)); + case UpToDateStatusType.Unbuildable: + return buildHost.verbose(Diagnostics.Failed_to_parse_file_0_Colon_1, + relName(configFileName), + status.reason); + case UpToDateStatusType.ContainerOnly: + // Don't report status on "solution" projects + break; + default: + assertTypeIsNever(status); + } + } + } +} diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index e3e0ea08f53..93834a6ea54 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -12,11 +12,6 @@ namespace ts { return count; } - function getDiagnosticText(_message: DiagnosticMessage, ..._args: any[]): string { - const diagnostic = createCompilerDiagnostic.apply(undefined, arguments); - return diagnostic.messageText; - } - let reportDiagnostic = createDiagnosticReporter(sys); function updateReportDiagnostic(options: CompilerOptions) { if (shouldBePretty(options)) { @@ -46,9 +41,33 @@ namespace ts { return s; } + function getOptionsForHelp(commandLine: ParsedCommandLine) { + // Sort our options by their names, (e.g. "--noImplicitAny" comes before "--watch") + return !!commandLine.options.all ? + sort(optionDeclarations, (a, b) => compareStringsCaseInsensitive(a.name, b.name)) : + filter(optionDeclarations.slice(), v => !!v.showInSimplifiedHelpView); + } + export function executeCommandLine(args: string[]): void { + if (args.length > 0 && ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b"))) { + const reportDiag = createDiagnosticReporter(sys, /*pretty*/ true); + const report = (message: DiagnosticMessage, ...args: string[]) => reportDiag(createCompilerDiagnostic(message, ...args)); + const buildHost: BuildHost = { + error: report, + verbose: report, + message: report, + errorDiagnostic: d => reportDiag(d) + }; + return performBuild(args.slice(1), createCompilerHost({}), buildHost, sys); + } + const commandLine = parseCommandLine(args); + if (commandLine.options.build) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_build_must_be_the_first_command_line_argument)); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + // Configuration file name (if any) let configFileName: string | undefined; if (commandLine.options.locale) { @@ -74,7 +93,7 @@ namespace ts { if (commandLine.options.help || commandLine.options.all) { printVersion(); - printHelp(!!commandLine.options.all); + printHelp(getOptionsForHelp(commandLine)); return sys.exit(ExitStatus.Success); } @@ -107,7 +126,7 @@ namespace ts { if (commandLine.fileNames.length === 0 && !configFileName) { printVersion(); - printHelp(!!commandLine.options.all); + printHelp(getOptionsForHelp(commandLine)); return sys.exit(ExitStatus.Success); } @@ -271,122 +290,6 @@ namespace ts { } } - function printVersion() { - sys.write(getDiagnosticText(Diagnostics.Version_0, version) + sys.newLine); - } - - function printHelp(showAllOptions: boolean) { - const output: string[] = []; - - // We want to align our "syntax" and "examples" commands to a certain margin. - const syntaxLength = getDiagnosticText(Diagnostics.Syntax_Colon_0, "").length; - const examplesLength = getDiagnosticText(Diagnostics.Examples_Colon_0, "").length; - let marginLength = Math.max(syntaxLength, examplesLength); - - // Build up the syntactic skeleton. - let syntax = makePadding(marginLength - syntaxLength); - syntax += "tsc [" + getDiagnosticText(Diagnostics.options) + "] [" + getDiagnosticText(Diagnostics.file) + " ...]"; - - output.push(getDiagnosticText(Diagnostics.Syntax_Colon_0, syntax)); - output.push(sys.newLine + sys.newLine); - - // Build up the list of examples. - const padding = makePadding(marginLength); - output.push(getDiagnosticText(Diagnostics.Examples_Colon_0, makePadding(marginLength - examplesLength) + "tsc hello.ts") + sys.newLine); - output.push(padding + "tsc --outFile file.js file.ts" + sys.newLine); - output.push(padding + "tsc @args.txt" + sys.newLine); - output.push(sys.newLine); - - output.push(getDiagnosticText(Diagnostics.Options_Colon) + sys.newLine); - - // Sort our options by their names, (e.g. "--noImplicitAny" comes before "--watch") - const optsList = showAllOptions ? - sort(optionDeclarations, (a, b) => compareStringsCaseInsensitive(a.name, b.name)) : - filter(optionDeclarations.slice(), v => !!v.showInSimplifiedHelpView); - - // We want our descriptions to align at the same column in our output, - // so we keep track of the longest option usage string. - marginLength = 0; - const usageColumn: string[] = []; // Things like "-d, --declaration" go in here. - const descriptionColumn: string[] = []; - - const optionsDescriptionMap = createMap(); // Map between option.description and list of option.type if it is a kind - - for (const option of optsList) { - // If an option lacks a description, - // it is not officially supported. - if (!option.description) { - continue; - } - - let usageText = " "; - if (option.shortName) { - usageText += "-" + option.shortName; - usageText += getParamType(option); - usageText += ", "; - } - - usageText += "--" + option.name; - usageText += getParamType(option); - - usageColumn.push(usageText); - let description: string; - - if (option.name === "lib") { - description = getDiagnosticText(option.description); - const element = (option).element; - const typeMap = >element.type; - optionsDescriptionMap.set(description, arrayFrom(typeMap.keys()).map(key => `'${key}'`)); - } - else { - description = getDiagnosticText(option.description); - } - - descriptionColumn.push(description); - - // Set the new margin for the description column if necessary. - marginLength = Math.max(usageText.length, marginLength); - } - - // Special case that can't fit in the loop. - const usageText = " @<" + getDiagnosticText(Diagnostics.file) + ">"; - usageColumn.push(usageText); - descriptionColumn.push(getDiagnosticText(Diagnostics.Insert_command_line_options_and_files_from_a_file)); - marginLength = Math.max(usageText.length, marginLength); - - // Print out each row, aligning all the descriptions on the same column. - for (let i = 0; i < usageColumn.length; i++) { - const usage = usageColumn[i]; - const description = descriptionColumn[i]; - const kindsList = optionsDescriptionMap.get(description); - output.push(usage + makePadding(marginLength - usage.length + 2) + description + sys.newLine); - - if (kindsList) { - output.push(makePadding(marginLength + 4)); - for (const kind of kindsList) { - output.push(kind + " "); - } - output.push(sys.newLine); - } - } - - for (const line of output) { - sys.write(line); - } - return; - - function getParamType(option: CommandLineOption) { - if (option.paramType !== undefined) { - return " " + getDiagnosticText(option.paramType); - } - return ""; - } - - function makePadding(paddingLength: number): string { - return Array(paddingLength + 1).join(" "); - } - } - function writeConfigFile(options: CompilerOptions, fileNames: string[]) { const currentDirectory = sys.getCurrentDirectory(); const file = normalizePath(combinePaths(currentDirectory, "tsconfig.json")); diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 5716a2a417d..b2ed458b8b4 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -47,6 +47,7 @@ "moduleSpecifiers.ts", "watch.ts", "commandLineParser.ts", - "tsc.ts" + "tsbuild.ts", + "tsc.ts", ] } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 32c6faea37c..1378be2a724 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2558,6 +2558,7 @@ namespace ts { fileName: string; /* @internal */ path: Path; text: string; + /* @internal */ resolvedPath: Path; /** * If two source files are for the same version of the same package, one will redirect to the other. @@ -2658,12 +2659,15 @@ namespace ts { export interface InputFiles extends Node { kind: SyntaxKind.InputFiles; javascriptText: string; + javascriptMapText?: string; declarationText: string; + declarationMapText?: string; } export interface UnparsedSource extends Node { kind: SyntaxKind.UnparsedSource; text: string; + sourceMapText?: string; } export interface JsonSourceFile extends SourceFile { @@ -4297,6 +4301,9 @@ namespace ts { allowUnusedLabels?: boolean; alwaysStrict?: boolean; // Always combine with strict property baseUrl?: string; + /** An error if set - this should only go through the -b pipeline and not actually be observed */ + /*@internal*/ + build?: boolean; charset?: string; checkJs?: boolean; /* @internal */ configFilePath?: string; @@ -4819,6 +4826,10 @@ namespace ts { /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; createHash?(data: string): string; + + getModifiedTime?(fileName: string): Date; + setModifiedTime?(fileName: string, date: Date): void; + deleteFile?(fileName: string): void; } /* @internal */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 8081f964ec4..156555124bd 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2861,13 +2861,26 @@ namespace ts { let lineCount: number; let linePos: number; + function updateLineCountAndPosFor(s: string) { + const lineStartsOfS = computeLineStarts(s); + if (lineStartsOfS.length > 1) { + lineCount = lineCount + lineStartsOfS.length - 1; + linePos = output.length - s.length + last(lineStartsOfS); + lineStart = (linePos - output.length) === 0; + } + else { + lineStart = false; + } + } + function write(s: string) { if (s && s.length) { if (lineStart) { - output += getIndentString(indent); + s = getIndentString(indent) + s; lineStart = false; } output += s; + updateLineCountAndPosFor(s); } } @@ -2881,21 +2894,14 @@ namespace ts { function rawWrite(s: string) { if (s !== undefined) { - if (lineStart) { - lineStart = false; - } output += s; + updateLineCountAndPosFor(s); } } function writeLiteral(s: string) { if (s && s.length) { write(s); - const lineStartsOfS = computeLineStarts(s); - if (lineStartsOfS.length > 1) { - lineCount = lineCount + lineStartsOfS.length - 1; - linePos = output.length - s.length + last(lineStartsOfS); - } } } @@ -2909,7 +2915,9 @@ namespace ts { } function writeTextOfNode(text: string, node: Node) { - write(getTextOfNodeFromSourceText(text, node)); + const s = getTextOfNodeFromSourceText(text, node); + write(s); + updateLineCountAndPosFor(s); } reset(); @@ -5487,6 +5495,10 @@ namespace ts { return node.kind === SyntaxKind.Bundle; } + export function isUnparsedSource(node: Node): node is UnparsedSource { + return node.kind === SyntaxKind.UnparsedSource; + } + // JSDoc export function isJSDocTypeExpression(node: Node): node is JSDocTypeExpression { diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index 84d9f1013f2..1bb358698a2 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -51,6 +51,10 @@ namespace fakes { this.vfs.writeFileSync(path, writeByteOrderMark ? utils.addUTF8ByteOrderMark(data) : data); } + public deleteFile(path: string) { + this.vfs.unlinkSync(path); + } + public fileExists(path: string) { const stats = this._getStats(path); return stats ? stats.isFile() : false; @@ -131,6 +135,10 @@ namespace fakes { return stats ? stats.mtime : undefined!; // TODO: GH#18217 } + public setModifiedTime(path: string, time: Date) { + this.vfs.utimesSync(path, time, time); + } + public createHash(data: string): string { return data; } @@ -244,6 +252,10 @@ namespace fakes { return this.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); } + public deleteFile(fileName: string) { + this.sys.deleteFile(fileName); + } + public fileExists(fileName: string): boolean { return this.sys.fileExists(fileName); } @@ -252,6 +264,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); } @@ -312,7 +332,7 @@ namespace fakes { if (cacheKey) { const meta = this.vfs.filemeta(canonicalFileName); const sourceFileFromMetadata = meta.get(cacheKey) as ts.SourceFile | undefined; - if (sourceFileFromMetadata) { + if (sourceFileFromMetadata && sourceFileFromMetadata.getFullText() === content) { this._sourceFiles.set(canonicalFileName, sourceFileFromMetadata); return sourceFileFromMetadata; } diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index c401b40a339..a62975b0300 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -53,6 +53,7 @@ "../compiler/resolutionCache.ts", "../compiler/moduleSpecifiers.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..9d093f61e09 --- /dev/null +++ b/src/harness/unittests/tsbuild.ts @@ -0,0 +1,425 @@ +namespace ts { + let currentTime = 100; + let lastDiagnostics: Diagnostic[] = []; + const reportDiagnostic: DiagnosticReporter = diagnostic => lastDiagnostics.push(diagnostic); + const report = (message: DiagnosticMessage, ...args: string[]) => reportDiagnostic(createCompilerDiagnostic(message, ...args)); + const buildHost: BuildHost = { + error: report, + verbose: report, + message: report, + errorDiagnostic: d => reportDiagnostic(d) + }; + + export namespace Sample1 { + tick(); + const projFs = loadProjectFromDisk("../../tests/projects/sample1"); + + const allExpectedOutputs = ["/src/tests/index.js", + "/src/core/index.js", "/src/core/index.d.ts", + "/src/logic/index.js", "/src/logic/index.d.ts"]; + + describe("tsbuild - sanity check of clean build of 'sample1' project", () => { + it("can build the sample project 'sample1' without error", () => { + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); + + clearDiagnostics(); + builder.buildAllProjects(); + assertDiagnosticMessages(/*empty*/); + + // Check for outputs. Not an exhaustive list + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + }); + }); + + describe("tsbuild - dry builds", () => { + it("doesn't write any files in a dry build", () => { + clearDiagnostics(); + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: true, force: false, verbose: false }); + builder.buildAllProjects(); + assertDiagnosticMessages(Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0); + + // Check for outputs to not be written. Not an exhaustive list + for (const output of allExpectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + }); + + it("indicates that it would skip builds during a dry build", () => { + clearDiagnostics(); + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + + let builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); + builder.buildAllProjects(); + tick(); + + clearDiagnostics(); + builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: true, force: false, verbose: false }); + builder.buildAllProjects(); + assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); + }); + }); + + describe("tsbuild - clean builds", () => { + it("removes all files it built", () => { + clearDiagnostics(); + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); + builder.buildAllProjects(); + // Verify they exist + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + builder.cleanAllProjects(); + // Verify they are gone + for (const output of allExpectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + // Subsequent clean shouldn't throw / etc + builder.cleanAllProjects(); + }); + }); + + describe("tsbuild - force builds", () => { + it("always builds under --force", () => { + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: true, verbose: false }); + builder.buildAllProjects(); + let currentTime = time(); + checkOutputTimestamps(currentTime); + + tick(); + Debug.assert(time() !== currentTime, "Time moves on"); + currentTime = time(); + builder.buildAllProjects(); + checkOutputTimestamps(currentTime); + + function checkOutputTimestamps(expected: number) { + // Check timestamps + for (const output of allExpectedOutputs) { + const actual = fs.statSync(output).mtimeMs; + assert(actual === expected, `File ${output} has timestamp ${actual}, expected ${expected}`); + } + } + }); + }); + + describe("tsbuild - can detect when and what to rebuild", () => { + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: true }); + + it("Builds the project", () => { + clearDiagnostics(); + builder.resetBuildContext(); + builder.buildAllProjects(); + assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0); + tick(); + }); + + // All three projects are up to date + it("Detects that all projects are up to date", () => { + clearDiagnostics(); + builder.resetBuildContext(); + builder.buildAllProjects(); + assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2); + tick(); + }); + + // Update a file in the leaf node (tests), only it should rebuild the last one + it("Only builds the leaf node project", () => { + clearDiagnostics(); + fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); + builder.resetBuildContext(); + builder.buildAllProjects(); + + assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + Diagnostics.Building_project_0); + tick(); + }); + + // Update a file in the parent (without affecting types), should get fast downstream builds + it("Detects type-only changes in upstream projects", () => { + clearDiagnostics(); + replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); + builder.resetBuildContext(); + builder.buildAllProjects(); + + assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, + Diagnostics.Updating_output_timestamps_of_project_0, + Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, + Diagnostics.Updating_output_timestamps_of_project_0); + }); + }); + + describe("tsbuild - downstream-blocked compilations", () => { + it("won't build downstream projects if upstream projects have errors", () => { + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: true }); + + clearDiagnostics(); + + // Induce an error in the middle project + replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); + builder.buildAllProjects(); + assertDiagnosticMessages( + Diagnostics.Projects_in_this_build_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Property_0_does_not_exist_on_type_1, + Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, + Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors + ); + }); + }); + + describe("tsbuild - project invalidation", () => { + it("invalidates projects correctly", () => { + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); + + clearDiagnostics(); + builder.buildAllProjects(); + assertDiagnosticMessages(/*empty*/); + + // Update a timestamp in the middle project + tick(); + touch(fs, "/src/logic/index.ts"); + // Because we haven't reset the build context, the builder should assume there's nothing to do right now + const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic")!); + assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); + + // Rebuild this project + tick(); + builder.invalidateProject("/src/logic"); + builder.buildInvalidatedProjects(); + // The file should be updated + assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); + assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); + + // Build downstream projects should update 'tests', but not 'core' + tick(); + builder.buildDependentInvalidatedProjects(); + assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have been rebuilt"); + assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); + }); + }); + } + + export namespace OutFile { + const outFileFs = loadProjectFromDisk("../../tests/projects/outfile-concat"); + + describe("tsbuild - baseline sectioned sourcemaps", () => { + const fs = outFileFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, buildHost, ["/src/third"], { dry: false, force: false, verbose: false }); + clearDiagnostics(); + builder.buildAllProjects(); + assertDiagnosticMessages(/*none*/); + + const files = [ + "/src/third/thirdjs/output/third-output.js", + "/src/third/thirdjs/output/third-output.js.map" + ]; + + for (const file of files) { + it(`Generates files matching the baseline - ${file}`, () => { + Harness.Baseline.runBaseline(getBaseFileName(file), () => { + return fs.readFileSync(file, "utf-8"); + }); + }); + } + + it(`Generates files matching the baseline - file listing for outFile-concat`, () => { + Harness.Baseline.runBaseline("outfile-concat-fileListing.txt", () => { + return fs.getFileListing(); + }); + }); + }); + } + + describe("tsbuild - graph-ordering", () => { + const fs = new vfs.FileSystem(false); + const host = new fakes.CompilerHost(fs); + const deps: [string, string][] = [ + ["A", "B"], + ["B", "C"], + ["A", "C"], + ["B", "D"], + ["C", "D"], + ["C", "E"], + ["F", "E"] + ]; + + writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); + + it("orders the graph correctly - specify two roots", () => { + checkGraphOrdering(["A", "G"], ["A", "B", "C", "D", "E", "G"]); + }); + + it("orders the graph correctly - multiple parts of the same graph in various orders", () => { + checkGraphOrdering(["A"], ["A", "B", "C", "D", "E"]); + checkGraphOrdering(["A", "C", "D"], ["A", "B", "C", "D", "E"]); + checkGraphOrdering(["D", "C", "A"], ["A", "B", "C", "D", "E"]); + }); + + it("orders the graph correctly - other orderings", () => { + checkGraphOrdering(["F"], ["F", "E"]); + checkGraphOrdering(["E"], ["E"]); + checkGraphOrdering(["F", "C", "A"], ["A", "B", "C", "D", "E", "F"]); + }); + + function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { + const builder = createSolutionBuilder(host, buildHost, rootNames, { dry: true, force: false, verbose: false }); + + const projFileNames = rootNames.map(getProjectFileName); + const graph = builder.getBuildGraph(projFileNames); + if (graph === undefined) throw new Error("Graph shouldn't be undefined"); + + assert.sameMembers(graph.buildQueue, expectedBuildSet.map(getProjectFileName)); + + for (const dep of deps) { + const child = getProjectFileName(dep[0]); + if (graph.buildQueue.indexOf(child) < 0) continue; + const parent = getProjectFileName(dep[1]); + assert.isAbove(graph.buildQueue.indexOf(child), graph.buildQueue.indexOf(parent), `Expecting child ${child} to be built after parent ${parent}`); + } + } + + function getProjectFileName(proj: string) { + return `/project/${proj}/tsconfig.json` as ResolvedConfigFileName; + } + + function writeProjects(fileSystem: vfs.FileSystem, projectNames: string[], deps: [string, string][]): string[] { + const projFileNames: string[] = []; + for (const dep of deps) { + if (projectNames.indexOf(dep[0]) < 0) throw new Error(`Invalid dependency - project ${dep[0]} does not exist`); + if (projectNames.indexOf(dep[1]) < 0) throw new Error(`Invalid dependency - project ${dep[1]} does not exist`); + } + for (const proj of projectNames) { + fileSystem.mkdirpSync(`/project/${proj}`); + fileSystem.writeFileSync(`/project/${proj}/${proj}.ts`, "export {}"); + const configFileName = getProjectFileName(proj); + const configContent = JSON.stringify({ + compilerOptions: { composite: true }, + files: [`./${proj}.ts`], + references: deps.filter(d => d[0] === proj).map(d => ({ path: `../${d[1]}` })) + }, undefined, 2); + fileSystem.writeFileSync(configFileName, configContent); + projFileNames.push(configFileName); + } + return projFileNames; + } + }); + + + function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); + } + const old = fs.readFileSync(path, "utf-8"); + if (old.indexOf(oldText) < 0) { + throw new Error(`Text "${oldText}" does not exist in file ${path}`); + } + const newContent = old.replace(oldText, newText); + fs.writeFileSync(path, newContent, "utf-8"); + } + + function assertDiagnosticMessages(...expected: DiagnosticMessage[]) { + const actual = lastDiagnostics.slice(); + if (actual.length !== expected.length) { + assert.fail(actual, expected, `Diagnostic arrays did not match - got\r\n${actual.map(a => " " + a.messageText).join("\r\n")}\r\nexpected\r\n${expected.map(e => " " + e.message).join("\r\n")}`); + } + 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 - expected diagnostic ${i} "${actual[i].messageText}" to match ${expected[i].message}`); + } + } + } + + function clearDiagnostics() { + lastDiagnostics = []; + } + + export function printDiagnostics(header = "== Diagnostics ==") { + const out = createDiagnosticReporter(sys); + sys.write(header + "\r\n"); + for (const d of lastDiagnostics) { + out(d); + } + } + + function tick() { + currentTime += 60_000; + } + + function time() { + return currentTime; + } + + function touch(fs: vfs.FileSystem, path: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); + } + fs.utimesSync(path, new Date(time()), new Date(time())); + } + + function loadProjectFromDisk(root: string): vfs.FileSystem { + const fs = new vfs.FileSystem(/*ignoreCase*/ false, { time }); + const rootPath = resolvePath(__dirname, root); + loadFsMirror(fs, rootPath, "/src"); + fs.mkdirpSync("/lib"); + const libs = ["es5", "dom", "webworker.importscripts", "scripthost"]; + for (const lib of libs) { + const content = Harness.IO.readFile(combinePaths(Harness.libFolder, `lib.${lib}.d.ts`)); + if (content === undefined) { + throw new Error(`Failed to read lib ${lib}`); + } + fs.writeFileSync(`/lib/lib.${lib}.d.ts`, content); + } + fs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))!); + fs.meta.set("defaultLibLocation", "/lib"); + fs.makeReadonly(); + return fs; + } + + 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 2bae5303996..2c1c6f2dd54 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 */ @@ -348,10 +353,7 @@ namespace vfs { if (!result.node) this._mkdir(result); } - /** - * Print diagnostic information about the structure of the file system to the console. - */ - public debugPrint(): void { + public getFileListing(): string { let result = ""; const printLinks = (dirname: string | undefined, links: collections.SortedMap) => { const iterator = collections.getIterator(links); @@ -379,7 +381,14 @@ namespace vfs { } }; printLinks(/*dirname*/ undefined, this._getRootLinks()); - console.log(result); + return result; + } + + /** + * Print diagnostic information about the structure of the file system to the console. + */ + public debugPrint(): void { + console.log(this.getFileListing()); } // POSIX API (aligns with NodeJS "fs" module API) @@ -404,7 +413,25 @@ 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) { + if (this.isReadonly) throw createIOError("EROFS"); + if (!isFinite(+atime) || !isFinite(+mtime)) throw createIOError("EINVAL"); + + const entry = this._walk(this._resolve(path)); + if (!entry || !entry.node) { + throw createIOError("ENOENT"); + } + entry.node.atimeMs = +atime; + entry.node.mtimeMs = +mtime; + entry.node.ctimeMs = this.time(); + } + + /** + * Get file status. If `path` is a symbolic link, it is dereferenced. * * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/lstat.html * @@ -414,9 +441,10 @@ 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"); + if (!node) throw createIOError(`ENOENT`, entry.realpath); return new Stats( node.dev, node.ino, @@ -1127,8 +1155,8 @@ namespace vfs { EROFS: "file system is read-only" }); - export function createIOError(code: keyof typeof IOErrorMessages) { - const err: NodeJS.ErrnoException = new Error(`${code}: ${IOErrorMessages[code]}`); + export function createIOError(code: keyof typeof IOErrorMessages, details = "") { + const err: NodeJS.ErrnoException = new Error(`${code}: ${IOErrorMessages[code]} ${details}`); err.code = code; if (Error.captureStackTrace) Error.captureStackTrace(err, createIOError); return err; @@ -1282,6 +1310,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/src/server/project.ts b/src/server/project.ts index 155569690b2..343c4a2e97d 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -635,8 +635,8 @@ namespace ts.server { return this.rootFiles; } return map(this.program.getSourceFiles(), sourceFile => { - const scriptInfo = this.projectService.getScriptInfoForPath(sourceFile.path); - Debug.assert(!!scriptInfo, "getScriptInfo", () => `scriptInfo for a file '${sourceFile.fileName}' Path: '${sourceFile.path}' is missing.`); + const scriptInfo = this.projectService.getScriptInfoForPath(sourceFile.resolvedPath || sourceFile.path); + Debug.assert(!!scriptInfo, "getScriptInfo", () => `scriptInfo for a file '${sourceFile.fileName}' Path: '${sourceFile.path}' / '${sourceFile.resolvedPath}' is missing.`); return scriptInfo!; }); } diff --git a/src/services/services.ts b/src/services/services.ts index 7d88d334b38..db0213ed88a 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -540,6 +540,7 @@ namespace ts { public _declarationBrand: any; public fileName: string; public path: Path; + public resolvedPath: Path; public text: string; public scriptSnapshot: IScriptSnapshot; public lineMap: ReadonlyArray; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index ba6ffcb80ac..2cfb99444e5 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1674,11 +1674,14 @@ declare namespace ts { interface InputFiles extends Node { kind: SyntaxKind.InputFiles; javascriptText: string; + javascriptMapText?: string; declarationText: string; + declarationMapText?: string; } interface UnparsedSource extends Node { kind: SyntaxKind.UnparsedSource; text: string; + sourceMapText?: string; } interface JsonSourceFile extends SourceFile { statements: NodeArray; @@ -2661,6 +2664,9 @@ declare namespace ts { resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; getEnvironmentVariable?(name: string): string | undefined; createHash?(data: string): string; + getModifiedTime?(fileName: string): Date; + setModifiedTime?(fileName: string, date: Date): void; + deleteFile?(fileName: string): void; } interface SourceMapRange extends TextRange { source?: SourceMapSource; @@ -3015,6 +3021,8 @@ declare namespace ts { getDirectories(path: string): string[]; readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; getModifiedTime?(path: string): Date; + setModifiedTime?(path: string, time: Date): void; + deleteFile?(path: string): void; /** * A good implementation is node.js' `crypto.createHash`. (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) */ @@ -3380,6 +3388,7 @@ declare namespace ts { function isEnumMember(node: Node): node is EnumMember; function isSourceFile(node: Node): node is SourceFile; function isBundle(node: Node): node is Bundle; + function isUnparsedSource(node: Node): node is UnparsedSource; function isJSDocTypeExpression(node: Node): node is JSDocTypeExpression; function isJSDocAllType(node: JSDocAllType): node is JSDocAllType; function isJSDocUnknownType(node: Node): node is JSDocUnknownType; @@ -3831,8 +3840,8 @@ declare namespace ts { function createCommaList(elements: ReadonlyArray): CommaListExpression; function updateCommaList(node: CommaListExpression, elements: ReadonlyArray): CommaListExpression; function createBundle(sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; - function createUnparsedSourceFile(text: string): UnparsedSource; - function createInputFiles(javascript: string, declaration: string): InputFiles; + function createUnparsedSourceFile(text: string, map?: string): UnparsedSource; + function createInputFiles(javascript: string, declaration: string, javascriptMapText?: string, declarationMapText?: string): InputFiles; function updateBundle(node: Bundle, sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray): CallExpression; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray, param: ParameterDeclaration, paramValue: Expression): CallExpression; @@ -4053,6 +4062,10 @@ declare namespace ts { * @returns A 'Program' object. */ function createProgram(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, configFileParsingDiagnostics?: ReadonlyArray): Program; + /** + * Returns the target config filename of a project reference + */ + function resolveProjectReferencePath(host: CompilerHost, ref: ProjectReference): string | undefined; } declare namespace ts { interface EmitOutput { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index c055400e628..acbcabf5bef 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -1674,11 +1674,14 @@ declare namespace ts { interface InputFiles extends Node { kind: SyntaxKind.InputFiles; javascriptText: string; + javascriptMapText?: string; declarationText: string; + declarationMapText?: string; } interface UnparsedSource extends Node { kind: SyntaxKind.UnparsedSource; text: string; + sourceMapText?: string; } interface JsonSourceFile extends SourceFile { statements: NodeArray; @@ -2661,6 +2664,9 @@ declare namespace ts { resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; getEnvironmentVariable?(name: string): string | undefined; createHash?(data: string): string; + getModifiedTime?(fileName: string): Date; + setModifiedTime?(fileName: string, date: Date): void; + deleteFile?(fileName: string): void; } interface SourceMapRange extends TextRange { source?: SourceMapSource; @@ -3015,6 +3021,8 @@ declare namespace ts { getDirectories(path: string): string[]; readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; getModifiedTime?(path: string): Date; + setModifiedTime?(path: string, time: Date): void; + deleteFile?(path: string): void; /** * A good implementation is node.js' `crypto.createHash`. (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) */ @@ -3380,6 +3388,7 @@ declare namespace ts { function isEnumMember(node: Node): node is EnumMember; function isSourceFile(node: Node): node is SourceFile; function isBundle(node: Node): node is Bundle; + function isUnparsedSource(node: Node): node is UnparsedSource; function isJSDocTypeExpression(node: Node): node is JSDocTypeExpression; function isJSDocAllType(node: JSDocAllType): node is JSDocAllType; function isJSDocUnknownType(node: Node): node is JSDocUnknownType; @@ -3831,8 +3840,8 @@ declare namespace ts { function createCommaList(elements: ReadonlyArray): CommaListExpression; function updateCommaList(node: CommaListExpression, elements: ReadonlyArray): CommaListExpression; function createBundle(sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; - function createUnparsedSourceFile(text: string): UnparsedSource; - function createInputFiles(javascript: string, declaration: string): InputFiles; + function createUnparsedSourceFile(text: string, map?: string): UnparsedSource; + function createInputFiles(javascript: string, declaration: string, javascriptMapText?: string, declarationMapText?: string): InputFiles; function updateBundle(node: Bundle, sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray): CallExpression; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray, param: ParameterDeclaration, paramValue: Expression): CallExpression; @@ -4053,6 +4062,10 @@ declare namespace ts { * @returns A 'Program' object. */ function createProgram(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, configFileParsingDiagnostics?: ReadonlyArray): Program; + /** + * Returns the target config filename of a project reference + */ + function resolveProjectReferencePath(host: CompilerHost, ref: ProjectReference): string | undefined; } declare namespace ts { interface EmitOutput { diff --git a/tests/baselines/reference/outfile-concat-fileListing.txt b/tests/baselines/reference/outfile-concat-fileListing.txt new file mode 100644 index 00000000000..fc6a1e7b28c --- /dev/null +++ b/tests/baselines/reference/outfile-concat-fileListing.txt @@ -0,0 +1,43 @@ +*/ + /lib/ + /lib/lib.d.ts + /lib/lib.dom.d.ts + /lib/lib.es5.d.ts + /lib/lib.scripthost.d.ts + /lib/lib.webworker.importscripts.d.ts + /src/ + /src/2/ + /src/2/second-output.d.ts + /src/2/second-output.d.ts.map + /src/2/second-output.js + /src/2/second-output.js.map + /src/first/ + /src/first/bin/ + /src/first/bin/first-output.d.ts + /src/first/bin/first-output.d.ts.map + /src/first/bin/first-output.js + /src/first/bin/first-output.js.map + /src/first/first_part1.ts + /src/first/first_part2.ts + /src/first/first_part3.ts + /src/first/tsconfig.json + /src/first_part1.ts + /src/first_part2.ts + /src/first_part3.ts + /src/second/ + /src/second/second_part1.ts + /src/second/second_part2.ts + /src/second/tsconfig.json + /src/second_part1.ts + /src/second_part2.ts + /src/third/ + /src/third/third_part1.ts + /src/third/thirdjs/ + /src/third/thirdjs/output/ + /src/third/thirdjs/output/third-output.d.ts + /src/third/thirdjs/output/third-output.d.ts.map + /src/third/thirdjs/output/third-output.js + /src/third/thirdjs/output/third-output.js.map + /src/third/tsconfig.json + /src/third_part1.ts + /src/tsconfig.json \ No newline at end of file diff --git a/tests/baselines/reference/outfile-concat.js b/tests/baselines/reference/outfile-concat.js new file mode 100644 index 00000000000..e3e5aa3d5f0 --- /dev/null +++ b/tests/baselines/reference/outfile-concat.js @@ -0,0 +1,26 @@ +var s = "Hello, world"; +console.log(s); +console.log(f()); +function f() { + return "JS does hoists"; +} +//# sourceMappingURL=first-output.js.map +var N; +(function (N) { + function f() { + console.log('testing'); + } + f(); +})(N || (N = {})); +var C = (function () { + function C() { + } + C.prototype.doSomething = function () { + console.log("something got done"); + }; + return C; +}()); +//# sourceMappingURL=second-output.js.map +var c = new C(); +c.doSomething(); +//# sourceMappingURL=third-output.js.map \ No newline at end of file diff --git a/tests/baselines/reference/outfile-concat.js.map b/tests/baselines/reference/outfile-concat.js.map new file mode 100644 index 00000000000..f8220832b2a --- /dev/null +++ b/tests/baselines/reference/outfile-concat.js.map @@ -0,0 +1 @@ +{"version":3,"file":"third-output.js","sections":[{"offset":{"line":0,"column":0},"map":{"version":3,"file":"first-output.js","sourceRoot":"","sources":["first_part1.ts","first_part2.ts","first_part3.ts"],"names":[],"mappings":"AAIA,IAAM,CAAC,GAAG,cAAc,CAAC;AAMzB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;ACVf,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;ACAjB;IACI,OAAO,gBAAgB,CAAC;AAC5B,CAAC"}},{"offset":{"line":7,"column":0},"map":{"version":3,"file":"second-output.js","sourceRoot":"","sources":["second_part1.ts","second_part2.ts"],"names":[],"mappings":"AAIA,IAAU,CAAC,CAMV;AAND,WAAU,CAAC;IACP;QACI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;IAED,CAAC,EAAE,CAAC;AACR,CAAC,EANS,CAAC,KAAD,CAAC,QAMV;ACVD;IAAA;IAIA,CAAC;IAHG,uBAAW,GAAX;QACI,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IACtC,CAAC;IACL,QAAC;AAAD,CAAC,AAJD,IAIC"}},{"offset":{"line":22,"column":41},"map":{"version":3,"file":"third-output.js","sourceRoot":"","sources":["third_part1.ts"],"names":[],"mappings":";AAAA,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;AAChB,CAAC,CAAC,WAAW,EAAE,CAAC"}}]} \ No newline at end of file diff --git a/tests/baselines/reference/third-output.js b/tests/baselines/reference/third-output.js new file mode 100644 index 00000000000..e3e5aa3d5f0 --- /dev/null +++ b/tests/baselines/reference/third-output.js @@ -0,0 +1,26 @@ +var s = "Hello, world"; +console.log(s); +console.log(f()); +function f() { + return "JS does hoists"; +} +//# sourceMappingURL=first-output.js.map +var N; +(function (N) { + function f() { + console.log('testing'); + } + f(); +})(N || (N = {})); +var C = (function () { + function C() { + } + C.prototype.doSomething = function () { + console.log("something got done"); + }; + return C; +}()); +//# sourceMappingURL=second-output.js.map +var c = new C(); +c.doSomething(); +//# sourceMappingURL=third-output.js.map \ No newline at end of file diff --git a/tests/baselines/reference/third-output.js.map b/tests/baselines/reference/third-output.js.map new file mode 100644 index 00000000000..70b9ad69d53 --- /dev/null +++ b/tests/baselines/reference/third-output.js.map @@ -0,0 +1 @@ +{"version":3,"file":"third-output.js","sections":[{"offset":{"line":0,"column":0},"map":{"version":3,"file":"first-output.js","sourceRoot":"","sources":["../first_part1.ts","../first_part2.ts","../first_part3.ts"],"names":[],"mappings":"AAIA,IAAM,CAAC,GAAG,cAAc,CAAC;AAMzB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;ACVf,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;ACAjB;IACI,OAAO,gBAAgB,CAAC;AAC5B,CAAC"}},{"offset":{"line":7,"column":0},"map":{"version":3,"file":"second-output.js","sourceRoot":"","sources":["../second/second_part1.ts","../second/second_part2.ts"],"names":[],"mappings":"AAIA,IAAU,CAAC,CAMV;AAND,WAAU,CAAC;IACP;QACI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;IAED,CAAC,EAAE,CAAC;AACR,CAAC,EANS,CAAC,KAAD,CAAC,QAMV;ACVD;IAAA;IAIA,CAAC;IAHG,uBAAW,GAAX;QACI,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IACtC,CAAC;IACL,QAAC;AAAD,CAAC,AAJD,IAIC"}},{"offset":{"line":22,"column":41},"map":{"version":3,"file":"third-output.js","sourceRoot":"","sources":["../../third_part1.ts"],"names":[],"mappings":";AAAA,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;AAChB,CAAC,CAAC,WAAW,EAAE,CAAC"}}]} \ No newline at end of file diff --git a/tests/baselines/reference/tsxErrorRecovery1.js b/tests/baselines/reference/tsxErrorRecovery1.js index 7abf1346c35..d91c464c6f9 100644 --- a/tests/baselines/reference/tsxErrorRecovery1.js +++ b/tests/baselines/reference/tsxErrorRecovery1.js @@ -14,5 +14,5 @@ function foo() { } // Shouldn't see any errors down here var y = {a} 1 }; -; + ; } diff --git a/tests/baselines/reference/tsxStatelessFunctionComponents3.js b/tests/baselines/reference/tsxStatelessFunctionComponents3.js index e7bfc980229..152c7e6ba4f 100644 --- a/tests/baselines/reference/tsxStatelessFunctionComponents3.js +++ b/tests/baselines/reference/tsxStatelessFunctionComponents3.js @@ -27,7 +27,7 @@ define(["require", "exports", "react"], function (require, exports, React) { // Should be OK var MainMenu = function (props) { return (

Main Menu

-
); }; + ); }; var App = function (_a) { var children = _a.children; return (
diff --git a/tests/projects/outfile-concat/first/first_part1.ts b/tests/projects/outfile-concat/first/first_part1.ts new file mode 100644 index 00000000000..b8810033aaa --- /dev/null +++ b/tests/projects/outfile-concat/first/first_part1.ts @@ -0,0 +1,11 @@ +interface TheFirst { + none: any; +} + +const s = "Hello, world"; + +interface NoJsForHereEither { + none: any; +} + +console.log(s); diff --git a/tests/projects/outfile-concat/first/first_part2.ts b/tests/projects/outfile-concat/first/first_part2.ts new file mode 100644 index 00000000000..bd60d3eba9f --- /dev/null +++ b/tests/projects/outfile-concat/first/first_part2.ts @@ -0,0 +1 @@ +console.log(f()); diff --git a/tests/projects/outfile-concat/first/first_part3.ts b/tests/projects/outfile-concat/first/first_part3.ts new file mode 100644 index 00000000000..6f497fc490a --- /dev/null +++ b/tests/projects/outfile-concat/first/first_part3.ts @@ -0,0 +1,3 @@ +function f() { + return "JS does hoists"; +} \ No newline at end of file diff --git a/tests/projects/outfile-concat/first/tsconfig.json b/tests/projects/outfile-concat/first/tsconfig.json new file mode 100644 index 00000000000..8370f6512b8 --- /dev/null +++ b/tests/projects/outfile-concat/first/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es5", + "composite": true, + "removeComments": true, + "strict": false, + "sourceMap": true, + "declarationMap": true, + "declaration": true, + "outFile": "./bin/first-output.js" + }, + "references": [ + ] +} diff --git a/tests/projects/outfile-concat/second/second_part1.ts b/tests/projects/outfile-concat/second/second_part1.ts new file mode 100644 index 00000000000..2b995fbe4a5 --- /dev/null +++ b/tests/projects/outfile-concat/second/second_part1.ts @@ -0,0 +1,11 @@ +namespace N { + // Comment text +} + +namespace N { + function f() { + console.log('testing'); + } + + f(); +} diff --git a/tests/projects/outfile-concat/second/second_part2.ts b/tests/projects/outfile-concat/second/second_part2.ts new file mode 100644 index 00000000000..b81737e8915 --- /dev/null +++ b/tests/projects/outfile-concat/second/second_part2.ts @@ -0,0 +1,5 @@ +class C { + doSomething() { + console.log("something got done"); + } +} diff --git a/tests/projects/outfile-concat/second/tsconfig.json b/tests/projects/outfile-concat/second/tsconfig.json new file mode 100644 index 00000000000..d835cff6d66 --- /dev/null +++ b/tests/projects/outfile-concat/second/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es5", + "composite": true, + "removeComments": true, + "strict": false, + "sourceMap": true, + "declarationMap": true, + "declaration": true, + "outFile": "../2/second-output.js" + }, + "references": [ + ] +} diff --git a/tests/projects/outfile-concat/third/third_part1.ts b/tests/projects/outfile-concat/third/third_part1.ts new file mode 100644 index 00000000000..948688ae5ff --- /dev/null +++ b/tests/projects/outfile-concat/third/third_part1.ts @@ -0,0 +1,2 @@ +var c = new C(); +c.doSomething(); diff --git a/tests/projects/outfile-concat/third/tsconfig.json b/tests/projects/outfile-concat/third/tsconfig.json new file mode 100644 index 00000000000..18c98608db1 --- /dev/null +++ b/tests/projects/outfile-concat/third/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es5", + "composite": true, + "removeComments": true, + "strict": false, + "sourceMap": true, + "declarationMap": true, + "declaration": true, + "outFile": "./thirdjs/output/third-output.js" + }, + "references": [ + { "path": "../first", "prepend": true }, + { "path": "../second", "prepend": true }, + ] +} diff --git a/tests/projects/sample1/core/index.ts b/tests/projects/sample1/core/index.ts new file mode 100644 index 00000000000..529a7f549ec --- /dev/null +++ b/tests/projects/sample1/core/index.ts @@ -0,0 +1,3 @@ +export const someString: string = "HELLO WORLD"; +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..b8332f5c476 --- /dev/null +++ b/tests/projects/sample1/core/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true + } +} \ 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..fd6b2106bb8 --- /dev/null +++ b/tests/projects/sample1/logic/index.ts @@ -0,0 +1,4 @@ +import * as c from '../core/index'; +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..a58b3a9f48e --- /dev/null +++ b/tests/projects/sample1/logic/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true + }, + "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..f89dcd08a82 --- /dev/null +++ b/tests/projects/sample1/tests/index.ts @@ -0,0 +1,5 @@ +import * as c from '../core/index'; +import * as logic from '../logic/index'; + +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..437d8ca6fb3 --- /dev/null +++ b/tests/projects/sample1/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "references": [ + { "path": "../core" }, + { "path": "../logic" } + ], + "files": ["index.ts"] +} \ 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..d843e35c549 --- /dev/null +++ b/tests/projects/sample1/ui/tsconfig.json @@ -0,0 +1,5 @@ +{ + "references": [ + { "path": "../logic/index" } + ] +}