diff --git a/.gitignore b/.gitignore index b78cd71b..a38eddea 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.log # Package artifacts -lib +/lib # Package archive used by e2e tests fork-ts-checker-webpack-plugin-0.0.0-semantic-release.tgz diff --git a/src/ForkTsCheckerWebpackPlugin.ts b/src/ForkTsCheckerWebpackPlugin.ts index a87b4579..e5c8b5ee 100644 --- a/src/ForkTsCheckerWebpackPlugin.ts +++ b/src/ForkTsCheckerWebpackPlugin.ts @@ -1,3 +1,5 @@ +import * as path from 'path'; + import { cosmiconfigSync } from 'cosmiconfig'; import merge from 'deepmerge'; import type { JSONSchema7 } from 'json-schema'; @@ -15,10 +17,12 @@ import { dependenciesPool, issuesPool } from './hooks/pluginPools'; import { tapAfterCompileToAddDependencies } from './hooks/tapAfterCompileToAddDependencies'; import { tapAfterEnvironmentToPatchWatching } from './hooks/tapAfterEnvironmentToPatchWatching'; import { tapErrorToLogMessage } from './hooks/tapErrorToLogMessage'; -import { tapStartToConnectAndRunReporter } from './hooks/tapStartToConnectAndRunReporter'; -import { tapStopToDisconnectReporter } from './hooks/tapStopToDisconnectReporter'; -import { createTypeScriptReporterRpcClient } from './typescript-reporter/reporter/TypeScriptReporterRpcClient'; -import { assertTypeScriptSupport } from './typescript-reporter/TypeScriptSupport'; +import { tapStartToRunWorkers } from './hooks/tapStartToRunWorkers'; +import { tapStopToTerminateWorkers } from './hooks/tapStopToTerminateWorkers'; +import { assertTypeScriptSupport } from './typescript/TypeScriptSupport'; +import type { GetDependenciesWorker } from './typescript/worker/get-dependencies-worker'; +import type { GetIssuesWorker } from './typescript/worker/get-issues-worker'; +import { createRpcWorker } from './utils/rpc'; class ForkTsCheckerWebpackPlugin { /** @@ -61,19 +65,20 @@ class ForkTsCheckerWebpackPlugin { const state = createForkTsCheckerWebpackPluginState(); assertTypeScriptSupport(configuration.typescript); - const issuesReporter = createTypeScriptReporterRpcClient(configuration.typescript); - const dependenciesReporter = createTypeScriptReporterRpcClient(configuration.typescript); + const getIssuesWorker = createRpcWorker( + path.resolve(__dirname, './typescript/worker/get-issues-worker.js'), + configuration.typescript, + configuration.typescript.memoryLimit + ); + const getDependenciesWorker = createRpcWorker( + path.resolve(__dirname, './typescript/worker/get-dependencies-worker.js'), + configuration.typescript + ); tapAfterEnvironmentToPatchWatching(compiler, state); - tapStartToConnectAndRunReporter( - compiler, - issuesReporter, - dependenciesReporter, - configuration, - state - ); + tapStartToRunWorkers(compiler, getIssuesWorker, getDependenciesWorker, configuration, state); tapAfterCompileToAddDependencies(compiler, configuration, state); - tapStopToDisconnectReporter(compiler, issuesReporter, dependenciesReporter, state); + tapStopToTerminateWorkers(compiler, getIssuesWorker, getDependenciesWorker, state); tapErrorToLogMessage(compiler, configuration); } } diff --git a/src/ForkTsCheckerWebpackPluginConfiguration.ts b/src/ForkTsCheckerWebpackPluginConfiguration.ts index e30ff180..b9f3b031 100644 --- a/src/ForkTsCheckerWebpackPluginConfiguration.ts +++ b/src/ForkTsCheckerWebpackPluginConfiguration.ts @@ -7,8 +7,8 @@ import type { IssueConfiguration } from './issue/IssueConfiguration'; import { createIssueConfiguration } from './issue/IssueConfiguration'; import type { LoggerConfiguration } from './logger/LoggerConfiguration'; import { createLoggerConfiguration } from './logger/LoggerConfiguration'; -import type { TypeScriptReporterConfiguration } from './typescript-reporter/TypeScriptReporterConfiguration'; -import { createTypeScriptReporterConfiguration } from './typescript-reporter/TypeScriptReporterConfiguration'; +import type { TypeScriptReporterConfiguration } from './typescript/TypeScriptReporterConfiguration'; +import { createTypeScriptReporterConfiguration } from './typescript/TypeScriptReporterConfiguration'; interface ForkTsCheckerWebpackPluginConfiguration { async: boolean; diff --git a/src/ForkTsCheckerWebpackPluginOptions.ts b/src/ForkTsCheckerWebpackPluginOptions.ts index 47d03289..06b8a2f7 100644 --- a/src/ForkTsCheckerWebpackPluginOptions.ts +++ b/src/ForkTsCheckerWebpackPluginOptions.ts @@ -1,7 +1,7 @@ import type { FormatterOptions } from './formatter'; import type { IssueOptions } from './issue/IssueOptions'; import type LoggerOptions from './logger/LoggerOptions'; -import type { TypeScriptReporterOptions } from './typescript-reporter/TypeScriptReporterOptions'; +import type { TypeScriptReporterOptions } from './typescript/TypeScriptReporterOptions'; interface ForkTsCheckerWebpackPluginOptions { async?: boolean; diff --git a/src/ForkTsCheckerWebpackPluginState.ts b/src/ForkTsCheckerWebpackPluginState.ts index 642aab84..3153164d 100644 --- a/src/ForkTsCheckerWebpackPluginState.ts +++ b/src/ForkTsCheckerWebpackPluginState.ts @@ -1,28 +1,26 @@ import type { FullTap } from 'tapable'; +import type { FilesMatch } from './files-match'; import type { Issue } from './issue'; -import type { FilesMatch, Report } from './reporter'; interface ForkTsCheckerWebpackPluginState { - issuesReportPromise: Promise; - dependenciesReportPromise: Promise; issuesPromise: Promise; dependenciesPromise: Promise; lastDependencies: FilesMatch | undefined; watching: boolean; initialized: boolean; + iteration: number; webpackDevServerDoneTap: FullTap | undefined; } function createForkTsCheckerWebpackPluginState(): ForkTsCheckerWebpackPluginState { return { - issuesReportPromise: Promise.resolve(undefined), - dependenciesReportPromise: Promise.resolve(undefined), issuesPromise: Promise.resolve(undefined), dependenciesPromise: Promise.resolve(undefined), lastDependencies: undefined, watching: false, initialized: false, + iteration: 0, webpackDevServerDoneTap: undefined, }; } diff --git a/src/error/OperationCanceledError.ts b/src/error/OperationCanceledError.ts deleted file mode 100644 index 1de63a5b..00000000 --- a/src/error/OperationCanceledError.ts +++ /dev/null @@ -1,5 +0,0 @@ -class OperationCanceledError extends Error { - readonly canceled = true; -} - -export { OperationCanceledError }; diff --git a/src/reporter/FilesChange.ts b/src/files-change.ts similarity index 72% rename from src/reporter/FilesChange.ts rename to src/files-change.ts index ee3a4c96..591d8096 100644 --- a/src/reporter/FilesChange.ts +++ b/src/files-change.ts @@ -1,24 +1,24 @@ -import type { Compiler } from 'webpack'; +import type * as webpack from 'webpack'; -import subtract from '../utils/array/substract'; -import unique from '../utils/array/unique'; +import subtract from './utils/array/substract'; +import unique from './utils/array/unique'; interface FilesChange { changedFiles?: string[]; deletedFiles?: string[]; } -const compilerFilesChangeMap = new WeakMap(); +const compilerFilesChangeMap = new WeakMap(); -function getFilesChange(compiler: Compiler): FilesChange { +function getFilesChange(compiler: webpack.Compiler): FilesChange { return compilerFilesChangeMap.get(compiler) || { changedFiles: [], deletedFiles: [] }; } -function updateFilesChange(compiler: Compiler, change: FilesChange): void { +function updateFilesChange(compiler: webpack.Compiler, change: FilesChange): void { compilerFilesChangeMap.set(compiler, aggregateFilesChanges([getFilesChange(compiler), change])); } -function clearFilesChange(compiler: Compiler): void { +function clearFilesChange(compiler: webpack.Compiler): void { compilerFilesChangeMap.delete(compiler); } diff --git a/src/reporter/FilesMatch.ts b/src/files-match.ts similarity index 100% rename from src/reporter/FilesMatch.ts rename to src/files-match.ts diff --git a/src/hooks/interceptDoneToGetWebpackDevServerTap.ts b/src/hooks/interceptDoneToGetWebpackDevServerTap.ts index dcba7df9..7f20d551 100644 --- a/src/hooks/interceptDoneToGetWebpackDevServerTap.ts +++ b/src/hooks/interceptDoneToGetWebpackDevServerTap.ts @@ -2,12 +2,15 @@ import type webpack from 'webpack'; import type { ForkTsCheckerWebpackPluginConfiguration } from '../ForkTsCheckerWebpackPluginConfiguration'; import type { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState'; +import { getInfrastructureLogger } from '../infrastructure-logger'; function interceptDoneToGetWebpackDevServerTap( compiler: webpack.Compiler, configuration: ForkTsCheckerWebpackPluginConfiguration, state: ForkTsCheckerWebpackPluginState ) { + const { debug } = getInfrastructureLogger(compiler); + // inspired by https://github.com/ypresto/fork-ts-checker-async-overlay-webpack-plugin compiler.hooks.done.intercept({ register: (tap) => { @@ -16,6 +19,7 @@ function interceptDoneToGetWebpackDevServerTap( tap.type === 'sync' && configuration.logger.devServer ) { + debug('Intercepting webpack-dev-server tap.'); state.webpackDevServerDoneTap = tap; } return tap; diff --git a/src/hooks/pluginHooks.ts b/src/hooks/pluginHooks.ts index e486f6bc..865d7dbf 100644 --- a/src/hooks/pluginHooks.ts +++ b/src/hooks/pluginHooks.ts @@ -1,8 +1,8 @@ import { SyncHook, SyncWaterfallHook, AsyncSeriesWaterfallHook } from 'tapable'; import type * as webpack from 'webpack'; +import type { FilesChange } from '../files-change'; import type { Issue } from '../issue'; -import type { FilesChange } from '../reporter'; const compilerHookMap = new WeakMap< webpack.Compiler | webpack.MultiCompiler, diff --git a/src/hooks/tapAfterCompileToAddDependencies.ts b/src/hooks/tapAfterCompileToAddDependencies.ts index cf21b5cd..3b9af69f 100644 --- a/src/hooks/tapAfterCompileToAddDependencies.ts +++ b/src/hooks/tapAfterCompileToAddDependencies.ts @@ -2,12 +2,15 @@ import type webpack from 'webpack'; import type { ForkTsCheckerWebpackPluginConfiguration } from '../ForkTsCheckerWebpackPluginConfiguration'; import type { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState'; +import { getInfrastructureLogger } from '../infrastructure-logger'; function tapAfterCompileToAddDependencies( compiler: webpack.Compiler, configuration: ForkTsCheckerWebpackPluginConfiguration, state: ForkTsCheckerWebpackPluginState ) { + const { debug } = getInfrastructureLogger(compiler); + compiler.hooks.afterCompile.tapPromise('ForkTsCheckerWebpackPlugin', async (compilation) => { if (compilation.compiler !== compiler) { // run only for the compiler that the plugin was registered for @@ -16,6 +19,7 @@ function tapAfterCompileToAddDependencies( const dependencies = await state.dependenciesPromise; + debug(`Got dependencies from the getDependenciesWorker.`, dependencies); if (dependencies) { state.lastDependencies = dependencies; diff --git a/src/hooks/tapAfterCompileToGetIssues.ts b/src/hooks/tapAfterCompileToGetIssues.ts index fdf7c16b..c3b07e28 100644 --- a/src/hooks/tapAfterCompileToGetIssues.ts +++ b/src/hooks/tapAfterCompileToGetIssues.ts @@ -2,6 +2,7 @@ import type webpack from 'webpack'; import type { ForkTsCheckerWebpackPluginConfiguration } from '../ForkTsCheckerWebpackPluginConfiguration'; import type { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState'; +import { getInfrastructureLogger } from '../infrastructure-logger'; import type { Issue } from '../issue'; import { IssueWebpackError } from '../issue/IssueWebpackError'; @@ -13,6 +14,7 @@ function tapAfterCompileToGetIssues( state: ForkTsCheckerWebpackPluginState ) { const hooks = getForkTsCheckerWebpackPluginHooks(compiler); + const { debug } = getInfrastructureLogger(compiler); compiler.hooks.afterCompile.tapPromise('ForkTsCheckerWebpackPlugin', async (compilation) => { if (compilation.compiler !== compiler) { @@ -29,6 +31,8 @@ function tapAfterCompileToGetIssues( return; } + debug('Got issues from getIssuesWorker.', issues?.length); + if (!issues) { // some error has been thrown or it was canceled return; diff --git a/src/hooks/tapAfterEnvironmentToPatchWatching.ts b/src/hooks/tapAfterEnvironmentToPatchWatching.ts index 2ba2220f..06a68edd 100644 --- a/src/hooks/tapAfterEnvironmentToPatchWatching.ts +++ b/src/hooks/tapAfterEnvironmentToPatchWatching.ts @@ -1,6 +1,7 @@ import type webpack from 'webpack'; import type { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState'; +import { getInfrastructureLogger } from '../infrastructure-logger'; import { InclusiveNodeWatchFileSystem } from '../watch/InclusiveNodeWatchFileSystem'; import type { WatchFileSystem } from '../watch/WatchFileSystem'; @@ -8,9 +9,12 @@ function tapAfterEnvironmentToPatchWatching( compiler: webpack.Compiler, state: ForkTsCheckerWebpackPluginState ) { + const { debug } = getInfrastructureLogger(compiler); + compiler.hooks.afterEnvironment.tap('ForkTsCheckerWebpackPlugin', () => { const watchFileSystem = compiler.watchFileSystem; if (watchFileSystem) { + debug("Overwriting webpack's watch file system."); // wrap original watch file system compiler.watchFileSystem = new InclusiveNodeWatchFileSystem( // we use some internals here @@ -18,6 +22,8 @@ function tapAfterEnvironmentToPatchWatching( compiler, state ); + } else { + debug('No watch file system found - plugin may not work correctly.'); } }); } diff --git a/src/hooks/tapDoneToAsyncGetIssues.ts b/src/hooks/tapDoneToAsyncGetIssues.ts index b0a443a8..d226699b 100644 --- a/src/hooks/tapDoneToAsyncGetIssues.ts +++ b/src/hooks/tapDoneToAsyncGetIssues.ts @@ -4,6 +4,7 @@ import type webpack from 'webpack'; import type { ForkTsCheckerWebpackPluginConfiguration } from '../ForkTsCheckerWebpackPluginConfiguration'; import type { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState'; import { createWebpackFormatter } from '../formatter/WebpackFormatter'; +import { getInfrastructureLogger } from '../infrastructure-logger'; import type { Issue } from '../issue'; import { IssueWebpackError } from '../issue/IssueWebpackError'; import isPending from '../utils/async/isPending'; @@ -17,6 +18,7 @@ function tapDoneToAsyncGetIssues( state: ForkTsCheckerWebpackPluginState ) { const hooks = getForkTsCheckerWebpackPluginHooks(compiler); + const { log, debug } = getInfrastructureLogger(compiler); compiler.hooks.done.tap('ForkTsCheckerWebpackPlugin', async (stats) => { if (stats.compilation.compiler !== compiler) { @@ -24,7 +26,6 @@ function tapDoneToAsyncGetIssues( return; } - const reportPromise = state.issuesReportPromise; const issuesPromise = state.issuesPromise; let issues: Issue[] | undefined; @@ -38,6 +39,7 @@ function tapDoneToAsyncGetIssues( } issues = await issuesPromise; + debug('Got issues from getIssuesWorker.', issues?.length); } catch (error) { hooks.error.call(error, stats.compilation); return; @@ -48,11 +50,6 @@ function tapDoneToAsyncGetIssues( return; } - if (reportPromise !== state.issuesReportPromise) { - // there is a newer report - ignore this one - return; - } - // filter list of issues by provided issue predicate issues = issues.filter(configuration.issue.predicate); @@ -81,13 +78,12 @@ function tapDoneToAsyncGetIssues( } }); + debug('Sending issues to the webpack-dev-server.'); state.webpackDevServerDoneTap.fn(stats); } if (stats.startTime) { - configuration.logger.infrastructure.log( - `Time: ${Math.round(Date.now() - stats.startTime).toString()} ms` - ); + log(`Time: ${Math.round(Date.now() - stats.startTime).toString()} ms`); } }); } diff --git a/src/hooks/tapErrorToLogMessage.ts b/src/hooks/tapErrorToLogMessage.ts index 0415dad2..c914b026 100644 --- a/src/hooks/tapErrorToLogMessage.ts +++ b/src/hooks/tapErrorToLogMessage.ts @@ -2,7 +2,7 @@ import chalk from 'chalk'; import type webpack from 'webpack'; import type { ForkTsCheckerWebpackPluginConfiguration } from '../ForkTsCheckerWebpackPluginConfiguration'; -import { RpcIpcMessagePortClosedError } from '../rpc/rpc-ipc/error/RpcIpcMessagePortClosedError'; +import { RpcExitError } from '../utils/rpc'; import { getForkTsCheckerWebpackPluginHooks } from './pluginHooks'; @@ -15,7 +15,7 @@ function tapErrorToLogMessage( hooks.error.tap('ForkTsCheckerWebpackPlugin', (error) => { configuration.logger.issues.error(String(error)); - if (error instanceof RpcIpcMessagePortClosedError) { + if (error instanceof RpcExitError) { if (error.signal === 'SIGINT') { configuration.logger.issues.error( chalk.red( diff --git a/src/hooks/tapStartToConnectAndRunReporter.ts b/src/hooks/tapStartToConnectAndRunReporter.ts deleted file mode 100644 index db9390bc..00000000 --- a/src/hooks/tapStartToConnectAndRunReporter.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type webpack from 'webpack'; - -import { OperationCanceledError } from '../error/OperationCanceledError'; -import type { ForkTsCheckerWebpackPluginConfiguration } from '../ForkTsCheckerWebpackPluginConfiguration'; -import type { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState'; -import type { Issue } from '../issue'; -import type { FilesMatch, FilesChange, ReporterRpcClient } from '../reporter'; -import { getFilesChange } from '../reporter'; - -import { interceptDoneToGetWebpackDevServerTap } from './interceptDoneToGetWebpackDevServerTap'; -import { getForkTsCheckerWebpackPluginHooks } from './pluginHooks'; -import { dependenciesPool, issuesPool } from './pluginPools'; -import { tapAfterCompileToGetIssues } from './tapAfterCompileToGetIssues'; -import { tapDoneToAsyncGetIssues } from './tapDoneToAsyncGetIssues'; - -function tapStartToConnectAndRunReporter( - compiler: webpack.Compiler, - issuesReporter: ReporterRpcClient, - dependenciesReporter: ReporterRpcClient, - configuration: ForkTsCheckerWebpackPluginConfiguration, - state: ForkTsCheckerWebpackPluginState -) { - const hooks = getForkTsCheckerWebpackPluginHooks(compiler); - - compiler.hooks.run.tap('ForkTsCheckerWebpackPlugin', () => { - if (!state.initialized) { - state.initialized = true; - - state.watching = false; - tapAfterCompileToGetIssues(compiler, configuration, state); - } - }); - - compiler.hooks.watchRun.tap('ForkTsCheckerWebpackPlugin', async () => { - if (!state.initialized) { - state.initialized = true; - - state.watching = true; - if (configuration.async) { - tapDoneToAsyncGetIssues(compiler, configuration, state); - interceptDoneToGetWebpackDevServerTap(compiler, configuration, state); - } else { - tapAfterCompileToGetIssues(compiler, configuration, state); - } - } - }); - - compiler.hooks.compilation.tap('ForkTsCheckerWebpackPlugin', async (compilation) => { - if (compilation.compiler !== compiler) { - // run only for the compiler that the plugin was registered for - return; - } - - let change: FilesChange = {}; - - if (state.watching) { - change = getFilesChange(compiler); - - configuration.logger.infrastructure.log( - [ - 'Calling reporter service for incremental check.', - ` Changed files: ${JSON.stringify(change.changedFiles)}`, - ` Deleted files: ${JSON.stringify(change.deletedFiles)}`, - ].join('\n') - ); - } else { - configuration.logger.infrastructure.log('Calling reporter service for single check.'); - } - - let resolveDependencies: (dependencies: FilesMatch | undefined) => void; - let rejectDependencies: (error: Error) => void; - let resolveIssues: (issues: Issue[] | undefined) => void; - let rejectIssues: (error: Error) => void; - - state.dependenciesPromise = new Promise((resolve, reject) => { - resolveDependencies = resolve; - rejectDependencies = reject; - }); - state.issuesPromise = new Promise((resolve, reject) => { - resolveIssues = resolve; - rejectIssues = reject; - }); - const previousIssuesReportPromise = state.issuesReportPromise; - const previousDependenciesReportPromise = state.dependenciesReportPromise; - - change = await hooks.start.promise(change, compilation); - - state.issuesReportPromise = issuesPool.submit( - (done) => - // eslint-disable-next-line no-async-promise-executor - new Promise(async (resolve) => { - try { - await issuesReporter.connect(); - - const previousReport = await previousIssuesReportPromise; - if (previousReport) { - await previousReport.close(); - } - - const report = await issuesReporter.getReport(change, state.watching); - resolve(report); - - report.getIssues().then(resolveIssues).catch(rejectIssues).finally(done); - } catch (error) { - if (error instanceof OperationCanceledError) { - hooks.canceled.call(compilation); - } else { - hooks.error.call(error, compilation); - } - - resolve(undefined); - resolveIssues(undefined); - done(); - } - }) - ); - state.dependenciesReportPromise = dependenciesPool.submit( - (done) => - // eslint-disable-next-line no-async-promise-executor - new Promise(async (resolve) => { - try { - await dependenciesReporter.connect(); - - const previousReport = await previousDependenciesReportPromise; - if (previousReport) { - await previousReport.close(); - } - - const report = await dependenciesReporter.getReport(change, state.watching); - resolve(report); - - report - .getDependencies() - .then(resolveDependencies) - .catch(rejectDependencies) - .finally(done); - } catch (error) { - if (error instanceof OperationCanceledError) { - hooks.canceled.call(compilation); - } else { - hooks.error.call(error, compilation); - } - - resolve(undefined); - resolveDependencies(undefined); - done(); - } - }) - ); - }); -} - -export { tapStartToConnectAndRunReporter }; diff --git a/src/hooks/tapStartToRunWorkers.ts b/src/hooks/tapStartToRunWorkers.ts new file mode 100644 index 00000000..a26f111d --- /dev/null +++ b/src/hooks/tapStartToRunWorkers.ts @@ -0,0 +1,108 @@ +import type * as webpack from 'webpack'; + +import type { FilesChange } from '../files-change'; +import { getFilesChange } from '../files-change'; +import type { ForkTsCheckerWebpackPluginConfiguration } from '../ForkTsCheckerWebpackPluginConfiguration'; +import type { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState'; +import { getInfrastructureLogger } from '../infrastructure-logger'; +import type { GetDependenciesWorker } from '../typescript/worker/get-dependencies-worker'; +import type { GetIssuesWorker } from '../typescript/worker/get-issues-worker'; +import type { RpcWorker } from '../utils/rpc'; + +import { interceptDoneToGetWebpackDevServerTap } from './interceptDoneToGetWebpackDevServerTap'; +import { getForkTsCheckerWebpackPluginHooks } from './pluginHooks'; +import { dependenciesPool, issuesPool } from './pluginPools'; +import { tapAfterCompileToGetIssues } from './tapAfterCompileToGetIssues'; +import { tapDoneToAsyncGetIssues } from './tapDoneToAsyncGetIssues'; + +function tapStartToRunWorkers( + compiler: webpack.Compiler, + getIssuesWorker: RpcWorker, + getDependenciesWorker: RpcWorker, + config: ForkTsCheckerWebpackPluginConfiguration, + state: ForkTsCheckerWebpackPluginState +) { + const hooks = getForkTsCheckerWebpackPluginHooks(compiler); + const { log, debug } = getInfrastructureLogger(compiler); + + compiler.hooks.run.tap('ForkTsCheckerWebpackPlugin', () => { + if (!state.initialized) { + debug('Initializing plugin for single run (not async).'); + state.initialized = true; + + state.watching = false; + tapAfterCompileToGetIssues(compiler, config, state); + } + }); + + compiler.hooks.watchRun.tap('ForkTsCheckerWebpackPlugin', async () => { + if (!state.initialized) { + state.initialized = true; + + state.watching = true; + if (config.async) { + debug('Initializing plugin for watch run (async).'); + + tapDoneToAsyncGetIssues(compiler, config, state); + interceptDoneToGetWebpackDevServerTap(compiler, config, state); + } else { + debug('Initializing plugin for watch run (not async).'); + + tapAfterCompileToGetIssues(compiler, config, state); + } + } + }); + + compiler.hooks.compilation.tap('ForkTsCheckerWebpackPlugin', async (compilation) => { + if (compilation.compiler !== compiler) { + // run only for the compiler that the plugin was registered for + return; + } + + const iteration = ++state.iteration; + + let change: FilesChange = {}; + + if (state.watching) { + change = getFilesChange(compiler); + log( + [ + 'Calling reporter service for incremental check.', + ` Changed files: ${JSON.stringify(change.changedFiles)}`, + ` Deleted files: ${JSON.stringify(change.deletedFiles)}`, + ].join('\n') + ); + } else { + log('Calling reporter service for single check.'); + } + + change = await hooks.start.promise(change, compilation); + + debug(`Submitting the getIssuesWorker to the pool, iteration ${iteration}.`); + state.issuesPromise = issuesPool.submit(async () => { + try { + debug(`Running the getIssuesWorker, iteration ${iteration}.`); + return await getIssuesWorker(change, state.watching); + } catch (error) { + hooks.error.call(error, compilation); + return undefined; + } finally { + debug(`The getIssuesWorker finished its job, iteration ${iteration}.`); + } + }); + debug(`Submitting the getDependenciesWorker to the pool, iteration ${iteration}.`); + state.dependenciesPromise = dependenciesPool.submit(async () => { + try { + debug(`Running the getDependenciesWorker, iteration ${iteration}.`); + return await getDependenciesWorker(change); + } catch (error) { + hooks.error.call(error, compilation); + return undefined; + } finally { + debug(`The getDependenciesWorker finished its job, iteration ${iteration}.`); + } + }); + }); +} + +export { tapStartToRunWorkers }; diff --git a/src/hooks/tapStopToDisconnectReporter.ts b/src/hooks/tapStopToDisconnectReporter.ts deleted file mode 100644 index 149e176e..00000000 --- a/src/hooks/tapStopToDisconnectReporter.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type webpack from 'webpack'; - -import type { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState'; -import type { ReporterRpcClient } from '../reporter'; - -function tapStopToDisconnectReporter( - compiler: webpack.Compiler, - issuesReporter: ReporterRpcClient, - dependenciesReporter: ReporterRpcClient, - state: ForkTsCheckerWebpackPluginState -) { - compiler.hooks.watchClose.tap('ForkTsCheckerWebpackPlugin', () => { - issuesReporter.disconnect(); - dependenciesReporter.disconnect(); - }); - - compiler.hooks.done.tap('ForkTsCheckerWebpackPlugin', async () => { - if (!state.watching) { - await Promise.all([issuesReporter.disconnect(), dependenciesReporter.disconnect()]); - } - }); - - compiler.hooks.failed.tap('ForkTsCheckerWebpackPlugin', () => { - if (!state.watching) { - issuesReporter.disconnect(); - dependenciesReporter.disconnect(); - } - }); -} - -export { tapStopToDisconnectReporter }; diff --git a/src/hooks/tapStopToTerminateWorkers.ts b/src/hooks/tapStopToTerminateWorkers.ts new file mode 100644 index 00000000..39961ac9 --- /dev/null +++ b/src/hooks/tapStopToTerminateWorkers.ts @@ -0,0 +1,38 @@ +import type webpack from 'webpack'; + +import type { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState'; +import { getInfrastructureLogger } from '../infrastructure-logger'; +import type { RpcWorker } from '../utils/rpc'; + +function tapStopToTerminateWorkers( + compiler: webpack.Compiler, + getIssuesWorker: RpcWorker, + getDependenciesWorker: RpcWorker, + state: ForkTsCheckerWebpackPluginState +) { + const { debug } = getInfrastructureLogger(compiler); + + const terminateWorkers = () => { + debug('Compiler is going to close - terminating workers...'); + getIssuesWorker.terminate(); + getDependenciesWorker.terminate(); + }; + + compiler.hooks.watchClose.tap('ForkTsCheckerWebpackPlugin', () => { + terminateWorkers(); + }); + + compiler.hooks.done.tap('ForkTsCheckerWebpackPlugin', () => { + if (!state.watching) { + terminateWorkers(); + } + }); + + compiler.hooks.failed.tap('ForkTsCheckerWebpackPlugin', () => { + if (!state.watching) { + terminateWorkers(); + } + }); +} + +export { tapStopToTerminateWorkers }; diff --git a/src/infrastructure-logger.ts b/src/infrastructure-logger.ts new file mode 100644 index 00000000..c9b3e688 --- /dev/null +++ b/src/infrastructure-logger.ts @@ -0,0 +1,22 @@ +import type webpack from 'webpack'; + +interface InfrastructureLogger { + log(...args: unknown[]): void; + debug(...args: unknown[]): void; + error(...args: unknown[]): void; + warn(...args: unknown[]): void; + info(...args: unknown[]): void; +} + +export function getInfrastructureLogger(compiler: webpack.Compiler): InfrastructureLogger { + const logger = compiler.getInfrastructureLogger('ForkTsCheckerWebpackPlugin'); + console.log(compiler.infrastructureLogger); + + return { + log: logger.log.bind(logger), + debug: logger.debug.bind(logger), + error: logger.error.bind(logger), + warn: logger.warn.bind(logger), + info: logger.info.bind(logger), + }; +} diff --git a/src/profile/Performance.ts b/src/profile/Performance.ts deleted file mode 100644 index a37a2e6e..00000000 --- a/src/profile/Performance.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { performance } from 'perf_hooks'; - -interface Performance { - enable(): void; - disable(): void; - mark(name: string): void; - markStart(name: string): void; - markEnd(name: string): void; - measure(name: string, startMark?: string, endMark?: string): void; - print(): void; -} - -function createPerformance(): Performance { - let enabled = false; - let timeOrigin: number; - let marks: Map; - let measurements: Map; - - function enable() { - enabled = true; - marks = new Map(); - measurements = new Map(); - timeOrigin = performance.now(); - } - - function disable() { - enabled = false; - } - - function mark(name: string) { - if (enabled) { - marks.set(name, performance.now()); - } - } - - function measure(name: string, startMark?: string, endMark?: string) { - if (enabled) { - const start = (startMark && marks.get(startMark)) || timeOrigin; - const end = (endMark && marks.get(endMark)) || performance.now(); - - measurements.set(name, (measurements.get(name) || 0) + (end - start)); - } - } - - function markStart(name: string) { - if (enabled) { - mark(`${name} start`); - } - } - - function markEnd(name: string) { - if (enabled) { - mark(`${name} end`); - measure(name, `${name} start`, `${name} end`); - } - } - - function formatName(name: string, width = 0) { - return `${name}:`.padEnd(width); - } - - function formatDuration(duration: number, width = 0) { - return `${(duration / 1000).toFixed(2)} s`.padStart(width); - } - - function print() { - if (enabled) { - let nameWidth = 0; - let durationWidth = 0; - - measurements.forEach((duration, name) => { - nameWidth = Math.max(nameWidth, formatName(name).length); - durationWidth = Math.max(durationWidth, formatDuration(duration).length); - }); - - measurements.forEach((duration, name) => { - console.log(`${formatName(name, nameWidth)} ${formatDuration(duration, durationWidth)}`); - }); - } - } - - return { enable, disable, mark, markStart, markEnd, measure, print }; -} - -export { Performance, createPerformance }; diff --git a/src/reporter/AggregatedReporter.ts b/src/reporter/AggregatedReporter.ts deleted file mode 100644 index 1d53fe3f..00000000 --- a/src/reporter/AggregatedReporter.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { OperationCanceledError } from '../error/OperationCanceledError'; - -import type { FilesChange } from './FilesChange'; -import { aggregateFilesChanges } from './FilesChange'; -import type { Reporter } from './Reporter'; - -/** - * This higher order reporter aggregates too frequent getReport requests to avoid unnecessary computation. - */ -function createAggregatedReporter(reporter: TReporter): TReporter { - let pendingPromise: Promise | undefined; - let queuedIndex = 0; - let queuedChanges: FilesChange[] = []; - - const aggregatedReporter: TReporter = { - ...reporter, - getReport: async (change, watching) => { - if (!pendingPromise) { - let resolvePending: () => void; - pendingPromise = new Promise((resolve) => { - resolvePending = () => { - resolve(undefined); - pendingPromise = undefined; - }; - }); - - return reporter - .getReport(change, watching) - .then((report) => ({ - ...report, - async close() { - await report.close(); - resolvePending(); - }, - })) - .catch((error) => { - resolvePending(); - - throw error; - }); - } else { - const currentIndex = ++queuedIndex; - queuedChanges.push(change); - - return pendingPromise.then(() => { - if (queuedIndex === currentIndex) { - const change = aggregateFilesChanges(queuedChanges); - queuedChanges = []; - - return aggregatedReporter.getReport(change, watching); - } else { - throw new OperationCanceledError('getReport canceled - new report requested.'); - } - }); - } - }, - }; - - return aggregatedReporter; -} - -export { createAggregatedReporter }; diff --git a/src/reporter/Report.ts b/src/reporter/Report.ts deleted file mode 100644 index a78c0e9c..00000000 --- a/src/reporter/Report.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Issue } from '../issue'; - -import type { FilesMatch } from './FilesMatch'; - -interface Report { - getDependencies(): Promise; - getIssues(): Promise; - close(): Promise; -} - -export { Report }; diff --git a/src/reporter/Reporter.ts b/src/reporter/Reporter.ts deleted file mode 100644 index bcead3f6..00000000 --- a/src/reporter/Reporter.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { FilesChange } from './FilesChange'; -import type { Report } from './Report'; - -interface Reporter { - getReport(change: FilesChange, watching: boolean): Promise; -} - -export { Reporter }; diff --git a/src/reporter/index.ts b/src/reporter/index.ts deleted file mode 100644 index e79c585d..00000000 --- a/src/reporter/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './Report'; -export * from './Reporter'; -export * from './AggregatedReporter'; - -export * from './FilesChange'; -export * from './FilesMatch'; - -export * from './reporter-rpc/ReporterRpcClient'; -export * from './reporter-rpc/ReporterRpcService'; diff --git a/src/reporter/reporter-rpc/ReporterRpcClient.ts b/src/reporter/reporter-rpc/ReporterRpcClient.ts deleted file mode 100644 index af74d761..00000000 --- a/src/reporter/reporter-rpc/ReporterRpcClient.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { RpcMessageChannel } from '../../rpc'; -import { createRpcClient } from '../../rpc'; -import flatten from '../../utils/array/flatten'; -import type { FilesChange } from '../FilesChange'; -import type { Reporter } from '../Reporter'; - -import { - configure, - getReport, - getDependencies, - getIssues, - closeReport, -} from './ReporterRpcProcedure'; - -interface ReporterRpcClient extends Reporter { - isConnected: () => boolean; - connect: () => Promise; - disconnect: () => Promise; -} - -// suppressing because it will be removed anyway -// eslint-disable-next-line @typescript-eslint/ban-types -function createReporterRpcClient( - channel: RpcMessageChannel, - configuration: TConfiguration -): ReporterRpcClient { - const rpcClient = createRpcClient(channel.clientPort); - - return { - isConnected: () => channel.isOpen() && rpcClient.isConnected(), - connect: async () => { - if (!channel.isOpen()) { - await channel.open(); - } - if (!rpcClient.isConnected()) { - try { - await rpcClient.connect(); - await rpcClient.dispatchCall(configure, configuration); - } catch (error) { - // connect or configure was not successful - - // close the reporter and re-throw an error - await rpcClient.disconnect(); - await channel.close(); - throw error; - } - } - }, - disconnect: async () => { - if (rpcClient.isConnected()) { - await rpcClient.disconnect(); - } - if (channel.isOpen()) { - await channel.close(); - } - }, - getReport: async (change, watching) => { - const reportId = await rpcClient.dispatchCall(getReport, { change, watching }); - - return { - getDependencies() { - return rpcClient.dispatchCall(getDependencies, reportId); - }, - getIssues() { - return rpcClient.dispatchCall(getIssues, reportId); - }, - close() { - return rpcClient.dispatchCall(closeReport, reportId); - }, - }; - }, - }; -} - -function composeReporterRpcClients(clients: ReporterRpcClient[]): ReporterRpcClient { - return { - isConnected: () => clients.every((client) => client.isConnected()), - connect: () => Promise.all(clients.map((client) => client.connect())).then(() => undefined), - disconnect: () => - Promise.all(clients.map((client) => client.disconnect())).then(() => undefined), - getReport: (change: FilesChange, watching: boolean) => - Promise.all(clients.map((client) => client.getReport(change, watching))).then((reports) => ({ - getDependencies: () => - Promise.all(reports.map((report) => report.getDependencies())).then((dependencies) => - dependencies.reduce( - (mergedDependencies, singleDependencies) => ({ - files: Array.from( - new Set([...mergedDependencies.files, ...singleDependencies.files]) - ), - dirs: Array.from(new Set([...mergedDependencies.dirs, ...singleDependencies.dirs])), - excluded: Array.from( - new Set([...mergedDependencies.excluded, ...singleDependencies.excluded]) - ), - extensions: Array.from( - new Set([...mergedDependencies.extensions, ...singleDependencies.extensions]) - ), - }), - { files: [], dirs: [], excluded: [], extensions: [] } - ) - ), - getIssues: () => - Promise.all(reports.map((report) => report.getIssues())).then((issues) => - flatten(issues) - ), - close: () => Promise.all(reports.map((report) => report.close())).then(() => undefined), - })), - }; -} - -export { ReporterRpcClient, createReporterRpcClient, composeReporterRpcClients }; diff --git a/src/reporter/reporter-rpc/ReporterRpcProcedure.ts b/src/reporter/reporter-rpc/ReporterRpcProcedure.ts deleted file mode 100644 index 70c044d0..00000000 --- a/src/reporter/reporter-rpc/ReporterRpcProcedure.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Issue } from '../../issue'; -import type { RpcProcedure } from '../../rpc'; -import type { FilesChange } from '../FilesChange'; -import type { FilesMatch } from '../FilesMatch'; - -// suppressing because it will be removed anyway -// eslint-disable-next-line @typescript-eslint/ban-types -const configure: RpcProcedure = 'configure'; -const getReport: RpcProcedure<{ change: FilesChange; watching: boolean }, void> = 'getReport'; -const getDependencies: RpcProcedure = 'getDependencies'; -const getIssues: RpcProcedure = 'getIssues'; -const closeReport: RpcProcedure = 'closeReport'; - -export { configure, getReport, getDependencies, getIssues, closeReport }; diff --git a/src/reporter/reporter-rpc/ReporterRpcService.ts b/src/reporter/reporter-rpc/ReporterRpcService.ts deleted file mode 100644 index 71d6c4cd..00000000 --- a/src/reporter/reporter-rpc/ReporterRpcService.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { RpcMessagePort } from '../../rpc'; -import { createRpcService } from '../../rpc'; -import type { Report } from '../Report'; -import type { Reporter } from '../Reporter'; - -import { - configure, - getReport, - getDependencies, - getIssues, - closeReport, -} from './ReporterRpcProcedure'; - -interface ReporterRpcService { - isOpen: () => boolean; - open: () => Promise; - close: () => Promise; -} - -// suppressing because it will be removed anyway -// eslint-disable-next-line @typescript-eslint/ban-types -function registerReporterRpcService( - servicePort: RpcMessagePort, - reporterFactory: (configuration: TConfiguration) => Reporter -): ReporterRpcService { - const rpcService = createRpcService(servicePort); - let reporterRegistered = false; - let report: Report | undefined = undefined; - - const registerReporter = () => { - rpcService.addCallHandler(configure, async (configuration: TConfiguration) => { - rpcService.removeCallHandler(configure); - - const reporter = reporterFactory(configuration); - - rpcService.addCallHandler(getReport, async ({ change, watching }) => { - if (report) { - throw new Error(`Close previous report before opening the next one.`); - } - - report = await reporter.getReport(change, watching); - }); - rpcService.addCallHandler(getDependencies, () => { - if (!report) { - throw new Error(`Cannot find active report.`); - } - - return report.getDependencies(); - }); - rpcService.addCallHandler(getIssues, () => { - if (!report) { - throw new Error(`Cannot find active report.`); - } - - return report.getIssues(); - }); - rpcService.addCallHandler(closeReport, async () => { - report = undefined; - }); - }); - }; - const unregisterReporter = () => { - rpcService.removeCallHandler(configure); - rpcService.removeCallHandler(getReport); - rpcService.removeCallHandler(getDependencies); - rpcService.removeCallHandler(getIssues); - rpcService.removeCallHandler(closeReport); - }; - - return { - isOpen: () => rpcService.isOpen() && reporterRegistered, - open: async () => { - if (!rpcService.isOpen()) { - await rpcService.open(); - } - - if (!reporterRegistered) { - registerReporter(); - reporterRegistered = true; - } - }, - close: async () => { - if (reporterRegistered) { - unregisterReporter(); - reporterRegistered = false; - } - - if (rpcService.isOpen()) { - await rpcService.close(); - } - }, - }; -} - -export { ReporterRpcService, registerReporterRpcService }; diff --git a/src/rpc/RpcClient.ts b/src/rpc/RpcClient.ts deleted file mode 100644 index d2ba68f0..00000000 --- a/src/rpc/RpcClient.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { RpcRemoteError } from './error/RpcRemoteError'; -import { - createRpcCall, - getRpcMessageKey, - isRpcReturnMessage, - isRpcThrowMessage, -} from './RpcMessage'; -import type { RpcMessagePort } from './RpcMessagePort'; -import type { RpcProcedure, RpcProcedurePayload, RpcProcedureResult } from './RpcProcedure'; - -interface RpcClient { - readonly isConnected: () => boolean; - readonly connect: () => Promise; - readonly disconnect: () => Promise; - readonly dispatchCall: ( - procedure: TProcedure, - payload: RpcProcedurePayload - ) => Promise>; -} - -/* eslint-disable @typescript-eslint/no-explicit-any */ -type RpcCallback = { - return: (result: TResult) => void; - throw: (error: any) => void; -}; -/* eslint-enable @typescript-eslint/no-explicit-any */ - -function createRpcClient(port: RpcMessagePort): RpcClient { - let callIndex = 0; - const callbacks = new Map(); - let isListenerRegistered = false; - - const returnOrThrowListener = async (message: unknown) => { - if (isRpcReturnMessage(message)) { - const key = getRpcMessageKey(message); - const callback = callbacks.get(key); - - if (callback) { - callback.return(message.payload); - callbacks.delete(key); - } - } - if (isRpcThrowMessage(message)) { - const key = getRpcMessageKey(message); - const callback = callbacks.get(key); - - if (callback) { - callback.throw(new RpcRemoteError(message.payload.message, message.payload.stack)); - callbacks.delete(key); - } - } - }; - - const errorListener = async (error: Error) => { - callbacks.forEach((callback, key) => { - callback.throw(error); - callbacks.delete(key); - }); - }; - - return { - isConnected: () => port.isOpen() && isListenerRegistered, - connect: async () => { - if (!port.isOpen()) { - await port.open(); - } - - if (!isListenerRegistered) { - port.addMessageListener(returnOrThrowListener); - port.addErrorListener(errorListener); - isListenerRegistered = true; - } - }, - disconnect: async () => { - if (isListenerRegistered) { - port.removeMessageListener(returnOrThrowListener); - port.removeErrorListener(errorListener); - isListenerRegistered = false; - } - - if (port.isOpen()) { - await port.close(); - } - }, - dispatchCall: async (procedure, payload) => - new Promise((resolve, reject) => { - const call = createRpcCall(procedure, callIndex++, payload); - const key = getRpcMessageKey(call); - - callbacks.set(key, { return: resolve, throw: reject }); - - port.dispatchMessage(call).catch((error) => { - callbacks.delete(key); - reject(error); - }); - }), - }; -} - -export { RpcClient, createRpcClient }; diff --git a/src/rpc/RpcHost.ts b/src/rpc/RpcHost.ts deleted file mode 100644 index c094e309..00000000 --- a/src/rpc/RpcHost.ts +++ /dev/null @@ -1,8 +0,0 @@ -type RpcDispatcher = (message: TMessage) => Promise; - -interface RpcHost { - dispatch: RpcDispatcher; - register: (dispatch: RpcDispatcher) => void; -} - -export { RpcHost, RpcDispatcher }; diff --git a/src/rpc/RpcMessage.ts b/src/rpc/RpcMessage.ts deleted file mode 100644 index bf9367b2..00000000 --- a/src/rpc/RpcMessage.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { RpcProcedure, RpcProcedurePayload, RpcProcedureResult } from './RpcProcedure'; - -interface RpcMessage< - TType extends string = string, - TProcedure extends RpcProcedure = RpcProcedure, - TPayload = unknown -> { - rpc: true; - type: TType; - procedure: TProcedure; - id: number; - payload: TPayload; - source?: string; -} -interface RpcRemoteError { - message: string; - stack?: string; -} -type RpcCall = RpcMessage< - 'call', - TProcedure, - RpcProcedurePayload ->; -type RpcReturn = RpcMessage< - 'return', - TProcedure, - RpcProcedureResult ->; -type RpcThrow = RpcMessage<'throw', TProcedure, RpcRemoteError>; - -function createRpcMessage< - TType extends string = string, - TProcedure extends RpcProcedure = RpcProcedure, - TPayload = unknown ->( - procedure: TProcedure, - id: number, - type: TType, - payload: TPayload, - source?: string -): RpcMessage { - return { - rpc: true, - type, - id, - procedure, - payload, - source, - }; -} - -function createRpcCall( - procedure: TProcedure, - index: number, - payload: RpcProcedurePayload -): RpcCall { - return createRpcMessage(procedure, index, 'call', payload); -} - -function createRpcReturn( - procedure: TProcedure, - index: number, - payload: RpcProcedureResult -): RpcReturn { - return createRpcMessage(procedure, index, 'return', payload); -} - -// suppressing as it will be removed anyway -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function createRpcThrow( - procedure: TProcedure, - index: number, - payload: RpcRemoteError -): RpcThrow { - return createRpcMessage(procedure, index, 'throw', payload); -} - -function isRpcMessage< - TType extends string = string, - TProcedure extends RpcProcedure = RpcProcedure ->(candidate: unknown): candidate is RpcMessage { - return !!(typeof candidate === 'object' && candidate && (candidate as { rpc: boolean }).rpc); -} - -function isRpcCallMessage< - // suppressing as it will be removed anyway - // eslint-disable-next-line @typescript-eslint/no-unused-vars - TType extends string = string, - TProcedure extends RpcProcedure = RpcProcedure ->(candidate: unknown): candidate is RpcCall { - return isRpcMessage(candidate) && candidate.type === 'call'; -} - -function isRpcReturnMessage< - // suppressing as it will be removed anyway - // eslint-disable-next-line @typescript-eslint/no-unused-vars - TType extends string = string, - TProcedure extends RpcProcedure = RpcProcedure ->(candidate: unknown): candidate is RpcReturn { - return isRpcMessage(candidate) && candidate.type === 'return'; -} - -function isRpcThrowMessage< - // suppressing as it will be removed anyway - // eslint-disable-next-line @typescript-eslint/no-unused-vars - TType extends string = string, - TProcedure extends RpcProcedure = RpcProcedure ->(candidate: unknown): candidate is RpcThrow { - return isRpcMessage(candidate) && candidate.type === 'throw'; -} - -function getRpcMessageKey(message: RpcMessage) { - return `${message.procedure}_${message.id}`; -} - -export { - RpcMessage, - RpcCall, - RpcReturn, - RpcThrow, - createRpcMessage, - createRpcCall, - createRpcReturn, - createRpcThrow, - isRpcMessage, - isRpcCallMessage, - isRpcReturnMessage, - isRpcThrowMessage, - getRpcMessageKey, -}; diff --git a/src/rpc/RpcMessageChannel.ts b/src/rpc/RpcMessageChannel.ts deleted file mode 100644 index af7b4779..00000000 --- a/src/rpc/RpcMessageChannel.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { RpcMessagePort } from './RpcMessagePort'; - -interface RpcMessageChannel { - readonly servicePort: RpcMessagePort; - readonly clientPort: RpcMessagePort; - readonly isOpen: () => boolean; - readonly open: () => Promise; - readonly close: () => Promise; -} - -function createRpcMessageChannel( - servicePort: RpcMessagePort, - clientPort: RpcMessagePort, - linkPorts?: () => Promise, - unlinkPorts?: () => Promise -): RpcMessageChannel { - // if there is not link and unlink function provided, we assume that channel is automatically linked - let arePortsLinked = !linkPorts && !unlinkPorts; - - return { - servicePort, - clientPort, - isOpen: () => servicePort.isOpen() && clientPort.isOpen() && arePortsLinked, - open: async () => { - if (!servicePort.isOpen()) { - await servicePort.open(); - } - if (!clientPort.isOpen()) { - await clientPort.open(); - } - if (!arePortsLinked) { - if (linkPorts) { - await linkPorts(); - } - arePortsLinked = true; - } - }, - close: async () => { - if (arePortsLinked) { - if (unlinkPorts) { - await unlinkPorts(); - } - arePortsLinked = false; - } - if (servicePort.isOpen()) { - await servicePort.close(); - } - if (clientPort.isOpen()) { - await clientPort.close(); - } - }, - }; -} - -export { RpcMessageChannel, createRpcMessageChannel }; diff --git a/src/rpc/RpcMessagePort.ts b/src/rpc/RpcMessagePort.ts deleted file mode 100644 index 2e4167d6..00000000 --- a/src/rpc/RpcMessagePort.ts +++ /dev/null @@ -1,16 +0,0 @@ -type RpcMessageDispatch = (message: TMessage) => Promise; -type RpcMessageListener = RpcMessageDispatch; -type RpcErrorListener = (error: Error) => void; - -interface RpcMessagePort { - readonly dispatchMessage: RpcMessageDispatch; - readonly addMessageListener: (listener: RpcMessageListener) => void; - readonly removeMessageListener: (listener: RpcMessageListener) => void; - readonly addErrorListener: (listener: RpcErrorListener) => void; - readonly removeErrorListener: (listener: RpcErrorListener) => void; - readonly isOpen: () => boolean; - readonly open: () => Promise; - readonly close: () => Promise; -} - -export { RpcMessagePort, RpcMessageDispatch, RpcMessageListener, RpcErrorListener }; diff --git a/src/rpc/RpcProcedure.ts b/src/rpc/RpcProcedure.ts deleted file mode 100644 index c96f7af0..00000000 --- a/src/rpc/RpcProcedure.ts +++ /dev/null @@ -1,20 +0,0 @@ -// suppressing as it will be removed anyway -// eslint-disable-next-line @typescript-eslint/no-empty-interface,@typescript-eslint/no-unused-vars -interface RpcProcedure extends String {} - -type RpcProcedurePayload = TProcedure extends RpcProcedure< - infer TPayload, - // suppressing as it will be removed anyway - // eslint-disable-next-line @typescript-eslint/no-unused-vars - infer TResult -> - ? TPayload - : never; - -// suppressing as it will be removed anyway -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type RpcProcedureResult = TProcedure extends RpcProcedure - ? TResult - : never; - -export { RpcProcedure, RpcProcedurePayload, RpcProcedureResult }; diff --git a/src/rpc/RpcService.ts b/src/rpc/RpcService.ts deleted file mode 100644 index 34df444c..00000000 --- a/src/rpc/RpcService.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { createRpcReturn, createRpcThrow, isRpcCallMessage } from './RpcMessage'; -import type { RpcMessagePort } from './RpcMessagePort'; -import type { RpcProcedure } from './RpcProcedure'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type RpcCallHandler = (payload: TPayload) => Promise; - -interface RpcService { - readonly isOpen: () => boolean; - readonly open: () => Promise; - readonly close: () => Promise; - readonly addCallHandler: ( - procedure: RpcProcedure, - handler: RpcCallHandler - ) => void; - readonly removeCallHandler: ( - procedure: RpcProcedure - ) => void; -} - -function createRpcService(port: RpcMessagePort): RpcService { - const handlers = new Map(); - let isListenerRegistered = false; - - const callListener = async (message: unknown) => { - if (isRpcCallMessage(message)) { - const handler = handlers.get(message.procedure); - - try { - if (!handler) { - throw new Error(`No handler found for procedure ${message.procedure}.`); - } - - const result = await handler(message.payload); - - await port.dispatchMessage(createRpcReturn(message.procedure, message.id, result)); - } catch (error) { - await port.dispatchMessage( - createRpcThrow(message.procedure, message.id, { - message: error.toString(), - stack: error.stack, - }) - ); - } - } - }; - - return { - isOpen: () => port.isOpen() && isListenerRegistered, - open: async () => { - if (!port.isOpen()) { - await port.open(); - } - - if (!isListenerRegistered) { - port.addMessageListener(callListener); - isListenerRegistered = true; - } - }, - close: async () => { - if (isListenerRegistered) { - port.removeMessageListener(callListener); - isListenerRegistered = false; - } - - if (port.isOpen()) { - await port.close(); - } - }, - addCallHandler: (procedure, handler) => { - if (handlers.has(procedure)) { - throw new Error(`Handler for '${procedure}' procedure has been already registered`); - } - - handlers.set(procedure, handler); - }, - removeCallHandler: (procedure) => handlers.delete(procedure), - }; -} - -export { RpcService, createRpcService }; diff --git a/src/rpc/error/RpcMessagePortClosedError.ts b/src/rpc/error/RpcMessagePortClosedError.ts deleted file mode 100644 index 4c307dfb..00000000 --- a/src/rpc/error/RpcMessagePortClosedError.ts +++ /dev/null @@ -1,3 +0,0 @@ -class RpcMessagePortClosedError extends Error {} - -export { RpcMessagePortClosedError }; diff --git a/src/rpc/error/RpcRemoteError.ts b/src/rpc/error/RpcRemoteError.ts deleted file mode 100644 index 5d9a17ec..00000000 --- a/src/rpc/error/RpcRemoteError.ts +++ /dev/null @@ -1,15 +0,0 @@ -class RpcRemoteError extends Error { - constructor(message: string, readonly stack?: string) { - super(message); - } - - toString() { - if (this.stack) { - return [this.message, this.stack].join('\n'); - } else { - return this.message; - } - } -} - -export { RpcRemoteError }; diff --git a/src/rpc/index.ts b/src/rpc/index.ts deleted file mode 100644 index 6eb79e7b..00000000 --- a/src/rpc/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './RpcMessage'; -export * from './RpcProcedure'; -export * from './RpcClient'; -export * from './RpcHost'; -export * from './RpcService'; -export * from './RpcMessagePort'; -export * from './RpcMessageChannel'; diff --git a/src/rpc/rpc-ipc/ProcessLike.ts b/src/rpc/rpc-ipc/ProcessLike.ts deleted file mode 100644 index b5c2a47d..00000000 --- a/src/rpc/rpc-ipc/ProcessLike.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -/** - * This interface describes Node.js process that is potentially able to communicate over IPC channel - */ -interface ProcessLike { - pid?: string | number; - send?: ( - message: any, - sendHandle?: any, - options?: any, - callback?: (error: any) => void - ) => boolean; - on: (event: any, listener: any) => any; - off: (event: any, listener?: any) => any; - connected?: boolean; - disconnect?: () => void; -} - -export { ProcessLike }; diff --git a/src/rpc/rpc-ipc/RpcIpcMessageChannel.ts b/src/rpc/rpc-ipc/RpcIpcMessageChannel.ts deleted file mode 100644 index 8272a487..00000000 --- a/src/rpc/rpc-ipc/RpcIpcMessageChannel.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { RpcMessageChannel } from '../index'; -import { createRpcMessageChannel } from '../index'; - -import { createRpcIpcForkedProcessMessagePort } from './RpcIpcMessagePort'; - -function createRpcIpcMessageChannel(servicePath: string, memoryLimit = 2048): RpcMessageChannel { - const port = createRpcIpcForkedProcessMessagePort(servicePath, memoryLimit); - - // linked by the child_process IPC implementation - no manual linking needed - return createRpcMessageChannel(port, port); -} - -export { createRpcIpcMessageChannel }; diff --git a/src/rpc/rpc-ipc/RpcIpcMessagePort.ts b/src/rpc/rpc-ipc/RpcIpcMessagePort.ts deleted file mode 100644 index 6e88048d..00000000 --- a/src/rpc/rpc-ipc/RpcIpcMessagePort.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { ChildProcess } from 'child_process'; -import { fork } from 'child_process'; - -import type { RpcMessagePort, RpcMessageListener, RpcErrorListener } from '../index'; - -import { RpcIpcMessagePortClosedError } from './error/RpcIpcMessagePortClosedError'; -import type { ProcessLike } from './ProcessLike'; - -function createRpcIpcMessagePort(process: ProcessLike): RpcMessagePort { - const messageListeners = new Set(); - const errorListeners = new Set(); - let closedError: Error | undefined; - - const handleExit = async (code: string | number | null, signal: string | null) => { - closedError = new RpcIpcMessagePortClosedError( - code - ? `Process ${process.pid} exited with code "${code}" [${signal}]` - : `Process ${process.pid} exited [${signal}].`, - code, - signal - ); - errorListeners.forEach((listener) => { - if (closedError) { - listener(closedError); - } - }); - - await port.close(); - }; - const handleMessage = (message: unknown) => { - messageListeners.forEach((listener) => { - listener(message); - }); - }; - process.on('message', handleMessage); - process.on('exit', handleExit); - - const port: RpcMessagePort = { - dispatchMessage: async (message) => - new Promise((resolve, reject) => { - if (!process.connected) { - reject( - closedError || - new RpcIpcMessagePortClosedError( - `Process ${process.pid} doesn't have open IPC channels` - ) - ); - } - - if (process.send) { - process.send({ ...message, source: process.pid }, undefined, undefined, (sendError) => { - if (sendError) { - if (!closedError) { - closedError = new RpcIpcMessagePortClosedError( - `Cannot send the message - the message port has been closed for the process ${process.pid}.` - ); - } - reject(closedError); - } else { - resolve(); - } - }); - } else { - reject( - new RpcIpcMessagePortClosedError(`Process ${process.pid} doesn't have IPC channels`) - ); - } - }), - addMessageListener: (listener) => { - messageListeners.add(listener); - }, - removeMessageListener: (listener) => { - messageListeners.delete(listener); - }, - addErrorListener: (listener) => { - errorListeners.add(listener); - }, - removeErrorListener: (listener) => { - errorListeners.delete(listener); - }, - isOpen: () => !!process.connected, - open: async () => { - if (!process.connected || closedError) { - throw ( - closedError || - new RpcIpcMessagePortClosedError( - `Cannot open closed IPC channel for process ${process.pid}.` - ) - ); - } - }, - close: async () => { - process.off('message', handleMessage); - process.off('exit', handleExit); - - messageListeners.clear(); - errorListeners.clear(); - - if (process.disconnect && process.connected) { - process.disconnect(); - } - }, - }; - - return port; -} - -function createRpcIpcForkedProcessMessagePort( - filePath: string, - memoryLimit = 2048, - autoRecreate = true -): RpcMessagePort { - function createChildProcess(): ChildProcess { - return fork(filePath, [], { - execArgv: [`--max-old-space-size=${memoryLimit}`], - stdio: ['inherit', 'inherit', 'inherit', 'ipc'], - }); - } - const messageListeners = new Set(); - const errorListeners = new Set(); - - let childProcess: ChildProcess | undefined = createChildProcess(); - let port = createRpcIpcMessagePort(childProcess); - - return { - dispatchMessage: (message) => port.dispatchMessage(message), - addMessageListener: (listener) => { - messageListeners.add(listener); - return port.addMessageListener(listener); - }, - removeMessageListener: (listener) => { - messageListeners.delete(listener); - return port.removeMessageListener(listener); - }, - addErrorListener: (listener) => { - errorListeners.add(listener); - return port.addErrorListener(listener); - }, - removeErrorListener: (listener) => { - errorListeners.delete(listener); - return port.removeErrorListener(listener); - }, - isOpen: () => port.isOpen(), - open: async () => { - if (!port.isOpen() && autoRecreate) { - // recreate the process and add existing message listeners - childProcess = createChildProcess(); - port = createRpcIpcMessagePort(childProcess); - - messageListeners.forEach((listener) => { - port.addMessageListener(listener); - }); - errorListeners.forEach((listener) => { - port.addErrorListener(listener); - }); - } else { - return port.open(); - } - }, - close: async () => { - await port.close(); - - messageListeners.clear(); - errorListeners.clear(); - - if (childProcess) { - childProcess.kill('SIGTERM'); - childProcess = undefined; - } - }, - }; -} - -export { createRpcIpcMessagePort, createRpcIpcForkedProcessMessagePort }; diff --git a/src/rpc/rpc-ipc/error/RpcIpcMessagePortClosedError.ts b/src/rpc/rpc-ipc/error/RpcIpcMessagePortClosedError.ts deleted file mode 100644 index 5654bbfa..00000000 --- a/src/rpc/rpc-ipc/error/RpcIpcMessagePortClosedError.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { RpcMessagePortClosedError } from '../../error/RpcMessagePortClosedError'; - -class RpcIpcMessagePortClosedError extends RpcMessagePortClosedError { - constructor( - message: string, - readonly code?: string | number | null, - readonly signal?: string | null - ) { - super(message); - this.name = 'RpcIpcMessagePortClosedError'; - } -} - -export { RpcIpcMessagePortClosedError }; diff --git a/src/rpc/rpc-ipc/index.ts b/src/rpc/rpc-ipc/index.ts deleted file mode 100644 index fa16da56..00000000 --- a/src/rpc/rpc-ipc/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './RpcIpcMessagePort'; -export * from './RpcIpcMessageChannel'; diff --git a/src/typescript-reporter/file-system/MemFileSystem.ts b/src/typescript-reporter/file-system/MemFileSystem.ts deleted file mode 100644 index aab185a3..00000000 --- a/src/typescript-reporter/file-system/MemFileSystem.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { Dirent, Stats } from 'fs'; -import { dirname } from 'path'; - -import { fs as mem } from 'memfs'; - -import type { FileSystem } from './FileSystem'; -// eslint-disable-next-line node/no-unsupported-features/node-builtins - -/** - * It's an implementation of FileSystem interface which reads and writes to the in-memory file system. - * - * @param realFileSystem - */ -function createMemFileSystem(realFileSystem: FileSystem): FileSystem { - function exists(path: string): boolean { - return mem.existsSync(realFileSystem.normalizePath(path)); - } - - function readStats(path: string): Stats | undefined { - return exists(path) ? mem.statSync(realFileSystem.normalizePath(path)) : undefined; - } - - function readFile(path: string, encoding?: string): string | undefined { - const stats = readStats(path); - - if (stats && stats.isFile()) { - return mem - .readFileSync(realFileSystem.normalizePath(path), { encoding: encoding as BufferEncoding }) - .toString(); - } - } - - function readDir(path: string): Dirent[] { - const stats = readStats(path); - - if (stats && stats.isDirectory()) { - return mem.readdirSync(realFileSystem.normalizePath(path), { - withFileTypes: true, - }) as Dirent[]; - } - - return []; - } - - function createDir(path: string) { - mem.mkdirSync(realFileSystem.normalizePath(path), { recursive: true }); - } - - function writeFile(path: string, data: string) { - if (!exists(dirname(path))) { - createDir(dirname(path)); - } - - mem.writeFileSync(realFileSystem.normalizePath(path), data); - } - - function deleteFile(path: string) { - if (exists(path)) { - mem.unlinkSync(realFileSystem.normalizePath(path)); - } - } - - function updateTimes(path: string, atime: Date, mtime: Date) { - if (exists(path)) { - mem.utimesSync(realFileSystem.normalizePath(path), atime, mtime); - } - } - - return { - ...realFileSystem, - exists(path: string) { - return exists(realFileSystem.realPath(path)); - }, - readFile(path: string, encoding?: string) { - return readFile(realFileSystem.realPath(path), encoding); - }, - readDir(path: string) { - return readDir(realFileSystem.realPath(path)); - }, - readStats(path: string) { - return readStats(realFileSystem.realPath(path)); - }, - writeFile(path: string, data: string) { - writeFile(realFileSystem.realPath(path), data); - }, - deleteFile(path: string) { - deleteFile(realFileSystem.realPath(path)); - }, - createDir(path: string) { - createDir(realFileSystem.realPath(path)); - }, - updateTimes(path: string, atime: Date, mtime: Date) { - updateTimes(realFileSystem.realPath(path), atime, mtime); - }, - clearCache() { - realFileSystem.clearCache(); - }, - }; -} - -export { createMemFileSystem }; diff --git a/src/typescript-reporter/file-system/PassiveFileSystem.ts b/src/typescript-reporter/file-system/PassiveFileSystem.ts deleted file mode 100644 index 0e0b11d5..00000000 --- a/src/typescript-reporter/file-system/PassiveFileSystem.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { FileSystem } from './FileSystem'; - -/** - * It's an implementation of FileSystem interface which reads from the real file system, but write to the in-memory file system. - * - * @param memFileSystem - * @param realFileSystem - */ -function createPassiveFileSystem( - memFileSystem: FileSystem, - realFileSystem: FileSystem -): FileSystem { - function exists(path: string) { - return realFileSystem.exists(path) || memFileSystem.exists(path); - } - - function readFile(path: string, encoding?: string) { - const fsStats = realFileSystem.readStats(path); - const memStats = memFileSystem.readStats(path); - - if (fsStats && memStats) { - return fsStats.mtimeMs > memStats.mtimeMs - ? realFileSystem.readFile(path, encoding) - : memFileSystem.readFile(path, encoding); - } else if (fsStats) { - return realFileSystem.readFile(path, encoding); - } else if (memStats) { - return memFileSystem.readFile(path, encoding); - } - } - - function readDir(path: string) { - const fsDirents = realFileSystem.readDir(path); - const memDirents = memFileSystem.readDir(path); - - // merge list of dirents from fs and mem - return fsDirents - .filter((fsDirent) => !memDirents.some((memDirent) => memDirent.name === fsDirent.name)) - .concat(memDirents); - } - - function readStats(path: string) { - const fsStats = realFileSystem.readStats(path); - const memStats = memFileSystem.readStats(path); - - if (fsStats && memStats) { - return fsStats.mtimeMs > memStats.mtimeMs ? fsStats : memStats; - } else if (fsStats) { - return fsStats; - } else if (memStats) { - return memStats; - } - } - - return { - ...memFileSystem, - exists(path: string) { - return exists(realFileSystem.realPath(path)); - }, - readFile(path: string, encoding?: string) { - return readFile(realFileSystem.realPath(path), encoding); - }, - readDir(path: string) { - return readDir(realFileSystem.realPath(path)); - }, - readStats(path: string) { - return readStats(realFileSystem.realPath(path)); - }, - realPath(path: string) { - return realFileSystem.realPath(path); - }, - clearCache() { - realFileSystem.clearCache(); - }, - }; -} - -export { createPassiveFileSystem }; diff --git a/src/typescript-reporter/file-system/RealFileSystem.ts b/src/typescript-reporter/file-system/RealFileSystem.ts deleted file mode 100644 index 3aa35ad1..00000000 --- a/src/typescript-reporter/file-system/RealFileSystem.ts +++ /dev/null @@ -1,216 +0,0 @@ -import type { Dirent, Stats } from 'fs'; -import { dirname, basename, join, normalize } from 'path'; - -import fs from 'fs-extra'; - -import type { FileSystem } from './FileSystem'; -// eslint-disable-next-line node/no-unsupported-features/node-builtins - -/** - * It's an implementation of the FileSystem interface which reads and writes directly to the real file system. - * - * @param caseSensitive - */ -function createRealFileSystem(caseSensitive = false): FileSystem { - // read cache - const existsCache = new Map(); - const readStatsCache = new Map(); - const readFileCache = new Map(); - const readDirCache = new Map(); - const realPathCache = new Map(); - - function normalizePath(path: string): string { - return caseSensitive ? normalize(path) : normalize(path).toLowerCase(); - } - - // read methods - function exists(path: string): boolean { - const normalizedPath = normalizePath(path); - - if (!existsCache.has(normalizedPath)) { - existsCache.set(normalizedPath, fs.existsSync(normalizedPath)); - } - - return !!existsCache.get(normalizedPath); - } - - function readStats(path: string): Stats | undefined { - const normalizedPath = normalizePath(path); - - if (!readStatsCache.has(normalizedPath)) { - if (exists(normalizedPath)) { - readStatsCache.set(normalizedPath, fs.statSync(normalizedPath)); - } - } - - return readStatsCache.get(normalizedPath); - } - - function readFile(path: string, encoding?: string): string | undefined { - const normalizedPath = normalizePath(path); - - if (!readFileCache.has(normalizedPath)) { - const stats = readStats(normalizedPath); - - if (stats && stats.isFile()) { - readFileCache.set( - normalizedPath, - fs.readFileSync(normalizedPath, { encoding: encoding as BufferEncoding }).toString() - ); - } else { - readFileCache.set(normalizedPath, undefined); - } - } - - return readFileCache.get(normalizedPath); - } - - function readDir(path: string): Dirent[] { - const normalizedPath = normalizePath(path); - - if (!readDirCache.has(normalizedPath)) { - const stats = readStats(normalizedPath); - - if (stats && stats.isDirectory()) { - readDirCache.set(normalizedPath, fs.readdirSync(normalizedPath, { withFileTypes: true })); - } else { - readDirCache.set(normalizedPath, []); - } - } - - return readDirCache.get(normalizedPath) || []; - } - - function getRealPath(path: string) { - const normalizedPath = normalizePath(path); - - if (!realPathCache.has(normalizedPath)) { - let base = normalizedPath; - let nested = ''; - - while (base !== dirname(base)) { - if (exists(base)) { - realPathCache.set(normalizedPath, normalizePath(join(fs.realpathSync(base), nested))); - break; - } - - nested = join(basename(base), nested); - base = dirname(base); - } - } - - return realPathCache.get(normalizedPath) || normalizedPath; - } - - function createDir(path: string) { - const normalizedPath = normalizePath(path); - - fs.mkdirSync(normalizedPath, { recursive: true }); - - // update cache - existsCache.set(normalizedPath, true); - if (readDirCache.has(dirname(normalizedPath))) { - readDirCache.delete(dirname(normalizedPath)); - } - if (readStatsCache.has(normalizedPath)) { - readStatsCache.delete(normalizedPath); - } - } - - function writeFile(path: string, data: string) { - const normalizedPath = normalizePath(path); - - if (!exists(dirname(normalizedPath))) { - createDir(dirname(normalizedPath)); - } - - fs.writeFileSync(normalizedPath, data); - - // update cache - existsCache.set(normalizedPath, true); - if (readDirCache.has(dirname(normalizedPath))) { - readDirCache.delete(dirname(normalizedPath)); - } - if (readStatsCache.has(normalizedPath)) { - readStatsCache.delete(normalizedPath); - } - if (readFileCache.has(normalizedPath)) { - readFileCache.delete(normalizedPath); - } - } - - function deleteFile(path: string) { - if (exists(path)) { - const normalizedPath = normalizePath(path); - - fs.unlinkSync(normalizedPath); - - // update cache - existsCache.set(normalizedPath, false); - if (readDirCache.has(dirname(normalizedPath))) { - readDirCache.delete(dirname(normalizedPath)); - } - if (readStatsCache.has(normalizedPath)) { - readStatsCache.delete(normalizedPath); - } - if (readFileCache.has(normalizedPath)) { - readFileCache.delete(normalizedPath); - } - } - } - - function updateTimes(path: string, atime: Date, mtime: Date) { - if (exists(path)) { - const normalizedPath = normalizePath(path); - - fs.utimesSync(normalizePath(path), atime, mtime); - - // update cache - if (readStatsCache.has(normalizedPath)) { - readStatsCache.delete(normalizedPath); - } - } - } - - return { - exists(path: string) { - return exists(getRealPath(path)); - }, - readFile(path: string, encoding?: string) { - return readFile(getRealPath(path), encoding); - }, - readDir(path: string) { - return readDir(getRealPath(path)); - }, - readStats(path: string) { - return readStats(getRealPath(path)); - }, - realPath(path: string) { - return getRealPath(path); - }, - normalizePath(path: string) { - return normalizePath(path); - }, - writeFile(path: string, data: string) { - writeFile(getRealPath(path), data); - }, - deleteFile(path: string) { - deleteFile(getRealPath(path)); - }, - createDir(path: string) { - createDir(getRealPath(path)); - }, - updateTimes(path: string, atime: Date, mtime: Date) { - updateTimes(getRealPath(path), atime, mtime); - }, - clearCache() { - existsCache.clear(); - readStatsCache.clear(); - readFileCache.clear(); - readDirCache.clear(); - realPathCache.clear(); - }, - }; -} - -export { createRealFileSystem }; diff --git a/src/typescript-reporter/issue/TypeScriptIssueFactory.ts b/src/typescript-reporter/issue/TypeScriptIssueFactory.ts deleted file mode 100644 index 53204263..00000000 --- a/src/typescript-reporter/issue/TypeScriptIssueFactory.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as os from 'os'; - -import type * as ts from 'typescript'; - -import type { Issue, IssueLocation } from '../../issue'; -import { deduplicateAndSortIssues } from '../../issue'; - -function createIssueFromTsDiagnostic(typescript: typeof ts, diagnostic: ts.Diagnostic): Issue { - let file: string | undefined; - let location: IssueLocation | undefined; - - if (diagnostic.file) { - file = diagnostic.file.fileName; - - if (diagnostic.start && diagnostic.length) { - const { line: startLine, character: startCharacter } = - diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); - const { line: endLine, character: endCharacter } = - diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start + diagnostic.length); - - location = { - start: { - line: startLine + 1, - column: startCharacter + 1, - }, - end: { - line: endLine + 1, - column: endCharacter + 1, - }, - }; - } - } - - return { - code: 'TS' + String(diagnostic.code), - // we don't handle Suggestion and Message diagnostics - severity: diagnostic.category === 0 ? 'warning' : 'error', - message: typescript.flattenDiagnosticMessageText(diagnostic.messageText, os.EOL), - file, - location, - }; -} - -function createIssuesFromTsDiagnostics( - typescript: typeof ts, - diagnostics: ts.Diagnostic[] -): Issue[] { - return deduplicateAndSortIssues( - diagnostics.map((diagnostic) => createIssueFromTsDiagnostic(typescript, diagnostic)) - ); -} - -export { createIssuesFromTsDiagnostics }; diff --git a/src/typescript-reporter/profile/TypeScriptPerformance.ts b/src/typescript-reporter/profile/TypeScriptPerformance.ts deleted file mode 100644 index 683fb41e..00000000 --- a/src/typescript-reporter/profile/TypeScriptPerformance.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type * as ts from 'typescript'; - -import type { Performance } from '../../profile/Performance'; - -interface TypeScriptPerformance { - enable(): void; - disable(): void; - mark(name: string): void; - measure(name: string, startMark?: string, endMark?: string): void; -} - -function getTypeScriptPerformance(typescript: typeof ts): TypeScriptPerformance | undefined { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (typescript as any).performance; -} - -function connectTypeScriptPerformance( - typescript: typeof ts, - performance: Performance -): Performance { - const typeScriptPerformance = getTypeScriptPerformance(typescript); - - if (typeScriptPerformance) { - const { mark, measure } = typeScriptPerformance; - const { enable, disable } = performance; - - typeScriptPerformance.mark = (name) => { - mark(name); - performance.mark(name); - }; - typeScriptPerformance.measure = (name, startMark, endMark) => { - measure(name, startMark, endMark); - performance.measure(name, startMark, endMark); - }; - - return { - ...performance, - enable() { - enable(); - typeScriptPerformance.enable(); - }, - disable() { - disable(); - typeScriptPerformance.disable(); - }, - }; - } else { - return performance; - } -} - -export { TypeScriptPerformance, connectTypeScriptPerformance }; diff --git a/src/typescript-reporter/reporter/ControlledCompilerHost.ts b/src/typescript-reporter/reporter/ControlledCompilerHost.ts deleted file mode 100644 index a72748b8..00000000 --- a/src/typescript-reporter/reporter/ControlledCompilerHost.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type * as ts from 'typescript'; - -import type { TypeScriptHostExtension } from '../extension/TypeScriptExtension'; - -import type { ControlledTypeScriptSystem } from './ControlledTypeScriptSystem'; - -function createControlledCompilerHost( - typescript: typeof ts, - parsedCommandLine: ts.ParsedCommandLine, - system: ControlledTypeScriptSystem, - hostExtensions: TypeScriptHostExtension[] = [] -): ts.CompilerHost { - const baseCompilerHost = typescript.createCompilerHost(parsedCommandLine.options); - - let controlledCompilerHost: ts.CompilerHost = { - ...baseCompilerHost, - fileExists: system.fileExists, - readFile: system.readFile, - directoryExists: system.directoryExists, - getDirectories: system.getDirectories, - realpath: system.realpath, - }; - - hostExtensions.forEach((hostExtension) => { - if (hostExtension.extendCompilerHost) { - controlledCompilerHost = hostExtension.extendCompilerHost( - controlledCompilerHost, - parsedCommandLine - ); - } - }); - - return controlledCompilerHost; -} - -export { createControlledCompilerHost }; diff --git a/src/typescript-reporter/reporter/ControlledTypeScriptSystem.ts b/src/typescript-reporter/reporter/ControlledTypeScriptSystem.ts deleted file mode 100644 index 00db48ad..00000000 --- a/src/typescript-reporter/reporter/ControlledTypeScriptSystem.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { dirname, join } from 'path'; - -import type * as ts from 'typescript'; - -import type { FilesMatch } from '../../reporter'; -import forwardSlash from '../../utils/path/forwardSlash'; -import { createMemFileSystem } from '../file-system/MemFileSystem'; -import { createPassiveFileSystem } from '../file-system/PassiveFileSystem'; -import { createRealFileSystem } from '../file-system/RealFileSystem'; - -interface ControlledTypeScriptSystem extends ts.System { - // control watcher - invokeFileCreated(path: string): void; - invokeFileChanged(path: string): void; - invokeFileDeleted(path: string): void; - // control cache - clearCache(): void; - // mark these methods as defined - not optional - getFileSize(path: string): number; - watchFile( - path: string, - callback: ts.FileWatcherCallback, - pollingInterval?: number, - options?: ts.WatchOptions - ): ts.FileWatcher; - watchDirectory( - path: string, - callback: ts.DirectoryWatcherCallback, - recursive?: boolean, - options?: ts.WatchOptions - ): ts.FileWatcher; - getModifiedTime(path: string): Date | undefined; - setModifiedTime(path: string, time: Date): void; - deleteFile(path: string): void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - clearTimeout(timeoutId: any): void; - // detect when all tasks scheduled by `setTimeout` finished - waitForQueued(): Promise; - setArtifacts(artifacts: FilesMatch): void; -} - -type FileSystemMode = 'readonly' | 'write-tsbuildinfo' | 'write-references'; - -function createControlledTypeScriptSystem( - typescript: typeof ts, - mode: FileSystemMode = 'readonly' -): ControlledTypeScriptSystem { - let artifacts: FilesMatch = { - files: [], - dirs: [], - excluded: [], - extensions: [], - }; - let isInitialRun = true; - // watchers - const fileWatcherCallbacksMap = new Map(); - const directoryWatcherCallbacksMap = new Map(); - const recursiveDirectoryWatcherCallbacksMap = new Map(); - const deletedFiles = new Map(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const timeoutCallbacks = new Set(); - // always use case-sensitive as normalization to lower-case can be a problem for some - // third-party libraries, like fsevents - const caseSensitive = true; - const realFileSystem = createRealFileSystem(caseSensitive); - const memFileSystem = createMemFileSystem(realFileSystem); - const passiveFileSystem = createPassiveFileSystem(memFileSystem, realFileSystem); - - // based on the ts.ignorePaths - const ignoredPaths = ['/node_modules/.', '/.git', '/.#']; - - function createWatcher( - watchersMap: Map, - path: string, - callback: TCallback - ) { - const normalizedPath = realFileSystem.normalizePath(path); - - const watchers = watchersMap.get(normalizedPath) || []; - const nextWatchers = [...watchers, callback]; - watchersMap.set(normalizedPath, nextWatchers); - - return { - close: () => { - const watchers = watchersMap.get(normalizedPath) || []; - const nextWatchers = watchers.filter((watcher) => watcher !== callback); - - if (nextWatchers.length > 0) { - watchersMap.set(normalizedPath, nextWatchers); - } else { - watchersMap.delete(normalizedPath); - } - }, - }; - } - - function invokeFileWatchers(path: string, event: ts.FileWatcherEventKind) { - const normalizedPath = realFileSystem.normalizePath(path); - if (normalizedPath.endsWith('.js')) { - // trigger relevant .d.ts file watcher - handles the case, when we have webpack watcher - // that points to a symlinked package - invokeFileWatchers(normalizedPath.slice(0, -3) + '.d.ts', event); - } - - const fileWatcherCallbacks = fileWatcherCallbacksMap.get(normalizedPath); - if (fileWatcherCallbacks) { - // typescript expects normalized paths with posix forward slash - fileWatcherCallbacks.forEach((fileWatcherCallback) => - fileWatcherCallback(forwardSlash(normalizedPath), event) - ); - } - } - - function invokeDirectoryWatchers(path: string) { - const normalizedPath = realFileSystem.normalizePath(path); - const directory = dirname(normalizedPath); - - if (ignoredPaths.some((ignoredPath) => forwardSlash(normalizedPath).includes(ignoredPath))) { - return; - } - - const directoryWatcherCallbacks = directoryWatcherCallbacksMap.get(directory); - if (directoryWatcherCallbacks) { - directoryWatcherCallbacks.forEach((directoryWatcherCallback) => - directoryWatcherCallback(forwardSlash(normalizedPath)) - ); - } - - recursiveDirectoryWatcherCallbacksMap.forEach( - (recursiveDirectoryWatcherCallbacks, watchedDirectory) => { - if ( - watchedDirectory === directory || - (directory.startsWith(watchedDirectory) && - forwardSlash(directory)[watchedDirectory.length] === '/') - ) { - recursiveDirectoryWatcherCallbacks.forEach((recursiveDirectoryWatcherCallback) => - recursiveDirectoryWatcherCallback(forwardSlash(normalizedPath)) - ); - } - } - ); - } - - function isArtifact(path: string) { - return ( - (artifacts.dirs.some((dir) => path.includes(dir)) || - artifacts.files.some((file) => path === file)) && - artifacts.extensions.some((extension) => path.endsWith(extension)) - ); - } - - function getReadFileSystem(path: string) { - if ( - !isInitialRun && - (mode === 'readonly' || mode === 'write-tsbuildinfo') && - isArtifact(path) - ) { - return memFileSystem; - } - - return passiveFileSystem; - } - - function getWriteFileSystem(path: string) { - if ( - mode === 'write-references' || - (mode === 'write-tsbuildinfo' && path.endsWith('.tsbuildinfo')) - ) { - return realFileSystem; - } - - return passiveFileSystem; - } - - const controlledSystem: ControlledTypeScriptSystem = { - ...typescript.sys, - useCaseSensitiveFileNames: caseSensitive, - fileExists(path: string): boolean { - const stats = getReadFileSystem(path).readStats(path); - - return !!stats && stats.isFile(); - }, - readFile(path: string, encoding?: string): string | undefined { - return getReadFileSystem(path).readFile(path, encoding); - }, - getFileSize(path: string): number { - const stats = getReadFileSystem(path).readStats(path); - - return stats ? stats.size : 0; - }, - writeFile(path: string, data: string): void { - getWriteFileSystem(path).writeFile(path, data); - - controlledSystem.invokeFileChanged(path); - }, - deleteFile(path: string): void { - getWriteFileSystem(path).deleteFile(path); - - controlledSystem.invokeFileDeleted(path); - }, - directoryExists(path: string): boolean { - return Boolean(getReadFileSystem(path).readStats(path)?.isDirectory()); - }, - createDirectory(path: string): void { - getWriteFileSystem(path).createDir(path); - - invokeDirectoryWatchers(path); - }, - getDirectories(path: string): string[] { - const dirents = getReadFileSystem(path).readDir(path); - - return dirents - .filter( - (dirent) => - dirent.isDirectory() || - (dirent.isSymbolicLink() && controlledSystem.directoryExists(join(path, dirent.name))) - ) - .map((dirent) => dirent.name); - }, - getModifiedTime(path: string): Date | undefined { - const stats = getReadFileSystem(path).readStats(path); - - if (stats) { - return stats.mtime; - } - }, - setModifiedTime(path: string, date: Date): void { - getWriteFileSystem(path).updateTimes(path, date, date); - - invokeDirectoryWatchers(path); - invokeFileWatchers(path, typescript.FileWatcherEventKind.Changed); - }, - watchFile(path: string, callback: ts.FileWatcherCallback): ts.FileWatcher { - return createWatcher(fileWatcherCallbacksMap, path, callback); - }, - watchDirectory( - path: string, - callback: ts.DirectoryWatcherCallback, - recursive = false - ): ts.FileWatcher { - return createWatcher( - recursive ? recursiveDirectoryWatcherCallbacksMap : directoryWatcherCallbacksMap, - path, - callback - ); - }, - // use immediate instead of timeout to avoid waiting 250ms for batching files changes - setTimeout: (callback, timeout, ...args) => { - const timeoutId = setImmediate(() => { - callback(...args); - timeoutCallbacks.delete(timeoutId); - }); - timeoutCallbacks.add(timeoutId); - - return timeoutId; - }, - clearTimeout: (timeoutId) => { - clearImmediate(timeoutId); - timeoutCallbacks.delete(timeoutId); - }, - async waitForQueued(): Promise { - while (timeoutCallbacks.size > 0) { - await new Promise((resolve) => setImmediate(resolve)); - } - isInitialRun = false; - }, - invokeFileCreated(path: string) { - const normalizedPath = realFileSystem.normalizePath(path); - - invokeFileWatchers(path, typescript.FileWatcherEventKind.Created); - invokeDirectoryWatchers(normalizedPath); - - deletedFiles.set(normalizedPath, false); - }, - invokeFileChanged(path: string) { - const normalizedPath = realFileSystem.normalizePath(path); - - if (deletedFiles.get(normalizedPath) || !fileWatcherCallbacksMap.has(normalizedPath)) { - invokeFileWatchers(path, typescript.FileWatcherEventKind.Created); - invokeDirectoryWatchers(normalizedPath); - - deletedFiles.set(normalizedPath, false); - } else { - invokeFileWatchers(path, typescript.FileWatcherEventKind.Changed); - } - }, - invokeFileDeleted(path: string) { - const normalizedPath = realFileSystem.normalizePath(path); - - if (!deletedFiles.get(normalizedPath)) { - invokeFileWatchers(path, typescript.FileWatcherEventKind.Deleted); - invokeDirectoryWatchers(path); - - deletedFiles.set(normalizedPath, true); - } - }, - clearCache() { - realFileSystem.clearCache(); - memFileSystem.clearCache(); - passiveFileSystem.clearCache(); - }, - setArtifacts(nextArtifacts: FilesMatch) { - artifacts = nextArtifacts; - }, - }; - - return controlledSystem; -} - -export { createControlledTypeScriptSystem, ControlledTypeScriptSystem }; diff --git a/src/typescript-reporter/reporter/TypeScriptConfigurationParser.ts b/src/typescript-reporter/reporter/TypeScriptConfigurationParser.ts deleted file mode 100644 index dbdda8d1..00000000 --- a/src/typescript-reporter/reporter/TypeScriptConfigurationParser.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { normalize, dirname, basename, resolve, relative } from 'path'; - -import type * as ts from 'typescript'; - -import type { FilesMatch } from '../../reporter'; -import forwardSlash from '../../utils/path/forwardSlash'; -import type { TypeScriptConfigurationOverwrite } from '../TypeScriptConfigurationOverwrite'; - -function parseTypeScriptConfiguration( - typescript: typeof ts, - configFileName: string, - configFileContext: string, - configOverwriteJSON: TypeScriptConfigurationOverwrite, - parseConfigFileHost: ts.ParseConfigFileHost -): ts.ParsedCommandLine { - const configFilePath = forwardSlash(configFileName); - const parsedConfigFileJSON = typescript.readConfigFile( - configFilePath, - parseConfigFileHost.readFile - ); - - const overwrittenConfigFileJSON = { - ...(parsedConfigFileJSON.config || {}), - ...configOverwriteJSON, - compilerOptions: { - ...((parsedConfigFileJSON.config || {}).compilerOptions || {}), - ...(configOverwriteJSON.compilerOptions || {}), - }, - }; - - const parsedConfigFile = typescript.parseJsonConfigFileContent( - overwrittenConfigFileJSON, - parseConfigFileHost, - configFileContext - ); - - return { - ...parsedConfigFile, - options: { - ...parsedConfigFile.options, - configFilePath: configFilePath, - }, - errors: parsedConfigFileJSON.error ? [parsedConfigFileJSON.error] : parsedConfigFile.errors, - }; -} - -function getDependenciesFromTypeScriptConfiguration( - typescript: typeof ts, - parsedConfiguration: ts.ParsedCommandLine, - configFileContext: string, - parseConfigFileHost: ts.ParseConfigFileHost, - processedConfigFiles: string[] = [] -): FilesMatch { - const files = new Set(parsedConfiguration.fileNames); - const configFilePath = parsedConfiguration.options.configFilePath; - if (typeof configFilePath === 'string') { - files.add(configFilePath); - } - const dirs = new Set(Object.keys(parsedConfiguration.wildcardDirectories || {})); - const excluded = new Set( - (parsedConfiguration.raw?.exclude || []).map((path: string) => resolve(configFileContext, path)) - ); - - for (const projectReference of parsedConfiguration.projectReferences || []) { - const childConfigFilePath = typescript.resolveProjectReferencePath(projectReference); - const childConfigContext = dirname(childConfigFilePath); - if (processedConfigFiles.includes(childConfigFilePath)) { - // handle circular dependencies - continue; - } - const childParsedConfiguration = parseTypeScriptConfiguration( - typescript, - childConfigFilePath, - childConfigContext, - {}, - parseConfigFileHost - ); - const childDependencies = getDependenciesFromTypeScriptConfiguration( - typescript, - childParsedConfiguration, - childConfigContext, - parseConfigFileHost, - [...processedConfigFiles, childConfigFilePath] - ); - childDependencies.files.forEach((file) => { - files.add(file); - }); - childDependencies.dirs.forEach((dir) => { - dirs.add(dir); - }); - } - - const extensions = [ - typescript.Extension.Ts, - typescript.Extension.Tsx, - typescript.Extension.Js, - typescript.Extension.Jsx, - typescript.Extension.TsBuildInfo, - ]; - - return { - files: Array.from(files).map((file) => normalize(file)), - dirs: Array.from(dirs).map((dir) => normalize(dir)), - excluded: Array.from(excluded).map((path) => normalize(path)), - extensions: extensions, - }; -} - -export function isIncrementalCompilation(options: ts.CompilerOptions) { - return Boolean((options.incremental || options.composite) && !options.outFile); -} - -function removeJsonExtension(path: string) { - if (path.endsWith('.json')) { - return path.slice(0, -'.json'.length); - } else { - return path; - } -} - -function getTsBuildInfoEmitOutputFilePath(typescript: typeof ts, options: ts.CompilerOptions) { - if (typeof typescript.getTsBuildInfoEmitOutputFilePath === 'function') { - // old TypeScript version doesn't provides this method - return typescript.getTsBuildInfoEmitOutputFilePath(options); - } - - // based on the implementation from typescript - const configFile = options.configFilePath as string; - if (!isIncrementalCompilation(options)) { - return undefined; - } - if (options.tsBuildInfoFile) { - return options.tsBuildInfoFile; - } - const outPath = options.outFile || options.out; - let buildInfoExtensionLess; - if (outPath) { - buildInfoExtensionLess = removeJsonExtension(outPath); - } else { - if (!configFile) { - return undefined; - } - const configFileExtensionLess = removeJsonExtension(configFile); - buildInfoExtensionLess = options.outDir - ? options.rootDir - ? resolve(options.outDir, relative(options.rootDir, configFileExtensionLess)) - : resolve(options.outDir, basename(configFileExtensionLess)) - : configFileExtensionLess; - } - return buildInfoExtensionLess + '.tsbuildinfo'; -} - -function getArtifactsFromTypeScriptConfiguration( - typescript: typeof ts, - parsedConfiguration: ts.ParsedCommandLine, - configFileContext: string, - parseConfigFileHost: ts.ParseConfigFileHost, - processedConfigFiles: string[] = [] -): FilesMatch { - const files = new Set(); - const dirs = new Set(); - if (parsedConfiguration.fileNames.length > 0) { - if (parsedConfiguration.options.outFile) { - files.add(resolve(configFileContext, parsedConfiguration.options.outFile)); - } - const tsBuildInfoPath = getTsBuildInfoEmitOutputFilePath( - typescript, - parsedConfiguration.options - ); - if (tsBuildInfoPath) { - files.add(resolve(configFileContext, tsBuildInfoPath)); - } - - if (parsedConfiguration.options.outDir) { - dirs.add(resolve(configFileContext, parsedConfiguration.options.outDir)); - } - } - - for (const projectReference of parsedConfiguration.projectReferences || []) { - const configFile = typescript.resolveProjectReferencePath(projectReference); - if (processedConfigFiles.includes(configFile)) { - // handle circular dependencies - continue; - } - const parsedConfiguration = parseTypeScriptConfiguration( - typescript, - configFile, - dirname(configFile), - {}, - parseConfigFileHost - ); - const childArtifacts = getArtifactsFromTypeScriptConfiguration( - typescript, - parsedConfiguration, - configFileContext, - parseConfigFileHost, - [...processedConfigFiles, configFile] - ); - childArtifacts.files.forEach((file) => { - files.add(file); - }); - childArtifacts.dirs.forEach((dir) => { - dirs.add(dir); - }); - } - - const extensions = [ - typescript.Extension.Dts, - typescript.Extension.Js, - typescript.Extension.TsBuildInfo, - ]; - - return { - files: Array.from(files).map((file) => normalize(file)), - dirs: Array.from(dirs).map((dir) => normalize(dir)), - excluded: [], - extensions, - }; -} - -export { - parseTypeScriptConfiguration, - getDependenciesFromTypeScriptConfiguration, - getArtifactsFromTypeScriptConfiguration, -}; diff --git a/src/typescript-reporter/reporter/TypeScriptReporter.ts b/src/typescript-reporter/reporter/TypeScriptReporter.ts deleted file mode 100644 index 07738c9f..00000000 --- a/src/typescript-reporter/reporter/TypeScriptReporter.ts +++ /dev/null @@ -1,542 +0,0 @@ -import path from 'path'; - -import type * as ts from 'typescript'; - -import { createPerformance } from '../../profile/Performance'; -import type { FilesMatch, Reporter } from '../../reporter'; -import type { TypeScriptExtension } from '../extension/TypeScriptExtension'; -import { createTypeScriptVueExtension } from '../extension/vue/TypeScriptVueExtension'; -import { createIssuesFromTsDiagnostics } from '../issue/TypeScriptIssueFactory'; -import { connectTypeScriptPerformance } from '../profile/TypeScriptPerformance'; -import type { TypeScriptReporterConfiguration } from '../TypeScriptReporterConfiguration'; - -import { createControlledCompilerHost } from './ControlledCompilerHost'; -import type { ControlledTypeScriptSystem } from './ControlledTypeScriptSystem'; -import { createControlledTypeScriptSystem } from './ControlledTypeScriptSystem'; -import { createControlledWatchCompilerHost } from './ControlledWatchCompilerHost'; -import { createControlledWatchSolutionBuilderHost } from './ControlledWatchSolutionBuilderHost'; -import { - getDependenciesFromTypeScriptConfiguration, - getArtifactsFromTypeScriptConfiguration, - parseTypeScriptConfiguration, - isIncrementalCompilation, -} from './TypeScriptConfigurationParser'; - -// write this type as it's available only in the newest TypeScript versions (^4.1.0) -interface Tracing { - startTracing(configFilePath: string, traceDirPath: string, isBuildMode: boolean): void; - stopTracing(typeCatalog: unknown): void; - dumpLegend(): void; -} - -function createTypeScriptReporter(configuration: TypeScriptReporterConfiguration): Reporter { - let parsedConfiguration: ts.ParsedCommandLine | undefined; - let parseConfigurationDiagnostics: ts.Diagnostic[] = []; - let dependencies: FilesMatch | undefined; - let artifacts: FilesMatch | undefined; - let configurationChanged = false; - let compilerHost: ts.CompilerHost | undefined; - let watchCompilerHost: - | ts.WatchCompilerHostOfFilesAndCompilerOptions - | undefined; - let watchSolutionBuilderHost: - | ts.SolutionBuilderWithWatchHost - | undefined; - let program: ts.Program | undefined; - let watchProgram: - | ts.WatchOfFilesAndCompilerOptions - | undefined; - let solutionBuilder: ts.SolutionBuilder | undefined; - let shouldUpdateRootFiles = false; - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const typescript: typeof ts = require(configuration.typescriptPath); - const extensions: TypeScriptExtension[] = []; - const system: ControlledTypeScriptSystem = createControlledTypeScriptSystem( - typescript, - configuration.mode - ); - const diagnosticsPerProject = new Map(); - const performance = connectTypeScriptPerformance(typescript, createPerformance()); - - if (configuration.extensions.vue.enabled) { - extensions.push(createTypeScriptVueExtension(configuration.extensions.vue)); - } - - function getConfigFilePathFromCompilerOptions(compilerOptions: ts.CompilerOptions): string { - return compilerOptions.configFilePath as unknown as string; - } - - function getProjectNameOfBuilderProgram(builderProgram: ts.BuilderProgram): string { - return getConfigFilePathFromCompilerOptions(builderProgram.getProgram().getCompilerOptions()); - } - - function getTracing(): Tracing | undefined { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (typescript as any).tracing; - } - - function getDiagnosticsOfProgram(program: ts.Program | ts.BuilderProgram) { - const diagnostics: ts.Diagnostic[] = []; - - if (configuration.diagnosticOptions.syntactic) { - performance.markStart('Syntactic Diagnostics'); - diagnostics.push(...program.getSyntacticDiagnostics()); - performance.markEnd('Syntactic Diagnostics'); - } - if (configuration.diagnosticOptions.global) { - performance.markStart('Global Diagnostics'); - diagnostics.push(...program.getGlobalDiagnostics()); - performance.markEnd('Global Diagnostics'); - } - if (configuration.diagnosticOptions.semantic) { - performance.markStart('Semantic Diagnostics'); - diagnostics.push(...program.getSemanticDiagnostics()); - performance.markEnd('Semantic Diagnostics'); - } - if (configuration.diagnosticOptions.declaration) { - performance.markStart('Declaration Diagnostics'); - diagnostics.push(...program.getDeclarationDiagnostics()); - performance.markEnd('Declaration Diagnostics'); - } - - return diagnostics; - } - - function emitTsBuildInfoFileForBuilderProgram(builderProgram: ts.BuilderProgram) { - if ( - configuration.mode !== 'readonly' && - parsedConfiguration && - isIncrementalCompilation(parsedConfiguration.options) - ) { - const program = builderProgram.getProgram(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (typeof (program as any).emitBuildInfo === 'function') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (program as any).emitBuildInfo(); - } - } - } - - function getParseConfigFileHost() { - const parseConfigDiagnostics: ts.Diagnostic[] = []; - - let parseConfigFileHost: ts.ParseConfigFileHost = { - ...system, - onUnRecoverableConfigFileDiagnostic: (diagnostic) => { - parseConfigDiagnostics.push(diagnostic); - }, - }; - - for (const extension of extensions) { - if (extension.extendParseConfigFileHost) { - parseConfigFileHost = extension.extendParseConfigFileHost(parseConfigFileHost); - } - } - - return [parseConfigFileHost, parseConfigDiagnostics] as const; - } - - function parseConfiguration() { - const [parseConfigFileHost, parseConfigDiagnostics] = getParseConfigFileHost(); - - const parsedConfiguration = parseTypeScriptConfiguration( - typescript, - configuration.configFile, - configuration.context, - configuration.configOverwrite, - parseConfigFileHost - ); - - if (parsedConfiguration.errors) { - parseConfigDiagnostics.push(...parsedConfiguration.errors); - } - - return [parsedConfiguration, parseConfigDiagnostics] as const; - } - - function parseConfigurationIfNeeded(): ts.ParsedCommandLine { - if (!parsedConfiguration) { - [parsedConfiguration, parseConfigurationDiagnostics] = parseConfiguration(); - } - - return parsedConfiguration; - } - - function getDependencies(): FilesMatch { - parsedConfiguration = parseConfigurationIfNeeded(); - - const [parseConfigFileHost] = getParseConfigFileHost(); - - let dependencies = getDependenciesFromTypeScriptConfiguration( - typescript, - parsedConfiguration, - configuration.context, - parseConfigFileHost - ); - - for (const extension of extensions) { - if (extension.extendDependencies) { - dependencies = extension.extendDependencies(dependencies); - } - } - - return dependencies; - } - - function getArtifacts(): FilesMatch { - parsedConfiguration = parseConfigurationIfNeeded(); - - const [parseConfigFileHost] = getParseConfigFileHost(); - - return getArtifactsFromTypeScriptConfiguration( - typescript, - parsedConfiguration, - configuration.context, - parseConfigFileHost - ); - } - - function getArtifactsIfNeeded(): FilesMatch { - if (!artifacts) { - artifacts = getArtifacts(); - } - - return artifacts; - } - - function startProfilingIfNeeded() { - if (configuration.profile) { - performance.enable(); - } - } - - function stopProfilingIfNeeded() { - if (configuration.profile) { - performance.print(); - performance.disable(); - } - } - - function startTracingIfNeeded(compilerOptions: ts.CompilerOptions) { - const tracing = getTracing(); - - if (compilerOptions.generateTrace && tracing) { - tracing.startTracing( - getConfigFilePathFromCompilerOptions(compilerOptions), - compilerOptions.generateTrace as string, - configuration.build - ); - } - } - - function stopTracingIfNeeded(program: ts.BuilderProgram) { - const tracing = getTracing(); - const compilerOptions = program.getCompilerOptions(); - - if (compilerOptions.generateTrace && tracing) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - tracing.stopTracing((program.getProgram() as any).getTypeCatalog()); - } - } - - function dumpTracingLegendIfNeeded() { - const tracing = getTracing(); - - if (tracing) { - tracing.dumpLegend(); - } - } - - return { - getReport: async ({ changedFiles = [], deletedFiles = [] }, watching) => { - // clear cache to be ready for next iteration and to free memory - system.clearCache(); - - if ( - [...changedFiles, ...deletedFiles] - .map((affectedFile) => path.normalize(affectedFile)) - .includes(path.normalize(configuration.configFile)) - ) { - // we need to re-create programs - parsedConfiguration = undefined; - dependencies = undefined; - artifacts = undefined; - compilerHost = undefined; - watchCompilerHost = undefined; - watchSolutionBuilderHost = undefined; - program = undefined; - watchProgram = undefined; - solutionBuilder = undefined; - - diagnosticsPerProject.clear(); - configurationChanged = true; - } else { - const previousParsedConfiguration = parsedConfiguration; - [parsedConfiguration, parseConfigurationDiagnostics] = parseConfiguration(); - - if ( - previousParsedConfiguration && - JSON.stringify(previousParsedConfiguration.fileNames) !== - JSON.stringify(parsedConfiguration.fileNames) - ) { - // root files changed - we need to recompute dependencies and artifacts - dependencies = getDependencies(); - artifacts = getArtifacts(); - shouldUpdateRootFiles = true; - } - } - - parsedConfiguration = parseConfigurationIfNeeded(); - system.setArtifacts(getArtifactsIfNeeded()); - - if (configurationChanged) { - configurationChanged = false; - - // try to remove outdated .tsbuildinfo file for incremental mode - if ( - typeof typescript.getTsBuildInfoEmitOutputFilePath === 'function' && - configuration.mode !== 'readonly' && - parsedConfiguration.options.incremental - ) { - const tsBuildInfoPath = typescript.getTsBuildInfoEmitOutputFilePath( - parsedConfiguration.options - ); - if (tsBuildInfoPath) { - try { - system.deleteFile(tsBuildInfoPath); - } catch (error) { - // silent - } - } - } - } - - return { - async getDependencies() { - if (!dependencies) { - dependencies = getDependencies(); - } - - return dependencies; - }, - async getIssues() { - startProfilingIfNeeded(); - - parsedConfiguration = parseConfigurationIfNeeded(); - - // report configuration diagnostics and exit - if (parseConfigurationDiagnostics.length) { - let issues = createIssuesFromTsDiagnostics(typescript, parseConfigurationDiagnostics); - - issues.forEach((issue) => { - if (!issue.file) { - issue.file = configuration.configFile; - } - }); - - extensions.forEach((extension) => { - if (extension.extendIssues) { - issues = extension.extendIssues(issues); - } - }); - - return issues; - } - - if (configuration.build) { - // solution builder case - // ensure watch solution builder host exists - if (!watchSolutionBuilderHost) { - performance.markStart('Create Solution Builder Host'); - watchSolutionBuilderHost = createControlledWatchSolutionBuilderHost( - typescript, - parsedConfiguration, - system, - ( - rootNames, - compilerOptions, - host, - oldProgram, - configFileParsingDiagnostics, - projectReferences - ) => { - if (compilerOptions) { - startTracingIfNeeded(compilerOptions); - } - return typescript.createSemanticDiagnosticsBuilderProgram( - rootNames, - compilerOptions, - host, - oldProgram, - configFileParsingDiagnostics, - projectReferences - ); - }, - undefined, - undefined, - undefined, - undefined, - (builderProgram) => { - const projectName = getProjectNameOfBuilderProgram(builderProgram); - const diagnostics = getDiagnosticsOfProgram(builderProgram); - - // update diagnostics - diagnosticsPerProject.set(projectName, diagnostics); - - // emit .tsbuildinfo file if needed - emitTsBuildInfoFileForBuilderProgram(builderProgram); - - stopTracingIfNeeded(builderProgram); - }, - extensions - ); - performance.markEnd('Create Solution Builder Host'); - solutionBuilder = undefined; - } - - // ensure solution builder exists and is up-to-date - if (!solutionBuilder || shouldUpdateRootFiles) { - // not sure if it's the best option - maybe there is a smarter way to do this - shouldUpdateRootFiles = false; - - performance.markStart('Create Solution Builder'); - solutionBuilder = typescript.createSolutionBuilderWithWatch( - watchSolutionBuilderHost, - [configuration.configFile], - {} - ); - performance.markEnd('Create Solution Builder'); - - performance.markStart('Build Solutions'); - solutionBuilder.build(); - performance.markEnd('Build Solutions'); - } - } else if (watching) { - // watch compiler case - // ensure watch compiler host exists - if (!watchCompilerHost) { - performance.markStart('Create Watch Compiler Host'); - watchCompilerHost = createControlledWatchCompilerHost( - typescript, - parsedConfiguration, - system, - ( - rootNames, - compilerOptions, - host, - oldProgram, - configFileParsingDiagnostics, - projectReferences - ) => { - if (compilerOptions) { - startTracingIfNeeded(compilerOptions); - } - return typescript.createSemanticDiagnosticsBuilderProgram( - rootNames, - compilerOptions, - host, - oldProgram, - configFileParsingDiagnostics, - projectReferences - ); - }, - undefined, - undefined, - (builderProgram) => { - const projectName = getProjectNameOfBuilderProgram(builderProgram); - const diagnostics = getDiagnosticsOfProgram(builderProgram); - - // update diagnostics - diagnosticsPerProject.set(projectName, diagnostics); - - // emit .tsbuildinfo file if needed - emitTsBuildInfoFileForBuilderProgram(builderProgram); - - stopTracingIfNeeded(builderProgram); - }, - extensions - ); - performance.markEnd('Create Watch Compiler Host'); - watchProgram = undefined; - } - - // ensure watch program exists - if (!watchProgram) { - performance.markStart('Create Watch Program'); - watchProgram = typescript.createWatchProgram(watchCompilerHost); - performance.markEnd('Create Watch Program'); - } - - if (shouldUpdateRootFiles && dependencies?.files) { - // we have to update root files manually as don't use config file as a program input - watchProgram.updateRootFileNames(dependencies.files); - shouldUpdateRootFiles = false; - } - } else { - if (!compilerHost) { - compilerHost = createControlledCompilerHost( - typescript, - parsedConfiguration, - system, - extensions - ); - } - if (!program) { - program = typescript.createProgram({ - rootNames: parsedConfiguration.fileNames, - options: parsedConfiguration.options, - projectReferences: parsedConfiguration.projectReferences, - host: compilerHost, - }); - } - const diagnostics = getDiagnosticsOfProgram(program); - const projectName = getConfigFilePathFromCompilerOptions(program.getCompilerOptions()); - - // update diagnostics - diagnosticsPerProject.set(projectName, diagnostics); - } - - changedFiles.forEach((changedFile) => { - if (system) { - system.invokeFileChanged(changedFile); - } - }); - deletedFiles.forEach((removedFile) => { - if (system) { - system.invokeFileDeleted(removedFile); - } - }); - - // wait for all queued events to be processed - performance.markStart('Queued Tasks'); - await system.waitForQueued(); - performance.markEnd('Queued Tasks'); - - // aggregate all diagnostics and map them to issues - const diagnostics: ts.Diagnostic[] = []; - diagnosticsPerProject.forEach((projectDiagnostics) => { - diagnostics.push(...projectDiagnostics); - }); - let issues = createIssuesFromTsDiagnostics(typescript, diagnostics); - - extensions.forEach((extension) => { - if (extension.extendIssues) { - issues = extension.extendIssues(issues); - } - }); - - dumpTracingLegendIfNeeded(); - stopProfilingIfNeeded(); - - return issues; - }, - async close() { - // do nothing - }, - }; - }, - }; -} - -export { createTypeScriptReporter }; diff --git a/src/typescript-reporter/reporter/TypeScriptReporterRpcClient.ts b/src/typescript-reporter/reporter/TypeScriptReporterRpcClient.ts deleted file mode 100644 index 414dfb97..00000000 --- a/src/typescript-reporter/reporter/TypeScriptReporterRpcClient.ts +++ /dev/null @@ -1,19 +0,0 @@ -import path from 'path'; - -import type { ReporterRpcClient } from '../../reporter'; -import { createReporterRpcClient } from '../../reporter'; -import { createRpcIpcMessageChannel } from '../../rpc/rpc-ipc'; -import type { TypeScriptReporterConfiguration } from '../TypeScriptReporterConfiguration'; - -function createTypeScriptReporterRpcClient( - configuration: TypeScriptReporterConfiguration -): ReporterRpcClient { - const channel = createRpcIpcMessageChannel( - path.resolve(__dirname, './TypeScriptReporterRpcService.js'), - configuration.memoryLimit - ); - - return createReporterRpcClient(channel, configuration); -} - -export { createTypeScriptReporterRpcClient }; diff --git a/src/typescript-reporter/reporter/TypeScriptReporterRpcService.ts b/src/typescript-reporter/reporter/TypeScriptReporterRpcService.ts deleted file mode 100644 index 326a9fdb..00000000 --- a/src/typescript-reporter/reporter/TypeScriptReporterRpcService.ts +++ /dev/null @@ -1,14 +0,0 @@ -import process from 'process'; - -import { registerReporterRpcService } from '../../reporter'; -import { createRpcIpcMessagePort } from '../../rpc/rpc-ipc'; -import type { TypeScriptReporterConfiguration } from '../TypeScriptReporterConfiguration'; - -import { createTypeScriptReporter } from './TypeScriptReporter'; - -const service = registerReporterRpcService( - createRpcIpcMessagePort(process), - createTypeScriptReporter -); - -service.open(); diff --git a/src/typescript-reporter/TypeScriptConfigurationOverwrite.ts b/src/typescript/TypeScriptConfigurationOverwrite.ts similarity index 100% rename from src/typescript-reporter/TypeScriptConfigurationOverwrite.ts rename to src/typescript/TypeScriptConfigurationOverwrite.ts diff --git a/src/typescript-reporter/TypeScriptDiagnosticsOptions.ts b/src/typescript/TypeScriptDiagnosticsOptions.ts similarity index 100% rename from src/typescript-reporter/TypeScriptDiagnosticsOptions.ts rename to src/typescript/TypeScriptDiagnosticsOptions.ts diff --git a/src/typescript-reporter/TypeScriptReporterConfiguration.ts b/src/typescript/TypeScriptReporterConfiguration.ts similarity index 100% rename from src/typescript-reporter/TypeScriptReporterConfiguration.ts rename to src/typescript/TypeScriptReporterConfiguration.ts diff --git a/src/typescript-reporter/TypeScriptReporterOptions.ts b/src/typescript/TypeScriptReporterOptions.ts similarity index 100% rename from src/typescript-reporter/TypeScriptReporterOptions.ts rename to src/typescript/TypeScriptReporterOptions.ts diff --git a/src/typescript-reporter/TypeScriptSupport.ts b/src/typescript/TypeScriptSupport.ts similarity index 100% rename from src/typescript-reporter/TypeScriptSupport.ts rename to src/typescript/TypeScriptSupport.ts diff --git a/src/typescript-reporter/extension/TypeScriptEmbeddedExtension.ts b/src/typescript/extension/TypeScriptEmbeddedExtension.ts similarity index 99% rename from src/typescript-reporter/extension/TypeScriptEmbeddedExtension.ts rename to src/typescript/extension/TypeScriptEmbeddedExtension.ts index de82a30d..753a900f 100644 --- a/src/typescript-reporter/extension/TypeScriptEmbeddedExtension.ts +++ b/src/typescript/extension/TypeScriptEmbeddedExtension.ts @@ -2,8 +2,8 @@ import { extname } from 'path'; import type * as ts from 'typescript'; +import type { FilesMatch } from '../../files-match'; import type { Issue } from '../../issue'; -import type { FilesMatch } from '../../reporter'; import type { TypeScriptExtension } from './TypeScriptExtension'; diff --git a/src/typescript-reporter/extension/TypeScriptExtension.ts b/src/typescript/extension/TypeScriptExtension.ts similarity index 95% rename from src/typescript-reporter/extension/TypeScriptExtension.ts rename to src/typescript/extension/TypeScriptExtension.ts index ae436a3d..2f9466e6 100644 --- a/src/typescript-reporter/extension/TypeScriptExtension.ts +++ b/src/typescript/extension/TypeScriptExtension.ts @@ -1,7 +1,7 @@ import type * as ts from 'typescript'; +import type { FilesMatch } from '../../files-match'; import type { Issue } from '../../issue'; -import type { FilesMatch } from '../../reporter'; interface TypeScriptHostExtension { extendWatchSolutionBuilderHost?< diff --git a/src/typescript-reporter/extension/vue/TypeScriptVueExtension.ts b/src/typescript/extension/vue/TypeScriptVueExtension.ts similarity index 100% rename from src/typescript-reporter/extension/vue/TypeScriptVueExtension.ts rename to src/typescript/extension/vue/TypeScriptVueExtension.ts diff --git a/src/typescript-reporter/extension/vue/TypeScriptVueExtensionConfiguration.ts b/src/typescript/extension/vue/TypeScriptVueExtensionConfiguration.ts similarity index 100% rename from src/typescript-reporter/extension/vue/TypeScriptVueExtensionConfiguration.ts rename to src/typescript/extension/vue/TypeScriptVueExtensionConfiguration.ts diff --git a/src/typescript-reporter/extension/vue/TypeScriptVueExtensionOptions.ts b/src/typescript/extension/vue/TypeScriptVueExtensionOptions.ts similarity index 100% rename from src/typescript-reporter/extension/vue/TypeScriptVueExtensionOptions.ts rename to src/typescript/extension/vue/TypeScriptVueExtensionOptions.ts diff --git a/src/typescript-reporter/extension/vue/TypeScriptVueExtensionSupport.ts b/src/typescript/extension/vue/TypeScriptVueExtensionSupport.ts similarity index 100% rename from src/typescript-reporter/extension/vue/TypeScriptVueExtensionSupport.ts rename to src/typescript/extension/vue/TypeScriptVueExtensionSupport.ts diff --git a/src/typescript-reporter/extension/vue/types/vue-template-compiler.ts b/src/typescript/extension/vue/types/vue-template-compiler.ts similarity index 100% rename from src/typescript-reporter/extension/vue/types/vue-template-compiler.ts rename to src/typescript/extension/vue/types/vue-template-compiler.ts diff --git a/src/typescript-reporter/extension/vue/types/vue__compiler-sfc.ts b/src/typescript/extension/vue/types/vue__compiler-sfc.ts similarity index 100% rename from src/typescript-reporter/extension/vue/types/vue__compiler-sfc.ts rename to src/typescript/extension/vue/types/vue__compiler-sfc.ts diff --git a/src/typescript/worker/get-dependencies-worker.ts b/src/typescript/worker/get-dependencies-worker.ts new file mode 100644 index 00000000..6558abca --- /dev/null +++ b/src/typescript/worker/get-dependencies-worker.ts @@ -0,0 +1,26 @@ +import type { FilesChange } from '../../files-change'; +import type { FilesMatch } from '../../files-match'; +import { exposeRpc } from '../../utils/rpc'; + +import { didConfigFileChanged, didRootFilesChanged, invalidateConfig } from './lib/config'; +import { getDependencies, invalidateDependencies } from './lib/dependencies'; +import { system } from './lib/system'; + +const getDependenciesWorker = ({ + changedFiles = [], + deletedFiles = [], +}: FilesChange): FilesMatch => { + system.invalidateCache(); + + if (didConfigFileChanged({ changedFiles, deletedFiles })) { + invalidateConfig(); + invalidateDependencies(); + } else if (didRootFilesChanged()) { + invalidateDependencies(); + } + + return getDependencies(); +}; + +exposeRpc(getDependenciesWorker); +export type GetDependenciesWorker = typeof getDependenciesWorker; diff --git a/src/typescript/worker/get-issues-worker.ts b/src/typescript/worker/get-issues-worker.ts new file mode 100644 index 00000000..fe5a4af6 --- /dev/null +++ b/src/typescript/worker/get-issues-worker.ts @@ -0,0 +1,96 @@ +import type { FilesChange } from '../../files-change'; +import type { Issue } from '../../issue'; +import { exposeRpc } from '../../utils/rpc'; + +import { invalidateArtifacts, registerArtifacts } from './lib/artifacts'; +import { + didConfigFileChanged, + didRootFilesChanged, + getParseConfigIssues, + invalidateConfig, +} from './lib/config'; +import { invalidateDependencies } from './lib/dependencies'; +import { getIssues, invalidateDiagnostics } from './lib/diagnostics'; +import { + disablePerformanceIfNeeded, + enablePerformanceIfNeeded, + printPerformanceMeasuresIfNeeded, +} from './lib/performance'; +import { invalidateProgram, useProgram } from './lib/program/program'; +import { invalidateSolutionBuilder, useSolutionBuilder } from './lib/program/solution-builder'; +import { + invalidateWatchProgram, + invalidateWatchProgramRootFileNames, + useWatchProgram, +} from './lib/program/watch-program'; +import { system } from './lib/system'; +import { dumpTracingLegendIfNeeded } from './lib/tracing'; +import { invalidateTsBuildInfo } from './lib/tsbuildinfo'; +import { config } from './lib/worker-config'; + +const getIssuesWorker = async ( + { changedFiles = [], deletedFiles = [] }: FilesChange, + watching: boolean +): Promise => { + system.invalidateCache(); + + if (didConfigFileChanged({ changedFiles, deletedFiles })) { + invalidateConfig(); + invalidateDependencies(); + invalidateArtifacts(); + invalidateDiagnostics(); + + invalidateProgram(true); + invalidateWatchProgram(true); + invalidateSolutionBuilder(true); + + invalidateTsBuildInfo(); + } else if (didRootFilesChanged()) { + invalidateDependencies(); + invalidateArtifacts(); + + invalidateWatchProgramRootFileNames(); + invalidateSolutionBuilder(); + } + + registerArtifacts(); + enablePerformanceIfNeeded(); + + const parseConfigIssues = getParseConfigIssues(); + if (parseConfigIssues.length) { + // report config parse issues and exit + return parseConfigIssues; + } + + // use proper implementation based on the config + if (config.build) { + useSolutionBuilder(); + } else if (watching) { + useWatchProgram(); + } else { + useProgram(); + } + + // simulate file system events + changedFiles.forEach((changedFile) => { + system?.invokeFileChanged(changedFile); + }); + deletedFiles.forEach((removedFile) => { + system?.invokeFileDeleted(removedFile); + }); + + // wait for all queued events to be processed + await system.waitForQueued(); + + // retrieve all collected diagnostics as normalized issues + const issues = getIssues(); + + dumpTracingLegendIfNeeded(); + printPerformanceMeasuresIfNeeded(); + disablePerformanceIfNeeded(); + + return issues; +}; + +exposeRpc(getIssuesWorker); +export type GetIssuesWorker = typeof getIssuesWorker; diff --git a/src/typescript/worker/lib/artifacts.ts b/src/typescript/worker/lib/artifacts.ts new file mode 100644 index 00000000..a2377c6a --- /dev/null +++ b/src/typescript/worker/lib/artifacts.ts @@ -0,0 +1,85 @@ +import * as path from 'path'; + +import type * as ts from 'typescript'; + +import type { FilesMatch } from '../../../files-match'; + +import { getParsedConfig, parseConfig } from './config'; +import { system } from './system'; +import { getTsBuildInfoEmitPath } from './tsbuildinfo'; +import { typescript } from './typescript'; +import { config } from './worker-config'; + +let artifacts: FilesMatch | undefined; + +export function getArtifacts(force = false): FilesMatch { + if (!artifacts || force) { + const parsedConfig = getParsedConfig(); + + artifacts = getArtifactsWorker(parsedConfig, config.context); + } + + return artifacts; +} + +export function invalidateArtifacts() { + artifacts = undefined; +} + +export function registerArtifacts() { + system.setArtifacts(getArtifacts()); +} + +function getArtifactsWorker( + parsedConfig: ts.ParsedCommandLine, + configFileContext: string, + processedConfigFiles: string[] = [] +): FilesMatch { + const files = new Set(); + const dirs = new Set(); + if (parsedConfig.fileNames.length > 0) { + if (parsedConfig.options.outFile) { + files.add(path.resolve(configFileContext, parsedConfig.options.outFile)); + } + const tsBuildInfoPath = getTsBuildInfoEmitPath(parsedConfig.options); + if (tsBuildInfoPath) { + files.add(path.resolve(configFileContext, tsBuildInfoPath)); + } + + if (parsedConfig.options.outDir) { + dirs.add(path.resolve(configFileContext, parsedConfig.options.outDir)); + } + } + + for (const projectReference of parsedConfig.projectReferences || []) { + const configFile = typescript.resolveProjectReferencePath(projectReference); + if (processedConfigFiles.includes(configFile)) { + // handle circular dependencies + continue; + } + const parsedConfiguration = parseConfig(configFile, path.dirname(configFile)); + const childArtifacts = getArtifactsWorker(parsedConfiguration, configFileContext, [ + ...processedConfigFiles, + configFile, + ]); + childArtifacts.files.forEach((file) => { + files.add(file); + }); + childArtifacts.dirs.forEach((dir) => { + dirs.add(dir); + }); + } + + const extensions = [ + typescript.Extension.Dts, + typescript.Extension.Js, + typescript.Extension.TsBuildInfo, + ]; + + return { + files: Array.from(files).map((file) => path.normalize(file)), + dirs: Array.from(dirs).map((dir) => path.normalize(dir)), + excluded: [], + extensions, + }; +} diff --git a/src/typescript/worker/lib/config.ts b/src/typescript/worker/lib/config.ts new file mode 100644 index 00000000..a3492e99 --- /dev/null +++ b/src/typescript/worker/lib/config.ts @@ -0,0 +1,153 @@ +import * as path from 'path'; + +import type * as ts from 'typescript'; + +import type { FilesChange } from '../../../files-change'; +import type { Issue } from '../../../issue'; +import forwardSlash from '../../../utils/path/forwardSlash'; +import type { TypeScriptConfigurationOverwrite } from '../../TypeScriptConfigurationOverwrite'; + +import { createIssuesFromDiagnostics } from './diagnostics'; +import { extensions } from './extensions'; +import { system } from './system'; +import { typescript } from './typescript'; +import { config } from './worker-config'; + +let parsedConfig: ts.ParsedCommandLine | undefined; +let parseConfigDiagnostics: ts.Diagnostic[] = []; + +// initialize ParseConfigFileHost +let parseConfigFileHost: ts.ParseConfigFileHost = { + ...system, + onUnRecoverableConfigFileDiagnostic: (diagnostic) => { + parseConfigDiagnostics.push(diagnostic); + }, +}; +for (const extension of extensions) { + if (extension.extendParseConfigFileHost) { + parseConfigFileHost = extension.extendParseConfigFileHost(parseConfigFileHost); + } +} + +export function parseConfig( + configFileName: string, + configFileContext: string, + configOverwriteJSON: TypeScriptConfigurationOverwrite = {} +): ts.ParsedCommandLine { + const configFilePath = forwardSlash(configFileName); + const parsedConfigFileJSON = typescript.readConfigFile( + configFilePath, + parseConfigFileHost.readFile + ); + + const overwrittenConfigFileJSON = { + ...(parsedConfigFileJSON.config || {}), + ...configOverwriteJSON, + compilerOptions: { + ...((parsedConfigFileJSON.config || {}).compilerOptions || {}), + ...(configOverwriteJSON.compilerOptions || {}), + }, + }; + + const parsedConfigFile = typescript.parseJsonConfigFileContent( + overwrittenConfigFileJSON, + parseConfigFileHost, + configFileContext + ); + + return { + ...parsedConfigFile, + options: { + ...parsedConfigFile.options, + configFilePath: configFilePath, + }, + errors: parsedConfigFileJSON.error ? [parsedConfigFileJSON.error] : parsedConfigFile.errors, + }; +} + +export function getParseConfigIssues(): Issue[] { + const issues = createIssuesFromDiagnostics(parseConfigDiagnostics); + + issues.forEach((issue) => { + if (!issue.file) { + issue.file = config.configFile; + } + }); + + return issues; +} + +export function getParsedConfig(force = false) { + if (!parsedConfig || force) { + parseConfigDiagnostics = []; + + parsedConfig = parseConfig(config.configFile, config.context, config.configOverwrite); + + const configFilePath = forwardSlash(config.configFile); + const parsedConfigFileJSON = typescript.readConfigFile( + configFilePath, + parseConfigFileHost.readFile + ); + const overwrittenConfigFileJSON = { + ...(parsedConfigFileJSON.config || {}), + ...config.configOverwrite, + compilerOptions: { + ...((parsedConfigFileJSON.config || {}).compilerOptions || {}), + ...(config.configOverwrite.compilerOptions || {}), + }, + }; + parsedConfig = typescript.parseJsonConfigFileContent( + overwrittenConfigFileJSON, + parseConfigFileHost, + config.context + ); + parsedConfig.options.configFilePath = configFilePath; + parsedConfig.errors = parsedConfigFileJSON.error + ? [parsedConfigFileJSON.error] + : parsedConfig.errors; + + if (parsedConfig.errors) { + parseConfigDiagnostics.push(...parsedConfig.errors); + } + } + + return parsedConfig; +} + +export function parseNextConfig() { + const prevParsedConfig = parsedConfig; + const nextParsedConfig = getParsedConfig(true); + + return [prevParsedConfig, nextParsedConfig] as const; +} + +export function invalidateConfig() { + parsedConfig = undefined; + parseConfigDiagnostics = []; +} + +export function getConfigFilePathFromCompilerOptions(compilerOptions: ts.CompilerOptions): string { + return compilerOptions.configFilePath as unknown as string; +} + +export function getConfigFilePathFromProgram(program: ts.Program): string { + return getConfigFilePathFromCompilerOptions(program.getCompilerOptions()); +} + +export function getConfigFilePathFromBuilderProgram(builderProgram: ts.BuilderProgram): string { + return getConfigFilePathFromCompilerOptions(builderProgram.getProgram().getCompilerOptions()); +} + +export function didConfigFileChanged({ changedFiles = [], deletedFiles = [] }: FilesChange) { + return [...changedFiles, ...deletedFiles] + .map((file) => path.normalize(file)) + .includes(path.normalize(config.configFile)); +} + +export function didRootFilesChanged() { + const [prevConfig, nextConfig] = parseNextConfig(); + + return ( + prevConfig && JSON.stringify(prevConfig.fileNames) !== JSON.stringify(nextConfig.fileNames) + ); +} diff --git a/src/typescript/worker/lib/dependencies.ts b/src/typescript/worker/lib/dependencies.ts new file mode 100644 index 00000000..2e7812f8 --- /dev/null +++ b/src/typescript/worker/lib/dependencies.ts @@ -0,0 +1,85 @@ +import * as path from 'path'; + +import type * as ts from 'typescript'; + +import type { FilesMatch } from '../../../files-match'; + +import { getParsedConfig, parseConfig } from './config'; +import { extensions } from './extensions'; +import { typescript } from './typescript'; +import { config } from './worker-config'; + +let dependencies: FilesMatch | undefined; + +export function getDependencies(force = false): FilesMatch { + if (!dependencies || force) { + const parsedConfig = getParsedConfig(); + + dependencies = getDependenciesWorker(parsedConfig, config.context); + + for (const extension of extensions) { + if (extension.extendDependencies) { + dependencies = extension.extendDependencies(dependencies); + } + } + } + + return dependencies; +} + +export function invalidateDependencies() { + dependencies = undefined; +} + +function getDependenciesWorker( + parsedConfig: ts.ParsedCommandLine, + configFileContext: string, + processedConfigFiles: string[] = [] +): FilesMatch { + const files = new Set(parsedConfig.fileNames); + const configFilePath = parsedConfig.options.configFilePath; + if (typeof configFilePath === 'string') { + files.add(configFilePath); + } + const dirs = new Set(Object.keys(parsedConfig.wildcardDirectories || {})); + const excluded = new Set( + (parsedConfig.raw?.exclude || []).map((filePath: string) => + path.resolve(configFileContext, filePath) + ) + ); + + for (const projectReference of parsedConfig.projectReferences || []) { + const childConfigFilePath = typescript.resolveProjectReferencePath(projectReference); + const childConfigContext = path.dirname(childConfigFilePath); + if (processedConfigFiles.includes(childConfigFilePath)) { + // handle circular dependencies + continue; + } + const childParsedConfiguration = parseConfig(childConfigFilePath, childConfigContext); + const childDependencies = getDependenciesWorker(childParsedConfiguration, childConfigContext, [ + ...processedConfigFiles, + childConfigFilePath, + ]); + childDependencies.files.forEach((file) => { + files.add(file); + }); + childDependencies.dirs.forEach((dir) => { + dirs.add(dir); + }); + } + + const extensions = [ + typescript.Extension.Ts, + typescript.Extension.Tsx, + typescript.Extension.Js, + typescript.Extension.Jsx, + typescript.Extension.TsBuildInfo, + ]; + + return { + files: Array.from(files).map((file) => path.normalize(file)), + dirs: Array.from(dirs).map((dir) => path.normalize(dir)), + excluded: Array.from(excluded).map((aPath) => path.normalize(aPath)), + extensions: extensions, + }; +} diff --git a/src/typescript/worker/lib/diagnostics.ts b/src/typescript/worker/lib/diagnostics.ts new file mode 100644 index 00000000..0e0b4f2a --- /dev/null +++ b/src/typescript/worker/lib/diagnostics.ts @@ -0,0 +1,99 @@ +import * as os from 'os'; + +import type * as ts from 'typescript'; + +import type { Issue, IssueLocation } from '../../../issue'; +import { deduplicateAndSortIssues } from '../../../issue'; + +import { extensions } from './extensions'; +import { typescript } from './typescript'; +import { config } from './worker-config'; + +const diagnosticsPerConfigFile = new Map(); + +export function updateDiagnostics(configFile: string, diagnostics: ts.Diagnostic[]): void { + diagnosticsPerConfigFile.set(configFile, diagnostics); +} + +export function getIssues(): Issue[] { + const allDiagnostics: ts.Diagnostic[] = []; + + diagnosticsPerConfigFile.forEach((diagnostics) => { + allDiagnostics.push(...diagnostics); + }); + + return createIssuesFromDiagnostics(allDiagnostics); +} + +export function invalidateDiagnostics(): void { + diagnosticsPerConfigFile.clear(); +} + +export function getDiagnosticsOfProgram(program: ts.Program | ts.BuilderProgram): ts.Diagnostic[] { + const programDiagnostics: ts.Diagnostic[] = []; + + if (config.diagnosticOptions.syntactic) { + programDiagnostics.push(...program.getSyntacticDiagnostics()); + } + if (config.diagnosticOptions.global) { + programDiagnostics.push(...program.getGlobalDiagnostics()); + } + if (config.diagnosticOptions.semantic) { + programDiagnostics.push(...program.getSemanticDiagnostics()); + } + if (config.diagnosticOptions.declaration) { + programDiagnostics.push(...program.getDeclarationDiagnostics()); + } + + return programDiagnostics; +} + +function createIssueFromDiagnostic(diagnostic: ts.Diagnostic): Issue { + let file: string | undefined; + let location: IssueLocation | undefined; + + if (diagnostic.file) { + file = diagnostic.file.fileName; + + if (diagnostic.start && diagnostic.length) { + const { line: startLine, character: startCharacter } = + diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + const { line: endLine, character: endCharacter } = + diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start + diagnostic.length); + + location = { + start: { + line: startLine + 1, + column: startCharacter + 1, + }, + end: { + line: endLine + 1, + column: endCharacter + 1, + }, + }; + } + } + + return { + code: 'TS' + String(diagnostic.code), + // we don't handle Suggestion and Message diagnostics + severity: diagnostic.category === 0 ? 'warning' : 'error', + message: typescript.flattenDiagnosticMessageText(diagnostic.messageText, os.EOL), + file, + location, + }; +} + +export function createIssuesFromDiagnostics(diagnostics: ts.Diagnostic[]): Issue[] { + let issues = deduplicateAndSortIssues( + diagnostics.map((diagnostic) => createIssueFromDiagnostic(diagnostic)) + ); + + extensions.forEach((extension) => { + if (extension.extendIssues) { + issues = extension.extendIssues(issues); + } + }); + + return issues; +} diff --git a/src/typescript/worker/lib/extensions.ts b/src/typescript/worker/lib/extensions.ts new file mode 100644 index 00000000..62bfdbf8 --- /dev/null +++ b/src/typescript/worker/lib/extensions.ts @@ -0,0 +1,12 @@ +import type { TypeScriptExtension } from '../../extension/TypeScriptExtension'; +import { createTypeScriptVueExtension } from '../../extension/vue/TypeScriptVueExtension'; + +import { config } from './worker-config'; + +const extensions: TypeScriptExtension[] = []; + +if (config.extensions?.vue?.enabled) { + extensions.push(createTypeScriptVueExtension(config.extensions.vue)); +} + +export { extensions }; diff --git a/src/typescript-reporter/file-system/FileSystem.ts b/src/typescript/worker/lib/file-system/file-system.ts similarity index 93% rename from src/typescript-reporter/file-system/FileSystem.ts rename to src/typescript/worker/lib/file-system/file-system.ts index 9aeddd87..9bfd270b 100644 --- a/src/typescript-reporter/file-system/FileSystem.ts +++ b/src/typescript/worker/lib/file-system/file-system.ts @@ -4,7 +4,7 @@ import type { Dirent, Stats } from 'fs'; /** * Interface to abstract file system implementation details. */ -interface FileSystem { +export interface FileSystem { // read exists(path: string): boolean; readFile(path: string, encoding?: string): string | undefined; @@ -22,5 +22,3 @@ interface FileSystem { // cache clearCache(): void; } - -export { FileSystem }; diff --git a/src/typescript/worker/lib/file-system/mem-file-system.ts b/src/typescript/worker/lib/file-system/mem-file-system.ts new file mode 100644 index 00000000..d1d53949 --- /dev/null +++ b/src/typescript/worker/lib/file-system/mem-file-system.ts @@ -0,0 +1,95 @@ +import type { Dirent, Stats } from 'fs'; +import { dirname } from 'path'; + +import { fs as mem } from 'memfs'; + +import type { FileSystem } from './file-system'; +import { realFileSystem } from './real-file-system'; + +/** + * It's an implementation of FileSystem interface which reads and writes to the in-memory file system. + */ +export const memFileSystem: FileSystem = { + ...realFileSystem, + exists(path: string) { + return exists(realFileSystem.realPath(path)); + }, + readFile(path: string, encoding?: string) { + return readFile(realFileSystem.realPath(path), encoding); + }, + readDir(path: string) { + return readDir(realFileSystem.realPath(path)); + }, + readStats(path: string) { + return readStats(realFileSystem.realPath(path)); + }, + writeFile(path: string, data: string) { + writeFile(realFileSystem.realPath(path), data); + }, + deleteFile(path: string) { + deleteFile(realFileSystem.realPath(path)); + }, + createDir(path: string) { + createDir(realFileSystem.realPath(path)); + }, + updateTimes(path: string, atime: Date, mtime: Date) { + updateTimes(realFileSystem.realPath(path), atime, mtime); + }, + clearCache() { + realFileSystem.clearCache(); + }, +}; + +function exists(path: string): boolean { + return mem.existsSync(realFileSystem.normalizePath(path)); +} + +function readStats(path: string): Stats | undefined { + return exists(path) ? mem.statSync(realFileSystem.normalizePath(path)) : undefined; +} + +function readFile(path: string, encoding?: string): string | undefined { + const stats = readStats(path); + + if (stats && stats.isFile()) { + return mem + .readFileSync(realFileSystem.normalizePath(path), { encoding: encoding as BufferEncoding }) + .toString(); + } +} + +function readDir(path: string): Dirent[] { + const stats = readStats(path); + + if (stats && stats.isDirectory()) { + return mem.readdirSync(realFileSystem.normalizePath(path), { + withFileTypes: true, + }) as Dirent[]; + } + + return []; +} + +function createDir(path: string) { + mem.mkdirSync(realFileSystem.normalizePath(path), { recursive: true }); +} + +function writeFile(path: string, data: string) { + if (!exists(dirname(path))) { + createDir(dirname(path)); + } + + mem.writeFileSync(realFileSystem.normalizePath(path), data); +} + +function deleteFile(path: string) { + if (exists(path)) { + mem.unlinkSync(realFileSystem.normalizePath(path)); + } +} + +function updateTimes(path: string, atime: Date, mtime: Date) { + if (exists(path)) { + mem.utimesSync(realFileSystem.normalizePath(path), atime, mtime); + } +} diff --git a/src/typescript/worker/lib/file-system/passive-file-system.ts b/src/typescript/worker/lib/file-system/passive-file-system.ts new file mode 100644 index 00000000..bffcb196 --- /dev/null +++ b/src/typescript/worker/lib/file-system/passive-file-system.ts @@ -0,0 +1,70 @@ +import type { FileSystem } from './file-system'; +import { memFileSystem } from './mem-file-system'; +import { realFileSystem } from './real-file-system'; + +/** + * It's an implementation of FileSystem interface which reads from the real file system, but write to the in-memory file system. + */ +export const passiveFileSystem: FileSystem = { + ...memFileSystem, + exists(path: string) { + return exists(realFileSystem.realPath(path)); + }, + readFile(path: string, encoding?: string) { + return readFile(realFileSystem.realPath(path), encoding); + }, + readDir(path: string) { + return readDir(realFileSystem.realPath(path)); + }, + readStats(path: string) { + return readStats(realFileSystem.realPath(path)); + }, + realPath(path: string) { + return realFileSystem.realPath(path); + }, + clearCache() { + realFileSystem.clearCache(); + }, +}; + +function exists(path: string) { + return realFileSystem.exists(path) || memFileSystem.exists(path); +} + +function readFile(path: string, encoding?: string) { + const fsStats = realFileSystem.readStats(path); + const memStats = memFileSystem.readStats(path); + + if (fsStats && memStats) { + return fsStats.mtimeMs > memStats.mtimeMs + ? realFileSystem.readFile(path, encoding) + : memFileSystem.readFile(path, encoding); + } else if (fsStats) { + return realFileSystem.readFile(path, encoding); + } else if (memStats) { + return memFileSystem.readFile(path, encoding); + } +} + +function readDir(path: string) { + const fsDirents = realFileSystem.readDir(path); + const memDirents = memFileSystem.readDir(path); + + // merge list of dirents from fs and mem + return fsDirents + .filter((fsDirent) => !memDirents.some((memDirent) => memDirent.name === fsDirent.name)) + .concat(memDirents); +} + +function readStats(path: string) { + const fsStats = realFileSystem.readStats(path); + const memStats = memFileSystem.readStats(path); + + if (fsStats && memStats) { + return fsStats.mtimeMs > memStats.mtimeMs ? fsStats : memStats; + } else if (fsStats) { + return fsStats; + } else if (memStats) { + return memStats; + } +} diff --git a/src/typescript/worker/lib/file-system/real-file-system.ts b/src/typescript/worker/lib/file-system/real-file-system.ts new file mode 100644 index 00000000..d6b75863 --- /dev/null +++ b/src/typescript/worker/lib/file-system/real-file-system.ts @@ -0,0 +1,204 @@ +import type { Dirent, Stats } from 'fs'; +import { dirname, basename, join, normalize } from 'path'; + +import * as fs from 'fs-extra'; + +import type { FileSystem } from './file-system'; + +const existsCache = new Map(); +const readStatsCache = new Map(); +const readFileCache = new Map(); +const readDirCache = new Map(); +const realPathCache = new Map(); + +/** + * It's an implementation of the FileSystem interface which reads and writes directly to the real file system. + */ +export const realFileSystem: FileSystem = { + exists(path: string) { + return exists(getRealPath(path)); + }, + readFile(path: string, encoding?: string) { + return readFile(getRealPath(path), encoding); + }, + readDir(path: string) { + return readDir(getRealPath(path)); + }, + readStats(path: string) { + return readStats(getRealPath(path)); + }, + realPath(path: string) { + return getRealPath(path); + }, + normalizePath(path: string) { + return normalize(path); + }, + writeFile(path: string, data: string) { + writeFile(getRealPath(path), data); + }, + deleteFile(path: string) { + deleteFile(getRealPath(path)); + }, + createDir(path: string) { + createDir(getRealPath(path)); + }, + updateTimes(path: string, atime: Date, mtime: Date) { + updateTimes(getRealPath(path), atime, mtime); + }, + clearCache() { + existsCache.clear(); + readStatsCache.clear(); + readFileCache.clear(); + readDirCache.clear(); + realPathCache.clear(); + }, +}; + +// read methods +function exists(path: string): boolean { + const normalizedPath = normalize(path); + + if (!existsCache.has(normalizedPath)) { + existsCache.set(normalizedPath, fs.existsSync(normalizedPath)); + } + + return !!existsCache.get(normalizedPath); +} + +function readStats(path: string): Stats | undefined { + const normalizedPath = normalize(path); + + if (!readStatsCache.has(normalizedPath)) { + if (exists(normalizedPath)) { + readStatsCache.set(normalizedPath, fs.statSync(normalizedPath)); + } + } + + return readStatsCache.get(normalizedPath); +} + +function readFile(path: string, encoding?: string): string | undefined { + const normalizedPath = normalize(path); + + if (!readFileCache.has(normalizedPath)) { + const stats = readStats(normalizedPath); + + if (stats && stats.isFile()) { + readFileCache.set( + normalizedPath, + fs.readFileSync(normalizedPath, { encoding: encoding as BufferEncoding }).toString() + ); + } else { + readFileCache.set(normalizedPath, undefined); + } + } + + return readFileCache.get(normalizedPath); +} + +function readDir(path: string): Dirent[] { + const normalizedPath = normalize(path); + + if (!readDirCache.has(normalizedPath)) { + const stats = readStats(normalizedPath); + + if (stats && stats.isDirectory()) { + readDirCache.set(normalizedPath, fs.readdirSync(normalizedPath, { withFileTypes: true })); + } else { + readDirCache.set(normalizedPath, []); + } + } + + return readDirCache.get(normalizedPath) || []; +} + +function getRealPath(path: string) { + const normalizedPath = normalize(path); + + if (!realPathCache.has(normalizedPath)) { + let base = normalizedPath; + let nested = ''; + + while (base !== dirname(base)) { + if (exists(base)) { + realPathCache.set(normalizedPath, normalize(join(fs.realpathSync(base), nested))); + break; + } + + nested = join(basename(base), nested); + base = dirname(base); + } + } + + return realPathCache.get(normalizedPath) || normalizedPath; +} + +function createDir(path: string) { + const normalizedPath = normalize(path); + + fs.mkdirSync(normalizedPath, { recursive: true }); + + // update cache + existsCache.set(normalizedPath, true); + if (readDirCache.has(dirname(normalizedPath))) { + readDirCache.delete(dirname(normalizedPath)); + } + if (readStatsCache.has(normalizedPath)) { + readStatsCache.delete(normalizedPath); + } +} + +function writeFile(path: string, data: string) { + const normalizedPath = normalize(path); + + if (!exists(dirname(normalizedPath))) { + createDir(dirname(normalizedPath)); + } + + fs.writeFileSync(normalizedPath, data); + + // update cache + existsCache.set(normalizedPath, true); + if (readDirCache.has(dirname(normalizedPath))) { + readDirCache.delete(dirname(normalizedPath)); + } + if (readStatsCache.has(normalizedPath)) { + readStatsCache.delete(normalizedPath); + } + if (readFileCache.has(normalizedPath)) { + readFileCache.delete(normalizedPath); + } +} + +function deleteFile(path: string) { + if (exists(path)) { + const normalizedPath = normalize(path); + + fs.unlinkSync(normalizedPath); + + // update cache + existsCache.set(normalizedPath, false); + if (readDirCache.has(dirname(normalizedPath))) { + readDirCache.delete(dirname(normalizedPath)); + } + if (readStatsCache.has(normalizedPath)) { + readStatsCache.delete(normalizedPath); + } + if (readFileCache.has(normalizedPath)) { + readFileCache.delete(normalizedPath); + } + } +} + +function updateTimes(path: string, atime: Date, mtime: Date) { + if (exists(path)) { + const normalizedPath = normalize(path); + + fs.utimesSync(normalize(path), atime, mtime); + + // update cache + if (readStatsCache.has(normalizedPath)) { + readStatsCache.delete(normalizedPath); + } + } +} diff --git a/src/typescript/worker/lib/host/compiler-host.ts b/src/typescript/worker/lib/host/compiler-host.ts new file mode 100644 index 00000000..e31e76f0 --- /dev/null +++ b/src/typescript/worker/lib/host/compiler-host.ts @@ -0,0 +1,26 @@ +import type * as ts from 'typescript'; + +import { extensions } from '../extensions'; +import { system } from '../system'; +import { typescript } from '../typescript'; + +export function createCompilerHost(parsedConfig: ts.ParsedCommandLine): ts.CompilerHost { + const baseCompilerHost = typescript.createCompilerHost(parsedConfig.options); + + let controlledCompilerHost: ts.CompilerHost = { + ...baseCompilerHost, + fileExists: system.fileExists, + readFile: system.readFile, + directoryExists: system.directoryExists, + getDirectories: system.getDirectories, + realpath: system.realpath, + }; + + extensions.forEach((extension) => { + if (extension.extendCompilerHost) { + controlledCompilerHost = extension.extendCompilerHost(controlledCompilerHost, parsedConfig); + } + }); + + return controlledCompilerHost; +} diff --git a/src/typescript-reporter/reporter/ControlledWatchCompilerHost.ts b/src/typescript/worker/lib/host/watch-compiler-host.ts similarity index 62% rename from src/typescript-reporter/reporter/ControlledWatchCompilerHost.ts rename to src/typescript/worker/lib/host/watch-compiler-host.ts index dc7c1c4a..794f519d 100644 --- a/src/typescript-reporter/reporter/ControlledWatchCompilerHost.ts +++ b/src/typescript/worker/lib/host/watch-compiler-host.ts @@ -1,27 +1,24 @@ import type * as ts from 'typescript'; -import type { TypeScriptHostExtension } from '../extension/TypeScriptExtension'; +import { extensions } from '../extensions'; +import { system } from '../system'; +import { typescript } from '../typescript'; -import type { ControlledTypeScriptSystem } from './ControlledTypeScriptSystem'; - -function createControlledWatchCompilerHost( - typescript: typeof ts, - parsedCommandLine: ts.ParsedCommandLine, - system: ControlledTypeScriptSystem, +export function createWatchCompilerHost( + parsedConfig: ts.ParsedCommandLine, createProgram?: ts.CreateProgram, reportDiagnostic?: ts.DiagnosticReporter, reportWatchStatus?: ts.WatchStatusReporter, - afterProgramCreate?: (program: TProgram) => void, - hostExtensions: TypeScriptHostExtension[] = [] + afterProgramCreate?: (program: TProgram) => void ): ts.WatchCompilerHostOfFilesAndCompilerOptions { const baseWatchCompilerHost = typescript.createWatchCompilerHost( - parsedCommandLine.fileNames, - parsedCommandLine.options, + parsedConfig.fileNames, + parsedConfig.options, system, createProgram, reportDiagnostic, reportWatchStatus, - parsedCommandLine.projectReferences + parsedConfig.projectReferences ); let controlledWatchCompilerHost: ts.WatchCompilerHostOfFilesAndCompilerOptions = { @@ -34,9 +31,9 @@ function createControlledWatchCompilerHost( configFileParsingDiagnostics?: ReadonlyArray, projectReferences?: ReadonlyArray | undefined ): TProgram { - hostExtensions.forEach((hostExtension) => { - if (compilerHost && hostExtension.extendCompilerHost) { - compilerHost = hostExtension.extendCompilerHost(compilerHost, parsedCommandLine); + extensions.forEach((extension) => { + if (compilerHost && extension.extendCompilerHost) { + compilerHost = extension.extendCompilerHost(compilerHost, parsedConfig); } }); @@ -68,16 +65,14 @@ function createControlledWatchCompilerHost( realpath: system.realpath, }; - hostExtensions.forEach((hostExtension) => { - if (hostExtension.extendWatchCompilerHost) { - controlledWatchCompilerHost = hostExtension.extendWatchCompilerHost< + extensions.forEach((extension) => { + if (extension.extendWatchCompilerHost) { + controlledWatchCompilerHost = extension.extendWatchCompilerHost< TProgram, ts.WatchCompilerHostOfFilesAndCompilerOptions - >(controlledWatchCompilerHost, parsedCommandLine); + >(controlledWatchCompilerHost, parsedConfig); } }); return controlledWatchCompilerHost; } - -export { createControlledWatchCompilerHost }; diff --git a/src/typescript-reporter/reporter/ControlledWatchSolutionBuilderHost.ts b/src/typescript/worker/lib/host/watch-solution-builder-host.ts similarity index 66% rename from src/typescript-reporter/reporter/ControlledWatchSolutionBuilderHost.ts rename to src/typescript/worker/lib/host/watch-solution-builder-host.ts index 77b92fec..b5eacf24 100644 --- a/src/typescript-reporter/reporter/ControlledWatchSolutionBuilderHost.ts +++ b/src/typescript/worker/lib/host/watch-solution-builder-host.ts @@ -1,31 +1,26 @@ import type * as ts from 'typescript'; -import type { TypeScriptHostExtension } from '../extension/TypeScriptExtension'; +import { extensions } from '../extensions'; +import { system } from '../system'; +import { typescript } from '../typescript'; -import type { ControlledTypeScriptSystem } from './ControlledTypeScriptSystem'; -import { createControlledWatchCompilerHost } from './ControlledWatchCompilerHost'; +import { createWatchCompilerHost } from './watch-compiler-host'; -function createControlledWatchSolutionBuilderHost( - typescript: typeof ts, - parsedCommandLine: ts.ParsedCommandLine, - system: ControlledTypeScriptSystem, +export function createWatchSolutionBuilderHost( + parsedConfig: ts.ParsedCommandLine, createProgram?: ts.CreateProgram, reportDiagnostic?: ts.DiagnosticReporter, reportWatchStatus?: ts.WatchStatusReporter, reportSolutionBuilderStatus?: (diagnostic: ts.Diagnostic) => void, afterProgramCreate?: (program: TProgram) => void, - afterProgramEmitAndDiagnostics?: (program: TProgram) => void, - hostExtensions: TypeScriptHostExtension[] = [] + afterProgramEmitAndDiagnostics?: (program: TProgram) => void ): ts.SolutionBuilderWithWatchHost { - const controlledWatchCompilerHost = createControlledWatchCompilerHost( - typescript, - parsedCommandLine, - system, + const controlledWatchCompilerHost = createWatchCompilerHost( + parsedConfig, createProgram, reportDiagnostic, reportWatchStatus, - afterProgramCreate, - hostExtensions + afterProgramCreate ); let controlledWatchSolutionBuilderHost: ts.SolutionBuilderWithWatchHost = { @@ -76,16 +71,14 @@ function createControlledWatchSolutionBuilderHost { - if (hostExtension.extendWatchSolutionBuilderHost) { - controlledWatchSolutionBuilderHost = hostExtension.extendWatchSolutionBuilderHost< + extensions.forEach((extension) => { + if (extension.extendWatchSolutionBuilderHost) { + controlledWatchSolutionBuilderHost = extension.extendWatchSolutionBuilderHost< TProgram, ts.SolutionBuilderWithWatchHost - >(controlledWatchSolutionBuilderHost, parsedCommandLine); + >(controlledWatchSolutionBuilderHost, parsedConfig); } }); return controlledWatchSolutionBuilderHost; } - -export { createControlledWatchSolutionBuilderHost }; diff --git a/src/typescript/worker/lib/performance.ts b/src/typescript/worker/lib/performance.ts new file mode 100644 index 00000000..6f0aa16b --- /dev/null +++ b/src/typescript/worker/lib/performance.ts @@ -0,0 +1,33 @@ +import { typescript } from './typescript'; +import { config } from './worker-config'; + +interface TypeScriptPerformance { + enable?(): void; + disable?(): void; + forEachMeasure?(callback: (measureName: string, duration: number) => void): void; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const performance: TypeScriptPerformance | undefined = (typescript as any).performance; + +export function enablePerformanceIfNeeded() { + if (config.profile) { + performance?.enable?.(); + } +} + +export function disablePerformanceIfNeeded() { + if (config.profile) { + performance?.disable?.(); + } +} + +export function printPerformanceMeasuresIfNeeded() { + if (config.profile) { + const measures: Record = {}; + performance?.forEachMeasure?.((measureName, duration) => { + measures[measureName] = duration; + }); + console.table(measures); + } +} diff --git a/src/typescript/worker/lib/program/program.ts b/src/typescript/worker/lib/program/program.ts new file mode 100644 index 00000000..2475f1dd --- /dev/null +++ b/src/typescript/worker/lib/program/program.ts @@ -0,0 +1,34 @@ +import type * as ts from 'typescript'; + +import { getConfigFilePathFromProgram, getParsedConfig } from '../config'; +import { updateDiagnostics, getDiagnosticsOfProgram } from '../diagnostics'; +import { createCompilerHost } from '../host/compiler-host'; +import { typescript } from '../typescript'; + +let compilerHost: ts.CompilerHost | undefined; +let program: ts.Program | undefined; + +export function useProgram() { + const parsedConfig = getParsedConfig(); + + if (!compilerHost) { + compilerHost = createCompilerHost(parsedConfig); + } + if (!program) { + program = typescript.createProgram({ + rootNames: parsedConfig.fileNames, + options: parsedConfig.options, + projectReferences: parsedConfig.projectReferences, + host: compilerHost, + }); + } + + updateDiagnostics(getConfigFilePathFromProgram(program), getDiagnosticsOfProgram(program)); +} + +export function invalidateProgram(withHost = false) { + if (withHost) { + compilerHost = undefined; + } + program = undefined; +} diff --git a/src/typescript/worker/lib/program/solution-builder.ts b/src/typescript/worker/lib/program/solution-builder.ts new file mode 100644 index 00000000..57a266cc --- /dev/null +++ b/src/typescript/worker/lib/program/solution-builder.ts @@ -0,0 +1,71 @@ +import type * as ts from 'typescript'; + +import { getConfigFilePathFromBuilderProgram, getParsedConfig } from '../config'; +import { updateDiagnostics, getDiagnosticsOfProgram } from '../diagnostics'; +import { createWatchSolutionBuilderHost } from '../host/watch-solution-builder-host'; +import { startTracingIfNeeded, stopTracingIfNeeded } from '../tracing'; +import { emitTsBuildInfoIfNeeded } from '../tsbuildinfo'; +import { typescript } from '../typescript'; +import { config } from '../worker-config'; + +let solutionBuilderHost: + | ts.SolutionBuilderWithWatchHost + | undefined; +let solutionBuilder: ts.SolutionBuilder | undefined; + +export function useSolutionBuilder() { + if (!solutionBuilderHost) { + const parsedConfig = getParsedConfig(); + + solutionBuilderHost = createWatchSolutionBuilderHost( + parsedConfig, + ( + rootNames, + compilerOptions, + host, + oldProgram, + configFileParsingDiagnostics, + projectReferences + ) => { + if (compilerOptions) { + startTracingIfNeeded(compilerOptions); + } + return typescript.createSemanticDiagnosticsBuilderProgram( + rootNames, + compilerOptions, + host, + oldProgram, + configFileParsingDiagnostics, + projectReferences + ); + }, + undefined, + undefined, + undefined, + undefined, + (builderProgram) => { + updateDiagnostics( + getConfigFilePathFromBuilderProgram(builderProgram), + getDiagnosticsOfProgram(builderProgram) + ); + emitTsBuildInfoIfNeeded(builderProgram); + stopTracingIfNeeded(builderProgram); + } + ); + } + if (!solutionBuilder) { + solutionBuilder = typescript.createSolutionBuilderWithWatch( + solutionBuilderHost, + [config.configFile], + { watch: true } + ); + solutionBuilder.build(); + } +} + +export function invalidateSolutionBuilder(withHost = false) { + if (withHost) { + solutionBuilderHost = undefined; + } + solutionBuilder = undefined; +} diff --git a/src/typescript/worker/lib/program/watch-program.ts b/src/typescript/worker/lib/program/watch-program.ts new file mode 100644 index 00000000..80df0600 --- /dev/null +++ b/src/typescript/worker/lib/program/watch-program.ts @@ -0,0 +1,78 @@ +import type * as ts from 'typescript'; + +import { getConfigFilePathFromBuilderProgram, getParsedConfig } from '../config'; +import { getDependencies } from '../dependencies'; +import { updateDiagnostics, getDiagnosticsOfProgram } from '../diagnostics'; +import { createWatchCompilerHost } from '../host/watch-compiler-host'; +import { startTracingIfNeeded, stopTracingIfNeeded } from '../tracing'; +import { emitTsBuildInfoIfNeeded } from '../tsbuildinfo'; +import { typescript } from '../typescript'; + +let watchCompilerHost: + | ts.WatchCompilerHostOfFilesAndCompilerOptions + | undefined; +let watchProgram: + | ts.WatchOfFilesAndCompilerOptions + | undefined; +let shouldUpdateRootFiles = false; + +export function useWatchProgram() { + if (!watchCompilerHost) { + const parsedConfig = getParsedConfig(); + + watchCompilerHost = createWatchCompilerHost( + parsedConfig, + ( + rootNames, + compilerOptions, + host, + oldProgram, + configFileParsingDiagnostics, + projectReferences + ) => { + if (compilerOptions) { + startTracingIfNeeded(compilerOptions); + } + return typescript.createSemanticDiagnosticsBuilderProgram( + rootNames, + compilerOptions, + host, + oldProgram, + configFileParsingDiagnostics, + projectReferences + ); + }, + undefined, + undefined, + (builderProgram) => { + updateDiagnostics( + getConfigFilePathFromBuilderProgram(builderProgram), + getDiagnosticsOfProgram(builderProgram) + ); + emitTsBuildInfoIfNeeded(builderProgram); + stopTracingIfNeeded(builderProgram); + } + ); + watchProgram = undefined; + } + if (!watchProgram) { + watchProgram = typescript.createWatchProgram(watchCompilerHost); + } + + if (shouldUpdateRootFiles) { + // we have to update root files manually as don't use config file as a program input + watchProgram.updateRootFileNames(getDependencies().files); + shouldUpdateRootFiles = false; + } +} + +export function invalidateWatchProgram(withHost = false) { + if (withHost) { + watchCompilerHost = undefined; + } + watchProgram = undefined; +} + +export function invalidateWatchProgramRootFileNames() { + shouldUpdateRootFiles = true; +} diff --git a/src/typescript/worker/lib/system.ts b/src/typescript/worker/lib/system.ts new file mode 100644 index 00000000..c4cf6974 --- /dev/null +++ b/src/typescript/worker/lib/system.ts @@ -0,0 +1,297 @@ +import { dirname, join } from 'path'; + +import type * as ts from 'typescript'; + +import type { FilesMatch } from '../../../files-match'; +import forwardSlash from '../../../utils/path/forwardSlash'; + +import { memFileSystem } from './file-system/mem-file-system'; +import { passiveFileSystem } from './file-system/passive-file-system'; +import { realFileSystem } from './file-system/real-file-system'; +import { typescript } from './typescript'; +import { config } from './worker-config'; + +export interface ControlledTypeScriptSystem extends ts.System { + // control watcher + invokeFileCreated(path: string): void; + invokeFileChanged(path: string): void; + invokeFileDeleted(path: string): void; + // control cache + invalidateCache(): void; + // mark these methods as defined - not optional + getFileSize(path: string): number; + watchFile( + path: string, + callback: ts.FileWatcherCallback, + pollingInterval?: number, + options?: ts.WatchOptions + ): ts.FileWatcher; + watchDirectory( + path: string, + callback: ts.DirectoryWatcherCallback, + recursive?: boolean, + options?: ts.WatchOptions + ): ts.FileWatcher; + getModifiedTime(path: string): Date | undefined; + setModifiedTime(path: string, time: Date): void; + deleteFile(path: string): void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + clearTimeout(timeoutId: any): void; + // detect when all tasks scheduled by `setTimeout` finished + waitForQueued(): Promise; + // keep local version of artifacts to prevent import cycle + setArtifacts(artifacts: FilesMatch): void; +} + +const mode = config.mode; + +let artifacts: FilesMatch = { + files: [], + dirs: [], + excluded: [], + extensions: [], +}; +let isInitialRun = true; +// watchers +const fileWatcherCallbacksMap = new Map(); +const directoryWatcherCallbacksMap = new Map(); +const recursiveDirectoryWatcherCallbacksMap = new Map(); +const deletedFiles = new Map(); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const timeoutCallbacks = new Set(); + +// based on the ts.ignorePaths +const ignoredPaths = ['/node_modules/.', '/.git', '/.#']; + +export const system: ControlledTypeScriptSystem = { + ...typescript.sys, + useCaseSensitiveFileNames: true, + fileExists(path: string): boolean { + const stats = getReadFileSystem(path).readStats(path); + + return !!stats && stats.isFile(); + }, + readFile(path: string, encoding?: string): string | undefined { + return getReadFileSystem(path).readFile(path, encoding); + }, + getFileSize(path: string): number { + const stats = getReadFileSystem(path).readStats(path); + + return stats ? stats.size : 0; + }, + writeFile(path: string, data: string): void { + getWriteFileSystem(path).writeFile(path, data); + + system.invokeFileChanged(path); + }, + deleteFile(path: string): void { + getWriteFileSystem(path).deleteFile(path); + + system.invokeFileDeleted(path); + }, + directoryExists(path: string): boolean { + return Boolean(getReadFileSystem(path).readStats(path)?.isDirectory()); + }, + createDirectory(path: string): void { + getWriteFileSystem(path).createDir(path); + + invokeDirectoryWatchers(path); + }, + getDirectories(path: string): string[] { + const dirents = getReadFileSystem(path).readDir(path); + + return dirents + .filter( + (dirent) => + dirent.isDirectory() || + (dirent.isSymbolicLink() && system.directoryExists(join(path, dirent.name))) + ) + .map((dirent) => dirent.name); + }, + getModifiedTime(path: string): Date | undefined { + const stats = getReadFileSystem(path).readStats(path); + + if (stats) { + return stats.mtime; + } + }, + setModifiedTime(path: string, date: Date): void { + getWriteFileSystem(path).updateTimes(path, date, date); + + invokeDirectoryWatchers(path); + invokeFileWatchers(path, typescript.FileWatcherEventKind.Changed); + }, + watchFile(path: string, callback: ts.FileWatcherCallback): ts.FileWatcher { + return createWatcher(fileWatcherCallbacksMap, path, callback); + }, + watchDirectory( + path: string, + callback: ts.DirectoryWatcherCallback, + recursive = false + ): ts.FileWatcher { + return createWatcher( + recursive ? recursiveDirectoryWatcherCallbacksMap : directoryWatcherCallbacksMap, + path, + callback + ); + }, + // use immediate instead of timeout to avoid waiting 250ms for batching files changes + setTimeout: (callback, timeout, ...args) => { + const timeoutId = setImmediate(() => { + callback(...args); + timeoutCallbacks.delete(timeoutId); + }); + timeoutCallbacks.add(timeoutId); + + return timeoutId; + }, + clearTimeout: (timeoutId) => { + clearImmediate(timeoutId); + timeoutCallbacks.delete(timeoutId); + }, + async waitForQueued(): Promise { + while (timeoutCallbacks.size > 0) { + await new Promise((resolve) => setImmediate(resolve)); + } + isInitialRun = false; + }, + invokeFileCreated(path: string) { + const normalizedPath = realFileSystem.normalizePath(path); + + invokeFileWatchers(path, typescript.FileWatcherEventKind.Created); + invokeDirectoryWatchers(normalizedPath); + + deletedFiles.set(normalizedPath, false); + }, + invokeFileChanged(path: string) { + const normalizedPath = realFileSystem.normalizePath(path); + + if (deletedFiles.get(normalizedPath) || !fileWatcherCallbacksMap.has(normalizedPath)) { + invokeFileWatchers(path, typescript.FileWatcherEventKind.Created); + invokeDirectoryWatchers(normalizedPath); + + deletedFiles.set(normalizedPath, false); + } else { + invokeFileWatchers(path, typescript.FileWatcherEventKind.Changed); + } + }, + invokeFileDeleted(path: string) { + const normalizedPath = realFileSystem.normalizePath(path); + + if (!deletedFiles.get(normalizedPath)) { + invokeFileWatchers(path, typescript.FileWatcherEventKind.Deleted); + invokeDirectoryWatchers(path); + + deletedFiles.set(normalizedPath, true); + } + }, + invalidateCache() { + realFileSystem.clearCache(); + memFileSystem.clearCache(); + passiveFileSystem.clearCache(); + }, + setArtifacts(nextArtifacts: FilesMatch) { + artifacts = nextArtifacts; + }, +}; + +function createWatcher( + watchersMap: Map, + path: string, + callback: TCallback +) { + const normalizedPath = realFileSystem.normalizePath(path); + + const watchers = watchersMap.get(normalizedPath) || []; + const nextWatchers = [...watchers, callback]; + watchersMap.set(normalizedPath, nextWatchers); + + return { + close: () => { + const watchers = watchersMap.get(normalizedPath) || []; + const nextWatchers = watchers.filter((watcher) => watcher !== callback); + + if (nextWatchers.length > 0) { + watchersMap.set(normalizedPath, nextWatchers); + } else { + watchersMap.delete(normalizedPath); + } + }, + }; +} + +function invokeFileWatchers(path: string, event: ts.FileWatcherEventKind) { + const normalizedPath = realFileSystem.normalizePath(path); + if (normalizedPath.endsWith('.js')) { + // trigger relevant .d.ts file watcher - handles the case, when we have webpack watcher + // that points to a symlinked package + invokeFileWatchers(normalizedPath.slice(0, -3) + '.d.ts', event); + } + + const fileWatcherCallbacks = fileWatcherCallbacksMap.get(normalizedPath); + if (fileWatcherCallbacks) { + // typescript expects normalized paths with posix forward slash + fileWatcherCallbacks.forEach((fileWatcherCallback) => + fileWatcherCallback(forwardSlash(normalizedPath), event) + ); + } +} + +function invokeDirectoryWatchers(path: string) { + const normalizedPath = realFileSystem.normalizePath(path); + const directory = dirname(normalizedPath); + + if (ignoredPaths.some((ignoredPath) => forwardSlash(normalizedPath).includes(ignoredPath))) { + return; + } + + const directoryWatcherCallbacks = directoryWatcherCallbacksMap.get(directory); + if (directoryWatcherCallbacks) { + directoryWatcherCallbacks.forEach((directoryWatcherCallback) => + directoryWatcherCallback(forwardSlash(normalizedPath)) + ); + } + + recursiveDirectoryWatcherCallbacksMap.forEach( + (recursiveDirectoryWatcherCallbacks, watchedDirectory) => { + if ( + watchedDirectory === directory || + (directory.startsWith(watchedDirectory) && + forwardSlash(directory)[watchedDirectory.length] === '/') + ) { + recursiveDirectoryWatcherCallbacks.forEach((recursiveDirectoryWatcherCallback) => + recursiveDirectoryWatcherCallback(forwardSlash(normalizedPath)) + ); + } + } + ); +} + +function isArtifact(path: string) { + return ( + (artifacts.dirs.some((dir) => path.includes(dir)) || + artifacts.files.some((file) => path === file)) && + artifacts.extensions.some((extension) => path.endsWith(extension)) + ); +} + +function getReadFileSystem(path: string) { + if (!isInitialRun && (mode === 'readonly' || mode === 'write-tsbuildinfo') && isArtifact(path)) { + return memFileSystem; + } + + return passiveFileSystem; +} + +function getWriteFileSystem(path: string) { + if ( + mode === 'write-references' || + (mode === 'write-tsbuildinfo' && path.endsWith('.tsbuildinfo')) + ) { + return realFileSystem; + } + + return passiveFileSystem; +} diff --git a/src/typescript/worker/lib/tracing.ts b/src/typescript/worker/lib/tracing.ts new file mode 100644 index 00000000..b2c473d1 --- /dev/null +++ b/src/typescript/worker/lib/tracing.ts @@ -0,0 +1,40 @@ +import type * as ts from 'typescript'; + +import { getConfigFilePathFromCompilerOptions } from './config'; +import { typescript } from './typescript'; +import { config } from './worker-config'; + +// write this type as it's available only starting from TypeScript 4.1.0 +interface Tracing { + startTracing(configFilePath: string, traceDirPath: string, isBuildMode: boolean): void; + stopTracing(typeCatalog: unknown): void; + dumpLegend(): void; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const tracing: Tracing | undefined = (typescript as any).tracing; + +export function startTracingIfNeeded(compilerOptions: ts.CompilerOptions) { + if (compilerOptions.generateTrace && tracing) { + tracing.startTracing( + getConfigFilePathFromCompilerOptions(compilerOptions), + compilerOptions.generateTrace as string, + config.build + ); + } +} + +export function stopTracingIfNeeded(program: ts.BuilderProgram) { + const compilerOptions = program.getCompilerOptions(); + + if (compilerOptions.generateTrace && tracing) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + tracing.stopTracing((program.getProgram() as any).getTypeCatalog()); + } +} + +export function dumpTracingLegendIfNeeded() { + if (tracing) { + tracing.dumpLegend(); + } +} diff --git a/src/typescript/worker/lib/tsbuildinfo.ts b/src/typescript/worker/lib/tsbuildinfo.ts new file mode 100644 index 00000000..4f76f405 --- /dev/null +++ b/src/typescript/worker/lib/tsbuildinfo.ts @@ -0,0 +1,84 @@ +import * as path from 'path'; + +import type * as ts from 'typescript'; + +import { getParsedConfig } from './config'; +import { system } from './system'; +import { typescript } from './typescript'; +import { config } from './worker-config'; + +export function invalidateTsBuildInfo() { + const parsedConfig = getParsedConfig(); + + // try to remove outdated .tsbuildinfo file for incremental mode + if ( + typeof typescript.getTsBuildInfoEmitOutputFilePath === 'function' && + config.mode !== 'readonly' && + parsedConfig.options.incremental + ) { + const tsBuildInfoPath = typescript.getTsBuildInfoEmitOutputFilePath(parsedConfig.options); + if (tsBuildInfoPath) { + try { + system.deleteFile(tsBuildInfoPath); + } catch (error) { + // silent + } + } + } +} + +export function emitTsBuildInfoIfNeeded(builderProgram: ts.BuilderProgram) { + const parsedConfig = getParsedConfig(); + + if (config.mode !== 'readonly' && parsedConfig && isIncrementalEnabled(parsedConfig.options)) { + const program = builderProgram.getProgram(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (typeof (program as any).emitBuildInfo === 'function') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (program as any).emitBuildInfo(); + } + } +} + +export function getTsBuildInfoEmitPath(compilerOptions: ts.CompilerOptions) { + if (typeof typescript.getTsBuildInfoEmitOutputFilePath === 'function') { + return typescript.getTsBuildInfoEmitOutputFilePath(compilerOptions); + } + + const removeJsonExtension = (filePath: string) => + filePath.endsWith('.json') ? filePath.slice(0, -'.json'.length) : filePath; + + // based on the implementation from typescript + const configFile = compilerOptions.configFilePath as string; + if (!isIncrementalEnabled(compilerOptions)) { + return undefined; + } + if (compilerOptions.tsBuildInfoFile) { + return compilerOptions.tsBuildInfoFile; + } + const outPath = compilerOptions.outFile || compilerOptions.out; + let buildInfoExtensionLess; + if (outPath) { + buildInfoExtensionLess = removeJsonExtension(outPath); + } else { + if (!configFile) { + return undefined; + } + const configFileExtensionLess = removeJsonExtension(configFile); + buildInfoExtensionLess = compilerOptions.outDir + ? compilerOptions.rootDir + ? path.resolve( + compilerOptions.outDir, + path.relative(compilerOptions.rootDir, configFileExtensionLess) + ) + : path.resolve(compilerOptions.outDir, path.basename(configFileExtensionLess)) + : configFileExtensionLess; + } + return buildInfoExtensionLess + '.tsbuildinfo'; +} + +function isIncrementalEnabled(compilerOptions: ts.CompilerOptions) { + return Boolean( + (compilerOptions.incremental || compilerOptions.composite) && !compilerOptions.outFile + ); +} diff --git a/src/typescript/worker/lib/typescript.ts b/src/typescript/worker/lib/typescript.ts new file mode 100644 index 00000000..1d97a5ae --- /dev/null +++ b/src/typescript/worker/lib/typescript.ts @@ -0,0 +1,6 @@ +import type * as ts from 'typescript'; + +import { config } from './worker-config'; + +// eslint-disable-next-line +export const typescript: typeof ts = require(config.typescriptPath); diff --git a/src/typescript/worker/lib/worker-config.ts b/src/typescript/worker/lib/worker-config.ts new file mode 100644 index 00000000..1e4c8868 --- /dev/null +++ b/src/typescript/worker/lib/worker-config.ts @@ -0,0 +1,4 @@ +import { getRpcWorkerData } from '../../../utils/rpc'; +import type { TypeScriptReporterConfiguration } from '../../TypeScriptReporterConfiguration'; + +export const config = getRpcWorkerData() as TypeScriptReporterConfiguration; diff --git a/src/utils/async/pool.ts b/src/utils/async/pool.ts index b8a08969..65627303 100644 --- a/src/utils/async/pool.ts +++ b/src/utils/async/pool.ts @@ -1,5 +1,4 @@ -// provide done callback because our promise chain is a little bit complicated -type Task = (done: () => void) => Promise; +type Task = () => Promise; interface Pool { submit(task: Task): Promise; @@ -17,24 +16,12 @@ function createPool(size: number): Pool { await Promise.race(pendingPromises).catch(() => undefined); } - let resolve: (result: T) => void; - let reject: (error: Error) => void; - const taskPromise = new Promise((taskResolve, taskReject) => { - resolve = taskResolve; - reject = taskReject; + const taskPromise = task().finally(() => { + pendingPromises = pendingPromises.filter( + (pendingPromise) => pendingPromise !== taskPromise + ); }); - - const donePromise = new Promise((doneResolve) => { - task(() => { - doneResolve(undefined); - pendingPromises = pendingPromises.filter( - (pendingPromise) => pendingPromise !== donePromise - ); - }) - .then(resolve) - .catch(reject); - }); - pendingPromises.push(donePromise); + pendingPromises.push(taskPromise); return taskPromise; }, diff --git a/src/utils/rpc/error.ts b/src/utils/rpc/error.ts new file mode 100644 index 00000000..1dec3c52 --- /dev/null +++ b/src/utils/rpc/error.ts @@ -0,0 +1,12 @@ +class RpcExitError extends Error { + constructor( + message: string, + readonly code?: string | number | null, + readonly signal?: string | null + ) { + super(message); + this.name = 'RpcExitError'; + } +} + +export { RpcExitError }; diff --git a/src/utils/rpc/expose.ts b/src/utils/rpc/expose.ts new file mode 100644 index 00000000..a0721588 --- /dev/null +++ b/src/utils/rpc/expose.ts @@ -0,0 +1,62 @@ +import process from 'process'; + +import type { RpcMessage } from './types'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function exposeRpc(fn: (...args: any[]) => any) { + const sendMessage = (message: RpcMessage) => + new Promise((resolve, reject) => { + if (!process.send) { + reject(new Error(`Process ${process.pid} doesn't have IPC channels`)); + } else if (!process.connected) { + reject(new Error(`Process ${process.pid} doesn't have open IPC channels`)); + } else { + process.send(message, undefined, undefined, (error) => { + if (error) { + reject(error); + } else { + resolve(undefined); + } + }); + } + }); + const handleMessage = async (message: RpcMessage) => { + if (message.type === 'call') { + if (!process.send) { + // process disconnected - skip + return; + } + + let value: unknown; + let error: unknown; + try { + value = await fn(...message.args); + } catch (fnError) { + error = fnError; + } + + try { + if (error) { + await sendMessage({ + type: 'reject', + id: message.id, + error, + }); + } else { + await sendMessage({ + type: 'resolve', + id: message.id, + value, + }); + } + } catch (sendError) { + // we can't send things back to the parent process - let's use stdout to communicate error + if (error) { + console.error(error); + } + console.error(sendError); + } + } + }; + process.on('message', handleMessage); +} diff --git a/src/utils/rpc/index.ts b/src/utils/rpc/index.ts new file mode 100644 index 00000000..84661390 --- /dev/null +++ b/src/utils/rpc/index.ts @@ -0,0 +1,5 @@ +export { exposeRpc } from './expose'; +export { wrapRpc } from './wrap'; +export { createRpcWorker, getRpcWorkerData, RpcWorker } from './worker'; +export { RpcExitError } from './error'; +export { RpcRemoteMethod } from './types'; diff --git a/src/utils/rpc/types.ts b/src/utils/rpc/types.ts new file mode 100644 index 00000000..28b41eb3 --- /dev/null +++ b/src/utils/rpc/types.ts @@ -0,0 +1,35 @@ +interface RpcCallMessage { + type: 'call'; + id: string; + args: unknown[]; +} +interface RpcResolveMessage { + type: 'resolve'; + id: string; + value: unknown; +} +interface RpcRejectMessage { + type: 'reject'; + id: string; + error: unknown; +} +type RpcMessage = RpcCallMessage | RpcResolveMessage | RpcRejectMessage; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type RpcMethod = (...args: any[]) => any; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type RpcRemoteMethod = T extends (...args: infer A) => infer R + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + R extends Promise + ? (...args: A) => R + : (...args: A) => Promise + : (...args: unknown[]) => Promise; + +export { + RpcCallMessage, + RpcResolveMessage, + RpcRejectMessage, + RpcMessage, + RpcMethod, + RpcRemoteMethod, +}; diff --git a/src/utils/rpc/worker.ts b/src/utils/rpc/worker.ts new file mode 100644 index 00000000..c32934d4 --- /dev/null +++ b/src/utils/rpc/worker.ts @@ -0,0 +1,81 @@ +import * as child_process from 'child_process'; +import type { ChildProcess, ForkOptions } from 'child_process'; +import * as process from 'process'; + +import type { RpcMethod, RpcRemoteMethod } from './types'; +import { wrapRpc } from './wrap'; + +const WORKER_DATA_ENV_KEY = 'WORKER_DATA'; + +interface RpcWorkerBase { + connect(): void; + terminate(): void; + readonly connected: boolean; + readonly process: ChildProcess | undefined; +} +type RpcWorker = RpcWorkerBase & RpcRemoteMethod; + +function createRpcWorker( + modulePath: string, + data: unknown, + memoryLimit?: number +): RpcWorker { + const options: ForkOptions = { + env: { + ...process.env, + [WORKER_DATA_ENV_KEY]: JSON.stringify(data || {}), + }, + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + serialization: 'advanced', + }; + if (memoryLimit) { + options.execArgv = [`--max-old-space-size=${memoryLimit}`]; + } + let childProcess: ChildProcess | undefined; + let remoteMethod: RpcRemoteMethod | undefined; + + const worker: RpcWorkerBase = { + connect() { + if (childProcess && !childProcess.connected) { + childProcess.kill('SIGTERM'); + childProcess = undefined; + remoteMethod = undefined; + } + if (!childProcess?.connected) { + childProcess = child_process.fork(modulePath, options); + remoteMethod = wrapRpc(childProcess); + } + }, + terminate() { + if (childProcess) { + childProcess.kill('SIGTERM'); + childProcess = undefined; + remoteMethod = undefined; + } + }, + get connected() { + return Boolean(childProcess?.connected); + }, + get process() { + return childProcess; + }, + }; + + return Object.assign((...args: unknown[]) => { + if (!worker.connected) { + // try to auto-connect + worker.connect(); + } + + if (!remoteMethod) { + return Promise.reject('Worker is not connected - cannot perform RPC.'); + } + + return remoteMethod(...args); + }, worker) as RpcWorker; +} +function getRpcWorkerData(): unknown { + return JSON.parse(process.env[WORKER_DATA_ENV_KEY] || '{}'); +} + +export { createRpcWorker, getRpcWorkerData, RpcWorker }; diff --git a/src/utils/rpc/wrap.ts b/src/utils/rpc/wrap.ts new file mode 100644 index 00000000..85c5980d --- /dev/null +++ b/src/utils/rpc/wrap.ts @@ -0,0 +1,83 @@ +import type { ChildProcess } from 'child_process'; +import * as process from 'process'; + +import { RpcExitError } from './error'; +import type { RpcRemoteMethod, RpcMessage } from './types'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function wrapRpc any>( + childProcess: ChildProcess +): RpcRemoteMethod { + return (async (...args: unknown[]): Promise => { + if (!childProcess.send) { + throw new Error(`Process ${childProcess.pid} doesn't have IPC channels`); + } else if (!childProcess.connected) { + throw new Error(`Process ${childProcess.pid} doesn't have open IPC channels`); + } + + const id = uuid(); + + const resultPromise = new Promise((resolve, reject) => { + const handleMessage = (message: RpcMessage) => { + if (message.id === id) { + if (message.type === 'resolve') { + resolve(message.value); + unsubscribe(); + } else if (message.type === 'reject') { + reject(message.error); + unsubscribe(); + } + } + }; + const handleClose = (code: string | number | null, signal: string | null) => { + reject( + new RpcExitError( + code + ? `Process ${process.pid} exited with code "${code}" [${signal}]` + : `Process ${process.pid} exited [${signal}].`, + code, + signal + ) + ); + unsubscribe(); + }; + + const subscribe = () => { + childProcess.on('message', handleMessage); + childProcess.on('close', handleClose); + }; + const unsubscribe = () => { + childProcess.off('message', handleMessage); + childProcess.off('exit', handleClose); + }; + + subscribe(); + }); + + await new Promise((resolve, reject) => { + childProcess.send( + { + type: 'call', + id, + args, + }, + (error) => { + if (error) { + reject(error); + } else { + resolve(undefined); + } + } + ); + }); + + return resultPromise; + }) as RpcRemoteMethod; +} + +function uuid(): string { + return new Array(4) + .fill(0) + .map(() => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16)) + .join('-'); +} diff --git a/src/watch/InclusiveNodeWatchFileSystem.ts b/src/watch/InclusiveNodeWatchFileSystem.ts index 237efafa..e174bf67 100644 --- a/src/watch/InclusiveNodeWatchFileSystem.ts +++ b/src/watch/InclusiveNodeWatchFileSystem.ts @@ -5,8 +5,8 @@ import chokidar from 'chokidar'; import minimatch from 'minimatch'; import type { Compiler } from 'webpack'; +import { clearFilesChange, updateFilesChange } from '../files-change'; import type { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState'; -import { clearFilesChange, updateFilesChange } from '../reporter'; import type { WatchFileSystem } from './WatchFileSystem'; diff --git a/test/e2e/TypeScriptWatchApi.spec.ts b/test/e2e/TypeScriptWatchApi.spec.ts index 12786926..9dbe8554 100644 --- a/test/e2e/TypeScriptWatchApi.spec.ts +++ b/test/e2e/TypeScriptWatchApi.spec.ts @@ -216,7 +216,7 @@ describe('TypeScript Watch API', () => { { async: false, typescript: '~3.8.0', 'ts-loader': '^7.0.0' }, { async: true, typescript: '~4.0.0', 'ts-loader': '^8.0.0' }, { async: false, typescript: '~4.3.0', 'ts-loader': '^8.0.0' }, - ])('reports semantic error for %p', async ({ async, ...dependencies }) => { + ])('reports semantic error for %p long', async ({ async, ...dependencies }) => { await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic')); await sandbox.install('yarn', { ...dependencies }); await sandbox.patch('webpack.config.js', 'async: false,', `async: ${JSON.stringify(async)},`); diff --git a/test/e2e/fixtures/typescript-basic/webpack.config.js b/test/e2e/fixtures/typescript-basic/webpack.config.js index 148d9210..84de9072 100644 --- a/test/e2e/fixtures/typescript-basic/webpack.config.js +++ b/test/e2e/fixtures/typescript-basic/webpack.config.js @@ -29,5 +29,6 @@ module.exports = { ], infrastructureLogging: { level: 'log', + debug: /ForkTsCheckerWebpackPlugin/, }, }; diff --git a/test/unit/typescript-reporter/issue/TypeScriptIssueFactory.spec.ts b/test/unit/typescript-reporter/issue/TypeScriptIssueFactory.spec.ts deleted file mode 100644 index f6fea4a0..00000000 --- a/test/unit/typescript-reporter/issue/TypeScriptIssueFactory.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createIssuesFromTsDiagnostics } from 'lib/typescript-reporter/issue/TypeScriptIssueFactory'; -import * as ts from 'typescript'; - -describe('typescript-reporter/issue/TypeScriptIssueFactory', () => { - const TS_DIAGNOSTIC_WARNING = { - start: 100, - code: 4221, - category: 1, - messageText: 'Cannot assign string to the number type', - length: 5, - file: { - fileName: 'src/test.ts', - getLineAndCharacterOfPosition: (position: number) => ({ - line: 2, - character: 2 + Math.max(0, position - 100), - }), - }, - }; - const TS_DIAGNOSTIC_ERROR = { - start: 12, - code: 1221, - category: 0, - messageText: 'Cannot assign object to the string type', - length: 1, - file: { - fileName: 'src/index.ts', - getLineAndCharacterOfPosition: (position: number) => ({ - line: 5 + Math.max(0, position - 12), - character: 10 + Math.max(0, position - 12), - }), - }, - }; - const TS_DIAGNOSTIC_WITHOUT_FILE = { - start: 12, - code: 1221, - category: 0, - length: 4, - messageText: 'Cannot assign object to the string type', - file: undefined, - }; - - it.each([[[TS_DIAGNOSTIC_WARNING, TS_DIAGNOSTIC_ERROR, TS_DIAGNOSTIC_WITHOUT_FILE]]])( - 'creates Issues from TsDiagnostics: %p', - (tsDiagnostics) => { - const issues = createIssuesFromTsDiagnostics(ts, tsDiagnostics as ts.Diagnostic[]); - - expect(issues).toMatchSnapshot(); - } - ); -}); diff --git a/test/unit/typescript-reporter/issue/__snapshots__/TypeScriptIssueFactory.spec.ts.snap b/test/unit/typescript-reporter/issue/__snapshots__/TypeScriptIssueFactory.spec.ts.snap deleted file mode 100644 index cb526851..00000000 --- a/test/unit/typescript-reporter/issue/__snapshots__/TypeScriptIssueFactory.spec.ts.snap +++ /dev/null @@ -1,45 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`typescript-reporter/issue/TypeScriptIssueFactory creates Issues from TsDiagnostics: [[Object], [Object], [Object]] 1`] = ` -Array [ - Object { - "code": "TS4221", - "file": "src/test.ts", - "location": Object { - "end": Object { - "column": 8, - "line": 3, - }, - "start": Object { - "column": 3, - "line": 3, - }, - }, - "message": "Cannot assign string to the number type", - "severity": "error", - }, - Object { - "code": "TS1221", - "file": undefined, - "location": undefined, - "message": "Cannot assign object to the string type", - "severity": "warning", - }, - Object { - "code": "TS1221", - "file": "src/index.ts", - "location": Object { - "end": Object { - "column": 12, - "line": 7, - }, - "start": Object { - "column": 11, - "line": 6, - }, - }, - "message": "Cannot assign object to the string type", - "severity": "warning", - }, -] -`; diff --git a/test/unit/typescript-reporter/TypeScriptReporterConfiguration.spec.ts b/test/unit/typescript/TypeScriptReporterConfiguration.spec.ts similarity index 89% rename from test/unit/typescript-reporter/TypeScriptReporterConfiguration.spec.ts rename to test/unit/typescript/TypeScriptReporterConfiguration.spec.ts index d8303fa5..149b8a7d 100644 --- a/test/unit/typescript-reporter/TypeScriptReporterConfiguration.spec.ts +++ b/test/unit/typescript/TypeScriptReporterConfiguration.spec.ts @@ -1,10 +1,10 @@ import path from 'path'; -import type { TypeScriptReporterConfiguration } from 'lib/typescript-reporter/TypeScriptReporterConfiguration'; -import type { TypeScriptReporterOptions } from 'lib/typescript-reporter/TypeScriptReporterOptions'; +import type { TypeScriptReporterConfiguration } from 'lib/typescript/TypeScriptReporterConfiguration'; +import type { TypeScriptReporterOptions } from 'lib/typescript/TypeScriptReporterOptions'; import type webpack from 'webpack'; -describe('typescript-reporter/TypeScriptsReporterConfiguration', () => { +describe('typescript/TypeScriptsReporterConfiguration', () => { let compiler: webpack.Compiler; let createTypeScriptVueExtensionConfiguration: jest.Mock; const context = '/webpack/context'; @@ -50,7 +50,7 @@ describe('typescript-reporter/TypeScriptsReporterConfiguration', () => { enabled: false, compiler: 'vue-template-compiler', })); - jest.setMock('lib/typescript-reporter/extension/vue/TypeScriptVueExtensionConfiguration', { + jest.setMock('lib/typescript/extension/vue/TypeScriptVueExtensionConfiguration', { createTypeScriptVueExtensionConfiguration, }); }); @@ -103,7 +103,7 @@ describe('typescript-reporter/TypeScriptsReporterConfiguration', () => { [{ profile: true }, { ...configuration, profile: true }], ])('creates configuration from options %p', async (options, expectedConfiguration) => { const { createTypeScriptReporterConfiguration } = await import( - 'lib/typescript-reporter/TypeScriptReporterConfiguration' + 'lib/typescript/TypeScriptReporterConfiguration' ); const configuration = createTypeScriptReporterConfiguration( compiler, @@ -118,7 +118,7 @@ describe('typescript-reporter/TypeScriptsReporterConfiguration', () => { () => 'returned from vue extension' ); const { createTypeScriptReporterConfiguration } = await import( - 'lib/typescript-reporter/TypeScriptReporterConfiguration' + 'lib/typescript/TypeScriptReporterConfiguration' ); const vueOptions = { diff --git a/test/unit/typescript-reporter/TypeScriptSupport.spec.ts b/test/unit/typescript/TypeScriptSupport.spec.ts similarity index 91% rename from test/unit/typescript-reporter/TypeScriptSupport.spec.ts rename to test/unit/typescript/TypeScriptSupport.spec.ts index eab86f73..a3111838 100644 --- a/test/unit/typescript-reporter/TypeScriptSupport.spec.ts +++ b/test/unit/typescript/TypeScriptSupport.spec.ts @@ -1,8 +1,8 @@ import os from 'os'; -import type { TypeScriptReporterConfiguration } from 'lib/typescript-reporter/TypeScriptReporterConfiguration'; +import type { TypeScriptReporterConfiguration } from 'lib/typescript/TypeScriptReporterConfiguration'; -describe('typescript-reporter/TypeScriptSupport', () => { +describe('typescript/TypeScriptSupport', () => { let configuration: TypeScriptReporterConfiguration; beforeEach(() => { @@ -36,7 +36,7 @@ describe('typescript-reporter/TypeScriptSupport', () => { it('throws error if typescript is not installed', async () => { jest.setMock('typescript', undefined); - const { assertTypeScriptSupport } = await import('lib/typescript-reporter/TypeScriptSupport'); + const { assertTypeScriptSupport } = await import('lib/typescript/TypeScriptSupport'); expect(() => assertTypeScriptSupport(configuration)).toThrowError( 'When you use ForkTsCheckerWebpackPlugin with typescript reporter enabled, you must install `typescript` package.' @@ -46,7 +46,7 @@ describe('typescript-reporter/TypeScriptSupport', () => { it('throws error if typescript version is lower then 3.6.0', async () => { jest.setMock('typescript', { version: '3.5.9' }); - const { assertTypeScriptSupport } = await import('lib/typescript-reporter/TypeScriptSupport'); + const { assertTypeScriptSupport } = await import('lib/typescript/TypeScriptSupport'); expect(() => assertTypeScriptSupport(configuration)).toThrowError( [ @@ -60,7 +60,7 @@ describe('typescript-reporter/TypeScriptSupport', () => { jest.setMock('typescript', { version: '3.6.0' }); jest.setMock('fs-extra', { existsSync: () => true }); - const { assertTypeScriptSupport } = await import('lib/typescript-reporter/TypeScriptSupport'); + const { assertTypeScriptSupport } = await import('lib/typescript/TypeScriptSupport'); expect(() => assertTypeScriptSupport(configuration)).not.toThrowError(); }); @@ -69,7 +69,7 @@ describe('typescript-reporter/TypeScriptSupport', () => { jest.setMock('typescript', { version: '3.8.0' }); jest.setMock('fs-extra', { existsSync: () => false }); - const { assertTypeScriptSupport } = await import('lib/typescript-reporter/TypeScriptSupport'); + const { assertTypeScriptSupport } = await import('lib/typescript/TypeScriptSupport'); expect(() => assertTypeScriptSupport(configuration)).toThrowError( [ diff --git a/test/unit/utils/async/pool.spec.ts b/test/unit/utils/async/pool.spec.ts index 7b170d4e..81f01fb3 100644 --- a/test/unit/utils/async/pool.spec.ts +++ b/test/unit/utils/async/pool.spec.ts @@ -1,5 +1,9 @@ import { createPool } from 'lib/utils/async/pool'; +function wait(timeout: number) { + return new Promise((resolve) => setTimeout(resolve, timeout)); +} + describe('createPool', () => { it('creates new pool', () => { const pool = createPool(10); @@ -11,21 +15,8 @@ describe('createPool', () => { it('limits concurrency', async () => { const pool = createPool(2); - const cleanups: (() => void)[] = []; - const shortTask = jest.fn(async (done: () => void) => { - const timeout = setTimeout(done, 10); - cleanups.push(() => { - done(); - clearTimeout(timeout); - }); - }); - const longTask = jest.fn(async (done: () => void) => { - const timeout = setTimeout(done, 500); - cleanups.push(() => { - done(); - clearTimeout(timeout); - }); - }); + const shortTask = jest.fn(() => wait(10)); + const longTask = jest.fn(() => wait(500)); pool.submit(shortTask); pool.submit(shortTask); @@ -36,13 +27,34 @@ describe('createPool', () => { expect(shortTask).toHaveBeenCalledTimes(2); expect(longTask).toHaveBeenCalledTimes(0); - await new Promise((resolve) => setTimeout(resolve, 200)); + await wait(200); expect(shortTask).toHaveBeenCalledTimes(2); expect(longTask).toHaveBeenCalledTimes(2); - // drain the pool - cleanups.forEach((cleanup) => cleanup()); + await pool.drained; + }); + + it('works after draining', async () => { + const pool = createPool(2); + const shortTask = jest.fn(() => wait(10)); + + pool.submit(shortTask); + pool.submit(shortTask); + pool.submit(shortTask); + pool.submit(shortTask); + + expect(shortTask).toHaveBeenCalledTimes(2); + + await wait(100); + + expect(shortTask).toHaveBeenCalledTimes(4); + + pool.submit(shortTask); + pool.submit(shortTask); + + expect(shortTask).toHaveBeenCalledTimes(6); + await pool.drained; }); });