From 7dd01ffe0c7ad3add87b11227964544f2586355a Mon Sep 17 00:00:00 2001 From: Ahn <27772165+ahnpnl@users.noreply.github.com> Date: Tue, 7 Jun 2022 21:38:31 +0200 Subject: [PATCH] fix(legacy): throw type check error in ESM mode with `reject` (#3618) Fixes #3507 --- examples/ts-only/tsconfig.json | 2 +- src/legacy/compiler/ts-compiler.spec.ts | 46 ++++++----------- src/legacy/compiler/ts-compiler.ts | 67 ++++++++++++++----------- src/legacy/compiler/ts-jest-compiler.ts | 6 +-- src/legacy/config/config-set.ts | 4 +- src/legacy/ts-jest-transformer.ts | 31 +++++++++--- src/types.ts | 6 ++- 7 files changed, 88 insertions(+), 74 deletions(-) diff --git a/examples/ts-only/tsconfig.json b/examples/ts-only/tsconfig.json index f60d5fb007..e3a6edbc6a 100644 --- a/examples/ts-only/tsconfig.json +++ b/examples/ts-only/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "CommonJS", - "target": "ES2015" + "target": "ES2021" }, "files": ["globals.d.ts"] } diff --git a/src/legacy/compiler/ts-compiler.spec.ts b/src/legacy/compiler/ts-compiler.spec.ts index e79bb2e5e8..68445a865b 100644 --- a/src/legacy/compiler/ts-compiler.spec.ts +++ b/src/legacy/compiler/ts-compiler.spec.ts @@ -5,8 +5,8 @@ import { type CompilerOptions, DiagnosticCategory, type EmitOutput, - type TranspileOutput, type transpileModule, + type TranspileOutput, } from 'typescript' import { createConfigSet, makeCompiler } from '../../__helpers__/fakers' @@ -217,7 +217,7 @@ describe('TsCompiler', () => { emitSkipped: false, } as EmitOutput) // @ts-expect-error testing purpose - compiler._doTypeChecking = jest.fn() + compiler.getDiagnostics = jest.fn().mockReturnValue([]) const output = compiler.getCompiledOutput(fileContent, fileName, { depGraphs: new Map(), @@ -234,6 +234,7 @@ describe('TsCompiler', () => { }).toMatchSnapshot() expect(output).toEqual({ code: updateOutput(jsOutput, fileName, sourceMap), + diagnostics: [], }) // @ts-expect-error testing purpose @@ -256,7 +257,7 @@ describe('TsCompiler', () => { // @ts-expect-error testing purpose compiler._logger.warn = jest.fn() // @ts-expect-error testing purpose - compiler._doTypeChecking = jest.fn() + compiler.getDiagnostics = jest.fn().mockReturnValue([]) const fileToCheck = fileName.replace('.ts', '.js') const output = compiler.getCompiledOutput(fileContent, fileToCheck, { @@ -286,7 +287,7 @@ describe('TsCompiler', () => { // @ts-expect-error testing purpose compiler._logger.warn = jest.fn() // @ts-expect-error testing purpose - compiler._doTypeChecking = jest.fn() + compiler.getDiagnostics = jest.fn().mockReturnValue([]) // @ts-expect-error testing purpose expect(compiler._logger.warn).not.toHaveBeenCalled() @@ -310,7 +311,7 @@ describe('TsCompiler', () => { emitSkipped: false, } as EmitOutput) // @ts-expect-error testing purpose - compiler._doTypeChecking = jest.fn() + compiler.getDiagnostics = jest.fn().mockReturnValue([]) expect(() => compiler.getCompiledOutput(fileContent, fileName, { @@ -527,7 +528,7 @@ describe('TsCompiler', () => { ) }) - describe('_doTypeChecking', () => { + describe('getDiagnostics', () => { const fileName = join(mockFolder, 'thing.ts') const fileName1 = join(mockFolder, 'thing1.ts') const fileContent = 'const bar = 1' @@ -596,10 +597,10 @@ describe('TsCompiler', () => { }, ) - test.each([true, false])( + test( 'should/should not report diagnostics in watch mode when shouldReportDiagnostics is %p ' + 'and processing file is used by other files', - (shouldReport) => { + () => { const compiler = makeCompiler({ tsJestConfig: { ...baseTsJestConfig, useESM: false }, }) @@ -627,11 +628,7 @@ describe('TsCompiler', () => { }, ] compiler.configSet.raiseDiagnostics = jest.fn() - compiler.configSet.shouldReportDiagnostics = jest - .fn<(f: string) => boolean>() - .mockImplementation((fileToCheck) => { - return fileToCheck === fileName1 ? shouldReport : false - }) + compiler.configSet.shouldReportDiagnostics = jest.fn<(f: string) => boolean>().mockReturnValue(false) // @ts-expect-error testing purpose compiler._languageService.getEmitOutput = jest.fn().mockReturnValueOnce({ outputFiles: [{ text: sourceMap }, { text: jsOutput }], @@ -654,24 +651,11 @@ describe('TsCompiler', () => { watchMode: true, }) - if (shouldReport) { - // @ts-expect-error testing purpose - expect(compiler._languageService?.getSemanticDiagnostics).toHaveBeenCalledWith(fileName1) - // @ts-expect-error testing purpose - expect(compiler._languageService?.getSyntacticDiagnostics).toHaveBeenCalledWith(fileName1) - expect(compiler.configSet.raiseDiagnostics).toHaveBeenCalledWith( - diagnostics, - fileName, - // @ts-expect-error testing purpose - compiler._logger, - ) - } else { - // @ts-expect-error testing purpose - expect(compiler._languageService?.getSemanticDiagnostics).not.toHaveBeenCalled() - // @ts-expect-error testing purpose - expect(compiler._languageService?.getSyntacticDiagnostics).not.toHaveBeenCalled() - expect(compiler.configSet.raiseDiagnostics).not.toHaveBeenCalled() - } + // @ts-expect-error testing purpose + expect(compiler._languageService?.getSemanticDiagnostics).not.toHaveBeenCalled() + // @ts-expect-error testing purpose + expect(compiler._languageService?.getSyntacticDiagnostics).not.toHaveBeenCalled() + expect(compiler.configSet.raiseDiagnostics).not.toHaveBeenCalled() }, ) diff --git a/src/legacy/compiler/ts-compiler.ts b/src/legacy/compiler/ts-compiler.ts index 2379869fad..9b7e3e82be 100644 --- a/src/legacy/compiler/ts-compiler.ts +++ b/src/legacy/compiler/ts-compiler.ts @@ -1,6 +1,5 @@ import { basename, normalize } from 'path' -import type { TransformedSource } from '@jest/transform' import { LogContexts, Logger, LogLevels } from 'bs-logger' import memoize from 'lodash.memoize' import type { @@ -25,13 +24,13 @@ import type { import { LINE_FEED, TS_TSX_REGEX } from '../../constants' import type { - DepGraphInfo, StringMap, TsCompilerInstance, TsJestAstTransformer, TsJestCompileOptions, TTypeScript, } from '../../types' +import { CompiledOutput } from '../../types' import { rootLogger } from '../../utils' import { Errors, interpolate } from '../../utils/messages' import type { ConfigSet } from '../config/config-set' @@ -146,11 +145,12 @@ export class TsCompiler implements TsCompilerInstance { return importedModulePaths } - getCompiledOutput(fileContent: string, fileName: string, options: TsJestCompileOptions): TransformedSource { + getCompiledOutput(fileContent: string, fileName: string, options: TsJestCompileOptions): CompiledOutput { let moduleKind = this._initialCompilerOptions.module let esModuleInterop = this._initialCompilerOptions.esModuleInterop let allowSyntheticDefaultImports = this._initialCompilerOptions.allowSyntheticDefaultImports const currentModuleKind = this._compilerOptions.module + const isEsmMode = this.configSet.useESM && options.supportsStaticESM if ( (this.configSet.babelJestTransformer || (!this.configSet.babelJestTransformer && options.supportsStaticESM)) && this.configSet.useESM @@ -179,7 +179,34 @@ export class TsCompiler implements TsCompilerInstance { // Must set memory cache before attempting to compile this._updateMemoryCache(fileContent, fileName, currentModuleKind === moduleKind) const output: EmitOutput = this._languageService.getEmitOutput(fileName) - this._doTypeChecking(fileName, options.depGraphs, options.watchMode) + const diagnostics = this.getDiagnostics(fileName) + if (!isEsmMode && diagnostics.length) { + this.configSet.raiseDiagnostics(diagnostics, fileName, this._logger) + if (options.watchMode) { + this._logger.debug({ fileName }, '_doTypeChecking(): starting watch mode computing diagnostics') + + for (const entry of options.depGraphs.entries()) { + const normalizedModuleNames = entry[1].resolvedModuleNames.map((moduleName) => normalize(moduleName)) + const fileToReTypeCheck = entry[0] + if (normalizedModuleNames.includes(fileName) && this.configSet.shouldReportDiagnostics(fileToReTypeCheck)) { + this._logger.debug( + { fileToReTypeCheck }, + '_doTypeChecking(): computing diagnostics using language service', + ) + + this._updateMemoryCache(this._getFileContentFromCache(fileToReTypeCheck), fileToReTypeCheck) + const importedModulesDiagnostics = [ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ...this._languageService!.getSemanticDiagnostics(fileToReTypeCheck), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ...this._languageService!.getSyntacticDiagnostics(fileToReTypeCheck), + ] + // will raise or just warn diagnostics depending on config + this.configSet.raiseDiagnostics(importedModulesDiagnostics, fileName, this._logger) + } + } + } + } if (output.emitSkipped) { if (TS_TSX_REGEX.test(fileName)) { throw new Error(interpolate(Errors.CannotProcessFile, { file: fileName })) @@ -204,9 +231,11 @@ export class TsCompiler implements TsCompilerInstance { return this._compilerOptions.sourceMap ? { code: updateOutput(outputFiles[1].text, fileName, outputFiles[0].text), + diagnostics, } : { code: updateOutput(outputFiles[0].text, fileName), + diagnostics, } } else { this._logger.debug({ fileName }, 'getCompiledOutput(): compiling as isolated module') @@ -425,40 +454,20 @@ export class TsCompiler implements TsCompilerInstance { /** * @internal */ - private _doTypeChecking(fileName: string, depGraphs: Map, watchMode: boolean): void { + private getDiagnostics(fileName: string): Diagnostic[] { + const diagnostics: Diagnostic[] = [] if (this.configSet.shouldReportDiagnostics(fileName)) { this._logger.debug({ fileName }, '_doTypeChecking(): computing diagnostics using language service') // Get the relevant diagnostics - this is 3x faster than `getPreEmitDiagnostics`. - const diagnostics: Diagnostic[] = [ + diagnostics.push( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ...this._languageService!.getSemanticDiagnostics(fileName), // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ...this._languageService!.getSyntacticDiagnostics(fileName), - ] - // will raise or just warn diagnostics depending on config - this.configSet.raiseDiagnostics(diagnostics, fileName, this._logger) + ) } - if (watchMode) { - this._logger.debug({ fileName }, '_doTypeChecking(): starting watch mode computing diagnostics') - - for (const entry of depGraphs.entries()) { - const normalizedModuleNames = entry[1].resolvedModuleNames.map((moduleName) => normalize(moduleName)) - const fileToReTypeCheck = entry[0] - if (normalizedModuleNames.includes(fileName) && this.configSet.shouldReportDiagnostics(fileToReTypeCheck)) { - this._logger.debug({ fileToReTypeCheck }, '_doTypeChecking(): computing diagnostics using language service') - this._updateMemoryCache(this._getFileContentFromCache(fileToReTypeCheck), fileToReTypeCheck) - const importedModulesDiagnostics = [ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ...this._languageService!.getSemanticDiagnostics(fileToReTypeCheck), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ...this._languageService!.getSyntacticDiagnostics(fileToReTypeCheck), - ] - // will raise or just warn diagnostics depending on config - this.configSet.raiseDiagnostics(importedModulesDiagnostics, fileName, this._logger) - } - } - } + return diagnostics } } diff --git a/src/legacy/compiler/ts-jest-compiler.ts b/src/legacy/compiler/ts-jest-compiler.ts index 3252e9ec2f..1b22075973 100644 --- a/src/legacy/compiler/ts-jest-compiler.ts +++ b/src/legacy/compiler/ts-jest-compiler.ts @@ -1,6 +1,4 @@ -import type { TransformedSource } from '@jest/transform' - -import type { CompilerInstance, StringMap, TsJestCompileOptions } from '../../types' +import type { CompilerInstance, CompiledOutput, StringMap, TsJestCompileOptions } from '../../types' import type { ConfigSet } from '../config/config-set' import { TsCompiler } from './ts-compiler' @@ -17,7 +15,7 @@ export class TsJestCompiler implements CompilerInstance { return this._compilerInstance.getResolvedModules(fileContent, fileName, runtimeCacheFS) } - getCompiledOutput(fileContent: string, fileName: string, options: TsJestCompileOptions): TransformedSource { + getCompiledOutput(fileContent: string, fileName: string, options: TsJestCompileOptions): CompiledOutput { return this._compilerInstance.getCompiledOutput(fileContent, fileName, options) } } diff --git a/src/legacy/config/config-set.ts b/src/legacy/config/config-set.ts index 8aee6db9b2..cc2dd7026b 100644 --- a/src/legacy/config/config-set.ts +++ b/src/legacy/config/config-set.ts @@ -593,7 +593,7 @@ export class ConfigSet { return !ignoreCodes.includes(diagnostic.code) }) if (!filteredDiagnostics.length) return - const error = this._createTsError(filteredDiagnostics) + const error = this.createTsError(filteredDiagnostics) // only throw if `warnOnly` and it is a warning or error const importantCategories = [DiagnosticCategory.Warning, DiagnosticCategory.Error] if (this._diagnostics.throws && filteredDiagnostics.some((d) => importantCategories.includes(d.category))) { @@ -614,7 +614,7 @@ export class ConfigSet { /** * @internal */ - private _createTsError(diagnostics: readonly ts.Diagnostic[]): TSError { + createTsError(diagnostics: readonly ts.Diagnostic[]): TSError { const formatDiagnostics = this._diagnostics.pretty ? this.compilerModule.formatDiagnosticsWithColorAndContext : this.compilerModule.formatDiagnostics diff --git a/src/legacy/ts-jest-transformer.ts b/src/legacy/ts-jest-transformer.ts index 4564f2ddc7..16597d9c7c 100644 --- a/src/legacy/ts-jest-transformer.ts +++ b/src/legacy/ts-jest-transformer.ts @@ -5,7 +5,13 @@ import type { SyncTransformer, TransformedSource } from '@jest/transform' import type { Logger } from 'bs-logger' import { DECLARATION_TYPE_EXT, JS_JSX_REGEX, TS_TSX_REGEX } from '../constants' -import type { CompilerInstance, DepGraphInfo, ProjectConfigTsJest, TransformOptionsTsJest } from '../types' +import type { + CompiledOutput, + CompilerInstance, + DepGraphInfo, + ProjectConfigTsJest, + TransformOptionsTsJest, +} from '../types' import { parse, stringify, JsonableValue, rootLogger } from '../utils' import { importer } from '../utils/importer' import { Errors, interpolate } from '../utils/messages' @@ -141,7 +147,9 @@ export class TsJestTransformer implements SyncTransformer { const configs = this._configsFor(transformOptions) const shouldStringifyContent = configs.shouldStringifyContent(sourcePath) const babelJest = shouldStringifyContent ? undefined : configs.babelJestTransformer - let result = this.processWithTs(sourceText, sourcePath, transformOptions) + let result: TransformedSource = { + code: this.processWithTs(sourceText, sourcePath, transformOptions).code, + } if (babelJest) { this._logger.debug({ fileName: sourcePath }, 'calling babel-jest processor') @@ -163,11 +171,18 @@ export class TsJestTransformer implements SyncTransformer { ): Promise { this._logger.debug({ fileName: sourcePath, transformOptions }, 'processing', sourcePath) - return new Promise(async (resolve) => { + return new Promise(async (resolve, reject) => { const configs = this._configsFor(transformOptions) const shouldStringifyContent = configs.shouldStringifyContent(sourcePath) const babelJest = shouldStringifyContent ? undefined : configs.babelJestTransformer - let result = this.processWithTs(sourceText, sourcePath, transformOptions) + let result: TransformedSource + const processWithTsResult = this.processWithTs(sourceText, sourcePath, transformOptions) + result = { + code: processWithTsResult.code, + } + if (processWithTsResult.diagnostics?.length) { + reject(configs.createTsError(processWithTsResult.diagnostics)) + } if (babelJest) { this._logger.debug({ fileName: sourcePath }, 'calling babel-jest processor') @@ -183,7 +198,11 @@ export class TsJestTransformer implements SyncTransformer { }) } - private processWithTs(sourceText: string, sourcePath: string, transformOptions: TransformOptionsTsJest) { + private processWithTs( + sourceText: string, + sourcePath: string, + transformOptions: TransformOptionsTsJest, + ): CompiledOutput { let result: TransformedSource const configs = this._configsFor(transformOptions) const shouldStringifyContent = configs.shouldStringifyContent(sourcePath) @@ -330,7 +349,7 @@ export class TsJestTransformer implements SyncTransformer { sourcePath: string, transformOptions: TransformOptionsTsJest, ): Promise { - return new Promise((resolve) => resolve(this.getCacheKey(sourceText, sourcePath, transformOptions))) + return Promise.resolve(this.getCacheKey(sourceText, sourcePath, transformOptions)) } /** diff --git a/src/types.ts b/src/types.ts index b92df70942..ffdbdfe0bc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -228,9 +228,13 @@ export interface TsJestCompileOptions { supportsStaticESM: boolean } +export interface CompiledOutput extends TransformedSource { + diagnostics?: _ts.Diagnostic[] +} + export interface CompilerInstance { getResolvedModules(fileContent: string, fileName: string, runtimeCacheFS: StringMap): string[] - getCompiledOutput(fileContent: string, fileName: string, options: TsJestCompileOptions): TransformedSource + getCompiledOutput(fileContent: string, fileName: string, options: TsJestCompileOptions): CompiledOutput } export interface TsCompilerInstance extends CompilerInstance { configSet: ConfigSet