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

feat(compiler): allow isolatedModule: true to have ESM support #2219

Merged
merged 1 commit into from
Dec 17, 2020
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,021 changes: 0 additions & 1,021 deletions e2e/__tests__/module-kinds/__snapshots__/es2015-esm.test.ts.snap

This file was deleted.

1,021 changes: 0 additions & 1,021 deletions e2e/__tests__/module-kinds/__snapshots__/esnext-esm.test.ts.snap

This file was deleted.

9 changes: 0 additions & 9 deletions e2e/__tests__/module-kinds/es2015-esm.test.ts

This file was deleted.

9 changes: 0 additions & 9 deletions e2e/__tests__/module-kinds/esnext-esm.test.ts

This file was deleted.

12 changes: 12 additions & 0 deletions src/compiler/__snapshots__/ts-compiler.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,15 @@ exports[`TsCompiler isolatedModule true should compile js file for allowJs true
version: 3
================================================================================
`;

exports[`TsCompiler isolatedModule true support ESM should transpile codes to correct syntax with supportsStaticESM and useESM options 1`] = `99`;

exports[`TsCompiler isolatedModule true support ESM should transpile codes to correct syntax with supportsStaticESM and useESM options 2`] = `99`;

exports[`TsCompiler isolatedModule true support ESM should transpile codes to correct syntax with supportsStaticESM and useESM options 3`] = `99`;

exports[`TsCompiler isolatedModule true support ESM should transpile codes to correct syntax with supportsStaticESM and useESM options 4`] = `1`;

exports[`TsCompiler isolatedModule true support ESM should transpile codes to correct syntax with supportsStaticESM and useESM options 5`] = `1`;

exports[`TsCompiler isolatedModule true support ESM should transpile codes to correct syntax with supportsStaticESM and useESM options 6`] = `1`;
52 changes: 52 additions & 0 deletions src/compiler/ts-compiler.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from 'fs'
import { LogLevels } from 'bs-logger'
import { join } from 'path'
import ts from 'typescript'

import { TS_JEST_OUT_DIR } from '../config/config-set'
import { makeCompiler } from '../__helpers__/fakers'
Expand Down Expand Up @@ -160,6 +161,57 @@ const t: string = f(5)
).not.toThrowError()
})
})

describe('support ESM', () => {
test.each([
{
supportsStaticESM: true,
useESM: true,
moduleKind: 'esnext',
},
{
supportsStaticESM: true,
useESM: true,
moduleKind: 'amd',
},
{
supportsStaticESM: true,
useESM: true,
moduleKind: undefined,
},
{
supportsStaticESM: false,
useESM: true,
moduleKind: 'esnext',
},
{
supportsStaticESM: true,
useESM: false,
moduleKind: 'amd',
},
{
supportsStaticESM: false,
useESM: false,
moduleKind: 'es2015',
},
])('should transpile codes to correct syntax with supportsStaticESM and useESM options', (data) => {
const transpileModuleSpy = (ts.transpileModule = jest.fn().mockReturnValueOnce({
outputText: 'var foo = 1',
diagnostics: [],
sourceMapText: '{}',
}))
const fileContent = `const foo = import('./foo')`
const fileName = 'foo.ts'

const compiler = makeCompiler({
tsJestConfig: { ...baseTsJestConfig, tsconfig: { module: data.moduleKind as any }, useESM: data.useESM },
})
compiler.getCompiledOutput(fileContent, fileName, data.supportsStaticESM)

expect(transpileModuleSpy).toHaveBeenCalled()
expect(transpileModuleSpy.mock.calls[0][1].compilerOptions.module).toMatchSnapshot()
})
})
})

