Skip to content

Commit

Permalink
custom typescript config file (closes #1845) (#3882)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexKamaev authored and AndreyBelym committed Jun 17, 2019
1 parent acb9100 commit 6083a38
Show file tree
Hide file tree
Showing 14 changed files with 502 additions and 268 deletions.
29 changes: 29 additions & 0 deletions src/compiler/compilers.js
@@ -0,0 +1,29 @@
import hammerhead from 'testcafe-hammerhead';
import { Compiler as LegacyTestFileCompiler } from 'testcafe-legacy-api';
import EsNextTestFileCompiler from './test-file/formats/es-next/compiler';
import TypeScriptTestFileCompiler from './test-file/formats/typescript/compiler';
import CoffeeScriptTestFileCompiler from './test-file/formats/coffeescript/compiler';
import RawTestFileCompiler from './test-file/formats/raw';

function createTestFileCompilers (options) {
return [
new LegacyTestFileCompiler(hammerhead.processScript),
new EsNextTestFileCompiler(),
new TypeScriptTestFileCompiler(options),
new CoffeeScriptTestFileCompiler(),
new RawTestFileCompiler()
];
}

let testFileCompilers = [];

export function getTestFileCompilers () {
if (!testFileCompilers.length)
initTestFileCompilers();

return testFileCompilers;
}

export function initTestFileCompilers (options = {}) {
testFileCompilers = createTestFileCompilers(options);
}
25 changes: 7 additions & 18 deletions src/compiler/index.js
@@ -1,34 +1,23 @@
import Promise from 'pinkie';
import { flattenDeep, find, chunk, uniq } from 'lodash';
import stripBom from 'strip-bom';
import { Compiler as LegacyTestFileCompiler } from 'testcafe-legacy-api';
import hammerhead from 'testcafe-hammerhead';
import EsNextTestFileCompiler from './test-file/formats/es-next/compiler';
import TypeScriptTestFileCompiler from './test-file/formats/typescript/compiler';
import CoffeeScriptTestFileCompiler from './test-file/formats/coffeescript/compiler';
import RawTestFileCompiler from './test-file/formats/raw';
import { readFile } from '../utils/promisified-functions';
import { GeneralError } from '../errors/runtime';
import { RUNTIME_ERRORS } from '../errors/types';
import { getTestFileCompilers, initTestFileCompilers } from './compilers';


const SOURCE_CHUNK_LENGTH = 1000;

const testFileCompilers = [
new LegacyTestFileCompiler(hammerhead.processScript),
new EsNextTestFileCompiler(),
new TypeScriptTestFileCompiler(),
new CoffeeScriptTestFileCompiler(),
new RawTestFileCompiler()
];

export default class Compiler {
constructor (sources) {
constructor (sources, options) {
this.sources = sources;

initTestFileCompilers(options);
}

static getSupportedTestFileExtensions () {
return uniq(testFileCompilers.map(compiler => compiler.getSupportedExtension()));
return uniq(getTestFileCompilers().map(compiler => compiler.getSupportedExtension()));
}

async _createTestFileInfo (filename) {
Expand All @@ -43,7 +32,7 @@ export default class Compiler {

code = stripBom(code).toString();

const compiler = find(testFileCompilers, someCompiler => someCompiler.canCompile(code, filename));
const compiler = find(getTestFileCompilers(), someCompiler => someCompiler.canCompile(code, filename));

if (!compiler)
return null;
Expand Down Expand Up @@ -128,6 +117,6 @@ export default class Compiler {
}

static cleanUp () {
testFileCompilers.forEach(compiler => compiler.cleanUp());
getTestFileCompilers().forEach(compiler => compiler.cleanUp());
}
}
50 changes: 25 additions & 25 deletions src/compiler/test-file/formats/typescript/compiler.js
Expand Up @@ -3,30 +3,18 @@ import { zipObject } from 'lodash';
import OS from 'os-family';
import APIBasedTestFileCompilerBase from '../../api-based';
import ESNextTestFileCompiler from '../es-next/compiler';

import TypescriptConfiguration from '../../../../configuration/typescript-configuration';

const RENAMED_DEPENDENCIES_MAP = new Map([['testcafe', APIBasedTestFileCompilerBase.EXPORTABLE_LIB_PATH]]);
const tsDefsPath = path.resolve(__dirname, '../../../../../ts-defs/index.d.ts');

export default class TypeScriptTestFileCompiler extends APIBasedTestFileCompilerBase {
static _getTypescriptOptions () {
// NOTE: lazy load the compiler
const ts = require('typescript');
constructor ({ typeScriptOptions } = {}) {
super();

return {
experimentalDecorators: true,
emitDecoratorMetadata: true,
allowJs: true,
pretty: true,
inlineSourceMap: true,
noImplicitAny: false,
module: ts.ModuleKind.CommonJS,
target: 2 /* ES6 */,
lib: ['lib.es6.d.ts'],
baseUrl: __dirname,
paths: { testcafe: ['../../../../../ts-defs/index.d.ts'] },
suppressOutputPathCheck: true,
skipLibCheck: true
};
const tsConfigPath = typeScriptOptions ? typeScriptOptions.tsConfigPath : null;

this.tsConfig = new TypescriptConfiguration(tsConfigPath);
}

static _reportErrors (diagnostics) {
Expand All @@ -35,11 +23,16 @@ export default class TypeScriptTestFileCompiler extends APIBasedTestFileCompiler
let errMsg = 'TypeScript compilation failed.\n';

diagnostics.forEach(d => {
const file = d.file;
const { line, character } = file.getLineAndCharacterOfPosition(d.start);
const message = ts.flattenDiagnosticMessageText(d.messageText, '\n');
const message = ts.flattenDiagnosticMessageText(d.messageText, '\n');
const file = d.file;

if (file) {
const { line, character } = file.getLineAndCharacterOfPosition(d.start);

errMsg += `${file.fileName} (${line + 1}, ${character + 1}): `;
}

errMsg += `${file.fileName} (${line + 1}, ${character + 1}): ${message}\n`;
errMsg += `${message}\n`;
});

throw new Error(errMsg);
Expand All @@ -54,19 +47,26 @@ export default class TypeScriptTestFileCompiler extends APIBasedTestFileCompiler
return filename;
}

_compileCodeForTestFiles (testFilesInfo) {
return this.tsConfig.init()
.then(() => {
return super._compileCodeForTestFiles(testFilesInfo);
});
}

_precompileCode (testFilesInfo) {
// NOTE: lazy load the compiler
const ts = require('typescript');

const filenames = testFilesInfo.map(({ filename }) => filename);
const filenames = testFilesInfo.map(({ filename }) => filename).concat([tsDefsPath]);
const normalizedFilenames = filenames.map(filename => TypeScriptTestFileCompiler._normalizeFilename(filename));
const normalizedFilenamesMap = zipObject(normalizedFilenames, filenames);

const uncachedFiles = normalizedFilenames
.filter(filename => !this.cache[filename])
.map(filename => normalizedFilenamesMap[filename]);

const opts = TypeScriptTestFileCompiler._getTypescriptOptions();
const opts = this.tsConfig.getOptions();
const program = ts.createProgram(uncachedFiles, opts);

program.getSourceFiles().forEach(sourceFile => {
Expand Down
5 changes: 3 additions & 2 deletions src/compiler/test-file/formats/typescript/get-test-list.js
@@ -1,7 +1,7 @@
import ts from 'typescript';
import { repeat, merge } from 'lodash';
import TypeScriptTestFileCompiler from './compiler';
import { TestFileParserBase } from '../../test-file-parser-base';
import TypescriptConfiguration from '../../../../configuration/typescript-configuration';

function replaceComments (code) {
return code.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, match => {
Expand Down Expand Up @@ -232,7 +232,8 @@ class TypeScriptTestFileParser extends TestFileParserBase {
this.codeArr = code.split('\n');
this.codeWithoutComments = replaceComments(code);

const sourceFile = ts.createSourceFile('', code, TypeScriptTestFileCompiler._getTypescriptOptions(), true);
const tsConfig = new TypescriptConfiguration();
const sourceFile = ts.createSourceFile('', code, tsConfig.getOptions(), true);

return this.analyze(sourceFile.statements);
}
Expand Down
179 changes: 179 additions & 0 deletions src/configuration/configuration-base.js
@@ -0,0 +1,179 @@
import debug from 'debug';
import { stat, readFile } from '../utils/promisified-functions';
import Option from './option';
import optionSource from './option-source';
import { cloneDeep, castArray } from 'lodash';
import resolvePathRelativelyCwd from '../utils/resolve-path-relatively-cwd';
import JSON5 from 'json5';
import renderTemplate from '../utils/render-template';
import WARNING_MESSAGES from '../notifications/warning-message';
import log from '../cli/log';

const DEBUG_LOGGER = debug('testcafe:configuration');

export default class Configuration {
constructor (configurationFileName) {
this._options = {};
this._filePath = resolvePathRelativelyCwd(configurationFileName);
this._overridenOptions = [];
}

static _fromObj (obj) {
const result = Object.create(null);

Object.entries(obj).forEach(([key, value]) => {
const option = new Option(key, value);

result[key] = option;
});

return result;
}

static async _isConfigurationFileExists (path) {
try {
await stat(path);

return true;
}
catch (error) {
DEBUG_LOGGER(renderTemplate(WARNING_MESSAGES.cannotFindConfigurationFile, path, error.stack));

return false;
}
}

static _showConsoleWarning (message) {
log.write(message);
}

static _showWarningForError (error, warningTemplate, ...args) {
const message = renderTemplate(warningTemplate, ...args);

Configuration._showConsoleWarning(message);

DEBUG_LOGGER(message);
DEBUG_LOGGER(error);
}

async init () {
}

mergeOptions (options) {
Object.entries(options).map(([key, value]) => {
const option = this._ensureOption(key, value, optionSource.input);

if (value === void 0)
return;

if (option.value !== value &&
option.source === optionSource.configuration)
this._overridenOptions.push(key);

this._setOptionValue(option, value);
});
}

getOption (key) {
if (!key)
return void 0;

const option = this._options[key];

if (!option)
return void 0;

return option.value;
}

getOptions () {
const result = Object.create(null);

Object.entries(this._options).forEach(([name, option]) => {
result[name] = option.value;
});

return result;
}

clone () {
return cloneDeep(this);
}

get filePath () {
return this._filePath;
}

async _load () {
const configurationFileExists = await Configuration._isConfigurationFileExists(this.filePath);

if (configurationFileExists) {
const configurationFileContent = await this._readConfigurationFileContent();

if (configurationFileContent)
return this._parseConfigurationFileContent(configurationFileContent);
}

return null;
}

async _readConfigurationFileContent () {
try {
return await readFile(this.filePath);
}
catch (error) {
Configuration._showWarningForError(error, WARNING_MESSAGES.cannotReadConfigFile);
}

return null;
}

_parseConfigurationFileContent (configurationFileContent) {
try {
return JSON5.parse(configurationFileContent);
}
catch (error) {
Configuration._showWarningForError(error, WARNING_MESSAGES.cannotParseConfigFile);
}

return null;
}

_ensureArrayOption (name) {
const options = this._options[name];

if (!options)
return;

options.value = castArray(options.value);
}

_ensureOption (name, value, source) {
let option = null;

if (name in this._options)
option = this._options[name];
else {
option = new Option(name, value, source);

this._options[name] = option;
}

return option;
}

_ensureOptionWithValue (name, defaultValue, source) {
const option = this._ensureOption(name, defaultValue, source);

if (option.value !== void 0)
return;

option.value = defaultValue;
option.source = source;
}

_setOptionValue (option, value) {
option.value = value;
option.source = optionSource.input;
}
}
13 changes: 13 additions & 0 deletions src/configuration/default-values.js
Expand Up @@ -15,3 +15,16 @@ export const DEFAULT_APP_INIT_DELAY = 1000;

export const DEFAULT_CONCURRENCY_VALUE = 1;

export const DEFAULT_TYPESCRIPT_COMPILER_OPTIONS = {
experimentalDecorators: true,
emitDecoratorMetadata: true,
allowJs: true,
pretty: true,
inlineSourceMap: true,
noImplicitAny: false,
module: 1 /* ts.ModuleKind.CommonJS */,
target: 2 /* ES6 */,
suppressOutputPathCheck: true
};

export const TYPESCRIPT_COMPILER_NON_OVERRIDABLE_OPTIONS = ['module', 'target'];
3 changes: 2 additions & 1 deletion src/configuration/option-names.js
Expand Up @@ -26,5 +26,6 @@ export default {
pageLoadTimeout: 'pageLoadTimeout',
videoPath: 'videoPath',
videoOptions: 'videoOptions',
videoEncodingOptions: 'videoEncodingOptions'
videoEncodingOptions: 'videoEncodingOptions',
tsConfigPath: 'tsConfigPath'
};

0 comments on commit 6083a38

Please sign in to comment.