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.
Jump to
Jump to file
Failed to load files.
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
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
@@ -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
@@ -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
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
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
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