@graphql-codegen/near-operation-file-preset 3.0.1 → 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.
- package/cjs/fragment-resolver.js +88 -75
- package/cjs/utils.js +100 -19
- package/esm/fragment-resolver.js +89 -76
- package/esm/utils.js +99 -19
- package/package.json +1 -1
- package/typings/fragment-resolver.d.cts +2 -1
- package/typings/fragment-resolver.d.ts +2 -1
- package/typings/utils.d.cts +14 -2
- package/typings/utils.d.ts +14 -2
package/cjs/fragment-resolver.js
CHANGED
|
@@ -5,65 +5,66 @@ const graphql_1 = require("graphql");
|
|
|
5
5
|
const visitor_plugin_common_1 = require("@graphql-codegen/visitor-plugin-common");
|
|
6
6
|
const utils_js_1 = require("./utils.js");
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Creates fragment imports based on possible types and usage
|
|
9
9
|
*/
|
|
10
|
-
function
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
fragmentVariableSuffix: (0, visitor_plugin_common_1.getConfigValue)(config.fragmentVariableSuffix, 'FragmentDoc'),
|
|
10
|
+
function createFragmentImports(baseVisitor, fragmentName, possibleTypes, usedTypes) {
|
|
11
|
+
const fragmentImports = [];
|
|
12
|
+
// Always include the document import
|
|
13
|
+
fragmentImports.push({
|
|
14
|
+
name: baseVisitor.getFragmentVariableName(fragmentName),
|
|
15
|
+
kind: 'document',
|
|
17
16
|
});
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
fragmentImports.push({
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
const fragmentSuffix = baseVisitor.getFragmentSuffix(fragmentName);
|
|
18
|
+
if (possibleTypes.length === 1) {
|
|
19
|
+
fragmentImports.push({
|
|
20
|
+
name: baseVisitor.convertName(fragmentName, {
|
|
21
|
+
useTypesPrefix: true,
|
|
22
|
+
suffix: fragmentSuffix,
|
|
23
|
+
}),
|
|
24
|
+
kind: 'type',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else if (possibleTypes.length > 0) {
|
|
28
|
+
const typesToImport = usedTypes && usedTypes.length > 0 ? usedTypes : possibleTypes;
|
|
29
|
+
typesToImport.forEach(typeName => {
|
|
23
30
|
fragmentImports.push({
|
|
24
|
-
name: baseVisitor.convertName(
|
|
31
|
+
name: baseVisitor.convertName(fragmentName, {
|
|
25
32
|
useTypesPrefix: true,
|
|
26
|
-
suffix: fragmentSuffix,
|
|
33
|
+
suffix: `_${typeName}` + (fragmentSuffix.length > 0 ? `_${fragmentSuffix}` : ''),
|
|
27
34
|
}),
|
|
28
35
|
kind: 'type',
|
|
29
36
|
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
kind: 'type',
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return fragmentImports;
|
|
43
|
-
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return fragmentImports;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Used by `buildFragmentResolver` to build a mapping of fragmentNames to paths, importNames, and other useful info
|
|
43
|
+
*/
|
|
44
|
+
function buildFragmentRegistry(baseVisitor, { generateFilePath }, { documents }, schemaObject) {
|
|
44
45
|
const duplicateFragmentNames = [];
|
|
45
46
|
const registry = documents.reduce((prev, documentRecord) => {
|
|
46
47
|
const fragments = documentRecord.document.definitions.filter(d => d.kind === graphql_1.Kind.FRAGMENT_DEFINITION);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
throw new Error(`Fragment "${fragment.name.value}" is set on non-existing type "${fragment.typeCondition.name.value}"!`);
|
|
52
|
-
}
|
|
53
|
-
const possibleTypes = (0, visitor_plugin_common_1.getPossibleTypes)(schemaObject, schemaType);
|
|
54
|
-
const filePath = generateFilePath(documentRecord.location);
|
|
55
|
-
const imports = getFragmentImports(possibleTypes.map(t => t.name), fragment.name.value);
|
|
56
|
-
if (prev[fragment.name.value] &&
|
|
57
|
-
(0, graphql_1.print)(fragment) !== (0, graphql_1.print)(prev[fragment.name.value].node)) {
|
|
58
|
-
duplicateFragmentNames.push(fragment.name.value);
|
|
59
|
-
}
|
|
60
|
-
prev[fragment.name.value] = {
|
|
61
|
-
filePath,
|
|
62
|
-
imports,
|
|
63
|
-
onType: fragment.typeCondition.name.value,
|
|
64
|
-
node: fragment,
|
|
65
|
-
};
|
|
48
|
+
for (const fragment of fragments) {
|
|
49
|
+
const schemaType = schemaObject.getType(fragment.typeCondition.name.value);
|
|
50
|
+
if (!schemaType) {
|
|
51
|
+
throw new Error(`Fragment "${fragment.name.value}" is set on non-existing type "${fragment.typeCondition.name.value}"!`);
|
|
66
52
|
}
|
|
53
|
+
const fragmentName = fragment.name.value;
|
|
54
|
+
const filePath = generateFilePath(documentRecord.location);
|
|
55
|
+
const possibleTypes = (0, visitor_plugin_common_1.getPossibleTypes)(schemaObject, schemaType);
|
|
56
|
+
const possibleTypeNames = possibleTypes.map(t => t.name);
|
|
57
|
+
const imports = createFragmentImports(baseVisitor, fragment.name.value, possibleTypeNames);
|
|
58
|
+
if (prev[fragmentName] && (0, graphql_1.print)(fragment) !== (0, graphql_1.print)(prev[fragmentName].node)) {
|
|
59
|
+
duplicateFragmentNames.push(fragmentName);
|
|
60
|
+
}
|
|
61
|
+
prev[fragmentName] = {
|
|
62
|
+
filePath,
|
|
63
|
+
imports,
|
|
64
|
+
onType: fragment.typeCondition.name.value,
|
|
65
|
+
node: fragment,
|
|
66
|
+
possibleTypes: possibleTypeNames,
|
|
67
|
+
};
|
|
67
68
|
}
|
|
68
69
|
return prev;
|
|
69
70
|
}, {});
|
|
@@ -73,44 +74,56 @@ function buildFragmentRegistry({ generateFilePath }, { documents, config }, sche
|
|
|
73
74
|
return registry;
|
|
74
75
|
}
|
|
75
76
|
/**
|
|
76
|
-
*
|
|
77
|
+
* Creates a BaseVisitor with standard configuration
|
|
78
|
+
*/
|
|
79
|
+
function createBaseVisitor(config, schemaObject) {
|
|
80
|
+
return new visitor_plugin_common_1.BaseVisitor(config, {
|
|
81
|
+
scalars: (0, visitor_plugin_common_1.buildScalarsFromConfig)(schemaObject, config),
|
|
82
|
+
dedupeOperationSuffix: (0, visitor_plugin_common_1.getConfigValue)(config.dedupeOperationSuffix, false),
|
|
83
|
+
omitOperationSuffix: (0, visitor_plugin_common_1.getConfigValue)(config.omitOperationSuffix, false),
|
|
84
|
+
fragmentVariablePrefix: (0, visitor_plugin_common_1.getConfigValue)(config.fragmentVariablePrefix, ''),
|
|
85
|
+
fragmentVariableSuffix: (0, visitor_plugin_common_1.getConfigValue)(config.fragmentVariableSuffix, 'FragmentDoc'),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
|
|
77
90
|
*/
|
|
78
91
|
function buildFragmentResolver(collectorOptions, presetOptions, schemaObject, dedupeFragments = false) {
|
|
79
|
-
const
|
|
92
|
+
const { config } = presetOptions;
|
|
93
|
+
const baseVisitor = createBaseVisitor(config, schemaObject);
|
|
94
|
+
const fragmentRegistry = buildFragmentRegistry(baseVisitor, collectorOptions, presetOptions, schemaObject);
|
|
80
95
|
const { baseOutputDir } = presetOptions;
|
|
81
96
|
const { baseDir, typesImport } = collectorOptions;
|
|
82
97
|
function resolveFragments(generatedFilePath, documentFileContent) {
|
|
83
|
-
const fragmentsInUse = (0, utils_js_1.
|
|
98
|
+
const { fragmentsInUse, usedFragmentTypes } = (0, utils_js_1.analyzeFragmentUsage)(documentFileContent, fragmentRegistry, schemaObject);
|
|
84
99
|
const externalFragments = [];
|
|
85
|
-
// fragment files to import names
|
|
86
100
|
const fragmentFileImports = {};
|
|
87
|
-
for (const fragmentName of Object.
|
|
88
|
-
const level = fragmentsInUse[fragmentName];
|
|
101
|
+
for (const [fragmentName, level] of Object.entries(fragmentsInUse)) {
|
|
89
102
|
const fragmentDetails = fragmentRegistry[fragmentName];
|
|
90
|
-
if (fragmentDetails)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
103
|
+
if (!fragmentDetails)
|
|
104
|
+
continue;
|
|
105
|
+
// add top level references to the import object
|
|
106
|
+
// we don't check or global namespace because the calling config can do so
|
|
107
|
+
if (level === 0 ||
|
|
108
|
+
(dedupeFragments &&
|
|
109
|
+
['OperationDefinition', 'FragmentDefinition'].includes(documentFileContent.definitions[0].kind))) {
|
|
110
|
+
if (fragmentDetails.filePath !== generatedFilePath) {
|
|
111
|
+
// don't emit imports to same location
|
|
112
|
+
const usedTypesForFragment = usedFragmentTypes[fragmentName] || [];
|
|
113
|
+
const filteredImports = createFragmentImports(baseVisitor, fragmentName, fragmentDetails.possibleTypes, usedTypesForFragment);
|
|
114
|
+
if (!fragmentFileImports[fragmentDetails.filePath]) {
|
|
115
|
+
fragmentFileImports[fragmentDetails.filePath] = [];
|
|
104
116
|
}
|
|
117
|
+
fragmentFileImports[fragmentDetails.filePath].push(...filteredImports);
|
|
105
118
|
}
|
|
106
|
-
externalFragments.push({
|
|
107
|
-
level,
|
|
108
|
-
isExternal: true,
|
|
109
|
-
name: fragmentName,
|
|
110
|
-
onType: fragmentDetails.onType,
|
|
111
|
-
node: fragmentDetails.node,
|
|
112
|
-
});
|
|
113
119
|
}
|
|
120
|
+
externalFragments.push({
|
|
121
|
+
level,
|
|
122
|
+
isExternal: true,
|
|
123
|
+
name: fragmentName,
|
|
124
|
+
onType: fragmentDetails.onType,
|
|
125
|
+
node: fragmentDetails.node,
|
|
126
|
+
});
|
|
114
127
|
}
|
|
115
128
|
return {
|
|
116
129
|
externalFragments,
|
package/cjs/utils.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.defineFilepathSubfolder = defineFilepathSubfolder;
|
|
4
4
|
exports.appendFileNameToFilePath = appendFileNameToFilePath;
|
|
5
|
+
exports.analyzeFragmentUsage = analyzeFragmentUsage;
|
|
5
6
|
exports.extractExternalFragmentsInUse = extractExternalFragmentsInUse;
|
|
6
7
|
const tslib_1 = require("tslib");
|
|
7
8
|
const path_1 = require("path");
|
|
9
|
+
const graphql_1 = require("graphql");
|
|
8
10
|
const parse_filepath_1 = tslib_1.__importDefault(require("parse-filepath"));
|
|
9
|
-
const
|
|
11
|
+
const visitor_plugin_common_1 = require("@graphql-codegen/visitor-plugin-common");
|
|
10
12
|
function defineFilepathSubfolder(baseFilePath, folder) {
|
|
11
13
|
const parsedPath = (0, parse_filepath_1.default)(baseFilePath);
|
|
12
14
|
return (0, path_1.join)(parsedPath.dir, folder, parsedPath.base).replace(/\\/g, '/');
|
|
@@ -16,29 +18,108 @@ function appendFileNameToFilePath(baseFilePath, fileName, extension) {
|
|
|
16
18
|
const name = fileName || parsedPath.name;
|
|
17
19
|
return (0, path_1.join)(parsedPath.dir, name + extension).replace(/\\/g, '/');
|
|
18
20
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Analyzes fragment usage in a GraphQL document.
|
|
23
|
+
* Returns information about which fragments are used and which specific types they're used with.
|
|
24
|
+
*/
|
|
25
|
+
function analyzeFragmentUsage(documentNode, fragmentRegistry, schema) {
|
|
26
|
+
const localFragments = getLocalFragments(documentNode);
|
|
27
|
+
const fragmentsInUse = extractExternalFragmentsInUse(documentNode, fragmentRegistry, localFragments);
|
|
28
|
+
const usedFragmentTypes = analyzeFragmentTypeUsage(documentNode, fragmentRegistry, schema, localFragments, fragmentsInUse);
|
|
29
|
+
return { fragmentsInUse, usedFragmentTypes };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get all fragment definitions that are local to this document
|
|
33
|
+
*/
|
|
34
|
+
function getLocalFragments(documentNode) {
|
|
35
|
+
const localFragments = new Set();
|
|
36
|
+
(0, graphql_1.visit)(documentNode, {
|
|
37
|
+
FragmentDefinition: node => {
|
|
38
|
+
localFragments.add(node.name.value);
|
|
27
39
|
},
|
|
28
40
|
});
|
|
41
|
+
return localFragments;
|
|
42
|
+
}
|
|
43
|
+
function extractExternalFragmentsInUse(documentNode, fragmentNameToFile, localFragment, result = {}, level = 0) {
|
|
29
44
|
// Then, look for all used fragments in this document
|
|
30
|
-
(0,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
extractExternalFragmentsInUse(fragmentNameToFile[node.name.value].node, fragmentNameToFile, result, level + 1);
|
|
38
|
-
}
|
|
45
|
+
(0, graphql_1.visit)(documentNode, {
|
|
46
|
+
FragmentSpread: node => {
|
|
47
|
+
if (!localFragment.has(node.name.value) &&
|
|
48
|
+
(result[node.name.value] === undefined || level < result[node.name.value])) {
|
|
49
|
+
result[node.name.value] = level;
|
|
50
|
+
if (fragmentNameToFile[node.name.value]) {
|
|
51
|
+
extractExternalFragmentsInUse(fragmentNameToFile[node.name.value].node, fragmentNameToFile, localFragment, result, level + 1);
|
|
39
52
|
}
|
|
40
|
-
}
|
|
53
|
+
}
|
|
41
54
|
},
|
|
42
55
|
});
|
|
43
56
|
return result;
|
|
44
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Analyze which specific types each fragment is used with (for polymorphic fragments)
|
|
60
|
+
*/
|
|
61
|
+
function analyzeFragmentTypeUsage(documentNode, fragmentRegistry, schema, localFragments, fragmentsInUse) {
|
|
62
|
+
const usedFragmentTypes = {};
|
|
63
|
+
const typeInfo = new graphql_1.TypeInfo(schema);
|
|
64
|
+
(0, graphql_1.visit)(documentNode, (0, graphql_1.visitWithTypeInfo)(typeInfo, {
|
|
65
|
+
Field: (node) => {
|
|
66
|
+
if (!node.selectionSet)
|
|
67
|
+
return;
|
|
68
|
+
const fieldType = typeInfo.getType();
|
|
69
|
+
if (!fieldType)
|
|
70
|
+
return;
|
|
71
|
+
const baseType = getBaseType(fieldType);
|
|
72
|
+
if ((0, graphql_1.isObjectType)(baseType) || (0, graphql_1.isInterfaceType)(baseType) || (0, graphql_1.isUnionType)(baseType)) {
|
|
73
|
+
analyzeSelectionSetTypeContext(node.selectionSet, baseType.name, usedFragmentTypes, fragmentRegistry, schema, localFragments);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
const result = {};
|
|
78
|
+
// Fill in missing types for multi-type fragments
|
|
79
|
+
for (const fragmentName in fragmentsInUse) {
|
|
80
|
+
const fragment = fragmentRegistry[fragmentName];
|
|
81
|
+
if (!fragment || fragment.possibleTypes.length <= 1)
|
|
82
|
+
continue;
|
|
83
|
+
const usedTypes = usedFragmentTypes[fragmentName];
|
|
84
|
+
result[fragmentName] = (usedTypes === null || usedTypes === void 0 ? void 0 : usedTypes.size) > 0 ? Array.from(usedTypes) : fragment.possibleTypes;
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Analyze fragment usage within a specific selection set and type context
|
|
90
|
+
*/
|
|
91
|
+
function analyzeSelectionSetTypeContext(selectionSet, currentTypeName, usedFragmentTypes, fragmentRegistry, schema, localFragments) {
|
|
92
|
+
var _a, _b;
|
|
93
|
+
var _c;
|
|
94
|
+
const { spreads, inlines } = (0, visitor_plugin_common_1.separateSelectionSet)(selectionSet.selections);
|
|
95
|
+
// Process fragment spreads in this type context
|
|
96
|
+
for (const spread of spreads) {
|
|
97
|
+
if (localFragments.has(spread.name.value))
|
|
98
|
+
continue;
|
|
99
|
+
const fragment = fragmentRegistry[spread.name.value];
|
|
100
|
+
if (!fragment || fragment.possibleTypes.length <= 1)
|
|
101
|
+
continue;
|
|
102
|
+
const currentType = schema.getType(currentTypeName);
|
|
103
|
+
if (!currentType)
|
|
104
|
+
continue;
|
|
105
|
+
const possibleTypes = (0, visitor_plugin_common_1.getPossibleTypes)(schema, currentType).map(t => t.name);
|
|
106
|
+
const matchingTypes = possibleTypes.filter(type => fragment.possibleTypes.includes(type));
|
|
107
|
+
if (matchingTypes.length > 0) {
|
|
108
|
+
const typeSet = ((_a = usedFragmentTypes[_c = spread.name.value]) !== null && _a !== void 0 ? _a : (usedFragmentTypes[_c] = new Set()));
|
|
109
|
+
matchingTypes.forEach(type => typeSet.add(type));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Process inline fragments
|
|
113
|
+
for (const inline of inlines) {
|
|
114
|
+
if (((_b = inline.typeCondition) === null || _b === void 0 ? void 0 : _b.name.value) && inline.selectionSet) {
|
|
115
|
+
analyzeSelectionSetTypeContext(inline.selectionSet, inline.typeCondition.name.value, usedFragmentTypes, fragmentRegistry, schema, localFragments);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function getBaseType(type) {
|
|
120
|
+
let baseType = type;
|
|
121
|
+
while ('ofType' in baseType && baseType.ofType) {
|
|
122
|
+
baseType = baseType.ofType;
|
|
123
|
+
}
|
|
124
|
+
return baseType;
|
|
125
|
+
}
|
package/esm/fragment-resolver.js
CHANGED
|
@@ -1,66 +1,67 @@
|
|
|
1
1
|
import { Kind, print } from 'graphql';
|
|
2
2
|
import { BaseVisitor, buildScalarsFromConfig, getConfigValue, getPossibleTypes, } from '@graphql-codegen/visitor-plugin-common';
|
|
3
|
-
import {
|
|
3
|
+
import { analyzeFragmentUsage } from './utils.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Creates fragment imports based on possible types and usage
|
|
6
6
|
*/
|
|
7
|
-
function
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
fragmentVariableSuffix: getConfigValue(config.fragmentVariableSuffix, 'FragmentDoc'),
|
|
7
|
+
function createFragmentImports(baseVisitor, fragmentName, possibleTypes, usedTypes) {
|
|
8
|
+
const fragmentImports = [];
|
|
9
|
+
// Always include the document import
|
|
10
|
+
fragmentImports.push({
|
|
11
|
+
name: baseVisitor.getFragmentVariableName(fragmentName),
|
|
12
|
+
kind: 'document',
|
|
14
13
|
});
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
fragmentImports.push({
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
const fragmentSuffix = baseVisitor.getFragmentSuffix(fragmentName);
|
|
15
|
+
if (possibleTypes.length === 1) {
|
|
16
|
+
fragmentImports.push({
|
|
17
|
+
name: baseVisitor.convertName(fragmentName, {
|
|
18
|
+
useTypesPrefix: true,
|
|
19
|
+
suffix: fragmentSuffix,
|
|
20
|
+
}),
|
|
21
|
+
kind: 'type',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
else if (possibleTypes.length > 0) {
|
|
25
|
+
const typesToImport = usedTypes && usedTypes.length > 0 ? usedTypes : possibleTypes;
|
|
26
|
+
typesToImport.forEach(typeName => {
|
|
20
27
|
fragmentImports.push({
|
|
21
|
-
name: baseVisitor.convertName(
|
|
28
|
+
name: baseVisitor.convertName(fragmentName, {
|
|
22
29
|
useTypesPrefix: true,
|
|
23
|
-
suffix: fragmentSuffix,
|
|
30
|
+
suffix: `_${typeName}` + (fragmentSuffix.length > 0 ? `_${fragmentSuffix}` : ''),
|
|
24
31
|
}),
|
|
25
32
|
kind: 'type',
|
|
26
33
|
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
kind: 'type',
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
return fragmentImports;
|
|
40
|
-
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return fragmentImports;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Used by `buildFragmentResolver` to build a mapping of fragmentNames to paths, importNames, and other useful info
|
|
40
|
+
*/
|
|
41
|
+
function buildFragmentRegistry(baseVisitor, { generateFilePath }, { documents }, schemaObject) {
|
|
41
42
|
const duplicateFragmentNames = [];
|
|
42
43
|
const registry = documents.reduce((prev, documentRecord) => {
|
|
43
44
|
const fragments = documentRecord.document.definitions.filter(d => d.kind === Kind.FRAGMENT_DEFINITION);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
throw new Error(`Fragment "${fragment.name.value}" is set on non-existing type "${fragment.typeCondition.name.value}"!`);
|
|
49
|
-
}
|
|
50
|
-
const possibleTypes = getPossibleTypes(schemaObject, schemaType);
|
|
51
|
-
const filePath = generateFilePath(documentRecord.location);
|
|
52
|
-
const imports = getFragmentImports(possibleTypes.map(t => t.name), fragment.name.value);
|
|
53
|
-
if (prev[fragment.name.value] &&
|
|
54
|
-
print(fragment) !== print(prev[fragment.name.value].node)) {
|
|
55
|
-
duplicateFragmentNames.push(fragment.name.value);
|
|
56
|
-
}
|
|
57
|
-
prev[fragment.name.value] = {
|
|
58
|
-
filePath,
|
|
59
|
-
imports,
|
|
60
|
-
onType: fragment.typeCondition.name.value,
|
|
61
|
-
node: fragment,
|
|
62
|
-
};
|
|
45
|
+
for (const fragment of fragments) {
|
|
46
|
+
const schemaType = schemaObject.getType(fragment.typeCondition.name.value);
|
|
47
|
+
if (!schemaType) {
|
|
48
|
+
throw new Error(`Fragment "${fragment.name.value}" is set on non-existing type "${fragment.typeCondition.name.value}"!`);
|
|
63
49
|
}
|
|
50
|
+
const fragmentName = fragment.name.value;
|
|
51
|
+
const filePath = generateFilePath(documentRecord.location);
|
|
52
|
+
const possibleTypes = getPossibleTypes(schemaObject, schemaType);
|
|
53
|
+
const possibleTypeNames = possibleTypes.map(t => t.name);
|
|
54
|
+
const imports = createFragmentImports(baseVisitor, fragment.name.value, possibleTypeNames);
|
|
55
|
+
if (prev[fragmentName] && print(fragment) !== print(prev[fragmentName].node)) {
|
|
56
|
+
duplicateFragmentNames.push(fragmentName);
|
|
57
|
+
}
|
|
58
|
+
prev[fragmentName] = {
|
|
59
|
+
filePath,
|
|
60
|
+
imports,
|
|
61
|
+
onType: fragment.typeCondition.name.value,
|
|
62
|
+
node: fragment,
|
|
63
|
+
possibleTypes: possibleTypeNames,
|
|
64
|
+
};
|
|
64
65
|
}
|
|
65
66
|
return prev;
|
|
66
67
|
}, {});
|
|
@@ -70,44 +71,56 @@ function buildFragmentRegistry({ generateFilePath }, { documents, config }, sche
|
|
|
70
71
|
return registry;
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
|
-
*
|
|
74
|
+
* Creates a BaseVisitor with standard configuration
|
|
75
|
+
*/
|
|
76
|
+
function createBaseVisitor(config, schemaObject) {
|
|
77
|
+
return new BaseVisitor(config, {
|
|
78
|
+
scalars: buildScalarsFromConfig(schemaObject, config),
|
|
79
|
+
dedupeOperationSuffix: getConfigValue(config.dedupeOperationSuffix, false),
|
|
80
|
+
omitOperationSuffix: getConfigValue(config.omitOperationSuffix, false),
|
|
81
|
+
fragmentVariablePrefix: getConfigValue(config.fragmentVariablePrefix, ''),
|
|
82
|
+
fragmentVariableSuffix: getConfigValue(config.fragmentVariableSuffix, 'FragmentDoc'),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
|
|
74
87
|
*/
|
|
75
88
|
export default function buildFragmentResolver(collectorOptions, presetOptions, schemaObject, dedupeFragments = false) {
|
|
76
|
-
const
|
|
89
|
+
const { config } = presetOptions;
|
|
90
|
+
const baseVisitor = createBaseVisitor(config, schemaObject);
|
|
91
|
+
const fragmentRegistry = buildFragmentRegistry(baseVisitor, collectorOptions, presetOptions, schemaObject);
|
|
77
92
|
const { baseOutputDir } = presetOptions;
|
|
78
93
|
const { baseDir, typesImport } = collectorOptions;
|
|
79
94
|
function resolveFragments(generatedFilePath, documentFileContent) {
|
|
80
|
-
const fragmentsInUse =
|
|
95
|
+
const { fragmentsInUse, usedFragmentTypes } = analyzeFragmentUsage(documentFileContent, fragmentRegistry, schemaObject);
|
|
81
96
|
const externalFragments = [];
|
|
82
|
-
// fragment files to import names
|
|
83
97
|
const fragmentFileImports = {};
|
|
84
|
-
for (const fragmentName of Object.
|
|
85
|
-
const level = fragmentsInUse[fragmentName];
|
|
98
|
+
for (const [fragmentName, level] of Object.entries(fragmentsInUse)) {
|
|
86
99
|
const fragmentDetails = fragmentRegistry[fragmentName];
|
|
87
|
-
if (fragmentDetails)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
100
|
+
if (!fragmentDetails)
|
|
101
|
+
continue;
|
|
102
|
+
// add top level references to the import object
|
|
103
|
+
// we don't check or global namespace because the calling config can do so
|
|
104
|
+
if (level === 0 ||
|
|
105
|
+
(dedupeFragments &&
|
|
106
|
+
['OperationDefinition', 'FragmentDefinition'].includes(documentFileContent.definitions[0].kind))) {
|
|
107
|
+
if (fragmentDetails.filePath !== generatedFilePath) {
|
|
108
|
+
// don't emit imports to same location
|
|
109
|
+
const usedTypesForFragment = usedFragmentTypes[fragmentName] || [];
|
|
110
|
+
const filteredImports = createFragmentImports(baseVisitor, fragmentName, fragmentDetails.possibleTypes, usedTypesForFragment);
|
|
111
|
+
if (!fragmentFileImports[fragmentDetails.filePath]) {
|
|
112
|
+
fragmentFileImports[fragmentDetails.filePath] = [];
|
|
101
113
|
}
|
|
114
|
+
fragmentFileImports[fragmentDetails.filePath].push(...filteredImports);
|
|
102
115
|
}
|
|
103
|
-
externalFragments.push({
|
|
104
|
-
level,
|
|
105
|
-
isExternal: true,
|
|
106
|
-
name: fragmentName,
|
|
107
|
-
onType: fragmentDetails.onType,
|
|
108
|
-
node: fragmentDetails.node,
|
|
109
|
-
});
|
|
110
116
|
}
|
|
117
|
+
externalFragments.push({
|
|
118
|
+
level,
|
|
119
|
+
isExternal: true,
|
|
120
|
+
name: fragmentName,
|
|
121
|
+
onType: fragmentDetails.onType,
|
|
122
|
+
node: fragmentDetails.node,
|
|
123
|
+
});
|
|
111
124
|
}
|
|
112
125
|
return {
|
|
113
126
|
externalFragments,
|
package/esm/utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
|
+
import { isInterfaceType, isObjectType, isUnionType, TypeInfo, visit, visitWithTypeInfo, } from 'graphql';
|
|
2
3
|
import parsePath from 'parse-filepath';
|
|
3
|
-
import {
|
|
4
|
+
import { getPossibleTypes, separateSelectionSet } from '@graphql-codegen/visitor-plugin-common';
|
|
4
5
|
export function defineFilepathSubfolder(baseFilePath, folder) {
|
|
5
6
|
const parsedPath = parsePath(baseFilePath);
|
|
6
7
|
return join(parsedPath.dir, folder, parsedPath.base).replace(/\\/g, '/');
|
|
@@ -10,29 +11,108 @@ export function appendFileNameToFilePath(baseFilePath, fileName, extension) {
|
|
|
10
11
|
const name = fileName || parsedPath.name;
|
|
11
12
|
return join(parsedPath.dir, name + extension).replace(/\\/g, '/');
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Analyzes fragment usage in a GraphQL document.
|
|
16
|
+
* Returns information about which fragments are used and which specific types they're used with.
|
|
17
|
+
*/
|
|
18
|
+
export function analyzeFragmentUsage(documentNode, fragmentRegistry, schema) {
|
|
19
|
+
const localFragments = getLocalFragments(documentNode);
|
|
20
|
+
const fragmentsInUse = extractExternalFragmentsInUse(documentNode, fragmentRegistry, localFragments);
|
|
21
|
+
const usedFragmentTypes = analyzeFragmentTypeUsage(documentNode, fragmentRegistry, schema, localFragments, fragmentsInUse);
|
|
22
|
+
return { fragmentsInUse, usedFragmentTypes };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get all fragment definitions that are local to this document
|
|
26
|
+
*/
|
|
27
|
+
function getLocalFragments(documentNode) {
|
|
28
|
+
const localFragments = new Set();
|
|
29
|
+
visit(documentNode, {
|
|
30
|
+
FragmentDefinition: node => {
|
|
31
|
+
localFragments.add(node.name.value);
|
|
21
32
|
},
|
|
22
33
|
});
|
|
34
|
+
return localFragments;
|
|
35
|
+
}
|
|
36
|
+
export function extractExternalFragmentsInUse(documentNode, fragmentNameToFile, localFragment, result = {}, level = 0) {
|
|
23
37
|
// Then, look for all used fragments in this document
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
extractExternalFragmentsInUse(fragmentNameToFile[node.name.value].node, fragmentNameToFile, result, level + 1);
|
|
32
|
-
}
|
|
38
|
+
visit(documentNode, {
|
|
39
|
+
FragmentSpread: node => {
|
|
40
|
+
if (!localFragment.has(node.name.value) &&
|
|
41
|
+
(result[node.name.value] === undefined || level < result[node.name.value])) {
|
|
42
|
+
result[node.name.value] = level;
|
|
43
|
+
if (fragmentNameToFile[node.name.value]) {
|
|
44
|
+
extractExternalFragmentsInUse(fragmentNameToFile[node.name.value].node, fragmentNameToFile, localFragment, result, level + 1);
|
|
33
45
|
}
|
|
34
|
-
}
|
|
46
|
+
}
|
|
35
47
|
},
|
|
36
48
|
});
|
|
37
49
|
return result;
|
|
38
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Analyze which specific types each fragment is used with (for polymorphic fragments)
|
|
53
|
+
*/
|
|
54
|
+
function analyzeFragmentTypeUsage(documentNode, fragmentRegistry, schema, localFragments, fragmentsInUse) {
|
|
55
|
+
const usedFragmentTypes = {};
|
|
56
|
+
const typeInfo = new TypeInfo(schema);
|
|
57
|
+
visit(documentNode, visitWithTypeInfo(typeInfo, {
|
|
58
|
+
Field: (node) => {
|
|
59
|
+
if (!node.selectionSet)
|
|
60
|
+
return;
|
|
61
|
+
const fieldType = typeInfo.getType();
|
|
62
|
+
if (!fieldType)
|
|
63
|
+
return;
|
|
64
|
+
const baseType = getBaseType(fieldType);
|
|
65
|
+
if (isObjectType(baseType) || isInterfaceType(baseType) || isUnionType(baseType)) {
|
|
66
|
+
analyzeSelectionSetTypeContext(node.selectionSet, baseType.name, usedFragmentTypes, fragmentRegistry, schema, localFragments);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
70
|
+
const result = {};
|
|
71
|
+
// Fill in missing types for multi-type fragments
|
|
72
|
+
for (const fragmentName in fragmentsInUse) {
|
|
73
|
+
const fragment = fragmentRegistry[fragmentName];
|
|
74
|
+
if (!fragment || fragment.possibleTypes.length <= 1)
|
|
75
|
+
continue;
|
|
76
|
+
const usedTypes = usedFragmentTypes[fragmentName];
|
|
77
|
+
result[fragmentName] = (usedTypes === null || usedTypes === void 0 ? void 0 : usedTypes.size) > 0 ? Array.from(usedTypes) : fragment.possibleTypes;
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Analyze fragment usage within a specific selection set and type context
|
|
83
|
+
*/
|
|
84
|
+
function analyzeSelectionSetTypeContext(selectionSet, currentTypeName, usedFragmentTypes, fragmentRegistry, schema, localFragments) {
|
|
85
|
+
var _a, _b;
|
|
86
|
+
var _c;
|
|
87
|
+
const { spreads, inlines } = separateSelectionSet(selectionSet.selections);
|
|
88
|
+
// Process fragment spreads in this type context
|
|
89
|
+
for (const spread of spreads) {
|
|
90
|
+
if (localFragments.has(spread.name.value))
|
|
91
|
+
continue;
|
|
92
|
+
const fragment = fragmentRegistry[spread.name.value];
|
|
93
|
+
if (!fragment || fragment.possibleTypes.length <= 1)
|
|
94
|
+
continue;
|
|
95
|
+
const currentType = schema.getType(currentTypeName);
|
|
96
|
+
if (!currentType)
|
|
97
|
+
continue;
|
|
98
|
+
const possibleTypes = getPossibleTypes(schema, currentType).map(t => t.name);
|
|
99
|
+
const matchingTypes = possibleTypes.filter(type => fragment.possibleTypes.includes(type));
|
|
100
|
+
if (matchingTypes.length > 0) {
|
|
101
|
+
const typeSet = ((_a = usedFragmentTypes[_c = spread.name.value]) !== null && _a !== void 0 ? _a : (usedFragmentTypes[_c] = new Set()));
|
|
102
|
+
matchingTypes.forEach(type => typeSet.add(type));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Process inline fragments
|
|
106
|
+
for (const inline of inlines) {
|
|
107
|
+
if (((_b = inline.typeCondition) === null || _b === void 0 ? void 0 : _b.name.value) && inline.selectionSet) {
|
|
108
|
+
analyzeSelectionSetTypeContext(inline.selectionSet, inline.typeCondition.name.value, usedFragmentTypes, fragmentRegistry, schema, localFragments);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function getBaseType(type) {
|
|
113
|
+
let baseType = type;
|
|
114
|
+
while ('ofType' in baseType && baseType.ofType) {
|
|
115
|
+
baseType = baseType.ofType;
|
|
116
|
+
}
|
|
117
|
+
return baseType;
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphql-codegen/near-operation-file-preset",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "GraphQL Code Generator preset for generating operation code near the operation file",
|
|
5
5
|
"peerDependencies": {
|
|
6
6
|
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
|
|
@@ -15,10 +15,11 @@ export type FragmentRegistry = {
|
|
|
15
15
|
onType: string;
|
|
16
16
|
node: FragmentDefinitionNode;
|
|
17
17
|
imports: Array<FragmentImport>;
|
|
18
|
+
possibleTypes: string[];
|
|
18
19
|
};
|
|
19
20
|
};
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
+
* Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
|
|
22
23
|
*/
|
|
23
24
|
export default function buildFragmentResolver<T>(collectorOptions: DocumentImportResolverOptions, presetOptions: Types.PresetFnArgs<T>, schemaObject: GraphQLSchema, dedupeFragments?: boolean): (generatedFilePath: string, documentFileContent: DocumentNode) => {
|
|
24
25
|
externalFragments: LoadedFragment<{
|
|
@@ -15,10 +15,11 @@ export type FragmentRegistry = {
|
|
|
15
15
|
onType: string;
|
|
16
16
|
node: FragmentDefinitionNode;
|
|
17
17
|
imports: Array<FragmentImport>;
|
|
18
|
+
possibleTypes: string[];
|
|
18
19
|
};
|
|
19
20
|
};
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
+
* Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
|
|
22
23
|
*/
|
|
23
24
|
export default function buildFragmentResolver<T>(collectorOptions: DocumentImportResolverOptions, presetOptions: Types.PresetFnArgs<T>, schemaObject: GraphQLSchema, dedupeFragments?: boolean): (generatedFilePath: string, documentFileContent: DocumentNode) => {
|
|
24
25
|
externalFragments: LoadedFragment<{
|
package/typings/utils.d.cts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
import { DocumentNode, FragmentDefinitionNode } from 'graphql';
|
|
1
|
+
import { DocumentNode, FragmentDefinitionNode, GraphQLSchema } from 'graphql';
|
|
2
2
|
import { FragmentRegistry } from './fragment-resolver.cjs';
|
|
3
3
|
export declare function defineFilepathSubfolder(baseFilePath: string, folder: string): string;
|
|
4
4
|
export declare function appendFileNameToFilePath(baseFilePath: string, fileName: string, extension: string): string;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Analyzes fragment usage in a GraphQL document.
|
|
7
|
+
* Returns information about which fragments are used and which specific types they're used with.
|
|
8
|
+
*/
|
|
9
|
+
export declare function analyzeFragmentUsage(documentNode: DocumentNode, fragmentRegistry: FragmentRegistry, schema: GraphQLSchema): {
|
|
10
|
+
fragmentsInUse: {
|
|
11
|
+
[fragmentName: string]: number;
|
|
12
|
+
};
|
|
13
|
+
usedFragmentTypes: {
|
|
14
|
+
[fragmentName: string]: string[];
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export declare function extractExternalFragmentsInUse(documentNode: DocumentNode | FragmentDefinitionNode, fragmentNameToFile: FragmentRegistry, localFragment: Set<string>, result?: {
|
|
6
18
|
[fragmentName: string]: number;
|
|
7
19
|
}, level?: number): {
|
|
8
20
|
[fragmentName: string]: number;
|
package/typings/utils.d.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
import { DocumentNode, FragmentDefinitionNode } from 'graphql';
|
|
1
|
+
import { DocumentNode, FragmentDefinitionNode, GraphQLSchema } from 'graphql';
|
|
2
2
|
import { FragmentRegistry } from './fragment-resolver.js';
|
|
3
3
|
export declare function defineFilepathSubfolder(baseFilePath: string, folder: string): string;
|
|
4
4
|
export declare function appendFileNameToFilePath(baseFilePath: string, fileName: string, extension: string): string;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Analyzes fragment usage in a GraphQL document.
|
|
7
|
+
* Returns information about which fragments are used and which specific types they're used with.
|
|
8
|
+
*/
|
|
9
|
+
export declare function analyzeFragmentUsage(documentNode: DocumentNode, fragmentRegistry: FragmentRegistry, schema: GraphQLSchema): {
|
|
10
|
+
fragmentsInUse: {
|
|
11
|
+
[fragmentName: string]: number;
|
|
12
|
+
};
|
|
13
|
+
usedFragmentTypes: {
|
|
14
|
+
[fragmentName: string]: string[];
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export declare function extractExternalFragmentsInUse(documentNode: DocumentNode | FragmentDefinitionNode, fragmentNameToFile: FragmentRegistry, localFragment: Set<string>, result?: {
|
|
6
18
|
[fragmentName: string]: number;
|
|
7
19
|
}, level?: number): {
|
|
8
20
|
[fragmentName: string]: number;
|