@nmarks/graphql-codegen-per-operation-file-preset 1.0.9 → 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.
Files changed (3) hide show
  1. package/cjs/index.js +113 -19
  2. package/esm/index.js +113 -19
  3. package/package.json +1 -1
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,6 +49,54 @@ 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.
@@ -327,25 +374,72 @@ exports.preset = {
327
374
  * resolver marked MyFrag as "local" (same document).
328
375
  *
329
376
  * Solution: Treat local fragments as external after splitting.
377
+ * Only include fragments that are actually referenced by this definition.
330
378
  */
331
- const localFragments = findLocalFragments(definitions, name);
332
- const localFragmentNodes = createLoadedFragments(localFragments);
333
- // 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)
334
407
  const allExternalFragments = [
335
- ...source.externalFragments,
408
+ ...filteredExternalFragments,
336
409
  ...localFragmentNodes,
337
410
  ];
338
411
  /**
339
412
  * UPDATE FRAGMENT IMPORTS
340
413
  *
341
- * 1. External fragments: Update outputPath to this operation's file
342
- * 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
343
419
  */
344
- 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 => ({
345
435
  ...fragmentImport,
346
436
  outputPath: filename,
347
437
  }));
348
- const localFragmentImports = createLocalFragmentImports(localFragments, fragmentRegistry, 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, {
349
443
  baseDir,
350
444
  baseOutputDir: options.baseOutputDir,
351
445
  emitLegacyCommonJSImports: options.config.emitLegacyCommonJSImports,
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,6 +45,54 @@ 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.
@@ -323,25 +370,72 @@ export const preset = {
323
370
  * resolver marked MyFrag as "local" (same document).
324
371
  *
325
372
  * Solution: Treat local fragments as external after splitting.
373
+ * Only include fragments that are actually referenced by this definition.
326
374
  */
327
- const localFragments = findLocalFragments(definitions, name);
328
- const localFragmentNodes = createLoadedFragments(localFragments);
329
- // 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)
330
403
  const allExternalFragments = [
331
- ...source.externalFragments,
404
+ ...filteredExternalFragments,
332
405
  ...localFragmentNodes,
333
406
  ];
334
407
  /**
335
408
  * UPDATE FRAGMENT IMPORTS
336
409
  *
337
- * 1. External fragments: Update outputPath to this operation's file
338
- * 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
339
415
  */
340
- 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 => ({
341
431
  ...fragmentImport,
342
432
  outputPath: filename,
343
433
  }));
344
- const localFragmentImports = createLocalFragmentImports(localFragments, fragmentRegistry, 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, {
345
439
  baseDir,
346
440
  baseOutputDir: options.baseOutputDir,
347
441
  emitLegacyCommonJSImports: options.config.emitLegacyCommonJSImports,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nmarks/graphql-codegen-per-operation-file-preset",
3
- "version": "1.0.9",
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"