diff --git a/src/index.ts b/src/index.ts index d8bfb72b42..31f4611ed3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,34 +1,37 @@ // tslint:disable:member-ordering -import { TsJestInstance, TsJestGlobalOptions } from './types'; -import TsProgram from './ts-program'; +import { TsJestGlobalOptions, TsJestConfig } from './types'; +import TsProgram from './ts-program.simple'; import Memoize from './memoize'; +import { normalizeDiagnosticTypes } from './utils/diagnostics'; + +const rootDirFor = (jestConfig: jest.ProjectConfig): string => { + return jestConfig.rootDir || process.cwd(); +}; + +// TODO: could be used if we need to handle the cache key ourself +// const normalizeJestConfig = (jestConfig: jest.ProjectConfig): jest.ProjectConfig => { +// const config = { ...jestConfig, rootDir: rootDirFor(jestConfig) }; +// delete config.cacheDirectory; +// delete config.name; +// return config; +// }; class TsJestTransformer implements jest.Transformer { - private _instances = new Map(); - @Memoize((_, rootDir) => rootDir) - instanceFor( - jestConfig: jest.ProjectConfig, - rootDir: jest.Path = jestConfig.rootDir || process.cwd(), - ): TsJestInstance { + @Memoize() + configFor(jestConfig: jest.ProjectConfig): TsJestConfig { const { globals = {} } = jestConfig as any; const options: TsJestGlobalOptions = { ...globals['ts-jest'] }; - - const shouldWrapHtml = !!globals.__TRANSFORM_HTML__; - - let tsProgram: TsProgram; - const instance: TsJestInstance = { - shouldWrapHtml, - get tsProgram(): TsProgram { - return tsProgram || (tsProgram = new TsProgram(rootDir, options)); - }, - get tsConfig() { - return this.tsProgram.compilerOptions; - }, - // TODO: get using babel-jest + return { + inputOptions: options, useBabelJest: !!options.useBabelJest, + diagnostics: normalizeDiagnosticTypes(options.diagnostics), }; - this._instances.set(rootDir, instance); - return instance; + } + + @Memoize() + programFor(jestConfig: jest.ProjectConfig): TsProgram { + const myConfig = this.configFor(jestConfig); + return new TsProgram(rootDirFor(jestConfig), myConfig); } process( @@ -40,17 +43,16 @@ class TsJestTransformer implements jest.Transformer { let result: string | jest.TransformedSource; // get the tranformer instance - const instance = this.instanceFor(jestConfig); + const program = this.programFor(jestConfig); + const config = this.configFor(jestConfig); + const instrument: boolean = + !!transformOptions && transformOptions.instrument; // transpile TS code (source maps are included) - result = instance.tsProgram.transpileModule( - path, - source, - transformOptions && transformOptions.instrument, - ); + result = program.transpileModule(path, source, instrument); // calling babel-jest transformer - if (instance.useBabelJest) { + if (config.useBabelJest) { result = this.babelJest.process( result, path, diff --git a/src/transformers/hoisting.ts b/src/transformers/hoisting.ts new file mode 100644 index 0000000000..cb1f9126c0 --- /dev/null +++ b/src/transformers/hoisting.ts @@ -0,0 +1,131 @@ +// import { +// TransformationContext, +// EmitHint, +// Node, +// JsxEmit, +// SyntaxKind, +// SourceFile, +// JsxSelfClosingElement, +// JsxClosingElement, +// JsxOpeningElement, +// Bundle, +// isPropertyAccessExpression, +// isPropertyAssignment, +// PropertyAccessExpression, +// Expression, +// createElementAccess, +// setTextRange, +// PropertyAssignment, +// isIdentifier, +// updatePropertyAssignment, +// Identifier, +// } from 'typescript'; +// import { chainBundle, getOriginalNodeId, getNodeId } from './utilis'; + +// export default function hoistingTransformer( +// context: TransformationContext, +// ): (x: SourceFile | Bundle) => SourceFile | Bundle { +// const compilerOptions = context.getCompilerOptions(); + +// // enable emit notification only if using --jsx preserve or react-native +// let previousOnEmitNode: (hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) => void; +// let noSubstitution: boolean[]; + +// if (compilerOptions.jsx === JsxEmit.Preserve || compilerOptions.jsx === JsxEmit.ReactNative) { +// previousOnEmitNode = context.onEmitNode; +// context.onEmitNode = onEmitNode; +// context.enableEmitNotification(SyntaxKind.JsxOpeningElement); +// context.enableEmitNotification(SyntaxKind.JsxClosingElement); +// context.enableEmitNotification(SyntaxKind.JsxSelfClosingElement); +// noSubstitution = []; +// } + +// const previousOnSubstituteNode = context.onSubstituteNode; +// context.onSubstituteNode = onSubstituteNode; +// context.enableSubstitution(SyntaxKind.PropertyAccessExpression); +// context.enableSubstitution(SyntaxKind.PropertyAssignment); +// return chainBundle(transformSourceFile); + +// function transformSourceFile(node: SourceFile) { +// return node; +// } + +// /** +// * Called by the printer just before a node is printed. +// * +// * @param hint A hint as to the intended usage of the node. +// * @param node The node to emit. +// * @param emitCallback A callback used to emit the node. +// */ +// function onEmitNode(hint: EmitHint, node: Node, emitCallback: (emitContext: EmitHint, node: Node) => void) { +// switch (node.kind) { +// case SyntaxKind.JsxOpeningElement: +// case SyntaxKind.JsxClosingElement: +// case SyntaxKind.JsxSelfClosingElement: +// const tagName = (node as JsxOpeningElement | JsxClosingElement | JsxSelfClosingElement).tagName; +// noSubstitution[getOriginalNodeId(tagName)] = true; +// break; +// } + +// previousOnEmitNode(hint, node, emitCallback); +// } + +// /** +// * Hooks node substitutions. +// * +// * @param hint A hint as to the intended usage of the node. +// * @param node The node to substitute. +// */ +// function onSubstituteNode(hint: EmitHint, node: Node) { +// if (getNodeId(node) && noSubstitution && noSubstitution[getNodeId(node)]) { +// return previousOnSubstituteNode(hint, node); +// } + +// node = previousOnSubstituteNode(hint, node); +// if (isPropertyAccessExpression(node)) { +// return substitutePropertyAccessExpression(node); +// } else if (isPropertyAssignment(node)) { +// return substitutePropertyAssignment(node); +// } +// return node; +// } + +// /** +// * Substitutes a PropertyAccessExpression whose name is a reserved word. +// * +// * @param node A PropertyAccessExpression +// */ +// function substitutePropertyAccessExpression(node: PropertyAccessExpression): Expression { +// const literalName = trySubstituteReservedName(node.name); +// if (literalName) { +// return setTextRange(createElementAccess(node.expression, literalName), node); +// } +// return node; +// } + +// /** +// * Substitutes a PropertyAssignment whose name is a reserved word. +// * +// * @param node A PropertyAssignment +// */ +// function substitutePropertyAssignment(node: PropertyAssignment): PropertyAssignment { +// const literalName = isIdentifier(node.name) && trySubstituteReservedName(node.name); +// if (literalName) { +// return updatePropertyAssignment(node, literalName, node.initializer); +// } +// return node; +// } + +// /** +// * If an identifier name is a reserved word, returns a string literal for the name. +// * +// * @param name An Identifier +// */ +// function trySubstituteReservedName(name: Identifier) { +// const token = name.originalKeywordKind || (nodeIsSynthesized(name) ? stringToToken(idText(name)) : undefined); +// if (token !== undefined && token >= SyntaxKind.FirstReservedWord && token <= SyntaxKind.LastReservedWord) { +// return setTextRange(createLiteral(name), name); +// } +// return undefined; +// } +// } diff --git a/src/ts-program.incremental.ts b/src/ts-program.incremental.ts new file mode 100644 index 0000000000..b1f48828f6 --- /dev/null +++ b/src/ts-program.incremental.ts @@ -0,0 +1,321 @@ +// tslint:disable:member-ordering +import { TsJestConfig, DiagnosticTypes } from './types'; +import { + FormatDiagnosticsHost, + sys, + findConfigFile, + CompilerOptions, + Diagnostic, + flattenDiagnosticMessageText, + readConfigFile, + parseJsonConfigFileContent, + ModuleKind, + CustomTransformers, + Program, + ParsedCommandLine, + ParseConfigHost, + createCompilerHost, + createProgram, + CompilerHost, +} from 'typescript'; +import { sep, resolve, dirname, basename } from 'path'; +import { existsSync, readFileSync } from 'fs'; +import Memoize from './memoize'; +import fileExtension from './utils/file-extension'; +// import { fixupCompilerOptions } from './utils/ts-internals'; + +export const compilerOptionsOverrides: Readonly = { + // ts-jest + module: ModuleKind.CommonJS, + esModuleInterop: true, + inlineSources: undefined, + sourceMap: false, + inlineSourceMap: true, + + // see https://github.com/Microsoft/TypeScript/blob/master/src/services/transpile.ts + isolatedModules: true, + // transpileModule does not write anything to disk so there is no need to verify that + // there are no conflicts between input and output paths. + suppressOutputPathCheck: true, + // Filename can be non-ts file. + allowNonTsExtensions: true, + // We are not returning a sourceFile for lib file when asked by the program, + // so pass --noLib to avoid reporting a file not found error. + noLib: true, + // Clear out other settings that would not be used in transpiling this module + // lib: undefined, + // types: undefined, + noEmit: undefined, + noEmitOnError: undefined, + // paths: undefined, + // rootDirs: undefined, + declaration: undefined, + declarationDir: undefined, + out: undefined, + outFile: undefined, + // We are not doing a full typecheck, we are not resolving the whole context, + // so pass --noResolve to avoid reporting missing file errors. + noResolve: true, +}; + +export default class TsProgram { + // a cache of all transpiled files + protected _inputSource = new Map(); + protected _transpiledSource = new Map(); + protected _transpiledMap = new Map(); + protected _transpiledDiagnostics = new Map(); + + constructor(readonly rootDir: string, readonly tsJestConfig: TsJestConfig) {} + + @Memoize() + get formatHost(): FormatDiagnosticsHost { + return { + getCanonicalFileName: path => path, + getCurrentDirectory: () => this.rootDir, + getNewLine: () => sys.newLine, + }; + } + + @Memoize() + get fileNameNormalizer() { + // const { rootDir } = this; + // return (path: string): string => resolve(rootDir, path); + return (path: string): string => path; + } + + @Memoize() + get compilerHost(): CompilerHost { + const { fileNameNormalizer, overriddenCompilerOptions } = this; + const options = { ...overriddenCompilerOptions }; + return { + ...createCompilerHost(options, true), + // overrides + // useCaseSensitiveFileNames: () => false, + // getCanonicalFileName: fileName => fileName, + writeFile: (name, text) => { + const key = fileNameNormalizer(name); + if (fileExtension(name) === 'map') { + this._transpiledMap.set(key, text); + } else { + this._transpiledSource.set(key, text); + } + }, + // getSourceFile: (fileName) => { + // const key = fileNameNormalizer(fileName); + // const content = this._inputSource.get(key); + // // if (content == null) { + // // throw new Error( + // // `[ts-jest] Trying to get a source file content outside of Jest (file: ${fileName}).`, + // // ); + // // } + // return createSourceFile(fileName, content || '', options.target!,); + // }, + fileExists: fileName => + this._inputSource.has(fileNameNormalizer(fileName)), + readFile: fileName => { + const content = this._inputSource.get(fileNameNormalizer(fileName)); + if (content == null) { + throw new Error( + `[ts-jest] Trying to get the content of a file outside of Jest (file: ${fileName}).`, + ); + } + return content; + }, + + // NOTE: below are the ones used in TypeScript's transpileModule() + // getDefaultLibFileName: () => 'lib.d.ts', + // getCurrentDirectory: () => this.rootDir, + // getNewLine: () => newLine, + // directoryExists: () => true, + // getDirectories: () => [], + }; + } + + @Memoize() + get configFile(): string | null { + const given = this.tsJestConfig.inputOptions.tsConfig; + let resolved: string | undefined; + if (typeof given === 'string') { + // we got a path to a custom (or not) tsconfig + resolved = given.replace('', `${this.rootDir}${sep}`); + resolved = resolve(this.rootDir, resolved); + if (!existsSync(resolved)) { + resolved = undefined; + } + } else if (typeof given === 'undefined') { + // we got undefined, go look for the default file + resolved = findConfigFile(this.rootDir, sys.fileExists, 'tsconfig.json'); + } else { + // what we got was compiler options + return null; + } + // could we find one? + if (!resolved) { + throw new Error( + `Could not find a TS config file (given: "${given}", root: "${ + this.rootDir + }")`, + ); + } + return resolved; + } + + @Memoize() + get program(): Program { + const { + parsedConfig: { fileNames }, + overriddenCompilerOptions: options, + } = this; + const compilerOptions = { ...options }; + + const host = this.compilerHost; + return createProgram(fileNames, compilerOptions, host); + } + + @Memoize() + get originalCompilerOptions() { + return { ...this.parsedConfig.options } as CompilerOptions; + } + + @Memoize() + get overriddenCompilerOptions() { + return { + ...this.originalCompilerOptions, + ...compilerOptionsOverrides, + } as CompilerOptions; + } + + @Memoize() + get parsedConfig(): ParsedCommandLine { + const { configFile } = this; + const { config, error } = configFile + ? readConfigFile(configFile, sys.readFile) + : { + config: { compilerOptions: this.tsJestConfig.inputOptions.tsConfig }, + error: undefined, + }; + if (error) throw error; // tslint:disable-line:curly + + const parseConfigHost: ParseConfigHost = { + fileExists: existsSync, + readDirectory: sys.readDirectory, + readFile: file => readFileSync(file, 'utf8'), + useCaseSensitiveFileNames: true, + }; + + const result = parseJsonConfigFileContent( + config, + parseConfigHost, + configFile ? dirname(configFile) : this.rootDir, + undefined, + configFile ? basename(configFile) : undefined, + ); + + // will throw if at least one error + this.reportDiagnostic(...result.errors); + + return result; + } + + // transpileModule( + // path: string, + // content: string, + // instrument: boolean = false, + // ): string { + // const options: TranspileOptions = { + // fileName: path, + // reportDiagnostics: false, // TODO: make this an option + // transformers: this.transformers, + // compilerOptions: { ...this.overriddenCompilerOptions }, + // }; + // const { diagnostics, outputText } = transpileModule(content, options); + // // TODO: handle diagnostics + // this.reportDiagnostic(...diagnostics); + + // // outputText will contain inline sourmaps + // return outputText; + // } + + transpileModule( + path: string, + content: string, + instrument: boolean = false, + ): string { + const { + program, + tsJestConfig: { diagnostics: diagnosticTypes }, + fileNameNormalizer, + } = this; + const diagnostics: Diagnostic[] = []; + + // register the source content + const fileKey = fileNameNormalizer(path); + this._inputSource.set(fileKey, content); + + // get the source file + const sourceFile = this.compilerHost.getSourceFile( + path, + this.overriddenCompilerOptions.target!, + ); + + // diagnostics + if (diagnosticTypes.includes(DiagnosticTypes.global)) { + diagnostics.push(...program.getGlobalDiagnostics()); + } + if (diagnosticTypes.includes(DiagnosticTypes.options)) { + diagnostics.push(...program.getOptionsDiagnostics()); + } + if (diagnosticTypes.includes(DiagnosticTypes.syntactic)) { + diagnostics.push(...program.getSyntacticDiagnostics(sourceFile)); + } + if (diagnosticTypes.includes(DiagnosticTypes.semantic)) { + diagnostics.push(...program.getSemanticDiagnostics(sourceFile)); + } + + // finally triger the compilation + program.emit( + /*targetSourceFile*/ sourceFile, + /*writeFile*/ undefined, + /*cancellationToken*/ undefined, + /*emitOnlyDtsFiles*/ undefined, + this.transformers, + ); + + // get the generated source + const transpiledSource = this._transpiledSource.get(fileKey); + if (transpiledSource == null) { + throw new Error(`[ts-jest] Output generation failed (file: ${path}).`); + } + + // source maps are inlined + return transpiledSource; + } + + get transformers(): CustomTransformers { + return { + // before: [() => this.beforeTransformer], + }; + } + + // @Memoize() + // get beforeTransformer(): Transformer { + // return (fileNode: SourceFile): SourceFile => { + // if (fileNode.isDeclarationFile) return fileNode; + // const nodeTransformer = (node: Node): Node => { + // return node; + // }; + // fileNode.getChildAt(0). + // }; + // } + + reportDiagnostic(...diagnostics: Diagnostic[]) { + const diagnostic = diagnostics[0]; + if (!diagnostic) return; // tslint:disable-line:curly + + const message = flattenDiagnosticMessageText( + diagnostic.messageText, + this.formatHost.getNewLine(), + ); + throw new Error(`${diagnostic.code}: ${message}`); + } +} diff --git a/src/ts-program.simple.ts b/src/ts-program.simple.ts new file mode 100644 index 0000000000..a38c242173 --- /dev/null +++ b/src/ts-program.simple.ts @@ -0,0 +1,177 @@ +// tslint:disable:member-ordering +import { TsJestConfig, DiagnosticTypes } from './types'; +import { + FormatDiagnosticsHost, + sys, + findConfigFile, + CompilerOptions, + Diagnostic, + flattenDiagnosticMessageText, + readConfigFile, + parseJsonConfigFileContent, + ModuleKind, + CustomTransformers, + Program, + ParsedCommandLine, + ParseConfigHost, + createCompilerHost, + createProgram, + CompilerHost, + TranspileOptions, + transpileModule, +} from 'typescript'; +import { sep, resolve, dirname, basename } from 'path'; +import { existsSync, readFileSync } from 'fs'; +import Memoize from './memoize'; +import fileExtension from './utils/file-extension'; +import { fixupCompilerOptions } from './utils/ts-internals'; + +export const compilerOptionsOverrides: Readonly = { + // ts-jest + module: ModuleKind.CommonJS, + esModuleInterop: true, + inlineSources: false, + sourceMap: false, + inlineSourceMap: true, +}; + +export default class TsProgram { + constructor(readonly rootDir: string, readonly tsJestConfig: TsJestConfig) {} + + @Memoize() + get formatHost(): FormatDiagnosticsHost { + return { + getCanonicalFileName: path => path, + getCurrentDirectory: () => this.rootDir, + getNewLine: () => sys.newLine, + }; + } + + @Memoize() + get fileNameNormalizer() { + // const { rootDir } = this; + // return (path: string): string => resolve(rootDir, path); + return (path: string): string => path; + } + + @Memoize() + get configFile(): string | null { + const given = this.tsJestConfig.inputOptions.tsConfig; + let resolved: string | undefined; + if (typeof given === 'string') { + // we got a path to a custom (or not) tsconfig + resolved = given.replace('', `${this.rootDir}${sep}`); + resolved = resolve(this.rootDir, resolved); + if (!existsSync(resolved)) { + resolved = undefined; + } + } else if (typeof given === 'undefined') { + // we got undefined, go look for the default file + resolved = findConfigFile(this.rootDir, sys.fileExists, 'tsconfig.json'); + } else { + // what we got was compiler options + return null; + } + // could we find one? + if (!resolved) { + throw new Error( + `Could not find a TS config file (given: "${given}", root: "${ + this.rootDir + }")`, + ); + } + return resolved; + } + + @Memoize() + get originalCompilerOptions() { + return { ...this.parsedConfig.options } as CompilerOptions; + } + + @Memoize() + get overriddenCompilerOptions() { + return { + ...this.originalCompilerOptions, + ...compilerOptionsOverrides, + } as CompilerOptions; + } + + @Memoize() + get parsedConfig(): ParsedCommandLine { + const { configFile } = this; + const { config, error } = configFile + ? readConfigFile(configFile, sys.readFile) + : { + config: { compilerOptions: this.tsJestConfig.inputOptions.tsConfig }, + error: undefined, + }; + if (error) throw error; // tslint:disable-line:curly + + const parseConfigHost: ParseConfigHost = { + fileExists: existsSync, + readDirectory: sys.readDirectory, + readFile: file => readFileSync(file, 'utf8'), + useCaseSensitiveFileNames: true, + }; + + const result = parseJsonConfigFileContent( + config, + parseConfigHost, + configFile ? dirname(configFile) : this.rootDir, + undefined, + configFile ? basename(configFile) : undefined, + ); + + // will throw if at least one error + this.reportDiagnostic(...result.errors); + + return result; + } + + transpileModule( + path: string, + content: string, + instrument: boolean = false, + ): string { + const options: TranspileOptions = { + fileName: path, + reportDiagnostics: false, + transformers: this.transformers, + compilerOptions: { ...this.overriddenCompilerOptions }, + }; + const { diagnostics, outputText } = transpileModule(content, options); + + this.reportDiagnostic(...diagnostics); + + // outputText will contain inline sourmaps + return outputText; + } + + get transformers(): CustomTransformers { + return { + // before: [() => this.beforeTransformer], + }; + } + + // @Memoize() + // get beforeTransformer(): Transformer { + // return (fileNode: SourceFile): SourceFile => { + // if (fileNode.isDeclarationFile) return fileNode; + // const nodeTransformer = (node: Node): Node => { + // return node; + // }; + // fileNode.getChildAt(0). + // }; + // } + + reportDiagnostic(...diagnostics: Diagnostic[]) { + const diagnostic = diagnostics[0]; + if (!diagnostic) return; // tslint:disable-line:curly + + const message = flattenDiagnosticMessageText( + diagnostic.messageText, + this.formatHost.getNewLine(), + ); + throw new Error(`${diagnostic.code}: ${message}`); + } +} diff --git a/src/ts-program.ts b/src/ts-program.ts deleted file mode 100644 index 0d7ec8d9cb..0000000000 --- a/src/ts-program.ts +++ /dev/null @@ -1,124 +0,0 @@ -// tslint:disable:member-ordering -import { TsJestGlobalOptions } from './types'; -import { - FormatDiagnosticsHost, - sys, - findConfigFile, - CompilerOptions, - Diagnostic, - flattenDiagnosticMessageText, - readConfigFile, - parseJsonConfigFileContent, - TranspileOptions, - ModuleKind, - transpileModule, -} from 'typescript'; -import { relative, sep, resolve, dirname } from 'path'; -import { existsSync } from 'fs'; -import Memoize from './memoize'; - -export default class TsProgram { - constructor( - readonly rootDir: string, - readonly tsJestConfig: TsJestGlobalOptions = {}, - ) {} - - @Memoize() - get formatHost(): FormatDiagnosticsHost { - return { - getCanonicalFileName: path => relative(this.rootDir, path), - getCurrentDirectory: () => this.rootDir, - getNewLine: () => sys.newLine, - }; - } - - @Memoize() - get configFile(): string | null { - const given = this.tsJestConfig.tsConfig; - let resolved: string | undefined; - if (typeof given === 'string') { - // we got a path to a custom (or not) tsconfig - resolved = given.replace('', `${this.rootDir}${sep}`); - resolved = resolve(this.rootDir, resolved); - if (!existsSync(resolved)) { - resolved = undefined; - } - } else if (typeof given === 'undefined') { - // we got undefined, go look for the default file - resolved = findConfigFile(this.rootDir, sys.fileExists, 'tsconfig.json'); - } else { - // what we got was compiler options - return null; - } - // could we find one? - if (!resolved) { - throw new Error( - `Could not find a TS config file (given: "${given}", root: "${ - this.rootDir - }")`, - ); - } - return resolved; - } - - @Memoize() - get compilerOptions() { - const { configFile } = this; - if (configFile == null) { - // if it's null it means it's not a file but directly some compiler options - return this.tsJestConfig.tsConfig as CompilerOptions; - } - const { config, error } = readConfigFile(configFile, sys.readFile); - if (error) throw error; // tslint:disable-line:curly - const { errors, options } = parseJsonConfigFileContent( - config, - sys, - dirname(configFile), - undefined, - configFile, - ); - - // will throw if at least one error - this.reportDiagnostic(...errors); - - return options; - } - - transpileModule( - path: string, - content: string, - instrument: boolean = false, - ): string { - const compilerOptions: CompilerOptions = { - ...this.compilerOptions, - module: ModuleKind.CommonJS, - esModuleInterop: true, - inlineSources: false, - sourceMap: false, - inlineSourceMap: true, - }; - const options: TranspileOptions = { - fileName: path, - reportDiagnostics: false, // TODO: make this an option - // transformers: {}, // TODO: use this to hie decorators and such - compilerOptions, - }; - const { diagnostics, outputText } = transpileModule(content, options); - // TODO: handle diagnostics - this.reportDiagnostic(...diagnostics); - - // outputText will contain inline sourmaps - return outputText; - } - - reportDiagnostic(...diagnostics: Diagnostic[]) { - const diagnostic = diagnostics[0]; - if (!diagnostic) return; // tslint:disable-line:curly - - const message = flattenDiagnosticMessageText( - diagnostic.messageText, - this.formatHost.getNewLine(), - ); - throw new Error(`${diagnostic.code}: ${message}`); - } -} diff --git a/src/types.ts b/src/types.ts index 635b77e8ab..a82566d471 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,19 +1,42 @@ import * as _babelJest from 'babel-jest'; import { CompilerOptions } from 'typescript'; -import TsProgram from './ts-program'; export type TBabelJest = typeof _babelJest; +// CAUTION: use same key-value pair allow us to not store a list of values somewhere +export enum DiagnosticTypes { + syntactic = 'syntactic', + options = 'options', + global = 'global', + semantic = 'sementic', +} +export enum DiagnosticSets { + none = 'none', + full = 'full', + default = 'default', +} +export const diagnosticSets = { + full: [ + DiagnosticTypes.global, + DiagnosticTypes.syntactic, + DiagnosticTypes.semantic, + DiagnosticTypes.options, + ], + default: [DiagnosticTypes.syntactic, DiagnosticTypes.options], + none: [], +}; + export interface TsJestGlobalOptions { // either a file to the ts config or compiler options tsConfig?: string | CompilerOptions; + // what kind of diagnostics to report + diagnostics?: DiagnosticTypes[] | DiagnosticTypes | DiagnosticSets | boolean; // whether to use babel jest under the hood or not useBabelJest?: boolean; } -export interface TsJestInstance { - tsConfig: CompilerOptions; +export interface TsJestConfig { + inputOptions: TsJestGlobalOptions; useBabelJest: boolean; - shouldWrapHtml: boolean; - tsProgram: TsProgram; + diagnostics: DiagnosticTypes[]; } diff --git a/src/utils/diagnostics.ts b/src/utils/diagnostics.ts new file mode 100644 index 0000000000..cf1db3462b --- /dev/null +++ b/src/utils/diagnostics.ts @@ -0,0 +1,39 @@ +import { DiagnosticTypes, DiagnosticSets, diagnosticSets } from '../types'; + +export const isDiagnosticType = (val: any): val is DiagnosticTypes => { + return val && DiagnosticTypes[val] === val; +}; + +export const isDiagnosticSet = (val: any): val is DiagnosticSets => { + return val && DiagnosticSets[val] === val; +}; + +export const normalizeDiagnosticTypes = ( + val?: DiagnosticTypes[] | DiagnosticTypes | DiagnosticSets | boolean, +): DiagnosticTypes[] => { + let res!: DiagnosticTypes[]; + if (typeof val === 'string') { + // string + if (isDiagnosticType(val)) { + res = [val]; + } else if (isDiagnosticSet(val)) { + res = diagnosticSets[val]; + } + } else if (Array.isArray(val)) { + // array + if (val.every(isDiagnosticType)) { + res = val; + } + } else if (!val) { + // undeifned or false + res = []; + } else if (val) { + // true + res = diagnosticSets.default; + } + if (!res) { + throw new TypeError(`Invalid value for diagnostics: ${val}.`); + } + // ensure we have another instance of array with unique items + return res.filter((item, index) => res.indexOf(item) === index); +}; diff --git a/src/utils/file-extension.ts b/src/utils/file-extension.ts new file mode 100644 index 0000000000..65c22e0a59 --- /dev/null +++ b/src/utils/file-extension.ts @@ -0,0 +1,4 @@ +export default function(path: string): string { + const dotIndex = path.lastIndexOf('.'); + return path.substr(dotIndex + 1); +} diff --git a/src/utils/ts-internals.ts b/src/utils/ts-internals.ts new file mode 100644 index 0000000000..f75c2384a1 --- /dev/null +++ b/src/utils/ts-internals.ts @@ -0,0 +1,20 @@ +import ts, { + SourceFile, + Bundle, + Node, + Diagnostic, + CompilerOptions, +} from 'typescript'; + +// typescript internals +// TODO: should we copy/paste their source since it's internal? +export const getNodeId: (node: Node) => any = (ts as any).getNodeId; +export const fixupCompilerOptions: ( + options: CompilerOptions, + diagnostics: Diagnostic[], +) => CompilerOptions = (ts as any).fixupCompilerOptions; +export const chainBundle: ( + transformSourceFile: (x: SourceFile) => SourceFile, +) => (x: SourceFile | Bundle) => SourceFile | Bundle = (ts as any).chainBundle; +export const getOriginalNodeId: (node: Node) => number = (ts as any) + .getOriginalNodeId; diff --git a/src/utils/values.ts b/src/utils/values.ts new file mode 100644 index 0000000000..8048ba319e --- /dev/null +++ b/src/utils/values.ts @@ -0,0 +1,7 @@ +// to avoid dependencies we have our own Object.values() +export default function values(obj: Record): T[] { + return Object.keys(obj).reduce( + (array, key) => [...array, obj[key]], + [] as T[], + ); +}