@khanacademy/graphql-flow 0.0.2 → 0.2.1

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 (52) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/Readme.md +64 -65
  3. package/dist/cli/config.js +73 -0
  4. package/dist/cli/config.js.flow +104 -0
  5. package/dist/cli/config.js.map +1 -0
  6. package/dist/cli/run.js +159 -0
  7. package/dist/cli/run.js.flow +165 -0
  8. package/dist/cli/run.js.map +1 -0
  9. package/dist/enums.js +2 -1
  10. package/dist/enums.js.map +1 -0
  11. package/dist/generateResponseType.js +170 -60
  12. package/dist/generateResponseType.js.flow +248 -82
  13. package/dist/generateResponseType.js.map +1 -0
  14. package/dist/generateTypeFiles.js +141 -0
  15. package/dist/generateTypeFiles.js.flow +167 -0
  16. package/dist/generateTypeFiles.js.map +1 -0
  17. package/dist/generateVariablesType.js +2 -1
  18. package/dist/generateVariablesType.js.map +1 -0
  19. package/dist/index.js +45 -4
  20. package/dist/index.js.flow +54 -4
  21. package/dist/index.js.map +1 -0
  22. package/dist/jest-mock-graphql-tag.js +22 -107
  23. package/dist/jest-mock-graphql-tag.js.flow +30 -138
  24. package/dist/jest-mock-graphql-tag.js.map +1 -0
  25. package/dist/parser/parse.js +349 -0
  26. package/dist/parser/parse.js.flow +403 -0
  27. package/dist/parser/parse.js.map +1 -0
  28. package/dist/parser/resolve.js +111 -0
  29. package/dist/parser/resolve.js.flow +117 -0
  30. package/dist/parser/resolve.js.map +1 -0
  31. package/dist/schemaFromIntrospectionData.js +2 -1
  32. package/dist/schemaFromIntrospectionData.js.map +1 -0
  33. package/dist/types.js +2 -1
  34. package/dist/types.js.flow +6 -0
  35. package/dist/types.js.map +1 -0
  36. package/dist/utils.js +2 -1
  37. package/dist/utils.js.map +1 -0
  38. package/package.json +9 -5
  39. package/src/__test__/example-schema.graphql +1 -1
  40. package/src/__test__/generateTypeFileContents.test.js +61 -0
  41. package/src/__test__/graphql-flow.test.js +309 -54
  42. package/src/__test__/{jest-mock-graphql-tag.test.js → processPragmas.test.js} +13 -1
  43. package/src/cli/config.js +104 -0
  44. package/src/cli/run.js +165 -0
  45. package/src/generateResponseType.js +248 -82
  46. package/src/generateTypeFiles.js +167 -0
  47. package/src/index.js +54 -4
  48. package/src/jest-mock-graphql-tag.js +30 -138
  49. package/src/parser/__test__/parse.test.js +247 -0
  50. package/src/parser/parse.js +403 -0
  51. package/src/parser/resolve.js +117 -0
  52. package/src/types.js +6 -0
