@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.
- package/CHANGELOG.md +12 -0
- package/Readme.md +49 -65
- package/dist/cli/config.js +73 -0
- package/dist/cli/config.js.flow +101 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/run.js +155 -0
- package/dist/cli/run.js.flow +161 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/enums.js +2 -1
- package/dist/enums.js.map +1 -0
- package/dist/generateResponseType.js +20 -9
- package/dist/generateResponseType.js.flow +25 -17
- package/dist/generateResponseType.js.map +1 -0
- package/dist/generateTypeFiles.js +102 -0
- package/dist/generateTypeFiles.js.flow +127 -0
- package/dist/generateTypeFiles.js.map +1 -0
- package/dist/generateVariablesType.js +2 -1
- package/dist/generateVariablesType.js.map +1 -0
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -0
- package/dist/jest-mock-graphql-tag.js +22 -107
- package/dist/jest-mock-graphql-tag.js.flow +30 -138
- package/dist/jest-mock-graphql-tag.js.map +1 -0
- package/dist/parser/parse.js +349 -0
- package/dist/parser/parse.js.flow +403 -0
- package/dist/parser/parse.js.map +1 -0
- package/dist/parser/resolve.js +111 -0
- package/dist/parser/resolve.js.flow +117 -0
- package/dist/parser/resolve.js.map +1 -0
- package/dist/schemaFromIntrospectionData.js +2 -1
- package/dist/schemaFromIntrospectionData.js.map +1 -0
- package/dist/types.js +2 -1
- package/dist/types.js.map +1 -0
- package/dist/utils.js +2 -1
- package/dist/utils.js.map +1 -0
- package/package.json +9 -5
- package/src/__test__/graphql-flow.test.js +68 -24
- package/src/__test__/{jest-mock-graphql-tag.test.js → processPragmas.test.js} +13 -1
- package/src/cli/config.js +101 -0
- package/src/cli/run.js +161 -0
- package/src/generateResponseType.js +25 -17
- package/src/generateTypeFiles.js +127 -0
- package/src/jest-mock-graphql-tag.js +30 -138
- package/src/parser/__test__/parse.test.js +247 -0
- package/src/parser/parse.js +403 -0
- 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
|
-
|
|
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:
|
|
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(
|
|
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
|
+
});
|