@khanacademy/graphql-flow 0.0.2 → 0.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 (46) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/Readme.md +49 -65
  3. package/dist/cli/config.js +73 -0
  4. package/dist/cli/config.js.flow +101 -0
  5. package/dist/cli/config.js.map +1 -0
  6. package/dist/cli/run.js +155 -0
  7. package/dist/cli/run.js.flow +161 -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 +20 -9
  12. package/dist/generateResponseType.js.flow +25 -17
  13. package/dist/generateResponseType.js.map +1 -0
  14. package/dist/generateTypeFiles.js +102 -0
  15. package/dist/generateTypeFiles.js.flow +127 -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 +2 -1
  20. package/dist/index.js.map +1 -0
  21. package/dist/jest-mock-graphql-tag.js +22 -107
  22. package/dist/jest-mock-graphql-tag.js.flow +30 -138
  23. package/dist/jest-mock-graphql-tag.js.map +1 -0
  24. package/dist/parser/parse.js +349 -0
  25. package/dist/parser/parse.js.flow +403 -0
  26. package/dist/parser/parse.js.map +1 -0
  27. package/dist/parser/resolve.js +111 -0
  28. package/dist/parser/resolve.js.flow +117 -0
  29. package/dist/parser/resolve.js.map +1 -0
  30. package/dist/schemaFromIntrospectionData.js +2 -1
  31. package/dist/schemaFromIntrospectionData.js.map +1 -0
  32. package/dist/types.js +2 -1
  33. package/dist/types.js.map +1 -0
  34. package/dist/utils.js +2 -1
  35. package/dist/utils.js.map +1 -0
  36. package/package.json +9 -5
  37. package/src/__test__/graphql-flow.test.js +68 -24
  38. package/src/__test__/{jest-mock-graphql-tag.test.js → processPragmas.test.js} +13 -1
  39. package/src/cli/config.js +101 -0
  40. package/src/cli/run.js +161 -0
  41. package/src/generateResponseType.js +25 -17
  42. package/src/generateTypeFiles.js +127 -0
  43. package/src/jest-mock-graphql-tag.js +30 -138
  44. package/src/parser/__test__/parse.test.js +247 -0
  45. package/src/parser/parse.js +403 -0
  46. package/src/parser/resolve.js +117 -0
