diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index c313cb3b3e..c01ad65dc8 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1422,7 +1422,7 @@ namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine { + export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine { return parseJsonConfigFileContentWorker(json, /*sourceFile*/ undefined, host, basePath, existingOptions, configFileName, resolutionStack, extraFileExtensions); } @@ -1433,7 +1433,7 @@ namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - export function parseJsonSourceFileConfigFileContent(sourceFile: JsonSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine { + export function parseJsonSourceFileConfigFileContent(sourceFile: JsonSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine { return parseJsonConfigFileContentWorker(/*json*/ undefined, sourceFile, host, basePath, existingOptions, configFileName, resolutionStack, extraFileExtensions); } @@ -1472,7 +1472,7 @@ namespace ts { existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = [], - extraFileExtensions: ReadonlyArray = [], + extraFileExtensions: ReadonlyArray = [], ): ParsedCommandLine { Debug.assert((json === undefined && sourceFile !== undefined) || (json !== undefined && sourceFile === undefined)); const errors: Diagnostic[] = []; @@ -2005,7 +2005,7 @@ namespace ts { options: CompilerOptions, host: ParseConfigHost, errors: Push, - extraFileExtensions: ReadonlyArray, + extraFileExtensions: ReadonlyArray, jsonSourceFile: JsonSourceFile ): ExpandResult { basePath = normalizePath(basePath); @@ -2043,7 +2043,7 @@ namespace ts { * @param extraFileExtensions optionaly file extra file extension information from host */ /* @internal */ - export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray = []): ExpandResult { + export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray = []): ExpandResult { basePath = normalizePath(basePath); const keyMapper = host.useCaseSensitiveFileNames ? identity : toLowerCase; diff --git a/src/compiler/core.ts b/src/compiler/core.ts index a65298f72f..800c26f6c0 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2681,16 +2681,23 @@ namespace ts { export const supportedJavascriptExtensions: ReadonlyArray = [Extension.Js, Extension.Jsx]; const allSupportedExtensions: ReadonlyArray = [...supportedTypeScriptExtensions, ...supportedJavascriptExtensions]; - export function getSupportedExtensions(options?: CompilerOptions, extraFileExtensions?: ReadonlyArray): ReadonlyArray { - const needAllExtensions = options && options.allowJs; - if (!extraFileExtensions || extraFileExtensions.length === 0 || !needAllExtensions) { - return needAllExtensions ? allSupportedExtensions : supportedTypeScriptExtensions; + export function getSupportedExtensions(options?: CompilerOptions, extraFileExtensions?: ReadonlyArray): ReadonlyArray { + const needJsExtensions = options && options.allowJs; + + if (!extraFileExtensions || extraFileExtensions.length === 0) { + return needJsExtensions ? allSupportedExtensions : supportedTypeScriptExtensions; } - return deduplicate( - [...allSupportedExtensions, ...extraFileExtensions.map(e => e.extension)], - equateStringsCaseSensitive, - compareStringsCaseSensitive - ); + + const extensions = [ + ...needJsExtensions ? allSupportedExtensions : supportedTypeScriptExtensions, + ...mapDefined(extraFileExtensions, x => x.scriptKind === ScriptKind.Deferred || needJsExtensions && isJavaScriptLike(x.scriptKind) ? x.extension : undefined) + ]; + + return deduplicate(extensions, equateStringsCaseSensitive, compareStringsCaseSensitive); + } + + function isJavaScriptLike(scriptKind: ScriptKind): boolean { + return scriptKind === ScriptKind.JS || scriptKind === ScriptKind.JSX; } export function hasJavaScriptFileExtension(fileName: string) { @@ -2701,7 +2708,7 @@ namespace ts { return forEach(supportedTypeScriptExtensions, extension => fileExtensionIs(fileName, extension)); } - export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, extraFileExtensions?: ReadonlyArray) { + export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, extraFileExtensions?: ReadonlyArray) { if (!fileName) { return false; } for (const extension of getSupportedExtensions(compilerOptions, extraFileExtensions)) { diff --git a/src/compiler/program.ts b/src/compiler/program.ts index cc5cd56437..b924944fdb 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1296,9 +1296,9 @@ namespace ts { Debug.assert(!!sourceFile.bindDiagnostics); const isCheckJs = isCheckJsEnabledForFile(sourceFile, options); - // By default, only type-check .ts, .tsx, and 'External' files (external files are added by plugins) + // By default, only type-check .ts, .tsx, 'Deferred' and 'External' files (external files are added by plugins) const includeBindAndCheckDiagnostics = sourceFile.scriptKind === ScriptKind.TS || sourceFile.scriptKind === ScriptKind.TSX || - sourceFile.scriptKind === ScriptKind.External || isCheckJs; + sourceFile.scriptKind === ScriptKind.External || isCheckJs || sourceFile.scriptKind === ScriptKind.Deferred; const bindDiagnostics = includeBindAndCheckDiagnostics ? sourceFile.bindDiagnostics : emptyArray; const checkDiagnostics = includeBindAndCheckDiagnostics ? typeChecker.getDiagnostics(sourceFile, cancellationToken) : emptyArray; const fileProcessingDiagnosticsInFile = fileProcessingDiagnostics.getDiagnostics(sourceFile.fileName); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index d7487b34f6..d59ea7dfbf 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4098,7 +4098,10 @@ namespace ts { Prototype, } - export interface JsFileExtensionInfo { + /** @deprecated Use FileExtensionInfo instead. */ + export type JsFileExtensionInfo = FileExtensionInfo; + + export interface FileExtensionInfo { extension: string; isMixedContent: boolean; scriptKind?: ScriptKind; @@ -4306,7 +4309,12 @@ namespace ts { TS = 3, TSX = 4, External = 5, - JSON = 6 + JSON = 6, + /** + * Used on extensions that doesn't define the ScriptKind but the content defines it. + * Deferred extensions are going to be included in all project contexts. + */ + Deferred = 7 } export const enum ScriptTarget { diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index bfa999a379..3e68688de8 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -3217,6 +3217,48 @@ namespace ts.projectSystem { }); }); + + it("includes deferred files in the project context", () => { + const file1 = { + path: "/a.deferred", + content: "const a = 1;" + }; + // Deferred extensions should not affect JS files. + const file2 = { + path: "/b.js", + content: "const b = 1;" + }; + const tsconfig = { + path: "/tsconfig.json", + content: "" + }; + + const host = createServerHost([file1, file2, tsconfig]); + const session = createSession(host); + const projectService = session.getProjectService(); + + // Configure the deferred extension. + const extraFileExtensions = [{ extension: ".deferred", scriptKind: ScriptKind.Deferred, isMixedContent: true }]; + const configureHostRequest = makeSessionRequest(CommandNames.Configure, { extraFileExtensions }); + session.executeCommand(configureHostRequest); + + // Open external project + const projectName = "/proj1"; + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([file1.path, file2.path, tsconfig.path]), + options: {} + }); + + // Assert + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + const configuredProject = configuredProjectAt(projectService, 0); + checkProjectActualFiles(configuredProject, [file1.path, tsconfig.path]); + + // Allow allowNonTsExtensions will be set to true for deferred extensions. + assert.isTrue(configuredProject.getCompilerOptions().allowNonTsExtensions); + }); }); describe("tsserverProjectSystem Proper errors", () => { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index ef82e6de30..b835e56637 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -194,7 +194,7 @@ namespace ts.server { formatCodeOptions: FormatCodeSettings; preferences: UserPreferences; hostInfo: string; - extraFileExtensions?: JsFileExtensionInfo[]; + extraFileExtensions?: FileExtensionInfo[]; } export interface OpenConfiguredProjectResult { @@ -204,8 +204,8 @@ namespace ts.server { interface FilePropertyReader { getFileName(f: T): string; - getScriptKind(f: T, extraFileExtensions?: JsFileExtensionInfo[]): ScriptKind; - hasMixedContent(f: T, extraFileExtensions: JsFileExtensionInfo[]): boolean; + getScriptKind(f: T, extraFileExtensions?: FileExtensionInfo[]): ScriptKind; + hasMixedContent(f: T, extraFileExtensions: FileExtensionInfo[]): boolean; } const fileNamePropertyReader: FilePropertyReader = { @@ -2426,5 +2426,15 @@ namespace ts.server { this.createExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition, excludedFiles); } } + + hasDeferredExtension() { + for (const extension of this.hostConfiguration.extraFileExtensions) { + if (extension.scriptKind === ScriptKind.Deferred) { + return true; + } + } + + return false; + } } } diff --git a/src/server/project.ts b/src/server/project.ts index 52ccd1be6a..9d8b3a36dd 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -207,7 +207,7 @@ namespace ts.server { this.compilerOptions.allowNonTsExtensions = true; this.compilerOptions.allowJs = true; } - else if (hasExplicitListOfFiles || this.compilerOptions.allowJs) { + else if (hasExplicitListOfFiles || this.compilerOptions.allowJs || this.projectService.hasDeferredExtension()) { // If files are listed explicitly or allowJs is specified, allow all extensions this.compilerOptions.allowNonTsExtensions = true; } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 5ab62a1c33..b933d88af7 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -1293,7 +1293,7 @@ namespace ts.server.protocol { /** * The host's additional supported .js file extensions */ - extraFileExtensions?: JsFileExtensionInfo[]; + extraFileExtensions?: FileExtensionInfo[]; } /** diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 70f99eaa8d..62a916e732 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2274,7 +2274,9 @@ declare namespace ts { AlwaysStrict = 64, PriorityImpliesCombination = 28 } - interface JsFileExtensionInfo { + /** @deprecated Use FileExtensionInfo instead. */ + type JsFileExtensionInfo = FileExtensionInfo; + interface FileExtensionInfo { extension: string; isMixedContent: boolean; scriptKind?: ScriptKind; @@ -2437,7 +2439,12 @@ declare namespace ts { TS = 3, TSX = 4, External = 5, - JSON = 6 + JSON = 6, + /** + * Used on extensions that doesn't define the ScriptKind but the content defines it. + * Deferred extensions are going to be included in all project contexts. + */ + Deferred = 7 } enum ScriptTarget { ES3 = 0, @@ -4225,7 +4232,7 @@ declare namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine; + function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine; /** * Parse the contents of a config file (tsconfig.json). * @param jsonNode The contents of the config file to parse @@ -4233,7 +4240,7 @@ declare namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - function parseJsonSourceFileConfigFileContent(sourceFile: JsonSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine; + function parseJsonSourceFileConfigFileContent(sourceFile: JsonSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine; function convertCompilerOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { options: CompilerOptions; errors: Diagnostic[]; @@ -6318,7 +6325,7 @@ declare namespace ts.server.protocol { /** * The host's additional supported .js file extensions */ - extraFileExtensions?: JsFileExtensionInfo[]; + extraFileExtensions?: FileExtensionInfo[]; } /** * Configure request; value of command field is "configure". Specifies @@ -7947,7 +7954,7 @@ declare namespace ts.server { formatCodeOptions: FormatCodeSettings; preferences: UserPreferences; hostInfo: string; - extraFileExtensions?: JsFileExtensionInfo[]; + extraFileExtensions?: FileExtensionInfo[]; } interface OpenConfiguredProjectResult { configFileName?: NormalizedPath; @@ -8195,6 +8202,7 @@ declare namespace ts.server { resetSafeList(): void; applySafeList(proj: protocol.ExternalProject): NormalizedPath[]; openExternalProject(proj: protocol.ExternalProject): void; + hasDeferredExtension(): boolean; } } declare namespace ts.server { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index d54eff0acb..e6ad7e5f3e 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2274,7 +2274,9 @@ declare namespace ts { AlwaysStrict = 64, PriorityImpliesCombination = 28 } - interface JsFileExtensionInfo { + /** @deprecated Use FileExtensionInfo instead. */ + type JsFileExtensionInfo = FileExtensionInfo; + interface FileExtensionInfo { extension: string; isMixedContent: boolean; scriptKind?: ScriptKind; @@ -2437,7 +2439,12 @@ declare namespace ts { TS = 3, TSX = 4, External = 5, - JSON = 6 + JSON = 6, + /** + * Used on extensions that doesn't define the ScriptKind but the content defines it. + * Deferred extensions are going to be included in all project contexts. + */ + Deferred = 7 } enum ScriptTarget { ES3 = 0, @@ -4225,7 +4232,7 @@ declare namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine; + function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine; /** * Parse the contents of a config file (tsconfig.json). * @param jsonNode The contents of the config file to parse @@ -4233,7 +4240,7 @@ declare namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - function parseJsonSourceFileConfigFileContent(sourceFile: JsonSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine; + function parseJsonSourceFileConfigFileContent(sourceFile: JsonSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine; function convertCompilerOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { options: CompilerOptions; errors: Diagnostic[];