@nmarks/graphql-codegen-per-operation-file-preset 1.0.5 → 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 CHANGED
@@ -24,97 +24,178 @@ function extractDefinitions(document) {
24
24
  return definitions;
25
25
  }
26
26
  /**
27
- * Check if a document actually needs type imports from the schema.
28
- * Only returns true if the document uses:
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
- * Does NOT return true for just referencing object types.
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
- // Check if document has an operation definition
42
- // ALL operations (query/mutation/subscription) generate a Variables type that uses Types.Exact
43
- // even if they have no variables: Types.Exact<{ [key: string]: never; }>
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
- // Helper to recursively check if a type needs importing
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 lists and non-null
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 to be imported
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 need to be imported UNLESS mapped to primitives
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
- if (mapping) {
83
- // If mapped to a primitive type string, no import needed
84
- if (typeof mapping === 'string' && primitiveTypes.includes(mapping)) {
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
- else {
97
- // No mapping specified, defaults to 'any', so no import needed
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
- // Scalar is mapped to something non-primitive, needs import
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 to be imported
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
- // Check variable types
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
- // Now we need to check all fields in the document to see if any return enums/custom scalars
117
- // We'll do a more thorough traversal with TypeInfo
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
- // Resolve fragment dependencies using our adapted fragment resolver
161
- // Fragment paths will be generated based on fragment names
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,36 +272,99 @@ 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
- // Now split each source into separate files per operation/fragment
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 just this operation/fragment
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, // TODO: might need to filter this
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
- // Update fragment imports to use the correct output path for this operation
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)
315
+ const allExternalFragments = [
316
+ ...source.externalFragments,
317
+ ...localFragmentNodes,
318
+ ];
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
+ */
189
325
  const updatedFragmentImports = source.fragmentImports.map(fragmentImport => ({
190
326
  ...fragmentImport,
191
- outputPath: filename, // Update to actual output path
327
+ outputPath: filename,
192
328
  }));
