-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Watch improvements in tsserver #17269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
21ad26b
ae33ae8
75698a8
f154910
0e44367
2a63827
96ffd53
6bd42b8
df6f75b
9ff9476
62871cc
48c6513
19a6a00
68def1b
029b1f2
f338a70
8fedcf7
e568976
048e67c
0365901
404aa8f
71d79c6
f12980d
00011a5
0572b15
62663a1
dcbd7b1
62ef6b1
2439e7a
ff34a77
1155c37
ae87838
802e283
499fabc
273569f
94a589b
ef5935b
e068475
6237b22
9b18f7b
85b9254
69e5abd
c814d8e
2dd6aed
89c61e7
bb91b32
46e3d1c
031a637
0d5e6c9
2762232
65a6ee0
d55150c
8dc6248
7474ba7
6385f7e
65521bc
27988bf
02b8a7d
f723beb
b071a86
8db05c2
594482d
d0a23bb
59d07dc
9895082
f1b1b12
136b091
6bf9133
a99c04e
b66b752
da0d374
e639ceb
8deef58
c425128
d217bec
60e2e68
84b2e23
3908325
7173da2
e500be2
6227a36
55931c4
e65df12
e711238
3b85f3f
ea95f3b
9e570c3
4c79033
5aafd3f
17565d8
10ea5bf
254e393
a3b9467
16cf7c4
d7ce95d
345f36d
2b97b2c
8d5d4c2
9e5e20c
13aafa2
6c61293
7b2bab5
54f64a1
0ff160f
e6eede1
2a5d954
680994e
c8e711c
29e93c3
b179cd1
67f9533
de28d02
5739b68
fdb104b
aea8630
b536f9d
4f7c0e5
cf72f2a
23acff5
14febe2
38f3a2b
68d3605
9e08cae
835153b
898559b
7f969e8
4bb4711
8ac01d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1421,23 +1421,24 @@ namespace ts { | |
const options = extend(existingOptions, parsedConfig.options || {}); | ||
options.configFilePath = configFileName; | ||
setConfigFileInOptions(options, sourceFile); | ||
const { fileNames, wildcardDirectories } = getFileNames(); | ||
const { fileNames, wildcardDirectories, spec } = getFileNames(); | ||
return { | ||
options, | ||
fileNames, | ||
typeAcquisition: parsedConfig.typeAcquisition || getDefaultTypeAcquisition(), | ||
raw, | ||
errors, | ||
wildcardDirectories, | ||
compileOnSave: !!raw.compileOnSave | ||
compileOnSave: !!raw.compileOnSave, | ||
configFileSpecs: spec | ||
}; | ||
|
||
function getFileNames(): ExpandResult { | ||
let fileNames: string[]; | ||
let filesSpecs: string[]; | ||
if (hasProperty(raw, "files")) { | ||
if (isArray(raw["files"])) { | ||
fileNames = <string[]>raw["files"]; | ||
if (fileNames.length === 0) { | ||
filesSpecs = <string[]>raw["files"]; | ||
if (filesSpecs.length === 0) { | ||
createCompilerDiagnosticOnlyIfJson(Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json"); | ||
} | ||
} | ||
|
@@ -1475,19 +1476,14 @@ namespace ts { | |
} | ||
} | ||
|
||
if (fileNames === undefined && includeSpecs === undefined) { | ||
if (filesSpecs === undefined && includeSpecs === undefined) { | ||
includeSpecs = ["**/*"]; | ||
} | ||
|
||
const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, extraFileExtensions, sourceFile); | ||
const result = matchFileNames(filesSpecs, includeSpecs, excludeSpecs, basePath, options, host, errors, extraFileExtensions, sourceFile); | ||
|
||
if (result.fileNames.length === 0 && !hasProperty(raw, "files") && resolutionStack.length === 0) { | ||
errors.push( | ||
createCompilerDiagnostic( | ||
Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, | ||
configFileName || "tsconfig.json", | ||
JSON.stringify(includeSpecs || []), | ||
JSON.stringify(excludeSpecs || []))); | ||
errors.push(getErrorForNoInputFiles(result.spec, configFileName)); | ||
} | ||
|
||
return result; | ||
|
@@ -1500,6 +1496,14 @@ namespace ts { | |
} | ||
} | ||
|
||
export function getErrorForNoInputFiles({ includeSpecs, excludeSpecs }: ConfigFileSpecs, configFileName?: string) { | ||
return createCompilerDiagnostic( | ||
Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, | ||
configFileName || "tsconfig.json", | ||
A3DB JSON.stringify(includeSpecs || []), | ||
JSON.stringify(excludeSpecs || [])); | ||
} | ||
|
||
interface ParsedTsconfig { | ||
raw: any; | ||
options?: CompilerOptions; | ||
|
@@ -1946,7 +1950,40 @@ namespace ts { | |
* @param host The host used to resolve files and directories. | ||
* @param errors An array for diagnostic reporting. | ||
*/ | ||
function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], extraFileExtensions: JsFileExtensionInfo[], jsonSourceFile: JsonSourceFile): ExpandResult { | ||
function matchFileNames(filesSpecs: string[], includeSpecs: string[], excludeSpecs: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], extraFileExtensions: JsFileExtensionInfo[], jsonSourceFile: JsonSourceFile): ExpandResult { | ||
basePath = normalizePath(basePath); | ||
let validatedIncludeSpecs: string[], validatedExcludeSpecs: string[]; | ||
|
||
if (includeSpecs) { | ||
validatedIncludeSpecs = validateSpecs(includeSpecs, errors, /*allowTrailingRecursion*/ false, jsonSourceFile, "include"); | ||
} | ||
|
||
if (excludeSpecs) { | ||
validatedExcludeSpecs = validateSpecs(excludeSpecs, errors, /*allowTrailingRecursion*/ true, jsonSourceFile, "exclude"); | ||
} | ||
|
||
// Wildcard directories (provided as part of a wildcard path) are stored in a | ||
// file map that marks whether it was a regular wildcard match (with a `*` or `?` token), | ||
// or a recursive directory. This information is used by filesystem watchers to monitor for | ||
// new entries in these paths. | ||
const wildcardDirectories = getWildcardDirectories(validatedIncludeSpecs, validatedExcludeSpecs, basePath, host.useCaseSensitiveFileNames); | ||
|
||
const spec: ConfigFileSpecs = { filesSpecs, includeSpecs, excludeSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories }; | ||
return getFileNamesFromConfigSpecs(spec, basePath, options, host, extraFileExtensions); | ||
} | ||
|
||
/** | ||
* Expands an array of file specifications. | ||
* | ||
* @param fileNames The literal file names to include. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
* @param include The wildcard file specifications to include. | ||
* @param exclude The wildcard file specifications to exclude. | ||
* @param basePath The base path for any relative file specifications. | ||
* @param options Compiler options. | ||
* @param host The host used to resolve files and directories. | ||
* @param errors An array for diagnostic reporting. | ||
*/ | ||
export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, e F438 xtraFileExtensions: JsFileExtensionInfo[]): ExpandResult { | ||
basePath = normalizePath(basePath); | ||
|
||
// The exclude spec list is converted into a regular expression, which allows us to quickly | ||
|
@@ -1964,35 +2001,23 @@ namespace ts { | |
// via wildcard, and to handle extension priority. | ||
const wildcardFileMap = createMap<string>(); | ||
|
||
if (include) { | ||
include = validateSpecs(include, errors, /*allowTrailingRecursion*/ false, jsonSourceFile, "include"); | ||
} | ||
|
||
if (exclude) { | ||
exclude = validateSpecs(exclude, errors, /*allowTrailingRecursion*/ true, jsonSourceFile, "exclude"); | ||
} | ||
|
||
// Wildcard directories (provided as part of a wildcard path) are stored in a | ||
// file map that marks whether it was a regular wildcard match (with a `*` or `?` token), | ||
// or a recursive directory. This information is used by filesystem watchers to monitor for | ||
// new entries in these paths. | ||
const wildcardDirectories = getWildcardDirectories(include, exclude, basePath, host.useCaseSensitiveFileNames); | ||
const { filesSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories } = spec; | ||
|
||
// Rather than requery this for each file and filespec, we query the supported extensions | ||
// once and store it on the expansion context. | ||
const supportedExtensions = getSupportedExtensions(options, extraFileExtensions); | ||
|
||
// Literal files are always included verbatim. An "include" or "exclude" specification cannot | ||
// remove a literal file. | ||
if (fileNames) { | ||
for (const fileName of fileNames) { | ||
if (filesSpecs) { | ||
for (const fileName of filesSpecs) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should |
||
const file = combinePaths(basePath, fileName); | ||
literalFileMap.set(keyMapper(file), file); | ||
} | ||
} | ||
|
||
if (include && include.length > 0) { | ||
for (const file of host.readDirectory(basePath, supportedExtensions, exclude, include, /*depth*/ undefined)) { | ||
if (validatedIncludeSpecs && validatedIncludeSpecs.length > 0) { | ||
for (const file of host.readDirectory(basePath, supportedExtensions, validatedExcludeSpecs, validatedIncludeSpecs, /*depth*/ undefined)) { | ||
// If we have already included a literal or wildcard path with a | ||
// higher priority extension, we should skip this file. | ||
// | ||
|
@@ -2020,7 +2045,8 @@ namespace ts { | |
const wildcardFiles = arrayFrom(wildcardFileMap.values()); | ||
return { | ||
fileNames: literalFiles.concat(wildcardFiles), | ||
wildcardDirectories | ||
wildcardDirectories, | ||
spec | ||
}; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3731,16 +3731,27 @@ namespace ts { | |
errors: Diagnostic[]; | ||
wildcardDirectories?: MapLike<WatchDirectoryFlags>; | ||
compileOnSave?: boolean; | ||
configFileSpecs?: ConfigFileSpecs; | ||
} | ||
|
||
export const enum WatchDirectoryFlags { | ||
None = 0, | ||
Recursive = 1 << 0, | ||
} | ||
|
||
export interface ConfigFileSpecs { | ||
filesSpecs: string[]; | ||
includeSpecs: string[]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a comment that |
||
excludeSpecs: string[]; | ||
validatedIncludeSpecs: string[]; | ||
validatedExcludeSpecs: string[]; | ||
wildcardDirectories: MapLike<WatchDirectoryFlags>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this be changed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to keep this MapLike just the way json has to ensure that the conversion is done only on need basis |
||
} | ||
|
||
export interface ExpandResult { | ||
fileNames: string[]; | ||
wildcardDirectories: MapLike<WatchDirectoryFlags>; | ||
spec: ConfigFileSpecs; | ||
} | ||
|
||
/* @internal */ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -617,33 +617,30 @@ namespace ts.server { | |
} | ||
|
||
this.logger.info(`Detected source file changes: ${fileName}`); | ||
this.throttledOperations.schedule( | ||
project.getConfigFilePath(), | ||
/*delay*/250, | ||
() => this.handleFileAddOrRemoveInWatchedDirectoryOfProject(project, fileName)); | ||
} | ||
|
||
private handleFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, triggerFile: string) { | ||
// TODO: (sheetalkamat) this actually doesnt need to re-read the config file from the disk | ||
// it just needs to update the file list of file names | ||
// We might be able to do that by caching the info from first parse and add reusing this with the change in the file path | ||
const { projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath()); | ||
this.reportConfigFileDiagnostics(project.getProjectName(), configFileErrors, triggerFile); | ||
|
||
const newRootFiles = projectOptions.files.map((f => this.getCanonicalFileName(f))); | ||
const currentRootFiles = project.getRootFiles().map((f => this.getCanonicalFileName(f))); | ||
|
||
// We check if the project file list has changed. If so, we update the project. | ||
if (!arrayIsEqualTo(currentRootFiles.sort(), newRootFiles.sort())) { | ||
// For configured projects, the change is made outside the tsconfig file, and | ||
// it is not likely to affect the project for other files opened by the client. We can | ||
// just update the current project. | ||
|
||
this.logger.info("Updating configured project"); | ||
this.updateConfiguredProject(project, projectOptions, configFileErrors); | ||
|
||
// Call refreshInferredProjects to clean up inferred projects we may have | ||
// created for the new files | ||
const configFileSpecs = project.configFileSpecs; | ||
const configFilename = normalizePath(project.getConfigFilePath()); | ||
// TODO: (sheetalkamat) use the host that caches - so we dont do file exists and read directory call | ||
const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), this.host, this.hostConfiguration.extraFileExtensions); | ||
const errors = project.getAllProjectErrors(); | ||
if (result.fileNames.length === 0) { | ||
if (!configFileSpecs.filesSpecs) { | ||
if (!some(errors, error => error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code)) { | ||
errors.push(getErrorForNoInputFiles(configFileSpecs, configFilename)); | ||
} | ||
if (!some(errors, error => error.code === Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files.code)) { | ||
errors.push(createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename)); | ||
} | ||
} | ||
} | ||
else { | ||
filterMutate(errors, error => | ||
error.code !== Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code && | ||
error.code !== Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files.code); | ||
} | ||
this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader); | ||
// TODO: (sheetalkamat) schedule the update graph | ||
if (!project.updateGraph()) { | ||
this.refreshInferredProjects(); | ||
} | ||
F43B | } | |
|
@@ -684,11 +681,6 @@ namespace ts.server { | |
this.reloadProjects(); | ||
} | ||
|
||
private getCanonicalFileName(fileName: string) { | ||
const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); | ||
return normalizePath(name); | ||
} | ||
|
||
private removeProject(project: Project) { | ||
this.logger.info(`remove project: ${project.getRootFiles().toString()}`); | ||
|
||
|
@@ -1009,7 +1001,7 @@ namespace ts.server { | |
}; | ||
} | ||
|
||
return { success, projectOptions, configFileErrors: errors }; | ||
return { success, projectOptions, configFileErrors: errors, configFileSpecs: parsedCommandLine.configFileSpecs }; | ||
} | ||
|
||
private exceededTotalSizeLimitForNonTsFiles<T>(name: string, options: CompilerOptions, fileNames: T[], propertyReader: FilePropertyReader<T>) { | ||
|
@@ -1113,7 +1105,7 @@ namespace ts.server { | |
}); | ||
} | ||
|
||
private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], clientFileName?: string) { | ||
private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, clientFileName?: string) { | ||
const sizeLimitExceeded = this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); | ||
const project = new ConfiguredProject( | ||
configFileName, | ||
|
@@ -1126,6 +1118,7 @@ namespace ts.server { | |
|
||
this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); | ||
|
||
project.configFileSpecs = configFileSpecs; | ||
project.watchConfigFile((project, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind)); | ||
project.watchWildcards(projectOptions.wildcardDirectories); | ||
project.watchTypeRoots((project, path) => this.onTypeRootFileChanged(project, path)); | ||
|
@@ -1156,15 +1149,15 @@ namespace ts.server { | |
} | ||
|
||
private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) { | ||
const { success, projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(configFileName); | ||
const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName); | ||
if (success) { | ||
this.logger.info(`Opened configuration file ${configFileName}`); | ||
} | ||
|
||
return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, clientFileName); | ||
return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, clientFileName); | ||
} | ||
|
||
private updateNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) { | ||
private updateNonInferredProjectFiles<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>) { | ||
const projectRootFilesMap = project.getRootFilesMap(); | ||
const newRootScriptInfoMap: Map<ProjectRoot> = createMap<ProjectRoot>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Type annotation not necessary |
||
|
||
|
@@ -1219,7 +1212,10 @@ namespace ts.server { | |
} | ||
}); | ||
} | ||
} | ||
|
||
private updateNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) { | ||
this.updateNonInferredProjectFiles(project, newUncheckedFiles, propertyReader); | ||
project.setCompilerOptions(newOptions); | ||
project.setTypeAcquisition(newTypeAcquisition); | ||
|
||
|
@@ -1243,7 +1239,8 @@ namespace ts.server { | |
// note: the returned "success" is true does not mean the "configFileErrors" is empty. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment out of date |
||
// because we might have tolerated the errors and kept going. So always return the configFileErrors | ||
// regardless the "success" here is true or not. | ||
const { success, projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath()); | ||
const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath()); | ||
project.configFileSpecs = configFileSpecs; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see why |
||
if (!success) { | ||
// reset project settings to default | ||
this.updateNonInferredProject(project, [], fileNamePropertyReader, {}, {}, /*compileOnSave*/ false, configFileErrors); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like
configFileName
is always provided. Shouldn't need|| "tsconfig.json"
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be i should type configFileName as string | undefined as it could be undefined as it is also us 67ED ed through parseJsonConfigFileContent which is exposed api and has configFileName as optional parameter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
x: string | undefined
is better thanx?: string
as it ensures that we don't forget to provide the parameter.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this expected to be part of the public API? While it makes sense for us to want to be sure we don't forget the parameter, it is a slight inconvenience for API consumers.