Skip to content

Commit

Permalink
Add compile option
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
szmarczak and novemberborn authored Apr 12, 2021
1 parent de9c6f7 commit 869760a
Show file tree
Hide file tree
Showing 18 changed files with 264 additions and 107 deletions.
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ insert_final_newline = true
[*.yml]
indent_style = space
indent_size = 2

[package.json]
indent_style = space
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/coverage
/node_modules
/test/fixtures/typescript/compiled
/test/broken-fixtures/typescript/compiled
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# @ava/typescript

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

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.
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.

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/`.
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`.

## Enabling TypeScript support

Expand All @@ -24,14 +24,17 @@ Then, enable TypeScript support either in `package.json` or `ava.config.*`:
"typescript": {
"rewritePaths": {
"src/": "build/"
}
},
"compile": false
}
}
}
```

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

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.

Output files are expected to have the `.js` extension.

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.
Expand Down
92 changes: 66 additions & 26 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,74 @@
'use strict';
const path = require('path');

const escapeStringRegexp = require('escape-string-regexp');

const execa = require('execa');
const pkg = require('./package.json');

const help = `See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md`;

function isPlainObject(x) {
return x !== null && typeof x === 'object' && Reflect.getPrototypeOf(x) === Object.prototype;
}

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

function isValidRewritePaths(rewritePaths) {
if (!isPlainObject(rewritePaths)) {
return false;
if (missing) {
if (required) {
throw new Error(`Missing '${key}' property in TypeScript configuration for AVA. ${help}`);
}

continue;
}

if (!isValid(target[key])) {
throw new Error(`Invalid '${key}' property in TypeScript configuration for AVA. ${help}`);
}
}

for (const key of Object.keys(target)) {
if (!Reflect.has(properties, key)) {
throw new Error(`Unexpected '${key}' property in TypeScript configuration for AVA. ${help}`);
}
}
}

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

const configProperties = {
compile: {
required: true,
isValid(compile) {
return compile === false || compile === 'tsc';
}
},
rewritePaths: {
required: true,
isValid(rewritePaths) {
if (!isPlainObject(rewritePaths)) {
return false;
}

return Object.entries(rewritePaths).every(([from, to]) => {
return from.endsWith('/') && typeof to === 'string' && to.endsWith('/');
});
}
},
extensions: {
required: false,
isValid(extensions) {
return Array.isArray(extensions) &&
extensions.length > 0 &&
extensions.every(ext => typeof ext === 'string' && ext !== '') &&
new Set(extensions).size === extensions.length;
}
}
};

module.exports = ({negotiateProtocol}) => {
const protocol = negotiateProtocol(['ava-3.2'], {version: pkg.version});
if (protocol === null) {
Expand All @@ -34,23 +77,16 @@ module.exports = ({negotiateProtocol}) => {

return {
main({config}) {
let valid = false;
if (isPlainObject(config)) {
const keys = Object.keys(config);
if (keys.every(key => key === 'extensions' || key === 'rewritePaths')) {
valid =
(config.extensions === undefined || isValidExtensions(config.extensions)) &&
isValidRewritePaths(config.rewritePaths);
}
if (!isPlainObject(config)) {
throw new Error(`Unexpected Typescript configuration for AVA. ${help}`);
}

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

const {
extensions = ['ts'],
rewritePaths: relativeRewritePaths
rewritePaths: relativeRewritePaths,
compile
} = config;

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

return {
async compile() {
if (compile === 'tsc') {
await compileTypeScript(protocol.projectDir);
}

return {
extensions: extensions.slice(),
rewritePaths: rewritePaths.slice()
Expand Down
15 changes: 13 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
"test": "xo && c8 ava"
},
"dependencies": {
"escape-string-regexp": "^4.0.0"
"escape-string-regexp": "^4.0.0",
"execa": "^5.0.0"
},
"devDependencies": {
"ava": "^3.15.0",
"c8": "^7.7.1",
"execa": "^5.0.0",
"del": "^6.0.0",
"typescript": "^4.2.4",
"xo": "^0.38.2"
},
"c8": {
Expand All @@ -34,7 +36,16 @@
"text"
]
},
"ava": {
"files": [
"!test/broken-fixtures/**"
],
"timeout": "60s"
},
"xo": {
"ignores": [
"test/broken-fixtures"
],
"rules": {
"import/order": "off"
}
Expand Down
23 changes: 23 additions & 0 deletions test/_with-provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const path = require('path');
const pkg = require('../package.json');
const makeProvider = require('..');

const createProviderMacro = (identifier, avaVersion, projectDir = __dirname) => {
return (t, run) => run(t, makeProvider({
negotiateProtocol(identifiers, {version}) {
t.true(identifiers.includes(identifier));
t.is(version, pkg.version);
return {
ava: {avaVersion},
identifier,
normalizeGlobPatterns: patterns => patterns,
async findFiles({patterns}) {
return patterns.map(file => path.join(projectDir, file));
},
projectDir
};
}
}));
};

module.exports = createProviderMacro;
8 changes: 8 additions & 0 deletions test/broken-fixtures/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"outDir": "typescript/compiled"
},
"include": [
"typescript"
]
}
1 change: 1 addition & 0 deletions test/broken-fixtures/typescript/typescript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
a
61 changes: 61 additions & 0 deletions test/compilation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const path = require('path');
const test = require('ava');
const del = require('del');
const execa = require('execa');
const createProviderMacro = require('./_with-provider');

const withProvider = createProviderMacro('ava-3.2', '3.2.0', path.join(__dirname, 'fixtures'));
const withAltProvider = createProviderMacro('ava-3.2', '3.2.0', path.join(__dirname, 'broken-fixtures'));

test.before('deleting compiled files', async t => {
t.log(await del('test/fixtures/typescript/compiled'));
t.log(await del('test/broken-fixtures/typescript/compiled'));
});

const compile = async provider => {
return {
state: await provider.main({
config: {
rewritePaths: {
'ts/': 'typescript/',
'compiled/': 'typescript/compiled/'
},
compile: 'tsc'
}
}).compile()
};
};

test('worker(): load rewritten paths files', withProvider, async (t, provider) => {
const {state} = await compile(provider);
const {stdout, stderr} = await execa.node(
path.join(__dirname, 'fixtures/install-and-load'),
[JSON.stringify(state), path.join(__dirname, 'fixtures/ts', 'file.ts')],
{cwd: path.join(__dirname, 'fixtures')}
);
if (stderr.length > 0) {
t.log(stderr);
}

t.snapshot(stdout);
});

test('worker(): runs compiled files', withProvider, async (t, provider) => {
const {state} = await compile(provider);
const {stdout, stderr} = await execa.node(
path.join(__dirname, 'fixtures/install-and-load'),
[JSON.stringify(state), path.join(__dirname, 'fixtures/compiled', 'index.ts')],
{cwd: path.join(__dirname, 'fixtures')}
);
if (stderr.length > 0) {
t.log(stderr);
}

t.snapshot(stdout);
});

test('compile() error', withAltProvider, async (t, provider) => {
const {message} = await t.throwsAsync(compile(provider));

t.snapshot(message);
});
6 changes: 3 additions & 3 deletions test/fixtures/install-and-load.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ const makeProvider = require('../..');

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

const worker = provider.worker({
extensionsToLoadAsModules: [],
state: JSON.parse(process.argv[3])
state: JSON.parse(process.argv[2])
});

const ref = path.resolve(process.argv[4]);
const ref = path.resolve(process.argv[3]);

if (worker.canLoad(ref)) {
worker.load(ref, {requireFn: require});
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"outDir": "typescript/compiled"
},
"include": [
"typescript"
]
}
File renamed without changes.
1 change: 1 addition & 0 deletions test/fixtures/typescript/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('logged in fixtures/typescript/index.ts');
Loading

0 comments on commit 869760a

Please sign in to comment.