@khanacademy/graphql-flow 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.eslintrc.js +15 -4
  2. package/.github/workflows/changeset-release.yml +3 -5
  3. package/.github/workflows/pr-checks.yml +17 -17
  4. package/.prettierrc +2 -2
  5. package/.vscode/settings.json +6 -0
  6. package/CHANGELOG.md +12 -0
  7. package/dist/cli/config.js +6 -15
  8. package/dist/cli/run.js +31 -37
  9. package/dist/enums.js +9 -9
  10. package/dist/generateResponseType.js +36 -40
  11. package/dist/generateTypeFiles.js +10 -10
  12. package/dist/generateVariablesType.js +12 -13
  13. package/dist/index.js +4 -6
  14. package/dist/parser/parse.js +47 -53
  15. package/dist/parser/resolve.js +16 -16
  16. package/dist/parser/utils.js +25 -11
  17. package/dist/schemaFromIntrospectionData.js +5 -5
  18. package/dist/utils.js +8 -8
  19. package/package.json +12 -13
  20. package/src/__test__/generateTypeFileContents.test.ts +26 -25
  21. package/src/__test__/graphql-flow.test.ts +32 -33
  22. package/src/__test__/processPragmas.test.ts +14 -13
  23. package/src/cli/__test__/config.test.ts +51 -56
  24. package/src/cli/config.ts +23 -20
  25. package/src/cli/run.ts +45 -46
  26. package/src/enums.ts +17 -17
  27. package/src/generateResponseType.ts +120 -91
  28. package/src/generateTypeFiles.ts +24 -22
  29. package/src/generateVariablesType.ts +20 -20
  30. package/src/index.ts +28 -29
  31. package/src/parser/__test__/parse.test.ts +114 -23
  32. package/src/parser/__test__/utils.test.ts +80 -0
  33. package/src/parser/parse.ts +122 -106
  34. package/src/parser/resolve.ts +61 -34
  35. package/src/parser/utils.ts +28 -11
  36. package/src/schemaFromIntrospectionData.ts +10 -8
  37. package/src/types.ts +57 -53
  38. package/src/utils.ts +30 -16
  39. package/tools/find-files-with-gql.ts +7 -11
@@ -1,15 +1,15 @@
1
- import generate from '@babel/generator'; // eslint-disable-line flowtype-errors/uncovered
2
- import * as babelTypes from '@babel/types';
3
- import type {OperationDefinitionNode, TypeNode} from 'graphql/language/ast';
4
- import type {IntrospectionInputTypeRef} from 'graphql/utilities/introspectionQuery';
5
- import {builtinScalars, enumTypeToFlow, scalarTypeToFlow} from './enums';
6
- import {nullableType, isnNullableType, objectTypeFromProperties} from './utils';
7
- import type {Context, Schema} from './types';
1
+ import generate from "@babel/generator";
2
+ import * as babelTypes from "@babel/types";
3
+ import type {OperationDefinitionNode, TypeNode} from "graphql/language/ast";
4
+ import type {IntrospectionInputTypeRef} from "graphql";
5
+ import {builtinScalars, enumTypeToFlow, scalarTypeToFlow} from "./enums";
6
+ import {nullableType, isnNullableType, objectTypeFromProperties} from "./utils";
7
+ import type {Context, Schema} from "./types";
8
8
  import {
9
9
  liftLeadingPropertyComments,
10
10
  maybeAddDescriptionComment,
11
11
  transferLeadingComments,
12
- } from './utils';
12
+ } from "./utils";
13
13
 
