diff --git a/.changeset/orange-keys-guess.md b/.changeset/orange-keys-guess.md new file mode 100644 index 00000000000..430c4aa75d6 --- /dev/null +++ b/.changeset/orange-keys-guess.md @@ -0,0 +1,5 @@ +--- +'@graphql-codegen/gql-tag-operations-preset': patch +--- + +fix: gql-tag-operations generates invalid types on Windows #7362 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..2080fad512a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# This fixture must contain CRLF as line breaks. See https://github.com/dotansimha/graphql-code-generator/issues/7362 +packages/presets/gql-tag-operations/tests/fixtures/crlf-operation.ts eol=crlf \ No newline at end of file diff --git a/packages/presets/gql-tag-operations/src/process-sources.ts b/packages/presets/gql-tag-operations/src/process-sources.ts index c1dcfb50e14..ebc3305384c 100644 --- a/packages/presets/gql-tag-operations/src/process-sources.ts +++ b/packages/presets/gql-tag-operations/src/process-sources.ts @@ -7,7 +7,8 @@ export type BuildNameFunction = (type: OperationDefinitionNode | FragmentDefinit export function processSources(sources: Array, buildName: BuildNameFunction) { const sourcesWithOperations: Array = []; - for (const source of sources) { + for (const originalSource of sources) { + const source = fixLinebreaks(originalSource); const { document } = source; const operations: Array = []; @@ -32,3 +33,54 @@ export function processSources(sources: Array, buildName: BuildNameFunct return sourcesWithOperations; } + +/** + * https://github.com/dotansimha/graphql-code-generator/issues/7362 + * + * Source file is read by @graphql/tools using fs.promises.readFile, + * which means that the linebreaks are read as-is and the result will be different + * depending on the OS: it will contain LF (\n) on Linux/MacOS and CRLF (\r\n) on Windows. + * + * In most scenarios that would be OK. However, gql-tag-operation is using the resulting string + * as a TypeScript type. Which means that the string will be compared against a template literal, + * for example: + * + *

+ * `
+ * query a {
+ *    a
+ *  }
+ * ` === '\n query a {\n    a\n  }\n '
+ * 
+ * + * According to clause 12.8.6.2 of ECMAScript Language Specification + * (https://tc39.es/ecma262/#sec-static-semantics-trv), + * when comparing strings, JavaScript doesn't care which linebreaks does the source file contain, + * any linebreak (CR, LF or CRLF) is LF from JavaScript standpoint + * (otherwise the result of the above comparison would be OS-dependent, which doesn't make sense). + * + * Therefore gql-tag-operation would break on Windows as it would generate + * + * '\r\n query a {\r\n a\r\n }\r\n ' + * + * which is NOT equal to + * + *

+ * `
+ * query a {
+ *    a
+ *  }
+ * `
+ * 
+ * + * Therefore we need to replace \r\n with \n in the string. + * + * @param source + */ +function fixLinebreaks(source: Source) { + const fixedSource = { ...source }; + + fixedSource.rawSDL = source.rawSDL.replace(/\r\n/g, '\n'); + + return fixedSource; +} diff --git a/packages/presets/gql-tag-operations/tests/fixtures/crlf-operation.ts b/packages/presets/gql-tag-operations/tests/fixtures/crlf-operation.ts new file mode 100644 index 00000000000..085ce59f581 --- /dev/null +++ b/packages/presets/gql-tag-operations/tests/fixtures/crlf-operation.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +//@ts-ignore +import gql from 'gql-tag'; + +//@ts-ignore +const A = gql(/* GraphQL */ ` + query a { + a + } +`); + +//@ts-ignore +const B = gql(/* GraphQL */ ` + query b { + b + } +`); + +//@ts-ignore +const C = gql(/* GraphQL */ ` + fragment C on Query { + c + } +`); diff --git a/packages/presets/gql-tag-operations/tests/gql-tag-operations.spec.ts b/packages/presets/gql-tag-operations/tests/gql-tag-operations.spec.ts index e098c88b74e..aa542f0fbe1 100644 --- a/packages/presets/gql-tag-operations/tests/gql-tag-operations.spec.ts +++ b/packages/presets/gql-tag-operations/tests/gql-tag-operations.spec.ts @@ -90,6 +90,49 @@ describe('gql-tag-operations-preset', () => { `); }); + it('generates \\n regardless of whether the source contains LF or CRLF', async () => { + const result = await executeCodegen({ + schema: [ + /* GraphQL */ ` + type Query { + a: String + b: String + c: String + } + `, + ], + documents: path.join(__dirname, 'fixtures/crlf-operation.ts'), + generates: { + 'out1.ts': { + preset, + plugins: [], + }, + }, + }); + expect(result[0].content).toMatchInlineSnapshot(` + "/* eslint-disable */ + import * as graphql from './graphql'; + import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; + + const documents = { + \\"\\\\n query a {\\\\n a\\\\n }\\\\n\\": graphql.ADocument, + \\"\\\\n query b {\\\\n b\\\\n }\\\\n\\": graphql.BDocument, + \\"\\\\n fragment C on Query {\\\\n c\\\\n }\\\\n\\": graphql.CFragmentDoc, + }; + + export function gql(source: \\"\\\\n query a {\\\\n a\\\\n }\\\\n\\"): (typeof documents)[\\"\\\\n query a {\\\\n a\\\\n }\\\\n\\"]; + export function gql(source: \\"\\\\n query b {\\\\n b\\\\n }\\\\n\\"): (typeof documents)[\\"\\\\n query b {\\\\n b\\\\n }\\\\n\\"]; + export function gql(source: \\"\\\\n fragment C on Query {\\\\n c\\\\n }\\\\n\\"): (typeof documents)[\\"\\\\n fragment C on Query {\\\\n c\\\\n }\\\\n\\"]; + + export function gql(source: string): unknown; + export function gql(source: string) { + return (documents as any)[source] ?? {}; + } + + export type DocumentType> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;" + `); + }); + it("follows 'useTypeImports': true", async () => { const result = await executeCodegen({ schema: [