diff --git a/e2e/__tests__/__snapshots__/logger.test.ts.snap b/e2e/__tests__/__snapshots__/logger.test.ts.snap index 54551edc59..a928cf7a42 100644 --- a/e2e/__tests__/__snapshots__/logger.test.ts.snap +++ b/e2e/__tests__/__snapshots__/logger.test.ts.snap @@ -27,13 +27,13 @@ Array [ "[level:20] getCompiledOutput(): compiling using language service", "[level:20] updateMemoryCache: update memory cache for language service", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getCompiledOutput(): computing diagnostics using language service", + "[level:20] _doTypeChecking(): computing diagnostics using language service", "[level:20] computing cache key for /Hello.ts", "[level:20] processing /Hello.ts", "[level:20] getCompiledOutput(): compiling using language service", "[level:20] updateMemoryCache: update memory cache for language service", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getCompiledOutput(): computing diagnostics using language service", + "[level:20] _doTypeChecking(): computing diagnostics using language service", ] `; @@ -68,14 +68,14 @@ Array [ "[level:20] getCompiledOutput(): compiling using language service", "[level:20] updateMemoryCache: update memory cache for language service", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getCompiledOutput(): computing diagnostics using language service", + "[level:20] _doTypeChecking(): computing diagnostics using language service", "[level:20] calling babel-jest processor", "[level:20] computing cache key for /Hello.ts", "[level:20] processing /Hello.ts", "[level:20] getCompiledOutput(): compiling using language service", "[level:20] updateMemoryCache: update memory cache for language service", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getCompiledOutput(): computing diagnostics using language service", + "[level:20] _doTypeChecking(): computing diagnostics using language service", "[level:20] calling babel-jest processor", ] `; @@ -112,14 +112,14 @@ Array [ "[level:20] getCompiledOutput(): compiling using language service", "[level:20] updateMemoryCache: update memory cache for language service", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getCompiledOutput(): computing diagnostics using language service", + "[level:20] _doTypeChecking(): computing diagnostics using language service", "[level:20] calling babel-jest processor", "[level:20] computing cache key for /Hello.ts", "[level:20] processing /Hello.ts", "[level:20] getCompiledOutput(): compiling using language service", "[level:20] updateMemoryCache: update memory cache for language service", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getCompiledOutput(): computing diagnostics using language service", + "[level:20] _doTypeChecking(): computing diagnostics using language service", "[level:20] calling babel-jest processor", ] `; diff --git a/src/compiler/__snapshots__/ts-compiler.spec.ts.snap b/src/compiler/__snapshots__/ts-compiler.spec.ts.snap deleted file mode 100644 index 828025107b..0000000000 --- a/src/compiler/__snapshots__/ts-compiler.spec.ts.snap +++ /dev/null @@ -1,107 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TsCompiler isolatedModule false allowJs option should compile js file for allowJs true with outDir 1`] = ` -"\\"use strict\\"; -Object.defineProperty(exports, \\"__esModule\\", { value: true }); -exports.default = 42; -//# " -`; - -exports[`TsCompiler isolatedModule false allowJs option should compile js file for allowJs true without outDir 1`] = ` -"\\"use strict\\"; -Object.defineProperty(exports, \\"__esModule\\", { value: true }); -exports.default = 42; -//# " -`; - -exports[`TsCompiler isolatedModule false diagnostics should throw error when cannot compile 1`] = ` -"Unable to require \`.d.ts\` file for file: test-cannot-compile.d.ts. -This is usually the result of a faulty configuration or import. Make sure there is a \`.js\`, \`.json\` or another executable extension available alongside \`test-cannot-compile.d.ts\`." -`; - -exports[`TsCompiler isolatedModule false jsx option should compile tsx file for jsx preserve 1`] = ` -"\\"use strict\\"; -const App = () => { - return <>Test; -}; -//# " -`; - -exports[`TsCompiler isolatedModule false jsx option should compile tsx file for other jsx options 1`] = ` -"\\"use strict\\"; -const App = () => { - return React.createElement(React.Fragment, null, \\"Test\\"); -}; -//# " -`; - -exports[`TsCompiler isolatedModule false should compile codes with useESM true 1`] = ` -"// @ts-expect-error testing purpose -import babelFooCfg from './babel-foo.config'; -import { getFoo } from './thing1'; -import { getFooBar } from './thing1'; -import { getBar } from './thing2'; -getFoo('foo'); -getBar('bar'); -getFooBar('foobar'); -getFoo(JSON.stringify(babelFooCfg.presets)); -//# " -`; - -exports[`TsCompiler isolatedModule false should compile ts file which has an existing js file 1`] = ` -"\\"use strict\\"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { \\"default\\": mod }; -}; -Object.defineProperty(exports, \\"__esModule\\", { value: true }); -// @ts-expect-error testing purpose -const babel_foo_config_1 = __importDefault(require(\\"./babel-foo.config\\")); -const thing1_1 = require(\\"./thing1\\"); -const thing1_2 = require(\\"./thing1\\"); -const thing2_1 = require(\\"./thing2\\"); -thing1_1.getFoo('foo'); -thing2_1.getBar('bar'); -thing1_2.getFooBar('foobar'); -thing1_1.getFoo(JSON.stringify(babel_foo_config_1.default.presets)); -//# " -`; - -exports[`TsCompiler isolatedModule true diagnostics should report diagnostics related to codes with exclude config is undefined 1`] = `"foo.ts(2,23): error TS1005: '=>' expected."`; - -exports[`TsCompiler isolatedModule true diagnostics should report diagnostics related to codes with exclude config matches file name 1`] = `"foo.ts(2,23): error TS1005: '=>' expected."`; - -exports[`TsCompiler isolatedModule true jsx option should compile tsx file for jsx preserve 1`] = ` -"\\"use strict\\"; -const App = () => { - return <>Test; -}; -//# " -`; - -exports[`TsCompiler isolatedModule true jsx option should compile tsx file for other jsx options 1`] = ` -"\\"use strict\\"; -const App = () => { - return React.createElement(React.Fragment, null, \\"Test\\"); -}; -//# " -`; - -exports[`TsCompiler isolatedModule true should compile js file for allowJs true 1`] = ` -"\\"use strict\\"; -Object.defineProperty(exports, \\"__esModule\\", { value: true }); -exports.default = 42; -//# " -`; - -exports[`TsCompiler isolatedModule true should transpile code with useESM true 1`] = ` -"// @ts-expect-error testing purpose -import babelFooCfg from './babel-foo.config'; -import { getFoo } from './thing1'; -import { getFooBar } from './thing1'; -import { getBar } from './thing2'; -getFoo('foo'); -getBar('bar'); -getFooBar('foobar'); -getFoo(JSON.stringify(babelFooCfg.presets)); -//# " -`; diff --git a/src/compiler/ts-compiler.spec.ts b/src/compiler/ts-compiler.spec.ts index 170a3d885b..804911ca1c 100644 --- a/src/compiler/ts-compiler.spec.ts +++ b/src/compiler/ts-compiler.spec.ts @@ -1,532 +1,604 @@ import { readFileSync } from 'fs' -import { join, normalize } from 'path' +import { basename, join, normalize } from 'path' -import { createConfigSet, makeCompiler } from '../__helpers__/fakers' -import { logTargetMock } from '../__helpers__/mocks' +import { DiagnosticCategory, EmitOutput, ModuleKind, ScriptTarget, TranspileOutput } from 'typescript' + +import { makeCompiler } from '../__helpers__/fakers' import { mockFolder } from '../__helpers__/path' -import ProcessedSource from '../__helpers__/processed-source' +import type { DepGraphInfo } from '../types' +import { Errors, interpolate } from '../utils/messages' -import { TsCompiler } from './ts-compiler' +import { updateOutput } from './compiler-utils' -const logTarget = logTargetMock() +const baseTsJestConfig = { tsconfig: join(process.cwd(), 'tsconfig.spec.json') } describe('TsCompiler', () => { - describe('isolatedModule true', () => { - const baseTsJestConfig = { - isolatedModules: true, - } + describe('getResolvedModules', () => { + const fileName = join(mockFolder, 'thing.ts') - test('should transpile code with useESM true', () => { + test('should return undefined when file name is not known to compiler', () => { const compiler = makeCompiler({ - tsJestConfig: { ...baseTsJestConfig, useESM: true }, + tsJestConfig: baseTsJestConfig, }) - const fileName = join(mockFolder, 'thing.ts') - const compiledOutput = compiler.getCompiledOutput(readFileSync(fileName, 'utf-8'), fileName, true) - - expect(new ProcessedSource(compiledOutput, fileName).outputCodeWithoutMaps).toMatchSnapshot() + expect(compiler.getResolvedModules('const foo = 1', fileName, new Map())).toEqual([]) }) - test('should compile js file for allowJs true', () => { - const fileName = 'foo.js' + test('should return undefined when it is isolatedModules true', () => { const compiler = makeCompiler({ - tsJestConfig: { ...baseTsJestConfig, tsconfig: { allowJs: true } }, + tsJestConfig: { + ...baseTsJestConfig, + isolatedModules: true, + }, }) - const source = 'export default 42' - const compiledOutput = compiler.getCompiledOutput(source, fileName, false) + expect(compiler.getResolvedModules('const foo = 1', fileName, new Map())).toEqual([]) + }) + + test('should return undefined when file has no resolved modules', () => { + const jestCacheFS = new Map() + jestCacheFS.set(fileName, 'const foo = 1') + const compiler = makeCompiler( + { + tsJestConfig: baseTsJestConfig, + }, + jestCacheFS, + ) - expect(new ProcessedSource(compiledOutput, fileName).outputCodeWithoutMaps).toMatchSnapshot() + expect(compiler.getResolvedModules('const foo = 1', fileName, new Map())).toEqual([]) }) - describe('jsx option', () => { - const fileName = 'foo.tsx' - const source = ` - const App = () => { - return <>Test - } - ` + test('should return resolved modules when file has resolved modules', () => { + const jestCacheFS = new Map() + const importedModule1 = join(mockFolder, 'thing1.ts') + const importedModule2 = join(mockFolder, 'thing2.ts') + const importedModule3 = join(mockFolder, 'babel-foo.config.js') + const fileContentWithModules = readFileSync(fileName, 'utf-8') + jestCacheFS.set(importedModule1, readFileSync(importedModule1, 'utf-8')) + const compiler = makeCompiler( + { + tsJestConfig: baseTsJestConfig, + }, + jestCacheFS, + ) - it('should compile tsx file for jsx preserve', () => { + expect( + compiler + .getResolvedModules(fileContentWithModules, fileName, new Map()) + .map((resolvedFileName) => normalize(resolvedFileName)), + ).toEqual([importedModule3, importedModule1, importedModule2]) + }) + }) + + describe('getCompiledOutput', () => { + describe('isolatedModules true', () => { + const fileName = join(mockFolder, 'thing.ts') + const fileContent = 'const bar = 1' + + test.each([true, false])('should transpile code with useESM %p', (useESM) => { const compiler = makeCompiler({ - tsJestConfig: { - ...baseTsJestConfig, - tsconfig: { - jsx: 'preserve', - }, + tsJestConfig: { ...baseTsJestConfig, isolatedModules: true, useESM }, + }) + const transformersStub = { + before: [], + after: [], + afterDeclarations: [], + } + // @ts-expect-error testing purpose + compiler._ts.transpileModule = jest.fn().mockReturnValueOnce({ + sourceMapText: '{}', + outputText: 'var bar = 1', + diagnostics: [], + } as TranspileOutput) + // @ts-expect-error testing purpose + compiler._makeTransformers = jest.fn().mockReturnValueOnce(transformersStub) + compiler.getCompiledOutput(fileContent, fileName, { + depGraphs: new Map(), + supportsStaticESM: true, + watchMode: false, + }) + // @ts-expect-error testing purpose + const compilerOptions = compiler._compilerOptions + + // @ts-expect-error testing purpose + expect(compiler._ts.transpileModule).toHaveBeenCalledWith(fileContent, { + fileName, + compilerOptions: { + ...compilerOptions, + module: useESM ? ModuleKind.ESNext : ModuleKind.CommonJS, + target: useESM ? ScriptTarget.ES2015 : compilerOptions.target, + esModuleInterop: useESM ? true : compilerOptions.esModuleInterop, + allowSyntheticDefaultImports: useESM ? true : compilerOptions.allowSyntheticDefaultImports, }, + transformers: transformersStub, + reportDiagnostics: compiler.configSet.shouldReportDiagnostics(fileName), }) - const compiledOutput = compiler.getCompiledOutput(source, fileName, false) - - expect(new ProcessedSource(compiledOutput, fileName).outputCodeWithoutMaps).toMatchSnapshot() }) - it('should compile tsx file for other jsx options', () => { + test.each([true, false])('should report diagnostics if shouldReportDiagnostics is %p', (shouldReport) => { const compiler = makeCompiler({ - tsJestConfig: { - ...baseTsJestConfig, - tsconfig: { - jsx: 'react', + tsJestConfig: { ...baseTsJestConfig, isolatedModules: true, useESM: false }, + }) + compiler.configSet.raiseDiagnostics = jest.fn() + compiler.configSet.shouldReportDiagnostics = jest.fn().mockReturnValue(shouldReport) + const compileOutput: TranspileOutput = { + sourceMapText: '{}', + outputText: 'var bar = 1', + diagnostics: [ + { + category: DiagnosticCategory.Error, + code: 123, + messageText: 'An error occurs', + file: undefined, + start: 0, + length: 1, }, - }, + ], + } + // @ts-expect-error testing purpose + compiler._ts.transpileModule = jest.fn().mockReturnValueOnce(compileOutput) + compiler.getCompiledOutput(fileContent, fileName, { + depGraphs: new Map(), + supportsStaticESM: true, + watchMode: false, }) - const compiledOutput = compiler.getCompiledOutput(source, fileName, false) - expect(new ProcessedSource(compiledOutput, fileName).outputCodeWithoutMaps).toMatchSnapshot() + if (shouldReport) { + expect(compiler.configSet.raiseDiagnostics).toHaveBeenCalledWith( + compileOutput.diagnostics, + fileName, + // @ts-expect-error testing purpose + compiler._logger, + ) + } else { + expect(compiler.configSet.raiseDiagnostics).not.toHaveBeenCalled() + } }) }) - describe('source maps', () => { - const source = 'const f = (v: number) => v\nconst t: number = f(5)' - const fileName = 'test-source-map-transpiler.ts' - - it('should have correct source maps without mapRoot', () => { - const compiler = makeCompiler({ tsJestConfig: { ...baseTsJestConfig, tsconfig: false } }) - const compiledOutput = compiler.getCompiledOutput(source, fileName, false) - - expect(new ProcessedSource(compiledOutput, fileName).outputSourceMaps).toMatchObject({ - file: fileName, - sources: [fileName], - sourcesContent: [source], - }) - }) + describe('isolatedModules false', () => { + const fileName = join(mockFolder, 'thing.ts') + const fileContent = 'const bar = 1' + const jsOutput = 'var bar = 1' + const sourceMap = '{}' - it('should have correct source maps with mapRoot', () => { + test.each([true, false])('should compile codes with useESM %p', (useESM) => { const compiler = makeCompiler({ - tsJestConfig: { - ...baseTsJestConfig, - tsconfig: { - mapRoot: './', - }, - }, + tsJestConfig: { ...baseTsJestConfig, useESM }, }) - const compiled = compiler.getCompiledOutput(source, fileName, false) - - expect(new ProcessedSource(compiled, fileName).outputSourceMaps).toMatchObject({ - file: fileName, - sources: [fileName], - sourcesContent: [source], + // @ts-expect-error testing purpose + compiler._languageService.getEmitOutput = jest.fn().mockReturnValueOnce({ + outputFiles: [{ text: sourceMap }, { text: jsOutput }], + emitSkipped: false, + } as EmitOutput) + // @ts-expect-error testing purpose + compiler._doTypeChecking = jest.fn() + + const output = compiler.getCompiledOutput(fileContent, fileName, { + depGraphs: new Map(), + supportsStaticESM: true, + watchMode: false, }) - }) - }) - - describe('diagnostics', () => { - it('should not report diagnostics related to typings', () => { - const compiler = makeCompiler({ tsJestConfig: { ...baseTsJestConfig, tsconfig: false } }) - expect(() => - compiler.getCompiledOutput( - ` -const f = (v: number) => v -const t: string = f(5) -const v: boolean = t -`, - 'foo.ts', - false, - ), - ).not.toThrowError() - }) - - it('should report diagnostics related to codes with exclude config is undefined', () => { - const compiler = makeCompiler({ tsJestConfig: { ...baseTsJestConfig, tsconfig: false } }) - - expect(() => - compiler.getCompiledOutput( - ` -const f = (v: number) = v -const t: string = f(5) -`, - 'foo.ts', - false, - ), - ).toThrowErrorMatchingSnapshot() + // @ts-expect-error testing purpose + const compileTarget = compiler._compilerOptions.target + // @ts-expect-error testing purpose + const moduleKind = compiler._compilerOptions.module + // @ts-expect-error testing purpose + const esModuleInterop = compiler._compilerOptions.esModuleInterop + // @ts-expect-error testing purpose + const allowSyntheticDefaultImports = compiler._compilerOptions.allowSyntheticDefaultImports + expect(compileTarget).toEqual(useESM ? ScriptTarget.ES2015 : compileTarget) + expect(moduleKind).toEqual(useESM ? ModuleKind.ESNext : moduleKind) + expect(esModuleInterop).toEqual(useESM ? true : esModuleInterop) + expect(allowSyntheticDefaultImports).toEqual(useESM ? true : allowSyntheticDefaultImports) + expect(output).toEqual(updateOutput(jsOutput, fileName, sourceMap)) }) - it('should report diagnostics related to codes with exclude config matches file name', () => { + test('should return original file content if emitSkipped is true', () => { const compiler = makeCompiler({ - tsJestConfig: { ...baseTsJestConfig, tsconfig: false, diagnostics: { exclude: ['foo.ts'] } }, + tsJestConfig: { ...baseTsJestConfig }, + }) + // @ts-expect-error testing purpose + compiler._languageService.getEmitOutput = jest.fn().mockReturnValueOnce({ + outputFiles: [{ text: sourceMap }, { text: jsOutput }], + emitSkipped: true, + } as EmitOutput) + // @ts-expect-error testing purpose + compiler._doTypeChecking = jest.fn() + const output = compiler.getCompiledOutput(fileContent, fileName, { + depGraphs: new Map(), + supportsStaticESM: false, + watchMode: false, }) - expect(() => - compiler.getCompiledOutput( - ` -const f = (v: number) = v -const t: string = f(5) -`, - 'foo.ts', - false, - ), - ).toThrowErrorMatchingSnapshot() + expect(output).toEqual(updateOutput(fileContent, fileName, sourceMap)) }) - it('should not report diagnostics related to codes with exclude config does not match file name', () => { + test('should throw error when there are no outputFiles', () => { const compiler = makeCompiler({ - tsJestConfig: { ...baseTsJestConfig, tsconfig: false, diagnostics: { exclude: ['bar.ts'] } }, + tsJestConfig: { ...baseTsJestConfig }, }) + // @ts-expect-error testing purpose + compiler._languageService.getEmitOutput = jest.fn().mockReturnValueOnce({ + outputFiles: [], + emitSkipped: false, + } as EmitOutput) + // @ts-expect-error testing purpose + compiler._doTypeChecking = jest.fn() expect(() => - compiler.getCompiledOutput( - ` -const f = (v: number) = v -const t: string = f(5) -`, - 'foo.ts', - false, + compiler.getCompiledOutput(fileContent, fileName, { + depGraphs: new Map(), + supportsStaticESM: false, + watchMode: false, + }), + ).toThrowError( + new TypeError( + interpolate(Errors.UnableToRequireDefinitionFile, { + file: basename(fileName), + }), ), - ).not.toThrowError() + ) }) }) + }) - test('should use correct custom AST transformers', () => { - // eslint-disable-next-line no-console - console.log = jest.fn() - const fileName = 'foo.js' + describe('_makeTransformers', () => { + test('should return the transformers object which contains before, after and afterDeclarations transformers', () => { const compiler = makeCompiler({ - tsJestConfig: { - ...baseTsJestConfig, - tsconfig: { - allowJs: true, + tsJestConfig: { ...baseTsJestConfig, isolatedModules: true, useESM: false }, + }) + const transformerStub = join(mockFolder, 'dummy-transformer.js') + console.log = jest.fn() + + // @ts-expect-error testing purpose + const transformers = compiler._makeTransformers({ + before: [ + { + name: 'dummy-transformer', + version: 1, + factory: require(transformerStub).factory, }, - astTransformers: { - before: ['dummy-transformer'], - after: ['dummy-transformer'], - afterDeclarations: ['dummy-transformer'], + ], + after: [ + { + name: 'dummy-transformer', + version: 1, + factory: require(transformerStub).factory, }, - }, + ], + afterDeclarations: [ + { + name: 'dummy-transformer', + version: 1, + factory: require(transformerStub).factory, + }, + ], }) - const source = 'export default 42' - compiler.getCompiledOutput(source, fileName, false) - - // eslint-disable-next-line no-console - expect(console.log).toHaveBeenCalledTimes(3) + expect(transformers.before?.length).toEqual(1) + expect(transformers.after?.length).toEqual(1) + expect(transformers.afterDeclarations?.length).toEqual(1) }) }) - describe('isolatedModule false', () => { - const baseTsJestConfig = { tsconfig: join(process.cwd(), 'tsconfig.spec.json') } - const jestCacheFS = new Map() + describe('_updateMemoryCache', () => { + const fileName = join(mockFolder, 'thing.ts') + const fileContent = 'const bar = 1' + const compiler = makeCompiler({ + tsJestConfig: { ...baseTsJestConfig, isolatedModules: true }, + }) + const fileContentCache = new Map() + const fileVersionCache = new Map() beforeEach(() => { - logTarget.clear() + // @ts-expect-error testing purpose + compiler._projectVersion = 1 + fileContentCache.clear() + fileVersionCache.clear() }) - test('should compile codes with useESM true', () => { - const compiler = makeCompiler({ - tsJestConfig: { - ...baseTsJestConfig, - tsconfig: { - module: 'ESNext', - esModuleInterop: false, - allowSyntheticDefaultImports: false, - }, - useESM: true, - }, - }) - const fileName = join(mockFolder, 'thing.ts') + test('should increase project version if processing file is not in _fileContentCache', () => { + // @ts-expect-error testing purpose + compiler._fileContentCache = fileContentCache + // @ts-expect-error testing purpose + compiler._fileVersionCache = fileVersionCache - const compiledOutput = compiler.getCompiledOutput(readFileSync(fileName, 'utf-8'), fileName, true) - - expect(new ProcessedSource(compiledOutput, fileName).outputCodeWithoutMaps).toMatchSnapshot() - // @ts-expect-error _compilerOptions is a private property - expect(compiler._compilerOptions.esModuleInterop).toEqual(true) - // @ts-expect-error _compilerOptions is a private property - expect(compiler._compilerOptions.allowSyntheticDefaultImports).toEqual(true) - // @ts-expect-error _initialCompilerOptions is a private property - expect(compiler._initialCompilerOptions.esModuleInterop).not.toEqual(true) - // @ts-expect-error _initialCompilerOptions is a private property - expect(compiler._initialCompilerOptions.allowSyntheticDefaultImports).not.toEqual(true) - }) + // @ts-expect-error testing purpose + compiler._updateMemoryCache(fileContent, fileName) - test('should compile ts file which has an existing js file', () => { - const configSet = createConfigSet({ - tsJestConfig: baseTsJestConfig, - }) - const fileName = join(mockFolder, 'thing.ts') - const fileContent = readFileSync(fileName, 'utf-8') - configSet.parsedTsConfig.fileNames.push(...[fileName.replace('.ts', '.js'), fileName]) - const compiler = new TsCompiler(configSet, new Map()) - - const compiledOutput = compiler.getCompiledOutput(fileContent, fileName, false) - - expect(new ProcessedSource(compiledOutput, fileName).outputCodeWithoutMaps).toMatchSnapshot() + // @ts-expect-error testing purpose + expect(compiler._projectVersion).toEqual(2) }) - describe('allowJs option', () => { - const fileName = 'test-allow-js.js' - const source = 'export default 42' - jestCacheFS.set(fileName, source) + test('should increase project version if processing file is not in _fileVersionCache', () => { + fileContentCache.set(fileName, fileContent) + // @ts-expect-error testing purpose + compiler._fileContentCache = fileContentCache + // @ts-expect-error testing purpose + compiler._fileVersionCache = fileVersionCache - it('should compile js file for allowJs true with outDir', () => { - const compiler = makeCompiler( - { - tsJestConfig: { tsconfig: { allowJs: true, outDir: '$$foo$$' } }, - }, - jestCacheFS, - ) - - const compiled = compiler.getCompiledOutput(source, fileName, false) - - expect(new ProcessedSource(compiled, fileName).outputCodeWithoutMaps).toMatchSnapshot() - }) - - it('should compile js file for allowJs true without outDir', () => { - const compiler = makeCompiler( - { - tsJestConfig: { tsconfig: { allowJs: true } }, - }, - jestCacheFS, - ) - const compiled = compiler.getCompiledOutput(source, fileName, false) + // @ts-expect-error testing purpose + compiler._updateMemoryCache(fileContent, fileName) - expect(new ProcessedSource(compiled, fileName).outputCodeWithoutMaps).toMatchSnapshot() - }) + // @ts-expect-error testing purpose + expect(compiler._projectVersion).toEqual(2) }) - describe('jsx option', () => { - const fileName = 'test-jsx.tsx' - const source = ` - const App = () => { - return <>Test - } - ` - jestCacheFS.set(fileName, source) - - it('should compile tsx file for jsx preserve', () => { - const compiler = makeCompiler( - { - tsJestConfig: { - tsconfig: { - jsx: 'preserve', - }, - }, - }, - jestCacheFS, - ) - - const compiled = compiler.getCompiledOutput(source, fileName, false) - - expect(new ProcessedSource(compiled, fileName).outputCodeWithoutMaps).toMatchSnapshot() - }) + test('should increase project version if processing file version is 0', () => { + fileContentCache.set(fileName, fileContent) + fileVersionCache.set(fileName, 0) + // @ts-expect-error testing purpose + compiler._fileContentCache = fileContentCache + // @ts-expect-error testing purpose + compiler._fileVersionCache = fileVersionCache - it('should compile tsx file for other jsx options', () => { - const compiler = makeCompiler( - { - tsJestConfig: { - tsconfig: { - jsx: 'react', - }, - }, - }, - jestCacheFS, - ) - const compiled = compiler.getCompiledOutput(source, fileName, false) + // @ts-expect-error testing purpose + compiler._updateMemoryCache(fileContent, fileName) - expect(new ProcessedSource(compiled, fileName).outputCodeWithoutMaps).toMatchSnapshot() - }) + // @ts-expect-error testing purpose + expect(compiler._projectVersion).toEqual(2) }) - describe('source maps', () => { - const source = 'const gsm = (v: number) => v\nconst h: number = gsm(5)' - const fileName = 'test-source-map.ts' - jestCacheFS.set(fileName, source) + test( + 'should increase file version in _fileVersionCache as well as ' + + 'update file content in _fileContentCache for processing file if previous ' + + 'content is not the same as new content and increase project version', + () => { + const newContent = 'const foo = 1' + fileContentCache.set(fileName, fileContent) + fileVersionCache.set(fileName, 1) + // @ts-expect-error testing purpose + compiler._fileContentCache = fileContentCache + // @ts-expect-error testing purpose + compiler._fileVersionCache = fileVersionCache + + // @ts-expect-error testing purpose + compiler._updateMemoryCache(newContent, fileName) + + // @ts-expect-error testing purpose + expect(compiler._fileVersionCache.get(fileName)).toEqual(2) + // @ts-expect-error testing purpose + expect(compiler._fileContentCache.get(fileName)).toEqual(newContent) + // @ts-expect-error testing purpose + expect(compiler._projectVersion).toEqual(2) + }, + ) + + test( + 'should only increase project version if the previous content is the ' + + 'same as current content and the processing file is not in the list of compiler file names', + () => { + fileContentCache.set(fileName, fileContent) + fileVersionCache.set(fileName, 1) + // @ts-expect-error testing purpose + compiler._fileContentCache = fileContentCache + // @ts-expect-error testing purpose + compiler._fileVersionCache = fileVersionCache + // @ts-expect-error testing purpose + compiler._parsedTsConfig.fileNames = [] + + // @ts-expect-error testing purpose + compiler._updateMemoryCache(fileContent, fileName) + + // @ts-expect-error testing purpose + expect(compiler._fileVersionCache.get(fileName)).toEqual(1) + // @ts-expect-error testing purpose + expect(compiler._fileContentCache.get(fileName)).toEqual(fileContent) + + // @ts-expect-error testing purpose + expect(compiler._projectVersion).toEqual(2) + }, + ) + + test( + 'should not increase project version if the previous content is ' + + 'the same as current content and the processing file is in the list of compiler file names', + () => { + fileContentCache.set(fileName, fileContent) + fileVersionCache.set(fileName, 1) + // @ts-expect-error testing purpose + compiler._fileContentCache = fileContentCache + // @ts-expect-error testing purpose + compiler._fileVersionCache = fileVersionCache + // @ts-expect-error testing purpose + compiler._parsedTsConfig.fileNames = [fileName] + + // @ts-expect-error testing purpose + compiler._updateMemoryCache(fileContent, fileName) + + // @ts-expect-error testing purpose + expect(compiler._fileVersionCache.get(fileName)).toEqual(1) + // @ts-expect-error testing purpose + expect(compiler._fileContentCache.get(fileName)).toEqual(fileContent) + + // @ts-expect-error testing purpose + expect(compiler._projectVersion).toEqual(1) + }, + ) + }) - it('should have correct source maps without mapRoot', () => { - const compiler = makeCompiler( - { tsJestConfig: { tsconfig: require.resolve('../../tsconfig.spec.json') } }, - jestCacheFS, - ) - const compiled = compiler.getCompiledOutput(source, fileName, false) + describe('_doTypeChecking', () => { + const fileName = join(mockFolder, 'thing.ts') + const fileName1 = join(mockFolder, 'thing1.ts') + const fileContent = 'const bar = 1' + const jsOutput = 'var bar = 1' + const sourceMap = '{}' - expect(new ProcessedSource(compiled, fileName).outputSourceMaps).toMatchObject({ - file: fileName, - sources: [fileName], - sourcesContent: [source], + test.each([true, false])( + 'should/should not report diagnostics if shouldReportDiagnostics is %p in non-watch mode', + (shouldReport) => { + const compiler = makeCompiler({ + tsJestConfig: baseTsJestConfig, }) - }) - - it('should have correct source maps with mapRoot', () => { - const compiler = makeCompiler( + compiler.configSet.raiseDiagnostics = jest.fn() + compiler.configSet.shouldReportDiagnostics = jest.fn().mockReturnValue(shouldReport) + // @ts-expect-error testing purpose + compiler._languageService.getEmitOutput = jest.fn().mockReturnValueOnce({ + outputFiles: [{ text: sourceMap }, { text: jsOutput }], + emitSkipped: false, + } as EmitOutput) + const diagnostics = [ { - tsJestConfig: { - tsconfig: { - mapRoot: './', - }, - }, + category: DiagnosticCategory.Error, + code: 123, + messageText: 'An error occurs', + file: undefined, + start: 0, + length: 1, }, - jestCacheFS, - ) - const compiled = compiler.getCompiledOutput(source, fileName, false) - - expect(new ProcessedSource(compiled, fileName).outputSourceMaps).toMatchObject({ - file: fileName, - sources: [fileName], - sourcesContent: [source], - }) - }) - }) - - describe('module resolution', () => { - it(`should use moduleResolutionCache`, () => { - jest.unmock('typescript') - const ts = require('typescript') - // eslint-disable-next-line @typescript-eslint/no-empty-function - const moduleResolutionCacheMock = (ts.createModuleResolutionCache = jest.fn().mockImplementation(() => {})) - - makeCompiler({ - tsJestConfig: baseTsJestConfig, + { + category: DiagnosticCategory.Error, + code: 456, + messageText: 'An error occurs', + file: undefined, + start: 0, + length: 1, + }, + ] + // @ts-expect-error testing purpose + compiler._languageService?.getSemanticDiagnostics = jest.fn().mockReturnValueOnce([diagnostics[0]]) + // @ts-expect-error testing purpose + compiler._languageService?.getSyntacticDiagnostics = jest.fn().mockReturnValueOnce([diagnostics[1]]) + compiler.getCompiledOutput(fileContent, fileName, { + depGraphs: new Map(), + supportsStaticESM: false, + watchMode: false, }) - expect(moduleResolutionCacheMock).toHaveBeenCalled() - expect(moduleResolutionCacheMock.mock.calls[0].length).toBe(3) - - moduleResolutionCacheMock.mockRestore() - }) - }) - - describe('getResolvedModules', () => { - const fileName = join(mockFolder, 'thing.ts') + if (shouldReport) { + // @ts-expect-error testing purpose + expect(compiler._languageService?.getSemanticDiagnostics).toHaveBeenCalledWith(fileName) + // @ts-expect-error testing purpose + expect(compiler._languageService?.getSyntacticDiagnostics).toHaveBeenCalledWith(fileName) + 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() + } + }, + ) - test('should return undefined when file name is not known to compiler', () => { + test.each([true, false])( + '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, + tsJestConfig: { ...baseTsJestConfig, useESM: false }, }) - - expect(compiler.getResolvedModules('const foo = 1', fileName, new Map())).toEqual([]) - }) - - test('should return undefined when it is isolatedModules true', () => { - const compiler = makeCompiler({ - tsJestConfig: { - ...baseTsJestConfig, - isolatedModules: true, - }, + const depGraphs = new Map() + depGraphs.set(fileName1, { + fileContent, + resolvedModuleNames: [fileName], }) - - expect(compiler.getResolvedModules('const foo = 1', fileName, new Map())).toEqual([]) - }) - - test('should return undefined when file has no resolved modules', () => { - const jestCacheFS = new Map() - jestCacheFS.set(fileName, 'const foo = 1') - const compiler = makeCompiler( + const diagnostics = [ { - tsJestConfig: baseTsJestConfig, + category: DiagnosticCategory.Error, + code: 123, + messageText: 'An error occurs', + file: undefined, + start: 0, + length: 1, }, - jestCacheFS, - ) - - expect(compiler.getResolvedModules('const foo = 1', fileName, new Map())).toEqual([]) - }) - - test('should return resolved modules when file has resolved modules', () => { - const jestCacheFS = new Map() - const importedModule1 = join(mockFolder, 'thing1.ts') - const importedModule2 = join(mockFolder, 'thing2.ts') - const importedModule3 = join(mockFolder, 'babel-foo.config.js') - const fileContentWithModules = readFileSync(fileName, 'utf-8') - jestCacheFS.set(importedModule1, readFileSync(importedModule1, 'utf-8')) - const compiler = makeCompiler( { - tsJestConfig: baseTsJestConfig, + category: DiagnosticCategory.Error, + code: 456, + messageText: 'An error occurs', + file: undefined, + start: 0, + length: 1, }, - jestCacheFS, - ) - - expect( - compiler - .getResolvedModules(fileContentWithModules, fileName, new Map()) - .map((resolvedFileName) => normalize(resolvedFileName)), - ).toEqual([importedModule3, importedModule1, importedModule2]) - }) - }) - - describe('diagnostics', () => { - const importedFileName = join(mockFolder, 'thing.ts') - const importedFileContent = readFileSync(importedFileName, 'utf-8') - - it(`shouldn't report diagnostics when file name doesn't match diagnostic file pattern`, () => { - jestCacheFS.set(importedFileName, importedFileContent) - const compiler = makeCompiler( - { - tsJestConfig: { - ...baseTsJestConfig, - diagnostics: { exclude: ['foo.spec.ts'] }, - }, - }, - jestCacheFS, - ) + ] + compiler.configSet.raiseDiagnostics = jest.fn() + compiler.configSet.shouldReportDiagnostics = jest.fn().mockImplementation((fileToCheck) => { + return fileToCheck === fileName1 ? shouldReport : false + }) + // @ts-expect-error testing purpose + compiler._languageService.getEmitOutput = jest.fn().mockReturnValueOnce({ + outputFiles: [{ text: sourceMap }, { text: jsOutput }], + emitSkipped: false, + } as EmitOutput) + // @ts-expect-error testing purpose + compiler._getFileContentFromCache = jest.fn() + // @ts-expect-error testing purpose + compiler._languageService?.getSemanticDiagnostics = jest.fn().mockImplementation((fileToGet) => { + return fileToGet === fileName ? [] : [diagnostics[0]] + }) + // @ts-expect-error testing purpose + compiler._languageService?.getSyntacticDiagnostics = jest.fn().mockImplementation((fileToGet) => { + return fileToGet === fileName ? [] : [diagnostics[1]] + }) - expect(() => compiler.getCompiledOutput(importedFileContent, importedFileName, false)).not.toThrowError() - }) + compiler.getCompiledOutput(fileContent, fileName, { + depGraphs, + supportsStaticESM: false, + watchMode: true, + }) - it('should throw error when cannot compile', () => { - const fileName = 'test-cannot-compile.d.ts' - const source = ` - interface Foo { - a: string + 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() } - ` - jestCacheFS.set(fileName, source) - const compiler = makeCompiler( - { - tsJestConfig: baseTsJestConfig, - }, - jestCacheFS, - ) + }, + ) - expect(() => compiler.getCompiledOutput(source, fileName, false)).toThrowErrorMatchingSnapshot() + test('should not report diagnostics in watch mode when processing file is not used by other files', () => { + const compiler = makeCompiler({ + tsJestConfig: baseTsJestConfig, }) - - test('should report correct diagnostics when file content has changed', () => { - const compiler = makeCompiler( - { - tsJestConfig: baseTsJestConfig, - }, - jestCacheFS, - ) - const fileName = join(mockFolder, 'thing.ts') - const oldSource = ` - foo.split('-'); - ` - const newSource = ` - const foo = 'bla-bla' - foo.split('-'); - ` - jestCacheFS.set(fileName, oldSource) - - expect(() => compiler.getCompiledOutput(oldSource, fileName, false)).toThrowError() - - jestCacheFS.set(fileName, newSource) - - expect(() => compiler.getCompiledOutput(newSource, fileName, false)).not.toThrowError() + const depGraphs = new Map() + depGraphs.set(fileName1, { + fileContent, + resolvedModuleNames: ['bar.ts'], + }) + compiler.configSet.raiseDiagnostics = jest.fn() + compiler.configSet.shouldReportDiagnostics = jest.fn().mockReturnValue(false) + // @ts-expect-error testing purpose + compiler._languageService.getEmitOutput = jest.fn().mockReturnValueOnce({ + outputFiles: [{ text: sourceMap }, { text: jsOutput }], + emitSkipped: false, + } as EmitOutput) + // @ts-expect-error testing purpose + compiler._getFileContentFromCache = jest.fn() + // @ts-expect-error testing purpose + compiler._languageService?.getSemanticDiagnostics = jest.fn().mockReturnValueOnce([]) + // @ts-expect-error testing purpose + compiler._languageService?.getSyntacticDiagnostics = jest.fn().mockReturnValueOnce([]) + + compiler.getCompiledOutput(fileContent, fileName, { + depGraphs, + supportsStaticESM: false, + watchMode: true, }) - }) - - test('should pass Program instance into custom transformers', () => { - // eslint-disable-next-line no-console - console.log = jest.fn() - const fileName = join(mockFolder, 'thing.ts') - const compiler = makeCompiler( - { - tsJestConfig: { - ...baseTsJestConfig, - astTransformers: { - before: ['dummy-transformer'], - after: ['dummy-transformer'], - afterDeclarations: ['dummy-transformer'], - }, - }, - }, - jestCacheFS, - ) - - compiler.getCompiledOutput(readFileSync(fileName, 'utf-8'), fileName, false) - // eslint-disable-next-line no-console - expect(console.log).toHaveBeenCalled() - // eslint-disable-next-line - expect(((console.log as any) as jest.MockInstance).mock.calls[0][0].emit).toBeDefined() + // @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/compiler/ts-compiler.ts b/src/compiler/ts-compiler.ts index b252beede4..cb61534c2c 100644 --- a/src/compiler/ts-compiler.ts +++ b/src/compiler/ts-compiler.ts @@ -19,11 +19,19 @@ import type { ModuleResolutionHost, ModuleResolutionCache, ResolvedModuleWithFailedLookupLocations, + Diagnostic, } from 'typescript' import type { ConfigSet } from '../config/config-set' import { LINE_FEED, TS_TSX_REGEX } from '../constants' -import type { StringMap, TsCompilerInstance, TsJestAstTransformer, TTypeScript } from '../types' +import type { + DepGraphInfo, + StringMap, + TsCompilerInstance, + TsJestAstTransformer, + TsJestCompileOptions, + TTypeScript, +} from '../types' import { rootLogger } from '../utils/logger' import { Errors, interpolate } from '../utils/messages' @@ -110,6 +118,114 @@ export class TsCompiler implements TsCompilerInstance { } } + getResolvedModules(fileContent: string, fileName: string, runtimeCacheFS: StringMap): string[] { + // In watch mode, it is possible that the initial cacheFS becomes empty + if (!this.runtimeCacheFS.size) { + this._runtimeCacheFS = runtimeCacheFS + } + + this._logger.debug({ fileName }, 'getResolvedModules(): resolve direct imported module paths') + + const importedModulePaths: string[] = Array.from(new Set(this._getImportedModulePaths(fileContent, fileName))) + + this._logger.debug( + { fileName }, + 'getResolvedModules(): resolve nested imported module paths from directed imported module paths', + ) + + importedModulePaths.forEach((importedModulePath) => { + const resolvedFileContent = this._getFileContentFromCache(importedModulePath) + importedModulePaths.push( + ...this._getImportedModulePaths(resolvedFileContent, importedModulePath).filter( + (modulePath) => !importedModulePaths.includes(modulePath), + ), + ) + }) + + return importedModulePaths + } + + getCompiledOutput(fileContent: string, fileName: string, options: TsJestCompileOptions): string { + let moduleKind = this._initialCompilerOptions.module + let esModuleInterop = this._initialCompilerOptions.esModuleInterop + let allowSyntheticDefaultImports = this._initialCompilerOptions.allowSyntheticDefaultImports + if (options.supportsStaticESM && this.configSet.useESM) { + moduleKind = + !moduleKind || + (moduleKind && + ![this._ts.ModuleKind.ES2015, this._ts.ModuleKind.ES2020, this._ts.ModuleKind.ESNext].includes(moduleKind)) + ? this._ts.ModuleKind.ESNext + : moduleKind + // Make sure `esModuleInterop` and `allowSyntheticDefaultImports` true to support import CJS into ESM + esModuleInterop = true + allowSyntheticDefaultImports = true + } else { + moduleKind = this._ts.ModuleKind.CommonJS + } + this._compilerOptions = { + ...this._compilerOptions, + allowSyntheticDefaultImports, + esModuleInterop, + module: moduleKind, + } + if (this._languageService) { + this._logger.debug({ fileName }, 'getCompiledOutput(): compiling using language service') + + // Must set memory cache before attempting to compile + this._updateMemoryCache(fileContent, fileName) + const output: EmitOutput = this._languageService.getEmitOutput(fileName) + this._doTypeChecking(fileName, options.depGraphs, options.watchMode) + if (output.emitSkipped) { + this._logger.warn(interpolate(Errors.CannotProcessFile, { file: fileName })) + + return updateOutput(fileContent, fileName, '{}') + } + // Throw an error when requiring `.d.ts` files. + if (!output.outputFiles.length) { + throw new TypeError( + interpolate(Errors.UnableToRequireDefinitionFile, { + file: basename(fileName), + }), + ) + } + + return updateOutput(output.outputFiles[1].text, fileName, output.outputFiles[0].text) + } else { + this._logger.debug({ fileName }, 'getCompiledOutput(): compiling as isolated module') + + const result: TranspileOutput = this._transpileOutput(fileContent, fileName) + if (result.diagnostics && this.configSet.shouldReportDiagnostics(fileName)) { + this.configSet.raiseDiagnostics(result.diagnostics, fileName, this._logger) + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return updateOutput(result.outputText, fileName, result.sourceMapText!) + } + } + + protected _transpileOutput(fileContent: string, fileName: string): TranspileOutput { + return this._ts.transpileModule(fileContent, { + fileName, + transformers: this._makeTransformers(this.configSet.resolvedTransformers), + compilerOptions: this._compilerOptions, + reportDiagnostics: this.configSet.shouldReportDiagnostics(fileName), + }) + } + + protected _makeTransformers(customTransformers: TsJestAstTransformer): CustomTransformers { + return { + before: customTransformers.before.map((beforeTransformer) => + beforeTransformer.factory(this, beforeTransformer.options), + ) as Array | CustomTransformerFactory>, + after: customTransformers.after.map((afterTransformer) => + afterTransformer.factory(this, afterTransformer.options), + ) as Array | CustomTransformerFactory>, + afterDeclarations: customTransformers.afterDeclarations.map((afterDeclarations) => + afterDeclarations.factory(this, afterDeclarations.options), + ) as Array>, + } + } + /** * @internal */ @@ -186,37 +302,19 @@ export class TsCompiler implements TsCompilerInstance { this.program = this._languageService.getProgram() } - getResolvedModules(fileContent: string, fileName: string, runtimeCacheFS: StringMap): string[] { - // In watch mode, it is possible that the initial cacheFS becomes empty - if (!this.runtimeCacheFS.size) { - this._runtimeCacheFS = runtimeCacheFS + /** + * @internal + */ + private _getFileContentFromCache(filePath: string): string { + const normalizedFilePath = normalize(filePath) + let resolvedFileContent = this._runtimeCacheFS.get(normalizedFilePath) + if (!resolvedFileContent) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + resolvedFileContent = this._moduleResolutionHost!.readFile(normalizedFilePath)! + this._runtimeCacheFS.set(normalizedFilePath, resolvedFileContent) } - this._logger.debug({ fileName }, 'getResolvedModules(): resolve direct imported module paths') - - const importedModulePaths: string[] = Array.from(new Set(this._getImportedModulePaths(fileContent, fileName))) - - this._logger.debug( - { fileName }, - 'getResolvedModules(): resolve nested imported module paths from directed imported module paths', - ) - - importedModulePaths.forEach((importedModulePath) => { - const normalizedImportedModulePath = normalize(importedModulePath) - let resolvedFileContent = this._runtimeCacheFS.get(normalizedImportedModulePath) - if (!resolvedFileContent) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - resolvedFileContent = this._moduleResolutionHost!.readFile(importedModulePath)! - this._runtimeCacheFS.set(normalizedImportedModulePath, resolvedFileContent) - } - importedModulePaths.push( - ...this._getImportedModulePaths(resolvedFileContent, importedModulePath).filter( - (modulePath) => !importedModulePaths.includes(modulePath), - ), - ) - }) - - return importedModulePaths + return resolvedFileContent } /** @@ -254,91 +352,6 @@ export class TsCompiler implements TsCompilerInstance { ) } - getCompiledOutput(fileContent: string, fileName: string, supportsStaticESM: boolean): string { - let moduleKind = this._initialCompilerOptions.module - let esModuleInterop = this._initialCompilerOptions.esModuleInterop - let allowSyntheticDefaultImports = this._initialCompilerOptions.allowSyntheticDefaultImports - if (supportsStaticESM && this.configSet.useESM) { - moduleKind = - !moduleKind || - (moduleKind && - ![this._ts.ModuleKind.ES2015, this._ts.ModuleKind.ES2020, this._ts.ModuleKind.ESNext].includes(moduleKind)) - ? this._ts.ModuleKind.ESNext - : moduleKind - // Make sure `esModuleInterop` and `allowSyntheticDefaultImports` true to support import CJS into ESM - esModuleInterop = true - allowSyntheticDefaultImports = true - } else { - moduleKind = this._ts.ModuleKind.CommonJS - } - this._compilerOptions = { - ...this._compilerOptions, - allowSyntheticDefaultImports, - esModuleInterop, - module: moduleKind, - } - if (this._languageService) { - this._logger.debug({ fileName }, 'getCompiledOutput(): compiling using language service') - - // Must set memory cache before attempting to compile - this._updateMemoryCache(fileContent, fileName) - const output: EmitOutput = this._languageService.getEmitOutput(fileName) - - this._logger.debug({ fileName }, 'getCompiledOutput(): computing diagnostics using language service') - - this._doTypeChecking(fileName) - /* istanbul ignore next (this should never happen but is kept for security) */ - if (output.emitSkipped) { - this._logger.warn(interpolate(Errors.CannotProcessFile, { file: fileName })) - - return fileContent - } - // Throw an error when requiring `.d.ts` files. - if (!output.outputFiles.length) { - throw new TypeError( - interpolate(Errors.UnableToRequireDefinitionFile, { - file: basename(fileName), - }), - ) - } - - return updateOutput(output.outputFiles[1].text, fileName, output.outputFiles[0].text) - } else { - this._logger.debug({ fileName }, 'getCompiledOutput(): compiling as isolated module') - - const result: TranspileOutput = this._transpileOutput(fileContent, fileName) - if (result.diagnostics && this.configSet.shouldReportDiagnostics(fileName)) { - this.configSet.raiseDiagnostics(result.diagnostics, fileName, this._logger) - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return updateOutput(result.outputText, fileName, result.sourceMapText!) - } - } - - protected _transpileOutput(fileContent: string, fileName: string): TranspileOutput { - return this._ts.transpileModule(fileContent, { - fileName, - transformers: this._makeTransformers(this.configSet.resolvedTransformers), - compilerOptions: this._compilerOptions, - reportDiagnostics: this.configSet.shouldReportDiagnostics(fileName), - }) - } - - protected _makeTransformers(customTransformers: TsJestAstTransformer): CustomTransformers { - return { - before: customTransformers.before.map((beforeTransformer) => - beforeTransformer.factory(this, beforeTransformer.options), - ) as Array | CustomTransformerFactory>, - after: customTransformers.after.map((afterTransformer) => - afterTransformer.factory(this, afterTransformer.options), - ) as Array | CustomTransformerFactory>, - afterDeclarations: customTransformers.afterDeclarations.map((afterDeclarations) => - afterDeclarations.factory(this, afterDeclarations.options), - ) as Array>, - } - } - /** * @internal */ @@ -356,7 +369,6 @@ export class TsCompiler implements TsCompilerInstance { /** * @internal */ - /* istanbul ignore next */ private _updateMemoryCache(contents: string, fileName: string): void { this._logger.debug({ fileName }, 'updateMemoryCache: update memory cache for language service') @@ -368,7 +380,7 @@ export class TsCompiler implements TsCompilerInstance { shouldIncrementProjectVersion = true } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const prevVersion = this._fileVersionCache!.get(fileName) ?? 0 + const prevVersion = this._fileVersionCache!.get(fileName)! // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const previousContents = this._fileContentCache!.get(fileName) // Avoid incrementing cache when nothing has changed. @@ -377,8 +389,7 @@ export class TsCompiler implements TsCompilerInstance { this._fileVersionCache!.set(fileName, prevVersion + 1) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this._fileContentCache!.set(fileName, contents) - // Only bump project version when file is modified in cache, not when discovered for the first time - if (hit) shouldIncrementProjectVersion = true + shouldIncrementProjectVersion = true } /** * When a file is from node_modules or referenced to a referenced project and jest wants to transform it, we need @@ -395,16 +406,40 @@ export class TsCompiler implements TsCompilerInstance { /** * @internal */ - private _doTypeChecking(fileName: string): void { + private _doTypeChecking(fileName: string, depGraphs: Map, watchMode: boolean): void { 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`. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const diagnostics = this._languageService!.getSemanticDiagnostics(fileName).concat( + const diagnostics: Diagnostic[] = [ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this._languageService!.getSyntacticDiagnostics(fileName), - ) + ...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) + } + } + } } } diff --git a/src/compiler/ts-jest-compiler.spec.ts b/src/compiler/ts-jest-compiler.spec.ts index 0bfa1e1e66..d37eb012ff 100644 --- a/src/compiler/ts-jest-compiler.spec.ts +++ b/src/compiler/ts-jest-compiler.spec.ts @@ -21,9 +21,17 @@ describe('TsJestCompiler', () => { describe('getCompiledOutput', () => { test('should call getCompiledOutput from compiler instance', () => { - compiler.getCompiledOutput(fileContent, fileName, false) + compiler.getCompiledOutput(fileContent, fileName, { + depGraphs: new Map(), + supportsStaticESM: false, + watchMode: false, + }) - expect(TsCompiler.prototype.getCompiledOutput).toHaveBeenCalledWith(fileContent, fileName, false) + expect(TsCompiler.prototype.getCompiledOutput).toHaveBeenCalledWith(fileContent, fileName, { + depGraphs: new Map(), + supportsStaticESM: false, + watchMode: false, + }) }) }) }) diff --git a/src/compiler/ts-jest-compiler.ts b/src/compiler/ts-jest-compiler.ts index cb4e2daa1c..54fe73bafa 100644 --- a/src/compiler/ts-jest-compiler.ts +++ b/src/compiler/ts-jest-compiler.ts @@ -1,5 +1,5 @@ import type { ConfigSet } from '../config/config-set' -import type { CompilerInstance, StringMap } from '../types' +import type { CompilerInstance, StringMap, TsJestCompileOptions } from '../types' import { TsCompiler } from './ts-compiler' @@ -15,7 +15,7 @@ export class TsJestCompiler implements CompilerInstance { return this._compilerInstance.getResolvedModules(fileContent, fileName, runtimeCacheFS) } - getCompiledOutput(fileContent: string, fileName: string, supportsStaticESM: boolean): string { - return this._compilerInstance.getCompiledOutput(fileContent, fileName, supportsStaticESM) + getCompiledOutput(fileContent: string, fileName: string, options: TsJestCompileOptions): string { + return this._compilerInstance.getCompiledOutput(fileContent, fileName, options) } } diff --git a/src/ts-jest-transformer.spec.ts b/src/ts-jest-transformer.spec.ts index 51b71b834a..fe8605c7d1 100644 --- a/src/ts-jest-transformer.spec.ts +++ b/src/ts-jest-transformer.spec.ts @@ -44,6 +44,7 @@ Array [ "compiler", "depGraphs", "tsResolvedModulesCachePath", + "watchMode", ] `) }) diff --git a/src/ts-jest-transformer.ts b/src/ts-jest-transformer.ts index c533288bcc..2f80563a60 100644 --- a/src/ts-jest-transformer.ts +++ b/src/ts-jest-transformer.ts @@ -25,6 +25,7 @@ interface CachedConfigSet { compiler: CompilerInstance depGraphs: Map tsResolvedModulesCachePath: string | undefined + watchMode: boolean } interface TsJestHooksMap { @@ -49,6 +50,7 @@ export class TsJestTransformer implements SyncTransformer { private _tsResolvedModulesCachePath: string | undefined private _transformCfgStr!: string private _depGraphs: Map = new Map() + private _watchMode = false constructor() { this._logger = rootLogger.child({ namespace: 'ts-jest-transformer' }) @@ -74,6 +76,7 @@ export class TsJestTransformer implements SyncTransformer { this._compiler = ccs.compiler this._depGraphs = ccs.depGraphs this._tsResolvedModulesCachePath = ccs.tsResolvedModulesCachePath + this._watchMode = ccs.watchMode configSet = ccs.configSet } else { // try to look-it up by stringified version @@ -90,6 +93,7 @@ export class TsJestTransformer implements SyncTransformer { this._compiler = serializedCcs.compiler this._depGraphs = serializedCcs.depGraphs this._tsResolvedModulesCachePath = serializedCcs.tsResolvedModulesCachePath + this._watchMode = serializedCcs.watchMode configSet = serializedCcs.configSet } else { // create the new record in the index @@ -104,6 +108,7 @@ export class TsJestTransformer implements SyncTransformer { this._transformCfgStr = `${new JsonableValue(jest).serialized}${configSet.cacheSuffix}` this._createCompiler(configSet, cacheFS) this._getFsCachedResolvedModules(configSet) + this._watchMode = process.argv.includes('--watch') TsJestTransformer._cachedConfigSets.push({ jestConfig: new JsonableValue(config), configSet, @@ -111,6 +116,7 @@ export class TsJestTransformer implements SyncTransformer { compiler: this._compiler, depGraphs: this._depGraphs, tsResolvedModulesCachePath: this._tsResolvedModulesCachePath, + watchMode: this._watchMode, }) } } @@ -168,7 +174,11 @@ export class TsJestTransformer implements SyncTransformer { result = fileContent } else if (isJsFile || isTsFile) { // transpile TS code (source maps are included) - result = this._compiler.getCompiledOutput(fileContent, filePath, transformOptions.supportsStaticESM) + result = this._compiler.getCompiledOutput(fileContent, filePath, { + depGraphs: this._depGraphs, + supportsStaticESM: transformOptions.supportsStaticESM, + watchMode: this._watchMode, + }) } else { // we should not get called for files with other extension than js[x], ts[x] and d.ts, // TypeScript will bail if we try to compile, and if it was to call babel, users can diff --git a/src/types.ts b/src/types.ts index 04d82f4fa1..09df14f741 100644 --- a/src/types.ts +++ b/src/types.ts @@ -213,9 +213,15 @@ export interface DepGraphInfo { resolvedModuleNames: string[] } +export interface TsJestCompileOptions { + depGraphs: Map + watchMode: boolean + supportsStaticESM: boolean +} + export interface CompilerInstance { getResolvedModules(fileContent: string, fileName: string, runtimeCacheFS: StringMap): string[] - getCompiledOutput(fileContent: string, fileName: string, supportsStaticESM: boolean): string + getCompiledOutput(fileContent: string, fileName: string, options: TsJestCompileOptions): string } export interface TsCompilerInstance extends CompilerInstance { configSet: ConfigSet diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 96822d1aaf..6f2bdb7379 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -1,28 +1,4 @@ { - "compilerOptions": { - "declaration": false, - "noEmit": true, - "downlevelIteration": true, - "esModuleInterop": true, - "experimentalDecorators": true, - "inlineSourceMap": true, - "lib": ["esnext"], - "moduleResolution": "node", - "resolveJsonModule": true, - "noEmitOnError": true, - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "importsNotUsedAsValues": "error", - "strict": true, - "skipLibCheck": true, - "sourceMap": false, - "types": [ - "jest", - "node", - "react" - ] - }, + "extends": "./tsconfig.json", "include": [] }