From 6f9f47ff859c96ec9241f567909df86304c6dc81 Mon Sep 17 00:00:00 2001 From: Alexey Sudilovskiy Date: Fri, 13 Mar 2026 15:15:40 +0000 Subject: [PATCH 1/6] feat(dev): add support for projects w/ `"references"` --- src/common/typescript/utils.ts | 27 ---------- src/common/typescript/watch.ts | 90 +++++++++++++++++++++------------- 2 files changed, 55 insertions(+), 62 deletions(-) diff --git a/src/common/typescript/utils.ts b/src/common/typescript/utils.ts index 8ec226f..bde11a6 100644 --- a/src/common/typescript/utils.ts +++ b/src/common/typescript/utils.ts @@ -79,33 +79,6 @@ export function displayFilename( return displayFunction; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function onHostEvent any}>( - host: T, - functionName: F, - before?: (...args: Parameters>) => void, - after?: (res: ReturnType>) => void, -) { - const originalFunction = host[functionName]; - - // eslint-disable-next-line no-param-reassign - host[functionName] = ((...args: Parameters>) => { - if (before) { - before(...args); - } - - let result; - if (typeof originalFunction === 'function') { - result = originalFunction(...args); - } - - if (after) { - after(result); - } - return result; - }) as T[F]; -} - export function resolveTypescript() { try { return require.resolve(path.resolve(paths.appNodeModules, 'typescript')); diff --git a/src/common/typescript/watch.ts b/src/common/typescript/watch.ts index 5e6d97b..e7c4e1a 100644 --- a/src/common/typescript/watch.ts +++ b/src/common/typescript/watch.ts @@ -1,7 +1,7 @@ import type Typescript from 'typescript'; import type {Logger} from '../logger'; import {createTransformPathsToLocalModules} from './transformers'; -import {displayFilename, getTsProjectConfigPath, onHostEvent} from './utils'; +import {displayFilename, getTsProjectConfigPath} from './utils'; import {formatDiagnosticBrief} from './diagnostic'; export function watch( @@ -19,58 +19,78 @@ export function watch( const createProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram; - const host = ts.createWatchCompilerHost( - configPath, - { - noEmit: false, - noEmitOnError: false, - inlineSourceMap: enableSourceMap, - inlineSources: enableSourceMap, - ...(enableSourceMap ? {sourceMap: false} : undefined), - }, + const host = ts.createSolutionBuilderWithWatchHost( ts.sys, createProgram, reportDiagnostic, + reportDiagnostic, reportWatchStatusChanged, ); - host.readFile = displayFilename(host.readFile, 'Reading', logger); + const transformPathsToLocalModules = createTransformPathsToLocalModules(ts); + + // `createSolutionBuilderWithWatch` creates an initial program, watches files, and updates + // the program over time. + const solutionBuilder = ts.createSolutionBuilderWithWatch(host, [configPath], { + noEmit: false, + noEmitOnError: false, + inlineSourceMap: enableSourceMap, + inlineSources: enableSourceMap, + ...(enableSourceMap ? {sourceMap: false} : undefined), + }); + + let project = solutionBuilder.getNextInvalidatedProject(); + + do { + if (project?.kind === ts.InvalidatedProjectKind.Build) { + const projectConfigPath = project.project.replace(process.cwd(), ''); + + const originalReadFile = host.readFile; + host.readFile = displayFilename(originalReadFile, 'Reading', logger); - onHostEvent( - host, - 'createProgram', - () => { logger.verbose("We're about to create the program"); - // @ts-expect-error + // @ts-expect-error We invoke method from overrided function host.readFile.enableDisplay(); - }, - () => { + + const program = project.getProgram(); + + if (!program) { + logger.verbose(`Program was not created, skip emitting for ${projectConfigPath}`); + + // @ts-expect-error We invoke method from overrided function + host.readFile.disableDisplay(); + host.readFile = originalReadFile; + + logger.verbose( + `We finished making the program for ${projectConfigPath}! Emitting...`, + ); + + next(); + + continue; + } + // @ts-expect-error const count = host.readFile.disableDisplay(); + host.readFile = originalReadFile; logger.verbose(`Program created, read ${count} files`); - }, - ); - onHostEvent( - host, - 'afterProgramCreate', - (program) => { - logger.verbose('We finished making the program! Emitting...'); - const transformPathsToLocalModules = createTransformPathsToLocalModules(ts); - program.emit(undefined, undefined, undefined, undefined, { + project.emit(undefined, undefined, undefined, undefined, { after: [transformPathsToLocalModules], afterDeclarations: [transformPathsToLocalModules], }); + logger.verbose('Emit completed!'); - }, - () => { - onAfterFilesEmitted?.(); - }, - ); - // `createWatchProgram` creates an initial program, watches files, and updates - // the program over time. - ts.createWatchProgram(host); + next(); + } + } while (project); + + onAfterFilesEmitted?.(); + + function next() { + project = solutionBuilder.getNextInvalidatedProject(); + } function reportDiagnostic(diagnostic: Typescript.Diagnostic) { const formatHost = { From e1b1107bb2ab6f9544f01022cd5283ce1bcfebac Mon Sep 17 00:00:00 2001 From: Alexey Sudilovskiy Date: Fri, 13 Mar 2026 15:55:35 +0000 Subject: [PATCH 2/6] revert: onHostEvent removal --- src/common/typescript/utils.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/common/typescript/utils.ts b/src/common/typescript/utils.ts index bde11a6..8ec226f 100644 --- a/src/common/typescript/utils.ts +++ b/src/common/typescript/utils.ts @@ -79,6 +79,33 @@ export function displayFilename( return displayFunction; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function onHostEvent any}>( + host: T, + functionName: F, + before?: (...args: Parameters>) => void, + after?: (res: ReturnType>) => void, +) { + const originalFunction = host[functionName]; + + // eslint-disable-next-line no-param-reassign + host[functionName] = ((...args: Parameters>) => { + if (before) { + before(...args); + } + + let result; + if (typeof originalFunction === 'function') { + result = originalFunction(...args); + } + + if (after) { + after(result); + } + return result; + }) as T[F]; +} + export function resolveTypescript() { try { return require.resolve(path.resolve(paths.appNodeModules, 'typescript')); From 87ecb47b17866acbb7da0a11ed594964736a385f Mon Sep 17 00:00:00 2001 From: Alexey Sudilovskiy Date: Fri, 13 Mar 2026 16:06:53 +0000 Subject: [PATCH 3/6] fix: actually watch for changes --- src/common/typescript/watch.ts | 86 +++++++++++++--------------------- 1 file changed, 33 insertions(+), 53 deletions(-) diff --git a/src/common/typescript/watch.ts b/src/common/typescript/watch.ts index e7c4e1a..f20f086 100644 --- a/src/common/typescript/watch.ts +++ b/src/common/typescript/watch.ts @@ -1,7 +1,7 @@ import type Typescript from 'typescript'; import type {Logger} from '../logger'; import {createTransformPathsToLocalModules} from './transformers'; -import {displayFilename, getTsProjectConfigPath} from './utils'; +import {displayFilename, getTsProjectConfigPath, onHostEvent} from './utils'; import {formatDiagnosticBrief} from './diagnostic'; export function watch( @@ -27,7 +27,33 @@ export function watch( reportWatchStatusChanged, ); - const transformPathsToLocalModules = createTransformPathsToLocalModules(ts); + host.readFile = displayFilename(host.readFile, 'Reading', logger); + + onHostEvent( + host, + 'createProgram', + () => { + logger.verbose("We're about to create the program"); + // @ts-expect-error + host.readFile.enableDisplay(); + }, + () => { + // @ts-expect-error + const count = host.readFile.disableDisplay(); + logger.verbose(`Program created, read ${count} files`); + }, + ); + + onHostEvent( + host, + 'afterProgramEmitAndDiagnostics', + () => { + logger.verbose('Emit completed!'); + }, + () => { + onAfterFilesEmitted?.(); + }, + ); // `createSolutionBuilderWithWatch` creates an initial program, watches files, and updates // the program over time. @@ -39,58 +65,12 @@ export function watch( ...(enableSourceMap ? {sourceMap: false} : undefined), }); - let project = solutionBuilder.getNextInvalidatedProject(); - - do { - if (project?.kind === ts.InvalidatedProjectKind.Build) { - const projectConfigPath = project.project.replace(process.cwd(), ''); - - const originalReadFile = host.readFile; - host.readFile = displayFilename(originalReadFile, 'Reading', logger); - - logger.verbose("We're about to create the program"); - // @ts-expect-error We invoke method from overrided function - host.readFile.enableDisplay(); - - const program = project.getProgram(); - - if (!program) { - logger.verbose(`Program was not created, skip emitting for ${projectConfigPath}`); - - // @ts-expect-error We invoke method from overrided function - host.readFile.disableDisplay(); - host.readFile = originalReadFile; - - logger.verbose( - `We finished making the program for ${projectConfigPath}! Emitting...`, - ); - - next(); - - continue; - } - - // @ts-expect-error - const count = host.readFile.disableDisplay(); - host.readFile = originalReadFile; - logger.verbose(`Program created, read ${count} files`); - - project.emit(undefined, undefined, undefined, undefined, { - after: [transformPathsToLocalModules], - afterDeclarations: [transformPathsToLocalModules], - }); - - logger.verbose('Emit completed!'); - - next(); - } - } while (project); - - onAfterFilesEmitted?.(); + const transformPathsToLocalModules = createTransformPathsToLocalModules(ts); - function next() { - project = solutionBuilder.getNextInvalidatedProject(); - } + solutionBuilder.build(undefined, undefined, undefined, () => ({ + after: [transformPathsToLocalModules], + afterDeclarations: [transformPathsToLocalModules], + })); function reportDiagnostic(diagnostic: Typescript.Diagnostic) { const formatHost = { From aafe92c9552f4def6e47f2208cdb31afa50ef5aa Mon Sep 17 00:00:00 2001 From: Alexey Sudilovskiy Date: Fri, 13 Mar 2026 16:44:19 +0000 Subject: [PATCH 4/6] fix: handle diagnotic codes to determine that compilation is finished --- src/common/typescript/utils.ts | 4 ++-- src/common/typescript/watch.ts | 42 +++++++++++++++++++--------------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/common/typescript/utils.ts b/src/common/typescript/utils.ts index 8ec226f..4c4e373 100644 --- a/src/common/typescript/utils.ts +++ b/src/common/typescript/utils.ts @@ -84,7 +84,7 @@ export function onHostEvent>) => void, - after?: (res: ReturnType>) => void, + after?: (res: ReturnType>, ...args: Parameters>) => void, ) { const originalFunction = host[functionName]; @@ -100,7 +100,7 @@ export function onHostEvent { + (_rootnames, _options, host) => { logger.verbose("We're about to create the program"); - // @ts-expect-error - host.readFile.enableDisplay(); - }, - () => { - // @ts-expect-error - const count = host.readFile.disableDisplay(); - logger.verbose(`Program created, read ${count} files`); - }, - ); - onHostEvent( - host, - 'afterProgramEmitAndDiagnostics', - () => { - logger.verbose('Emit completed!'); + if (host) { + host.readFile = displayFilename(host.readFile, 'Reading', logger); + + // @ts-expect-error + host.readFile.enableDisplay(); + } }, - () => { - onAfterFilesEmitted?.(); + (_result, _rootnames, _options, host) => { + if (host) { + // @ts-expect-error + const count = host.readFile.disableDisplay(); + + logger.verbose(`Program created, read ${count} files`); + } }, ); + onHostEvent(host, 'afterProgramEmitAndDiagnostics', (program) => { + const project = program.getCompilerOptions().project; + logger.verbose(`Emit completed for ${project}!`); + }); + // `createSolutionBuilderWithWatch` creates an initial program, watches files, and updates // the program over time. const solutionBuilder = ts.createSolutionBuilderWithWatch(host, [configPath], { @@ -93,5 +93,9 @@ export function watch( if (diagnostic.messageText) { logger.message(ts.flattenDiagnosticMessageText(diagnostic.messageText, ts.sys.newLine)); } + + if (diagnostic.code === 6194 || diagnostic.code === 6193) { + onAfterFilesEmitted?.(); + } } } From 491795f1c7db13ddd2a639d9acc2bd6cd86855eb Mon Sep 17 00:00:00 2001 From: Alexey Sudilovskiy Date: Fri, 13 Mar 2026 17:25:09 +0000 Subject: [PATCH 5/6] fix: tsconfig path output --- src/common/typescript/watch.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/common/typescript/watch.ts b/src/common/typescript/watch.ts index 14846d6..40e4a2c 100644 --- a/src/common/typescript/watch.ts +++ b/src/common/typescript/watch.ts @@ -51,8 +51,13 @@ export function watch( ); onHostEvent(host, 'afterProgramEmitAndDiagnostics', (program) => { - const project = program.getCompilerOptions().project; - logger.verbose(`Emit completed for ${project}!`); + const project = program.getCompilerOptions().configFilePath; + + logger.verbose( + typeof project === 'string' + ? `Emit completed for ${project.replace(process.cwd(), '')}!` + : 'Emit completed!', + ); }); // `createSolutionBuilderWithWatch` creates an initial program, watches files, and updates From 59cce983108f12be1e0155c41ba20aa1e093af2e Mon Sep 17 00:00:00 2001 From: Alexey Sudilovskiy Date: Mon, 16 Mar 2026 14:43:51 +0000 Subject: [PATCH 6/6] refactor: extract codes to var --- src/common/typescript/watch.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/common/typescript/watch.ts b/src/common/typescript/watch.ts index 40e4a2c..743b40b 100644 --- a/src/common/typescript/watch.ts +++ b/src/common/typescript/watch.ts @@ -4,6 +4,12 @@ import {createTransformPathsToLocalModules} from './transformers'; import {displayFilename, getTsProjectConfigPath, onHostEvent} from './utils'; import {formatDiagnosticBrief} from './diagnostic'; +/** @see https://github.com/microsoft/TypeScript/blob/9059e5bda0bb603ae6b41eca09dcd2a071af45fd/src/compiler/diagnosticMessages.json#L5400-L5403 */ +const COMPILATION_COMPLETE_WITH_ERROR = 6193; + +/** @see https://github.com/microsoft/TypeScript/blob/9059e5bda0bb603ae6b41eca09dcd2a071af45fd/src/compiler/diagnosticMessages.json#L5404-L5407 */ +const COMPILATION_COMPLETE_WITH_N_ERRORS = 6194; + export function watch( ts: typeof Typescript, projectPath: string, @@ -99,7 +105,10 @@ export function watch( logger.message(ts.flattenDiagnosticMessageText(diagnostic.messageText, ts.sys.newLine)); } - if (diagnostic.code === 6194 || diagnostic.code === 6193) { + if ( + diagnostic.code === COMPILATION_COMPLETE_WITH_ERROR || + diagnostic.code === COMPILATION_COMPLETE_WITH_N_ERRORS + ) { onAfterFilesEmitted?.(); } }