@nmarks/graphql-codegen-per-operation-file-preset 1.0.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/README.md +209 -0
- package/cjs/fragment-resolver.js +149 -0
- package/cjs/index.js +111 -0
- package/cjs/package.json +1 -0
- package/cjs/resolve-document-imports.js +58 -0
- package/cjs/utils.js +120 -0
- package/esm/fragment-resolver.js +146 -0
- package/esm/index.js +107 -0
- package/esm/resolve-document-imports.js +54 -0
- package/esm/utils.js +115 -0
- package/package.json +48 -0
- package/typings/fragment-resolver.d.cts +29 -0
- package/typings/fragment-resolver.d.ts +29 -0
- package/typings/index.d.cts +131 -0
- package/typings/index.d.ts +131 -0
- package/typings/resolve-document-imports.d.cts +48 -0
- package/typings/resolve-document-imports.d.ts +48 -0
- package/typings/utils.d.cts +23 -0
- package/typings/utils.d.ts +23 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Kind, print } from 'graphql';
|
|
2
|
+
import { BaseVisitor, buildScalarsFromConfig, getConfigValue, getPossibleTypes, } from '@graphql-codegen/visitor-plugin-common';
|
|
3
|
+
import { analyzeFragmentUsage, generateOperationFilePath } from './utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Creates fragment imports based on possible types and usage
|
|
6
|
+
*/
|
|
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',
|
|
13
|
+
});
|
|
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 => {
|
|
27
|
+
fragmentImports.push({
|
|
28
|
+
name: baseVisitor.convertName(fragmentName, {
|
|
29
|
+
useTypesPrefix: true,
|
|
30
|
+
suffix: `_${typeName}` + (fragmentSuffix.length > 0 ? `_${fragmentSuffix}` : ''),
|
|
31
|
+
}),
|
|
32
|
+
kind: 'type',
|
|
33
|
+
});
|
|
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
|
+
* KEY DIFFERENCE from near-operation-file:
|
|
42
|
+
* The filePath is generated based on the fragment NAME, not the source file location
|
|
43
|
+
*/
|
|
44
|
+
function buildFragmentRegistry(baseVisitor, { folder, extension }, { documents }, schemaObject) {
|
|
45
|
+
const duplicateFragmentNames = [];
|
|
46
|
+
const registry = documents.reduce((prev, documentRecord) => {
|
|
47
|
+
const fragments = documentRecord.document.definitions.filter(d => d.kind === Kind.FRAGMENT_DEFINITION);
|
|
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 fragmentName = fragment.name.value;
|
|
54
|
+
// KEY CHANGE: Generate path based on fragment name, not source file
|
|
55
|
+
const filePath = generateOperationFilePath(documentRecord.location, fragmentName, folder, extension);
|
|
56
|
+
const possibleTypes = getPossibleTypes(schemaObject, schemaType);
|
|
57
|
+
const possibleTypeNames = possibleTypes.map(t => t.name);
|
|
58
|
+
const imports = createFragmentImports(baseVisitor, fragment.name.value, possibleTypeNames);
|
|
59
|
+
if (prev[fragmentName] && print(fragment) !== print(prev[fragmentName].node)) {
|
|
60
|
+
duplicateFragmentNames.push(fragmentName);
|
|
61
|
+
}
|
|
62
|
+
prev[fragmentName] = {
|
|
63
|
+
filePath,
|
|
64
|
+
imports,
|
|
65
|
+
onType: fragment.typeCondition.name.value,
|
|
66
|
+
node: fragment,
|
|
67
|
+
possibleTypes: possibleTypeNames,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return prev;
|
|
71
|
+
}, {});
|
|
72
|
+
if (duplicateFragmentNames.length) {
|
|
73
|
+
throw new Error(`Multiple fragments with the name(s) "${duplicateFragmentNames.join(', ')}" were found.`);
|
|
74
|
+
}
|
|
75
|
+
return registry;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Creates a BaseVisitor with standard configuration
|
|
79
|
+
*/
|
|
80
|
+
function createBaseVisitor(config, schemaObject) {
|
|
81
|
+
return new BaseVisitor(config, {
|
|
82
|
+
scalars: buildScalarsFromConfig(schemaObject, config),
|
|
83
|
+
dedupeOperationSuffix: getConfigValue(config.dedupeOperationSuffix, false),
|
|
84
|
+
omitOperationSuffix: getConfigValue(config.omitOperationSuffix, false),
|
|
85
|
+
fragmentVariablePrefix: getConfigValue(config.fragmentVariablePrefix, ''),
|
|
86
|
+
fragmentVariableSuffix: getConfigValue(config.fragmentVariableSuffix, 'FragmentDoc'),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
|
|
91
|
+
*/
|
|
92
|
+
export default function buildFragmentResolver(collectorOptions, presetOptions, schemaObject, dedupeFragments = false) {
|
|
93
|
+
const { config } = presetOptions;
|
|
94
|
+
const baseVisitor = createBaseVisitor(config, schemaObject);
|
|
95
|
+
const fragmentRegistry = buildFragmentRegistry(baseVisitor, collectorOptions, presetOptions, schemaObject);
|
|
96
|
+
const { baseOutputDir } = presetOptions;
|
|
97
|
+
const { baseDir, typesImport } = collectorOptions;
|
|
98
|
+
function resolveFragments(generatedFilePath, documentFileContent) {
|
|
99
|
+
const { fragmentsInUse, usedFragmentTypes } = analyzeFragmentUsage(documentFileContent, fragmentRegistry, schemaObject);
|
|
100
|
+
const externalFragments = [];
|
|
101
|
+
const fragmentFileImports = {};
|
|
102
|
+
for (const [fragmentName, level] of Object.entries(fragmentsInUse)) {
|
|
103
|
+
const fragmentDetails = fragmentRegistry[fragmentName];
|
|
104
|
+
if (!fragmentDetails)
|
|
105
|
+
continue;
|
|
106
|
+
// add top level references to the import object
|
|
107
|
+
// we don't check or global namespace because the calling config can do so
|
|
108
|
+
if (level === 0 ||
|
|
109
|
+
(dedupeFragments &&
|
|
110
|
+
['OperationDefinition', 'FragmentDefinition'].includes(documentFileContent.definitions[0].kind))) {
|
|
111
|
+
if (fragmentDetails.filePath !== generatedFilePath) {
|
|
112
|
+
// don't emit imports to same location
|
|
113
|
+
const usedTypesForFragment = usedFragmentTypes[fragmentName] || [];
|
|
114
|
+
const filteredImports = createFragmentImports(baseVisitor, fragmentName, fragmentDetails.possibleTypes, usedTypesForFragment);
|
|
115
|
+
if (!fragmentFileImports[fragmentDetails.filePath]) {
|
|
116
|
+
fragmentFileImports[fragmentDetails.filePath] = [];
|
|
117
|
+
}
|
|
118
|
+
fragmentFileImports[fragmentDetails.filePath].push(...filteredImports);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
externalFragments.push({
|
|
122
|
+
level,
|
|
123
|
+
isExternal: true,
|
|
124
|
+
name: fragmentName,
|
|
125
|
+
onType: fragmentDetails.onType,
|
|
126
|
+
node: fragmentDetails.node,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
externalFragments,
|
|
131
|
+
fragmentImports: Object.entries(fragmentFileImports).map(([fragmentsFilePath, identifiers]) => ({
|
|
132
|
+
baseDir,
|
|
133
|
+
baseOutputDir,
|
|
134
|
+
outputPath: generatedFilePath,
|
|
135
|
+
importSource: {
|
|
136
|
+
path: fragmentsFilePath,
|
|
137
|
+
identifiers,
|
|
138
|
+
},
|
|
139
|
+
emitLegacyCommonJSImports: presetOptions.config.emitLegacyCommonJSImports,
|
|
140
|
+
importExtension: presetOptions.config.importExtension,
|
|
141
|
+
typesImport,
|
|
142
|
+
})),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return resolveFragments;
|
|
146
|
+
}
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { buildASTSchema, Kind } from 'graphql';
|
|
3
|
+
import addPlugin from '@graphql-codegen/add';
|
|
4
|
+
import { getConfigValue, } from '@graphql-codegen/visitor-plugin-common';
|
|
5
|
+
import { resolveDocumentImports } from './resolve-document-imports.js';
|
|
6
|
+
import { generateOperationFilePath } from './utils.js';
|
|
7
|
+
/**
|
|
8
|
+
* Extract operation and fragment names from a document
|
|
9
|
+
*/
|
|
10
|
+
function extractDefinitions(document) {
|
|
11
|
+
const definitions = [];
|
|
12
|
+
for (const def of document.definitions) {
|
|
13
|
+
if (def.kind === Kind.OPERATION_DEFINITION && def.name) {
|
|
14
|
+
definitions.push({ name: def.name.value, definition: def });
|
|
15
|
+
}
|
|
16
|
+
else if (def.kind === Kind.FRAGMENT_DEFINITION) {
|
|
17
|
+
definitions.push({ name: def.name.value, definition: def });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return definitions;
|
|
21
|
+
}
|
|
22
|
+
export const preset = {
|
|
23
|
+
buildGeneratesSection: options => {
|
|
24
|
+
var _a;
|
|
25
|
+
const schemaObject = options.schemaAst
|
|
26
|
+
? options.schemaAst
|
|
27
|
+
: buildASTSchema(options.schema, options.config);
|
|
28
|
+
const baseDir = options.presetConfig.cwd || process.cwd();
|
|
29
|
+
const extension = options.presetConfig.extension || '.ts';
|
|
30
|
+
const folder = options.presetConfig.folder || '__generated__';
|
|
31
|
+
const importTypesNamespace = options.presetConfig.importTypesNamespace || 'Types';
|
|
32
|
+
const { baseTypesPath } = options.presetConfig;
|
|
33
|
+
if (!baseTypesPath) {
|
|
34
|
+
throw new Error(`Preset "per-operation-file" requires you to specify "baseTypesPath" configuration!`);
|
|
35
|
+
}
|
|
36
|
+
const shouldAbsolute = !baseTypesPath.startsWith('~');
|
|
37
|
+
const pluginMap = {
|
|
38
|
+
...options.pluginMap,
|
|
39
|
+
add: addPlugin,
|
|
40
|
+
};
|
|
41
|
+
// Resolve fragment dependencies using our adapted fragment resolver
|
|
42
|
+
// Fragment paths will be generated based on fragment names
|
|
43
|
+
const sources = resolveDocumentImports(options, schemaObject, {
|
|
44
|
+
baseDir,
|
|
45
|
+
folder,
|
|
46
|
+
extension,
|
|
47
|
+
schemaTypesSource: {
|
|
48
|
+
path: shouldAbsolute ? join(options.baseOutputDir, baseTypesPath) : baseTypesPath,
|
|
49
|
+
namespace: importTypesNamespace,
|
|
50
|
+
},
|
|
51
|
+
typesImport: (_a = options.config.useTypeImports) !== null && _a !== void 0 ? _a : false,
|
|
52
|
+
}, getConfigValue(options.config.dedupeFragments, false));
|
|
53
|
+
const artifacts = [];
|
|
54
|
+
// Now split each source into separate files per operation/fragment
|
|
55
|
+
for (const source of sources) {
|
|
56
|
+
const definitions = extractDefinitions(source.documents[0].document);
|
|
57
|
+
for (const { name, definition } of definitions) {
|
|
58
|
+
const filename = generateOperationFilePath(source.documents[0].location, name, folder, extension);
|
|
59
|
+
// Create a document with just this operation/fragment
|
|
60
|
+
const singleDefDocument = {
|
|
61
|
+
kind: Kind.DOCUMENT,
|
|
62
|
+
definitions: [definition],
|
|
63
|
+
};
|
|
64
|
+
const singleDefSource = {
|
|
65
|
+
rawSDL: source.documents[0].rawSDL, // TODO: might need to filter this
|
|
66
|
+
document: singleDefDocument,
|
|
67
|
+
location: source.documents[0].location,
|
|
68
|
+
};
|
|
69
|
+
// Update fragment imports to use the correct output path for this operation
|
|
70
|
+
const updatedFragmentImports = source.fragmentImports.map(fragmentImport => ({
|
|
71
|
+
...fragmentImport,
|
|
72
|
+
outputPath: filename, // Update to actual output path
|
|
73
|
+
}));
|
|
74
|
+
const plugins = [
|
|
75
|
+
...(options.config.globalNamespace
|
|
76
|
+
? []
|
|
77
|
+
: source.importStatements.map(importStatement => ({
|
|
78
|
+
add: { content: importStatement },
|
|
79
|
+
}))),
|
|
80
|
+
...options.plugins,
|
|
81
|
+
];
|
|
82
|
+
const config = {
|
|
83
|
+
...options.config,
|
|
84
|
+
exportFragmentSpreadSubTypes: true,
|
|
85
|
+
namespacedImportName: importTypesNamespace,
|
|
86
|
+
externalFragments: source.externalFragments,
|
|
87
|
+
fragmentImports: updatedFragmentImports,
|
|
88
|
+
};
|
|
89
|
+
artifacts.push({
|
|
90
|
+
...options,
|
|
91
|
+
filename,
|
|
92
|
+
documents: [singleDefSource],
|
|
93
|
+
plugins,
|
|
94
|
+
pluginMap,
|
|
95
|
+
config,
|
|
96
|
+
schema: options.schema,
|
|
97
|
+
schemaAst: schemaObject,
|
|
98
|
+
skipDocumentsValidation: typeof options.config.skipDocumentsValidation === 'undefined'
|
|
99
|
+
? { skipDuplicateValidation: true }
|
|
100
|
+
: options.config.skipDocumentsValidation,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return artifacts;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
export default preset;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isUsingTypes } from '@graphql-codegen/plugin-helpers';
|
|
2
|
+
import { generateImportStatement, resolveImportSource, } from '@graphql-codegen/visitor-plugin-common';
|
|
3
|
+
import buildFragmentResolver from './fragment-resolver.js';
|
|
4
|
+
/**
|
|
5
|
+
* Transform the preset's provided documents into single-file generator sources, while resolving fragment and user-defined imports
|
|
6
|
+
*
|
|
7
|
+
* Resolves user provided imports and fragment imports using the `DocumentImportResolverOptions`.
|
|
8
|
+
* Does not define specific plugins, but rather returns a string[] of `importStatements` for the calling plugin to make use of
|
|
9
|
+
*/
|
|
10
|
+
export function resolveDocumentImports(presetOptions, schemaObject, importResolverOptions, dedupeFragments = false) {
|
|
11
|
+
const resolveFragments = buildFragmentResolver(importResolverOptions, presetOptions, schemaObject, dedupeFragments);
|
|
12
|
+
const { baseOutputDir, documents } = presetOptions;
|
|
13
|
+
const { schemaTypesSource, baseDir, typesImport } = importResolverOptions;
|
|
14
|
+
return documents.map(documentFile => {
|
|
15
|
+
try {
|
|
16
|
+
// NOTE: We pass a placeholder filename here since we'll generate proper filenames per-operation later
|
|
17
|
+
// The important part is that fragment resolution will use the correct paths from the registry
|
|
18
|
+
const placeholderFilePath = documentFile.location;
|
|
19
|
+
const importStatements = [];
|
|
20
|
+
const { externalFragments, fragmentImports } = resolveFragments(placeholderFilePath, documentFile.document);
|
|
21
|
+
const externalFragmentsInjectedDocument = {
|
|
22
|
+
...documentFile.document,
|
|
23
|
+
definitions: [
|
|
24
|
+
...documentFile.document.definitions,
|
|
25
|
+
...externalFragments.map(fragment => fragment.node),
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
if (isUsingTypes(externalFragmentsInjectedDocument, [], schemaObject)) {
|
|
29
|
+
const schemaTypesImportStatement = generateImportStatement({
|
|
30
|
+
baseDir,
|
|
31
|
+
emitLegacyCommonJSImports: presetOptions.config.emitLegacyCommonJSImports,
|
|
32
|
+
importExtension: presetOptions.config.importExtension,
|
|
33
|
+
importSource: resolveImportSource(schemaTypesSource),
|
|
34
|
+
baseOutputDir,
|
|
35
|
+
outputPath: placeholderFilePath,
|
|
36
|
+
typesImport,
|
|
37
|
+
});
|
|
38
|
+
importStatements.unshift(schemaTypesImportStatement);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
filename: placeholderFilePath,
|
|
42
|
+
documents: [documentFile],
|
|
43
|
+
importStatements,
|
|
44
|
+
fragmentImports,
|
|
45
|
+
externalFragments,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
throw new Error(`Unable to validate GraphQL document! \n
|
|
50
|
+
File ${documentFile.location} caused error:
|
|
51
|
+
${e.message || e.toString()}`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
package/esm/utils.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { join, dirname } from 'path';
|
|
2
|
+
import { isInterfaceType, isObjectType, isUnionType, TypeInfo, visit, visitWithTypeInfo, } from 'graphql';
|
|
3
|
+
import { getPossibleTypes, separateSelectionSet } from '@graphql-codegen/visitor-plugin-common';
|
|
4
|
+
/**
|
|
5
|
+
* Generate output file path for a specific operation/fragment
|
|
6
|
+
*/
|
|
7
|
+
export function generateOperationFilePath(sourceLocation, operationName, folder, extension) {
|
|
8
|
+
const dir = dirname(sourceLocation);
|
|
9
|
+
return join(dir, folder, `${operationName}${extension}`).replace(/\\/g, '/');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Analyzes fragment usage in a GraphQL document.
|
|
13
|
+
* Returns information about which fragments are used and which specific types they're used with.
|
|
14
|
+
*/
|
|
15
|
+
export function analyzeFragmentUsage(documentNode, fragmentRegistry, schema) {
|
|
16
|
+
const localFragments = getLocalFragments(documentNode);
|
|
17
|
+
const fragmentsInUse = extractExternalFragmentsInUse(documentNode, fragmentRegistry, localFragments);
|
|
18
|
+
const usedFragmentTypes = analyzeFragmentTypeUsage(documentNode, fragmentRegistry, schema, localFragments, fragmentsInUse);
|
|
19
|
+
return { fragmentsInUse, usedFragmentTypes };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get all fragment definitions that are local to this document
|
|
23
|
+
*/
|
|
24
|
+
function getLocalFragments(documentNode) {
|
|
25
|
+
const localFragments = new Set();
|
|
26
|
+
visit(documentNode, {
|
|
27
|
+
FragmentDefinition: node => {
|
|
28
|
+
localFragments.add(node.name.value);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
return localFragments;
|
|
32
|
+
}
|
|
33
|
+
export function extractExternalFragmentsInUse(documentNode, fragmentNameToFile, localFragment, result = {}, level = 0) {
|
|
34
|
+
// Then, look for all used fragments in this document
|
|
35
|
+
visit(documentNode, {
|
|
36
|
+
FragmentSpread: node => {
|
|
37
|
+
if (!localFragment.has(node.name.value) &&
|
|
38
|
+
(result[node.name.value] === undefined || level < result[node.name.value])) {
|
|
39
|
+
result[node.name.value] = level;
|
|
40
|
+
if (fragmentNameToFile[node.name.value]) {
|
|
41
|
+
extractExternalFragmentsInUse(fragmentNameToFile[node.name.value].node, fragmentNameToFile, localFragment, result, level + 1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Analyze which specific types each fragment is used with (for polymorphic fragments)
|
|
50
|
+
*/
|
|
51
|
+
function analyzeFragmentTypeUsage(documentNode, fragmentRegistry, schema, localFragments, fragmentsInUse) {
|
|
52
|
+
const usedFragmentTypes = {};
|
|
53
|
+
const typeInfo = new TypeInfo(schema);
|
|
54
|
+
visit(documentNode, visitWithTypeInfo(typeInfo, {
|
|
55
|
+
Field: (node) => {
|
|
56
|
+
if (!node.selectionSet)
|
|
57
|
+
return;
|
|
58
|
+
const fieldType = typeInfo.getType();
|
|
59
|
+
if (!fieldType)
|
|
60
|
+
return;
|
|
61
|
+
const baseType = getBaseType(fieldType);
|
|
62
|
+
if (isObjectType(baseType) || isInterfaceType(baseType) || isUnionType(baseType)) {
|
|
63
|
+
analyzeSelectionSetTypeContext(node.selectionSet, baseType.name, usedFragmentTypes, fragmentRegistry, schema, localFragments);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
}));
|
|
67
|
+
const result = {};
|
|
68
|
+
// Fill in missing types for multi-type fragments
|
|
69
|
+
for (const fragmentName in fragmentsInUse) {
|
|
70
|
+
const fragment = fragmentRegistry[fragmentName];
|
|
71
|
+
if (!fragment || fragment.possibleTypes.length <= 1)
|
|
72
|
+
continue;
|
|
73
|
+
const usedTypes = usedFragmentTypes[fragmentName];
|
|
74
|
+
result[fragmentName] = (usedTypes === null || usedTypes === void 0 ? void 0 : usedTypes.size) > 0 ? Array.from(usedTypes) : fragment.possibleTypes;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Analyze fragment usage within a specific selection set and type context
|
|
80
|
+
*/
|
|
81
|
+
function analyzeSelectionSetTypeContext(selectionSet, currentTypeName, usedFragmentTypes, fragmentRegistry, schema, localFragments) {
|
|
82
|
+
var _a, _b;
|
|
83
|
+
var _c;
|
|
84
|
+
const { spreads, inlines } = separateSelectionSet(selectionSet.selections);
|
|
85
|
+
// Process fragment spreads in this type context
|
|
86
|
+
for (const spread of spreads) {
|
|
87
|
+
if (localFragments.has(spread.name.value))
|
|
88
|
+
continue;
|
|
89
|
+
const fragment = fragmentRegistry[spread.name.value];
|
|
90
|
+
if (!fragment || fragment.possibleTypes.length <= 1)
|
|
91
|
+
continue;
|
|
92
|
+
const currentType = schema.getType(currentTypeName);
|
|
93
|
+
if (!currentType)
|
|
94
|
+
continue;
|
|
95
|
+
const possibleTypes = getPossibleTypes(schema, currentType).map(t => t.name);
|
|
96
|
+
const matchingTypes = possibleTypes.filter(type => fragment.possibleTypes.includes(type));
|
|
97
|
+
if (matchingTypes.length > 0) {
|
|
98
|
+
const typeSet = ((_a = usedFragmentTypes[_c = spread.name.value]) !== null && _a !== void 0 ? _a : (usedFragmentTypes[_c] = new Set()));
|
|
99
|
+
matchingTypes.forEach(type => typeSet.add(type));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Process inline fragments
|
|
103
|
+
for (const inline of inlines) {
|
|
104
|
+
if (((_b = inline.typeCondition) === null || _b === void 0 ? void 0 : _b.name.value) && inline.selectionSet) {
|
|
105
|
+
analyzeSelectionSetTypeContext(inline.selectionSet, inline.typeCondition.name.value, usedFragmentTypes, fragmentRegistry, schema, localFragments);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function getBaseType(type) {
|
|
110
|
+
let baseType = type;
|
|
111
|
+
while ('ofType' in baseType && baseType.ofType) {
|
|
112
|
+
baseType = baseType.ofType;
|
|
113
|
+
}
|
|
114
|
+
return baseType;
|
|
115
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nmarks/graphql-codegen-per-operation-file-preset",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "GraphQL Code Generator preset for generating one file per operation/fragment",
|
|
5
|
+
"peerDependencies": {
|
|
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"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@graphql-codegen/add": "^6.0.0",
|
|
10
|
+
"@graphql-codegen/plugin-helpers": "^6.1.0",
|
|
11
|
+
"@graphql-codegen/visitor-plugin-common": "^6.2.1",
|
|
12
|
+
"@graphql-tools/utils": "^10.0.0",
|
|
13
|
+
"tslib": "^2.8.1"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/dotansimha/graphql-code-generator-community.git",
|
|
18
|
+
"directory": "packages/presets/per-operation-file"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">= 16.0.0"
|
|
23
|
+
},
|
|
24
|
+
"main": "cjs/index.js",
|
|
25
|
+
"module": "esm/index.js",
|
|
26
|
+
"typings": "typings/index.d.ts",
|
|
27
|
+
"typescript": {
|
|
28
|
+
"definition": "typings/index.d.ts"
|
|
29
|
+
},
|
|
30
|
+
"type": "module",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"require": {
|
|
34
|
+
"types": "./typings/index.d.cts",
|
|
35
|
+
"default": "./cjs/index.js"
|
|
36
|
+
},
|
|
37
|
+
"import": {
|
|
38
|
+
"types": "./typings/index.d.ts",
|
|
39
|
+
"default": "./esm/index.js"
|
|
40
|
+
},
|
|
41
|
+
"default": {
|
|
42
|
+
"types": "./typings/index.d.ts",
|
|
43
|
+
"default": "./esm/index.js"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"./package.json": "./package.json"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DocumentNode, FragmentDefinitionNode, GraphQLSchema } from 'graphql';
|
|
2
|
+
import { Types } from '@graphql-codegen/plugin-helpers';
|
|
3
|
+
import { FragmentImport, ImportDeclaration, LoadedFragment, ParsedConfig } from '@graphql-codegen/visitor-plugin-common';
|
|
4
|
+
import { DocumentImportResolverOptions } from './resolve-document-imports.cjs';
|
|
5
|
+
export interface PerOperationFileParsedConfig extends ParsedConfig {
|
|
6
|
+
importTypesNamespace?: string;
|
|
7
|
+
dedupeOperationSuffix: boolean;
|
|
8
|
+
omitOperationSuffix: boolean;
|
|
9
|
+
fragmentVariablePrefix: string;
|
|
10
|
+
fragmentVariableSuffix: string;
|
|
11
|
+
}
|
|
12
|
+
export type FragmentRegistry = {
|
|
13
|
+
[fragmentName: string]: {
|
|
14
|
+
filePath: string;
|
|
15
|
+
onType: string;
|
|
16
|
+
node: FragmentDefinitionNode;
|
|
17
|
+
imports: Array<FragmentImport>;
|
|
18
|
+
possibleTypes: string[];
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
|
|
23
|
+
*/
|
|
24
|
+
export default function buildFragmentResolver<T>(collectorOptions: DocumentImportResolverOptions, presetOptions: Types.PresetFnArgs<T>, schemaObject: GraphQLSchema, dedupeFragments?: boolean): (generatedFilePath: string, documentFileContent: DocumentNode) => {
|
|
25
|
+
externalFragments: LoadedFragment<{
|
|
26
|
+
level: number;
|
|
27
|
+
}>[];
|
|
28
|
+
fragmentImports: ImportDeclaration<FragmentImport>[];
|
|
29
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DocumentNode, FragmentDefinitionNode, GraphQLSchema } from 'graphql';
|
|
2
|
+
import { Types } from '@graphql-codegen/plugin-helpers';
|
|
3
|
+
import { FragmentImport, ImportDeclaration, LoadedFragment, ParsedConfig } from '@graphql-codegen/visitor-plugin-common';
|
|
4
|
+
import { DocumentImportResolverOptions } from './resolve-document-imports.js';
|
|
5
|
+
export interface PerOperationFileParsedConfig extends ParsedConfig {
|
|
6
|
+
importTypesNamespace?: string;
|
|
7
|
+
dedupeOperationSuffix: boolean;
|
|
8
|
+
omitOperationSuffix: boolean;
|
|
9
|
+
fragmentVariablePrefix: string;
|
|
10
|
+
fragmentVariableSuffix: string;
|
|
11
|
+
}
|
|
12
|
+
export type FragmentRegistry = {
|
|
13
|
+
[fragmentName: string]: {
|
|
14
|
+
filePath: string;
|
|
15
|
+
onType: string;
|
|
16
|
+
node: FragmentDefinitionNode;
|
|
17
|
+
imports: Array<FragmentImport>;
|
|
18
|
+
possibleTypes: string[];
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
|
|
23
|
+
*/
|
|
24
|
+
export default function buildFragmentResolver<T>(collectorOptions: DocumentImportResolverOptions, presetOptions: Types.PresetFnArgs<T>, schemaObject: GraphQLSchema, dedupeFragments?: boolean): (generatedFilePath: string, documentFileContent: DocumentNode) => {
|
|
25
|
+
externalFragments: LoadedFragment<{
|
|
26
|
+
level: number;
|
|
27
|
+
}>[];
|
|
28
|
+
fragmentImports: ImportDeclaration<FragmentImport>[];
|
|
29
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Types } from '@graphql-codegen/plugin-helpers';
|
|
2
|
+
export type PerOperationFileConfig = {
|
|
3
|
+
/**
|
|
4
|
+
* @description Required, should point to the base schema types file.
|
|
5
|
+
* The key of the output is used a the base path for this file.
|
|
6
|
+
*
|
|
7
|
+
* If you wish to use an NPM package or a local workspace package, make sure to prefix the package name with `~`.
|
|
8
|
+
*
|
|
9
|
+
* @exampleMarkdown
|
|
10
|
+
* ```ts filename="codegen.ts"
|
|
11
|
+
* import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
12
|
+
*
|
|
13
|
+
* const config: CodegenConfig = {
|
|
14
|
+
* // ...
|
|
15
|
+
* generates: {
|
|
16
|
+
* 'path/to/file.ts': {
|
|
17
|
+
* preset: 'per-operation-file',
|
|
18
|
+
* plugins: ['typescript-operations'],
|
|
19
|
+
* presetConfig: {
|
|
20
|
+
* baseTypesPath: 'types.ts'
|
|
21
|
+
* },
|
|
22
|
+
* },
|
|
23
|
+
* },
|
|
24
|
+
* };
|
|
25
|
+
* export default config;
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
baseTypesPath: string;
|
|
29
|
+
/**
|
|
30
|
+
* @description Optional, sets the extension for the generated files.
|
|
31
|
+
* @default .ts
|
|
32
|
+
*
|
|
33
|
+
* @exampleMarkdown
|
|
34
|
+
* ```ts filename="codegen.ts"
|
|
35
|
+
* import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
36
|
+
*
|
|
37
|
+
* const config: CodegenConfig = {
|
|
38
|
+
* // ...
|
|
39
|
+
* generates: {
|
|
40
|
+
* 'path/to/file.ts': {
|
|
41
|
+
* preset: 'per-operation-file',
|
|
42
|
+
* plugins: ['typescript-operations'],
|
|
43
|
+
* presetConfig: {
|
|
44
|
+
* baseTypesPath: 'types.ts',
|
|
45
|
+
* extension: '.generated.ts',
|
|
46
|
+
* },
|
|
47
|
+
* },
|
|
48
|
+
* },
|
|
49
|
+
* };
|
|
50
|
+
* export default config;
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
extension?: string;
|
|
54
|
+
/**
|
|
55
|
+
* @description Optional, override the `cwd` of the execution.
|
|
56
|
+
* @default process.cwd()
|
|
57
|
+
*
|
|
58
|
+
* @exampleMarkdown
|
|
59
|
+
* ```ts filename="codegen.ts"
|
|
60
|
+
* import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
61
|
+
*
|
|
62
|
+
* const config: CodegenConfig = {
|
|
63
|
+
* // ...
|
|
64
|
+
* generates: {
|
|
65
|
+
* 'path/to/file.ts': {
|
|
66
|
+
* preset: 'per-operation-file',
|
|
67
|
+
* plugins: ['typescript-operations'],
|
|
68
|
+
* presetConfig: {
|
|
69
|
+
* baseTypesPath: 'types.ts',
|
|
70
|
+
* cwd: '/some/path'
|
|
71
|
+
* },
|
|
72
|
+
* },
|
|
73
|
+
* },
|
|
74
|
+
* };
|
|
75
|
+
* export default config;
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
cwd?: string;
|
|
79
|
+
/**
|
|
80
|
+
* @description Optional, defines a folder where the generated files will be created.
|
|
81
|
+
* @default '__generated__'
|
|
82
|
+
*
|
|
83
|
+
* @exampleMarkdown
|
|
84
|
+
* ```ts filename="codegen.ts"
|
|
85
|
+
* import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
86
|
+
*
|
|
87
|
+
* const config: CodegenConfig = {
|
|
88
|
+
* // ...
|
|
89
|
+
* generates: {
|
|
90
|
+
* 'path/to/file.ts': {
|
|
91
|
+
* preset: 'per-operation-file',
|
|
92
|
+
* plugins: ['typescript-operations'],
|
|
93
|
+
* presetConfig: {
|
|
94
|
+
* baseTypesPath: 'types.ts',
|
|
95
|
+
* folder: '__generated__'
|
|
96
|
+
* },
|
|
97
|
+
* },
|
|
98
|
+
* },
|
|
99
|
+
* };
|
|
100
|
+
* export default config;
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
folder?: string;
|
|
104
|
+
/**
|
|
105
|
+
* @description Optional, override the name of the import namespace used to import from the `baseTypesPath` file.
|
|
106
|
+
* @default Types
|
|
107
|
+
*
|
|
108
|
+
* @exampleMarkdown
|
|
109
|
+
* ```ts filename="codegen.ts"
|
|
110
|
+
* import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
111
|
+
*
|
|
112
|
+
* const config: CodegenConfig = {
|
|
113
|
+
* // ...
|
|
114
|
+
* generates: {
|
|
115
|
+
* 'path/to/file.ts': {
|
|
116
|
+
* preset: 'per-operation-file',
|
|
117
|
+
* plugins: ['typescript-operations'],
|
|
118
|
+
* presetConfig: {
|
|
119
|
+
* baseTypesPath: 'types.ts',
|
|
120
|
+
* importTypesNamespace: 'SchemaTypes'
|
|
121
|
+
* },
|
|
122
|
+
* },
|
|
123
|
+
* },
|
|
124
|
+
* };
|
|
125
|
+
* export default config;
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
importTypesNamespace?: string;
|
|
129
|
+
};
|
|
130
|
+
export declare const preset: Types.OutputPreset<PerOperationFileConfig>;
|
|
131
|
+
export default preset;
|