UNPKG

16.8 kBPlain TextView Raw
1import * as path from "path";
2import * as t from "@babel/types";
3import { stripIndent } from "common-tags";
4import { GraphQLEnumType, GraphQLInputObjectType } from "graphql";
5
6import {
7 CompilerContext,
8 Operation,
9 Fragment,
10 Selection,
11 SelectionSet,
12 Field,
13 FragmentSpread
14} from "apollo-codegen-core/lib/compiler";
15
16import {
17 typeCaseForSelectionSet,
18 Variant
19} from "apollo-codegen-core/lib/compiler/visitors/typeCase";
20
21import { collectAndMergeFields } from "apollo-codegen-core/lib/compiler/visitors/collectAndMergeFields";
22
23import { BasicGeneratedFile } from "apollo-codegen-core/lib/utilities/CodeGenerator";
24import TypescriptGenerator, {
25 ObjectProperty,
26 TypescriptCompilerOptions
27} from "./language";
28import Printer from "./printer";
29import { GraphQLType } from "graphql/type/definition";
30import {
31 GraphQLNonNull,
32 GraphQLOutputType,
33 getNullableType,
34 GraphQLList,
35 GraphQLObjectType
36} from "graphql";
37import { maybePush } from "apollo-codegen-core/lib/utilities/array";
38
39class TypescriptGeneratedFile implements BasicGeneratedFile {
40 fileContents: string;
41
42 constructor(fileContents: string) {
43 this.fileContents = fileContents;
44 }
45 get output() {
46 return this.fileContents;
47 }
48}
49
50function printEnumsAndInputObjects(
51 generator: TypescriptAPIGenerator,
52 typesUsed: GraphQLType[]
53) {
54 generator.printer.enqueue(stripIndent`
55 //==============================================================
56 // START Enums and Input Objects
57 //==============================================================
58 `);
59
60 typesUsed
61 .filter(type => type instanceof GraphQLEnumType)
62 .sort()
63 .forEach(enumType => {
64 generator.typeAliasForEnumType(enumType as GraphQLEnumType);
65 });
66
67 typesUsed
68 .filter(type => type instanceof GraphQLInputObjectType)
69 .sort()
70 .forEach(inputObjectType => {
71 generator.typeAliasForInputObjectType(
72 inputObjectType as GraphQLInputObjectType
73 );
74 });
75
76 generator.printer.enqueue(stripIndent`
77 //==============================================================
78 // END Enums and Input Objects
79 //==============================================================
80 `);
81}
82
83function printGlobalImport(
84 generator: TypescriptAPIGenerator,
85 typesUsed: GraphQLType[],
86 outputPath: string,
87 globalSourcePath: string
88) {
89 if (typesUsed.length > 0) {
90 const relative = path.relative(
91 path.dirname(outputPath),
92 path.join(
93 path.dirname(globalSourcePath),
94 path.basename(globalSourcePath, ".ts")
95 )
96 );
97 generator.printer.enqueue(generator.import(typesUsed, "./" + relative));
98 }
99}
100
101// TODO: deprecate this, use generateLocalSource and generateGlobalSource instead.
102export function generateSource(context: CompilerContext) {
103 const generator = new TypescriptAPIGenerator(context);
104 const generatedFiles: {
105 sourcePath: string;
106 fileName: string;
107 content: TypescriptGeneratedFile;
108 }[] = [];
109
110 Object.values(context.operations).forEach(operation => {
111 generator.fileHeader();
112 generator.interfacesForOperation(operation);
113
114 const output = generator.printer.printAndClear();
115
116 generatedFiles.push({
117 sourcePath: operation.filePath,
118 fileName: `${operation.operationName}.ts`,
119 content: new TypescriptGeneratedFile(output)
120 });
121 });
122
123 Object.values(context.fragments).forEach(fragment => {
124 generator.fileHeader();
125 generator.interfacesForFragment(fragment);
126
127 const output = generator.printer.printAndClear();
128
129 generatedFiles.push({
130 sourcePath: fragment.filePath,
131 fileName: `${fragment.fragmentName}.ts`,
132 content: new TypescriptGeneratedFile(output)
133 });
134 });
135
136 generator.fileHeader();
137 printEnumsAndInputObjects(generator, context.typesUsed);
138 const common = generator.printer.printAndClear();
139
140 return {
141 generatedFiles,
142 common
143 };
144}
145
146interface IGeneratedFileOptions {
147 outputPath?: string;
148 globalSourcePath?: string;
149}
150
151interface IGeneratedFile {
152 sourcePath: string;
153 fileName: string;
154 content: (options?: IGeneratedFileOptions) => TypescriptGeneratedFile;
155}
156
157export function generateLocalSource(
158 context: CompilerContext
159): IGeneratedFile[] {
160 const generator = new TypescriptAPIGenerator(context);
161
162 const operations = Object.values(context.operations).map(operation => ({
163 sourcePath: operation.filePath,
164 fileName: `${operation.operationName}.ts`,
165 content: (options?: IGeneratedFileOptions) => {
166 generator.fileHeader();
167 if (options && options.outputPath && options.globalSourcePath) {
168 printGlobalImport(
169 generator,
170 generator.getGlobalTypesUsedForOperation(operation),
171 options.outputPath,
172 options.globalSourcePath
173 );
174 }
175 generator.interfacesForOperation(operation);
176 const output = generator.printer.printAndClear();
177 return new TypescriptGeneratedFile(output);
178 }
179 }));
180
181 const fragments = Object.values(context.fragments).map(fragment => ({
182 sourcePath: fragment.filePath,
183 fileName: `${fragment.fragmentName}.ts`,
184 content: (options?: IGeneratedFileOptions) => {
185 generator.fileHeader();
186 if (options && options.outputPath && options.globalSourcePath) {
187 printGlobalImport(
188 generator,
189 generator.getGlobalTypesUsedForFragment(fragment),
190 options.outputPath,
191 options.globalSourcePath
192 );
193 }
194 generator.interfacesForFragment(fragment);
195 const output = generator.printer.printAndClear();
196 return new TypescriptGeneratedFile(output);
197 }
198 }));
199
200 return operations.concat(fragments);
201}
202
203export function generateGlobalSource(
204 context: CompilerContext
205): TypescriptGeneratedFile {
206 const generator = new TypescriptAPIGenerator(context);
207 generator.fileHeader();
208 printEnumsAndInputObjects(generator, context.typesUsed);
209 const output = generator.printer.printAndClear();
210 return new TypescriptGeneratedFile(output);
211}
212
213export class TypescriptAPIGenerator extends TypescriptGenerator {
214 context: CompilerContext;
215 printer: Printer;
216 scopeStack: string[];
217
218 constructor(context: CompilerContext) {
219 super(context.options as TypescriptCompilerOptions);
220
221 this.context = context;
222 this.printer = new Printer();
223 this.scopeStack = [];
224 }
225
226 fileHeader() {
227 this.printer.enqueue(
228 stripIndent`
229 /* tslint:disable */
230 // This file was automatically generated and should not be edited.
231 `
232 );
233 }
234
235 public typeAliasForEnumType(enumType: GraphQLEnumType) {
236 this.printer.enqueue(this.enumerationDeclaration(enumType));
237 }
238
239 public typeAliasForInputObjectType(inputObjectType: GraphQLInputObjectType) {
240 this.printer.enqueue(this.inputObjectDeclaration(inputObjectType));
241 }
242
243 public interfacesForOperation(operation: Operation) {
244 const { operationType, operationName, variables, selectionSet } = operation;
245
246 this.scopeStackPush(operationName);
247
248 this.printer.enqueue(stripIndent`
249 // ====================================================
250 // GraphQL ${operationType} operation: ${operationName}
251 // ====================================================
252 `);
253
254 // The root operation only has one variant
255 // Do we need to get exhaustive variants anyway?
256 const variants = this.getVariantsForSelectionSet(selectionSet);
257
258 const variant = variants[0];
259 const properties = this.getPropertiesForVariant(variant);
260
261 const exportedTypeAlias = this.exportDeclaration(
262 this.interface(operationName, properties)
263 );
264
265 this.printer.enqueue(exportedTypeAlias);
266 this.scopeStackPop();
267
268 // Generate the variables interface if the operation has any variables
269 if (variables.length > 0) {
270 const interfaceName = operationName + "Variables";
271 this.scopeStackPush(interfaceName);
272 this.printer.enqueue(
273 this.exportDeclaration(
274 this.interface(
275 interfaceName,
276 variables.map(variable => ({
277 name: variable.name,
278 type: this.typeFromGraphQLType(variable.type)
279 })),
280 { keyInheritsNullability: true }
281 )
282 )
283 );
284 this.scopeStackPop();
285 }
286 }
287
288 public interfacesForFragment(fragment: Fragment) {
289 const { fragmentName, selectionSet } = fragment;
290 this.scopeStackPush(fragmentName);
291
292 this.printer.enqueue(stripIndent`
293 // ====================================================
294 // GraphQL fragment: ${fragmentName}
295 // ====================================================
296 `);
297
298 const variants = this.getVariantsForSelectionSet(selectionSet);
299
300 if (variants.length === 1) {
301 const properties = this.getPropertiesForVariant(variants[0]);
302
303 const name = this.nameFromScopeStack(this.scopeStack);
304 const exportedTypeAlias = this.exportDeclaration(
305 this.interface(name, properties)
306 );
307
308 this.printer.enqueue(exportedTypeAlias);
309 } else {
310 const unionMembers: t.Identifier[] = [];
311 variants.forEach(variant => {
312 this.scopeStackPush(variant.possibleTypes[0].toString());
313 const properties = this.getPropertiesForVariant(variant);
314
315 const name = this.nameFromScopeStack(this.scopeStack);
316 const exportedTypeAlias = this.exportDeclaration(
317 this.interface(name, properties)
318 );
319
320 this.printer.enqueue(exportedTypeAlias);
321
322 unionMembers.push(
323 t.identifier(this.nameFromScopeStack(this.scopeStack))
324 );
325
326 this.scopeStackPop();
327 });
328
329 this.printer.enqueue(
330 this.exportDeclaration(
331 this.typeAliasGenericUnion(
332 this.nameFromScopeStack(this.scopeStack),
333 unionMembers.map(id => t.TSTypeReference(id))
334 )
335 )
336 );
337 }
338
339 this.scopeStackPop();
340 }
341
342 public getGlobalTypesUsedForOperation = (doc: Operation) => {
343 const typesUsed = doc.variables.reduce(
344 (acc: GraphQLType[], { type }: { type: GraphQLType }) => {
345 const t = this.getUnderlyingType(type);
346 if (this.isGlobalType(t)) {
347 return maybePush(acc, t);
348 }
349 return acc;
350 },
351 []
352 );
353 return doc.selectionSet.selections.reduce(this.reduceSelection, typesUsed);
354 };
355
356 public getGlobalTypesUsedForFragment = (doc: Fragment) => {
357 return doc.selectionSet.selections.reduce(this.reduceSelection, []);
358 };
359
360 private reduceSelection = (
361 acc: GraphQLType[],
362 selection: Selection
363 ): GraphQLType[] => {
364 if (selection.kind === "Field" || selection.kind === "TypeCondition") {
365 const type = this.getUnderlyingType(selection.type);
366 if (this.isGlobalType(type)) {
367 acc = maybePush(acc, type);
368 }
369 }
370
371 if (selection.selectionSet) {
372 return selection.selectionSet.selections.reduce(
373 this.reduceSelection,
374 acc
375 );
376 }
377
378 return acc;
379 };
380
381 private isGlobalType = (type: GraphQLType) => {
382 return (
383 type instanceof GraphQLEnumType || type instanceof GraphQLInputObjectType
384 );
385 };
386
387 private getUnderlyingType = (type: GraphQLType): GraphQLType => {
388 if (type instanceof GraphQLNonNull) {
389 return this.getUnderlyingType(getNullableType(type));
390 }
391 if (type instanceof GraphQLList) {
392 return this.getUnderlyingType(type.ofType);
393 }
394 return type;
395 };
396
397 public getTypesUsedForOperation(
398 doc: Operation | Fragment,
399 context: CompilerContext
400 ) {
401 let docTypesUsed: GraphQLType[] = [];
402
403 if (doc.hasOwnProperty("operationName")) {
404 const operation = doc as Operation;
405 docTypesUsed = operation.variables.map(({ type }) => type);
406 }
407
408 const reduceTypesForDocument = (
409 nestDoc: Operation | Fragment | FragmentSpread,
410 acc: GraphQLType[]
411 ) => {
412 const {
413 selectionSet: { possibleTypes, selections }
414 } = nestDoc;
415
416 acc = possibleTypes.reduce(maybePush, acc);
417
418 acc = selections.reduce((selectionAcc, selection) => {
419 switch (selection.kind) {
420 case "Field":
421 case "TypeCondition":
422 selectionAcc = maybePush(selectionAcc, selection.type);
423 break;
424 case "FragmentSpread":
425 selectionAcc = reduceTypesForDocument(selection, selectionAcc);
426 break;
427 default:
428 break;
429 }
430
431 return selectionAcc;
432 }, acc);
433
434 return acc;
435 };
436
437 docTypesUsed = reduceTypesForDocument(doc, docTypesUsed).reduce(
438 this.reduceTypesUsed,
439 []
440 );
441
442 return context.typesUsed.filter(type => {
443 return docTypesUsed.find(typeUsed => type === typeUsed);
444 });
445 }
446
447 private reduceTypesUsed = (
448 acc: (GraphQLType | GraphQLOutputType)[],
449 type: GraphQLType
450 ) => {
451 if (type instanceof GraphQLNonNull) {
452 type = getNullableType(type);
453 }
454
455 if (type instanceof GraphQLList) {
456 type = type.ofType;
457 }
458
459 if (
460 type instanceof GraphQLInputObjectType ||
461 type instanceof GraphQLObjectType
462 ) {
463 acc = maybePush(acc, type);
464 const fields = type.getFields();
465 acc = Object.keys(fields)
466 .map(key => fields[key] && fields[key].type)
467 .reduce(this.reduceTypesUsed, acc);
468 } else {
469 acc = maybePush(acc, type);
470 }
471
472 return acc;
473 };
474
475 private getVariantsForSelectionSet(selectionSet: SelectionSet) {
476 return this.getTypeCasesForSelectionSet(selectionSet).exhaustiveVariants;
477 }
478
479 private getTypeCasesForSelectionSet(selectionSet: SelectionSet) {
480 return typeCaseForSelectionSet(
481 selectionSet,
482 this.context.options.mergeInFieldsFromFragmentSpreads
483 );
484 }
485
486 private getPropertiesForVariant(variant: Variant): ObjectProperty[] {
487 const fields = collectAndMergeFields(
488 variant,
489 this.context.options.mergeInFieldsFromFragmentSpreads
490 );
491 return fields.map(field => {
492 const fieldName = field.alias !== undefined ? field.alias : field.name;
493 this.scopeStackPush(fieldName);
494
495 let res: ObjectProperty;
496 if (field.selectionSet) {
497 res = this.handleFieldSelectionSetValue(
498 t.identifier(this.nameFromScopeStack(this.scopeStack)),
499 field
500 );
501 } else {
502 res = this.handleFieldValue(field, variant);
503 }
504
505 this.scopeStackPop();
506 return res;
507 });
508 }
509
510 private handleFieldSelectionSetValue(
511 generatedIdentifier: t.Identifier,
512 field: Field
513 ): ObjectProperty {
514 const { selectionSet } = field;
515
516 const type = this.typeFromGraphQLType(field.type, generatedIdentifier.name);
517
518 const typeCase = this.getTypeCasesForSelectionSet(
519 selectionSet as SelectionSet
520 );
521 const variants = typeCase.exhaustiveVariants;
522
523 let exportedTypeAlias;
524 if (variants.length === 1) {
525 const variant = variants[0];
526 const properties = this.getPropertiesForVariant(variant);
527 exportedTypeAlias = this.exportDeclaration(
528 this.interface(this.nameFromScopeStack(this.scopeStack), properties)
529 );
530 } else {
531 const identifiers = variants.map(variant => {
532 this.scopeStackPush(variant.possibleTypes[0].toString());
533 const properties = this.getPropertiesForVariant(variant);
534 const identifierName = this.nameFromScopeStack(this.scopeStack);
535
536 this.printer.enqueue(
537 this.exportDeclaration(this.interface(identifierName, properties))
538 );
539
540 this.scopeStackPop();
541 return t.identifier(identifierName);
542 });
543
544 exportedTypeAlias = this.exportDeclaration(
545 this.typeAliasGenericUnion(
546 generatedIdentifier.name,
547 identifiers.map(i => t.TSTypeReference(i))
548 )
549 );
550 }
551
552 this.printer.enqueue(exportedTypeAlias);
553
554 return {
555 name: field.alias ? field.alias : field.name,
556 description: field.description,
557 type
558 };
559 }
560
561 private handleFieldValue(field: Field, variant: Variant): ObjectProperty {
562 let res: ObjectProperty;
563 if (field.name === "__typename") {
564 const types = variant.possibleTypes.map(type => {
565 return t.TSLiteralType(t.stringLiteral(type.toString()));
566 });
567
568 res = {
569 name: field.alias ? field.alias : field.name,
570 description: field.description,
571 type: t.TSUnionType(types)
572 };
573 } else {
574 // TODO: Double check that this works
575 res = {
576 name: field.alias ? field.alias : field.name,
577 description: field.description,
578 type: this.typeFromGraphQLType(field.type)
579 };
580 }
581
582 return res;
583 }
584
585 public get output(): string {
586 return this.printer.print();
587 }
588
589 scopeStackPush(name: string) {
590 this.scopeStack.push(name);
591 }
592
593 scopeStackPop() {
594 const popped = this.scopeStack.pop();
595 return popped;
596 }
597}