From f8cd7e4eca8fbb5d6740dc712bf0aa71253ba04a Mon Sep 17 00:00:00 2001 From: stephentuso Date: Wed, 28 Nov 2018 00:43:58 -0500 Subject: [PATCH] feat: allow use of enums with any values fix: nullable/wrapper issues --- package.json | 2 + src/__tests__/typeHelpers.test.ts | 120 ++++++++++++++++++ .../__tests__/getFieldConfigMap.test.ts | 27 ++++ src/builders/getFieldConfigMap.ts | 19 ++- src/decorators/__tests__/types.test.ts | 31 +---- src/typeHelpers.ts | 85 +++++-------- src/wrappers/TSGraphQLEnumType.ts | 40 +++++- src/wrappers/Wrapper.ts | 16 +-- .../__tests__/TSGraphQLEnumType.test.ts | 46 +++++++ src/wrappers/__tests__/nullable.test.ts | 19 +++ src/wrappers/list.ts | 7 +- src/wrappers/nullable.ts | 17 +-- tsconfig.json | 3 +- typeChecking.ts | 30 +++++ yarn.lock | 55 ++++++++ 15 files changed, 401 insertions(+), 116 deletions(-) create mode 100644 src/__tests__/typeHelpers.test.ts create mode 100644 src/wrappers/__tests__/TSGraphQLEnumType.test.ts create mode 100644 src/wrappers/__tests__/nullable.test.ts create mode 100644 typeChecking.ts diff --git a/package.json b/package.json index 5f5e95b..c8191eb 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,9 @@ "typescript": "^3.1.6" }, "dependencies": { + "constant-case": "^2.0.0", "lodash": "^4.17.11", + "pascal-case": "^2.0.1", "reflect-metadata": "^0.1.12" }, "peerDependencies": { diff --git a/src/__tests__/typeHelpers.test.ts b/src/__tests__/typeHelpers.test.ts new file mode 100644 index 0000000..c089c30 --- /dev/null +++ b/src/__tests__/typeHelpers.test.ts @@ -0,0 +1,120 @@ +import 'jest'; +import ObjectType from '../decorators/ObjectType'; +import Field from '../decorators/Field'; +import InputObjectType from '../decorators/InputObjectType'; +import InputField from '../decorators/InputField'; +import nullable from '../wrappers/nullable'; +import { getInputType, getNamedType, getOutputType, TSGraphQLString } from '..'; +import InterfaceType from '../decorators/InterfaceType'; +import { + isEnumType, + isInputObjectType, isInterfaceType, + isListType, + isNonNullType, + isObjectType, + isScalarType, + isUnionType, +} from 'graphql'; +import { getType } from '../typeHelpers'; +import list from '../wrappers/list'; +import TSGraphQLEnumType from '../wrappers/TSGraphQLEnumType'; +import TSGraphQLUnionType from '../wrappers/TSGraphQLUnionType'; + +@ObjectType() +class AnObjectType { + @Field() + foo!: string; +} + +@ObjectType() +class AnotherObjectType { + @Field() + bar!: string; +} + +@InputObjectType() +class AnInputObjectType { + @InputField() + foo!: string; +} + +@InterfaceType() +class AnInterfaceType { + @Field() + foo!: string; +} + +enum AnEnum { + Foo, +} + +const AnEnumType = new TSGraphQLEnumType(AnEnum, { name: 'AnEnumType' }) + +const AUnionType = new TSGraphQLUnionType({ + name: 'AUnionType', + types: [AnObjectType, AnotherObjectType] +}) + +const ANullableType = nullable(TSGraphQLString); +const AListType = list(TSGraphQLString); + +describe('typeHelpers', () => { + describe('getType', () => { + it('should never return GraphQLNonNull if nonNull is false', () => { + expect(isNonNullType(getType(AnObjectType))).toBeFalsy(); + expect(isNonNullType(getType(AnInputObjectType))).toBeFalsy(); + expect(isNonNullType(getType(AnInterfaceType))).toBeFalsy(); + expect(isNonNullType(getType(ANullableType))).toBeFalsy(); + expect(isNonNullType(getType(TSGraphQLString))).toBeFalsy(); + }); + + it('should return GraphQLNonNull if nonNull true and type not nullable', () => { + expect(isNonNullType(getType(ANullableType, true))).toBeFalsy(); + + expect(isNonNullType(getType(AnObjectType, true))).toBeTruthy(); + expect(isNonNullType(getType(AnInputObjectType, true))).toBeTruthy(); + expect(isNonNullType(getType(AnInterfaceType, true))).toBeTruthy(); + expect(isNonNullType(getType(TSGraphQLString, true))).toBeTruthy(); + }); + }); + + describe('getInputType', () => { + it('should correctly return input types', () => { + expect(isEnumType(getInputType(AnEnumType))).toBeTruthy(); + expect(isScalarType(getInputType(TSGraphQLString))).toBeTruthy(); + expect(isInputObjectType(getInputType(AnInputObjectType))).toBeTruthy(); + expect(isListType(getInputType(AListType))).toBeTruthy(); + expect(isNonNullType(getInputType(TSGraphQLString, true))).toBeTruthy(); + }); + + it('should throw if not an input type', () => { + expect(() => getInputType(AnObjectType)).toThrow(); + }); + }); + + describe('getOutputType', () => { + it('should correctly return output types', () => { + expect(isEnumType(getOutputType(AnEnumType))).toBeTruthy(); + expect(isScalarType(getOutputType(TSGraphQLString))).toBeTruthy(); + expect(isObjectType(getOutputType(AnObjectType))).toBeTruthy(); + expect(isListType(getOutputType(AListType))).toBeTruthy(); + expect(isInterfaceType(getOutputType(AnInterfaceType))).toBeTruthy(); + expect(isNonNullType(getOutputType(TSGraphQLString, true))).toBeTruthy(); + }); + + it('should throw if not an output type', () => { + expect(() => getOutputType(AnInputObjectType)).toThrow(); + }); + }); + + describe('getNamedType', () => { + it('should correctly return named types', () => { + expect(isEnumType(getNamedType(AnEnumType))).toBeTruthy(); + expect(isScalarType(getNamedType(TSGraphQLString))).toBeTruthy(); + expect(isObjectType(getNamedType(AnObjectType))).toBeTruthy(); + expect(isInputObjectType(getNamedType(AnInputObjectType))).toBeTruthy(); + expect(isUnionType(getNamedType(AUnionType))).toBeTruthy(); + expect(isInterfaceType(getNamedType(AnInterfaceType))).toBeTruthy(); + }); + }); +}); diff --git a/src/builders/__tests__/getFieldConfigMap.test.ts b/src/builders/__tests__/getFieldConfigMap.test.ts index 2c6227c..71b3064 100644 --- a/src/builders/__tests__/getFieldConfigMap.test.ts +++ b/src/builders/__tests__/getFieldConfigMap.test.ts @@ -8,6 +8,12 @@ import { fields } from '../../fields'; import { ObjectType, TSGraphQLInt, TSGraphQLString } from '../../index'; import Args from '../../decorators/Args'; import Arg from '../../decorators/Arg'; +import list from '../../wrappers/list'; +import nullable from '../../wrappers/nullable'; +import TSGraphQLEnumType, { TSGraphQLEnumCase } from '../../wrappers/TSGraphQLEnumType'; +import { Maybe } from '../../types'; +import { Wrapper } from '../../wrappers/Wrapper'; +import { GraphQLEnumType } from 'graphql'; class Simple { @Field() @@ -185,4 +191,25 @@ describe('getFieldConfigMap', () => { config.configTest.resolve!(new ArgsTest(), { foo: 'test' }, null, null as any); config.methodTest.resolve!(new ArgsTest(), { foo: 'test' }, null, null as any); }); + + it('should correctly run wrapper transformers', () => { + enum AnEnum { + Foo, + Bar, + } + + const AnEnumType = new TSGraphQLEnumType(AnEnum, { name: 'AnEnum', changeCase: TSGraphQLEnumCase.Constant }); + + @ObjectType() + class Foo { + @Field({ type: list(nullable(AnEnumType)) }) + foo() { + return [AnEnum.Foo, AnEnum.Bar, null]; + } + } + + const config = resolveThunk(getFieldConfigMap(Foo)); + expect(config).toHaveProperty('foo'); + expect(config.foo!.resolve!(null, null as any, null, null as any)).toEqual(['FOO', 'BAR', null]); + }); }); diff --git a/src/builders/getFieldConfigMap.ts b/src/builders/getFieldConfigMap.ts index 79bf86a..8572ed6 100644 --- a/src/builders/getFieldConfigMap.ts +++ b/src/builders/getFieldConfigMap.ts @@ -1,6 +1,6 @@ import { AnyConstructor, EmptyConstructor, ObjectLiteral } from '../types'; import { resolveThunk, Thunk } from '../utils/thunk'; -import { GraphQLFieldConfigMap } from 'graphql'; +import { GraphQLFieldConfigMap, GraphQLFieldResolver } from 'graphql'; import { FieldConfig, FieldConfigMap, FieldResolver } from '../fields'; import { getFieldConfig, getImplements, getSavedFieldConfigMap } from '../metadata'; import { FieldResolverMethod } from '../decorators/Field'; @@ -8,7 +8,7 @@ import { getConstructorChain } from './utils'; import { mapValues } from 'lodash'; import getArgs from './getArgs'; import { getOutputType } from '../typeHelpers'; -import { flow } from 'lodash/fp'; +import { isWrapper } from '../wrappers/Wrapper'; const convertResolverMethod = (prototype: ObjectLiteral, key: string, Args?: EmptyConstructor): FieldResolver | null => { if (typeof prototype[key] === 'function') { @@ -24,12 +24,17 @@ const convertResolverMethod = (prototype: ObjectLiteral, key: string, Args?: Emp const wrapResolver = ( config: FieldConfig ): FieldConfig => { - const { resolve, args: Args } = config; - if (resolve && Args) { + const { resolve, type, args: Args } = config; + const transformOutput = isWrapper(type) && type.transformOutput; + if (resolve) { + const wrapped: GraphQLFieldResolver = Args + ? (source: any, args: ObjectLiteral, ...rest) => resolve(source, Object.assign(new Args(), args), ...rest) + : resolve; return { ...config, - resolve: (source: any, args: ObjectLiteral, ...rest) => - resolve(source, Object.assign(new Args(), args), ...rest), + resolve: transformOutput + ? (...args) => transformOutput(wrapped(...args)) + : wrapped, } } return config; @@ -72,7 +77,7 @@ export default (source: AnyConstructor): Thunk, FieldConfig>({ ...allMaps, ...allFields, - }, flow(addDefaultResolver, wrapResolver)); + }, (value, key) => wrapResolver(addDefaultResolver(value, key))); return buildFieldConfigMap(merged); }; diff --git a/src/decorators/__tests__/types.test.ts b/src/decorators/__tests__/types.test.ts index 2ab235e..d672cca 100644 --- a/src/decorators/__tests__/types.test.ts +++ b/src/decorators/__tests__/types.test.ts @@ -1,39 +1,14 @@ import 'jest'; -import { exec } from 'child_process'; -import { readdir } from 'fs'; -import pify from 'pify'; -import path from 'path'; - -// Would be better to do this programmatically, didn't look straightforward though. -const typeCheckFiles = async (files: string[], expectError?: boolean) => { - await Promise.all(files.map(file => new Promise((resolve, reject) => { - const tscArgs = '--strict --experimentalDecorators --emitDecoratorMetadata --noEmit --lib esnext'; - exec(`$(npm bin)/tsc ${tscArgs} ${file}`, (err) => { - if ((err && expectError) || (!err && !expectError)) { - resolve(); - } else { - reject(new Error(expectError - ? 'Types valid, expected invalid' - : 'Types invalid, expected valid', - )); - } - }); - }))); -}; - -const getFiles = async (directory: string): Promise => { - const files = await pify(readdir)(path.join(__dirname, directory)); - return files.map((file: string) => path.join(__dirname, directory, file)); -} +import { getFilesInDir, typeCheckFiles } from '../../../typeChecking'; describe('Decorator type validation', () => { it('passes for valid files', async () => { - const validFiles = await getFiles('valid'); + const validFiles = await getFilesInDir(__dirname, 'valid'); await typeCheckFiles(validFiles); }, 30000); it('fails for invalid files', async () => { - const invalidFiles = await getFiles('invalid'); + const invalidFiles = await getFilesInDir(__dirname, 'invalid'); await typeCheckFiles(invalidFiles, true); }, 30000); }); diff --git a/src/typeHelpers.ts b/src/typeHelpers.ts index c94c6f0..3b2a3dd 100644 --- a/src/typeHelpers.ts +++ b/src/typeHelpers.ts @@ -4,40 +4,23 @@ import { GraphQLNamedType, GraphQLNonNull, GraphQLOutputType, - GraphQLType, + GraphQLType, isInputType, isNamedType, isOutputType, } from 'graphql'; import { isInputObjectType, isInterfaceType, isObjectType } from './metadata'; import getInputObjectType from './builders/getInputObjectType'; import getObjectType from './builders/getObjectType'; import getInterfaceType from './builders/getInterfaceType'; - -export const getNamedType = (target: WrapperOrType): GraphQLNamedType => { +import { Constructor } from './types'; + +export function getType(target: WrapperOrType, nonNull?: false): GraphQLType; +export function getType(target: WrapperOrType, nonNull: true): GraphQLNonNull; +export function getType(target: WrapperOrType, nonNull?: boolean): GraphQLType | GraphQLNonNull; +export function getType( + target: WrapperOrType, + nonNull?: boolean, +): GraphQLType | GraphQLNonNull { if (isWrapper(target)) { - return resolveWrapper(target, true); - } - - if (isInputObjectType(target)) { - return getInputObjectType(target); - } - - if (isObjectType(target)) { - return getObjectType(target); - } - - if (isInterfaceType(target)) { - return getInterfaceType(target); - } - - throw new Error(`Named type not found for ${target.name}`); -} - -export const getNamedTypes = (targets: Array>): GraphQLNamedType[] => { - return targets.map(getNamedType); -} - -export function getType(target: WrapperOrType, nonNull?: boolean): GraphQLType { - if (isWrapper(target)) { - return resolveWrapper(target); + return resolveWrapper(target, nonNull); } let type; @@ -60,40 +43,30 @@ export function getType(target: WrapperOrType, nonNull?: boole throw new Error(`Type not found for ${target.name}`); } -export const getOutputType = (target: WrapperOrType, nonNull?: boolean): GraphQLOutputType => { - if (isWrapper(target)) { - return resolveWrapper(target); - } - - let type; - if (isObjectType(target)) { - type = getObjectType(target); +export const getNamedType = (target: WrapperOrType): GraphQLNamedType => { + const type = getType(target); + if (!type || !isNamedType(type)) { + throw new Error(`Named type not found for ${(target as Constructor).name}`); } + return type; +} - if (isInterfaceType(target)) { - type = getInterfaceType(target); - } +export const getNamedTypes = (targets: Array>): GraphQLNamedType[] => { + return targets.map(getNamedType); +} - if (type) { - return nonNull ? new GraphQLNonNull(type) : type; +export const getOutputType = (target: WrapperOrType, nonNull?: boolean): GraphQLOutputType => { + const type = getType(target, nonNull); + if (!type || !isOutputType(type)) { + throw new Error(`Output type not found for ${(target as Constructor).name}`); } - - throw new Error(`Output type not found for ${target.name}`); + return type; } export const getInputType = (target: WrapperOrType, nonNull?: boolean): GraphQLInputType => { - if (isWrapper(target)) { - return resolveWrapper(target); - } - - let type; - if (isInputObjectType(target)) { - type = getInputObjectType(target); - } - - if (type) { - return nonNull ? new GraphQLNonNull(type) : type; + const type = getType(target, nonNull); + if (!type || !isInputType(type)) { + throw new Error(`Input type not found for ${(target as Constructor).name}`); } - - throw new Error(`Input type not found for ${target.name}`); + return type; } diff --git a/src/wrappers/TSGraphQLEnumType.ts b/src/wrappers/TSGraphQLEnumType.ts index d768a2b..1385951 100644 --- a/src/wrappers/TSGraphQLEnumType.ts +++ b/src/wrappers/TSGraphQLEnumType.ts @@ -1,21 +1,49 @@ import { Wrapper } from './Wrapper'; -import { GraphQLEnumType, GraphQLNonNull, GraphQLType } from 'graphql'; +import { GraphQLEnumType } from 'graphql'; import { mapValues } from 'lodash'; +import pascalCase from 'pascal-case'; +import constantCase from 'constant-case'; + +export enum TSGraphQLEnumCase { + Pascal, + Constant, +} export type TSGraphQLEnumTypeConfig = { - name: string; + name: string, + changeCase?: TSGraphQLEnumCase, } -export default class TSGraphQLEnumType implements Wrapper { +const performChangeCase = (type: TSGraphQLEnumCase, value: string): string => { + switch (type) { + case TSGraphQLEnumCase.Constant: + return constantCase(value); + case TSGraphQLEnumCase.Pascal: + return pascalCase(value); + } +}; + +export default class TSGraphQLEnumType implements Wrapper { graphQLType: GraphQLEnumType; - type: K; + type: TEnum; + private readonly valueToOutput: Record; constructor(enumType: Record, config: TSGraphQLEnumTypeConfig) { + const { changeCase } = config; + this.valueToOutput = Object.keys(enumType).reduce((map, key) => ({ + ...map as any, + [enumType[key as K]]: changeCase ? performChangeCase(changeCase, key) : key, + }), {} as Record); + this.graphQLType = new GraphQLEnumType({ ...config, values: mapValues(enumType, (value) => ({ value, - })), + })) }); - this.type = null as any as K; + this.type = null as any as TEnum; + } + + transformOutput(value: TEnum) { + return this.valueToOutput[value] as any; } } diff --git a/src/wrappers/Wrapper.ts b/src/wrappers/Wrapper.ts index 6d159c8..ae9ddc4 100644 --- a/src/wrappers/Wrapper.ts +++ b/src/wrappers/Wrapper.ts @@ -5,12 +5,12 @@ import { GraphQLScalarType, GraphQLScalarValueParser, GraphQLType, - isNonNullType, } from 'graphql'; export type Wrapper = { graphQLType: G, type: T, + transformOutput?: (output: T) => T; nullable?: boolean, }; @@ -25,14 +25,14 @@ export const isWrapper = (x: WrapperOrType): x i return 'graphQLType' in x && 'type' in x; }; -export function resolveWrapper(wrapper: Wrapper, dontWrap: true): G; -export function resolveWrapper(wrapper: Wrapper, dontWrap: false): G | GraphQLNonNull; -export function resolveWrapper(wrapper: Wrapper): G | GraphQLNonNull; -export function resolveWrapper(wrapper: Wrapper, dontWrap?: boolean): G | GraphQLNonNull { +export function resolveWrapper(wrapper: Wrapper): G; +export function resolveWrapper(wrapper: Wrapper, nonNull: false): G; +export function resolveWrapper(wrapper: Wrapper, nonNull?: boolean): G | GraphQLNonNull; +export function resolveWrapper(wrapper: Wrapper, nonNull?: boolean): G | GraphQLNonNull { const type = wrapper.graphQLType; - return (wrapper.nullable || dontWrap) - ? type - : new GraphQLNonNull(type); + return (nonNull && !wrapper.nullable) + ? new GraphQLNonNull(type) + : type; } export const wrapScalar = (scalar: TypedGraphQLScalar): Wrapper => { diff --git a/src/wrappers/__tests__/TSGraphQLEnumType.test.ts b/src/wrappers/__tests__/TSGraphQLEnumType.test.ts new file mode 100644 index 0000000..b15cbb0 --- /dev/null +++ b/src/wrappers/__tests__/TSGraphQLEnumType.test.ts @@ -0,0 +1,46 @@ +import 'jest'; +import TSGraphQLEnumType, { TSGraphQLEnumCase } from '../TSGraphQLEnumType'; + +enum IntEnum { + Foo, + Bar, + FooBar, +} + +enum StringEnum { + Foo = 'foo', + Bar = 'bar', + FooBar = 'foobar', +} + +describe('TSGraphQLEnumType', () => { + describe('#transformOutput', () => { + it('should output correct value for number enum, no case change', () => { + const AnEnum = new TSGraphQLEnumType(IntEnum, { name: 'AnEnum' }); + expect(AnEnum.transformOutput(IntEnum.Foo)).toEqual('Foo'); + expect(AnEnum.transformOutput(IntEnum.Bar)).toEqual('Bar'); + expect(AnEnum.transformOutput(IntEnum.FooBar)).toEqual('FooBar'); + }); + + it('should output correct value for number enum with case change', () => { + const AnEnum = new TSGraphQLEnumType(IntEnum, { name: 'AnEnum', changeCase: TSGraphQLEnumCase.Constant }); + expect(AnEnum.transformOutput(IntEnum.Foo)).toEqual('FOO'); + expect(AnEnum.transformOutput(IntEnum.Bar)).toEqual('BAR'); + expect(AnEnum.transformOutput(IntEnum.FooBar)).toEqual('FOO_BAR'); + }); + + it('should output correct value for string enum, no case change', () => { + const AnEnum = new TSGraphQLEnumType(StringEnum, { name: 'AnEnum' }); + expect(AnEnum.transformOutput(StringEnum.Foo)).toEqual('Foo'); + expect(AnEnum.transformOutput(StringEnum.Bar)).toEqual('Bar'); + expect(AnEnum.transformOutput(StringEnum.FooBar)).toEqual('FooBar'); + }); + + it('should output correct value for string enum with case change', () => { + const AnEnum = new TSGraphQLEnumType(StringEnum, { name: 'AnEnum', changeCase: TSGraphQLEnumCase.Constant }); + expect(AnEnum.transformOutput(StringEnum.Foo)).toEqual('FOO'); + expect(AnEnum.transformOutput(StringEnum.Bar)).toEqual('BAR'); + expect(AnEnum.transformOutput(StringEnum.FooBar)).toEqual('FOO_BAR'); + }); + }); +}); diff --git a/src/wrappers/__tests__/nullable.test.ts b/src/wrappers/__tests__/nullable.test.ts new file mode 100644 index 0000000..e78805d --- /dev/null +++ b/src/wrappers/__tests__/nullable.test.ts @@ -0,0 +1,19 @@ +import 'jest'; +import { Wrapper } from '../Wrapper'; +import { GraphQLScalarType, GraphQLString } from 'graphql'; +import nullable from '../nullable'; + +const output = 'OUTPUT'; + +const wrapper: Wrapper = { + graphQLType: GraphQLString, + type: '', + transformOutput: () => output, +}; + +describe('nullable', () => { + it('should call transform output for non null falsy values', () => { + const wrapped = nullable(wrapper); + expect(wrapped.transformOutput!('')).toEqual(output); + }); +}); diff --git a/src/wrappers/list.ts b/src/wrappers/list.ts index c1866e3..8f8c3c5 100644 --- a/src/wrappers/list.ts +++ b/src/wrappers/list.ts @@ -1,12 +1,15 @@ -import { Wrapper, WrapperOrType } from './Wrapper'; +import { isWrapper, Wrapper, WrapperOrType } from './Wrapper'; import { GraphQLList } from 'graphql'; import { getType } from '../typeHelpers'; -// See note re. any in nullable.ts export default function list(type: WrapperOrType): Wrapper> { const currentType = getType(type, true); + const transformOutput = isWrapper(type) && type.transformOutput; return { graphQLType: new GraphQLList(currentType), + transformOutput: transformOutput + ? (values: T[]) => values.map(transformOutput.bind(type)) + : undefined, type: [], } } diff --git a/src/wrappers/nullable.ts b/src/wrappers/nullable.ts index 4eb9d3c..9a0560c 100644 --- a/src/wrappers/nullable.ts +++ b/src/wrappers/nullable.ts @@ -1,17 +1,18 @@ -import { Wrapper, WrapperOrType } from './Wrapper'; +import { isWrapper, Wrapper, WrapperOrType } from './Wrapper'; import { getType } from '../typeHelpers'; -import { GraphQLNonNull, GraphQLNullableType } from 'graphql'; +import { GraphQLNullableType } from 'graphql'; import { Maybe } from '../types'; export default function nullable( type: WrapperOrType, -): Wrapper, Q | GraphQLNonNull> { - const currentType = getType(type, true); +): Wrapper, Q> { + const currentType = getType(type) as Q; + const transformOutput = isWrapper(type) && type.transformOutput; return { - // Can't find way around using any here :( - // Will at least fail fast, throws runtime error immediately if - // input type used for output and vice versa - graphQLType: currentType as any, + graphQLType: currentType, + transformOutput: transformOutput + ? (output: Maybe) => output != null ? transformOutput.bind(type)(output) : output + : undefined, type: null, nullable: true, }; diff --git a/tsconfig.json b/tsconfig.json index 22a6a0c..ca87054 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "node_modules", "./examples", "./lib", - "./**/__tests__/" + "./**/__tests__/", + "./typeChecking.ts" ], "compilerOptions": { "target": "es5", diff --git a/typeChecking.ts b/typeChecking.ts new file mode 100644 index 0000000..490127f --- /dev/null +++ b/typeChecking.ts @@ -0,0 +1,30 @@ +import { exec } from "child_process"; +import pify from 'pify'; +import { readdir } from "fs"; +import path from "path"; + +// Would be better to do this programmatically, didn't look straightforward though. +export const typeCheckFiles = async (files: string[], expectError?: boolean) => { + await Promise.all(files.map(file => new Promise((resolve, reject) => { + const tscArgs = '--strict --experimentalDecorators --emitDecoratorMetadata --esModuleInterop --noEmit --lib esnext'; + exec(`$(npm bin)/tsc ${tscArgs} ${file}`, (err) => { + if ((err && expectError) || (!err && !expectError)) { + resolve(); + } else { + let errorText = expectError + ? `Types in file ${file} valid, expected invalid` + : `Types in file ${file} invalid, expected valid`; + + if (err) { + errorText = `${errorText}\nOriginal error: ${err.toString()}` + } + reject(new Error(errorText)); + } + }); + }))); +}; + +export const getFilesInDir = async (...paths: string[]): Promise => { + const files = await pify(readdir)(path.join(...paths)); + return files.map((file: string) => path.join(...paths, file)); +} diff --git a/yarn.lock b/yarn.lock index 01fd753..99c47e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -979,6 +979,14 @@ callsites@^2.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= +camel-case@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" + integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= + dependencies: + no-case "^2.2.0" + upper-case "^1.1.1" + camelcase-keys@^4.0.0: version "4.2.0" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" @@ -1245,6 +1253,14 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control- resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= +constant-case@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-2.0.0.tgz#4175764d389d3fa9c8ecd29186ed6005243b6a46" + integrity sha1-QXV2TTidP6nI7NKRhu1gBSQ7akY= + dependencies: + snake-case "^2.1.0" + upper-case "^1.1.1" + content-disposition@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" @@ -3855,6 +3871,11 @@ loud-rejection@^1.0.0: currently-unhandled "^0.4.1" signal-exit "^3.0.0" +lower-case@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= + lowercase-keys@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -4280,6 +4301,13 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +no-case@^2.2.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" + integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== + dependencies: + lower-case "^1.1.1" + node-emoji@^1.4.1: version "1.8.1" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.8.1.tgz#6eec6bfb07421e2148c75c6bba72421f8530a826" @@ -4970,6 +4998,14 @@ parseurl@~1.3.2: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M= +pascal-case@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-2.0.1.tgz#2d578d3455f660da65eca18ef95b4e0de912761e" + integrity sha1-LVeNNFX2YNpl7KGO+VtODekSdh4= + dependencies: + camel-case "^3.0.0" + upper-case-first "^1.1.0" + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -5828,6 +5864,13 @@ smart-buffer@^4.0.1: resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.0.1.tgz#07ea1ca8d4db24eb4cac86537d7d18995221ace3" integrity sha512-RFqinRVJVcCAL9Uh1oVqE6FZkqsyLiVOYEZ20TqIOjuX7iFVJ+zsbs4RIghnw/pTs7mZvt8ZHhvm1ZUrR4fykg== +snake-case@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f" + integrity sha1-Qb2xtz8w7GagTU4srRt2OH1NbZ8= + dependencies: + no-case "^2.2.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -6517,6 +6560,18 @@ update-notifier@^2.3.0, update-notifier@^2.5.0: semver-diff "^2.0.0" xdg-basedir "^3.0.0" +upper-case-first@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115" + integrity sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU= + dependencies: + upper-case "^1.1.1" + +upper-case@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" + integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= + uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"