Skip to content

Commit

Permalink
Add TypeScript support. Closes #1025 (#1026)
Browse files Browse the repository at this point in the history
* Add TypeScript support. Closes #1025

* Add TS leaks and override module target

* Fix problems with incorrect double path in sourcemaps

* Fix typo

* Address comments

Co-authored-by: Gil Pedersen <[email protected]>
  • Loading branch information
hueniverse and kanongil authored Dec 31, 2021
1 parent 2328482 commit 6dd0d2c
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 19 deletions.
5 changes: 3 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ To use source transforms, you must specify a file with the `-T` command line opt

#### TypeScript

A TypeScript definition file is included with **lab** to make it easier to use inside of an existing TypeScript project. Below is a TypeScript test example that uses the [lab-transform-typescript](https://www.npmjs.com/package/lab-transform-typescript) module to manage the transform:
A TypeScript definition file is included with **lab** to make it easier to use inside of an existing TypeScript project. To enable running test files written in TypeScript use the `--typescript` CLI option.

```typescript
import * as Lab from '@hapi/lab';
Expand All @@ -253,7 +253,7 @@ describe('experiment', () => {
Then the test can be executed using the following command line:

```sh
$ lab --sourcemaps --transform node_modules/lab-transform-typescript
$ lab --typescript
```

## Command Line
Expand Down Expand Up @@ -306,6 +306,7 @@ $ lab --sourcemaps --transform node_modules/lab-transform-typescript
- `-t`, `--threshold` - sets the minimum code test coverage percentage to 100%.
- `--types-test` - sets a single TypeScript definition test file (implies `-Y`). Use when the test directory contains other TypeScript files that should not be loaded for definition testing.
- `-T`, `--transform` - javascript file that exports an array of objects ie. `[ { ext: ".js", transform: (content, filename) => { ... } } ]`. Note that if you use this option with -c (--coverage), then you must generate sourcemaps and pass sourcemaps option to get proper line numbers.
- `--typescript` - enables the built-in TypeScript transpiler which uses the project own's `typescript` module and `tsconfig.json` file (or its other formats).
- `-v`, `--verbose` - verbose test output, defaults to false.
- `-V`, `--version` - display lab version information.
- `-Y`, `--types` - validate the module TypeScript types definitions. This is designed exclusively for JavaScript modules that export a TypeScript definition file.
Expand Down
22 changes: 21 additions & 1 deletion lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,11 @@ internals.options = function () {
description: 'location of types definitions test file',
default: null
},
typescript: {
type: 'boolean',
description: 'Enables TypeScript support',
default: null
},
verbose: {
alias: 'v',
type: 'boolean',
Expand Down Expand Up @@ -518,9 +523,24 @@ internals.options = function () {

const pattern = options.pattern ? '.*' + options.pattern + '.*?' : '';

if (options.typescript) {
if (options.transform) {
console.error('Cannot use "typescript" with "transform"');
process.exit(1);
}

if (options.types) {
console.error('Cannot use "typescript" with "types"');
process.exit(1);
}

options.transform = Modules.typescript.extensions;
options.sourcemaps = true;
}

let exts = '\\.(js)$';
if (options.transform) {
const transform = require(Path.resolve(options.transform));
const transform = typeof options.transform === 'string' ? require(Path.resolve(options.transform)) : options.transform;

Hoek.assert(Array.isArray(transform) && transform.length > 0, 'transform module must export an array of objects {ext: ".js", transform: null or function (content, filename)}');
options.transform = transform;
Expand Down
2 changes: 1 addition & 1 deletion lib/modules/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const internals = {
list: ['coverage', 'leaks', 'lint', 'transform', 'types']
list: ['coverage', 'leaks', 'lint', 'transform', 'types', 'typescript']
};


Expand Down
35 changes: 33 additions & 2 deletions lib/modules/leaks.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,46 @@ const internals = {
symbols: [
Symbol.toStringTag,
Symbol.for('@hapi/lab/coverage/_state')
],

typescript: [
'__extends',
'__assign',
'__rest',
'__decorate',
'__param',
'__metadata',
'__awaiter',
'__generator',
'__exportStar',
'__createBinding',
'__values',
'__read',
'__spread',
'__spreadArrays',
'__await',
'__asyncGenerator',
'__asyncDelegator',
'__asyncValues',
'__makeTemplateObject',
'__importStar',
'__importDefault',
'__classPrivateFieldGet',
'__classPrivateFieldSet',
'__classPrivateFieldIn'
]
};


exports.detect = function (customGlobals) {
exports.detect = function (customGlobals, options = {}) {

let allowed = internals.allowed;
if (customGlobals) {
allowed = allowed.concat(customGlobals);
allowed = [...allowed, ...customGlobals];
}

if (options.typescript) {
allowed = [...allowed, ...internals.typescript];
}

const symbols = [];
Expand Down
5 changes: 2 additions & 3 deletions lib/modules/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,15 @@ exports.transform = function (filename, content) {
}
});

const relativeFilename = Path.relative(process.cwd(), filename);
const relativeFilename = Path.resolve(filename);
internals.fileCache[relativeFilename] = (typeof transform === 'function') ? transform(content, relativeFilename) : content;
return internals.fileCache[relativeFilename];
};


exports.retrieveFile = function (path) {

const cwd = process.cwd();
const cacheKey = path.indexOf(cwd) === 0 ? path.substr(cwd.length + 1) : path;
const cacheKey = Path.resolve(path);
if (internals.fileCache[cacheKey]) {
return internals.fileCache[cacheKey];
}
Expand Down
43 changes: 43 additions & 0 deletions lib/modules/typescript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';

const Typescript = require('typescript');

const internals = {
configs: new Map()
};

internals.transform = function (content, fileName) {

const configFile = Typescript.findConfigFile(Typescript.getDirectoryPath(fileName), Typescript.sys.fileExists);

if (!internals.configs.has(configFile)) {
try {
var { config, error } = Typescript.readConfigFile(configFile, Typescript.sys.readFile);
if (error) {
throw new Error(`TypeScript config error in ${configFile}: ${error.messageText}`);
}
}
catch (err) {
throw new Error(`Cannot find a tsconfig file for ${fileName}`);
}

const { options } = Typescript.parseJsonConfigFileContent(config, Typescript.sys, Typescript.getDirectoryPath(configFile), {}, configFile);
options.sourceMap = false;
options.inlineSourceMap = true;
options.module = 1; // CommonJS
internals.configs.set(configFile, options);
}

const compilerOptions = internals.configs.get(configFile);
const { outputText } = Typescript.transpileModule(content, { fileName, compilerOptions });
return outputText;
};

exports.extensions = [
{ ext: '.ts', transform: internals.transform },
{ ext: '.tsx', transform: internals.transform }
];


// Adapted from https://github.com/garthk/lab-transform-typescript
// Copyright (C) 2016-2017 Garth Kidd <[email protected]>, MIT Licensed
2 changes: 1 addition & 1 deletion lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ exports.report = async function (scripts, options) {
const result = await exports.execute(scripts, settings, reporter);

if (settings.leaks) {
result.leaks = Modules.leaks.detect(settings.globals);
result.leaks = Modules.leaks.detect(settings.globals, settings);
}

if (settings.coverage) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"lab-event-reporter": "1.x.x",
"rimraf": "3.x.x",
"semver": "7.x.x",
"typescript": "~4.0.2"
"typescript": "^4.5.4"
},
"bin": {
"lab": "./bin/lab"
Expand Down
12 changes: 12 additions & 0 deletions test/cli_typescript/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { expect } from '@hapi/code';
import * as _Lab from '../../test_runner';

const { describe, it } = exports.lab = _Lab.script();

describe('Test CLI', () => {

it('adds two numbers together', () => {

expect(1 + 1).to.equal(4);
});
});
17 changes: 17 additions & 0 deletions test/cli_typescript/simple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { expect } from '@hapi/code';
import * as _Lab from '../../test_runner';

const { describe, it } = exports.lab = _Lab.script();

describe('Test CLI', () => {

it('adds two numbers together', () => {

expect(1 + 1).to.equal(2);
});

it('subtracts two numbers', () => {

expect(2 - 2).to.equal(0);
});
});
7 changes: 7 additions & 0 deletions test/cli_typescript/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"target": "es2021",
"module": "commonjs",
"moduleResolution": "node"
}
}
13 changes: 5 additions & 8 deletions test/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -1964,9 +1964,11 @@ describe('Runner', () => {
notebook.tests.forEach(assertNulledContext);
});

it('runs when typescript fails to load', async () => {
it('runs when typescript fails to load in types module', async () => {

// Override the _Lab runner because it's the one loading the transplier extension used
const desc = Object.getOwnPropertyDescriptor(_Lab, 'types');

try {
Object.defineProperty(_Lab, 'types', {
configurable: true,
Expand All @@ -1978,14 +1980,9 @@ describe('Runner', () => {
});

const script = Lab.script();
script.test('a', () => true);

script.test('a', () => {

return true;
});

const res1 = await Lab.report(script, { output: false, assert: false, types: true });

const res1 = await Lab.report(script, { output: false, assert: false, types: true, 'types-test': 'test/*.ts' });
expect(res1.code).to.equal(1);
expect(res1.output).to.contain([`Cannot find module 'typescript'`]);

Expand Down
40 changes: 40 additions & 0 deletions test/typescript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict';

const Path = require('path');

const Code = require('@hapi/code');
const _Lab = require('../test_runner');
const RunCli = require('./run_cli');


const internals = {
cwd: process.cwd()
};


const { describe, it, afterEach } = exports.lab = _Lab.script();
const expect = Code.expect;


describe('TypeScript', () => {

afterEach(() => process.chdir(internals.cwd));

it('supports TypeScript', async () => {

process.chdir(Path.join(__dirname, 'cli_typescript'));
const result = await RunCli(['simple.ts', '-m', '2000', '--typescript']);
expect(result.errorOutput).to.equal('');
expect(result.code).to.equal(0);
expect(result.output).to.contain('2 tests complete');
});

it('handles errors', async () => {

process.chdir(Path.join(__dirname, 'cli_typescript'));
const result = await RunCli(['error.ts', '-m', '2000', '--typescript']);
expect(result.errorOutput).to.equal('');
expect(result.code).to.equal(1);
expect(result.output).to.contain('error.ts:10:26');
});
});

0 comments on commit 6dd0d2c

Please sign in to comment.