diff --git a/.changeset/@graphql-codegen_client-preset-9137-dependencies.md b/.changeset/@graphql-codegen_client-preset-9137-dependencies.md new file mode 100644 index 00000000000..c899084638a --- /dev/null +++ b/.changeset/@graphql-codegen_client-preset-9137-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-codegen/client-preset": patch +--- +dependencies updates: + - Updated dependency [`@graphql-typed-document-node/core@3.2.0` ↗︎](https://www.npmjs.com/package/@graphql-typed-document-node/core/v/3.2.0) (from `3.1.2`, in `dependencies`) diff --git a/.changeset/gold-dragons-poke.md b/.changeset/gold-dragons-poke.md new file mode 100644 index 00000000000..e63476db204 --- /dev/null +++ b/.changeset/gold-dragons-poke.md @@ -0,0 +1,8 @@ +--- +'@graphql-codegen/typed-document-node': major +'@graphql-codegen/gql-tag-operations': major +'@graphql-codegen/client-preset': major +'@graphql-codegen/gql-tag-operations-preset': major +--- + +Add `TypedDocumentNode` string alternative that doesn't require GraphQL AST on the client. This change requires `@graphql-typed-document-node/core` in version `3.2.0` or higher. diff --git a/dev-test/gql-tag-operations/graphql/fragment-masking.ts b/dev-test/gql-tag-operations/graphql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/dev-test/gql-tag-operations/graphql/fragment-masking.ts +++ b/dev-test/gql-tag-operations/graphql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/persisted-documents-string-mode/README.md b/examples/persisted-documents-string-mode/README.md new file mode 100644 index 00000000000..a0aab1c6bdd --- /dev/null +++ b/examples/persisted-documents-string-mode/README.md @@ -0,0 +1,11 @@ +# Yoga Persisted Documents Example + +Example for showing how to use GraphQL Code Generator for only allowing the execution of persisted operations. + +[Learn more about Yoga Persisted Operations](https://the-guild.dev/graphql/yoga-server/docs/features/persisted-operations) + +## Usage + +Run `yarn codegen --watch` for starting GraphQL Code Generator in watch mode. + +Run `yarn test` for running a tests located within `yoga.spec.ts`. diff --git a/examples/persisted-documents-string-mode/babel.config.js b/examples/persisted-documents-string-mode/babel.config.js new file mode 100644 index 00000000000..3e4899f4a74 --- /dev/null +++ b/examples/persisted-documents-string-mode/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + ['@babel/preset-env', { targets: { node: process.versions.node.split('.')[0] } }], + '@babel/preset-typescript', + ], +}; diff --git a/examples/persisted-documents-string-mode/codegen.ts b/examples/persisted-documents-string-mode/codegen.ts new file mode 100644 index 00000000000..420c68416fa --- /dev/null +++ b/examples/persisted-documents-string-mode/codegen.ts @@ -0,0 +1,21 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { type CodegenConfig } from '@graphql-codegen/cli'; + +const config: CodegenConfig = { + schema: './src/yoga.ts', + documents: ['src/**/*.ts'], + generates: { + './src/gql/': { + preset: 'client-preset', + presetConfig: { + persistedDocuments: true, + }, + config: { + documentMode: 'string', + }, + }, + }, + hooks: { afterAllFileWrite: ['prettier --write'] }, +}; + +export default config; diff --git a/examples/persisted-documents-string-mode/jest.config.js b/examples/persisted-documents-string-mode/jest.config.js new file mode 100644 index 00000000000..8a54dc0d796 --- /dev/null +++ b/examples/persisted-documents-string-mode/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + transform: { '^.+\\.ts': 'babel-jest' }, +}; diff --git a/examples/persisted-documents-string-mode/package.json b/examples/persisted-documents-string-mode/package.json new file mode 100644 index 00000000000..0c90cd29a55 --- /dev/null +++ b/examples/persisted-documents-string-mode/package.json @@ -0,0 +1,26 @@ +{ + "name": "example-persisted-documents-string-mode", + "version": "0.0.0", + "private": true, + "dependencies": { + "graphql-yoga": "3.7.2", + "@graphql-yoga/plugin-persisted-operations": "1.7.2" + }, + "devDependencies": { + "@graphql-typed-document-node/core": "3.2.0", + "jest": "28.1.3", + "babel-jest": "28.1.3", + "@graphql-codegen/cli": "3.2.2", + "@graphql-codegen/client-preset": "2.1.1", + "@babel/core": "7.21.0", + "@babel/preset-env": "7.20.2", + "@babel/preset-typescript": "7.21.0" + }, + "scripts": { + "test": "jest", + "codegen": "graphql-codegen --config codegen.ts", + "build": "tsc", + "test:end2end": "yarn test" + }, + "bob": false +} diff --git a/examples/persisted-documents-string-mode/src/gql/fragment-masking.ts b/examples/persisted-documents-string-mode/src/gql/fragment-masking.ts new file mode 100644 index 00000000000..dc2836d43e3 --- /dev/null +++ b/examples/persisted-documents-string-mode/src/gql/fragment-masking.ts @@ -0,0 +1,48 @@ +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; + +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never + : never + : never; + +// return non-nullable if `fragmentType` is non-nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> +): TType; +// return nullable if `fragmentType` is nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined +): TType | null | undefined; +// return array of non-nullable if `fragmentType` is array of non-nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> +): ReadonlyArray; +// return array of nullable if `fragmentType` is array of nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined +): ReadonlyArray | null | undefined; +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: + | FragmentType> + | ReadonlyArray>> + | null + | undefined +): TType | ReadonlyArray | null | undefined { + return fragmentType as any; +} + +export function makeFragmentData, FT extends ResultOf>( + data: FT, + _fragment: F +): FragmentType { + return data as FragmentType; +} diff --git a/examples/persisted-documents-string-mode/src/gql/gql.ts b/examples/persisted-documents-string-mode/src/gql/gql.ts new file mode 100644 index 00000000000..071a2b6a679 --- /dev/null +++ b/examples/persisted-documents-string-mode/src/gql/gql.ts @@ -0,0 +1,27 @@ +/* eslint-disable */ +import * as types from './graphql'; + +/** + * Map of all GraphQL operations in the project. + * + * This map has several performance disadvantages: + * 1. It is not tree-shakeable, so it will include all operations in the project. + * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. + * 3. It does not support dead code elimination, so it will add unused operations. + * + * Therefore it is highly recommended to use the babel or swc plugin for production. + */ +const documents = { + '\n query HelloQuery {\n hello\n }\n': types.HelloQueryDocument, +}; + +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: '\n query HelloQuery {\n hello\n }\n' +): typeof import('./graphql').HelloQueryDocument; + +export function graphql(source: string) { + return (documents as any)[source] ?? {}; +} diff --git a/examples/persisted-documents-string-mode/src/gql/graphql.ts b/examples/persisted-documents-string-mode/src/gql/graphql.ts new file mode 100644 index 00000000000..eec941afa44 --- /dev/null +++ b/examples/persisted-documents-string-mode/src/gql/graphql.ts @@ -0,0 +1,57 @@ +/* eslint-disable */ +import { DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; +}; + +export type Mutation = { + __typename?: 'Mutation'; + echo: Scalars['String']; +}; + +export type MutationEchoArgs = { + message: Scalars['String']; +}; + +export type Query = { + __typename?: 'Query'; + hello: Scalars['String']; +}; + +export type HelloQueryQueryVariables = Exact<{ [key: string]: never }>; + +export type HelloQueryQuery = { __typename?: 'Query'; hello: string }; + +export class TypedDocumentString + extends String + implements DocumentTypeDecoration +{ + __apiType?: DocumentTypeDecoration['__apiType']; + + constructor(private value: string, public __meta__?: { hash: string }) { + super(value); + } + + toString(): string & DocumentTypeDecoration { + return this.value; + } +} + +export const HelloQueryDocument = new TypedDocumentString( + ` + query HelloQuery { + hello +} + `, + { hash: '86f01e23de1c770cabbc35b2d87f2e5fd7557b6f' } +) as unknown as TypedDocumentString; diff --git a/examples/persisted-documents-string-mode/src/gql/index.ts b/examples/persisted-documents-string-mode/src/gql/index.ts new file mode 100644 index 00000000000..c682b1e2f99 --- /dev/null +++ b/examples/persisted-documents-string-mode/src/gql/index.ts @@ -0,0 +1,2 @@ +export * from './fragment-masking'; +export * from './gql'; diff --git a/examples/persisted-documents-string-mode/src/gql/persisted-documents.json b/examples/persisted-documents-string-mode/src/gql/persisted-documents.json new file mode 100644 index 00000000000..c9ef75fe915 --- /dev/null +++ b/examples/persisted-documents-string-mode/src/gql/persisted-documents.json @@ -0,0 +1,3 @@ +{ + "86f01e23de1c770cabbc35b2d87f2e5fd7557b6f": "query HelloQuery { hello }" +} diff --git a/examples/persisted-documents-string-mode/src/main.ts b/examples/persisted-documents-string-mode/src/main.ts new file mode 100644 index 00000000000..3e0cd27da5f --- /dev/null +++ b/examples/persisted-documents-string-mode/src/main.ts @@ -0,0 +1,15 @@ +import { createServer } from 'http'; +import { makeYoga } from './yoga.js'; + +import persistedDocumentsDictionary from './gql/persisted-documents.json'; + +const persistedDocuments = new Map(Object.entries(persistedDocumentsDictionary)); + +const yoga = makeYoga({ persistedDocuments }); +const server = createServer(yoga); + +// Start the server and you're done! +server.listen(4000, () => { + // eslint-disable-next-line no-console + console.info('Server is running on http://localhost:4000/graphql'); +}); diff --git a/examples/persisted-documents-string-mode/src/yoga.spec.ts b/examples/persisted-documents-string-mode/src/yoga.spec.ts new file mode 100644 index 00000000000..78b71fa7806 --- /dev/null +++ b/examples/persisted-documents-string-mode/src/yoga.spec.ts @@ -0,0 +1,85 @@ +import { graphql } from './gql/index'; +import { makeYoga } from './yoga'; +import persistedDocumentsDictionary from './gql/persisted-documents.json'; + +const persistedDocuments = new Map(Object.entries(persistedDocumentsDictionary)); + +const HelloQuery = graphql(/* GraphQL */ ` + query HelloQuery { + hello + } +`); + +describe('Persisted Documents', () => { + it('execute document without persisted operation enabled', async () => { + const yoga = makeYoga({ persistedDocuments: null }); + const result = await yoga.fetch('http://yoga/graphql', { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'application/json', + }, + body: JSON.stringify({ + query: HelloQuery, + }), + }); + expect(await result.json()).toMatchInlineSnapshot(` + Object { + "data": Object { + "hello": "Hello world!", + }, + } + `); + }); + + it('can not execute arbitrary operation with persisted operations enabled', async () => { + const yoga = makeYoga({ persistedDocuments }); + + const result = await yoga.fetch('http://yoga/graphql', { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'application/json', + }, + body: JSON.stringify({ + query: HelloQuery, + }), + }); + expect(await result.json()).toMatchInlineSnapshot(` + Object { + "errors": Array [ + Object { + "message": "PersistedQueryOnly", + }, + ], + } + `); + }); + + it('can execute persisted operation with persisted operations enabled', async () => { + const yoga = makeYoga({ persistedDocuments }); + const result = await yoga.fetch('http://yoga/graphql', { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'application/json', + }, + body: JSON.stringify({ + extensions: { + persistedQuery: { + version: 1, + sha256Hash: (HelloQuery as any)['__meta__']['hash'], + }, + }, + }), + }); + + expect(await result.json()).toMatchInlineSnapshot(` + Object { + "data": Object { + "hello": "Hello world!", + }, + } + `); + }); +}); diff --git a/examples/persisted-documents-string-mode/src/yoga.ts b/examples/persisted-documents-string-mode/src/yoga.ts new file mode 100644 index 00000000000..f0c1494be03 --- /dev/null +++ b/examples/persisted-documents-string-mode/src/yoga.ts @@ -0,0 +1,38 @@ +import { createSchema, createYoga, type Plugin } from 'graphql-yoga'; +import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'; + +const schema = createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hello: String! + } + + type Mutation { + echo(message: String!): String! + } + `, + resolvers: { + Query: { + hello: () => 'Hello world!', + }, + Mutation: { + echo: (_, args) => args.message, + }, + }, +}); + +export function makeYoga(args: { persistedDocuments: null | Map }) { + const plugins: Array> = []; + if (args.persistedDocuments !== null) { + const { persistedDocuments } = args; + plugins.push( + usePersistedOperations({ + getPersistedOperation: hash => persistedDocuments.get(hash), + }) + ); + } + return createYoga({ + schema, + plugins, + }); +} diff --git a/examples/persisted-documents-string-mode/tsconfig.json b/examples/persisted-documents-string-mode/tsconfig.json new file mode 100644 index 00000000000..d4d788c4b40 --- /dev/null +++ b/examples/persisted-documents-string-mode/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "Node16", + "outDir": "dist", + "resolveJsonModule": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/examples/persisted-documents/package.json b/examples/persisted-documents/package.json index efc5009d744..a126be5a058 100644 --- a/examples/persisted-documents/package.json +++ b/examples/persisted-documents/package.json @@ -7,7 +7,7 @@ "@graphql-yoga/plugin-persisted-operations": "1.7.3" }, "devDependencies": { - "@graphql-typed-document-node/core": "3.1.2", + "@graphql-typed-document-node/core": "3.2.0", "jest": "28.1.3", "babel-jest": "28.1.3", "@graphql-codegen/cli": "3.2.2", diff --git a/examples/persisted-documents/src/gql/fragment-masking.ts b/examples/persisted-documents/src/gql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/examples/persisted-documents/src/gql/fragment-masking.ts +++ b/examples/persisted-documents/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/react/apollo-client-swc-plugin/src/gql/fragment-masking.ts b/examples/react/apollo-client-swc-plugin/src/gql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/examples/react/apollo-client-swc-plugin/src/gql/fragment-masking.ts +++ b/examples/react/apollo-client-swc-plugin/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/react/apollo-client/src/gql/fragment-masking.ts b/examples/react/apollo-client/src/gql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/examples/react/apollo-client/src/gql/fragment-masking.ts +++ b/examples/react/apollo-client/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/react/http-executor/src/gql/fragment-masking.ts b/examples/react/http-executor/src/gql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/examples/react/http-executor/src/gql/fragment-masking.ts +++ b/examples/react/http-executor/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/react/nextjs-swr/gql/fragment-masking.ts b/examples/react/nextjs-swr/gql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/examples/react/nextjs-swr/gql/fragment-masking.ts +++ b/examples/react/nextjs-swr/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/react/tanstack-react-query/codegen.ts b/examples/react/tanstack-react-query/codegen.ts index 6d94d628656..1a100a3ccf1 100644 --- a/examples/react/tanstack-react-query/codegen.ts +++ b/examples/react/tanstack-react-query/codegen.ts @@ -7,6 +7,9 @@ const config: CodegenConfig = { generates: { './src/gql/': { preset: 'client', + config: { + documentMode: 'string', + }, }, }, hooks: { afterAllFileWrite: ['prettier --write'] }, diff --git a/examples/react/tanstack-react-query/package.json b/examples/react/tanstack-react-query/package.json index 6d167cc25dd..c51f8bd23a5 100644 --- a/examples/react/tanstack-react-query/package.json +++ b/examples/react/tanstack-react-query/package.json @@ -3,7 +3,6 @@ "version": "0.1.0", "private": true, "dependencies": { - "@graphql-tools/executor-http": "^0.1.8", "@tanstack/react-query": "4.26.1", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/examples/react/tanstack-react-query/src/gql/fragment-masking.ts b/examples/react/tanstack-react-query/src/gql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/examples/react/tanstack-react-query/src/gql/fragment-masking.ts +++ b/examples/react/tanstack-react-query/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/react/tanstack-react-query/src/gql/gql.ts b/examples/react/tanstack-react-query/src/gql/gql.ts index 6b28626a405..ab23ba6aa75 100644 --- a/examples/react/tanstack-react-query/src/gql/gql.ts +++ b/examples/react/tanstack-react-query/src/gql/gql.ts @@ -1,6 +1,5 @@ /* eslint-disable */ import * as types from './graphql'; -import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; /** * Map of all GraphQL operations in the project. @@ -19,40 +18,19 @@ const documents = { types.FilmItemFragmentDoc, }; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - * - * - * @example - * ```ts - * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); - * ``` - * - * The query argument is unknown! - * Please regenerate the types. - */ -export function graphql(source: string): unknown; - /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( source: '\n query allFilmsWithVariablesQuery($first: Int!) {\n allFilms(first: $first) {\n edges {\n node {\n ...FilmItem\n }\n }\n }\n }\n' -): (typeof documents)['\n query allFilmsWithVariablesQuery($first: Int!) {\n allFilms(first: $first) {\n edges {\n node {\n ...FilmItem\n }\n }\n }\n }\n']; +): typeof import('./graphql').AllFilmsWithVariablesQueryDocument; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( source: '\n fragment FilmItem on Film {\n id\n title\n releaseDate\n producers\n }\n' -): (typeof documents)['\n fragment FilmItem on Film {\n id\n title\n releaseDate\n producers\n }\n']; +): typeof import('./graphql').FilmItemFragmentDoc; export function graphql(source: string) { return (documents as any)[source] ?? {}; } - -export type DocumentType> = TDocumentNode extends DocumentNode< - infer TType, - any -> - ? TType - : never; diff --git a/examples/react/tanstack-react-query/src/gql/graphql.ts b/examples/react/tanstack-react-query/src/gql/graphql.ts index 9ca792fa77d..239024a493c 100644 --- a/examples/react/tanstack-react-query/src/gql/graphql.ts +++ b/examples/react/tanstack-react-query/src/gql/graphql.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { DocumentTypeDecoration } from '@graphql-typed-document-node/core'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -1296,91 +1296,41 @@ export type FilmItemFragment = { producers?: Array | null; } & { ' $fragmentName'?: 'FilmItemFragment' }; -export const FilmItemFragmentDoc = { - kind: 'Document', - definitions: [ - { - kind: 'FragmentDefinition', - name: { kind: 'Name', value: 'FilmItem' }, - typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Film' } }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'id' } }, - { kind: 'Field', name: { kind: 'Name', value: 'title' } }, - { kind: 'Field', name: { kind: 'Name', value: 'releaseDate' } }, - { kind: 'Field', name: { kind: 'Name', value: 'producers' } }, - ], - }, - }, - ], -} as unknown as DocumentNode; -export const AllFilmsWithVariablesQueryDocument = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'query', - name: { kind: 'Name', value: 'allFilmsWithVariablesQuery' }, - variableDefinitions: [ - { - kind: 'VariableDefinition', - variable: { kind: 'Variable', name: { kind: 'Name', value: 'first' } }, - type: { kind: 'NonNullType', type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } } }, - }, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'allFilms' }, - arguments: [ - { - kind: 'Argument', - name: { kind: 'Name', value: 'first' }, - value: { kind: 'Variable', name: { kind: 'Name', value: 'first' } }, - }, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'edges' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'node' }, - selectionSet: { - kind: 'SelectionSet', - selections: [{ kind: 'FragmentSpread', name: { kind: 'Name', value: 'FilmItem' } }], - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - { - kind: 'FragmentDefinition', - name: { kind: 'Name', value: 'FilmItem' }, - typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Film' } }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'id' } }, - { kind: 'Field', name: { kind: 'Name', value: 'title' } }, - { kind: 'Field', name: { kind: 'Name', value: 'releaseDate' } }, - { kind: 'Field', name: { kind: 'Name', value: 'producers' } }, - ], - }, - }, - ], -} as unknown as DocumentNode; +export class TypedDocumentString + extends String + implements DocumentTypeDecoration +{ + __apiType?: DocumentTypeDecoration['__apiType']; + + constructor(private value: string, public __meta__?: { hash: string }) { + super(value); + } + + toString(): string & DocumentTypeDecoration { + return this.value; + } +} +export const FilmItemFragmentDoc = new TypedDocumentString(` + fragment FilmItem on Film { + id + title + releaseDate + producers +} + `) as unknown as TypedDocumentString; +export const AllFilmsWithVariablesQueryDocument = new TypedDocumentString(` + query allFilmsWithVariablesQuery($first: Int!) { + allFilms(first: $first) { + edges { + node { + ...FilmItem + } + } + } +} + fragment FilmItem on Film { + id + title + releaseDate + producers +}`) as unknown as TypedDocumentString; diff --git a/examples/react/tanstack-react-query/src/use-graphql.ts b/examples/react/tanstack-react-query/src/use-graphql.ts index eab06ff2171..97131041e0d 100644 --- a/examples/react/tanstack-react-query/src/use-graphql.ts +++ b/examples/react/tanstack-react-query/src/use-graphql.ts @@ -1,29 +1,29 @@ /* eslint-disable import/no-extraneous-dependencies */ -import { buildHTTPExecutor } from '@graphql-tools/executor-http'; -import { TypedDocumentNode } from '@graphql-typed-document-node/core'; -import { ASTNode, ExecutionResult, Kind, OperationDefinitionNode } from 'graphql'; +import { ExecutionResult } from 'graphql'; import { useQuery } from '@tanstack/react-query'; - -const executor = buildHTTPExecutor({ - endpoint: 'https://swapi-graphql.netlify.app/.netlify/functions/index', -}); - -const isOperationDefinition = (def: ASTNode): def is OperationDefinitionNode => def.kind === Kind.OPERATION_DEFINITION; +import { TypedDocumentString } from './gql/graphql'; export function useGraphQL( - document: TypedDocumentNode, + document: TypedDocumentString, ...[variables]: TVariables extends Record ? [] : [TVariables] ) { return useQuery( [ // This logic can be customized as desired - document.definitions.find(isOperationDefinition)?.name, + document, variables, ] as const, - async ({ queryKey }) => - executor({ - document: document as any, - variables: queryKey[1] as any, - }) as Promise> + async ({ queryKey }) => { + return fetch('https://swapi-graphql.netlify.app/.netlify/functions/index', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: queryKey[0].toString(), + variables: queryKey[1], + }), + }).then(response => response.json()) as Promise>; + } ); } diff --git a/examples/react/urql/codegen.ts b/examples/react/urql/codegen.ts index 40823b13be7..abe3f780e4f 100644 --- a/examples/react/urql/codegen.ts +++ b/examples/react/urql/codegen.ts @@ -7,6 +7,9 @@ const config: CodegenConfig = { generates: { './src/gql/': { preset: 'client', + config: { + documentMode: 'string', + }, }, }, hooks: { afterAllFileWrite: ['prettier --write'] }, diff --git a/examples/react/urql/src/App.tsx b/examples/react/urql/src/App.tsx index 1c639dd9cae..883c007bf7d 100644 --- a/examples/react/urql/src/App.tsx +++ b/examples/react/urql/src/App.tsx @@ -1,11 +1,12 @@ -import React from 'react'; +import { DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +import { AnyVariables, OperationContext, RequestPolicy, useQuery, UseQueryResponse } from 'urql'; +import type { DocumentNode } from 'graphql'; import './App.css'; import Film from './Film'; import { graphql } from './gql'; -import { useQuery } from 'urql'; const allFilmsWithVariablesQueryDocument = graphql(/* GraphQL */ ` - query allFilmsWithVariablesQuery($first: Int!) { + query allFilmsWithVariablesQuery199($first: Int!) { allFilms(first: $first) { edges { node { @@ -16,9 +17,35 @@ const allFilmsWithVariablesQueryDocument = graphql(/* GraphQL */ ` } `); +declare module 'urql' { + // @ts-expect-error this is just temporary until we update types in urql + export type UseQueryArgs = { + query: string | DocumentNode | DocumentTypeDecoration; + requestPolicy?: RequestPolicy; + context?: Partial; + pause?: boolean; + } & (Variables extends void + ? { + variables?: Variables; + } + : Variables extends { + [P in keyof Variables]: Variables[P] | null; + } + ? { + variables?: Variables; + } + : { + variables: Variables; + }); + + export function useQuery( + args: UseQueryArgs + ): UseQueryResponse; +} + function App() { const [{ data }] = useQuery({ - query: allFilmsWithVariablesQueryDocument, + query: allFilmsWithVariablesQueryDocument.toString(), variables: { first: 10, }, diff --git a/examples/react/urql/src/gql/fragment-masking.ts b/examples/react/urql/src/gql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/examples/react/urql/src/gql/fragment-masking.ts +++ b/examples/react/urql/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/react/urql/src/gql/gql.ts b/examples/react/urql/src/gql/gql.ts index 6b28626a405..1e0041c240a 100644 --- a/examples/react/urql/src/gql/gql.ts +++ b/examples/react/urql/src/gql/gql.ts @@ -1,6 +1,5 @@ /* eslint-disable */ import * as types from './graphql'; -import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; /** * Map of all GraphQL operations in the project. @@ -13,46 +12,25 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ * Therefore it is highly recommended to use the babel or swc plugin for production. */ const documents = { - '\n query allFilmsWithVariablesQuery($first: Int!) {\n allFilms(first: $first) {\n edges {\n node {\n ...FilmItem\n }\n }\n }\n }\n': - types.AllFilmsWithVariablesQueryDocument, + '\n query allFilmsWithVariablesQuery199($first: Int!) {\n allFilms(first: $first) {\n edges {\n node {\n ...FilmItem\n }\n }\n }\n }\n': + types.AllFilmsWithVariablesQuery199Document, '\n fragment FilmItem on Film {\n id\n title\n releaseDate\n producers\n }\n': types.FilmItemFragmentDoc, }; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - * - * - * @example - * ```ts - * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); - * ``` - * - * The query argument is unknown! - * Please regenerate the types. - */ -export function graphql(source: string): unknown; - /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: '\n query allFilmsWithVariablesQuery($first: Int!) {\n allFilms(first: $first) {\n edges {\n node {\n ...FilmItem\n }\n }\n }\n }\n' -): (typeof documents)['\n query allFilmsWithVariablesQuery($first: Int!) {\n allFilms(first: $first) {\n edges {\n node {\n ...FilmItem\n }\n }\n }\n }\n']; + source: '\n query allFilmsWithVariablesQuery199($first: Int!) {\n allFilms(first: $first) {\n edges {\n node {\n ...FilmItem\n }\n }\n }\n }\n' +): typeof import('./graphql').AllFilmsWithVariablesQuery199Document; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( source: '\n fragment FilmItem on Film {\n id\n title\n releaseDate\n producers\n }\n' -): (typeof documents)['\n fragment FilmItem on Film {\n id\n title\n releaseDate\n producers\n }\n']; +): typeof import('./graphql').FilmItemFragmentDoc; export function graphql(source: string) { return (documents as any)[source] ?? {}; } - -export type DocumentType> = TDocumentNode extends DocumentNode< - infer TType, - any -> - ? TType - : never; diff --git a/examples/react/urql/src/gql/graphql.ts b/examples/react/urql/src/gql/graphql.ts index 9ca792fa77d..b62e6096cf6 100644 --- a/examples/react/urql/src/gql/graphql.ts +++ b/examples/react/urql/src/gql/graphql.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { DocumentTypeDecoration } from '@graphql-typed-document-node/core'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -1273,11 +1273,11 @@ export type VehiclesEdge = { node?: Maybe; }; -export type AllFilmsWithVariablesQueryQueryVariables = Exact<{ +export type AllFilmsWithVariablesQuery199QueryVariables = Exact<{ first: Scalars['Int']; }>; -export type AllFilmsWithVariablesQueryQuery = { +export type AllFilmsWithVariablesQuery199Query = { __typename?: 'Root'; allFilms?: { __typename?: 'FilmsConnection'; @@ -1296,91 +1296,41 @@ export type FilmItemFragment = { producers?: Array | null; } & { ' $fragmentName'?: 'FilmItemFragment' }; -export const FilmItemFragmentDoc = { - kind: 'Document', - definitions: [ - { - kind: 'FragmentDefinition', - name: { kind: 'Name', value: 'FilmItem' }, - typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Film' } }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'id' } }, - { kind: 'Field', name: { kind: 'Name', value: 'title' } }, - { kind: 'Field', name: { kind: 'Name', value: 'releaseDate' } }, - { kind: 'Field', name: { kind: 'Name', value: 'producers' } }, - ], - }, - }, - ], -} as unknown as DocumentNode; -export const AllFilmsWithVariablesQueryDocument = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'query', - name: { kind: 'Name', value: 'allFilmsWithVariablesQuery' }, - variableDefinitions: [ - { - kind: 'VariableDefinition', - variable: { kind: 'Variable', name: { kind: 'Name', value: 'first' } }, - type: { kind: 'NonNullType', type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } } }, - }, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'allFilms' }, - arguments: [ - { - kind: 'Argument', - name: { kind: 'Name', value: 'first' }, - value: { kind: 'Variable', name: { kind: 'Name', value: 'first' } }, - }, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'edges' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'node' }, - selectionSet: { - kind: 'SelectionSet', - selections: [{ kind: 'FragmentSpread', name: { kind: 'Name', value: 'FilmItem' } }], - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - { - kind: 'FragmentDefinition', - name: { kind: 'Name', value: 'FilmItem' }, - typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Film' } }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'id' } }, - { kind: 'Field', name: { kind: 'Name', value: 'title' } }, - { kind: 'Field', name: { kind: 'Name', value: 'releaseDate' } }, - { kind: 'Field', name: { kind: 'Name', value: 'producers' } }, - ], - }, - }, - ], -} as unknown as DocumentNode; +export class TypedDocumentString + extends String + implements DocumentTypeDecoration +{ + __apiType?: DocumentTypeDecoration['__apiType']; + + constructor(private value: string, public __meta__?: { hash: string }) { + super(value); + } + + toString(): string & DocumentTypeDecoration { + return this.value; + } +} +export const FilmItemFragmentDoc = new TypedDocumentString(` + fragment FilmItem on Film { + id + title + releaseDate + producers +} + `) as unknown as TypedDocumentString; +export const AllFilmsWithVariablesQuery199Document = new TypedDocumentString(` + query allFilmsWithVariablesQuery199($first: Int!) { + allFilms(first: $first) { + edges { + node { + ...FilmItem + } + } + } +} + fragment FilmItem on Film { + id + title + releaseDate + producers +}`) as unknown as TypedDocumentString; diff --git a/examples/typescript-esm/package.json b/examples/typescript-esm/package.json index 0667eff5b57..ad5379b7ebf 100644 --- a/examples/typescript-esm/package.json +++ b/examples/typescript-esm/package.json @@ -7,7 +7,7 @@ "@graphql-codegen/client-preset": "2.1.1" }, "dependencies": { - "@graphql-typed-document-node/core": "3.1.2", + "@graphql-typed-document-node/core": "3.2.0", "graphql": "16.6.0" }, "scripts": { diff --git a/examples/typescript-esm/src/gql/fragment-masking.ts b/examples/typescript-esm/src/gql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/examples/typescript-esm/src/gql/fragment-masking.ts +++ b/examples/typescript-esm/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/typescript-graphql-request/codegen.ts b/examples/typescript-graphql-request/codegen.ts index e06cff00297..8152d8f114c 100644 --- a/examples/typescript-graphql-request/codegen.ts +++ b/examples/typescript-graphql-request/codegen.ts @@ -7,6 +7,9 @@ const config: CodegenConfig = { generates: { './src/gql/': { preset: 'client', + config: { + documentMode: 'string', + }, }, }, hooks: { afterAllFileWrite: ['prettier --write'] }, diff --git a/examples/typescript-graphql-request/src/gql/fragment-masking.ts b/examples/typescript-graphql-request/src/gql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/examples/typescript-graphql-request/src/gql/fragment-masking.ts +++ b/examples/typescript-graphql-request/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/typescript-graphql-request/src/gql/gql.ts b/examples/typescript-graphql-request/src/gql/gql.ts index 532ceaa9e8b..95a4068c89c 100644 --- a/examples/typescript-graphql-request/src/gql/gql.ts +++ b/examples/typescript-graphql-request/src/gql/gql.ts @@ -1,6 +1,5 @@ /* eslint-disable */ import * as types from './graphql'; -import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; /** * Map of all GraphQL operations in the project. @@ -19,40 +18,19 @@ const documents = { types.AllPeopleWithVariablesQueryDocument, }; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - * - * - * @example - * ```ts - * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); - * ``` - * - * The query argument is unknown! - * Please regenerate the types. - */ -export function graphql(source: string): unknown; - /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( source: '\n query AllPeopleQuery {\n allPeople(first: 5) {\n edges {\n node {\n name\n homeworld {\n name\n }\n }\n }\n }\n }\n' -): (typeof documents)['\n query AllPeopleQuery {\n allPeople(first: 5) {\n edges {\n node {\n name\n homeworld {\n name\n }\n }\n }\n }\n }\n']; +): typeof import('./graphql').AllPeopleQueryDocument; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( source: '\n query AllPeopleWithVariablesQuery($first: Int!) {\n allPeople(first: $first) {\n edges {\n node {\n name\n homeworld {\n name\n }\n }\n }\n }\n }\n' -): (typeof documents)['\n query AllPeopleWithVariablesQuery($first: Int!) {\n allPeople(first: $first) {\n edges {\n node {\n name\n homeworld {\n name\n }\n }\n }\n }\n }\n']; +): typeof import('./graphql').AllPeopleWithVariablesQueryDocument; export function graphql(source: string) { return (documents as any)[source] ?? {}; } - -export type DocumentType> = TDocumentNode extends DocumentNode< - infer TType, - any -> - ? TType - : never; diff --git a/examples/typescript-graphql-request/src/gql/graphql.ts b/examples/typescript-graphql-request/src/gql/graphql.ts index a689c8a2080..1b9d64da87e 100644 --- a/examples/typescript-graphql-request/src/gql/graphql.ts +++ b/examples/typescript-graphql-request/src/gql/graphql.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { DocumentTypeDecoration } from '@graphql-typed-document-node/core'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -1309,122 +1309,46 @@ export type AllPeopleWithVariablesQueryQuery = { } | null; }; -export const AllPeopleQueryDocument = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'query', - name: { kind: 'Name', value: 'AllPeopleQuery' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'allPeople' }, - arguments: [ - { kind: 'Argument', name: { kind: 'Name', value: 'first' }, value: { kind: 'IntValue', value: '5' } }, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'edges' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'node' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'name' } }, - { - kind: 'Field', - name: { kind: 'Name', value: 'homeworld' }, - selectionSet: { - kind: 'SelectionSet', - selections: [{ kind: 'Field', name: { kind: 'Name', value: 'name' } }], - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - ], -} as unknown as DocumentNode; -export const AllPeopleWithVariablesQueryDocument = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'query', - name: { kind: 'Name', value: 'AllPeopleWithVariablesQuery' }, - variableDefinitions: [ - { - kind: 'VariableDefinition', - variable: { kind: 'Variable', name: { kind: 'Name', value: 'first' } }, - type: { kind: 'NonNullType', type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } } }, - }, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'allPeople' }, - arguments: [ - { - kind: 'Argument', - name: { kind: 'Name', value: 'first' }, - value: { kind: 'Variable', name: { kind: 'Name', value: 'first' } }, - }, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'edges' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'node' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'name' } }, - { - kind: 'Field', - name: { kind: 'Name', value: 'homeworld' }, - selectionSet: { - kind: 'SelectionSet', - selections: [{ kind: 'Field', name: { kind: 'Name', value: 'name' } }], - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - ], -} as unknown as DocumentNode; +export class TypedDocumentString + extends String + implements DocumentTypeDecoration +{ + __apiType?: DocumentTypeDecoration['__apiType']; + + constructor(private value: string, public __meta__?: { hash: string }) { + super(value); + } + + toString(): string & DocumentTypeDecoration { + return this.value; + } +} + +export const AllPeopleQueryDocument = new TypedDocumentString(` + query AllPeopleQuery { + allPeople(first: 5) { + edges { + node { + name + homeworld { + name + } + } + } + } +} + `) as unknown as TypedDocumentString; +export const AllPeopleWithVariablesQueryDocument = new TypedDocumentString(` + query AllPeopleWithVariablesQuery($first: Int!) { + allPeople(first: $first) { + edges { + node { + name + homeworld { + name + } + } + } + } +} + `) as unknown as TypedDocumentString; diff --git a/examples/typescript-graphql-request/src/main.ts b/examples/typescript-graphql-request/src/main.ts index 211959f19ab..890790df416 100644 --- a/examples/typescript-graphql-request/src/main.ts +++ b/examples/typescript-graphql-request/src/main.ts @@ -40,9 +40,9 @@ const client = new GraphQLClient(apiUrl); export const getPeople = async (first?: number) => { let res: AllPeopleQueryQuery; if (first) { - res = await client.request(AllPeopleWithVariablesQueryDocument, { first }); + res = await client.request(AllPeopleWithVariablesQueryDocument.toString(), { first }); } else { - res = await client.request(AllPeopleQueryDocument); + res = await client.request(AllPeopleQueryDocument.toString()); } return res?.allPeople?.edges; }; diff --git a/examples/vite/vite-react-cts/package.json b/examples/vite/vite-react-cts/package.json index 56f8d86babb..63607388361 100644 --- a/examples/vite/vite-react-cts/package.json +++ b/examples/vite/vite-react-cts/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@apollo/client": "^3.6.9", - "@graphql-typed-document-node/core": "3.1.2", + "@graphql-typed-document-node/core": "3.2.0", "@vitejs/plugin-react-swc": "^3.0.0", "graphql": "16.6.0", "react": "^18.2.0", diff --git a/examples/vite/vite-react-cts/src/gql/fragment-masking.ts b/examples/vite/vite-react-cts/src/gql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/examples/vite/vite-react-cts/src/gql/fragment-masking.ts +++ b/examples/vite/vite-react-cts/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/vite/vite-react-mts/package.json b/examples/vite/vite-react-mts/package.json index 0444755fb8b..153b96d86ee 100644 --- a/examples/vite/vite-react-mts/package.json +++ b/examples/vite/vite-react-mts/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@apollo/client": "^3.6.9", - "@graphql-typed-document-node/core": "3.1.2", + "@graphql-typed-document-node/core": "3.2.0", "@vitejs/plugin-react-swc": "^3.0.0", "graphql": "16.6.0", "react": "^18.2.0", diff --git a/examples/vite/vite-react-mts/src/gql/fragment-masking.ts b/examples/vite/vite-react-mts/src/gql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/examples/vite/vite-react-mts/src/gql/fragment-masking.ts +++ b/examples/vite/vite-react-mts/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/vite/vite-react-ts/package.json b/examples/vite/vite-react-ts/package.json index 92843028384..a03c588f5cc 100644 --- a/examples/vite/vite-react-ts/package.json +++ b/examples/vite/vite-react-ts/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@apollo/client": "^3.6.9", - "@graphql-typed-document-node/core": "3.1.2", + "@graphql-typed-document-node/core": "3.2.0", "@vitejs/plugin-react-swc": "^3.0.0", "graphql": "16.6.0", "react": "^18.2.0", diff --git a/examples/vite/vite-react-ts/src/gql/fragment-masking.ts b/examples/vite/vite-react-ts/src/gql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/examples/vite/vite-react-ts/src/gql/fragment-masking.ts +++ b/examples/vite/vite-react-ts/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/vue/apollo-composable/src/gql/fragment-masking.ts b/examples/vue/apollo-composable/src/gql/fragment-masking.ts index 6d54e46cde8..bb601eea66b 100644 --- a/examples/vue/apollo-composable/src/gql/fragment-masking.ts +++ b/examples/vue/apollo-composable/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import type { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import type { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/vue/urql/src/gql/fragment-masking.ts b/examples/vue/urql/src/gql/fragment-masking.ts index 6d54e46cde8..bb601eea66b 100644 --- a/examples/vue/urql/src/gql/fragment-masking.ts +++ b/examples/vue/urql/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import type { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import type { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/vue/villus/src/gql/fragment-masking.ts b/examples/vue/villus/src/gql/fragment-masking.ts index 6d54e46cde8..bb601eea66b 100644 --- a/examples/vue/villus/src/gql/fragment-masking.ts +++ b/examples/vue/villus/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import type { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import type { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/examples/yoga-tests/package.json b/examples/yoga-tests/package.json index f276130a18c..e338557969b 100644 --- a/examples/yoga-tests/package.json +++ b/examples/yoga-tests/package.json @@ -6,7 +6,7 @@ "graphql-yoga": "3.7.3" }, "devDependencies": { - "@graphql-typed-document-node/core": "3.1.2", + "@graphql-typed-document-node/core": "3.2.0", "jest": "28.1.3", "babel-jest": "28.1.3", "@graphql-codegen/cli": "3.2.2", diff --git a/examples/yoga-tests/src/gql/fragment-masking.ts b/examples/yoga-tests/src/gql/fragment-masking.ts index 195a11ebd3e..dc2836d43e3 100644 --- a/examples/yoga-tests/src/gql/fragment-masking.ts +++ b/examples/yoga-tests/src/gql/fragment-masking.ts @@ -1,48 +1,46 @@ -import { ResultOf, TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'; -export type FragmentType> = TDocumentType extends DocumentNode< - infer TType, - any -> - ? TType extends { ' $fragmentName'?: infer TKey } - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } +export type FragmentType> = + TDocumentType extends DocumentTypeDecoration + ? TType extends { ' $fragmentName'?: infer TKey } + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never : never - : never - : never; + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, + _documentNode: DocumentTypeDecoration, fragmentType: - | FragmentType> - | ReadonlyArray>> + | FragmentType> + | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData>( +export function makeFragmentData, FT extends ResultOf>( data: FT, _fragment: F ): FragmentType { diff --git a/packages/plugins/other/visitor-plugin-common/src/client-side-base-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/client-side-base-visitor.ts index bba119b7e00..df6196f24e7 100644 --- a/packages/plugins/other/visitor-plugin-common/src/client-side-base-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/client-side-base-visitor.ts @@ -330,7 +330,7 @@ export class ClientSideBaseVisitor< protected _includeFragments(fragments: string[], nodeKind: 'FragmentDefinition' | 'OperationDefinition'): string { if (fragments && fragments.length > 0) { - if (this.config.documentMode === DocumentMode.documentNode) { + if (this.config.documentMode === DocumentMode.documentNode || this.config.documentMode === DocumentMode.string) { return Array.from(this._fragments.values()) .filter(f => fragments.includes(this.getFragmentVariableName(f.name))) .map(fragment => print(fragment.node)) @@ -427,7 +427,23 @@ export class ClientSideBaseVisitor< } if (this.config.documentMode === DocumentMode.string) { - return '`' + doc + '`'; + let meta: ExecutableDocumentNodeMeta | void; + + if (this._onExecutableDocumentNode && node.kind === Kind.OPERATION_DEFINITION) { + meta = this._onExecutableDocumentNode({ + kind: Kind.DOCUMENT, + definitions: gqlTag([doc]).definitions, + }); + + if (meta && this._omitDefinitions === true) { + return `{${`"__meta__":${JSON.stringify(meta)},`.slice(0, -1)}}`; + } + } + if (meta) { + return `new TypedDocumentString(\`${doc}\`, ${JSON.stringify(meta)})`; + } + + return `new TypedDocumentString(\`${doc}\`)`; } const gqlImport = this._parseImport(this.config.gqlImport || 'graphql-tag'); diff --git a/packages/plugins/typescript/gql-tag-operations/src/index.ts b/packages/plugins/typescript/gql-tag-operations/src/index.ts index ee6344abf59..a925fa00e5b 100644 --- a/packages/plugins/typescript/gql-tag-operations/src/index.ts +++ b/packages/plugins/typescript/gql-tag-operations/src/index.ts @@ -1,4 +1,5 @@ import { PluginFunction } from '@graphql-codegen/plugin-helpers'; +import { DocumentMode } from '@graphql-codegen/visitor-plugin-common'; import { Source } from '@graphql-tools/utils'; import { FragmentDefinitionNode, OperationDefinitionNode } from 'graphql'; @@ -27,12 +28,48 @@ export const plugin: PluginFunction<{ augmentedModuleName?: string; gqlTagName?: string; emitLegacyCommonJSImports?: boolean; + documentMode?: DocumentMode; }> = ( _, __, - { sourcesWithOperations, useTypeImports, augmentedModuleName, gqlTagName = 'gql', emitLegacyCommonJSImports }, + { + sourcesWithOperations, + useTypeImports, + augmentedModuleName, + gqlTagName = 'gql', + emitLegacyCommonJSImports, + documentMode, + }, _info ) => { + if (documentMode === DocumentMode.string) { + const code = [`import * as types from './graphql${emitLegacyCommonJSImports ? '' : '.js'}';\n`, `\n`]; + + // We need the mapping from source as written to full document source to + // handle fragments. An identity function would not suffice. + if (sourcesWithOperations.length > 0) { + code.push([...getDocumentRegistryChunk(sourcesWithOperations)].join('')); + } else { + code.push('const documents = {}'); + } + + if (sourcesWithOperations.length > 0) { + code.push( + [...getGqlOverloadChunk(sourcesWithOperations, gqlTagName, 'augmented', emitLegacyCommonJSImports), `\n`].join( + '' + ) + ); + } + + code.push( + [`export function ${gqlTagName}(source: string) {\n`, ` return (documents as any)[source] ?? {};\n`, `}\n`].join( + '' + ) + ); + + return code.join('\n'); + } + if (augmentedModuleName == null) { const code = [ `import * as types from './graphql${emitLegacyCommonJSImports ? '' : '.js'}';\n`, diff --git a/packages/plugins/typescript/typed-document-node/src/index.ts b/packages/plugins/typescript/typed-document-node/src/index.ts index bbc95cd78a8..4602a2e2e45 100644 --- a/packages/plugins/typescript/typed-document-node/src/index.ts +++ b/packages/plugins/typescript/typed-document-node/src/index.ts @@ -33,22 +33,41 @@ export const plugin: PluginFunction = ( const visitor = new TypeScriptDocumentNodesVisitor(schema, allFragments, config, documents); const visitorResult = oldVisit(allAst, { leave: visitor }); + let content: string[] = []; + if (config.documentMode === DocumentMode.string) { + content = [ + `\ +export class TypedDocumentString + extends String + implements DocumentTypeDecoration +{ + __apiType?: DocumentTypeDecoration['__apiType']; + + constructor(private value: string, public __meta__?: { hash: string }) { + super(value); + } + + toString(): string & DocumentTypeDecoration { + return this.value; + } +}`, + ]; + } + return { prepend: allAst.definitions.length === 0 ? [] : visitor.getImports(), - content: [visitor.fragments, ...visitorResult.definitions.filter(t => typeof t === 'string')].join('\n'), + content: [...content, visitor.fragments, ...visitorResult.definitions.filter(t => typeof t === 'string')].join( + '\n' + ), }; }; export const validate: PluginValidateFn = async ( - schema: GraphQLSchema, - documents: Types.DocumentFile[], - config, + _schema: GraphQLSchema, + _documents: Types.DocumentFile[], + _config, outputFile: string ) => { - if (config && config.documentMode === DocumentMode.string) { - throw new Error(`Plugin "typed-document-node" does not allow using 'documentMode: string' configuration!`); - } - if (extname(outputFile) !== '.ts' && extname(outputFile) !== '.tsx') { throw new Error(`Plugin "typed-document-node" requires extension to be ".ts" or ".tsx"!`); } diff --git a/packages/plugins/typescript/typed-document-node/src/visitor.ts b/packages/plugins/typescript/typed-document-node/src/visitor.ts index dd0977eae2f..343c1d3ffe5 100644 --- a/packages/plugins/typescript/typed-document-node/src/visitor.ts +++ b/packages/plugins/typescript/typed-document-node/src/visitor.ts @@ -29,9 +29,9 @@ export class TypeScriptDocumentNodesVisitor extends ClientSideBaseVisitor< schema, fragments, { - documentMode: DocumentMode.documentNodeImportFragments, documentNodeImport: '@graphql-typed-document-node/core#TypedDocumentNode', ...config, + documentMode: config.documentMode || DocumentMode.documentNodeImportFragments, }, {}, documents @@ -46,6 +46,13 @@ export class TypeScriptDocumentNodesVisitor extends ClientSideBaseVisitor< const documentNodeImport = this._parseImport(this.config.documentNodeImport || 'graphql#DocumentNode'); const tagImport = this._generateImport(documentNodeImport, 'DocumentNode', true); this._imports.add(tagImport); + } else if (this.config.documentMode === DocumentMode.string) { + const tagImport = this._generateImport( + { moduleName: '@graphql-typed-document-node/core', propName: 'DocumentTypeDecoration' }, + 'DocumentTypeDecoration', + true + ); + this._imports.add(tagImport); } } @@ -99,6 +106,10 @@ export class TypeScriptDocumentNodesVisitor extends ClientSideBaseVisitor< return ` as unknown as DocumentNode<${resultType}, ${variablesTypes}>`; } + if (this.config.documentMode === DocumentMode.string) { + return ` as unknown as TypedDocumentString<${resultType}, ${variablesTypes}>`; + } + return super.getDocumentNodeSignature(resultType, variablesTypes, node); } } diff --git a/packages/presets/client/package.json b/packages/presets/client/package.json index 7f9273f5587..c38c9e9736b 100644 --- a/packages/presets/client/package.json +++ b/packages/presets/client/package.json @@ -26,7 +26,7 @@ "@graphql-codegen/gql-tag-operations": "2.0.2", "@graphql-codegen/plugin-helpers": "^4.1.0", "@graphql-codegen/visitor-plugin-common": "^3.0.2", - "@graphql-typed-document-node/core": "3.1.2", + "@graphql-typed-document-node/core": "3.2.0", "@graphql-tools/documents": "^0.1.0", "@graphql-tools/utils": "^9.0.0", "tslib": "~2.5.0" diff --git a/packages/presets/client/src/fragment-masking-plugin.ts b/packages/presets/client/src/fragment-masking-plugin.ts index b8b63e276f5..df1224c21ce 100644 --- a/packages/presets/client/src/fragment-masking-plugin.ts +++ b/packages/presets/client/src/fragment-masking-plugin.ts @@ -1,7 +1,7 @@ import type { PluginFunction } from '@graphql-codegen/plugin-helpers'; const fragmentTypeHelper = ` -export type FragmentType> = TDocumentType extends DocumentNode< +export type FragmentType> = TDocumentType extends DocumentTypeDecoration< infer TType, any > @@ -14,7 +14,7 @@ export type FragmentType> = TDocume const makeFragmentDataHelper = ` export function makeFragmentData< - F extends DocumentNode, + F extends DocumentTypeDecoration, FT extends ResultOf >(data: FT, _fragment: F): FragmentType { return data as FragmentType; @@ -35,8 +35,8 @@ const createUnmaskFunctionTypeDefinition = ( unmaskFunctionName = defaultUnmaskFunctionName, opts: { nullable: boolean; list: 'with-list' | 'only-list' | false } ) => `export function ${unmaskFunctionName}( - _documentNode: DocumentNode, - fragmentType: ${modifyType('FragmentType>', opts)} + _documentNode: DocumentTypeDecoration, + fragmentType: ${modifyType('FragmentType>', opts)} ): ${modifyType('TType', opts)}`; const createUnmaskFunctionTypeDefinitions = (unmaskFunctionName = defaultUnmaskFunctionName) => [ @@ -76,7 +76,7 @@ export const plugin: PluginFunction<{ }> = (_, __, { useTypeImports, augmentedModuleName, unmaskFunctionName }, _info) => { const documentNodeImport = `${ useTypeImports ? 'import type' : 'import' - } { ResultOf, TypedDocumentNode as DocumentNode, } from '@graphql-typed-document-node/core';\n`; + } { ResultOf, DocumentTypeDecoration, } from '@graphql-typed-document-node/core';\n`; if (augmentedModuleName == null) { return [ diff --git a/packages/presets/client/src/index.ts b/packages/presets/client/src/index.ts index f72e1b6392c..92d4beddbab 100644 --- a/packages/presets/client/src/index.ts +++ b/packages/presets/client/src/index.ts @@ -118,6 +118,7 @@ export const preset: Types.OutputPreset = { dedupeFragments: options.config.dedupeFragments, nonOptionalTypename: options.config.nonOptionalTypename, avoidOptionals: options.config.avoidOptionals, + documentMode: options.config.documentMode, }; const visitor = new ClientSideBaseVisitor(options.schemaAst!, [], options.config, options.config); diff --git a/packages/presets/client/tests/client-preset.spec.ts b/packages/presets/client/tests/client-preset.spec.ts index b2bb984e8b7..6673406cc4a 100644 --- a/packages/presets/client/tests/client-preset.spec.ts +++ b/packages/presets/client/tests/client-preset.spec.ts @@ -748,10 +748,10 @@ export * from "./gql";`); expect(result).toHaveLength(4); const gqlFile = result.find(file => file.filename === 'out1/fragment-masking.ts'); expect(gqlFile.content).toMatchInlineSnapshot(` - "import { ResultOf, TypedDocumentNode as DocumentNode, } from '@graphql-typed-document-node/core'; + "import { ResultOf, DocumentTypeDecoration, } from '@graphql-typed-document-node/core'; - export type FragmentType> = TDocumentType extends DocumentNode< + export type FragmentType> = TDocumentType extends DocumentTypeDecoration< infer TType, any > @@ -764,34 +764,34 @@ export * from "./gql";`); // return non-nullable if \`fragmentType\` is non-nullable export function iLikeTurtles( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if \`fragmentType\` is nullable export function iLikeTurtles( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if \`fragmentType\` is array of non-nullable export function iLikeTurtles( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if \`fragmentType\` is array of nullable export function iLikeTurtles( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function iLikeTurtles( - _documentNode: DocumentNode, - fragmentType: FragmentType> | ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } export function makeFragmentData< - F extends DocumentNode, + F extends DocumentTypeDecoration, FT extends ResultOf >(data: FT, _fragment: F): FragmentType { return data as FragmentType; @@ -800,32 +800,32 @@ export * from "./gql";`); expect(gqlFile.content).toBeSimilarStringTo(` export function iLikeTurtles( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; `); expect(gqlFile.content).toBeSimilarStringTo(` export function iLikeTurtles( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; `); expect(gqlFile.content).toBeSimilarStringTo(` export function iLikeTurtles( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; `); expect(gqlFile.content).toBeSimilarStringTo(` export function iLikeTurtles( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; `); expect(gqlFile.content).toBeSimilarStringTo(` export function iLikeTurtles( - _documentNode: DocumentNode, - fragmentType: FragmentType> | ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } @@ -1060,6 +1060,67 @@ export * from "./gql.js";`); await cleanUp(); }); + it('should dedupe fragments in a "string" document mode', async () => { + const dir = path.join(__dirname, 'tmp/duplicate-fragments'); + const cleanUp = async () => { + await fs.promises.rm(dir, { recursive: true, force: true }); + }; + + const docPath = path.join(__dirname, 'fixtures/reused-fragment.ts'); + const result = await executeCodegen({ + schema: [ + /* GraphQL */ ` + type Query { + user(id: ID!): User! + event(id: ID!): Event! + } + + type User { + id: ID! + username: String! + email: String! + } + + type Event { + id: ID! + owner: User! + attendees: [User!]! + } + `, + ], + documents: [docPath], + generates: { + 'out1/': { + preset, + plugins: [], + config: { + documentMode: 'string', + }, + }, + }, + }); + + // TODO: Consider using in-memory file system for tests like this. + try { + await cleanUp(); + } catch {} + await fs.promises.mkdir(path.join(dir, 'out1'), { recursive: true }); + for (const file of result) { + if (file.filename === 'out1/graphql.ts') { + await fs.promises.writeFile(path.join(dir, file.filename), file.content, 'utf-8'); + } + } + + const { default: jiti } = await import('jiti'); + const loader = jiti('', {}); + + const { EventQueryDocument } = loader(path.join(dir, 'out1/graphql.ts')); + + expect(EventQueryDocument.match(/fragment SharedComponentFragment on User/g)?.length).toBe(1); + + await cleanUp(); + }); + describe('when no operations are found', () => { it('still generates the helper `graphql()` (or under another `presetConfig.gqlTagName` name) function', async () => { const result = await executeCodegen({ @@ -1612,4 +1673,187 @@ export * from "./gql.js";`); export const OiDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"OI"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"a"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AB"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AC"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AC"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"A"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AA"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"A"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AB"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"A"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AC"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AA"}}]}}]} as unknown as DocumentNode;" `); }); + + describe('documentMode: "string"', () => { + it('generates correct types', async () => { + const result = await executeCodegen({ + schema: [ + /* GraphQL */ ` + type Query { + foo: Foo + foos: [Foo] + } + + type Foo { + value: String + } + `, + ], + documents: path.join(__dirname, 'fixtures/with-fragment.ts'), + generates: { + 'out1/': { + preset, + config: { + documentMode: 'string', + }, + }, + }, + }); + + const graphqlFile = result.find(file => file.filename === 'out1/graphql.ts'); + expect(graphqlFile.content).toMatchInlineSnapshot(` + "/* eslint-disable */ + import { DocumentTypeDecoration } from '@graphql-typed-document-node/core'; + export type Maybe = T | null; + export type InputMaybe = Maybe; + export type Exact = { [K in keyof T]: T[K] }; + export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; + export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; + /** All built-in and custom scalars, mapped to their actual values */ + export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + }; + + export type Foo = { + __typename?: 'Foo'; + value?: Maybe; + }; + + export type Query = { + __typename?: 'Query'; + foo?: Maybe; + foos?: Maybe>>; + }; + + export type FooQueryVariables = Exact<{ [key: string]: never; }>; + + + export type FooQuery = { __typename?: 'Query', foo?: ( + { __typename?: 'Foo' } + & { ' $fragmentRefs'?: { 'FooFragment': FooFragment } } + ) | null }; + + export type FoosQueryVariables = Exact<{ [key: string]: never; }>; + + + export type FoosQuery = { __typename?: 'Query', foos?: Array<( + { __typename?: 'Foo' } + & { ' $fragmentRefs'?: { 'FooFragment': FooFragment } } + ) | null> | null }; + + export type FooFragment = { __typename?: 'Foo', value?: string | null } & { ' $fragmentName'?: 'FooFragment' }; + + export class TypedDocumentString + extends String + implements DocumentTypeDecoration + { + __apiType?: DocumentTypeDecoration['__apiType']; + + constructor(private value: string, public __meta__?: { hash: string }) { + super(value); + } + + toString(): string & DocumentTypeDecoration { + return this.value; + } + } + export const FooFragmentDoc = new TypedDocumentString(\` + fragment Foo on Foo { + value + } + \`) as unknown as TypedDocumentString; + export const FooDocument = new TypedDocumentString(\` + query Foo { + foo { + ...Foo + } + } + fragment Foo on Foo { + value + }\`) as unknown as TypedDocumentString; + export const FoosDocument = new TypedDocumentString(\` + query Foos { + foos { + ...Foo + } + } + fragment Foo on Foo { + value + }\`) as unknown as TypedDocumentString;" + `); + }); + + it('graphql overloads have a nice result type', async () => { + const result = await executeCodegen({ + schema: [ + /* GraphQL */ ` + type Query { + foo: Foo + foos: [Foo] + } + + type Foo { + value: String + } + `, + ], + documents: path.join(__dirname, 'fixtures/with-fragment.ts'), + generates: { + 'out1/': { + preset, + config: { + documentMode: 'string', + }, + }, + }, + }); + + const gqlFile = result.find(file => file.filename === 'out1/gql.ts'); + expect(gqlFile.content).toMatchInlineSnapshot(` + "/* eslint-disable */ + import * as types from './graphql'; + + + + /** + * Map of all GraphQL operations in the project. + * + * This map has several performance disadvantages: + * 1. It is not tree-shakeable, so it will include all operations in the project. + * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. + * 3. It does not support dead code elimination, so it will add unused operations. + * + * Therefore it is highly recommended to use the babel or swc plugin for production. + */ + const documents = { + "\\n query Foo {\\n foo {\\n ...Foo\\n }\\n }\\n": types.FooDocument, + "\\n query Foos {\\n foos {\\n ...Foo\\n }\\n }\\n": types.FoosDocument, + "\\n fragment Foo on Foo {\\n value\\n }\\n": types.FooFragmentDoc, + }; + + /** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ + export function graphql(source: "\\n query Foo {\\n foo {\\n ...Foo\\n }\\n }\\n"): typeof import('./graphql').FooDocument; + /** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ + export function graphql(source: "\\n query Foos {\\n foos {\\n ...Foo\\n }\\n }\\n"): typeof import('./graphql').FoosDocument; + /** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ + export function graphql(source: "\\n fragment Foo on Foo {\\n value\\n }\\n"): typeof import('./graphql').FooFragmentDoc; + + + export function graphql(source: string) { + return (documents as any)[source] ?? {}; + } + " + `); + }); + }); }); diff --git a/website/src/pages/docs/getting-started/esm-typescript-usage.mdx b/website/src/pages/docs/getting-started/esm-typescript-usage.mdx index 811be608a93..a573d447977 100644 --- a/website/src/pages/docs/getting-started/esm-typescript-usage.mdx +++ b/website/src/pages/docs/getting-started/esm-typescript-usage.mdx @@ -76,7 +76,7 @@ This step, however, is fully optional. "@graphql-codegen/gql-tag-operations-preset": "1.5.2" }, "dependencies": { - "@graphql-typed-document-node/core": "3.1.1", + "@graphql-typed-document-node/core": "3.2.0", "graphql": "16.5.0" }, "scripts": { diff --git a/website/src/pages/plugins/presets/preset-client.mdx b/website/src/pages/plugins/presets/preset-client.mdx index 681b26a26ab..df230ca740d 100644 --- a/website/src/pages/plugins/presets/preset-client.mdx +++ b/website/src/pages/plugins/presets/preset-client.mdx @@ -51,6 +51,7 @@ The `client` preset allows the following `config` options: - [`dedupeFragments`](/plugins/typescript/typescript#dedupefragments): Removes fragment duplicates for reducing data transfer. It is done by removing sub-fragments imports from fragment definition. - [`nonOptionalTypename`](/plugins/typescript/typescript#nonoptionaltypename): Automatically adds `__typename` field to the generated types, even when they are not specified in the selection set, and makes it non-optional. - [`avoidOptionals`](/plugins/typescript/typescript#avoidoptionals): This will cause the generator to avoid using TypeScript optionals (`?`) on types. +- [`documentMode`](#documentmode): Allows you to control how the documents are generated. For more information or feature request, please [refer to the repository discussions](https://github.com/dotansimha/graphql-code-generator/discussions). @@ -525,3 +526,86 @@ const nextConfig = { ``` Note that you will need to provide the `artifactDirectory` path that should be the same as the one configured in your `codegen.ts` + +## DocumentMode + +The `DocumentMode` option can be used to control how the plugin will generate the document nodes. + +By default, the generated documents are of type `TypedDocumentNode` which is a fully typed GraphQL operation AST. Example: + +```ts filename="out/gql.ts" +export type HelloQueryVariables = Exact<{ [key: string]: never }> + +export type HelloQuery = { __typename?: 'Query'; hello: string } + +export const HelloDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'hello' }, + selectionSet: { kind: 'SelectionSet', selections: [{ kind: 'Field', name: { kind: 'Name', value: 'hello' } }] } + } + ] +} as unknown as DocumentNode +``` + +The `documentMode` option can be used to change the generated documents to `string`: + +```ts filename="codegen.ts" {9-11} +import { type CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = { + schema: 'schema.graphql', + documents: ['src/**/*.tsx'], + generates: { + './src/gql/': { + preset: 'client', + config: { + documentMode: 'string' + } + } + } +} +``` + +This will generate the following: + +```ts filename="out/gql.ts" +export type HelloQueryVariables = Exact<{ [key: string]: never }> + +export type HelloQuery = { __typename?: 'Query'; hello: string } + +export const HelloDocument = new TypedDocumentString(` + query hello { + hello + } +`) as unknown as TypedDocumentString +``` + +It can then be used as follow: + +```ts filename="Fetch example" +import { graphql } from './gql' + +const helloQuery = graphql(` + query hello { + hello + } +`) + +fetch('https:', { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + query: helloQuery.toString() + }) +}) +``` + +### When to use a string DocumentMode? + +The `string` DocumentMode is useful when you want to reduce the bundle size of your application as you will get string literals instead of typed ASTs. This is useful when your GraphQL client allows you to send a string literal as the query and you don't need to use the AST on the client, e.g. when using `graphql-request`, SWR, React Query, etc. diff --git a/yarn.lock b/yarn.lock index 2f7ce9b90a3..c79bc6b6cdc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2694,11 +2694,16 @@ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052" integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg== -"@graphql-typed-document-node/core@3.1.2", "@graphql-typed-document-node/core@^3.1.1": +"@graphql-typed-document-node/core@3.1.2": version "3.1.2" resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.2.tgz#6fc464307cbe3c8ca5064549b806360d84457b04" integrity sha512-9anpBMM9mEgZN4wr2v8wHJI2/u5TnnggewRN6OlvXTTnuVyoY19X6rOv9XTqKRw6dcGKwZsBi8n0kDE2I5i4VA== +"@graphql-typed-document-node/core@3.2.0", "@graphql-typed-document-node/core@^3.1.1": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" + integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== + "@graphql-yoga/logger@^0.0.1": version "0.0.1" resolved "https://registry.yarnpkg.com/@graphql-yoga/logger/-/logger-0.0.1.tgz#48504fa6ecaee487d9df00fd44c28e356635a324"