diff --git a/src/__helpers__/fakers.ts b/src/__helpers__/fakers.ts index b23207a319..325461cf6d 100644 --- a/src/__helpers__/fakers.ts +++ b/src/__helpers__/fakers.ts @@ -3,7 +3,7 @@ import { resolve } from 'path' import type { Config } from '@jest/types' import type { Logger } from 'bs-logger' -import { TsJestCompiler } from '../compiler/ts-jest-compiler' +import { TsCompiler } from '../compiler/ts-compiler' import { ConfigSet } from '../config/config-set' import type { StringMap, TsJestGlobalOptions } from '../types' import type { ImportReasons } from '../utils/messages' @@ -85,7 +85,7 @@ export function makeCompiler( parentConfig?: TsJestGlobalOptions } = {}, jestCacheFS: StringMap = new Map(), -): TsJestCompiler { +): TsCompiler { tsJestConfig = { ...tsJestConfig } tsJestConfig.diagnostics = { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -100,5 +100,5 @@ export function makeCompiler( } const cs = createConfigSet({ jestConfig, tsJestConfig, parentConfig, resolve: null }) - return new TsJestCompiler(cs, jestCacheFS) + return new TsCompiler(cs, jestCacheFS) } diff --git a/src/__mocks__/thing.spec.ts b/src/__mocks__/thing.spec.ts deleted file mode 100644 index 83b07c0d5f..0000000000 --- a/src/__mocks__/thing.spec.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { Thing } from './thing' - -export const thing: Thing = { a: 1, b: 2 } diff --git a/src/__mocks__/thing.ts b/src/__mocks__/thing.ts index f11e5f04f0..1d4cc258f1 100644 --- a/src/__mocks__/thing.ts +++ b/src/__mocks__/thing.ts @@ -1,4 +1,7 @@ -export interface Thing { - a: number - b: number -} +import { getFoo } from './thing1' +import { getFooBar } from './thing1' +import { getBar } from './thing2' + +getFoo('foo') +getBar('bar') +getFooBar('foobar') diff --git a/src/__mocks__/thing1.spec.ts b/src/__mocks__/thing1.spec.ts deleted file mode 100644 index 83b07c0d5f..0000000000 --- a/src/__mocks__/thing1.spec.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { Thing } from './thing' - -export const thing: Thing = { a: 1, b: 2 } diff --git a/src/__mocks__/thing1.ts b/src/__mocks__/thing1.ts new file mode 100644 index 0000000000..074d9378ee --- /dev/null +++ b/src/__mocks__/thing1.ts @@ -0,0 +1,11 @@ +import { camelCase } from 'lodash' + +import { getBar } from './thing2' + +export function getFoo(msg: string): string { + return camelCase(msg) + getBar(msg) +} + +export function getFooBar(msg: string): string { + return getBar(msg) + 'foo' +} diff --git a/src/__mocks__/thing2.ts b/src/__mocks__/thing2.ts new file mode 100644 index 0000000000..44e836fa75 --- /dev/null +++ b/src/__mocks__/thing2.ts @@ -0,0 +1,5 @@ +import { camelCase } from 'lodash' + +export function getBar(msg: string): string { + return camelCase(msg) + 'foo' +} diff --git a/src/compiler/__snapshots__/ts-compiler.spec.ts.snap b/src/compiler/__snapshots__/ts-compiler.spec.ts.snap index 8225e676e9..6ed799cb5b 100644 --- a/src/compiler/__snapshots__/ts-compiler.spec.ts.snap +++ b/src/compiler/__snapshots__/ts-compiler.spec.ts.snap @@ -19,19 +19,6 @@ exports[`TsCompiler isolatedModule false diagnostics should throw error when can 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 diagnostics shouldn't report diagnostic when processing file isn't used by any test files 1`] = ` -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 -", -] -`; - exports[`TsCompiler isolatedModule false jsx option should compile tsx file for jsx preserve 1`] = ` "\\"use strict\\"; const App = () => { @@ -49,7 +36,12 @@ const App = () => { `; exports[`TsCompiler isolatedModule false should compile codes with useESM true 1`] = ` -"export const thing = { a: 1, b: 2 }; +"import { getFoo } from './thing1'; +import { getFooBar } from './thing1'; +import { getBar } from './thing2'; +getFoo('foo'); +getBar('bar'); +getFooBar('foobar'); //# " `; @@ -81,6 +73,11 @@ exports.default = 42; `; exports[`TsCompiler isolatedModule true should transpile code with useESM true 1`] = ` -"export const thing = { a: 1, b: 2 }; +"import { getFoo } from './thing1'; +import { getFooBar } from './thing1'; +import { getBar } from './thing2'; +getFoo('foo'); +getBar('bar'); +getFooBar('foobar'); //# " `; diff --git a/src/compiler/ts-compiler.spec.ts b/src/compiler/ts-compiler.spec.ts index db1256bb6f..2a833fb3b4 100644 --- a/src/compiler/ts-compiler.spec.ts +++ b/src/compiler/ts-compiler.spec.ts @@ -1,15 +1,11 @@ import { readFileSync } from 'fs' -import { join } from 'path' +import { join, normalize } from 'path' -import { LogLevels } from 'bs-logger' - -import { createConfigSet, makeCompiler } from '../__helpers__/fakers' +import { makeCompiler } from '../__helpers__/fakers' import { logTargetMock } from '../__helpers__/mocks' import { mockFolder } from '../__helpers__/path' import ProcessedSource from '../__helpers__/processed-source' -import { TsCompiler } from './ts-compiler' - const logTarget = logTargetMock() describe('TsCompiler', () => { @@ -22,7 +18,7 @@ describe('TsCompiler', () => { const compiler = makeCompiler({ tsJestConfig: { ...baseTsJestConfig, useESM: true }, }) - const fileName = join(mockFolder, 'thing.spec.ts') + const fileName = join(mockFolder, 'thing.ts') const compiledOutput = compiler.getCompiledOutput(readFileSync(fileName, 'utf-8'), fileName, true) @@ -206,7 +202,7 @@ const t: string = f(5) }) describe('isolatedModule false', () => { - const baseTsJestConfig = { tsconfig: require.resolve('../../tsconfig.spec.json') } + const baseTsJestConfig = { tsconfig: join(process.cwd(), 'tsconfig.spec.json') } const jestCacheFS = new Map() beforeEach(() => { @@ -214,20 +210,18 @@ const t: string = f(5) }) test('should compile codes with useESM true', () => { - const compiler = new TsCompiler( - createConfigSet({ - tsJestConfig: { - ...baseTsJestConfig, - useESM: true, - tsconfig: { - esModuleInterop: false, - allowSyntheticDefaultImports: false, - }, + const compiler = makeCompiler({ + tsJestConfig: { + ...baseTsJestConfig, + tsconfig: { + module: 'ESNext', + esModuleInterop: false, + allowSyntheticDefaultImports: false, }, - }), - new Map(), - ) - const fileName = join(mockFolder, 'thing.spec.ts') + useESM: true, + }, + }) + const fileName = join(mockFolder, 'thing.ts') const compiledOutput = compiler.getCompiledOutput(readFileSync(fileName, 'utf-8'), fileName, true) @@ -375,15 +369,14 @@ const t: string = f(5) }) describe('getResolvedModules', () => { - const fileName = join(__dirname, '..', '__mocks__', 'thing.spec.ts') - const fileContent = 'const foo = 1' + const fileName = join(mockFolder, 'thing.ts') test('should return undefined when file name is not known to compiler', () => { const compiler = makeCompiler({ tsJestConfig: baseTsJestConfig, }) - expect(compiler.getResolvedModules(fileContent, fileName, new Map())).toEqual([]) + expect(compiler.getResolvedModules('const foo = 1', fileName, new Map())).toEqual([]) }) test('should return undefined when it is isolatedModules true', () => { @@ -394,12 +387,12 @@ const t: string = f(5) }, }) - expect(compiler.getResolvedModules(fileContent, fileName, new Map())).toEqual([]) + 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, fileContent) + jestCacheFS.set(fileName, 'const foo = 1') const compiler = makeCompiler( { tsJestConfig: baseTsJestConfig, @@ -407,13 +400,15 @@ const t: string = f(5) jestCacheFS, ) - expect(compiler.getResolvedModules(fileContent, fileName, new Map())).toEqual([]) + 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 fileContentWithModules = readFileSync(fileName, 'utf-8') - jestCacheFS.set(fileName, fileContentWithModules) + jestCacheFS.set(importedModule1, readFileSync(importedModule1, 'utf-8')) const compiler = makeCompiler( { tsJestConfig: baseTsJestConfig, @@ -421,12 +416,16 @@ const t: string = f(5) jestCacheFS, ) - expect(compiler.getResolvedModules(fileContentWithModules, fileName, new Map())).not.toEqual([]) + expect( + compiler + .getResolvedModules(fileContentWithModules, fileName, new Map()) + .map((resolvedFileName) => normalize(resolvedFileName)), + ).toEqual([importedModule1, importedModule2]) }) }) describe('diagnostics', () => { - const importedFileName = require.resolve('../__mocks__/thing.ts') + 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`, () => { @@ -444,21 +443,6 @@ const t: string = f(5) expect(() => compiler.getCompiledOutput(importedFileContent, importedFileName, false)).not.toThrowError() }) - it(`shouldn't report diagnostic when processing file isn't used by any test files`, () => { - jestCacheFS.set('foo.ts', importedFileContent) - const compiler = makeCompiler( - { - tsJestConfig: baseTsJestConfig, - }, - jestCacheFS, - ) - logTarget.clear() - - compiler.getCompiledOutput(importedFileContent, 'foo.ts', false) - - expect(logTarget.filteredLines(LogLevels.debug, Infinity)).toMatchSnapshot() - }) - it('should throw error when cannot compile', () => { const fileName = 'test-cannot-compile.d.ts' const source = ` @@ -484,7 +468,7 @@ const t: string = f(5) }, jestCacheFS, ) - const fileName = join(process.cwd(), 'src', '__mocks__', 'thing.spec.ts') + const fileName = join(mockFolder, 'thing.ts') const oldSource = ` foo.split('-'); ` @@ -505,7 +489,7 @@ const t: string = f(5) test('should pass Program instance into custom transformers', () => { // eslint-disable-next-line no-console console.log = jest.fn() - const fileName = join(mockFolder, 'thing.spec.ts') + const fileName = join(mockFolder, 'thing.ts') const compiler = makeCompiler( { tsJestConfig: { diff --git a/src/compiler/ts-compiler.ts b/src/compiler/ts-compiler.ts index d8e9a829d9..87dc043a3f 100644 --- a/src/compiler/ts-compiler.ts +++ b/src/compiler/ts-compiler.ts @@ -18,6 +18,7 @@ import type { CustomTransformers, ModuleResolutionHost, ModuleResolutionCache, + ResolvedModuleWithFailedLookupLocations, } from 'typescript' import { ConfigSet, TS_JEST_OUT_DIR } from '../config/config-set' @@ -181,19 +182,7 @@ export class TsCompiler implements TsCompilerInstance { getDefaultLibFileName: () => this._ts.getDefaultLibFilePath(this._compilerOptions), getCustomTransformers: () => this._makeTransformers(this.configSet.resolvedTransformers), resolveModuleNames: (moduleNames: string[], containingFile: string): Array => - moduleNames.map((moduleName) => { - const { resolvedModule } = this._ts.resolveModuleName( - moduleName, - containingFile, - this._compilerOptions, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this._moduleResolutionHost!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this._moduleResolutionCache!, - ) - - return resolvedModule - }), + moduleNames.map((moduleName) => this._resolveModuleName(moduleName, containingFile).resolvedModule), } this._logger.debug('created language service') @@ -208,23 +197,66 @@ export class TsCompiler implements TsCompilerInstance { 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 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 + } + + /** + * @internal + */ + private _getImportedModulePaths(resolvedFileContent: string, containingFile: string): string[] { return this._ts - .preProcessFile(fileContent, true, true) + .preProcessFile(resolvedFileContent, true, true) .importedFiles.map((importedFile) => { - const { resolvedModule } = this._ts.resolveModuleName( - importedFile.fileName, - fileName, - this._compilerOptions, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this._moduleResolutionHost!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this._moduleResolutionCache!, - ) + const { resolvedModule } = this._resolveModuleName(importedFile.fileName, containingFile) + /* istanbul ignore next already covered */ + const resolvedFileName = resolvedModule?.resolvedFileName - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return resolvedModule?.resolvedFileName ?? '' + /* istanbul ignore next already covered */ + return resolvedFileName && !resolvedModule?.isExternalLibraryImport ? resolvedFileName : '' }) - .filter((resolvedFileName) => !!resolvedFileName) + .filter((resolveFileName) => !!resolveFileName) + } + + /** + * @internal + */ + private _resolveModuleName( + moduleNameToResolve: string, + containingFile: string, + ): ResolvedModuleWithFailedLookupLocations { + return this._ts.resolveModuleName( + moduleNameToResolve, + containingFile, + this._compilerOptions, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this._moduleResolutionHost!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this._moduleResolutionCache!, + ) } getCompiledOutput(fileContent: string, fileName: string, supportsStaticESM: boolean): string { diff --git a/src/compiler/ts-jest-compiler.spec.ts b/src/compiler/ts-jest-compiler.spec.ts new file mode 100644 index 0000000000..0bfa1e1e66 --- /dev/null +++ b/src/compiler/ts-jest-compiler.spec.ts @@ -0,0 +1,29 @@ +import { createConfigSet } from '../__helpers__/fakers' + +import { TsCompiler } from './ts-compiler' +import { TsJestCompiler } from './ts-jest-compiler' + +describe('TsJestCompiler', () => { + TsCompiler.prototype.getResolvedModules = jest.fn() + TsCompiler.prototype.getCompiledOutput = jest.fn() + const runtimeCacheFS = new Map() + const fileContent = 'const foo = 1' + const fileName = 'foo.ts' + const compiler = new TsJestCompiler(createConfigSet(), runtimeCacheFS) + + describe('getResolvedModules', () => { + test('should call getResolvedModules from compiler instance', () => { + compiler.getResolvedModules(fileContent, fileName, runtimeCacheFS) + + expect(TsCompiler.prototype.getResolvedModules).toHaveBeenCalledWith(fileContent, fileName, runtimeCacheFS) + }) + }) + + describe('getCompiledOutput', () => { + test('should call getCompiledOutput from compiler instance', () => { + compiler.getCompiledOutput(fileContent, fileName, false) + + expect(TsCompiler.prototype.getCompiledOutput).toHaveBeenCalledWith(fileContent, fileName, false) + }) + }) +})