├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── guess.js ├── index.js ├── package-lock.json ├── package.json ├── test ├── demo-es6-import.ts ├── demo.ts ├── lib │ ├── expectErrorStacksCorrect.ts │ ├── expectPowerAssertMessage.ts │ ├── hello.ts │ └── mycomponent.tsx ├── test-allow-js │ ├── package.json │ ├── test │ │ └── to_be_instrumented_test.js │ └── tsconfig.json ├── test-outdir │ ├── package.json │ ├── test │ │ └── to_be_instrumented_test.ts │ └── tsconfig.json ├── to_be_instrumented_test.ts └── to_be_instrumented_test.tsx └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["teppeis/node-v10", "teppeis/+prettier", "teppeis/+mocha"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [10.x, 12.x, 14.x, 16.x, 18.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm install 20 | - run: npm test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | test/test-outdir/**/*.js 3 | yarn.lock 4 | *.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # espower-typescript 2 | 3 | > power-assert instrumentor for TypeScript 4 | 5 | [![npm version][npm-image]][npm-url] 6 | ![Node.js Version Support][node-version] 7 | ![TypeScript Version Support][ts-version] 8 | [![build status][ci-image]][ci-url] 9 | [![Dependency Status][deps-image]][deps-url] 10 | ![monthly downloads][npm-downloads-image] 11 | ![License][license] 12 | 13 | ## TypeScript versions 14 | 15 | espower-typescript v10.x is compatible with TypeScript v2.7+ 16 | 17 | ## Usage (zero-config mode) 18 | 19 | Install 20 | 21 | ```console 22 | $ npm install -D espower-typescript power-assert mocha typescript @types/node @types/mocha 23 | ``` 24 | 25 | Create a test file (intensionally failed) 26 | 27 | ```typescript 28 | // test/test.ts 29 | import assert = require('assert'); 30 | 31 | describe('Array#join', () => { 32 | it('joins all elements into a string with separator', () => { 33 | assert(['a', 'b', 'c'].join(':') === 'a:b:c:'); 34 | }); 35 | }); 36 | ``` 37 | 38 | Run test 39 | 40 | ```console 41 | $ ./node_modules/.bin/mocha --require espower-typescript/guess "test/**/*.ts" 42 | ``` 43 | 44 | Output 45 | 46 | ``` 47 | 1) Array#join 48 | joins all elements into a string with separator: 49 | 50 | AssertionError [ERR_ASSERTION]: # test.ts:6 51 | 52 | assert(['a','b','c'].join(':') === 'a:b:c:') 53 | | | | 54 | ["a","b","c"] "a:b:c" false 55 | ``` 56 | 57 | ### CAUTION: don't use `import assert from 'assert'` 58 | 59 | Just use old style `import assert = require('assert')` for assert module. 60 | This is limitation. 61 | 62 | ## Configure 63 | 64 | ### If your tests are not in `test` directory 65 | 66 | You can set test directory in your `package.json` 67 | 68 | ```json 69 | { 70 | "name": "your-module", 71 | "description": "Your module", 72 | "version": "0.0.1", 73 | "directories": { 74 | "test": "spec/" 75 | }, 76 | ... 77 | } 78 | ``` 79 | 80 | Then, run mocha with `--require espower-typescript/guess` 81 | 82 | ```console 83 | $ ./node_modules/.bin/mocha --require espower-typescript/guess "spec/**/*.ts" 84 | ``` 85 | 86 | Note: `'espower-typescript/guess'` is inspired by [intelli-espower-loader](https://github.com/azu/intelli-espower-loader) 87 | 88 | ### ts-node and `tsconfig.json` 89 | 90 | espower-typescript uses [ts-node](https://github.com/TypeStrong/ts-node) internally. 91 | It loads your [tsconfig.json](https://github.com/Microsoft/TypeScript/wiki/tsconfig.json) automatically. 92 | 93 | ### Disable type check (transpile only) 94 | 95 | Use `TS_NODE_TRANSPILE_ONLY` env of ts-node 96 | 97 | ```console 98 | $ TS_NODE_TRANSPILE_ONLY=1 ./node_modules/.bin/mocha --require espower-typescript/guess "test/**/*.ts" 99 | ``` 100 | 101 | ### JSX/React 102 | 103 | `.tsx` files are supported. 104 | 105 | ### `allowJs` 106 | 107 | If `allowJs: true` in your `tsconfig.json`, assertions in `test/**/*.(js|jsx)` are empowered. 108 | 109 | ## License 110 | 111 | - MIT License: Teppei Sato <teppeis@gmail.com> 112 | - Includes [yosuke-furukawa/espower-traceur](https://github.com/yosuke-furukawa/espower-traceur) 113 | - Includes [azu/espower-babel](https://github.com/azu/espower-babel) 114 | 115 | [npm-image]: https://badgen.net/npm/v/espower-typescript?icon=npm&label= 116 | [npm-url]: https://npmjs.org/package/espower-typescript 117 | [npm-downloads-image]: https://badgen.net/npm/dm/espower-typescript 118 | [ci-image]: https://github.com/power-assert-js/espower-typescript/workflows/Node.js%20CI/badge.svg 119 | [ci-url]: https://github.com/power-assert-js/espower-typescript/actions?query=workflow%3A%22Node.js+CI%22 120 | [deps-image]: https://badgen.net/david/dep/power-assert-js/espower-typescript 121 | [deps-url]: https://david-dm.org/power-assert-js/espower-typescript 122 | [node-version]: https://badgen.net/npm/node/espower-typescript 123 | [ts-version]: https://badgen.net/badge/typescript/%3E=2.7?icon=typescript 124 | [license]: https://badgen.net/npm/license/espower-typescript 125 | -------------------------------------------------------------------------------- /guess.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const ts = require("typescript"); 5 | 6 | const cwd = process.cwd(); 7 | const compilerOptions = loadCompilerOptions(cwd) || {}; 8 | const extensions = ["ts", "tsx"]; 9 | if (compilerOptions.allowJs) { 10 | extensions.push("js"); 11 | extensions.push("jsx"); 12 | } 13 | 14 | let testDir = "test"; 15 | const packageData = require(path.join(cwd, "package.json")); 16 | if ( 17 | packageData && 18 | typeof packageData.directories === "object" && 19 | typeof packageData.directories.test === "string" 20 | ) { 21 | testDir = packageData.directories.test; 22 | } 23 | const pattern = path.join(testDir, `**/*.@(${extensions.join("|")})`); 24 | 25 | require("./index")({ cwd, pattern, extensions }); 26 | 27 | function loadCompilerOptions(cwd) { 28 | const tsconfigPath = ts.findConfigFile(cwd, ts.sys.fileExists); 29 | if (!tsconfigPath) { 30 | return null; 31 | } 32 | const result = ts.readConfigFile(tsconfigPath, ts.sys.readFile); 33 | if (result.error) { 34 | throw new Error(result.error.messageText); 35 | } 36 | if (result.config && result.config.compilerOptions) { 37 | const basepath = path.dirname(tsconfigPath); 38 | const { options } = ts.parseJsonConfigFileContent(result.config, ts.sys, basepath); 39 | return options; 40 | } 41 | return null; 42 | } 43 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint node/no-deprecated-api: [error, {ignoreGlobalItems: ["require.extensions"]}] */ 2 | 3 | "use strict"; 4 | 5 | const path = require("path"); 6 | const espowerSource = require("espower-source"); 7 | const minimatch = require("minimatch"); 8 | const sourceMapSupport = require("source-map-support"); 9 | const tsNodeRegister = require("ts-node").register; 10 | const sourceCache = {}; 11 | 12 | function espowerTypeScript(options, tsNodeOptions) { 13 | tsNodeRegister(tsNodeOptions); 14 | 15 | // install source-map-support again to correct the source-map 16 | sourceMapSupport.install({ 17 | environment: "node", 18 | retrieveFile: (path) => sourceCache[path], 19 | }); 20 | 21 | const { extensions = ["ts", "tsx"] } = options; 22 | extensions.forEach((ext) => { 23 | espowerTsRegister(`.${ext}`, options); 24 | }); 25 | } 26 | 27 | function espowerTsRegister(ext, options) { 28 | const cwd = options.cwd || process.cwd(); 29 | const pattern = path.join(cwd, options.pattern); 30 | 31 | const originalExtension = require.extensions[ext]; 32 | require.extensions[ext] = (module, filepath) => { 33 | if (!minimatch(filepath, pattern)) { 34 | return originalExtension(module, filepath); 35 | } 36 | const originalCompile = module._compile; 37 | module._compile = function (code, filepath) { 38 | const newSource = espowerSource(code, filepath, options); 39 | sourceCache[filepath] = newSource; 40 | return originalCompile.call(this, newSource, filepath); 41 | }; 42 | return originalExtension(module, filepath); 43 | }; 44 | } 45 | 46 | module.exports = espowerTypeScript; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "espower-typescript", 3 | "description": "power-assert instrumentor for TypeScript", 4 | "version": "10.0.1", 5 | "author": "Teppei Sato ", 6 | "engines": { 7 | "node": ">=10.17" 8 | }, 9 | "main": "index.js", 10 | "files": [ 11 | "*.js" 12 | ], 13 | "scripts": { 14 | "demo": "mocha --require './guess' test/demo.ts", 15 | "lint": "eslint *.js", 16 | "test": "run-s lint test:*", 17 | "test:allow-js": "cd test/test-allow-js && mocha --require ../../guess test/*_test.js", 18 | "test:outdir": "cd test/test-outdir && mocha --require ../../guess test/*_test.ts", 19 | "test:ts": "mocha --require './guess' test/*_test.ts", 20 | "test:tsx": "mocha --require './guess' test/*_test.tsx" 21 | }, 22 | "dependencies": { 23 | "espower-source": "^2.3.0", 24 | "minimatch": "^5.1.0", 25 | "source-map-support": "^0.5.12", 26 | "ts-node": "^10.9.1" 27 | }, 28 | "devDependencies": { 29 | "@types/mocha": "^8.2.0", 30 | "@types/node": "^10.17.51", 31 | "@types/react": "^17.0.0", 32 | "eslint": "^7.18.0", 33 | "eslint-config-teppeis": "^12.0.0", 34 | "mocha": "^8.2.1", 35 | "npm-run-all": "^4.1.3", 36 | "power-assert": "^1.6.1", 37 | "react": "^17.0.1", 38 | "typescript": "^4.7.4" 39 | }, 40 | "peerDependencies": { 41 | "typescript": ">= 2.7" 42 | }, 43 | "homepage": "https://github.com/power-assert-js/espower-typescript", 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/power-assert-js/espower-typescript" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/power-assert-js/espower-typescript/issues" 50 | }, 51 | "keywords": [ 52 | "power-assert", 53 | "typescript" 54 | ], 55 | "license": "MIT" 56 | } 57 | -------------------------------------------------------------------------------- /test/demo-es6-import.ts: -------------------------------------------------------------------------------- 1 | import hello from './lib/hello'; 2 | 3 | let assert = require('assert'); 4 | 5 | class Person { 6 | constructor(public name: string, public age: number) { 7 | } 8 | getAge(): string { 9 | return this.age; 10 | } 11 | } 12 | 13 | describe('Person', () => { 14 | let alice = new Person('alice', 3); 15 | let bob = new Person('bob', 5); 16 | it('#getAge', () => { 17 | assert(alice.getAge() === 3) 18 | }) 19 | it('#name', () => { 20 | assert(alice.name === 'alice') 21 | }) 22 | // failed 23 | it('#mistake', () => { 24 | assert(alice.name === bob.name) 25 | }) 26 | // failed 27 | it('hello', () => { 28 | assert(hello() === 'whoa!'); 29 | }); 30 | }) 31 | -------------------------------------------------------------------------------- /test/demo.ts: -------------------------------------------------------------------------------- 1 | // Run with Node.js v5+ 2 | 3 | 'use strict'; 4 | 5 | let assert = require('assert'); 6 | 7 | class Person { 8 | constructor(public name: string, public age: number) { 9 | } 10 | getAge(): string { 11 | return this.age; 12 | } 13 | } 14 | 15 | describe('Person', () => { 16 | let alice = new Person('alice', 3); 17 | let bob = new Person('bob', 5); 18 | it('#getAge', () => { 19 | assert(alice.getAge() === 3); 20 | }); 21 | it('#name', () => { 22 | assert(alice.name === 'alice'); 23 | }); 24 | // failed 25 | it('#mistake', () => { 26 | assert(alice.name === bob.name); 27 | }); 28 | // failed 29 | it('arrow function', () => { 30 | assert(alice.name === (() => 1)); 31 | }); 32 | // failed 33 | it('TypeScript 2.0', () => { 34 | function upperCase(this:string): string { 35 | return this.toUpperCase(); 36 | } 37 | assert(alice.name === upperCase.call('test')); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/lib/expectErrorStacksCorrect.ts: -------------------------------------------------------------------------------- 1 | import assert = require('assert'); 2 | 3 | export default function expectErrorStacksCorrect(line: number, column: number) { 4 | try { 5 | assert.fail('AssertionError should be thrown'); 6 | } catch(e) { 7 | const matches = e.stack.match(/expectErrorStacksCorrect\.ts:(\d+):(\d+)/); 8 | assert(!!matches); 9 | assert.equal(matches[1], line); 10 | assert.equal(matches[2], column); 11 | } 12 | } -------------------------------------------------------------------------------- /test/lib/expectPowerAssertMessage.ts: -------------------------------------------------------------------------------- 1 | // don't use `assert` not to instrument 2 | import ass = require('assert'); 3 | 4 | export default function expectPowerAssertMessage(body: () => void, expectedLines: string) { 5 | try { 6 | body(); 7 | ass.fail('AssertionError should be thrown'); 8 | } catch(e) { 9 | ass.equal(e.message.split('\n').slice(2, -1).join('\n'), expectedLines); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/lib/hello.ts: -------------------------------------------------------------------------------- 1 | export default function hello(): string { 2 | return 'hello'; 3 | } 4 | -------------------------------------------------------------------------------- /test/lib/mycomponent.tsx: -------------------------------------------------------------------------------- 1 | import React = require('react'); 2 | 3 | export default function foo(): any { 4 | return (); 5 | } 6 | -------------------------------------------------------------------------------- /test/test-allow-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-allow-js", 3 | "version": "1.0.0", 4 | "description": "This is a dummy file. See ../../package.json", 5 | "devDependencies": { 6 | }, 7 | "dependencies": { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/test-allow-js/test/to_be_instrumented_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | describe('test for allowJs option', () => { 6 | beforeEach(function() { 7 | // don't use `assert` not to instrument 8 | const ass = assert; 9 | this.expectPowerAssertMessage = (body, expectedLines) => { 10 | try { 11 | body(); 12 | ass.fail('AssertionError should be thrown'); 13 | } catch (e) { 14 | ass.equal( 15 | e.message 16 | .split('\n') 17 | .slice(2, -1) 18 | .join('\n'), 19 | expectedLines 20 | ); 21 | } 22 | }; 23 | }); 24 | 25 | it('equal with Literal and Identifier: assert.equal(1, minusOne)', function() { 26 | const minusOne = -1; 27 | const expected = ` assert.equal(1, minusOne) 28 | | 29 | -1 `; 30 | this.expectPowerAssertMessage(() => { 31 | assert.equal(1, minusOne); 32 | }, expected); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/test-allow-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "outDir": "build", 5 | "module": "commonjs", 6 | "target": "ES5", 7 | "noImplicitAny": true, 8 | "jsx": "react" 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /test/test-outdir/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-outdir", 3 | "version": "1.0.0", 4 | "description": "This is a dummy file. See ../../package.json", 5 | "devDependencies": { 6 | }, 7 | "dependencies": { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/test-outdir/test/to_be_instrumented_test.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import assert = require('assert'); 4 | import expectPowerAssertMessage from '../../lib/expectPowerAssertMessage'; 5 | 6 | describe('espower-typescript: `outDir` option', function() { 7 | it('equal with Literal and Identifier: assert.equal(1, minusOne)', function() { 8 | let minusOne: number = -1; 9 | let expected: string = 10 | ` assert.equal(1, minusOne) 11 | | 12 | -1 `; 13 | expectPowerAssertMessage(() => { 14 | assert.equal(1, minusOne); 15 | }, expected); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/test-outdir/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build", 4 | "module": "commonjs", 5 | "target": "ES5", 6 | "noImplicitAny": true, 7 | "jsx": "react" 8 | }, 9 | "exclude": [ 10 | "node_modules" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/to_be_instrumented_test.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import assert = require('assert'); 4 | import expectErrorStacksCorrect from './lib/expectErrorStacksCorrect'; 5 | import expectPowerAssertMessage from './lib/expectPowerAssertMessage'; 6 | import MyComponent from './lib/mycomponent'; 7 | 8 | describe('espower-typescript: ts', function() { 9 | it('Nested CallExpression with BinaryExpression: assert((three * (seven * ten)) === three)', function() { 10 | let one: number = 1; 11 | let two: number = 2; 12 | let three: number = 3; 13 | let seven: number = 7; 14 | let ten: number = 10; 15 | let expected: string = 16 | ` assert(three * (seven * ten) === three) 17 | | | | | | | | 18 | | | | | | | 3 19 | | | | | 10 false 20 | | | 7 70 21 | 3 210 22 | 23 | [number] three 24 | => 3 25 | [number] three * (seven * ten) 26 | => 210`; 27 | expectPowerAssertMessage(() => { 28 | assert(three * (seven * ten) === three); 29 | }, expected); 30 | }); 31 | 32 | it('equal with Literal and Identifier: assert.equal(1, minusOne)', function() { 33 | let minusOne: number = -1; 34 | let expected: string = 35 | ` assert.equal(1, minusOne) 36 | | 37 | -1 `; 38 | expectPowerAssertMessage(() => { 39 | assert.equal(1, minusOne); 40 | }, expected); 41 | }); 42 | 43 | it('error stack line number and column number should correct', function() { 44 | expectErrorStacksCorrect(5, 12); 45 | }); 46 | 47 | it('jsx:react', function() { 48 | let expected = 49 | ` assert.equal(1, (0, mycomponent_1.default)()) 50 | | | | 51 | | | #function# 52 | | Object{default:#function#} 53 | Object{"$$typeof":Symbol(react.element),type:"input",key:null,ref:null,props:#Object#,_owner:null,_store:#Object#}`; 54 | expectPowerAssertMessage(() => { 55 | assert.equal(1, MyComponent()); 56 | }, expected); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/to_be_instrumented_test.tsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import assert = require('assert'); 4 | import React = require('react'); 5 | import expectPowerAssertMessage from './lib/expectPowerAssertMessage'; 6 | 7 | describe('espower-typescript: tsx', function() { 8 | it('jsx:react', function() { 9 | let Foo = (): any => { 10 | return (); 11 | }; 12 | 13 | let expected = 14 | ` assert.equal(1, Foo()) 15 | | 16 | Object{"$$typeof":Symbol(react.element),type:"input",key:null,ref:null,props:#Object#,_owner:null,_store:#Object#}`; 17 | expectPowerAssertMessage(() => { 18 | assert.equal(1, Foo()); 19 | }, expected); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES5", 5 | "noImplicitAny": true, 6 | "jsx": "react" 7 | }, 8 | "exclude": [ 9 | "node_modules" 10 | ] 11 | } 12 | --------------------------------------------------------------------------------