@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 +150 -19
- package/cjs/persisted-documents.js +179 -0
- package/esm/index.js +150 -19
- package/esm/persisted-documents.js +172 -0
- package/package.json +1 -1
- package/typings/index.d.cts +29 -0
- package/typings/index.d.ts +29 -0
- package/typings/persisted-documents.d.cts +44 -0
- package/typings/persisted-documents.d.ts +44 -0
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
|
-
*
|
|
28
|
-
*
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
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
|
-
...
|
|
409
|
+
...filteredExternalFragments,
|
|
336
410
|
...localFragmentNodes,
|
|
337
411
|
];
|
|
338
412
|
/**
|
|
339
413
|
* UPDATE FRAGMENT IMPORTS
|
|
340
414
|
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
24
|
-
*
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
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
|
-
...
|
|
405
|
+
...filteredExternalFragments,
|
|
332
406
|
...localFragmentNodes,
|
|
333
407
|
];
|
|
334
408
|
/**
|
|
335
409
|
* UPDATE FRAGMENT IMPORTS
|
|
336
410
|
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
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
|
|
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
|
-
|
|
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.
|
|
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"
|
package/typings/index.d.cts
CHANGED
|
@@ -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
|
package/typings/index.d.ts
CHANGED
|
@@ -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>;
|