Skip to content

Commit 869760a

Browse files
Add compile option
Support compilation using `tsc` before AVA runs tests. This is a breaking change. To retain the old behavior you must configure `compile: false`. Co-authored-by: Mark Wubben <[email protected]>
1 parent de9c6f7 commit 869760a

18 files changed

+264
-107
lines changed

.editorconfig

+3
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ insert_final_newline = true
1010
[*.yml]
1111
indent_style = space
1212
indent_size = 2
13+
14+
[package.json]
15+
indent_style = space

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
/coverage
22
/node_modules
3+
/test/fixtures/typescript/compiled
4+
/test/broken-fixtures/typescript/compiled

README.md

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# @ava/typescript
22

3-
Adds rudimentary [TypeScript](https://www.typescriptlang.org/) support to [AVA](https://avajs.dev).
3+
Adds [TypeScript](https://www.typescriptlang.org/) support to [AVA](https://avajs.dev).
44

5-
This is designed to work for projects that precompile their TypeScript code, including tests. It allows AVA to load the resulting JavaScript, while configuring AVA to use the TypeScript paths.
5+
This is designed to work for projects that precompile TypeScript. It allows AVA to load the compiled JavaScript, while configuring AVA to treat the TypeScript files as test files.
66

7-
In other words, say you have a test file at `src/test.ts`. You've configured TypeScript to output to `build/`. Using `@ava/typescript` you can run the `build/test.js` file using `npx ava src/test.ts`. AVA won't pick up any of the JavaScript files present in the `build/` directory, unless they have a TypeScript counterpart in `src/`.
7+
In other words, say you have a test file at `src/test.ts`. You've configured TypeScript to output to `build/`. Using `@ava/typescript` you can run the test using `npx ava src/test.ts`.
88

99
## Enabling TypeScript support
1010

@@ -24,14 +24,17 @@ Then, enable TypeScript support either in `package.json` or `ava.config.*`:
2424
"typescript": {
2525
"rewritePaths": {
2626
"src/": "build/"
27-
}
27+
},
28+
"compile": false
2829
}
2930
}
3031
}
3132
```
3233

3334
Both keys and values of the `rewritePaths` object must end with a `/`. Paths are relative to your project directory.
3435

36+
You can enable compilation via the `compile` property. If `false`, AVA will assume you have already compiled your project. If set to `'tsc'`, AVA will run the TypeScript compiler before running your tests. This can be inefficient when using AVA in watch mode.
37+
3538
Output files are expected to have the `.js` extension.
3639

3740
AVA searches your entire project for `*.js`, `*.cjs`, `*.mjs` and `*.ts` files (or other extensions you've configured). It will ignore such files found in the `rewritePaths` targets (e.g. `build/`). If you use more specific paths, for instance `build/main/`, you may need to change AVA's `files` configuration to ignore other directories.

index.js

+66-26
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,74 @@
11
'use strict';
22
const path = require('path');
3-
43
const escapeStringRegexp = require('escape-string-regexp');
5-
4+
const execa = require('execa');
65
const pkg = require('./package.json');
76

7+
const help = `See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md`;
8+
89
function isPlainObject(x) {
910
return x !== null && typeof x === 'object' && Reflect.getPrototypeOf(x) === Object.prototype;
1011
}
1112

12-
function isValidExtensions(extensions) {
13-
return Array.isArray(extensions) &&
14-
extensions.length > 0 &&
15-
extensions.every(ext => typeof ext === 'string' && ext !== '') &&
16-
new Set(extensions).size === extensions.length;
17-
}
13+
function validate(target, properties) {
14+
for (const key of Object.keys(properties)) {
15+
const {required, isValid} = properties[key];
16+
const missing = !Reflect.has(target, key);
1817

19-
function isValidRewritePaths(rewritePaths) {
20-
if (!isPlainObject(rewritePaths)) {
21-
return false;
18+
if (missing) {
19+
if (required) {
20+
throw new Error(`Missing '${key}' property in TypeScript configuration for AVA. ${help}`);
21+
}
22+
23+
continue;
24+
}
25+
26+
if (!isValid(target[key])) {
27+
throw new Error(`Invalid '${key}' property in TypeScript configuration for AVA. ${help}`);
28+
}
29+
}
30+
31+
for (const key of Object.keys(target)) {
32+
if (!Reflect.has(properties, key)) {
33+
throw new Error(`Unexpected '${key}' property in TypeScript configuration for AVA. ${help}`);
34+
}
2235
}
36+
}
2337

24-
return Object.entries(rewritePaths).every(([from, to]) => {
25-
return from.endsWith('/') && typeof to === 'string' && to.endsWith('/');
26-
});
38+
async function compileTypeScript(projectDir) {
39+
return execa('tsc', ['--incremental'], {preferLocal: true, cwd: projectDir});
2740
}
2841

42+
const configProperties = {
43+
compile: {
44+
required: true,
45+
isValid(compile) {
46+
return compile === false || compile === 'tsc';
47+
}
48+
},
49+
rewritePaths: {
50+
required: true,
51+
isValid(rewritePaths) {
52+
if (!isPlainObject(rewritePaths)) {
53+
return false;
54+
}
55+
56+
return Object.entries(rewritePaths).every(([from, to]) => {
57+
return from.endsWith('/') && typeof to === 'string' && to.endsWith('/');
58+
});
59+
}
60+
},
61+
extensions: {
62+
required: false,
63+
isValid(extensions) {
64+
return Array.isArray(extensions) &&
65+
extensions.length > 0 &&
66+
extensions.every(ext => typeof ext === 'string' && ext !== '') &&
67+
new Set(extensions).size === extensions.length;
68+
}
69+
}
70+
};
71+
2972
module.exports = ({negotiateProtocol}) => {
3073
const protocol = negotiateProtocol(['ava-3.2'], {version: pkg.version});
3174
if (protocol === null) {
@@ -34,23 +77,16 @@ module.exports = ({negotiateProtocol}) => {
3477

3578
return {
3679
main({config}) {
37-
let valid = false;
38-
if (isPlainObject(config)) {
39-
const keys = Object.keys(config);
40-
if (keys.every(key => key === 'extensions' || key === 'rewritePaths')) {
41-
valid =
42-
(config.extensions === undefined || isValidExtensions(config.extensions)) &&
43-
isValidRewritePaths(config.rewritePaths);
44-
}
80+
if (!isPlainObject(config)) {
81+
throw new Error(`Unexpected Typescript configuration for AVA. ${help}`);
4582
}
4683

47-
if (!valid) {
48-
throw new Error(`Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md for allowed values.`);
49-
}
84+
validate(config, configProperties);
5085

5186
const {
5287
extensions = ['ts'],
53-
rewritePaths: relativeRewritePaths
88+
rewritePaths: relativeRewritePaths,
89+
compile
5490
} = config;
5591

5692
const rewritePaths = Object.entries(relativeRewritePaths).map(([from, to]) => [
@@ -61,6 +97,10 @@ module.exports = ({negotiateProtocol}) => {
6197

6298
return {
6399
async compile() {
100+
if (compile === 'tsc') {
101+
await compileTypeScript(protocol.projectDir);
102+
}
103+
64104
return {
65105
extensions: extensions.slice(),
66106
rewritePaths: rewritePaths.slice()

package.json

+13-2
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
"test": "xo && c8 ava"
2020
},
2121
"dependencies": {
22-
"escape-string-regexp": "^4.0.0"
22+
"escape-string-regexp": "^4.0.0",
23+
"execa": "^5.0.0"
2324
},
2425
"devDependencies": {
2526
"ava": "^3.15.0",
2627
"c8": "^7.7.1",
27-
"execa": "^5.0.0",
28+
"del": "^6.0.0",
29+
"typescript": "^4.2.4",
2830
"xo": "^0.38.2"
2931
},
3032
"c8": {
@@ -34,7 +36,16 @@
3436
"text"
3537
]
3638
},
39+
"ava": {
40+
"files": [
41+
"!test/broken-fixtures/**"
42+
],
43+
"timeout": "60s"
44+
},
3745
"xo": {
46+
"ignores": [
47+
"test/broken-fixtures"
48+
],
3849
"rules": {
3950
"import/order": "off"
4051
}

test/_with-provider.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const path = require('path');
2+
const pkg = require('../package.json');
3+
const makeProvider = require('..');
4+
5+
const createProviderMacro = (identifier, avaVersion, projectDir = __dirname) => {
6+
return (t, run) => run(t, makeProvider({
7+
negotiateProtocol(identifiers, {version}) {
8+
t.true(identifiers.includes(identifier));
9+
t.is(version, pkg.version);
10+
return {
11+
ava: {avaVersion},
12+
identifier,
13+
normalizeGlobPatterns: patterns => patterns,
14+
async findFiles({patterns}) {
15+
return patterns.map(file => path.join(projectDir, file));
16+
},
17+
projectDir
18+
};
19+
}
20+
}));
21+
};
22+
23+
module.exports = createProviderMacro;

test/broken-fixtures/tsconfig.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"compilerOptions": {
3+
"outDir": "typescript/compiled"
4+
},
5+
"include": [
6+
"typescript"
7+
]
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
a

test/compilation.js

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const path = require('path');
2+
const test = require('ava');
3+
const del = require('del');
4+
const execa = require('execa');
5+
const createProviderMacro = require('./_with-provider');
6+
7+
const withProvider = createProviderMacro('ava-3.2', '3.2.0', path.join(__dirname, 'fixtures'));
8+
const withAltProvider = createProviderMacro('ava-3.2', '3.2.0', path.join(__dirname, 'broken-fixtures'));
9+
10+
test.before('deleting compiled files', async t => {
11+
t.log(await del('test/fixtures/typescript/compiled'));
12+
t.log(await del('test/broken-fixtures/typescript/compiled'));
13+
});
14+
15+
const compile = async provider => {
16+
return {
17+
state: await provider.main({
18+
config: {
19+
rewritePaths: {
20+
'ts/': 'typescript/',
21+
'compiled/': 'typescript/compiled/'
22+
},
23+
compile: 'tsc'
24+
}
25+
}).compile()
26+
};
27+
};
28+
29+
test('worker(): load rewritten paths files', withProvider, async (t, provider) => {
30+
const {state} = await compile(provider);
31+
const {stdout, stderr} = await execa.node(
32+
path.join(__dirname, 'fixtures/install-and-load'),
33+
[JSON.stringify(state), path.join(__dirname, 'fixtures/ts', 'file.ts')],
34+
{cwd: path.join(__dirname, 'fixtures')}
35+
);
36+
if (stderr.length > 0) {
37+
t.log(stderr);
38+
}
39+
40+
t.snapshot(stdout);
41+
});
42+
43+
test('worker(): runs compiled files', withProvider, async (t, provider) => {
44+
const {state} = await compile(provider);
45+
const {stdout, stderr} = await execa.node(
46+
path.join(__dirname, 'fixtures/install-and-load'),
47+
[JSON.stringify(state), path.join(__dirname, 'fixtures/compiled', 'index.ts')],
48+
{cwd: path.join(__dirname, 'fixtures')}
49+
);
50+
if (stderr.length > 0) {
51+
t.log(stderr);
52+
}
53+
54+
t.snapshot(stdout);
55+
});
56+
57+
test('compile() error', withAltProvider, async (t, provider) => {
58+
const {message} = await t.throwsAsync(compile(provider));
59+
60+
t.snapshot(message);
61+
});

test/fixtures/install-and-load.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ const makeProvider = require('../..');
33

44
const provider = makeProvider({
55
negotiateProtocol() {
6-
return {identifier: process.argv[2], ava: {version: '3.0.0'}, projectDir: path.resolve(__dirname, '..')};
6+
return {identifier: 'ava-3.2', ava: {version: '3.15.0'}, projectDir: __dirname};
77
}
88
});
99

1010
const worker = provider.worker({
1111
extensionsToLoadAsModules: [],
12-
state: JSON.parse(process.argv[3])
12+
state: JSON.parse(process.argv[2])
1313
});
1414

15-
const ref = path.resolve(process.argv[4]);
15+
const ref = path.resolve(process.argv[3]);
1616

1717
if (worker.canLoad(ref)) {
1818
worker.load(ref, {requireFn: require});

test/fixtures/tsconfig.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"compilerOptions": {
3+
"outDir": "typescript/compiled"
4+
},
5+
"include": [
6+
"typescript"
7+
]
8+
}
File renamed without changes.

test/fixtures/typescript/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log('logged in fixtures/typescript/index.ts');

0 commit comments

Comments
 (0)