diff --git a/packages/example/src/modern/getDate.ts b/packages/example/src/modern/getDate.ts index 62a1f54..d606204 100644 --- a/packages/example/src/modern/getDate.ts +++ b/packages/example/src/modern/getDate.ts @@ -3,7 +3,7 @@ import { someVar } from '../legacy/getDate'; export const getDate = (date) => { const modern: string | undefined = undefined; - // Show string | undefined on hover in IDE + // Show `string | undefined` on hover in IDE console.log(someVar); modern.split(''); diff --git a/packages/plugin/src/cli/index.ts b/packages/plugin/src/cli/index.ts index 2778e02..5ba2013 100644 --- a/packages/plugin/src/cli/index.ts +++ b/packages/plugin/src/cli/index.ts @@ -1,167 +1,83 @@ -import outmatch from 'outmatch'; import * as path from 'path'; import type { PluginConfig, ProgramTransformer } from 'ts-patch'; import ts from 'typescript'; + import { Override } from '../types/Override'; +import { + getDiagnosticForFile, + getDiagnosticsForProject, + getOverridePrograms, + OverridePrograms, +} from './utils'; + interface CliPluginConfig extends PluginConfig { overrides: Override[]; } -const getOverridePrograms = ( - rootPath: string, - typescript: typeof ts, - overridesFromConfig: Override[], - originalProgram: ts.Program, - host?: ts.CompilerHost -) => { - let filesToOriginalDiagnostic: string[] = [...originalProgram.getRootFileNames()]; - const { plugins, ...defaultCompilerOptions } = originalProgram.getCompilerOptions(); - - const sortedOverrides = [...overridesFromConfig].reverse(); - - const resultOverrides: ts.Program[] = []; - - for (const override of sortedOverrides) { - const isMatch = outmatch(override.files); - const filesToCurrentOverrideDiagnostic: string[] = []; - - for (const fileName of filesToOriginalDiagnostic) { - const toOverrideDiagnostic = isMatch(path.relative(rootPath, fileName)); - - if (toOverrideDiagnostic) { - filesToCurrentOverrideDiagnostic.push(fileName); - } - } - - const overrideProgram = typescript.createProgram( - filesToCurrentOverrideDiagnostic, - { - ...defaultCompilerOptions, - ...override.compilerOptions, - }, - host - ); - resultOverrides.push(overrideProgram); - - filesToOriginalDiagnostic = filesToOriginalDiagnostic.filter( - (fileName) => !filesToCurrentOverrideDiagnostic.includes(fileName) - ); - } - - return { resultOverrides, filesToOriginalDiagnostic }; -}; - -const getDiagnosticsForProject = ( - overridePrograms: ts.Program[], - originalProgram: ts.Program, - filesToOriginalDiagnostic: string[], - cancellationToken?: ts.CancellationToken -): ts.Diagnostic[] => { - const diagnostics: ts.Diagnostic[] = []; - for (const overrideProgram of overridePrograms) { - for (const rootFileName of overrideProgram.getRootFileNames()) { - const sourceFile = overrideProgram.getSourceFile(rootFileName); - - const diagnosticsForOverride = overrideProgram.getSemanticDiagnostics( - sourceFile, - cancellationToken - ); - - diagnostics.push(...diagnosticsForOverride); - } - } - - for (const rootFileName of filesToOriginalDiagnostic) { - if (filesToOriginalDiagnostic.includes(rootFileName)) { - const sourceFile = originalProgram.getSourceFile(rootFileName); - - const diagnosticsForOriginal = originalProgram.getSemanticDiagnostics( - sourceFile, - cancellationToken - ); - - diagnostics.push(...diagnosticsForOriginal); - } - } - - return diagnostics; -}; - -let overridePrograms: { - filesToOriginalDiagnostic: string[]; - resultOverrides: ts.Program[]; -} | null = null; +let overridePrograms: OverridePrograms | null = null; const plugin: ProgramTransformer = (program, host, pluginConfig, extras) => { const { overrides: overridesFromConfig } = pluginConfig as CliPluginConfig; const { plugins, ...defaultCompilerOptions } = program.getCompilerOptions(); + const sortedOverridesFromConfig = [...overridesFromConfig].reverse(); const rootPath = defaultCompilerOptions.project ? path.dirname(defaultCompilerOptions.project) : process.cwd(); + overridePrograms = null; - overridePrograms = getOverridePrograms(rootPath, extras.ts, overridesFromConfig, program, host); + overridePrograms = getOverridePrograms( + rootPath, + extras.ts, + sortedOverridesFromConfig, + program.getRootFileNames(), + defaultCompilerOptions, + host + ); - // Возвращать новую программу без файлов, которые подменяются оверрайдами return new Proxy(program, { get: (target, property: keyof ts.Program) => { + // for watch mode - ForkTsCheckerWebpackPlugin and tspc if (property === 'getBindAndCheckDiagnostics') { return ((sourceFile, cancellationToken) => { - const { fileName } = sourceFile; - - if (!overridePrograms || overridePrograms.filesToOriginalDiagnostic.includes(fileName)) { - return target.getBindAndCheckDiagnostics(sourceFile, cancellationToken); - } - - const overrideProgramForFile = overridePrograms?.resultOverrides.find( - (overrideProgram) => { - return overrideProgram.getRootFileNames().includes(fileName); - } + return getDiagnosticForFile( + overridePrograms, + target, + sourceFile, + 'getBindAndCheckDiagnostics', + cancellationToken ); - - return overrideProgramForFile - ? overrideProgramForFile.getBindAndCheckDiagnostics(sourceFile, cancellationToken) - : target.getBindAndCheckDiagnostics(sourceFile, cancellationToken); }) as ts.Program['getBindAndCheckDiagnostics']; } + // for build mode + // for watch mode - ts-loader if (property === 'getSemanticDiagnostics') { return ((sourceFile, cancellationToken) => { + // for build ForkTsCheckerWebpackPlugin and tspc if (!sourceFile) { overridePrograms = null; - const overrideProgramsForBuild = getOverridePrograms( + return getDiagnosticsForProject( rootPath, extras.ts, - overridesFromConfig, + sortedOverridesFromConfig, target, + defaultCompilerOptions, + cancellationToken, host ); - - return getDiagnosticsForProject( - overrideProgramsForBuild.resultOverrides, - target, - overrideProgramsForBuild.filesToOriginalDiagnostic, - cancellationToken - ); } - const { fileName } = sourceFile; - - if (!overridePrograms || overridePrograms.filesToOriginalDiagnostic.includes(fileName)) { - return target.getSemanticDiagnostics(sourceFile, cancellationToken); - } - - const overrideProgramForFile = overridePrograms?.resultOverrides.find( - (overrideProgram) => { - return overrideProgram.getRootFileNames().includes(fileName); - } + // for ts-loader - watch and build + return getDiagnosticForFile( + overridePrograms, + target, + sourceFile, + 'getSemanticDiagnostics', + cancellationToken ); - - return overrideProgramForFile - ? overrideProgramForFile.getSemanticDiagnostics(sourceFile, cancellationToken) - : target.getSemanticDiagnostics(sourceFile, cancellationToken); }) as ts.Program['getSemanticDiagnostics']; } diff --git a/packages/plugin/src/cli/utils.ts b/packages/plugin/src/cli/utils.ts new file mode 100644 index 0000000..259432b --- /dev/null +++ b/packages/plugin/src/cli/utils.ts @@ -0,0 +1,136 @@ +import path from 'node:path'; +import outmatch from 'outmatch'; +import type ts from 'typescript'; + +import { Override } from '../types/Override'; + +const getOverrideProgram = ( + rootPath: string, + typescript: typeof ts, + override: Override, + filesToOriginalDiagnostic: string[], + defaultCompilerOptions: ts.CompilerOptions, + host?: ts.CompilerHost +): { + overrideProgram: ts.Program; + filesToCurrentOverrideDiagnostic: string[]; +} => { + const isMatch = outmatch(override.files); + const filesToCurrentOverrideDiagnostic: string[] = filesToOriginalDiagnostic.filter((fileName) => + isMatch(path.relative(rootPath, fileName)) + ); + + const overrideProgram = typescript.createProgram( + filesToCurrentOverrideDiagnostic, + { + ...defaultCompilerOptions, + ...override.compilerOptions, + }, + host + ); + + return { overrideProgram, filesToCurrentOverrideDiagnostic }; +}; + +export const getDiagnosticsForProject = ( + rootPath: string, + typescript: typeof ts, + overridesFromConfig: Override[], + program: ts.Program, + defaultCompilerOptions: ts.CompilerOptions, + cancellationToken?: ts.CancellationToken, + host?: ts.CompilerHost +): ts.Diagnostic[] => { + let filesToOriginalDiagnostic: string[] = [...program.getRootFileNames()]; + + const resultDiagnostic: ts.Diagnostic[] = overridesFromConfig.flatMap((override) => { + const { overrideProgram, filesToCurrentOverrideDiagnostic } = getOverrideProgram( + rootPath, + typescript, + override, + filesToOriginalDiagnostic, + defaultCompilerOptions, + host + ); + + filesToOriginalDiagnostic = filesToOriginalDiagnostic.filter( + (fileName) => !filesToCurrentOverrideDiagnostic.includes(fileName) + ); + + return filesToCurrentOverrideDiagnostic.flatMap((fileName) => { + const sourceFile = overrideProgram.getSourceFile(fileName); + + return sourceFile + ? overrideProgram.getSemanticDiagnostics(sourceFile, cancellationToken) + : []; + }); + }); + + const originalDiagnostics = filesToOriginalDiagnostic.flatMap((fileName) => { + const sourceFile = program.getSourceFile(fileName); + + return sourceFile ? program.getSemanticDiagnostics(sourceFile, cancellationToken) : []; + }); + + return [...resultDiagnostic, ...originalDiagnostics]; +}; + +export const getOverridePrograms = ( + rootPath: string, + typescript: typeof ts, + overridesFromConfig: Override[], + rootFileNames: readonly string[], + defaultCompilerOptions: ts.CompilerOptions, + host?: ts.CompilerHost +): { + resultOverrides: ts.Program[]; + filesToOriginalDiagnostic: string[]; +} => { + let filesToOriginalDiagnostic: string[] = [...rootFileNames]; + + const resultOverrides: ts.Program[] = overridesFromConfig.map((override) => { + const { overrideProgram, filesToCurrentOverrideDiagnostic } = getOverrideProgram( + rootPath, + typescript, + override, + filesToOriginalDiagnostic, + defaultCompilerOptions, + host + ); + + filesToOriginalDiagnostic = filesToOriginalDiagnostic.filter( + (fileName) => !filesToCurrentOverrideDiagnostic.includes(fileName) + ); + + return overrideProgram; + }); + + return { resultOverrides, filesToOriginalDiagnostic }; +}; + +export interface OverridePrograms { + filesToOriginalDiagnostic: string[]; + resultOverrides: ts.Program[]; +} + +export const getDiagnosticForFile = ( + overridePrograms: OverridePrograms | null, + target: ts.Program, + sourceFile: ts.SourceFile, + method: 'getSemanticDiagnostics' | 'getBindAndCheckDiagnostics', + cancellationToken?: ts.CancellationToken +): readonly ts.Diagnostic[] => { + const { fileName } = sourceFile; + + if (!overridePrograms || overridePrograms.filesToOriginalDiagnostic.includes(fileName)) { + return target[method](sourceFile, cancellationToken); + } + + const overrideProgramForFile = overridePrograms?.resultOverrides.find((overrideProgram) => { + return overrideProgram.getRootFileNames().includes(fileName); + }); + + return overrideProgramForFile + ? overrideProgramForFile[method](sourceFile, cancellationToken) + : target[method](sourceFile, cancellationToken); +}; diff --git a/packages/plugin/src/ide/index.ts b/packages/plugin/src/ide/index.ts index ef30e59..8d4b590 100644 --- a/packages/plugin/src/ide/index.ts +++ b/packages/plugin/src/ide/index.ts @@ -1,30 +1,31 @@ +import outmatch from 'outmatch'; import * as path from 'path'; +import type ts from 'typescript/lib/tsserverlibrary'; -import outmatch from 'outmatch'; -import ts from 'typescript'; import { Override } from '../types/Override'; interface IdePluginConfig { overrides: Override[]; } -const getOverridePrograms = ( +const getOverrideLanguageServices = ( typescript: typeof ts, overridesFromConfig: Override[], languageServiceHost: ts.LanguageServiceHost, - docRegistry: ts.DocumentRegistry, + docRegistry: ts.DocumentRegistry ) => { return [...overridesFromConfig].reverse().map((override) => { - return typescript.createLanguageService(new Proxy( - languageServiceHost, - { + return typescript.createLanguageService( + new Proxy(languageServiceHost, { get(target, property: keyof ts.LanguageServiceHost) { if (property === 'getScriptFileNames') { return (() => { const originalFiles = target.getScriptFileNames(); const isMatch = outmatch(override.files); - return originalFiles.filter((fileName) => isMatch(path.relative(target.getCurrentDirectory(), fileName))); + return originalFiles.filter((fileName) => + isMatch(path.relative(target.getCurrentDirectory(), fileName)) + ); }) as ts.LanguageServiceHost['getScriptFileNames']; } @@ -39,15 +40,16 @@ const getOverridePrograms = ( return target[property as keyof ts.LanguageServiceHost]; }, - } - ), docRegistry); - }) + }), + docRegistry + ); + }); }; const getLanguageServiceForFile = ( fileName: string, overrideLanguageServices: ts.LanguageService[], - originalLanguageService: ts.LanguageService, + originalLanguageService: ts.LanguageService ) => { const overrideForFile = overrideLanguageServices.find((override) => { return override.getProgram()?.getRootFileNames().includes(fileName); @@ -58,47 +60,58 @@ const getLanguageServiceForFile = ( } return originalLanguageService; -} +}; const plugin: ts.server.PluginModuleFactory = ({ typescript }) => { - function create(info: ts.server.PluginCreateInfo) { - const { overrides: overridesFromConfig } = info.config as IdePluginConfig; - - const docRegistry = typescript.createDocumentRegistry(); - - const overrideLanguageServices = getOverridePrograms( - typescript, - overridesFromConfig, - info.languageServiceHost, - docRegistry, - ); - - const originalLanguageServiceWithDocRegistry = typescript.createLanguageService(info.languageServiceHost, docRegistry); - - return new Proxy(originalLanguageServiceWithDocRegistry, { - get(target, property: keyof ts.LanguageService) { - if (property === 'getQuickInfoAtPosition') { - return ((fileName, position) => { - const overrideForFile = getLanguageServiceForFile(fileName, overrideLanguageServices, target); - - return overrideForFile.getQuickInfoAtPosition(fileName, position); - }) as ts.LanguageService['getQuickInfoAtPosition']; - } - - if (property === 'getSemanticDiagnostics') { - return ((fileName) => { - const overrideForFile = getLanguageServiceForFile(fileName, overrideLanguageServices, target); + return { + create: (info) => { + const { overrides: overridesFromConfig } = info.config as IdePluginConfig; + + const docRegistry = typescript.createDocumentRegistry(); + + const overrideLanguageServices = getOverrideLanguageServices( + typescript, + overridesFromConfig, + info.languageServiceHost, + docRegistry + ); + + const originalLanguageServiceWithDocRegistry = typescript.createLanguageService( + info.languageServiceHost, + docRegistry + ); + + return new Proxy(originalLanguageServiceWithDocRegistry, { + get(target, property: keyof ts.LanguageService) { + if (property === 'getQuickInfoAtPosition') { + return ((fileName, position) => { + const overrideForFile = getLanguageServiceForFile( + fileName, + overrideLanguageServices, + target + ); + + return overrideForFile.getQuickInfoAtPosition(fileName, position); + }) as ts.LanguageService['getQuickInfoAtPosition']; + } - return overrideForFile.getSemanticDiagnostics(fileName); - }) as ts.LanguageService['getSemanticDiagnostics']; - } + if (property === 'getSemanticDiagnostics') { + return ((fileName) => { + const overrideForFile = getLanguageServiceForFile( + fileName, + overrideLanguageServices, + target + ); - return target[property as keyof ts.LanguageService]; - }, - }); - } + return overrideForFile.getSemanticDiagnostics(fileName); + }) as ts.LanguageService['getSemanticDiagnostics']; + } - return { create }; + return target[property as keyof ts.LanguageService]; + }, + }); + }, + }; }; export = plugin;