UNPKG

69.5 kBJavaScriptView Raw
1import { isDetailedError, DetailedError, createNoopProfiler, createProfiler, getCachedDocumentNodeFromSchema, normalizeInstanceOrArray, normalizeConfig, normalizeOutputParam } from '@graphql-codegen/plugin-helpers';
2import { codegen } from '@graphql-codegen/core';
3import { AggregateError, isValidPath } from '@graphql-tools/utils';
4import chalk from 'chalk';
5import logSymbols from 'log-symbols';
6import ansiEscapes from 'ansi-escapes';
7import wrapAnsi from 'wrap-ansi';
8import { stripIndent } from 'common-tags';
9import { dummyLogger } from 'ts-log';
10import UpdateRenderer from 'listr-update-renderer';
11import { print, GraphQLError } from 'graphql';
12import path, { resolve, extname, join, delimiter, sep, dirname, isAbsolute, relative } from 'path';
13import { cosmiconfig, defaultLoaders } from 'cosmiconfig';
14import { env } from 'string-env-interpolation';
15import yargs from 'yargs';
16import { loadConfig } from 'graphql-config';
17import { ApolloEngineLoader } from '@graphql-tools/apollo-engine-loader';
18import { CodeFileLoader } from '@graphql-tools/code-file-loader';
19import { GitLoader } from '@graphql-tools/git-loader';
20import { GithubLoader } from '@graphql-tools/github-loader';
21import { PrismaLoader } from '@graphql-tools/prisma-loader';
22import { loadSchema as loadSchema$1, loadDocuments as loadDocuments$1 } from '@graphql-tools/load';
23import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
24import { JsonFileLoader } from '@graphql-tools/json-file-loader';
25import { UrlLoader } from '@graphql-tools/url-loader';
26import yaml from 'yaml';
27import { createRequire } from 'module';
28import fs, { promises, unlink, writeFileSync, readFileSync } from 'fs';
29import { createHash } from 'crypto';
30import { cpus } from 'os';
31import Listr from 'listr';
32import { exec } from 'child_process';
33import isGlob from 'is-glob';
34import debounce from 'debounce';
35import mkdirp from 'mkdirp';
36import inquirer from 'inquirer';
37import detectIndent from 'detect-indent';
38import getLatestVersion from 'latest-version';
39
40/**
41Indent each line in a string.
42@param string - The string to indent.
43@param count - How many times you want `options.indent` repeated. Default: `1`.
44@example
45```
46import indentString from 'indent-string';
47indentString('Unicorns\nRainbows', 4);
48//=> ' Unicorns\n Rainbows'
49indentString('Unicorns\nRainbows', 4, {indent: '♥'});
50//=> '♥♥♥♥Unicorns\n♥♥♥♥Rainbows'
51```
52*/
53function indentString(string, count = 1, options = {}) {
54 const { indent = ' ', includeEmptyLines = false } = options;
55 if (typeof string !== 'string') {
56 throw new TypeError(`Expected \`input\` to be a \`string\`, got \`${typeof string}\``);
57 }
58 if (typeof count !== 'number') {
59 throw new TypeError(`Expected \`count\` to be a \`number\`, got \`${typeof count}\``);
60 }
61 if (count < 0) {
62 throw new RangeError(`Expected \`count\` to be at least 0, got \`${count}\``);
63 }
64 if (typeof indent !== 'string') {
65 throw new TypeError(`Expected \`options.indent\` to be a \`string\`, got \`${typeof indent}\``);
66 }
67 if (count === 0) {
68 return string;
69 }
70 const regex = includeEmptyLines ? /^/gm : /^(?!\s*$)/gm;
71 return string.replace(regex, indent.repeat(count));
72}
73
74let logger;
75function getLogger() {
76 return logger || dummyLogger;
77}
78useWinstonLogger();
79function useWinstonLogger() {
80 if (logger && logger.levels) {
81 return;
82 }
83 logger = console;
84}
85
86let queue = [];
87function debugLog(message, ...meta) {
88 if (!process.env.GQL_CODEGEN_NODEBUG && process.env.DEBUG !== undefined) {
89 queue.push({
90 message,
91 meta,
92 });
93 }
94}
95function printLogs() {
96 if (!process.env.GQL_CODEGEN_NODEBUG && process.env.DEBUG !== undefined) {
97 queue.forEach(log => {
98 getLogger().info(log.message, ...log.meta);
99 });
100 resetLogs();
101 }
102}
103function resetLogs() {
104 queue = [];
105}
106
107class Renderer {
108 constructor(tasks, options) {
109 this.updateRenderer = new UpdateRenderer(tasks, options);
110 }
111 render() {
112 return this.updateRenderer.render();
113 }
114 end(err) {
115 this.updateRenderer.end(err);
116 if (typeof err === 'undefined') {
117 logUpdate.clear();
118 return;
119 }
120 // persist the output
121 logUpdate.done();
122 // show errors
123 if (err) {
124 const errorCount = err.errors ? err.errors.length : 0;
125 if (errorCount > 0) {
126 const count = indentString(chalk.red.bold(`Found ${errorCount} error${errorCount > 1 ? 's' : ''}`), 1);
127 const details = err.errors
128 .map(error => {
129 debugLog(`[CLI] Exited with an error`, error);
130 return { msg: isDetailedError(error) ? error.details : null, rawError: error };
131 })
132 .map(({ msg, rawError }, i) => {
133 const source = err.errors[i].source;
134 msg = msg ? chalk.gray(indentString(stripIndent(`${msg}`), 4)) : null;
135 const stack = rawError.stack ? chalk.gray(indentString(stripIndent(rawError.stack), 4)) : null;
136 if (source) {
137 const sourceOfError = typeof source === 'string' ? source : source.name;
138 const title = indentString(`${logSymbols.error} ${sourceOfError}`, 2);
139 return [title, msg, stack, stack].filter(Boolean).join('\n');
140 }
141 return [msg, stack].filter(Boolean).join('\n');
142 })
143 .join('\n\n');
144 logUpdate.emit(['', count, details, ''].join('\n\n'));
145 }
146 else {
147 const details = err.details ? err.details : '';
148 logUpdate.emit(`${chalk.red.bold(`${indentString(err.message, 2)}`)}\n${details}\n${chalk.grey(err.stack)}`);
149 }
150 }
151 logUpdate.done();
152 printLogs();
153 }
154}
155const render = tasks => {
156 for (const task of tasks) {
157 task.subscribe(event => {
158 if (event.type === 'SUBTASKS') {
159 render(task.subtasks);
160 return;
161 }
162 if (event.type === 'DATA') {
163 logUpdate.emit(chalk.dim(`${event.data}`));
164 }
165 logUpdate.done();
166 }, err => {
167 logUpdate.emit(err);
168 logUpdate.done();
169 });
170 }
171};
172class ErrorRenderer {
173 constructor(tasks, _options) {
174 this.tasks = tasks;
175 }
176 render() {
177 render(this.tasks);
178 }
179 static get nonTTY() {
180 return true;
181 }
182 end() { }
183}
184class LogUpdate {
185 constructor() {
186 this.stream = process.stdout;
187 // state
188 this.previousLineCount = 0;
189 this.previousOutput = '';
190 this.previousWidth = this.getWidth();
191 }
192 emit(...args) {
193 let output = args.join(' ') + '\n';
194 const width = this.getWidth();
195 if (output === this.previousOutput && this.previousWidth === width) {
196 return;
197 }
198 this.previousOutput = output;
199 this.previousWidth = width;
200 output = wrapAnsi(output, width, {
201 trim: false,
202 hard: true,
203 wordWrap: false,
204 });
205 this.stream.write(ansiEscapes.eraseLines(this.previousLineCount) + output);
206 this.previousLineCount = output.split('\n').length;
207 }
208 clear() {
209 this.stream.write(ansiEscapes.eraseLines(this.previousLineCount));
210 this.previousOutput = '';
211 this.previousWidth = this.getWidth();
212 this.previousLineCount = 0;
213 }
214 done() {
215 this.previousOutput = '';
216 this.previousWidth = this.getWidth();
217 this.previousLineCount = 0;
218 }
219 getWidth() {
220 const { columns } = this.stream;
221 if (!columns) {
222 return 80;
223 }
224 return columns;
225 }
226}
227const logUpdate = new LogUpdate();
228
229async function getPluginByName(name, pluginLoader) {
230 const possibleNames = [
231 `@graphql-codegen/${name}`,
232 `@graphql-codegen/${name}-template`,
233 `@graphql-codegen/${name}-plugin`,
234 `graphql-codegen-${name}`,
235 `graphql-codegen-${name}-template`,
236 `graphql-codegen-${name}-plugin`,
237 `codegen-${name}`,
238 `codegen-${name}-template`,
239 name,
240 ];
241 const possibleModules = possibleNames.concat(resolve(process.cwd(), name));
242 for (const moduleName of possibleModules) {
243 try {
244 return await pluginLoader(moduleName);
245 }
246 catch (err) {
247 if (err.code !== 'MODULE_NOT_FOUND') {
248 throw new DetailedError(`Unable to load template plugin matching ${name}`, `
249 Unable to load template plugin matching '${name}'.
250 Reason:
251 ${err.message}
252 `);
253 }
254 }
255 }
256 const possibleNamesMsg = possibleNames
257 .map(name => `
258 - ${name}
259 `.trimRight())
260 .join('');
261 throw new DetailedError(`Unable to find template plugin matching ${name}`, `
262 Unable to find template plugin matching '${name}'
263 Install one of the following packages:
264
265 ${possibleNamesMsg}
266 `);
267}
268
269async function getPresetByName(name, loader) {
270 const possibleNames = [
271 `@graphql-codegen/${name}`,
272 `@graphql-codegen/${name}-preset`,
273 name,
274 resolve(process.cwd(), name),
275 ];
276 for (const moduleName of possibleNames) {
277 try {
278 const loaded = await loader(moduleName);
279 if (loaded && loaded.preset) {
280 return loaded.preset;
281 }
282 else if (loaded && loaded.default) {
283 return loaded.default;
284 }
285 return loaded;
286 }
287 catch (err) {
288 if (err.code !== 'MODULE_NOT_FOUND') {
289 throw new DetailedError(`Unable to load preset matching ${name}`, `
290 Unable to load preset matching '${name}'.
291 Reason:
292 ${err.message}
293 `);
294 }
295 }
296 }
297 const possibleNamesMsg = possibleNames
298 .map(name => `
299 - ${name}
300 `.trimRight())
301 .join('');
302 throw new DetailedError(`Unable to find preset matching ${name}`, `
303 Unable to find preset matching '${name}'
304 Install one of the following packages:
305
306 ${possibleNamesMsg}
307 `);
308}
309
310const CodegenExtension = (api) => {
311 // Schema
312 api.loaders.schema.register(new CodeFileLoader({
313 pluckConfig: {
314 skipIndent: true,
315 },
316 }));
317 api.loaders.schema.register(new GitLoader());
318 api.loaders.schema.register(new GithubLoader());
319 api.loaders.schema.register(new ApolloEngineLoader());
320 api.loaders.schema.register(new PrismaLoader());
321 // Documents
322 api.loaders.documents.register(new CodeFileLoader({
323 pluckConfig: {
324 skipIndent: true,
325 },
326 }));
327 api.loaders.documents.register(new GitLoader());
328 api.loaders.documents.register(new GithubLoader());
329 return {
330 name: 'codegen',
331 };
332};
333async function findAndLoadGraphQLConfig(filepath) {
334 const config = await loadConfig({
335 filepath,
336 rootDir: process.cwd(),
337 extensions: [CodegenExtension],
338 throwOnEmpty: false,
339 throwOnMissing: false,
340 });
341 if (isGraphQLConfig(config)) {
342 return config;
343 }
344}
345// Kamil: user might load a config that is not GraphQL Config
346// so we need to check if it's a regular config or not
347function isGraphQLConfig(config) {
348 if (!config) {
349 return false;
350 }
351 try {
352 return config.getDefault().hasExtension('codegen');
353 }
354 catch (e) { }
355 try {
356 for (const projectName in config.projects) {
357 if (config.projects.hasOwnProperty(projectName)) {
358 const project = config.projects[projectName];
359 if (project.hasExtension('codegen')) {
360 return true;
361 }
362 }
363 }
364 }
365 catch (e) { }
366 return false;
367}
368
369const defaultSchemaLoadOptions = {
370 assumeValidSDL: true,
371 sort: true,
372 convertExtensions: true,
373 includeSources: true,
374};
375const defaultDocumentsLoadOptions = {
376 sort: true,
377 skipGraphQLImport: true,
378};
379async function loadSchema(schemaPointers, config) {
380 try {
381 const loaders = [
382 new CodeFileLoader(),
383 new GitLoader(),
384 new GithubLoader(),
385 new GraphQLFileLoader(),
386 new JsonFileLoader(),
387 new UrlLoader(),
388 new ApolloEngineLoader(),
389 new PrismaLoader(),
390 ];
391 const schema = await loadSchema$1(schemaPointers, {
392 ...defaultSchemaLoadOptions,
393 loaders,
394 ...config,
395 ...config.config,
396 });
397 return schema;
398 }
399 catch (e) {
400 throw new DetailedError('Failed to load schema', `
401 Failed to load schema from ${Object.keys(schemaPointers).join(',')}:
402
403 ${e.message || e}
404 ${e.stack || ''}
405
406 GraphQL Code Generator supports:
407 - ES Modules and CommonJS exports (export as default or named export "schema")
408 - Introspection JSON File
409 - URL of GraphQL endpoint
410 - Multiple files with type definitions (glob expression)
411 - String in config file
412
413 Try to use one of above options and run codegen again.
414
415 `);
416 }
417}
418async function loadDocuments(documentPointers, config) {
419 const loaders = [
420 new CodeFileLoader({
421 pluckConfig: {
422 skipIndent: true,
423 },
424 }),
425 new GitLoader(),
426 new GithubLoader(),
427 new GraphQLFileLoader(),
428 ];
429 const ignore = [];
430 for (const generatePath of Object.keys(config.generates)) {
431 if (extname(generatePath) === '') {
432 // we omit paths that don't resolve to a specific file
433 continue;
434 }
435 ignore.push(join(process.cwd(), generatePath));
436 }
437 const loadedFromToolkit = await loadDocuments$1(documentPointers, {
438 ...defaultDocumentsLoadOptions,
439 ignore,
440 loaders,
441 ...config,
442 ...config.config,
443 });
444 return loadedFromToolkit;
445}
446
447const { lstat } = promises;
448function generateSearchPlaces(moduleName) {
449 const extensions = ['json', 'yaml', 'yml', 'js', 'config.js'];
450 // gives codegen.json...
451 const regular = extensions.map(ext => `${moduleName}.${ext}`);
452 // gives .codegenrc.json... but no .codegenrc.config.js
453 const dot = extensions.filter(ext => ext !== 'config.js').map(ext => `.${moduleName}rc.${ext}`);
454 return [...regular.concat(dot), 'package.json'];
455}
456function customLoader(ext) {
457 function loader(filepath, content) {
458 if (typeof process !== 'undefined' && 'env' in process) {
459 content = env(content);
460 }
461 if (ext === 'json') {
462 return defaultLoaders['.json'](filepath, content);
463 }
464 if (ext === 'yaml') {
465 try {
466 const result = yaml.parse(content, { prettyErrors: true, merge: true });
467 return result;
468 }
469 catch (error) {
470 error.message = `YAML Error in ${filepath}:\n${error.message}`;
471 throw error;
472 }
473 }
474 if (ext === 'js') {
475 return defaultLoaders['.js'](filepath, content);
476 }
477 }
478 return loader;
479}
480async function loadCodegenConfig({ configFilePath, moduleName, searchPlaces: additionalSearchPlaces, packageProp, loaders: customLoaders, }) {
481 configFilePath = configFilePath || process.cwd();
482 moduleName = moduleName || 'codegen';
483 packageProp = packageProp || moduleName;
484 const cosmi = cosmiconfig(moduleName, {
485 searchPlaces: generateSearchPlaces(moduleName).concat(additionalSearchPlaces || []),
486 packageProp,
487 loaders: {
488 '.json': customLoader('json'),
489 '.yaml': customLoader('yaml'),
490 '.yml': customLoader('yaml'),
491 '.js': customLoader('js'),
492 noExt: customLoader('yaml'),
493 ...customLoaders,
494 },
495 });
496 const pathStats = await lstat(configFilePath);
497 return pathStats.isDirectory() ? cosmi.search(configFilePath) : cosmi.load(configFilePath);
498}
499async function loadContext(configFilePath) {
500 const graphqlConfig = await findAndLoadGraphQLConfig(configFilePath);
501 if (graphqlConfig) {
502 return new CodegenContext({
503 graphqlConfig,
504 });
505 }
506 const result = await loadCodegenConfig({ configFilePath });
507 if (!result) {
508 if (configFilePath) {
509 throw new DetailedError(`Config ${configFilePath} does not exist`, `
510 Config ${configFilePath} does not exist.
511
512 $ graphql-codegen --config ${configFilePath}
513
514 Please make sure the --config points to a correct file.
515 `);
516 }
517 throw new DetailedError(`Unable to find Codegen config file!`, `
518 Please make sure that you have a configuration file under the current directory!
519 `);
520 }
521 if (result.isEmpty) {
522 throw new DetailedError(`Found Codegen config file but it was empty!`, `
523 Please make sure that you have a valid configuration file under the current directory!
524 `);
525 }
526 return new CodegenContext({
527 filepath: result.filepath,
528 config: result.config,
529 });
530}
531function getCustomConfigPath(cliFlags) {
532 const configFile = cliFlags.config;
533 return configFile ? resolve(process.cwd(), configFile) : null;
534}
535function buildOptions() {
536 return {
537 c: {
538 alias: 'config',
539 type: 'string',
540 describe: 'Path to GraphQL codegen YAML config file, defaults to "codegen.yml" on the current directory',
541 },
542 w: {
543 alias: 'watch',
544 describe: 'Watch for changes and execute generation automatically. You can also specify a glob expreession for custom watch list.',
545 coerce: (watch) => {
546 if (watch === 'false') {
547 return false;
548 }
549 if (typeof watch === 'string' || Array.isArray(watch)) {
550 return watch;
551 }
552 return !!watch;
553 },
554 },
555 r: {
556 alias: 'require',
557 describe: 'Loads specific require.extensions before running the codegen and reading the configuration',
558 type: 'array',
559 default: [],
560 },
561 o: {
562 alias: 'overwrite',
563 describe: 'Overwrites existing files',
564 type: 'boolean',
565 },
566 s: {
567 alias: 'silent',
568 describe: 'Suppresses printing errors',
569 type: 'boolean',
570 },
571 e: {
572 alias: 'errors-only',
573 describe: 'Only print errors',
574 type: 'boolean',
575 },
576 profile: {
577 describe: 'Use profiler to measure performance',
578 type: 'boolean',
579 },
580 p: {
581 alias: 'project',
582 describe: 'Name of a project in GraphQL Config',
583 type: 'string',
584 },
585 };
586}
587function parseArgv(argv = process.argv) {
588 return yargs.options(buildOptions()).parse(argv);
589}
590async function createContext(cliFlags = parseArgv(process.argv)) {
591 if (cliFlags.require && cliFlags.require.length > 0) {
592 const relativeRequire = createRequire(process.cwd());
593 await Promise.all(cliFlags.require.map(mod => import(relativeRequire.resolve(mod, {
594 paths: [process.cwd()],
595 }))));
596 }
597 const customConfigPath = getCustomConfigPath(cliFlags);
598 const context = await loadContext(customConfigPath);
599 updateContextWithCliFlags(context, cliFlags);
600 return context;
601}
602function updateContextWithCliFlags(context, cliFlags) {
603 const config = {
604 configFilePath: context.filepath,
605 };
606 if (cliFlags.watch) {
607 config.watch = cliFlags.watch;
608 }
609 if (cliFlags.overwrite === true) {
610 config.overwrite = cliFlags.overwrite;
611 }
612 if (cliFlags.silent === true) {
613 config.silent = cliFlags.silent;
614 }
615 if (cliFlags.errorsOnly === true) {
616 config.errorsOnly = cliFlags.errorsOnly;
617 }
618 if (cliFlags.project) {
619 context.useProject(cliFlags.project);
620 }
621 if (cliFlags.profile === true) {
622 context.useProfiler();
623 }
624 context.updateConfig(config);
625}
626class CodegenContext {
627 constructor({ config, graphqlConfig, filepath, }) {
628 this._pluginContext = {};
629 this._config = config;
630 this._graphqlConfig = graphqlConfig;
631 this.filepath = this._graphqlConfig ? this._graphqlConfig.filepath : filepath;
632 this.cwd = this._graphqlConfig ? this._graphqlConfig.dirpath : process.cwd();
633 this.profiler = createNoopProfiler();
634 }
635 useProject(name) {
636 this._project = name;
637 }
638 getConfig(extraConfig) {
639 if (!this.config) {
640 if (this._graphqlConfig) {
641 const project = this._graphqlConfig.getProject(this._project);
642 this.config = {
643 ...project.extension('codegen'),
644 schema: project.schema,
645 documents: project.documents,
646 pluginContext: this._pluginContext,
647 };
648 }
649 else {
650 this.config = { ...this._config, pluginContext: this._pluginContext };
651 }
652 }
653 return {
654 ...extraConfig,
655 ...this.config,
656 };
657 }
658 updateConfig(config) {
659 this.config = {
660 ...this.getConfig(),
661 ...config,
662 };
663 }
664 useProfiler() {
665 this.profiler = createProfiler();
666 const now = new Date(); // 2011-10-05T14:48:00.000Z
667 const datetime = now.toISOString().split('.')[0]; // 2011-10-05T14:48:00
668 const datetimeNormalized = datetime.replace(/-|:/g, ''); // 20111005T144800
669 this.profilerOutput = `codegen-${datetimeNormalized}.json`;
670 }
671 getPluginContext() {
672 return this._pluginContext;
673 }
674 async loadSchema(pointer) {
675 const config = this.getConfig(defaultSchemaLoadOptions);
676 if (this._graphqlConfig) {
677 // TODO: SchemaWithLoader won't work here
678 return addHashToSchema(this._graphqlConfig.getProject(this._project).loadSchema(pointer, 'GraphQLSchema', config));
679 }
680 return addHashToSchema(loadSchema(pointer, config));
681 }
682 async loadDocuments(pointer) {
683 const config = this.getConfig(defaultDocumentsLoadOptions);
684 if (this._graphqlConfig) {
685 // TODO: pointer won't work here
686 return addHashToDocumentFiles(this._graphqlConfig.getProject(this._project).loadDocuments(pointer, config));
687 }
688 return addHashToDocumentFiles(loadDocuments(pointer, config));
689 }
690}
691function ensureContext(input) {
692 return input instanceof CodegenContext ? input : new CodegenContext({ config: input });
693}
694function hashContent(content) {
695 return createHash('sha256').update(content).digest('hex');
696}
697function hashSchema(schema) {
698 return hashContent(print(getCachedDocumentNodeFromSchema(schema)));
699}
700function addHashToSchema(schemaPromise) {
701 return schemaPromise.then(schema => {
702 // It's consumed later on. The general purpose is to use it for caching.
703 if (!schema.extensions) {
704 schema.extensions = {};
705 }
706 schema.extensions['hash'] = hashSchema(schema);
707 return schema;
708 });
709}
710function hashDocument(doc) {
711 if (doc.rawSDL) {
712 return hashContent(doc.rawSDL);
713 }
714 if (doc.document) {
715 return hashContent(print(doc.document));
716 }
717 return null;
718}
719function addHashToDocumentFiles(documentFilesPromise) {
720 return documentFilesPromise.then(documentFiles => documentFiles.map(doc => {
721 doc.hash = hashDocument(doc);
722 return doc;
723 }));
724}
725
726const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
727const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
728
729function isListrError(err) {
730 return err.name === 'ListrError' && Array.isArray(err.errors) && err.errors.length > 0;
731}
732function cliError(err, exitOnError = true) {
733 let msg;
734 if (err instanceof Error) {
735 msg = err.message || err.toString();
736 }
737 else if (typeof err === 'string') {
738 msg = err;
739 }
740 else {
741 msg = JSON.stringify(err);
742 }
743 // eslint-disable-next-line no-console
744 console.error(msg);
745 if (exitOnError && isNode) {
746 process.exit(1);
747 }
748 else if (exitOnError && isBrowser) {
749 throw err;
750 }
751}
752
753const makeDefaultLoader = (from) => {
754 if (fs.statSync(from).isDirectory()) {
755 from = path.join(from, '__fake.js');
756 }
757 const relativeRequire = createRequire(from);
758 return (mod) => {
759 return import(relativeRequire.resolve(mod));
760 };
761};
762function createCache() {
763 const cache = new Map();
764 return function ensure(namespace, key, factory) {
765 const cacheKey = `${namespace}:${key}`;
766 const cachedValue = cache.get(cacheKey);
767 if (cachedValue) {
768 return cachedValue;
769 }
770 const value = factory();
771 cache.set(cacheKey, value);
772 return value;
773 };
774}
775async function executeCodegen(input) {
776 const context = ensureContext(input);
777 const config = context.getConfig();
778 const pluginContext = context.getPluginContext();
779 const result = [];
780 const commonListrOptions = {
781 exitOnError: true,
782 };
783 let listr;
784 if (process.env.VERBOSE) {
785 listr = new Listr({
786 ...commonListrOptions,
787 renderer: 'verbose',
788 nonTTYRenderer: 'verbose',
789 });
790 }
791 else if (process.env.NODE_ENV === 'test') {
792 listr = new Listr({
793 ...commonListrOptions,
794 renderer: 'silent',
795 nonTTYRenderer: 'silent',
796 });
797 }
798 else {
799 listr = new Listr({
800 ...commonListrOptions,
801 renderer: config.silent ? 'silent' : config.errorsOnly ? ErrorRenderer : Renderer,
802 nonTTYRenderer: config.silent ? 'silent' : 'default',
803 collapse: true,
804 clearOutput: false,
805 });
806 }
807 let rootConfig = {};
808 let rootSchemas;
809 let rootDocuments;
810 const generates = {};
811 const cache = createCache();
812 function wrapTask(task, source, taskName) {
813 return () => {
814 return context.profiler.run(async () => {
815 try {
816 await Promise.resolve().then(() => task());
817 }
818 catch (error) {
819 if (source && !(error instanceof GraphQLError)) {
820 error.source = source;
821 }
822 throw error;
823 }
824 }, taskName);
825 };
826 }
827 async function normalize() {
828 /* Load Require extensions */
829 const requireExtensions = normalizeInstanceOrArray(config.require);
830 const loader = makeDefaultLoader(context.cwd);
831 for (const mod of requireExtensions) {
832 await loader(mod);
833 }
834 /* Root plugin config */
835 rootConfig = config.config || {};
836 /* Normalize root "schema" field */
837 rootSchemas = normalizeInstanceOrArray(config.schema);
838 /* Normalize root "documents" field */
839 rootDocuments = normalizeInstanceOrArray(config.documents);
840 /* Normalize "generators" field */
841 const generateKeys = Object.keys(config.generates || {});
842 if (generateKeys.length === 0) {
843 throw new DetailedError('Invalid Codegen Configuration!', `
844 Please make sure that your codegen config file contains the "generates" field, with a specification for the plugins you need.
845
846 It should looks like that:
847
848 schema:
849 - my-schema.graphql
850 generates:
851 my-file.ts:
852 - plugin1
853 - plugin2
854 - plugin3
855 `);
856 }
857 for (const filename of generateKeys) {
858 const output = (generates[filename] = normalizeOutputParam(config.generates[filename]));
859 if (!output.preset && (!output.plugins || output.plugins.length === 0)) {
860 throw new DetailedError('Invalid Codegen Configuration!', `
861 Please make sure that your codegen config file has defined plugins list for output "${filename}".
862
863 It should looks like that:
864
865 schema:
866 - my-schema.graphql
867 generates:
868 my-file.ts:
869 - plugin1
870 - plugin2
871 - plugin3
872 `);
873 }
874 }
875 if (rootSchemas.length === 0 &&
876 Object.keys(generates).some(filename => !generates[filename].schema || generates[filename].schema.length === 0)) {
877 throw new DetailedError('Invalid Codegen Configuration!', `
878 Please make sure that your codegen config file contains either the "schema" field
879 or every generated file has its own "schema" field.
880
881 It should looks like that:
882 schema:
883 - my-schema.graphql
884
885 or:
886 generates:
887 path/to/output:
888 schema: my-schema.graphql
889 `);
890 }
891 }
892 listr.add({
893 title: 'Parse configuration',
894 task: () => normalize(),
895 });
896 listr.add({
897 title: 'Generate outputs',
898 task: () => {
899 return new Listr(Object.keys(generates).map(filename => {
900 const outputConfig = generates[filename];
901 const hasPreset = !!outputConfig.preset;
902 return {
903 title: hasPreset
904 ? `Generate to ${filename} (using EXPERIMENTAL preset "${outputConfig.preset}")`
905 : `Generate ${filename}`,
906 task: () => {
907 let outputSchemaAst;
908 let outputSchema;
909 const outputFileTemplateConfig = outputConfig.config || {};
910 const outputDocuments = [];
911 const outputSpecificSchemas = normalizeInstanceOrArray(outputConfig.schema);
912 const outputSpecificDocuments = normalizeInstanceOrArray(outputConfig.documents);
913 return new Listr([
914 {
915 title: 'Load GraphQL schemas',
916 task: wrapTask(async () => {
917 debugLog(`[CLI] Loading Schemas`);
918 const schemaPointerMap = {};
919 const allSchemaUnnormalizedPointers = [...rootSchemas, ...outputSpecificSchemas];
920 for (const unnormalizedPtr of allSchemaUnnormalizedPointers) {
921 if (typeof unnormalizedPtr === 'string') {
922 schemaPointerMap[unnormalizedPtr] = {};
923 }
924 else if (typeof unnormalizedPtr === 'object') {
925 Object.assign(schemaPointerMap, unnormalizedPtr);
926 }
927 }
928 const hash = JSON.stringify(schemaPointerMap);
929 const result = await cache('schema', hash, async () => {
930 const outputSchemaAst = await context.loadSchema(schemaPointerMap);
931 const outputSchema = getCachedDocumentNodeFromSchema(outputSchemaAst);
932 return {
933 outputSchemaAst: outputSchemaAst,
934 outputSchema: outputSchema,
935 };
936 });
937 outputSchemaAst = await result.outputSchemaAst;
938 outputSchema = result.outputSchema;
939 }, filename, `Load GraphQL schemas: ${filename}`),
940 },
941 {
942 title: 'Load GraphQL documents',
943 task: wrapTask(async () => {
944 debugLog(`[CLI] Loading Documents`);
945 // get different cache for shared docs and output specific docs
946 const results = await Promise.all([rootDocuments, outputSpecificDocuments].map(docs => {
947 const hash = JSON.stringify(docs);
948 return cache('documents', hash, async () => {
949 const documents = await context.loadDocuments(docs);
950 return {
951 documents: documents,
952 };
953 });
954 }));
955 const documents = [];
956 results.forEach(source => documents.push(...source.documents));
957 if (documents.length > 0) {
958 outputDocuments.push(...documents);
959 }
960 }, filename, `Load GraphQL documents: ${filename}`),
961 },
962 {
963 title: 'Generate',
964 task: wrapTask(async () => {
965 debugLog(`[CLI] Generating output`);
966 const normalizedPluginsArray = normalizeConfig(outputConfig.plugins);
967 const pluginLoader = config.pluginLoader || makeDefaultLoader(context.cwd);
968 const pluginPackages = await Promise.all(normalizedPluginsArray.map(plugin => getPluginByName(Object.keys(plugin)[0], pluginLoader)));
969 const pluginMap = {};
970 const preset = hasPreset
971 ? typeof outputConfig.preset === 'string'
972 ? await getPresetByName(outputConfig.preset, makeDefaultLoader(context.cwd))
973 : outputConfig.preset
974 : null;
975 pluginPackages.forEach((pluginPackage, i) => {
976 const plugin = normalizedPluginsArray[i];
977 const name = Object.keys(plugin)[0];
978 pluginMap[name] = pluginPackage;
979 });
980 const mergedConfig = {
981 ...rootConfig,
982 ...(typeof outputFileTemplateConfig === 'string'
983 ? { value: outputFileTemplateConfig }
984 : outputFileTemplateConfig),
985 };
986 let outputs = [];
987 if (hasPreset) {
988 outputs = await context.profiler.run(async () => preset.buildGeneratesSection({
989 baseOutputDir: filename,
990 presetConfig: outputConfig.presetConfig || {},
991 plugins: normalizedPluginsArray,
992 schema: outputSchema,
993 schemaAst: outputSchemaAst,
994 documents: outputDocuments,
995 config: mergedConfig,
996 pluginMap,
997 pluginContext,
998 profiler: context.profiler,
999 }), `Build Generates Section: ${filename}`);
1000 }
1001 else {
1002 outputs = [
1003 {
1004 filename,
1005 plugins: normalizedPluginsArray,
1006 schema: outputSchema,
1007 schemaAst: outputSchemaAst,
1008 documents: outputDocuments,
1009 config: mergedConfig,
1010 pluginMap,
1011 pluginContext,
1012 profiler: context.profiler,
1013 },
1014 ];
1015 }
1016 const process = async (outputArgs) => {
1017 const output = await codegen({
1018 ...outputArgs,
1019 cache,
1020 });
1021 result.push({
1022 filename: outputArgs.filename,
1023 content: output,
1024 hooks: outputConfig.hooks || {},
1025 });
1026 };
1027 await context.profiler.run(() => Promise.all(outputs.map(process)), `Codegen: ${filename}`);
1028 }, filename, `Generate: ${filename}`),
1029 },
1030 ], {
1031 // it stops when one of tasks failed
1032 exitOnError: true,
1033 });
1034 },
1035 };
1036 }), {
1037 // it doesn't stop when one of tasks failed, to finish at least some of outputs
1038 exitOnError: false,
1039 concurrent: cpus().length,
1040 });
1041 },
1042 });
1043 try {
1044 await listr.run();
1045 }
1046 catch (err) {
1047 if (isListrError(err)) {
1048 const allErrs = err.errors.map(subErr => isDetailedError(subErr)
1049 ? `${subErr.message} for "${subErr.source}"${subErr.details}`
1050 : subErr.message || subErr.toString());
1051 const newErr = new AggregateError(err.errors, `${err.message} ${allErrs.join('\n\n')}`);
1052 // Best-effort to all stack traces for debugging
1053 newErr.stack = `${newErr.stack}\n\n${err.errors.map(subErr => subErr.stack).join('\n\n')}`;
1054 throw newErr;
1055 }
1056 throw err;
1057 }
1058 return result;
1059}
1060
1061const DEFAULT_HOOKS = {
1062 afterStart: [],
1063 beforeDone: [],
1064 onWatchTriggered: [],
1065 onError: [],
1066 afterOneFileWrite: [],
1067 afterAllFileWrite: [],
1068 beforeOneFileWrite: [],
1069 beforeAllFileWrite: [],
1070};
1071function normalizeHooks(_hooks) {
1072 const keys = Object.keys({
1073 ...DEFAULT_HOOKS,
1074 ...(_hooks || {}),
1075 });
1076 return keys.reduce((prev, hookName) => {
1077 if (typeof _hooks[hookName] === 'string') {
1078 return {
1079 ...prev,
1080 [hookName]: [_hooks[hookName]],
1081 };
1082 }
1083 else if (typeof _hooks[hookName] === 'function') {
1084 return {
1085 ...prev,
1086 [hookName]: [_hooks[hookName]],
1087 };
1088 }
1089 else if (Array.isArray(_hooks[hookName])) {
1090 return {
1091 ...prev,
1092 [hookName]: _hooks[hookName],
1093 };
1094 }
1095 else {
1096 return prev;
1097 }
1098 }, {});
1099}
1100function execShellCommand(cmd) {
1101 return new Promise((resolve, reject) => {
1102 exec(cmd, {
1103 env: {
1104 ...process.env,
1105 PATH: `${process.env.PATH}${delimiter}${process.cwd()}${sep}node_modules${sep}.bin`,
1106 },
1107 }, (error, stdout, stderr) => {
1108 if (error) {
1109 reject(error);
1110 }
1111 else {
1112 resolve(stdout || stderr);
1113 }
1114 });
1115 });
1116}
1117async function executeHooks(hookName, scripts = [], args = []) {
1118 debugLog(`Running lifecycle hook "${hookName}" scripts...`);
1119 for (const script of scripts) {
1120 if (typeof script === 'string') {
1121 debugLog(`Running lifecycle hook "${hookName}" script: ${script} with args: ${args.join(' ')}...`);
1122 await execShellCommand(`${script} ${args.join(' ')}`);
1123 }
1124 else {
1125 debugLog(`Running lifecycle hook "${hookName}" script: ${script.name} with args: ${args.join(' ')}...`);
1126 await script(...args);
1127 }
1128 }
1129}
1130const lifecycleHooks = (_hooks = {}) => {
1131 const hooks = normalizeHooks(_hooks);
1132 return {
1133 afterStart: async () => executeHooks('afterStart', hooks.afterStart),
1134 onWatchTriggered: async (event, path) => executeHooks('onWatchTriggered', hooks.onWatchTriggered, [event, path]),
1135 onError: async (error) => executeHooks('onError', hooks.onError, [`"${error}"`]),
1136 afterOneFileWrite: async (path) => executeHooks('afterOneFileWrite', hooks.afterOneFileWrite, [path]),
1137 afterAllFileWrite: async (paths) => executeHooks('afterAllFileWrite', hooks.afterAllFileWrite, paths),
1138 beforeOneFileWrite: async (path) => executeHooks('beforeOneFileWrite', hooks.beforeOneFileWrite, [path]),
1139 beforeAllFileWrite: async (paths) => executeHooks('beforeAllFileWrite', hooks.beforeAllFileWrite, paths),
1140 beforeDone: async () => executeHooks('beforeDone', hooks.beforeDone),
1141 };
1142};
1143
1144function log(msg) {
1145 // double spaces to inline the message with Listr
1146 getLogger().info(` ${msg}`);
1147}
1148function emitWatching() {
1149 log(`${logSymbols.info} Watching for changes...`);
1150}
1151const createWatcher = (initalContext, onNext) => {
1152 debugLog(`[Watcher] Starting watcher...`);
1153 let config = initalContext.getConfig();
1154 const files = [initalContext.filepath].filter(a => a);
1155 const documents = normalizeInstanceOrArray(config.documents);
1156 const schemas = normalizeInstanceOrArray(config.schema);
1157 // Add schemas and documents from "generates"
1158 Object.keys(config.generates)
1159 .map(filename => normalizeOutputParam(config.generates[filename]))
1160 .forEach(conf => {
1161 schemas.push(...normalizeInstanceOrArray(conf.schema));
1162 documents.push(...normalizeInstanceOrArray(conf.documents));
1163 });
1164 if (documents) {
1165 documents.forEach(doc => {
1166 if (typeof doc === 'string') {
1167 files.push(doc);
1168 }
1169 else {
1170 files.push(...Object.keys(doc));
1171 }
1172 });
1173 }
1174 schemas.forEach((schema) => {
1175 if (isGlob(schema) || isValidPath(schema)) {
1176 files.push(schema);
1177 }
1178 });
1179 if (typeof config.watch !== 'boolean') {
1180 files.push(...normalizeInstanceOrArray(config.watch));
1181 }
1182 let watcher;
1183 const runWatcher = async () => {
1184 var _a, _b;
1185 const chokidar = await import('chokidar');
1186 let isShutdown = false;
1187 const debouncedExec = debounce(() => {
1188 if (!isShutdown) {
1189 executeCodegen(initalContext)
1190 .then(onNext, () => Promise.resolve())
1191 .then(() => emitWatching());
1192 }
1193 }, 100);
1194 emitWatching();
1195 const ignored = [];
1196 Object.keys(config.generates)
1197 .map(filename => ({ filename, config: normalizeOutputParam(config.generates[filename]) }))
1198 .forEach(entry => {
1199 if (entry.config.preset) {
1200 const extension = entry.config.presetConfig && entry.config.presetConfig.extension;
1201 if (extension) {
1202 ignored.push(join(entry.filename, '**', '*' + extension));
1203 }
1204 }
1205 else {
1206 ignored.push(entry.filename);
1207 }
1208 });
1209 watcher = chokidar.watch(files, {
1210 persistent: true,
1211 ignoreInitial: true,
1212 followSymlinks: true,
1213 cwd: process.cwd(),
1214 disableGlobbing: false,
1215 usePolling: (_a = config.watchConfig) === null || _a === void 0 ? void 0 : _a.usePolling,
1216 interval: (_b = config.watchConfig) === null || _b === void 0 ? void 0 : _b.interval,
1217 depth: 99,
1218 awaitWriteFinish: true,
1219 ignorePermissionErrors: false,
1220 atomic: true,
1221 ignored,
1222 });
1223 debugLog(`[Watcher] Started`);
1224 const shutdown = () => {
1225 isShutdown = true;
1226 debugLog(`[Watcher] Shutting down`);
1227 log(`Shutting down watch...`);
1228 watcher.close();
1229 lifecycleHooks(config.hooks).beforeDone();
1230 };
1231 // it doesn't matter what has changed, need to run whole process anyway
1232 watcher.on('all', async (eventName, path) => {
1233 lifecycleHooks(config.hooks).onWatchTriggered(eventName, path);
1234 debugLog(`[Watcher] triggered due to a file ${eventName} event: ${path}`);
1235 const fullPath = join(process.cwd(), path);
1236 // In ESM require is not defined
1237 try {
1238 delete require.cache[fullPath];
1239 }
1240 catch (err) { }
1241 if (eventName === 'change' && config.configFilePath && fullPath === config.configFilePath) {
1242 log(`${logSymbols.info} Config file has changed, reloading...`);
1243 const context = await loadContext(config.configFilePath);
1244 const newParsedConfig = context.getConfig();
1245 newParsedConfig.watch = config.watch;
1246 newParsedConfig.silent = config.silent;
1247 newParsedConfig.overwrite = config.overwrite;
1248 newParsedConfig.configFilePath = config.configFilePath;
1249 config = newParsedConfig;
1250 initalContext.updateConfig(config);
1251 }
1252 debouncedExec();
1253 });
1254 process.once('SIGINT', shutdown);
1255 process.once('SIGTERM', shutdown);
1256 };
1257 // the promise never resolves to keep process running
1258 return new Promise((resolve, reject) => {
1259 executeCodegen(initalContext)
1260 .then(onNext, () => Promise.resolve())
1261 .then(runWatcher)
1262 .catch(err => {
1263 watcher.close();
1264 reject(err);
1265 });
1266 });
1267};
1268
1269const { writeFile: fsWriteFile, readFile: fsReadFile, stat: fsStat } = promises;
1270function writeFile(filepath, content) {
1271 return fsWriteFile(filepath, content);
1272}
1273function readFile(filepath) {
1274 return fsReadFile(filepath, 'utf-8');
1275}
1276async function fileExists(filePath) {
1277 try {
1278 return (await fsStat(filePath)).isFile();
1279 }
1280 catch (err) {
1281 return false;
1282 }
1283}
1284function unlinkFile(filePath, cb) {
1285 unlink(filePath, cb);
1286}
1287
1288const hash = (content) => createHash('sha1').update(content).digest('base64');
1289async function generate(input, saveToFile = true) {
1290 const context = ensureContext(input);
1291 const config = context.getConfig();
1292 await context.profiler.run(() => lifecycleHooks(config.hooks).afterStart(), 'Lifecycle: afterStart');
1293 let previouslyGeneratedFilenames = [];
1294 function removeStaleFiles(config, generationResult) {
1295 const filenames = generationResult.map(o => o.filename);
1296 // find stale files from previous build which are not present in current build
1297 const staleFilenames = previouslyGeneratedFilenames.filter(f => !filenames.includes(f));
1298 staleFilenames.forEach(filename => {
1299 if (shouldOverwrite(config, filename)) {
1300 return unlinkFile(filename, err => {
1301 const prettyFilename = filename.replace(`${input.cwd || process.cwd()}/`, '');
1302 if (err) {
1303 debugLog(`Cannot remove stale file: ${prettyFilename}\n${err}`);
1304 }
1305 else {
1306 debugLog(`Removed stale file: ${prettyFilename}`);
1307 }
1308 });
1309 }
1310 });
1311 previouslyGeneratedFilenames = filenames;
1312 }
1313 const recentOutputHash = new Map();
1314 async function writeOutput(generationResult) {
1315 if (!saveToFile) {
1316 return generationResult;
1317 }
1318 if (config.watch) {
1319 removeStaleFiles(config, generationResult);
1320 }
1321 await context.profiler.run(async () => {
1322 await lifecycleHooks(config.hooks).beforeAllFileWrite(generationResult.map(r => r.filename));
1323 }, 'Lifecycle: beforeAllFileWrite');
1324 await context.profiler.run(() => Promise.all(generationResult.map(async (result) => {
1325 const exists = await fileExists(result.filename);
1326 if (!shouldOverwrite(config, result.filename) && exists) {
1327 return;
1328 }
1329 const content = result.content || '';
1330 const currentHash = hash(content);
1331 let previousHash = recentOutputHash.get(result.filename);
1332 if (!previousHash && exists) {
1333 previousHash = hash(await readFile(result.filename));
1334 }
1335 if (previousHash && currentHash === previousHash) {
1336 debugLog(`Skipping file (${result.filename}) writing due to indentical hash...`);
1337 return;
1338 }
1339 if (content.length === 0) {
1340 return;
1341 }
1342 recentOutputHash.set(result.filename, currentHash);
1343 const basedir = dirname(result.filename);
1344 await lifecycleHooks(result.hooks).beforeOneFileWrite(result.filename);
1345 await lifecycleHooks(config.hooks).beforeOneFileWrite(result.filename);
1346 await mkdirp(basedir);
1347 const absolutePath = isAbsolute(result.filename)
1348 ? result.filename
1349 : join(input.cwd || process.cwd(), result.filename);
1350 await writeFile(absolutePath, result.content);
1351 await lifecycleHooks(result.hooks).afterOneFileWrite(result.filename);
1352 await lifecycleHooks(config.hooks).afterOneFileWrite(result.filename);
1353 })), 'Write files');
1354 await context.profiler.run(() => lifecycleHooks(config.hooks).afterAllFileWrite(generationResult.map(r => r.filename)), 'Lifecycle: afterAllFileWrite');
1355 return generationResult;
1356 }
1357 // watch mode
1358 if (config.watch) {
1359 return createWatcher(context, writeOutput);
1360 }
1361 const outputFiles = await context.profiler.run(() => executeCodegen(context), 'executeCodegen');
1362 await context.profiler.run(() => writeOutput(outputFiles), 'writeOutput');
1363 await context.profiler.run(() => lifecycleHooks(config.hooks).beforeDone(), 'Lifecycle: beforeDone');
1364 if (context.profilerOutput) {
1365 await writeFile(join(context.cwd, context.profilerOutput), JSON.stringify(context.profiler.collect()));
1366 }
1367 return outputFiles;
1368}
1369function shouldOverwrite(config, outputPath) {
1370 const globalValue = config.overwrite === undefined ? true : !!config.overwrite;
1371 const outputConfig = config.generates[outputPath];
1372 if (!outputConfig) {
1373 debugLog(`Couldn't find a config of ${outputPath}`);
1374 return globalValue;
1375 }
1376 if (isConfiguredOutput(outputConfig) && typeof outputConfig.overwrite === 'boolean') {
1377 return outputConfig.overwrite;
1378 }
1379 return globalValue;
1380}
1381function isConfiguredOutput(output) {
1382 return typeof output.plugins !== 'undefined';
1383}
1384
1385// Parses config and writes it to a file
1386async function writeConfig(answers, config) {
1387 const YAML = await import('json-to-pretty-yaml').then(m => ('default' in m ? m.default : m));
1388 const ext = answers.config.toLocaleLowerCase().endsWith('.json') ? 'json' : 'yml';
1389 const content = ext === 'json' ? JSON.stringify(config) : YAML.stringify(config);
1390 const fullPath = resolve(process.cwd(), answers.config);
1391 const relativePath = relative(process.cwd(), answers.config);
1392 writeFileSync(fullPath, content, {
1393 encoding: 'utf-8',
1394 });
1395 return {
1396 relativePath,
1397 fullPath,
1398 };
1399}
1400// Updates package.json (script and plugins as dependencies)
1401async function writePackage(answers, configLocation) {
1402 // script
1403 const pkgPath = resolve(process.cwd(), 'package.json');
1404 const pkgContent = readFileSync(pkgPath, {
1405 encoding: 'utf-8',
1406 });
1407 const pkg = JSON.parse(pkgContent);
1408 const { indent } = detectIndent(pkgContent);
1409 if (!pkg.scripts) {
1410 pkg.scripts = {};
1411 }
1412 pkg.scripts[answers.script] = `graphql-codegen --config ${configLocation}`;
1413 // plugin
1414 if (!pkg.devDependencies) {
1415 pkg.devDependencies = {};
1416 }
1417 await Promise.all(answers.plugins.map(async (plugin) => {
1418 pkg.devDependencies[plugin.package] = await getLatestVersion(plugin.package);
1419 }));
1420 if (answers.introspection) {
1421 pkg.devDependencies['@graphql-codegen/introspection'] = await getLatestVersion('@graphql-codegen/introspection');
1422 }
1423 pkg.devDependencies['@graphql-codegen/cli'] = await getLatestVersion('@graphql-codegen/cli');
1424 writeFileSync(pkgPath, JSON.stringify(pkg, null, indent));
1425}
1426function bold(str) {
1427 return chalk.bold(str);
1428}
1429function grey(str) {
1430 return chalk.grey(str);
1431}
1432function italic(str) {
1433 return chalk.italic(str);
1434}
1435
1436var Tags;
1437(function (Tags) {
1438 Tags["browser"] = "Browser";
1439 Tags["node"] = "Node";
1440 Tags["typescript"] = "TypeScript";
1441 Tags["flow"] = "Flow";
1442 Tags["angular"] = "Angular";
1443 Tags["stencil"] = "Stencil";
1444 Tags["react"] = "React";
1445 Tags["vue"] = "Vue";
1446})(Tags || (Tags = {}));
1447
1448const plugins = [
1449 {
1450 name: `TypeScript ${italic('(required by other typescript plugins)')}`,
1451 package: '@graphql-codegen/typescript',
1452 value: 'typescript',
1453 pathInRepo: 'typescript/typescript',
1454 available: hasTag(Tags.typescript),
1455 shouldBeSelected: tags => oneOf(tags, Tags.angular, Tags.stencil) || allOf(tags, Tags.typescript, Tags.react) || noneOf(tags, Tags.flow),
1456 defaultExtension: '.ts',
1457 },
1458 {
1459 name: `TypeScript Operations ${italic('(operations and fragments)')}`,
1460 package: '@graphql-codegen/typescript-operations',
1461 value: 'typescript-operations',
1462 pathInRepo: 'typescript/operations',
1463 available: tags => allOf(tags, Tags.browser, Tags.typescript),
1464 shouldBeSelected: tags => oneOf(tags, Tags.angular, Tags.stencil) || allOf(tags, Tags.typescript, Tags.react),
1465 defaultExtension: '.ts',
1466 },
1467 {
1468 name: `TypeScript Resolvers ${italic('(strongly typed resolve functions)')}`,
1469 package: '@graphql-codegen/typescript-resolvers',
1470 value: 'typescript-resolvers',
1471 pathInRepo: 'typescript/resolvers',
1472 available: tags => allOf(tags, Tags.node, Tags.typescript),
1473 shouldBeSelected: tags => noneOf(tags, Tags.flow),
1474 defaultExtension: '.ts',
1475 },
1476 {
1477 name: `Flow ${italic('(required by other flow plugins)')}`,
1478 package: '@graphql-codegen/flow',
1479 value: 'flow',
1480 pathInRepo: 'flow/flow',
1481 available: hasTag(Tags.flow),
1482 shouldBeSelected: tags => noneOf(tags, Tags.typescript),
1483 defaultExtension: '.js',
1484 },
1485 {
1486 name: `Flow Operations ${italic('(operations and fragments)')}`,
1487 package: '@graphql-codegen/flow-operations',
1488 value: 'flow-operations',
1489 pathInRepo: 'flow/operations',
1490 available: tags => allOf(tags, Tags.browser, Tags.flow),
1491 shouldBeSelected: tags => noneOf(tags, Tags.typescript),
1492 defaultExtension: '.js',
1493 },
1494 {
1495 name: `Flow Resolvers ${italic('(strongly typed resolve functions)')}`,
1496 package: '@graphql-codegen/flow-resolvers',
1497 value: 'flow-resolvers',
1498 pathInRepo: 'flow/resolvers',
1499 available: tags => allOf(tags, Tags.node, Tags.flow),
1500 shouldBeSelected: tags => noneOf(tags, Tags.typescript),
1501 defaultExtension: '.js',
1502 },
1503 {
1504 name: `TypeScript Apollo Angular ${italic('(typed GQL services)')}`,
1505 package: '@graphql-codegen/typescript-apollo-angular',
1506 value: 'typescript-apollo-angular',
1507 pathInRepo: 'typescript/apollo-angular',
1508 available: hasTag(Tags.angular),
1509 shouldBeSelected: () => true,
1510 defaultExtension: '.js',
1511 },
1512 {
1513 name: `TypeScript Vue Apollo Composition API ${italic('(typed functions)')}`,
1514 package: '@graphql-codegen/typescript-vue-apollo',
1515 value: 'typescript-vue-apollo',
1516 pathInRepo: 'typescript/vue-apollo',
1517 available: tags => allOf(tags, Tags.vue, Tags.typescript),
1518 shouldBeSelected: () => true,
1519 defaultExtension: '.ts',
1520 },
1521 {
1522 name: `TypeScript Vue Apollo Smart Operations ${italic('(typed functions)')}`,
1523 package: '@graphql-codegen/typescript-vue-apollo-smart-ops',
1524 value: 'typescript-vue-apollo-smart-ops',
1525 pathInRepo: 'typescript/vue-apollo-smart-ops',
1526 available: tags => allOf(tags, Tags.vue, Tags.typescript),
1527 shouldBeSelected: () => true,
1528 defaultExtension: '.ts',
1529 },
1530 {
1531 name: `TypeScript React Apollo ${italic('(typed components and HOCs)')}`,
1532 package: '@graphql-codegen/typescript-react-apollo',
1533 value: 'typescript-react-apollo',
1534 pathInRepo: 'typescript/react-apollo',
1535 available: tags => allOf(tags, Tags.react, Tags.typescript),
1536 shouldBeSelected: () => true,
1537 defaultExtension: '.tsx',
1538 },
1539 {
1540 name: `TypeScript Stencil Apollo ${italic('(typed components)')}`,
1541 package: '@graphql-codegen/typescript-stencil-apollo',
1542 value: 'typescript-stencil-apollo',
1543 pathInRepo: 'typescript/stencil-apollo',
1544 available: hasTag(Tags.stencil),
1545 shouldBeSelected: () => true,
1546 defaultExtension: '.tsx',
1547 },
1548 {
1549 name: `TypeScript MongoDB ${italic('(typed MongoDB objects)')}`,
1550 package: '@graphql-codegen/typescript-mongodb',
1551 value: 'typescript-mongodb',
1552 pathInRepo: 'typescript/mongodb',
1553 available: tags => allOf(tags, Tags.node, Tags.typescript),
1554 shouldBeSelected: () => false,
1555 defaultExtension: '.ts',
1556 },
1557 {
1558 name: `TypeScript GraphQL files modules ${italic('(declarations for .graphql files)')}`,
1559 package: '@graphql-codegen/typescript-graphql-files-modules',
1560 value: 'typescript-graphql-files-modules',
1561 pathInRepo: 'typescript/graphql-files-modules',
1562 available: tags => allOf(tags, Tags.browser, Tags.typescript),
1563 shouldBeSelected: () => false,
1564 defaultExtension: '.ts',
1565 },
1566 {
1567 name: `TypeScript GraphQL document nodes ${italic('(embedded GraphQL document)')}`,
1568 package: '@graphql-codegen/typescript-document-nodes',
1569 value: 'typescript-document-nodes',
1570 pathInRepo: 'typescript/document-nodes',
1571 available: tags => allOf(tags, Tags.typescript),
1572 shouldBeSelected: () => false,
1573 defaultExtension: '.ts',
1574 },
1575 {
1576 name: `Introspection Fragment Matcher ${italic('(for Apollo Client)')}`,
1577 package: '@graphql-codegen/fragment-matcher',
1578 value: 'fragment-matcher',
1579 pathInRepo: 'other/fragment-matcher',
1580 available: hasTag(Tags.browser),
1581 shouldBeSelected: () => false,
1582 defaultExtension: '.ts',
1583 },
1584 {
1585 name: `Urql Introspection ${italic('(for Urql Client)')}`,
1586 package: '@graphql-codegen/urql-introspection',
1587 value: 'urql-introspection',
1588 pathInRepo: 'other/urql-introspection',
1589 available: hasTag(Tags.browser),
1590 shouldBeSelected: () => false,
1591 defaultExtension: '.ts',
1592 },
1593];
1594function hasTag(tag) {
1595 return (tags) => tags.includes(tag);
1596}
1597function oneOf(list, ...items) {
1598 return list.some(i => items.includes(i));
1599}
1600function noneOf(list, ...items) {
1601 return !list.some(i => items.includes(i));
1602}
1603function allOf(list, ...items) {
1604 return items.every(i => list.includes(i));
1605}
1606
1607function getQuestions(possibleTargets) {
1608 return [
1609 {
1610 type: 'checkbox',
1611 name: 'targets',
1612 message: `What type of application are you building?`,
1613 choices: getApplicationTypeChoices(possibleTargets),
1614 validate: ((targets) => targets.length > 0),
1615 },
1616 {
1617 type: 'input',
1618 name: 'schema',
1619 message: 'Where is your schema?:',
1620 suffix: grey(' (path or url)'),
1621 default: 'http://localhost:4000',
1622 validate: (str) => str.length > 0,
1623 },
1624 {
1625 type: 'input',
1626 name: 'documents',
1627 message: 'Where are your operations and fragments?:',
1628 when: answers => {
1629 // flatten targets
1630 // I can't find an API in Inquirer that would do that
1631 answers.targets = normalizeTargets(answers.targets);
1632 return answers.targets.includes(Tags.browser);
1633 },
1634 default: 'src/**/*.graphql',
1635 validate: (str) => str.length > 0,
1636 },
1637 {
1638 type: 'checkbox',
1639 name: 'plugins',
1640 message: 'Pick plugins:',
1641 choices: getPluginChoices,
1642 validate: ((plugins) => plugins.length > 0),
1643 },
1644 {
1645 type: 'input',
1646 name: 'output',
1647 message: 'Where to write the output:',
1648 default: getOutputDefaultValue,
1649 validate: (str) => str.length > 0,
1650 },
1651 {
1652 type: 'confirm',
1653 name: 'introspection',
1654 message: 'Do you want to generate an introspection file?',
1655 },
1656 {
1657 type: 'input',
1658 name: 'config',
1659 message: 'How to name the config file?',
1660 default: 'codegen.yml',
1661 validate: (str) => {
1662 const isNotEmpty = str.length > 0;
1663 const hasCorrectExtension = ['json', 'yml', 'yaml'].some(ext => str.toLocaleLowerCase().endsWith(`.${ext}`));
1664 return isNotEmpty && hasCorrectExtension;
1665 },
1666 },
1667 {
1668 type: 'input',
1669 name: 'script',
1670 message: 'What script in package.json should run the codegen?',
1671 validate: (str) => str.length > 0,
1672 },
1673 ];
1674}
1675function getApplicationTypeChoices(possibleTargets) {
1676 function withFlowOrTypescript(tags) {
1677 if (possibleTargets.TypeScript) {
1678 tags.push(Tags.typescript);
1679 }
1680 else if (possibleTargets.Flow) {
1681 tags.push(Tags.flow);
1682 }
1683 else {
1684 tags.push(Tags.flow, Tags.typescript);
1685 }
1686 return tags;
1687 }
1688 return [
1689 {
1690 name: 'Backend - API or server',
1691 key: 'backend',
1692 value: withFlowOrTypescript([Tags.node]),
1693 checked: possibleTargets.Node,
1694 },
1695 {
1696 name: 'Application built with Angular',
1697 key: 'angular',
1698 value: [Tags.angular, Tags.browser, Tags.typescript],
1699 checked: possibleTargets.Angular,
1700 },
1701 {
1702 name: 'Application built with React',
1703 key: 'react',
1704 value: withFlowOrTypescript([Tags.react, Tags.browser]),
1705 checked: possibleTargets.React,
1706 },
1707 {
1708 name: 'Application built with Stencil',
1709 key: 'stencil',
1710 value: [Tags.stencil, Tags.browser, Tags.typescript],
1711 checked: possibleTargets.Stencil,
1712 },
1713 {
1714 name: 'Application built with other framework or vanilla JS',
1715 key: 'client',
1716 value: [Tags.browser, Tags.typescript, Tags.flow],
1717 checked: possibleTargets.Browser && !possibleTargets.Angular && !possibleTargets.React && !possibleTargets.Stencil,
1718 },
1719 ];
1720}
1721function getPluginChoices(answers) {
1722 return plugins
1723 .filter(p => p.available(answers.targets))
1724 .map(p => {
1725 return {
1726 name: p.name,
1727 value: p,
1728 checked: p.shouldBeSelected(answers.targets),
1729 };
1730 });
1731}
1732function normalizeTargets(targets) {
1733 return [].concat(...targets);
1734}
1735function getOutputDefaultValue(answers) {
1736 if (answers.plugins.some(plugin => plugin.defaultExtension === '.tsx')) {
1737 return 'src/generated/graphql.tsx';
1738 }
1739 else if (answers.plugins.some(plugin => plugin.defaultExtension === '.ts')) {
1740 return 'src/generated/graphql.ts';
1741 }
1742 else {
1743 return 'src/generated/graphql.js';
1744 }
1745}
1746
1747async function guessTargets() {
1748 const pkg = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), {
1749 encoding: 'utf-8',
1750 }));
1751 const dependencies = Object.keys({
1752 ...pkg.dependencies,
1753 ...pkg.devDependencies,
1754 });
1755 return {
1756 [Tags.angular]: isAngular(dependencies),
1757 [Tags.react]: isReact(dependencies),
1758 [Tags.stencil]: isStencil(dependencies),
1759 [Tags.vue]: isVue(dependencies),
1760 [Tags.browser]: false,
1761 [Tags.node]: false,
1762 [Tags.typescript]: isTypescript(dependencies),
1763 [Tags.flow]: isFlow(dependencies),
1764 };
1765}
1766function isAngular(dependencies) {
1767 return dependencies.includes('@angular/core');
1768}
1769function isReact(dependencies) {
1770 return dependencies.includes('react');
1771}
1772function isStencil(dependencies) {
1773 return dependencies.includes('@stencil/core');
1774}
1775function isVue(dependencies) {
1776 return dependencies.includes('vue') || dependencies.includes('nuxt');
1777}
1778function isTypescript(dependencies) {
1779 return dependencies.includes('typescript');
1780}
1781function isFlow(dependencies) {
1782 return dependencies.includes('flow');
1783}
1784
1785function log$1(...msgs) {
1786 // eslint-disable-next-line no-console
1787 console.log(...msgs);
1788}
1789async function init() {
1790 log$1(`
1791 Welcome to ${bold('GraphQL Code Generator')}!
1792 Answer few questions and we will setup everything for you.
1793 `);
1794 const possibleTargets = await guessTargets();
1795 const answers = await inquirer.prompt(getQuestions(possibleTargets));
1796 // define config
1797 const config = {
1798 overwrite: true,
1799 schema: answers.schema,
1800 documents: answers.targets.includes(Tags.browser) ? answers.documents : null,
1801 generates: {
1802 [answers.output]: {
1803 plugins: answers.plugins.map(p => p.value),
1804 },
1805 },
1806 };
1807 // introspection
1808 if (answers.introspection) {
1809 addIntrospection(config);
1810 }
1811 // config file
1812 const { relativePath } = await writeConfig(answers, config);
1813 log$1(`Fetching latest versions of selected plugins...`);
1814 // write package.json
1815 await writePackage(answers, relativePath);
1816 // Emit status to the terminal
1817 log$1(`
1818 Config file generated at ${bold(relativePath)}
1819
1820 ${bold('$ npm install')}
1821
1822 To install the plugins.
1823
1824 ${bold(`$ npm run ${answers.script}`)}
1825
1826 To run GraphQL Code Generator.
1827 `);
1828}
1829// adds an introspection to `generates`
1830function addIntrospection(config) {
1831 config.generates['./graphql.schema.json'] = {
1832 plugins: ['introspection'],
1833 };
1834}
1835
1836async function runCli(cmd) {
1837 await ensureGraphQlPackage();
1838 switch (cmd) {
1839 case 'init':
1840 return init();
1841 default: {
1842 return createContext().then(context => {
1843 return generate(context).catch(async (error) => {
1844 await lifecycleHooks(context.getConfig().hooks).onError(error.toString());
1845 throw error;
1846 });
1847 });
1848 }
1849 }
1850}
1851async function ensureGraphQlPackage() {
1852 try {
1853 await import('graphql');
1854 }
1855 catch (e) {
1856 throw new DetailedError(`Unable to load "graphql" package. Please make sure to install "graphql" as a dependency!`, `
1857 To install "graphql", run:
1858 yarn add graphql
1859 Or, with NPM:
1860 npm install --save graphql
1861`);
1862 }
1863}
1864
1865export { CodegenContext, CodegenExtension, buildOptions, cliError, createContext, ensureContext, ensureGraphQlPackage, executeCodegen, findAndLoadGraphQLConfig, generate, generateSearchPlaces, init, isListrError, loadCodegenConfig, loadContext, parseArgv, runCli, updateContextWithCliFlags };