1 | import * as path from "path";
|
2 | import * as t from "@babel/types";
|
3 | import { stripIndent } from "common-tags";
|
4 | import { GraphQLEnumType, GraphQLInputObjectType } from "graphql";
|
5 |
|
6 | import {
|
7 | CompilerContext,
|
8 | Operation,
|
9 | Fragment,
|
10 | Selection,
|
11 | SelectionSet,
|
12 | Field,
|
13 | FragmentSpread
|
14 | } from "apollo-codegen-core/lib/compiler";
|
15 |
|
16 | import {
|
17 | typeCaseForSelectionSet,
|
18 | Variant
|
19 | } from "apollo-codegen-core/lib/compiler/visitors/typeCase";
|
20 |
|
21 | import { collectAndMergeFields } from "apollo-codegen-core/lib/compiler/visitors/collectAndMergeFields";
|
22 |
|
23 | import { BasicGeneratedFile } from "apollo-codegen-core/lib/utilities/CodeGenerator";
|
24 | import TypescriptGenerator, {
|
25 | ObjectProperty,
|
26 | TypescriptCompilerOptions
|
27 | } from "./language";
|
28 | import Printer from "./printer";
|
29 | import { GraphQLType } from "graphql/type/definition";
|
30 | import {
|
31 | GraphQLNonNull,
|
32 | GraphQLOutputType,
|
33 | getNullableType,
|
34 | GraphQLList,
|
35 | GraphQLObjectType
|
36 | } from "graphql";
|
37 | import { maybePush } from "apollo-codegen-core/lib/utilities/array";
|
38 |
|
39 | class 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 |
|
50 | function 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 |
|
83 | function 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 |
|
102 | export 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 |
|
146 | interface IGeneratedFileOptions {
|
147 | outputPath?: string;
|
148 | globalSourcePath?: string;
|
149 | }
|
150 |
|
151 | interface IGeneratedFile {
|
152 | sourcePath: string;
|
153 | fileName: string;
|
154 | content: (options?: IGeneratedFileOptions) => TypescriptGeneratedFile;
|
155 | }
|
156 |
|
157 | export 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 |
|
203 | export 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 |
|
213 | export 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 |
|
255 |
|
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 |
|
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 |
|
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 | }
|