@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.
@@ -145,5 +145,5 @@ function buildFragmentResolver(collectorOptions, presetOptions, schemaObject, de
145
145
  })),
146
146
  };
147
147
  }
148
- return resolveFragments;
148
+ return { resolveFragments, fragmentRegistry };
149
149
  }
package/cjs/index.js CHANGED
@@ -24,18 +24,17 @@ function extractDefinitions(document) {
24
24
  return definitions;
25
25
  }
26
26
  /**
27
- * Finds "local" fragments - fragments defined in the same source file as the current definition.
28
- * These are initially local but become external after we split the file per-operation.
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 findLocalFragments(allDefinitions, currentDefinitionName) {
35
- return allDefinitions
36
- .filter(d => d.definition.kind === graphql_1.Kind.FRAGMENT_DEFINITION &&
37
- d.name !== currentDefinitionName)
38
- .map(d => d.definition);
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
- // Generate import identifiers: both the document (for runtime) and type (for TypeScript)
64
- const identifiers = [
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
- * All operations generate a Variables type that uses Types.Exact, even without variables:
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)(document, (0, graphql_1.visitWithTypeInfo)(typeInfo, {
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
- const localFragments = findLocalFragments(definitions, name);
313
- const localFragmentNodes = createLoadedFragments(localFragments);
314
- // Combine external fragments (from other files) + local fragments (same source file)
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
- ...source.externalFragments,
408
+ ...filteredExternalFragments,
317
409
  ...localFragmentNodes,
318
410
  ];
319
411
  /**
320
412
  * UPDATE FRAGMENT IMPORTS
321
413
  *
322
- * 1. External fragments: Update outputPath to this operation's file
323
- * 2. Local fragments: Generate new imports pointing to their split files
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.map(fragmentImport => ({
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
- const localFragmentImports = createLocalFragmentImports(localFragments, source.documents[0].location, filename, folder, extension, {
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
- return documents.map(documentFile => {
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
  }
@@ -142,5 +142,5 @@ export default function buildFragmentResolver(collectorOptions, presetOptions, s
142
142
  })),
143
143
  };
144
144
  }
145
- return resolveFragments;
145
+ return { resolveFragments, fragmentRegistry };
146
146
  }
package/esm/index.js CHANGED
@@ -20,18 +20,17 @@ function extractDefinitions(document) {
20
20
  return definitions;
21
21
  }
22
22
  /**
23
- * Finds "local" fragments - fragments defined in the same source file as the current definition.
24
- * These are initially local but become external after we split the file per-operation.
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 findLocalFragments(allDefinitions, currentDefinitionName) {
31
- return allDefinitions
32
- .filter(d => d.definition.kind === Kind.FRAGMENT_DEFINITION &&
33
- d.name !== currentDefinitionName)
34
- .map(d => d.definition);
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
- // Generate import identifiers: both the document (for runtime) and type (for TypeScript)
60
- const identifiers = [
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
- * All operations generate a Variables type that uses Types.Exact, even without variables:
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(document, visitWithTypeInfo(typeInfo, {
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
- const localFragments = findLocalFragments(definitions, name);
309
- const localFragmentNodes = createLoadedFragments(localFragments);
310
- // Combine external fragments (from other files) + local fragments (same source file)
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
- ...source.externalFragments,
404
+ ...filteredExternalFragments,
313
405
  ...localFragmentNodes,
314
406
  ];
315
407
  /**
316
408
  * UPDATE FRAGMENT IMPORTS
317
409
  *
318
- * 1. External fragments: Update outputPath to this operation's file
319
- * 2. Local fragments: Generate new imports pointing to their split files
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.map(fragmentImport => ({
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
- const localFragmentImports = createLocalFragmentImports(localFragments, source.documents[0].location, filename, folder, extension, {
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
- return documents.map(documentFile => {
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.8",
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): (generatedFilePath: string, documentFileContent: DocumentNode) => {
25
- externalFragments: LoadedFragment<{
26
- level: number;
27
- }>[];
28
- fragmentImports: ImportDeclaration<FragmentImport>[];
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): (generatedFilePath: string, documentFileContent: DocumentNode) => {
25
- externalFragments: LoadedFragment<{
26
- level: number;
27
- }>[];
28
- fragmentImports: ImportDeclaration<FragmentImport>[];
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): Array<ResolveDocumentImportResult>;
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): Array<ResolveDocumentImportResult>;
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 {};