From a061673176770d49b5d1d0d7be25e4a15368fd8a Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Mon, 29 May 2017 23:08:55 -0700 Subject: [PATCH 01/14] Generate GQL fragment based on types --- bin/ts2gql | 2 +- bin/ts2gqlfragment | 9 +++ log.log | 13 +++++ package.json | 7 ++- src/Collector.ts | 6 +- src/Fragment.ts | 140 +++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 4 +- test/component.ts | 47 +++++++++++++++ test/input.ts | 76 ++++++++++++++++++++++++ 9 files changed, 296 insertions(+), 8 deletions(-) create mode 100755 bin/ts2gqlfragment create mode 100644 log.log create mode 100644 src/Fragment.ts create mode 100644 test/component.ts create mode 100644 test/input.ts diff --git a/bin/ts2gql b/bin/ts2gql index 21145ff..de87eeb 100755 --- a/bin/ts2gql +++ b/bin/ts2gql @@ -6,4 +6,4 @@ if (process.argv.length < 3) { process.exit(1); } -require('..').emit(process.argv[2], process.argv.slice(3), process.stdout); +require('../dist/src').emit(process.argv[2], process.argv.slice(3), process.stdout); diff --git a/bin/ts2gqlfragment b/bin/ts2gqlfragment new file mode 100755 index 0000000..7e63b58 --- /dev/null +++ b/bin/ts2gqlfragment @@ -0,0 +1,9 @@ +#!/usr/bin/env node +if (process.argv.length < 3) { + process.stderr.write('\n'); + process.stderr.write('Usage: ts2gqlfragment root/module.ts\n'); + process.stderr.write('\n'); + process.exit(1); +} + +require('../dist/src').generateFragments(process.argv[2]); diff --git a/log.log b/log.log new file mode 100644 index 0000000..3b310e6 --- /dev/null +++ b/log.log @@ -0,0 +1,13 @@ +/Users/RylanH/Desktop/convoy/ts2gql/test/graphql/getPosts.grapql +fragment getPosts on Post { + id + title + postedAt + author { + name + photo + } + editor { + name + } +} diff --git a/package.json b/package.json index 9586145..0b3443c 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,9 @@ "lodash": "^4.0.0" }, "devDependencies": { - "tslint": "^3.13.0", - "typescript": "^2.0.0", - "typings": "^1.3.1" + "graphql-tag": "^2.2.0", + "tslint": "5.3.2", + "typescript": "2.3.3", + "typings": "2.1.1" } } diff --git a/src/Collector.ts b/src/Collector.ts index c7ab280..26c6c43 100644 --- a/src/Collector.ts +++ b/src/Collector.ts @@ -203,11 +203,11 @@ export default class Collector { // Type Walking _walkType = (type:typescript.Type):types.Node => { - if (type.flags & TypeFlags.Reference) { + if (type.flags & TypeFlags.Object) { return this._walkTypeReference(type); - } else if (type.flags & TypeFlags.Interface) { + } else if (type.flags & TypeFlags.BooleanLike) { return this._walkInterfaceType(type); - } else if (type.flags & TypeFlags.Anonymous) { + } else if (type.flags & TypeFlags.Index) { return this._walkNode(type.getSymbol().declarations[0]); } else if (type.flags & TypeFlags.String) { return {type: 'string'}; diff --git a/src/Fragment.ts b/src/Fragment.ts new file mode 100644 index 0000000..95c91ed --- /dev/null +++ b/src/Fragment.ts @@ -0,0 +1,140 @@ +import * as _ from 'lodash'; +import * as typescript from 'typescript'; +import * as path from 'path'; + +// Find files that import { fragment } from '@convoy/ts2gql' +// Log that '* as' is not supported +// Support 'as' in import + +export function generateFragments(rootPath:string) { + rootPath = path.resolve(rootPath); + const program = typescript.createProgram([rootPath], {}); + const files = program.getSourceFiles(); + const checker = program.getTypeChecker(); + + const calls = files.map(file => ({ + filePath: file.fileName, + calls: collectFromFile(file, checker, rootPath), + })); + + calls.forEach(file => { + file.calls.forEach(call => { + const gqlPath = path.resolve(file.filePath, call.relativePath); + const fileName = path.basename(gqlPath, path.extname(gqlPath)); + console.log(gqlPath); + console.log(`fragment ${fileName} on ${call.baseName} {`); + emitFields(call.properties); + console.log('}'); + }); + }); +} + +function emitFields(fields, indent = ' ') { + fields.forEach(field => { + if (field.subfields) { + console.log(`${indent}${field.name} {`); + emitFields(field.subfields, `${indent} `); + console.log(`${indent}}`); + } else { + console.log(`${indent}${field.name}`); + } + }); +} + +function collectFromFile(file:typescript.SourceFile, checker:typescript.TypeChecker, rootPath:string) { + // Find the actual fragment function call imported from ts2gql + let fragmentIdentifier; + typescript.forEachChild(file, child => { + if (child.kind === typescript.SyntaxKind.ImportDeclaration) { + const declaration = child as typescript.ImportDeclaration; + // TODO config @convoy/ts2gql + if ((declaration.moduleSpecifier as typescript.StringLiteral).text === '../src') { + const bindings = (declaration.importClause as typescript.ImportClause).namedBindings as typescript.NamedImports; + const elements = bindings.elements as typescript.ImportSpecifier[]; + const importSpecifier = _.find(elements, element => (element.propertyName || element.name).text === 'fragment'); + if (!importSpecifier) return null; + fragmentIdentifier = importSpecifier.name; + } + } + }); + if (!fragmentIdentifier) return []; + return collectFragmentCalls(file, checker, fragmentIdentifier.text); +} + +function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeChecker, fragmentCallIdentifier:string) { + let calls = []; + typescript.forEachChild(node, child => { + const childCalls = collectFragmentCalls(child, checker, fragmentCallIdentifier); + if (childCalls) { + calls = calls.concat(childCalls); + } + if (child.kind !== typescript.SyntaxKind.CallExpression) return null; + const call = child as typescript.CallExpression; + + // TODO Can we get the actual symbol? + if ((call.expression as typescript.Identifier).text !== fragmentCallIdentifier) return null; + if (call.typeArguments.length !== 2) { + throw new Error('ts2gql.fragment(require(relGQLPath)) should have two type arguments'); + } + if (call.arguments.length !== 1) { + throw new Error('ts2gql.fragment(require(relGQLPath)): Must have one argument'); + } + + const data = call.typeArguments[0]; + if (data.kind !== typescript.SyntaxKind.TypeReference) { + throw new Error('ts2gql.fragment(require(relGQLPath)): TFragment must be a TypeReference'); + } + const base = call.typeArguments[1]; + if (base.kind !== typescript.SyntaxKind.TypeReference) { + throw new Error('ts2gql.fragment(require(relGQLPath)): TFragmentBase must be a TypeReference'); + } + const argument = call.arguments[0]; + if (argument.kind !== typescript.SyntaxKind.CallExpression) { + throw new Error('ts2gql.fragment(require(relGQLPath)): First argument must be a require call'); + } + const requireCall = argument as typescript.CallExpression; + if (requireCall.arguments.length !== 1) { + throw new Error('ts2gql.fragment(require(relGQLPath)): Require call must have 1 argument'); + } + const gqlToken = requireCall.arguments[0]; + if (gqlToken.kind !== typescript.SyntaxKind.StringLiteral) { + throw new Error('ts2gql.fragment(require(relGQLPath)): Require call argument must be a string literal'); + } + const relativePath = (gqlToken as typescript.StringLiteral).text; + + const properties = collectProperties(data, checker); + const baseName = ((base as typescript.TypeReferenceNode).typeName as typescript.Identifier).text; + + calls.push({ + properties, + baseName, + relativePath, + }); + }); + return calls; +} + +function collectProperties(typeNode:typescript.TypeNode, checker:typescript.TypeChecker) { + const fields = []; + const type = checker.getTypeFromTypeNode(typeNode); + + // For unstructured types (like string, number, etc) we don't need to loop through their properties + if (!(type.flags & typescript.TypeFlags.StructuredType)) return null; + const properties = checker.getPropertiesOfType(type); + + properties.forEach(symbol => { + let subfields = null; + if (symbol.valueDeclaration) { + if (symbol.valueDeclaration.kind === typescript.SyntaxKind.PropertySignature) { + const propertySignature = symbol.valueDeclaration as typescript.PropertySignature; + subfields = collectProperties(propertySignature.type, checker); + } + } + fields.push({ name: symbol.name, subfields }); + }); + return fields; +} + +export function fragment(document:any) { + return document; +} diff --git a/src/index.ts b/src/index.ts index cf36d7b..71e3c2d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,8 @@ import * as util from './util'; import Collector from './Collector'; import Emitter from './Emitter'; +export { generateFragments, fragment } from './Fragment'; + export function load(schemaRootPath:string, rootNodeNames:string[]):types.TypeMap { schemaRootPath = path.resolve(schemaRootPath); const program = typescript.createProgram([schemaRootPath], {}); @@ -56,6 +58,6 @@ export function emit(schemaRootPath:string, rootNodeNames:string[], stream:NodeJ } function isNodeExported(node:typescript.Node):boolean { - return (node.flags & typescript.NodeFlags.Export) !== 0 + return (node.flags & typescript.NodeFlags.ExportContext) !== 0 || (node.parent && node.parent.kind === typescript.SyntaxKind.SourceFile); } diff --git a/test/component.ts b/test/component.ts new file mode 100644 index 0000000..9409188 --- /dev/null +++ b/test/component.ts @@ -0,0 +1,47 @@ +import { fragment as fragmentCall } from '../src'; +import { Post, User } from './input'; +import 'graphql-tag'; + +interface AuthorProps { + author:Pick; +} +interface DateTimeProps { + post:Pick; +} + +type PostProps = + // Displaying direct props of the entity + Pick & + // Passing the entire entity to another component + DateTimeProps['post'] & + // Passing a prop of the entity to another component + { + author:AuthorProps['author']; + } & + // Deeply retrieving a prop from an entity + { + editor:{ + name:User['name'], + }, + }; + +const query = ` + query getPosts() { + posts() { + ...${fragmentCall(require('../graphql/getPosts.grapql'))} + } + } +`; + +// fragment() === ` +// fragment PostProps on Post { +// id +// title +// postedAt +// author { +// id +// name +// photo +// } +// } +// `; diff --git a/test/input.ts b/test/input.ts new file mode 100644 index 0000000..b92ce2f --- /dev/null +++ b/test/input.ts @@ -0,0 +1,76 @@ +// Type aliases become GraphQL scalars. +export type Url = string; + +// If you want an explicit GraphQL ID type, you can do that too: +/** @graphql ID */ +export type Id = string; + +export type Email = string; + +// Interfaces become GraphQL types. +export interface User { + id:Id; + name:string; + photo:Url; +} + +export interface PostContent { + title:string; + body:string; +} + +export interface Post extends PostContent { + id:Id; + postedAt:Date; + author:User; + editor:User; +} + +export interface Category { + id:Id; + name:string; + posts:Post[]; +} + +// Methods are transformed into parameteried edges: +export interface QueryRoot { + users(args:{id:Id}):User[]; + posts(args:{id:Id, authorId:Id, categoryId:Id}):Post[]; + categories(args:{id:Id}):Category[]; +} + +export interface MutationRoot { + login(args:{username:string, password:string}):QueryRoot; + sendEmail(args:{recipients:EmailRecipients[]}):QueryRoot; +} + +// Don't forget to declare your schema and the root types! +/** @graphql schema */ +export interface Schema { + query:QueryRoot; + mutation:MutationRoot; +} + +// If you have input objects (http://docs.apollostack.com/graphql/schemas.html#input-objects) +/** @graphql input */ +export interface EmailRecipients { + type:string; + name:string; + email?:Email; +} + +/** + * @graphql required authorId + */ +export interface PostArguments { + authorId:Id; +} + +// You may also wish to expose some GraphQL specific fields or parameterized +// calls on particular types, while still preserving the shape of your +// interfaces for more general use: +/** @graphql override Category */ +export interface CategoryOverrides { + // for example, we may want to be able to filter or paginate posts: + posts(args:PostArguments):Post[]; +} From c1fa86de98c884aadcbf4904220b8500b8d3f496 Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Sat, 26 Aug 2017 13:31:04 -0700 Subject: [PATCH 02/14] Update dependencies, fixup some references/typings, write to files --- bin/ts2gqlfragment | 2 +- package.json | 6 ++-- src/Collector.ts | 4 +-- src/Emitter.ts | 10 +++--- src/Fragment.ts | 56 +++++++++++++++++++++--------- src/index.ts | 4 +-- test/{component.ts => fragment.ts} | 24 ++++--------- 7 files changed, 59 insertions(+), 47 deletions(-) rename test/{component.ts => fragment.ts} (51%) diff --git a/bin/ts2gqlfragment b/bin/ts2gqlfragment index 7e63b58..83ff3e4 100755 --- a/bin/ts2gqlfragment +++ b/bin/ts2gqlfragment @@ -6,4 +6,4 @@ if (process.argv.length < 3) { process.exit(1); } -require('../dist/src').generateFragments(process.argv[2]); +require('../dist').generateFragments(process.argv[2]); diff --git a/package.json b/package.json index 41c25b1..e69f063 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,9 @@ "lodash": "^4.0.0" }, "devDependencies": { - "graphql-tag": "2.2.0", - "tslint": "5.3.2", - "typescript": "2.3.3", + "graphql-tag": "2.4.2", + "tslint": "5.7.0", + "typescript": "2.4.2", "typings": "2.1.1" } } diff --git a/src/Collector.ts b/src/Collector.ts index c60264d..2d47517 100644 --- a/src/Collector.ts +++ b/src/Collector.ts @@ -22,7 +22,7 @@ export default class Collector { this.checker = program.getTypeChecker(); } - addRootNode(node:typescript.Declaration):void { + addRootNode(node:typescript.InterfaceDeclaration):void { this._walkNode(node); const simpleNode = this.types[this._nameForSymbol(this._symbolForNode(node.name))]; simpleNode.concrete = true; @@ -239,7 +239,7 @@ export default class Collector { // Utility - _addType(node:typescript.Declaration, typeBuilder:() => types.Node):types.Node { + _addType(node:typescript.InterfaceDeclaration|typescript.TypeAliasDeclaration|typescript.EnumDeclaration, typeBuilder:() => types.Node):types.Node { const name = this._nameForSymbol(this._symbolForNode(node.name)); if (this.types[name]) return this.types[name]; const type = typeBuilder(); diff --git a/src/Emitter.ts b/src/Emitter.ts index 06750bc..3b7c841 100644 --- a/src/Emitter.ts +++ b/src/Emitter.ts @@ -4,10 +4,12 @@ import * as types from './types'; // https://raw.githubusercontent.com/sogko/graphql-shorthand-notation-cheat-sheet/master/graphql-shorthand-notation-cheat-sheet.png export default class Emitter { + private types:types.TypeMap; + renames:{[key:string]:string} = {}; - constructor(private types:types.TypeMap) { - this.types = _.omitBy(types, (node, name) => this._preprocessNode(node, name)); + constructor(typeMap:types.TypeMap) { + this.types = _.omitBy(typeMap, (node, name) => this._preprocessNode(node, name)); } emitAll(stream:NodeJS.WritableStream) { @@ -54,7 +56,7 @@ export default class Emitter { } else if (node.target.type === 'reference') { return `union ${this._name(name)} = ${this._name(node.target.target)}`; } else if (node.target.type === 'union') { - const types = node.target.types.map(type => { + const typeList = node.target.types.map(type => { if (type.type !== 'reference') { throw new Error(`GraphQL unions require that all types are references. Got a ${type.type}`); } @@ -66,7 +68,7 @@ export default class Emitter { }); return this._emitEnum({ type: 'enum', - values: _.uniq(_.flatten(types)), + values: _.uniq(_.flatten(typeList)), }, this._name(name)); } else { throw new Error(`Can't serialize ${JSON.stringify(node.target)} as an alias`); diff --git a/src/Fragment.ts b/src/Fragment.ts index 95c91ed..9507e2e 100644 --- a/src/Fragment.ts +++ b/src/Fragment.ts @@ -1,10 +1,18 @@ import * as _ from 'lodash'; import * as typescript from 'typescript'; import * as path from 'path'; +import * as fs from 'fs'; -// Find files that import { fragment } from '@convoy/ts2gql' -// Log that '* as' is not supported -// Support 'as' in import +// TODO: +// Find all files that import { fragment } from '@convoy/ts2gql' +// Use config to find calls to @convoy/ts2gql not ../src +// or better yet use the symbol itself +// Log that 'import * as ts2gql from ...' is not supported +// or actually suppor it +// Create directory to file you are writing to +// Convert Namespace.Type to appropriate name of Namespace_Type +// Verify that interface adheres to being a partial of the interface its on +// or does GraphQL do that for us? export function generateFragments(rootPath:string) { rootPath = path.resolve(rootPath); @@ -21,33 +29,37 @@ export function generateFragments(rootPath:string) { file.calls.forEach(call => { const gqlPath = path.resolve(file.filePath, call.relativePath); const fileName = path.basename(gqlPath, path.extname(gqlPath)); - console.log(gqlPath); - console.log(`fragment ${fileName} on ${call.baseName} {`); - emitFields(call.properties); - console.log('}'); + const stream = fs.createWriteStream(gqlPath, { autoClose: false } as any); + stream.write(`fragment ${fileName} on ${call.baseName} {\n`); + emitFields(call.properties, stream); + stream.write('}\n'); + console.log(`Created fragment at ${gqlPath}`); }); }); } -function emitFields(fields, indent = ' ') { +function emitFields(fields:Field[], stream:NodeJS.WritableStream, indent = ' ') { fields.forEach(field => { if (field.subfields) { - console.log(`${indent}${field.name} {`); - emitFields(field.subfields, `${indent} `); - console.log(`${indent}}`); + stream.write(`${indent}${field.name} {\n`); + emitFields(field.subfields, stream, `${indent} `); + stream.write(`${indent}}\n`); } else { - console.log(`${indent}${field.name}`); + stream.write(`${indent}${field.name}\n`); } }); } +/** + * Finds all calls to ts2gql fragment() in a given file. + */ function collectFromFile(file:typescript.SourceFile, checker:typescript.TypeChecker, rootPath:string) { // Find the actual fragment function call imported from ts2gql - let fragmentIdentifier; + let fragmentIdentifier:typescript.Identifier; typescript.forEachChild(file, child => { if (child.kind === typescript.SyntaxKind.ImportDeclaration) { const declaration = child as typescript.ImportDeclaration; - // TODO config @convoy/ts2gql + if ((declaration.moduleSpecifier as typescript.StringLiteral).text === '../src') { const bindings = (declaration.importClause as typescript.ImportClause).namedBindings as typescript.NamedImports; const elements = bindings.elements as typescript.ImportSpecifier[]; @@ -61,8 +73,14 @@ function collectFromFile(file:typescript.SourceFile, checker:typescript.TypeChec return collectFragmentCalls(file, checker, fragmentIdentifier.text); } +interface FragmentCall { + properties:Field[]; + baseName:string; + relativePath:string; +} + function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeChecker, fragmentCallIdentifier:string) { - let calls = []; + let calls:FragmentCall[] = []; typescript.forEachChild(node, child => { const childCalls = collectFragmentCalls(child, checker, fragmentCallIdentifier); if (childCalls) { @@ -71,7 +89,6 @@ function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeCheck if (child.kind !== typescript.SyntaxKind.CallExpression) return null; const call = child as typescript.CallExpression; - // TODO Can we get the actual symbol? if ((call.expression as typescript.Identifier).text !== fragmentCallIdentifier) return null; if (call.typeArguments.length !== 2) { throw new Error('ts2gql.fragment(require(relGQLPath)) should have two type arguments'); @@ -114,8 +131,13 @@ function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeCheck return calls; } +interface Field { + name:string; + subfields:Field[]; +} + function collectProperties(typeNode:typescript.TypeNode, checker:typescript.TypeChecker) { - const fields = []; + const fields:Field[] = []; const type = checker.getTypeFromTypeNode(typeNode); // For unstructured types (like string, number, etc) we don't need to loop through their properties diff --git a/src/index.ts b/src/index.ts index 71e3c2d..c7fae8d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,8 +52,8 @@ export function load(schemaRootPath:string, rootNodeNames:string[]):types.TypeMa } export function emit(schemaRootPath:string, rootNodeNames:string[], stream:NodeJS.WritableStream = process.stdout):void { - const types = load(schemaRootPath, rootNodeNames); - const emitter = new Emitter(types); + const typeMap = load(schemaRootPath, rootNodeNames); + const emitter = new Emitter(typeMap); emitter.emitAll(stream); } diff --git a/test/component.ts b/test/fragment.ts similarity index 51% rename from test/component.ts rename to test/fragment.ts index 9409188..cf61109 100644 --- a/test/component.ts +++ b/test/fragment.ts @@ -1,24 +1,25 @@ -import { fragment as fragmentCall } from '../src'; +import { fragment } from '../src'; import { Post, User } from './input'; import 'graphql-tag'; interface AuthorProps { author:Pick; } + interface DateTimeProps { post:Pick; } type PostProps = - // Displaying direct props of the entity + // When current component directly displays properties of the entity Pick & - // Passing the entire entity to another component + // When passing the entire entity to another component DateTimeProps['post'] & // Passing a prop of the entity to another component { author:AuthorProps['author']; } & - // Deeply retrieving a prop from an entity + // When current component displays deep properties of an entity { editor:{ name:User['name'], @@ -28,20 +29,7 @@ type PostProps = const query = ` query getPosts() { posts() { - ...${fragmentCall(require('../graphql/getPosts.grapql'))} + ...${fragment(require('../graphql/getPosts.graphql'))} } } `; - -// fragment() === ` -// fragment PostProps on Post { -// id -// title -// postedAt -// author { -// id -// name -// photo -// } -// } -// `; From 27fb01f9adb7ab6802319ec5c50be792178c891d Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Sat, 26 Aug 2017 13:32:09 -0700 Subject: [PATCH 03/14] Remove TODO --- src/Fragment.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Fragment.ts b/src/Fragment.ts index 9507e2e..e2f9859 100644 --- a/src/Fragment.ts +++ b/src/Fragment.ts @@ -4,7 +4,6 @@ import * as path from 'path'; import * as fs from 'fs'; // TODO: -// Find all files that import { fragment } from '@convoy/ts2gql' // Use config to find calls to @convoy/ts2gql not ../src // or better yet use the symbol itself // Log that 'import * as ts2gql from ...' is not supported From a7c1dbf94c5aa42bb76a51fca1457d81296dfb5b Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Mon, 28 Aug 2017 17:35:12 -0700 Subject: [PATCH 04/14] Using the actual symbol --- src/Fragment.ts | 68 ++++++++++++++++++++++++------------------------ test/fragment.ts | 2 +- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/Fragment.ts b/src/Fragment.ts index e2f9859..c0e7d4e 100644 --- a/src/Fragment.ts +++ b/src/Fragment.ts @@ -4,14 +4,9 @@ import * as path from 'path'; import * as fs from 'fs'; // TODO: -// Use config to find calls to @convoy/ts2gql not ../src -// or better yet use the symbol itself -// Log that 'import * as ts2gql from ...' is not supported -// or actually suppor it // Create directory to file you are writing to // Convert Namespace.Type to appropriate name of Namespace_Type -// Verify that interface adheres to being a partial of the interface its on -// or does GraphQL do that for us? +// Move require to the fragment call export function generateFragments(rootPath:string) { rootPath = path.resolve(rootPath); @@ -19,9 +14,16 @@ export function generateFragments(rootPath:string) { const files = program.getSourceFiles(); const checker = program.getTypeChecker(); + // Get the declaration of the fragment function contained in this file + const fragmentDeclaration = getFragmentDeclaration(files); + + if (!fragmentDeclaration) { + throw new Error(`ts2gql.fragments is not imported and used anywhere in this program`); + } + const calls = files.map(file => ({ filePath: file.fileName, - calls: collectFromFile(file, checker, rootPath), + calls: collectFragmentCalls(file, checker, fragmentDeclaration), })); calls.forEach(file => { @@ -37,6 +39,19 @@ export function generateFragments(rootPath:string) { }); } +function getFragmentDeclaration(files:typescript.SourceFile[]) { + let fragmentDeclaration:typescript.FunctionDeclaration|null = null; + const thisTypeFile = files.find(f => f.fileName === `${__filename.substr(0, __filename.length - 3)}.d.ts`); + thisTypeFile.forEachChild(child => { + if (child.kind !== typescript.SyntaxKind.FunctionDeclaration) return; + const declaration = child as typescript.FunctionDeclaration; + if (declaration.name.text === 'fragment') { + fragmentDeclaration = declaration; + } + }); + return fragmentDeclaration; +} + function emitFields(fields:Field[], stream:NodeJS.WritableStream, indent = ' ') { fields.forEach(field => { if (field.subfields) { @@ -49,46 +64,31 @@ function emitFields(fields:Field[], stream:NodeJS.WritableStream, indent = ' ') }); } -/** - * Finds all calls to ts2gql fragment() in a given file. - */ -function collectFromFile(file:typescript.SourceFile, checker:typescript.TypeChecker, rootPath:string) { - // Find the actual fragment function call imported from ts2gql - let fragmentIdentifier:typescript.Identifier; - typescript.forEachChild(file, child => { - if (child.kind === typescript.SyntaxKind.ImportDeclaration) { - const declaration = child as typescript.ImportDeclaration; - - if ((declaration.moduleSpecifier as typescript.StringLiteral).text === '../src') { - const bindings = (declaration.importClause as typescript.ImportClause).namedBindings as typescript.NamedImports; - const elements = bindings.elements as typescript.ImportSpecifier[]; - const importSpecifier = _.find(elements, element => (element.propertyName || element.name).text === 'fragment'); - if (!importSpecifier) return null; - fragmentIdentifier = importSpecifier.name; - } - } - }); - if (!fragmentIdentifier) return []; - return collectFragmentCalls(file, checker, fragmentIdentifier.text); -} - interface FragmentCall { properties:Field[]; baseName:string; relativePath:string; } -function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeChecker, fragmentCallIdentifier:string) { +function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeChecker, fragmentDeclaration:typescript.FunctionDeclaration) { + let calls:FragmentCall[] = []; typescript.forEachChild(node, child => { - const childCalls = collectFragmentCalls(child, checker, fragmentCallIdentifier); + const childCalls = collectFragmentCalls(child, checker, fragmentDeclaration); if (childCalls) { calls = calls.concat(childCalls); } if (child.kind !== typescript.SyntaxKind.CallExpression) return null; const call = child as typescript.CallExpression; - if ((call.expression as typescript.Identifier).text !== fragmentCallIdentifier) return null; + const symbol = checker.getSymbolAtLocation(call.expression); + + if (!symbol) return null; + + const type = checker.getTypeOfSymbolAtLocation(symbol, call.expression); + + if (type.symbol.valueDeclaration !== fragmentDeclaration) return null; + if (call.typeArguments.length !== 2) { throw new Error('ts2gql.fragment(require(relGQLPath)) should have two type arguments'); } @@ -156,6 +156,6 @@ function collectProperties(typeNode:typescript.TypeNode, checker:typescript.Type return fields; } -export function fragment(document:any) { +export function fragment, TFragmentBase>(document:any) { return document; } diff --git a/test/fragment.ts b/test/fragment.ts index cf61109..0a44cb7 100644 --- a/test/fragment.ts +++ b/test/fragment.ts @@ -1,4 +1,4 @@ -import { fragment } from '../src'; +import { fragment } from '../dist'; import { Post, User } from './input'; import 'graphql-tag'; From 59164d0ddfad86ab3f5425af7c2e2a13f4f427de Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Tue, 29 Aug 2017 08:53:26 -0700 Subject: [PATCH 05/14] Cleanup type system, make directory p, use correct name --- package.json | 7 +++--- src/Emitter.ts | 4 ++-- src/Fragment.ts | 27 ++++++------------------ test/fragment.ts | 2 +- log.log => test/graphql/getPosts.graphql | 1 - typings.json | 9 -------- 6 files changed, 14 insertions(+), 36 deletions(-) rename log.log => test/graphql/getPosts.graphql (63%) delete mode 100644 typings.json diff --git a/package.json b/package.json index e69f063..46a3e54 100644 --- a/package.json +++ b/package.json @@ -26,12 +26,13 @@ }, "dependencies": { "doctrine": "^1.2.2", - "lodash": "^4.0.0" + "lodash": "^4.17.4", + "mkdirp": "^0.5.1" }, "devDependencies": { + "@types/mkdirp": "^0.5.1", "graphql-tag": "2.4.2", "tslint": "5.7.0", - "typescript": "2.4.2", - "typings": "2.1.1" + "typescript": "2.4.2" } } diff --git a/src/Emitter.ts b/src/Emitter.ts index 3b7c841..86b4158 100644 --- a/src/Emitter.ts +++ b/src/Emitter.ts @@ -211,8 +211,8 @@ export default class Emitter { } _indent(content:string|string[]):string { - if (!_.isArray(content)) content = content.split('\n'); - return content.map(s => ` ${s}`).join('\n'); + if (!_.isArray(content)) content = (content as string).split('\n'); + return (content as string[]).map(s => ` ${s}`).join('\n'); } _transitiveInterfaces(node:types.InterfaceNode):types.InterfaceNode[] { diff --git a/src/Fragment.ts b/src/Fragment.ts index c0e7d4e..ad0140b 100644 --- a/src/Fragment.ts +++ b/src/Fragment.ts @@ -2,11 +2,7 @@ import * as _ from 'lodash'; import * as typescript from 'typescript'; import * as path from 'path'; import * as fs from 'fs'; - -// TODO: -// Create directory to file you are writing to -// Convert Namespace.Type to appropriate name of Namespace_Type -// Move require to the fragment call +import * as mkdirp from 'mkdirp'; export function generateFragments(rootPath:string) { rootPath = path.resolve(rootPath); @@ -30,6 +26,7 @@ export function generateFragments(rootPath:string) { file.calls.forEach(call => { const gqlPath = path.resolve(file.filePath, call.relativePath); const fileName = path.basename(gqlPath, path.extname(gqlPath)); + mkdirp.sync(path.dirname(gqlPath)); const stream = fs.createWriteStream(gqlPath, { autoClose: false } as any); stream.write(`fragment ${fileName} on ${call.baseName} {\n`); emitFields(call.properties, stream); @@ -104,22 +101,12 @@ function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeCheck if (base.kind !== typescript.SyntaxKind.TypeReference) { throw new Error('ts2gql.fragment(require(relGQLPath)): TFragmentBase must be a TypeReference'); } - const argument = call.arguments[0]; - if (argument.kind !== typescript.SyntaxKind.CallExpression) { - throw new Error('ts2gql.fragment(require(relGQLPath)): First argument must be a require call'); - } - const requireCall = argument as typescript.CallExpression; - if (requireCall.arguments.length !== 1) { - throw new Error('ts2gql.fragment(require(relGQLPath)): Require call must have 1 argument'); - } - const gqlToken = requireCall.arguments[0]; - if (gqlToken.kind !== typescript.SyntaxKind.StringLiteral) { - throw new Error('ts2gql.fragment(require(relGQLPath)): Require call argument must be a string literal'); - } + const gqlToken = call.arguments[0]; const relativePath = (gqlToken as typescript.StringLiteral).text; const properties = collectProperties(data, checker); - const baseName = ((base as typescript.TypeReferenceNode).typeName as typescript.Identifier).text; + const baseNameRaw = ((base as typescript.TypeReferenceNode).typeName as typescript.Identifier).text; + const baseName = baseNameRaw.replace(/\W/g, '_'); calls.push({ properties, @@ -156,6 +143,6 @@ function collectProperties(typeNode:typescript.TypeNode, checker:typescript.Type return fields; } -export function fragment, TFragmentBase>(document:any) { - return document; +export function fragment, TFragmentBase>(filepath:string) { + return require(filepath); } diff --git a/test/fragment.ts b/test/fragment.ts index 0a44cb7..9e24634 100644 --- a/test/fragment.ts +++ b/test/fragment.ts @@ -29,7 +29,7 @@ type PostProps = const query = ` query getPosts() { posts() { - ...${fragment(require('../graphql/getPosts.graphql'))} + ...${fragment('../graphql/getPosts.graphql')} } } `; diff --git a/log.log b/test/graphql/getPosts.graphql similarity index 63% rename from log.log rename to test/graphql/getPosts.graphql index 3b310e6..4faa8d2 100644 --- a/log.log +++ b/test/graphql/getPosts.graphql @@ -1,4 +1,3 @@ -/Users/RylanH/Desktop/convoy/ts2gql/test/graphql/getPosts.grapql fragment getPosts on Post { id title diff --git a/typings.json b/typings.json deleted file mode 100644 index 0ea171d..0000000 --- a/typings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "ts2gql", - "dependencies": { - "lodash": "registry:npm/lodash#4.0.0+20160701205107" - }, - "globalDependencies": { - "node": "registry:env/node#6.0.0+20160622202520" - } -} From 6e6f9c29b2feddea6f2351c706ef939651b4379e Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Tue, 29 Aug 2017 09:05:24 -0700 Subject: [PATCH 06/14] Fixup ts2gql --- bin/ts2gql | 2 +- scripts/prepublish | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/bin/ts2gql b/bin/ts2gql index de87eeb..58608ea 100755 --- a/bin/ts2gql +++ b/bin/ts2gql @@ -6,4 +6,4 @@ if (process.argv.length < 3) { process.exit(1); } -require('../dist/src').emit(process.argv[2], process.argv.slice(3), process.stdout); +require('../dist').emit(process.argv[2], process.argv.slice(3), process.stdout); diff --git a/scripts/prepublish b/scripts/prepublish index af763a7..902aedb 100755 --- a/scripts/prepublish +++ b/scripts/prepublish @@ -1,8 +1,6 @@ #!/usr/bin/env bash set -e -./node_modules/.bin/typings install - NPM_COMMAND=$(node -e "console.log(JSON.parse(process.env.npm_config_argv).original[0])") # Skip builds if we're being run via a basic npm install. if [[ "${NPM_COMMAND}" == "install" ]]; then From fd2453ce41d97e2ae275b7b0ea226c06ed7eccb7 Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Fri, 8 Sep 2017 22:14:34 -0700 Subject: [PATCH 07/14] Fixing some TS 2.4 issues --- src/Emitter.ts | 6 +++--- src/index.ts | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Emitter.ts b/src/Emitter.ts index 86b4158..c778c7b 100644 --- a/src/Emitter.ts +++ b/src/Emitter.ts @@ -9,7 +9,7 @@ export default class Emitter { renames:{[key:string]:string} = {}; constructor(typeMap:types.TypeMap) { - this.types = _.omitBy(typeMap, (node, name) => this._preprocessNode(node, name)); + this.types = _.omitBy(typeMap, (node, name) => this._preprocessNode(node, name, typeMap)); } emitAll(stream:NodeJS.WritableStream) { @@ -33,9 +33,9 @@ export default class Emitter { // Preprocessing - _preprocessNode(node:types.Node, name:types.SymbolName):boolean { + _preprocessNode(node:types.Node, name:types.SymbolName, typeMap:types.TypeMap):boolean { if (node.type === 'alias' && node.target.type === 'reference') { - const referencedNode = this.types[node.target.target]; + const referencedNode = typeMap[node.target.target]; if (this._isPrimitive(referencedNode) || referencedNode.type === 'enum') { this.renames[name] = node.target.target; return true; diff --git a/src/index.ts b/src/index.ts index c7fae8d..7bd38fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,6 +58,5 @@ export function emit(schemaRootPath:string, rootNodeNames:string[], stream:NodeJ } function isNodeExported(node:typescript.Node):boolean { - return (node.flags & typescript.NodeFlags.ExportContext) !== 0 - || (node.parent && node.parent.kind === typescript.SyntaxKind.SourceFile); + return node.modifiers && node.modifiers.some(m => m.kind === typescript.SyntaxKind.ExportKeyword); } From 81322acc101641aba264a3787fe9ee5dfb910cc8 Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Sun, 10 Sep 2017 14:22:29 -0700 Subject: [PATCH 08/14] Some fixes when running against more production scenarios --- package.json | 3 ++- src/Fragment.ts | 37 ++++++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 46a3e54..971e91a 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "test:style": "./scripts/test:style" }, "bin": { - "ts2gql": "./bin/ts2gql" + "ts2gql": "./bin/ts2gql", + "ts2gqlfragment": "./bin/ts2gqlfragment" }, "dependencies": { "doctrine": "^1.2.2", diff --git a/src/Fragment.ts b/src/Fragment.ts index ad0140b..a9b1cd5 100644 --- a/src/Fragment.ts +++ b/src/Fragment.ts @@ -6,7 +6,9 @@ import * as mkdirp from 'mkdirp'; export function generateFragments(rootPath:string) { rootPath = path.resolve(rootPath); - const program = typescript.createProgram([rootPath], {}); + const program = typescript.createProgram([rootPath], { + jsx: typescript.JsxEmit.React, + }); const files = program.getSourceFiles(); const checker = program.getTypeChecker(); @@ -39,6 +41,9 @@ export function generateFragments(rootPath:string) { function getFragmentDeclaration(files:typescript.SourceFile[]) { let fragmentDeclaration:typescript.FunctionDeclaration|null = null; const thisTypeFile = files.find(f => f.fileName === `${__filename.substr(0, __filename.length - 3)}.d.ts`); + if (!thisTypeFile) { + throw new Error(`ts2gqlfragment is not imported in the project`); + } thisTypeFile.forEachChild(child => { if (child.kind !== typescript.SyntaxKind.FunctionDeclaration) return; const declaration = child as typescript.FunctionDeclaration; @@ -84,13 +89,13 @@ function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeCheck const type = checker.getTypeOfSymbolAtLocation(symbol, call.expression); - if (type.symbol.valueDeclaration !== fragmentDeclaration) return null; + if (!type.symbol || type.symbol.valueDeclaration !== fragmentDeclaration) return null; - if (call.typeArguments.length !== 2) { - throw new Error('ts2gql.fragment(require(relGQLPath)) should have two type arguments'); + if (!call.typeArguments || call.typeArguments.length !== 2) { + throw new Error('ts2gql.fragment(graphQLFilePath) should have two type arguments'); } if (call.arguments.length !== 1) { - throw new Error('ts2gql.fragment(require(relGQLPath)): Must have one argument'); + throw new Error('ts2gql.fragment(graphQLFilePath): Must have one argument'); } const data = call.typeArguments[0]; @@ -104,7 +109,8 @@ function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeCheck const gqlToken = call.arguments[0]; const relativePath = (gqlToken as typescript.StringLiteral).text; - const properties = collectProperties(data, checker); + const propertyType = checker.getTypeFromTypeNode(data); + const properties = collectProperties(propertyType, checker, data); const baseNameRaw = ((base as typescript.TypeReferenceNode).typeName as typescript.Identifier).text; const baseName = baseNameRaw.replace(/\W/g, '_'); @@ -122,22 +128,23 @@ interface Field { subfields:Field[]; } -function collectProperties(typeNode:typescript.TypeNode, checker:typescript.TypeChecker) { +function collectProperties(type:typescript.Type, checker:typescript.TypeChecker, typeNode:typescript.TypeNode) { const fields:Field[] = []; - const type = checker.getTypeFromTypeNode(typeNode); // For unstructured types (like string, number, etc) we don't need to loop through their properties if (!(type.flags & typescript.TypeFlags.StructuredType)) return null; + + // A bit strange, but a boolean is a union of true and false therefore a StructuredType + if (type.flags & typescript.TypeFlags.Boolean) return null; + + // For Date's we don't need to loop through their properties + if (type.symbol && type.symbol.name === 'Date') return null; + const properties = checker.getPropertiesOfType(type); properties.forEach(symbol => { - let subfields = null; - if (symbol.valueDeclaration) { - if (symbol.valueDeclaration.kind === typescript.SyntaxKind.PropertySignature) { - const propertySignature = symbol.valueDeclaration as typescript.PropertySignature; - subfields = collectProperties(propertySignature.type, checker); - } - } + const propertyType = checker.getTypeOfSymbolAtLocation(symbol, typeNode); + const subfields = collectProperties(propertyType, checker, typeNode); fields.push({ name: symbol.name, subfields }); }); return fields; From c4fc4b34ca3064bcb83074eeca95cfc9d7b0c8d0 Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Sun, 10 Sep 2017 19:41:22 -0700 Subject: [PATCH 09/14] Fixing up arrays --- src/Fragment.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Fragment.ts b/src/Fragment.ts index a9b1cd5..d3d3bdf 100644 --- a/src/Fragment.ts +++ b/src/Fragment.ts @@ -131,6 +131,12 @@ interface Field { function collectProperties(type:typescript.Type, checker:typescript.TypeChecker, typeNode:typescript.TypeNode) { const fields:Field[] = []; + // For Arrays we want to use the type of the array + if (type.symbol && type.symbol.name === 'Array') { + const arrayType = type as typescript.TypeReference; + type = arrayType.typeArguments[0]; + } + // For unstructured types (like string, number, etc) we don't need to loop through their properties if (!(type.flags & typescript.TypeFlags.StructuredType)) return null; From 499ba9c0fb72600dcd2a1bc15c2150ebd337f683 Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Tue, 7 Nov 2017 09:09:10 -0800 Subject: [PATCH 10/14] Adding some comments --- src/Fragment.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Fragment.ts b/src/Fragment.ts index d3d3bdf..70abe32 100644 --- a/src/Fragment.ts +++ b/src/Fragment.ts @@ -40,6 +40,8 @@ export function generateFragments(rootPath:string) { function getFragmentDeclaration(files:typescript.SourceFile[]) { let fragmentDeclaration:typescript.FunctionDeclaration|null = null; + // Looks for this file's (src/Fragment.ts's) .d.ts file to see if the fragment function + // is called const thisTypeFile = files.find(f => f.fileName === `${__filename.substr(0, __filename.length - 3)}.d.ts`); if (!thisTypeFile) { throw new Error(`ts2gqlfragment is not imported in the project`); @@ -89,6 +91,7 @@ function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeCheck const type = checker.getTypeOfSymbolAtLocation(symbol, call.expression); + // Short-circuit if a function call is not to ts2gql's fragment function if (!type.symbol || type.symbol.valueDeclaration !== fragmentDeclaration) return null; if (!call.typeArguments || call.typeArguments.length !== 2) { @@ -107,6 +110,7 @@ function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeCheck throw new Error('ts2gql.fragment(require(relGQLPath)): TFragmentBase must be a TypeReference'); } const gqlToken = call.arguments[0]; + console.log(gqlToken); const relativePath = (gqlToken as typescript.StringLiteral).text; const propertyType = checker.getTypeFromTypeNode(data); From 793be06fb2a53df56c138eda1762957d191dfeaf Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Sun, 17 Dec 2017 22:35:50 -0800 Subject: [PATCH 11/14] Write first test and get it running --- package.json | 2 + scripts/test:clean.sh | 4 ++ scripts/test:style.sh | 2 +- scripts/test:unit.sh | 1 + src/Fragment.ts | 65 ++++++++++++++--------- test/{fragment.ts => fixtures/post.ts} | 13 +++-- test/{input.ts => fixtures/post_types.ts} | 0 test/unit/Fragment.ts | 29 ++++++++++ test_output/getPosts.graphql | 12 +++++ tsconfig.json | 3 ++ 10 files changed, 99 insertions(+), 32 deletions(-) create mode 100755 scripts/test:clean.sh rename test/{fragment.ts => fixtures/post.ts} (63%) rename test/{input.ts => fixtures/post_types.ts} (100%) create mode 100644 test/unit/Fragment.ts create mode 100644 test_output/getPosts.graphql diff --git a/package.json b/package.json index 2ed90ef..e769b25 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "prepublish": "./scripts/prepublish.sh", "release": "./scripts/release.sh", "start": "./scripts/start.sh", + "test:clean": "./scripts/test:clean.sh", "test:compile": "./scripts/test:compile.sh", "test:integration": "./scripts/test:integration.sh", "test:style": "./scripts/test:style.sh", @@ -45,6 +46,7 @@ "chai": "3.5.0", "chai-as-promised": "6.0.0", "chai-jest-diff": "nevir/chai-jest-diff#built-member-assertions", + "graphql": "0.12.3", "graphql-tag": "2.4.2", "jest": "21.1.0", "jest-junit": "3.0.0", diff --git a/scripts/test:clean.sh b/scripts/test:clean.sh new file mode 100755 index 0000000..ce11add --- /dev/null +++ b/scripts/test:clean.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -e + +rm -rf ./test/output diff --git a/scripts/test:style.sh b/scripts/test:style.sh index c88760b..264a17d 100755 --- a/scripts/test:style.sh +++ b/scripts/test:style.sh @@ -5,7 +5,7 @@ source ./scripts/include/node.sh FILES=("${@}") if [[ "${#FILES[@]}" = "0" ]]; then - FILES+=($(find scripts src test ! -name "*.d.ts" -and -name "*.ts" -or -name "*.tsx")) + FILES+=($(find scripts src test ! -name "*.d.ts" -not -path "test/fixtures/*" -and -name "*.ts" -or -name "*.tsx")) fi OPTIONS=( diff --git a/scripts/test:unit.sh b/scripts/test:unit.sh index 46adc17..454192b 100755 --- a/scripts/test:unit.sh +++ b/scripts/test:unit.sh @@ -35,6 +35,7 @@ for option in "${OPTIONS_FLAGS[@]}"; do fi done +npm run test:clean npm run compile # For jest-junit diff --git a/src/Fragment.ts b/src/Fragment.ts index 70abe32..fdcd105 100644 --- a/src/Fragment.ts +++ b/src/Fragment.ts @@ -1,4 +1,3 @@ -import * as _ from 'lodash'; import * as typescript from 'typescript'; import * as path from 'path'; import * as fs from 'fs'; @@ -29,16 +28,18 @@ export function generateFragments(rootPath:string) { const gqlPath = path.resolve(file.filePath, call.relativePath); const fileName = path.basename(gqlPath, path.extname(gqlPath)); mkdirp.sync(path.dirname(gqlPath)); - const stream = fs.createWriteStream(gqlPath, { autoClose: false } as any); - stream.write(`fragment ${fileName} on ${call.baseName} {\n`); - emitFields(call.properties, stream); - stream.write('}\n'); - console.log(`Created fragment at ${gqlPath}`); + + let contents = ''; + contents += `fragment ${fileName} on ${call.baseName} {\n`; + contents += emitFields(call.properties); + contents += '}\n'; + fs.writeFileSync(gqlPath, contents); + console.log(`Created fragment at ${gqlPath}`); // tslint:disable-line }); }); } -function getFragmentDeclaration(files:typescript.SourceFile[]) { +function getFragmentDeclaration(files:ReadonlyArray):typescript.FunctionDeclaration|null { let fragmentDeclaration:typescript.FunctionDeclaration|null = null; // Looks for this file's (src/Fragment.ts's) .d.ts file to see if the fragment function // is called @@ -49,23 +50,25 @@ function getFragmentDeclaration(files:typescript.SourceFile[]) { thisTypeFile.forEachChild(child => { if (child.kind !== typescript.SyntaxKind.FunctionDeclaration) return; const declaration = child as typescript.FunctionDeclaration; - if (declaration.name.text === 'fragment') { + if (declaration.name!.text === 'fragment') { fragmentDeclaration = declaration; } }); return fragmentDeclaration; } -function emitFields(fields:Field[], stream:NodeJS.WritableStream, indent = ' ') { +function emitFields(fields:Field[], indent = ' ') { + let contents = ''; fields.forEach(field => { - if (field.subfields) { - stream.write(`${indent}${field.name} {\n`); - emitFields(field.subfields, stream, `${indent} `); - stream.write(`${indent}}\n`); + if (field.subfields.length) { + contents += `${indent}${field.name} {\n`; + contents += emitFields(field.subfields, `${indent} `); + contents += `${indent}}\n`; } else { - stream.write(`${indent}${field.name}\n`); + contents += `${indent}${field.name}\n`; } }); + return contents; } interface FragmentCall { @@ -74,7 +77,11 @@ interface FragmentCall { relativePath:string; } -function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeChecker, fragmentDeclaration:typescript.FunctionDeclaration) { +function collectFragmentCalls( + node:typescript.Node, + checker:typescript.TypeChecker, + fragmentDeclaration:typescript.FunctionDeclaration, +) { let calls:FragmentCall[] = []; typescript.forEachChild(node, child => { @@ -82,17 +89,17 @@ function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeCheck if (childCalls) { calls = calls.concat(childCalls); } - if (child.kind !== typescript.SyntaxKind.CallExpression) return null; + if (child.kind !== typescript.SyntaxKind.CallExpression) return; const call = child as typescript.CallExpression; const symbol = checker.getSymbolAtLocation(call.expression); - if (!symbol) return null; + if (!symbol) return; const type = checker.getTypeOfSymbolAtLocation(symbol, call.expression); // Short-circuit if a function call is not to ts2gql's fragment function - if (!type.symbol || type.symbol.valueDeclaration !== fragmentDeclaration) return null; + if (!type.symbol || type.symbol.valueDeclaration !== fragmentDeclaration) return; if (!call.typeArguments || call.typeArguments.length !== 2) { throw new Error('ts2gql.fragment(graphQLFilePath) should have two type arguments'); @@ -103,14 +110,15 @@ function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeCheck const data = call.typeArguments[0]; if (data.kind !== typescript.SyntaxKind.TypeReference) { - throw new Error('ts2gql.fragment(require(relGQLPath)): TFragment must be a TypeReference'); + throw new Error(`ts2gql.fragment(require(relGQLPath)):` + + `TFragment must be a TypeReference`); } const base = call.typeArguments[1]; if (base.kind !== typescript.SyntaxKind.TypeReference) { - throw new Error('ts2gql.fragment(require(relGQLPath)): TFragmentBase must be a TypeReference'); + throw new Error(`ts2gql.fragment(require(relGQLPath)):` + + `TFragmentBase must be a TypeReference`); } const gqlToken = call.arguments[0]; - console.log(gqlToken); const relativePath = (gqlToken as typescript.StringLiteral).text; const propertyType = checker.getTypeFromTypeNode(data); @@ -123,6 +131,7 @@ function collectFragmentCalls(node:typescript.Node, checker:typescript.TypeCheck baseName, relativePath, }); + return; }); return calls; } @@ -132,23 +141,23 @@ interface Field { subfields:Field[]; } -function collectProperties(type:typescript.Type, checker:typescript.TypeChecker, typeNode:typescript.TypeNode) { +function collectProperties(type:typescript.Type, checker:typescript.TypeChecker, typeNode:typescript.TypeNode):Field[] { const fields:Field[] = []; // For Arrays we want to use the type of the array if (type.symbol && type.symbol.name === 'Array') { const arrayType = type as typescript.TypeReference; - type = arrayType.typeArguments[0]; + type = arrayType.typeArguments![0]; } // For unstructured types (like string, number, etc) we don't need to loop through their properties - if (!(type.flags & typescript.TypeFlags.StructuredType)) return null; + if (!(type.flags & typescript.TypeFlags.StructuredType)) return []; // A bit strange, but a boolean is a union of true and false therefore a StructuredType - if (type.flags & typescript.TypeFlags.Boolean) return null; + if (type.flags & typescript.TypeFlags.Boolean) return []; // For Date's we don't need to loop through their properties - if (type.symbol && type.symbol.name === 'Date') return null; + if (type.symbol && type.symbol.name === 'Date') return []; const properties = checker.getPropertiesOfType(type); @@ -161,5 +170,9 @@ function collectProperties(type:typescript.Type, checker:typescript.TypeChecker, } export function fragment, TFragmentBase>(filepath:string) { + // Some pointless code to appease error TS6133: 'TFragment' is declared but its value is never read. + const ignore:TFragment|null = null; + if (ignore !== null) return; + return require(filepath); } diff --git a/test/fragment.ts b/test/fixtures/post.ts similarity index 63% rename from test/fragment.ts rename to test/fixtures/post.ts index 9e24634..85dd462 100644 --- a/test/fragment.ts +++ b/test/fixtures/post.ts @@ -1,6 +1,6 @@ -import { fragment } from '../dist'; -import { Post, User } from './input'; -import 'graphql-tag'; +import { fragment } from '../../dist/src'; +import { Post, User } from './post_types'; +import gql from 'graphql-tag'; interface AuthorProps { author:Pick; @@ -26,10 +26,13 @@ type PostProps = }, }; -const query = ` +const query = gql` query getPosts() { posts() { - ...${fragment('../graphql/getPosts.graphql')} + ...${fragment('../../../test_output/getPosts.graphql')} } } `; + +// Pointless code to appease error TS6133: 'query' is declared but its value is never read. +if (!query) process.exit(); diff --git a/test/input.ts b/test/fixtures/post_types.ts similarity index 100% rename from test/input.ts rename to test/fixtures/post_types.ts diff --git a/test/unit/Fragment.ts b/test/unit/Fragment.ts new file mode 100644 index 0000000..94a068e --- /dev/null +++ b/test/unit/Fragment.ts @@ -0,0 +1,29 @@ +import * as path from 'path'; +import * as fs from 'fs'; + +import { generateFragments } from '../../src'; + +describe(`generateFragments`, () => { + + it(`reads complex stuff`, async () => { + const program = '../../../test/fixtures/post.ts'; + generateFragments(path.join(__dirname, program)); + const fragmentFile = fs.readFileSync(path.join(__dirname, '../../../test_output/getPosts.graphql'), 'utf8'); + + expect(fragmentFile).to.be.equal( +`fragment getPosts on Post { + id + title + postedAt + author { + name + photo + } + editor { + name + } +} +`); + }); + +}); diff --git a/test_output/getPosts.graphql b/test_output/getPosts.graphql new file mode 100644 index 0000000..4faa8d2 --- /dev/null +++ b/test_output/getPosts.graphql @@ -0,0 +1,12 @@ +fragment getPosts on Post { + id + title + postedAt + author { + name + photo + } + editor { + name + } +} diff --git a/tsconfig.json b/tsconfig.json index f1c563d..05a0fe3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,8 @@ "src/*", "test/**/*", "typings/**/*" + ], + "exclude": [ + "test/fixtures/**/*" ] } From 05110eae63371e019ccb511c55854c151133344a Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Mon, 18 Dec 2017 08:39:30 -0800 Subject: [PATCH 12/14] Reorganize files and test --- .gitignore | 3 +++ scripts/test:clean.sh | 2 +- scripts/test:style.sh | 2 +- src/Fragment.ts | 2 +- .../data/post/expected.graphql | 2 +- test/{fixtures/post.ts => data/post/index.ts} | 4 ++-- test/{fixtures => data/post}/post_types.ts | 0 test/unit/Fragment.ts | 24 +++++-------------- test/unit/Thing.ts | 7 ------ tsconfig.json | 2 +- 10 files changed, 16 insertions(+), 32 deletions(-) rename test_output/getPosts.graphql => test/data/post/expected.graphql (75%) rename test/{fixtures/post.ts => data/post/index.ts} (86%) rename test/{fixtures => data/post}/post_types.ts (100%) delete mode 100644 test/unit/Thing.ts diff --git a/.gitignore b/.gitignore index 6b94555..b898dd5 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ package-lock.json # Coverage .nyc_output/ coverage/ + +# Test Artifacts +test/data/**/output.graphql diff --git a/scripts/test:clean.sh b/scripts/test:clean.sh index ce11add..9a9f0e0 100755 --- a/scripts/test:clean.sh +++ b/scripts/test:clean.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash set -e -rm -rf ./test/output +rm -rf ./test/data/**/output.graphql diff --git a/scripts/test:style.sh b/scripts/test:style.sh index 264a17d..d86398f 100755 --- a/scripts/test:style.sh +++ b/scripts/test:style.sh @@ -5,7 +5,7 @@ source ./scripts/include/node.sh FILES=("${@}") if [[ "${#FILES[@]}" = "0" ]]; then - FILES+=($(find scripts src test ! -name "*.d.ts" -not -path "test/fixtures/*" -and -name "*.ts" -or -name "*.tsx")) + FILES+=($(find scripts src test ! -name "*.d.ts" -not -path "test/data/*" -and -name "*.ts" -or -name "*.tsx")) fi OPTIONS=( diff --git a/src/Fragment.ts b/src/Fragment.ts index fdcd105..292d4c0 100644 --- a/src/Fragment.ts +++ b/src/Fragment.ts @@ -25,7 +25,7 @@ export function generateFragments(rootPath:string) { calls.forEach(file => { file.calls.forEach(call => { - const gqlPath = path.resolve(file.filePath, call.relativePath); + const gqlPath = path.join(path.dirname(file.filePath), call.relativePath); const fileName = path.basename(gqlPath, path.extname(gqlPath)); mkdirp.sync(path.dirname(gqlPath)); diff --git a/test_output/getPosts.graphql b/test/data/post/expected.graphql similarity index 75% rename from test_output/getPosts.graphql rename to test/data/post/expected.graphql index 4faa8d2..681636d 100644 --- a/test_output/getPosts.graphql +++ b/test/data/post/expected.graphql @@ -1,4 +1,4 @@ -fragment getPosts on Post { +fragment output on Post { id title postedAt diff --git a/test/fixtures/post.ts b/test/data/post/index.ts similarity index 86% rename from test/fixtures/post.ts rename to test/data/post/index.ts index 85dd462..6a0102a 100644 --- a/test/fixtures/post.ts +++ b/test/data/post/index.ts @@ -1,4 +1,4 @@ -import { fragment } from '../../dist/src'; +import { fragment } from '../../../dist/src'; import { Post, User } from './post_types'; import gql from 'graphql-tag'; @@ -29,7 +29,7 @@ type PostProps = const query = gql` query getPosts() { posts() { - ...${fragment('../../../test_output/getPosts.graphql')} + ...${fragment('./output.graphql')} } } `; diff --git a/test/fixtures/post_types.ts b/test/data/post/post_types.ts similarity index 100% rename from test/fixtures/post_types.ts rename to test/data/post/post_types.ts diff --git a/test/unit/Fragment.ts b/test/unit/Fragment.ts index 94a068e..6621592 100644 --- a/test/unit/Fragment.ts +++ b/test/unit/Fragment.ts @@ -5,25 +5,13 @@ import { generateFragments } from '../../src'; describe(`generateFragments`, () => { - it(`reads complex stuff`, async () => { - const program = '../../../test/fixtures/post.ts'; - generateFragments(path.join(__dirname, program)); - const fragmentFile = fs.readFileSync(path.join(__dirname, '../../../test_output/getPosts.graphql'), 'utf8'); + it(`generates fragment with nested fields`, async () => { + const rootDir = path.join(__dirname, '../../../test/data/post'); - expect(fragmentFile).to.be.equal( -`fragment getPosts on Post { - id - title - postedAt - author { - name - photo - } - editor { - name - } -} -`); + generateFragments(path.join(rootDir, 'index.ts')); + const output = fs.readFileSync(path.join(rootDir, 'output.graphql'), 'utf8'); + const expected = fs.readFileSync(path.join(rootDir, 'expected.graphql'), 'utf8'); + expect(output).to.be.equal(expected); }); }); diff --git a/test/unit/Thing.ts b/test/unit/Thing.ts deleted file mode 100644 index abb9b90..0000000 --- a/test/unit/Thing.ts +++ /dev/null @@ -1,7 +0,0 @@ -describe(`TODO`, () => { - - it(`Need to write a test`, () => { - expect(true).to.be.ok; - }); - - }); diff --git a/tsconfig.json b/tsconfig.json index 05a0fe3..1914202 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,6 @@ "typings/**/*" ], "exclude": [ - "test/fixtures/**/*" + "test/data/**/*" ] } From 5fc0550638f410f582267907c1f85a6572a06547 Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Mon, 18 Dec 2017 09:05:00 -0800 Subject: [PATCH 13/14] Fixing circle --- circle.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/circle.yml b/circle.yml index 677eb93..07c9906 100644 --- a/circle.yml +++ b/circle.yml @@ -16,9 +16,6 @@ test: - npm run test:style: parallel: true - files: - - src/**/*.{ts,tsx} - - test/**/*.{ts,tsx} - npm run test:unit: parallel: true From 4fe9a74ef18b663d3335ffaaad77323f3660c096 Mon Sep 17 00:00:00 2001 From: Rylan Hawkins Date: Mon, 18 Dec 2017 09:14:31 -0800 Subject: [PATCH 14/14] Undo bin directory references --- bin/ts2gql | 2 +- bin/ts2gqlfragment | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/ts2gql b/bin/ts2gql index 58608ea..21145ff 100755 --- a/bin/ts2gql +++ b/bin/ts2gql @@ -6,4 +6,4 @@ if (process.argv.length < 3) { process.exit(1); } -require('../dist').emit(process.argv[2], process.argv.slice(3), process.stdout); +require('..').emit(process.argv[2], process.argv.slice(3), process.stdout); diff --git a/bin/ts2gqlfragment b/bin/ts2gqlfragment index 83ff3e4..670b9dd 100755 --- a/bin/ts2gqlfragment +++ b/bin/ts2gqlfragment @@ -6,4 +6,4 @@ if (process.argv.length < 3) { process.exit(1); } -require('../dist').generateFragments(process.argv[2]); +require('..').generateFragments(process.argv[2]);