@nmarks/graphql-codegen-per-operation-file-preset 1.0.9 → 1.0.11

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/index.js CHANGED
@@ -8,6 +8,7 @@ const add_1 = tslib_1.__importDefault(require("@graphql-codegen/add"));
8
8
  const visitor_plugin_common_1 = require("@graphql-codegen/visitor-plugin-common");
9
9
  const resolve_document_imports_js_1 = require("./resolve-document-imports.js");
10
10
  const utils_js_1 = require("./utils.js");
11
+ const persisted_documents_js_1 = require("./persisted-documents.js");
11
12
  /**
12
13
  * Extract operation and fragment names from a document
13
14
  */
@@ -24,18 +25,17 @@ function extractDefinitions(document) {
24
25
  return definitions;
25
26
  }
26
27
  /**
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
28
+ * Collects all fragment names directly referenced by a definition via FragmentSpread nodes.
29
+ * This only gets DIRECT references - not transitive dependencies.
33
30
  */
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);
31
+ function getDirectlyReferencedFragments(definition) {
32
+ const referencedFragments = new Set();
33
+ (0, graphql_1.visit)(definition, {
34
+ FragmentSpread: (node) => {
35
+ referencedFragments.add(node.name.value);
36
+ },
37
+ });
38
+ return referencedFragments;
39
39
  }
40
40
  /**
41
41
  * Creates LoadedFragment objects for local fragments so they can be treated as external fragments
@@ -50,6 +50,54 @@ function createLoadedFragments(fragments) {
50
50
  node: frag,
51
51
  }));
52
52
  }
53
+ /**
54
+ * Analyzes fragment dependencies for a definition.
55
+ *
56
+ * Returns two sets:
57
+ * 1. `allNeeded` - ALL fragments needed (including transitive) - for externalFragments
58
+ * 2. `directlyNeeded` - Only DIRECT dependencies - for fragmentImports
59
+ *
60
+ * Why the distinction?
61
+ * - `externalFragments` must include all transitive deps so the plugin can resolve types
62
+ * - `fragmentImports` should only include direct deps; transitive deps are imported by intermediate files
63
+ *
64
+ * Example: Query → FragA → FragB
65
+ * - Query's externalFragments: [FragA, FragB] (plugin needs both)
66
+ * - Query's fragmentImports: [FragA] (only direct; FragA.ts imports FragB)
67
+ *
68
+ * @param definition - The operation/fragment definition to analyze
69
+ * @param externalFragments - All external fragments available (from other files)
70
+ * @param localFragmentMap - Map of local fragment names to their definitions
71
+ */
72
+ function analyzeFragmentDependencies(definition, externalFragments, localFragmentMap) {
73
+ const allNeeded = new Set();
74
+ // Build a map for quick lookup of external fragment nodes
75
+ const externalFragmentMap = new Map();
76
+ for (const frag of externalFragments) {
77
+ externalFragmentMap.set(frag.name, frag.node);
78
+ }
79
+ function collectTransitive(fragmentNames) {
80
+ for (const name of fragmentNames) {
81
+ if (allNeeded.has(name))
82
+ continue;
83
+ // Check if this fragment exists (either local or external)
84
+ const localDef = localFragmentMap.get(name);
85
+ const externalDef = externalFragmentMap.get(name);
86
+ const fragmentDef = localDef || externalDef;
87
+ if (fragmentDef) {
88
+ allNeeded.add(name);
89
+ // Get transitive dependencies
90
+ const transitiveDeps = getDirectlyReferencedFragments(fragmentDef);
91
+ collectTransitive(transitiveDeps);
92
+ }
93
+ }
94
+ }
95
+ // Get direct references from the definition
96
+ const directlyNeeded = getDirectlyReferencedFragments(definition);
97
+ // Collect all transitive dependencies (includes direct ones)
98
+ collectTransitive(directlyNeeded);
99
+ return { allNeeded, directlyNeeded };
100
+ }
53
101
  /**
54
102
  * Creates fragment import declarations for local fragments.
55
103
  * After splitting, these fragments will be in separate files, so we need to generate imports.
@@ -327,25 +375,72 @@ exports.preset = {
327
375
  * resolver marked MyFrag as "local" (same document).
328
376
  *
329
377
  * Solution: Treat local fragments as external after splitting.
378
+ * Only include fragments that are actually referenced by this definition.
330
379
  */
