Skip to content

Commit

Permalink
fix: gql-tag-operations generates invalid types on Windows #7362 (#7369)
Browse files Browse the repository at this point in the history
* test: add test reproducing CRLF issue #7362

* fix: gql-tag-operations generates invalid types on Windows #7362

* chore: add changeset
  • Loading branch information
vyacheslav-pushkin committed Jan 17, 2022
1 parent 723321c commit 015d344
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .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
2 changes: 2 additions & 0 deletions .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
54 changes: 53 additions & 1 deletion packages/presets/gql-tag-operations/src/process-sources.ts
Expand Up @@ -7,7 +7,8 @@ export type BuildNameFunction = (type: OperationDefinitionNode | FragmentDefinit
export function processSources(sources: Array<Source>, buildName: BuildNameFunction) {
const sourcesWithOperations: Array<SourceWithOperations> = [];

for (const source of sources) {
for (const originalSource of sources) {
const source = fixLinebreaks(originalSource);
const { document } = source;
const operations: Array<OperationOrFragment> = [];

Expand All @@ -32,3 +33,54 @@ export function processSources(sources: Array<Source>, 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:
*
* <pre><code>
* `
* query a {
* a
* }
* ` === '\n query a {\n a\n }\n '
* </code></pre>
*
* 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
*
* <pre><code>
* `
* query a {
* a
* }
* `
* </code></pre>
*
* 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;
}
@@ -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
}
`);
Expand Up @@ -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<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;"
`);
});

it("follows 'useTypeImports': true", async () => {
const result = await executeCodegen({
schema: [
Expand Down

1 comment on commit 015d344

@vercel
Copy link

@vercel vercel bot commented on 015d344 Jan 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.