describe('isolatedModule false', () => {
Expand Down
38 changes: 27 additions & 11 deletions src/compiler/ts-compiler.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { LogContexts, Logger, LogLevels } from 'bs-logger'
import memoize from 'lodash.memoize'
import { basename, normalize, relative } from 'path'
import type {
import {
EmitOutput,
LanguageService,
LanguageServiceHost,
ParsedCommandLine,
ResolvedModuleFull,
TranspileOutput,
ModuleKind,
} from 'typescript'

import { updateOutput } from './compiler-utils'
Expand All @@ -17,6 +18,8 @@ import type { CompilerInstance, ResolvedModulesMap, StringMap, TTypeScript } fro
import { rootLogger } from '../utils/logger'
import { Errors, interpolate } from '../utils/messages'

const AVAILABLE_ESM_MODULE_KINDS = [ModuleKind.ES2015, ModuleKind.ES2020, ModuleKind.ESNext]

/**
* @internal
*/
Expand All @@ -41,6 +44,10 @@ export class TsCompiler implements CompilerInstance {
}

private _createLanguageService(): void {
const compilerOptions = {
...this._parsedTsConfig.options,
module: ModuleKind.CommonJS,
}
const serviceHostTraceCtx = {
namespace: 'ts:serviceHost',
call: null,
Expand All @@ -60,11 +67,7 @@ export class TsCompiler implements CompilerInstance {
realpath: this._ts.sys.realpath && memoize(this._ts.sys.realpath),
getDirectories: memoize(this._ts.sys.getDirectories),
}
const moduleResolutionCache = this._ts.createModuleResolutionCache(
this.configSet.cwd,
(x) => x,
this._parsedTsConfig.options,
)
const moduleResolutionCache = this._ts.createModuleResolutionCache(this.configSet.cwd, (x) => x, compilerOptions)
/* istanbul ignore next */
const serviceHost: LanguageServiceHost = {
getProjectVersion: () => String(this._projectVersion),
Expand Down Expand Up @@ -109,15 +112,15 @@ export class TsCompiler implements CompilerInstance {
realpath: this._ts.sys.realpath && memoize(this._ts.sys.realpath),
getNewLine: () => LINE_FEED,
getCurrentDirectory: () => this.configSet.cwd,
getCompilationSettings: () => this._parsedTsConfig.options,
getDefaultLibFileName: () => this._ts.getDefaultLibFilePath(this._parsedTsConfig.options),
getCompilationSettings: () => compilerOptions,
getDefaultLibFileName: () => this._ts.getDefaultLibFilePath(compilerOptions),
getCustomTransformers: () => this.configSet.customTransformers,
resolveModuleNames: (moduleNames: string[], containingFile: string): (ResolvedModuleFull | undefined)[] =>
moduleNames.map((moduleName) => {
const { resolvedModule } = this._ts.resolveModuleName(
moduleName,
containingFile,
this._parsedTsConfig.options,
compilerOptions,
moduleResolutionHost,
moduleResolutionCache,
)
Expand All @@ -138,7 +141,7 @@ export class TsCompiler implements CompilerInstance {
return (this._languageService?.getProgram()?.getSourceFile(fileName) as any)?.resolvedModules
}

getCompiledOutput(fileContent: string, fileName: string): string {
getCompiledOutput(fileContent: string, fileName: string, supportsStaticESM: boolean): string {
if (this._languageService) {
this._logger.debug({ fileName }, 'getCompiledOutput(): compiling using language service')

Expand All @@ -164,12 +167,25 @@ export class TsCompiler implements CompilerInstance {

return updateOutput(output.outputFiles[1].text, fileName, output.outputFiles[0].text)
} else {
let moduleKind = this._parsedTsConfig.options.module
if (supportsStaticESM && this.configSet.useESM) {
moduleKind =
!moduleKind || (moduleKind && !AVAILABLE_ESM_MODULE_KINDS.includes(moduleKind))
? ModuleKind.ESNext
: moduleKind
} else {
moduleKind = ModuleKind.CommonJS
}

this._logger.debug({ fileName }, 'getCompiledOutput(): compiling as isolated module')

const result: TranspileOutput = this._ts.transpileModule(fileContent, {
fileName,
transformers: this.configSet.customTransformers,
compilerOptions: this._parsedTsConfig.options,
compilerOptions: {
...this._parsedTsConfig.options,
module: moduleKind,
},
reportDiagnostics: this.configSet.shouldReportDiagnostics(fileName),
})
if (result.diagnostics && this.configSet.shouldReportDiagnostics(fileName)) {
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/ts-jest-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class TsJestCompiler implements CompilerInstance {
return this._compilerInstance.getResolvedModulesMap(fileContent, fileName)
}

getCompiledOutput(fileContent: string, fileName: string): string {
return this._compilerInstance.getCompiledOutput(fileContent, fileName)
getCompiledOutput(fileContent: string, fileName: string, supportsStaticESM = false): string {
return this._compilerInstance.getCompiledOutput(fileContent, fileName, supportsStaticESM)
}
}
17 changes: 4 additions & 13 deletions src/config/config-set.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ describe('parsedTsConfig', () => {
})

it('should override some options', () => {
expect(get({ tsconfig: { module: 'esnext' as any, inlineSources: false } }).options).toMatchObject({
module: ts.ModuleKind.CommonJS,
expect(get({ tsconfig: { inlineSources: false } }).options).toMatchObject({
inlineSources: true,
})
})
Expand All @@ -70,19 +69,19 @@ describe('parsedTsConfig', () => {
})
})

it('should warn about possibly wrong module config and set synth. default imports', () => {
it('should warn about possibly wrong module config and set synth. default imports with module None/AMD/UMD/System', () => {
const target = logTargetMock()
target.clear()
const cs = createConfigSet({
tsJestConfig: {
tsconfig: { module: 'ES6', esModuleInterop: false } as any,
tsconfig: { module: 'AMD', esModuleInterop: false } as any,
diagnostics: { warnOnly: true, pretty: false },
},
resolve: null,
})

expect(cs.parsedTsConfig.options).toMatchObject({
module: ts.ModuleKind.CommonJS,
module: ts.ModuleKind.AMD,
allowSyntheticDefaultImports: true,
esModuleInterop: false,
})
Expand Down Expand Up @@ -692,7 +691,6 @@ describe('_resolveTsConfig', () => {
expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub)
expect(conf.options.allowSyntheticDefaultImports).toEqual(true)
expect(conf.errors).toMatchSnapshot()
expect(cs.parsedTsConfig.options.module).toEqual(ts.ModuleKind.CommonJS)
})

it('should use given tsconfig path', () => {
Expand All @@ -715,7 +713,6 @@ describe('_resolveTsConfig', () => {
expect(parseConfig.mock.calls[0][2]).toBe('/foo')
expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub)
expect(conf.errors).toMatchSnapshot()
expect(cs.parsedTsConfig.options.module).not.toEqual(ts.ModuleKind.CommonJS)
})
})

Expand Down Expand Up @@ -756,7 +753,6 @@ describe('_resolveTsConfig', () => {
expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub)
expect(conf.options.allowSyntheticDefaultImports).toEqual(true)
expect(conf.errors).toMatchSnapshot()
expect(cs.parsedTsConfig.options.module).toEqual(ts.ModuleKind.CommonJS)
})

it('should use given tsconfig path', () => {
Expand All @@ -779,7 +775,6 @@ describe('_resolveTsConfig', () => {
expect(parseConfig.mock.calls[0][2]).toBe('/foo')
expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub)
expect(conf.errors).toMatchSnapshot()
expect(cs.parsedTsConfig.options.module).not.toEqual(ts.ModuleKind.CommonJS)
})
})

Expand Down Expand Up @@ -820,7 +815,6 @@ describe('_resolveTsConfig', () => {
expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub)
expect(conf.options.allowSyntheticDefaultImports).toBeUndefined()
expect(conf.errors).toEqual([])
expect(cs.parsedTsConfig.options.module).toEqual(ts.ModuleKind.CommonJS)
})

it('should use given tsconfig path', () => {
Expand All @@ -843,7 +837,6 @@ describe('_resolveTsConfig', () => {
expect(parseConfig.mock.calls[0][2]).toBe('/foo')
expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub)
expect(conf.errors).toEqual([])
expect(cs.parsedTsConfig.options.module).not.toEqual(ts.ModuleKind.CommonJS)
})
})

Expand Down Expand Up @@ -884,7 +877,6 @@ describe('_resolveTsConfig', () => {
expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub)
expect(conf.errors).toEqual([])
expect(conf.options.allowSyntheticDefaultImports).toEqual(true)
expect(cs.parsedTsConfig.options.module).toEqual(ts.ModuleKind.CommonJS)
})

it('should use given tsconfig path', () => {
Expand All @@ -908,7 +900,6 @@ describe('_resolveTsConfig', () => {
expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub)
expect(conf.errors).toEqual([])
expect(conf.options.allowSyntheticDefaultImports).toEqual(true)
expect(cs.parsedTsConfig.options.module).not.toEqual(ts.ModuleKind.CommonJS)
})
})
})
Expand Down
30 changes: 15 additions & 15 deletions src/config/config-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import type { Config } from '@jest/types'
import { LogContexts, Logger } from 'bs-logger'
import { existsSync, readFileSync } from 'fs'
import { globsToMatcher } from 'jest-util'
import json5 = require('json5')
import json5 from 'json5'
import { dirname, extname, isAbsolute, join, normalize, resolve } from 'path'
import {
CompilerOptions,
CustomTransformers,
Diagnostic,
FormatDiagnosticsHost,
ParsedCommandLine,
ModuleKind,
ScriptTarget,
} from 'typescript'

Expand Down Expand Up @@ -115,6 +116,7 @@ export class ConfigSet {
tsCacheDir: string | undefined
parsedTsConfig!: ParsedCommandLine | Record<string, any>
customTransformers: CustomTransformers = Object.create(null)
useESM = false
/**
* @internal
*/
Expand Down Expand Up @@ -183,7 +185,7 @@ export class ConfigSet {
this.logger.debug({ compilerModule: this.compilerModule }, 'normalized compiler module config via ts-jest option')

this._backportJestCfg()
this._setupTsJestCfg(options)
this._setupConfigSet(options)
this._resolveTsCacheDir()
this._matchablePatterns = [...this._jestCfg.testMatch, ...this._jestCfg.testRegex].filter(
(pattern) =>
Expand Down Expand Up @@ -215,7 +217,10 @@ export class ConfigSet {
/**
* @internal
*/
private _setupTsJestCfg(options: TsJestGlobalOptions): void {
private _setupConfigSet(options: TsJestGlobalOptions): void {
// useESM
this.useESM = options.useESM ?? false

// babel config (for babel-jest) default is undefined so we don't need to have fallback like tsConfig
if (!options.babelConfig) {
this.logger.debug('babel is disabled')
Expand Down Expand Up @@ -244,12 +249,7 @@ export class ConfigSet {
}

this.logger.debug({ babelConfig: this.babelConfig }, 'normalized babel config via ts-jest option')
}
if (!this.babelConfig) {
this._overriddenCompilerOptions.module = this.jestConfig.extensionsToTreatAsEsm.length
? undefined
: this.compilerModule.ModuleKind.CommonJS
} else {

this.babelJestTransformer = importer
.babelJest(ImportReasons.BabelJest)
.createTransformer(this.babelConfig) as BabelJestTransformer
Expand Down Expand Up @@ -412,19 +412,19 @@ export class ConfigSet {
const finalOptions = result.options
// Target ES2015 output by default (instead of ES3).
if (finalOptions.target === undefined) {
finalOptions.target = ts.ScriptTarget.ES2015
finalOptions.target = ScriptTarget.ES2015
}

// check the module interoperability
const target = finalOptions.target
// compute the default if not set
const defaultModule = [ts.ScriptTarget.ES3, ts.ScriptTarget.ES5].includes(target)
? ts.ModuleKind.CommonJS
: ts.ModuleKind.ESNext
const defaultModule = [ScriptTarget.ES3, ScriptTarget.ES5].includes(target)
? ModuleKind.CommonJS
: ModuleKind.ESNext
const moduleValue = finalOptions.module ?? defaultModule
if (
'module' in forcedOptions &&
moduleValue !== forcedOptions.module &&
!this.babelConfig &&
moduleValue !== ModuleKind.CommonJS &&
!(finalOptions.esModuleInterop || finalOptions.allowSyntheticDefaultImports)
) {
result.errors.push({
Expand Down
Loading