331
- const localFragments = findLocalFragments(definitions, name);
332
- const localFragmentNodes = createLoadedFragments(localFragments);
333
- // Combine external fragments (from other files) + local fragments (same source file)
380
+ // Build a map of local fragment names -> definitions for lookup
381
+ const localFragmentMap = new Map();
382
+ for (const d of definitions) {
383
+ if (d.definition.kind === graphql_1.Kind.FRAGMENT_DEFINITION && d.name !== name) {
384
+ localFragmentMap.set(d.name, d.definition);
385
+ }
386
+ }
387
+ /**
388
+ * ANALYZE FRAGMENT DEPENDENCIES
389
+ *
390
+ * We need two different sets of fragments:
391
+ * 1. allNeeded - ALL transitive deps for externalFragments (plugin needs these for type resolution)
392
+ * 2. directlyNeeded - Only DIRECT deps for fragmentImports (transitive deps handled by intermediate files)
393
+ *
394
+ * Example: Query → FragA → FragB
395
+ * - externalFragments: [FragA, FragB] (plugin needs both for types)
396
+ * - fragmentImports: [FragA] (only direct; FragA.ts already imports FragB)
397
+ */
398
+ const { allNeeded, directlyNeeded } = analyzeFragmentDependencies(definition, source.externalFragments, localFragmentMap);
399
+ // Filter local fragments to only those in the transitive dependency set
400
+ // (for externalFragments - plugin needs all of them)
401
+ const allLocalFragments = Array.from(localFragmentMap.entries())
402
+ .filter(([fragName]) => allNeeded.has(fragName))
403
+ .map(([, fragDef]) => fragDef);
404
+ const localFragmentNodes = createLoadedFragments(allLocalFragments);
405
+ // Filter external fragments to only those actually needed by this definition
406
+ const filteredExternalFragments = source.externalFragments.filter(frag => allNeeded.has(frag.name));
407
+ // Combine filtered external fragments + local fragments (all transitive deps)
334
408
  const allExternalFragments = [
335
- ...source.externalFragments,
409
+ ...filteredExternalFragments,
336
410
  ...localFragmentNodes,
337
411
  ];
338
412
  /**
339
413
  * UPDATE FRAGMENT IMPORTS
340
414
  *
341
- * 1. External fragments: Update outputPath to this operation's file
342
- * 2. Local fragments: Generate new imports pointing to their split files
415
+ * Only import DIRECT dependencies, not transitive ones.
416
+ * Transitive dependencies are imported by the intermediate fragment files.
417
+ *
418
+ * 1. External fragments: Filter to only DIRECTLY needed ones, update outputPath
419
+ * 2. Local fragments: Generate imports only for DIRECTLY referenced fragments
343
420
  */
