├── .gitignore ├── .nvmrc ├── .vscode └── settings.json ├── APACHEv2-LICENSE.md ├── README.md ├── bin └── ts2gql ├── circle.yml ├── package.json ├── scripts ├── clean.sh ├── compile.sh ├── include │ ├── node.sh │ └── shell.sh ├── prepublish.sh ├── release.sh ├── start.sh ├── test.sh ├── test:compile.sh ├── test:integration.sh ├── test:style.sh ├── test:unit.sh └── test:unit:coverage.sh ├── src ├── Collector.ts ├── Emitter.ts ├── index.ts ├── types.ts └── util.ts ├── test ├── env │ ├── base.ts │ ├── integration.ts │ └── unit.ts ├── helpers │ ├── index.ts │ └── mocha.ts ├── integration │ ├── Emitter.ts │ └── jest.json ├── schema.ts └── unit │ ├── Thing.ts │ └── jest.json ├── tsconfig.json ├── tslint.json ├── typings ├── chai-jest-diff │ └── index.d.ts ├── doctrine │ └── index.d.ts ├── global │ └── test │ │ ├── base.d.ts │ │ └── unit.d.ts └── jest │ └── index.d.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # TypeScript 2 | /dist/ 3 | 4 | # MacOS 5 | **/.DS_Store 6 | 7 | # Node 8 | /node_modules/ 9 | npm-debug.log* 10 | npm-shrinkwrap.json 11 | package-lock.json 12 | 13 | # Build Artifacts 14 | /output/ 15 | *.d.ts 16 | *.js 17 | !/scripts/*.js 18 | 19 | # TypeScript 20 | !/typings/**/*.d.ts 21 | 22 | # Coverage 23 | .nyc_output/ 24 | coverage/ 25 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.13.0 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "typescript.tsdk": "./node_modules/typescript/lib" 4 | } 5 | -------------------------------------------------------------------------------- /APACHEv2-LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License, Version 2.0 2 | =========================== 3 | 4 | Copyright 2016 Convoy, Inc. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 7 | this file except in compliance with the License. You may obtain a copy of the 8 | License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software distributed 13 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 14 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 15 | specific language governing permissions and limitations under the License. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts2gql 2 | 3 | Walks a TypeScript type hierarchy and translates it into GraphQL's IDL. 4 | 5 | Usage: `ts2gql root/module.ts` 6 | 7 | `ts2gql` will load `root/module.ts` (and transitive dependencies), and look for an exported interface annotated with `/** @graphql schema */`, which will become the GraphQL `schema` declaration. All types referenced by that interface will be converted into GraphQL's IDL. 8 | 9 | ## Example (and "Docs") 10 | 11 | `input.ts` 12 | ```ts 13 | // Type aliases become GraphQL scalars. 14 | export type Url = string; 15 | 16 | // If you want an explicit GraphQL ID type, you can do that too: 17 | /** @graphql ID */ 18 | export type Id = string; 19 | 20 | // Interfaces become GraphQL types. 21 | export interface User { 22 | id: Id; 23 | name: string; 24 | photo: Url; 25 | } 26 | 27 | export interface PostContent { 28 | title: string; 29 | body: string; 30 | } 31 | 32 | export interface Post extends PostContent { 33 | id: Id; 34 | postedAt: Date; 35 | author: User; 36 | } 37 | 38 | export interface Category { 39 | id: Id; 40 | name: string; 41 | posts: Post[]; 42 | } 43 | 44 | /** @graphql input */ 45 | export interface IdQuery { 46 | id: Id; 47 | } 48 | 49 | /** @graphql input */ 50 | export interface PostQuery extends IdQuery { 51 | authorId: Id; 52 | categoryId: Id; 53 | } 54 | 55 | // Methods are transformed into parameteried edges: 56 | export interface QueryRoot { 57 | users(args: {id: Id}): User[] 58 | posts(args: {id: Id, authorId: Id, categoryId: Id}): Post[] 59 | categories(args: {id: Id}): Category[] 60 | } 61 | 62 | // Input types can be composed with inheritence. This renders to the same graphql as QueryRoot above. 63 | export interface QueryRootUsingTypes { 64 | users(args:IdQuery): User[] 65 | posts(args:PostQuery): Post[] 66 | categories(args:IdQuery): Category[] 67 | } 68 | 69 | export interface MutationRoot { 70 | login(args: {username: string, password: string}): QueryRoot; 71 | } 72 | 73 | // Don't forget to declare your schema and the root types! 74 | /** @graphql schema */ 75 | export interface Schema { 76 | query: QueryRoot; 77 | mutation: MutationRoot; 78 | } 79 | 80 | // If you have input objects (http://docs.apollostack.com/graphql/schemas.html#input-objects) 81 | /** @graphql input */ 82 | export interface EmailRecipients { 83 | type:string 84 | name:string 85 | email:Email 86 | } 87 | 88 | // You may also wish to expose some GraphQL specific fields or parameterized 89 | // calls on particular types, while still preserving the shape of your 90 | // interfaces for more general use: 91 | /** @graphql override Category */ 92 | export interface CategoryOverrides { 93 | // for example, we may want to be able to filter or paginate posts: 94 | posts(args: {authorId:Id}): Post[] 95 | } 96 | ``` 97 | 98 | ``` 99 | > ts2gql input.ts 100 | 101 | scalar Date 102 | 103 | scalar Url 104 | 105 | type User { 106 | id: ID 107 | name: String 108 | photo: Url 109 | } 110 | 111 | interface PostContent { 112 | body: String 113 | title: String 114 | } 115 | 116 | type Post { 117 | author: User 118 | body: String 119 | id: ID 120 | postedAt: Date 121 | title: String 122 | } 123 | 124 | type Category { 125 | id: ID 126 | name: String 127 | posts(authorId: ID): [Post] 128 | } 129 | 130 | type QueryRoot { 131 | categories(id: ID): [Category] 132 | posts(id: ID, authorId: ID, categoryId: ID): [Post] 133 | users(id: ID): [User] 134 | } 135 | 136 | type QueryRootUsingTypes { 137 | categories(id: ID): [Category] 138 | posts(id: ID, authorId: ID, categoryId: ID): [Post] 139 | users(id: ID): [User] 140 | } 141 | 142 | type MutationRoot { 143 | login(username: String, password: String): QueryRoot 144 | } 145 | 146 | schema { 147 | mutation: MutationRoot 148 | query: QueryRoot 149 | } 150 | 151 | ``` 152 | -------------------------------------------------------------------------------- /bin/ts2gql: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | if (process.argv.length < 3) { 3 | process.stderr.write('\n'); 4 | process.stderr.write('Usage: ts2gql root/module.ts\n'); 5 | process.stderr.write('\n'); 6 | process.exit(1); 7 | } 8 | 9 | require('..').emit(process.argv[2], process.argv.slice(3), process.stdout); 10 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | pre: 3 | - echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc 4 | override: 5 | - nvm install && nvm alias default $(cat .nvmrc) 6 | - npm install 7 | 8 | test: 9 | # Rather than just running `npm test` (which would be Circle's default) 10 | # we run explicit steps so that a build failure has a clearer source. 11 | # 12 | # Note that most tests are configured to run in parallel, so that you can get 13 | # immediate gains by configuring # of containers via Circle. 14 | override: 15 | - npm run test:compile 16 | 17 | - npm run test:style: 18 | parallel: true 19 | files: 20 | - src/**/*.{ts,tsx} 21 | - test/**/*.{ts,tsx} 22 | 23 | - npm run test:unit: 24 | parallel: true 25 | files: 26 | - test/unit/**/*.{ts,tsx} 27 | environment: 28 | MOCHA_FILE: "$CIRCLE_TEST_REPORTS/test:unit.xml" 29 | 30 | - npm run test:integration: 31 | parallel: true 32 | files: 33 | - test/integration/**/*.{ts,tsx} 34 | environment: 35 | MOCHA_FILE: "$CIRCLE_TEST_REPORTS/test:integration.xml" 36 | 37 | deployment: 38 | deploy: 39 | branch: master 40 | commands: 41 | - git config --global user.email "donvoy@convoy.com" 42 | - git config --global user.name "Don Voy" 43 | - npm run release 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts2gql", 3 | "version": "2.4.0", 4 | "description": "Converts a TypeScript type hierarchy into GraphQL's IDL.", 5 | "homepage": "https://github.com/convoyinc/ts2gql", 6 | "bugs": "https://github.com/convoyinc/ts2gql/issues", 7 | "license": "Apache-2.0", 8 | "repository": "convoyinc/ts2gql", 9 | "main": "./dist/src/index.js", 10 | "typings": "./dist/src/index.d.ts", 11 | "files": [ 12 | "dist/src", 13 | "*.md", 14 | "bin/*" 15 | ], 16 | "scripts": { 17 | "clean": "./scripts/clean.sh", 18 | "compile": "./scripts/compile.sh", 19 | "prepublish": "./scripts/prepublish.sh", 20 | "release": "./scripts/release.sh", 21 | "start": "./scripts/start.sh", 22 | "test:compile": "./scripts/test:compile.sh", 23 | "test:integration": "./scripts/test:integration.sh", 24 | "test:style": "./scripts/test:style.sh", 25 | "test:unit": "./scripts/test:unit.sh", 26 | "test": "./scripts/test.sh" 27 | }, 28 | "bin": { 29 | "ts2gql": "./bin/ts2gql" 30 | }, 31 | "dependencies": { 32 | "doctrine": "^1.2.2", 33 | "lodash": "^4.17.4" 34 | }, 35 | "devDependencies": { 36 | "@types/chai": "3.5.2", 37 | "@types/chai-as-promised": "0.0.30", 38 | "@types/lodash": "4.14.85", 39 | "@types/node": "12.12.14", 40 | "@types/sinon": "2.3.4", 41 | "@types/sinon-chai": "2.7.26", 42 | "chai": "3.5.0", 43 | "chai-as-promised": "6.0.0", 44 | "chai-jest-diff": "nevir/chai-jest-diff#built-member-assertions", 45 | "jest": "21.1.0", 46 | "jest-junit": "3.0.0", 47 | "sinon": "3.2.1", 48 | "sinon-chai": "2.13.0", 49 | "tslint": "5.8.0", 50 | "tslint-no-unused-expression-chai": "0.0.3", 51 | "typescript": "2.6.2" 52 | }, 53 | "peerDependencies": { 54 | "typescript": ">=2.0.9" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /scripts/clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | FILES_TO_REMOVE=($( 5 | find . \ 6 | \( -name "*.d.ts" -or -name "*.js" -or -name "*.js.map" \) \ 7 | -not -path "./scripts/*" \ 8 | -not -path "./coverage/*" \ 9 | -not -path "./node_modules/*" \ 10 | -not -path "./typings/*" 11 | )) 12 | 13 | if [[ "${#FILES_TO_REMOVE[@]}" != "0" ]]; then 14 | echo 15 | for file in "${FILES_TO_REMOVE[@]}"; do 16 | echo " ${file}" 17 | rm "${file}" 18 | done 19 | echo 20 | fi 21 | 22 | # We also just drop some trees completely. 23 | rm -rf ./output 24 | -------------------------------------------------------------------------------- /scripts/compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/node.sh 5 | 6 | npm run clean 7 | tsc "${@}" 8 | -------------------------------------------------------------------------------- /scripts/include/node.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | (( __NODE_INCLUDED__ )) && return 4 | __NODE_INCLUDED__=1 5 | 6 | if [[ ! -f ./.nvmrc ]]; then 7 | echo ".nvmrc is missing from the repo root (or paths are screwed up?)" 1>&2 8 | exit 1 9 | fi 10 | 11 | CURRENT_NODE_VERSION=$(node --version 2> /dev/null || echo 'none') 12 | DESIRED_NODE_VERSION=v$(cat ./.nvmrc) 13 | if [[ "${CURRENT_NODE_VERSION}" != "${DESIRED_NODE_VERSION}" ]]; then 14 | # Attempt to switch to the correct node for this shell… 15 | if ! type nvm >/dev/null 2>&1 && [[ -x "${NVM_DIR}"/nvm.sh ]]; then 16 | source "${NVM_DIR}"/nvm.sh 17 | fi 18 | if type nvm > /dev/null 2>&1; then 19 | nvm use 20 | fi 21 | 22 | CURRENT_NODE_VERSION=$(node --version 2>/dev/null || echo 'none') 23 | if [[ "${CURRENT_NODE_VERSION}" != "${DESIRED_NODE_VERSION}" ]]; then 24 | echo "Wrong node version. Found ${CURRENT_NODE_VERSION}, expected ${DESIRED_NODE_VERSION}…" 1>&2 25 | echo "…and was unable to switch to ${DESIRED_NODE_VERSION} via nvm." 1>&2 26 | exit 1 27 | fi 28 | fi 29 | 30 | write_package_key() { 31 | local KEY="${1}" 32 | local VALUE="${2}" 33 | 34 | node <<-end_script 35 | const _ = require('lodash'); 36 | const fs = require('fs'); 37 | 38 | const packageInfo = JSON.parse(fs.readFileSync('package.json')); 39 | _.set(packageInfo, '${KEY}', '${VALUE}'); 40 | fs.writeFileSync('package.json', JSON.stringify(packageInfo, null, 2)); 41 | end_script 42 | } 43 | -------------------------------------------------------------------------------- /scripts/include/shell.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | (( __SHELL_INCLUDED__ )) && return 4 | __SHELL_INCLUDED__=1 5 | 6 | export OPTIONS_FLAGS=() 7 | export OPTIONS_ARGS=() 8 | for argument in "${@}"; do 9 | if [[ "${argument}" =~ ^- ]]; then 10 | OPTIONS_FLAGS+=("${argument}") 11 | else 12 | OPTIONS_ARGS+=("${argument}") 13 | fi 14 | done 15 | -------------------------------------------------------------------------------- /scripts/prepublish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | NPM_COMMAND=$(node -e "console.log(JSON.parse(process.env.npm_config_argv).original[0])") 5 | # Skip builds if we're being run via a basic npm install. 6 | if [[ "${NPM_COMMAND}" == "install" ]]; then 7 | exit 0 8 | fi 9 | 10 | ./scripts/compile.sh 11 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/node.sh 5 | 6 | PACKAGE_XY=$(node -e "console.log(JSON.parse(fs.readFileSync('package.json')).version.replace(/\.\d+$/, ''))") 7 | PACKAGE_VERSION="${PACKAGE_XY}.${CIRCLE_BUILD_NUM}" 8 | 9 | npm run compile 10 | 11 | echo "Releasing ${PACKAGE_VERSION}" 12 | write_package_key version "${PACKAGE_VERSION}" 13 | git add . 14 | git commit -m "v${PACKAGE_VERSION}" 15 | git tag v${PACKAGE_VERSION} 16 | 17 | git push --tags 18 | npm publish 19 | -------------------------------------------------------------------------------- /scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/node.sh 5 | 6 | # Kill all child processes on exit. 7 | trap 'trap - SIGTERM && kill 0' SIGINT SIGTERM EXIT 8 | 9 | # Compile TypeScript sources in the background; but wait for it to have 10 | # completed the first compile run before passing control off to Jest. 11 | 12 | tsc_fifo=$(mktemp -u) 13 | mkfifo "${tsc_fifo}" 14 | 15 | npm run compile --watch >"${tsc_fifo}" 2>&1 & 16 | while read -r line; do 17 | echo "${line}" 18 | if [[ "${line}" =~ "Compilation complete" ]]; then 19 | break 20 | fi 21 | done <"${tsc_fifo}" 22 | # Finally, redirect tsc back to stdout 23 | cat "${tsc_fifo}" >&1 & 24 | 25 | # Let jest own our process & stdin. 26 | npm run test:unit --watch 27 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | npm run test:compile 5 | npm run test:style 6 | npm run test:unit 7 | npm run test:integration 8 | -------------------------------------------------------------------------------- /scripts/test:compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/node.sh 5 | 6 | tsc --noEmit 7 | -------------------------------------------------------------------------------- /scripts/test:integration.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/shell.sh 5 | source ./scripts/include/node.sh 6 | 7 | FILES=("${OPTIONS_ARGS[@]}") 8 | if [[ "${#FILES[@]}" == "0" ]]; then 9 | FILES+=($( 10 | find ./test/integration \ 11 | \( -name "*.ts" -not -name "*.d.ts" \) \ 12 | -or -name "*.tsx" 13 | )) 14 | fi 15 | 16 | # We take ts files as arguments for everyone's sanity; but redirect to their 17 | # compiled sources under the covers. 18 | for i in "${!FILES[@]}"; do 19 | file="${FILES[$i]}" 20 | if [[ "${file##*.}" == "ts" ]]; then 21 | FILES[$i]="${file%.*}.js" 22 | fi 23 | done 24 | 25 | OPTIONS=( 26 | --config ./test/integration/jest.json 27 | ) 28 | # Jest doesn't handle debugger flags directly. 29 | NODE_OPTIONS=() 30 | for option in "${OPTIONS_FLAGS[@]}"; do 31 | if [[ "${option}" =~ ^--(inspect|debug-brk|nolazy) ]]; then 32 | NODE_OPTIONS+=("${option}") 33 | else 34 | OPTIONS+=("${option}") 35 | fi 36 | done 37 | 38 | npm run compile 39 | 40 | # For jest-junit 41 | export JEST_SUITE_NAME="test:integration" 42 | export JEST_JUNIT_OUTPUT=./output/test:integration/report.xml 43 | 44 | set -x 45 | node "${NODE_OPTIONS[@]}" ./node_modules/.bin/jest "${OPTIONS[@]}" "${FILES[@]}" 46 | -------------------------------------------------------------------------------- /scripts/test:style.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/node.sh 5 | 6 | FILES=("${@}") 7 | if [[ "${#FILES[@]}" = "0" ]]; then 8 | FILES+=($(find scripts src test ! -name "*.d.ts" -and -name "*.ts" -or -name "*.tsx")) 9 | fi 10 | 11 | OPTIONS=( 12 | --format codeFrame 13 | --project tsconfig.json 14 | ) 15 | 16 | set -x 17 | tslint "${OPTIONS[@]}" "${FILES[@]}" 18 | -------------------------------------------------------------------------------- /scripts/test:unit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/shell.sh 5 | source ./scripts/include/node.sh 6 | 7 | FILES=("${OPTIONS_ARGS[@]}") 8 | if [[ "${#FILES[@]}" == "0" ]]; then 9 | FILES+=($( 10 | find ./test/unit \ 11 | \( -name "*.ts" -not -name "*.d.ts" \) \ 12 | -or -name "*.tsx" 13 | )) 14 | fi 15 | 16 | # We take ts files as arguments for everyone's sanity; but redirect to their 17 | # compiled sources under the covers. 18 | for i in "${!FILES[@]}"; do 19 | file="${FILES[$i]}" 20 | if [[ "${file##*.}" == "ts" ]]; then 21 | FILES[$i]="${file%.*}.js" 22 | fi 23 | done 24 | 25 | OPTIONS=( 26 | --config ./test/unit/jest.json 27 | ) 28 | # Jest doesn't handle debugger flags directly. 29 | NODE_OPTIONS=() 30 | for option in "${OPTIONS_FLAGS[@]}"; do 31 | if [[ "${option}" =~ ^--(inspect|debug-brk|nolazy) ]]; then 32 | NODE_OPTIONS+=("${option}") 33 | else 34 | OPTIONS+=("${option}") 35 | fi 36 | done 37 | 38 | npm run compile 39 | 40 | # For jest-junit 41 | export JEST_SUITE_NAME="test:unit" 42 | export JEST_JUNIT_OUTPUT=./output/test:unit/report.xml 43 | 44 | node "${NODE_OPTIONS[@]}" ./node_modules/.bin/jest "${OPTIONS[@]}" "${FILES[@]}" 45 | -------------------------------------------------------------------------------- /scripts/test:unit:coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source ./scripts/include/node.sh 5 | 6 | OPTIONS=() 7 | if [[ "${CI}" == "" ]]; then 8 | OPTIONS+=( 9 | --coverageReporters html 10 | ) 11 | fi 12 | 13 | npm run test:unit --coverage "${OPTIONS[@]}" 14 | 15 | if [[ "${CI}" == "" ]]; then 16 | open ./output/test:unit/index.html 17 | else 18 | codecov --file=./output/test:unit/lcov.info 19 | fi 20 | -------------------------------------------------------------------------------- /src/Collector.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import * as typescript from 'typescript'; 3 | 4 | import * as types from './types'; 5 | import * as util from './util'; 6 | 7 | const SyntaxKind = typescript.SyntaxKind; 8 | const TypeFlags = typescript.TypeFlags; 9 | 10 | /** 11 | * Walks declarations from a TypeScript programs, and builds up a map of 12 | * referenced types. 13 | */ 14 | export default class Collector { 15 | types:types.TypeMap = { 16 | Date: {type: 'alias', target: {type: 'string'}}, 17 | }; 18 | private checker:typescript.TypeChecker; 19 | private nodeMap:Map = new Map(); 20 | 21 | constructor(program:typescript.Program) { 22 | this.checker = program.getTypeChecker(); 23 | } 24 | 25 | addRootNode(node:typescript.InterfaceDeclaration):void { 26 | this._walkNode(node); 27 | const simpleNode = this.types[this._nameForSymbol(this._symbolForNode(node.name))]; 28 | simpleNode.concrete = true; 29 | } 30 | 31 | mergeOverrides(node:typescript.InterfaceDeclaration, name:types.SymbolName):void { 32 | const existing = this.types[name]; 33 | if (!existing) { 34 | throw new Error(`Cannot override "${name}" - it was never included`); 35 | } 36 | const overrides = node.members.map(this._walkNode); 37 | const overriddenNames = new Set(overrides.map(o => (o).name)); 38 | existing.members = _(existing.members) 39 | .filter(m => !overriddenNames.has(m.name)) 40 | .concat(overrides) 41 | .value(); 42 | } 43 | 44 | // Node Walking 45 | 46 | _walkNode = (node:typescript.Node):types.Node => { 47 | // Reentrant node walking. 48 | if (this.nodeMap.has(node)) { 49 | return this.nodeMap.get(node) as types.Node; 50 | } 51 | const nodeReference:types.Node = {}; 52 | this.nodeMap.set(node, nodeReference); 53 | 54 | let result:types.Node|null = null; 55 | if (node.kind === SyntaxKind.InterfaceDeclaration) { 56 | result = this._walkInterfaceDeclaration(node); 57 | } else if (node.kind === SyntaxKind.MethodSignature) { 58 | result = this._walkMethodSignature(node); 59 | } else if (node.kind === SyntaxKind.PropertySignature) { 60 | result = this._walkPropertySignature(node); 61 | } else if (node.kind === SyntaxKind.TypeReference) { 62 | result = this._walkTypeReferenceNode(node); 63 | } else if (node.kind === SyntaxKind.TypeAliasDeclaration) { 64 | result = this._walkTypeAliasDeclaration(node); 65 | } else if (node.kind === SyntaxKind.EnumDeclaration) { 66 | result = this._walkEnumDeclaration(node); 67 | } else if (node.kind === SyntaxKind.TypeLiteral) { 68 | result = this._walkTypeLiteralNode(node); 69 | } else if (node.kind === SyntaxKind.ArrayType) { 70 | result = this._walkArrayTypeNode(node); 71 | } else if (node.kind === SyntaxKind.UnionType) { 72 | result = this._walkUnionTypeNode(node); 73 | } else if (node.kind === SyntaxKind.LiteralType) { 74 | result = { 75 | type: 'string literal', 76 | value: _.trim((node).literal.getText(), "'\""), 77 | }; 78 | } else if (node.kind === SyntaxKind.StringKeyword) { 79 | result = {type: 'string'}; 80 | } else if (node.kind === SyntaxKind.NumberKeyword) { 81 | result = {type: 'number'}; 82 | } else if (node.kind === SyntaxKind.BooleanKeyword) { 83 | result = {type: 'boolean'}; 84 | } else if (node.kind === SyntaxKind.AnyKeyword) { 85 | result = { type: 'any' }; 86 | } else if (node.kind === SyntaxKind.ModuleDeclaration) { 87 | // Nada. 88 | } else if (node.kind === SyntaxKind.VariableDeclaration) { 89 | // Nada. 90 | } else { 91 | console.error(node); 92 | console.error(node.getSourceFile().fileName); 93 | throw new Error(`Don't know how to handle ${SyntaxKind[node.kind]} nodes`); 94 | } 95 | 96 | if (result) { 97 | Object.assign(nodeReference, result); 98 | } 99 | return nodeReference; 100 | } 101 | 102 | _walkSymbol = (symbol:typescript.Symbol):types.Node[] => { 103 | return _.map(symbol.getDeclarations(), d => this._walkNode(d)); 104 | } 105 | 106 | _walkInterfaceDeclaration(node:typescript.InterfaceDeclaration):types.Node { 107 | // TODO: How can we determine for sure that this is the global date? 108 | if (node.name.text === 'Date') { 109 | return {type: 'reference', target: 'Date'}; 110 | } 111 | 112 | return this._addType(node, () => { 113 | const inherits = []; 114 | if (node.heritageClauses) { 115 | for (const clause of node.heritageClauses) { 116 | for (const type of clause.types) { 117 | const symbol = this._symbolForNode(type.expression); 118 | this._walkSymbol(symbol); 119 | inherits.push(this._nameForSymbol(symbol)); 120 | } 121 | } 122 | } 123 | 124 | return { 125 | type: 'interface', 126 | members: node.members.map(this._walkNode), 127 | inherits, 128 | }; 129 | }); 130 | } 131 | 132 | _walkMethodSignature(node:typescript.MethodSignature):types.Node { 133 | const signature = this.checker.getSignatureFromDeclaration(node); 134 | const parameters:types.TypeMap = {}; 135 | for (const parameter of signature!.getParameters()) { 136 | const parameterNode = parameter.valueDeclaration; 137 | parameters[parameter.getName()] = this._walkNode(parameterNode.type!); 138 | } 139 | 140 | return { 141 | type: 'method', 142 | name: node.name.getText(), 143 | documentation: util.documentationForNode(node), 144 | parameters, 145 | returns: this._walkNode(node.type!), 146 | }; 147 | } 148 | 149 | _walkPropertySignature(node:typescript.PropertySignature):types.Node { 150 | return { 151 | type: 'property', 152 | name: node.name.getText(), 153 | documentation: util.documentationForNode(node), 154 | signature: this._walkNode(node.type!), 155 | }; 156 | } 157 | 158 | _walkTypeReferenceNode(node:typescript.TypeReferenceNode):types.Node { 159 | return this._referenceForSymbol(this._symbolForNode(node.typeName)); 160 | } 161 | 162 | _walkTypeAliasDeclaration(node:typescript.TypeAliasDeclaration):types.Node { 163 | return this._addType(node, () => ({ 164 | type: 'alias', 165 | target: this._walkNode(node.type), 166 | })); 167 | } 168 | 169 | _walkEnumDeclaration(node:typescript.EnumDeclaration):types.Node { 170 | return this._addType(node, () => { 171 | const values = node.members.map(m => { 172 | // If the user provides an initializer, use the value of the initializer 173 | // as the GQL enum value _unless_ the initializer is a numeric literal. 174 | if (m.initializer && m.initializer.kind !== SyntaxKind.NumericLiteral) { 175 | /** 176 | * Enums with initializers can look like: 177 | * 178 | * export enum Type { 179 | * CREATED = 'CREATED', 180 | * ACCEPTED = 'ACCEPTED', 181 | * } 182 | * 183 | * export enum Type { 184 | * CREATED = 'CREATED', 185 | * ACCEPTED = 'ACCEPTED', 186 | * } 187 | * 188 | * export enum Type { 189 | * CREATED = "CREATED", 190 | * ACCEPTED = "ACCEPTED", 191 | * } 192 | */ 193 | const target = _.last(m.initializer.getChildren()) || m.initializer; 194 | return _.trim(target.getText(), "'\""); 195 | } else { 196 | /** 197 | * For Enums without initializers (or with numeric literal initializers), emit the 198 | * EnumMember name as the value. Example: 199 | * export enum Type { 200 | * CREATED, 201 | * ACCEPTED, 202 | * } 203 | */ 204 | return _.trim(m.name.getText(), "'\""); 205 | } 206 | }); 207 | return { 208 | type: 'enum', 209 | values, 210 | }; 211 | }); 212 | } 213 | 214 | _walkTypeLiteralNode(node:typescript.TypeLiteralNode):types.Node { 215 | return { 216 | type: 'literal object', 217 | members: node.members.map(this._walkNode), 218 | }; 219 | } 220 | 221 | _walkArrayTypeNode(node:typescript.ArrayTypeNode):types.Node { 222 | return { 223 | type: 'array', 224 | elements: [this._walkNode(node.elementType)], 225 | }; 226 | } 227 | 228 | _walkUnionTypeNode(node:typescript.UnionTypeNode):types.Node { 229 | return { 230 | type: 'union', 231 | types: node.types.map(this._walkNode), 232 | }; 233 | } 234 | 235 | // Type Walking 236 | 237 | _walkType = (type:typescript.Type):types.Node => { 238 | if (type.flags & TypeFlags.Object) { 239 | return this._walkTypeReference(type); 240 | } else if (type.flags & TypeFlags.BooleanLike) { 241 | return this._walkInterfaceType(type); 242 | } else if (type.flags & TypeFlags.Index) { 243 | return this._walkNode(type.getSymbol()!.declarations![0]); 244 | } else if (type.flags & TypeFlags.String) { 245 | return {type: 'string'}; 246 | } else if (type.flags & TypeFlags.Number) { 247 | return {type: 'number'}; 248 | } else if (type.flags & TypeFlags.Boolean) { 249 | return {type: 'boolean'}; 250 | } else { 251 | console.error(type); 252 | console.error(type.getSymbol()!.declarations![0].getSourceFile().fileName); 253 | throw new Error(`Don't know how to handle type with flags: ${type.flags}`); 254 | } 255 | } 256 | 257 | _walkTypeReference(type:typescript.TypeReference):types.Node { 258 | if (type.target && type.target.getSymbol()!.name === 'Array') { 259 | return { 260 | type: 'array', 261 | elements: type.typeArguments!.map(this._walkType), 262 | }; 263 | } else { 264 | throw new Error('Non-array type references not yet implemented'); 265 | } 266 | } 267 | 268 | _walkInterfaceType(type:typescript.InterfaceType):types.Node { 269 | return this._referenceForSymbol(this._expandSymbol(type.getSymbol()!)); 270 | } 271 | 272 | // Utility 273 | 274 | _addType( 275 | node:typescript.InterfaceDeclaration|typescript.TypeAliasDeclaration|typescript.EnumDeclaration, 276 | typeBuilder:() => types.Node, 277 | ):types.Node { 278 | const name = this._nameForSymbol(this._symbolForNode(node.name)); 279 | if (this.types[name]) return this.types[name]; 280 | const type = typeBuilder(); 281 | (type).documentation = util.documentationForNode(node); 282 | this.types[name] = type; 283 | return type; 284 | } 285 | 286 | _symbolForNode(node:typescript.Node):typescript.Symbol { 287 | return this._expandSymbol(this.checker.getSymbolAtLocation(node)!); 288 | } 289 | 290 | _nameForSymbol(symbol:typescript.Symbol):types.SymbolName { 291 | symbol = this._expandSymbol(symbol); 292 | const parts = []; 293 | while (symbol) { 294 | parts.unshift(this.checker.symbolToString(symbol)); 295 | symbol = symbol['parent']; 296 | // Don't include raw module names. 297 | if (symbol && symbol.flags === typescript.SymbolFlags.ValueModule) break; 298 | } 299 | 300 | return parts.join('.'); 301 | } 302 | 303 | _expandSymbol(symbol:typescript.Symbol):typescript.Symbol { 304 | while (symbol.flags & typescript.SymbolFlags.Alias) { 305 | symbol = this.checker.getAliasedSymbol(symbol); 306 | } 307 | return symbol; 308 | } 309 | 310 | _referenceForSymbol(symbol:typescript.Symbol):types.ReferenceNode { 311 | this._walkSymbol(symbol); 312 | const referenced = this.types[this._nameForSymbol(symbol)]; 313 | if (referenced && referenced.type === 'interface') { 314 | referenced.concrete = true; 315 | } 316 | 317 | return { 318 | type: 'reference', 319 | target: this._nameForSymbol(symbol), 320 | }; 321 | } 322 | 323 | } 324 | -------------------------------------------------------------------------------- /src/Emitter.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import * as Types from './types'; 4 | import { ReferenceNode } from './types'; 5 | 6 | // tslint:disable-next-line 7 | // https://raw.githubusercontent.com/sogko/graphql-shorthand-notation-cheat-sheet/master/graphql-shorthand-notation-cheat-sheet.png 8 | export default class Emitter { 9 | renames:{[key:string]:string} = {}; 10 | 11 | constructor(private types:Types.TypeMap) { 12 | this.types = _.omitBy(types, (node, name) => this._preprocessNode(node, name!)); 13 | } 14 | 15 | emitAll(stream:NodeJS.WritableStream) { 16 | stream.write('\n'); 17 | _.each(this.types, (node, name) => this.emitTopLevelNode(node, name!, stream)); 18 | } 19 | 20 | emitTopLevelNode(node:Types.Node, name:Types.SymbolName, stream:NodeJS.WritableStream) { 21 | let content; 22 | if (node.type === 'alias') { 23 | content = this._emitAlias(node, name); 24 | } else if (node.type === 'interface') { 25 | content = this._emitInterface(node, name); 26 | } else if (node.type === 'enum') { 27 | content = this._emitEnum(node, name); 28 | } else { 29 | throw new Error(`Don't know how to emit ${node.type} as a top level node`); 30 | } 31 | stream.write(`${content}\n\n`); 32 | } 33 | 34 | // Preprocessing 35 | 36 | _preprocessNode(node:Types.Node, name:Types.SymbolName):boolean { 37 | if (node.type === 'alias' && node.target.type === 'reference') { 38 | const referencedNode = this.types[node.target.target]; 39 | if (this._isPrimitive(referencedNode) || referencedNode.type === 'enum') { 40 | this.renames[name] = node.target.target; 41 | return true; 42 | } 43 | } else if (node.type === 'alias' && this._hasDocTag(node, 'ID')) { 44 | this.renames[name] = 'ID'; 45 | return true; 46 | } 47 | 48 | return false; 49 | } 50 | 51 | // Nodes 52 | 53 | _emitAlias(node:Types.AliasNode, name:Types.SymbolName):string { 54 | if (this._isPrimitive(node.target)) { 55 | return `scalar ${this._name(name)}`; 56 | } else if (node.target.type === 'reference') { 57 | return `union ${this._name(name)} = ${this._name(node.target.target)}`; 58 | } else if (node.target.type === 'union') { 59 | return this._emitUnion(node.target, name); 60 | } else { 61 | throw new Error(`Can't serialize ${JSON.stringify(node.target)} as an alias`); 62 | } 63 | } 64 | 65 | _emitUnion(node:Types.UnionNode, name:Types.SymbolName):string { 66 | if (_.every(node.types, entry => entry.type === 'string literal')) { 67 | const nodeValues = node.types.map((type:Types.StringLiteralNode) => type.value); 68 | return this._emitEnum({ 69 | type: 'enum', 70 | values: _.uniq(nodeValues), 71 | }, this._name(name)); 72 | } 73 | 74 | node.types.map(type => { 75 | if (type.type !== 'reference') { 76 | throw new Error(`GraphQL unions require that all types are references. Got a ${type.type}`); 77 | } 78 | }); 79 | 80 | const firstChild = node.types[0] as ReferenceNode; 81 | const firstChildType = this.types[firstChild.target]; 82 | if (firstChildType.type === 'enum') { 83 | const nodeTypes = node.types.map((type:ReferenceNode) => { 84 | const subNode = this.types[type.target]; 85 | if (subNode.type !== 'enum') { 86 | throw new Error(`ts2gql expected a union of only enums since first child is an enum. Got a ${type.type}`); 87 | } 88 | return subNode.values; 89 | }); 90 | return this._emitEnum({ 91 | type: 'enum', 92 | values: _.uniq(_.flatten(nodeTypes)), 93 | }, this._name(name)); 94 | } else if (firstChildType.type === 'interface') { 95 | const nodeNames = node.types.map((type:ReferenceNode) => { 96 | const subNode = this.types[type.target]; 97 | if (subNode.type !== 'interface') { 98 | throw new Error(`ts2gql expected a union of only interfaces since first child is an interface. ` + 99 | `Got a ${type.type}`); 100 | } 101 | return type.target; 102 | }); 103 | return `union ${this._name(name)} = ${nodeNames.join(' | ')}`; 104 | } else { 105 | throw new Error(`ts2gql currently does not support unions for type: ${firstChildType.type}`); 106 | } 107 | } 108 | 109 | _emitInterface(node:Types.InterfaceNode, name:Types.SymbolName):string { 110 | // GraphQL expects denormalized type interfaces 111 | const members = _(this._transitiveInterfaces(node)) 112 | .map(i => i.members) 113 | .flatten() 114 | .uniqBy('name') 115 | .sortBy('name') 116 | .value(); 117 | 118 | // GraphQL can't handle empty types or interfaces, but we also don't want 119 | // to remove all references (complicated). 120 | if (!members.length) { 121 | members.push({ 122 | type: 'property', 123 | name: '_placeholder', 124 | signature: {type: 'boolean'}, 125 | }); 126 | } 127 | 128 | const properties = _.map(members, (member) => { 129 | if (member.type === 'method') { 130 | let parameters = ''; 131 | if (_.size(member.parameters) > 1) { 132 | throw new Error(`Methods can have a maximum of 1 argument`); 133 | } else if (_.size(member.parameters) === 1) { 134 | let argType = _.values(member.parameters)[0] as Types.Node; 135 | if (argType.type === 'reference') { 136 | argType = this.types[argType.target]; 137 | } 138 | parameters = `(${this._emitExpression(argType)})`; 139 | } 140 | const nonNullable = this._hasDocTag(member, 'non-nullable'); 141 | const returnType = this._emitExpression(member.returns, nonNullable); 142 | const directives = this._buildDirectives(member); 143 | return `${this._name(member.name)}${parameters}: ${returnType}${directives}`; 144 | } else if (member.type === 'property') { 145 | const directives = this._buildDirectives(member); 146 | const nonNullable = this._hasDocTag(member, 'non-nullable'); 147 | return `${this._name(member.name)}: ${this._emitExpression(member.signature, nonNullable)}${directives}`; 148 | } else { 149 | throw new Error(`Can't serialize ${member.type} as a property of an interface`); 150 | } 151 | }); 152 | 153 | if (this._getDocTag(node, 'schema')) { 154 | return `schema {\n${this._indent(properties)}\n}`; 155 | } else if (this._getDocTag(node, 'input')) { 156 | return `input ${this._name(name)} {\n${this._indent(properties)}\n}`; 157 | } 158 | 159 | if (node.concrete) { 160 | const directives = this._buildDirectives(node); 161 | const extendKeyword = this._hasDocTag(node, 'extend') ? 'extend ' : ''; 162 | return `${extendKeyword}type ${this._name(name)}${directives} {\n${this._indent(properties)}\n}`; 163 | } 164 | 165 | let result = `interface ${this._name(name)} {\n${this._indent(properties)}\n}`; 166 | const fragmentDeclaration = this._getDocTag(node, 'fragment'); 167 | if (fragmentDeclaration) { 168 | result = `${result}\n\n${fragmentDeclaration} {\n${this._indent(members.map((m:any) => m.name))}\n}`; 169 | } 170 | 171 | return result; 172 | } 173 | 174 | _emitEnum(node:Types.EnumNode, name:Types.SymbolName):string { 175 | return `enum ${this._name(name)} {\n${this._indent(node.values)}\n}`; 176 | } 177 | 178 | _emitExpression = (node:Types.Node, nonNullable?:boolean):string => { 179 | const suffix = nonNullable ? '!' : ''; 180 | if (!node) { 181 | return ''; 182 | } else if (node.type === 'string') { 183 | return `String${suffix}`; // TODO: ID annotation 184 | } else if (node.type === 'number') { 185 | return `Float${suffix}`; // TODO: Int/Float annotation 186 | } else if (node.type === 'boolean') { 187 | return `Boolean${suffix}`; 188 | } else if (node.type === 'reference') { 189 | return `${this._name(node.target)}${suffix}`; 190 | } else if (node.type === 'array') { 191 | return `[${node.elements.map(e => this._emitExpression(e, nonNullable)).join(' | ')}]${suffix}`; 192 | } else if (node.type === 'literal object' || node.type === 'interface') { 193 | return _(this._collectMembers(node)) 194 | .map((member:Types.PropertyNode) => { 195 | const nonNullable = this._hasDocTag(member, 'non-nullable'); 196 | return `${this._name(member.name)}: ${this._emitExpression(member.signature, nonNullable)}`; 197 | }) 198 | .join(', ') + suffix; 199 | } else { 200 | throw new Error(`Can't serialize ${node.type} as an expression`); 201 | } 202 | } 203 | 204 | _collectMembers = (node:Types.InterfaceNode|Types.LiteralObjectNode):Types.PropertyNode[] => { 205 | let members:Types.Node[] = []; 206 | if (node.type === 'literal object') { 207 | members = node.members; 208 | } else { 209 | const seenProps = new Set(); 210 | let interfaceNode:Types.InterfaceNode|null; 211 | interfaceNode = node; 212 | 213 | // loop through this interface and any super-interfaces 214 | while (interfaceNode) { 215 | for (const member of interfaceNode.members) { 216 | if (seenProps.has(member.name)) continue; 217 | seenProps.add(member.name); 218 | members.push(member); 219 | } 220 | if (interfaceNode.inherits.length > 1) { 221 | throw new Error(`No support for multiple inheritence: ${JSON.stringify(interfaceNode.inherits)}`); 222 | } else if (interfaceNode.inherits.length === 1) { 223 | const supertype:Types.Node = this.types[interfaceNode.inherits[0]]; 224 | if (supertype.type !== 'interface') { 225 | throw new Error(`Expected supertype to be an interface node: ${supertype}`); 226 | } 227 | interfaceNode = supertype; 228 | } else { 229 | interfaceNode = null; 230 | } 231 | } 232 | } 233 | 234 | for (const member of members) { 235 | if (member.type !== 'property') { 236 | throw new Error(`Expected members to be properties; got ${member.type}`); 237 | } 238 | } 239 | return members as Types.PropertyNode[]; 240 | } 241 | 242 | // Utility 243 | 244 | _name = (name:Types.SymbolName):string => { 245 | name = this.renames[name] || name; 246 | return name.replace(/\W/g, '_'); 247 | } 248 | 249 | _isPrimitive(node:Types.Node):boolean { 250 | return node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'any'; 251 | } 252 | 253 | _indent(content:string|string[]):string { 254 | if (!_.isArray(content)) content = content.split('\n'); 255 | return content.map(s => ` ${s}`).join('\n'); 256 | } 257 | 258 | _transitiveInterfaces(node:Types.InterfaceNode):Types.InterfaceNode[] { 259 | let interfaces = [node]; 260 | for (const name of node.inherits) { 261 | const inherited = this.types[name]; 262 | interfaces = interfaces.concat(this._transitiveInterfaces(inherited)); 263 | } 264 | return _.uniq(interfaces); 265 | } 266 | 267 | _hasDocTag(node:Types.ComplexNode, prefix:string):boolean { 268 | return !!this._getDocTag(node, prefix); 269 | } 270 | 271 | _getDocTag(node:Types.ComplexNode, prefix:string):string|null { 272 | if (!node.documentation) return null; 273 | for (const tag of node.documentation.tags) { 274 | if (tag.title !== 'graphql') continue; 275 | if (tag.description.startsWith(prefix)) return tag.description; 276 | } 277 | return null; 278 | } 279 | 280 | // Returns ALL matching tags from the given node. 281 | _getDocTags(node:Types.ComplexNode, prefix:string):string[] { 282 | const matchingTags:string[] = []; 283 | if (!node.documentation) return matchingTags; 284 | for (const tag of node.documentation.tags) { 285 | if (tag.title !== 'graphql') continue; 286 | if (tag.description.startsWith(prefix)) matchingTags.push(tag.description); 287 | } 288 | return matchingTags; 289 | } 290 | 291 | _buildDirectives(node:Types.ComplexNode) { 292 | // @key (on types) - federation 293 | let directives = this._getDocTags(node, 'key') 294 | .map(tag => ` @key(fields: "${tag.substring(4)}")`) 295 | .join(''); 296 | // @cost (on types or fields) 297 | const costExists = this._getDocTag(node, 'cost'); 298 | if (costExists) { 299 | directives = `${directives} @cost${costExists.substring(5)}`; 300 | } 301 | // @external (on fields) - federation 302 | if (this._hasDocTag(node, 'external')) { 303 | directives = `${directives} @external`; 304 | } 305 | // @requires (on fields) - federation 306 | const requires = this._getDocTag(node, 'requires'); 307 | if (requires) { 308 | directives = `${directives} @${requires}`; 309 | } 310 | return directives; 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import * as typescript from 'typescript'; 3 | import * as path from 'path'; 4 | 5 | import * as types from './types'; 6 | import * as util from './util'; 7 | import Collector from './Collector'; 8 | import Emitter from './Emitter'; 9 | 10 | export * from './types'; 11 | export { Emitter }; 12 | 13 | export function load(schemaRootPath:string, rootNodeNames:string[]):types.TypeMap { 14 | schemaRootPath = path.resolve(schemaRootPath); 15 | const program = typescript.createProgram([schemaRootPath], {}); 16 | const schemaRoot = program.getSourceFile(schemaRootPath); 17 | 18 | const interfaces:{[key:string]:typescript.InterfaceDeclaration} = {}; 19 | typescript.forEachChild(schemaRoot, (node) => { 20 | if (!isNodeExported(node)) return; 21 | if (node.kind === typescript.SyntaxKind.InterfaceDeclaration) { 22 | const interfaceNode = node; 23 | interfaces[interfaceNode.name.text] = interfaceNode; 24 | 25 | const documentation = util.documentationForNode(interfaceNode, schemaRoot.text); 26 | if (documentation && _.find(documentation.tags, {title: 'graphql', description: 'schema'})) { 27 | rootNodeNames.push(interfaceNode.name.text); 28 | } 29 | } 30 | }); 31 | 32 | rootNodeNames = _.uniq(rootNodeNames); 33 | 34 | const collector = new Collector(program); 35 | for (const name of rootNodeNames) { 36 | const rootInterface = interfaces[name]; 37 | if (!rootInterface) { 38 | throw new Error(`No interface named ${name} was exported by ${schemaRootPath}`); 39 | } 40 | collector.addRootNode(rootInterface); 41 | } 42 | 43 | _.each(interfaces, (node, name) => { 44 | const documentation = util.documentationForNode(node); 45 | if (!documentation) return; 46 | const override = _.find(documentation.tags, t => t.title === 'graphql' && t.description.startsWith('override')); 47 | if (!override) return; 48 | const overrideName = override.description.split(' ')[1] || name!; 49 | collector.mergeOverrides(node, overrideName); 50 | }); 51 | 52 | return collector.types; 53 | } 54 | 55 | export function emit( 56 | schemaRootPath:string, 57 | rootNodeNames:string[], 58 | stream:NodeJS.WritableStream = process.stdout, 59 | ):void { 60 | const loadedTypes = load(schemaRootPath, rootNodeNames); 61 | const emitter = new Emitter(loadedTypes); 62 | emitter.emitAll(stream); 63 | } 64 | 65 | function isNodeExported(node:typescript.Node):boolean { 66 | return !!node.modifiers && node.modifiers.some(m => m.kind === typescript.SyntaxKind.ExportKeyword); 67 | } 68 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import * as doctrine from 'doctrine'; 2 | 3 | export type SymbolName = string; 4 | 5 | export interface ComplexNode { 6 | documentation?:doctrine.ParseResult; 7 | } 8 | 9 | export interface InterfaceNode extends ComplexNode { 10 | type:'interface'; 11 | members:NamedNode[]; 12 | inherits:SymbolName[]; 13 | concrete?:boolean; // Whether the type is directly used (returned). 14 | } 15 | 16 | export interface MethodNode extends ComplexNode { 17 | type:'method'; 18 | name:string; 19 | parameters:{[key:string]:Node}; 20 | returns:Node; 21 | } 22 | 23 | export interface ArrayNode extends ComplexNode { 24 | type:'array'; 25 | elements:Node[]; 26 | } 27 | 28 | export interface ReferenceNode extends ComplexNode { 29 | type:'reference'; 30 | target:SymbolName; 31 | } 32 | 33 | export interface PropertyNode extends ComplexNode { 34 | type:'property'; 35 | name:string; 36 | signature:Node; 37 | } 38 | 39 | export interface AliasNode extends ComplexNode { 40 | type:'alias'; 41 | target:Node; 42 | } 43 | 44 | export interface EnumNode extends ComplexNode { 45 | type:'enum'; 46 | values:string[]; 47 | } 48 | 49 | export interface UnionNode extends ComplexNode { 50 | type:'union'; 51 | types:Node[]; 52 | } 53 | 54 | export interface LiteralObjectNode { 55 | type:'literal object'; 56 | members:Node[]; 57 | } 58 | 59 | export interface StringLiteralNode { 60 | type:'string literal'; 61 | value:string; 62 | } 63 | 64 | export interface StringNode { 65 | type:'string'; 66 | } 67 | 68 | export interface NumberNode { 69 | type:'number'; 70 | } 71 | 72 | export interface BooleanNode { 73 | type:'boolean'; 74 | } 75 | 76 | export interface AnyNode { 77 | type:'any'; 78 | } 79 | 80 | export type Node = 81 | InterfaceNode | 82 | MethodNode | 83 | ArrayNode | 84 | ReferenceNode | 85 | PropertyNode | 86 | AliasNode | 87 | EnumNode | 88 | UnionNode | 89 | LiteralObjectNode | 90 | StringLiteralNode | 91 | StringNode | 92 | NumberNode | 93 | BooleanNode | 94 | AnyNode; 95 | 96 | export type NamedNode = MethodNode | PropertyNode; 97 | 98 | export type TypeMap = {[key:string]:Node}; 99 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import * as doctrine from 'doctrine'; 3 | import * as typescript from 'typescript'; 4 | 5 | export function documentationForNode(node:typescript.Node, source?:string):doctrine.ParseResult|undefined { 6 | source = source || node.getSourceFile().text; 7 | const commentRanges = typescript.getLeadingCommentRanges(source, node.getFullStart()); 8 | if (!commentRanges) return undefined; 9 | // We only care about the closest comment to the node. 10 | const lastRange = _.last(commentRanges); 11 | if (!lastRange) return undefined; 12 | const comment = source.substr(lastRange.pos, lastRange.end - lastRange.pos).trim(); 13 | 14 | return doctrine.parse(comment, {unwrap: true}); 15 | } 16 | -------------------------------------------------------------------------------- /test/env/base.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import * as chaiAsPromised from 'chai-as-promised'; 3 | 4 | // Chai 5 | 6 | // We prefer Chai's `expect` interface. 7 | global.expect = chai.expect; 8 | // Give us all the info! 9 | chai.config.truncateThreshold = 0; 10 | 11 | // Promise-aware chai assertions (that return promises themselves): 12 | // 13 | // await expect(promise).to.be.rejectedWith(/error/i); 14 | // 15 | chai.use(chaiAsPromised); 16 | -------------------------------------------------------------------------------- /test/env/integration.ts: -------------------------------------------------------------------------------- 1 | import './base'; 2 | -------------------------------------------------------------------------------- /test/env/unit.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import * as sinon from 'sinon'; 3 | import * as sinonChai from 'sinon-chai'; 4 | 5 | import { withMocha } from '../helpers'; 6 | 7 | import './base'; 8 | 9 | // Chai 10 | 11 | // http://chaijs.com/plugins/sinon-chai 12 | // 13 | // Adds assertions for sinon spies. 14 | // 15 | // expect(aSpy).to.have.been.calledWith('abc', 123) 16 | // 17 | chai.use(sinonChai); 18 | 19 | // Test Environment 20 | 21 | withMocha(() => { 22 | 23 | beforeEach(() => { 24 | // Prefer accessing sinon via the `sandbox` global. 25 | global.sandbox = sinon.sandbox.create(); 26 | }); 27 | 28 | afterEach(() => { 29 | global.sandbox.restore(); 30 | delete global.sandbox; 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /test/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mocha'; 2 | -------------------------------------------------------------------------------- /test/helpers/mocha.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calls `callback` once Mocha has loaded its environment. 3 | * 4 | * See https://github.com/mochajs/mocha/issues/764 5 | */ 6 | export function withMocha(callback:() => void):void { 7 | if ('beforeEach' in global) { 8 | callback(); 9 | return; 10 | } 11 | 12 | setImmediate(() => { 13 | withMocha(callback); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /test/integration/Emitter.ts: -------------------------------------------------------------------------------- 1 | import Emitter from '../../src/Emitter'; 2 | import * as types from '../../src/types'; 3 | import * as ts2gql from '../../src/index'; 4 | import { UnionNode, AliasNode, EnumNode } from '../../src/types'; 5 | 6 | describe(`Emitter`, () => { 7 | 8 | let loadedTypes:types.TypeMap; 9 | let emitter:Emitter; 10 | beforeEach(() => { 11 | loadedTypes = ts2gql.load('./test/schema.ts', ['Schema']); 12 | emitter = new Emitter(loadedTypes); 13 | }); 14 | 15 | describe(`_emitUnion`, () => { 16 | it(`emits GQL type union for union of interface types`, () => { 17 | const expected = `union FooSearchResult = Human | Droid | Starship`; 18 | const aliasNode = loadedTypes['UnionOfInterfaceTypes'] as AliasNode; 19 | const unionNode = aliasNode.target as UnionNode; 20 | const val = emitter._emitUnion(unionNode, 'FooSearchResult'); 21 | expect(val).to.eq(expected); 22 | }); 23 | 24 | it(`emits GQL enum union for union of enum types`, () => { 25 | const expected = 26 | `enum FooSearchResult { 27 | Red 28 | Yellow 29 | Blue 30 | Big 31 | Small 32 | }`; 33 | const aliasNode = loadedTypes['UnionOfEnumTypes'] as AliasNode; 34 | const unionNode = aliasNode.target as UnionNode; 35 | const val = emitter._emitUnion(unionNode, 'FooSearchResult'); 36 | expect(val).to.eq(expected); 37 | }); 38 | 39 | it(`emits GQL type enum union for a union type of strings`, () => { 40 | const expected = 41 | `enum QuarkFlavor { 42 | UP 43 | DOWN 44 | CHARM 45 | STRANGE 46 | TOP 47 | BOTTOM 48 | }`; 49 | const aliasNode = loadedTypes['QuarkFlavor'] as AliasNode; 50 | const unionNode = aliasNode.target as UnionNode; 51 | const val = emitter._emitUnion(unionNode, 'QuarkFlavor'); 52 | expect(val).to.eq(expected); 53 | }); 54 | 55 | it(`throws error if union combines interfaces with other node types`, () => { 56 | const aliasNode = loadedTypes['UnionOfInterfaceAndOtherTypes'] as AliasNode; 57 | const unionNode = aliasNode.target as UnionNode; 58 | expect(() => { 59 | emitter._emitUnion(unionNode, 'FooSearchResult'); 60 | }).to.throw('ts2gql expected a union of only interfaces since first child is an interface. Got a reference'); 61 | }); 62 | 63 | it(`throws error if union combines enums with other node types`, () => { 64 | const aliasNode = loadedTypes['UnionOfEnumAndOtherTypes'] as AliasNode; 65 | const unionNode = aliasNode.target as UnionNode; 66 | expect(() => { 67 | emitter._emitUnion(unionNode, 'FooSearchResult'); 68 | }).to.throw('ts2gql expected a union of only enums since first child is an enum. Got a reference'); 69 | }); 70 | 71 | it(`throws error if union contains non-reference types`, () => { 72 | const aliasNode = loadedTypes['UnionOfNonReferenceTypes'] as AliasNode; 73 | const unionNode = aliasNode.target as UnionNode; 74 | expect(() => { 75 | emitter._emitUnion(unionNode, 'FooSearchResult'); 76 | }).to.throw('GraphQL unions require that all types are references. Got a boolean'); 77 | }); 78 | }); 79 | 80 | describe(`_emitEnum`, () => { 81 | it(`emits GQL type enum for string enum with single quotes`, () => { 82 | const expected = 83 | `enum Planet { 84 | CHTHONIAN 85 | CIRCUMBINARY 86 | PLUTOID 87 | }`; 88 | const enumNode = loadedTypes['Planet'] as EnumNode; 89 | const val = emitter._emitEnum(enumNode, 'Planet'); 90 | expect(val).to.eq(expected); 91 | }); 92 | 93 | it(`emits GQL type enum for string enum with double quotes`, () => { 94 | const expected = 95 | `enum Seasons { 96 | SPRING 97 | SUMMER 98 | FALL 99 | WINTER 100 | }`; 101 | const enumNode = loadedTypes['Seasons'] as EnumNode; 102 | const val = emitter._emitEnum(enumNode, 'Seasons'); 103 | expect(val).to.eq(expected); 104 | }); 105 | 106 | it(`emits GQL type enum for enum with 'any' typed initializers`, () => { 107 | const expected = 108 | `enum Cloud { 109 | ALTOSTRATUS 110 | CIRROCUMULUS 111 | CUMULONIMBUS 112 | }`; 113 | const enumNode = loadedTypes['Cloud'] as EnumNode; 114 | const val = emitter._emitEnum(enumNode, 'Cloud'); 115 | expect(val).to.eq(expected); 116 | }); 117 | 118 | it(`emits GQL type enum for enum with numeric literal initializers`, () => { 119 | const expected = 120 | `enum Ordinal { 121 | FIRST 122 | SECOND 123 | THIRD 124 | }`; 125 | const enumNode = loadedTypes['Ordinal'] as EnumNode; 126 | const val = emitter._emitEnum(enumNode, 'Ordinal'); 127 | expect(val).to.eq(expected); 128 | }); 129 | }); 130 | 131 | describe(`federation decoration`, () => { 132 | it(`basic type generation`, () => { 133 | const expected = 134 | `type Starship { 135 | length: Float 136 | name: String 137 | }`; 138 | const node = loadedTypes['Starship'] as types.InterfaceNode; 139 | const val = emitter._emitInterface(node, 'Starship'); 140 | expect(val).to.eq(expected); 141 | }); 142 | 143 | it(`basic key decoration`, () => { 144 | const expected = 145 | `type StarshipFederated @key(fields: "name") { 146 | length: Float 147 | name: String 148 | }`; 149 | const node = loadedTypes['StarshipFederated'] as types.InterfaceNode; 150 | const val = emitter._emitInterface(node, 'StarshipFederated'); 151 | expect(val).to.eq(expected); 152 | }); 153 | 154 | it(`compound key decoration`, () => { 155 | const expected = 156 | `type StarshipFederatedCompoundKey @key(fields: "name id") { 157 | id: String 158 | length: Float 159 | name: String 160 | }`; 161 | const node = loadedTypes['StarshipFederatedCompoundKey'] as types.InterfaceNode; 162 | const val = emitter._emitInterface(node, 'StarshipFederatedCompoundKey'); 163 | expect(val).to.eq(expected); 164 | }); 165 | 166 | it(`multiple keys decoration`, () => { 167 | const expected = 168 | `type StarshipFederatedMultipleKeys @key(fields: "name") @key(fields: "id") { 169 | id: String 170 | length: Float 171 | name: String 172 | }`; 173 | const node = loadedTypes['StarshipFederatedMultipleKeys'] as types.InterfaceNode; 174 | const val = emitter._emitInterface(node, 'StarshipFederatedMultipleKeys'); 175 | expect(val).to.eq(expected); 176 | }); 177 | 178 | it(`extending a foreign entity`, () => { 179 | const expected = 180 | `extend type ExtendingExternalEntity @key(fields: "name") { 181 | length: Float @requires(fields: "name") 182 | name: String @external 183 | }`; 184 | const node = loadedTypes['ExtendingExternalEntity'] as types.InterfaceNode; 185 | const val = emitter._emitInterface(node, 'ExtendingExternalEntity'); 186 | expect(val).to.eq(expected); 187 | }); 188 | }); 189 | 190 | it(`cost decoration field`, () => { 191 | const expected = 192 | `type CostDecorationField { 193 | bar: [String] 194 | baz: Float @cost(useMultipliers: false, complexity: 2) 195 | }`; 196 | const node = loadedTypes['CostDecorationField'] as types.InterfaceNode; 197 | const val = emitter._emitInterface(node, 'CostDecorationField'); 198 | expect(val).to.eq(expected); 199 | }); 200 | 201 | it(`cost decoration multiple fields`, () => { 202 | const expected = 203 | `type CostDecorationMultipleFields { 204 | bar: [String] @cost(useMultipliers: false, complexity: 2) 205 | baz: Float @cost(useMultipliers: false, complexity: 2) 206 | }`; 207 | const node = loadedTypes['CostDecorationMultipleFields'] as types.InterfaceNode; 208 | const val = emitter._emitInterface(node, 'CostDecorationMultipleFields'); 209 | expect(val).to.eq(expected); 210 | }); 211 | 212 | it(`cost decoration type`, () => { 213 | const expected = 214 | `type CostDecorationType @cost(useMultipliers: false, complexity: 2) { 215 | bar: [String] 216 | baz: Float 217 | }`; 218 | const node = loadedTypes['CostDecorationType'] as types.InterfaceNode; 219 | const val = emitter._emitInterface(node, 'CostDecorationType'); 220 | expect(val).to.eq(expected); 221 | }); 222 | 223 | it(`cost decoration field with key`, () => { 224 | const expected = 225 | `type CostDecorationFieldWithKey @key(fields: "name") { 226 | bar: [String] 227 | baz: Float @cost(useMultipliers: false, complexity: 2) 228 | }`; 229 | const node = loadedTypes['CostDecorationFieldWithKey'] as types.InterfaceNode; 230 | const val = emitter._emitInterface(node, 'CostDecorationFieldWithKey'); 231 | expect(val).to.eq(expected); 232 | }); 233 | 234 | it(`cost decoration type with key`, () => { 235 | const expected = 236 | `type CostDecorationTypeWithKey @key(fields: "name") @cost(useMultipliers: false, complexity: 2) { 237 | bar: [String] 238 | baz: Float 239 | }`; 240 | const node = loadedTypes['CostDecorationTypeWithKey'] as types.InterfaceNode; 241 | const val = emitter._emitInterface(node, 'CostDecorationTypeWithKey'); 242 | expect(val).to.eq(expected); 243 | }); 244 | 245 | it(`not-nullable types`, () => { 246 | const expected = 247 | `type NonNullableProperties { 248 | nonNullArray: [String!]! 249 | nonNullString: String! 250 | nullableString: String 251 | someMethod: Starship! 252 | }`; 253 | const node = loadedTypes['NonNullableProperties'] as types.InterfaceNode; 254 | const val = emitter._emitInterface(node, 'NonNullableProperties'); 255 | expect(val).to.eq(expected); 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /test/integration/jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "automock": false, 3 | "collectCoverageFrom": [ 4 | "dist/src/**/*.js" 5 | ], 6 | "coverageDirectory": "output/test:integration", 7 | "coverageReporters": ["lcovonly", "text"], 8 | "coveragePathIgnorePatterns": [ 9 | "/node_modules/", 10 | "/dist/test/" 11 | ], 12 | "mapCoverage": true, 13 | "rootDir": "../..", 14 | "setupTestFrameworkScriptFile": "./dist/test/env/integration.js", 15 | "testMatch": ["/dist/test/integration/**/*.js"], 16 | "testResultsProcessor": "./node_modules/jest-junit" 17 | } 18 | -------------------------------------------------------------------------------- /test/schema.ts: -------------------------------------------------------------------------------- 1 | export interface Human { 2 | name:string; 3 | height:number; 4 | } 5 | 6 | export interface Droid { 7 | name:string; 8 | primaryFunction:string; 9 | } 10 | 11 | export interface Starship { 12 | name:string; 13 | length:number; 14 | } 15 | 16 | /** @graphql key name */ 17 | export interface StarshipFederated { 18 | name:string; 19 | length:number; 20 | } 21 | 22 | /** @graphql key name id */ 23 | export interface StarshipFederatedCompoundKey { 24 | name:string; 25 | id:string; 26 | length:number; 27 | } 28 | 29 | /** 30 | * @graphql key name 31 | * @graphql key id 32 | */ 33 | export interface StarshipFederatedMultipleKeys { 34 | name:string; 35 | id:string; 36 | length:number; 37 | } 38 | 39 | export interface CostDecorationField { 40 | bar:string[]; 41 | /** @graphql cost (useMultipliers: false, complexity: 2) */ 42 | baz:number; 43 | } 44 | 45 | export interface CostDecorationMultipleFields { 46 | /** @graphql cost (useMultipliers: false, complexity: 2) */ 47 | bar:string[]; 48 | /** @graphql cost (useMultipliers: false, complexity: 2) */ 49 | baz:number; 50 | } 51 | 52 | /** @graphql cost (useMultipliers: false, complexity: 2) */ 53 | export interface CostDecorationType { 54 | bar:string[]; 55 | baz:number; 56 | } 57 | 58 | /** @graphql key name */ 59 | export interface CostDecorationFieldWithKey { 60 | bar:string[]; 61 | /** @graphql cost (useMultipliers: false, complexity: 2) */ 62 | baz:number; 63 | } 64 | 65 | /** 66 | * @graphql cost (useMultipliers: false, complexity: 2) 67 | * @graphql key name 68 | */ 69 | export interface CostDecorationTypeWithKey { 70 | bar:string[]; 71 | baz:number; 72 | } 73 | 74 | /** 75 | * @graphql key name 76 | * @graphql extend 77 | */ 78 | export interface ExtendingExternalEntity { 79 | /** @graphql external */ 80 | name:string; 81 | /** @graphql requires(fields: "name") */ 82 | length:number; 83 | } 84 | 85 | export interface NonNullableProperties { 86 | nullableString:string; 87 | /* @graphql non-nullable */ 88 | nonNullString:string; 89 | /* @graphql non-nullable */ 90 | nonNullArray:string[]; 91 | /* @graphql non-nullable */ 92 | someMethod(): Starship; 93 | } 94 | 95 | export enum Color { 96 | 'Red', 97 | 'Yellow', 98 | 'Blue', 99 | } 100 | 101 | export enum Size { 102 | 'Big', 103 | 'Small', 104 | } 105 | 106 | export enum Planet { 107 | CHTHONIAN = 'CHTHONIAN', 108 | CIRCUMBINARY = 'CIRCUMBINARY', 109 | PLUTOID = 'PLUTOID', 110 | } 111 | 112 | export enum Seasons { 113 | SPRING = "SPRING", 114 | SUMMER = "SUMMER", 115 | FALL = "FALL", 116 | WINTER = "WINTER", 117 | } 118 | 119 | export enum Cloud { 120 | ALTOSTRATUS = 'ALTOSTRATUS', 121 | CIRROCUMULUS = 'CIRROCUMULUS', 122 | CUMULONIMBUS = 'CUMULONIMBUS', 123 | } 124 | 125 | export enum Ordinal { 126 | FIRST = 1, 127 | SECOND, 128 | THIRD, 129 | } 130 | 131 | export type QuarkFlavor = "UP" | "DOWN" | "CHARM" | "STRANGE" | "TOP" | "BOTTOM"; 132 | 133 | export type UnionOfInterfaceTypes = Human | Droid | Starship; 134 | 135 | export type UnionOfEnumTypes = Color | Size; 136 | 137 | export type UnionOfInterfaceAndOtherTypes = Human | UnionOfEnumTypes; 138 | 139 | export type UnionOfEnumAndOtherTypes = Color | UnionOfInterfaceTypes; 140 | 141 | export type UnionOfNonReferenceTypes = boolean | string; 142 | 143 | export interface QueryRoot { 144 | unionOfInterfaceTypes():UnionOfInterfaceTypes[]; 145 | unionOfEnumTypes():UnionOfEnumTypes[]; 146 | unionOfInterfaceAndOtherTypes():UnionOfInterfaceAndOtherTypes[]; 147 | unionOfEnumAndOtherTypes():UnionOfEnumAndOtherTypes[]; 148 | unionOfNonReferenceTypes():UnionOfNonReferenceTypes[]; 149 | planetTypes():Planet; 150 | seasonTypes():Seasons; 151 | cloudTypes():Cloud; 152 | ordinalTypes():Ordinal; 153 | quarkFlavorTypes():QuarkFlavor; 154 | starshipFederated():StarshipFederated; 155 | starshipFederatedCompound():StarshipFederatedCompoundKey; 156 | starshipFederatedMultiple():StarshipFederatedMultipleKeys; 157 | extendingExternalEntity():ExtendingExternalEntity; 158 | costDecorationField():CostDecorationField; 159 | costDecorationMultipleFields():CostDecorationMultipleFields; 160 | costDecorationType():CostDecorationType; 161 | costDecorationFieldWithKey():CostDecorationFieldWithKey; 162 | costDecorationTypeWithKey():CostDecorationTypeWithKey; 163 | nonNullableProperties():NonNullableProperties; 164 | } 165 | 166 | export interface MutationRoot { 167 | } 168 | 169 | /** @graphql schema */ 170 | export interface Schema { 171 | query:QueryRoot; 172 | mutation:MutationRoot; 173 | } 174 | -------------------------------------------------------------------------------- /test/unit/Thing.ts: -------------------------------------------------------------------------------- 1 | describe(`TODO`, () => { 2 | 3 | it(`Need to write a test`, () => { 4 | expect(true).to.be.ok; 5 | }); 6 | 7 | }); 8 | -------------------------------------------------------------------------------- /test/unit/jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "automock": false, 3 | "collectCoverageFrom": [ 4 | "dist/src/**/*.js" 5 | ], 6 | "coverageDirectory": "output/test:unit", 7 | "coverageReporters": ["lcovonly", "text"], 8 | "coveragePathIgnorePatterns": [ 9 | "/node_modules/", 10 | "/dist/test/" 11 | ], 12 | "mapCoverage": true, 13 | "rootDir": "../..", 14 | "setupTestFrameworkScriptFile": "./dist/test/env/unit.js", 15 | "testMatch": ["/dist/test/unit/**/*.js"], 16 | "testResultsProcessor": "./node_modules/jest-junit" 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "charset": "utf-8", 4 | "declaration": true, 5 | "downlevelIteration": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "inlineSourceMap": true, 8 | "lib": [ 9 | "es2015", 10 | "scripthost" 11 | ], 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "newLine": "LF", 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "outDir": "./dist/", 21 | "pretty": true, 22 | "stripInternal": true, 23 | "strictNullChecks": true, 24 | "suppressImplicitAnyIndexErrors": true, 25 | "target": "es5" 26 | }, 27 | "include": [ 28 | "src/*", 29 | "test/**/*", 30 | "typings/**/*" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "tslint-no-unused-expression-chai" 4 | ], 5 | // https://palantir.github.io/tslint/rules/ 6 | "rules": { 7 | // TypeScript Specific 8 | 9 | // If the compiler is sure of the type of something, don't bother declaring 10 | // the type of it. 11 | "no-inferrable-types": true, 12 | 13 | // /// void; 3 | } 4 | -------------------------------------------------------------------------------- /typings/doctrine/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'doctrine' { 2 | export interface ParseOptions { 3 | unwrap?:boolean; 4 | tags?:string[]; 5 | recoverable?:boolean; 6 | sloppy?:boolean; 7 | lineNumbers?:boolean; 8 | } 9 | export interface ParseResult { 10 | description:string; 11 | tags:Tag[]; 12 | } 13 | export function parse(comment:string, options?:ParseOptions):ParseResult; 14 | 15 | export interface Tag { 16 | title:string; 17 | description:string; 18 | type:Type; 19 | name:string; 20 | } 21 | 22 | export interface Type { 23 | // We don't use this. 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /typings/global/test/base.d.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | 3 | declare global { 4 | const expect: typeof chai.expect; 5 | 6 | namespace NodeJS { 7 | export interface Global { 8 | expect: typeof chai.expect; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /typings/global/test/unit.d.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from 'sinon'; 2 | 3 | declare global { 4 | const sandbox: sinon.SinonSandbox; 5 | 6 | namespace NodeJS { 7 | export interface Global { 8 | sandbox: sinon.SinonSandbox; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /typings/jest/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // MODIFIED BY CONVOY: 3 | // 4 | // * Global `expect` was removed to make room for our global chai `expect`. 5 | // 6 | 7 | // Type definitions for Jest 20.0.5 8 | // Project: http://facebook.github.io/jest/ 9 | // Definitions by: Asana , Ivo Stratev , jwbay , Alexey Svetliakov , Alex Jover Morales , Allan Lukwago 10 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 11 | // TypeScript Version: 2.1 12 | 13 | declare var beforeAll: jest.Lifecycle; 14 | declare var beforeEach: jest.Lifecycle; 15 | declare var afterAll: jest.Lifecycle; 16 | declare var afterEach: jest.Lifecycle; 17 | declare var describe: jest.Describe; 18 | declare var fdescribe: jest.Describe; 19 | declare var xdescribe: jest.Describe; 20 | declare var it: jest.It; 21 | declare var fit: jest.It; 22 | declare var xit: jest.It; 23 | declare var test: jest.It; 24 | declare var xtest: jest.It; 25 | 26 | interface NodeRequire { 27 | /** Returns the actual module instead of a mock, bypassing all checks on whether the module should receive a mock implementation or not. */ 28 | requireActual(moduleName: string): any; 29 | /** Returns a mock module instead of the actual module, bypassing all checks on whether the module should be required normally or not. */ 30 | requireMock(moduleName: string): any; 31 | } 32 | 33 | declare namespace jest { 34 | /** Provides a way to add Jasmine-compatible matchers into your Jest context. */ 35 | function addMatchers(matchers: jasmine.CustomMatcherFactories): typeof jest; 36 | /** Disables automatic mocking in the module loader. */ 37 | function autoMockOff(): typeof jest; 38 | /** Enables automatic mocking in the module loader. */ 39 | function autoMockOn(): typeof jest; 40 | /** 41 | * @deprecated use resetAllMocks instead 42 | */ 43 | function clearAllMocks(): typeof jest; 44 | /** Clears the mock.calls and mock.instances properties of all mocks. Equivalent to calling .mockClear() on every mocked function. */ 45 | function resetAllMocks(): typeof jest; 46 | /** Removes any pending timers from the timer system. If any timers have been scheduled, they will be cleared and will never have the opportunity to execute in the future. */ 47 | function clearAllTimers(): typeof jest; 48 | /** Indicates that the module system should never return a mocked version of the specified module, including all of the specificied module's dependencies. */ 49 | function deepUnmock(moduleName: string): typeof jest; 50 | /** Disables automatic mocking in the module loader. */ 51 | function disableAutomock(): typeof jest; 52 | /** Mocks a module with an auto-mocked version when it is being required. */ 53 | function doMock(moduleName: string): typeof jest; 54 | /** Indicates that the module system should never return a mocked version of the specified module from require() (e.g. that it should always return the real module). */ 55 | function dontMock(moduleName: string): typeof jest; 56 | /** Enables automatic mocking in the module loader. */ 57 | function enableAutomock(): typeof jest; 58 | /** Creates a mock function. Optionally takes a mock implementation. */ 59 | function callback(implementation: (...args: any[]) => T): Mock; 60 | function callback(implementation?: Function): Mock; 61 | /** Generate a mock function. */ 62 | function fn(): Mock; 63 | /** Use the automatic mocking system to generate a mocked version of the given module. */ 64 | function genMockFromModule(moduleName: string): T; 65 | /** Returns whether the given function is a mock function. */ 66 | function isMockFunction(callback: any): callback is Mock; 67 | /** Mocks a module with an auto-mocked version when it is being required. */ 68 | function mock(moduleName: string, factory?: any, options?: MockOptions): typeof jest; 69 | /** Resets the module registry - the cache of all required modules. This is useful to isolate modules where local state might conflict between tests. */ 70 | function resetModuleRegistry(): typeof jest; 71 | /** Resets the module registry - the cache of all required modules. This is useful to isolate modules where local state might conflict between tests. */ 72 | function resetModules(): typeof jest; 73 | /** Exhausts tasks queued by setImmediate(). */ 74 | function runAllImmediates(): typeof jest; 75 | /** Exhausts the micro-task queue (usually interfaced in node via process.nextTick). */ 76 | function runAllTicks(): typeof jest; 77 | /** Exhausts the macro-task queue (i.e., all tasks queued by setTimeout() and setInterval()). */ 78 | function runAllTimers(): typeof jest; 79 | /** Executes only the macro-tasks that are currently pending (i.e., only the tasks that have been queued by setTimeout() or setInterval() up to this point). 80 | * If any of the currently pending macro-tasks schedule new macro-tasks, those new tasks will not be executed by this call. */ 81 | function runOnlyPendingTimers(): typeof jest; 82 | /** Executes only the macro task queue (i.e. all tasks queued by setTimeout() or setInterval() and setImmediate()). */ 83 | function runTimersToTime(msToRun: number): typeof jest; 84 | /** Explicitly supplies the mock object that the module system should return for the specified module. */ 85 | function setMock(moduleName: string, moduleExports: T): typeof jest; 86 | /** Creates a mock function similar to jest.callback but also tracks calls to object[methodName] */ 87 | function spyOn(object: T, method: M): SpyInstance; 88 | /** Indicates that the module system should never return a mocked version of the specified module from require() (e.g. that it should always return the real module). */ 89 | function unmock(moduleName: string): typeof jest; 90 | /** Instructs Jest to use fake versions of the standard timer functions. */ 91 | function useFakeTimers(): typeof jest; 92 | /** Instructs Jest to use the real versions of the standard timer functions. */ 93 | function useRealTimers(): typeof jest; 94 | 95 | interface MockOptions { 96 | virtual?: boolean; 97 | } 98 | 99 | interface EmptyFunction { 100 | (): void; 101 | } 102 | 103 | interface DoneCallback { 104 | (...args: any[]): any 105 | fail(error?: string | { message: string }): any; 106 | } 107 | 108 | interface ProvidesCallback { 109 | (callback: DoneCallback): any; 110 | } 111 | 112 | interface Lifecycle { 113 | (callback: ProvidesCallback): any; 114 | } 115 | 116 | /** Creates a test closure */ 117 | interface It { 118 | /** 119 | * Creates a test closure. 120 | */ 121 | (name: string, callback?: ProvidesCallback, timeout?: number): void; 122 | /** Only runs this test in the current file. */ 123 | only: It; 124 | skip: It; 125 | concurrent: It; 126 | } 127 | 128 | interface Describe { 129 | (name: string, callback: EmptyFunction): void 130 | only: Describe; 131 | skip: Describe; 132 | } 133 | 134 | interface MatcherUtils { 135 | readonly isNot: boolean; 136 | utils: { 137 | readonly EXPECTED_COLOR: string; 138 | readonly RECEIVED_COLOR: string; 139 | ensureActualIsNumber(actual: any, matcherName?: string): void; 140 | ensureExpectedIsNumber(actual: any, matcherName?: string): void; 141 | ensureNoExpected(actual: any, matcherName?: string): void; 142 | ensureNumbers(actual: any, expected: any, matcherName?: string): void; 143 | /** get the type of a value with handling of edge cases like `typeof []` and `typeof null` */ 144 | getType(value: any): string; 145 | matcherHint(matcherName: string, received?: string, expected?: string, options?: { secondArgument?: string, isDirectExpectCall?: boolean }): string; 146 | pluralize(word: string, count: number): string; 147 | printExpected(value: any): string; 148 | printReceived(value: any): string; 149 | printWithType(name: string, received: any, print: (value: any) => string): string; 150 | stringify(object: {}, maxDepth?: number): string; 151 | } 152 | } 153 | 154 | interface ExpectExtendMap { 155 | [key: string]: (this: MatcherUtils, received: any, actual: any) => { message: () => string, pass: boolean }; 156 | } 157 | 158 | interface SnapshotSerializerOptions { 159 | callToJSON?: boolean; 160 | edgeSpacing?: string; 161 | spacing?: string; 162 | escapeRegex?: boolean; 163 | highlight?: boolean; 164 | indent?: number; 165 | maxDepth?: number; 166 | min?: boolean; 167 | plugins?: Array 168 | printFunctionName?: boolean; 169 | theme?: SnapshotSerializerOptionsTheme; 170 | 171 | // see https://github.com/facebook/jest/blob/e56103cf142d2e87542ddfb6bd892bcee262c0e6/types/PrettyFormat.js 172 | } 173 | interface SnapshotSerializerOptionsTheme { 174 | comment?: string; 175 | content?: string; 176 | prop?: string; 177 | tag?: string; 178 | value?: string; 179 | } 180 | interface SnapshotSerializerColor { 181 | close: string; 182 | open: string; 183 | } 184 | interface SnapshotSerializerColors { 185 | comment: SnapshotSerializerColor; 186 | content: SnapshotSerializerColor; 187 | prop: SnapshotSerializerColor; 188 | tag: SnapshotSerializerColor; 189 | value: SnapshotSerializerColor; 190 | } 191 | interface SnapshotSerializerPlugin { 192 | print(val:any, serialize:((val:any) => string), indent:((str:string) => string), opts:SnapshotSerializerOptions, colors: SnapshotSerializerColors) : string; 193 | test(val:any) : boolean; 194 | } 195 | 196 | /** The `expect` function is used every time you want to test a value. You will rarely call `expect` by itself. */ 197 | interface Expect { 198 | /** 199 | * The `expect` function is used every time you want to test a value. You will rarely call `expect` by itself. 200 | */ 201 | (actual: any): Matchers; 202 | anything(): any; 203 | /** Matches anything that was created with the given constructor. You can use it inside `toEqual` or `toBeCalledWith` instead of a literal value. */ 204 | any(classType: any): any; 205 | /** Matches any array made up entirely of elements in the provided array. You can use it inside `toEqual` or `toBeCalledWith` instead of a literal value. */ 206 | arrayContaining(arr: any[]): any; 207 | /** Verifies that a certain number of assertions are called during a test. This is often useful when testing asynchronous code, in order to make sure that assertions in a callback actually got called. */ 208 | assertions(num: number): void; 209 | /** Verifies that at least one assertion is called during a test. This is often useful when testing asynchronous code, in order to make sure that assertions in a callback actually got called. */ 210 | hasAssertions(): void; 211 | /** You can use `expect.extend` to add your own matchers to Jest. */ 212 | extend(obj: ExpectExtendMap): void; 213 | /** Adds a module to format application-specific data structures for serialization. */ 214 | addSnapshotSerializer(serializer: SnapshotSerializerPlugin) : void; 215 | /** Matches any object that recursively matches the provided keys. This is often handy in conjunction with other asymmetric matchers. */ 216 | objectContaining(obj: {}): any; 217 | /** Matches any string that contains the exact provided string */ 218 | stringMatching(str: string | RegExp): any; 219 | } 220 | 221 | interface Matchers { 222 | /** If you know how to test something, `.not` lets you test its opposite. */ 223 | not: Matchers; 224 | /** Use resolves to unwrap the value of a fulfilled promise so any other matcher can be chained. If the promise is rejected the assertion fails. */ 225 | resolves: Matchers>; 226 | /** Unwraps the reason of a rejected promise so any other matcher can be chained. If the promise is fulfilled the assertion fails. */ 227 | rejects: Matchers>; 228 | lastCalledWith(...args: any[]): R; 229 | /** Checks that a value is what you expect. It uses `===` to check strict equality. Don't use `toBe` with floating-point numbers. */ 230 | toBe(expected: any): R; 231 | /** Ensures that a mock function is called. */ 232 | toBeCalled(): R; 233 | /** Ensure that a mock function is called with specific arguments. */ 234 | toBeCalledWith(...args: any[]): R; 235 | /** Using exact equality with floating point numbers is a bad idea. Rounding means that intuitive things fail. */ 236 | toBeCloseTo(expected: number, delta?: number): R; 237 | /** Ensure that a variable is not undefined. */ 238 | toBeDefined(): R; 239 | /** When you don't care what a value is, you just want to ensure a value is false in a boolean context. */ 240 | toBeFalsy(): R; 241 | /** For comparing floating point numbers. */ 242 | toBeGreaterThan(expected: number): R; 243 | /** For comparing floating point numbers. */ 244 | toBeGreaterThanOrEqual(expected: number): R; 245 | /** Ensure that an object is an instance of a class. This matcher uses `instanceof` underneath. */ 246 | toBeInstanceOf(expected: any): R 247 | /** For comparing floating point numbers. */ 248 | toBeLessThan(expected: number): R; 249 | /** For comparing floating point numbers. */ 250 | toBeLessThanOrEqual(expected: number): R; 251 | /** This is the same as `.toBe(null)` but the error messages are a bit nicer. So use `.toBeNull()` when you want to check that something is null. */ 252 | toBeNull(): R; 253 | /** Use when you don't care what a value is, you just want to ensure a value is true in a boolean context. In JavaScript, there are six falsy values: `false`, `0`, `''`, `null`, `undefined`, and `NaN`. Everything else is truthy. */ 254 | toBeTruthy(): R; 255 | /** Used to check that a variable is undefined. */ 256 | toBeUndefined(): R; 257 | /** Used to check that a variable is NaN. */ 258 | toBeNaN(): R; 259 | /** Used when you want to check that an item is in a list. For testing the items in the list, this uses `===`, a strict equality check. */ 260 | toContain(expected: any): R; 261 | /** Used when you want to check that an item is in a list. For testing the items in the list, this matcher recursively checks the equality of all fields, rather than checking for object identity. */ 262 | toContainEqual(expected: any): R; 263 | /** Used when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity. */ 264 | toEqual(expected: any): R; 265 | /** Ensures that a mock function is called. */ 266 | toHaveBeenCalled(): R; 267 | /** Ensures that a mock function is called an exact number of times. */ 268 | toHaveBeenCalledTimes(expected: number): R; 269 | /** Ensure that a mock function is called with specific arguments. */ 270 | toHaveBeenCalledWith(...params: any[]): R; 271 | /** If you have a mock function, you can use `.toHaveBeenLastCalledWith` to test what arguments it was last called with. */ 272 | toHaveBeenLastCalledWith(...params: any[]): R; 273 | /** Used to check that an object has a `.length` property and it is set to a certain numeric value. */ 274 | toHaveLength(expected: number): R; 275 | toHaveProperty(propertyPath: string, value?: any): R; 276 | /** Check that a string matches a regular expression. */ 277 | toMatch(expected: string | RegExp): R; 278 | /** Used to check that a JavaScript object matches a subset of the properties of an objec */ 279 | toMatchObject(expected: {}): R; 280 | /** This ensures that a value matches the most recent snapshot. Check out [the Snapshot Testing guide](http://facebook.github.io/jest/docs/snapshot-testing.html) for more information. */ 281 | toMatchSnapshot(snapshotName?: string): R; 282 | /** Used to test that a function throws when it is called. */ 283 | toThrow(error?: string | Constructable | RegExp): R; 284 | /** If you want to test that a specific error is thrown inside a function. */ 285 | toThrowError(error?: string | Constructable | RegExp): R; 286 | /** Used to test that a function throws a error matching the most recent snapshot when it is called. */ 287 | toThrowErrorMatchingSnapshot(): R; 288 | } 289 | 290 | interface Constructable { 291 | new (...args: any[]): any 292 | } 293 | 294 | interface Mock extends Function, MockInstance { 295 | new (): T; 296 | (...args: any[]): any; 297 | } 298 | 299 | interface SpyInstance extends MockInstance { 300 | mockRestore(): void; 301 | } 302 | 303 | /** 304 | * Wrap module with mock definitions 305 | * @example 306 | * jest.mock("../api"); 307 | * import { Api } from "../api"; 308 | * 309 | * const myApi: jest.Mocked = new Api() as any; 310 | * myApi.myApiMethod.mockImplementation(() => "test"); 311 | */ 312 | type Mocked = { 313 | [P in keyof T]: T[P] & MockInstance; 314 | } & T; 315 | 316 | interface MockInstance { 317 | mock: MockContext; 318 | mockClear(): void; 319 | mockReset(): void; 320 | mockImplementation(callback: Function): Mock; 321 | mockImplementationOnce(callback: Function): Mock; 322 | mockReturnThis(): Mock; 323 | mockReturnValue(value: any): Mock; 324 | mockReturnValueOnce(value: any): Mock; 325 | } 326 | 327 | interface MockContext { 328 | calls: any[][]; 329 | instances: T[]; 330 | } 331 | } 332 | 333 | //Jest ships with a copy of Jasmine. They monkey-patch its APIs and divergence/deprecation are expected. 334 | //Relevant parts of Jasmine's API are below so they can be changed and removed over time. 335 | //This file can't reference jasmine.d.ts since the globals aren't compatible. 336 | 337 | declare function spyOn(object: any, method: string): jasmine.Spy; 338 | /** If you call the function pending anywhere in the spec body, no matter the expectations, the spec will be marked pending. */ 339 | declare function pending(reason?: string): void; 340 | /** Fails a test when called within one. */ 341 | declare function fail(error?: any): void; 342 | declare namespace jasmine { 343 | var DEFAULT_TIMEOUT_INTERVAL: number; 344 | var clock: () => Clock; 345 | function any(aclass: any): Any; 346 | function anything(): Any; 347 | function arrayContaining(sample: any[]): ArrayContaining; 348 | function objectContaining(sample: any): ObjectContaining; 349 | function createSpy(name: string, originalFn?: Function): Spy; 350 | function createSpyObj(baseName: string, methodNames: any[]): any; 351 | function createSpyObj(baseName: string, methodNames: any[]): T; 352 | function pp(value: any): string; 353 | function addCustomEqualityTester(equalityTester: CustomEqualityTester): void; 354 | function addMatchers(matchers: CustomMatcherFactories): void; 355 | function stringMatching(value: string | RegExp): Any; 356 | 357 | interface Clock { 358 | install(): void; 359 | uninstall(): void; 360 | /** Calls to any registered callback are triggered when the clock is ticked forward via the jasmine.clock().tick function, which takes a number of milliseconds. */ 361 | tick(ms: number): void; 362 | mockDate(date?: Date): void; 363 | } 364 | 365 | interface Any { 366 | new (expectedClass: any): any; 367 | jasmineMatches(other: any): boolean; 368 | jasmineToString(): string; 369 | } 370 | 371 | interface ArrayContaining { 372 | new (sample: any[]): any; 373 | asymmetricMatch(other: any): boolean; 374 | jasmineToString(): string; 375 | } 376 | 377 | interface ObjectContaining { 378 | new (sample: any): any; 379 | jasmineMatches(other: any, mismatchKeys: any[], mismatchValues: any[]): boolean; 380 | jasmineToString(): string; 381 | } 382 | 383 | interface Spy { 384 | (...params: any[]): any; 385 | identity: string; 386 | and: SpyAnd; 387 | calls: Calls; 388 | mostRecentCall: { args: any[]; }; 389 | argsForCall: any[]; 390 | wasCalled: boolean; 391 | } 392 | 393 | interface SpyAnd { 394 | /** By chaining the spy with and.callThrough, the spy will still track all calls to it but in addition it will delegate to the actual implementation. */ 395 | callThrough(): Spy; 396 | /** By chaining the spy with and.returnValue, all calls to the function will return a specific value. */ 397 | returnValue(val: any): Spy; 398 | /** By chaining the spy with and.returnValues, all calls to the function will return specific values in order until it reaches the end of the return values list. */ 399 | returnValues(...values: any[]): Spy; 400 | /** By chaining the spy with and.callFake, all calls to the spy will delegate to the supplied function. */ 401 | callFake(callback: Function): Spy; 402 | /** By chaining the spy with and.throwError, all calls to the spy will throw the specified value. */ 403 | throwError(msg: string): Spy; 404 | /** When a calling strategy is used for a spy, the original stubbing behavior can be returned at any time with and.stub. */ 405 | stub(): Spy; 406 | } 407 | 408 | interface Calls { 409 | /** By chaining the spy with calls.any(), will return false if the spy has not been called at all, and then true once at least one call happens. */ 410 | any(): boolean; 411 | /** By chaining the spy with calls.count(), will return the number of times the spy was called */ 412 | count(): number; 413 | /** By chaining the spy with calls.argsFor(), will return the arguments passed to call number index */ 414 | argsFor(index: number): any[]; 415 | /** By chaining the spy with calls.allArgs(), will return the arguments to all calls */ 416 | allArgs(): any[]; 417 | /** By chaining the spy with calls.all(), will return the context (the this) and arguments passed all calls */ 418 | all(): CallInfo[]; 419 | /** By chaining the spy with calls.mostRecent(), will return the context (the this) and arguments for the most recent call */ 420 | mostRecent(): CallInfo; 421 | /** By chaining the spy with calls.first(), will return the context (the this) and arguments for the first call */ 422 | first(): CallInfo; 423 | /** By chaining the spy with calls.reset(), will clears all tracking for a spy */ 424 | reset(): void; 425 | } 426 | 427 | interface CallInfo { 428 | /** The context (the this) for the call */ 429 | object: any; 430 | /** All arguments passed to the call */ 431 | args: any[]; 432 | /** The return value of the call */ 433 | returnValue: any; 434 | } 435 | 436 | interface CustomMatcherFactories { 437 | [index: string]: CustomMatcherFactory; 438 | } 439 | 440 | interface CustomMatcherFactory { 441 | (util: MatchersUtil, customEqualityTesters: Array): CustomMatcher; 442 | } 443 | 444 | interface MatchersUtil { 445 | equals(a: any, b: any, customTesters?: Array): boolean; 446 | contains(haystack: ArrayLike | string, needle: any, customTesters?: Array): boolean; 447 | buildFailureMessage(matcherName: string, isNot: boolean, actual: any, ...expected: Array): string; 448 | } 449 | 450 | interface CustomEqualityTester { 451 | (first: any, second: any): boolean; 452 | } 453 | 454 | interface CustomMatcher { 455 | compare(actual: T, expected: T): CustomMatcherResult; 456 | compare(actual: any, expected: any): CustomMatcherResult; 457 | } 458 | 459 | interface CustomMatcherResult { 460 | pass: boolean; 461 | message: string | (() => string); 462 | } 463 | 464 | interface ArrayLike { 465 | length: number; 466 | [n: number]: T; 467 | } 468 | } 469 | --------------------------------------------------------------------------------