193
- // Check if THIS specific operation uses types (not the whole source file)
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,
335
+ });
336
+ const allFragmentImports = [
337
+ ...updatedFragmentImports,
338
+ ...localFragmentImports,
339
+ ];
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
+ */
194
346
  const singleDefDocumentWithFragments = {
195
347
  ...singleDefDocument,
196
348
  definitions: [
197
349
  ...singleDefDocument.definitions,
198
- ...source.externalFragments.map(fragment => fragment.node),
350
+ ...allExternalFragments.map(fragment => fragment.node),
199
351
  ],
200
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
+ */
201
366
  const needsTypesImport = needsSchemaTypesImport(singleDefDocumentWithFragments, schemaObject, options.config);
202
- // Generate the types import statement if needed
367
+ // Generate the schema types import statement if needed
203
368
  const importStatements = [];
204
369
  if (needsTypesImport && !options.config.globalNamespace) {
205
370
  const schemaTypesImportStatement = (0, visitor_plugin_common_1.generateImportStatement)({
@@ -216,6 +381,13 @@ exports.preset = {
216
381
  });
217
382
  importStatements.push(schemaTypesImportStatement);
218
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
+ */
219
391
  const plugins = [
220
392
  ...importStatements.map(importStatement => ({
221
393
  add: { content: importStatement },
@@ -226,9 +398,26 @@ exports.preset = {
226
398
  ...options.config,
227
399
  exportFragmentSpreadSubTypes: true,
228
400
  namespacedImportName: importTypesNamespace,
229
- externalFragments: source.externalFragments,
230
- fragmentImports: updatedFragmentImports,
401
+ externalFragments: allExternalFragments, // Includes both external + local fragments
402
+ fragmentImports: allFragmentImports, // Import declarations for all fragments
231
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
+ */
232
421
  artifacts.push({
233
422
  ...options,
234
423
  filename,
@@ -239,7 +428,7 @@ exports.preset = {
239
428
  schema: options.schema,
240
429
  schemaAst: schemaObject,
241
430
  skipDocumentsValidation: typeof options.config.skipDocumentsValidation === 'undefined'
242
- ? { skipDuplicateValidation: true }
431
+ ? { skipDuplicateValidation: true } // Skip duplicate validation by default
243
432
  : options.config.skipDocumentsValidation,
244
433
  });
245
434
  }
package/esm/index.js CHANGED
@@ -20,97 +20,178 @@ function extractDefinitions(document) {
20
20
  return definitions;
21
21
  }
22
22
  /**
23
- * Check if a document actually needs type imports from the schema.
24
- * Only returns true if the document uses:
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
- * Does NOT return true for just referencing object types.
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
- // Check if document has an operation definition
38
- // ALL operations (query/mutation/subscription) generate a Variables type that uses Types.Exact
39
- // even if they have no variables: Types.Exact<{ [key: string]: never; }>
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
- // Helper to recursively check if a type needs importing
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 lists and non-null
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 to be imported
148
+ // Enums always need import: `role: Types.Role`
71
149
  if (isEnumType(type)) {
72
150
  return true;
73
151
  }
74
- // Custom scalars need to be imported UNLESS mapped to primitives
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
- if (mapping) {
79
- // If mapped to a primitive type string, no import needed
80
- if (typeof mapping === 'string' && primitiveTypes.includes(mapping)) {
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
- else {
93
- // No mapping specified, defaults to 'any', so no import needed
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
- // Scalar is mapped to something non-primitive, needs import
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 to be imported
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
- // Check variable types
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
- // Now we need to check all fields in the document to see if any return enums/custom scalars
113
- // We'll do a more thorough traversal with TypeInfo
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
- // Resolve fragment dependencies using our adapted fragment resolver
157
- // Fragment paths will be generated based on fragment names
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,36 +268,99 @@ 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
- // Now split each source into separate files per operation/fragment
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 just this operation/fragment
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, // TODO: might need to filter this
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
- // Update fragment imports to use the correct output path for this operation
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)
311
+ const allExternalFragments = [
312
+ ...source.externalFragments,
313
+ ...localFragmentNodes,
314
+ ];
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
+ */
185
321
  const updatedFragmentImports = source.fragmentImports.map(fragmentImport => ({
186
322
  ...fragmentImport,
187
- outputPath: filename, // Update to actual output path
323
+ outputPath: filename,
188
324
  }));
189
- // Check if THIS specific operation uses types (not the whole source file)
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,
331
+ });
332
+ const allFragmentImports = [
333
+ ...updatedFragmentImports,
334
+ ...localFragmentImports,
335
+ ];
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
+ */
190
342
  const singleDefDocumentWithFragments = {
191
343
  ...singleDefDocument,
192
344
  definitions: [
193
345
  ...singleDefDocument.definitions,
194
- ...source.externalFragments.map(fragment => fragment.node),
346
+ ...allExternalFragments.map(fragment => fragment.node),
195
347
  ],
196
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
+ */
197
362
  const needsTypesImport = needsSchemaTypesImport(singleDefDocumentWithFragments, schemaObject, options.config);
198
- // Generate the types import statement if needed
363
+ // Generate the schema types import statement if needed
199
364
  const importStatements = [];
200
365
  if (needsTypesImport && !options.config.globalNamespace) {
201
366
  const schemaTypesImportStatement = generateImportStatement({
@@ -212,6 +377,13 @@ export const preset = {
212
377
  });
213
378
  importStatements.push(schemaTypesImportStatement);
214
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
+ */
215
387
  const plugins = [
216
388
  ...importStatements.map(importStatement => ({
217
389
  add: { content: importStatement },
@@ -222,9 +394,26 @@ export const preset = {
222
394
  ...options.config,
223
395
  exportFragmentSpreadSubTypes: true,
224
396
  namespacedImportName: importTypesNamespace,
225
- externalFragments: source.externalFragments,
226
- fragmentImports: updatedFragmentImports,
397
+ externalFragments: allExternalFragments, // Includes both external + local fragments
398
+ fragmentImports: allFragmentImports, // Import declarations for all fragments
227
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
+ */
228
417
  artifacts.push({
229
418
  ...options,
230
419
  filename,
@@ -235,7 +424,7 @@ export const preset = {
235
424
  schema: options.schema,
236
425
  schemaAst: schemaObject,
237
426
  skipDocumentsValidation: typeof options.config.skipDocumentsValidation === 'undefined'
238
- ? { skipDuplicateValidation: true }
427
+ ? { skipDuplicateValidation: true } // Skip duplicate validation by default
239
428
  : options.config.skipDocumentsValidation,
240
429
  });
241
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.5",
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"
@@ -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;
@@ -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;