344
- const updatedFragmentImports = source.fragmentImports.map(fragmentImport => ({
421
+ const updatedFragmentImports = source.fragmentImports
422
+ // Filter to only include imports for fragments DIRECTLY referenced
423
+ .filter(fragmentImport => {
424
+ // Check if any of the import's identifiers are for directly needed fragments
425
+ return fragmentImport.importSource.identifiers.some(id => {
426
+ // Check if this identifier's name matches a directly needed fragment
427
+ for (const neededName of directlyNeeded) {
428
+ if (id.name === neededName || id.name.startsWith(neededName)) {
429
+ return true;
430
+ }
431
+ }
432
+ return false;
433
+ });
434
+ })
435
+ .map(fragmentImport => ({
345
436
  ...fragmentImport,
346
437
  outputPath: filename,
347
438
  }));
348
- const localFragmentImports = createLocalFragmentImports(localFragments, fragmentRegistry, source.documents[0].location, filename, folder, extension, {
439
+ // Only create imports for DIRECTLY referenced local fragments
440
+ const directlyNeededLocalFragments = Array.from(localFragmentMap.entries())
441
+ .filter(([fragName]) => directlyNeeded.has(fragName))
442
+ .map(([, fragDef]) => fragDef);
443
+ const localFragmentImports = createLocalFragmentImports(directlyNeededLocalFragments, fragmentRegistry, source.documents[0].location, filename, folder, extension, {
349
444
  baseDir,
350
445
  baseOutputDir: options.baseOutputDir,
351
446
  emitLegacyCommonJSImports: options.config.emitLegacyCommonJSImports,
@@ -436,6 +531,42 @@ exports.preset = {
436
531
  });
437
532
  }
438
533
  }
534
+ /**
535
+ * PERSISTED DOCUMENTS GENERATION
536
+ *
537
+ * If persistedDocuments is configured, generate a JSON file mapping
538
+ * operation hashes to their full query bodies (with __typename added).
539
+ * This happens in a single pass - no second document scan required.
540
+ */
541
+ const { persistedDocuments: persistedDocumentsConfig } = options.presetConfig;
542
+ if (persistedDocumentsConfig === null || persistedDocumentsConfig === void 0 ? void 0 : persistedDocumentsConfig.output) {
543
+ // Collect all source documents for persisted document generation
544
+ const allDocuments = options.documents;
545
+ // Generate the persisted documents map
546
+ const persistedDocsMap = (0, persisted_documents_js_1.generatePersistedDocuments)(allDocuments);
547
+ // Add artifact for the persisted documents JSON file
548
+ artifacts.push({
549
+ ...options,
550
+ filename: (0, path_1.join)(options.baseOutputDir, persistedDocumentsConfig.output),
551
+ plugins: [
552
+ {
553
+ [`persisted-documents`]: {},
554
+ },
555
+ ],
556
+ pluginMap: {
557
+ [`persisted-documents`]: {
558
+ plugin: () => ({
559
+ content: JSON.stringify(persistedDocsMap, null, 2),
560
+ }),
561
+ },
562
+ },
563
+ schema: options.schema,
564
+ schemaAst: schemaObject,
565
+ config: {},
566
+ documents: allDocuments,
567
+ skipDocumentsValidation: true,
568
+ });
569
+ }
439
570
  return artifacts;
440
571
  },
441
572
  };
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addTypenameToDocument = addTypenameToDocument;
4
+ exports.collectFragmentReferences = collectFragmentReferences;
5
+ exports.collectAllFragmentDeps = collectAllFragmentDeps;
6
+ exports.sortTopLevelDefinitions = sortTopLevelDefinitions;
7
+ exports.generatePersistedDocuments = generatePersistedDocuments;
8
+ const crypto_1 = require("crypto");
9
+ const graphql_1 = require("graphql");
10
+ /**
11
+ * Adds __typename field to all selection sets matching Apollo Client's InMemoryCache behavior.
12
+ *
13
+ * Rules:
14
+ * - Add __typename at the END of selection sets
15
+ * - Skip root selection set of operations (query/mutation/subscription)
16
+ * - DO add __typename to fragment root selection sets
17
+ */
18
+ function addTypenameToDocument(doc) {
19
+ return (0, graphql_1.visit)(doc, {
20
+ SelectionSet(node, _key, parent) {
21
+ // Skip root selection set of operations (parent is OperationDefinition)
22
+ // But DON'T skip root selection set of fragments (they should get __typename)
23
+ if (parent && parent.kind === graphql_1.Kind.OPERATION_DEFINITION) {
24
+ return node;
25
+ }
26
+ // Check if __typename already exists
27
+ const hasTypename = node.selections.some(selection => selection.kind === graphql_1.Kind.FIELD && selection.name.value === '__typename');
28
+ if (hasTypename) {
29
+ return node;
30
+ }
31
+ // Add __typename at the END (matching Apollo Client behavior)
32
+ return {
33
+ ...node,
34
+ selections: [
35
+ ...node.selections,
36
+ {
37
+ kind: graphql_1.Kind.FIELD,
38
+ name: { kind: graphql_1.Kind.NAME, value: '__typename' },
39
+ },
40
+ ],
41
+ };
42
+ },
43
+ });
44
+ }
45
+ /**
46
+ * Collects all fragment names referenced in a document via FragmentSpread nodes
47
+ */
48
+ function collectFragmentReferences(doc) {
49
+ const refs = new Set();
50
+ (0, graphql_1.visit)(doc, {
51
+ FragmentSpread(node) {
52
+ refs.add(node.name.value);
53
+ },
54
+ });
55
+ return refs;
56
+ }
57
+ /**
58
+ * Recursively collects all fragment dependencies (including nested)
59
+ */
60
+ function collectAllFragmentDeps(fragmentName, fragmentMap, collected = new Set()) {
61
+ if (collected.has(fragmentName))
62
+ return collected;
63
+ collected.add(fragmentName);
64
+ const fragment = fragmentMap.get(fragmentName);
65
+ if (fragment) {
66
+ const refs = collectFragmentReferences(fragment);
67
+ for (const ref of refs) {
68
+ collectAllFragmentDeps(ref, fragmentMap, collected);
69
+ }
70
+ }
71
+ return collected;
72
+ }
73
+ /**
74
+ * Sort the definitions in a document so that operations come before fragments,
75
+ * and so that each kind of definition is sorted by name.
76
+ *
77
+ * This is a direct implementation of the sorting logic from @apollo/persisted-query-lists
78
+ * to avoid an external dependency.
79
+ */
80
+ function sortTopLevelDefinitions(query) {
81
+ const definitions = [...query.definitions];
82
+ definitions.sort((a, b) => {
83
+ var _a, _b, _c, _d;
84
+ // This is a reverse sort by kind, so that OperationDefinition precedes FragmentDefinition.
85
+ if (a.kind > b.kind) {
86
+ return -1;
87
+ }
88
+ if (a.kind < b.kind) {
89
+ return 1;
90
+ }
91
+ // Extract the name from each definition
92
+ const aName = a.kind === graphql_1.Kind.OPERATION_DEFINITION || a.kind === graphql_1.Kind.FRAGMENT_DEFINITION
93
+ ? ((_b = (_a = a.name) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : '')
94
+ : '';
95
+ const bName = b.kind === graphql_1.Kind.OPERATION_DEFINITION || b.kind === graphql_1.Kind.FRAGMENT_DEFINITION
96
+ ? ((_d = (_c = b.name) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : '')
97
+ : '';
98
+ // Sort by name ascending.
99
+ if (aName < bName) {
100
+ return -1;
101
+ }
102
+ if (aName > bName) {
103
+ return 1;
104
+ }
105
+ return 0;
106
+ });
107
+ return {
108
+ ...query,
109
+ definitions,
110
+ };
111
+ }
112
+ /**
113
+ * Generates persisted documents map from all documents.
114
+ *
115
+ * Returns a map of hash -> query body for all operations (not standalone fragments).
116
+ * Each operation includes its fragment dependencies inline.
117
+ */
118
+ function generatePersistedDocuments(documents) {
119
+ // Build fragment registry from all documents
120
+ const fragmentMap = new Map();
121
+ for (const doc of documents) {
122
+ if (!doc.document)
123
+ continue;
124
+ for (const def of doc.document.definitions) {
125
+ if (def.kind === graphql_1.Kind.FRAGMENT_DEFINITION) {
126
+ fragmentMap.set(def.name.value, def);
127
+ }
128
+ }
129
+ }
130
+ const result = {};
131
+ for (const doc of documents) {
132
+ if (!doc.document)
133
+ continue;
134
+ // Find operations in this document
135
+ const operations = doc.document.definitions.filter((def) => def.kind === graphql_1.Kind.OPERATION_DEFINITION);
136
+ // Skip documents with no operations (fragment-only files)
137
+ if (operations.length === 0)
138
+ continue;
139
+ for (const operation of operations) {
140
+ // Create a document with just this operation
141
+ let operationDoc = {
142
+ kind: graphql_1.Kind.DOCUMENT,
143
+ definitions: [operation],
144
+ };
145
+ // Collect all required fragments (recursively)
146
+ const directRefs = collectFragmentReferences(operationDoc);
147
+ const allFragments = new Set();
148
+ for (const ref of directRefs) {
149
+ collectAllFragmentDeps(ref, fragmentMap, allFragments);
150
+ }
151
+ // Add fragment definitions to the document
152
+ const fragmentDefs = [];
153
+ for (const fragName of allFragments) {
154
+ const frag = fragmentMap.get(fragName);
155
+ if (frag)
156
+ fragmentDefs.push(frag);
157
+ }
158
+ if (fragmentDefs.length > 0) {
159
+ operationDoc = {
160
+ ...operationDoc,
161
+ definitions: [...operationDoc.definitions, ...fragmentDefs],
162
+ };
163
+ }
164
+ // Add __typename to all selection sets (matching Apollo Client behavior)
165
+ const docWithTypename = addTypenameToDocument(operationDoc);
166
+ // Sort definitions (operation first, then fragments alphabetically)
167
+ const sortedDoc = sortTopLevelDefinitions(docWithTypename);
168
+ // Print the document
169
+ const body = (0, graphql_1.print)(sortedDoc);
170
+ // Skip empty documents
171
+ if (!body || body.trim() === '')
172
+ continue;
173
+ // Hash is just for the JSON key - gets re-computed by generate-persisted-query-manifest
174
+ const hash = (0, crypto_1.createHash)('sha256').update(body).digest('hex');
175
+ result[hash] = body;
176
+ }
177
+ }
178
+ return result;
179
+ }
package/esm/index.js CHANGED
@@ -4,6 +4,7 @@ import addPlugin from '@graphql-codegen/add';
4
4
  import { generateImportStatement, getConfigValue, resolveImportSource, } from '@graphql-codegen/visitor-plugin-common';
5
5
  import { resolveDocumentImports } from './resolve-document-imports.js';
6
6
  import { generateOperationFilePath } from './utils.js';
7
+ import { generatePersistedDocuments } from './persisted-documents.js';
7
8
  /**
8
9
  * Extract operation and fragment names from a document
9
10
  */
@@ -20,18 +21,17 @@ function extractDefinitions(document) {
20
21
  return definitions;
21
22
  }
22
23
  /**
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
24
+ * Collects all fragment names directly referenced by a definition via FragmentSpread nodes.
25
+ * This only gets DIRECT references - not transitive dependencies.
29
26
  */
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);
27
+ function getDirectlyReferencedFragments(definition) {
28
+ const referencedFragments = new Set();
29
+ visit(definition, {
30
+ FragmentSpread: (node) => {
31
+ referencedFragments.add(node.name.value);
32
+ },
33
+ });
34
+ return referencedFragments;
35
35
  }
36
36
  /**
37
37
  * Creates LoadedFragment objects for local fragments so they can be treated as external fragments
@@ -46,6 +46,54 @@ function createLoadedFragments(fragments) {
46
46
  node: frag,
47
47
  }));
48
48
  }
49
+ /**
50
+ * Analyzes fragment dependencies for a definition.
51
+ *
52
+ * Returns two sets:
53
+ * 1. `allNeeded` - ALL fragments needed (including transitive) - for externalFragments
54
+ * 2. `directlyNeeded` - Only DIRECT dependencies - for fragmentImports
55
+ *
56
+ * Why the distinction?
57
+ * - `externalFragments` must include all transitive deps so the plugin can resolve types
58
+ * - `fragmentImports` should only include direct deps; transitive deps are imported by intermediate files
59
+ *
60
+ * Example: Query → FragA → FragB
61
+ * - Query's externalFragments: [FragA, FragB] (plugin needs both)
62
+ * - Query's fragmentImports: [FragA] (only direct; FragA.ts imports FragB)
63
+ *
64
+ * @param definition - The operation/fragment definition to analyze
65
+ * @param externalFragments - All external fragments available (from other files)
66
+ * @param localFragmentMap - Map of local fragment names to their definitions
67
+ */
68
+ function analyzeFragmentDependencies(definition, externalFragments, localFragmentMap) {
69
+ const allNeeded = new Set();
70
+ // Build a map for quick lookup of external fragment nodes
71
+ const externalFragmentMap = new Map();
72
+ for (const frag of externalFragments) {
73
+ externalFragmentMap.set(frag.name, frag.node);
74
+ }
75
+ function collectTransitive(fragmentNames) {
76
+ for (const name of fragmentNames) {
77
+ if (allNeeded.has(name))
78
+ continue;
79
+ // Check if this fragment exists (either local or external)
80
+ const localDef = localFragmentMap.get(name);
81
+ const externalDef = externalFragmentMap.get(name);
82
+ const fragmentDef = localDef || externalDef;
83
+ if (fragmentDef) {
84
+ allNeeded.add(name);
85
+ // Get transitive dependencies
86
+ const transitiveDeps = getDirectlyReferencedFragments(fragmentDef);
87
+ collectTransitive(transitiveDeps);
88
+ }
89
+ }
90
+ }
91
+ // Get direct references from the definition
92
+ const directlyNeeded = getDirectlyReferencedFragments(definition);
93
+ // Collect all transitive dependencies (includes direct ones)
94
+ collectTransitive(directlyNeeded);
95
+ return { allNeeded, directlyNeeded };
96
+ }
49
97
  /**
50
98
  * Creates fragment import declarations for local fragments.
51
99
  * After splitting, these fragments will be in separate files, so we need to generate imports.
@@ -323,25 +371,72 @@ export const preset = {
323
371
  * resolver marked MyFrag as "local" (same document).
324
372
  *
325
373
  * Solution: Treat local fragments as external after splitting.
374
+ * Only include fragments that are actually referenced by this definition.
326
375
  */
327
- const localFragments = findLocalFragments(definitions, name);
328
- const localFragmentNodes = createLoadedFragments(localFragments);
329
- // Combine external fragments (from other files) + local fragments (same source file)
376
+ // Build a map of local fragment names -> definitions for lookup
377
+ const localFragmentMap = new Map();
378
+ for (const d of definitions) {
379
+ if (d.definition.kind === Kind.FRAGMENT_DEFINITION && d.name !== name) {
380
+ localFragmentMap.set(d.name, d.definition);
381
+ }
382
+ }
383
+ /**
384
+ * ANALYZE FRAGMENT DEPENDENCIES
385
+ *
386
+ * We need two different sets of fragments:
387
+ * 1. allNeeded - ALL transitive deps for externalFragments (plugin needs these for type resolution)
388
+ * 2. directlyNeeded - Only DIRECT deps for fragmentImports (transitive deps handled by intermediate files)
389
+ *
390
+ * Example: Query → FragA → FragB
391
+ * - externalFragments: [FragA, FragB] (plugin needs both for types)
392
+ * - fragmentImports: [FragA] (only direct; FragA.ts already imports FragB)
393
+ */
394
+ const { allNeeded, directlyNeeded } = analyzeFragmentDependencies(definition, source.externalFragments, localFragmentMap);
395
+ // Filter local fragments to only those in the transitive dependency set
396
+ // (for externalFragments - plugin needs all of them)
397
+ const allLocalFragments = Array.from(localFragmentMap.entries())
398
+ .filter(([fragName]) => allNeeded.has(fragName))
399
+ .map(([, fragDef]) => fragDef);
400
+ const localFragmentNodes = createLoadedFragments(allLocalFragments);
401
+ // Filter external fragments to only those actually needed by this definition
402
+ const filteredExternalFragments = source.externalFragments.filter(frag => allNeeded.has(frag.name));
403
+ // Combine filtered external fragments + local fragments (all transitive deps)
330
404
  const allExternalFragments = [
331
- ...source.externalFragments,
405
+ ...filteredExternalFragments,
332
406
  ...localFragmentNodes,
333
407
  ];
334
408
  /**
335
409
  * UPDATE FRAGMENT IMPORTS
336
410
  *
337
- * 1. External fragments: Update outputPath to this operation's file
338
- * 2. Local fragments: Generate new imports pointing to their split files
411
+ * Only import DIRECT dependencies, not transitive ones.
412
+ * Transitive dependencies are imported by the intermediate fragment files.
413
+ *
414
+ * 1. External fragments: Filter to only DIRECTLY needed ones, update outputPath
415
+ * 2. Local fragments: Generate imports only for DIRECTLY referenced fragments
339
416
  */
340
- const updatedFragmentImports = source.fragmentImports.map(fragmentImport => ({
417
+ const updatedFragmentImports = source.fragmentImports
418
+ // Filter to only include imports for fragments DIRECTLY referenced
419
+ .filter(fragmentImport => {
420
+ // Check if any of the import's identifiers are for directly needed fragments
421
+ return fragmentImport.importSource.identifiers.some(id => {
422
+ // Check if this identifier's name matches a directly needed fragment
423
+ for (const neededName of directlyNeeded) {
424
+ if (id.name === neededName || id.name.startsWith(neededName)) {
425
+ return true;
426
+ }
427
+ }
428
+ return false;
429
+ });
430
+ })
431
+ .map(fragmentImport => ({
341
432
  ...fragmentImport,
342
433
  outputPath: filename,
343
434
  }));
344
- const localFragmentImports = createLocalFragmentImports(localFragments, fragmentRegistry, source.documents[0].location, filename, folder, extension, {
435
+ // Only create imports for DIRECTLY referenced local fragments
436
+ const directlyNeededLocalFragments = Array.from(localFragmentMap.entries())
437
+ .filter(([fragName]) => directlyNeeded.has(fragName))
438
+ .map(([, fragDef]) => fragDef);
439
+ const localFragmentImports = createLocalFragmentImports(directlyNeededLocalFragments, fragmentRegistry, source.documents[0].location, filename, folder, extension, {
345
440
  baseDir,
346
441
  baseOutputDir: options.baseOutputDir,
347
442
  emitLegacyCommonJSImports: options.config.emitLegacyCommonJSImports,
@@ -432,6 +527,42 @@ export const preset = {
432
527
  });
433
528
  }
434
529
  }
530
+ /**
531
+ * PERSISTED DOCUMENTS GENERATION
532
+ *
533
+ * If persistedDocuments is configured, generate a JSON file mapping
534
+ * operation hashes to their full query bodies (with __typename added).
535
+ * This happens in a single pass - no second document scan required.
536
+ */
537
+ const { persistedDocuments: persistedDocumentsConfig } = options.presetConfig;
538
+ if (persistedDocumentsConfig === null || persistedDocumentsConfig === void 0 ? void 0 : persistedDocumentsConfig.output) {
539
+ // Collect all source documents for persisted document generation
540
+ const allDocuments = options.documents;
541
+ // Generate the persisted documents map
542
+ const persistedDocsMap = generatePersistedDocuments(allDocuments);
543
+ // Add artifact for the persisted documents JSON file
544
+ artifacts.push({
545
+ ...options,
546
+ filename: join(options.baseOutputDir, persistedDocumentsConfig.output),
547
+ plugins: [
548
+ {
549
+ [`persisted-documents`]: {},
550
+ },
551
+ ],
552
+ pluginMap: {
553
+ [`persisted-documents`]: {
554
+ plugin: () => ({
555
+ content: JSON.stringify(persistedDocsMap, null, 2),
556
+ }),
557
+ },
558
+ },
559
+ schema: options.schema,
560
+ schemaAst: schemaObject,
561
+ config: {},
562
+ documents: allDocuments,
563
+ skipDocumentsValidation: true,
564
+ });
565
+ }
435
566
  return artifacts;
436
567
  },
437
568
  };
@@ -0,0 +1,172 @@
1
+ import { createHash } from 'crypto';
2
+ import { Kind, print, visit, } from 'graphql';
3
+ /**
4
+ * Adds __typename field to all selection sets matching Apollo Client's InMemoryCache behavior.
5
+ *
6
+ * Rules:
7
+ * - Add __typename at the END of selection sets
8
+ * - Skip root selection set of operations (query/mutation/subscription)
9
+ * - DO add __typename to fragment root selection sets
10
+ */
11
+ export function addTypenameToDocument(doc) {
12
+ return visit(doc, {
13
+ SelectionSet(node, _key, parent) {
14
+ // Skip root selection set of operations (parent is OperationDefinition)
15
+ // But DON'T skip root selection set of fragments (they should get __typename)
16
+ if (parent && parent.kind === Kind.OPERATION_DEFINITION) {
17
+ return node;
18
+ }
19
+ // Check if __typename already exists
20
+ const hasTypename = node.selections.some(selection => selection.kind === Kind.FIELD && selection.name.value === '__typename');
21
+ if (hasTypename) {
22
+ return node;
23
+ }
24
+ // Add __typename at the END (matching Apollo Client behavior)
25
+ return {
26
+ ...node,
27
+ selections: [
28
+ ...node.selections,
29
+ {
30
+ kind: Kind.FIELD,
31
+ name: { kind: Kind.NAME, value: '__typename' },
32
+ },
33
+ ],
34
+ };
35
+ },
36
+ });
37
+ }
38
+ /**
39
+ * Collects all fragment names referenced in a document via FragmentSpread nodes
40
+ */
41
+ export function collectFragmentReferences(doc) {
42
+ const refs = new Set();
43
+ visit(doc, {
44
+ FragmentSpread(node) {
45
+ refs.add(node.name.value);
46
+ },
47
+ });
48
+ return refs;
49
+ }
50
+ /**
51
+ * Recursively collects all fragment dependencies (including nested)
52
+ */
53
+ export function collectAllFragmentDeps(fragmentName, fragmentMap, collected = new Set()) {
54
+ if (collected.has(fragmentName))
55
+ return collected;
56
+ collected.add(fragmentName);
57
+ const fragment = fragmentMap.get(fragmentName);
58
+ if (fragment) {
59
+ const refs = collectFragmentReferences(fragment);
60
+ for (const ref of refs) {
61
+ collectAllFragmentDeps(ref, fragmentMap, collected);
62
+ }
63
+ }
64
+ return collected;
65
+ }
66
+ /**
67
+ * Sort the definitions in a document so that operations come before fragments,
68
+ * and so that each kind of definition is sorted by name.
69
+ *
70
+ * This is a direct implementation of the sorting logic from @apollo/persisted-query-lists
71
+ * to avoid an external dependency.
72
+ */
73
+ export function sortTopLevelDefinitions(query) {
74
+ const definitions = [...query.definitions];
75
+ definitions.sort((a, b) => {
76
+ var _a, _b, _c, _d;
77
+ // This is a reverse sort by kind, so that OperationDefinition precedes FragmentDefinition.
78
+ if (a.kind > b.kind) {
79
+ return -1;
80
+ }
81
+ if (a.kind < b.kind) {
82
+ return 1;
83
+ }
84
+ // Extract the name from each definition
85
+ const aName = a.kind === Kind.OPERATION_DEFINITION || a.kind === Kind.FRAGMENT_DEFINITION
86
+ ? ((_b = (_a = a.name) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : '')
87
+ : '';
88
+ const bName = b.kind === Kind.OPERATION_DEFINITION || b.kind === Kind.FRAGMENT_DEFINITION
89
+ ? ((_d = (_c = b.name) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : '')
90
+ : '';
91
+ // Sort by name ascending.
92
+ if (aName < bName) {
93
+ return -1;
94
+ }
95
+ if (aName > bName) {
96
+ return 1;
97
+ }
98
+ return 0;
99
+ });
100
+ return {
101
+ ...query,
102
+ definitions,
103
+ };
104
+ }
105
+ /**
106
+ * Generates persisted documents map from all documents.
107
+ *
108
+ * Returns a map of hash -> query body for all operations (not standalone fragments).
109
+ * Each operation includes its fragment dependencies inline.
110
+ */
111
+ export function generatePersistedDocuments(documents) {
112
+ // Build fragment registry from all documents
113
+ const fragmentMap = new Map();
114
+ for (const doc of documents) {
115
+ if (!doc.document)
116
+ continue;
117
+ for (const def of doc.document.definitions) {
118
+ if (def.kind === Kind.FRAGMENT_DEFINITION) {
119
+ fragmentMap.set(def.name.value, def);
120
+ }
121
+ }
122
+ }
123
+ const result = {};
124
+ for (const doc of documents) {
125
+ if (!doc.document)
126
+ continue;
127
+ // Find operations in this document
128
+ const operations = doc.document.definitions.filter((def) => def.kind === Kind.OPERATION_DEFINITION);
129
+ // Skip documents with no operations (fragment-only files)
130
+ if (operations.length === 0)
131
+ continue;
132
+ for (const operation of operations) {
133
+ // Create a document with just this operation
134
+ let operationDoc = {
135
+ kind: Kind.DOCUMENT,
136
+ definitions: [operation],
137
+ };
138
+ // Collect all required fragments (recursively)
139
+ const directRefs = collectFragmentReferences(operationDoc);
140
+ const allFragments = new Set();
141
+ for (const ref of directRefs) {
142
+ collectAllFragmentDeps(ref, fragmentMap, allFragments);
143
+ }
144
+ // Add fragment definitions to the document
145
+ const fragmentDefs = [];
146
+ for (const fragName of allFragments) {
147
+ const frag = fragmentMap.get(fragName);
148
+ if (frag)
149
+ fragmentDefs.push(frag);
150
+ }
151
+ if (fragmentDefs.length > 0) {
152
+ operationDoc = {
153
+ ...operationDoc,
154
+ definitions: [...operationDoc.definitions, ...fragmentDefs],
155
+ };
156
+ }
157
+ // Add __typename to all selection sets (matching Apollo Client behavior)
158
+ const docWithTypename = addTypenameToDocument(operationDoc);
159
+ // Sort definitions (operation first, then fragments alphabetically)
160
+ const sortedDoc = sortTopLevelDefinitions(docWithTypename);
161
+ // Print the document
162
+ const body = print(sortedDoc);
163
+ // Skip empty documents
164
+ if (!body || body.trim() === '')
165
+ continue;
166
+ // Hash is just for the JSON key - gets re-computed by generate-persisted-query-manifest
167
+ const hash = createHash('sha256').update(body).digest('hex');
168
+ result[hash] = body;
169
+ }
170
+ }
171
+ return result;
172
+ }
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.11",
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"
@@ -1,4 +1,5 @@
1
1
  import { Types } from '@graphql-codegen/plugin-helpers';
2
+ import { PersistedDocumentsConfig } from './persisted-documents.cjs';
2
3
  export type PerOperationFileConfig = {
3
4
  /**
4
5
  * @description Required, should point to the base schema types file.
@@ -126,6 +127,34 @@ export type PerOperationFileConfig = {
126
127
  * ```
127
128
  */
128
129
  importTypesNamespace?: string;
130
+ /**
131
+ * @description Optional, enables generation of a persisted-documents.json file
132
+ * that maps operation hashes to their full query bodies (with __typename added).
133
+ * Compatible with Apollo Client's persisted queries.
134
+ *
135
+ * @exampleMarkdown
136
+ * ```ts filename="codegen.ts"
137
+ * import type { CodegenConfig } from '@graphql-codegen/cli';
138
+ *
139
+ * const config: CodegenConfig = {
140
+ * // ...
141
+ * generates: {
142
+ * 'path/to/file.ts': {
143
+ * preset: 'per-operation-file',
144
+ * plugins: ['typescript-operations'],
145
+ * presetConfig: {
146
+ * baseTypesPath: 'types.ts',
147
+ * persistedDocuments: {
148
+ * output: 'build/persisted-documents.json'
149
+ * }
150
+ * },
151
+ * },
152
+ * },
153
+ * };
154
+ * export default config;
155
+ * ```
156
+ */
157
+ persistedDocuments?: PersistedDocumentsConfig;
129
158
  };
130
159
  /**
131
160
  * PER-OPERATION-FILE PRESET
@@ -1,4 +1,5 @@
1
1
  import { Types } from '@graphql-codegen/plugin-helpers';
2
+ import { PersistedDocumentsConfig } from './persisted-documents.js';
2
3
  export type PerOperationFileConfig = {
3
4
  /**
4
5
  * @description Required, should point to the base schema types file.
@@ -126,6 +127,34 @@ export type PerOperationFileConfig = {
126
127
  * ```
127
128
  */
128
129
  importTypesNamespace?: string;
130
+ /**
131
+ * @description Optional, enables generation of a persisted-documents.json file
132
+ * that maps operation hashes to their full query bodies (with __typename added).
133
+ * Compatible with Apollo Client's persisted queries.
134
+ *
135
+ * @exampleMarkdown
136
+ * ```ts filename="codegen.ts"
137
+ * import type { CodegenConfig } from '@graphql-codegen/cli';
138
+ *
139
+ * const config: CodegenConfig = {
140
+ * // ...
141
+ * generates: {
142
+ * 'path/to/file.ts': {
143
+ * preset: 'per-operation-file',
144
+ * plugins: ['typescript-operations'],
145
+ * presetConfig: {
146
+ * baseTypesPath: 'types.ts',
147
+ * persistedDocuments: {
148
+ * output: 'build/persisted-documents.json'
149
+ * }
150
+ * },
151
+ * },
152
+ * },
153
+ * };
154
+ * export default config;
155
+ * ```
156
+ */
157
+ persistedDocuments?: PersistedDocumentsConfig;
129
158
  };
130
159
  /**
131
160
  * PER-OPERATION-FILE PRESET
@@ -0,0 +1,44 @@
1
+ import { DocumentNode, FragmentDefinitionNode } from 'graphql';
2
+ import type { Source } from '@graphql-tools/utils';
3
+ /**
4
+ * Configuration for persisted documents output
5
+ */
6
+ export interface PersistedDocumentsConfig {
7
+ /**
8
+ * Output path for the persisted-documents.json file
9
+ * (relative to the baseOutputDir or absolute)
10
+ */
11
+ output: string;
12
+ }
13
+ /**
14
+ * Adds __typename field to all selection sets matching Apollo Client's InMemoryCache behavior.
15
+ *
16
+ * Rules:
17
+ * - Add __typename at the END of selection sets
18
+ * - Skip root selection set of operations (query/mutation/subscription)
19
+ * - DO add __typename to fragment root selection sets
20
+ */
21
+ export declare function addTypenameToDocument(doc: DocumentNode): DocumentNode;
22
+ /**
23
+ * Collects all fragment names referenced in a document via FragmentSpread nodes
24
+ */
25
+ export declare function collectFragmentReferences(doc: DocumentNode | FragmentDefinitionNode): Set<string>;
26
+ /**
27
+ * Recursively collects all fragment dependencies (including nested)
28
+ */
29
+ export declare function collectAllFragmentDeps(fragmentName: string, fragmentMap: Map<string, FragmentDefinitionNode>, collected?: Set<string>): Set<string>;
30
+ /**
31
+ * Sort the definitions in a document so that operations come before fragments,
32
+ * and so that each kind of definition is sorted by name.
33
+ *
34
+ * This is a direct implementation of the sorting logic from @apollo/persisted-query-lists
35
+ * to avoid an external dependency.
36
+ */
37
+ export declare function sortTopLevelDefinitions(query: DocumentNode): DocumentNode;
38
+ /**
39
+ * Generates persisted documents map from all documents.
40
+ *
41
+ * Returns a map of hash -> query body for all operations (not standalone fragments).
42
+ * Each operation includes its fragment dependencies inline.
43
+ */
44
+ export declare function generatePersistedDocuments(documents: Source[]): Record<string, string>;
@@ -0,0 +1,44 @@
1
+ import { DocumentNode, FragmentDefinitionNode } from 'graphql';
2
+ import type { Source } from '@graphql-tools/utils';
3
+ /**
4
+ * Configuration for persisted documents output
5
+ */
6
+ export interface PersistedDocumentsConfig {
7
+ /**
8
+ * Output path for the persisted-documents.json file
9
+ * (relative to the baseOutputDir or absolute)
10
+ */
11
+ output: string;
12
+ }
13
+ /**
14
+ * Adds __typename field to all selection sets matching Apollo Client's InMemoryCache behavior.
15
+ *
16
+ * Rules:
17
+ * - Add __typename at the END of selection sets
18
+ * - Skip root selection set of operations (query/mutation/subscription)
19
+ * - DO add __typename to fragment root selection sets
20
+ */
21
+ export declare function addTypenameToDocument(doc: DocumentNode): DocumentNode;
22
+ /**
23
+ * Collects all fragment names referenced in a document via FragmentSpread nodes
24
+ */
25
+ export declare function collectFragmentReferences(doc: DocumentNode | FragmentDefinitionNode): Set<string>;
26
+ /**
27
+ * Recursively collects all fragment dependencies (including nested)
28
+ */
29
+ export declare function collectAllFragmentDeps(fragmentName: string, fragmentMap: Map<string, FragmentDefinitionNode>, collected?: Set<string>): Set<string>;
30
+ /**
31
+ * Sort the definitions in a document so that operations come before fragments,
32
+ * and so that each kind of definition is sorted by name.
33
+ *
34
+ * This is a direct implementation of the sorting logic from @apollo/persisted-query-lists
35
+ * to avoid an external dependency.
36
+ */
37
+ export declare function sortTopLevelDefinitions(query: DocumentNode): DocumentNode;
38
+ /**
39
+ * Generates persisted documents map from all documents.
40
+ *
41
+ * Returns a map of hash -> query body for all operations (not standalone fragments).
42
+ * Each operation includes its fragment dependencies inline.
43
+ */
44
+ export declare function generatePersistedDocuments(documents: Source[]): Record<string, string>;