@@ -0,0 +1,167 @@
1
+ // @flow
2
+ // Import this in your jest setup, to mock out graphql-tag!
3
+ import type {DocumentNode} from 'graphql';
4
+ import type {Options, Schema, Scalars} from './types';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import {documentToFlowTypes} from '.';
8
+
9
+ export type ExternalOptions = {
10
+ pragma?: string,
11
+ loosePragma?: string,
12
+ ignorePragma?: string,
13
+ scalars?: Scalars,
14
+ strictNullability?: boolean,
15
+ /**
16
+ * The command that users should run to regenerate the types files.
17
+ */
18
+ regenerateCommand?: string,
19
+ readOnlyArray?: boolean,
20
+ splitTypes?: boolean,
21
+ generatedDirectory?: string,
22
+ exportAllObjectTypes?: boolean,
23
+ };
24
+
25
+ export const indexPrelude = (regenerateCommand?: string): string => `// @flow
26
+ //
27
+ // AUTOGENERATED
28
+ // NOTE: New response types are added to this file automatically.
29
+ // Outdated response types can be removed manually as they are deprecated.
30
+ //${regenerateCommand ? ' To regenerate, run ' + regenerateCommand : ''}
31
+ //
32
+
33
+ `;
34
+
35
+ export const generateTypeFileContents = (
36
+ fileName: string,
37
+ schema: Schema,
38
+ document: DocumentNode,
39
+ options: Options,
40
+ generatedDir: string,
41
+ indexContents: string,
42
+ ): {indexContents: string, files: {[key: string]: string}} => {
43
+ const files = {};
44
+
45
+ /// Write export for __generated__/index.js if it doesn't exist
46
+ const addToIndex = (filePath, typeName) => {
47
+ const newLine = `export type {${typeName}} from './${path.basename(
48
+ filePath,
49
+ )}';`;
50
+ if (indexContents.indexOf('./' + path.basename(filePath)) === -1) {
51
+ indexContents += newLine + '\n';
52
+ } else {
53
+ const lines = indexContents.split('\n').map((line) => {
54
+ if (line.includes('./' + path.basename(filePath))) {
55
+ return newLine;
56
+ }
57
+ return line;
58
+ });
59
+ indexContents = lines.join('\n');
60
+ }
61
+ };
62
+
63
+ const generated = documentToFlowTypes(document, schema, options);
64
+ generated.forEach(({name, typeName, code, isFragment, extraTypes}) => {
65
+ // We write all generated files to a `__generated__` subdir to keep
66
+ // things tidy.
67
+ const targetFileName = `${name}.js`;
68
+ const targetPath = path.join(generatedDir, targetFileName);
69
+
70
+ let fileContents =
71
+ '// @' +
72
+ `flow\n// AUTOGENERATED -- DO NOT EDIT\n` +
73
+ `// Generated for operation '${name}' in file '../${path.basename(
74
+ fileName,
75
+ )}'\n` +
76
+ (options.regenerateCommand
77
+ ? `// To regenerate, run '${options.regenerateCommand}'.\n`
78
+ : '') +
79
+ code;
80
+ if (options.splitTypes && !isFragment) {
81
+ fileContents +=
82
+ `\nexport type ${name} = ${typeName}['response'];\n` +
83
+ `export type ${name}Variables = ${typeName}['variables'];\n`;
84
+ }
85
+ Object.keys(extraTypes).forEach((name) => {
86
+ fileContents += `\n\nexport type ${name} = ${extraTypes[name]};`;
87
+ });
88
+
89
+ addToIndex(targetPath, typeName);
90
+ files[targetPath] =
91
+ fileContents
92
+ // Remove whitespace from the ends of lines; babel's generate sometimes
93
+ // leaves them hanging around.
94
+ .replace(/\s+$/gm, '') + '\n';
95
+ });
96
+
97
+ return {files, indexContents};
98
+ };
99
+
100
+ export const generateTypeFiles = (
101
+ fileName: string,
102
+ schema: Schema,
103
+ document: DocumentNode,
104
+ options: Options,
105
+ ) => {
106
+ const generatedDir = path.join(
107
+ path.dirname(fileName),
108
+ options.generatedDirectory ?? '__generated__',
109
+ );
110
+ const indexFile = path.join(generatedDir, 'index.js');
111
+
112
+ if (!fs.existsSync(generatedDir)) {
113
+ fs.mkdirSync(generatedDir, {recursive: true});
114
+ }
115
+ if (!fs.existsSync(indexFile)) {
116
+ fs.writeFileSync(indexFile, indexPrelude(options.regenerateCommand));
117
+ }
118
+
119
+ const {indexContents, files} = generateTypeFileContents(
120
+ fileName,
121
+ schema,
122
+ document,
123
+ options,
124
+ generatedDir,
125
+ fs.readFileSync(indexFile, 'utf8'),
126
+ );
127
+
128
+ fs.writeFileSync(indexFile, indexContents);
129
+ Object.keys(files).forEach((key) => {
130
+ fs.writeFileSync(key, files[key]);
131
+ });
132
+
133
+ fs.writeFileSync(indexFile, indexContents);
134
+ };
135
+
136
+ export const processPragmas = (
137
+ options: ExternalOptions,
138
+ rawSource: string,
139
+ ): null | Options => {
140
+ if (options.ignorePragma && rawSource.includes(options.ignorePragma)) {
141
+ return null;
142
+ }
143
+
144
+ const autogen = options.loosePragma
145
+ ? rawSource.includes(options.loosePragma)
146
+ : false;
147
+ const autogenStrict = options.pragma
148
+ ? rawSource.includes(options.pragma)
149
+ : false;
150
+ const noPragmas = !options.loosePragma && !options.pragma;
151
+
152
+ if (autogen || autogenStrict || noPragmas) {
153
+ return {
154
+ regenerateCommand: options.regenerateCommand,
155
+ strictNullability: noPragmas
156
+ ? options.strictNullability
157
+ : autogenStrict || !autogen,
158
+ readOnlyArray: options.readOnlyArray,
159
+ scalars: options.scalars,
160
+ splitTypes: options.splitTypes,
161
+ generatedDirectory: options.generatedDirectory,
162
+ exportAllObjectTypes: options.exportAllObjectTypes,
163
+ };
164
+ } else {
165
+ return null;
166
+ }
167
+ };
package/src/index.js CHANGED
@@ -9,7 +9,11 @@
9
9
  */
