Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add compile option #12

Merged
merged 22 commits into from
Apr 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
novemberborn marked this conversation as resolved.
Show resolved Hide resolved

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');