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 custom transformers to access internal Program #2299

Merged
merged 1 commit into from Jan 25, 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 e2e/__cases__/ast-transformers/with-extra-options/foo.js
@@ -1,8 +1,8 @@
const { LogContexts, LogLevels } = require('bs-logger')

function factory(cs, extraOpts = Object.create(null)) {
const logger = cs.logger.child({ namespace: 'dummy-transformer' })
const ts = cs.compilerModule
function factory({ configSet }, extraOpts = Object.create(null)) {
const logger = configSet.logger.child({ namespace: 'dummy-transformer' })
const ts = configSet.compilerModule
logger.debug('Dummy transformer with extra options', JSON.stringify(extraOpts))

function createVisitor(_ctx, _sf) {
Expand Down
8 changes: 5 additions & 3 deletions src/__mocks__/dummy-transformer.js
@@ -1,8 +1,10 @@
const { LogContexts, LogLevels } = require('bs-logger')

function factory(cs) {
const logger = cs.logger.child({ namespace: 'dummy-transformer' })
const ts = cs.compilerModule
function factory(tsCompiler) {
const logger = tsCompiler.configSet.logger.child({ namespace: 'dummy-transformer' })
const ts = tsCompiler.configSet.compilerModule
// eslint-disable-next-line no-console
console.log(tsCompiler.program)

function createVisitor(_ctx, _) {
return (node) => node
Expand Down
54 changes: 53 additions & 1 deletion src/compiler/ts-compiler.spec.ts
Expand Up @@ -30,7 +30,7 @@ describe('TsCompiler', () => {
expect(new ProcessedSource(compiledOutput, fileName).outputCodeWithoutMaps).toMatchSnapshot()
})

it('should compile js file for allowJs true', () => {
test('should compile js file for allowJs true', () => {
const fileName = 'foo.js'
const compiler = makeCompiler({
tsJestConfig: { ...baseTsJestConfig, tsconfig: { allowJs: true, outDir: TS_JEST_OUT_DIR } },
Expand Down Expand Up @@ -179,6 +179,32 @@ const t: string = f(5)
).not.toThrowError()
})
})

test('should use correct custom AST transformers', () => {
// eslint-disable-next-line no-console
console.log = jest.fn()
const fileName = 'foo.js'
const compiler = makeCompiler({
tsJestConfig: {
...baseTsJestConfig,
tsconfig: {
allowJs: true,
outDir: TS_JEST_OUT_DIR,
},
astTransformers: {
before: ['dummy-transformer'],
after: ['dummy-transformer'],
afterDeclarations: ['dummy-transformer'],
},
},
})
const source = 'export default 42'

compiler.getCompiledOutput(source, fileName, false)

// eslint-disable-next-line no-console
expect(console.log).toHaveBeenCalledTimes(3)
})
})

describe('isolatedModule false', () => {
Expand Down Expand Up @@ -452,5 +478,31 @@ const t: string = f(5)
expect(() => compiler.getCompiledOutput(source, fileName, false)).toThrowErrorMatchingSnapshot()
})
})

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 compiler = makeCompiler(
{
tsJestConfig: {
...baseTsJestConfig,
astTransformers: {
before: ['dummy-transformer'],
after: ['dummy-transformer'],
afterDeclarations: ['dummy-transformer'],
},
},
},
jestCacheFS,
)

compiler.getCompiledOutput(readFileSync(fileName, 'utf-8'), fileName, false)

// eslint-disable-next-line no-console
expect(console.log).toHaveBeenCalled()
// eslint-disable-next-line no-console
expect(((console.log as any) as jest.MockInstance<any, any>).mock.calls[0][0].emit).toBeDefined()
})
})
})
49 changes: 36 additions & 13 deletions src/compiler/ts-compiler.ts
Expand Up @@ -10,11 +10,16 @@ import type {
ResolvedModuleFull,
TranspileOutput,
CompilerOptions,
SourceFile,
Program,
TransformerFactory,
Bundle,
CustomTransformerFactory,
} from 'typescript'

