From 58a22768b2d935580a9f12fec26fe09a2c1d54c2 Mon Sep 17 00:00:00 2001 From: Jesse Hallett Date: Fri, 13 Nov 2020 17:40:03 -0500 Subject: [PATCH 1/3] feat: emit `TypedDocumentNode` alias in generated code This change adds an exported type alias from each generated module based on the `TypedDocumentNode` type from `@graphql-typed-document-node/core`. Apollo Client and Villus are able to consume this type and propagate result data and variables types correctly. It looks like support for this type is also [coming][1] to Apollo Angular and to GraphQL.js itself. For example, import { gql, useQuery } from '@apollo/client'; const query = gql` query GitHubQuery($first: Int!) { viewer { repositories(first: $first) { nodes { id description } } } } ` as import("./__generated__/git-hub-query.ts").GithubQueryDocument; // ^ // type assertion is co-located with query export default () => { const { data } = useQuery(query, { variables: { first: 100 } }); // ^ ^ // inferred type is `GithubQuery` | // | // inferred type is `GithubQueryVariables` /* ... */ }; As a result types can be hooked up with one type assertion that is applied directly to the query expression. [1]: https://github.com/dotansimha/graphql-typed-document-node#upcoming-built-in-support --- package.json | 1 + .../src/__generated__/git-hub-query.ts | 2 + .../__snapshots__/analyzer.test.ts.snap | 2 + .../type-gen-visitor.test.ts.snap | 92 ++++++++++++++----- src/typegen/type-gen-visitor.ts | 54 +++++++++-- yarn.lock | 5 + 6 files changed, 124 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 7dcbf4767..5cb5f156f 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "url": "https://github.com/Quramy/ts-graphql-plugin.git" }, "dependencies": { + "@graphql-typed-document-node/core": "*", "graphql-language-service-interface": "^2.4.1", "graphql-language-service-types": "^1.6.1" }, diff --git a/project-fixtures/react-apollo-prj/src/__generated__/git-hub-query.ts b/project-fixtures/react-apollo-prj/src/__generated__/git-hub-query.ts index 220acd471..2e7d921a7 100644 --- a/project-fixtures/react-apollo-prj/src/__generated__/git-hub-query.ts +++ b/project-fixtures/react-apollo-prj/src/__generated__/git-hub-query.ts @@ -1,5 +1,6 @@ /* eslint-disable */ /* This is an autogenerated file. Do not edit this file directly! */ +import { TypedDocumentNode } from "@graphql-typed-document-node/core"; export type RepositoryFragment = { description: string | null; }; @@ -15,3 +16,4 @@ export type GitHubQuery = { export type GitHubQueryVariables = { first: number; }; +export type GitHubQueryDocument = TypedDocumentNode; diff --git a/src/analyzer/__snapshots__/analyzer.test.ts.snap b/src/analyzer/__snapshots__/analyzer.test.ts.snap index 8f22f803e..cb8b7555a 100644 --- a/src/analyzer/__snapshots__/analyzer.test.ts.snap +++ b/src/analyzer/__snapshots__/analyzer.test.ts.snap @@ -76,10 +76,12 @@ Extracted by [ts-graphql-plugin](https://github.com/Quramy/ts-graphql-plugin)" exports[`Analyzer typegen should create type files 1`] = ` "/* eslint-disable */ /* This is an autogenerated file. Do not edit this file directly! */ +import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; export type MyQuery = { hello: string; }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; diff --git a/src/typegen/__snapshots__/type-gen-visitor.test.ts.snap b/src/typegen/__snapshots__/type-gen-visitor.test.ts.snap index 8e1389317..84ff507d3 100644 --- a/src/typegen/__snapshots__/type-gen-visitor.test.ts.snap +++ b/src/typegen/__snapshots__/type-gen-visitor.test.ts.snap @@ -1,30 +1,36 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`typegen result type generation __typename field should gen __typename type from interface type 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { node: { __typename: string; }; }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation __typename field should gen __typename type from object type 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { __typename: \\"Query\\"; }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation __typename field should gen __typename type from union type 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { item: { __typename: \\"User\\" | \\"Page\\"; }; }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; @@ -36,50 +42,61 @@ exports[`typegen result type generation definition node pattern should gen type `; exports[`typegen result type generation definition node pattern should gen type from mutation operation def 1`] = ` -"export type MyMutaion = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyMutaion = { like: string; }; export type MyMutaionVariables = {}; +export type MyMutaionDocument = TypedDocumentNode; " `; exports[`typegen result type generation definition node pattern should gen type from query operation def 1`] = ` -"export type QueryResult = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type QueryResult = { hello: string; }; export type QueryVariables = {}; +export type QueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation definition node pattern should gen type from subscription operation def 1`] = ` -"export type MySubscription = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MySubscription = { issue: string; }; export type MySubscriptionVariables = {}; +export type MySubscriptionDocument = TypedDocumentNode; " `; exports[`typegen result type generation fragment spread reference should gen type from inline fragment with type condition 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { id: string; } & ({ hello: string; }); export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation fragment spread reference should gen type from inline fragment without type condition 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { id: string; hello: string; }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation fragment spread reference should gen type reference from fragment spread on field selection 1`] = ` -"export type MyFragment = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyFragment = { name: string; age: number; }; @@ -89,6 +106,7 @@ export type MyQuery = { } & MyFragment)[]; }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; @@ -103,18 +121,21 @@ export type B = { `; exports[`typegen result type generation fragment spread reference should gen type reference from fragment spread on operation 1`] = ` -"export type MyFragment = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyFragment = { hello: string; }; export type MyQuery = { bye: string; } & MyFragment; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation fragment spread reference should gen union object literal type from inline fragment concrete type condition 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { item: ({ id: string; }) & (({ @@ -124,11 +145,13 @@ exports[`typegen result type generation fragment spread reference should gen uni })); }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation fragment spread reference should gen union of type references from fragment concrete type condition 1`] = ` -"export type NodeFragment = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type NodeFragment = { id: string; }; export type UserFragment = { @@ -141,11 +164,13 @@ export type MyQuery = { item: NodeFragment & (UserFragment | PageFragment); }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation output types pattern should gen type from built-in scalar types 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { idField: string; stringField: string; intField: number; @@ -153,37 +178,45 @@ exports[`typegen result type generation output types pattern should gen type fro boolField: boolean; }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation output types pattern should gen type from enum 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { color: (\\"RED\\" | \\"BLUE\\" | \\"GREEN\\") | null; }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation output types pattern should gen type from interface type 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { node: { id: string; }; }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation output types pattern should gen type from object type 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { hello: string; }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation output types pattern should gen type with correct list/nonNull modifiers 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { nullableField: string | null; strictField: string; nullableFieldNullableList: (string | null)[] | null; @@ -194,39 +227,47 @@ exports[`typegen result type generation output types pattern should gen type wit strictNestedList: string[][]; }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation output types pattern should gen type with field alias 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { greeting: string; }; export type MyQueryVariables = {}; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen variable type generation should gen optional type with default value variable 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { hello: string; }; export type MyQueryVariables = { count?: number | null; }; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen variable type generation should gen type from enum 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { enumInput: string; }; export type MyQueryVariables = { color: \\"RED\\" | \\"BLUE\\" | \\"GREEN\\"; }; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen variable type generation should gen type from input object type 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { lineInput: number; }; export type MyQueryVariables = { @@ -241,11 +282,13 @@ export type MyQueryVariables = { }; }; }; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen variable type generation should gen type from scalar types 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { idInput: string; strInput: string; intInput: string; @@ -261,11 +304,13 @@ export type MyQueryVariables = { boolVar: boolean; scalarVar: any; }; +export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen variable type generation should gen type with correct list/nonNull modifiers 1`] = ` -"export type MyQuery = { +"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +export type MyQuery = { nullableField: string; strictField: string; nullableFieldNullableList: string; @@ -285,5 +330,6 @@ export type MyQueryVariables = { var7?: ((string | null)[] | null)[] | null; var8: string[][]; }; +export type MyQueryDocument = TypedDocumentNode; " `; diff --git a/src/typegen/type-gen-visitor.ts b/src/typegen/type-gen-visitor.ts index 02884e2bf..ad1b23dc4 100644 --- a/src/typegen/type-gen-visitor.ts +++ b/src/typegen/type-gen-visitor.ts @@ -139,16 +139,21 @@ export class TypeGenVisitor { variableElementStack.stack(); }, leave: node => { + const resultTypeName = node.name ? node.name.value : 'QueryResult'; + const variablesTypeName = node.name ? node.name.value + 'Variables' : 'QueryVariables'; + const documentTypeName = node.name ? node.name.value + 'Document' : 'QueryDocument'; + statements.push(this._createTsTypeDeclaration(resultTypeName, resultFieldElementStack.consume())); + statements.push(this._createTsTypeDeclaration(variablesTypeName, variableElementStack.consume())); statements.push( - this._createTsTypeDeclaration( - node.name ? node.name.value : 'QueryResult', - resultFieldElementStack.consume(), - ), - ); - statements.push( - this._createTsTypeDeclaration( - node.name ? node.name.value + 'Variables' : 'QueryVariables', - variableElementStack.consume(), + ts.createTypeAliasDeclaration( + undefined, + ts.createModifiersFromModifierFlags(ts.ModifierFlags.Export), + documentTypeName, + undefined, + ts.createTypeReferenceNode('TypedDocumentNode', [ + ts.createTypeReferenceNode(resultTypeName), + ts.createTypeReferenceNode(variablesTypeName), + ]), ), ); parentTypeStack.consume(); @@ -315,6 +320,12 @@ export class TypeGenVisitor { }, }); + // Prepend `TypedDocumentNode` import to statements if it is referenced in + // one of the existing statements. + if (statements.some(statement => this._isTypedDocumentNodeAliasDeclaration(statement))) { + statements.unshift(this._createTypedDocumentNodeImportDeclaration()); + } + const sourceFile = ts.createSourceFile(outputFileName, '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS); const resultFile = ts.updateSourceFileNode(sourceFile, statements); if (resultFile.statements.length) { @@ -481,4 +492,29 @@ export class TypeGenVisitor { } return ts.createIntersectionTypeNode(toIntersectionElements); } + + private _isTypedDocumentNodeAliasDeclaration(statement: ts.Statement): boolean { + const type = ts.isTypeAliasDeclaration(statement) && statement.type; + if (type && ts.isTypeReferenceNode(type)) { + if (ts.isIdentifier(type.typeName)) { + return type.typeName.text === 'TypedDocumentNode'; + } + } + return false; + } + + private _createTypedDocumentNodeImportDeclaration(): ts.Statement { + return ts.factory.createImportDeclaration( + undefined, + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports([ + ts.factory.createImportSpecifier(undefined, ts.factory.createIdentifier('TypedDocumentNode')), + ]), + ), + ts.factory.createStringLiteral('@graphql-typed-document-node/core'), + ); + } } diff --git a/yarn.lock b/yarn.lock index 01b6bc4fc..d7f08eaf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -518,6 +518,11 @@ is-promise "4.0.0" tslib "~2.0.1" +"@graphql-typed-document-node/core@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.0.tgz#0eee6373e11418bfe0b5638f654df7a4ca6a3950" + integrity sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg== + "@iarna/toml@^2.2.5": version "2.2.5" resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" From 07e47d4217cde751ab18ddb8b2b3f460072c95db Mon Sep 17 00:00:00 2001 From: Jesse Hallett Date: Fri, 13 Nov 2020 21:49:13 -0500 Subject: [PATCH 2/3] add config option; switch to `TypedQueryDocumentNode` type --- README.md | 51 ++++++++ package.json | 1 - .../src/__generated__/git-hub-query.ts | 2 - .../__snapshots__/analyzer.test.ts.snap | 14 ++- src/analyzer/analyzer.test.ts | 34 +++++- src/analyzer/analyzer.ts | 7 +- .../type-gen-visitor.test.ts.snap | 110 +++++++----------- src/typegen/type-gen-visitor.test.ts | 56 ++++++++- src/typegen/type-gen-visitor.ts | 92 +++++++++------ src/types.ts | 1 + yarn.lock | 5 - 11 files changed, 254 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 57b58582c..79be392d4 100644 --- a/README.md +++ b/README.md @@ -338,6 +338,57 @@ This option affects all editor supporting functions, results of CLI commands and If you set this option `false`, this plugin passes through query document without removing duplication. +### `exportTypedQueryDocumentNode` + +It's optional and default: `false`. This option enables an experimental feature that requires `graphql` v15.4.0 or +later. When enabled generated files export a type based on +[`TypedQueryDocumentNode`](https://github.com/graphql/graphql-js/blob/master/src/utilities/typedQueryDocumentNode.d.ts) +from GraphQL. The type extends the standard `DocumentNode` AST type but also includes types for result data and +variables as type arguments. + +To use this feature you can apply a type assertion to `gql` template tag expressions that evaluate to a `DocumentNode` +value. + +For example: + +```ts +const query = gql` + query MyQuery($take: Int!) { + recipes(take: $take) { + id + title + } + } +` as import('./__generated__/my-query.ts').MyQueryDocument; +``` + +With that type assertion in place result data and variable types will automatically flow through any function that +accepts the `TypedQueryDocumentNode` type. + +For example here is how you can write a wrapper for the `useQuery` function from Apollo Client: + +```ts +import { gql, QueryHookOptions, QueryResult, useQuery } from '@apollo/client'; +import { TypedQueryDocumentNode } from 'graphql'; + +function useTypedQuery( + query: TypedQueryDocumentNode, + options: QueryHookOptions, +): QueryResult { + return useQuery(query, options); +} + +// example usage +const { data } = useTypedQuery(query, { variables: { take: 100 } }); +// ^ ^ +// inferred type is `MyQuery` | +// | +// inferred type is `MyQueryVariables` +``` + +The result is that generated types are associated with queries at the point where the query is defined instead of at the +points where the query is executed. + ## webpack custom transformer ts-graphql-plugin provides TypeScript custom transformer to static transform from query template strings to GraphQL AST. It's useful if you use https://github.com/apollographql/graphql-tag diff --git a/package.json b/package.json index 885caa3f4..01b510f6e 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "url": "https://github.com/Quramy/ts-graphql-plugin.git" }, "dependencies": { - "@graphql-typed-document-node/core": "*", "graphql-language-service-interface": "^2.4.1", "graphql-language-service-types": "^1.6.1" }, diff --git a/project-fixtures/react-apollo-prj/src/__generated__/git-hub-query.ts b/project-fixtures/react-apollo-prj/src/__generated__/git-hub-query.ts index 2e7d921a7..220acd471 100644 --- a/project-fixtures/react-apollo-prj/src/__generated__/git-hub-query.ts +++ b/project-fixtures/react-apollo-prj/src/__generated__/git-hub-query.ts @@ -1,6 +1,5 @@ /* eslint-disable */ /* This is an autogenerated file. Do not edit this file directly! */ -import { TypedDocumentNode } from "@graphql-typed-document-node/core"; export type RepositoryFragment = { description: string | null; }; @@ -16,4 +15,3 @@ export type GitHubQuery = { export type GitHubQueryVariables = { first: number; }; -export type GitHubQueryDocument = TypedDocumentNode; diff --git a/src/analyzer/__snapshots__/analyzer.test.ts.snap b/src/analyzer/__snapshots__/analyzer.test.ts.snap index cb8b7555a..ee80206b9 100644 --- a/src/analyzer/__snapshots__/analyzer.test.ts.snap +++ b/src/analyzer/__snapshots__/analyzer.test.ts.snap @@ -76,12 +76,22 @@ Extracted by [ts-graphql-plugin](https://github.com/Quramy/ts-graphql-plugin)" exports[`Analyzer typegen should create type files 1`] = ` "/* eslint-disable */ /* This is an autogenerated file. Do not edit this file directly! */ -import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; export type MyQuery = { hello: string; }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; +" +`; + +exports[`Analyzer typegen should export alias of \`TypedQueryDocumentNode\` 1`] = ` +"/* eslint-disable */ +/* This is an autogenerated file. Do not edit this file directly! */ +import { TypedQueryDocumentNode } from \\"graphql\\"; +export type MyQuery = { + hello: string; +}; +export type MyQueryVariables = {}; +export type MyQueryDocument = TypedQueryDocumentNode; " `; diff --git a/src/analyzer/analyzer.test.ts b/src/analyzer/analyzer.test.ts index 5dea8d9c5..d25929e3f 100644 --- a/src/analyzer/analyzer.test.ts +++ b/src/analyzer/analyzer.test.ts @@ -9,8 +9,14 @@ type CreateTestingAnalyzerOptions = { sdl: string; files: { fileName: string; content: string }[]; localSchemaExtension?: { fileName: string; content: string }; + exportTypedQueryDocumentNode?: boolean; }; -function createTestingAnalyzer({ files: sourceFiles, sdl, localSchemaExtension }: CreateTestingAnalyzerOptions) { +function createTestingAnalyzer({ + files: sourceFiles, + sdl, + localSchemaExtension, + exportTypedQueryDocumentNode, +}: CreateTestingAnalyzerOptions) { const files = [...sourceFiles]; files.push({ fileName: '/schema.graphql', content: sdl }); if (localSchemaExtension) { @@ -21,6 +27,7 @@ function createTestingAnalyzer({ files: sourceFiles, sdl, localSchemaExtension } name: 'ts-graphql-plugin', schema: '/schema.graphql', localSchemaExtensions: localSchemaExtension ? [localSchemaExtension.fileName] : [], + exportTypedQueryDocumentNode, removeDuplicatedFragments: true, tag: 'gql', }; @@ -131,6 +138,21 @@ const complexOperationsPrj = { ], }; +const exportTypedQueryDocumentNode = { + sdl: ` + type Query { + hello: String! + } + `, + files: [ + { + fileName: 'main.ts', + content: 'const query = gql`query MyQuery { hello }`;', + }, + ], + exportTypedQueryDocumentNode: true, +}; + describe(Analyzer, () => { describe(Analyzer.prototype.extractToManifest, () => { it('should extract manifest', () => { @@ -229,5 +251,15 @@ describe(Analyzer, () => { expect(errors.length).toBe(1); expect(errors[0].message).toMatchSnapshot(); }); + + it('should export alias of `TypedQueryDocumentNode`', async () => { + const analyzer = createTestingAnalyzer(exportTypedQueryDocumentNode); + const { outputSourceFiles } = await analyzer.typegen(); + if (!outputSourceFiles) return fail(); + expect(outputSourceFiles.length).toBe(1); + expect(outputSourceFiles[0].fileName.endsWith('__generated__/my-query.ts')).toBeTruthy(); + const printer = ts.createPrinter(); + expect(printer.printFile(outputSourceFiles[0])).toMatchSnapshot(); + }); }); }); diff --git a/src/analyzer/analyzer.ts b/src/analyzer/analyzer.ts index 838745c1f..c2d1fd72b 100644 --- a/src/analyzer/analyzer.ts +++ b/src/analyzer/analyzer.ts @@ -130,7 +130,12 @@ export class Analyzer { dasherize(operationOrFragmentName) + '.ts', ); try { - outputSourceFiles.push(visitor.visit(r.documentNode, { outputFileName })); + outputSourceFiles.push( + visitor.visit(r.documentNode, { + exportTypedQueryDocumentNode: this._pluginConfig.exportTypedQueryDocumentNode ?? false, + outputFileName, + }), + ); this._debug( `Create type source file '${path.relative(this._prjRootPath, outputFileName)}' from '${path.relative( this._prjRootPath, diff --git a/src/typegen/__snapshots__/type-gen-visitor.test.ts.snap b/src/typegen/__snapshots__/type-gen-visitor.test.ts.snap index 84ff507d3..bd767b1af 100644 --- a/src/typegen/__snapshots__/type-gen-visitor.test.ts.snap +++ b/src/typegen/__snapshots__/type-gen-visitor.test.ts.snap @@ -1,36 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`typegen result type generation __typename field should gen __typename type from interface type 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { node: { __typename: string; }; }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation __typename field should gen __typename type from object type 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { __typename: \\"Query\\"; }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation __typename field should gen __typename type from union type 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { item: { __typename: \\"User\\" | \\"Page\\"; }; }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; @@ -42,61 +36,50 @@ exports[`typegen result type generation definition node pattern should gen type `; exports[`typegen result type generation definition node pattern should gen type from mutation operation def 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyMutaion = { +"export type MyMutaion = { like: string; }; export type MyMutaionVariables = {}; -export type MyMutaionDocument = TypedDocumentNode; " `; exports[`typegen result type generation definition node pattern should gen type from query operation def 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type QueryResult = { +"export type QueryResult = { hello: string; }; export type QueryVariables = {}; -export type QueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation definition node pattern should gen type from subscription operation def 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MySubscription = { +"export type MySubscription = { issue: string; }; export type MySubscriptionVariables = {}; -export type MySubscriptionDocument = TypedDocumentNode; " `; exports[`typegen result type generation fragment spread reference should gen type from inline fragment with type condition 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { id: string; } & ({ hello: string; }); export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation fragment spread reference should gen type from inline fragment without type condition 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { id: string; hello: string; }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation fragment spread reference should gen type reference from fragment spread on field selection 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyFragment = { +"export type MyFragment = { name: string; age: number; }; @@ -106,7 +89,6 @@ export type MyQuery = { } & MyFragment)[]; }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; @@ -121,21 +103,18 @@ export type B = { `; exports[`typegen result type generation fragment spread reference should gen type reference from fragment spread on operation 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyFragment = { +"export type MyFragment = { hello: string; }; export type MyQuery = { bye: string; } & MyFragment; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation fragment spread reference should gen union object literal type from inline fragment concrete type condition 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { item: ({ id: string; }) & (({ @@ -145,13 +124,11 @@ export type MyQuery = { })); }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation fragment spread reference should gen union of type references from fragment concrete type condition 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type NodeFragment = { +"export type NodeFragment = { id: string; }; export type UserFragment = { @@ -164,13 +141,11 @@ export type MyQuery = { item: NodeFragment & (UserFragment | PageFragment); }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation output types pattern should gen type from built-in scalar types 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { idField: string; stringField: string; intField: number; @@ -178,45 +153,37 @@ export type MyQuery = { boolField: boolean; }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation output types pattern should gen type from enum 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { color: (\\"RED\\" | \\"BLUE\\" | \\"GREEN\\") | null; }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation output types pattern should gen type from interface type 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { node: { id: string; }; }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation output types pattern should gen type from object type 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { hello: string; }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation output types pattern should gen type with correct list/nonNull modifiers 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { nullableField: string | null; strictField: string; nullableFieldNullableList: (string | null)[] | null; @@ -227,47 +194,57 @@ export type MyQuery = { strictNestedList: string[][]; }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen result type generation output types pattern should gen type with field alias 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { greeting: string; }; export type MyQueryVariables = {}; -export type MyQueryDocument = TypedDocumentNode; " `; -exports[`typegen variable type generation should gen optional type with default value variable 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; +exports[`typegen result type generation with exportTypedDocumentNode enabled should export an alias of \`TypedQueryDocumentNode\` for a query 1`] = ` +"import { TypedQueryDocumentNode } from \\"graphql\\"; export type MyQuery = { hello: string; + bye: string; +}; +export type MyQueryVariables = {}; +export type MyQueryDocument = TypedQueryDocumentNode; +" +`; + +exports[`typegen result type generation with exportTypedDocumentNode enabled should not import \`TypedQueryDocumentNode\` in a module with no query 1`] = ` +"export type MyFragment = { + hello: string | null; +}; +" +`; + +exports[`typegen variable type generation should gen optional type with default value variable 1`] = ` +"export type MyQuery = { + hello: string; }; export type MyQueryVariables = { count?: number | null; }; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen variable type generation should gen type from enum 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { enumInput: string; }; export type MyQueryVariables = { color: \\"RED\\" | \\"BLUE\\" | \\"GREEN\\"; }; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen variable type generation should gen type from input object type 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { lineInput: number; }; export type MyQueryVariables = { @@ -282,13 +259,11 @@ export type MyQueryVariables = { }; }; }; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen variable type generation should gen type from scalar types 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { idInput: string; strInput: string; intInput: string; @@ -304,13 +279,11 @@ export type MyQueryVariables = { boolVar: boolean; scalarVar: any; }; -export type MyQueryDocument = TypedDocumentNode; " `; exports[`typegen variable type generation should gen type with correct list/nonNull modifiers 1`] = ` -"import { TypedDocumentNode } from \\"@graphql-typed-document-node/core\\"; -export type MyQuery = { +"export type MyQuery = { nullableField: string; strictField: string; nullableFieldNullableList: string; @@ -330,6 +303,5 @@ export type MyQueryVariables = { var7?: ((string | null)[] | null)[] | null; var8: string[][]; }; -export type MyQueryDocument = TypedDocumentNode; " `; diff --git a/src/typegen/type-gen-visitor.test.ts b/src/typegen/type-gen-visitor.test.ts index 605441994..fdbf54ac3 100644 --- a/src/typegen/type-gen-visitor.test.ts +++ b/src/typegen/type-gen-visitor.test.ts @@ -1,11 +1,23 @@ -import { TypeGenVisitor, TypeGenError } from './type-gen-visitor'; +import { TypeGenVisitor, TypeGenError, VisitOption } from './type-gen-visitor'; import ts from 'typescript'; import { parse, buildSchema } from 'graphql'; -function generateAstAndPrint({ schemaSDL, documentContent }: { schemaSDL: string; documentContent: string }) { +function generateAstAndPrint({ + schemaSDL, + documentContent, + visitOptions, +}: { + schemaSDL: string; + documentContent: string; + visitOptions?: Partial; +}) { const schema = buildSchema(schemaSDL); const documentNode = parse(documentContent); - const source = new TypeGenVisitor({ schema }).visit(documentNode, { outputFileName: 'out.ts' }); + const source = new TypeGenVisitor({ schema }).visit(documentNode, { + exportTypedQueryDocumentNode: false, + outputFileName: 'out.ts', + ...visitOptions, + }); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, removeComments: true }); return printer.printFile(source); } @@ -717,5 +729,43 @@ describe('typegen', () => { expect(result).toMatchSnapshot(); }); }); + + describe('with exportTypedDocumentNode enabled', () => { + it('should export an alias of `TypedQueryDocumentNode` for a query', () => { + const result = generateAstAndPrint({ + visitOptions: { exportTypedQueryDocumentNode: true }, + schemaSDL: ` + type Query { + hello: String! + bye: String! + } + `, + documentContent: ` + query MyQuery { + hello + bye + } + `, + }); + expect(result).toMatchSnapshot(); + }); + + it('should not import `TypedQueryDocumentNode` in a module with no query', () => { + const result = generateAstAndPrint({ + visitOptions: { exportTypedQueryDocumentNode: true }, + schemaSDL: ` + type Query { + hello: String + } + `, + documentContent: ` + fragment MyFragment on Query { + hello + } + `, + }); + expect(result).toMatchSnapshot(); + }); + }); }); }); diff --git a/src/typegen/type-gen-visitor.ts b/src/typegen/type-gen-visitor.ts index ad1b23dc4..9760455b9 100644 --- a/src/typegen/type-gen-visitor.ts +++ b/src/typegen/type-gen-visitor.ts @@ -1,26 +1,26 @@ -import ts from 'typescript'; import { - GraphQLSchema, + ASTNode, DocumentNode, FieldNode, FragmentDefinitionNode, - ASTNode, - NamedTypeNode, - TypeNode, - GraphQLScalarType, GraphQLEnumType, - GraphQLObjectType, - GraphQLUnionType, - GraphQLInterfaceType, - GraphQLInputObjectType, - GraphQLList, - GraphQLNonNull, GraphQLField, GraphQLInputField, + GraphQLInputObjectType, GraphQLInputType, + GraphQLInterfaceType, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, GraphQLOutputType, + GraphQLScalarType, + GraphQLSchema, + GraphQLUnionType, + NamedTypeNode, + TypeNode, } from 'graphql'; import { visit } from 'graphql/language'; +import ts from 'typescript'; class Stack { private _array: T[] = []; @@ -84,6 +84,7 @@ export type TypeGenVisitorOptions = { }; export type VisitOption = { + exportTypedQueryDocumentNode: boolean; outputFileName: string; }; @@ -92,7 +93,7 @@ export class TypeGenVisitor { constructor({ schema }: TypeGenVisitorOptions) { this._schema = schema; } - visit(documentNode: DocumentNode, { outputFileName }: VisitOption) { + visit(documentNode: DocumentNode, { exportTypedQueryDocumentNode, outputFileName }: VisitOption) { const statements: ts.Statement[] = []; const parentTypeStack = new Stack(); const resultFieldElementStack = new Stack(() => ({ @@ -141,21 +142,17 @@ export class TypeGenVisitor { leave: node => { const resultTypeName = node.name ? node.name.value : 'QueryResult'; const variablesTypeName = node.name ? node.name.value + 'Variables' : 'QueryVariables'; - const documentTypeName = node.name ? node.name.value + 'Document' : 'QueryDocument'; statements.push(this._createTsTypeDeclaration(resultTypeName, resultFieldElementStack.consume())); statements.push(this._createTsTypeDeclaration(variablesTypeName, variableElementStack.consume())); - statements.push( - ts.createTypeAliasDeclaration( - undefined, - ts.createModifiersFromModifierFlags(ts.ModifierFlags.Export), - documentTypeName, - undefined, - ts.createTypeReferenceNode('TypedDocumentNode', [ - ts.createTypeReferenceNode(resultTypeName), - ts.createTypeReferenceNode(variablesTypeName), - ]), - ), - ); + if (exportTypedQueryDocumentNode) { + statements.push( + this._createTypedQueryDocumentNodeAliasDeclaration({ + operationName: node.name?.value, + resultTypeName, + variablesTypeName, + }), + ); + } parentTypeStack.consume(); }, }, @@ -320,10 +317,13 @@ export class TypeGenVisitor { }, }); - // Prepend `TypedDocumentNode` import to statements if it is referenced in - // one of the existing statements. - if (statements.some(statement => this._isTypedDocumentNodeAliasDeclaration(statement))) { - statements.unshift(this._createTypedDocumentNodeImportDeclaration()); + // Prepend `TypedQueryDocumentNode` import to statements if it is referenced + // in one of the existing statements. + if ( + exportTypedQueryDocumentNode && + statements.some(statement => this._isTypedQueryDocumentNodeAliasDeclaration(statement)) + ) { + statements.unshift(this._createTypedQueryDocumentNodeImportDeclaration()); } const sourceFile = ts.createSourceFile(outputFileName, '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS); @@ -493,17 +493,39 @@ export class TypeGenVisitor { return ts.createIntersectionTypeNode(toIntersectionElements); } - private _isTypedDocumentNodeAliasDeclaration(statement: ts.Statement): boolean { + private _isTypedQueryDocumentNodeAliasDeclaration(statement: ts.Statement): boolean { const type = ts.isTypeAliasDeclaration(statement) && statement.type; if (type && ts.isTypeReferenceNode(type)) { if (ts.isIdentifier(type.typeName)) { - return type.typeName.text === 'TypedDocumentNode'; + return type.typeName.text === 'TypedQueryDocumentNode'; } } return false; } - private _createTypedDocumentNodeImportDeclaration(): ts.Statement { + private _createTypedQueryDocumentNodeAliasDeclaration({ + operationName, + resultTypeName, + variablesTypeName, + }: { + operationName: string | undefined; + resultTypeName: string; + variablesTypeName: string; + }): ts.Statement { + const documentTypeName = operationName ? operationName + 'Document' : 'QueryDocument'; + return ts.createTypeAliasDeclaration( + undefined, + ts.createModifiersFromModifierFlags(ts.ModifierFlags.Export), + documentTypeName, + undefined, + ts.createTypeReferenceNode('TypedQueryDocumentNode', [ + ts.createTypeReferenceNode(resultTypeName), + ts.createTypeReferenceNode(variablesTypeName), + ]), + ); + } + + private _createTypedQueryDocumentNodeImportDeclaration(): ts.Statement { return ts.factory.createImportDeclaration( undefined, undefined, @@ -511,10 +533,10 @@ export class TypeGenVisitor { false, undefined, ts.factory.createNamedImports([ - ts.factory.createImportSpecifier(undefined, ts.factory.createIdentifier('TypedDocumentNode')), + ts.factory.createImportSpecifier(undefined, ts.factory.createIdentifier('TypedQueryDocumentNode')), ]), ), - ts.factory.createStringLiteral('@graphql-typed-document-node/core'), + ts.factory.createStringLiteral('graphql'), ); } } diff --git a/src/types.ts b/src/types.ts index fc070e8b2..421feece2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,7 @@ import { SchemaConfig } from './schema-manager/types'; export type TsGraphQLPluginConfigOptions = SchemaConfig & { + exportTypedQueryDocumentNode?: boolean; name: string; removeDuplicatedFragments?: boolean; tag?: string; diff --git a/yarn.lock b/yarn.lock index 8985cb8af..6215b6e3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -518,11 +518,6 @@ is-promise "4.0.0" tslib "~2.0.1" -"@graphql-typed-document-node/core@*": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.0.tgz#0eee6373e11418bfe0b5638f654df7a4ca6a3950" - integrity sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg== - "@iarna/toml@^2.2.5": version "2.2.5" resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" From 5b960766e3a410dbe4b0d7fa24c8a8d8b763a232 Mon Sep 17 00:00:00 2001 From: Jesse Hallett Date: Tue, 17 Nov 2020 15:17:30 -0500 Subject: [PATCH 3/3] undo import re-ordering --- src/typegen/type-gen-visitor.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/typegen/type-gen-visitor.ts b/src/typegen/type-gen-visitor.ts index 9760455b9..6bd028d7c 100644 --- a/src/typegen/type-gen-visitor.ts +++ b/src/typegen/type-gen-visitor.ts @@ -1,26 +1,26 @@ +import ts from 'typescript'; import { - ASTNode, + GraphQLSchema, DocumentNode, FieldNode, FragmentDefinitionNode, + ASTNode, + NamedTypeNode, + TypeNode, + GraphQLScalarType, GraphQLEnumType, - GraphQLField, - GraphQLInputField, - GraphQLInputObjectType, - GraphQLInputType, + GraphQLObjectType, + GraphQLUnionType, GraphQLInterfaceType, + GraphQLInputObjectType, GraphQLList, GraphQLNonNull, - GraphQLObjectType, + GraphQLField, + GraphQLInputField, + GraphQLInputType, GraphQLOutputType, - GraphQLScalarType, - GraphQLSchema, - GraphQLUnionType, - NamedTypeNode, - TypeNode, } from 'graphql'; import { visit } from 'graphql/language'; -import ts from 'typescript'; class Stack { private _array: T[] = [];