Merge pull request #17669 from Microsoft/builder
Improvements to tsc --watch
This commit is contained in:
commit
9e570c375b
|
@ -91,6 +91,7 @@ var languageServiceLibrarySources = filesFromConfig(path.join(serverDirectory, "
|
|||
var harnessCoreSources = [
|
||||
"harness.ts",
|
||||
"virtualFileSystem.ts",
|
||||
"virtualFileSystemWithWatch.ts",
|
||||
"sourceMapRecorder.ts",
|
||||
"harnessLanguageService.ts",
|
||||
"fourslash.ts",
|
||||
|
@ -128,6 +129,7 @@ var harnessSources = harnessCoreSources.concat([
|
|||
"convertCompilerOptionsFromJson.ts",
|
||||
"convertTypeAcquisitionFromJson.ts",
|
||||
"tsserverProjectSystem.ts",
|
||||
"tscWatchMode.ts",
|
||||
"compileOnSave.ts",
|
||||
"typingsInstaller.ts",
|
||||
"projectErrors.ts",
|
||||
|
|
492
src/compiler/builder.ts
Normal file
492
src/compiler/builder.ts
Normal file
|
@ -0,0 +1,492 @@
|
|||
/// <reference path="program.ts" />
|
||||
|
||||
namespace ts {
|
||||
export interface EmitOutput {
|
||||
outputFiles: OutputFile[];
|
||||
emitSkipped: boolean;
|
||||
}
|
||||
|
||||
export interface EmitOutputDetailed extends EmitOutput {
|
||||
diagnostics: Diagnostic[];
|
||||
sourceMaps: SourceMapData[];
|
||||
emittedSourceFiles: SourceFile[];
|
||||
}
|
||||
|
||||
export interface OutputFile {
|
||||
name: string;
|
||||
writeByteOrderMark: boolean;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ChangedProgramFiles {
|
||||
/** Minimal set of list of files that require emit */
|
||||
readonly filesToEmit: ReadonlyArray<string>;
|
||||
/** File paths of source files changed/added/removed or affected by changed files */
|
||||
readonly changedFiles: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
export interface Builder {
|
||||
/**
|
||||
* This is the callback when file infos in the builder are updated
|
||||
*/
|
||||
onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution): void;
|
||||
getFilesAffectedBy(program: Program, path: Path): string[];
|
||||
emitFile(program: Program, path: Path): EmitOutput;
|
||||
|
||||
/** Emit the changed files and clear the cache of the changed files */
|
||||
emitChangedFiles(program: Program): EmitOutputDetailed[];
|
||||
/** Get the changed files since last query and then clear the cache of changed files */
|
||||
getChangedProgramFiles(program: Program): ChangedProgramFiles;
|
||||
/** When called gets the semantic diagnostics for the program. It also caches the diagnostics and manage them */
|
||||
getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[];
|
||||
|
||||
/** Called to reset the status of the builder */
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
interface EmitHandler {
|
||||
addScriptInfo(program: Program, sourceFile: SourceFile): void;
|
||||
removeScriptInfo(path: Path): void;
|
||||
updateScriptInfo(program: Program, sourceFile: SourceFile): void;
|
||||
/**
|
||||
* Gets the files affected by the script info which has updated shape from the known one
|
||||
*/
|
||||
getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[];
|
||||
}
|
||||
|
||||
export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean,
|
||||
cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed {
|
||||
const outputFiles: OutputFile[] = [];
|
||||
let emittedSourceFiles: SourceFile[];
|
||||
const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers);
|
||||
if (!isDetailed) {
|
||||
return { outputFiles, emitSkipped: emitResult.emitSkipped };
|
||||
}
|
||||
|
||||
return {
|
||||
outputFiles,
|
||||
emitSkipped: emitResult.emitSkipped,
|
||||
diagnostics: emitResult.diagnostics,
|
||||
sourceMaps: emitResult.sourceMaps,
|
||||
emittedSourceFiles
|
||||
};
|
||||
|
||||
function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, _onError: (message: string) => void, sourceFiles: SourceFile[]) {
|
||||
outputFiles.push({ name: fileName, writeByteOrderMark, text });
|
||||
if (isDetailed) {
|
||||
emittedSourceFiles = addRange(emittedSourceFiles, sourceFiles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createBuilder(
|
||||
getCanonicalFileName: (fileName: string) => string,
|
||||
getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean) => EmitOutput | EmitOutputDetailed,
|
||||
computeHash: (data: string) => string,
|
||||
shouldEmitFile: (sourceFile: SourceFile) => boolean
|
||||
): Builder {
|
||||
let isModuleEmit: boolean | undefined;
|
||||
// Last checked shape signature for the file info
|
||||
type FileInfo = { fileName: string; version: string; signature: string; };
|
||||
const fileInfos = createMap<FileInfo>();
|
||||
const semanticDiagnosticsPerFile = createMap<Diagnostic[]>();
|
||||
/** The map has key by source file's path that has been changed */
|
||||
const changedFileNames = createMap<string>();
|
||||
let emitHandler: EmitHandler;
|
||||
return {
|
||||
onProgramUpdateGraph,
|
||||
getFilesAffectedBy,
|
||||
emitFile,
|
||||
emitChangedFiles,
|
||||
getChangedProgramFiles,
|
||||
getSemanticDiagnostics,
|
||||
clear
|
||||
};
|
||||
|
||||
function createProgramGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution) {
|
||||
const currentIsModuleEmit = program.getCompilerOptions().module !== ModuleKind.None;
|
||||
if (isModuleEmit !== currentIsModuleEmit) {
|
||||
isModuleEmit = currentIsModuleEmit;
|
||||
emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler();
|
||||
fileInfos.clear();
|
||||
semanticDiagnosticsPerFile.clear();
|
||||
}
|
||||
mutateMap(
|
||||
fileInfos,
|
||||
arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path),
|
||||
{
|
||||
// Add new file info
|
||||
createNewValue: (_path, sourceFile) => addNewFileInfo(program, sourceFile),
|
||||
// Remove existing file info
|
||||
onDeleteValue: removeExistingFileInfo,
|
||||
// We will update in place instead of deleting existing value and adding new one
|
||||
onExistingValue: (_key, existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile, hasInvalidatedResolution)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function registerChangedFile(path: Path, fileName: string) {
|
||||
changedFileNames.set(path, fileName);
|
||||
// All changed files need to re-evaluate its semantic diagnostics
|
||||
semanticDiagnosticsPerFile.delete(path);
|
||||
}
|
||||
|
||||
function addNewFileInfo(program: Program, sourceFile: SourceFile): FileInfo {
|
||||
registerChangedFile(sourceFile.path, sourceFile.fileName);
|
||||
emitHandler.addScriptInfo(program, sourceFile);
|
||||
return { fileName: sourceFile.fileName, version: sourceFile.version, signature: undefined };
|
||||
}
|
||||
|
||||
function removeExistingFileInfo(path: Path, existingFileInfo: FileInfo) {
|
||||
registerChangedFile(path, existingFileInfo.fileName);
|
||||
emitHandler.removeScriptInfo(path);
|
||||
}
|
||||
|
||||
function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile, hasInvalidatedResolution: HasInvalidatedResolution) {
|
||||
if (existingInfo.version !== sourceFile.version || hasInvalidatedResolution(sourceFile.path)) {
|
||||
registerChangedFile(sourceFile.path, sourceFile.fileName);
|
||||
existingInfo.version = sourceFile.version;
|
||||
emitHandler.updateScriptInfo(program, sourceFile);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureProgramGraph(program: Program) {
|
||||
if (!emitHandler) {
|
||||
createProgramGraph(program, returnFalse);
|
||||
}
|
||||
}
|
||||
|
||||
function onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution) {
|
||||
if (emitHandler) {
|
||||
createProgramGraph(program, hasInvalidatedResolution);
|
||||
}
|
||||
}
|
||||
|
||||
function getFilesAffectedBy(program: Program, path: Path): string[] {
|
||||
ensureProgramGraph(program);
|
||||
|
||||
const sourceFile = program.getSourceFile(path);
|
||||
const singleFileResult = sourceFile && shouldEmitFile(sourceFile) ? [sourceFile.fileName] : [];
|
||||
const info = fileInfos.get(path);
|
||||
if (!info || !updateShapeSignature(program, sourceFile, info)) {
|
||||
return singleFileResult;
|
||||
}
|
||||
|
||||
Debug.assert(!!sourceFile);
|
||||
return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile, singleFileResult);
|
||||
}
|
||||
|
||||
function emitFile(program: Program, path: Path) {
|
||||
ensureProgramGraph(program);
|
||||
if (!fileInfos.has(path)) {
|
||||
return { outputFiles: [], emitSkipped: true };
|
||||
}
|
||||
|
||||
return getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false, /*isDetailed*/ false);
|
||||
}
|
||||
|
||||
function enumerateChangedFilesSet(
|
||||
program: Program,
|
||||
onChangedFile: (fileName: string, path: Path) => void,
|
||||
onAffectedFile: (fileName: string, sourceFile: SourceFile) => void
|
||||
) {
|
||||
changedFileNames.forEach((fileName, path) => {
|
||||
onChangedFile(fileName, path as Path);
|
||||
const affectedFiles = getFilesAffectedBy(program, path as Path);
|
||||
for (const file of affectedFiles) {
|
||||
onAffectedFile(file, program.getSourceFile(file));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function enumerateChangedFilesEmitOutput(
|
||||
program: Program,
|
||||
emitOnlyDtsFiles: boolean,
|
||||
onChangedFile: (fileName: string, path: Path) => void,
|
||||
onEmitOutput: (emitOutput: EmitOutputDetailed, sourceFile: SourceFile) => void
|
||||
) {
|
||||
const seenFiles = createMap<SourceFile>();
|
||||
enumerateChangedFilesSet(program, onChangedFile, (fileName, sourceFile) => {
|
||||
if (!seenFiles.has(fileName)) {
|
||||
seenFiles.set(fileName, sourceFile);
|
||||
if (sourceFile) {
|
||||
// Any affected file shouldnt have the cached diagnostics
|
||||
semanticDiagnosticsPerFile.delete(sourceFile.path);
|
||||
|
||||
const emitOutput = getEmitOutput(program, sourceFile, emitOnlyDtsFiles, /*isDetailed*/ true) as EmitOutputDetailed;
|
||||
onEmitOutput(emitOutput, sourceFile);
|
||||
|
||||
// mark all the emitted source files as seen
|
||||
if (emitOutput.emittedSourceFiles) {
|
||||
for (const file of emitOutput.emittedSourceFiles) {
|
||||
seenFiles.set(file.fileName, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function emitChangedFiles(program: Program): EmitOutputDetailed[] {
|
||||
ensureProgramGraph(program);
|
||||
const result: EmitOutputDetailed[] = [];
|
||||
enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ false,
|
||||
/*onChangedFile*/ noop, emitOutput => result.push(emitOutput));
|
||||
changedFileNames.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[] {
|
||||
ensureProgramGraph(program);
|
||||
|
||||
// Ensure that changed files have cleared their respective
|
||||
enumerateChangedFilesSet(program, /*onChangedFile*/ noop, (_affectedFileName, sourceFile) => {
|
||||
if (sourceFile) {
|
||||
semanticDiagnosticsPerFile.delete(sourceFile.path);
|
||||
}
|
||||
});
|
||||
|
||||
let diagnostics: Diagnostic[];
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
const path = sourceFile.path;
|
||||
const cachedDiagnostics = semanticDiagnosticsPerFile.get(path);
|
||||
// Report the semantic diagnostics from the cache if we already have those diagnostics present
|
||||
if (cachedDiagnostics) {
|
||||
diagnostics = concatenate(diagnostics, cachedDiagnostics);
|
||||
}
|
||||
else {
|
||||
// Diagnostics werent cached, get them from program, and cache the result
|
||||
const cachedDiagnostics = program.getSemanticDiagnostics(sourceFile, cancellationToken);
|
||||
semanticDiagnosticsPerFile.set(path, cachedDiagnostics);
|
||||
diagnostics = concatenate(diagnostics, cachedDiagnostics);
|
||||
}
|
||||
}
|
||||
return diagnostics || emptyArray;
|
||||
}
|
||||
|
||||
function getChangedProgramFiles(program: Program): ChangedProgramFiles {
|
||||
ensureProgramGraph(program);
|
||||
|
||||
let filesToEmit: string[];
|
||||
const changedFiles = createMap<string>();
|
||||
enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ true,
|
||||
// All the changed files are required to get diagnostics
|
||||
(changedFileName, changedFilePath) => addFileForDiagnostics(changedFileName, changedFilePath),
|
||||
// Emitted file is for emit as well as diagnostic
|
||||
(_emitOutput, sourceFile) => {
|
||||
(filesToEmit || (filesToEmit = [])).push(sourceFile.fileName);
|
||||
addFileForDiagnostics(sourceFile.fileName, sourceFile.path);
|
||||
});
|
||||
changedFileNames.clear();
|
||||
return {
|
||||
filesToEmit: filesToEmit || emptyArray,
|
||||
changedFiles: arrayFrom(changedFiles.values())
|
||||
};
|
||||
|
||||
function addFileForDiagnostics(fileName: string, path: Path) {
|
||||
changedFiles.set(path, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
isModuleEmit = undefined;
|
||||
emitHandler = undefined;
|
||||
fileInfos.clear();
|
||||
semanticDiagnosticsPerFile.clear();
|
||||
changedFileNames.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* For script files that contains only ambient external modules, although they are not actually external module files,
|
||||
* they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore,
|
||||
* there are no point to rebuild all script files if these special files have changed. However, if any statement
|
||||
* in the file is not ambient external module, we treat it as a regular script file.
|
||||
*/
|
||||
function containsOnlyAmbientModules(sourceFile: SourceFile) {
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (!isModuleWithStringLiteralName(statement)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean} indicates if the shape signature has changed since last update.
|
||||
*/
|
||||
function updateShapeSignature(program: Program, sourceFile: SourceFile, info: FileInfo) {
|
||||
const prevSignature = info.signature;
|
||||
let latestSignature: string;
|
||||
if (sourceFile.isDeclarationFile) {
|
||||
latestSignature = computeHash(sourceFile.text);
|
||||
info.signature = latestSignature;
|
||||
}
|
||||
else {
|
||||
const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true, /*isDetailed*/ false);
|
||||
if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) {
|
||||
latestSignature = computeHash(emitOutput.outputFiles[0].text);
|
||||
info.signature = latestSignature;
|
||||
}
|
||||
else {
|
||||
latestSignature = prevSignature;
|
||||
}
|
||||
}
|
||||
|
||||
return !prevSignature || latestSignature !== prevSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true
|
||||
*/
|
||||
function getReferencedFiles(program: Program, sourceFile: SourceFile): Map<true> {
|
||||
const referencedFiles = createMap<true>();
|
||||
|
||||
// We need to use a set here since the code can contain the same import twice,
|
||||
// but that will only be one dependency.
|
||||
// To avoid invernal conversion, the key of the referencedFiles map must be of type Path
|
||||
if (sourceFile.imports && sourceFile.imports.length > 0) {
|
||||
const checker: TypeChecker = program.getTypeChecker();
|
||||
for (const importName of sourceFile.imports) {
|
||||
const symbol = checker.getSymbolAtLocation(importName);
|
||||
if (symbol && symbol.declarations && symbol.declarations[0]) {
|
||||
const declarationSourceFile = getSourceFileOfNode(symbol.declarations[0]);
|
||||
if (declarationSourceFile) {
|
||||
referencedFiles.set(declarationSourceFile.path, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sourceFileDirectory = getDirectoryPath(sourceFile.path);
|
||||
// Handle triple slash references
|
||||
if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) {
|
||||
for (const referencedFile of sourceFile.referencedFiles) {
|
||||
const referencedPath = toPath(referencedFile.fileName, sourceFileDirectory, getCanonicalFileName);
|
||||
referencedFiles.set(referencedPath, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle type reference directives
|
||||
if (sourceFile.resolvedTypeReferenceDirectiveNames) {
|
||||
sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => {
|
||||
if (!resolvedTypeReferenceDirective) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = resolvedTypeReferenceDirective.resolvedFileName;
|
||||
const typeFilePath = toPath(fileName, sourceFileDirectory, getCanonicalFileName);
|
||||
referencedFiles.set(typeFilePath, true);
|
||||
});
|
||||
}
|
||||
|
||||
return referencedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the emittable files from the program
|
||||
*/
|
||||
function getAllEmittableFiles(program: Program) {
|
||||
const defaultLibraryFileName = getDefaultLibFileName(program.getCompilerOptions());
|
||||
const sourceFiles = program.getSourceFiles();
|
||||
const result: string[] = [];
|
||||
for (const sourceFile of sourceFiles) {
|
||||
if (getBaseFileName(sourceFile.fileName) !== defaultLibraryFileName && shouldEmitFile(sourceFile)) {
|
||||
result.push(sourceFile.fileName);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getNonModuleEmitHandler(): EmitHandler {
|
||||
return {
|
||||
addScriptInfo: noop,
|
||||
removeScriptInfo: noop,
|
||||
updateScriptInfo: noop,
|
||||
getFilesAffectedByUpdatedShape
|
||||
};
|
||||
|
||||
function getFilesAffectedByUpdatedShape(program: Program, _sourceFile: SourceFile, singleFileResult: string[]): string[] {
|
||||
const options = program.getCompilerOptions();
|
||||
// If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project,
|
||||
// so returning the file itself is good enough.
|
||||
if (options && (options.out || options.outFile)) {
|
||||
return singleFileResult;
|
||||
}
|
||||
return getAllEmittableFiles(program);
|
||||
}
|
||||
}
|
||||
|
||||
function getModuleEmitHandler(): EmitHandler {
|
||||
const references = createMap<Map<true>>();
|
||||
return {
|
||||
addScriptInfo: setReferences,
|
||||
removeScriptInfo,
|
||||
updateScriptInfo: setReferences,
|
||||
getFilesAffectedByUpdatedShape
|
||||
};
|
||||
|
||||
function setReferences(program: Program, sourceFile: SourceFile) {
|
||||
references.set(sourceFile.path, getReferencedFiles(program, sourceFile));
|
||||
}
|
||||
|
||||
function removeScriptInfo(removedFilePath: Path) {
|
||||
// Remove existing references
|
||||
references.forEach((referencesInFile, filePath) => {
|
||||
if (referencesInFile.has(removedFilePath)) {
|
||||
// add files referencing the removedFilePath, as changed files too
|
||||
const referencedByInfo = fileInfos.get(filePath);
|
||||
if (referencedByInfo) {
|
||||
registerChangedFile(filePath as Path, referencedByInfo.fileName);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Delete the entry for the removed file path
|
||||
references.delete(removedFilePath);
|
||||
}
|
||||
|
||||
function getReferencedByPaths(referencedFilePath: Path) {
|
||||
return mapDefinedIter(references.entries(), ([filePath, referencesInFile]) =>
|
||||
referencesInFile.has(referencedFilePath) ? filePath as Path : undefined
|
||||
);
|
||||
}
|
||||
|
||||
function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] {
|
||||
if (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile)) {
|
||||
return getAllEmittableFiles(program);
|
||||
}
|
||||
|
||||
const options = program.getCompilerOptions();
|
||||
if (options && (options.isolatedModules || options.out || options.outFile)) {
|
||||
return singleFileResult;
|
||||
}
|
||||
|
||||
// Now we need to if each file in the referencedBy list has a shape change as well.
|
||||
// Because if so, its own referencedBy files need to be saved as well to make the
|
||||
// emitting result consistent with files on disk.
|
||||
|
||||
const seenFileNamesMap = createMap<string>();
|
||||
const setSeenFileName = (path: Path, sourceFile: SourceFile) => {
|
||||
seenFileNamesMap.set(path, sourceFile && shouldEmitFile(sourceFile) ? sourceFile.fileName : undefined);
|
||||
};
|
||||
|
||||
// Start with the paths this file was referenced by
|
||||
const path = sourceFile.path;
|
||||
setSeenFileName(path, sourceFile);
|
||||
const queue = getReferencedByPaths(path);
|
||||
while (queue.length > 0) {
|
||||
const currentPath = queue.pop();
|
||||
if (!seenFileNamesMap.has(currentPath)) {
|
||||
const currentSourceFile = program.getSourceFileByPath(currentPath);
|
||||
if (currentSourceFile && updateShapeSignature(program, currentSourceFile, fileInfos.get(currentPath))) {
|
||||
queue.push(...getReferencedByPaths(currentPath));
|
||||
}
|
||||
setSeenFileName(currentPath, currentSourceFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Return array of values that needs emit
|
||||
return arrayFrom(seenFileNamesMap.values());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1166,7 +1166,7 @@ namespace ts {
|
|||
}
|
||||
|
||||
function diagnosticName(nameArg: __String | Identifier) {
|
||||
return typeof nameArg === "string" ? unescapeLeadingUnderscores(nameArg as __String) : declarationNameToString(nameArg as Identifier);
|
||||
return isString(nameArg) ? unescapeLeadingUnderscores(nameArg as __String) : declarationNameToString(nameArg as Identifier);
|
||||
}
|
||||
|
||||
function isTypeParameterSymbolDeclaredInContainer(symbol: Symbol, container: Node) {
|
||||
|
|
|
@ -885,7 +885,7 @@ namespace ts {
|
|||
*/
|
||||
export function readConfigFile(fileName: string, readFile: (path: string) => string | undefined): { config?: any; error?: Diagnostic } {
|
||||
const textOrDiagnostic = tryReadFile(fileName, readFile);
|
||||
return typeof textOrDiagnostic === "string" ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic };
|
||||
return isString(textOrDiagnostic) ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -907,7 +907,7 @@ namespace ts {
|
|||
*/
|
||||
export function readJsonConfigFile(fileName: string, readFile: (path: string) => string | undefined): JsonSourceFile {
|
||||
const textOrDiagnostic = tryReadFile(fileName, readFile);
|
||||
return typeof textOrDiagnostic === "string" ? parseJsonText(fileName, textOrDiagnostic) : <JsonSourceFile>{ parseDiagnostics: [textOrDiagnostic] };
|
||||
return isString(textOrDiagnostic) ? parseJsonText(fileName, textOrDiagnostic) : <JsonSourceFile>{ parseDiagnostics: [textOrDiagnostic] };
|
||||
}
|
||||
|
||||
function tryReadFile(fileName: string, readFile: (path: string) => string | undefined): string | Diagnostic {
|
||||
|
@ -1111,9 +1111,9 @@ namespace ts {
|
|||
if (!isDoubleQuotedString(valueExpression)) {
|
||||
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.String_literal_with_double_quotes_expected));
|
||||
}
|
||||
reportInvalidOptionValue(option && (typeof option.type === "string" && option.type !== "string"));
|
||||
reportInvalidOptionValue(option && (isString(option.type) && option.type !== "string"));
|
||||
const text = (<StringLiteral>valueExpression).text;
|
||||
if (option && typeof option.type !== "string") {
|
||||
if (option && !isString(option.type)) {
|
||||
const customOption = <CommandLineOptionOfCustomType>option;
|
||||
// Validate custom option type
|
||||
if (!customOption.type.has(text.toLowerCase())) {
|
||||
|
@ -1184,7 +1184,7 @@ namespace ts {
|
|||
function getCompilerOptionValueTypeString(option: CommandLineOption) {
|
||||
return option.type === "list" ?
|
||||
"Array" :
|
||||
typeof option.type === "string" ? option.type : "string";
|
||||
isString(option.type) ? option.type : "string";
|
||||
}
|
||||
|
||||
function isCompilerOptionsValue(option: CommandLineOption, value: any): value is CompilerOptionsValue {
|
||||
|
@ -1192,7 +1192,7 @@ namespace ts {
|
|||
if (option.type === "list") {
|
||||
return isArray(value);
|
||||
}
|
||||
const expectedType = typeof option.type === "string" ? option.type : "string";
|
||||
const expectedType = isString(option.type) ? option.type : "string";
|
||||
return typeof value === expectedType;
|
||||
}
|
||||
}
|
||||
|
@ -1578,7 +1578,7 @@ namespace ts {
|
|||
let extendedConfigPath: Path;
|
||||
|
||||
if (json.extends) {
|
||||
if (typeof json.extends !== "string") {
|
||||
if (!isString(json.extends)) {
|
||||
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string"));
|
||||
}
|
||||
else {
|
||||
|
@ -1803,7 +1803,7 @@ namespace ts {
|
|||
if (optType === "list" && isArray(value)) {
|
||||
return convertJsonOptionOfListType(<CommandLineOptionOfListType>opt, value, basePath, errors);
|
||||
}
|
||||
else if (typeof optType !== "string") {
|
||||
else if (!isString(optType)) {
|
||||
return convertJsonOptionOfCustomType(<CommandLineOptionOfCustomType>opt, <string>value, errors);
|
||||
}
|
||||
return normalizeNonListOptionValue(opt, basePath, value);
|
||||
|
@ -1816,13 +1816,13 @@ namespace ts {
|
|||
function normalizeOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue {
|
||||
if (option.type === "list") {
|
||||
const listOption = <CommandLineOptionOfListType>option;
|
||||
if (listOption.element.isFilePath || typeof listOption.element.type !== "string") {
|
||||
if (listOption.element.isFilePath || !isString(listOption.element.type)) {
|
||||
return <CompilerOptionsValue>filter(map(value, v => normalizeOptionValue(listOption.element, basePath, v)), v => !!v);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
else if (typeof option.type !== "string") {
|
||||
return option.type.get(typeof value === "string" ? value.toLowerCase() : value);
|
||||
else if (!isString(option.type)) {
|
||||
return option.type.get(isString(value) ? value.toLowerCase() : value);
|
||||
}
|
||||
return normalizeNonListOptionValue(option, basePath, value);
|
||||
}
|
||||
|
@ -1984,7 +1984,7 @@ namespace ts {
|
|||
* @param host The host used to resolve files and directories.
|
||||
* @param extraFileExtensions optionaly file extra file extension information from host
|
||||
*/
|
||||
export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray<JsFileExtensionInfo>): ExpandResult {
|
||||
export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray<JsFileExtensionInfo> = []): ExpandResult {
|
||||
basePath = normalizePath(basePath);
|
||||
|
||||
const keyMapper = host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper;
|
||||
|
|
|
@ -1202,6 +1202,13 @@ namespace ts {
|
|||
return Array.isArray ? Array.isArray(value) : value instanceof Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether a value is string
|
||||
*/
|
||||
export function isString(text: any): text is string {
|
||||
return typeof text === "string";
|
||||
}
|
||||
|
||||
export function tryCast<TOut extends TIn, TIn = any>(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined {
|
||||
return value !== undefined && test(value) ? value : undefined;
|
||||
}
|
||||
|
@ -1212,7 +1219,10 @@ namespace ts {
|
|||
}
|
||||
|
||||
/** Does nothing. */
|
||||
export function noop(): void {}
|
||||
export function noop(): void { }
|
||||
|
||||
/** Do nothing and return false */
|
||||
export function returnFalse(): false { return false; }
|
||||
|
||||
/** Throws an error because a function is not implemented. */
|
||||
export function notImplemented(): never {
|
||||
|
@ -1455,16 +1465,16 @@ namespace ts {
|
|||
function compareMessageText(text1: string | DiagnosticMessageChain, text2: string | DiagnosticMessageChain): Comparison {
|
||||
while (text1 && text2) {
|
||||
// We still have both chains.
|
||||
const string1 = typeof text1 === "string" ? text1 : text1.messageText;
|
||||
const string2 = typeof text2 === "string" ? text2 : text2.messageText;
|
||||
const string1 = isString(text1) ? text1 : text1.messageText;
|
||||
const string2 = isString(text2) ? text2 : text2.messageText;
|
||||
|
||||
const res = compareValues(string1, string2);
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
|
||||
text1 = typeof text1 === "string" ? undefined : text1.next;
|
||||
text2 = typeof text2 === "string" ? undefined : text2.next;
|
||||
text1 = isString(text1) ? undefined : text1.next;
|
||||
text2 = isString(text2) ? undefined : text2.next;
|
||||
}
|
||||
|
||||
if (!text1 && !text2) {
|
||||
|
@ -2066,8 +2076,8 @@ namespace ts {
|
|||
}
|
||||
|
||||
export interface FileSystemEntries {
|
||||
files: ReadonlyArray<string>;
|
||||
directories: ReadonlyArray<string>;
|
||||
readonly files: ReadonlyArray<string>;
|
||||
readonly directories: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
export interface FileMatcherPatterns {
|
||||
|
@ -2620,4 +2630,204 @@ namespace ts {
|
|||
export function isCheckJsEnabledForFile(sourceFile: SourceFile, compilerOptions: CompilerOptions) {
|
||||
return sourceFile.checkJsDirective ? sourceFile.checkJsDirective.enabled : compilerOptions.checkJs;
|
||||
}
|
||||
|
||||
export interface HostForCaching extends PartialSystem {
|
||||
useCaseSensitiveFileNames: boolean;
|
||||
}
|
||||
|
||||
export interface CachedHost {
|
||||
addOrDeleteFileOrFolder(fileOrFolder: string, fileOrFolderPath: Path): void;
|
||||
addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind): void;
|
||||
clearCache(): void;
|
||||
}
|
||||
|
||||
export interface CachedPartialSystem extends PartialSystem, CachedHost {
|
||||
}
|
||||
|
||||
interface MutableFileSystemEntries {
|
||||
readonly files: string[];
|
||||
readonly directories: string[];
|
||||
}
|
||||
|
||||
export function createCachedPartialSystem(host: HostForCaching): CachedPartialSystem {
|
||||
const cachedReadDirectoryResult = createMap<MutableFileSystemEntries>();
|
||||
const getCurrentDirectory = memoize(() => host.getCurrentDirectory());
|
||||
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
|
||||
return {
|
||||
writeFile,
|
||||
fileExists,
|
||||
directoryExists,
|
||||
createDirectory,
|
||||
getCurrentDirectory,
|
||||
getDirectories,
|
||||
readDirectory,
|
||||
addOrDeleteFileOrFolder,
|
||||
addOrDeleteFile,
|
||||
clearCache
|
||||
};
|
||||
|
||||
function toPath(fileName: string) {
|
||||
return ts.toPath(fileName, getCurrentDirectory(), getCanonicalFileName);
|
||||
}
|
||||
|
||||
function getCachedFileSystemEntries(rootDirPath: Path): MutableFileSystemEntries | undefined {
|
||||
return cachedReadDirectoryResult.get(rootDirPath);
|
||||
}
|
||||
|
||||
function getCachedFileSystemEntriesForBaseDir(path: Path): MutableFileSystemEntries | undefined {
|
||||
return getCachedFileSystemEntries(getDirectoryPath(path));
|
||||
}
|
||||
|
||||
function getBaseNameOfFileName(fileName: string) {
|
||||
return getBaseFileName(normalizePath(fileName));
|
||||
}
|
||||
|
||||
function createCachedFileSystemEntries(rootDir: string, rootDirPath: Path) {
|
||||
const resultFromHost: MutableFileSystemEntries = {
|
||||
files: map(host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]), getBaseNameOfFileName) || [],
|
||||
directories: host.getDirectories(rootDir) || []
|
||||
};
|
||||
|
||||
cachedReadDirectoryResult.set(rootDirPath, resultFromHost);
|
||||
return resultFromHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the readDirectory result was already cached, it returns that
|
||||
* Otherwise gets result from host and caches it.
|
||||
* The host request is done under try catch block to avoid caching incorrect result
|
||||
*/
|
||||
function tryReadDirectory(rootDir: string, rootDirPath: Path): MutableFileSystemEntries | undefined {
|
||||
const cachedResult = getCachedFileSystemEntries(rootDirPath);
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
try {
|
||||
return createCachedFileSystemEntries(rootDir, rootDirPath);
|
||||
}
|
||||
catch (_e) {
|
||||
// If there is exception to read directories, dont cache the result and direct the calls to host
|
||||
Debug.assert(!cachedReadDirectoryResult.has(rootDirPath));
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function fileNameEqual(name1: string, name2: string) {
|
||||
return getCanonicalFileName(name1) === getCanonicalFileName(name2);
|
||||
}
|
||||
|
||||
function hasEntry(entries: ReadonlyArray<string>, name: string) {
|
||||
return some(entries, file => fileNameEqual(file, name));
|
||||
}
|
||||
|
||||
function updateFileSystemEntry(entries: string[], baseName: string, isValid: boolean) {
|
||||
if (hasEntry(entries, baseName)) {
|
||||
if (!isValid) {
|
||||
return filterMutate(entries, entry => !fileNameEqual(entry, baseName));
|
||||
}
|
||||
}
|
||||
else if (isValid) {
|
||||
return entries.push(baseName);
|
||||
}
|
||||
}
|
||||
|
||||
function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void {
|
||||
const path = toPath(fileName);
|
||||
const result = getCachedFileSystemEntriesForBaseDir(path);
|
||||
if (result) {
|
||||
updateFilesOfFileSystemEntry(result, getBaseNameOfFileName(fileName), /*fileExists*/ true);
|
||||
}
|
||||
return host.writeFile(fileName, data, writeByteOrderMark);
|
||||
}
|
||||
|
||||
function fileExists(fileName: string): boolean {
|
||||
const path = toPath(fileName);
|
||||
const result = getCachedFileSystemEntriesForBaseDir(path);
|
||||
return result && hasEntry(result.files, getBaseNameOfFileName(fileName)) ||
|
||||
host.fileExists(fileName);
|
||||
}
|
||||
|
||||
function directoryExists(dirPath: string): boolean {
|
||||
const path = toPath(dirPath);
|
||||
return cachedReadDirectoryResult.has(path) || host.directoryExists(dirPath);
|
||||
}
|
||||
|
||||
function createDirectory(dirPath: string) {
|
||||
const path = toPath(dirPath);
|
||||
const result = getCachedFileSystemEntriesForBaseDir(path);
|
||||
const baseFileName = getBaseNameOfFileName(dirPath);
|
||||
if (result) {
|
||||
updateFileSystemEntry(result.directories, baseFileName, /*isValid*/ true);
|
||||
}
|
||||
host.createDirectory(dirPath);
|
||||
}
|
||||
|
||||
function getDirectories(rootDir: string): string[] {
|
||||
const rootDirPath = toPath(rootDir);
|
||||
const result = tryReadDirectory(rootDir, rootDirPath);
|
||||
if (result) {
|
||||
return result.directories.slice();
|
||||
}
|
||||
return host.getDirectories(rootDir);
|
||||
}
|
||||
|
||||
function readDirectory(rootDir: string, extensions?: ReadonlyArray<string>, excludes?: ReadonlyArray<string>, includes?: ReadonlyArray<string>, depth?: number): string[] {
|
||||
const rootDirPath = toPath(rootDir);
|
||||
const result = tryReadDirectory(rootDir, rootDirPath);
|
||||
if (result) {
|
||||
return matchFiles(rootDir, extensions, excludes, includes, host.useCaseSensitiveFileNames, getCurrentDirectory(), depth, getFileSystemEntries);
|
||||
}
|
||||
return host.readDirectory(rootDir, extensions, excludes, includes, depth);
|
||||
|
||||
function getFileSystemEntries(dir: string) {
|
||||
const path = toPath(dir);
|
||||
if (path === rootDirPath) {
|
||||
return result;
|
||||
}
|
||||
return getCachedFileSystemEntries(path) || createCachedFileSystemEntries(dir, path);
|
||||
}
|
||||
}
|
||||
|
||||
function addOrDeleteFileOrFolder(fileOrFolder: string, fileOrFolderPath: Path) {
|
||||
const existingResult = getCachedFileSystemEntries(fileOrFolderPath);
|
||||
if (existingResult) {
|
||||
// This was a folder already present, remove it if this doesnt exist any more
|
||||
if (!host.directoryExists(fileOrFolder)) {
|
||||
cachedReadDirectoryResult.delete(fileOrFolderPath);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// This was earlier a file (hence not in cached directory contents)
|
||||
// or we never cached the directory containing it
|
||||
const parentResult = getCachedFileSystemEntriesForBaseDir(fileOrFolderPath);
|
||||
if (parentResult) {
|
||||
const baseName = getBaseNameOfFileName(fileOrFolder);
|
||||
if (parentResult) {
|
||||
updateFilesOfFileSystemEntry(parentResult, baseName, host.fileExists(fileOrFolderPath));
|
||||
updateFileSystemEntry(parentResult.directories, baseName, host.directoryExists(fileOrFolderPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind) {
|
||||
if (eventKind === FileWatcherEventKind.Changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentResult = getCachedFileSystemEntriesForBaseDir(filePath);
|
||||
if (parentResult) {
|
||||
updateFilesOfFileSystemEntry(parentResult, getBaseNameOfFileName(fileName), eventKind === FileWatcherEventKind.Created);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFilesOfFileSystemEntry(parentResult: MutableFileSystemEntries, baseName: string, fileExists: boolean) {
|
||||
updateFileSystemEntry(parentResult.files, baseName, fileExists);
|
||||
}
|
||||
|
||||
function clearCache() {
|
||||
cachedReadDirectoryResult.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ namespace ts {
|
|||
if (typeof value === "boolean") {
|
||||
return value ? createTrue() : createFalse();
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
if (isString(value)) {
|
||||
return createStringLiteral(value);
|
||||
}
|
||||
return createLiteralFromNode(value);
|
||||
|
@ -2130,7 +2130,7 @@ namespace ts {
|
|||
|
||||
export function createCatchClause(variableDeclaration: string | VariableDeclaration | undefined, block: Block) {
|
||||
const node = <CatchClause>createSynthesizedNode(SyntaxKind.CatchClause);
|
||||
node.variableDeclaration = typeof variableDeclaration === "string" ? createVariableDeclaration(variableDeclaration) : variableDeclaration;
|
||||
node.variableDeclaration = isString(variableDeclaration) ? createVariableDeclaration(variableDeclaration) : variableDeclaration;
|
||||
node.block = block;
|
||||
return node;
|
||||
}
|
||||
|
@ -2438,11 +2438,11 @@ namespace ts {
|
|||
function asName(name: string | EntityName): EntityName;
|
||||
function asName(name: string | Identifier | ThisTypeNode): Identifier | ThisTypeNode;
|
||||
function asName(name: string | Identifier | BindingName | PropertyName | QualifiedName | ThisTypeNode) {
|
||||
return typeof name === "string" ? createIdentifier(name) : name;
|
||||
return isString(name) ? createIdentifier(name) : name;
|
||||
}
|
||||
|
||||
function asExpression(value: string | number | Expression) {
|
||||
return typeof value === "string" || typeof value === "number" ? createLiteral(value) : value;
|
||||
return isString(value) || typeof value === "number" ? createLiteral(value) : value;
|
||||
}
|
||||
|
||||
function asNodeArray<T extends Node>(array: ReadonlyArray<T> | undefined): NodeArray<T> | undefined {
|
||||
|
|
|
@ -94,7 +94,7 @@ namespace ts {
|
|||
}
|
||||
|
||||
const fileName = jsonContent[fieldName];
|
||||
if (typeof fileName !== "string") {
|
||||
if (!isString(fileName)) {
|
||||
if (state.traceEnabled) {
|
||||
trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, fieldName, typeof fileName);
|
||||
}
|
||||
|
@ -659,8 +659,8 @@ namespace ts {
|
|||
}
|
||||
|
||||
if (matchedPattern) {
|
||||
const matchedStar = typeof matchedPattern === "string" ? undefined : matchedText(matchedPattern, moduleName);
|
||||
const matchedPatternText = typeof matchedPattern === "string" ? matchedPattern : patternText(matchedPattern);
|
||||
const matchedStar = isString(matchedPattern) ? undefined : matchedText(matchedPattern, moduleName);
|
||||
const matchedPatternText = isString(matchedPattern) ? matchedPattern : patternText(matchedPattern);
|
||||
if (state.traceEnabled) {
|
||||
trace(state.host, Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPatternText);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/// <reference path="sys.ts" />
|
||||
/// <reference path="emitter.ts" />
|
||||
/// <reference path="core.ts" />
|
||||
/// <reference path="builder.ts" />
|
||||
|
||||
namespace ts {
|
||||
const ignoreDiagnosticCommentRegEx = /(^\s*$)|(^\s*\/\/\/?\s*(@ts-ignore)?)/;
|
||||
|
@ -227,19 +228,25 @@ namespace ts {
|
|||
let output = "";
|
||||
|
||||
for (const diagnostic of diagnostics) {
|
||||
if (diagnostic.file) {
|
||||
const { line, character } = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
|
||||
const fileName = diagnostic.file.fileName;
|
||||
const relativeFileName = convertToRelativePath(fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName));
|
||||
output += `${relativeFileName}(${line + 1},${character + 1}): `;
|
||||
}
|
||||
|
||||
const category = DiagnosticCategory[diagnostic.category].toLowerCase();
|
||||
output += `${category} TS${diagnostic.code}: ${flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine())}${host.getNewLine()}`;
|
||||
output += formatDiagnostic(diagnostic, host);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function formatDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string {
|
||||
const category = DiagnosticCategory[diagnostic.category].toLowerCase();
|
||||
const errorMessage = `${category} TS${diagnostic.code}: ${flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine())}${host.getNewLine()}`;
|
||||
|
||||
if (diagnostic.file) {
|
||||
const { line, character } = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
|
||||
const fileName = diagnostic.file.fileName;
|
||||
const relativeFileName = convertToRelativePath(fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName));
|
||||
return `${relativeFileName}(${line + 1},${character + 1}): ` + errorMessage;
|
||||
}
|
||||
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
const redForegroundEscapeSequence = "\u001b[91m";
|
||||
const yellowForegroundEscapeSequence = "\u001b[93m";
|
||||
const blueForegroundEscapeSequence = "\u001b[93m";
|
||||
|
@ -337,7 +344,7 @@ namespace ts {
|
|||
}
|
||||
|
||||
export function flattenDiagnosticMessageText(messageText: string | DiagnosticMessageChain, newLine: string): string {
|
||||
if (typeof messageText === "string") {
|
||||
if (isString(messageText)) {
|
||||
return messageText;
|
||||
}
|
||||
else {
|
||||
|
@ -386,6 +393,142 @@ namespace ts {
|
|||
allDiagnostics?: Diagnostic[];
|
||||
}
|
||||
|
||||
export function isProgramUptoDate(program: Program | undefined, rootFileNames: string[], newOptions: CompilerOptions,
|
||||
getSourceVersion: (path: Path) => string, fileExists: (fileName: string) => boolean, hasInvalidatedResolution: HasInvalidatedResolution): boolean {
|
||||
// If we haven't create a program yet, then it is not up-to-date
|
||||
if (!program) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If number of files in the program do not match, it is not up-to-date
|
||||
if (program.getRootFileNames().length !== rootFileNames.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If any file is not up-to-date, then the whole program is not up-to-date
|
||||
if (program.getSourceFiles().some(sourceFileNotUptoDate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If any of the missing file paths are now created
|
||||
if (program.getMissingFilePaths().some(fileExists)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentOptions = program.getCompilerOptions();
|
||||
// If the compilation settings do no match, then the program is not up-to-date
|
||||
if (!compareDataObjects(currentOptions, newOptions)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If everything matches but the text of config file is changed,
|
||||
// error locations can change for program options, so update the program
|
||||
if (currentOptions.configFile && newOptions.configFile) {
|
||||
return currentOptions.configFile.text === newOptions.configFile.text;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
function sourceFileNotUptoDate(sourceFile: SourceFile): boolean {
|
||||
return sourceFile.version !== getSourceVersion(sourceFile.path) ||
|
||||
hasInvalidatedResolution(sourceFile.path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determined if source file needs to be re-created even if its text hasnt changed
|
||||
*/
|
||||
function shouldProgramCreateNewSourceFiles(program: Program, newOptions: CompilerOptions) {
|
||||
// If any of these options change, we cant reuse old source file even if version match
|
||||
// The change in options like these could result in change in syntax tree change
|
||||
const oldOptions = program && program.getCompilerOptions();
|
||||
return oldOptions &&
|
||||
(oldOptions.target !== newOptions.target ||
|
||||
oldOptions.module !== newOptions.module ||
|
||||
oldOptions.moduleResolution !== newOptions.moduleResolution ||
|
||||
oldOptions.noResolve !== newOptions.noResolve ||
|
||||
oldOptions.jsx !== newOptions.jsx ||
|
||||
oldOptions.allowJs !== newOptions.allowJs ||
|
||||
oldOptions.disableSizeLimit !== newOptions.disableSizeLimit ||
|
||||
oldOptions.baseUrl !== newOptions.baseUrl ||
|
||||
!equalOwnProperties(oldOptions.paths, newOptions.paths));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the existing missing file watches with the new set of missing files after new program is created
|
||||
*/
|
||||
export function updateMissingFilePathsWatch(
|
||||
program: Program,
|
||||
missingFileWatches: Map<FileWatcher>,
|
||||
createMissingFileWatch: (missingFilePath: Path) => FileWatcher,
|
||||
closeExistingMissingFilePathFileWatcher: (missingFilePath: Path, fileWatcher: FileWatcher) => void
|
||||
) {
|
||||
const missingFilePaths = program.getMissingFilePaths();
|
||||
const newMissingFilePathMap = arrayToSet(missingFilePaths);
|
||||
// Update the missing file paths watcher
|
||||
mutateMap(
|
||||
missingFileWatches,
|
||||
newMissingFilePathMap,
|
||||
{
|
||||
// Watch the missing files
|
||||
createNewValue: createMissingFileWatch,
|
||||
// Files that are no longer missing (e.g. because they are no longer required)
|
||||
// should no longer be watched.
|
||||
onDeleteValue: closeExistingMissingFilePathFileWatcher
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export interface WildcardDirectoryWatcher {
|
||||
watcher: FileWatcher;
|
||||
flags: WatchDirectoryFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the existing wild card directory watches with the new set of wild card directories from the config file
|
||||
* after new program is created because the config file was reloaded or program was created first time from the config file
|
||||
* Note that there is no need to call this function when the program is updated with additional files without reloading config files,
|
||||
* as wildcard directories wont change unless reloading config file
|
||||
*/
|
||||
export function updateWatchingWildcardDirectories(
|
||||
existingWatchedForWildcards: Map<WildcardDirectoryWatcher>,
|
||||
wildcardDirectories: Map<WatchDirectoryFlags>,
|
||||
watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher,
|
||||
closeDirectoryWatcher: (directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatcher, flagsChanged: boolean) => void
|
||||
) {
|
||||
mutateMap(
|
||||
existingWatchedForWildcards,
|
||||
wildcardDirectories,
|
||||
{
|
||||
// Create new watch and recursive info
|
||||
createNewValue: createWildcardDirectoryWatcher,
|
||||
// Close existing watch thats not needed any more
|
||||
onDeleteValue: (directory, wildcardDirectoryWatcher) =>
|
||||
closeDirectoryWatcher(directory, wildcardDirectoryWatcher, /*flagsChanged*/ false),
|
||||
// Close existing watch that doesnt match in the flags
|
||||
onExistingValue: updateWildcardDirectoryWatcher
|
||||
}
|
||||
);
|
||||
|
||||
function createWildcardDirectoryWatcher(directory: string, flags: WatchDirectoryFlags): WildcardDirectoryWatcher {
|
||||
// Create new watch and recursive info
|
||||
return {
|
||||
watcher: watchDirectory(directory, flags),
|
||||
flags
|
||||
};
|
||||
}
|
||||
|
||||
function updateWildcardDirectoryWatcher(directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatcher, flags: WatchDirectoryFlags) {
|
||||
// Watcher needs to be updated if the recursive flags dont match
|
||||
if (wildcardDirectoryWatcher.flags === flags) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeDirectoryWatcher(directory, wildcardDirectoryWatcher, /*flagsChanged*/ true);
|
||||
existingWatchedForWildcards.set(directory, createWildcardDirectoryWatcher(directory, flags));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new 'Program' instance. A Program is an immutable collection of 'SourceFile's and a 'CompilerOptions'
|
||||
* that represent a compilation unit.
|
||||
|
@ -446,6 +589,7 @@ namespace ts {
|
|||
|
||||
let moduleResolutionCache: ModuleResolutionCache;
|
||||
let resolveModuleNamesWorker: (moduleNames: string[], containingFile: string) => ResolvedModuleFull[];
|
||||
const hasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse;
|
||||
if (host.resolveModuleNames) {
|
||||
resolveModuleNamesWorker = (moduleNames, containingFile) => host.resolveModuleNames(checkAllDefined(moduleNames), containingFile).map(resolved => {
|
||||
// An older host may have omitted extension, in which case we should infer it from the file extension of resolvedFileName.
|
||||
|
@ -482,10 +626,12 @@ namespace ts {
|
|||
let redirectTargetsSet = createMap<true>();
|
||||
|
||||
const filesByName = createMap<SourceFile | undefined>();
|
||||
let missingFilePaths: ReadonlyArray<Path>;
|
||||
// stores 'filename -> file association' ignoring case
|
||||
// used to track cases when two file names differ only in casing
|
||||
const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createMap<SourceFile>() : undefined;
|
||||
|
||||
const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options);
|
||||
const structuralIsReused = tryReuseStructureFromOldProgram();
|
||||
if (structuralIsReused !== StructureIsReused.Completely) {
|
||||
forEach(rootNames, name => processRootFile(name, /*isDefaultLib*/ false));
|
||||
|
@ -520,13 +666,26 @@ namespace ts {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
missingFilePaths = arrayFrom(filesByName.keys(), p => <Path>p).filter(p => !filesByName.get(p));
|
||||
}
|
||||
|
||||
const missingFilePaths = arrayFrom(filesByName.keys(), p => <Path>p).filter(p => !filesByName.get(p));
|
||||
Debug.assert(!!missingFilePaths);
|
||||
|
||||
// unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks
|
||||
moduleResolutionCache = undefined;
|
||||
|
||||
// Release any files we have acquired in the old program but are
|
||||
// not part of the new program.
|
||||
if (oldProgram && host.onReleaseOldSourceFile) {
|
||||
const oldSourceFiles = oldProgram.getSourceFiles();
|
||||
for (const oldSourceFile of oldSourceFiles) {
|
||||
if (!getSourceFile(oldSourceFile.path) || shouldCreateNewSourceFile) {
|
||||
host.onReleaseOldSourceFile(oldSourceFile, oldProgram.getCompilerOptions());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unconditionally set oldProgram to undefined to prevent it from being captured in closure
|
||||
oldProgram = undefined;
|
||||
|
||||
|
@ -682,7 +841,7 @@ namespace ts {
|
|||
trace(host, Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, containingFile);
|
||||
}
|
||||
}
|
||||
else {
|
||||
else if (!hasInvalidatedResolution(oldProgramState.file.path)) {
|
||||
resolvesToAmbientModuleInNonModifiedFile = moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName, oldProgramState);
|
||||
}
|
||||
|
||||
|
@ -784,14 +943,21 @@ namespace ts {
|
|||
const modifiedSourceFiles: { oldFile: SourceFile, newFile: SourceFile }[] = [];
|
||||
oldProgram.structureIsReused = StructureIsReused.Completely;
|
||||
|
||||
// If the missing file paths are now present, it can change the progam structure,
|
||||
// and hence cant reuse the structure.
|
||||
// This is same as how we dont reuse the structure if one of the file from old program is now missing
|
||||
if (oldProgram.getMissingFilePaths().some(missingFilePath => host.fileExists(missingFilePath))) {
|
||||
return oldProgram.structureIsReused = StructureIsReused.Not;
|
||||
}
|
||||
|
||||
const oldSourceFiles = oldProgram.getSourceFiles();
|
||||
const enum SeenPackageName { Exists, Modified }
|
||||
const seenPackageNames = createMap<SeenPackageName>();
|
||||
|
||||
for (const oldSourceFile of oldSourceFiles) {
|
||||
let newSourceFile = host.getSourceFileByPath
|
||||
? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target)
|
||||
: host.getSourceFile(oldSourceFile.fileName, options.target);
|
||||
? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target, /*onError*/ undefined, shouldCreateNewSourceFile)
|
||||
: host.getSourceFile(oldSourceFile.fileName, options.target, /*onError*/ undefined, shouldCreateNewSourceFile);
|
||||
|
||||
if (!newSourceFile) {
|
||||
return oldProgram.structureIsReused = StructureIsReused.Not;
|
||||
|
@ -874,6 +1040,13 @@ namespace ts {
|
|||
// tentatively approve the file
|
||||
modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile });
|
||||
}
|
||||
else if (hasInvalidatedResolution(oldSourceFile.path)) {
|
||||
// 'module/types' references could have changed
|
||||
oldProgram.structureIsReused = StructureIsReused.SafeModules;
|
||||
|
||||
// add file to the modified list so that we will resolve it later
|
||||
modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile });
|
||||
}
|
||||
|
||||
// if file has passed all checks it should be safe to reuse it
|
||||
newSourceFiles.push(newSourceFile);
|
||||
|
@ -920,20 +1093,7 @@ namespace ts {
|
|||
return oldProgram.structureIsReused;
|
||||
}
|
||||
|
||||
// If a file has ceased to be missing, then we need to discard some of the old
|
||||
// structure in order to pick it up.
|
||||
// Caution: if the file has created and then deleted between since it was discovered to
|
||||
// be missing, then the corresponding file watcher will have been closed and no new one
|
||||
// will be created until we encounter a change that prevents complete structure reuse.
|
||||
// During this interval, creation of the file will go unnoticed. We expect this to be
|
||||
// both rare and low-impact.
|
||||
if (oldProgram.getMissingFilePaths().some(missingFilePath => host.fileExists(missingFilePath))) {
|
||||
return oldProgram.structureIsReused = StructureIsReused.SafeModules;
|
||||
}
|
||||
|
||||
for (const p of oldProgram.getMissingFilePaths()) {
|
||||
filesByName.set(p, undefined);
|
||||
}
|
||||
missingFilePaths = oldProgram.getMissingFilePaths();
|
||||
|
||||
// update fileName -> file mapping
|
||||
for (let i = 0; i < newSourceFiles.length; i++) {
|
||||
|
@ -1670,7 +1830,7 @@ namespace ts {
|
|||
else {
|
||||
fileProcessingDiagnostics.add(createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, hostErrorMessage));
|
||||
}
|
||||
});
|
||||
}, shouldCreateNewSourceFile);
|
||||
|
||||
if (packageId) {
|
||||
const packageIdKey = `${packageId.name}@${packageId.version}`;
|
||||
|
|
304
src/compiler/resolutionCache.ts
Normal file
304
src/compiler/resolutionCache.ts
Normal file
|
@ -0,0 +1,304 @@
|
|||
/// <reference path="types.ts"/>
|
||||
/// <reference path="core.ts"/>
|
||||
|
||||
/*@internal*/
|
||||
namespace ts {
|
||||
/** This is the cache of module/typedirectives resolution that can be retained across program */
|
||||
export interface ResolutionCache {
|
||||
setModuleResolutionHost(host: ModuleResolutionHost): void;
|
||||
|
||||
startRecordingFilesWithChangedResolutions(): void;
|
||||
finishRecordingFilesWithChangedResolutions(): Path[];
|
||||
|
||||
resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[];
|
||||
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
|
||||
|
||||
invalidateResolutionOfFile(filePath: Path): void;
|
||||
invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath: Path): void;
|
||||
|
||||
createHasInvalidatedResolution(): HasInvalidatedResolution;
|
||||
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
interface NameResolutionWithFailedLookupLocations {
|
||||
readonly failedLookupLocations: ReadonlyArray<string>;
|
||||
isInvalidated?: boolean;
|
||||
}
|
||||
|
||||
interface FailedLookupLocationsWatcher {
|
||||
fileWatcher: FileWatcher;
|
||||
refCount: number;
|
||||
}
|
||||
|
||||
export function createResolutionCache(
|
||||
toPath: (fileName: string) => Path,
|
||||
getCompilerOptions: () => CompilerOptions,
|
||||
watchForFailedLookupLocation: (failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) => FileWatcher,
|
||||
log: (s: string) => void,
|
||||
projectName?: string,
|
||||
getGlobalCache?: () => string | undefined): ResolutionCache {
|
||||
|
||||
let host: ModuleResolutionHost;
|
||||
let filesWithChangedSetOfUnresolvedImports: Path[] | undefined;
|
||||
let filesWithInvalidatedResolutions: Map<true> | undefined;
|
||||
|
||||
// The resolvedModuleNames and resolvedTypeReferenceDirectives are the cache of resolutions per file.
|
||||
// The key in the map is source file's path.
|
||||
// The values are Map of resolutions with key being name lookedup.
|
||||
const resolvedModuleNames = createMap<Map<ResolvedModuleWithFailedLookupLocations>>();
|
||||
const resolvedTypeReferenceDirectives = createMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
|
||||
|
||||
const failedLookupLocationsWatches = createMap<FailedLookupLocationsWatcher>();
|
||||
|
||||
return {
|
||||
setModuleResolutionHost,
|
||||
startRecordingFilesWithChangedResolutions,
|
||||
finishRecordingFilesWithChangedResolutions,
|
||||
resolveModuleNames,
|
||||
resolveTypeReferenceDirectives,
|
||||
invalidateResolutionOfFile,
|
||||
invalidateResolutionOfChangedFailedLookupLocation,
|
||||
createHasInvalidatedResolution,
|
||||
clear
|
||||
};
|
||||
|
||||
function setModuleResolutionHost(updatedHost: ModuleResolutionHost) {
|
||||
host = updatedHost;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
// Close all the watches for failed lookup locations, irrespective of refcounts for them since this is to clear the cache
|
||||
clearMap(failedLookupLocationsWatches, (failedLookupLocationPath, failedLookupLocationWatcher) => {
|
||||
log(`Watcher: FailedLookupLocations: Status: ForceClose: LocationPath: ${failedLookupLocationPath}, refCount: ${failedLookupLocationWatcher.refCount}`);
|
||||
failedLookupLocationWatcher.fileWatcher.close();
|
||||
});
|
||||
resolvedModuleNames.clear();
|
||||
resolvedTypeReferenceDirectives.clear();
|
||||
}
|
||||
|
||||
function startRecordingFilesWithChangedResolutions() {
|
||||
filesWithChangedSetOfUnresolvedImports = [];
|
||||
}
|
||||
|
||||
function finishRecordingFilesWithChangedResolutions() {
|
||||
const collected = filesWithChangedSetOfUnresolvedImports;
|
||||
filesWithChangedSetOfUnresolvedImports = undefined;
|
||||
return collected;
|
||||
}
|
||||
|
||||
function createHasInvalidatedResolution(): HasInvalidatedResolution {
|
||||
const collected = filesWithInvalidatedResolutions;
|
||||
filesWithInvalidatedResolutions = undefined;
|
||||
return path => collected && collected.has(path);
|
||||
}
|
||||
|
||||
function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
|
||||
const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host);
|
||||
// return result immediately only if global cache support is not enabled or if it is .ts, .tsx or .d.ts
|
||||
if (!getGlobalCache) {
|
||||
return primaryResult;
|
||||
}
|
||||
|
||||
// otherwise try to load typings from @types
|
||||
const globalCache = getGlobalCache();
|
||||
if (globalCache !== undefined && !isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension))) {
|
||||
// create different collection of failed lookup locations for second pass
|
||||
// if it will fail and we've already found something during the first pass - we don't want to pollute its results
|
||||
const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, projectName, compilerOptions, host, globalCache);
|
||||
if (resolvedModule) {
|
||||
return { resolvedModule, failedLookupLocations: addRange(primaryResult.failedLookupLocations as Array<string>, failedLookupLocations) };
|
||||
}
|
||||
}
|
||||
|
||||
// Default return the result from the first pass
|
||||
return primaryResult;
|
||||
}
|
||||
|
||||
function resolveNamesWithLocalCache<T extends NameResolutionWithFailedLookupLocations, R>(
|
||||
names: string[],
|
||||
containingFile: string,
|
||||
cache: Map<Map<T>>,
|
||||
loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T,
|
||||
getResult: (s: T) => R,
|
||||
getResultFileName: (result: R) => string | undefined,
|
||||
logChanges: boolean): R[] {
|
||||
|
||||
const path = toPath(containingFile);
|
||||
const currentResolutionsInFile = cache.get(path);
|
||||
|
||||
const newResolutions: Map<T> = createMap<T>();
|
||||
const resolvedModules: R[] = [];
|
||||
const compilerOptions = getCompilerOptions();
|
||||
|
||||
for (const name of names) {
|
||||
// check if this is a duplicate entry in the list
|
||||
let resolution = newResolutions.get(name);
|
||||
if (!resolution) {
|
||||
const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name);
|
||||
if (moduleResolutionIsValid(existingResolution)) {
|
||||
// ok, it is safe to use existing name resolution results
|
||||
resolution = existingResolution;
|
||||
}
|
||||
else {
|
||||
resolution = loader(name, containingFile, compilerOptions, host);
|
||||
updateFailedLookupLocationWatches(containingFile, name, existingResolution && existingResolution.failedLookupLocations, resolution.failedLookupLocations);
|
||||
}
|
||||
newResolutions.set(name, resolution);
|
||||
if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) {
|
||||
filesWithChangedSetOfUnresolvedImports.push(path);
|
||||
// reset log changes to avoid recording the same file multiple times
|
||||
logChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.assert(resolution !== undefined);
|
||||
|
||||
resolvedModules.push(getResult(resolution));
|
||||
}
|
||||
|
||||
// replace old results with a new one
|
||||
cache.set(path, newResolutions);
|
||||
return resolvedModules;
|
||||
|
||||
function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean {
|
||||
if (oldResolution === newResolution) {
|
||||
return true;
|
||||
}
|
||||
if (!oldResolution || !newResolution || oldResolution.isInvalidated) {
|
||||
return false;
|
||||
}
|
||||
const oldResult = getResult(oldResolution);
|
||||
const newResult = getResult(newResolution);
|
||||
if (oldResult === newResult) {
|
||||
return true;
|
||||
}
|
||||
if (!oldResult || !newResult) {
|
||||
return false;
|
||||
}
|
||||
return getResultFileName(oldResult) === getResultFileName(newResult);
|
||||
}
|
||||
|
||||
function moduleResolutionIsValid(resolution: T): boolean {
|
||||
if (!resolution || resolution.isInvalidated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = getResult(resolution);
|
||||
if (result) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// consider situation if we have no candidate locations as valid resolution.
|
||||
// after all there is no point to invalidate it if we have no idea where to look for the module.
|
||||
return resolution.failedLookupLocations.length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] {
|
||||
return resolveNamesWithLocalCache(typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, resolveTypeReferenceDirective,
|
||||
m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false);
|
||||
}
|
||||
|
||||
function resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[] {
|
||||
return resolveNamesWithLocalCache(moduleNames, containingFile, resolvedModuleNames, resolveModuleName,
|
||||
m => m.resolvedModule, r => r.resolvedFileName, logChanges);
|
||||
}
|
||||
|
||||
function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) {
|
||||
const failedLookupLocationWatcher = failedLookupLocationsWatches.get(failedLookupLocationPath);
|
||||
if (failedLookupLocationWatcher) {
|
||||
failedLookupLocationWatcher.refCount++;
|
||||
log(`Watcher: FailedLookupLocations: Status: Using existing watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name} refCount: ${failedLookupLocationWatcher.refCount}`);
|
||||
}
|
||||
else {
|
||||
log(`Watcher: FailedLookupLocations: Status: new watch: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`);
|
||||
const fileWatcher = watchForFailedLookupLocation(failedLookupLocation, failedLookupLocationPath, containingFile, name);
|
||||
failedLookupLocationsWatches.set(failedLookupLocationPath, { fileWatcher, refCount: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
function closeFailedLookupLocationWatcher(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) {
|
||||
const failedLookupLocationWatcher = failedLookupLocationsWatches.get(failedLookupLocationPath);
|
||||
Debug.assert(!!failedLookupLocationWatcher);
|
||||
failedLookupLocationWatcher.refCount--;
|
||||
if (failedLookupLocationWatcher.refCount) {
|
||||
log(`Watcher: FailedLookupLocations: Status: Removing existing watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}: refCount: ${failedLookupLocationWatcher.refCount}`);
|
||||
}
|
||||
else {
|
||||
log(`Watcher: FailedLookupLocations: Status: Closing the file watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`);
|
||||
failedLookupLocationWatcher.fileWatcher.close();
|
||||
failedLookupLocationsWatches.delete(failedLookupLocationPath);
|
||||
}
|
||||
}
|
||||
|
||||
type FailedLookupLocationAction = (failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) => void;
|
||||
function withFailedLookupLocations(failedLookupLocations: ReadonlyArray<string>, containingFile: string, name: string, fn: FailedLookupLocationAction) {
|
||||
forEach(failedLookupLocations, failedLookupLocation => {
|
||||
fn(failedLookupLocation, toPath(failedLookupLocation), containingFile, name);
|
||||
});
|
||||
}
|
||||
|
||||
function updateFailedLookupLocationWatches(containingFile: string, name: string, existingFailedLookupLocations: ReadonlyArray<string> | undefined, failedLookupLocations: ReadonlyArray<string>) {
|
||||
// Watch all the failed lookup locations
|
||||
withFailedLookupLocations(failedLookupLocations, containingFile, name, watchFailedLookupLocation);
|
||||
|
||||
// Close existing watches for the failed locations
|
||||
withFailedLookupLocations(existingFailedLookupLocations, containingFile, name, closeFailedLookupLocationWatcher);
|
||||
}
|
||||
|
||||
function invalidateResolutionCacheOfDeletedFile<T extends NameResolutionWithFailedLookupLocations, R>(
|
||||
deletedFilePath: Path,
|
||||
cache: Map<Map<T>>,
|
||||
getResult: (s: T) => R,
|
||||
getResultFileName: (result: R) => string | undefined) {
|
||||
cache.forEach((value, path) => {
|
||||
if (path === deletedFilePath) {
|
||||
cache.delete(path);
|
||||
value.forEach((resolution, name) => {
|
||||
withFailedLookupLocations(resolution.failedLookupLocations, path, name, closeFailedLookupLocationWatcher);
|
||||
});
|
||||
}
|
||||
else if (value) {
|
||||
value.forEach(resolution => {
|
||||
if (resolution && !resolution.isInvalidated) {
|
||||
const result = getResult(resolution);
|
||||
if (result) {
|
||||
if (toPath(getResultFileName(result)) === deletedFilePath) {
|
||||
resolution.isInvalidated = true;
|
||||
(filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap<true>())).set(path, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function invalidateResolutionCacheOfChangedFailedLookupLocation<T extends NameResolutionWithFailedLookupLocations>(
|
||||
failedLookupLocationPath: Path,
|
||||
cache: Map<Map<T>>) {
|
||||
cache.forEach((value, containingFile) => {
|
||||
if (value) {
|
||||
value.forEach(resolution => {
|
||||
if (resolution && !resolution.isInvalidated && some(resolution.failedLookupLocations, location => toPath(location) === failedLookupLocationPath)) {
|
||||
// Mark the file as needing re-evaluation of module resolution instead of using it blindly.
|
||||
resolution.isInvalidated = true;
|
||||
(filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap<true>())).set(containingFile, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function invalidateResolutionOfFile(filePath: Path) {
|
||||
invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName);
|
||||
invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName);
|
||||
}
|
||||
|
||||
function invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath: Path) {
|
||||
invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocationPath, resolvedModuleNames);
|
||||
invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocationPath, resolvedTypeReferenceDirectives);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,14 +30,26 @@ namespace ts {
|
|||
mtime?: Date;
|
||||
}
|
||||
|
||||
export interface System {
|
||||
/**
|
||||
* Partial interface of the System thats needed to support the caching of directory structure
|
||||
*/
|
||||
export interface PartialSystem {
|
||||
writeFile(path: string, data: string, writeByteOrderMark?: boolean): void;
|
||||
fileExists(path: string): boolean;
|
||||
directoryExists(path: string): boolean;
|
||||
createDirectory(path: string): void;
|
||||
getCurrentDirectory(): string;
|
||||
getDirectories(path: string): string[];
|
||||
readDirectory(path: string, extensions?: ReadonlyArray<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, depth?: number): string[];
|
||||
}
|
||||
|
||||
export interface System extends PartialSystem {
|
||||
args: string[];
|
||||
newLine: string;
|
||||
useCaseSensitiveFileNames: boolean;
|
||||
write(s: string): void;
|
||||
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
|
||||
|
@ -45,13 +57,7 @@ namespace ts {
|
|||
watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher;
|
||||
watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher;
|
||||
resolvePath(path: string): string;
|
||||
fileExists(path: string): boolean;
|
||||
directoryExists(path: string): boolean;
|
||||
createDirectory(path: string): void;
|
||||
getExecutingFilePath(): string;
|
||||
getCurrentDirectory(): string;
|
||||
getDirectories(path: string): string[];
|
||||
readDirectory(path: string, extensions?: ReadonlyArray<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, depth?: number): string[];
|
||||
getModifiedTime?(path: string): Date;
|
||||
/**
|
||||
* This should be cryptographically secure.
|
||||
|
@ -184,7 +190,7 @@ namespace ts {
|
|||
|
||||
function fileEventHandler(eventName: string, relativeFileName: string, baseDirPath: string) {
|
||||
// When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined"
|
||||
const fileName = typeof relativeFileName !== "string"
|
||||
const fileName = !isString(relativeFileName)
|
||||
? undefined
|
||||
: ts.getNormalizedAbsolutePath(relativeFileName, baseDirPath);
|
||||
// Some applications save a working file via rename operations
|
||||
|
|
|
@ -1,48 +1,13 @@
|
|||
/// <reference path="program.ts"/>
|
||||
/// <reference path="watchedProgram.ts"/>
|
||||
/// <reference path="commandLineParser.ts"/>
|
||||
|
||||
namespace ts {
|
||||
export interface SourceFile {
|
||||
fileWatcher?: FileWatcher;
|
||||
}
|
||||
|
||||
interface Statistic {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = {
|
||||
getCurrentDirectory: () => sys.getCurrentDirectory(),
|
||||
getNewLine: () => sys.newLine,
|
||||
getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames)
|
||||
};
|
||||
|
||||
let reportDiagnosticWorker = reportDiagnosticSimply;
|
||||
|
||||
function reportDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost) {
|
||||
reportDiagnosticWorker(diagnostic, host || defaultFormatDiagnosticsHost);
|
||||
}
|
||||
|
||||
function reportDiagnostics(diagnostics: Diagnostic[], host: FormatDiagnosticsHost): void {
|
||||
for (const diagnostic of diagnostics) {
|
||||
reportDiagnostic(diagnostic, host);
|
||||
}
|
||||
}
|
||||
|
||||
function reportEmittedFiles(files: string[]): void {
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDir = sys.getCurrentDirectory();
|
||||
|
||||
for (const file of files) {
|
||||
const filepath = getNormalizedAbsolutePath(file, currentDir);
|
||||
|
||||
sys.write(`TSFILE: ${filepath}${sys.newLine}`);
|
||||
}
|
||||
}
|
||||
|
||||
function countLines(program: Program): number {
|
||||
let count = 0;
|
||||
forEach(program.getSourceFiles(), file => {
|
||||
|
@ -56,25 +21,11 @@ namespace ts {
|
|||
return <string>diagnostic.messageText;
|
||||
}
|
||||
|
||||
function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHost): void {
|
||||
sys.write(ts.formatDiagnostics([diagnostic], host));
|
||||
}
|
||||
|
||||
function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHost): void {
|
||||
sys.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + sys.newLine);
|
||||
}
|
||||
|
||||
function reportWatchDiagnostic(diagnostic: Diagnostic) {
|
||||
let output = new Date().toLocaleTimeString() + " - ";
|
||||
|
||||
if (diagnostic.file) {
|
||||
const loc = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
|
||||
output += `${ diagnostic.file.fileName }(${ loc.line + 1 },${ loc.character + 1 }): `;
|
||||
let reportDiagnostic = createDiagnosticReporter(sys, reportDiagnosticSimply);
|
||||
function udpateReportDiagnostic(options: CompilerOptions) {
|
||||
if (options.pretty) {
|
||||
reportDiagnostic = createDiagnosticReporter(sys, reportDiagnosticWithColorAndContext);
|
||||
}
|
||||
|
||||
output += `${ flattenDiagnosticMessageText(diagnostic.messageText, sys.newLine) }${ sys.newLine + sys.newLine + sys.newLine }`;
|
||||
|
||||
sys.write(output);
|
||||
}
|
||||
|
||||
function padLeft(s: string, length: number) {
|
||||
|
@ -98,26 +49,12 @@ namespace ts {
|
|||
|
||||
export function executeCommandLine(args: string[]): void {
|
||||
const commandLine = parseCommandLine(args);
|
||||
let configFileName: string; // Configuration file name (if any)
|
||||
let cachedConfigFileText: string; // Cached configuration file text, used for reparsing (if any)
|
||||
let configFileWatcher: FileWatcher; // Configuration file watcher
|
||||
let directoryWatcher: FileWatcher; // Directory watcher to monitor source file addition/removal
|
||||
let cachedProgram: Program; // Program cached from last compilation
|
||||
let rootFileNames: string[]; // Root fileNames for compilation
|
||||
let compilerOptions: CompilerOptions; // Compiler options for compilation
|
||||
let compilerHost: CompilerHost; // Compiler host
|
||||
let hostGetSourceFile: typeof compilerHost.getSourceFile; // getSourceFile method from default host
|
||||
let timerHandleForRecompilation: any; // Handle for 0.25s wait timer to trigger recompilation
|
||||
let timerHandleForDirectoryChanges: any; // Handle for 0.25s wait timer to trigger directory change handler
|
||||
|
||||
// This map stores and reuses results of fileExists check that happen inside 'createProgram'
|
||||
// This allows to save time in module resolution heavy scenarios when existence of the same file might be checked multiple times.
|
||||
let cachedExistingFiles: Map<boolean>;
|
||||
let hostFileExists: typeof compilerHost.fileExists;
|
||||
|
||||
// Configuration file name (if any)
|
||||
let configFileName: string;
|
||||
if (commandLine.options.locale) {
|
||||
if (!isJSONSupported()) {
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--locale"), /* host */ undefined);
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--locale"));
|
||||
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
}
|
||||
validateLocaleAndSetLanguage(commandLine.options.locale, sys, commandLine.errors);
|
||||
|
@ -126,7 +63,7 @@ namespace ts {
|
|||
// If there are any errors due to command line parsing and/or
|
||||
// setting up localization, report them and quit.
|
||||
if (commandLine.errors.length > 0) {
|
||||
reportDiagnostics(commandLine.errors, compilerHost);
|
||||
reportDiagnostics(commandLine.errors, reportDiagnostic);
|
||||
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
}
|
||||
|
||||
|
@ -148,11 +85,11 @@ namespace ts {
|
|||
|
||||
if (commandLine.options.project) {
|
||||
if (!isJSONSupported()) {
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--project"), /* host */ undefined);
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--project"));
|
||||
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
}
|
||||
if (commandLine.fileNames.length !== 0) {
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_project_cannot_be_mixed_with_source_files_on_a_command_line), /* host */ undefined);
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_project_cannot_be_mixed_with_source_files_on_a_command_line));
|
||||
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
}
|
||||
|
||||
|
@ -160,14 +97,14 @@ namespace ts {
|
|||
if (!fileOrDirectory /* current directory "." */ || sys.directoryExists(fileOrDirectory)) {
|
||||
configFileName = combinePaths(fileOrDirectory, "tsconfig.json");
|
||||
if (!sys.fileExists(configFileName)) {
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Cannot_find_a_tsconfig_json_file_at_the_specified_directory_Colon_0, commandLine.options.project), /* host */ undefined);
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Cannot_find_a_tsconfig_json_file_at_the_specified_directory_Colon_0, commandLine.options.project));
|
||||
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
}
|
||||
}
|
||||
else {
|
||||
configFileName = fileOrDirectory;
|
||||
if (!sys.fileExists(configFileName)) {
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_specified_path_does_not_exist_Colon_0, commandLine.options.project), /* host */ undefined);
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_specified_path_does_not_exist_Colon_0, commandLine.options.project));
|
||||
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
}
|
||||
}
|
||||
|
@ -183,249 +120,94 @@ namespace ts {
|
|||
return sys.exit(ExitStatus.Success);
|
||||
}
|
||||
|
||||
if (isWatchSet(commandLine.options)) {
|
||||
if (!sys.watchFile) {
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"), /* host */ undefined);
|
||||
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
}
|
||||
if (configFileName) {
|
||||
configFileWatcher = sys.watchFile(configFileName, configFileChanged);
|
||||
}
|
||||
if (sys.watchDirectory && configFileName) {
|
||||
const directory = ts.getDirectoryPath(configFileName);
|
||||
directoryWatcher = sys.watchDirectory(
|
||||
// When the configFileName is just "tsconfig.json", the watched directory should be
|
||||
// the current directory; if there is a given "project" parameter, then the configFileName
|
||||
// is an absolute file name.
|
||||
directory === "" ? "." : directory,
|
||||
watchedDirectoryChanged, /*recursive*/ true);
|
||||
}
|
||||
}
|
||||
|
||||
performCompilation();
|
||||
|
||||
function parseConfigFile(): ParsedCommandLine {
|
||||
if (!cachedConfigFileText) {
|
||||
try {
|
||||
cachedConfigFileText = sys.readFile(configFileName);
|
||||
}
|
||||
catch (e) {
|
||||
const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message);
|
||||
reportWatchDiagnostic(error);
|
||||
sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!cachedConfigFileText) {
|
||||
const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName);
|
||||
reportDiagnostics([error], /* compilerHost */ undefined);
|
||||
sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = parseJsonText(configFileName, cachedConfigFileText);
|
||||
reportDiagnostics(result.parseDiagnostics, /* compilerHost */ undefined);
|
||||
|
||||
const cwd = sys.getCurrentDirectory();
|
||||
const configParseResult = parseJsonSourceFileConfigFileContent(result, sys, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), commandLine.options, getNormalizedAbsolutePath(configFileName, cwd));
|
||||
reportDiagnostics(configParseResult.errors, /* compilerHost */ undefined);
|
||||
|
||||
const commandLineOptions = commandLine.options;
|
||||
if (configFileName) {
|
||||
const reportWatchDiagnostic = createWatchDiagnosticReporter();
|
||||
const configParseResult = parseConfigFile(configFileName, commandLineOptions, sys, reportDiagnostic, reportWatchDiagnostic);
|
||||
udpateReportDiagnostic(configParseResult.options);
|
||||
if (isWatchSet(configParseResult.options)) {
|
||||
if (!sys.watchFile) {
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"), /* host */ undefined);
|
||||
sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
}
|
||||
|
||||
if (!directoryWatcher && sys.watchDirectory && configFileName) {
|
||||
const directory = ts.getDirectoryPath(configFileName);
|
||||
directoryWatcher = sys.watchDirectory(
|
||||
// When the configFileName is just "tsconfig.json", the watched directory should be
|
||||
// the current directory; if there is a given "project" parameter, then the configFileName
|
||||
// is an absolute file name.
|
||||
directory === "" ? "." : directory,
|
||||
watchedDirectoryChanged, /*recursive*/ true);
|
||||
}
|
||||
reportWatchModeWithoutSysSupport();
|
||||
createWatchModeWithConfigFile(configParseResult, commandLineOptions, createWatchingSystemHost(reportWatchDiagnostic));
|
||||
}
|
||||
return configParseResult;
|
||||
}
|
||||
|
||||
// Invoked to perform initial compilation or re-compilation in watch mode
|
||||
function performCompilation() {
|
||||
|
||||
if (!cachedProgram) {
|
||||
if (configFileName) {
|
||||
const configParseResult = parseConfigFile();
|
||||
rootFileNames = configParseResult.fileNames;
|
||||
compilerOptions = configParseResult.options;
|
||||
}
|
||||
else {
|
||||
rootFileNames = commandLine.fileNames;
|
||||
compilerOptions = commandLine.options;
|
||||
}
|
||||
compilerHost = createCompilerHost(compilerOptions);
|
||||
hostGetSourceFile = compilerHost.getSourceFile;
|
||||
compilerHost.getSourceFile = getSourceFile;
|
||||
|
||||
hostFileExists = compilerHost.fileExists;
|
||||
compilerHost.fileExists = cachedFileExists;
|
||||
}
|
||||
|
||||
if (compilerOptions.pretty) {
|
||||
reportDiagnosticWorker = reportDiagnosticWithColorAndContext;
|
||||
}
|
||||
|
||||
// reset the cache of existing files
|
||||
cachedExistingFiles = createMap<boolean>();
|
||||
|
||||
const compileResult = compile(rootFileNames, compilerOptions, compilerHost);
|
||||
|
||||
if (!isWatchSet(compilerOptions)) {
|
||||
return sys.exit(compileResult.exitStatus);
|
||||
}
|
||||
|
||||
setCachedProgram(compileResult.program);
|
||||
reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes));
|
||||
|
||||
const missingPaths = compileResult.program.getMissingFilePaths();
|
||||
missingPaths.forEach(path => {
|
||||
const fileWatcher = sys.watchFile(path, (_fileName, eventKind) => {
|
||||
if (eventKind === FileWatcherEventKind.Created) {
|
||||
fileWatcher.close();
|
||||
startTimerForRecompilation();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cachedFileExists(fileName: string): boolean {
|
||||
let fileExists = cachedExistingFiles.get(fileName);
|
||||
if (fileExists === undefined) {
|
||||
cachedExistingFiles.set(fileName, fileExists = hostFileExists(fileName));
|
||||
}
|
||||
return fileExists;
|
||||
}
|
||||
|
||||
function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void) {
|
||||
// Return existing SourceFile object if one is available
|
||||
if (cachedProgram) {
|
||||
const sourceFile = cachedProgram.getSourceFile(fileName);
|
||||
// A modified source file has no watcher and should not be reused
|
||||
if (sourceFile && sourceFile.fileWatcher) {
|
||||
return sourceFile;
|
||||
}
|
||||
}
|
||||
// Use default host function
|
||||
const sourceFile = hostGetSourceFile(fileName, languageVersion, onError);
|
||||
if (sourceFile && isWatchSet(compilerOptions) && sys.watchFile) {
|
||||
// Attach a file watcher
|
||||
sourceFile.fileWatcher = sys.watchFile(sourceFile.fileName, (_fileName, eventKind) => sourceFileChanged(sourceFile, eventKind));
|
||||
}
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
// Change cached program to the given program
|
||||
function setCachedProgram(program: Program) {
|
||||
if (cachedProgram) {
|
||||
const newSourceFiles = program ? program.getSourceFiles() : undefined;
|
||||
forEach(cachedProgram.getSourceFiles(), sourceFile => {
|
||||
if (!(newSourceFiles && contains(newSourceFiles, sourceFile))) {
|
||||
if (sourceFile.fileWatcher) {
|
||||
sourceFile.fileWatcher.close();
|
||||
sourceFile.fileWatcher = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
cachedProgram = program;
|
||||
}
|
||||
|
||||
// If a source file changes, mark it as unwatched and start the recompilation timer
|
||||
function sourceFileChanged(sourceFile: SourceFile, eventKind: FileWatcherEventKind) {
|
||||
sourceFile.fileWatcher.close();
|
||||
sourceFile.fileWatcher = undefined;
|
||||
if (eventKind === FileWatcherEventKind.Deleted) {
|
||||
unorderedRemoveItem(rootFileNames, sourceFile.fileName);
|
||||
}
|
||||
startTimerForRecompilation();
|
||||
}
|
||||
|
||||
// If the configuration file changes, forget cached program and start the recompilation timer
|
||||
function configFileChanged() {
|
||||
setCachedProgram(undefined);
|
||||
cachedConfigFileText = undefined;
|
||||
startTimerForRecompilation();
|
||||
}
|
||||
|
||||
function watchedDirectoryChanged(fileName: string) {
|
||||
if (fileName && !ts.isSupportedSourceFileName(fileName, compilerOptions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
startTimerForHandlingDirectoryChanges();
|
||||
}
|
||||
|
||||
function startTimerForHandlingDirectoryChanges() {
|
||||
if (!sys.setTimeout || !sys.clearTimeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timerHandleForDirectoryChanges) {
|
||||
sys.clearTimeout(timerHandleForDirectoryChanges);
|
||||
}
|
||||
timerHandleForDirectoryChanges = sys.setTimeout(directoryChangeHandler, 250);
|
||||
}
|
||||
|
||||
function directoryChangeHandler() {
|
||||
const parsedCommandLine = parseConfigFile();
|
||||
const newFileNames = ts.map(parsedCommandLine.fileNames, compilerHost.getCanonicalFileName);
|
||||
const canonicalRootFileNames = ts.map(rootFileNames, compilerHost.getCanonicalFileName);
|
||||
|
||||
// We check if the project file list has changed. If so, we just throw away the old program and start fresh.
|
||||
if (!arrayIsEqualTo(newFileNames && newFileNames.sort(), canonicalRootFileNames && canonicalRootFileNames.sort())) {
|
||||
setCachedProgram(undefined);
|
||||
startTimerForRecompilation();
|
||||
else {
|
||||
performCompilation(configParseResult.fileNames, configParseResult.options);
|
||||
}
|
||||
}
|
||||
|
||||
// Upon detecting a file change, wait for 250ms and then perform a recompilation. This gives batch
|
||||
// operations (such as saving all modified files in an editor) a chance to complete before we kick
|
||||
// off a new compilation.
|
||||
function startTimerForRecompilation() {
|
||||
if (!sys.setTimeout || !sys.clearTimeout) {
|
||||
return;
|
||||
else {
|
||||
udpateReportDiagnostic(commandLineOptions);
|
||||
if (isWatchSet(commandLineOptions)) {
|
||||
reportWatchModeWithoutSysSupport();
|
||||
createWatchModeWithoutConfigFile(commandLine.fileNames, commandLineOptions, createWatchingSystemHost());
|
||||
}
|
||||
|
||||
if (timerHandleForRecompilation) {
|
||||
sys.clearTimeout(timerHandleForRecompilation);
|
||||
else {
|
||||
performCompilation(commandLine.fileNames, commandLineOptions);
|
||||
}
|
||||
timerHandleForRecompilation = sys.setTimeout(recompile, 250);
|
||||
}
|
||||
|
||||
function recompile() {
|
||||
timerHandleForRecompilation = undefined;
|
||||
reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation));
|
||||
performCompilation();
|
||||
}
|
||||
}
|
||||
|
||||
function compile(fileNames: string[], compilerOptions: CompilerOptions, compilerHost: CompilerHost) {
|
||||
const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics;
|
||||
let statistics: Statistic[];
|
||||
if (hasDiagnostics) {
|
||||
function reportWatchModeWithoutSysSupport() {
|
||||
if (!sys.watchFile || !sys.watchDirectory) {
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"));
|
||||
sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
}
|
||||
}
|
||||
|
||||
function performCompilation(rootFileNames: string[], compilerOptions: CompilerOptions) {
|
||||
const compilerHost = createCompilerHost(compilerOptions);
|
||||
enableStatistics(compilerOptions);
|
||||
|
||||
const program = createProgram(rootFileNames, compilerOptions, compilerHost);
|
||||
const exitStatus = compileProgram(program);
|
||||
|
||||
reportStatistics(program);
|
||||
return sys.exit(exitStatus);
|
||||
}
|
||||
|
||||
function createWatchingSystemHost(reportWatchDiagnostic?: DiagnosticReporter) {
|
||||
const watchingHost = ts.createWatchingSystemHost(/*pretty*/ undefined, sys, parseConfigFile, reportDiagnostic, reportWatchDiagnostic);
|
||||
watchingHost.beforeCompile = enableStatistics;
|
||||
const afterCompile = watchingHost.afterCompile;
|
||||
watchingHost.afterCompile = (host, program, builder) => {
|
||||
afterCompile(host, program, builder);
|
||||
reportStatistics(program);
|
||||
};
|
||||
return watchingHost;
|
||||
}
|
||||
|
||||
function compileProgram(program: Program): ExitStatus {
|
||||
let diagnostics: Diagnostic[];
|
||||
|
||||
// First get and report any syntactic errors.
|
||||
diagnostics = program.getSyntacticDiagnostics();
|
||||
|
||||
// If we didn't have any syntactic errors, then also try getting the global and
|
||||
// semantic errors.
|
||||
if (diagnostics.length === 0) {
|
||||
diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics());
|
||||
|
||||
if (diagnostics.length === 0) {
|
||||
diagnostics = program.getSemanticDiagnostics();
|
||||
}
|
||||
}
|
||||
|
||||
// Emit and report any errors we ran into.
|
||||
const { emittedFiles, emitSkipped, diagnostics: emitDiagnostics } = program.emit();
|
||||
diagnostics = diagnostics.concat(emitDiagnostics);
|
||||
|
||||
return handleEmitOutputAndReportErrors(sys, program, emittedFiles, emitSkipped, diagnostics, reportDiagnostic);
|
||||
}
|
||||
|
||||
function enableStatistics(compilerOptions: CompilerOptions) {
|
||||
if (compilerOptions.diagnostics || compilerOptions.extendedDiagnostics) {
|
||||
performance.enable();
|
||||
}
|
||||
}
|
||||
|
||||
function reportStatistics(program: Program) {
|
||||
let statistics: Statistic[];
|
||||
const compilerOptions = program.getCompilerOptions();
|
||||
if (compilerOptions.diagnostics || compilerOptions.extendedDiagnostics) {
|
||||
statistics = [];
|
||||
}
|
||||
|
||||
const program = createProgram(fileNames, compilerOptions, compilerHost);
|
||||
const exitStatus = compileProgram();
|
||||
|
||||
if (compilerOptions.listFiles) {
|
||||
forEach(program.getSourceFiles(), file => {
|
||||
sys.write(file.fileName + sys.newLine);
|
||||
});
|
||||
}
|
||||
|
||||
if (hasDiagnostics) {
|
||||
const memoryUsed = sys.getMemoryUsage ? sys.getMemoryUsage() : -1;
|
||||
reportCountStatistic("Files", program.getSourceFiles().length);
|
||||
reportCountStatistic("Lines", countLines(program));
|
||||
|
@ -463,44 +245,6 @@ namespace ts {
|
|||
performance.disable();
|
||||
}
|
||||
|
||||
return { program, exitStatus };
|
||||
|
||||
function compileProgram(): ExitStatus {
|
||||
let diagnostics: Diagnostic[];
|
||||
|
||||
// First get and report any syntactic errors.
|
||||
diagnostics = program.getSyntacticDiagnostics();
|
||||
|
||||
// If we didn't have any syntactic errors, then also try getting the global and
|
||||
// semantic errors.
|
||||
if (diagnostics.length === 0) {
|
||||
diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics());
|
||||
|
||||
if (diagnostics.length === 0) {
|
||||
diagnostics = program.getSemanticDiagnostics();
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, emit and report any errors we ran into.
|
||||
const emitOutput = program.emit();
|
||||
diagnostics = diagnostics.concat(emitOutput.diagnostics);
|
||||
|
||||
reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), compilerHost);
|
||||
|
||||
reportEmittedFiles(emitOutput.emittedFiles);
|
||||
|
||||
if (emitOutput.emitSkipped && diagnostics.length > 0) {
|
||||
// If the emitter didn't emit anything, then pass that value along.
|
||||
return ExitStatus.DiagnosticsPresent_OutputsSkipped;
|
||||
}
|
||||
else if (diagnostics.length > 0) {
|
||||
// The emitter emitted something, inform the caller if that happened in the presence
|
||||
// of diagnostics or not.
|
||||
return ExitStatus.DiagnosticsPresent_OutputsGenerated;
|
||||
}
|
||||
return ExitStatus.Success;
|
||||
}
|
||||
|
||||
function reportStatistics() {
|
||||
let nameSize = 0;
|
||||
let valueSize = 0;
|
||||
|
@ -654,19 +398,17 @@ namespace ts {
|
|||
const currentDirectory = sys.getCurrentDirectory();
|
||||
const file = normalizePath(combinePaths(currentDirectory, "tsconfig.json"));
|
||||
if (sys.fileExists(file)) {
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file), /* host */ undefined);
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file));
|
||||
}
|
||||
else {
|
||||
sys.writeFile(file, generateTSConfig(options, fileNames, sys.newLine));
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Successfully_created_a_tsconfig_json_file), /* host */ undefined);
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Successfully_created_a_tsconfig_json_file));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ts.setStackTraceLimit();
|
||||
|
||||
if (ts.Debug.isDebugging) {
|
||||
ts.Debug.enableDebugInfo();
|
||||
}
|
||||
|
@ -674,5 +416,4 @@ if (ts.Debug.isDebugging) {
|
|||
if (ts.sys.tryEnableSourceMapsForHost && /^development$/i.test(ts.sys.getEnvironmentVariable("NODE_ENV"))) {
|
||||
ts.sys.tryEnableSourceMapsForHost();
|
||||
}
|
||||
|
||||
ts.executeCommandLine(ts.sys.args);
|
||||
|
|
|
@ -36,6 +36,9 @@
|
|||
"declarationEmitter.ts",
|
||||
"emitter.ts",
|
||||
"program.ts",
|
||||
"builder.ts",
|
||||
"resolutionCache.ts",
|
||||
"watchedProgram.ts",
|
||||
"commandLineParser.ts",
|
||||
"tsc.ts",
|
||||
"diagnosticInformationMap.generated.ts"
|
||||
|
|
|
@ -2347,6 +2347,7 @@ namespace ts {
|
|||
/* @internal */ patternAmbientModules?: PatternAmbientModule[];
|
||||
/* @internal */ ambientModuleNames: ReadonlyArray<string>;
|
||||
/* @internal */ checkJsDirective: CheckJsDirective | undefined;
|
||||
/* @internal */ version: string;
|
||||
}
|
||||
|
||||
export interface Bundle extends Node {
|
||||
|
@ -2410,7 +2411,7 @@ namespace ts {
|
|||
* program source file but could not be located.
|
||||
*/
|
||||
/* @internal */
|
||||
getMissingFilePaths(): Path[];
|
||||
getMissingFilePaths(): ReadonlyArray<Path>;
|
||||
|
||||
/**
|
||||
* Emits the JavaScript and declaration files. If targetSourceFile is not specified, then
|
||||
|
@ -3570,6 +3571,7 @@ namespace ts {
|
|||
charset?: string;
|
||||
checkJs?: boolean;
|
||||
/* @internal */ configFilePath?: string;
|
||||
/** configFile is set as non enumerable property so as to avoid checking of json source files */
|
||||
/* @internal */ readonly configFile?: JsonSourceFile;
|
||||
declaration?: boolean;
|
||||
declarationDir?: string;
|
||||
|
@ -4014,7 +4016,7 @@ namespace ts {
|
|||
export interface ResolvedModuleWithFailedLookupLocations {
|
||||
readonly resolvedModule: ResolvedModuleFull | undefined;
|
||||
/* @internal */
|
||||
readonly failedLookupLocations: string[];
|
||||
readonly failedLookupLocations: ReadonlyArray<string>;
|
||||
/*@internal*/
|
||||
isInvalidated?: boolean;
|
||||
}
|
||||
|
@ -4028,14 +4030,18 @@ namespace ts {
|
|||
|
||||
export interface ResolvedTypeReferenceDirectiveWithFailedLookupLocations {
|
||||
readonly resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective;
|
||||
readonly failedLookupLocations: string[];
|
||||
readonly failedLookupLocations: ReadonlyArray<string>;
|
||||
/*@internal*/
|
||||
isInvalidated?: boolean;
|
||||
}
|
||||
|
||||
export interface HasInvalidatedResolution {
|
||||
(sourceFile: Path): boolean;
|
||||
}
|
||||
|
||||
export interface CompilerHost extends ModuleResolutionHost {
|
||||
getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile;
|
||||
getSourceFileByPath?(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile;
|
||||
getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile;
|
||||
getSourceFileByPath?(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile;
|
||||
getCancellationToken?(): CancellationToken;
|
||||
getDefaultLibFileName(options: CompilerOptions): string;
|
||||
getDefaultLibLocation?(): string;
|
||||
|
@ -4059,6 +4065,8 @@ namespace ts {
|
|||
*/
|
||||
resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
|
||||
getEnvironmentVariable?(name: string): string;
|
||||
onReleaseOldSourceFile?(oldSourceFile: SourceFile, oldOptions: CompilerOptions): void;
|
||||
hasInvalidatedResolution?: HasInvalidatedResolution;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
|
|
|
@ -366,7 +366,7 @@ namespace ts {
|
|||
}
|
||||
|
||||
export function getTextOfConstantValue(value: string | number) {
|
||||
return typeof value === "string" ? '"' + escapeNonAsciiString(value) + '"' : "" + value;
|
||||
return isString(value) ? '"' + escapeNonAsciiString(value) + '"' : "" + value;
|
||||
}
|
||||
|
||||
// Add an extra underscore to identifiers that start with two underscores to avoid issues with magic names like '__proto__'
|
||||
|
@ -403,7 +403,10 @@ namespace ts {
|
|||
((<ModuleDeclaration>node).name.kind === SyntaxKind.StringLiteral || isGlobalScopeAugmentation(<ModuleDeclaration>node));
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export function isModuleWithStringLiteralName(node: Node): node is ModuleDeclaration {
|
||||
return isModuleDeclaration(node) && node.name.kind === SyntaxKind.StringLiteral;
|
||||
}
|
||||
|
||||
export function isNonGlobalAmbientModule(node: Node): node is ModuleDeclaration & { name: StringLiteral } {
|
||||
return isModuleDeclaration(node) && isStringLiteral(node.name);
|
||||
}
|
||||
|
@ -1416,8 +1419,8 @@ namespace ts {
|
|||
if (node.kind === SyntaxKind.ExportDeclaration) {
|
||||
return (<ExportDeclaration>node).moduleSpecifier;
|
||||
}
|
||||
if (node.kind === SyntaxKind.ModuleDeclaration && (<ModuleDeclaration>node).name.kind === SyntaxKind.StringLiteral) {
|
||||
return (<ModuleDeclaration>node).name;
|
||||
if (isModuleWithStringLiteralName(node)) {
|
||||
return node.name;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3202,17 +3205,14 @@ namespace ts {
|
|||
|
||||
const carriageReturnLineFeed = "\r\n";
|
||||
const lineFeed = "\n";
|
||||
export function getNewLineCharacter(options: CompilerOptions | PrinterOptions): string {
|
||||
export function getNewLineCharacter(options: CompilerOptions | PrinterOptions, system?: System): string {
|
||||
switch (options.newLine) {
|
||||
case NewLineKind.CarriageReturnLineFeed:
|
||||
return carriageReturnLineFeed;
|
||||
case NewLineKind.LineFeed:
|
||||
return lineFeed;
|
||||
}
|
||||
if (sys) {
|
||||
return sys.newLine;
|
||||
}
|
||||
return carriageReturnLineFeed;
|
||||
return system ? system.newLine : sys ? sys.newLine : carriageReturnLineFeed;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3466,6 +3466,84 @@ namespace ts {
|
|||
export function getCombinedLocalAndExportSymbolFlags(symbol: Symbol): SymbolFlags {
|
||||
return symbol.exportSymbol ? symbol.exportSymbol.flags | symbol.flags : symbol.flags;
|
||||
}
|
||||
|
||||
export function compareDataObjects(dst: any, src: any): boolean {
|
||||
if (!dst || !src || Object.keys(dst).length !== Object.keys(src).length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const e in dst) {
|
||||
if (typeof dst[e] === "object") {
|
||||
if (!compareDataObjects(dst[e], src[e])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (typeof dst[e] !== "function") {
|
||||
if (dst[e] !== src[e]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* clears already present map by calling onDeleteExistingValue callback before deleting that key/value
|
||||
*/
|
||||
export function clearMap<T>(map: Map<T>, onDeleteValue: (key: string, existingValue: T) => void) {
|
||||
// Remove all
|
||||
map.forEach((existingValue, key) => {
|
||||
onDeleteValue(key, existingValue);
|
||||
});
|
||||
map.clear();
|
||||
}
|
||||
|
||||
export interface MutateMapOptions<T, U> {
|
||||
createNewValue(key: string, valueInNewMap: U): T;
|
||||
onDeleteValue(key: string, existingValue: T): void;
|
||||
|
||||
/**
|
||||
* If present this is called with the key when there is value for that key both in new map as well as existing map provided
|
||||
* Caller can then decide to update or remove this key.
|
||||
* If the key is removed, caller will get callback of createNewValue for that key.
|
||||
* If this callback is not provided, the value of such keys is not updated.
|
||||
*/
|
||||
onExistingValue?(key: string, existingValue: T, valueInNewMap: U): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutates the map with newMap such that keys in map will be same as newMap.
|
||||
*/
|
||||
export function mutateMap<T, U>(map: Map<T>, newMap: ReadonlyMap<U>, options: MutateMapOptions<T, U>) {
|
||||
// If there are new values update them
|
||||
if (newMap) {
|
||||
const { createNewValue, onDeleteValue, onExistingValue } = options;
|
||||
// Needs update
|
||||
map.forEach((existingValue, key) => {
|
||||
const valueInNewMap = newMap.get(key);
|
||||
// Not present any more in new map, remove it
|
||||
if (valueInNewMap === undefined) {
|
||||
map.delete(key);
|
||||
onDeleteValue(key, existingValue);
|
||||
}
|
||||
// If present notify about existing values
|
||||
else if (onExistingValue) {
|
||||
onExistingValue(key, existingValue, valueInNewMap);
|
||||
}
|
||||
});
|
||||
|
||||
// Add new values that are not already present
|
||||
newMap.forEach((valueInNewMap, key) => {
|
||||
if (!map.has(key)) {
|
||||
// New values
|
||||
map.set(key, createNewValue(key, valueInNewMap));
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
clearMap(map, options.onDeleteValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace ts {
|
||||
|
|
712
src/compiler/watchedProgram.ts
Normal file
712
src/compiler/watchedProgram.ts
Normal file
|
@ -0,0 +1,712 @@
|
|||
/// <reference path="program.ts" />
|
||||
/// <reference path="builder.ts" />
|
||||
/// <reference path="resolutionCache.ts"/>
|
||||
|
||||
namespace ts {
|
||||
export type DiagnosticReporter = (diagnostic: Diagnostic) => void;
|
||||
export type DiagnosticWorker = (diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System) => void;
|
||||
export type ParseConfigFile = (configFileName: string, optionsToExtend: CompilerOptions, system: System, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter) => ParsedCommandLine;
|
||||
export interface WatchingSystemHost {
|
||||
// FS system to use
|
||||
system: System;
|
||||
|
||||
// parse config file
|
||||
parseConfigFile: ParseConfigFile;
|
||||
|
||||
// Reporting errors
|
||||
reportDiagnostic: DiagnosticReporter;
|
||||
reportWatchDiagnostic: DiagnosticReporter;
|
||||
|
||||
// Callbacks to do custom action before creating program and after creating program
|
||||
beforeCompile(compilerOptions: CompilerOptions): void;
|
||||
afterCompile(host: System, program: Program, builder: Builder): void;
|
||||
}
|
||||
|
||||
const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = sys ? {
|
||||
getCurrentDirectory: () => sys.getCurrentDirectory(),
|
||||
getNewLine: () => sys.newLine,
|
||||
getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames)
|
||||
} : undefined;
|
||||
|
||||
export function createDiagnosticReporter(system = sys, worker = reportDiagnosticSimply, formatDiagnosticsHost?: FormatDiagnosticsHost): DiagnosticReporter {
|
||||
return diagnostic => worker(diagnostic, getFormatDiagnosticsHost(), system);
|
||||
|
||||
function getFormatDiagnosticsHost() {
|
||||
return formatDiagnosticsHost || (formatDiagnosticsHost = system === sys ? defaultFormatDiagnosticsHost : {
|
||||
getCurrentDirectory: () => system.getCurrentDirectory(),
|
||||
getNewLine: () => system.newLine,
|
||||
getCanonicalFileName: createGetCanonicalFileName(system.useCaseSensitiveFileNames),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function createWatchDiagnosticReporter(system = sys): DiagnosticReporter {
|
||||
return diagnostic => {
|
||||
let output = new Date().toLocaleTimeString() + " - ";
|
||||
output += `${flattenDiagnosticMessageText(diagnostic.messageText, system.newLine)}${system.newLine + system.newLine + system.newLine}`;
|
||||
system.write(output);
|
||||
};
|
||||
}
|
||||
|
||||
export function reportDiagnostics(diagnostics: Diagnostic[], reportDiagnostic: DiagnosticReporter): void {
|
||||
for (const diagnostic of diagnostics) {
|
||||
reportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
export function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System): void {
|
||||
system.write(ts.formatDiagnostic(diagnostic, host));
|
||||
}
|
||||
|
||||
export function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System): void {
|
||||
system.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + host.getNewLine());
|
||||
}
|
||||
|
||||
export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, system: System, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter): ParsedCommandLine {
|
||||
let configFileText: string;
|
||||
try {
|
||||
configFileText = system.readFile(configFileName);
|
||||
}
|
||||
catch (e) {
|
||||
const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message);
|
||||
reportWatchDiagnostic(error);
|
||||
system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
return;
|
||||
}
|
||||
if (!configFileText) {
|
||||
const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName);
|
||||
reportDiagnostics([error], reportDiagnostic);
|
||||
system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = parseJsonText(configFileName, configFileText);
|
||||
reportDiagnostics(result.parseDiagnostics, reportDiagnostic);
|
||||
|
||||
const cwd = system.getCurrentDirectory();
|
||||
const configParseResult = parseJsonSourceFileConfigFileContent(result, system, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd));
|
||||
reportDiagnostics(configParseResult.errors, reportDiagnostic);
|
||||
|
||||
return configParseResult;
|
||||
}
|
||||
|
||||
function reportEmittedFiles(files: string[], system: System): void {
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentDir = system.getCurrentDirectory();
|
||||
for (const file of files) {
|
||||
const filepath = getNormalizedAbsolutePath(file, currentDir);
|
||||
system.write(`TSFILE: ${filepath}${system.newLine}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleEmitOutputAndReportErrors(system: System, program: Program,
|
||||
emittedFiles: string[], emitSkipped: boolean,
|
||||
diagnostics: Diagnostic[], reportDiagnostic: DiagnosticReporter
|
||||
): ExitStatus {
|
||||
reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), reportDiagnostic);
|
||||
reportEmittedFiles(emittedFiles, system);
|
||||
|
||||
if (program.getCompilerOptions().listFiles) {
|
||||
forEach(program.getSourceFiles(), file => {
|
||||
system.write(file.fileName + system.newLine);
|
||||
});
|
||||
}
|
||||
|
||||
if (emitSkipped && diagnostics.length > 0) {
|
||||
// If the emitter didn't emit anything, then pass that value along.
|
||||
return ExitStatus.DiagnosticsPresent_OutputsSkipped;
|
||||
}
|
||||
else if (diagnostics.length > 0) {
|
||||
// The emitter emitted something, inform the caller if that happened in the presence
|
||||
// of diagnostics or not.
|
||||
return ExitStatus.DiagnosticsPresent_OutputsGenerated;
|
||||
}
|
||||
return ExitStatus.Success;
|
||||
}
|
||||
|
||||
export function createWatchingSystemHost(pretty?: DiagnosticStyle, system = sys,
|
||||
parseConfigFile?: ParseConfigFile, reportDiagnostic?: DiagnosticReporter,
|
||||
reportWatchDiagnostic?: DiagnosticReporter
|
||||
): WatchingSystemHost {
|
||||
reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system, pretty ? reportDiagnosticWithColorAndContext : reportDiagnosticSimply);
|
||||
reportWatchDiagnostic = reportWatchDiagnostic || createWatchDiagnosticReporter(system);
|
||||
parseConfigFile = parseConfigFile || ts.parseConfigFile;
|
||||
return {
|
||||
system,
|
||||
parseConfigFile,
|
||||
reportDiagnostic,
|
||||
reportWatchDiagnostic,
|
||||
beforeCompile: noop,
|
||||
afterCompile: compileWatchedProgram,
|
||||
};
|
||||
|
||||
function compileWatchedProgram(host: System, program: Program, builder: Builder) {
|
||||
// First get and report any syntactic errors.
|
||||
let diagnostics = program.getSyntacticDiagnostics();
|
||||
let reportSemanticDiagnostics = false;
|
||||
|
||||
// If we didn't have any syntactic errors, then also try getting the global and
|
||||
// semantic errors.
|
||||
if (diagnostics.length === 0) {
|
||||
diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics());
|
||||
|
||||
if (diagnostics.length === 0) {
|
||||
reportSemanticDiagnostics = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Emit and report any errors we ran into.
|
||||
const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined;
|
||||
let sourceMaps: SourceMapData[];
|
||||
let emitSkipped: boolean;
|
||||
|
||||
const result = builder.emitChangedFiles(program);
|
||||
if (result.length === 0) {
|
||||
emitSkipped = true;
|
||||
}
|
||||
else {
|
||||
for (const emitOutput of result) {
|
||||
if (emitOutput.emitSkipped) {
|
||||
emitSkipped = true;
|
||||
}
|
||||
diagnostics = concatenate(diagnostics, emitOutput.diagnostics);
|
||||
sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps);
|
||||
writeOutputFiles(emitOutput.outputFiles);
|
||||
}
|
||||
}
|
||||
|
||||
if (reportSemanticDiagnostics) {
|
||||
diagnostics = diagnostics.concat(builder.getSemanticDiagnostics(program));
|
||||
}
|
||||
return handleEmitOutputAndReportErrors(host, program, emittedFiles, emitSkipped,
|
||||
diagnostics, reportDiagnostic);
|
||||
|
||||
function ensureDirectoriesExist(directoryPath: string) {
|
||||
if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) {
|
||||
const parentDirectory = getDirectoryPath(directoryPath);
|
||||
ensureDirectoriesExist(parentDirectory);
|
||||
host.createDirectory(directoryPath);
|
||||
}
|
||||
}
|
||||
|
||||
function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) {
|
||||
try {
|
||||
performance.mark("beforeIOWrite");
|
||||
ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName)));
|
||||
|
||||
host.writeFile(fileName, data, writeByteOrderMark);
|
||||
|
||||
performance.mark("afterIOWrite");
|
||||
performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite");
|
||||
}
|
||||
catch (e) {
|
||||
return createCompilerDiagnostic(Diagnostics.Could_not_write_file_0_Colon_1, fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
function writeOutputFiles(outputFiles: OutputFile[]) {
|
||||
if (outputFiles) {
|
||||
for (const outputFile of outputFiles) {
|
||||
const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark);
|
||||
if (error) {
|
||||
diagnostics.push(error);
|
||||
}
|
||||
if (emittedFiles) {
|
||||
emittedFiles.push(outputFile.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createWatchModeWithConfigFile(configParseResult: ParsedCommandLine, optionsToExtend: CompilerOptions = {}, watchingHost?: WatchingSystemHost) {
|
||||
return createWatchMode(configParseResult.fileNames, configParseResult.options, watchingHost, configParseResult.options.configFilePath, configParseResult.configFileSpecs, configParseResult.wildcardDirectories, optionsToExtend);
|
||||
}
|
||||
|
||||
export function createWatchModeWithoutConfigFile(rootFileNames: string[], compilerOptions: CompilerOptions, watchingHost?: WatchingSystemHost) {
|
||||
return createWatchMode(rootFileNames, compilerOptions, watchingHost);
|
||||
}
|
||||
|
||||
interface HostFileInfo {
|
||||
version: number;
|
||||
sourceFile: SourceFile;
|
||||
fileWatcher: FileWatcher;
|
||||
}
|
||||
|
||||
function createWatchMode(rootFileNames: string[], compilerOptions: CompilerOptions, watchingHost?: WatchingSystemHost, configFileName?: string, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike<WatchDirectoryFlags>, optionsToExtendForConfigFile?: CompilerOptions) {
|
||||
let program: Program;
|
||||
let needsReload: boolean; // true if the config file changed and needs to reload it from the disk
|
||||
let missingFilesMap: Map<FileWatcher>; // Map of file watchers for the missing files
|
||||
let configFileWatcher: FileWatcher; // watcher for the config file
|
||||
let watchedWildcardDirectories: Map<WildcardDirectoryWatcher>; // map of watchers for the wild card directories in the config file
|
||||
let timerToUpdateProgram: any; // timer callback to recompile the program
|
||||
|
||||
const sourceFilesCache = createMap<HostFileInfo | string>(); // Cache that stores the source file and version info
|
||||
let missingFilePathsRequestedForRelease: Path[]; // These paths are held temparirly so that we can remove the entry from source file cache if the file is not tracked by missing files
|
||||
let hasInvalidatedResolution: HasInvalidatedResolution; // Passed along to see if source file has invalidated resolutions
|
||||
let hasChangedCompilerOptions = false; // True if the compiler options have changed between compilations
|
||||
|
||||
watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty);
|
||||
const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost;
|
||||
|
||||
let host: System;
|
||||
if (configFileName) {
|
||||
host = createCachedSystem(system);
|
||||
configFileWatcher = system.watchFile(configFileName, onConfigFileChanged);
|
||||
}
|
||||
else {
|
||||
host = system;
|
||||
}
|
||||
const currentDirectory = host.getCurrentDirectory();
|
||||
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
|
||||
|
||||
// Cache for the module resolution
|
||||
const resolutionCache = createResolutionCache(
|
||||
fileName => toPath(fileName),
|
||||
() => compilerOptions,
|
||||
watchFailedLookupLocation,
|
||||
s => writeLog(s)
|
||||
);
|
||||
|
||||
// There is no extra check needed since we can just rely on the program to decide emit
|
||||
const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true);
|
||||
|
||||
synchronizeProgram();
|
||||
|
||||
// Update the wild card directory watch
|
||||
watchConfigFileWildCardDirectories();
|
||||
|
||||
return () => program;
|
||||
|
||||
function synchronizeProgram() {
|
||||
writeLog(`Synchronizing program`);
|
||||
|
||||
hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution();
|
||||
if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the compiler host
|
||||
const compilerHost = createWatchedCompilerHost(compilerOptions);
|
||||
resolutionCache.setModuleResolutionHost(compilerHost);
|
||||
if (hasChangedCompilerOptions && changesAffectModuleResolution(program && program.getCompilerOptions(), compilerOptions)) {
|
||||
resolutionCache.clear();
|
||||
}
|
||||
hasChangedCompilerOptions = false;
|
||||
beforeCompile(compilerOptions);
|
||||
|
||||
// Compile the program
|
||||
program = createProgram(rootFileNames, compilerOptions, compilerHost, program);
|
||||
builder.onProgramUpdateGraph(program, hasInvalidatedResolution);
|
||||
|
||||
// Update watches
|
||||
updateMissingFilePathsWatch(program, missingFilesMap || (missingFilesMap = createMap()), watchMissingFilePath, closeMissingFilePathWatcher);
|
||||
if (missingFilePathsRequestedForRelease) {
|
||||
// These are the paths that program creater told us as not in use any more but were missing on the disk.
|
||||
// We didnt remove the entry for them from sourceFiles cache so that we dont have to do File IO,
|
||||
// if there is already watcher for it (for missing files)
|
||||
// At that point our watches were updated, hence now we know that these paths are not tracked and need to be removed
|
||||
// so that at later time we have correct result of their presence
|
||||
for (const missingFilePath of missingFilePathsRequestedForRelease) {
|
||||
if (!missingFilesMap.has(missingFilePath)) {
|
||||
sourceFilesCache.delete(missingFilePath);
|
||||
}
|
||||
}
|
||||
missingFilePathsRequestedForRelease = undefined;
|
||||
}
|
||||
|
||||
afterCompile(host, program, builder);
|
||||
reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes));
|
||||
}
|
||||
|
||||
function createWatchedCompilerHost(options: CompilerOptions): CompilerHost {
|
||||
const newLine = getNewLineCharacter(options, system);
|
||||
const realpath = host.realpath && ((path: string) => host.realpath(path));
|
||||
|
||||
return {
|
||||
getSourceFile: getVersionedSourceFile,
|
||||
getSourceFileByPath: getVersionedSourceFileByPath,
|
||||
getDefaultLibLocation,
|
||||
getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)),
|
||||
writeFile: (_fileName, _data, _writeByteOrderMark, _onError?, _sourceFiles?) => { },
|
||||
getCurrentDirectory: memoize(() => host.getCurrentDirectory()),
|
||||
useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames,
|
||||
getCanonicalFileName,
|
||||
getNewLine: () => newLine,
|
||||
fileExists,
|
||||
readFile: fileName => host.readFile(fileName),
|
||||
trace: (s: string) => host.write(s + newLine),
|
||||
directoryExists: directoryName => host.directoryExists(directoryName),
|
||||
getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "",
|
||||
getDirectories: (path: string) => host.getDirectories(path),
|
||||
realpath,
|
||||
resolveTypeReferenceDirectives: (typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile),
|
||||
resolveModuleNames: (moduleNames, containingFile) => resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ false),
|
||||
onReleaseOldSourceFile,
|
||||
hasInvalidatedResolution
|
||||
};
|
||||
}
|
||||
|
||||
function toPath(fileName: string) {
|
||||
return ts.toPath(fileName, currentDirectory, getCanonicalFileName);
|
||||
}
|
||||
|
||||
function fileExists(fileName: string) {
|
||||
const path = toPath(fileName);
|
||||
const hostSourceFileInfo = sourceFilesCache.get(path);
|
||||
if (hostSourceFileInfo !== undefined) {
|
||||
return !isString(hostSourceFileInfo);
|
||||
}
|
||||
|
||||
return host.fileExists(fileName);
|
||||
}
|
||||
|
||||
function getDefaultLibLocation(): string {
|
||||
return getDirectoryPath(normalizePath(host.getExecutingFilePath()));
|
||||
}
|
||||
|
||||
function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
|
||||
return getVersionedSourceFileByPath(fileName, toPath(fileName), languageVersion, onError, shouldCreateNewSourceFile);
|
||||
}
|
||||
|
||||
function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
|
||||
const hostSourceFile = sourceFilesCache.get(path);
|
||||
// No source file on the host
|
||||
if (isString(hostSourceFile)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Create new source file if requested or the versions dont match
|
||||
if (!hostSourceFile || shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) {
|
||||
const sourceFile = getNewSourceFile();
|
||||
if (hostSourceFile) {
|
||||
if (shouldCreateNewSourceFile) {
|
||||
hostSourceFile.version++;
|
||||
}
|
||||
if (sourceFile) {
|
||||
hostSourceFile.sourceFile = sourceFile;
|
||||
sourceFile.version = hostSourceFile.version.toString();
|
||||
if (!hostSourceFile.fileWatcher) {
|
||||
hostSourceFile.fileWatcher = watchSourceFileForChanges(path);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// There is no source file on host any more, close the watch, missing file paths will track it
|
||||
hostSourceFile.fileWatcher.close();
|
||||
sourceFilesCache.set(path, hostSourceFile.version.toString());
|
||||
}
|
||||
}
|
||||
else {
|
||||
let fileWatcher: FileWatcher;
|
||||
if (sourceFile) {
|
||||
sourceFile.version = "0";
|
||||
fileWatcher = watchSourceFileForChanges(path);
|
||||
sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher });
|
||||
}
|
||||
else {
|
||||
sourceFilesCache.set(path, "0");
|
||||
}
|
||||
}
|
||||
return sourceFile;
|
||||
}
|
||||
return hostSourceFile.sourceFile;
|
||||
|
||||
function getNewSourceFile() {
|
||||
let text: string;
|
||||
try {
|
||||
performance.mark("beforeIORead");
|
||||
text = host.readFile(fileName, compilerOptions.charset);
|
||||
performance.mark("afterIORead");
|
||||
performance.measure("I/O Read", "beforeIORead", "afterIORead");
|
||||
}
|
||||
catch (e) {
|
||||
if (onError) {
|
||||
onError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
return text !== undefined ? createSourceFile(fileName, text, languageVersion) : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function removeSourceFile(path: Path) {
|
||||
const hostSourceFile = sourceFilesCache.get(path);
|
||||
if (hostSourceFile !== undefined) {
|
||||
if (!isString(hostSourceFile)) {
|
||||
hostSourceFile.fileWatcher.close();
|
||||
resolutionCache.invalidateResolutionOfFile(path);
|
||||
}
|
||||
sourceFilesCache.delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
function getSourceVersion(path: Path): string {
|
||||
const hostSourceFile = sourceFilesCache.get(path);
|
||||
return !hostSourceFile || isString(hostSourceFile) ? undefined : hostSourceFile.version.toString();
|
||||
}
|
||||
|
||||
function onReleaseOldSourceFile(oldSourceFile: SourceFile, _oldOptions: CompilerOptions) {
|
||||
const hostSourceFileInfo = sourceFilesCache.get(oldSourceFile.path);
|
||||
// If this is the source file thats in the cache and new program doesnt need it,
|
||||
// remove the cached entry.
|
||||
// Note we arent deleting entry if file became missing in new program or
|
||||
// there was version update and new source file was created.
|
||||
if (hostSourceFileInfo) {
|
||||
// record the missing file paths so they can be removed later if watchers arent tracking them
|
||||
if (isString(hostSourceFileInfo)) {
|
||||
(missingFilePathsRequestedForRelease || (missingFilePathsRequestedForRelease = [])).push(oldSourceFile.path);
|
||||
}
|
||||
else if (hostSourceFileInfo.sourceFile === oldSourceFile) {
|
||||
sourceFilesCache.delete(oldSourceFile.path);
|
||||
resolutionCache.invalidateResolutionOfFile(oldSourceFile.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upon detecting a file change, wait for 250ms and then perform a recompilation. This gives batch
|
||||
// operations (such as saving all modified files in an editor) a chance to complete before we kick
|
||||
// off a new compilation.
|
||||
function scheduleProgramUpdate() {
|
||||
if (!system.setTimeout || !system.clearTimeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timerToUpdateProgram) {
|
||||
system.clearTimeout(timerToUpdateProgram);
|
||||
}
|
||||
timerToUpdateProgram = system.setTimeout(updateProgram, 250);
|
||||
}
|
||||
|
||||
function scheduleProgramReload() {
|
||||
Debug.assert(!!configFileName);
|
||||
needsReload = true;
|
||||
scheduleProgramUpdate();
|
||||
}
|
||||
|
||||
function updateProgram() {
|
||||
timerToUpdateProgram = undefined;
|
||||
reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation));
|
||||
|
||||
if (needsReload) {
|
||||
reloadConfigFile();
|
||||
}
|
||||
else {
|
||||
synchronizeProgram();
|
||||
}
|
||||
}
|
||||
|
||||
function reloadConfigFile() {
|
||||
writeLog(`Reloading config file: ${configFileName}`);
|
||||
needsReload = false;
|
||||
|
||||
const cachedHost = host as CachedSystem;
|
||||
cachedHost.clearCache();
|
||||
const configParseResult = parseConfigFile(configFileName, optionsToExtendForConfigFile, cachedHost, reportDiagnostic, reportWatchDiagnostic);
|
||||
rootFileNames = configParseResult.fileNames;
|
||||
compilerOptions = configParseResult.options;
|
||||
hasChangedCompilerOptions = true;
|
||||
configFileSpecs = configParseResult.configFileSpecs;
|
||||
configFileWildCardDirectories = configParseResult.wildcardDirectories;
|
||||
|
||||
synchronizeProgram();
|
||||
|
||||
// Update the wild card directory watch
|
||||
watchConfigFileWildCardDirectories();
|
||||
}
|
||||
|
||||
function watchSourceFileForChanges(path: Path) {
|
||||
return host.watchFile(path, (fileName, eventKind) => onSourceFileChange(fileName, path, eventKind));
|
||||
}
|
||||
|
||||
function onSourceFileChange(fileName: string, path: Path, eventKind: FileWatcherEventKind) {
|
||||
writeLog(`Source file path : ${path} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`);
|
||||
|
||||
updateCachedSystemWithFile(fileName, path, eventKind);
|
||||
const hostSourceFile = sourceFilesCache.get(path);
|
||||
if (hostSourceFile) {
|
||||
// Update the cache
|
||||
if (eventKind === FileWatcherEventKind.Deleted) {
|
||||
resolutionCache.invalidateResolutionOfFile(path);
|
||||
if (!isString(hostSourceFile)) {
|
||||
hostSourceFile.fileWatcher.close();
|
||||
sourceFilesCache.set(path, (hostSourceFile.version++).toString());
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Deleted file created
|
||||
if (isString(hostSourceFile)) {
|
||||
sourceFilesCache.delete(path);
|
||||
}
|
||||
else {
|
||||
// file changed - just update the version
|
||||
hostSourceFile.version++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the program
|
||||
scheduleProgramUpdate();
|
||||
}
|
||||
|
||||
function updateCachedSystemWithFile(fileName: string, path: Path, eventKind: FileWatcherEventKind) {
|
||||
if (configFileName) {
|
||||
(host as CachedSystem).addOrDeleteFile(fileName, path, eventKind);
|
||||
}
|
||||
}
|
||||
|
||||
function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) {
|
||||
return host.watchFile(failedLookupLocation, (fileName, eventKind) => onFailedLookupLocationChange(fileName, eventKind, failedLookupLocation, failedLookupLocationPath, containingFile, name));
|
||||
}
|
||||
|
||||
function onFailedLookupLocationChange(fileName: string, eventKind: FileWatcherEventKind, failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) {
|
||||
writeLog(`Failed lookup location : ${failedLookupLocation} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName} containingFile: ${containingFile}, name: ${name}`);
|
||||
updateCachedSystemWithFile(fileName, failedLookupLocationPath, eventKind);
|
||||
resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath);
|
||||
scheduleProgramUpdate();
|
||||
}
|
||||
|
||||
function watchMissingFilePath(missingFilePath: Path) {
|
||||
return host.watchFile(missingFilePath, (fileName, eventKind) => onMissingFileChange(fileName, missingFilePath, eventKind));
|
||||
}
|
||||
|
||||
function closeMissingFilePathWatcher(_missingFilePath: Path, fileWatcher: FileWatcher) {
|
||||
fileWatcher.close();
|
||||
}
|
||||
|
||||
function onMissingFileChange(fileName: string, missingFilePath: Path, eventKind: FileWatcherEventKind) {
|
||||
writeLog(`Missing file path : ${missingFilePath} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`);
|
||||
updateCachedSystemWithFile(fileName, missingFilePath, eventKind);
|
||||
|
||||
if (eventKind === FileWatcherEventKind.Created && missingFilesMap.has(missingFilePath)) {
|
||||
closeMissingFilePathWatcher(missingFilePath, missingFilesMap.get(missingFilePath));
|
||||
missingFilesMap.delete(missingFilePath);
|
||||
|
||||
// Delete the entry in the source files cache so that new source file is created
|
||||
removeSourceFile(missingFilePath);
|
||||
|
||||
// When a missing file is created, we should update the graph.
|
||||
scheduleProgramUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
function watchConfigFileWildCardDirectories() {
|
||||
updateWatchingWildcardDirectories(
|
||||
watchedWildcardDirectories || (watchedWildcardDirectories = createMap()),
|
||||
createMapFromTemplate(configFileWildCardDirectories),
|
||||
watchWildCardDirectory,
|
||||
stopWatchingWildCardDirectory
|
||||
);
|
||||
}
|
||||
|
||||
function watchWildCardDirectory(directory: string, flags: WatchDirectoryFlags) {
|
||||
return host.watchDirectory(directory, fileOrFolder =>
|
||||
onFileAddOrRemoveInWatchedDirectory(getNormalizedAbsolutePath(fileOrFolder, directory)),
|
||||
(flags & WatchDirectoryFlags.Recursive) !== 0);
|
||||
}
|
||||
|
||||
function stopWatchingWildCardDirectory(_directory: string, { watcher }: WildcardDirectoryWatcher, _recursiveChanged: boolean) {
|
||||
watcher.close();
|
||||
}
|
||||
|
||||
function onFileAddOrRemoveInWatchedDirectory(fileOrFolder: string) {
|
||||
Debug.assert(!!configFileName);
|
||||
|
||||
const fileOrFolderPath = toPath(fileOrFolder);
|
||||
|
||||
// Since the file existance changed, update the sourceFiles cache
|
||||
(host as CachedSystem).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath);
|
||||
removeSourceFile(fileOrFolderPath);
|
||||
|
||||
// If a change was made inside "folder/file", node will trigger the callback twice:
|
||||
// one with the fileName being "folder/file", and the other one with "folder".
|
||||
// We don't respond to the second one.
|
||||
if (fileOrFolder && !isSupportedSourceFileName(fileOrFolder, compilerOptions)) {
|
||||
writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrFolder}`);
|
||||
return;
|
||||
}
|
||||
|
||||
writeLog(`Project: ${configFileName} Detected file add/remove of supported extension: ${fileOrFolder}`);
|
||||
|
||||
// Reload is pending, do the reload
|
||||
if (!needsReload) {
|
||||
const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host);
|
||||
if (!configFileSpecs.filesSpecs && result.fileNames.length === 0) {
|
||||
reportDiagnostic(getErrorForNoInputFiles(configFileSpecs, configFileName));
|
||||
}
|
||||
rootFileNames = result.fileNames;
|
||||
|
||||
// Schedule Update the program
|
||||
scheduleProgramUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
function onConfigFileChanged(fileName: string, eventKind: FileWatcherEventKind) {
|
||||
writeLog(`Config file : ${configFileName} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`);
|
||||
scheduleProgramReload();
|
||||
}
|
||||
|
||||
function writeLog(s: string) {
|
||||
const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics;
|
||||
if (hasDiagnostics) {
|
||||
host.write(s);
|
||||
}
|
||||
}
|
||||
|
||||
function computeHash(data: string) {
|
||||
return system.createHash ? system.createHash(data) : data;
|
||||
}
|
||||
}
|
||||
|
||||
interface CachedSystem extends System, CachedHost {
|
||||
}
|
||||
|
||||
function createCachedSystem(host: System): CachedSystem {
|
||||
const getFileSize = host.getFileSize ? (path: string) => host.getFileSize(path) : undefined;
|
||||
const watchFile = host.watchFile ? (path: string, callback: FileWatcherCallback, pollingInterval?: number) => host.watchFile(path, callback, pollingInterval) : undefined;
|
||||
const watchDirectory = host.watchDirectory ? (path: string, callback: DirectoryWatcherCallback, recursive?: boolean) => host.watchDirectory(path, callback, recursive) : undefined;
|
||||
const getModifiedTime = host.getModifiedTime ? (path: string) => host.getModifiedTime(path) : undefined;
|
||||
const createHash = host.createHash ? (data: string) => host.createHash(data) : undefined;
|
||||
const getMemoryUsage = host.getMemoryUsage ? () => host.getMemoryUsage() : undefined;
|
||||
const realpath = host.realpath ? (path: string) => host.realpath(path) : undefined;
|
||||
const tryEnableSourceMapsForHost = host.tryEnableSourceMapsForHost ? () => host.tryEnableSourceMapsForHost() : undefined;
|
||||
const setTimeout = host.setTimeout ? (callback: (...args: any[]) => void, ms: number, ...args: any[]) => host.setTimeout(callback, ms, ...args) : undefined;
|
||||
const clearTimeout = host.clearTimeout ? (timeoutId: any) => host.clearTimeout(timeoutId) : undefined;
|
||||
|
||||
const cachedPartialSystem = createCachedPartialSystem(host);
|
||||
return {
|
||||
args: host.args,
|
||||
newLine: host.newLine,
|
||||
useCaseSensitiveFileNames: host.useCaseSensitiveFileNames,
|
||||
write: s => host.write(s),
|
||||
readFile: (path, encoding?) => host.readFile(path, encoding),
|
||||
getFileSize,
|
||||
writeFile: (fileName, data, writeByteOrderMark?) => cachedPartialSystem.writeFile(fileName, data, writeByteOrderMark),
|
||||
watchFile,
|
||||
watchDirectory,
|
||||
resolvePath: path => host.resolvePath(path),
|
||||
fileExists: fileName => cachedPartialSystem.fileExists(fileName),
|
||||
directoryExists: dir => cachedPartialSystem.directoryExists(dir),
|
||||
createDirectory: dir => cachedPartialSystem.createDirectory(dir),
|
||||
getExecutingFilePath: () => host.getExecutingFilePath(),
|
||||
getCurrentDirectory: () => cachedPartialSystem.getCurrentDirectory(),
|
||||
getDirectories: dir => cachedPartialSystem.getDirectories(dir),
|
||||
readDirectory: (path, extensions, excludes, includes, depth) => cachedPartialSystem.readDirectory(path, extensions, excludes, includes, depth),
|
||||
getModifiedTime,
|
||||
createHash,
|
||||
getMemoryUsage,
|
||||
exit: exitCode => host.exit(exitCode),
|
||||
realpath,
|
||||
getEnvironmentVariable: name => host.getEnvironmentVariable(name),
|
||||
tryEnableSourceMapsForHost,
|
||||
debugMode: host.debugMode,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
addOrDeleteFileOrFolder: (fileOrFolder, fileOrFolderPath) => cachedPartialSystem.addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath),
|
||||
addOrDeleteFile: (file, filePath, eventKind) => cachedPartialSystem.addOrDeleteFile(file, filePath, eventKind),
|
||||
clearCache: () => cachedPartialSystem.clearCache()
|
||||
};
|
||||
}
|
||||
}
|
|
@ -394,7 +394,7 @@ namespace FourSlash {
|
|||
|
||||
// Entry points from fourslash.ts
|
||||
public goToMarker(name: string | Marker = "") {
|
||||
const marker = typeof name === "string" ? this.getMarkerByName(name) : name;
|
||||
const marker = ts.isString(name) ? this.getMarkerByName(name) : name;
|
||||
if (this.activeFile.fileName !== marker.fileName) {
|
||||
this.openFile(marker.fileName);
|
||||
}
|
||||
|
@ -403,7 +403,7 @@ namespace FourSlash {
|
|||
if (marker.position === -1 || marker.position > content.length) {
|
||||
throw new Error(`Marker "${name}" has been invalidated by unrecoverable edits to the file.`);
|
||||
}
|
||||
const mName = typeof name === "string" ? name : this.markerName(marker);
|
||||
const mName = ts.isString(name) ? name : this.markerName(marker);
|
||||
this.lastKnownMarker = mName;
|
||||
this.goToPosition(marker.position);
|
||||
}
|
||||
|
@ -1026,7 +1026,7 @@ namespace FourSlash {
|
|||
|
||||
public verifyNoReferences(markerNameOrRange?: string | Range) {
|
||||
if (markerNameOrRange) {
|
||||
if (typeof markerNameOrRange === "string") {
|
||||
if (ts.isString(markerNameOrRange)) {
|
||||
this.goToMarker(markerNameOrRange);
|
||||
}
|
||||
else {
|
||||
|
@ -1522,7 +1522,7 @@ namespace FourSlash {
|
|||
resultString += "Diagnostics:" + Harness.IO.newLine();
|
||||
const diagnostics = ts.getPreEmitDiagnostics(this.languageService.getProgram());
|
||||
for (const diagnostic of diagnostics) {
|
||||
if (typeof diagnostic.messageText !== "string") {
|
||||
if (!ts.isString(diagnostic.messageText)) {
|
||||
let chainedMessage = <ts.DiagnosticMessageChain>diagnostic.messageText;
|
||||
let indentation = " ";
|
||||
while (chainedMessage) {
|
||||
|
@ -2913,7 +2913,7 @@ namespace FourSlash {
|
|||
result = this.testData.files[index];
|
||||
}
|
||||
}
|
||||
else if (typeof indexOrName === "string") {
|
||||
else if (ts.isString(indexOrName)) {
|
||||
let name = <string>indexOrName;
|
||||
|
||||
// names are stored in the compiler with this relative path, this allows people to use goTo.file on just the fileName
|
||||
|
|
|
@ -225,7 +225,7 @@ namespace Utils {
|
|||
return JSON.stringify(file, (_, v) => isNodeOrArray(v) ? serializeNode(v) : v, " ");
|
||||
|
||||
function getKindName(k: number | string): string {
|
||||
if (typeof k === "string") {
|
||||
if (ts.isString(k)) {
|
||||
return k;
|
||||
}
|
||||
|
||||
|
@ -754,6 +754,10 @@ namespace Harness {
|
|||
}
|
||||
}
|
||||
|
||||
export function mockHash(s: string): string {
|
||||
return `hash-${s}`;
|
||||
}
|
||||
|
||||
const environment = Utils.getExecutionEnvironment();
|
||||
switch (environment) {
|
||||
case Utils.ExecutionEnvironment.Node:
|
||||
|
|
|
@ -855,8 +855,4 @@ namespace Harness.LanguageService {
|
|||
getClassifier(): ts.Classifier { throw new Error("getClassifier is not available using the server interface."); }
|
||||
getPreProcessedFileInfo(): ts.PreProcessedFileInfo { throw new Error("getPreProcessedFileInfo is not available using the server interface."); }
|
||||
}
|
||||
|
||||
export function mockHash(s: string): string {
|
||||
return `hash-${s}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -254,7 +254,7 @@ class ProjectRunner extends RunnerBase {
|
|||
if (option) {
|
||||
const optType = option.type;
|
||||
let value = <any>testCase[name];
|
||||
if (typeof optType !== "string") {
|
||||
if (!ts.isString(optType)) {
|
||||
const key = value.toLowerCase();
|
||||
const optTypeValue = optType.get(key);
|
||||
if (optTypeValue) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
{
|
||||
"extends": "../tsconfig-base",
|
||||
"compilerOptions": {
|
||||
"removeComments": false,
|
||||
|
@ -44,6 +44,7 @@
|
|||
"../compiler/declarationEmitter.ts",
|
||||
"../compiler/emitter.ts",
|
||||
"../compiler/program.ts",
|
||||
"../compiler/builder.ts",
|
||||
"../compiler/commandLineParser.ts",
|
||||
"../compiler/diagnosticInformationMap.generated.ts",
|
||||
"../services/breakpoints.ts",
|
||||
|
@ -92,6 +93,7 @@
|
|||
"rwcRunner.ts",
|
||||
"test262Runner.ts",
|
||||
"runner.ts",
|
||||
"virtualFileSystemWithWatch.ts",
|
||||
"../server/protocol.ts",
|
||||
"../server/session.ts",
|
||||
"../server/client.ts",
|
||||
|
@ -115,6 +117,7 @@
|
|||
"./unittests/convertCompilerOptionsFromJson.ts",
|
||||
"./unittests/convertTypeAcquisitionFromJson.ts",
|
||||
"./unittests/tsserverProjectSystem.ts",
|
||||
"./unittests/tscWatchMode.ts",
|
||||
"./unittests/matchFiles.ts",
|
||||
"./unittests/initializeTSConfig.ts",
|
||||
"./unittests/compileOnSave.ts",
|
||||
|
|
|
@ -47,7 +47,7 @@ namespace ts {
|
|||
clearTimeout,
|
||||
setImmediate: typeof setImmediate !== "undefined" ? setImmediate : action => setTimeout(action, 0),
|
||||
clearImmediate: typeof clearImmediate !== "undefined" ? clearImmediate : clearTimeout,
|
||||
createHash: Harness.LanguageService.mockHash,
|
||||
createHash: Harness.mockHash,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ namespace ts {
|
|||
typingsInstaller: undefined
|
||||
};
|
||||
const projectService = new server.ProjectService(svcOpts);
|
||||
const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */ true, /*containingProject*/ undefined);
|
||||
const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */ true, serverHost);
|
||||
|
||||
const project = projectService.assignOrphanScriptInfoToInferredProject(rootScriptInfo);
|
||||
project.setCompilerOptions({ module: ts.ModuleKind.AMD, noLib: true } );
|
||||
|
@ -188,7 +188,8 @@ namespace ts {
|
|||
let diags = project.getLanguageService().getSemanticDiagnostics(root.name);
|
||||
assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called");
|
||||
assert.isTrue(diags.length === 1, "one diagnostic expected");
|
||||
assert.isTrue(typeof diags[0].messageText === "string" && ((<string>diags[0].messageText).indexOf("Cannot find module") === 0), "should be 'cannot find module' message");
|
||||
const messageText = diags[0].messageText;
|
||||
assert.isTrue(isString(messageText) && messageText.indexOf("Cannot find module") === 0, "should be 'cannot find module' message");
|
||||
|
||||
fileMap.set(imported.name, imported);
|
||||
fileExistsCalledForBar = false;
|
||||
|
|
|
@ -87,7 +87,7 @@ namespace ts {
|
|||
"/dev/tests/scenarios/first.json": "",
|
||||
"/dev/tests/baselines/first/output.ts": ""
|
||||
});
|
||||
const testContents = mapEntries(testContentsJson, (k, v) => [k, typeof v === "string" ? v : JSON.stringify(v)]);
|
||||
const testContents = mapEntries(testContentsJson, (k, v) => [k, isString(v) ? v : JSON.stringify(v)]);
|
||||
|
||||
const caseInsensitiveBasePath = "c:/dev/";
|
||||
const caseInsensitiveHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, mapEntries(testContents, (key, content) => [`c:${key}`, content]));
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
/// <reference path="..\harness.ts" />
|
||||
|
||||
namespace ts {
|
||||
function verifyMissingFilePaths(missingPaths: ReadonlyArray<Path>, expected: ReadonlyArray<string>) {
|
||||
assert.isDefined(missingPaths);
|
||||
const map = arrayToSet(expected) as Map<boolean>;
|
||||
for (const missing of missingPaths) {
|
||||
const value = map.get(missing);
|
||||
assert.isTrue(value, `${missing} to be ${value === undefined ? "not present" : "present only once"}, in actual: ${missingPaths} expected: ${expected}`);
|
||||
map.set(missing, false);
|
||||
}
|
||||
const notFound = mapDefinedIter(map.keys(), k => map.get(k) === true ? k : undefined);
|
||||
assert.equal(notFound.length, 0, `Not found ${notFound} in actual: ${missingPaths} expected: ${expected}`);
|
||||
}
|
||||
|
||||
describe("Program.getMissingFilePaths", () => {
|
||||
|
||||
const options: CompilerOptions = {
|
||||
|
@ -40,34 +52,31 @@ namespace ts {
|
|||
it("handles no missing root files", () => {
|
||||
const program = createProgram([emptyFileRelativePath], options, testCompilerHost);
|
||||
const missing = program.getMissingFilePaths();
|
||||
assert.isDefined(missing);
|
||||
assert.deepEqual(missing, []);
|
||||
verifyMissingFilePaths(missing, []);
|
||||
});
|
||||
|
||||
it("handles missing root file", () => {
|
||||
const program = createProgram(["./nonexistent.ts"], options, testCompilerHost);
|
||||
const missing = program.getMissingFilePaths();
|
||||
assert.isDefined(missing);
|
||||
assert.deepEqual(missing, ["d:/pretend/nonexistent.ts"]); // Absolute path
|
||||
verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]); // Absolute path
|
||||
});
|
||||
|
||||
it("handles multiple missing root files", () => {
|
||||
const program = createProgram(["./nonexistent0.ts", "./nonexistent1.ts"], options, testCompilerHost);
|
||||
const missing = program.getMissingFilePaths().sort();
|
||||
assert.deepEqual(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]);
|
||||
const missing = program.getMissingFilePaths();
|
||||
verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]);
|
||||
});
|
||||
|
||||
it("handles a mix of present and missing root files", () => {
|
||||
const program = createProgram(["./nonexistent0.ts", emptyFileRelativePath, "./nonexistent1.ts"], options, testCompilerHost);
|
||||
const missing = program.getMissingFilePaths().sort();
|
||||
assert.deepEqual(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]);
|
||||
const missing = program.getMissingFilePaths();
|
||||
verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]);
|
||||
});
|
||||
|
||||
it("handles repeatedly specified root files", () => {
|
||||
const program = createProgram(["./nonexistent.ts", "./nonexistent.ts"], options, testCompilerHost);
|
||||
const missing = program.getMissingFilePaths();
|
||||
assert.isDefined(missing);
|
||||
assert.deepEqual(missing, ["d:/pretend/nonexistent.ts"]);
|
||||
verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]);
|
||||
});
|
||||
|
||||
it("normalizes file paths", () => {
|
||||
|
@ -81,9 +90,8 @@ namespace ts {
|
|||
|
||||
it("handles missing triple slash references", () => {
|
||||
const program = createProgram([referenceFileRelativePath], options, testCompilerHost);
|
||||
const missing = program.getMissingFilePaths().sort();
|
||||
assert.isDefined(missing);
|
||||
assert.deepEqual(missing, [
|
||||
const missing = program.getMissingFilePaths();
|
||||
verifyMissingFilePaths(missing, [
|
||||
// From absolute reference
|
||||
"d:/imaginary/nonexistent1.ts",
|
||||
|
||||
|
@ -100,4 +108,4 @@ namespace ts {
|
|||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -346,7 +346,7 @@ namespace ts {
|
|||
const program_2 = updateProgram(program_1, ["a.ts"], options, noop, newTexts);
|
||||
assert.deepEqual(emptyArray, program_2.getMissingFilePaths());
|
||||
|
||||
assert.equal(StructureIsReused.SafeModules, program_1.structureIsReused);
|
||||
assert.equal(StructureIsReused.Not, program_1.structureIsReused);
|
||||
});
|
||||
|
||||
it("resolution cache follows imports", () => {
|
||||
|
|
|
@ -25,7 +25,7 @@ namespace ts.server {
|
|||
clearTimeout: noop,
|
||||
setImmediate: () => 0,
|
||||
clearImmediate: noop,
|
||||
createHash: Harness.LanguageService.mockHash,
|
||||
createHash: Harness.mockHash,
|
||||
};
|
||||
|
||||
class TestSession extends Session {
|
||||
|
|
|
@ -53,7 +53,7 @@ namespace ts.projectSystem {
|
|||
|
||||
// TODO: Apparently compilerOptions is mutated, so have to repeat it here!
|
||||
et.assertProjectInfoTelemetryEvent({
|
||||
projectId: Harness.LanguageService.mockHash("/hunter2/foo.csproj"),
|
||||
projectId: Harness.mockHash("/hunter2/foo.csproj"),
|
||||
compilerOptions: { strict: true },
|
||||
compileOnSave: true,
|
||||
// These properties can't be present for an external project, so they are undefined instead of false.
|
||||
|
@ -195,7 +195,7 @@ namespace ts.projectSystem {
|
|||
const et = new EventTracker([jsconfig, file]);
|
||||
et.service.openClientFile(file.path);
|
||||
et.assertProjectInfoTelemetryEvent({
|
||||
projectId: Harness.LanguageService.mockHash("/jsconfig.json"),
|
||||
projectId: Harness.mockHash("/jsconfig.json"),
|
||||
fileStats: fileStats({ js: 1 }),
|
||||
compilerOptions: autoJsCompilerOptions,
|
||||
typeAcquisition: {
|
||||
|
@ -215,7 +215,7 @@ namespace ts.projectSystem {
|
|||
et.service.openClientFile(file.path);
|
||||
et.getEvent<server.ProjectLanguageServiceStateEvent>(server.ProjectLanguageServiceStateEvent, /*mayBeMore*/ true);
|
||||
et.assertProjectInfoTelemetryEvent({
|
||||
projectId: Harness.LanguageService.mockHash("/jsconfig.json"),
|
||||
projectId: Harness.mockHash("/jsconfig.json"),
|
||||
fileStats: fileStats({ js: 1 }),
|
||||
compilerOptions: autoJsCompilerOptions,
|
||||
configFileName: "jsconfig.json",
|
||||
|
@ -251,7 +251,7 @@ namespace ts.projectSystem {
|
|||
|
||||
assertProjectInfoTelemetryEvent(partial: Partial<server.ProjectInfoTelemetryEventData>): void {
|
||||
assert.deepEqual(this.getEvent<server.ProjectInfoTelemetryEvent>(ts.server.ProjectInfoTelemetryEvent), {
|
||||
projectId: Harness.LanguageService.mockHash("/tsconfig.json"),
|
||||
projectId: Harness.mockHash("/tsconfig.json"),
|
||||
fileStats: fileStats({ ts: 1 }),
|
||||
compilerOptions: {},
|
||||
extends: false,
|
||||
|
@ -282,7 +282,7 @@ namespace ts.projectSystem {
|
|||
}
|
||||
|
||||
function makeFile(path: string, content: {} = ""): projectSystem.FileOrFolder {
|
||||
return { path, content: typeof content === "string" ? "" : JSON.stringify(content) };
|
||||
return { path, content: isString(content) ? "" : JSON.stringify(content) };
|
||||
}
|
||||
|
||||
function fileStats(nonZeroStats: Partial<server.FileStats>): server.FileStats {
|
||||
|
|
1657
src/harness/unittests/tscWatchMode.ts
Normal file
1657
src/harness/unittests/tscWatchMode.ts
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
550
src/harness/virtualFileSystemWithWatch.ts
Normal file
550
src/harness/virtualFileSystemWithWatch.ts
Normal file
|
@ -0,0 +1,550 @@
|
|||
/// <reference path="harness.ts" />
|
||||
|
||||
namespace ts.TestFSWithWatch {
|
||||
const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO);
|
||||
export const libFile: FileOrFolder = {
|
||||
path: "/a/lib/lib.d.ts",
|
||||
content: libFileContent
|
||||
};
|
||||
|
||||
export const safeList = {
|
||||
path: <Path>"/safeList.json",
|
||||
content: JSON.stringify({
|
||||
commander: "commander",
|
||||
express: "express",
|
||||
jquery: "jquery",
|
||||
lodash: "lodash",
|
||||
moment: "moment",
|
||||
chroma: "chroma-js"
|
||||
})
|
||||
};
|
||||
|
||||
function getExecutingFilePathFromLibFile(): string {
|
||||
return combinePaths(getDirectoryPath(libFile.path), "tsc.js");
|
||||
}
|
||||
|
||||
interface TestServerHostCreationParameters {
|
||||
useCaseSensitiveFileNames?: boolean;
|
||||
executingFilePath?: string;
|
||||
currentDirectory?: string;
|
||||
newLine?: string;
|
||||
}
|
||||
|
||||
export function createWatchedSystem(fileOrFolderList: FileOrFolder[], params?: TestServerHostCreationParameters): TestServerHost {
|
||||
if (!params) {
|
||||
params = {};
|
||||
}
|
||||
const host = new TestServerHost(/*withSafelist*/ false,
|
||||
params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false,
|
||||
params.executingFilePath || getExecutingFilePathFromLibFile(),
|
||||
params.currentDirectory || "/",
|
||||
fileOrFolderList,
|
||||
params.newLine);
|
||||
return host;
|
||||
}
|
||||
|
||||
export function createServerHost(fileOrFolderList: FileOrFolder[], params?: TestServerHostCreationParameters): TestServerHost {
|
||||
if (!params) {
|
||||
params = {};
|
||||
}
|
||||
const host = new TestServerHost(/*withSafelist*/ true,
|
||||
params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false,
|
||||
params.executingFilePath || getExecutingFilePathFromLibFile(),
|
||||
params.currentDirectory || "/",
|
||||
fileOrFolderList,
|
||||
params.newLine);
|
||||
return host;
|
||||
}
|
||||
|
||||
export interface FileOrFolder {
|
||||
path: string;
|
||||
content?: string;
|
||||
fileSize?: number;
|
||||
}
|
||||
|
||||
interface FSEntry {
|
||||
path: Path;
|
||||
fullPath: string;
|
||||
}
|
||||
|
||||
interface File extends FSEntry {
|
||||
content: string;
|
||||
fileSize?: number;
|
||||
}
|
||||
|
||||
interface Folder extends FSEntry {
|
||||
entries: FSEntry[];
|
||||
}
|
||||
|
||||
function isFolder(s: FSEntry): s is Folder {
|
||||
return s && isArray((<Folder>s).entries);
|
||||
}
|
||||
|
||||
function isFile(s: FSEntry): s is File {
|
||||
return s && isString((<File>s).content);
|
||||
}
|
||||
|
||||
function invokeWatcherCallbacks<T>(callbacks: T[], invokeCallback: (cb: T) => void): void {
|
||||
if (callbacks) {
|
||||
// The array copy is made to ensure that even if one of the callback removes the callbacks,
|
||||
// we dont miss any callbacks following it
|
||||
const cbs = callbacks.slice();
|
||||
for (const cb of cbs) {
|
||||
invokeCallback(cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkMapKeys(caption: string, map: Map<any>, expectedKeys: string[]) {
|
||||
assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}`);
|
||||
for (const name of expectedKeys) {
|
||||
assert.isTrue(map.has(name), `${caption} is expected to contain ${name}, actual keys: ${arrayFrom(map.keys())}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkFileNames(caption: string, actualFileNames: string[], expectedFileNames: string[]) {
|
||||
assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected ${JSON.stringify(expectedFileNames)}, got ${actualFileNames}`);
|
||||
for (const f of expectedFileNames) {
|
||||
assert.isTrue(contains(actualFileNames, f), `${caption}: expected to find ${f} in ${JSON.stringify(actualFileNames)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) {
|
||||
checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles);
|
||||
}
|
||||
|
||||
export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive = false) {
|
||||
checkMapKeys("watchedDirectories", recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories);
|
||||
}
|
||||
|
||||
export function checkOutputContains(host: TestServerHost, expected: ReadonlyArray<string>) {
|
||||
const mapExpected = arrayToSet(expected);
|
||||
const mapSeen = createMap<true>();
|
||||
for (const f of host.getOutput()) {
|
||||
assert.isUndefined(mapSeen.get(f), `Already found ${f} in ${JSON.stringify(host.getOutput())}`);
|
||||
if (mapExpected.has(f)) {
|
||||
mapExpected.delete(f);
|
||||
mapSeen.set(f, true);
|
||||
}
|
||||
}
|
||||
assert.equal(mapExpected.size, 0, `Output has missing ${JSON.stringify(flatMapIter(mapExpected.keys(), key => key))} in ${JSON.stringify(host.getOutput())}`);
|
||||
}
|
||||
|
||||
export function checkOutputDoesNotContain(host: TestServerHost, expectedToBeAbsent: string[] | ReadonlyArray<string>) {
|
||||
const mapExpectedToBeAbsent = arrayToSet(expectedToBeAbsent);
|
||||
for (const f of host.getOutput()) {
|
||||
assert.isFalse(mapExpectedToBeAbsent.has(f), `Contains ${f} in ${JSON.stringify(host.getOutput())}`);
|
||||
}
|
||||
}
|
||||
|
||||
class Callbacks {
|
||||
private map: TimeOutCallback[] = [];
|
||||
private nextId = 1;
|
||||
|
||||
register(cb: (...args: any[]) => void, args: any[]) {
|
||||
const timeoutId = this.nextId;
|
||||
this.nextId++;
|
||||
this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args);
|
||||
return timeoutId;
|
||||
}
|
||||
|
||||
unregister(id: any) {
|
||||
if (typeof id === "number") {
|
||||
delete this.map[id];
|
||||
}
|
||||
}
|
||||
|
||||
count() {
|
||||
let n = 0;
|
||||
for (const _ in this.map) {
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
invoke() {
|
||||
// Note: invoking a callback may result in new callbacks been queued,
|
||||
// so do not clear the entire callback list regardless. Only remove the
|
||||
// ones we have invoked.
|
||||
for (const key in this.map) {
|
||||
this.map[key]();
|
||||
delete this.map[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TimeOutCallback = () => any;
|
||||
|
||||
export class TestServerHost implements server.ServerHost {
|
||||
args: string[] = [];
|
||||
|
||||
private readonly output: string[] = [];
|
||||
|
||||
private fs: Map<FSEntry> = createMap<FSEntry>();
|
||||
private getCanonicalFileName: (s: string) => string;
|
||||
private toPath: (f: string) => Path;
|
||||
private timeoutCallbacks = new Callbacks();
|
||||
private immediateCallbacks = new Callbacks();
|
||||
|
||||
readonly watchedDirectories = createMultiMap<DirectoryWatcherCallback>();
|
||||
readonly watchedDirectoriesRecursive = createMultiMap<DirectoryWatcherCallback>();
|
||||
readonly watchedFiles = createMultiMap<FileWatcherCallback>();
|
||||
|
||||
constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, private executingFilePath: string, private currentDirectory: string, fileOrFolderList: FileOrFolder[], public readonly newLine = "\n") {
|
||||
this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
|
||||
this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName);
|
||||
|
||||
this.reloadFS(fileOrFolderList);
|
||||
}
|
||||
|
||||
toNormalizedAbsolutePath(s: string) {
|
||||
return getNormalizedAbsolutePath(s, this.currentDirectory);
|
||||
}
|
||||
|
||||
toFullPath(s: string) {
|
||||
return this.toPath(this.toNormalizedAbsolutePath(s));
|
||||
}
|
||||
|
||||
reloadFS(fileOrFolderList: FileOrFolder[]) {
|
||||
const mapNewLeaves = createMap<true>();
|
||||
const isNewFs = this.fs.size === 0;
|
||||
// always inject safelist file in the list of files
|
||||
for (const fileOrFolder of fileOrFolderList.concat(this.withSafeList ? safeList : [])) {
|
||||
const path = this.toFullPath(fileOrFolder.path);
|
||||
mapNewLeaves.set(path, true);
|
||||
// If its a change
|
||||
const currentEntry = this.fs.get(path);
|
||||
if (currentEntry) {
|
||||
if (isFile(currentEntry)) {
|
||||
if (isString(fileOrFolder.content)) {
|
||||
// Update file
|
||||
if (currentEntry.content !== fileOrFolder.content) {
|
||||
currentEntry.content = fileOrFolder.content;
|
||||
this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// TODO: Changing from file => folder
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Folder
|
||||
if (isString(fileOrFolder.content)) {
|
||||
// TODO: Changing from folder => file
|
||||
}
|
||||
else {
|
||||
// Folder update: Nothing to do.
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.ensureFileOrFolder(fileOrFolder);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNewFs) {
|
||||
this.fs.forEach((fileOrFolder, path) => {
|
||||
// If this entry is not from the new file or folder
|
||||
if (!mapNewLeaves.get(path)) {
|
||||
// Leaf entries that arent in new list => remove these
|
||||
if (isFile(fileOrFolder) || isFolder(fileOrFolder) && fileOrFolder.entries.length === 0) {
|
||||
this.removeFileOrFolder(fileOrFolder, folder => !mapNewLeaves.get(folder.path));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ensureFileOrFolder(fileOrFolder: FileOrFolder) {
|
||||
if (isString(fileOrFolder.content)) {
|
||||
const file = this.toFile(fileOrFolder);
|
||||
Debug.assert(!this.fs.get(file.path));
|
||||
const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath));
|
||||
this.addFileOrFolderInFolder(baseFolder, file);
|
||||
}
|
||||
else {
|
||||
const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory);
|
||||
this.ensureFolder(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureFolder(fullPath: string): Folder {
|
||||
const path = this.toPath(fullPath);
|
||||
let folder = this.fs.get(path) as Folder;
|
||||
if (!folder) {
|
||||
folder = this.toFolder(fullPath);
|
||||
const baseFullPath = getDirectoryPath(fullPath);
|
||||
if (fullPath !== baseFullPath) {
|
||||
// Add folder in the base folder
|
||||
const baseFolder = this.ensureFolder(baseFullPath);
|
||||
this.addFileOrFolderInFolder(baseFolder, folder);
|
||||
}
|
||||
else {
|
||||
// root folder
|
||||
Debug.assert(this.fs.size === 0);
|
||||
this.fs.set(path, folder);
|
||||
}
|
||||
}
|
||||
Debug.assert(isFolder(folder));
|
||||
return folder;
|
||||
}
|
||||
|
||||
private addFileOrFolderInFolder(folder: Folder, fileOrFolder: File | Folder) {
|
||||
folder.entries.push(fileOrFolder);
|
||||
this.fs.set(fileOrFolder.path, fileOrFolder);
|
||||
|
||||
if (isFile(fileOrFolder)) {
|
||||
this.invokeFileWatcher(fileOrFolder.fullPath, FileWatcherEventKind.Created);
|
||||
}
|
||||
this.invokeDirectoryWatcher(folder.fullPath, fileOrFolder.fullPath);
|
||||
}
|
||||
|
||||
private removeFileOrFolder(fileOrFolder: File | Folder, isRemovableLeafFolder: (folder: Folder) => boolean) {
|
||||
const basePath = getDirectoryPath(fileOrFolder.path);
|
||||
const baseFolder = this.fs.get(basePath) as Folder;
|
||||
if (basePath !== fileOrFolder.path) {
|
||||
Debug.assert(!!baseFolder);
|
||||
filterMutate(baseFolder.entries, entry => entry !== fileOrFolder);
|
||||
}
|
||||
this.fs.delete(fileOrFolder.path);
|
||||
|
||||
if (isFile(fileOrFolder)) {
|
||||
this.invokeFileWatcher(fileOrFolder.fullPath, FileWatcherEventKind.Deleted);
|
||||
}
|
||||
else {
|
||||
Debug.assert(fileOrFolder.entries.length === 0);
|
||||
const relativePath = this.getRelativePathToDirectory(fileOrFolder.fullPath, fileOrFolder.fullPath);
|
||||
// Invoke directory and recursive directory watcher for the folder
|
||||
// Here we arent invoking recursive directory watchers for the base folders
|
||||
// since that is something we would want to do for both file as well as folder we are deleting
|
||||
invokeWatcherCallbacks(this.watchedDirectories.get(fileOrFolder.path), cb => cb(relativePath));
|
||||
invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(fileOrFolder.path), cb => cb(relativePath));
|
||||
}
|
||||
|
||||
if (basePath !== fileOrFolder.path) {
|
||||
if (baseFolder.entries.length === 0 && isRemovableLeafFolder(baseFolder)) {
|
||||
this.removeFileOrFolder(baseFolder, isRemovableLeafFolder);
|
||||
}
|
||||
else {
|
||||
this.invokeRecursiveDirectoryWatcher(baseFolder.fullPath, fileOrFolder.fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind) {
|
||||
const callbacks = this.watchedFiles.get(this.toPath(fileFullPath));
|
||||
const fileName = getBaseFileName(fileFullPath);
|
||||
invokeWatcherCallbacks(callbacks, cb => cb(fileName, eventKind));
|
||||
}
|
||||
|
||||
private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) {
|
||||
return getRelativePathToDirectoryOrUrl(directoryFullPath, fileFullPath, this.currentDirectory, this.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will call the directory watcher for the folderFullPath and recursive directory watchers for this and base folders
|
||||
*/
|
||||
private invokeDirectoryWatcher(folderFullPath: string, fileName: string) {
|
||||
const relativePath = this.getRelativePathToDirectory(folderFullPath, fileName);
|
||||
invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => cb(relativePath));
|
||||
this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will call the recursive directory watcher for this directory as well as all the base directories
|
||||
*/
|
||||
private invokeRecursiveDirectoryWatcher(fullPath: string, fileName: string) {
|
||||
const relativePath = this.getRelativePathToDirectory(fullPath, fileName);
|
||||
invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(this.toPath(fullPath)), cb => cb(relativePath));
|
||||
const basePath = getDirectoryPath(fullPath);
|
||||
if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) {
|
||||
this.invokeRecursiveDirectoryWatcher(basePath, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
private toFile(fileOrFolder: FileOrFolder): File {
|
||||
const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory);
|
||||
return {
|
||||
path: this.toPath(fullPath),
|
||||
content: fileOrFolder.content,
|
||||
fullPath,
|
||||
fileSize: fileOrFolder.fileSize
|
||||
};
|
||||
}
|
||||
|
||||
private toFolder(path: string): Folder {
|
||||
const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory);
|
||||
return {
|
||||
path: this.toPath(fullPath),
|
||||
entries: [],
|
||||
fullPath
|
||||
};
|
||||
}
|
||||
|
||||
fileExists(s: string) {
|
||||
const path = this.toFullPath(s);
|
||||
return isFile(this.fs.get(path));
|
||||
}
|
||||
|
||||
readFile(s: string) {
|
||||
const fsEntry = this.fs.get(this.toFullPath(s));
|
||||
return isFile(fsEntry) ? fsEntry.content : undefined;
|
||||
}
|
||||
|
||||
getFileSize(s: string) {
|
||||
const path = this.toFullPath(s);
|
||||
const entry = this.fs.get(path);
|
||||
if (isFile(entry)) {
|
||||
return entry.fileSize ? entry.fileSize : entry.content.length;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
directoryExists(s: string) {
|
||||
const path = this.toFullPath(s);
|
||||
return isFolder(this.fs.get(path));
|
||||
}
|
||||
|
||||
getDirectories(s: string) {
|
||||
const path = this.toFullPath(s);
|
||||
const folder = this.fs.get(path);
|
||||
if (isFolder(folder)) {
|
||||
return mapDefined(folder.entries, entry => isFolder(entry) ? getBaseFileName(entry.fullPath) : undefined);
|
||||
}
|
||||
Debug.fail(folder ? "getDirectories called on file" : "getDirectories called on missing folder");
|
||||
return [];
|
||||
}
|
||||
|
||||
readDirectory(path: string, extensions?: ReadonlyArray<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, depth?: number): string[] {
|
||||
return ts.matchFiles(this.toNormalizedAbsolutePath(path), extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => {
|
||||
const directories: string[] = [];
|
||||
const files: string[] = [];
|
||||
const dirEntry = this.fs.get(this.toPath(dir));
|
||||
if (isFolder(dirEntry)) {
|
||||
dirEntry.entries.forEach((entry) => {
|
||||
if (isFolder(entry)) {
|
||||
directories.push(getBaseFileName(entry.fullPath));
|
||||
}
|
||||
else if (isFile(entry)) {
|
||||
files.push(getBaseFileName(entry.fullPath));
|
||||
}
|
||||
else {
|
||||
Debug.fail("Unknown entry");
|
||||
}
|
||||
});
|
||||
}
|
||||
return { directories, files };
|
||||
});
|
||||
}
|
||||
|
||||
watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean): DirectoryWatcher {
|
||||
const path = this.toFullPath(directoryName);
|
||||
const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories;
|
||||
map.add(path, callback);
|
||||
return {
|
||||
referenceCount: 0,
|
||||
directoryName,
|
||||
close: () => map.remove(path, callback)
|
||||
};
|
||||
}
|
||||
|
||||
createHash(s: string): string {
|
||||
return Harness.mockHash(s);
|
||||
}
|
||||
|
||||
watchFile(fileName: string, callback: FileWatcherCallback) {
|
||||
const path = this.toFullPath(fileName);
|
||||
this.watchedFiles.add(path, callback);
|
||||
return { close: () => this.watchedFiles.remove(path, callback) };
|
||||
}
|
||||
|
||||
// TOOD: record and invoke callbacks to simulate timer events
|
||||
setTimeout(callback: TimeOutCallback, _time: number, ...args: any[]) {
|
||||
return this.timeoutCallbacks.register(callback, args);
|
||||
}
|
||||
|
||||
clearTimeout(timeoutId: any): void {
|
||||
this.timeoutCallbacks.unregister(timeoutId);
|
||||
}
|
||||
|
||||
checkTimeoutQueueLengthAndRun(expected: number) {
|
||||
this.checkTimeoutQueueLength(expected);
|
||||
this.runQueuedTimeoutCallbacks();
|
||||
}
|
||||
|
||||
checkTimeoutQueueLength(expected: number) {
|
||||
const callbacksCount = this.timeoutCallbacks.count();
|
||||
assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`);
|
||||
}
|
||||
|
||||
runQueuedTimeoutCallbacks() {
|
||||
try {
|
||||
this.timeoutCallbacks.invoke();
|
||||
}
|
||||
catch (e) {
|
||||
if (e.message === this.existMessage) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
runQueuedImmediateCallbacks() {
|
||||
this.immediateCallbacks.invoke();
|
||||
}
|
||||
|
||||
setImmediate(callback: TimeOutCallback, _time: number, ...args: any[]) {
|
||||
return this.immediateCallbacks.register(callback, args);
|
||||
}
|
||||
|
||||
clearImmediate(timeoutId: any): void {
|
||||
this.immediateCallbacks.unregister(timeoutId);
|
||||
}
|
||||
|
||||
createDirectory(directoryName: string): void {
|
||||
const folder = this.toFolder(directoryName);
|
||||
|
||||
// base folder has to be present
|
||||
const base = getDirectoryPath(folder.fullPath);
|
||||
const baseFolder = this.fs.get(base) as Folder;
|
||||
Debug.assert(isFolder(baseFolder));
|
||||
|
||||
Debug.assert(!this.fs.get(folder.path));
|
||||
this.addFileOrFolderInFolder(baseFolder, folder);
|
||||
}
|
||||
|
||||
writeFile(path: string, content: string): void {
|
||||
const file = this.toFile({ path, content });
|
||||
|
||||
// base folder has to be present
|
||||
const base = getDirectoryPath(file.fullPath);
|
||||
const folder = this.fs.get(base) as Folder;
|
||||
Debug.assert(isFolder(folder));
|
||||
|
||||
this.addFileOrFolderInFolder(folder, file);
|
||||
}
|
||||
|
||||
write(message: string) {
|
||||
this.output.push(message);
|
||||
}
|
||||
|
||||
getOutput(): ReadonlyArray<string> {
|
||||
return this.output;
|
||||
}
|
||||
|
||||
clearOutput() {
|
||||
clear(this.output);
|
||||
}
|
||||
|
||||
readonly existMessage = "System Exit";
|
||||
exitCode: number;
|
||||
readonly resolvePath = (s: string) => s;
|
||||
readonly getExecutingFilePath = () => this.executingFilePath;
|
||||
readonly getCurrentDirectory = () => this.currentDirectory;
|
||||
exit(exitCode?: number) {
|
||||
this.exitCode = exitCode;
|
||||
throw new Error(this.existMessage);
|
||||
}
|
||||
readonly getEnvironmentVariable = notImplemented;
|
||||
}
|
||||
}
|
|
@ -1,359 +0,0 @@
|
|||
/// <reference path="..\compiler\commandLineParser.ts" />
|
||||
/// <reference path="..\services\services.ts" />
|
||||
/// <reference path="session.ts" />
|
||||
|
||||
namespace ts.server {
|
||||
|
||||
export function shouldEmitFile(scriptInfo: ScriptInfo) {
|
||||
return !scriptInfo.hasMixedContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract file info that maintains a shape signature.
|
||||
*/
|
||||
export class BuilderFileInfo {
|
||||
|
||||
private lastCheckedShapeSignature: string;
|
||||
|
||||
constructor(public readonly scriptInfo: ScriptInfo, public readonly project: Project) {
|
||||
}
|
||||
|
||||
public isExternalModuleOrHasOnlyAmbientExternalModules() {
|
||||
const sourceFile = this.getSourceFile();
|
||||
return isExternalModule(sourceFile) || this.containsOnlyAmbientModules(sourceFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* For script files that contains only ambient external modules, although they are not actually external module files,
|
||||
* they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore,
|
||||
* there are no point to rebuild all script files if these special files have changed. However, if any statement
|
||||
* in the file is not ambient external module, we treat it as a regular script file.
|
||||
*/
|
||||
private containsOnlyAmbientModules(sourceFile: SourceFile) {
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (statement.kind !== SyntaxKind.ModuleDeclaration || (<ModuleDeclaration>statement).name.kind !== SyntaxKind.StringLiteral) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private computeHash(text: string): string {
|
||||
return this.project.projectService.host.createHash(text);
|
||||
}
|
||||
|
||||
private getSourceFile(): SourceFile {
|
||||
return this.project.getSourceFile(this.scriptInfo.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean} indicates if the shape signature has changed since last update.
|
||||
*/
|
||||
public updateShapeSignature() {
|
||||
const sourceFile = this.getSourceFile();
|
||||
if (!sourceFile) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lastSignature = this.lastCheckedShapeSignature;
|
||||
if (sourceFile.isDeclarationFile) {
|
||||
this.lastCheckedShapeSignature = this.computeHash(sourceFile.text);
|
||||
}
|
||||
else {
|
||||
const emitOutput = this.project.getFileEmitOutput(this.scriptInfo, /*emitOnlyDtsFiles*/ true);
|
||||
if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) {
|
||||
this.lastCheckedShapeSignature = this.computeHash(emitOutput.outputFiles[0].text);
|
||||
}
|
||||
}
|
||||
return !lastSignature || this.lastCheckedShapeSignature !== lastSignature;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Builder {
|
||||
readonly project: Project;
|
||||
getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
|
||||
onProjectUpdateGraph(): void;
|
||||
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
abstract class AbstractBuilder<T extends BuilderFileInfo> implements Builder {
|
||||
|
||||
/**
|
||||
* stores set of files from the project.
|
||||
* NOTE: this field is created on demand and should not be accessed directly.
|
||||
* Use 'getFileInfos' instead.
|
||||
*/
|
||||
private fileInfos_doNotAccessDirectly: Map<T>;
|
||||
|
||||
constructor(public readonly project: Project, private ctor: { new (scriptInfo: ScriptInfo, project: Project): T }) {
|
||||
}
|
||||
|
||||
private getFileInfos() {
|
||||
return this.fileInfos_doNotAccessDirectly || (this.fileInfos_doNotAccessDirectly = createMap<T>());
|
||||
}
|
||||
|
||||
protected hasFileInfos() {
|
||||
return !!this.fileInfos_doNotAccessDirectly;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
// drop the existing list - it will be re-created as necessary
|
||||
this.fileInfos_doNotAccessDirectly = undefined;
|
||||
}
|
||||
|
||||
protected getFileInfo(path: Path): T {
|
||||
return this.getFileInfos().get(path);
|
||||
}
|
||||
|
||||
protected getOrCreateFileInfo(path: Path): T {
|
||||
let fileInfo = this.getFileInfo(path);
|
||||
if (!fileInfo) {
|
||||
const scriptInfo = this.project.getScriptInfo(path);
|
||||
fileInfo = new this.ctor(scriptInfo, this.project);
|
||||
this.setFileInfo(path, fileInfo);
|
||||
}
|
||||
return fileInfo;
|
||||
}
|
||||
|
||||
protected getFileInfoPaths(): Path[] {
|
||||
return arrayFrom(this.getFileInfos().keys() as Iterator<Path>);
|
||||
}
|
||||
|
||||
protected setFileInfo(path: Path, info: T) {
|
||||
this.getFileInfos().set(path, info);
|
||||
}
|
||||
|
||||
protected removeFileInfo(path: Path) {
|
||||
this.getFileInfos().delete(path);
|
||||
}
|
||||
|
||||
protected forEachFileInfo(action: (fileInfo: T) => any) {
|
||||
this.getFileInfos().forEach(action);
|
||||
}
|
||||
|
||||
abstract getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
|
||||
abstract onProjectUpdateGraph(): void;
|
||||
protected abstract ensureFileInfoIfInProject(scriptInfo: ScriptInfo): void;
|
||||
|
||||
/**
|
||||
* @returns {boolean} whether the emit was conducted or not
|
||||
*/
|
||||
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean {
|
||||
this.ensureFileInfoIfInProject(scriptInfo);
|
||||
const fileInfo = this.getFileInfo(scriptInfo.path);
|
||||
if (!fileInfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { emitSkipped, outputFiles } = this.project.getFileEmitOutput(fileInfo.scriptInfo, /*emitOnlyDtsFiles*/ false);
|
||||
if (!emitSkipped) {
|
||||
const projectRootPath = this.project.getProjectRootPath();
|
||||
for (const outputFile of outputFiles) {
|
||||
const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName));
|
||||
writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark);
|
||||
}
|
||||
}
|
||||
return !emitSkipped;
|
||||
}
|
||||
}
|
||||
|
||||
class NonModuleBuilder extends AbstractBuilder<BuilderFileInfo> {
|
||||
|
||||
constructor(public readonly project: Project) {
|
||||
super(project, BuilderFileInfo);
|
||||
}
|
||||
|
||||
protected ensureFileInfoIfInProject(scriptInfo: ScriptInfo) {
|
||||
if (this.project.containsScriptInfo(scriptInfo)) {
|
||||
this.getOrCreateFileInfo(scriptInfo.path);
|
||||
}
|
||||
}
|
||||
|
||||
onProjectUpdateGraph() {
|
||||
if (this.hasFileInfos()) {
|
||||
this.forEachFileInfo(fileInfo => {
|
||||
if (!this.project.containsScriptInfo(fileInfo.scriptInfo)) {
|
||||
// This file was deleted from this project
|
||||
this.removeFileInfo(fileInfo.scriptInfo.path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: didn't use path as parameter because the returned file names will be directly
|
||||
* consumed by the API user, which will use it to interact with file systems. Path
|
||||
* should only be used internally, because the case sensitivity is not trustable.
|
||||
*/
|
||||
getFilesAffectedBy(scriptInfo: ScriptInfo): string[] {
|
||||
const info = this.getOrCreateFileInfo(scriptInfo.path);
|
||||
const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName];
|
||||
if (info.updateShapeSignature()) {
|
||||
const options = this.project.getCompilerOptions();
|
||||
// If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project,
|
||||
// so returning the file itself is good enough.
|
||||
if (options && (options.out || options.outFile)) {
|
||||
return singleFileResult;
|
||||
}
|
||||
return this.project.getAllEmittableFiles();
|
||||
}
|
||||
return singleFileResult;
|
||||
}
|
||||
}
|
||||
|
||||
class ModuleBuilderFileInfo extends BuilderFileInfo {
|
||||
references = createSortedArray<ModuleBuilderFileInfo>();
|
||||
readonly referencedBy = createSortedArray<ModuleBuilderFileInfo>();
|
||||
scriptVersionForReferences: string;
|
||||
|
||||
static compareFileInfos(lf: ModuleBuilderFileInfo, rf: ModuleBuilderFileInfo): Comparison {
|
||||
return compareStrings(lf.scriptInfo.fileName, rf.scriptInfo.fileName);
|
||||
}
|
||||
|
||||
addReferencedBy(fileInfo: ModuleBuilderFileInfo): void {
|
||||
insertSorted(this.referencedBy, fileInfo, ModuleBuilderFileInfo.compareFileInfos);
|
||||
}
|
||||
|
||||
removeReferencedBy(fileInfo: ModuleBuilderFileInfo): void {
|
||||
removeSorted(this.referencedBy, fileInfo, ModuleBuilderFileInfo.compareFileInfos);
|
||||
}
|
||||
|
||||
removeFileReferences() {
|
||||
for (const reference of this.references) {
|
||||
reference.removeReferencedBy(this);
|
||||
}
|
||||
clear(this.references);
|
||||
}
|
||||
}
|
||||
|
||||
class ModuleBuilder extends AbstractBuilder<ModuleBuilderFileInfo> {
|
||||
|
||||
constructor(public readonly project: Project) {
|
||||
super(project, ModuleBuilderFileInfo);
|
||||
}
|
||||
|
||||
private projectVersionForDependencyGraph: string;
|
||||
|
||||
public clear() {
|
||||
this.projectVersionForDependencyGraph = undefined;
|
||||
super.clear();
|
||||
}
|
||||
|
||||
private getReferencedFileInfos(fileInfo: ModuleBuilderFileInfo): SortedArray<ModuleBuilderFileInfo> {
|
||||
if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) {
|
||||
return createSortedArray();
|
||||
}
|
||||
|
||||
const referencedFilePaths = this.project.getReferencedFiles(fileInfo.scriptInfo.path);
|
||||
return toSortedArray(referencedFilePaths.map(f => this.getOrCreateFileInfo(f)), ModuleBuilderFileInfo.compareFileInfos);
|
||||
}
|
||||
|
||||
protected ensureFileInfoIfInProject(_scriptInfo: ScriptInfo) {
|
||||
this.ensureProjectDependencyGraphUpToDate();
|
||||
}
|
||||
|
||||
onProjectUpdateGraph() {
|
||||
// Update the graph only if we have computed graph earlier
|
||||
if (this.hasFileInfos()) {
|
||||
this.ensureProjectDependencyGraphUpToDate();
|
||||
}
|
||||
}
|
||||
|
||||
private ensureProjectDependencyGraphUpToDate() {
|
||||
if (!this.projectVersionForDependencyGraph || this.project.getProjectVersion() !== this.projectVersionForDependencyGraph) {
|
||||
const currentScriptInfos = this.project.getScriptInfos();
|
||||
for (const scriptInfo of currentScriptInfos) {
|
||||
const fileInfo = this.getOrCreateFileInfo(scriptInfo.path);
|
||||
this.updateFileReferences(fileInfo);
|
||||
}
|
||||
this.forEachFileInfo(fileInfo => {
|
||||
if (!this.project.containsScriptInfo(fileInfo.scriptInfo)) {
|
||||
// This file was deleted from this project
|
||||
fileInfo.removeFileReferences();
|
||||
this.removeFileInfo(fileInfo.scriptInfo.path);
|
||||
}
|
||||
});
|
||||
this.projectVersionForDependencyGraph = this.project.getProjectVersion();
|
||||
}
|
||||
}
|
||||
|
||||
private updateFileReferences(fileInfo: ModuleBuilderFileInfo) {
|
||||
// Only need to update if the content of the file changed.
|
||||
if (fileInfo.scriptVersionForReferences === fileInfo.scriptInfo.getLatestVersion()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newReferences = this.getReferencedFileInfos(fileInfo);
|
||||
const oldReferences = fileInfo.references;
|
||||
enumerateInsertsAndDeletes(newReferences, oldReferences,
|
||||
/*inserted*/ newReference => newReference.addReferencedBy(fileInfo),
|
||||
/*deleted*/ oldReference => {
|
||||
// New reference is greater then current reference. That means
|
||||
// the current reference doesn't exist anymore after parsing. So delete
|
||||
// references.
|
||||
oldReference.removeReferencedBy(fileInfo);
|
||||
},
|
||||
/*compare*/ ModuleBuilderFileInfo.compareFileInfos);
|
||||
|
||||
fileInfo.references = newReferences;
|
||||
fileInfo.scriptVersionForReferences = fileInfo.scriptInfo.getLatestVersion();
|
||||
}
|
||||
|
||||
getFilesAffectedBy(scriptInfo: ScriptInfo): string[] {
|
||||
this.ensureProjectDependencyGraphUpToDate();
|
||||
|
||||
const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName];
|
||||
const fileInfo = this.getFileInfo(scriptInfo.path);
|
||||
if (!fileInfo || !fileInfo.updateShapeSignature()) {
|
||||
return singleFileResult;
|
||||
}
|
||||
|
||||
if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) {
|
||||
return this.project.getAllEmittableFiles();
|
||||
}
|
||||
|
||||
const options = this.project.getCompilerOptions();
|
||||
if (options && (options.isolatedModules || options.out || options.outFile)) {
|
||||
return singleFileResult;
|
||||
}
|
||||
|
||||
// Now we need to if each file in the referencedBy list has a shape change as well.
|
||||
// Because if so, its own referencedBy files need to be saved as well to make the
|
||||
// emitting result consistent with files on disk.
|
||||
|
||||
// Use slice to clone the array to avoid manipulating in place
|
||||
const queue = fileInfo.referencedBy.slice(0);
|
||||
const fileNameSet = createMap<ScriptInfo>();
|
||||
fileNameSet.set(scriptInfo.fileName, scriptInfo);
|
||||
while (queue.length > 0) {
|
||||
const processingFileInfo = queue.pop();
|
||||
if (processingFileInfo.updateShapeSignature() && processingFileInfo.referencedBy.length > 0) {
|
||||
for (const potentialFileInfo of processingFileInfo.referencedBy) {
|
||||
if (!fileNameSet.has(potentialFileInfo.scriptInfo.fileName)) {
|
||||
queue.push(potentialFileInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
fileNameSet.set(processingFileInfo.scriptInfo.fileName, processingFileInfo.scriptInfo);
|
||||
}
|
||||
const result: string[] = [];
|
||||
fileNameSet.forEach((scriptInfo, fileName) => {
|
||||
if (shouldEmitFile(scriptInfo)) {
|
||||
result.push(fileName);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function createBuilder(project: Project): Builder {
|
||||
const moduleKind = project.getCompilerOptions().module;
|
||||
switch (moduleKind) {
|
||||
case ModuleKind.None:
|
||||
return new NonModuleBuilder(project);
|
||||
default:
|
||||
return new ModuleBuilder(project);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -342,7 +342,7 @@ namespace ts.server {
|
|||
convertDiagnostic(entry: protocol.DiagnosticWithLinePosition, _fileName: string): Diagnostic {
|
||||
let category: DiagnosticCategory;
|
||||
for (const id in DiagnosticCategory) {
|
||||
if (typeof id === "string" && entry.category === id.toLowerCase()) {
|
||||
if (isString(id) && entry.category === id.toLowerCase()) {
|
||||
category = (<any>DiagnosticCategory)[id];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,14 +10,14 @@
|
|||
namespace ts.server {
|
||||
export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024;
|
||||
|
||||
export const ContextEvent = "context";
|
||||
export const ProjectChangedEvent = "projectChanged";
|
||||
export const ConfigFileDiagEvent = "configFileDiag";
|
||||
export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState";
|
||||
export const ProjectInfoTelemetryEvent = "projectInfo";
|
||||
|
||||
export interface ContextEvent {
|
||||
eventName: typeof ContextEvent;
|
||||
data: { project: Project; fileName: NormalizedPath };
|
||||
export interface ProjectChangedEvent {
|
||||
eventName: typeof ProjectChangedEvent;
|
||||
data: { project: Project; filesToEmit: string[]; changedFiles: string[] };
|
||||
}
|
||||
|
||||
export interface ConfigFileDiagEvent {
|
||||
|
@ -77,7 +77,7 @@ namespace ts.server {
|
|||
readonly dts: number;
|
||||
}
|
||||
|
||||
export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent;
|
||||
export type ProjectServiceEvent = ProjectChangedEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent;
|
||||
|
||||
export interface ProjectServiceEventHandler {
|
||||
(event: ProjectServiceEvent): void;
|
||||
|
@ -159,7 +159,7 @@ namespace ts.server {
|
|||
};
|
||||
|
||||
export function convertFormatOptions(protocolOptions: protocol.FormatCodeSettings): FormatCodeSettings {
|
||||
if (typeof protocolOptions.indentStyle === "string") {
|
||||
if (isString(protocolOptions.indentStyle)) {
|
||||
protocolOptions.indentStyle = indentStyle.get(protocolOptions.indentStyle.toLowerCase());
|
||||
Debug.assert(protocolOptions.indentStyle !== undefined);
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ namespace ts.server {
|
|||
export function convertCompilerOptions(protocolOptions: protocol.ExternalProjectCompilerOptions): CompilerOptions & protocol.CompileOnSaveMixin {
|
||||
compilerOptionConverters.forEach((mappedValues, id) => {
|
||||
const propertyValue = protocolOptions[id];
|
||||
if (typeof propertyValue === "string") {
|
||||
if (isString(propertyValue)) {
|
||||
protocolOptions[id] = mappedValues.get(propertyValue.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
@ -177,9 +177,7 @@ namespace ts.server {
|
|||
}
|
||||
|
||||
export function tryConvertScriptKindName(scriptKindName: protocol.ScriptKindName | ScriptKind): ScriptKind {
|
||||
return typeof scriptKindName === "string"
|
||||
? convertScriptKindName(scriptKindName)
|
||||
: scriptKindName;
|
||||
return isString(scriptKindName) ? convertScriptKindName(scriptKindName) : scriptKindName;
|
||||
}
|
||||
|
||||
export function convertScriptKindName(scriptKindName: protocol.ScriptKindName) {
|
||||
|
@ -249,7 +247,8 @@ namespace ts.server {
|
|||
WildcardDirectories = "Wild card directory",
|
||||
TypeRoot = "Type root of the project",
|
||||
ClosedScriptInfo = "Closed Script info",
|
||||
ConfigFileForInferredRoot = "Config file for the inferred project root"
|
||||
ConfigFileForInferredRoot = "Config file for the inferred project root",
|
||||
FailedLookupLocation = "Failed lookup locations in module resolution"
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
|
@ -493,9 +492,32 @@ namespace ts.server {
|
|||
if (this.pendingProjectUpdates.delete(projectName)) {
|
||||
project.updateGraph();
|
||||
}
|
||||
// Send the update event to notify about the project changes
|
||||
this.sendProjectChangedEvent(project);
|
||||
});
|
||||
}
|
||||
|
||||
private sendProjectChangedEvent(project: Project) {
|
||||
if (project.isClosed() || !this.eventHandler || !project.languageServiceEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { filesToEmit, changedFiles } = project.getChangedFiles();
|
||||
if (changedFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event: ProjectChangedEvent = {
|
||||
eventName: ProjectChangedEvent,
|
||||
data: {
|
||||
project,
|
||||
filesToEmit: filesToEmit as string[],
|
||||
changedFiles: changedFiles as string[]
|
||||
}
|
||||
};
|
||||
this.eventHandler(event);
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
delayUpdateProjectGraphAndInferredProjectsRefresh(project: Project) {
|
||||
this.delayUpdateProjectGraph(project);
|
||||
|
@ -567,6 +589,11 @@ namespace ts.server {
|
|||
return scriptInfo && !scriptInfo.isOrphan() && scriptInfo.getDefaultProject();
|
||||
}
|
||||
|
||||
getScriptInfoEnsuringProjectsUptoDate(uncheckedFileName: string) {
|
||||
this.ensureProjectStructuresUptoDate();
|
||||
return this.getScriptInfo(uncheckedFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the project structures are upto date
|
||||
* This means,
|
||||
|
@ -674,25 +701,12 @@ namespace ts.server {
|
|||
|
||||
// update projects to make sure that set of referenced files is correct
|
||||
this.delayUpdateProjectGraphs(containingProjects);
|
||||
|
||||
// TODO: (sheetalkamat) Someway to send this event so that error checks are updated?
|
||||
// if (!this.eventHandler) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// for (const openFile of this.openFiles) {
|
||||
// const event: ContextEvent = {
|
||||
// eventName: ContextEvent,
|
||||
// data: { project: openFile.getDefaultProject(), fileName: openFile.fileName }
|
||||
// };
|
||||
// this.eventHandler(event);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
onTypeRootFileChanged(project: ConfiguredProject, fileName: NormalizedPath) {
|
||||
project.getCachedServerHost().addOrDeleteFileOrFolder(fileName);
|
||||
onTypeRootFileChanged(project: ConfiguredProject, fileOrFolder: NormalizedPath) {
|
||||
project.getCachedServerHost().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder));
|
||||
project.updateTypes();
|
||||
this.delayUpdateProjectGraphAndInferredProjectsRefresh(project);
|
||||
}
|
||||
|
@ -703,15 +717,15 @@ namespace ts.server {
|
|||
* @param fileName the absolute file name that changed in watched directory
|
||||
*/
|
||||
/* @internal */
|
||||
onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileName: NormalizedPath) {
|
||||
project.getCachedServerHost().addOrDeleteFileOrFolder(fileName);
|
||||
onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileOrFolder: NormalizedPath) {
|
||||
project.getCachedServerHost().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder));
|
||||
const configFilename = project.getConfigFilePath();
|
||||
|
||||
// If a change was made inside "folder/file", node will trigger the callback twice:
|
||||
// one with the fileName being "folder/file", and the other one with "folder".
|
||||
// We don't respond to the second one.
|
||||
if (fileName && !isSupportedSourceFileName(fileName, project.getCompilerOptions(), this.hostConfiguration.extraFileExtensions)) {
|
||||
this.logger.info(`Project: ${configFilename} Detected file add/remove of non supported extension: ${fileName}`);
|
||||
if (fileOrFolder && !isSupportedSourceFileName(fileOrFolder, project.getCompilerOptions(), this.hostConfiguration.extraFileExtensions)) {
|
||||
this.logger.info(`Project: ${configFilename} Detected file add/remove of non supported extension: ${fileOrFolder}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1019,7 +1033,7 @@ namespace ts.server {
|
|||
if (this.configuredProjects.has(canonicalConfigFilePath)) {
|
||||
watches.push(WatchType.ConfigFilePath);
|
||||
}
|
||||
this.logger.info(`ConfigFilePresence:: Current Watches: ['${watches.join("','")}']:: File: ${configFileName} Currently impacted open files: RootsOfInferredProjects: ${inferredRoots} OtherOpenFiles: ${otherFiles} Status: ${status}`);
|
||||
this.logger.info(`ConfigFilePresence:: Current Watches: ${watches}:: File: ${configFileName} Currently impacted open files: RootsOfInferredProjects: ${inferredRoots} OtherOpenFiles: ${otherFiles} Status: ${status}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1371,7 +1385,7 @@ namespace ts.server {
|
|||
}
|
||||
|
||||
private createConfiguredProject(configFileName: NormalizedPath, clientFileName?: string) {
|
||||
const cachedServerHost = new CachedServerHost(this.host, this.toCanonicalFileName);
|
||||
const cachedServerHost = new CachedServerHost(this.host);
|
||||
const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedServerHost);
|
||||
this.logger.info(`Opened configuration file ${configFileName}`);
|
||||
const languageServiceEnabled = !this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader);
|
||||
|
@ -1425,7 +1439,7 @@ namespace ts.server {
|
|||
else {
|
||||
const scriptKind = propertyReader.getScriptKind(f);
|
||||
const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions);
|
||||
scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ clientFileName === newRootFile, /*fileContent*/ undefined, scriptKind, hasMixedContent);
|
||||
scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ clientFileName === newRootFile, /*fileContent*/ undefined, scriptKind, hasMixedContent, project.lsHost.host);
|
||||
path = scriptInfo.path;
|
||||
// If this script info is not already a root add it
|
||||
if (!project.isRoot(scriptInfo)) {
|
||||
|
@ -1579,9 +1593,12 @@ namespace ts.server {
|
|||
* @param uncheckedFileName is absolute pathname
|
||||
* @param fileContent is a known version of the file content that is more up to date than the one on disk
|
||||
*/
|
||||
|
||||
getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) {
|
||||
return this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName), openedByClient, fileContent, scriptKind);
|
||||
/*@internal*/
|
||||
getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, hostToQueryFileExistsOn: ServerHost) {
|
||||
return this.getOrCreateScriptInfoForNormalizedPath(
|
||||
toNormalizedPath(uncheckedFileName), openedByClient, /*fileContent*/ undefined,
|
||||
/*scriptKind*/ undefined, /*hasMixedContent*/ undefined, hostToQueryFileExistsOn
|
||||
);
|
||||
}
|
||||
|
||||
getScriptInfo(uncheckedFileName: string) {
|
||||
|
@ -1606,11 +1623,11 @@ namespace ts.server {
|
|||
}
|
||||
}
|
||||
|
||||
getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean) {
|
||||
getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: ServerHost) {
|
||||
const path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName);
|
||||
let info = this.getScriptInfoForPath(path);
|
||||
if (!info) {
|
||||
if (openedByClient || this.host.fileExists(fileName)) {
|
||||
if (openedByClient || (hostToQueryFileExistsOn || this.host).fileExists(fileName)) {
|
||||
info = new ScriptInfo(this.host, fileName, scriptKind, hasMixedContent, path);
|
||||
|
||||
this.filenameToScriptInfo.set(info.path, info);
|
||||
|
@ -2063,7 +2080,7 @@ namespace ts.server {
|
|||
// RegExp group numbers are 1-based, but the first element in groups
|
||||
// is actually the original string, so it all works out in the end.
|
||||
if (typeof groupNumberOrString === "number") {
|
||||
if (typeof groups[groupNumberOrString] !== "string") {
|
||||
if (!isString(groups[groupNumberOrString])) {
|
||||
// Specification was wrong - exclude nothing!
|
||||
this.logger.info(`Incorrect RegExp specification in safelist rule ${name} - not enough groups`);
|
||||
// * can't appear in a filename; escape it because it's feeding into a RegExp
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
/// <reference path="..\services\services.ts" />
|
||||
/// <reference path="utilities.ts" />
|
||||
/// <reference path="scriptInfo.ts" />
|
||||
/// <reference path="..\compiler\resolutionCache.ts" />
|
||||
|
||||
namespace ts.server {
|
||||
/*@internal*/
|
||||
export class CachedServerHost implements ServerHost {
|
||||
args: string[];
|
||||
newLine: string;
|
||||
useCaseSensitiveFileNames: boolean;
|
||||
|
||||
private readonly cachedPartialSystem: CachedPartialSystem;
|
||||
|
||||
readonly trace: (s: string) => void;
|
||||
readonly realpath?: (path: string) => string;
|
||||
|
||||
private cachedReadDirectoryResult = createMap<FileSystemEntries>();
|
||||
private readonly currentDirectory: string;
|
||||
|
||||
constructor(private readonly host: ServerHost, private getCanonicalFileName: (fileName: string) => string) {
|
||||
constructor(private readonly host: ServerHost) {
|
||||
this.args = host.args;
|
||||
this.newLine = host.newLine;
|
||||
this.useCaseSensitiveFileNames = host.useCaseSensitiveFileNames;
|
||||
|
@ -24,41 +25,7 @@ namespace ts.server {
|
|||
if (this.host.realpath) {
|
||||
this.realpath = path => this.host.realpath(path);
|
||||
}
|
||||
this.currentDirectory = this.host.getCurrentDirectory();
|
||||
}
|
||||
|
||||
private toPath(fileName: string) {
|
||||
return toPath(fileName, this.currentDirectory, this.getCanonicalFileName);
|
||||
}
|
||||
|
||||
private getFileSystemEntries(rootDir: string) {
|
||||
const path = this.toPath(rootDir);
|
||||
const cachedResult = this.cachedReadDirectoryResult.get(path);
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const resultFromHost: FileSystemEntries = {
|
||||
files: this.host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]) || [],
|
||||
directories: this.host.getDirectories(rootDir) || []
|
||||
};
|
||||
|
||||
this.cachedReadDirectoryResult.set(path, resultFromHost);
|
||||
return resultFromHost;
|
||||
}
|
||||
|
||||
private canWorkWithCacheForDir(rootDir: string) {
|
||||
// Some of the hosts might not be able to handle read directory or getDirectories
|
||||
const path = this.toPath(rootDir);
|
||||
if (this.cachedReadDirectoryResult.get(path)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return this.getFileSystemEntries(rootDir);
|
||||
}
|
||||
catch (_e) {
|
||||
return false;
|
||||
}
|
||||
this.cachedPartialSystem = createCachedPartialSystem(host);
|
||||
}
|
||||
|
||||
write(s: string) {
|
||||
|
@ -66,13 +33,7 @@ namespace ts.server {
|
|||
}
|
||||
|
||||
writeFile(fileName: string, data: string, writeByteOrderMark?: boolean) {
|
||||
const path = this.toPath(fileName);
|
||||
const result = this.cachedReadDirectoryResult.get(getDirectoryPath(path));
|
||||
const baseFileName = getBaseFileName(toNormalizedPath(fileName));
|
||||
if (result) {
|
||||
result.files = this.updateFileSystemEntry(result.files, baseFileName, /*isValid*/ true);
|
||||
}
|
||||
return this.host.writeFile(fileName, data, writeByteOrderMark);
|
||||
this.cachedPartialSystem.writeFile(fileName, data, writeByteOrderMark);
|
||||
}
|
||||
|
||||
resolvePath(path: string) {
|
||||
|
@ -88,7 +49,7 @@ namespace ts.server {
|
|||
}
|
||||
|
||||
getCurrentDirectory() {
|
||||
return this.currentDirectory;
|
||||
return this.cachedPartialSystem.getCurrentDirectory();
|
||||
}
|
||||
|
||||
exit(exitCode?: number) {
|
||||
|
@ -101,78 +62,35 @@ namespace ts.server {
|
|||
}
|
||||
|
||||
getDirectories(rootDir: string) {
|
||||
if (this.canWorkWithCacheForDir(rootDir)) {
|
||||
return this.getFileSystemEntries(rootDir).directories.slice();
|
||||
}
|
||||
return this.host.getDirectories(rootDir);
|
||||
return this.cachedPartialSystem.getDirectories(rootDir);
|
||||
}
|
||||
|
||||
readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] {
|
||||
if (this.canWorkWithCacheForDir(rootDir)) {
|
||||
return matchFiles(rootDir, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.currentDirectory, depth, path => this.getFileSystemEntries(path));
|
||||
}
|
||||
return this.host.readDirectory(rootDir, extensions, excludes, includes, depth);
|
||||
return this.cachedPartialSystem.readDirectory(rootDir, extensions, excludes, includes, depth);
|
||||
}
|
||||
|
||||
fileExists(fileName: string): boolean {
|
||||
const path = this.toPath(fileName);
|
||||
const result = this.cachedReadDirectoryResult.get(getDirectoryPath(path));
|
||||
const baseName = getBaseFileName(toNormalizedPath(fileName));
|
||||
return (result && this.hasEntry(result.files, baseName)) || this.host.fileExists(fileName);
|
||||
return this.cachedPartialSystem.fileExists(fileName);
|
||||
}
|
||||
|
||||
directoryExists(dirPath: string) {
|
||||
const path = this.toPath(dirPath);
|
||||
return this.cachedReadDirectoryResult.has(path) || this.host.directoryExists(dirPath);
|
||||
return this.cachedPartialSystem.directoryExists(dirPath);
|
||||
}
|
||||
|
||||
readFile(path: string, encoding?: string): string {
|
||||
return this.host.readFile(path, encoding);
|
||||
}
|
||||
|
||||
private fileNameEqual(name1: string, name2: string) {
|
||||
return this.getCanonicalFileName(name1) === this.getCanonicalFileName(name2);
|
||||
addOrDeleteFileOrFolder(fileOrFolder: NormalizedPath, fileOrFolderPath: Path) {
|
||||
return this.cachedPartialSystem.addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath);
|
||||
}
|
||||
|
||||
private hasEntry(entries: ReadonlyArray<string>, name: string) {
|
||||
return some(entries, file => this.fileNameEqual(file, name));
|
||||
}
|
||||
|
||||
private updateFileSystemEntry(entries: ReadonlyArray<string>, baseName: string, isValid: boolean) {
|
||||
if (this.hasEntry(entries, baseName)) {
|
||||
if (!isValid) {
|
||||
return filter(entries, entry => !this.fileNameEqual(entry, baseName));
|
||||
}
|
||||
}
|
||||
else if (isValid) {
|
||||
return entries.concat(baseName);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
addOrDeleteFileOrFolder(fileOrFolder: NormalizedPath) {
|
||||
const path = this.toPath(fileOrFolder);
|
||||
const existingResult = this.cachedReadDirectoryResult.get(path);
|
||||
if (existingResult) {
|
||||
// This was a folder already present, remove it if this doesnt exist any more
|
||||
if (!this.host.directoryExists(fileOrFolder)) {
|
||||
this.cachedReadDirectoryResult.delete(path);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// This was earlier a file (hence not in cached directory contents)
|
||||
// or we never cached the directory containing it
|
||||
const parentResult = this.cachedReadDirectoryResult.get(getDirectoryPath(path));
|
||||
if (parentResult) {
|
||||
const baseName = getBaseFileName(fileOrFolder);
|
||||
parentResult.files = this.updateFileSystemEntry(parentResult.files, baseName, this.host.fileExists(path));
|
||||
parentResult.directories = this.updateFileSystemEntry(parentResult.directories, baseName, this.host.directoryExists(path));
|
||||
}
|
||||
}
|
||||
addOrDeleteFile(file: string, path: Path, eventKind: FileWatcherEventKind) {
|
||||
return this.cachedPartialSystem.addOrDeleteFile(file, path, eventKind);
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
this.cachedReadDirectoryResult = createMap<FileSystemEntries>();
|
||||
return this.cachedPartialSystem.clearCache();
|
||||
}
|
||||
|
||||
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]) {
|
||||
|
@ -190,17 +108,16 @@ namespace ts.server {
|
|||
|
||||
}
|
||||
|
||||
type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean };
|
||||
export class LSHost implements LanguageServiceHost, ModuleResolutionHost {
|
||||
private compilationSettings: CompilerOptions;
|
||||
private readonly resolvedModuleNames = createMap<Map<ResolvedModuleWithFailedLookupLocations>>();
|
||||
private readonly resolvedTypeReferenceDirectives = createMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
|
||||
/*@internal*/
|
||||
compilationSettings: CompilerOptions;
|
||||
|
||||
private filesWithChangedSetOfUnresolvedImports: Path[];
|
||||
|
||||
private resolveModuleName: typeof resolveModuleName;
|
||||
readonly trace: (s: string) => void;
|
||||
readonly realpath?: (path: string) => string;
|
||||
|
||||
/*@internal*/
|
||||
hasInvalidatedResolution: HasInvalidatedResolution;
|
||||
|
||||
/**
|
||||
* This is the host that is associated with the project. This is normally same as projectService's host
|
||||
* except in Configured projects where it is CachedServerHost so that we can cache the results of the
|
||||
|
@ -217,25 +134,6 @@ namespace ts.server {
|
|||
this.trace = s => host.trace(s);
|
||||
}
|
||||
|
||||
this.resolveModuleName = (moduleName, containingFile, compilerOptions, host) => {
|
||||
const globalCache = this.project.getTypeAcquisition().enable
|
||||
? this.project.projectService.typingsInstaller.globalTypingsCacheLocation
|
||||
: undefined;
|
||||
const primaryResult = resolveModuleName(moduleName, containingFile, compilerOptions, host);
|
||||
// return result immediately only if it is .ts, .tsx or .d.ts
|
||||
if (!isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension)) && globalCache !== undefined) {
|
||||
// otherwise try to load typings from @types
|
||||
|
||||
// create different collection of failed lookup locations for second pass
|
||||
// if it will fail and we've already found something during the first pass - we don't want to pollute its results
|
||||
const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, this.project.getProjectName(), compilerOptions, host, globalCache);
|
||||
if (resolvedModule) {
|
||||
return { resolvedModule, failedLookupLocations: primaryResult.failedLookupLocations.concat(failedLookupLocations) };
|
||||
}
|
||||
}
|
||||
return primaryResult;
|
||||
};
|
||||
|
||||
if (this.host.realpath) {
|
||||
this.realpath = path => this.host.realpath(path);
|
||||
}
|
||||
|
@ -243,99 +141,9 @@ namespace ts.server {
|
|||
|
||||
dispose() {
|
||||
this.project = undefined;
|
||||
this.resolveModuleName = undefined;
|
||||
this.host = undefined;
|
||||
}
|
||||
|
||||
public startRecordingFilesWithChangedResolutions() {
|
||||
this.filesWithChangedSetOfUnresolvedImports = [];
|
||||
}
|
||||
|
||||
public finishRecordingFilesWithChangedResolutions() {
|
||||
const collected = this.filesWithChangedSetOfUnresolvedImports;
|
||||
this.filesWithChangedSetOfUnresolvedImports = undefined;
|
||||
return collected;
|
||||
}
|
||||
|
||||
private resolveNamesWithLocalCache<T extends NameResolutionWithFailedLookupLocations, R>(
|
||||
names: string[],
|
||||
containingFile: string,
|
||||
cache: Map<Map<T>>,
|
||||
loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T,
|
||||
getResult: (s: T) => R,
|
||||
getResultFileName: (result: R) => string | undefined,
|
||||
logChanges: boolean): R[] {
|
||||
|
||||
const path = this.project.projectService.toPath(containingFile);
|
||||
const currentResolutionsInFile = cache.get(path);
|
||||
|
||||
const newResolutions: Map<T> = createMap<T>();
|
||||
const resolvedModules: R[] = [];
|
||||
const compilerOptions = this.getCompilationSettings();
|
||||
|
||||
for (const name of names) {
|
||||
// check if this is a duplicate entry in the list
|
||||
let resolution = newResolutions.get(name);
|
||||
if (!resolution) {
|
||||
const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name);
|
||||
if (moduleResolutionIsValid(existingResolution)) {
|
||||
// ok, it is safe to use existing name resolution results
|
||||
resolution = existingResolution;
|
||||
}
|
||||
else {
|
||||
resolution = loader(name, containingFile, compilerOptions, this);
|
||||
newResolutions.set(name, resolution);
|
||||
}
|
||||
if (logChanges && this.filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) {
|
||||
this.filesWithChangedSetOfUnresolvedImports.push(path);
|
||||
// reset log changes to avoid recording the same file multiple times
|
||||
logChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.assert(resolution !== undefined);
|
||||
|
||||
resolvedModules.push(getResult(resolution));
|
||||
}
|
||||
|
||||
// replace old results with a new one
|
||||
cache.set(path, newResolutions);
|
||||
return resolvedModules;
|
||||
|
||||
function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean {
|
||||
if (oldResolution === newResolution) {
|
||||
return true;
|
||||
}
|
||||
if (!oldResolution || !newResolution || oldResolution.isInvalidated) {
|
||||
return false;
|
||||
}
|
||||
const oldResult = getResult(oldResolution);
|
||||
const newResult = getResult(newResolution);
|
||||
if (oldResult === newResult) {
|
||||
return true;
|
||||
}
|
||||
if (!oldResult || !newResult) {
|
||||
return false;
|
||||
}
|
||||
return getResultFileName(oldResult) === getResultFileName(newResult);
|
||||
}
|
||||
|
||||
function moduleResolutionIsValid(resolution: T): boolean {
|
||||
if (!resolution || resolution.isInvalidated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = getResult(resolution);
|
||||
if (result) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// consider situation if we have no candidate locations as valid resolution.
|
||||
// after all there is no point to invalidate it if we have no idea where to look for the module.
|
||||
return resolution.failedLookupLocations.length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
getNewLine() {
|
||||
return this.host.newLine;
|
||||
}
|
||||
|
@ -357,13 +165,11 @@ namespace ts.server {
|
|||
}
|
||||
|
||||
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] {
|
||||
return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective,
|
||||
m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false);
|
||||
return this.project.resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile);
|
||||
}
|
||||
|
||||
resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModuleFull[] {
|
||||
return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, this.resolveModuleName,
|
||||
m => m.resolvedModule, r => r.resolvedFileName, /*logChanges*/ true);
|
||||
return this.project.resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ true);
|
||||
}
|
||||
|
||||
getDefaultLibFileName() {
|
||||
|
@ -426,44 +232,5 @@ namespace ts.server {
|
|||
getDirectories(path: string): string[] {
|
||||
return this.host.getDirectories(path);
|
||||
}
|
||||
|
||||
notifyFileRemoved(info: ScriptInfo) {
|
||||
this.invalidateResolutionOfDeletedFile(info, this.resolvedModuleNames,
|
||||
m => m.resolvedModule, r => r.resolvedFileName);
|
||||
this.invalidateResolutionOfDeletedFile(info, this.resolvedTypeReferenceDirectives,
|
||||
m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName);
|
||||
}
|
||||
|
||||
private invalidateResolutionOfDeletedFile<T extends NameResolutionWithFailedLookupLocations, R>(
|
||||
deletedInfo: ScriptInfo,
|
||||
cache: Map<Map<T>>,
|
||||
getResult: (s: T) => R,
|
||||
getResultFileName: (result: R) => string | undefined) {
|
||||
cache.forEach((value, path) => {
|
||||
if (path === deletedInfo.path) {
|
||||
cache.delete(path);
|
||||
}
|
||||
else if (value) {
|
||||
value.forEach((resolution) => {
|
||||
if (resolution && !resolution.isInvalidated) {
|
||||
const result = getResult(resolution);
|
||||
if (result) {
|
||||
if (getResultFileName(result) === deletedInfo.path) {
|
||||
resolution.isInvalidated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setCompilationSettings(opt: CompilerOptions) {
|
||||
if (changesAffectModuleResolution(this.compilationSettings, opt)) {
|
||||
this.resolvedModuleNames.clear();
|
||||
this.resolvedTypeReferenceDirectives.clear();
|
||||
}
|
||||
this.compilationSettings = opt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/// <reference path="scriptInfo.ts"/>
|
||||
/// <reference path="lsHost.ts"/>
|
||||
/// <reference path="typingsCache.ts"/>
|
||||
/// <reference path="builder.ts"/>
|
||||
/// <reference path="..\compiler\builder.ts"/>
|
||||
|
||||
namespace ts.server {
|
||||
|
||||
|
@ -127,10 +127,13 @@ namespace ts.server {
|
|||
|
||||
public languageServiceEnabled = true;
|
||||
|
||||
/*@internal*/
|
||||
resolutionCache: ResolutionCache;
|
||||
|
||||
/*@internal*/
|
||||
lsHost: LSHost;
|
||||
|
||||
builder: Builder;
|
||||
private builder: Builder;
|
||||
/**
|
||||
* Set of files names that were updated since the last call to getChangesSinceVersion.
|
||||
*/
|
||||
|
@ -210,7 +213,16 @@ namespace ts.server {
|
|||
this.setInternalCompilerOptionsForEmittingJsFiles();
|
||||
|
||||
this.lsHost = new LSHost(host, this, this.projectService.cancellationToken);
|
||||
this.lsHost.setCompilationSettings(this.compilerOptions);
|
||||
this.resolutionCache = createResolutionCache(
|
||||
fileName => this.projectService.toPath(fileName),
|
||||
() => this.compilerOptions,
|
||||
(failedLookupLocation, failedLookupLocationPath, containingFile, name) => this.watchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath, containingFile, name),
|
||||
s => this.projectService.logger.info(s),
|
||||
this.getProjectName(),
|
||||
() => this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined
|
||||
);
|
||||
this.lsHost.compilationSettings = this.compilerOptions;
|
||||
this.resolutionCache.setModuleResolutionHost(this.lsHost);
|
||||
|
||||
this.languageService = createLanguageService(this.lsHost, this.documentRegistry);
|
||||
|
||||
|
@ -218,10 +230,22 @@ namespace ts.server {
|
|||
this.disableLanguageService();
|
||||
}
|
||||
|
||||
this.builder = createBuilder(this);
|
||||
this.markAsDirty();
|
||||
}
|
||||
|
||||
private watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) {
|
||||
// There is some kind of change in the failed lookup location, update the program
|
||||
return this.projectService.addFileWatcher(WatchType.FailedLookupLocation, this, failedLookupLocation, (fileName, eventKind) => {
|
||||
this.projectService.logger.info(`Watcher: FailedLookupLocations: Status: ${FileWatcherEventKind[eventKind]}: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`);
|
||||
if (this.projectKind === ProjectKind.Configured) {
|
||||
(this.lsHost.host as CachedServerHost).addOrDeleteFile(fileName, failedLookupLocationPath, eventKind);
|
||||
}
|
||||
this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath);
|
||||
this.markAsDirty();
|
||||
this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this);
|
||||
});
|
||||
}
|
||||
|
||||
private setInternalCompilerOptionsForEmittingJsFiles() {
|
||||
if (this.projectKind === ProjectKind.Inferred || this.projectKind === ProjectKind.External) {
|
||||
this.compilerOptions.noEmitForJsFiles = true;
|
||||
|
@ -246,12 +270,47 @@ namespace ts.server {
|
|||
return this.languageService;
|
||||
}
|
||||
|
||||
private ensureBuilder() {
|
||||
if (!this.builder) {
|
||||
this.builder = createBuilder(
|
||||
this.projectService.toCanonicalFileName,
|
||||
(_program, sourceFile, emitOnlyDts, isDetailed) => this.getFileEmitOutput(sourceFile, emitOnlyDts, isDetailed),
|
||||
data => this.projectService.host.createHash(data),
|
||||
sourceFile => !this.projectService.getScriptInfoForPath(sourceFile.path).hasMixedContent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] {
|
||||
if (!this.languageServiceEnabled) {
|
||||
return [];
|
||||
}
|
||||
this.updateGraph();
|
||||
return this.builder.getFilesAffectedBy(scriptInfo);
|
||||
this.ensureBuilder();
|
||||
return this.builder.getFilesAffectedBy(this.program, scriptInfo.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if emit was conducted
|
||||
*/
|
||||
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean {
|
||||
this.ensureBuilder();
|
||||
const { emitSkipped, outputFiles } = this.builder.emitFile(this.program, scriptInfo.path);
|
||||
if (!emitSkipped) {
|
||||
const projectRootPath = this.getProjectRootPath();
|
||||
for (const outputFile of outputFiles) {
|
||||
const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName));
|
||||
writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark);
|
||||
}
|
||||
}
|
||||
|
||||
return !emitSkipped;
|
||||
}
|
||||
|
||||
getChangedFiles() {
|
||||
Debug.assert(this.languageServiceEnabled);
|
||||
this.ensureBuilder();
|
||||
return this.builder.getChangedProgramFiles(this.program);
|
||||
}
|
||||
|
||||
getProjectVersion() {
|
||||
|
@ -320,6 +379,8 @@ namespace ts.server {
|
|||
this.rootFilesMap = undefined;
|
||||
this.program = undefined;
|
||||
this.builder = undefined;
|
||||
this.resolutionCache.clear();
|
||||
this.resolutionCache = undefined;
|
||||
this.cachedUnresolvedImportsPerFile = undefined;
|
||||
this.lsHost.dispose();
|
||||
this.lsHost = undefined;
|
||||
|
@ -337,6 +398,10 @@ namespace ts.server {
|
|||
this.languageService = undefined;
|
||||
}
|
||||
|
||||
isClosed() {
|
||||
return this.lsHost === undefined;
|
||||
}
|
||||
|
||||
getCompilerOptions() {
|
||||
return this.compilerOptions;
|
||||
}
|
||||
|
@ -391,11 +456,11 @@ namespace ts.server {
|
|||
});
|
||||
}
|
||||
|
||||
getFileEmitOutput(info: ScriptInfo, emitOnlyDtsFiles: boolean) {
|
||||
private getFileEmitOutput(sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean) {
|
||||
if (!this.languageServiceEnabled) {
|
||||
return undefined;
|
||||
}
|
||||
return this.getLanguageService().getEmitOutput(info.fileName, emitOnlyDtsFiles);
|
||||
return this.getLanguageService().getEmitOutput(sourceFile.fileName, emitOnlyDtsFiles, isDetailed);
|
||||
}
|
||||
|
||||
getFileNames(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean) {
|
||||
|
@ -454,21 +519,6 @@ namespace ts.server {
|
|||
return false;
|
||||
}
|
||||
|
||||
getAllEmittableFiles() {
|
||||
if (!this.languageServiceEnabled) {
|
||||
return [];
|
||||
}
|
||||
const defaultLibraryFileName = getDefaultLibFileName(this.compilerOptions);
|
||||
const infos = this.getScriptInfos();
|
||||
const result: string[] = [];
|
||||
for (const info of infos) {
|
||||
if (getBaseFileName(info.fileName) !== defaultLibraryFileName && shouldEmitFile(info)) {
|
||||
result.push(info.fileName);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
containsScriptInfo(info: ScriptInfo): boolean {
|
||||
return this.isRoot(info) || (this.program && this.program.getSourceFileByPath(info.path) !== undefined);
|
||||
}
|
||||
|
@ -505,7 +555,7 @@ namespace ts.server {
|
|||
if (this.isRoot(info)) {
|
||||
this.removeRoot(info);
|
||||
}
|
||||
this.lsHost.notifyFileRemoved(info);
|
||||
this.resolutionCache.invalidateResolutionOfFile(info.path);
|
||||
this.cachedUnresolvedImportsPerFile.remove(info.path);
|
||||
|
||||
if (detachFromProject) {
|
||||
|
@ -560,11 +610,12 @@ namespace ts.server {
|
|||
* @returns: true if set of files in the project stays the same and false - otherwise.
|
||||
*/
|
||||
updateGraph(): boolean {
|
||||
this.lsHost.startRecordingFilesWithChangedResolutions();
|
||||
this.resolutionCache.startRecordingFilesWithChangedResolutions();
|
||||
this.lsHost.hasInvalidatedResolution = this.resolutionCache.createHasInvalidatedResolution();
|
||||
|
||||
let hasChanges = this.updateGraphWorker();
|
||||
|
||||
const changedFiles: ReadonlyArray<Path> = this.lsHost.finishRecordingFilesWithChangedResolutions() || emptyArray;
|
||||
const changedFiles: ReadonlyArray<Path> = this.resolutionCache.finishRecordingFilesWithChangedResolutions() || emptyArray;
|
||||
|
||||
for (const file of changedFiles) {
|
||||
// delete cached information for changed files
|
||||
|
@ -594,11 +645,14 @@ namespace ts.server {
|
|||
|
||||
// update builder only if language service is enabled
|
||||
// otherwise tell it to drop its internal state
|
||||
if (this.languageServiceEnabled) {
|
||||
this.builder.onProjectUpdateGraph();
|
||||
}
|
||||
else {
|
||||
this.builder.clear();
|
||||
// Note we are retaining builder so we can send events for project change
|
||||
if (this.builder) {
|
||||
if (this.languageServiceEnabled) {
|
||||
this.builder.onProgramUpdateGraph(this.program, this.lsHost.hasInvalidatedResolution);
|
||||
}
|
||||
else {
|
||||
this.builder.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
|
@ -639,41 +693,15 @@ namespace ts.server {
|
|||
}
|
||||
}
|
||||
|
||||
const missingFilePaths = this.program.getMissingFilePaths();
|
||||
const newMissingFilePathMap = arrayToSet(missingFilePaths);
|
||||
// Update the missing file paths watcher
|
||||
mutateMap(
|
||||
updateMissingFilePathsWatch(
|
||||
this.program,
|
||||
this.missingFilesMap || (this.missingFilesMap = createMap()),
|
||||
newMissingFilePathMap,
|
||||
{
|
||||
// Watch the missing files
|
||||
createNewValue: missingFilePath => {
|
||||
const fileWatcher = this.projectService.addFileWatcher(
|
||||
WatchType.MissingFilePath, this, missingFilePath,
|
||||
(filename, eventKind) => {
|
||||
if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) {
|
||||
this.missingFilesMap.delete(missingFilePath);
|
||||
this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.FileCreated);
|
||||
|
||||
if (this.projectKind === ProjectKind.Configured) {
|
||||
const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath));
|
||||
(this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath));
|
||||
}
|
||||
|
||||
// When a missing file is created, we should update the graph.
|
||||
this.markAsDirty();
|
||||
this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this);
|
||||
}
|
||||
}
|
||||
);
|
||||
return fileWatcher;
|
||||
},
|
||||
// Files that are no longer missing (e.g. because they are no longer required)
|
||||
// should no longer be watched.
|
||||
onDeleteExistingValue: (missingFilePath, fileWatcher) => {
|
||||
this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.NotNeeded);
|
||||
}
|
||||
}
|
||||
// Watch the missing files
|
||||
missingFilePath => this.addMissingFileWatcher(missingFilePath),
|
||||
// Files that are no longer missing (e.g. because they are no longer required)
|
||||
// should no longer be watched.
|
||||
(missingFilePath, fileWatcher) => this.closeMissingFileWatcher(missingFilePath, fileWatcher, WatcherCloseReason.NotNeeded)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -684,7 +712,7 @@ namespace ts.server {
|
|||
// by the LSHost for files in the program when the program is retrieved above but
|
||||
// the program doesn't contain external files so this must be done explicitly.
|
||||
inserted => {
|
||||
const scriptInfo = this.projectService.getOrCreateScriptInfo(inserted, /*openedByClient*/ false);
|
||||
const scriptInfo = this.projectService.getOrCreateScriptInfo(inserted, /*openedByClient*/ false, this.lsHost.host);
|
||||
scriptInfo.attachToProject(this);
|
||||
},
|
||||
removed => {
|
||||
|
@ -697,12 +725,37 @@ namespace ts.server {
|
|||
return hasChanges;
|
||||
}
|
||||
|
||||
private addMissingFileWatcher(missingFilePath: Path) {
|
||||
const fileWatcher = this.projectService.addFileWatcher(
|
||||
WatchType.MissingFilePath, this, missingFilePath,
|
||||
(fileName, eventKind) => {
|
||||
if (this.projectKind === ProjectKind.Configured) {
|
||||
(this.lsHost.host as CachedServerHost).addOrDeleteFile(fileName, missingFilePath, eventKind);
|
||||
}
|
||||
|
||||
if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) {
|
||||
this.missingFilesMap.delete(missingFilePath);
|
||||
this.closeMissingFileWatcher(missingFilePath, fileWatcher, WatcherCloseReason.FileCreated);
|
||||
|
||||
// When a missing file is created, we should update the graph.
|
||||
this.markAsDirty();
|
||||
this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this);
|
||||
}
|
||||
}
|
||||
);
|
||||
return fileWatcher;
|
||||
}
|
||||
|
||||
private closeMissingFileWatcher(missingFilePath: Path, fileWatcher: FileWatcher, reason: WatcherCloseReason) {
|
||||
this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, reason);
|
||||
}
|
||||
|
||||
isWatchedMissingFile(path: Path) {
|
||||
return this.missingFilesMap && this.missingFilesMap.has(path);
|
||||
}
|
||||
|
||||
getScriptInfoLSHost(fileName: string) {
|
||||
const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false);
|
||||
const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false, this.lsHost.host);
|
||||
if (scriptInfo) {
|
||||
const existingValue = this.rootFilesMap.get(scriptInfo.path);
|
||||
if (existingValue !== undefined && existingValue !== scriptInfo) {
|
||||
|
@ -716,7 +769,10 @@ namespace ts.server {
|
|||
}
|
||||
|
||||
getScriptInfoForNormalizedPath(fileName: NormalizedPath) {
|
||||
const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false);
|
||||
const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(
|
||||
fileName, /*openedByClient*/ false, /*fileContent*/ undefined,
|
||||
/*scriptKind*/ undefined, /*hasMixedContent*/ undefined, this.lsHost.host
|
||||
);
|
||||
if (scriptInfo && !scriptInfo.isAttached(this)) {
|
||||
return Errors.ThrowProjectDoesNotContainDocument(fileName, this);
|
||||
}
|
||||
|
@ -746,9 +802,13 @@ namespace ts.server {
|
|||
this.cachedUnresolvedImportsPerFile.clear();
|
||||
this.lastCachedUnresolvedImportsList = undefined;
|
||||
}
|
||||
const oldOptions = this.compilerOptions;
|
||||
this.compilerOptions = compilerOptions;
|
||||
this.setInternalCompilerOptionsForEmittingJsFiles();
|
||||
this.lsHost.setCompilationSettings(compilerOptions);
|
||||
if (changesAffectModuleResolution(oldOptions, compilerOptions)) {
|
||||
this.resolutionCache.clear();
|
||||
}
|
||||
this.lsHost.compilationSettings = this.compilerOptions;
|
||||
|
||||
this.markAsDirty();
|
||||
}
|
||||
|
@ -814,58 +874,6 @@ namespace ts.server {
|
|||
}
|
||||
}
|
||||
|
||||
getReferencedFiles(path: Path): Path[] {
|
||||
if (!this.languageServiceEnabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sourceFile = this.getSourceFile(path);
|
||||
if (!sourceFile) {
|
||||
return [];
|
||||
}
|
||||
// We need to use a set here since the code can contain the same import twice,
|
||||
// but that will only be one dependency.
|
||||
// To avoid invernal conversion, the key of the referencedFiles map must be of type Path
|
||||
const referencedFiles = createMap<true>();
|
||||
if (sourceFile.imports && sourceFile.imports.length > 0) {
|
||||
const checker: TypeChecker = this.program.getTypeChecker();
|
||||
for (const importName of sourceFile.imports) {
|
||||
const symbol = checker.getSymbolAtLocation(importName);
|
||||
if (symbol && symbol.declarations && symbol.declarations[0]) {
|
||||
const declarationSourceFile = symbol.declarations[0].getSourceFile();
|
||||
if (declarationSourceFile) {
|
||||
referencedFiles.set(declarationSourceFile.path, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentDirectory = getDirectoryPath(path);
|
||||
// Handle triple slash references
|
||||
if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) {
|
||||
for (const referencedFile of sourceFile.referencedFiles) {
|
||||
const referencedPath = this.projectService.toPath(referencedFile.fileName, currentDirectory);
|
||||
referencedFiles.set(referencedPath, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle type reference directives
|
||||
if (sourceFile.resolvedTypeReferenceDirectiveNames) {
|
||||
sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => {
|
||||
if (!resolvedTypeReferenceDirective) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = resolvedTypeReferenceDirective.resolvedFileName;
|
||||
const typeFilePath = this.projectService.toPath(fileName, currentDirectory);
|
||||
referencedFiles.set(typeFilePath, true);
|
||||
});
|
||||
}
|
||||
|
||||
const allFileNames = arrayFrom(referencedFiles.keys()) as Path[];
|
||||
return filter(allFileNames, file => this.lsHost.host.fileExists(file));
|
||||
}
|
||||
|
||||
// remove a root file from project
|
||||
protected removeRoot(info: ScriptInfo): void {
|
||||
orderedRemoveItem(this.rootFiles, info);
|
||||
|
@ -973,11 +981,6 @@ namespace ts.server {
|
|||
}
|
||||
}
|
||||
|
||||
interface WildcardDirectoryWatcher {
|
||||
watcher: FileWatcher;
|
||||
flags: WatchDirectoryFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a file is opened, the server will look for a tsconfig (or jsconfig)
|
||||
* and if successfull create a ConfiguredProject for it.
|
||||
|
@ -1160,30 +1163,19 @@ namespace ts.server {
|
|||
|
||||
/*@internal*/
|
||||
watchWildcards(wildcardDirectories: Map<WatchDirectoryFlags>) {
|
||||
mutateMap(
|
||||
updateWatchingWildcardDirectories(
|
||||
this.directoriesWatchedForWildcards || (this.directoriesWatchedForWildcards = createMap()),
|
||||
wildcardDirectories,
|
||||
{
|
||||
// Watcher is same if the recursive flags match
|
||||
isSameValue: ({ flags: existingFlags }, flags) => existingFlags !== flags,
|
||||
// Create new watch and recursive info
|
||||
createNewValue: (directory, flags) => {
|
||||
return {
|
||||
watcher: this.projectService.addDirectoryWatcher(
|
||||
WatchType.WildcardDirectories, this, directory,
|
||||
path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path),
|
||||
flags
|
||||
),
|
||||
flags
|
||||
};
|
||||
},
|
||||
// Close existing watch thats not needed any more
|
||||
onDeleteExistingValue: (directory, wildcardDirectoryWatcher) =>
|
||||
this.closeWildcardDirectoryWatcher(directory, wildcardDirectoryWatcher, WatcherCloseReason.NotNeeded),
|
||||
// Close existing watch that doesnt match in the recursive flags
|
||||
onDeleteExistingMismatchValue: (directory, wildcardDirectoryWatcher) =>
|
||||
this.closeWildcardDirectoryWatcher(directory, wildcardDirectoryWatcher, WatcherCloseReason.RecursiveChanged),
|
||||
}
|
||||
// Create new directory watcher
|
||||
(directory, flags) => this.projectService.addDirectoryWatcher(
|
||||
WatchType.WildcardDirectories, this, directory,
|
||||
path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path),
|
||||
flags
|
||||
),
|
||||
// Close directory watcher
|
||||
(directory, wildcardDirectoryWatcher, flagsChanged) => this.closeWildcardDirectoryWatcher(
|
||||
directory, wildcardDirectoryWatcher, flagsChanged ? WatcherCloseReason.RecursiveChanged : WatcherCloseReason.NotNeeded
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1214,7 +1206,7 @@ namespace ts.server {
|
|||
path => this.projectService.onTypeRootFileChanged(this, path), WatchDirectoryFlags.None
|
||||
),
|
||||
// Close existing watch thats not needed any more
|
||||
onDeleteExistingValue: (directory, watcher) => this.projectService.closeDirectoryWatcher(
|
||||
onDeleteValue: (directory, watcher) => this.projectService.closeDirectoryWatcher(
|
||||
WatchType.TypeRoot, this, directory, watcher, WatchDirectoryFlags.None, WatcherCloseReason.NotNeeded
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2040,6 +2040,29 @@ namespace ts.server.protocol {
|
|||
languageServiceEnabled: boolean;
|
||||
}
|
||||
|
||||
export type ProjectChangedEventName = "projectChanged";
|
||||
export interface ProjectStructureChangedEvent extends Event {
|
||||
event: ProjectChangedEventName;
|
||||
body: ProjectChangedEventBody;
|
||||
}
|
||||
|
||||
export interface ProjectChangedEventBody {
|
||||
/**
|
||||
* Project name that has changes
|
||||
*/
|
||||
projectName: string;
|
||||
|
||||
/**
|
||||
* Minimum set of file names to emit
|
||||
*/
|
||||
fileNamesToEmit: string[];
|
||||
|
||||
/**
|
||||
* List of files that have changed/added/removed or could have been affected by the changed files
|
||||
*/
|
||||
changedFiles: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments for reload request.
|
||||
*/
|
||||
|
|
|
@ -237,7 +237,7 @@ namespace ts.server {
|
|||
detachAllProjects() {
|
||||
for (const p of this.containingProjects) {
|
||||
if (p.projectKind === ProjectKind.Configured) {
|
||||
(p.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(this.fileName);
|
||||
(p.lsHost.host as CachedServerHost).addOrDeleteFile(this.fileName, this.path, FileWatcherEventKind.Deleted);
|
||||
}
|
||||
const isInfoRoot = p.isRoot(this);
|
||||
// detach is unnecessary since we'll clean the list of containing projects anyways
|
||||
|
|
|
@ -538,7 +538,15 @@ namespace ts.server {
|
|||
|
||||
fs.stat(watchedFile.fileName, (err: any, stats: any) => {
|
||||
if (err) {
|
||||
watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Changed);
|
||||
if (err.code === "ENOENT") {
|
||||
if (watchedFile.mtime.getTime() !== 0) {
|
||||
watchedFile.mtime = new Date(0);
|
||||
watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Deleted);
|
||||
}
|
||||
}
|
||||
else {
|
||||
watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Changed);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const oldTime = watchedFile.mtime.getTime();
|
||||
|
|
|
@ -332,14 +332,18 @@ namespace ts.server {
|
|||
|
||||
private defaultEventHandler(event: ProjectServiceEvent) {
|
||||
switch (event.eventName) {
|
||||
case ContextEvent:
|
||||
const { project, fileName } = event.data;
|
||||
this.projectService.logger.info(`got context event, updating diagnostics for ${fileName}`);
|
||||
this.errorCheck.startNew(next => this.updateErrorCheck(next, [{ fileName, project }], 100));
|
||||
case ProjectChangedEvent:
|
||||
const { project, filesToEmit, changedFiles } = event.data;
|
||||
this.projectChangedEvent(project, filesToEmit, changedFiles);
|
||||
break;
|
||||
case ConfigFileDiagEvent:
|
||||
const { triggerFile, configFileName, diagnostics } = event.data;
|
||||
this.configFileDiagnosticEvent(triggerFile, configFileName, diagnostics);
|
||||
const { triggerFile, configFileName: configFile, diagnostics } = event.data;
|
||||
const bakedDiags = map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true));
|
||||
this.event<protocol.ConfigFileDiagnosticEventBody>({
|
||||
triggerFile,
|
||||
configFile,
|
||||
diagnostics: bakedDiags
|
||||
}, "configFileDiag");
|
||||
break;
|
||||
case ProjectLanguageServiceStateEvent: {
|
||||
const eventName: protocol.ProjectLanguageServiceStateEventName = "projectLanguageServiceState";
|
||||
|
@ -360,6 +364,24 @@ namespace ts.server {
|
|||
}
|
||||
}
|
||||
|
||||
private projectChangedEvent(project: Project, fileNamesToEmit: string[], changedFiles: string[]): void {
|
||||
this.projectService.logger.info(`got project changed event, updating diagnostics for ${changedFiles}`);
|
||||
if (changedFiles.length) {
|
||||
const checkList = this.createCheckList(changedFiles, project);
|
||||
|
||||
// For now only queue error checking for open files. We can change this to include non open files as well
|
||||
this.errorCheck.startNew(next => this.updateErrorCheck(next, checkList, 100, /*requireOpen*/ true));
|
||||
|
||||
|
||||
// Send project changed event
|
||||
this.event<protocol.ProjectChangedEventBody>({
|
||||
projectName: project.getProjectName(),
|
||||
changedFiles,
|
||||
fileNamesToEmit
|
||||
}, "projectChanged");
|
||||
}
|
||||
}
|
||||
|
||||
public logError(err: Error, cmd: string) {
|
||||
let msg = "Exception on executing command " + cmd;
|
||||
if (err.message) {
|
||||
|
@ -381,21 +403,6 @@ namespace ts.server {
|
|||
this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine));
|
||||
}
|
||||
|
||||
public configFileDiagnosticEvent(triggerFile: string, configFile: string, diagnostics: ReadonlyArray<Diagnostic>) {
|
||||
const bakedDiags = map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true));
|
||||
const ev: protocol.ConfigFileDiagnosticEvent = {
|
||||
seq: 0,
|
||||
type: "event",
|
||||
event: "configFileDiag",
|
||||
body: {
|
||||
triggerFile,
|
||||
configFile,
|
||||
diagnostics: bakedDiags
|
||||
}
|
||||
};
|
||||
this.send(ev);
|
||||
}
|
||||
|
||||
public event<T>(info: T, eventName: string) {
|
||||
const ev: protocol.Event = {
|
||||
seq: 0,
|
||||
|
@ -1197,13 +1204,13 @@ namespace ts.server {
|
|||
}
|
||||
|
||||
private getCompileOnSaveAffectedFileList(args: protocol.FileRequestArgs): ReadonlyArray<protocol.CompileOnSaveAffectedFileListSingleProject> {
|
||||
const info = this.projectService.getScriptInfo(args.file);
|
||||
const result: protocol.CompileOnSaveAffectedFileListSingleProject[] = [];
|
||||
|
||||
const info = this.projectService.getScriptInfoEnsuringProjectsUptoDate(args.file);
|
||||
if (!info) {
|
||||
return emptyArray;
|
||||
}
|
||||
|
||||
const result: protocol.CompileOnSaveAffectedFileListSingleProject[] = [];
|
||||
|
||||
// if specified a project, we only return affected file list in this project
|
||||
const projectsToSearch = args.projectFileName ? [this.projectService.findProject(args.projectFileName)] : info.containingProjects;
|
||||
for (const project of projectsToSearch) {
|
||||
|
@ -1227,7 +1234,7 @@ namespace ts.server {
|
|||
return false;
|
||||
}
|
||||
const scriptInfo = project.getScriptInfo(file);
|
||||
return project.builder.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark));
|
||||
return project.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark));
|
||||
}
|
||||
|
||||
private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems {
|
||||
|
@ -1257,13 +1264,16 @@ namespace ts.server {
|
|||
}
|
||||
}
|
||||
|
||||
private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void {
|
||||
const checkList = mapDefined<string, PendingErrorCheck>(fileNames, uncheckedFileName => {
|
||||
private createCheckList(fileNames: string[], defaultProject?: Project): PendingErrorCheck[] {
|
||||
return mapDefined<string, PendingErrorCheck>(fileNames, uncheckedFileName => {
|
||||
const fileName = toNormalizedPath(uncheckedFileName);
|
||||
const project = this.projectService.getDefaultProjectForFile(fileName, /*refreshInferredProjects*/ true);
|
||||
const project = defaultProject || this.projectService.getDefaultProjectForFile(fileName, /*refreshInferredProjects*/ true);
|
||||
return project && { fileName, project };
|
||||
});
|
||||
}
|
||||
|
||||
private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void {
|
||||
const checkList = this.createCheckList(fileNames);
|
||||
if (checkList.length > 0) {
|
||||
this.updateErrorCheck(next, checkList, delay);
|
||||
}
|
||||
|
|
|
@ -304,59 +304,4 @@ namespace ts.server {
|
|||
deleted(oldItems[oldIndex++]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* clears already present map by calling onDeleteExistingValue callback before deleting that key/value
|
||||
*/
|
||||
export function clearMap<T>(map: Map<T>, onDeleteExistingValue: (key: string, existingValue: T) => void) {
|
||||
// Remove all
|
||||
map.forEach((existingValue, key) => {
|
||||
onDeleteExistingValue(key, existingValue);
|
||||
});
|
||||
map.clear();
|
||||
}
|
||||
|
||||
export interface MutateMapOptions<T, U> {
|
||||
createNewValue(key: string, valueInNewMap: U): T;
|
||||
onDeleteExistingValue(key: string, existingValue: T, isNotSame?: boolean): void;
|
||||
|
||||
isSameValue?(existingValue: T, valueInNewMap: U): boolean;
|
||||
onDeleteExistingMismatchValue?(key: string, existingValue: T): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutates the map with newMap such that keys in map will be same as newMap.
|
||||
*/
|
||||
export function mutateMap<T, U>(map: Map<T>, newMap: ReadonlyMap<U>, options: MutateMapOptions<T, U>) {
|
||||
// If there are new values update them
|
||||
if (newMap) {
|
||||
const { isSameValue, createNewValue, onDeleteExistingValue, onDeleteExistingMismatchValue } = options;
|
||||
// Needs update
|
||||
map.forEach((existingValue, key) => {
|
||||
const valueInNewMap = newMap.get(key);
|
||||
// Not present any more in new map, remove it
|
||||
if (valueInNewMap === undefined) {
|
||||
map.delete(key);
|
||||
onDeleteExistingValue(key, existingValue);
|
||||
}
|
||||
// different value - remove it
|
||||
else if (isSameValue && !isSameValue(existingValue, valueInNewMap)) {
|
||||
Debug.assert(!!onDeleteExistingMismatchValue);
|
||||
map.delete(key);
|
||||
onDeleteExistingMismatchValue(key, existingValue);
|
||||
}
|
||||
});
|
||||
|
||||
// Add new values that are not already present
|
||||
newMap.forEach((valueInNewMap, key) => {
|
||||
if (!map.has(key)) {
|
||||
// New values
|
||||
map.set(key, createNewValue(key, valueInNewMap));
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
clearMap(map, options.onDeleteExistingValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -814,18 +814,21 @@ namespace ts {
|
|||
return codefix.getSupportedErrorCodes();
|
||||
}
|
||||
|
||||
// Either it will be file name if host doesnt have file or it will be the host's file information
|
||||
type CachedHostFileInformation = HostFileInformation | string;
|
||||
|
||||
// Cache host information about script Should be refreshed
|
||||
// at each language service public entry point, since we don't know when
|
||||
// the set of scripts handled by the host changes.
|
||||
class HostCache {
|
||||
private fileNameToEntry: Map<HostFileInformation>;
|
||||
private fileNameToEntry: Map<CachedHostFileInformation>;
|
||||
private _compilationSettings: CompilerOptions;
|
||||
private currentDirectory: string;
|
||||
|
||||
constructor(private host: LanguageServiceHost, getCanonicalFileName: (fileName: string) => string) {
|
||||
// script id => script index
|
||||
this.currentDirectory = host.getCurrentDirectory();
|
||||
this.fileNameToEntry = createMap<HostFileInformation>();
|
||||
this.fileNameToEntry = createMap<CachedHostFileInformation>();
|
||||
|
||||
// Initialize the list with the root file names
|
||||
const rootFileNames = host.getScriptFileNames();
|
||||
|
@ -842,7 +845,7 @@ namespace ts {
|
|||
}
|
||||
|
||||
private createEntry(fileName: string, path: Path) {
|
||||
let entry: HostFileInformation;
|
||||
let entry: CachedHostFileInformation;
|
||||
const scriptSnapshot = this.host.getScriptSnapshot(fileName);
|
||||
if (scriptSnapshot) {
|
||||
entry = {
|
||||
|
@ -852,36 +855,41 @@ namespace ts {
|
|||
scriptKind: getScriptKind(fileName, this.host)
|
||||
};
|
||||
}
|
||||
else {
|
||||
entry = fileName;
|
||||
}
|
||||
|
||||
this.fileNameToEntry.set(path, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
public getEntryByPath(path: Path): HostFileInformation {
|
||||
public getEntryByPath(path: Path): CachedHostFileInformation | undefined {
|
||||
return this.fileNameToEntry.get(path);
|
||||
}
|
||||
|
||||
public containsEntryByPath(path: Path): boolean {
|
||||
return this.fileNameToEntry.has(path);
|
||||
public getHostFileInformation(path: Path): HostFileInformation | undefined {
|
||||
const entry = this.fileNameToEntry.get(path);
|
||||
return !isString(entry) ? entry : undefined;
|
||||
}
|
||||
|
||||
public getOrCreateEntryByPath(fileName: string, path: Path): HostFileInformation {
|
||||
return this.containsEntryByPath(path)
|
||||
? this.getEntryByPath(path)
|
||||
: this.createEntry(fileName, path);
|
||||
const info = this.getEntryByPath(path) || this.createEntry(fileName, path);
|
||||
return isString(info) ? undefined : info;
|
||||
}
|
||||
|
||||
public getRootFileNames(): string[] {
|
||||
return this.host.getScriptFileNames();
|
||||
return arrayFrom(this.fileNameToEntry.values(), entry => {
|
||||
return isString(entry) ? entry : entry.hostFileName;
|
||||
});
|
||||
}
|
||||
|
||||
public getVersion(path: Path): string {
|
||||
const file = this.getEntryByPath(path);
|
||||
const file = this.getHostFileInformation(path);
|
||||
return file && file.version;
|
||||
}
|
||||
|
||||
public getScriptSnapshot(path: Path): IScriptSnapshot {
|
||||
const file = this.getEntryByPath(path);
|
||||
const file = this.getHostFileInformation(path);
|
||||
return file && file.scriptSnapshot;
|
||||
}
|
||||
}
|
||||
|
@ -1108,9 +1116,12 @@ namespace ts {
|
|||
|
||||
// Get a fresh cache of the host information
|
||||
let hostCache = new HostCache(host, getCanonicalFileName);
|
||||
const rootFileNames = hostCache.getRootFileNames();
|
||||
|
||||
const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse;
|
||||
|
||||
// If the program is already up-to-date, we can reuse it
|
||||
if (programUpToDate()) {
|
||||
if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists, hasInvalidatedResolution)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1120,18 +1131,7 @@ namespace ts {
|
|||
// the program points to old source files that have been invalidated because of
|
||||
// incremental parsing.
|
||||
|
||||
const oldSettings = program && program.getCompilerOptions();
|
||||
const newSettings = hostCache.compilationSettings();
|
||||
const shouldCreateNewSourceFiles = oldSettings &&
|
||||
(oldSettings.target !== newSettings.target ||
|
||||
oldSettings.module !== newSettings.module ||
|
||||
oldSettings.moduleResolution !== newSettings.moduleResolution ||
|
||||
oldSettings.noResolve !== newSettings.noResolve ||
|
||||
oldSettings.jsx !== newSettings.jsx ||
|
||||
oldSettings.allowJs !== newSettings.allowJs ||
|
||||
oldSettings.disableSizeLimit !== oldSettings.disableSizeLimit ||
|
||||
oldSettings.baseUrl !== newSettings.baseUrl ||
|
||||
!equalOwnProperties(oldSettings.paths, newSettings.paths));
|
||||
|
||||
// Now create a new compiler
|
||||
const compilerHost: CompilerHost = {
|
||||
|
@ -1144,19 +1144,13 @@ namespace ts {
|
|||
getDefaultLibFileName: (options) => host.getDefaultLibFileName(options),
|
||||
writeFile: noop,
|
||||
getCurrentDirectory: () => currentDirectory,
|
||||
fileExists: (fileName): boolean => {
|
||||
// stub missing host functionality
|
||||
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
|
||||
return hostCache.containsEntryByPath(path) ?
|
||||
!!hostCache.getEntryByPath(path) :
|
||||
(host.fileExists && host.fileExists(fileName));
|
||||
},
|
||||
fileExists,
|
||||
readFile(fileName) {
|
||||
// stub missing host functionality
|
||||
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
|
||||
if (hostCache.containsEntryByPath(path)) {
|
||||
const entry = hostCache.getEntryByPath(path);
|
||||
return entry && entry.scriptSnapshot.getText(0, entry.scriptSnapshot.getLength());
|
||||
const entry = hostCache.getEntryByPath(path);
|
||||
if (entry) {
|
||||
return isString(entry) ? undefined : entry.scriptSnapshot.getText(0, entry.scriptSnapshot.getLength());
|
||||
}
|
||||
return host.readFile && host.readFile(fileName);
|
||||
},
|
||||
|
@ -1165,7 +1159,9 @@ namespace ts {
|
|||
},
|
||||
getDirectories: path => {
|
||||
return host.getDirectories ? host.getDirectories(path) : [];
|
||||
}
|
||||
},
|
||||
onReleaseOldSourceFile,
|
||||
hasInvalidatedResolution
|
||||
};
|
||||
if (host.trace) {
|
||||
compilerHost.trace = message => host.trace(message);
|
||||
|
@ -1181,36 +1177,37 @@ namespace ts {
|
|||
}
|
||||
|
||||
const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings);
|
||||
const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program);
|
||||
|
||||
// Release any files we have acquired in the old program but are
|
||||
// not part of the new program.
|
||||
if (program) {
|
||||
const oldSourceFiles = program.getSourceFiles();
|
||||
const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldSettings);
|
||||
for (const oldSourceFile of oldSourceFiles) {
|
||||
if (!newProgram.getSourceFile(oldSourceFile.fileName) || shouldCreateNewSourceFiles) {
|
||||
documentRegistry.releaseDocumentWithKey(oldSourceFile.path, oldSettingsKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
program = createProgram(rootFileNames, newSettings, compilerHost, program);
|
||||
|
||||
// hostCache is captured in the closure for 'getOrCreateSourceFile' but it should not be used past this point.
|
||||
// It needs to be cleared to allow all collected snapshots to be released
|
||||
hostCache = undefined;
|
||||
|
||||
program = newProgram;
|
||||
|
||||
// Make sure all the nodes in the program are both bound, and have their parent
|
||||
// pointers set property.
|
||||
program.getTypeChecker();
|
||||
return;
|
||||
|
||||
function getOrCreateSourceFile(fileName: string): SourceFile {
|
||||
return getOrCreateSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName));
|
||||
function fileExists(fileName: string) {
|
||||
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
|
||||
const entry = hostCache.getEntryByPath(path);
|
||||
return entry ?
|
||||
!isString(entry) :
|
||||
(host.fileExists && host.fileExists(fileName));
|
||||
}
|
||||
|
||||
function getOrCreateSourceFileByPath(fileName: string, path: Path): SourceFile {
|
||||
// Release any files we have acquired in the old program but are
|
||||
// not part of the new program.
|
||||
function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) {
|
||||
const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldOptions);
|
||||
documentRegistry.releaseDocumentWithKey(oldSourceFile.path, oldSettingsKey);
|
||||
}
|
||||
|
||||
function getOrCreateSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
|
||||
return getOrCreateSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile);
|
||||
}
|
||||
|
||||
function getOrCreateSourceFileByPath(fileName: string, path: Path, _languageVersion: ScriptTarget, _onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
|
||||
Debug.assert(hostCache !== undefined);
|
||||
// The program is asking for this file, check first if the host can locate it.
|
||||
// If the host can not locate the file, then it does not exist. return undefined
|
||||
|
@ -1223,7 +1220,7 @@ namespace ts {
|
|||
// Check if the language version has changed since we last created a program; if they are the same,
|
||||
// it is safe to reuse the sourceFiles; if not, then the shape of the AST can change, and the oldSourceFile
|
||||
// can not be reused. we have to dump all syntax trees and create new ones.
|
||||
if (!shouldCreateNewSourceFiles) {
|
||||
if (!shouldCreateNewSourceFile) {
|
||||
// Check if the old program had this file already
|
||||
const oldSourceFile = program && program.getSourceFileByPath(path);
|
||||
if (oldSourceFile) {
|
||||
|
@ -1263,49 +1260,6 @@ namespace ts {
|
|||
// Could not find this file in the old program, create a new SourceFile for it.
|
||||
return documentRegistry.acquireDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind);
|
||||
}
|
||||
|
||||
function sourceFileUpToDate(sourceFile: SourceFile): boolean {
|
||||
if (!sourceFile) {
|
||||
return false;
|
||||
}
|
||||
const path = sourceFile.path || toPath(sourceFile.fileName, currentDirectory, getCanonicalFileName);
|
||||
return sourceFile.version === hostCache.getVersion(path);
|
||||
}
|
||||
|
||||
function programUpToDate(): boolean {
|
||||
// If we haven't create a program yet, then it is not up-to-date
|
||||
if (!program) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If number of files in the program do not match, it is not up-to-date
|
||||
const rootFileNames = hostCache.getRootFileNames();
|
||||
if (program.getSourceFiles().length !== rootFileNames.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If any file is not up-to-date, then the whole program is not up-to-date
|
||||
for (const fileName of rootFileNames) {
|
||||
if (!sourceFileUpToDate(program.getSourceFile(fileName))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const currentOptions = program.getCompilerOptions();
|
||||
const newOptions = hostCache.compilationSettings();
|
||||
// If the compilation settings do no match, then the program is not up-to-date
|
||||
if (!compareDataObjects(currentOptions, newOptions)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If everything matches but the text of config file is changed,
|
||||
// error locations can change for program options, so update the program
|
||||
if (currentOptions.configFile && newOptions.configFile) {
|
||||
return currentOptions.configFile.text === newOptions.configFile.text;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function getProgram(): Program {
|
||||
|
@ -1520,23 +1474,12 @@ namespace ts {
|
|||
return ts.NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles);
|
||||
}
|
||||
|
||||
function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput {
|
||||
function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, isDetailed?: boolean) {
|
||||
synchronizeHostData();
|
||||
|
||||
const sourceFile = getValidSourceFile(fileName);
|
||||
const outputFiles: OutputFile[] = [];
|
||||
|
||||
function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) {
|
||||
outputFiles.push({ name: fileName, writeByteOrderMark, text });
|
||||
}
|
||||
|
||||
const customTransformers = host.getCustomTransformers && host.getCustomTransformers();
|
||||
const emitOutput = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers);
|
||||
|
||||
return {
|
||||
outputFiles,
|
||||
emitSkipped: emitOutput.emitSkipped
|
||||
};
|
||||
return getFileEmitOutput(program, sourceFile, emitOnlyDtsFiles, isDetailed, cancellationToken, customTransformers);
|
||||
}
|
||||
|
||||
// Signature help
|
||||
|
|
|
@ -527,7 +527,7 @@ namespace ts {
|
|||
if (logPerformance) {
|
||||
const end = timestamp();
|
||||
logger.log(`${actionDescription} completed in ${end - start} msec`);
|
||||
if (typeof result === "string") {
|
||||
if (isString(result)) {
|
||||
let str = result;
|
||||
if (str.length > 128) {
|
||||
str = str.substring(0, 128) + "...";
|
||||
|
|
|
@ -139,7 +139,7 @@ namespace ts {
|
|||
|
||||
const value = options[opt.name];
|
||||
// Value should be a key of opt.type
|
||||
if (typeof value === "string") {
|
||||
if (isString(value)) {
|
||||
// If value is not a string, this will fail
|
||||
options[opt.name] = parseCustomTypeOption(opt, value, diagnostics);
|
||||
}
|
||||
|
|
|
@ -185,6 +185,7 @@ namespace ts {
|
|||
*/
|
||||
resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[];
|
||||
resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
|
||||
hasInvalidatedResolution?: HasInvalidatedResolution;
|
||||
directoryExists?(directoryName: string): boolean;
|
||||
|
||||
/*
|
||||
|
@ -276,7 +277,7 @@ namespace ts {
|
|||
getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[];
|
||||
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined;
|
||||
|
||||
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput;
|
||||
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, isDetailed?: boolean): EmitOutput | EmitOutputDetailed;
|
||||
|
||||
getProgram(): Program;
|
||||
|
||||
|
@ -694,23 +695,12 @@ namespace ts {
|
|||
autoCollapse: boolean;
|
||||
}
|
||||
|
||||
export interface EmitOutput {
|
||||
outputFiles: OutputFile[];
|
||||
emitSkipped: boolean;
|
||||
}
|
||||
|
||||
export const enum OutputFileType {
|
||||
JavaScript,
|
||||
SourceMap,
|
||||
Declaration
|
||||
}
|
||||
|
||||
export interface OutputFile {
|
||||
name: string;
|
||||
writeByteOrderMark: boolean;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const enum EndOfLineState {
|
||||
None,
|
||||
InMultiLineCommentTrivia,
|
||||
|
|
|
@ -979,26 +979,6 @@ namespace ts {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function compareDataObjects(dst: any, src: any): boolean {
|
||||
if (!dst || !src || Object.keys(dst).length !== Object.keys(src).length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const e in dst) {
|
||||
if (typeof dst[e] === "object") {
|
||||
if (!compareDataObjects(dst[e], src[e])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (typeof dst[e] !== "function") {
|
||||
if (dst[e] !== src[e]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isArrayLiteralOrObjectLiteralDestructuringPattern(node: Node) {
|
||||
if (node.kind === SyntaxKind.ArrayLiteralExpression ||
|
||||
node.kind === SyntaxKind.ObjectLiteralExpression) {
|
||||
|
|
Loading…
Reference in a new issue