import type { ConfigSet } from '../config/config-set'
import { LINE_FEED } from '../constants'
import type { CompilerInstance, ResolvedModulesMap, StringMap, TTypeScript } from '../types'
import type { ResolvedModulesMap, StringMap, TsCompilerInstance, TTypeScript } from '../types'
import { rootLogger } from '../utils/logger'
import { Errors, interpolate } from '../utils/messages'

Expand All @@ -23,23 +28,22 @@ import { updateOutput } from './compiler-utils'
/**
* @internal
*/
export class TsCompiler implements CompilerInstance {
export class TsCompiler implements TsCompilerInstance {
private readonly _logger: Logger
private readonly _ts: TTypeScript
private readonly _parsedTsConfig: ParsedCommandLine
private readonly _compilerCacheFS: Map<string, number> = new Map<string, number>()
private readonly _jestCacheFS: StringMap
private readonly _initialCompilerOptions: CompilerOptions
private _compilerOptions: CompilerOptions
private _cachedReadFile: ((fileName: string) => string | undefined) | undefined
private _projectVersion = 1
private _languageService: LanguageService | undefined
program: Program | undefined

constructor(readonly configSet: ConfigSet, readonly jestCacheFS: StringMap) {
this._ts = configSet.compilerModule
this._logger = rootLogger.child({ namespace: 'ts-compiler' })
this._parsedTsConfig = this.configSet.parsedTsConfig as ParsedCommandLine
this._jestCacheFS = jestCacheFS
this._initialCompilerOptions = { ...this._parsedTsConfig.options }
this._compilerOptions = { ...this._initialCompilerOptions }
if (!this.configSet.isolatedModules) {
Expand Down Expand Up @@ -96,13 +100,13 @@ export class TsCompiler implements CompilerInstance {
// Read contents from TypeScript memory cache.
if (!hit) {
const fileContent =
this._jestCacheFS.get(normalizedFileName) ?? this._cachedReadFile?.(normalizedFileName) ?? undefined
this.jestCacheFS.get(normalizedFileName) ?? this._cachedReadFile?.(normalizedFileName) ?? undefined
if (fileContent) {
this._jestCacheFS.set(normalizedFileName, fileContent)
this.jestCacheFS.set(normalizedFileName, fileContent)
this._compilerCacheFS.set(normalizedFileName, 1)
}
}
const contents = this._jestCacheFS.get(normalizedFileName)
const contents = this.jestCacheFS.get(normalizedFileName)

if (contents === undefined) return

Expand All @@ -118,7 +122,17 @@ export class TsCompiler implements CompilerInstance {
getCurrentDirectory: () => this.configSet.cwd,
getCompilationSettings: () => this._compilerOptions,
getDefaultLibFileName: () => this._ts.getDefaultLibFilePath(this._compilerOptions),
getCustomTransformers: () => this.configSet.customTransformers,
getCustomTransformers: () => ({
before: this.configSet.resolvedTransformers.before.map((beforeTransformer) =>
beforeTransformer.factory(this, beforeTransformer.options),
) as (TransformerFactory<SourceFile> | CustomTransformerFactory)[],
after: this.configSet.resolvedTransformers.after.map((afterTransformer) =>
afterTransformer.factory(this, afterTransformer.options),
) as (TransformerFactory<SourceFile> | CustomTransformerFactory)[],
afterDeclarations: this.configSet.resolvedTransformers.afterDeclarations.map((afterDeclarations) =>
afterDeclarations.factory(this, afterDeclarations.options),
) as TransformerFactory<SourceFile | Bundle>[],
}),
resolveModuleNames: (moduleNames: string[], containingFile: string): (ResolvedModuleFull | undefined)[] =>
moduleNames.map((moduleName) => {
const { resolvedModule } = this._ts.resolveModuleName(
Expand All @@ -136,6 +150,7 @@ export class TsCompiler implements CompilerInstance {
this._logger.debug('created language service')

this._languageService = this._ts.createLanguageService(serviceHost, this._ts.createDocumentRegistry())
this.program = this._languageService.getProgram()
}

getResolvedModulesMap(fileContent: string, fileName: string): ResolvedModulesMap {
Expand Down Expand Up @@ -197,7 +212,17 @@ export class TsCompiler implements CompilerInstance {

const result: TranspileOutput = this._ts.transpileModule(fileContent, {
fileName,
transformers: this.configSet.customTransformers,
transformers: {
before: this.configSet.resolvedTransformers.before.map((beforeTransformer) =>
beforeTransformer.factory(this, beforeTransformer.options),
) as (TransformerFactory<SourceFile> | CustomTransformerFactory)[],
after: this.configSet.resolvedTransformers.after.map((afterTransformer) =>
afterTransformer.factory(this, afterTransformer.options),
) as (TransformerFactory<SourceFile> | CustomTransformerFactory)[],
afterDeclarations: this.configSet.resolvedTransformers.afterDeclarations.map((afterDeclarations) =>
afterDeclarations.factory(this, afterDeclarations.options),
) as TransformerFactory<SourceFile | Bundle>[],
},
compilerOptions: this._compilerOptions,
reportDiagnostics: this.configSet.shouldReportDiagnostics(fileName),
})
Expand All @@ -212,9 +237,7 @@ export class TsCompiler implements CompilerInstance {

private _isFileInCache(fileName: string): boolean {
return (
this._jestCacheFS.has(fileName) &&
this._compilerCacheFS.has(fileName) &&
this._compilerCacheFS.get(fileName) !== 0
this.jestCacheFS.has(fileName) && this._compilerCacheFS.has(fileName) && this._compilerCacheFS.get(fileName) !== 0
)
}

Expand All @@ -229,7 +252,7 @@ export class TsCompiler implements CompilerInstance {
shouldIncrementProjectVersion = true
} else {
const prevVersion = this._compilerCacheFS.get(fileName) ?? 0
const previousContents = this._jestCacheFS.get(fileName)
const previousContents = this.jestCacheFS.get(fileName)
// Avoid incrementing cache when nothing has changed.
if (previousContents !== contents) {
this._compilerCacheFS.set(fileName, prevVersion + 1)
Expand Down
55 changes: 46 additions & 9 deletions src/config/__snapshots__/config-set.spec.ts.snap
Expand Up @@ -54,48 +54,85 @@ Array [

exports[`customTransformers should return an object containing all resolved transformers 1`] = `
Object {
"after": Array [],
"afterDeclarations": Array [],
"before": Array [
[Function],
Object {
"factory": [Function],
"name": "hoisting-jest-mock",
"version": 4,
},
],
}
`;

exports[`customTransformers should return an object containing all resolved transformers 2`] = `
Object {
"after": Array [],
"afterDeclarations": Array [],
"before": Array [
[Function],
[Function],
Object {
"factory": [Function],
"name": "hoisting-jest-mock",
"version": 4,
},
Object {
"factory": [Function],
},
],
}
`;

exports[`customTransformers should return an object containing all resolved transformers 3`] = `
Object {
"after": Array [
[Function],
Object {
"factory": [Function],
},
],
"afterDeclarations": Array [],
"before": Array [
[Function],
Object {
"factory": [Function],
"name": "hoisting-jest-mock",
"version": 4,
},
],
}
`;

exports[`customTransformers should return an object containing all resolved transformers 4`] = `
Object {
"after": Array [],
"afterDeclarations": Array [
[Function],
Object {
"factory": [Function],
},
],
"before": Array [
[Function],
Object {
"factory": [Function],
"name": "hoisting-jest-mock",
"version": 4,
},
],
}
`;

exports[`customTransformers should return an object containing all resolved transformers 5`] = `
Object {
"after": Array [],
"afterDeclarations": Array [],
"before": Array [
[Function],
[Function],
Object {
"factory": [Function],
"name": "hoisting-jest-mock",
"version": 4,
},
Object {
"factory": [Function],
"options": Object {},
},
],
}
`;
Expand Down
2 changes: 1 addition & 1 deletion src/config/config-set.spec.ts
Expand Up @@ -169,7 +169,7 @@ describe('customTransformers', () => {
resolve: null,
})

expect(cs.customTransformers).toMatchSnapshot()
expect(cs.resolvedTransformers).toMatchSnapshot()
})
})

Expand Down