Skip to content

Commit 970d4a8

Browse files
authored
Use language server to get proper reflection types (#505)
* Add Date reflection bug test suite * Add array props test cases * test commit * working commit * .. * commmit * added flag * added reflect-metadata * removed logging * cleanup * bumped version
1 parent 7d5a9fb commit 970d4a8

23 files changed

Lines changed: 558 additions & 272 deletions

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ ts-jest tries to support that. If `allowSyntheticDefaultImports` is set to true
117117
to automatically create the synthetic default exports for you - nothing else needed.
118118
You can opt-out of this behaviour with the [skipBabel flag](#skipping-babel)
119119

120+
**Typescript 2.7 has built-in support for this feature via the `esModuleInterop` flag. We're looking to deprecate this feature.
121+
Please use `esModuleInterop` instead. More details [here](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html)**
122+
120123
### Supports automatic of jest.mock() calls
121124
[Just like Jest](https://facebook.github.io/jest/docs/manual-mocks.html#using-with-es-module-imports) ts-jest
122125
automatically uses babel to hoist your `jest.mock()` calls to the top of your file.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
{
22
"name": "ts-jest",
3-
"version": "22.4.4",
3+
"version": "22.4.5",
44
"main": "index.js",
55
"types": "./dist/index.d.ts",
66
"description": "A preprocessor with sourcemap support to help use Typescript with Jest",
77
"scripts": {
88
"build": "cpx index.d.ts dist/ && tsc -p .",
99
"build:watch": "cpx index.d.ts dist/ && tsc -p . -w",
10-
"clean": "rimraf dist/**/* && rimraf tests/simple/coverage && rimraf tests/simple-async/coverage && rimraf tests/**/*/debug.txt",
10+
"test:nolint": "npm run clean-build && node scripts/tests.js",
11+
"clean": "rimraf dist/**/* && rimraf tests/simple/coverage && rimraf tests/simple-async/coverage && rimraf tests/**/*/debug.txt && rimraf tests/**/node_modules",
1112
"clean-build": "npm run clean && npm run build",
1213
"pretest": "npm run tslint && npm run clean-build",
1314
"test": "node scripts/tests.js",
@@ -94,6 +95,7 @@
9495
"prettier": "^1.12.1",
9596
"react": "16.3.2",
9697
"react-test-renderer": "16.3.2",
98+
"reflect-metadata": "^0.1.12",
9799
"rimraf": "latest",
98100
"ts-jest": "22.4.4",
99101
"tslint": "^5.10.0",

src/jest-types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface BabelTransformOptions extends BabelTransformOpts {
2525

2626
export type PostProcessHook = (
2727
src: string,
28-
filename: string,
28+
filePath: string,
2929
config: JestConfig,
3030
transformOptions: TransformOptions,
3131
) => string;
@@ -88,4 +88,5 @@ export interface TsJestConfig {
8888
disableSourceMapSupport?: boolean;
8989
ignoreCoverageForDecorators?: boolean;
9090
ignoreCoverageForAllDecorators?: boolean;
91+
useExperimentalLanguageServer?: boolean;
9192
}

src/logger.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import * as path from 'path';
99

1010
const logs: any[] = [];
1111
let logsFlushed: boolean = false;
12+
// Set this to true to also log to the console. It's very nice for local debugging.
13+
const outputToConsole: boolean = false;
1214

1315
function shouldLog(): boolean {
1416
// If the env variable is set and the logs have not already been flushed, log the line
15-
return process.env.TS_JEST_DEBUG && !logsFlushed;
17+
return (process.env.TS_JEST_DEBUG || outputToConsole) && !logsFlushed;
1618
}
1719

1820
// Log function. Only logs prior to calls to flushLogs.
@@ -33,7 +35,12 @@ export function flushLogs() {
3335
const JSONifiedLogs = logs.map(convertToJSONIfPossible);
3436
const logString = JSONifiedLogs.join('\n');
3537
const filePath = path.resolve(rootPath, 'debug.txt');
36-
fs.writeFileSync(filePath, logString);
38+
if (outputToConsole) {
39+
// tslint:disable-next-line
40+
console.log(logString);
41+
} else {
42+
fs.writeFileSync(filePath, logString);
43+
}
3744
}
3845

3946
function convertToJSONIfPossible(object: any): string {

src/postprocess.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,24 @@ import {
1616
} from './jest-types';
1717
import { logOnce } from './logger';
1818

19+
// Function that takes the transpiled typescript and runs it through babel/whatever.
20+
export function postProcessCode(
21+
compilerOptions: CompilerOptions,
22+
jestConfig: JestConfig,
23+
tsJestConfig: TsJestConfig,
24+
transformOptions: TransformOptions,
25+
transpiledText: string,
26+
filePath: string,
27+
): string {
28+
const postHook = getPostProcessHook(
29+
compilerOptions,
30+
jestConfig,
31+
tsJestConfig,
32+
);
33+
34+
return postHook(transpiledText, filePath, jestConfig, transformOptions);
35+
}
36+
1937
function createBabelTransformer(options: BabelTransformOptions) {
2038
options = {
2139
...options,

src/preprocessor.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import * as crypto from 'crypto';
2-
import * as tsc from 'typescript';
32
import { JestConfig, Path, TransformOptions } from './jest-types';
43
import { flushLogs, logOnce } from './logger';
5-
import { getPostProcessHook } from './postprocess';
4+
import { getPostProcessHook, postProcessCode } from './postprocess';
65
import {
76
getTSConfig,
87
getTSJestConfig,
98
runTsDiagnostics,
109
injectSourcemapHook,
1110
} from './utils';
11+
import { transpileTypescript } from './transpiler';
1212

1313
export function process(
1414
src: string,
@@ -41,16 +41,19 @@ export function process(
4141
const tsJestConfig = getTSJestConfig(jestConfig.globals);
4242
logOnce('tsJestConfig: ', tsJestConfig);
4343

44+
// We can potentially do this faster by using the language service.
45+
// See https://github.com/TypeStrong/ts-node/blob/master/src/index.ts#L268
4446
if (tsJestConfig.enableTsDiagnostics) {
4547
runTsDiagnostics(filePath, compilerOptions);
4648
}
4749

48-
const tsTranspiled = tsc.transpileModule(src, {
50+
let tsTranspiledText = transpileTypescript(
51+
filePath,
52+
src,
4953
compilerOptions,
50-
fileName: filePath,
51-
});
54+
tsJestConfig,
55+
);
5256

53-
let tsTranspiledText = tsTranspiled.outputText;
5457
if (tsJestConfig.ignoreCoverageForAllDecorators === true) {
5558
tsTranspiledText = tsTranspiledText.replace(
5659
/__decorate/g,
@@ -64,17 +67,13 @@ export function process(
6467
);
6568
}
6669

67-
const postHook = getPostProcessHook(
70+
const outputText = postProcessCode(
6871
compilerOptions,
6972
jestConfig,
7073
tsJestConfig,
71-
);
72-
73-
const outputText = postHook(
74+
transformOptions,
7475
tsTranspiledText,
7576
filePath,
76-
jestConfig,
77-
transformOptions,
7877
);
7978

8079
const modified =

src/transpiler.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import * as fs from 'fs';
2+
import { cwd } from 'process';
3+
import * as ts from 'typescript';
4+
import { logOnce } from './logger';
5+
import { TsJestConfig } from './jest-types';
6+
7+
// Takes the typescript code and by whatever method configured, makes it into javascript code.
8+
export function transpileTypescript(
9+
filePath: string,
10+
fileSrc: string,
11+
compilerOptions: ts.CompilerOptions,
12+
tsJestConfig: TsJestConfig,
13+
): string {
14+
if (tsJestConfig.useExperimentalLanguageServer) {
15+
logOnce('Using experimental language server.');
16+
return transpileViaLanguageServer(filePath, fileSrc, compilerOptions);
17+
}
18+
logOnce('Compiling via normal transpileModule call');
19+
return transpileViaTranspileModile(filePath, fileSrc, compilerOptions);
20+
}
21+
22+
/**
23+
* This is slower, but can properly parse enums and deal with reflect metadata.
24+
* This is an experimental approach from our side. Potentially we should cache
25+
* the languageServer between calls.
26+
*/
27+
function transpileViaLanguageServer(
28+
filePath: string,
29+
fileSrc: string,
30+
compilerOptions: ts.CompilerOptions,
31+
) {
32+
const serviceHost: ts.LanguageServiceHost = {
33+
// Returns an array of the files we need to consider
34+
getScriptFileNames: () => {
35+
return [filePath];
36+
},
37+
38+
getScriptVersion: fileName => {
39+
// We're not doing any watching or changing files, so versioning is not relevant for us
40+
return undefined;
41+
},
42+
43+
getCurrentDirectory: () => {
44+
return cwd();
45+
},
46+
47+
getScriptSnapshot: fileName => {
48+
if (fileName === filePath) {
49+
// jest has already served this file for us, so no need to hit disk again.
50+
return ts.ScriptSnapshot.fromString(fileSrc);
51+
}
52+
// Read file from disk. I think this could be problematic if the files are not saved as utf8.
53+
const result = fs.readFileSync(fileName, 'utf8');
54+
return ts.ScriptSnapshot.fromString(result);
55+
},
56+
57+
getCompilationSettings: () => {
58+
return compilerOptions;
59+
},
60+
61+
getDefaultLibFileName: () => {
62+
return ts.getDefaultLibFilePath(compilerOptions);
63+
},
64+
fileExists: ts.sys.fileExists,
65+
readFile: ts.sys.readFile,
66+
readDirectory: ts.sys.readDirectory,
67+
getDirectories: ts.sys.getDirectories,
68+
directoryExists: ts.sys.directoryExists,
69+
};
70+
const service = ts.createLanguageService(serviceHost);
71+
const serviceOutput = service.getEmitOutput(filePath);
72+
const files = serviceOutput.outputFiles.filter(file => {
73+
// Service outputs both d.ts and .js files - we're not interested in the declarations.
74+
return file.name.endsWith('js');
75+
});
76+
logOnce('JS files parsed', files.map(f => f.name));
77+
78+
// Log some diagnostics here:
79+
const diagnostics = service
80+
.getCompilerOptionsDiagnostics()
81+
.concat(service.getSyntacticDiagnostics(filePath))
82+
.concat(service.getSemanticDiagnostics(filePath));
83+
84+
if (diagnostics.length > 0) {
85+
const errors = `${diagnostics.map(d => d.messageText)}\n`;
86+
logOnce(`Diagnostic errors from TSC: ${errors}`);
87+
// Maybe we should keep compiling even though there are errors. This can possibly be configured.
88+
throw Error(
89+
`TSC language server encountered errors while transpiling. Errors: ${errors}`,
90+
);
91+
}
92+
93+
return files[0].text;
94+
}
95+
96+
/**
97+
* This is faster, and considers the modules in isolation
98+
*/
99+
function transpileViaTranspileModile(
100+
filePath: string,
101+
fileSource: string,
102+
compilerOptions: ts.CompilerOptions,
103+
) {
104+
return ts.transpileModule(fileSource, {
105+
compilerOptions,
106+
fileName: filePath,
107+
}).outputText;
108+
}

0 commit comments

Comments
 (0)