@nmarks/graphql-codegen-per-operation-file-preset 1.0.6 → 1.0.7
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 +233 -92
- package/esm/index.js +233 -92
- package/package.json +1 -1
- package/typings/index.d.cts +15 -0
- package/typings/index.d.ts +15 -0
package/cjs/index.js
CHANGED
|
@@ -24,97 +24,178 @@ function extractDefinitions(document) {
|
|
|
24
24
|
return definitions;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* - Enums
|
|
30
|
-
* - Custom scalars that aren't mapped to primitives
|
|
31
|
-
* - Input types (in variables)
|
|
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.
|
|
32
29
|
*
|
|
33
|
-
*
|
|
30
|
+
* Example:
|
|
31
|
+
* Source file has: query MyQuery { ...MyFragment } + fragment MyFragment { ... }
|
|
32
|
+
* After split: MyQuery.ts needs to import from MyFragment.ts
|
|
33
|
+
*/
|
|
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);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Creates LoadedFragment objects for local fragments so they can be treated as external fragments
|
|
42
|
+
* during code generation. This ensures proper validation and type generation.
|
|
43
|
+
*/
|
|
44
|
+
function createLoadedFragments(fragments) {
|
|
45
|
+
return fragments.map(frag => ({
|
|
46
|
+
level: 0,
|
|
47
|
+
isExternal: true,
|
|
48
|
+
name: frag.name.value,
|
|
49
|
+
onType: frag.typeCondition.name.value,
|
|
50
|
+
node: frag,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Creates fragment import declarations for local fragments.
|
|
55
|
+
* After splitting, these fragments will be in separate files, so we need to generate imports.
|
|
56
|
+
*
|
|
57
|
+
* Example: query references RetailerServicesData → import from './RetailerServicesData.ts'
|
|
58
|
+
*/
|
|
59
|
+
function createLocalFragmentImports(localFragments, sourceLocation, currentFilename, folder, extension, options) {
|
|
60
|
+
return localFragments.map((frag) => {
|
|
61
|
+
var _a;
|
|
62
|
+
const fragmentFilePath = (0, utils_js_1.generateOperationFilePath)(sourceLocation, frag.name.value, folder, extension);
|
|
63
|
+
// Generate import identifiers: both the document (for runtime) and type (for TypeScript)
|
|
64
|
+
const identifiers = [
|
|
65
|
+
{
|
|
66
|
+
name: `${frag.name.value}FragmentDoc`,
|
|
67
|
+
kind: 'document',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: `${frag.name.value}Fragment`,
|
|
71
|
+
kind: 'type',
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
return {
|
|
75
|
+
baseDir: options.baseDir,
|
|
76
|
+
baseOutputDir: options.baseOutputDir,
|
|
77
|
+
outputPath: currentFilename,
|
|
78
|
+
importSource: {
|
|
79
|
+
path: fragmentFilePath,
|
|
80
|
+
identifiers,
|
|
81
|
+
},
|
|
82
|
+
emitLegacyCommonJSImports: options.emitLegacyCommonJSImports,
|
|
83
|
+
importExtension: options.importExtension,
|
|
84
|
+
typesImport: (_a = options.useTypeImports) !== null && _a !== void 0 ? _a : false,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* TYPES IMPORT OPTIMIZATION
|
|
90
|
+
*
|
|
91
|
+
* Determines if `import * as Types from '...'` is actually needed for this specific operation/fragment.
|
|
92
|
+
*
|
|
93
|
+
* Background:
|
|
94
|
+
* The standard `isUsingTypes()` function is too broad - it returns true for ANY schema type reference,
|
|
95
|
+
* including object types. But object types don't need imports since they're generated inline.
|
|
96
|
+
*
|
|
97
|
+
* We ONLY need the Types import when generated code actually references the Types namespace:
|
|
98
|
+
*
|
|
99
|
+
* ✅ NEEDS IMPORT:
|
|
100
|
+
* - Operations (query/mutation/subscription) - generate `Types.Exact<{...}>` for Variables type
|
|
101
|
+
* - Enums - generate `role: Types.Role`
|
|
102
|
+
* - Custom scalars (unless mapped to primitives) - generate `date: Types.DateTime`
|
|
103
|
+
* - Input types - used in variables as `Types.MyInput`
|
|
104
|
+
*
|
|
105
|
+
* ❌ NO IMPORT NEEDED:
|
|
106
|
+
* - Fragments with only scalars/object types - no Types namespace references
|
|
107
|
+
* - Custom scalars mapped to primitives (JSON → any) - uses TypeScript primitives
|
|
108
|
+
* - Object type references (User, Product) - generated inline, not from Types
|
|
109
|
+
*
|
|
110
|
+
* This optimization reduces unnecessary imports and makes generated files cleaner.
|
|
34
111
|
*/
|
|
35
112
|
function needsSchemaTypesImport(document, schema, config) {
|
|
36
113
|
const builtInScalars = ['String', 'Int', 'Float', 'Boolean', 'ID'];
|
|
37
114
|
const primitiveTypes = ['any', 'string', 'number', 'boolean', 'null', 'undefined', 'void', 'never', 'unknown'];
|
|
38
115
|
let needsImport = false;
|
|
39
|
-
// Get scalar mappings from config
|
|
40
116
|
const scalarMappings = config.scalars || {};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
117
|
+
/**
|
|
118
|
+
* STEP 1: Check for operations (queries/mutations/subscriptions)
|
|
119
|
+
* All operations generate a Variables type that uses Types.Exact, even without variables:
|
|
120
|
+
* `export type MyQueryVariables = Types.Exact<{ [key: string]: never; }>;`
|
|
121
|
+
*/
|
|
44
122
|
let hasOperation = false;
|
|
45
|
-
// Collect all type names referenced in the document
|
|
46
123
|
const referencedTypeNames = new Set();
|
|
47
124
|
(0, graphql_1.visit)(document, {
|
|
48
|
-
// Operations always need Types import for their Variables type
|
|
49
125
|
OperationDefinition: () => {
|
|
50
126
|
hasOperation = true;
|
|
51
127
|
},
|
|
52
|
-
// Check variable types for input types or custom scalars
|
|
53
128
|
VariableDefinition: (node) => {
|
|
54
129
|
const typeName = getBaseTypeName(node.type);
|
|
55
130
|
referencedTypeNames.add(typeName);
|
|
56
131
|
},
|
|
57
|
-
// Collect fragment type conditions
|
|
58
132
|
FragmentDefinition: (node) => {
|
|
59
133
|
referencedTypeNames.add(node.typeCondition.name.value);
|
|
60
134
|
},
|
|
61
135
|
});
|
|
62
|
-
// All operations generate Variables type that uses Types.Exact
|
|
63
136
|
if (hasOperation) {
|
|
64
137
|
return true;
|
|
65
138
|
}
|
|
66
|
-
|
|
139
|
+
/**
|
|
140
|
+
* STEP 2: Check if any schema types actually need importing
|
|
141
|
+
*
|
|
142
|
+
* Helper that checks if a GraphQL type requires an import from Types namespace.
|
|
143
|
+
* Returns true only for enums, custom scalars (unless mapped to primitives), and input types.
|
|
144
|
+
*/
|
|
67
145
|
const checkType = (type) => {
|
|
68
146
|
if (!type)
|
|
69
147
|
return false;
|
|
70
|
-
// Unwrap
|
|
148
|
+
// Unwrap wrapper types (List, NonNull)
|
|
71
149
|
while ('ofType' in type && type.ofType) {
|
|
72
150
|
type = type.ofType;
|
|
73
151
|
}
|
|
74
|
-
// Enums always need
|
|
152
|
+
// Enums always need import: `role: Types.Role`
|
|
75
153
|
if ((0, graphql_1.isEnumType)(type)) {
|
|
76
154
|
return true;
|
|
77
155
|
}
|
|
78
|
-
// Custom scalars
|
|
156
|
+
// Custom scalars - depends on mapping
|
|
79
157
|
if ((0, graphql_1.isScalarType)(type) && !builtInScalars.includes(type.name)) {
|
|
80
|
-
// Check if this scalar is mapped to a primitive type
|
|
81
158
|
const mapping = scalarMappings[type.name];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
// If mapped to an object like { input: 'any', output: 'any' }, check both
|
|
88
|
-
if (typeof mapping === 'object') {
|
|
89
|
-
const inputMapping = mapping.input || mapping.output;
|
|
90
|
-
const outputMapping = mapping.output || mapping.input;
|
|
91
|
-
if (primitiveTypes.includes(inputMapping) && primitiveTypes.includes(outputMapping)) {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
159
|
+
// No mapping = defaults to 'any' = no import needed
|
|
160
|
+
if (!mapping) {
|
|
161
|
+
return false;
|
|
95
162
|
}
|
|
96
|
-
|
|
97
|
-
|
|
163
|
+
// Mapped to primitive (string, any, number, etc.) = no import needed
|
|
164
|
+
if (typeof mapping === 'string' && primitiveTypes.includes(mapping)) {
|
|
98
165
|
return false;
|
|
99
166
|
}
|
|
100
|
-
//
|
|
167
|
+
// Mapped to object with input/output
|
|
168
|
+
if (typeof mapping === 'object') {
|
|
169
|
+
const inputMapping = mapping.input || mapping.output;
|
|
170
|
+
const outputMapping = mapping.output || mapping.input;
|
|
171
|
+
if (primitiveTypes.includes(inputMapping) && primitiveTypes.includes(outputMapping)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Scalar mapped to custom type (e.g., Date) = needs import
|
|
101
176
|
return true;
|
|
102
177
|
}
|
|
103
|
-
// Input types need
|
|
178
|
+
// Input types always need import: `input: Types.MyInput`
|
|
104
179
|
if ((0, graphql_1.isInputObjectType)(type)) {
|
|
105
180
|
return true;
|
|
106
181
|
}
|
|
107
182
|
return false;
|
|
108
183
|
};
|
|
109
|
-
|
|
184
|
+
/**
|
|
185
|
+
* STEP 3: Check variable types
|
|
186
|
+
* Variables can use enums, input types, or custom scalars directly
|
|
187
|
+
*/
|
|
110
188
|
for (const typeName of referencedTypeNames) {
|
|
111
189
|
const type = schema.getType(typeName);
|
|
112
190
|
if (checkType(type)) {
|
|
113
191
|
return true;
|
|
114
192
|
}
|
|
115
193
|
}
|
|
116
|
-
|
|
117
|
-
|
|
194
|
+
/**
|
|
195
|
+
* STEP 4: Check field return types
|
|
196
|
+
* Use TypeInfo to properly resolve field types through the schema.
|
|
197
|
+
* This catches cases like: user { role } where 'role' returns an enum.
|
|
198
|
+
*/
|
|
118
199
|
const typeInfo = new graphql_1.TypeInfo(schema);
|
|
119
200
|
(0, graphql_1.visit)(document, (0, graphql_1.visitWithTypeInfo)(typeInfo, {
|
|
120
201
|
Field: () => {
|
|
@@ -138,12 +219,28 @@ function getBaseTypeName(type) {
|
|
|
138
219
|
}
|
|
139
220
|
return '';
|
|
140
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* PER-OPERATION-FILE PRESET
|
|
224
|
+
*
|
|
225
|
+
* Generates ONE FILE PER OPERATION/FRAGMENT instead of one file per source file.
|
|
226
|
+
*
|
|
227
|
+
* Architecture:
|
|
228
|
+
* 1. Fragment Resolution: Analyze all documents to build fragment registry and dependencies
|
|
229
|
+
* 2. Document Splitting: Split each source document into separate operations/fragments
|
|
230
|
+
* 3. Local Fragment Handling: Treat same-file fragments as external after splitting
|
|
231
|
+
* 4. Import Optimization: Only add Types import when actually needed (enums, scalars, operations)
|
|
232
|
+
*
|
|
233
|
+
* Example:
|
|
234
|
+
* Input: src/user.ts with query GetUser + fragment UserFields
|
|
235
|
+
* Output: src/__generated__/GetUser.ts + src/__generated__/UserFields.ts
|
|
236
|
+
*/
|
|
141
237
|
exports.preset = {
|
|
142
238
|
buildGeneratesSection: options => {
|
|
143
239
|
var _a, _b;
|
|
144
240
|
const schemaObject = options.schemaAst
|
|
145
241
|
? options.schemaAst
|
|
146
242
|
: (0, graphql_1.buildASTSchema)(options.schema, options.config);
|
|
243
|
+
// Extract configuration with defaults
|
|
147
244
|
const baseDir = options.presetConfig.cwd || process.cwd();
|
|
148
245
|
const extension = options.presetConfig.extension || '.ts';
|
|
149
246
|
const folder = options.presetConfig.folder || '__generated__';
|
|
@@ -157,8 +254,13 @@ exports.preset = {
|
|
|
157
254
|
...options.pluginMap,
|
|
158
255
|
add: add_1.default,
|
|
159
256
|
};
|
|
160
|
-
|
|
161
|
-
|
|
257
|
+
/**
|
|
258
|
+
* PHASE 1: FRAGMENT RESOLUTION
|
|
259
|
+
*
|
|
260
|
+
* Analyze all documents to build fragment registry and resolve dependencies.
|
|
261
|
+
* This uses our adapted fragment-resolver that generates paths based on fragment NAMES
|
|
262
|
+
* (not source file names), enabling proper imports after splitting.
|
|
263
|
+
*/
|
|
162
264
|
const sources = (0, resolve_document_imports_js_1.resolveDocumentImports)(options, schemaObject, {
|
|
163
265
|
baseDir,
|
|
164
266
|
folder,
|
|
@@ -170,75 +272,77 @@ exports.preset = {
|
|
|
170
272
|
typesImport: (_a = options.config.useTypeImports) !== null && _a !== void 0 ? _a : false,
|
|
171
273
|
}, (0, visitor_plugin_common_1.getConfigValue)(options.config.dedupeFragments, false));
|
|
172
274
|
const artifacts = [];
|
|
173
|
-
|
|
275
|
+
/**
|
|
276
|
+
* MAIN SPLITTING LOGIC
|
|
277
|
+
*
|
|
278
|
+
* For each source document (input file), we:
|
|
279
|
+
* 1. Extract all operations and fragments
|
|
280
|
+
* 2. Create a separate output file for EACH operation/fragment
|
|
281
|
+
* 3. Handle fragment dependencies (both external and local)
|
|
282
|
+
*/
|
|
174
283
|
for (const source of sources) {
|
|
175
284
|
const definitions = extractDefinitions(source.documents[0].document);
|
|
285
|
+
// Process each operation/fragment definition separately
|
|
176
286
|
for (const { name, definition } of definitions) {
|
|
287
|
+
// Generate output file path: sourceDir/folder/OperationName.extension
|
|
177
288
|
const filename = (0, utils_js_1.generateOperationFilePath)(source.documents[0].location, name, folder, extension);
|
|
178
|
-
// Create a document with
|
|
289
|
+
// Create a document with ONLY this single definition
|
|
179
290
|
const singleDefDocument = {
|
|
180
291
|
kind: graphql_1.Kind.DOCUMENT,
|
|
181
292
|
definitions: [definition],
|
|
182
293
|
};
|
|
294
|
+
// Note: Initially create source with just the single definition
|
|
295
|
+
// We'll update it later to include external fragments
|
|
183
296
|
const singleDefSource = {
|
|
184
|
-
rawSDL: source.documents[0].rawSDL, //
|
|
185
|
-
document: singleDefDocument,
|
|
297
|
+
rawSDL: source.documents[0].rawSDL, // Contains all original SDL (not filtered)
|
|
298
|
+
document: singleDefDocument, // Initially just this one definition
|
|
186
299
|
location: source.documents[0].location,
|
|
187
300
|
};
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
301
|
+
/**
|
|
302
|
+
* HANDLE LOCAL FRAGMENTS
|
|
303
|
+
*
|
|
304
|
+
* Problem: If source file has both query + fragment in same gql template:
|
|
305
|
+
* gql`query MyQuery { ...MyFrag } fragment MyFrag { ... }`
|
|
306
|
+
*
|
|
307
|
+
* After splitting, MyQuery needs to import from MyFrag.ts, but fragment
|
|
308
|
+
* resolver marked MyFrag as "local" (same document).
|
|
309
|
+
*
|
|
310
|
+
* Solution: Treat local fragments as external after splitting.
|
|
311
|
+
*/
|
|
312
|
+
const localFragments = findLocalFragments(definitions, name);
|
|
313
|
+
const localFragmentNodes = createLoadedFragments(localFragments);
|
|
314
|
+
// Combine external fragments (from other files) + local fragments (same source file)
|
|
200
315
|
const allExternalFragments = [
|
|
201
316
|
...source.externalFragments,
|
|
202
317
|
...localFragmentNodes,
|
|
203
318
|
];
|
|
204
|
-
|
|
319
|
+
/**
|
|
320
|
+
* UPDATE FRAGMENT IMPORTS
|
|
321
|
+
*
|
|
322
|
+
* 1. External fragments: Update outputPath to this operation's file
|
|
323
|
+
* 2. Local fragments: Generate new imports pointing to their split files
|
|
324
|
+
*/
|
|
205
325
|
const updatedFragmentImports = source.fragmentImports.map(fragmentImport => ({
|
|
206
326
|
...fragmentImport,
|
|
207
|
-
outputPath: filename,
|
|
327
|
+
outputPath: filename,
|
|
208
328
|
}));
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
{
|
|
216
|
-
name: `${frag.name.value}FragmentDoc`,
|
|
217
|
-
kind: 'document',
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
name: `${frag.name.value}Fragment`,
|
|
221
|
-
kind: 'type',
|
|
222
|
-
},
|
|
223
|
-
];
|
|
224
|
-
return {
|
|
225
|
-
baseDir,
|
|
226
|
-
baseOutputDir: options.baseOutputDir,
|
|
227
|
-
outputPath: filename,
|
|
228
|
-
importSource: {
|
|
229
|
-
path: fragmentFilePath,
|
|
230
|
-
identifiers,
|
|
231
|
-
},
|
|
232
|
-
emitLegacyCommonJSImports: options.config.emitLegacyCommonJSImports,
|
|
233
|
-
importExtension: options.config.importExtension,
|
|
234
|
-
typesImport: (_a = options.config.useTypeImports) !== null && _a !== void 0 ? _a : false,
|
|
235
|
-
};
|
|
329
|
+
const localFragmentImports = createLocalFragmentImports(localFragments, source.documents[0].location, filename, folder, extension, {
|
|
330
|
+
baseDir,
|
|
331
|
+
baseOutputDir: options.baseOutputDir,
|
|
332
|
+
emitLegacyCommonJSImports: options.config.emitLegacyCommonJSImports,
|
|
333
|
+
importExtension: options.config.importExtension,
|
|
334
|
+
useTypeImports: options.config.useTypeImports,
|
|
236
335
|
});
|
|
237
336
|
const allFragmentImports = [
|
|
238
337
|
...updatedFragmentImports,
|
|
239
338
|
...localFragmentImports,
|
|
240
339
|
];
|
|
241
|
-
|
|
340
|
+
/**
|
|
341
|
+
* ADD EXTERNAL FRAGMENTS TO DOCUMENT
|
|
342
|
+
*
|
|
343
|
+
* The plugin needs external fragment definitions in the document to properly resolve types.
|
|
344
|
+
* Without these, fragment spreads can't be expanded correctly.
|
|
345
|
+
*/
|
|
242
346
|
const singleDefDocumentWithFragments = {
|
|
243
347
|
...singleDefDocument,
|
|
244
348
|
definitions: [
|
|
@@ -246,8 +350,21 @@ exports.preset = {
|
|
|
246
350
|
...allExternalFragments.map(fragment => fragment.node),
|
|
247
351
|
],
|
|
248
352
|
};
|
|
353
|
+
// Update the source to use the document with external fragments
|
|
354
|
+
singleDefSource.document = singleDefDocumentWithFragments;
|
|
355
|
+
/**
|
|
356
|
+
* CHECK IF TYPES IMPORT IS NEEDED
|
|
357
|
+
*
|
|
358
|
+
* We only add `import * as Types from '...'` if the operation actually uses:
|
|
359
|
+
* - Enums (e.g., Role.ADMIN)
|
|
360
|
+
* - Custom scalars mapped to non-primitives
|
|
361
|
+
* - Input types (in variables)
|
|
362
|
+
* - Operations (all operations generate Variables type using Types.Exact)
|
|
363
|
+
*
|
|
364
|
+
* We DON'T import for just object type references (e.g., fragment on User)
|
|
365
|
+
*/
|
|
249
366
|
const needsTypesImport = needsSchemaTypesImport(singleDefDocumentWithFragments, schemaObject, options.config);
|
|
250
|
-
// Generate the types import statement if needed
|
|
367
|
+
// Generate the schema types import statement if needed
|
|
251
368
|
const importStatements = [];
|
|
252
369
|
if (needsTypesImport && !options.config.globalNamespace) {
|
|
253
370
|
const schemaTypesImportStatement = (0, visitor_plugin_common_1.generateImportStatement)({
|
|
@@ -264,6 +381,13 @@ exports.preset = {
|
|
|
264
381
|
});
|
|
265
382
|
importStatements.push(schemaTypesImportStatement);
|
|
266
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* BUILD PLUGIN CONFIGURATION
|
|
386
|
+
*
|
|
387
|
+
* Plugins are processed in order:
|
|
388
|
+
* 1. 'add' plugin - adds import statements
|
|
389
|
+
* 2. User plugins - generate actual operation code (typescript-operations, etc.)
|
|
390
|
+
*/
|
|
267
391
|
const plugins = [
|
|
268
392
|
...importStatements.map(importStatement => ({
|
|
269
393
|
add: { content: importStatement },
|
|
@@ -274,9 +398,26 @@ exports.preset = {
|
|
|
274
398
|
...options.config,
|
|
275
399
|
exportFragmentSpreadSubTypes: true,
|
|
276
400
|
namespacedImportName: importTypesNamespace,
|
|
277
|
-
externalFragments: allExternalFragments,
|
|
278
|
-
fragmentImports: allFragmentImports,
|
|
401
|
+
externalFragments: allExternalFragments, // Includes both external + local fragments
|
|
402
|
+
fragmentImports: allFragmentImports, // Import declarations for all fragments
|
|
279
403
|
};
|
|
404
|
+
// DEBUG: Log fragment generation details
|
|
405
|
+
if (name === 'Items_item') {
|
|
406
|
+
console.log('\n=== GENERATING Items_item ===');
|
|
407
|
+
console.log('External fragments:', allExternalFragments.map(f => ({ name: f.name, level: f.level })));
|
|
408
|
+
console.log('Fragment imports:', allFragmentImports.map(fi => fi.importSource.path));
|
|
409
|
+
console.log('Document selections:', definition.selectionSet.selections.map((s) => s.kind === 'Field' ? s.name.value : `...${s.name.value}`));
|
|
410
|
+
console.log('Document definitions (should be 1):', singleDefDocument.definitions.length);
|
|
411
|
+
console.log('singleDefDocumentWithFragments definitions:', singleDefDocumentWithFragments.definitions.length);
|
|
412
|
+
console.log(' - Main definition:', singleDefDocumentWithFragments.definitions[0].kind);
|
|
413
|
+
console.log(' - External fragments added:', singleDefDocumentWithFragments.definitions.slice(1).map((d) => d.name.value));
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* CREATE GENERATION ARTIFACT
|
|
417
|
+
*
|
|
418
|
+
* Each artifact represents one output file with its configuration.
|
|
419
|
+
* The codegen core will process each artifact through the plugin pipeline.
|
|
420
|
+
*/
|
|
280
421
|
artifacts.push({
|
|
281
422
|
...options,
|
|
282
423
|
filename,
|
|
@@ -287,7 +428,7 @@ exports.preset = {
|
|
|
287
428
|
schema: options.schema,
|
|
288
429
|
schemaAst: schemaObject,
|
|
289
430
|
skipDocumentsValidation: typeof options.config.skipDocumentsValidation === 'undefined'
|
|
290
|
-
? { skipDuplicateValidation: true }
|
|
431
|
+
? { skipDuplicateValidation: true } // Skip duplicate validation by default
|
|
291
432
|
: options.config.skipDocumentsValidation,
|
|
292
433
|
});
|
|
293
434
|
}
|
package/esm/index.js
CHANGED
|
@@ -20,97 +20,178 @@ function extractDefinitions(document) {
|
|
|
20
20
|
return definitions;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* - Enums
|
|
26
|
-
* - Custom scalars that aren't mapped to primitives
|
|
27
|
-
* - Input types (in variables)
|
|
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.
|
|
28
25
|
*
|
|
29
|
-
*
|
|
26
|
+
* Example:
|
|
27
|
+
* Source file has: query MyQuery { ...MyFragment } + fragment MyFragment { ... }
|
|
28
|
+
* After split: MyQuery.ts needs to import from MyFragment.ts
|
|
29
|
+
*/
|
|
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);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates LoadedFragment objects for local fragments so they can be treated as external fragments
|
|
38
|
+
* during code generation. This ensures proper validation and type generation.
|
|
39
|
+
*/
|
|
40
|
+
function createLoadedFragments(fragments) {
|
|
41
|
+
return fragments.map(frag => ({
|
|
42
|
+
level: 0,
|
|
43
|
+
isExternal: true,
|
|
44
|
+
name: frag.name.value,
|
|
45
|
+
onType: frag.typeCondition.name.value,
|
|
46
|
+
node: frag,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Creates fragment import declarations for local fragments.
|
|
51
|
+
* After splitting, these fragments will be in separate files, so we need to generate imports.
|
|
52
|
+
*
|
|
53
|
+
* Example: query references RetailerServicesData → import from './RetailerServicesData.ts'
|
|
54
|
+
*/
|
|
55
|
+
function createLocalFragmentImports(localFragments, sourceLocation, currentFilename, folder, extension, options) {
|
|
56
|
+
return localFragments.map((frag) => {
|
|
57
|
+
var _a;
|
|
58
|
+
const fragmentFilePath = generateOperationFilePath(sourceLocation, frag.name.value, folder, extension);
|
|
59
|
+
// Generate import identifiers: both the document (for runtime) and type (for TypeScript)
|
|
60
|
+
const identifiers = [
|
|
61
|
+
{
|
|
62
|
+
name: `${frag.name.value}FragmentDoc`,
|
|
63
|
+
kind: 'document',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: `${frag.name.value}Fragment`,
|
|
67
|
+
kind: 'type',
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
return {
|
|
71
|
+
baseDir: options.baseDir,
|
|
72
|
+
baseOutputDir: options.baseOutputDir,
|
|
73
|
+
outputPath: currentFilename,
|
|
74
|
+
importSource: {
|
|
75
|
+
path: fragmentFilePath,
|
|
76
|
+
identifiers,
|
|
77
|
+
},
|
|
78
|
+
emitLegacyCommonJSImports: options.emitLegacyCommonJSImports,
|
|
79
|
+
importExtension: options.importExtension,
|
|
80
|
+
typesImport: (_a = options.useTypeImports) !== null && _a !== void 0 ? _a : false,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* TYPES IMPORT OPTIMIZATION
|
|
86
|
+
*
|
|
87
|
+
* Determines if `import * as Types from '...'` is actually needed for this specific operation/fragment.
|
|
88
|
+
*
|
|
89
|
+
* Background:
|
|
90
|
+
* The standard `isUsingTypes()` function is too broad - it returns true for ANY schema type reference,
|
|
91
|
+
* including object types. But object types don't need imports since they're generated inline.
|
|
92
|
+
*
|
|
93
|
+
* We ONLY need the Types import when generated code actually references the Types namespace:
|
|
94
|
+
*
|
|
95
|
+
* ✅ NEEDS IMPORT:
|
|
96
|
+
* - Operations (query/mutation/subscription) - generate `Types.Exact<{...}>` for Variables type
|
|
97
|
+
* - Enums - generate `role: Types.Role`
|
|
98
|
+
* - Custom scalars (unless mapped to primitives) - generate `date: Types.DateTime`
|
|
99
|
+
* - Input types - used in variables as `Types.MyInput`
|
|
100
|
+
*
|
|
101
|
+
* ❌ NO IMPORT NEEDED:
|
|
102
|
+
* - Fragments with only scalars/object types - no Types namespace references
|
|
103
|
+
* - Custom scalars mapped to primitives (JSON → any) - uses TypeScript primitives
|
|
104
|
+
* - Object type references (User, Product) - generated inline, not from Types
|
|
105
|
+
*
|
|
106
|
+
* This optimization reduces unnecessary imports and makes generated files cleaner.
|
|
30
107
|
*/
|
|
31
108
|
function needsSchemaTypesImport(document, schema, config) {
|
|
32
109
|
const builtInScalars = ['String', 'Int', 'Float', 'Boolean', 'ID'];
|
|
33
110
|
const primitiveTypes = ['any', 'string', 'number', 'boolean', 'null', 'undefined', 'void', 'never', 'unknown'];
|
|
34
111
|
let needsImport = false;
|
|
35
|
-
// Get scalar mappings from config
|
|
36
112
|
const scalarMappings = config.scalars || {};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
113
|
+
/**
|
|
114
|
+
* STEP 1: Check for operations (queries/mutations/subscriptions)
|
|
115
|
+
* All operations generate a Variables type that uses Types.Exact, even without variables:
|
|
116
|
+
* `export type MyQueryVariables = Types.Exact<{ [key: string]: never; }>;`
|
|
117
|
+
*/
|
|
40
118
|
let hasOperation = false;
|
|
41
|
-
// Collect all type names referenced in the document
|
|
42
119
|
const referencedTypeNames = new Set();
|
|
43
120
|
visit(document, {
|
|
44
|
-
// Operations always need Types import for their Variables type
|
|
45
121
|
OperationDefinition: () => {
|
|
46
122
|
hasOperation = true;
|
|
47
123
|
},
|
|
48
|
-
// Check variable types for input types or custom scalars
|
|
49
124
|
VariableDefinition: (node) => {
|
|
50
125
|
const typeName = getBaseTypeName(node.type);
|
|
51
126
|
referencedTypeNames.add(typeName);
|
|
52
127
|
},
|
|
53
|
-
// Collect fragment type conditions
|
|
54
128
|
FragmentDefinition: (node) => {
|
|
55
129
|
referencedTypeNames.add(node.typeCondition.name.value);
|
|
56
130
|
},
|
|
57
131
|
});
|
|
58
|
-
// All operations generate Variables type that uses Types.Exact
|
|
59
132
|
if (hasOperation) {
|
|
60
133
|
return true;
|
|
61
134
|
}
|
|
62
|
-
|
|
135
|
+
/**
|
|
136
|
+
* STEP 2: Check if any schema types actually need importing
|
|
137
|
+
*
|
|
138
|
+
* Helper that checks if a GraphQL type requires an import from Types namespace.
|
|
139
|
+
* Returns true only for enums, custom scalars (unless mapped to primitives), and input types.
|
|
140
|
+
*/
|
|
63
141
|
const checkType = (type) => {
|
|
64
142
|
if (!type)
|
|
65
143
|
return false;
|
|
66
|
-
// Unwrap
|
|
144
|
+
// Unwrap wrapper types (List, NonNull)
|
|
67
145
|
while ('ofType' in type && type.ofType) {
|
|
68
146
|
type = type.ofType;
|
|
69
147
|
}
|
|
70
|
-
// Enums always need
|
|
148
|
+
// Enums always need import: `role: Types.Role`
|
|
71
149
|
if (isEnumType(type)) {
|
|
72
150
|
return true;
|
|
73
151
|
}
|
|
74
|
-
// Custom scalars
|
|
152
|
+
// Custom scalars - depends on mapping
|
|
75
153
|
if (isScalarType(type) && !builtInScalars.includes(type.name)) {
|
|
76
|
-
// Check if this scalar is mapped to a primitive type
|
|
77
154
|
const mapping = scalarMappings[type.name];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
// If mapped to an object like { input: 'any', output: 'any' }, check both
|
|
84
|
-
if (typeof mapping === 'object') {
|
|
85
|
-
const inputMapping = mapping.input || mapping.output;
|
|
86
|
-
const outputMapping = mapping.output || mapping.input;
|
|
87
|
-
if (primitiveTypes.includes(inputMapping) && primitiveTypes.includes(outputMapping)) {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
155
|
+
// No mapping = defaults to 'any' = no import needed
|
|
156
|
+
if (!mapping) {
|
|
157
|
+
return false;
|
|
91
158
|
}
|
|
92
|
-
|
|
93
|
-
|
|
159
|
+
// Mapped to primitive (string, any, number, etc.) = no import needed
|
|
160
|
+
if (typeof mapping === 'string' && primitiveTypes.includes(mapping)) {
|
|
94
161
|
return false;
|
|
95
162
|
}
|
|
96
|
-
//
|
|
163
|
+
// Mapped to object with input/output
|
|
164
|
+
if (typeof mapping === 'object') {
|
|
165
|
+
const inputMapping = mapping.input || mapping.output;
|
|
166
|
+
const outputMapping = mapping.output || mapping.input;
|
|
167
|
+
if (primitiveTypes.includes(inputMapping) && primitiveTypes.includes(outputMapping)) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Scalar mapped to custom type (e.g., Date) = needs import
|
|
97
172
|
return true;
|
|
98
173
|
}
|
|
99
|
-
// Input types need
|
|
174
|
+
// Input types always need import: `input: Types.MyInput`
|
|
100
175
|
if (isInputObjectType(type)) {
|
|
101
176
|
return true;
|
|
102
177
|
}
|
|
103
178
|
return false;
|
|
104
179
|
};
|
|
105
|
-
|
|
180
|
+
/**
|
|
181
|
+
* STEP 3: Check variable types
|
|
182
|
+
* Variables can use enums, input types, or custom scalars directly
|
|
183
|
+
*/
|
|
106
184
|
for (const typeName of referencedTypeNames) {
|
|
107
185
|
const type = schema.getType(typeName);
|
|
108
186
|
if (checkType(type)) {
|
|
109
187
|
return true;
|
|
110
188
|
}
|
|
111
189
|
}
|
|
112
|
-
|
|
113
|
-
|
|
190
|
+
/**
|
|
191
|
+
* STEP 4: Check field return types
|
|
192
|
+
* Use TypeInfo to properly resolve field types through the schema.
|
|
193
|
+
* This catches cases like: user { role } where 'role' returns an enum.
|
|
194
|
+
*/
|
|
114
195
|
const typeInfo = new TypeInfo(schema);
|
|
115
196
|
visit(document, visitWithTypeInfo(typeInfo, {
|
|
116
197
|
Field: () => {
|
|
@@ -134,12 +215,28 @@ function getBaseTypeName(type) {
|
|
|
134
215
|
}
|
|
135
216
|
return '';
|
|
136
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* PER-OPERATION-FILE PRESET
|
|
220
|
+
*
|
|
221
|
+
* Generates ONE FILE PER OPERATION/FRAGMENT instead of one file per source file.
|
|
222
|
+
*
|
|
223
|
+
* Architecture:
|
|
224
|
+
* 1. Fragment Resolution: Analyze all documents to build fragment registry and dependencies
|
|
225
|
+
* 2. Document Splitting: Split each source document into separate operations/fragments
|
|
226
|
+
* 3. Local Fragment Handling: Treat same-file fragments as external after splitting
|
|
227
|
+
* 4. Import Optimization: Only add Types import when actually needed (enums, scalars, operations)
|
|
228
|
+
*
|
|
229
|
+
* Example:
|
|
230
|
+
* Input: src/user.ts with query GetUser + fragment UserFields
|
|
231
|
+
* Output: src/__generated__/GetUser.ts + src/__generated__/UserFields.ts
|
|
232
|
+
*/
|
|
137
233
|
export const preset = {
|
|
138
234
|
buildGeneratesSection: options => {
|
|
139
235
|
var _a, _b;
|
|
140
236
|
const schemaObject = options.schemaAst
|
|
141
237
|
? options.schemaAst
|
|
142
238
|
: buildASTSchema(options.schema, options.config);
|
|
239
|
+
// Extract configuration with defaults
|
|
143
240
|
const baseDir = options.presetConfig.cwd || process.cwd();
|
|
144
241
|
const extension = options.presetConfig.extension || '.ts';
|
|
145
242
|
const folder = options.presetConfig.folder || '__generated__';
|
|
@@ -153,8 +250,13 @@ export const preset = {
|
|
|
153
250
|
...options.pluginMap,
|
|
154
251
|
add: addPlugin,
|
|
155
252
|
};
|
|
156
|
-
|
|
157
|
-
|
|
253
|
+
/**
|
|
254
|
+
* PHASE 1: FRAGMENT RESOLUTION
|
|
255
|
+
*
|
|
256
|
+
* Analyze all documents to build fragment registry and resolve dependencies.
|
|
257
|
+
* This uses our adapted fragment-resolver that generates paths based on fragment NAMES
|
|
258
|
+
* (not source file names), enabling proper imports after splitting.
|
|
259
|
+
*/
|
|
158
260
|
const sources = resolveDocumentImports(options, schemaObject, {
|
|
159
261
|
baseDir,
|
|
160
262
|
folder,
|
|
@@ -166,75 +268,77 @@ export const preset = {
|
|
|
166
268
|
typesImport: (_a = options.config.useTypeImports) !== null && _a !== void 0 ? _a : false,
|
|
167
269
|
}, getConfigValue(options.config.dedupeFragments, false));
|
|
168
270
|
const artifacts = [];
|
|
169
|
-
|
|
271
|
+
/**
|
|
272
|
+
* MAIN SPLITTING LOGIC
|
|
273
|
+
*
|
|
274
|
+
* For each source document (input file), we:
|
|
275
|
+
* 1. Extract all operations and fragments
|
|
276
|
+
* 2. Create a separate output file for EACH operation/fragment
|
|
277
|
+
* 3. Handle fragment dependencies (both external and local)
|
|
278
|
+
*/
|
|
170
279
|
for (const source of sources) {
|
|
171
280
|
const definitions = extractDefinitions(source.documents[0].document);
|
|
281
|
+
// Process each operation/fragment definition separately
|
|
172
282
|
for (const { name, definition } of definitions) {
|
|
283
|
+
// Generate output file path: sourceDir/folder/OperationName.extension
|
|
173
284
|
const filename = generateOperationFilePath(source.documents[0].location, name, folder, extension);
|
|
174
|
-
// Create a document with
|
|
285
|
+
// Create a document with ONLY this single definition
|
|
175
286
|
const singleDefDocument = {
|
|
176
287
|
kind: Kind.DOCUMENT,
|
|
177
288
|
definitions: [definition],
|
|
178
289
|
};
|
|
290
|
+
// Note: Initially create source with just the single definition
|
|
291
|
+
// We'll update it later to include external fragments
|
|
179
292
|
const singleDefSource = {
|
|
180
|
-
rawSDL: source.documents[0].rawSDL, //
|
|
181
|
-
document: singleDefDocument,
|
|
293
|
+
rawSDL: source.documents[0].rawSDL, // Contains all original SDL (not filtered)
|
|
294
|
+
document: singleDefDocument, // Initially just this one definition
|
|
182
295
|
location: source.documents[0].location,
|
|
183
296
|
};
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
297
|
+
/**
|
|
298
|
+
* HANDLE LOCAL FRAGMENTS
|
|
299
|
+
*
|
|
300
|
+
* Problem: If source file has both query + fragment in same gql template:
|
|
301
|
+
* gql`query MyQuery { ...MyFrag } fragment MyFrag { ... }`
|
|
302
|
+
*
|
|
303
|
+
* After splitting, MyQuery needs to import from MyFrag.ts, but fragment
|
|
304
|
+
* resolver marked MyFrag as "local" (same document).
|
|
305
|
+
*
|
|
306
|
+
* Solution: Treat local fragments as external after splitting.
|
|
307
|
+
*/
|
|
308
|
+
const localFragments = findLocalFragments(definitions, name);
|
|
309
|
+
const localFragmentNodes = createLoadedFragments(localFragments);
|
|
310
|
+
// Combine external fragments (from other files) + local fragments (same source file)
|
|
196
311
|
const allExternalFragments = [
|
|
197
312
|
...source.externalFragments,
|
|
198
313
|
...localFragmentNodes,
|
|
199
314
|
];
|
|
200
|
-
|
|
315
|
+
/**
|
|
316
|
+
* UPDATE FRAGMENT IMPORTS
|
|
317
|
+
*
|
|
318
|
+
* 1. External fragments: Update outputPath to this operation's file
|
|
319
|
+
* 2. Local fragments: Generate new imports pointing to their split files
|
|
320
|
+
*/
|
|
201
321
|
const updatedFragmentImports = source.fragmentImports.map(fragmentImport => ({
|
|
202
322
|
...fragmentImport,
|
|
203
|
-
outputPath: filename,
|
|
323
|
+
outputPath: filename,
|
|
204
324
|
}));
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
{
|
|
212
|
-
name: `${frag.name.value}FragmentDoc`,
|
|
213
|
-
kind: 'document',
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
name: `${frag.name.value}Fragment`,
|
|
217
|
-
kind: 'type',
|
|
218
|
-
},
|
|
219
|
-
];
|
|
220
|
-
return {
|
|
221
|
-
baseDir,
|
|
222
|
-
baseOutputDir: options.baseOutputDir,
|
|
223
|
-
outputPath: filename,
|
|
224
|
-
importSource: {
|
|
225
|
-
path: fragmentFilePath,
|
|
226
|
-
identifiers,
|
|
227
|
-
},
|
|
228
|
-
emitLegacyCommonJSImports: options.config.emitLegacyCommonJSImports,
|
|
229
|
-
importExtension: options.config.importExtension,
|
|
230
|
-
typesImport: (_a = options.config.useTypeImports) !== null && _a !== void 0 ? _a : false,
|
|
231
|
-
};
|
|
325
|
+
const localFragmentImports = createLocalFragmentImports(localFragments, source.documents[0].location, filename, folder, extension, {
|
|
326
|
+
baseDir,
|
|
327
|
+
baseOutputDir: options.baseOutputDir,
|
|
328
|
+
emitLegacyCommonJSImports: options.config.emitLegacyCommonJSImports,
|
|
329
|
+
importExtension: options.config.importExtension,
|
|
330
|
+
useTypeImports: options.config.useTypeImports,
|
|
232
331
|
});
|
|
233
332
|
const allFragmentImports = [
|
|
234
333
|
...updatedFragmentImports,
|
|
235
334
|
...localFragmentImports,
|
|
236
335
|
];
|
|
237
|
-
|
|
336
|
+
/**
|
|
337
|
+
* ADD EXTERNAL FRAGMENTS TO DOCUMENT
|
|
338
|
+
*
|
|
339
|
+
* The plugin needs external fragment definitions in the document to properly resolve types.
|
|
340
|
+
* Without these, fragment spreads can't be expanded correctly.
|
|
341
|
+
*/
|
|
238
342
|
const singleDefDocumentWithFragments = {
|
|
239
343
|
...singleDefDocument,
|
|
240
344
|
definitions: [
|
|
@@ -242,8 +346,21 @@ export const preset = {
|
|
|
242
346
|
...allExternalFragments.map(fragment => fragment.node),
|
|
243
347
|
],
|
|
244
348
|
};
|
|
349
|
+
// Update the source to use the document with external fragments
|
|
350
|
+
singleDefSource.document = singleDefDocumentWithFragments;
|
|
351
|
+
/**
|
|
352
|
+
* CHECK IF TYPES IMPORT IS NEEDED
|
|
353
|
+
*
|
|
354
|
+
* We only add `import * as Types from '...'` if the operation actually uses:
|
|
355
|
+
* - Enums (e.g., Role.ADMIN)
|
|
356
|
+
* - Custom scalars mapped to non-primitives
|
|
357
|
+
* - Input types (in variables)
|
|
358
|
+
* - Operations (all operations generate Variables type using Types.Exact)
|
|
359
|
+
*
|
|
360
|
+
* We DON'T import for just object type references (e.g., fragment on User)
|
|
361
|
+
*/
|
|
245
362
|
const needsTypesImport = needsSchemaTypesImport(singleDefDocumentWithFragments, schemaObject, options.config);
|
|
246
|
-
// Generate the types import statement if needed
|
|
363
|
+
// Generate the schema types import statement if needed
|
|
247
364
|
const importStatements = [];
|
|
248
365
|
if (needsTypesImport && !options.config.globalNamespace) {
|
|
249
366
|
const schemaTypesImportStatement = generateImportStatement({
|
|
@@ -260,6 +377,13 @@ export const preset = {
|
|
|
260
377
|
});
|
|
261
378
|
importStatements.push(schemaTypesImportStatement);
|
|
262
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* BUILD PLUGIN CONFIGURATION
|
|
382
|
+
*
|
|
383
|
+
* Plugins are processed in order:
|
|
384
|
+
* 1. 'add' plugin - adds import statements
|
|
385
|
+
* 2. User plugins - generate actual operation code (typescript-operations, etc.)
|
|
386
|
+
*/
|
|
263
387
|
const plugins = [
|
|
264
388
|
...importStatements.map(importStatement => ({
|
|
265
389
|
add: { content: importStatement },
|
|
@@ -270,9 +394,26 @@ export const preset = {
|
|
|
270
394
|
...options.config,
|
|
271
395
|
exportFragmentSpreadSubTypes: true,
|
|
272
396
|
namespacedImportName: importTypesNamespace,
|
|
273
|
-
externalFragments: allExternalFragments,
|
|
274
|
-
fragmentImports: allFragmentImports,
|
|
397
|
+
externalFragments: allExternalFragments, // Includes both external + local fragments
|
|
398
|
+
fragmentImports: allFragmentImports, // Import declarations for all fragments
|
|
275
399
|
};
|
|
400
|
+
// DEBUG: Log fragment generation details
|
|
401
|
+
if (name === 'Items_item') {
|
|
402
|
+
console.log('\n=== GENERATING Items_item ===');
|
|
403
|
+
console.log('External fragments:', allExternalFragments.map(f => ({ name: f.name, level: f.level })));
|
|
404
|
+
console.log('Fragment imports:', allFragmentImports.map(fi => fi.importSource.path));
|
|
405
|
+
console.log('Document selections:', definition.selectionSet.selections.map((s) => s.kind === 'Field' ? s.name.value : `...${s.name.value}`));
|
|
406
|
+
console.log('Document definitions (should be 1):', singleDefDocument.definitions.length);
|
|
407
|
+
console.log('singleDefDocumentWithFragments definitions:', singleDefDocumentWithFragments.definitions.length);
|
|
408
|
+
console.log(' - Main definition:', singleDefDocumentWithFragments.definitions[0].kind);
|
|
409
|
+
console.log(' - External fragments added:', singleDefDocumentWithFragments.definitions.slice(1).map((d) => d.name.value));
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* CREATE GENERATION ARTIFACT
|
|
413
|
+
*
|
|
414
|
+
* Each artifact represents one output file with its configuration.
|
|
415
|
+
* The codegen core will process each artifact through the plugin pipeline.
|
|
416
|
+
*/
|
|
276
417
|
artifacts.push({
|
|
277
418
|
...options,
|
|
278
419
|
filename,
|
|
@@ -283,7 +424,7 @@ export const preset = {
|
|
|
283
424
|
schema: options.schema,
|
|
284
425
|
schemaAst: schemaObject,
|
|
285
426
|
skipDocumentsValidation: typeof options.config.skipDocumentsValidation === 'undefined'
|
|
286
|
-
? { skipDuplicateValidation: true }
|
|
427
|
+
? { skipDuplicateValidation: true } // Skip duplicate validation by default
|
|
287
428
|
: options.config.skipDocumentsValidation,
|
|
288
429
|
});
|
|
289
430
|
}
|
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.7",
|
|
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
|
@@ -127,5 +127,20 @@ export type PerOperationFileConfig = {
|
|
|
127
127
|
*/
|
|
128
128
|
importTypesNamespace?: string;
|
|
129
129
|
};
|
|
130
|
+
/**
|
|
131
|
+
* PER-OPERATION-FILE PRESET
|
|
132
|
+
*
|
|
133
|
+
* Generates ONE FILE PER OPERATION/FRAGMENT instead of one file per source file.
|
|
134
|
+
*
|
|
135
|
+
* Architecture:
|
|
136
|
+
* 1. Fragment Resolution: Analyze all documents to build fragment registry and dependencies
|
|
137
|
+
* 2. Document Splitting: Split each source document into separate operations/fragments
|
|
138
|
+
* 3. Local Fragment Handling: Treat same-file fragments as external after splitting
|
|
139
|
+
* 4. Import Optimization: Only add Types import when actually needed (enums, scalars, operations)
|
|
140
|
+
*
|
|
141
|
+
* Example:
|
|
142
|
+
* Input: src/user.ts with query GetUser + fragment UserFields
|
|
143
|
+
* Output: src/__generated__/GetUser.ts + src/__generated__/UserFields.ts
|
|
144
|
+
*/
|
|
130
145
|
export declare const preset: Types.OutputPreset<PerOperationFileConfig>;
|
|
131
146
|
export default preset;
|
package/typings/index.d.ts
CHANGED
|
@@ -127,5 +127,20 @@ export type PerOperationFileConfig = {
|
|
|
127
127
|
*/
|
|
128
128
|
importTypesNamespace?: string;
|
|
129
129
|
};
|
|
130
|
+
/**
|
|
131
|
+
* PER-OPERATION-FILE PRESET
|
|
132
|
+
*
|
|
133
|
+
* Generates ONE FILE PER OPERATION/FRAGMENT instead of one file per source file.
|
|
134
|
+
*
|
|
135
|
+
* Architecture:
|
|
136
|
+
* 1. Fragment Resolution: Analyze all documents to build fragment registry and dependencies
|
|
137
|
+
* 2. Document Splitting: Split each source document into separate operations/fragments
|
|
138
|
+
* 3. Local Fragment Handling: Treat same-file fragments as external after splitting
|
|
139
|
+
* 4. Import Optimization: Only add Types import when actually needed (enums, scalars, operations)
|
|
140
|
+
*
|
|
141
|
+
* Example:
|
|
142
|
+
* Input: src/user.ts with query GetUser + fragment UserFields
|
|
143
|
+
* Output: src/__generated__/GetUser.ts + src/__generated__/UserFields.ts
|
|
144
|
+
*/
|
|
130
145
|
export declare const preset: Types.OutputPreset<PerOperationFileConfig>;
|
|
131
146
|
export default preset;
|