Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(compiler): resolve nested imported modules for each processing file #2436

Merged
merged 1 commit into from Mar 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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