@nmarks/graphql-codegen-per-operation-file-preset 1.0.8 → 1.0.10
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 +1 -1
- package/cjs/index.js +139 -26
- package/cjs/resolve-document-imports.js +3 -2
- package/esm/fragment-resolver.js +1 -1
- package/esm/index.js +139 -26
- package/esm/resolve-document-imports.js +3 -2
- package/package.json +1 -1
- package/typings/fragment-resolver.d.cts +8 -5
- package/typings/fragment-resolver.d.ts +8 -5
- package/typings/resolve-document-imports.d.cts +4 -1
- package/typings/resolve-document-imports.d.ts +4 -1
package/cjs/fragment-resolver.js
CHANGED
package/cjs/index.js
CHANGED
|
@@ -24,18 +24,17 @@ function extractDefinitions(document) {
|
|
|
24
24
|
return definitions;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* Example:
|
|
31
|
-
* Source file has: query MyQuery { ...MyFragment } + fragment MyFragment { ... }
|
|
32
|
-
* After split: MyQuery.ts needs to import from MyFragment.ts
|
|
27
|
+
* Collects all fragment names directly referenced by a definition via FragmentSpread nodes.
|
|
28
|
+
* This only gets DIRECT references - not transitive dependencies.
|
|
33
29
|
*/
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
function getDirectlyReferencedFragments(definition) {
|
|
31
|
+
const referencedFragments = new Set();
|
|
32
|
+
(0, graphql_1.visit)(definition, {
|
|
33
|
+
FragmentSpread: (node) => {
|
|
34
|
+
referencedFragments.add(node.name.value);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
return referencedFragments;
|
|
39
38
|
}
|
|
40
39
|
/**
|
|
41
40
|
* Creates LoadedFragment objects for local fragments so they can be treated as external fragments
|
|
@@ -50,18 +49,72 @@ function createLoadedFragments(fragments) {
|
|
|
50
49
|
node: frag,
|
|
51
50
|
}));
|
|
52
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Analyzes fragment dependencies for a definition.
|
|
54
|
+
*
|
|
55
|
+
* Returns two sets:
|
|
56
|
+
* 1. `allNeeded` - ALL fragments needed (including transitive) - for externalFragments
|
|
57
|
+
* 2. `directlyNeeded` - Only DIRECT dependencies - for fragmentImports
|
|
58
|
+
*
|
|
59
|
+
* Why the distinction?
|
|
60
|
+
* - `externalFragments` must include all transitive deps so the plugin can resolve types
|
|
61
|
+
* - `fragmentImports` should only include direct deps; transitive deps are imported by intermediate files
|
|
62
|
+
*
|
|
63
|
+
* Example: Query → FragA → FragB
|
|
64
|
+
* - Query's externalFragments: [FragA, FragB] (plugin needs both)
|
|
65
|
+
* - Query's fragmentImports: [FragA] (only direct; FragA.ts imports FragB)
|
|
66
|
+
*
|
|
67
|
+
* @param definition - The operation/fragment definition to analyze
|
|
68
|
+
* @param externalFragments - All external fragments available (from other files)
|
|
69
|
+
* @param localFragmentMap - Map of local fragment names to their definitions
|
|
70
|
+
*/
|
|
71
|
+
function analyzeFragmentDependencies(definition, externalFragments, localFragmentMap) {
|
|
72
|
+
const allNeeded = new Set();
|
|
73
|
+
// Build a map for quick lookup of external fragment nodes
|
|
74
|
+
const externalFragmentMap = new Map();
|
|
75
|
+
for (const frag of externalFragments) {
|
|
76
|
+
externalFragmentMap.set(frag.name, frag.node);
|
|
77
|
+
}
|
|
78
|
+
function collectTransitive(fragmentNames) {
|
|
79
|
+
for (const name of fragmentNames) {
|
|
80
|
+
if (allNeeded.has(name))
|
|
81
|
+
continue;
|
|
82
|
+
// Check if this fragment exists (either local or external)
|
|
83
|
+
const localDef = localFragmentMap.get(name);
|
|
84
|
+
const externalDef = externalFragmentMap.get(name);
|
|
85
|
+
const fragmentDef = localDef || externalDef;
|
|
86
|
+
if (fragmentDef) {
|
|
87
|
+
allNeeded.add(name);
|
|
88
|
+
// Get transitive dependencies
|
|
89
|
+
const transitiveDeps = getDirectlyReferencedFragments(fragmentDef);
|
|
90
|
+
collectTransitive(transitiveDeps);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Get direct references from the definition
|
|
95
|
+
const directlyNeeded = getDirectlyReferencedFragments(definition);
|
|
96
|
+
// Collect all transitive dependencies (includes direct ones)
|
|
97
|
+
collectTransitive(directlyNeeded);
|
|
98
|
+
return { allNeeded, directlyNeeded };
|
|
99
|
+
}
|
|
53
100
|
/**
|
|
54
101
|
* Creates fragment import declarations for local fragments.
|
|
55
102
|
* After splitting, these fragments will be in separate files, so we need to generate imports.
|
|
56
103
|
*
|
|
57
104
|
* Example: query references RetailerServicesData → import from './RetailerServicesData.ts'
|
|
105
|
+
*
|
|
106
|
+
* NOTE: We use the fragment registry to get the proper import identifiers (with correct naming/suffix)
|
|
107
|
+
* instead of hardcoding them, since naming depends on config (dedupeOperationSuffix, etc.)
|
|
58
108
|
*/
|
|
59
|
-
function createLocalFragmentImports(localFragments, sourceLocation, currentFilename, folder, extension, options) {
|
|
109
|
+
function createLocalFragmentImports(localFragments, fragmentRegistry, sourceLocation, currentFilename, folder, extension, options) {
|
|
60
110
|
return localFragments.map((frag) => {
|
|
61
111
|
var _a;
|
|
62
112
|
const fragmentFilePath = (0, utils_js_1.generateOperationFilePath)(sourceLocation, frag.name.value, folder, extension);
|
|
63
|
-
//
|
|
64
|
-
|
|
113
|
+
// Get the proper import identifiers from the fragment registry
|
|
114
|
+
// (calculated by fragment-resolver using BaseVisitor with correct naming conventions)
|
|
115
|
+
const registryEntry = fragmentRegistry[frag.name.value];
|
|
116
|
+
const identifiers = (registryEntry === null || registryEntry === void 0 ? void 0 : registryEntry.imports) || [
|
|
117
|
+
// Fallback if not in registry (shouldn't happen, but be safe)
|
|
65
118
|
{
|
|
66
119
|
name: `${frag.name.value}FragmentDoc`,
|
|
67
120
|
kind: 'document',
|
|
@@ -97,7 +150,8 @@ function createLocalFragmentImports(localFragments, sourceLocation, currentFilen
|
|
|
97
150
|
* We ONLY need the Types import when generated code actually references the Types namespace:
|
|
98
151
|
*
|
|
99
152
|
* ✅ NEEDS IMPORT:
|
|
100
|
-
* - Operations (query/mutation/subscription) - generate `Types.Exact<{...}>` for Variables type
|
|
153
|
+
* - Operations (query/mutation/subscription) - ALWAYS generate `Types.Exact<{...}>` for Variables type
|
|
154
|
+
* (even operations without variables: `Types.Exact<{ [key: string]: never; }>`)
|
|
101
155
|
* - Enums - generate `role: Types.Role`
|
|
102
156
|
* - Custom scalars (unless mapped to primitives) - generate `date: Types.DateTime`
|
|
103
157
|
* - Input types - used in variables as `Types.MyInput`
|
|
@@ -116,8 +170,11 @@ function needsSchemaTypesImport(document, schema, config) {
|
|
|
116
170
|
const scalarMappings = config.scalars || {};
|
|
117
171
|
/**
|
|
118
172
|
* STEP 1: Check for operations (queries/mutations/subscriptions)
|
|
119
|
-
*
|
|
173
|
+
* ALL operations generate a Variables type that uses Types.Exact, even without variables:
|
|
120
174
|
* `export type MyQueryVariables = Types.Exact<{ [key: string]: never; }>;`
|
|
175
|
+
*
|
|
176
|
+
* This is true for typescript-operations plugin regardless of whether the operation
|
|
177
|
+
* has any variable definitions or not.
|
|
121
178
|
*/
|
|
122
179
|
let hasOperation = false;
|
|
123
180
|
const referencedTypeNames = new Set();
|
|
@@ -133,6 +190,7 @@ function needsSchemaTypesImport(document, schema, config) {
|
|
|
133
190
|
referencedTypeNames.add(node.typeCondition.name.value);
|
|
134
191
|
},
|
|
135
192
|
});
|
|
193
|
+
// All operations need Types import for Variables type
|
|
136
194
|
if (hasOperation) {
|
|
137
195
|
return true;
|
|
138
196
|
}
|
|
@@ -195,9 +253,17 @@ function needsSchemaTypesImport(document, schema, config) {
|
|
|
195
253
|
* STEP 4: Check field return types
|
|
196
254
|
* Use TypeInfo to properly resolve field types through the schema.
|
|
197
255
|
* This catches cases like: user { role } where 'role' returns an enum.
|
|
256
|
+
*
|
|
257
|
+
* IMPORTANT: Only check the FIRST definition (the actual operation/fragment being generated).
|
|
258
|
+
* Don't check external fragments - those will have their own imports.
|
|
198
259
|
*/
|
|
260
|
+
// Create a document with ONLY the first definition for type checking
|
|
261
|
+
const singleDefOnly = {
|
|
262
|
+
kind: graphql_1.Kind.DOCUMENT,
|
|
263
|
+
definitions: [document.definitions[0]],
|
|
264
|
+
};
|
|
199
265
|
const typeInfo = new graphql_1.TypeInfo(schema);
|
|
200
|
-
(0, graphql_1.visit)(
|
|
266
|
+
(0, graphql_1.visit)(singleDefOnly, (0, graphql_1.visitWithTypeInfo)(typeInfo, {
|
|
201
267
|
Field: () => {
|
|
202
268
|
const fieldType = typeInfo.getType();
|
|
203
269
|
if (checkType(fieldType)) {
|
|
@@ -261,7 +327,7 @@ exports.preset = {
|
|
|
261
327
|
* This uses our adapted fragment-resolver that generates paths based on fragment NAMES
|
|
262
328
|
* (not source file names), enabling proper imports after splitting.
|
|
263
329
|
*/
|
|
264
|
-
const sources = (0, resolve_document_imports_js_1.resolveDocumentImports)(options, schemaObject, {
|
|
330
|
+
const { results: sources, fragmentRegistry } = (0, resolve_document_imports_js_1.resolveDocumentImports)(options, schemaObject, {
|
|
265
331
|
baseDir,
|
|
266
332
|
folder,
|
|
267
333
|
extension,
|
|
@@ -308,25 +374,72 @@ exports.preset = {
|
|
|
308
374
|
* resolver marked MyFrag as "local" (same document).
|
|
309
375
|
*
|
|
310
376
|
* Solution: Treat local fragments as external after splitting.
|
|
377
|
+
* Only include fragments that are actually referenced by this definition.
|
|
311
378
|
*/
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
379
|
+
// Build a map of local fragment names -> definitions for lookup
|
|
380
|
+
const localFragmentMap = new Map();
|
|
381
|
+
for (const d of definitions) {
|
|
382
|
+
if (d.definition.kind === graphql_1.Kind.FRAGMENT_DEFINITION && d.name !== name) {
|
|
383
|
+
localFragmentMap.set(d.name, d.definition);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* ANALYZE FRAGMENT DEPENDENCIES
|
|
388
|
+
*
|
|
389
|
+
* We need two different sets of fragments:
|
|
390
|
+
* 1. allNeeded - ALL transitive deps for externalFragments (plugin needs these for type resolution)
|
|
391
|
+
* 2. directlyNeeded - Only DIRECT deps for fragmentImports (transitive deps handled by intermediate files)
|
|
392
|
+
*
|
|
393
|
+
* Example: Query → FragA → FragB
|
|
394
|
+
* - externalFragments: [FragA, FragB] (plugin needs both for types)
|
|
395
|
+
* - fragmentImports: [FragA] (only direct; FragA.ts already imports FragB)
|
|
396
|
+
*/
|
|
397
|
+
const { allNeeded, directlyNeeded } = analyzeFragmentDependencies(definition, source.externalFragments, localFragmentMap);
|
|
398
|
+
// Filter local fragments to only those in the transitive dependency set
|
|
399
|
+
// (for externalFragments - plugin needs all of them)
|
|
400
|
+
const allLocalFragments = Array.from(localFragmentMap.entries())
|
|
401
|
+
.filter(([fragName]) => allNeeded.has(fragName))
|
|
402
|
+
.map(([, fragDef]) => fragDef);
|
|
403
|
+
const localFragmentNodes = createLoadedFragments(allLocalFragments);
|
|
404
|
+
// Filter external fragments to only those actually needed by this definition
|
|
405
|
+
const filteredExternalFragments = source.externalFragments.filter(frag => allNeeded.has(frag.name));
|
|
406
|
+
// Combine filtered external fragments + local fragments (all transitive deps)
|
|
315
407
|
const allExternalFragments = [
|
|
316
|
-
...
|
|
408
|
+
...filteredExternalFragments,
|
|
317
409
|
...localFragmentNodes,
|
|
318
410
|
];
|
|
319
411
|
/**
|
|
320
412
|
* UPDATE FRAGMENT IMPORTS
|
|
321
413
|
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
414
|
+
* Only import DIRECT dependencies, not transitive ones.
|
|
415
|
+
* Transitive dependencies are imported by the intermediate fragment files.
|
|
416
|
+
*
|
|
417
|
+
* 1. External fragments: Filter to only DIRECTLY needed ones, update outputPath
|
|
418
|
+
* 2. Local fragments: Generate imports only for DIRECTLY referenced fragments
|
|
324
419
|
*/
|
|
325
|
-
const updatedFragmentImports = source.fragmentImports
|
|
420
|
+
const updatedFragmentImports = source.fragmentImports
|
|
421
|
+
// Filter to only include imports for fragments DIRECTLY referenced
|
|
422
|
+
.filter(fragmentImport => {
|
|
423
|
+
// Check if any of the import's identifiers are for directly needed fragments
|
|
424
|
+
return fragmentImport.importSource.identifiers.some(id => {
|
|
425
|
+
// Check if this identifier's name matches a directly needed fragment
|
|
426
|
+
for (const neededName of directlyNeeded) {
|
|
427
|
+
if (id.name === neededName || id.name.startsWith(neededName)) {
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return false;
|
|
432
|
+
});
|
|
433
|
+
})
|
|
434
|
+
.map(fragmentImport => ({
|
|
326
435
|
...fragmentImport,
|
|
327
436
|
outputPath: filename,
|
|
328
437
|
}));
|
|
329
|
-
|
|
438
|
+
// Only create imports for DIRECTLY referenced local fragments
|
|
439
|
+
const directlyNeededLocalFragments = Array.from(localFragmentMap.entries())
|
|
440
|
+
.filter(([fragName]) => directlyNeeded.has(fragName))
|
|
441
|
+
.map(([, fragDef]) => fragDef);
|
|
442
|
+
const localFragmentImports = createLocalFragmentImports(directlyNeededLocalFragments, fragmentRegistry, source.documents[0].location, filename, folder, extension, {
|
|
330
443
|
baseDir,
|
|
331
444
|
baseOutputDir: options.baseOutputDir,
|
|
332
445
|
emitLegacyCommonJSImports: options.config.emitLegacyCommonJSImports,
|
|
@@ -12,10 +12,10 @@ const fragment_resolver_js_1 = tslib_1.__importDefault(require("./fragment-resol
|
|
|
12
12
|
* Does not define specific plugins, but rather returns a string[] of `importStatements` for the calling plugin to make use of
|
|
13
13
|
*/
|
|
14
14
|
function resolveDocumentImports(presetOptions, schemaObject, importResolverOptions, dedupeFragments = false) {
|
|
15
|
-
const resolveFragments = (0, fragment_resolver_js_1.default)(importResolverOptions, presetOptions, schemaObject, dedupeFragments);
|
|
15
|
+
const { resolveFragments, fragmentRegistry } = (0, fragment_resolver_js_1.default)(importResolverOptions, presetOptions, schemaObject, dedupeFragments);
|
|
16
16
|
const { baseOutputDir, documents } = presetOptions;
|
|
17
17
|
const { schemaTypesSource, baseDir, typesImport } = importResolverOptions;
|
|
18
|
-
|
|
18
|
+
const results = documents.map(documentFile => {
|
|
19
19
|
try {
|
|
20
20
|
// NOTE: We pass a placeholder filename here since we'll generate proper filenames per-operation later
|
|
21
21
|
// The important part is that fragment resolution will use the correct paths from the registry
|
|
@@ -55,4 +55,5 @@ function resolveDocumentImports(presetOptions, schemaObject, importResolverOptio
|
|
|
55
55
|
${e.message || e.toString()}`);
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
|
+
return { results, fragmentRegistry };
|
|
58
59
|
}
|
package/esm/fragment-resolver.js
CHANGED
package/esm/index.js
CHANGED
|
@@ -20,18 +20,17 @@ function extractDefinitions(document) {
|
|
|
20
20
|
return definitions;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Example:
|
|
27
|
-
* Source file has: query MyQuery { ...MyFragment } + fragment MyFragment { ... }
|
|
28
|
-
* After split: MyQuery.ts needs to import from MyFragment.ts
|
|
23
|
+
* Collects all fragment names directly referenced by a definition via FragmentSpread nodes.
|
|
24
|
+
* This only gets DIRECT references - not transitive dependencies.
|
|
29
25
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
function getDirectlyReferencedFragments(definition) {
|
|
27
|
+
const referencedFragments = new Set();
|
|
28
|
+
visit(definition, {
|
|
29
|
+
FragmentSpread: (node) => {
|
|
30
|
+
referencedFragments.add(node.name.value);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
return referencedFragments;
|
|
35
34
|
}
|
|
36
35
|
/**
|
|
37
36
|
* Creates LoadedFragment objects for local fragments so they can be treated as external fragments
|
|
@@ -46,18 +45,72 @@ function createLoadedFragments(fragments) {
|
|
|
46
45
|
node: frag,
|
|
47
46
|
}));
|
|
48
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Analyzes fragment dependencies for a definition.
|
|
50
|
+
*
|
|
51
|
+
* Returns two sets:
|
|
52
|
+
* 1. `allNeeded` - ALL fragments needed (including transitive) - for externalFragments
|
|
53
|
+
* 2. `directlyNeeded` - Only DIRECT dependencies - for fragmentImports
|
|
54
|
+
*
|
|
55
|
+
* Why the distinction?
|
|
56
|
+
* - `externalFragments` must include all transitive deps so the plugin can resolve types
|
|
57
|
+
* - `fragmentImports` should only include direct deps; transitive deps are imported by intermediate files
|
|
58
|
+
*
|
|
59
|
+
* Example: Query → FragA → FragB
|
|
60
|
+
* - Query's externalFragments: [FragA, FragB] (plugin needs both)
|
|
61
|
+
* - Query's fragmentImports: [FragA] (only direct; FragA.ts imports FragB)
|
|
62
|
+
*
|
|
63
|
+
* @param definition - The operation/fragment definition to analyze
|
|
64
|
+
* @param externalFragments - All external fragments available (from other files)
|
|
65
|
+
* @param localFragmentMap - Map of local fragment names to their definitions
|
|
66
|
+
*/
|
|
67
|
+
function analyzeFragmentDependencies(definition, externalFragments, localFragmentMap) {
|
|
68
|
+
const allNeeded = new Set();
|
|
69
|
+
// Build a map for quick lookup of external fragment nodes
|
|
70
|
+
const externalFragmentMap = new Map();
|
|
71
|
+
for (const frag of externalFragments) {
|
|
72
|
+
externalFragmentMap.set(frag.name, frag.node);
|
|
73
|
+
}
|
|
74
|
+
function collectTransitive(fragmentNames) {
|
|
75
|
+
for (const name of fragmentNames) {
|
|
76
|
+
if (allNeeded.has(name))
|
|
77
|
+
continue;
|
|
78
|
+
// Check if this fragment exists (either local or external)
|
|
79
|
+
const localDef = localFragmentMap.get(name);
|
|
80
|
+
const externalDef = externalFragmentMap.get(name);
|
|
81
|
+
const fragmentDef = localDef || externalDef;
|
|
82
|
+
if (fragmentDef) {
|
|
83
|
+
allNeeded.add(name);
|
|
84
|
+
// Get transitive dependencies
|
|
85
|
+
const transitiveDeps = getDirectlyReferencedFragments(fragmentDef);
|
|
86
|
+
collectTransitive(transitiveDeps);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Get direct references from the definition
|
|
91
|
+
const directlyNeeded = getDirectlyReferencedFragments(definition);
|
|
92
|
+
// Collect all transitive dependencies (includes direct ones)
|
|
93
|
+
collectTransitive(directlyNeeded);
|
|
94
|
+
return { allNeeded, directlyNeeded };
|
|
95
|
+
}
|
|
49
96
|
/**
|
|
50
97
|
* Creates fragment import declarations for local fragments.
|
|
51
98
|
* After splitting, these fragments will be in separate files, so we need to generate imports.
|
|
52
99
|
*
|
|
53
100
|
* Example: query references RetailerServicesData → import from './RetailerServicesData.ts'
|
|
101
|
+
*
|
|
102
|
+
* NOTE: We use the fragment registry to get the proper import identifiers (with correct naming/suffix)
|
|
103
|
+
* instead of hardcoding them, since naming depends on config (dedupeOperationSuffix, etc.)
|
|
54
104
|
*/
|
|
55
|
-
function createLocalFragmentImports(localFragments, sourceLocation, currentFilename, folder, extension, options) {
|
|
105
|
+
function createLocalFragmentImports(localFragments, fragmentRegistry, sourceLocation, currentFilename, folder, extension, options) {
|
|
56
106
|
return localFragments.map((frag) => {
|
|
57
107
|
var _a;
|
|
58
108
|
const fragmentFilePath = generateOperationFilePath(sourceLocation, frag.name.value, folder, extension);
|
|
59
|
-
//
|
|
60
|
-
|
|
109
|
+
// Get the proper import identifiers from the fragment registry
|
|
110
|
+
// (calculated by fragment-resolver using BaseVisitor with correct naming conventions)
|
|
111
|
+
const registryEntry = fragmentRegistry[frag.name.value];
|
|
112
|
+
const identifiers = (registryEntry === null || registryEntry === void 0 ? void 0 : registryEntry.imports) || [
|
|
113
|
+
// Fallback if not in registry (shouldn't happen, but be safe)
|
|
61
114
|
{
|
|
62
115
|
name: `${frag.name.value}FragmentDoc`,
|
|
63
116
|
kind: 'document',
|
|
@@ -93,7 +146,8 @@ function createLocalFragmentImports(localFragments, sourceLocation, currentFilen
|
|
|
93
146
|
* We ONLY need the Types import when generated code actually references the Types namespace:
|
|
94
147
|
*
|
|
95
148
|
* ✅ NEEDS IMPORT:
|
|
96
|
-
* - Operations (query/mutation/subscription) - generate `Types.Exact<{...}>` for Variables type
|
|
149
|
+
* - Operations (query/mutation/subscription) - ALWAYS generate `Types.Exact<{...}>` for Variables type
|
|
150
|
+
* (even operations without variables: `Types.Exact<{ [key: string]: never; }>`)
|
|
97
151
|
* - Enums - generate `role: Types.Role`
|
|
98
152
|
* - Custom scalars (unless mapped to primitives) - generate `date: Types.DateTime`
|
|
99
153
|
* - Input types - used in variables as `Types.MyInput`
|
|
@@ -112,8 +166,11 @@ function needsSchemaTypesImport(document, schema, config) {
|
|
|
112
166
|
const scalarMappings = config.scalars || {};
|
|
113
167
|
/**
|
|
114
168
|
* STEP 1: Check for operations (queries/mutations/subscriptions)
|
|
115
|
-
*
|
|
169
|
+
* ALL operations generate a Variables type that uses Types.Exact, even without variables:
|
|
116
170
|
* `export type MyQueryVariables = Types.Exact<{ [key: string]: never; }>;`
|
|
171
|
+
*
|
|
172
|
+
* This is true for typescript-operations plugin regardless of whether the operation
|
|
173
|
+
* has any variable definitions or not.
|
|
117
174
|
*/
|
|
118
175
|
let hasOperation = false;
|
|
119
176
|
const referencedTypeNames = new Set();
|
|
@@ -129,6 +186,7 @@ function needsSchemaTypesImport(document, schema, config) {
|
|
|
129
186
|
referencedTypeNames.add(node.typeCondition.name.value);
|
|
130
187
|
},
|
|
131
188
|
});
|
|
189
|
+
// All operations need Types import for Variables type
|
|
132
190
|
if (hasOperation) {
|
|
133
191
|
return true;
|
|
134
192
|
}
|
|
@@ -191,9 +249,17 @@ function needsSchemaTypesImport(document, schema, config) {
|
|
|
191
249
|
* STEP 4: Check field return types
|
|
192
250
|
* Use TypeInfo to properly resolve field types through the schema.
|
|
193
251
|
* This catches cases like: user { role } where 'role' returns an enum.
|
|
252
|
+
*
|
|
253
|
+
* IMPORTANT: Only check the FIRST definition (the actual operation/fragment being generated).
|
|
254
|
+
* Don't check external fragments - those will have their own imports.
|
|
194
255
|
*/
|
|
256
|
+
// Create a document with ONLY the first definition for type checking
|
|
257
|
+
const singleDefOnly = {
|
|
258
|
+
kind: Kind.DOCUMENT,
|
|
259
|
+
definitions: [document.definitions[0]],
|
|
260
|
+
};
|
|
195
261
|
const typeInfo = new TypeInfo(schema);
|
|
196
|
-
visit(
|
|
262
|
+
visit(singleDefOnly, visitWithTypeInfo(typeInfo, {
|
|
197
263
|
Field: () => {
|
|
198
264
|
const fieldType = typeInfo.getType();
|
|
199
265
|
if (checkType(fieldType)) {
|
|
@@ -257,7 +323,7 @@ export const preset = {
|
|
|
257
323
|
* This uses our adapted fragment-resolver that generates paths based on fragment NAMES
|
|
258
324
|
* (not source file names), enabling proper imports after splitting.
|
|
259
325
|
*/
|
|
260
|
-
const sources = resolveDocumentImports(options, schemaObject, {
|
|
326
|
+
const { results: sources, fragmentRegistry } = resolveDocumentImports(options, schemaObject, {
|
|
261
327
|
baseDir,
|
|
262
328
|
folder,
|
|
263
329
|
extension,
|
|
@@ -304,25 +370,72 @@ export const preset = {
|
|
|
304
370
|
* resolver marked MyFrag as "local" (same document).
|
|
305
371
|
*
|
|
306
372
|
* Solution: Treat local fragments as external after splitting.
|
|
373
|
+
* Only include fragments that are actually referenced by this definition.
|
|
307
374
|
*/
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
375
|
+
// Build a map of local fragment names -> definitions for lookup
|
|
376
|
+
const localFragmentMap = new Map();
|
|
377
|
+
for (const d of definitions) {
|
|
378
|
+
if (d.definition.kind === Kind.FRAGMENT_DEFINITION && d.name !== name) {
|
|
379
|
+
localFragmentMap.set(d.name, d.definition);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* ANALYZE FRAGMENT DEPENDENCIES
|
|
384
|
+
*
|
|
385
|
+
* We need two different sets of fragments:
|
|
386
|
+
* 1. allNeeded - ALL transitive deps for externalFragments (plugin needs these for type resolution)
|
|
387
|
+
* 2. directlyNeeded - Only DIRECT deps for fragmentImports (transitive deps handled by intermediate files)
|
|
388
|
+
*
|
|
389
|
+
* Example: Query → FragA → FragB
|
|
390
|
+
* - externalFragments: [FragA, FragB] (plugin needs both for types)
|
|
391
|
+
* - fragmentImports: [FragA] (only direct; FragA.ts already imports FragB)
|
|
392
|
+
*/
|
|
393
|
+
const { allNeeded, directlyNeeded } = analyzeFragmentDependencies(definition, source.externalFragments, localFragmentMap);
|
|
394
|
+
// Filter local fragments to only those in the transitive dependency set
|
|
395
|
+
// (for externalFragments - plugin needs all of them)
|
|
396
|
+
const allLocalFragments = Array.from(localFragmentMap.entries())
|
|
397
|
+
.filter(([fragName]) => allNeeded.has(fragName))
|
|
398
|
+
.map(([, fragDef]) => fragDef);
|
|
399
|
+
const localFragmentNodes = createLoadedFragments(allLocalFragments);
|
|
400
|
+
// Filter external fragments to only those actually needed by this definition
|
|
401
|
+
const filteredExternalFragments = source.externalFragments.filter(frag => allNeeded.has(frag.name));
|
|
402
|
+
// Combine filtered external fragments + local fragments (all transitive deps)
|
|
311
403
|
const allExternalFragments = [
|
|
312
|
-
...
|
|
404
|
+
...filteredExternalFragments,
|
|
313
405
|
...localFragmentNodes,
|
|
314
406
|
];
|
|
315
407
|
/**
|
|
316
408
|
* UPDATE FRAGMENT IMPORTS
|
|
317
409
|
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
410
|
+
* Only import DIRECT dependencies, not transitive ones.
|
|
411
|
+
* Transitive dependencies are imported by the intermediate fragment files.
|
|
412
|
+
*
|
|
413
|
+
* 1. External fragments: Filter to only DIRECTLY needed ones, update outputPath
|
|
414
|
+
* 2. Local fragments: Generate imports only for DIRECTLY referenced fragments
|
|
320
415
|
*/
|
|
321
|
-
const updatedFragmentImports = source.fragmentImports
|
|
416
|
+
const updatedFragmentImports = source.fragmentImports
|
|
417
|
+
// Filter to only include imports for fragments DIRECTLY referenced
|
|
418
|
+
.filter(fragmentImport => {
|
|
419
|
+
// Check if any of the import's identifiers are for directly needed fragments
|
|
420
|
+
return fragmentImport.importSource.identifiers.some(id => {
|
|
421
|
+
// Check if this identifier's name matches a directly needed fragment
|
|
422
|
+
for (const neededName of directlyNeeded) {
|
|
423
|
+
if (id.name === neededName || id.name.startsWith(neededName)) {
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return false;
|
|
428
|
+
});
|
|
429
|
+
})
|
|
430
|
+
.map(fragmentImport => ({
|
|
322
431
|
...fragmentImport,
|
|
323
432
|
outputPath: filename,
|
|
324
433
|
}));
|
|
325
|
-
|
|
434
|
+
// Only create imports for DIRECTLY referenced local fragments
|
|
435
|
+
const directlyNeededLocalFragments = Array.from(localFragmentMap.entries())
|
|
436
|
+
.filter(([fragName]) => directlyNeeded.has(fragName))
|
|
437
|
+
.map(([, fragDef]) => fragDef);
|
|
438
|
+
const localFragmentImports = createLocalFragmentImports(directlyNeededLocalFragments, fragmentRegistry, source.documents[0].location, filename, folder, extension, {
|
|
326
439
|
baseDir,
|
|
327
440
|
baseOutputDir: options.baseOutputDir,
|
|
328
441
|
emitLegacyCommonJSImports: options.config.emitLegacyCommonJSImports,
|
|
@@ -8,10 +8,10 @@ import buildFragmentResolver from './fragment-resolver.js';
|
|
|
8
8
|
* Does not define specific plugins, but rather returns a string[] of `importStatements` for the calling plugin to make use of
|
|
9
9
|
*/
|
|
10
10
|
export function resolveDocumentImports(presetOptions, schemaObject, importResolverOptions, dedupeFragments = false) {
|
|
11
|
-
const resolveFragments = buildFragmentResolver(importResolverOptions, presetOptions, schemaObject, dedupeFragments);
|
|
11
|
+
const { resolveFragments, fragmentRegistry } = buildFragmentResolver(importResolverOptions, presetOptions, schemaObject, dedupeFragments);
|
|
12
12
|
const { baseOutputDir, documents } = presetOptions;
|
|
13
13
|
const { schemaTypesSource, baseDir, typesImport } = importResolverOptions;
|
|
14
|
-
|
|
14
|
+
const results = documents.map(documentFile => {
|
|
15
15
|
try {
|
|
16
16
|
// NOTE: We pass a placeholder filename here since we'll generate proper filenames per-operation later
|
|
17
17
|
// The important part is that fragment resolution will use the correct paths from the registry
|
|
@@ -51,4 +51,5 @@ export function resolveDocumentImports(presetOptions, schemaObject, importResolv
|
|
|
51
51
|
${e.message || e.toString()}`);
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
|
+
return { results, fragmentRegistry };
|
|
54
55
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nmarks/graphql-codegen-per-operation-file-preset",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "GraphQL Code Generator preset for generating one file per operation/fragment",
|
|
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"
|
|
@@ -21,9 +21,12 @@ export type FragmentRegistry = {
|
|
|
21
21
|
/**
|
|
22
22
|
* Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
|
|
23
23
|
*/
|
|
24
|
-
export default function buildFragmentResolver<T>(collectorOptions: DocumentImportResolverOptions, presetOptions: Types.PresetFnArgs<T>, schemaObject: GraphQLSchema, dedupeFragments?: boolean):
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
export default function buildFragmentResolver<T>(collectorOptions: DocumentImportResolverOptions, presetOptions: Types.PresetFnArgs<T>, schemaObject: GraphQLSchema, dedupeFragments?: boolean): {
|
|
25
|
+
resolveFragments: (generatedFilePath: string, documentFileContent: DocumentNode) => {
|
|
26
|
+
externalFragments: LoadedFragment<{
|
|
27
|
+
level: number;
|
|
28
|
+
}>[];
|
|
29
|
+
fragmentImports: ImportDeclaration<FragmentImport>[];
|
|
30
|
+
};
|
|
31
|
+
fragmentRegistry: FragmentRegistry;
|
|
29
32
|
};
|
|
@@ -21,9 +21,12 @@ export type FragmentRegistry = {
|
|
|
21
21
|
/**
|
|
22
22
|
* Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
|
|
23
23
|
*/
|
|
24
|
-
export default function buildFragmentResolver<T>(collectorOptions: DocumentImportResolverOptions, presetOptions: Types.PresetFnArgs<T>, schemaObject: GraphQLSchema, dedupeFragments?: boolean):
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
export default function buildFragmentResolver<T>(collectorOptions: DocumentImportResolverOptions, presetOptions: Types.PresetFnArgs<T>, schemaObject: GraphQLSchema, dedupeFragments?: boolean): {
|
|
25
|
+
resolveFragments: (generatedFilePath: string, documentFileContent: DocumentNode) => {
|
|
26
|
+
externalFragments: LoadedFragment<{
|
|
27
|
+
level: number;
|
|
28
|
+
}>[];
|
|
29
|
+
fragmentImports: ImportDeclaration<FragmentImport>[];
|
|
30
|
+
};
|
|
31
|
+
fragmentRegistry: FragmentRegistry;
|
|
29
32
|
};
|
|
@@ -44,5 +44,8 @@ interface ResolveDocumentImportResult {
|
|
|
44
44
|
* Resolves user provided imports and fragment imports using the `DocumentImportResolverOptions`.
|
|
45
45
|
* Does not define specific plugins, but rather returns a string[] of `importStatements` for the calling plugin to make use of
|
|
46
46
|
*/
|
|
47
|
-
export declare function resolveDocumentImports<T>(presetOptions: Types.PresetFnArgs<T>, schemaObject: GraphQLSchema, importResolverOptions: DocumentImportResolverOptions, dedupeFragments?: boolean):
|
|
47
|
+
export declare function resolveDocumentImports<T>(presetOptions: Types.PresetFnArgs<T>, schemaObject: GraphQLSchema, importResolverOptions: DocumentImportResolverOptions, dedupeFragments?: boolean): {
|
|
48
|
+
results: ResolveDocumentImportResult[];
|
|
49
|
+
fragmentRegistry: any;
|
|
50
|
+
};
|
|
48
51
|
export {};
|
|
@@ -44,5 +44,8 @@ interface ResolveDocumentImportResult {
|
|
|
44
44
|
* Resolves user provided imports and fragment imports using the `DocumentImportResolverOptions`.
|
|
45
45
|
* Does not define specific plugins, but rather returns a string[] of `importStatements` for the calling plugin to make use of
|
|
46
46
|
*/
|
|
47
|
-
export declare function resolveDocumentImports<T>(presetOptions: Types.PresetFnArgs<T>, schemaObject: GraphQLSchema, importResolverOptions: DocumentImportResolverOptions, dedupeFragments?: boolean):
|
|
47
|
+
export declare function resolveDocumentImports<T>(presetOptions: Types.PresetFnArgs<T>, schemaObject: GraphQLSchema, importResolverOptions: DocumentImportResolverOptions, dedupeFragments?: boolean): {
|
|
48
|
+
results: ResolveDocumentImportResult[];
|
|
49
|
+
fragmentRegistry: any;
|
|
50
|
+
};
|
|
48
51
|
export {};
|