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

Use language server to get proper reflection types #505

Merged
merged 14 commits into from
May 5, 2018
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ ts-jest tries to support that. If `allowSyntheticDefaultImports` is set to true
to automatically create the synthetic default exports for you - nothing else needed.
You can opt-out of this behaviour with the [skipBabel flag](#skipping-babel)

**Typescript 2.7 has built-in support for this feature via the `esModuleInterop` flag. We're looking to deprecate this feature.
Please use `esModuleInterop` instead. More details [here](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html)**

### Supports automatic of jest.mock() calls
[Just like Jest](https://facebook.github.io/jest/docs/manual-mocks.html#using-with-es-module-imports) ts-jest
automatically uses babel to hoist your `jest.mock()` calls to the top of your file.
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"name": "ts-jest",
"version": "22.4.4",
"version": "22.4.5",
"main": "index.js",
"types": "./dist/index.d.ts",
"description": "A preprocessor with sourcemap support to help use Typescript with Jest",
"scripts": {
"build": "cpx index.d.ts dist/ && tsc -p .",
"build:watch": "cpx index.d.ts dist/ && tsc -p . -w",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ran into several issues where I'd like to clean the node_modules inside tests also, so I'm sure that there's no old files left-over

"clean": "rimraf dist/**/* && rimraf tests/simple/coverage && rimraf tests/simple-async/coverage && rimraf tests/**/*/debug.txt",
"test:nolint": "npm run clean-build && node scripts/tests.js",
"clean": "rimraf dist/**/* && rimraf tests/simple/coverage && rimraf tests/simple-async/coverage && rimraf tests/**/*/debug.txt && rimraf tests/**/node_modules",
"clean-build": "npm run clean && npm run build",
"pretest": "npm run tslint && npm run clean-build",
"test": "node scripts/tests.js",
Expand Down Expand Up @@ -94,6 +95,7 @@
"prettier": "^1.12.1",
"react": "16.3.2",
"react-test-renderer": "16.3.2",
"reflect-metadata": "^0.1.12",
"rimraf": "latest",
"ts-jest": "22.4.4",
"tslint": "^5.10.0",
Expand Down
3 changes: 2 additions & 1 deletion src/jest-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface BabelTransformOptions extends BabelTransformOpts {

export type PostProcessHook = (
src: string,
filename: string,
filePath: string,
config: JestConfig,
transformOptions: TransformOptions,
) => string;
Expand Down Expand Up @@ -88,4 +88,5 @@ export interface TsJestConfig {
disableSourceMapSupport?: boolean;
ignoreCoverageForDecorators?: boolean;
ignoreCoverageForAllDecorators?: boolean;
useExperimentalLanguageServer?: boolean;
}
11 changes: 9 additions & 2 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import * as path from 'path';

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

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

// Log function. Only logs prior to calls to flushLogs.
Expand All @@ -33,7 +35,12 @@ export function flushLogs() {
const JSONifiedLogs = logs.map(convertToJSONIfPossible);
const logString = JSONifiedLogs.join('\n');
const filePath = path.resolve(rootPath, 'debug.txt');
fs.writeFileSync(filePath, logString);
if (outputToConsole) {
// tslint:disable-next-line
console.log(logString);
} else {
fs.writeFileSync(filePath, logString);
}
}

function convertToJSONIfPossible(object: any): string {
Expand Down
18 changes: 18 additions & 0 deletions src/postprocess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@ import {
} from './jest-types';
import { logOnce } from './logger';

// Function that takes the transpiled typescript and runs it through babel/whatever.
export function postProcessCode(
compilerOptions: CompilerOptions,
jestConfig: JestConfig,
tsJestConfig: TsJestConfig,
transformOptions: TransformOptions,
transpiledText: string,
filePath: string,
): string {
const postHook = getPostProcessHook(
compilerOptions,
jestConfig,
tsJestConfig,
);

return postHook(transpiledText, filePath, jestConfig, transformOptions);
}

function createBabelTransformer(options: BabelTransformOptions) {
options = {
...options,
Expand Down
23 changes: 11 additions & 12 deletions src/preprocessor.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as crypto from 'crypto';
import * as tsc from 'typescript';
import { JestConfig, Path, TransformOptions } from './jest-types';
import { flushLogs, logOnce } from './logger';
import { getPostProcessHook } from './postprocess';
import { getPostProcessHook, postProcessCode } from './postprocess';
import {
getTSConfig,
getTSJestConfig,
runTsDiagnostics,
injectSourcemapHook,
} from './utils';
import { transpileTypescript } from './transpiler';

export function process(
src: string,
Expand Down Expand Up @@ -41,16 +41,19 @@ export function process(
const tsJestConfig = getTSJestConfig(jestConfig.globals);
logOnce('tsJestConfig: ', tsJestConfig);

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

const tsTranspiled = tsc.transpileModule(src, {
let tsTranspiledText = transpileTypescript(
filePath,
src,
compilerOptions,
fileName: filePath,
});
tsJestConfig,
);

let tsTranspiledText = tsTranspiled.outputText;
if (tsJestConfig.ignoreCoverageForAllDecorators === true) {
tsTranspiledText = tsTranspiledText.replace(
/__decorate/g,
Expand All @@ -64,17 +67,13 @@ export function process(
);
}

const postHook = getPostProcessHook(
const outputText = postProcessCode(
compilerOptions,
jestConfig,
tsJestConfig,
);

const outputText = postHook(
transformOptions,
tsTranspiledText,
filePath,
jestConfig,
transformOptions,
);

const modified =
Expand Down
108 changes: 108 additions & 0 deletions src/transpiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as fs from 'fs';
import { cwd } from 'process';
import * as ts from 'typescript';
import { logOnce } from './logger';
import { TsJestConfig } from './jest-types';

// Takes the typescript code and by whatever method configured, makes it into javascript code.
export function transpileTypescript(
filePath: string,
fileSrc: string,
compilerOptions: ts.CompilerOptions,
tsJestConfig: TsJestConfig,
): string {
if (tsJestConfig.useExperimentalLanguageServer) {
logOnce('Using experimental language server.');
return transpileViaLanguageServer(filePath, fileSrc, compilerOptions);
}
logOnce('Compiling via normal transpileModule call');
return transpileViaTranspileModile(filePath, fileSrc, compilerOptions);
}

/**
* This is slower, but can properly parse enums and deal with reflect metadata.
* This is an experimental approach from our side. Potentially we should cache
* the languageServer between calls.
*/
function transpileViaLanguageServer(
filePath: string,
fileSrc: string,
compilerOptions: ts.CompilerOptions,
) {
const serviceHost: ts.LanguageServiceHost = {
// Returns an array of the files we need to consider
getScriptFileNames: () => {
return [filePath];
},

getScriptVersion: fileName => {
// We're not doing any watching or changing files, so versioning is not relevant for us
return undefined;
},

getCurrentDirectory: () => {
return cwd();
},

getScriptSnapshot: fileName => {
if (fileName === filePath) {
// jest has already served this file for us, so no need to hit disk again.
return ts.ScriptSnapshot.fromString(fileSrc);
}
// Read file from disk. I think this could be problematic if the files are not saved as utf8.
const result = fs.readFileSync(fileName, 'utf8');
return ts.ScriptSnapshot.fromString(result);
},

getCompilationSettings: () => {
return compilerOptions;
},

getDefaultLibFileName: () => {
return ts.getDefaultLibFilePath(compilerOptions);
},
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
readDirectory: ts.sys.readDirectory,
getDirectories: ts.sys.getDirectories,
directoryExists: ts.sys.directoryExists,
};
const service = ts.createLanguageService(serviceHost);
const serviceOutput = service.getEmitOutput(filePath);
const files = serviceOutput.outputFiles.filter(file => {
// Service outputs both d.ts and .js files - we're not interested in the declarations.
return file.name.endsWith('js');
});
logOnce('JS files parsed', files.map(f => f.name));

// Log some diagnostics here:
const diagnostics = service
.getCompilerOptionsDiagnostics()
.concat(service.getSyntacticDiagnostics(filePath))
.concat(service.getSemanticDiagnostics(filePath));

if (diagnostics.length > 0) {
const errors = `${diagnostics.map(d => d.messageText)}\n`;
logOnce(`Diagnostic errors from TSC: ${errors}`);
// Maybe we should keep compiling even though there are errors. This can possibly be configured.
throw Error(
`TSC language server encountered errors while transpiling. Errors: ${errors}`,
);
}

return files[0].text;
}

/**
* This is faster, and considers the modules in isolation
*/
function transpileViaTranspileModile(
filePath: string,
fileSource: string,
compilerOptions: ts.CompilerOptions,
) {
return ts.transpileModule(fileSource, {
compilerOptions,
fileName: filePath,
}).outputText;
}
Loading