@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.
@@ -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
- * Used by `buildFragmentResolver` to build a mapping of fragmentNames to paths, importNames, and other useful info
8
+ * Creates fragment imports based on possible types and usage
9
9
  */
10
- function buildFragmentRegistry({ generateFilePath }, { documents, config }, schemaObject) {
11
- const baseVisitor = new visitor_plugin_common_1.BaseVisitor(config, {
12
- scalars: (0, visitor_plugin_common_1.buildScalarsFromConfig)(schemaObject, config),
13
- dedupeOperationSuffix: (0, visitor_plugin_common_1.getConfigValue)(config.dedupeOperationSuffix, false),
14
- omitOperationSuffix: (0, visitor_plugin_common_1.getConfigValue)(config.omitOperationSuffix, false),
15
- fragmentVariablePrefix: (0, visitor_plugin_common_1.getConfigValue)(config.fragmentVariablePrefix, ''),
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 getFragmentImports = (possbileTypes, name) => {
19
- const fragmentImports = [];
20
- fragmentImports.push({ name: baseVisitor.getFragmentVariableName(name), kind: 'document' });
21
- const fragmentSuffix = baseVisitor.getFragmentSuffix(name);
22
- if (possbileTypes.length === 1) {
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(name, {
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
- else if (possbileTypes.length !== 0) {
32
- possbileTypes.forEach(typeName => {
33
- fragmentImports.push({
34
- name: baseVisitor.convertName(name, {
35
- useTypesPrefix: true,
36
- suffix: `_${typeName}_${fragmentSuffix}`,
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
- if (fragments.length > 0) {
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}"!`);
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
- * Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
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 fragmentRegistry = buildFragmentRegistry(collectorOptions, presetOptions, schemaObject);
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.extractExternalFragmentsInUse)(documentFileContent, fragmentRegistry);
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.keys(fragmentsInUse)) {
88
- const level = fragmentsInUse[fragmentName];
101
+ for (const [fragmentName, level] of Object.entries(fragmentsInUse)) {
89
102
  const fragmentDetails = fragmentRegistry[fragmentName];
90
- if (fragmentDetails) {
91
- // add top level references to the import object
92
- // we don't checkf or global namespace because the calling config can do so
93
- if (level === 0 ||
94
- (dedupeFragments &&
95
- ['OperationDefinition', 'FragmentDefinition'].includes(documentFileContent.definitions[0].kind))) {
96
- if (fragmentDetails.filePath !== generatedFilePath) {
97
- // don't emit imports to same location
98
- if (fragmentFileImports[fragmentDetails.filePath] === undefined) {
99
- fragmentFileImports[fragmentDetails.filePath] = fragmentDetails.imports;
100
- }
101
- else {
102
- fragmentFileImports[fragmentDetails.filePath].push(...fragmentDetails.imports);
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 plugin_helpers_1 = require("@graphql-codegen/plugin-helpers");
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
- function extractExternalFragmentsInUse(documentNode, fragmentNameToFile, result = {}, level = 0) {
20
- const ignoreList = new Set();
21
- // First, take all fragments definition from the current file, and mark them as ignored
22
- (0, plugin_helpers_1.oldVisit)(documentNode, {
23
- enter: {
24
- FragmentDefinition: (node) => {
25
- ignoreList.add(node.name.value);
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, plugin_helpers_1.oldVisit)(documentNode, {
31
- enter: {
32
- FragmentSpread: (node) => {
33
- if (!ignoreList.has(node.name.value) &&
34
- (result[node.name.value] === undefined || level < result[node.name.value])) {
35
- result[node.name.value] = level;
36
- if (fragmentNameToFile[node.name.value]) {
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
+ }
@@ -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 { extractExternalFragmentsInUse } from './utils.js';
3
+ import { analyzeFragmentUsage } from './utils.js';
4
4
  /**
5
- * Used by `buildFragmentResolver` to build a mapping of fragmentNames to paths, importNames, and other useful info
5
+ * Creates fragment imports based on possible types and usage
6
6
  */
7
- function buildFragmentRegistry({ generateFilePath }, { documents, config }, schemaObject) {
8
- const baseVisitor = new BaseVisitor(config, {
9
- scalars: buildScalarsFromConfig(schemaObject, config),
10
- dedupeOperationSuffix: getConfigValue(config.dedupeOperationSuffix, false),
11
- omitOperationSuffix: getConfigValue(config.omitOperationSuffix, false),
12
- fragmentVariablePrefix: getConfigValue(config.fragmentVariablePrefix, ''),
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 getFragmentImports = (possbileTypes, name) => {
16
- const fragmentImports = [];
17
- fragmentImports.push({ name: baseVisitor.getFragmentVariableName(name), kind: 'document' });
18
- const fragmentSuffix = baseVisitor.getFragmentSuffix(name);
19
- if (possbileTypes.length === 1) {
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(name, {
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
- else if (possbileTypes.length !== 0) {
29
- possbileTypes.forEach(typeName => {
30
- fragmentImports.push({
31
- name: baseVisitor.convertName(name, {
32
- useTypesPrefix: true,
33
- suffix: `_${typeName}_${fragmentSuffix}`,
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
- if (fragments.length > 0) {
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}"!`);
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
- * Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
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 fragmentRegistry = buildFragmentRegistry(collectorOptions, presetOptions, schemaObject);
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 = extractExternalFragmentsInUse(documentFileContent, fragmentRegistry);
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.keys(fragmentsInUse)) {
85
- const level = fragmentsInUse[fragmentName];
98
+ for (const [fragmentName, level] of Object.entries(fragmentsInUse)) {
86
99
  const fragmentDetails = fragmentRegistry[fragmentName];
87
- if (fragmentDetails) {
88
- // add top level references to the import object
89
- // we don't checkf or global namespace because the calling config can do so
90
- if (level === 0 ||
91
- (dedupeFragments &&
92
- ['OperationDefinition', 'FragmentDefinition'].includes(documentFileContent.definitions[0].kind))) {
93
- if (fragmentDetails.filePath !== generatedFilePath) {
94
- // don't emit imports to same location
95
- if (fragmentFileImports[fragmentDetails.filePath] === undefined) {
96
- fragmentFileImports[fragmentDetails.filePath] = fragmentDetails.imports;
97
- }
98
- else {
99
- fragmentFileImports[fragmentDetails.filePath].push(...fragmentDetails.imports);
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 { oldVisit } from '@graphql-codegen/plugin-helpers';
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
- export function extractExternalFragmentsInUse(documentNode, fragmentNameToFile, result = {}, level = 0) {
14
- const ignoreList = new Set();
15
- // First, take all fragments definition from the current file, and mark them as ignored
16
- oldVisit(documentNode, {
17
- enter: {
18
- FragmentDefinition: (node) => {
19
- ignoreList.add(node.name.value);
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
- oldVisit(documentNode, {
25
- enter: {
26
- FragmentSpread: (node) => {
27
- if (!ignoreList.has(node.name.value) &&
28
- (result[node.name.value] === undefined || level < result[node.name.value])) {
29
- result[node.name.value] = level;
30
- if (fragmentNameToFile[node.name.value]) {
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.1",
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
- * Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
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
- * Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
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<{
@@ -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
- export declare function extractExternalFragmentsInUse(documentNode: DocumentNode | FragmentDefinitionNode, fragmentNameToFile: FragmentRegistry, result?: {
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;
@@ -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
- export declare function extractExternalFragmentsInUse(documentNode: DocumentNode | FragmentDefinitionNode, fragmentNameToFile: FragmentRegistry, result?: {
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;