diff --git a/.changeset/proud-cycles-design.md b/.changeset/proud-cycles-design.md new file mode 100644 index 00000000000..a0335013563 --- /dev/null +++ b/.changeset/proud-cycles-design.md @@ -0,0 +1,5 @@ +--- +'@graphql-codegen/client-preset': minor +--- + +Preserving Array or ReadonlyArray in useFragment() return type. diff --git a/packages/presets/client/src/fragment-masking-plugin.ts b/packages/presets/client/src/fragment-masking-plugin.ts index fa374b18132..ff50359855a 100644 --- a/packages/presets/client/src/fragment-masking-plugin.ts +++ b/packages/presets/client/src/fragment-masking-plugin.ts @@ -22,52 +22,51 @@ export function makeFragmentData< const defaultUnmaskFunctionName = 'useFragment'; -const modifyType = ( - rawType: string, - opts: { nullable: boolean; list: 'with-list' | 'only-list' | false; empty?: boolean } -) => { - return `${ - opts.list === 'only-list' - ? `ReadonlyArray<${rawType}>` - : opts.list === 'with-list' - ? `${rawType} | ReadonlyArray<${rawType}>` - : rawType - }${opts.nullable ? ' | null | undefined' : ''}`; -}; +const createUnmaskFunctionTypeDefinitions = (unmaskFunctionName = defaultUnmaskFunctionName) => + [ + `// return non-nullable if \`fragmentType\` is non-nullable +export function ${unmaskFunctionName}( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> +): TType;`, -const createUnmaskFunctionTypeDefinition = ( - unmaskFunctionName = defaultUnmaskFunctionName, - opts: { nullable: boolean; list: 'with-list' | 'only-list' | false } -) => { - return `export function ${unmaskFunctionName}( + `// return nullable if \`fragmentType\` is nullable +export function ${unmaskFunctionName}( _documentNode: DocumentTypeDecoration, - fragmentType: ${modifyType(`FragmentType>`, opts)} -): ${modifyType('TType', opts)}`; -}; + fragmentType: FragmentType> | null | undefined +): TType | null | undefined;`, -const createUnmaskFunctionTypeDefinitions = (unmaskFunctionName = defaultUnmaskFunctionName) => [ - `// return non-nullable if \`fragmentType\` is non-nullable\n${createUnmaskFunctionTypeDefinition( - unmaskFunctionName, - { nullable: false, list: false } - )}`, - `// return nullable if \`fragmentType\` is nullable\n${createUnmaskFunctionTypeDefinition(unmaskFunctionName, { - nullable: true, - list: false, - })}`, - `// return array of non-nullable if \`fragmentType\` is array of non-nullable\n${createUnmaskFunctionTypeDefinition( - unmaskFunctionName, - { nullable: false, list: 'only-list' } - )}`, - `// return array of nullable if \`fragmentType\` is array of nullable\n${createUnmaskFunctionTypeDefinition( - unmaskFunctionName, - { nullable: true, list: 'only-list' } - )}`, -]; + `// return array of non-nullable if \`fragmentType\` is array of non-nullable +export function ${unmaskFunctionName}( + _documentNode: DocumentTypeDecoration, + fragmentType: Array>> +): Array;`, + + `// return array of nullable if \`fragmentType\` is array of nullable +export function ${unmaskFunctionName}( + _documentNode: DocumentTypeDecoration, + fragmentType: Array>> | null | undefined +): Array | null | undefined;`, + + `// return readonly array of non-nullable if \`fragmentType\` is array of non-nullable +export function ${unmaskFunctionName}( + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> +): ReadonlyArray;`, + + `// return readonly array of nullable if \`fragmentType\` is array of nullable +export function ${unmaskFunctionName}( + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined +): ReadonlyArray | null | undefined;`, + ].join('\n'); const createUnmaskFunction = (unmaskFunctionName = defaultUnmaskFunctionName) => ` -${createUnmaskFunctionTypeDefinitions(unmaskFunctionName) - .concat(createUnmaskFunctionTypeDefinition(unmaskFunctionName, { nullable: true, list: 'with-list' })) - .join(';\n')} { +${createUnmaskFunctionTypeDefinitions(unmaskFunctionName)} +export function ${unmaskFunctionName}( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | Array>> | ReadonlyArray>> | null | undefined +): TType | Array | ReadonlyArray | null | undefined { return fragmentType as any; } `; diff --git a/packages/presets/client/tests/client-preset.spec.ts b/packages/presets/client/tests/client-preset.spec.ts index adf32fff491..28855aa3853 100644 --- a/packages/presets/client/tests/client-preset.spec.ts +++ b/packages/presets/client/tests/client-preset.spec.ts @@ -781,19 +781,29 @@ export * from "./gql";`); fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if \`fragmentType\` is array of non-nullable + export function iLikeTurtles( + _documentNode: DocumentTypeDecoration, + fragmentType: Array>> + ): Array; + // return array of nullable if \`fragmentType\` is array of nullable + export function iLikeTurtles( + _documentNode: DocumentTypeDecoration, + fragmentType: Array>> | null | undefined + ): Array | null | undefined; + // return readonly array of non-nullable if \`fragmentType\` is array of non-nullable export function iLikeTurtles( _documentNode: DocumentTypeDecoration, fragmentType: ReadonlyArray>> ): ReadonlyArray; - // return array of nullable if \`fragmentType\` is array of nullable + // return readonly array of nullable if \`fragmentType\` is array of nullable export function iLikeTurtles( _documentNode: DocumentTypeDecoration, fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function iLikeTurtles( _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> | ReadonlyArray>> | null | undefined - ): TType | ReadonlyArray | null | undefined { + fragmentType: FragmentType> | Array>> | ReadonlyArray>> | null | undefined + ): TType | Array | ReadonlyArray | null | undefined { return fragmentType as any; } @@ -822,39 +832,6 @@ export * from "./gql";`); } " `); - - expect(gqlFile.content).toBeSimilarStringTo(` - export function iLikeTurtles( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> - ): TType; - `); - expect(gqlFile.content).toBeSimilarStringTo(` - export function iLikeTurtles( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> | null | undefined - ): TType | null | undefined; - `); - expect(gqlFile.content).toBeSimilarStringTo(` - export function iLikeTurtles( - _documentNode: DocumentTypeDecoration, - fragmentType: ReadonlyArray>> - ): ReadonlyArray; - `); - expect(gqlFile.content).toBeSimilarStringTo(` - export function iLikeTurtles( - _documentNode: DocumentTypeDecoration, - fragmentType: ReadonlyArray>> | null | undefined - ): ReadonlyArray | null | undefined; - `); - expect(gqlFile.content).toBeSimilarStringTo(` - export function iLikeTurtles( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> | ReadonlyArray>> | null | undefined - ): TType | ReadonlyArray | null | undefined { - return fragmentType as any; - } - `); }); it('can accept null in useFragment', async () => { @@ -922,6 +899,46 @@ export * from "./gql";`); }, }); + const content = mergeOutputs([ + ...result, + fs.readFileSync(docPath, 'utf8'), + ` + function App(props: { foos: Array> }) { + const fragments: Array = useFragment(Fragment, props.foos); + return fragments.map(f => f.value); + } + `, + ]); + + validateTs(content, undefined, false, true, [`Duplicate identifier 'DocumentNode'.`], true); + }); + + it('useFragment preserves ReadonlyArray type', async () => { + const docPath = path.join(__dirname, 'fixtures/with-fragment.ts'); + const result = await executeCodegen({ + schema: [ + /* GraphQL */ ` + type Query { + foo: Foo + foos: [Foo!] + } + + type Foo { + value: String + } + `, + ], + documents: docPath, + generates: { + 'out1/': { + preset, + presetConfig: { + fragmentMasking: true, + }, + }, + }, + }); + const content = mergeOutputs([ ...result, fs.readFileSync(docPath, 'utf8'),