diff --git a/docs/06-configuration.md b/docs/06-configuration.md index 0666d4c75..07d1208b8 100644 --- a/docs/06-configuration.md +++ b/docs/06-configuration.md @@ -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. diff --git a/docs/recipes/typescript.md b/docs/recipes/typescript.md index a96c926dc..79782b15b 100644 --- a/docs/recipes/typescript.md +++ b/docs/recipes/typescript.md @@ -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 @@ -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 diff --git a/eslint-plugin-helper.js b/eslint-plugin-helper.js index 82ae57fcb..63db7ba1e 100644 --- a/eslint-plugin-helper.js +++ b/eslint-plugin-helper.js @@ -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(); @@ -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) { diff --git a/lib/api.js b/lib/api.js index 013a3c9cf..e2bb2fbe1 100644 --- a/lib/api.js +++ b/lib/api.js @@ -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); @@ -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 diff --git a/lib/cli.js b/lib/cli.js index 48974eb88..d3bb162fc 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -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 { @@ -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); } @@ -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); } @@ -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, diff --git a/lib/extensions.js b/lib/extensions.js index b74a23b7e..1d7636dd2 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -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(); @@ -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'); diff --git a/lib/babel-manager.js b/lib/provider-manager.js similarity index 69% rename from lib/babel-manager.js rename to lib/provider-manager.js index ca634e5b3..31b8e6683 100644 --- a/lib/babel-manager.js +++ b/lib/provider-manager.js @@ -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; } @@ -30,4 +30,7 @@ module.exports = ({projectDir}) => { } return provider; -}; +} + +exports.babel = projectDir => load('@ava/babel', projectDir); +exports.typescript = projectDir => load('@ava/typescript', projectDir); diff --git a/lib/worker/subprocess.js b/lib/worker/subprocess.js index a764238e8..c4d50b75f 100644 --- a/lib/worker/subprocess.js +++ b/lib/worker/subprocess.js @@ -15,8 +15,8 @@ ipc.options.then(async options => { global.console = Object.assign(global.console, new console.Console({stdout, stderr, colorMode: true})); } - const babelManager = require('../babel-manager'); const nowAndTimers = require('../now-and-timers'); + const providerManager = require('../provider-manager'); const Runner = require('../runner'); const serializeError = require('../serialize-error'); const dependencyTracking = require('./dependency-tracker'); @@ -118,12 +118,20 @@ ipc.options.then(async options => { // Install before processing options.require, so if helpers are added to the // require configuration the *compiled* helper will be loaded. - let babelProvider; - if (options.babelState !== null) { - const {projectDir} = options; - babelProvider = babelManager({projectDir}).worker({extensionsToLoadAsModules, state: options.babelState}); - runner.powerAssert = babelProvider.powerAssert; - } + const {projectDir, providerStates = []} = options; + const providers = providerStates.map(({type, state}) => { + if (type === 'babel') { + const provider = providerManager.babel(projectDir).worker({extensionsToLoadAsModules, state}); + runner.powerAssert = provider.powerAssert; + return provider; + } + + if (type === 'typescript') { + return providerManager.typescript(projectDir).worker({extensionsToLoadAsModules, state}); + } + + return null; + }).filter(provider => provider !== null); let requireFn = require; const load = async ref => { @@ -135,8 +143,10 @@ ipc.options.then(async options => { } } - if (babelProvider !== undefined && babelProvider.canLoad(ref)) { - return babelProvider.load(ref, {requireFn}); + for (const provider of providers) { + if (provider.canLoad(ref)) { + return provider.load(ref, {requireFn}); + } } return requireFn(ref); diff --git a/test/api.js b/test/api.js index 629682f1b..77eb5adaa 100644 --- a/test/api.js +++ b/test/api.js @@ -6,15 +6,18 @@ const fs = require('fs'); const del = require('del'); const {test} = require('tap'); const Api = require('../lib/api'); -const babelManager = require('../lib/babel-manager'); const {normalizeGlobs} = require('../lib/globs'); +const providerManager = require('../lib/provider-manager'); const ROOT_DIR = path.join(__dirname, '..'); function apiCreator(options = {}) { options.projectDir = options.projectDir || ROOT_DIR; if (options.babelConfig !== undefined) { - options.babelProvider = babelManager({projectDir: options.projectDir}).main({config: options.babelConfig}); + options.providers = [{ + type: 'babel', + main: providerManager.babel(options.projectDir).main({config: options.babelConfig}) + }]; } options.chalkOptions = {level: 0}; diff --git a/test/helper/report.js b/test/helper/report.js index 479a093d7..bfcbbe814 100644 --- a/test/helper/report.js +++ b/test/helper/report.js @@ -6,8 +6,8 @@ const globby = require('globby'); const proxyquire = require('proxyquire'); const replaceString = require('replace-string'); const pkg = require('../../package.json'); -const babelManager = require('../../lib/babel-manager'); const {normalizeGlobs} = require('../../lib/globs'); +const providerManager = require('../../lib/provider-manager'); let _Api = null; const createApi = options => { @@ -72,7 +72,10 @@ exports.projectDir = type => path.join(__dirname, '../fixture/report', type.toLo const run = (type, reporter, match = []) => { const projectDir = exports.projectDir(type); - const babelProvider = babelManager({projectDir}).main({config: true}); + const providers = [{ + type: 'babel', + main: providerManager.babel(projectDir).main({config: true}) + }]; const options = { extensions: ['js'], @@ -83,7 +86,7 @@ const run = (type, reporter, match = []) => { cacheEnabled: true, experiments: {}, match, - babelProvider, + providers, projectDir, timeout: type.startsWith('timeout') ? '10s' : undefined, concurrency: 1,