14
14
  export const inputObjectToFlow = (
15
15
  ctx: Context,
@@ -59,7 +59,7 @@ export const inputRefToFlow = (
59
59
  ctx: Context,
60
60
  inputRef: IntrospectionInputTypeRef,
61
61
  ): babelTypes.TSType => {
62
- if (inputRef.kind === 'NON_NULL') {
62
+ if (inputRef.kind === "NON_NULL") {
63
63
  return _inputRefToFlow(ctx, inputRef.ofType);
64
64
  }
65
65
  const result = _inputRefToFlow(ctx, inputRef);
@@ -70,18 +70,18 @@ const _inputRefToFlow = (
70
70
  ctx: Context,
71
71
  inputRef: IntrospectionInputTypeRef,
72
72
  ): babelTypes.TSType => {
73
- if (inputRef.kind === 'SCALAR') {
73
+ if (inputRef.kind === "SCALAR") {
74
74
  return scalarTypeToFlow(ctx, inputRef.name);
75
75
  }
76
- if (inputRef.kind === 'ENUM') {
76
+ if (inputRef.kind === "ENUM") {
77
77
  return enumTypeToFlow(ctx, inputRef.name);
78
78
  }
79
- if (inputRef.kind === 'INPUT_OBJECT') {
79
+ if (inputRef.kind === "INPUT_OBJECT") {
80
80
  return inputObjectToFlow(ctx, inputRef.name);
81
81
  }
82
- if (inputRef.kind === 'LIST') {
82
+ if (inputRef.kind === "LIST") {
83
83
  return babelTypes.tsTypeReference(
84
- babelTypes.identifier('ReadonlyArray'),
84
+ babelTypes.identifier("ReadonlyArray"),
85
85
  babelTypes.tsTypeParameterInstantiation([
86
86
  inputRefToFlow(ctx, inputRef.ofType),
87
87
  ]),
@@ -93,7 +93,7 @@ const _inputRefToFlow = (
93
93
  };
94
94
 
95
95
  const variableToFlow = (ctx: Context, type: TypeNode): babelTypes.TSType => {
96
- if (type.kind === 'NonNullType') {
96
+ if (type.kind === "NonNullType") {
97
97
  return _variableToFlow(ctx, type.type);
98
98
  }
99
99
  const result = _variableToFlow(ctx, type);
@@ -101,7 +101,7 @@ const variableToFlow = (ctx: Context, type: TypeNode): babelTypes.TSType => {
101
101
  };
102
102
 
103
103
  const _variableToFlow = (ctx: Context, type: TypeNode): babelTypes.TSType => {
104
- if (type.kind === 'NamedType') {
104
+ if (type.kind === "NamedType") {
105
105
  if (builtinScalars[type.name.value]) {
106
106
  return scalarTypeToFlow(ctx, type.name.value);
107
107
  }
@@ -116,16 +116,16 @@ const _variableToFlow = (ctx: Context, type: TypeNode): babelTypes.TSType => {
116
116
  }
117
117
  return inputObjectToFlow(ctx, type.name.value);
118
118
  }
119
- if (type.kind === 'ListType') {
119
+ if (type.kind === "ListType") {
120
120
  return babelTypes.tsTypeReference(
121
- babelTypes.identifier('ReadonlyArray'),
121
+ babelTypes.identifier("ReadonlyArray"),
122
122
  babelTypes.tsTypeParameterInstantiation([
123
123
  variableToFlow(ctx, type.type),
124
124
  ]),
125
125
  );
126
126
  }
127
127
  return babelTypes.tsLiteralType(
128
- babelTypes.stringLiteral('UNKNOWN' + JSON.stringify(type)),
128
+ babelTypes.stringLiteral("UNKNOWN" + JSON.stringify(type)),
129
129
  );
130
130
  };
131
131
 
@@ -142,5 +142,5 @@ export const generateVariablesType = (
142
142
  );
143
143
  }),
144
144
  );
145
- return generate(variableObject).code; // eslint-disable-line flowtype-errors/uncovered
145
+ return generate(variableObject).code;
146
146
  };
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {isTruthy} from '@khanacademy/wonder-stuff-core';
1
+ import {isTruthy} from "@khanacademy/wonder-stuff-core";
2
2
  /* eslint-disable no-console */
3
3
  /* flow-uncovered-file */
4
4
  /**
@@ -7,17 +7,17 @@ import {isTruthy} from '@khanacademy/wonder-stuff-core';
7
7
  * It relies on `introspection-query.json` existing in this directory,
8
8
  * which is produced by running `./tools/graphql-flow/sendIntrospection.js`.
9
9
  */
10
- import type {DefinitionNode, DocumentNode} from 'graphql';
10
+ import type {DefinitionNode, DocumentNode} from "graphql";
11
11
 
12
- import generate from '@babel/generator'; // eslint-disable-line flowtype-errors/uncovered
12
+ import generate from "@babel/generator";
13
13
  import {
14
14
  generateFragmentType,
15
15
  generateResponseType,
16
- } from './generateResponseType';
17
- import {generateVariablesType} from './generateVariablesType';
18
- import type {Node} from '@babel/types';
16
+ } from "./generateResponseType";
17
+ import {generateVariablesType} from "./generateVariablesType";
18
+ import type {Node} from "@babel/types";
19
19
 
20
- import type {Context, Schema, GenerateConfig} from './types';
20
+ import type {Context, Schema, GenerateConfig} from "./types";
21
21
 
22
22
  const optionsToConfig = (
23
23
  schema: Schema,
@@ -34,7 +34,7 @@ const optionsToConfig = (
34
34
  } as const;
35
35
  const fragments: Record<string, any> = {};
36
36
  definitions.forEach((def) => {
37
- if (def.kind === 'FragmentDefinition') {
37
+ if (def.kind === "FragmentDefinition") {
38
38
  fragments[def.name.value] = def;
39
39
  }
40
40
  });
@@ -56,22 +56,26 @@ const optionsToConfig = (
56
56
  export class FlowGenerationError extends Error {
57
57
  messages: Array<string>;
58
58
  constructor(errors: Array<string>) {
59
- super(`Graphql-flow type generation failed! ${errors.join('; ')}`);
59
+ super(`Graphql-flow type generation failed! ${errors.join("; ")}`);
60
60
  this.messages = errors;
61
61
  }
62
62
  }
63
63
 
64
- export const documentToFlowTypes = (document: DocumentNode, schema: Schema, options?: GenerateConfig): ReadonlyArray<{
65
- name: string
66
- typeName: string
67
- code: string
68
- isFragment?: boolean
64
+ export const documentToFlowTypes = (
65
+ document: DocumentNode,
66
+ schema: Schema,
67
+ options?: GenerateConfig,
68
+ ): ReadonlyArray<{
69
+ name: string;
70
+ typeName: string;
71
+ code: string;
72
+ isFragment?: boolean;
69
73
  extraTypes: {
70
- [key: string]: string
71
- }
74
+ [key: string]: string;
75
+ };
72
76
  experimentalEnums: {
73
- [key: string]: string
74
- }
77
+ [key: string]: string;
78
+ };
75
79
  }> => {
76
80
  const errors: Array<string> = [];
77
81
  const config = optionsToConfig(
@@ -82,7 +86,7 @@ export const documentToFlowTypes = (document: DocumentNode, schema: Schema, opti
82
86
  );
83
87
  const result = document.definitions
84
88
  .map((item) => {
85
- if (item.kind === 'FragmentDefinition') {
89
+ if (item.kind === "FragmentDefinition") {
86
90
  const name = item.name.value;
87
91
  const types: Record<string, any> = {};
88
92
  const code = `export type ${name} = ${generateFragmentType(
@@ -112,8 +116,8 @@ export const documentToFlowTypes = (document: DocumentNode, schema: Schema, opti
112
116
  };
113
117
  }
114
118
  if (
115
- item.kind === 'OperationDefinition' &&
116
- (item.operation === 'query' || item.operation === 'mutation') &&
119
+ item.kind === "OperationDefinition" &&
120
+ (item.operation === "query" || item.operation === "mutation") &&
117
121
  item.name
118
122
  ) {
119
123
  const types: Record<string, any> = {};
@@ -150,18 +154,13 @@ export const documentToFlowTypes = (document: DocumentNode, schema: Schema, opti
150
154
  return result;
151
155
  };
152
156
 
153
- function codegenExtraTypes(
154
- types: {
155
- [key: string]: Node
156
- },
157
- ): {
158
- [key: string]: string
157
+ function codegenExtraTypes(types: {[key: string]: Node}): {
158
+ [key: string]: string;
159
159
  } {
160
160
  const extraTypes: {
161
- [key: string]: string
161
+ [key: string]: string;
162
162
  } = {};
163
163
  Object.keys(types).forEach((k: string) => {
164
- // eslint-disable-next-line flowtype-errors/uncovered
165
164
  extraTypes[k] = generate(types[k]).code;
166
165
  });
167
166
  return extraTypes;
@@ -1,15 +1,20 @@
1
- import {processFiles} from '../parse';
2
- import {resolveDocuments} from '../resolve';
1
+ import {describe, it, expect} from "@jest/globals";
3
2
 
4
- import {print} from 'graphql/language/printer';
3
+ import {Config} from "../../types";
4
+ import {processFiles} from "../parse";
5
+ import {resolveDocuments} from "../resolve";
6
+
7
+ import {print} from "graphql/language/printer";
5
8
 
6
9
  const fixtureFiles: {
7
- [key: string]: string | {
8
- text: string
9
- resolvedPath: string
10
- }
10
+ [key: string]:
11
+ | string
12
+ | {
13
+ text: string;
14
+ resolvedPath: string;
15
+ };
11
16
  } = {
12
- '/firstFile.js': `
17
+ "/firstFile.js": `
13
18
  // Note that you can import graphql-tag as
14
19
  // something other than gql.
15
20
  import tagme from 'graphql-tag';
@@ -35,7 +40,7 @@ const fixtureFiles: {
35
40
  }
36
41
  \`;`,
37
42
 
38
- '/secondFile.js': `
43
+ "/secondFile.js": `
39
44
  import gql from 'graphql-tag';
40
45
  import {fromFirstFile} from './firstFile.js';
41
46
  // This import won't be followed, because it's not exported
@@ -52,7 +57,7 @@ const fixtureFiles: {
52
57
  \`;
53
58
  export {secondFragment};`,
54
59
 
55
- '/thirdFile.js': `
60
+ "/thirdFile.js": `
56
61
  import {fromFirstFile, alsoFirst, secondFragment} from './secondFile.js';
57
62
  import gql from 'graphql-tag';
58
63
  import type {someType} from './somePlace';
@@ -91,7 +96,7 @@ const fixtureFiles: {
91
96
  \`;
92
97
  }`,
93
98
 
94
- '/invalidThings.js': `
99
+ "/invalidThings.js": `
95
100
  import gql from 'graphql-tag';
96
101
  // Importing a fragment from an npm module is invalid.
97
102
  import someExternalFragment from 'somewhere';
@@ -107,7 +112,7 @@ const fixtureFiles: {
107
112
  \`;
108
113
  `,
109
114
 
110
- '/circular.js': `
115
+ "/circular.js": `
111
116
  import gql from 'graphql-tag';
112
117
  export {otherThing} from './invalidReferences.js';
113
118
  import {one} from './invalidReferences.js';
@@ -119,7 +124,7 @@ const fixtureFiles: {
119
124
  \`;
120
125
  `,
121
126
 
122
- '/invalidReferences.js': `
127
+ "/invalidReferences.js": `
123
128
  import gql from 'graphql-tag';
124
129
  import {otherThing, two, doesntExist} from './circular.js';
125
130
  // 'otherThing' is imported circularly
@@ -149,13 +154,39 @@ const getFileSource = (name: string) => {
149
154
  return fixtureFiles[name];
150
155
  };
151
156
 
152
- describe('processing fragments in various ways', () => {
153
- it('should work', () => {
154
- const files = processFiles(['/thirdFile.js'], getFileSource);
157
+ describe("processing fragments in various ways", () => {
158
+ it("should work", () => {
159
+ const config: Config = {
160
+ crawl: {
161
+ root: "/here/we/crawl",
162
+ },
163
+ generate: {
164
+ match: [/\.fixture\.js$/],
165
+ exclude: [
166
+ "_test\\.js$",
167
+ "\\bcourse-editor-package\\b",
168
+ "\\.fixture\\.js$",
169
+ "\\b__flowtests__\\b",
170
+ "\\bcourse-editor\\b",
171
+ ],
172
+ readOnlyArray: false,
173
+ regenerateCommand: "make gqlflow",
174
+ scalars: {
175
+ JSONString: "string",
176
+ KALocale: "string",
177
+ NaiveDateTime: "string",
178
+ },
179
+ splitTypes: true,
180
+ generatedDirectory: "__graphql-types__",
181
+ exportAllObjectTypes: true,
182
+ schemaFilePath: "./composed_schema.graphql",
183
+ },
184
+ };
185
+ const files = processFiles(["/thirdFile.js"], config, getFileSource);
155
186
  Object.keys(files).forEach((k: any) => {
156
187
  expect(files[k].errors).toEqual([]);
157
188
  });
158
- const {resolved, errors} = resolveDocuments(files);
189
+ const {resolved, errors} = resolveDocuments(files, config);
159
190
  expect(errors).toEqual([]);
160
191
  const printed: Record<string, any> = {};
161
192
  Object.keys(resolved).map(
@@ -215,9 +246,39 @@ describe('processing fragments in various ways', () => {
215
246
  `);
216
247
  });
217
248
 
218
- it('should flag things it doesnt support', () => {
219
- const files = processFiles(['/invalidThings.js'], getFileSource);
220
- expect(files['/invalidThings.js'].errors.map((m: any) => m.message))
249
+ it("should flag things it doesnt support", () => {
250
+ const config: Config = {
251
+ crawl: {
252
+ root: "/here/we/crawl",
253
+ },
254
+ generate: {
255
+ match: [/\.fixture\.js$/],
256
+ exclude: [
257
+ "_test\\.js$",
258
+ "\\bcourse-editor-package\\b",
259
+ "\\.fixture\\.js$",
260
+ "\\b__flowtests__\\b",
261
+ "\\bcourse-editor\\b",
262
+ ],
263
+ readOnlyArray: false,
264
+ regenerateCommand: "make gqlflow",
265
+ scalars: {
266
+ JSONString: "string",
267
+ KALocale: "string",
268
+ NaiveDateTime: "string",
269
+ },
270
+ splitTypes: true,
271
+ generatedDirectory: "__graphql-types__",
272
+ exportAllObjectTypes: true,
273
+ schemaFilePath: "./composed_schema.graphql",
274
+ },
275
+ };
276
+ const files = processFiles(
277
+ ["/invalidThings.js"],
278
+ config,
279
+ getFileSource,
280
+ );
281
+ expect(files["/invalidThings.js"].errors.map((m: any) => m.message))
221
282
  .toMatchInlineSnapshot(`
222
283
  Array [
223
284
  "Unable to resolve someExternalFragment",
@@ -227,12 +288,42 @@ describe('processing fragments in various ways', () => {
227
288
  `);
228
289
  });
229
290
 
230
- it('should flag resolution errors', () => {
231
- const files = processFiles(['/invalidReferences.js'], getFileSource);
291
+ it("should flag resolution errors", () => {
292
+ const config: Config = {
293
+ crawl: {
294
+ root: "/here/we/crawl",
295
+ },
296
+ generate: {
297
+ match: [/\.fixture\.js$/],
298
+ exclude: [
299
+ "_test\\.js$",
300
+ "\\bcourse-editor-package\\b",
301
+ "\\.fixture\\.js$",
302
+ "\\b__flowtests__\\b",
303
+ "\\bcourse-editor\\b",
304
+ ],
305
+ readOnlyArray: false,
306
+ regenerateCommand: "make gqlflow",
307
+ scalars: {
308
+ JSONString: "string",
309
+ KALocale: "string",
310
+ NaiveDateTime: "string",
311
+ },
312
+ splitTypes: true,
313
+ generatedDirectory: "__graphql-types__",
314
+ exportAllObjectTypes: true,
315
+ schemaFilePath: "./composed_schema.graphql",
316
+ },
317
+ };
318
+ const files = processFiles(
319
+ ["/invalidReferences.js"],
320
+ config,
321
+ getFileSource,
322
+ );
232
323
  Object.keys(files).forEach((k: any) => {
233
324
  expect(files[k].errors).toEqual([]);
234
325
  });
235
- const {resolved, errors} = resolveDocuments(files);
326
+ const {resolved, errors} = resolveDocuments(files, config);
236
327
  expect(errors.map((m: any) => m.message)).toMatchInlineSnapshot(`
237
328
  Array [
238
329
  "Circular import /circular.js -> /invalidReferences.js -> /circular.js",
@@ -0,0 +1,80 @@
1
+ import fs from "fs";
2
+ import {describe, it, expect, jest} from "@jest/globals";
3
+ import type {Config} from "../../types";
4
+
5
+ import {getPathWithExtension} from "../utils";
6
+
7
+ const generate = {
8
+ match: [/\.fixture\.js$/],
9
+ exclude: [
10
+ "_test\\.js$",
11
+ "\\bcourse-editor-package\\b",
12
+ "\\.fixture\\.js$",
13
+ "\\b__flowtests__\\b",
14
+ "\\bcourse-editor\\b",
15
+ ],
16
+ readOnlyArray: false,
17
+ regenerateCommand: "make gqlflow",
18
+ scalars: {
19
+ JSONString: "string",
20
+ KALocale: "string",
21
+ NaiveDateTime: "string",
22
+ },
23
+ splitTypes: true,
24
+ generatedDirectory: "__graphql-types__",
25
+ exportAllObjectTypes: true,
26
+ schemaFilePath: "./composed_schema.graphql",
27
+ } as const;
28
+
29
+ const config: Config = {
30
+ crawl: {
31
+ root: "/here/we/crawl",
32
+ },
33
+ generate: [
34
+ {...generate, match: [/^static/], exportAllObjectTypes: false},
35
+ generate,
36
+ ],
37
+ };
38
+
39
+ describe("getPathWithExtension", () => {
40
+ it("should handle a basic missing extension", () => {
41
+ // Arrange
42
+ jest.spyOn(fs, "existsSync").mockImplementation((path) =>
43
+ typeof path === "string" ? path.endsWith(".js") : false,
44
+ );
45
+
46
+ // Act
47
+ const result = getPathWithExtension("/path/to/file", config);
48
+
49
+ // Assert
50
+ expect(result).toBe("/path/to/file.js");
51
+ });
52
+
53
+ it("returns an empty string if no file is found", () => {
54
+ // Arrange
55
+ jest.spyOn(fs, "existsSync").mockImplementation((path) => false);
56
+
57
+ // Act
58
+ const result = getPathWithExtension("/path/to/file", config);
59
+
60
+ // Assert
61
+ expect(result).toBe("");
62
+ });
63
+
64
+ it("maps aliases to their correct value", () => {
65
+ // Arrange
66
+ jest.spyOn(fs, "existsSync").mockImplementation((path) =>
67
+ typeof path === "string" ? path.endsWith(".js") : false,
68
+ );
69
+ const tmpConfig: Config = {
70
+ ...config,
71
+ alias: [{find: "~", replacement: "../../some/prefix"}],
72
+ };
73
+
74
+ // Act
75
+ const result = getPathWithExtension("~/dir/file", tmpConfig);
76
+
77
+ // Assert
78
+ expect(result).toBe("../../some/prefix/dir/file.js");
79
+ });
80
+ });