@@ -0,0 +1,127 @@
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
+
6
+ export type ExternalOptions = {
7
+ pragma?: string,
8
+ loosePragma?: string,
9
+ ignorePragma?: string,
10
+ scalars?: Scalars,
11
+ strictNullability?: boolean,
12
+ /**
13
+ * The command that users should run to regenerate the types files.
14
+ */
15
+ regenerateCommand?: string,
16
+ readOnlyArray?: boolean,
17
+ };
18
+
19
+ const indexPrelude = (regenerateCommand?: string) => `// @flow
20
+ //
21
+ // AUTOGENERATED
22
+ // NOTE: New response types are added to this file automatically.
23
+ // Outdated response types can be removed manually as they are deprecated.
24
+ //${regenerateCommand ? ' To regenerate, run ' + regenerateCommand : ''}
25
+ //
26
+
27
+ `;
28
+
29
+ export const generateTypeFiles = (
30
+ fileName: string,
31
+ schema: Schema,
32
+ document: DocumentNode,
33
+ options: Options,
34
+ ) => {
35
+ const {documentToFlowTypes} = require('.');
36
+ const path = require('path');
37
+ const fs = require('fs');
38
+
39
+ const indexFile = (generatedDir) => path.join(generatedDir, 'index.js');
40
+
41
+ const maybeCreateGeneratedDir = (generatedDir) => {
42
+ if (!fs.existsSync(generatedDir)) {
43
+ fs.mkdirSync(generatedDir, {recursive: true});
44
+
45
+ // Now write an index.js for each __generated__ dir.
46
+ fs.writeFileSync(
47
+ indexFile(generatedDir),
48
+ indexPrelude(options.regenerateCommand),
49
+ );
50
+ }
51
+ };
52
+
53
+ /// Write export for __generated__/index.js if it doesn't exist
54
+ const writeToIndex = (filePath, typeName) => {
55
+ const index = indexFile(path.dirname(filePath));
56
+ const indexContents = fs.readFileSync(index, 'utf8');
57
+ const newLine = `export type {${typeName}} from './${path.basename(
58
+ filePath,
59
+ )}';`;
60
+ if (indexContents.indexOf(path.basename(filePath)) === -1) {
61
+ fs.appendFileSync(index, newLine + '\n');
62
+ } else {
63
+ const lines = indexContents.split('\n').map((line) => {
64
+ if (line.includes(path.basename(filePath))) {
65
+ return newLine;
66
+ }
67
+ return line;
68
+ });
69
+ fs.writeFileSync(index, lines.join('\n'));
70
+ }
71
+ };
72
+
73
+ const generated = documentToFlowTypes(document, schema, options);
74
+ generated.forEach(({name, typeName, code}) => {
75
+ // We write all generated files to a `__generated__` subdir to keep
76
+ // things tidy.
77
+ const targetFileName = `${typeName}.js`;
78
+ const generatedDir = path.join(path.dirname(fileName), '__generated__');
79
+ const targetPath = path.join(generatedDir, targetFileName);
80
+
81
+ maybeCreateGeneratedDir(generatedDir);
82
+
83
+ const fileContents =
84
+ '// @' +
85
+ `flow\n// AUTOGENERATED -- DO NOT EDIT\n` +
86
+ `// Generated for operation '${name}' in file '../${path.basename(
87
+ fileName,
88
+ )}'\n` +
89
+ (options.regenerateCommand
90
+ ? `// To regenerate, run '${options.regenerateCommand}'.\n`
91
+ : '') +
92
+ code;
93
+ fs.writeFileSync(targetPath, fileContents);
94
+
95
+ writeToIndex(targetPath, typeName);
96
+ });
97
+ };
98
+
99
+ export const processPragmas = (
100
+ options: ExternalOptions,
101
+ rawSource: string,
102
+ ): null | Options => {
103
+ if (options.ignorePragma && rawSource.includes(options.ignorePragma)) {
104
+ return null;
105
+ }
106
+
107
+ const autogen = options.loosePragma
108
+ ? rawSource.includes(options.loosePragma)
109
+ : false;
110
+ const autogenStrict = options.pragma
111
+ ? rawSource.includes(options.pragma)
112
+ : false;
113
+ const noPragmas = !options.loosePragma && !options.pragma;
114
+
115
+ if (autogen || autogenStrict || noPragmas) {
116
+ return {
117
+ regenerateCommand: options.regenerateCommand,
118
+ strictNullability: noPragmas
119
+ ? options.strictNullability
120
+ : autogenStrict || !autogen,
121
+ readOnlyArray: options.readOnlyArray,
122
+ scalars: options.scalars,
123
+ };
124
+ } else {
125
+ return null;
126
+ }
127
+ };
@@ -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
  };