10
10
  import type {DefinitionNode, DocumentNode} from 'graphql';
11
11
 
12
- import {generateResponseType} from './generateResponseType';
12
+ import generate from '@babel/generator'; // eslint-disable-line flowtype-errors/uncovered
13
+ import {
14
+ generateFragmentType,
15
+ generateResponseType,
16
+ } from './generateResponseType';
13
17
  import {generateVariablesType} from './generateVariablesType';
14
18
  export {spyOnGraphqlTagToCollectQueries} from './jest-mock-graphql-tag';
15
19
 
@@ -36,6 +40,8 @@ const optionsToConfig = (
36
40
  fragments,
37
41
  schema,
38
42
  errors,
43
+ allObjectTypes: null,
44
+ path: [],
39
45
  ...internalOptions,
40
46
  };
41
47
 
@@ -58,6 +64,8 @@ export const documentToFlowTypes = (
58
64
  name: string,
59
65
  typeName: string,
60
66
  code: string,
67
+ isFragment?: boolean,
68
+ extraTypes: {[key: string]: string},
61
69
  }> => {
62
70
  const errors: Array<string> = [];
63
71
  const config = optionsToConfig(
@@ -68,21 +76,63 @@ export const documentToFlowTypes = (
68
76
  );
69
77
  const result = document.definitions
70
78
  .map((item) => {
79
+ if (item.kind === 'FragmentDefinition') {
80
+ const name = item.name.value;
81
+ const types = {};
82
+ const code = `export type ${name} = ${generateFragmentType(
83
+ schema,
84
+ item,
85
+ {
86
+ ...config,
87
+ path: [name],
88
+ allObjectTypes: options?.exportAllObjectTypes
89
+ ? types
90
+ : null,
91
+ },
92
+ )};`;
93
+ const extraTypes: {[key: string]: string} = {};
94
+ Object.keys(types).forEach((k) => {
95
+ // eslint-disable-next-line flowtype-errors/uncovered
96
+ extraTypes[k] = generate(types[k]).code;
97
+ });
98
+ return {
99
+ name,
100
+ typeName: name,
101
+ code,
102
+ isFragment: true,
103
+ extraTypes,
104
+ };
105
+ }
71
106
  if (
72
107
  item.kind === 'OperationDefinition' &&
73
108
  (item.operation === 'query' || item.operation === 'mutation') &&
74
109
  item.name
75
110
  ) {
111
+ const types = {};
76
112
  const name = item.name.value;
77
- const response = generateResponseType(schema, item, config);
78
- const variables = generateVariablesType(schema, item, config);
113
+ const response = generateResponseType(schema, item, {
114
+ ...config,
115
+ path: [name],
116
+ allObjectTypes: options?.exportAllObjectTypes
117
+ ? types
118
+ : null,
119
+ });
120
+ const variables = generateVariablesType(schema, item, {
121
+ ...config,
122
+ path: [name],
123
+ });
79
124
 
80
125
  const typeName = `${name}Type`;
81
126
  // TODO(jared): Maybe make this template configurable?
82
127
  // We'll see what's required to get webapp on board.
83
128
  const code = `export type ${typeName} = {|\n variables: ${variables},\n response: ${response}\n|};`;
84
129
 
85
- return {name, typeName, code};
130
+ const extraTypes: {[key: string]: string} = {};
131
+ Object.keys(types).forEach((k) => {
132
+ // eslint-disable-next-line flowtype-errors/uncovered
133
+ extraTypes[k] = generate(types[k]).code;
134
+ });
135
+ return {name, typeName, code, extraTypes};
86
136
  }
87
137
  })
88
138
  .filter(Boolean);
@@ -1,119 +1,16 @@
1
1
  // @flow
2
2
  // Import this in your jest setup, to mock out graphql-tag!
3
3
  import type {DocumentNode, IntrospectionQuery} from 'graphql';
4
- import type {Schema, Options, Scalars} from './types';
5
4
  import {validate} from 'graphql/validation';
6
5
  import {buildClientSchema} from 'graphql';
7
6
  import {print} from 'graphql/language/printer';
8
7
  import {addTypenameToDocument} from 'apollo-utilities'; // eslint-disable-line flowtype-errors/uncovered
9
8
  import {schemaFromIntrospectionData} from './schemaFromIntrospectionData';
10
-
11
- const indexPrelude = (regenerateCommand?: string) => `// @flow
12
- //
13
- // AUTOGENERATED
14
- // NOTE: New response types are added to this file automatically.
15
- // Outdated response types can be removed manually as they are deprecated.
16
- //${regenerateCommand ? ' To regenerate, run ' + regenerateCommand : ''}
17
- //
18
-
19
- `;
20
-
21
- const generateTypeFiles = (
22
- schema: Schema,
23
- document: DocumentNode,
24
- options: Options,
25
- ) => {
26
- const {documentToFlowTypes} = require('.');
27
- const path = require('path');
28
- const fs = require('fs');
29
- const format: ({text: string}) => string = require('prettier-eslint'); // eslint-disable-line flowtype-errors/uncovered
30
-
31
- const indexFile = (generatedDir) => path.join(generatedDir, 'index.js');
32
-
33
- const maybeCreateGeneratedDir = (generatedDir) => {
34
- if (!fs.existsSync(generatedDir)) {
35
- fs.mkdirSync(generatedDir, {recursive: true});
36
-
37
- // Now write an index.js for each __generated__ dir.
38
- fs.writeFileSync(
39
- indexFile(generatedDir),
40
- indexPrelude(options.regenerateCommand),
41
- );
42
- }
43
- };
44
-
45
- /// Write export for __generated__/index.js if it doesn't exist
46
- const writeToIndex = (filePath, typeName) => {
47
- const index = indexFile(path.dirname(filePath));
48
- const indexContents = fs.readFileSync(index, 'utf8');
49
- const newLine = `export type {${typeName}} from './${path.basename(
50
- filePath,
51
- )}';`;
52
- if (indexContents.indexOf(path.basename(filePath)) === -1) {
53
- fs.appendFileSync(index, newLine + '\n');
54
- } else {
55
- const lines = indexContents.split('\n').map((line) => {
56
- if (line.includes(path.basename(filePath))) {
57
- return newLine;
58
- }
59
- return line;
60
- });
61
- fs.writeFileSync(index, lines.join('\n'));
62
- }
63
- };
64
-
65
- // Get the name of the file that `gql` was called from
66
- const errorLines = new Error().stack.split('\n');
67
- const fileName = errorLines[3].split('(').slice(-1)[0].split(':')[0];
68
-
69
- const generated = documentToFlowTypes(document, schema, options);
70
- generated.forEach(({name, typeName, code}) => {
71
- // We write all generated files to a `__generated__` subdir to keep
72
- // things tidy.
73
- const targetFileName = `${typeName}.js`;
74
- const generatedDir = path.join(path.dirname(fileName), '__generated__');
75
- const targetPath = path.join(generatedDir, targetFileName);
76
-
77
- maybeCreateGeneratedDir(generatedDir);
78
-
79
- // NOTE: Uncomment this to write the query definitions to disk if
80
- // you need to add new features to the flow type generation
81
- // fs.writeFileSync(
82
- // targetFileName + '.query',
83
- // JSON.stringify(definitions, null, 2),
84
- // );
85
- const fileContents = format({
86
- text:
87
- '// @' +
88
- `flow\n// AUTOGENERATED -- DO NOT EDIT\n` +
89
- `// Generated for operation '${name}' in file '../${path.basename(
90
- fileName,
91
- )}'\n` +
92
- (options.regenerateCommand
93
- ? `// To regenerate, run '${options.regenerateCommand}'.\n`
94
- : '') +
95
- code,
96
- });
97
- fs.writeFileSync(targetPath, fileContents);
98
-
99
- writeToIndex(targetPath, typeName);
100
- });
101
- };
9
+ import type {ExternalOptions} from './generateTypeFiles';
10
+ import {generateTypeFiles, processPragmas} from './generateTypeFiles';
102
11
 
103
12
  type GraphqlTagFn = (raw: string, ...args: Array<any>) => DocumentNode;
104
13
 
105
- type SpyOptions = {
106
- pragma?: string,
107
- loosePragma?: string,
108
- scalars?: Scalars,
109
- strictNullability?: boolean,
110
- /**
111
- * The command that users should run to regenerate the types files.
112
- */
113
- regenerateCommand?: string,
114
- readOnlyArray?: boolean,
115
- };
116
-
117
14
  /**
118
15
  * This function is expected to be called like so:
119
16
  *
@@ -133,11 +30,14 @@ type SpyOptions = {
133
30
  * If both pragma and loosePragma are empty, then all graphql
134
31
  * documents will be processed. Otherwise, only documents
135
32
  * with one of the pragmas will be processed.
33
+ * Any operations containing `ignorePragma` (if provided)
34
+ * will be skipped, regardless of whether they contain
35
+ * another specified pragma.
136
36
  */
137
37
  const spyOnGraphqlTagToCollectQueries = (
138
38
  realGraphqlTag: GraphqlTagFn,
139
39
  introspectionData: IntrospectionQuery,
140
- options: SpyOptions = {},
40
+ options: ExternalOptions = {},
141
41
  ): GraphqlTagFn => {
142
42
  const collection: Array<{
143
43
  raw: string,
@@ -148,23 +48,42 @@ const spyOnGraphqlTagToCollectQueries = (
148
48
  const schema = schemaFromIntrospectionData(introspectionData);
149
49
 
150
50
  const wrapper = function gql() {
51
+ // Get the name of the file that `gql` was called from
52
+ const errorLines = new Error().stack.split('\n');
53
+ const fileName = errorLines[2].split('(').slice(-1)[0].split(':')[0];
54
+
151
55
  const document: DocumentNode = realGraphqlTag.apply(this, arguments); // eslint-disable-line flowtype-errors/uncovered
152
56
  const hasNonFragments = document.definitions.some(
153
57
  ({kind}) => kind !== 'FragmentDefinition',
154
58
  );
59
+
60
+ if (
61
+ fileName.includes('course-editor') ||
62
+ fileName.endsWith('_test.js') ||
63
+ fileName.endsWith('.fixture.js')
64
+ ) {
65
+ return document;
66
+ }
67
+
155
68
  if (hasNonFragments) {
156
69
  // eslint-disable-next-line flowtype-errors/uncovered
157
70
  const withTypeNames: DocumentNode = addTypenameToDocument(document);
158
- collection.push({
159
- raw: print(withTypeNames),
160
- errors: validate(clientSchema, document),
161
- });
162
71
 
163
72
  const rawSource: string = arguments[0].raw[0]; // eslint-disable-line flowtype-errors/uncovered
164
73
  const processedOptions = processPragmas(options, rawSource);
165
74
  if (processedOptions) {
166
- generateTypeFiles(schema, withTypeNames, processedOptions);
75
+ generateTypeFiles(
76
+ fileName,
77
+ schema,
78
+ withTypeNames,
79
+ processedOptions,
80
+ );
167
81
  }
82
+ collection.push({
83
+ raw: print(withTypeNames),
84
+ errors: validate(clientSchema, document),
85
+ processed: !!processedOptions,
86
+ });
168
87
  }
169
88
  return document;
170
89
  };
@@ -172,33 +91,6 @@ const spyOnGraphqlTagToCollectQueries = (
172
91
  return wrapper;
173
92
  };
174
93
 
175
- const processPragmas = (
176
- options: SpyOptions,
177
- rawSource: string,
178
- ): null | Options => {
179
- const autogen = options.loosePragma
180
- ? rawSource.includes(options.loosePragma)
181
- : false;
182
- const autogenStrict = options.pragma
183
- ? rawSource.includes(options.pragma)
184
- : false;
185
- const noPragmas = !options.loosePragma && !options.pragma;
186
-
187
- if (autogen || autogenStrict || noPragmas) {
188
- return {
189
- regenerateCommand: options.regenerateCommand,
190
- strictNullability: noPragmas
191
- ? options.strictNullability
192
- : autogenStrict || !autogen,
193
- readOnlyArray: options.readOnlyArray,
194
- scalars: options.scalars,
195
- };
196
- } else {
197
- return null;
198
- }
199
- };
200
-
201
94
  module.exports = {
202
- processPragmas,
203
95
  spyOnGraphqlTagToCollectQueries,
204
96
  };