Skip to content

Commit

Permalink
fix(compiler): resolve nested imported modules for each processing fi…
Browse files Browse the repository at this point in the history
…le (#2436)

Related to #1390 and #1747
  • Loading branch information
ahnpnl committed Mar 10, 2021
1 parent c697264 commit 3cb9019
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 101 deletions.
6 changes: 3 additions & 3 deletions src/__helpers__/fakers.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -85,7 +85,7 @@ export function makeCompiler(
parentConfig?: TsJestGlobalOptions
} = {},
jestCacheFS: StringMap = new Map<string, string>(),
): TsJestCompiler {
): TsCompiler {
tsJestConfig = { ...tsJestConfig }
tsJestConfig.diagnostics = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -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)
}
3 changes: 0 additions & 3 deletions src/__mocks__/thing.spec.ts

This file was deleted.

11 changes: 7 additions & 4 deletions 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')
3 changes: 0 additions & 3 deletions src/__mocks__/thing1.spec.ts

This file was deleted.

11 changes: 11 additions & 0 deletions 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'
}
5 changes: 5 additions & 0 deletions src/__mocks__/thing2.ts
@@ -0,0 +1,5 @@
import { camelCase } from 'lodash'

export function getBar(msg: string): string {
return camelCase(msg) + 'foo'
}
27 changes: 12 additions & 15 deletions src/compiler/__snapshots__/ts-compiler.spec.ts.snap
Expand Up @@ -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 = () => {
Expand All @@ -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');
//# "
`;

Expand Down Expand Up @@ -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');
//# "
`;
78 changes: 31 additions & 47 deletions 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', () => {
Expand All @@ -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)

Expand Down Expand Up @@ -206,28 +202,26 @@ 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<string, string>()

beforeEach(() => {
logTarget.clear()
})

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)

Expand Down Expand Up @@ -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', () => {
Expand All @@ -394,39 +387,45 @@ 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<string, string>()
jestCacheFS.set(fileName, fileContent)
jestCacheFS.set(fileName, 'const foo = 1')
const compiler = makeCompiler(
{
tsJestConfig: baseTsJestConfig,
},
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<string, string>()
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,
},
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`, () => {
Expand All @@ -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 = `
Expand All @@ -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('-');
`
Expand All @@ -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: {
Expand Down
84 changes: 58 additions & 26 deletions src/compiler/ts-compiler.ts
Expand Up @@ -18,6 +18,7 @@ import type {
CustomTransformers,
ModuleResolutionHost,
ModuleResolutionCache,
ResolvedModuleWithFailedLookupLocations,
} from 'typescript'

import { ConfigSet, TS_JEST_OUT_DIR } from '../config/config-set'
Expand Down Expand Up @@ -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<ResolvedModuleFull | undefined> =>
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')
Expand All @@ -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 {
Expand Down

0 comments on commit 3cb9019

Please sign in to comment.