@@ -0,0 +1,247 @@
1
+ // @flow
2
+
3
+ import {processFiles} from '../parse';
4
+ import {resolveDocuments} from '../resolve';
5
+
6
+ import {print} from 'graphql/language/printer';
7
+
8
+ const fixtureFiles: {[key: string]: string} = {
9
+ '/firstFile.js': `
10
+ // Note that you can import graphql-tag as
11
+ // something other than gql.
12
+ import tagme from 'graphql-tag';
13
+
14
+ // Some complex syntax
15
+ const {x} = {x: 10}
16
+
17
+ const notExported = tagme\`
18
+ fragment Something on Otherthing {
19
+ notExportedAttr
20
+ }
21
+ \`;
22
+
23
+ export const fromFirstFile = tagme\`
24
+ fragment FromFirstFile on Something {
25
+ firstFile
26
+ }
27
+ \`;
28
+
29
+ export const alsoFirst = tagme\`
30
+ fragment AlsoFromFirst on Something {
31
+ name
32
+ }
33
+ \`;`,
34
+
35
+ '/secondFile.js': `
36
+ import gql from 'graphql-tag';
37
+ import {fromFirstFile} from './firstFile.js';
38
+ // This import won't be followed, because it's not exported
39
+ // or used in any graphql documents.
40
+ import hello from './someOtherFile.js';
41
+ // Re-exporting a fragment!
42
+ export {fromFirstFile}
43
+ export {alsoFirst} from './firstFile.js';
44
+
45
+ const secondFragment = gql\`
46
+ fragment SecondFragment on Thing {
47
+ secondAttribute
48
+ }
49
+ \`;
50
+ export {secondFragment};`,
51
+
52
+ '/thirdFile.js': `
53
+ import {fromFirstFile, alsoFirst, secondFragment} from './secondFile.js';
54
+ import gql from 'graphql-tag';
55
+ import type {someType} from './somePlace';
56
+
57
+ export const renamedSecond = secondFragment;
58
+
59
+ const otherTemplate = styled\`lets do this\`;
60
+
61
+ const myQuery = gql\`
62
+ query Some {
63
+ hello
64
+ ...FromFirstFile
65
+ ...AlsoFromFirst
66
+ }
67
+ \${fromFirstFile}
68
+ \${alsoFirst}
69
+ \`;
70
+
71
+ export const runInlineQuery = () => {
72
+ // Here's a fragment defined inline!
73
+ const anotherFragment = gql\`fragment Hello on Something { id }\`;
74
+
75
+ return gql\`
76
+ query InlineQuery {
77
+ hello
78
+
79
+ ok {
80
+ ...Hello
81
+ ...FromFirstFile
82
+ }
83
+ ...SecondFragment
84
+ }
85
+ \${anotherFragment}
86
+ \${fromFirstFile}
87
+ \${renamedSecond}
88
+ \`;
89
+ }`,
90
+
91
+ '/invalidThings.js': `
92
+ import gql from 'graphql-tag';
93
+ // Importing a fragment from an npm module is invalid.
94
+ import someExternalFragment from 'somewhere';
95
+
96
+ const myQuery = gql\`
97
+ query Hello {
98
+ id
99
+ }
100
+ \${someExternalFragment}
101
+ \${someUndefinedFragment}
102
+ // Fancy fragment expressions not supported
103
+ \${2 + 3}
104
+ \`;
105
+ `,
106
+
107
+ '/circular.js': `
108
+ import gql from 'graphql-tag';
109
+ export {otherThing} from './invalidReferences.js';
110
+ import {one} from './invalidReferences.js';
111
+ export const two = gql\`
112
+ fragment Two {
113
+ id
114
+ }
115
+ \${one}
116
+ \`;
117
+ `,
118
+
119
+ '/invalidReferences.js': `
120
+ import gql from 'graphql-tag';
121
+ import {otherThing, two, doesntExist} from './circular.js';
122
+ // 'otherThing' is imported circularly
123
+ export {otherThing}
124
+ const ok = gql\`
125
+ query Hello {
126
+ ...Ok
127
+ }
128
+ \${otherThing}
129
+ \${doesntExist}
130
+ \`;
131
+
132
+ // fragments 'one' & 'two' depend on each other
133
+ export const one = gql\`
134
+ fragment One {
135
+ ...Ok
136
+ }
137
+ \${two}
138
+ \`;
139
+ `,
140
+ };
141
+
142
+ const getFileSource = (name: string) => {
143
+ if (!fixtureFiles[name]) {
144
+ throw new Error(`No file ${name}`);
145
+ }
146
+ return fixtureFiles[name];
147
+ };
148
+
149
+ describe('processing fragments in various ways', () => {
150
+ it('should work', () => {
151
+ const files = processFiles(['/thirdFile.js'], getFileSource);
152
+ Object.keys(files).forEach((k) => {
153
+ expect(files[k].errors).toEqual([]);
154
+ });
155
+ const {resolved, errors} = resolveDocuments(files);
156
+ expect(errors).toEqual([]);
157
+ const printed = {};
158
+ Object.keys(resolved).map(
159
+ (k) => (printed[k] = print(resolved[k].document).trim()),
160
+ );
161
+ expect(printed).toMatchInlineSnapshot(`
162
+ Object {
163
+ "/firstFile.js:15": "fragment FromFirstFile on Something {
164
+ firstFile
165
+ }",
166
+ "/firstFile.js:21": "fragment AlsoFromFirst on Something {
167
+ name
168
+ }",
169
+ "/firstFile.js:9": "fragment Something on Otherthing {
170
+ notExportedAttr
171
+ }",
172
+ "/secondFile.js:11": "fragment SecondFragment on Thing {
173
+ secondAttribute
174
+ }",
175
+ "/thirdFile.js:10": "query Some {
176
+ hello
177
+ ...FromFirstFile
178
+ ...AlsoFromFirst
179
+ }
180
+
181
+ fragment FromFirstFile on Something {
182
+ firstFile
183
+ }
184
+
185
+ fragment AlsoFromFirst on Something {
186
+ name
187
+ }",
188
+ "/thirdFile.js:22": "fragment Hello on Something {
189
+ id
190
+ }",
191
+ "/thirdFile.js:24": "query InlineQuery {
192
+ hello
193
+ ok {
194
+ ...Hello
195
+ ...FromFirstFile
196
+ }
197
+ ...SecondFragment
198
+ }
199
+
200
+ fragment Hello on Something {
201
+ id
202
+ }
203
+
204
+ fragment FromFirstFile on Something {
205
+ firstFile
206
+ }
207
+
208
+ fragment SecondFragment on Thing {
209
+ secondAttribute
210
+ }",
211
+ }
212
+ `);
213
+ });
214
+
215
+ it('should flag things it doesnt support', () => {
216
+ const files = processFiles(['/invalidThings.js'], getFileSource);
217
+ expect(files['/invalidThings.js'].errors.map((m) => m.message))
218
+ .toMatchInlineSnapshot(`
219
+ Array [
220
+ "Unable to resolve someExternalFragment",
221
+ "Unable to resolve someUndefinedFragment",
222
+ "Template literal interpolation must be an identifier",
223
+ ]
224
+ `);
225
+ });
226
+
227
+ it('should flag resolution errors', () => {
228
+ const files = processFiles(['/invalidReferences.js'], getFileSource);
229
+ Object.keys(files).forEach((k) => {
230
+ expect(files[k].errors).toEqual([]);
231
+ });
232
+ const {resolved, errors} = resolveDocuments(files);
233
+ expect(errors.map((m) => m.message)).toMatchInlineSnapshot(`
234
+ Array [
235
+ "Circular import /circular.js -> /invalidReferences.js -> /circular.js",
236
+ "/circular.js has no valid gql export doesntExist",
237
+ "Recursive template dependency! /invalidReferences.js:15 ~ 1,2 -> /circular.js:5 ~ 1,2 -> /invalidReferences.js:15",
238
+ "Recursive template dependency! /circular.js:5 ~ 1,2 -> /invalidReferences.js:15 ~ 1,2 -> /circular.js:5",
239
+ ]
240
+ `);
241
+ const printed = {};
242
+ Object.keys(resolved).map(
243
+ (k) => (printed[k] = print(resolved[k].document).trim()),
244
+ );
245
+ expect(printed).toMatchInlineSnapshot(`Object {}`);
246
+ });
247
+ });