Skip to content

Commit

Permalink
Rudimentary support for loading TypeScript test files
Browse files Browse the repository at this point in the history
* Generalize providers rather than supporting just the Babel one

* Add rudimentary TypeScript support
  • Loading branch information
novemberborn committed Jan 26, 2020
1 parent 91a0086 commit e4fef0c
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 112 deletions.
2 changes: 2 additions & 0 deletions docs/06-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ Note that providing files on the CLI overrides the `files` option.

Provide the `babel` option (and install [`@ava/babel`](https://github.com/avajs/babel) as an additional dependency) to enable Babel compilation.

Provide the `typescript` option (and install [`@ava/typescript`](https://github.com/avajs/typescript) as an additional dependency) to enable (rudimentary) TypeScript support.

## Using `ava.config.*` files

Rather than specifying the configuration in the `package.json` file you can use `ava.config.js` or `ava.config.cjs` files.
Expand Down
145 changes: 71 additions & 74 deletions docs/recipes/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,84 +4,15 @@ Translations: [Español](https://github.com/avajs/ava-docs/blob/master/es_ES/doc

AVA comes bundled with a TypeScript definition file. This allows developers to leverage TypeScript for writing tests.

This guide assumes you've already set up TypeScript for your project. Note that AVA's definition has been tested with version 3.7.5.

## Configuring AVA to compile TypeScript files on the fly

You can configure AVA to recognize TypeScript files. Then, with `ts-node` installed, you can compile them on the fly.

`package.json`:
Out of the box AVA does not load TypeScript test files, however. Rudimentary support is available via the [`@ava/typescript`] package. You can also use AVA with [`ts-node`]. Read on for details.

```json
{
"ava": {
"extensions": [
"ts"
],
"require": [
"ts-node/register"
]
}
}
```

It's worth noting that with this configuration tests will fail if there are TypeScript build errors. If you want to test while ignoring these errors you can use `ts-node/register/transpile-only` instead of `ts-node/register`.

### Using module path mapping

`ts-node` [does not support module path mapping](https://github.com/TypeStrong/ts-node/issues/138), however you can use [`tsconfig-paths`](https://github.com/dividab/tsconfig-paths#readme).

Once installed, add the `tsconfig-paths/register` entry to the `require` section of AVA's config:

`package.json`:

```json
{
"ava": {
"extensions": [
"ts"
],
"require": [
"ts-node/register",
"tsconfig-paths/register"
]
}
}
```

Then you can start using module aliases:

`tsconfig.json`:
```json
{
"baseUrl": ".",
"paths": {
"@helpers/*": ["helpers/*"]
}
}
```

Test:

```ts
import myHelper from '@helpers/myHelper';

// Rest of the file
```

## Compiling TypeScript files before running AVA
This guide assumes you've already set up TypeScript for your project. Note that AVA's definition has been tested with version 3.7.5.

Add a `test` script in the `package.json` file. It will compile the project first and then run AVA.
## Enabling AVA's TypeScript support

```json
{
"scripts": {
"test": "tsc && ava"
}
}
```
Currently, AVA's TypeScript support is designed to work for projects that precompile TypeScript. Please see [`@ava/typescript`] for setup instructions.

Make sure that AVA runs your built TypeScript files.
Read on until the end to learn how to use [`ts-node`] with AVA.

## Writing tests

Expand Down Expand Up @@ -221,3 +152,69 @@ test('throwsAsync', async t => {
```

Note that, despite the typing, the assertion returns `undefined` if it fails. Typing the assertions as returning `Error | undefined` didn't seem like the pragmatic choice.

## On the fly compilation using `ts-node`

If [`@ava/typescript`] doesn't do the trick you can use [`ts-node`]. Make sure it's installed and then configure AVA to recognize TypeScript files and register [`ts-node`]:

`package.json`:

```json
{
"ava": {
"extensions": [
"ts"
],
"require": [
"ts-node/register"
]
}
}
```

It's worth noting that with this configuration tests will fail if there are TypeScript build errors. If you want to test while ignoring these errors you can use `ts-node/register/transpile-only` instead of `ts-node/register`.

### Using module path mapping

`ts-node` [does not support module path mapping](https://github.com/TypeStrong/ts-node/issues/138), however you can use [`tsconfig-paths`](https://github.com/dividab/tsconfig-paths#readme).

Once installed, add the `tsconfig-paths/register` entry to the `require` section of AVA's config:

`package.json`:

```json
{
"ava": {
"extensions": [
"ts"
],
"require": [
"ts-node/register",
"tsconfig-paths/register"
]
}
}
```

Then you can start using module aliases:

`tsconfig.json`:
```json
{
"baseUrl": ".",
"paths": {
"@helpers/*": ["helpers/*"]
}
}
```

Test:

```ts
import myHelper from '@helpers/myHelper';

// Rest of the file
```

[`@ava/typescript`]: https://github.com/avajs/typescript
[`ts-node`]: https://www.npmjs.com/package/ts-node
23 changes: 17 additions & 6 deletions eslint-plugin-helper.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use strict';
const babelManager = require('./lib/babel-manager');
const normalizeExtensions = require('./lib/extensions');
const {classify, hasExtension, isHelperish, matches, normalizeFileForMatching, normalizeGlobs, normalizePatterns} = require('./lib/globs');
const loadConfig = require('./lib/load-config');
const providerManager = require('./lib/provider-manager');

const configCache = new Map();
const helperCache = new Map();
Expand All @@ -14,22 +14,33 @@ function load(projectDir, overrides) {
}

let conf;
let babelProvider;
let providers;
if (configCache.has(projectDir)) {
({conf, babelProvider} = configCache.get(projectDir));
({conf, providers} = configCache.get(projectDir));
} else {
conf = loadConfig({resolveFrom: projectDir});

providers = [];
if (Reflect.has(conf, 'babel')) {
babelProvider = babelManager({projectDir}).main({config: conf.babel});
providers.push({
type: 'babel',
main: providerManager.babel(projectDir).main({config: conf.babel})
});
}

configCache.set(projectDir, {conf, babelProvider});
if (Reflect.has(conf, 'typescript')) {
providers.push({
type: 'typescript',
main: providerManager.typescript(projectDir).main({config: conf.typescript})
});
}

configCache.set(projectDir, {conf, providers});
}

const extensions = overrides && overrides.extensions ?
normalizeExtensions(overrides.extensions) :
normalizeExtensions(conf.extensions, babelProvider);
normalizeExtensions(conf.extensions, providers);

let helperPatterns = [];
if (overrides && overrides.helpers !== undefined) {
Expand Down
9 changes: 6 additions & 3 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,11 @@ class Api extends Emittery {
}
});

const {babelProvider} = this.options;
const babelState = babelProvider === undefined ? null : await babelProvider.compile({cacheDir, files: testFiles});
const {providers = []} = this.options;
const providerStates = (await Promise.all(providers.map(async ({type, main}) => {
const state = await main.compile({cacheDir, files: testFiles});
return state === null ? null : {type, state};
}))).filter(state => state !== null);

// Resolve the correct concurrency value.
let concurrency = Math.min(os.cpus().length, isCi ? 2 : Infinity);
Expand All @@ -208,7 +211,7 @@ class Api extends Emittery {

const options = {
...apiOptions,
babelState,
providerStates,
recordNewSnapshots: !isCi,
// If we're looking for matches, run every single test process in exclusive-only mode
runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true
Expand Down
28 changes: 21 additions & 7 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,11 @@ exports.run = async () => { // eslint-disable-line complexity
const MiniReporter = require('./reporters/mini');
const TapReporter = require('./reporters/tap');
const Watcher = require('./watcher');
const babelManager = require('./babel-manager');
const normalizeExtensions = require('./extensions');
const {normalizeGlobs, normalizePatterns} = require('./globs');
const normalizeNodeArguments = require('./node-arguments');
const validateEnvironmentVariables = require('./environment-variables');
const providerManager = require('./provider-manager');

let pkg;
try {
Expand All @@ -279,10 +279,24 @@ exports.run = async () => { // eslint-disable-line complexity
js: defaultModuleType
};

let babelProvider;
const providers = [];
if (Reflect.has(conf, 'babel')) {
try {
babelProvider = babelManager({projectDir}).main({config: conf.babel});
providers.push({
type: 'babel',
main: providerManager.babel(projectDir).main({config: conf.babel})
});
} catch (error) {
exit(error.message);
}
}

if (Reflect.has(conf, 'typescript')) {
try {
providers.push({
type: 'typescript',
main: providerManager.typescript(projectDir).main({config: conf.typescript})
});
} catch (error) {
exit(error.message);
}
Expand All @@ -297,7 +311,7 @@ exports.run = async () => { // eslint-disable-line complexity

let extensions;
try {
extensions = normalizeExtensions(conf.extensions, babelProvider);
extensions = normalizeExtensions(conf.extensions, providers);
} catch (error) {
exit(error.message);
}
Expand Down Expand Up @@ -328,22 +342,22 @@ exports.run = async () => { // eslint-disable-line complexity
const filter = normalizePatterns(input.map(fileOrPattern => path.relative(projectDir, path.resolve(process.cwd(), fileOrPattern))));

const api = new Api({
babelProvider,
cacheEnabled: combined.cache !== false,
chalkOptions,
concurrency: combined.concurrency || 0,
debug,
environmentVariables,
experiments,
extensions,
failFast: combined.failFast,
failWithoutAssertions: combined.failWithoutAssertions !== false,
globs,
moduleTypes,
environmentVariables,
match,
moduleTypes,
nodeArguments,
parallelRuns,
projectDir,
providers,
ranFromCli: true,
require: arrify(combined.require),
serial: combined.serial,
Expand Down
8 changes: 4 additions & 4 deletions lib/extensions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = (configuredExtensions, babelProvider) => {
module.exports = (configuredExtensions, providers = []) => {
// Combine all extensions possible for testing. Remove duplicate extensions.
const duplicates = new Set();
const seen = new Set();
Expand All @@ -16,15 +16,15 @@ module.exports = (configuredExtensions, babelProvider) => {
combine(configuredExtensions);
}

if (babelProvider !== undefined) {
combine(babelProvider.extensions);
for (const {main} of providers) {
combine(main.extensions);
}

if (duplicates.size > 0) {
throw new Error(`Unexpected duplicate extensions in options: '${[...duplicates].join('\', \'')}'.`);
}

// Unless the default was used by `babelProvider`, as long as the extensions aren't explicitly set, set the default.
// Unless the default was used by providers, as long as the extensions aren't explicitly set, set the default.
if (configuredExtensions === undefined) {
if (!seen.has('cjs')) {
seen.add('cjs');
Expand Down
11 changes: 7 additions & 4 deletions lib/babel-manager.js → lib/provider-manager.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
const pkg = require('../package.json');
const globs = require('./globs');

module.exports = ({projectDir}) => {
function load(providerModule, projectDir) {
const ava = {version: pkg.version};
const makeProvider = require('@ava/babel');
const makeProvider = require(providerModule);

let fatal;
const provider = makeProvider({
negotiateProtocol(identifiers, {version}) {
if (!identifiers.includes('ava-3')) {
fatal = new Error(`This version of AVA (${ava.version}) is not compatible with@ava/babel@${version}`);
fatal = new Error(`This version of AVA (${ava.version}) is not compatible with ${providerModule}@${version}`);
return null;
}

Expand All @@ -30,4 +30,7 @@ module.exports = ({projectDir}) => {
}

return provider;
};
}

exports.babel = projectDir => load('@ava/babel', projectDir);
exports.typescript = projectDir => load('@ava/typescript', projectDir);
Loading

0 comments on commit e4fef0c

Please sign in to comment.