UNPKG

@graphql-codegen/cli

Version:

<p align="center"> <img src="https://github.com/dotansimha/graphql-code-generator/blob/master/logo.png?raw=true" /> </p>

1,315 lines (1,282 loc) 71.5 kB
#!/usr/bin/env node 'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } function _interopNamespace(e) { if (e && e.__esModule) { return e; } else { var n = {}; if (e) { Object.keys(e).forEach(function (k) { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); }); } n['default'] = e; return n; } } const tsLog = require('ts-log'); const child_process = require('child_process'); const path = require('path'); const path__default = _interopDefault(path); const pluginHelpers = require('@graphql-codegen/plugin-helpers'); const core = require('@graphql-codegen/core'); const utils = require('@graphql-tools/utils'); const chalk = _interopDefault(require('chalk')); const logSymbols = _interopDefault(require('log-symbols')); const ansiEscapes = _interopDefault(require('ansi-escapes')); const wrapAnsi = _interopDefault(require('wrap-ansi')); const commonTags = require('common-tags'); const UpdateRenderer = _interopDefault(require('listr-update-renderer')); const graphql = require('graphql'); const cosmiconfig = require('cosmiconfig'); const stringEnvInterpolation = require('string-env-interpolation'); const yargs = _interopDefault(require('yargs')); const graphqlConfig = require('graphql-config'); const apolloEngineLoader = require('@graphql-tools/apollo-engine-loader'); const codeFileLoader = require('@graphql-tools/code-file-loader'); const gitLoader = require('@graphql-tools/git-loader'); const githubLoader = require('@graphql-tools/github-loader'); const prismaLoader = require('@graphql-tools/prisma-loader'); const load = require('@graphql-tools/load'); const graphqlFileLoader = require('@graphql-tools/graphql-file-loader'); const jsonFileLoader = require('@graphql-tools/json-file-loader'); const urlLoader = require('@graphql-tools/url-loader'); const yaml = _interopDefault(require('yaml')); const module$1 = require('module'); const fs = require('fs'); const fs__default = _interopDefault(fs); const crypto = require('crypto'); const os = require('os'); const Listr = _interopDefault(require('listr')); const isGlob = _interopDefault(require('is-glob')); const debounce = _interopDefault(require('debounce')); const mkdirp = _interopDefault(require('mkdirp')); const inquirer = _interopDefault(require('inquirer')); const detectIndent = _interopDefault(require('detect-indent')); const getLatestVersion = _interopDefault(require('latest-version')); let logger; function getLogger() { return logger || tsLog.dummyLogger; } useWinstonLogger(); function useWinstonLogger() { if (logger && logger.levels) { return; } logger = console; } let queue = []; function debugLog(message, ...meta) { if (!process.env.GQL_CODEGEN_NODEBUG && process.env.DEBUG !== undefined) { queue.push({ message, meta, }); } } function printLogs() { if (!process.env.GQL_CODEGEN_NODEBUG && process.env.DEBUG !== undefined) { queue.forEach(log => { getLogger().info(log.message, ...log.meta); }); resetLogs(); } } function resetLogs() { queue = []; } const DEFAULT_HOOKS = { afterStart: [], beforeDone: [], onWatchTriggered: [], onError: [], afterOneFileWrite: [], afterAllFileWrite: [], beforeOneFileWrite: [], beforeAllFileWrite: [], }; function normalizeHooks(_hooks) { const keys = Object.keys({ ...DEFAULT_HOOKS, ...(_hooks || {}), }); return keys.reduce((prev, hookName) => { if (typeof _hooks[hookName] === 'string') { return { ...prev, [hookName]: [_hooks[hookName]], }; } else if (typeof _hooks[hookName] === 'function') { return { ...prev, [hookName]: [_hooks[hookName]], }; } else if (Array.isArray(_hooks[hookName])) { return { ...prev, [hookName]: _hooks[hookName], }; } else { return prev; } }, {}); } function execShellCommand(cmd) { return new Promise((resolve, reject) => { child_process.exec(cmd, { env: { ...process.env, PATH: `${process.env.PATH}${path.delimiter}${process.cwd()}${path.sep}node_modules${path.sep}.bin`, }, }, (error, stdout, stderr) => { if (error) { reject(error); } else { resolve(stdout || stderr); } }); }); } async function executeHooks(hookName, scripts = [], args = []) { debugLog(`Running lifecycle hook "${hookName}" scripts...`); for (const script of scripts) { if (typeof script === 'string') { debugLog(`Running lifecycle hook "${hookName}" script: ${script} with args: ${args.join(' ')}...`); await execShellCommand(`${script} ${args.join(' ')}`); } else { debugLog(`Running lifecycle hook "${hookName}" script: ${script.name} with args: ${args.join(' ')}...`); await script(...args); } } } const lifecycleHooks = (_hooks = {}) => { const hooks = normalizeHooks(_hooks); return { afterStart: async () => executeHooks('afterStart', hooks.afterStart), onWatchTriggered: async (event, path) => executeHooks('onWatchTriggered', hooks.onWatchTriggered, [event, path]), onError: async (error) => executeHooks('onError', hooks.onError, [`"${error}"`]), afterOneFileWrite: async (path) => executeHooks('afterOneFileWrite', hooks.afterOneFileWrite, [path]), afterAllFileWrite: async (paths) => executeHooks('afterAllFileWrite', hooks.afterAllFileWrite, paths), beforeOneFileWrite: async (path) => executeHooks('beforeOneFileWrite', hooks.beforeOneFileWrite, [path]), beforeAllFileWrite: async (paths) => executeHooks('beforeAllFileWrite', hooks.beforeAllFileWrite, paths), beforeDone: async () => executeHooks('beforeDone', hooks.beforeDone), }; }; /** Indent each line in a string. @param string - The string to indent. @param count - How many times you want `options.indent` repeated. Default: `1`. @example ``` import indentString from 'indent-string'; indentString('Unicorns\nRainbows', 4); //=> ' Unicorns\n Rainbows' indentString('Unicorns\nRainbows', 4, {indent: '♥'}); //=> '♥♥♥♥Unicorns\n♥♥♥♥Rainbows' ``` */ function indentString(string, count = 1, options = {}) { const { indent = ' ', includeEmptyLines = false } = options; if (typeof string !== 'string') { throw new TypeError(`Expected \`input\` to be a \`string\`, got \`${typeof string}\``); } if (typeof count !== 'number') { throw new TypeError(`Expected \`count\` to be a \`number\`, got \`${typeof count}\``); } if (count < 0) { throw new RangeError(`Expected \`count\` to be at least 0, got \`${count}\``); } if (typeof indent !== 'string') { throw new TypeError(`Expected \`options.indent\` to be a \`string\`, got \`${typeof indent}\``); } if (count === 0) { return string; } const regex = includeEmptyLines ? /^/gm : /^(?!\s*$)/gm; return string.replace(regex, indent.repeat(count)); } class Renderer { constructor(tasks, options) { this.updateRenderer = new UpdateRenderer(tasks, options); } render() { return this.updateRenderer.render(); } end(err) { this.updateRenderer.end(err); if (typeof err === 'undefined') { logUpdate.clear(); return; } // persist the output logUpdate.done(); // show errors if (err) { const errorCount = err.errors ? err.errors.length : 0; if (errorCount > 0) { const count = indentString(chalk.red.bold(`Found ${errorCount} error${errorCount > 1 ? 's' : ''}`), 1); const details = err.errors .map(error => { debugLog(`[CLI] Exited with an error`, error); return { msg: pluginHelpers.isDetailedError(error) ? error.details : null, rawError: error }; }) .map(({ msg, rawError }, i) => { const source = err.errors[i].source; msg = msg ? chalk.gray(indentString(commonTags.stripIndent(`${msg}`), 4)) : null; const stack = rawError.stack ? chalk.gray(indentString(commonTags.stripIndent(rawError.stack), 4)) : null; if (source) { const sourceOfError = typeof source === 'string' ? source : source.name; const title = indentString(`${logSymbols.error} ${sourceOfError}`, 2); return [title, msg, stack, stack].filter(Boolean).join('\n'); } return [msg, stack].filter(Boolean).join('\n'); }) .join('\n\n'); logUpdate.emit(['', count, details, ''].join('\n\n')); } else { const details = err.details ? err.details : ''; logUpdate.emit(`${chalk.red.bold(`${indentString(err.message, 2)}`)}\n${details}\n${chalk.grey(err.stack)}`); } } logUpdate.done(); printLogs(); } } const render = tasks => { for (const task of tasks) { task.subscribe(event => { if (event.type === 'SUBTASKS') { render(task.subtasks); return; } if (event.type === 'DATA') { logUpdate.emit(chalk.dim(`${event.data}`)); } logUpdate.done(); }, err => { logUpdate.emit(err); logUpdate.done(); }); } }; class ErrorRenderer { constructor(tasks, _options) { this.tasks = tasks; } render() { render(this.tasks); } static get nonTTY() { return true; } end() { } } class LogUpdate { constructor() { this.stream = process.stdout; // state this.previousLineCount = 0; this.previousOutput = ''; this.previousWidth = this.getWidth(); } emit(...args) { let output = args.join(' ') + '\n'; const width = this.getWidth(); if (output === this.previousOutput && this.previousWidth === width) { return; } this.previousOutput = output; this.previousWidth = width; output = wrapAnsi(output, width, { trim: false, hard: true, wordWrap: false, }); this.stream.write(ansiEscapes.eraseLines(this.previousLineCount) + output); this.previousLineCount = output.split('\n').length; } clear() { this.stream.write(ansiEscapes.eraseLines(this.previousLineCount)); this.previousOutput = ''; this.previousWidth = this.getWidth(); this.previousLineCount = 0; } done() { this.previousOutput = ''; this.previousWidth = this.getWidth(); this.previousLineCount = 0; } getWidth() { const { columns } = this.stream; if (!columns) { return 80; } return columns; } } const logUpdate = new LogUpdate(); async function getPluginByName(name, pluginLoader) { const possibleNames = [ `@graphql-codegen/${name}`, `@graphql-codegen/${name}-template`, `@graphql-codegen/${name}-plugin`, `graphql-codegen-${name}`, `graphql-codegen-${name}-template`, `graphql-codegen-${name}-plugin`, `codegen-${name}`, `codegen-${name}-template`, name, ]; const possibleModules = possibleNames.concat(path.resolve(process.cwd(), name)); for (const moduleName of possibleModules) { try { return await pluginLoader(moduleName); } catch (err) { if (err.code !== 'MODULE_NOT_FOUND') { throw new pluginHelpers.DetailedError(`Unable to load template plugin matching ${name}`, ` Unable to load template plugin matching '${name}'. Reason: ${err.message} `); } } } const possibleNamesMsg = possibleNames .map(name => ` - ${name} `.trimRight()) .join(''); throw new pluginHelpers.DetailedError(`Unable to find template plugin matching ${name}`, ` Unable to find template plugin matching '${name}' Install one of the following packages: ${possibleNamesMsg} `); } async function getPresetByName(name, loader) { const possibleNames = [ `@graphql-codegen/${name}`, `@graphql-codegen/${name}-preset`, name, path.resolve(process.cwd(), name), ]; for (const moduleName of possibleNames) { try { const loaded = await loader(moduleName); if (loaded && loaded.preset) { return loaded.preset; } else if (loaded && loaded.default) { return loaded.default; } return loaded; } catch (err) { if (err.code !== 'MODULE_NOT_FOUND') { throw new pluginHelpers.DetailedError(`Unable to load preset matching ${name}`, ` Unable to load preset matching '${name}'. Reason: ${err.message} `); } } } const possibleNamesMsg = possibleNames .map(name => ` - ${name} `.trimRight()) .join(''); throw new pluginHelpers.DetailedError(`Unable to find preset matching ${name}`, ` Unable to find preset matching '${name}' Install one of the following packages: ${possibleNamesMsg} `); } const CodegenExtension = (api) => { // Schema api.loaders.schema.register(new codeFileLoader.CodeFileLoader({ pluckConfig: { skipIndent: true, }, })); api.loaders.schema.register(new gitLoader.GitLoader()); api.loaders.schema.register(new githubLoader.GithubLoader()); api.loaders.schema.register(new apolloEngineLoader.ApolloEngineLoader()); api.loaders.schema.register(new prismaLoader.PrismaLoader()); // Documents api.loaders.documents.register(new codeFileLoader.CodeFileLoader({ pluckConfig: { skipIndent: true, }, })); api.loaders.documents.register(new gitLoader.GitLoader()); api.loaders.documents.register(new githubLoader.GithubLoader()); return { name: 'codegen', }; }; async function findAndLoadGraphQLConfig(filepath) { const config = await graphqlConfig.loadConfig({ filepath, rootDir: process.cwd(), extensions: [CodegenExtension], throwOnEmpty: false, throwOnMissing: false, }); if (isGraphQLConfig(config)) { return config; } } // Kamil: user might load a config that is not GraphQL Config // so we need to check if it's a regular config or not function isGraphQLConfig(config) { if (!config) { return false; } try { return config.getDefault().hasExtension('codegen'); } catch (e) { } try { for (const projectName in config.projects) { if (config.projects.hasOwnProperty(projectName)) { const project = config.projects[projectName]; if (project.hasExtension('codegen')) { return true; } } } } catch (e) { } return false; } const defaultSchemaLoadOptions = { assumeValidSDL: true, sort: true, convertExtensions: true, includeSources: true, }; const defaultDocumentsLoadOptions = { sort: true, skipGraphQLImport: true, }; async function loadSchema(schemaPointers, config) { try { const loaders = [ new codeFileLoader.CodeFileLoader(), new gitLoader.GitLoader(), new githubLoader.GithubLoader(), new graphqlFileLoader.GraphQLFileLoader(), new jsonFileLoader.JsonFileLoader(), new urlLoader.UrlLoader(), new apolloEngineLoader.ApolloEngineLoader(), new prismaLoader.PrismaLoader(), ]; const schema = await load.loadSchema(schemaPointers, { ...defaultSchemaLoadOptions, loaders, ...config, ...config.config, }); return schema; } catch (e) { throw new pluginHelpers.DetailedError('Failed to load schema', ` Failed to load schema from ${Object.keys(schemaPointers).join(',')}: ${e.message || e} ${e.stack || ''} GraphQL Code Generator supports: - ES Modules and CommonJS exports (export as default or named export "schema") - Introspection JSON File - URL of GraphQL endpoint - Multiple files with type definitions (glob expression) - String in config file Try to use one of above options and run codegen again. `); } } async function loadDocuments(documentPointers, config) { const loaders = [ new codeFileLoader.CodeFileLoader({ pluckConfig: { skipIndent: true, }, }), new gitLoader.GitLoader(), new githubLoader.GithubLoader(), new graphqlFileLoader.GraphQLFileLoader(), ]; const ignore = []; for (const generatePath of Object.keys(config.generates)) { if (path.extname(generatePath) === '') { // we omit paths that don't resolve to a specific file continue; } ignore.push(path.join(process.cwd(), generatePath)); } const loadedFromToolkit = await load.loadDocuments(documentPointers, { ...defaultDocumentsLoadOptions, ignore, loaders, ...config, ...config.config, }); return loadedFromToolkit; } const { lstat } = fs.promises; function generateSearchPlaces(moduleName) { const extensions = ['json', 'yaml', 'yml', 'js', 'config.js']; // gives codegen.json... const regular = extensions.map(ext => `${moduleName}.${ext}`); // gives .codegenrc.json... but no .codegenrc.config.js const dot = extensions.filter(ext => ext !== 'config.js').map(ext => `.${moduleName}rc.${ext}`); return [...regular.concat(dot), 'package.json']; } function customLoader(ext) { function loader(filepath, content) { if (typeof process !== 'undefined' && 'env' in process) { content = stringEnvInterpolation.env(content); } if (ext === 'json') { return cosmiconfig.defaultLoaders['.json'](filepath, content); } if (ext === 'yaml') { try { const result = yaml.parse(content, { prettyErrors: true, merge: true }); return result; } catch (error) { error.message = `YAML Error in ${filepath}:\n${error.message}`; throw error; } } if (ext === 'js') { return cosmiconfig.defaultLoaders['.js'](filepath, content); } } return loader; } async function loadCodegenConfig({ configFilePath, moduleName, searchPlaces: additionalSearchPlaces, packageProp, loaders: customLoaders, }) { configFilePath = configFilePath || process.cwd(); moduleName = moduleName || 'codegen'; packageProp = packageProp || moduleName; const cosmi = cosmiconfig.cosmiconfig(moduleName, { searchPlaces: generateSearchPlaces(moduleName).concat(additionalSearchPlaces || []), packageProp, loaders: { '.json': customLoader('json'), '.yaml': customLoader('yaml'), '.yml': customLoader('yaml'), '.js': customLoader('js'), noExt: customLoader('yaml'), ...customLoaders, }, }); const pathStats = await lstat(configFilePath); return pathStats.isDirectory() ? cosmi.search(configFilePath) : cosmi.load(configFilePath); } async function loadContext(configFilePath) { const graphqlConfig = await findAndLoadGraphQLConfig(configFilePath); if (graphqlConfig) { return new CodegenContext({ graphqlConfig, }); } const result = await loadCodegenConfig({ configFilePath }); if (!result) { if (configFilePath) { throw new pluginHelpers.DetailedError(`Config ${configFilePath} does not exist`, ` Config ${configFilePath} does not exist. $ graphql-codegen --config ${configFilePath} Please make sure the --config points to a correct file. `); } throw new pluginHelpers.DetailedError(`Unable to find Codegen config file!`, ` Please make sure that you have a configuration file under the current directory! `); } if (result.isEmpty) { throw new pluginHelpers.DetailedError(`Found Codegen config file but it was empty!`, ` Please make sure that you have a valid configuration file under the current directory! `); } return new CodegenContext({ filepath: result.filepath, config: result.config, }); } function getCustomConfigPath(cliFlags) { const configFile = cliFlags.config; return configFile ? path.resolve(process.cwd(), configFile) : null; } function buildOptions() { return { c: { alias: 'config', type: 'string', describe: 'Path to GraphQL codegen YAML config file, defaults to "codegen.yml" on the current directory', }, w: { alias: 'watch', describe: 'Watch for changes and execute generation automatically. You can also specify a glob expreession for custom watch list.', coerce: (watch) => { if (watch === 'false') { return false; } if (typeof watch === 'string' || Array.isArray(watch)) { return watch; } return !!watch; }, }, r: { alias: 'require', describe: 'Loads specific require.extensions before running the codegen and reading the configuration', type: 'array', default: [], }, o: { alias: 'overwrite', describe: 'Overwrites existing files', type: 'boolean', }, s: { alias: 'silent', describe: 'Suppresses printing errors', type: 'boolean', }, e: { alias: 'errors-only', describe: 'Only print errors', type: 'boolean', }, profile: { describe: 'Use profiler to measure performance', type: 'boolean', }, p: { alias: 'project', describe: 'Name of a project in GraphQL Config', type: 'string', }, }; } function parseArgv(argv = process.argv) { return yargs.options(buildOptions()).parse(argv); } async function createContext(cliFlags = parseArgv(process.argv)) { if (cliFlags.require && cliFlags.require.length > 0) { const relativeRequire = module$1.createRequire(process.cwd()); await Promise.all(cliFlags.require.map(mod => new Promise(function (resolve) { resolve(_interopNamespace(require(relativeRequire.resolve(mod, { paths: [process.cwd()], })))); }))); } const customConfigPath = getCustomConfigPath(cliFlags); const context = await loadContext(customConfigPath); updateContextWithCliFlags(context, cliFlags); return context; } function updateContextWithCliFlags(context, cliFlags) { const config = { configFilePath: context.filepath, }; if (cliFlags.watch) { config.watch = cliFlags.watch; } if (cliFlags.overwrite === true) { config.overwrite = cliFlags.overwrite; } if (cliFlags.silent === true) { config.silent = cliFlags.silent; } if (cliFlags.errorsOnly === true) { config.errorsOnly = cliFlags.errorsOnly; } if (cliFlags.project) { context.useProject(cliFlags.project); } if (cliFlags.profile === true) { context.useProfiler(); } context.updateConfig(config); } class CodegenContext { constructor({ config, graphqlConfig, filepath, }) { this._pluginContext = {}; this._config = config; this._graphqlConfig = graphqlConfig; this.filepath = this._graphqlConfig ? this._graphqlConfig.filepath : filepath; this.cwd = this._graphqlConfig ? this._graphqlConfig.dirpath : process.cwd(); this.profiler = pluginHelpers.createNoopProfiler(); } useProject(name) { this._project = name; } getConfig(extraConfig) { if (!this.config) { if (this._graphqlConfig) { const project = this._graphqlConfig.getProject(this._project); this.config = { ...project.extension('codegen'), schema: project.schema, documents: project.documents, pluginContext: this._pluginContext, }; } else { this.config = { ...this._config, pluginContext: this._pluginContext }; } } return { ...extraConfig, ...this.config, }; } updateConfig(config) { this.config = { ...this.getConfig(), ...config, }; } useProfiler() { this.profiler = pluginHelpers.createProfiler(); const now = new Date(); // 2011-10-05T14:48:00.000Z const datetime = now.toISOString().split('.')[0]; // 2011-10-05T14:48:00 const datetimeNormalized = datetime.replace(/-|:/g, ''); // 20111005T144800 this.profilerOutput = `codegen-${datetimeNormalized}.json`; } getPluginContext() { return this._pluginContext; } async loadSchema(pointer) { const config = this.getConfig(defaultSchemaLoadOptions); if (this._graphqlConfig) { // TODO: SchemaWithLoader won't work here return addHashToSchema(this._graphqlConfig.getProject(this._project).loadSchema(pointer, 'GraphQLSchema', config)); } return addHashToSchema(loadSchema(pointer, config)); } async loadDocuments(pointer) { const config = this.getConfig(defaultDocumentsLoadOptions); if (this._graphqlConfig) { // TODO: pointer won't work here return addHashToDocumentFiles(this._graphqlConfig.getProject(this._project).loadDocuments(pointer, config)); } return addHashToDocumentFiles(loadDocuments(pointer, config)); } } function ensureContext(input) { return input instanceof CodegenContext ? input : new CodegenContext({ config: input }); } function hashContent(content) { return crypto.createHash('sha256').update(content).digest('hex'); } function hashSchema(schema) { return hashContent(graphql.print(pluginHelpers.getCachedDocumentNodeFromSchema(schema))); } function addHashToSchema(schemaPromise) { return schemaPromise.then(schema => { // It's consumed later on. The general purpose is to use it for caching. if (!schema.extensions) { schema.extensions = {}; } schema.extensions['hash'] = hashSchema(schema); return schema; }); } function hashDocument(doc) { if (doc.rawSDL) { return hashContent(doc.rawSDL); } if (doc.document) { return hashContent(graphql.print(doc.document)); } return null; } function addHashToDocumentFiles(documentFilesPromise) { return documentFilesPromise.then(documentFiles => documentFiles.map(doc => { doc.hash = hashDocument(doc); return doc; })); } const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null; function isListrError(err) { return err.name === 'ListrError' && Array.isArray(err.errors) && err.errors.length > 0; } function cliError(err, exitOnError = true) { let msg; if (err instanceof Error) { msg = err.message || err.toString(); } else if (typeof err === 'string') { msg = err; } else { msg = JSON.stringify(err); } // eslint-disable-next-line no-console console.error(msg); if (exitOnError && isNode) { process.exit(1); } else if (exitOnError && isBrowser) { throw err; } } const makeDefaultLoader = (from) => { if (fs__default.statSync(from).isDirectory()) { from = path__default.join(from, '__fake.js'); } const relativeRequire = module$1.createRequire(from); return (mod) => { return new Promise(function (resolve) { resolve(_interopNamespace(require(relativeRequire.resolve(mod)))); }); }; }; function createCache() { const cache = new Map(); return function ensure(namespace, key, factory) { const cacheKey = `${namespace}:${key}`; const cachedValue = cache.get(cacheKey); if (cachedValue) { return cachedValue; } const value = factory(); cache.set(cacheKey, value); return value; }; } async function executeCodegen(input) { const context = ensureContext(input); const config = context.getConfig(); const pluginContext = context.getPluginContext(); const result = []; const commonListrOptions = { exitOnError: true, }; let listr; if (process.env.VERBOSE) { listr = new Listr({ ...commonListrOptions, renderer: 'verbose', nonTTYRenderer: 'verbose', }); } else if (process.env.NODE_ENV === 'test') { listr = new Listr({ ...commonListrOptions, renderer: 'silent', nonTTYRenderer: 'silent', }); } else { listr = new Listr({ ...commonListrOptions, renderer: config.silent ? 'silent' : config.errorsOnly ? ErrorRenderer : Renderer, nonTTYRenderer: config.silent ? 'silent' : 'default', collapse: true, clearOutput: false, }); } let rootConfig = {}; let rootSchemas; let rootDocuments; const generates = {}; const cache = createCache(); function wrapTask(task, source, taskName) { return () => { return context.profiler.run(async () => { try { await Promise.resolve().then(() => task()); } catch (error) { if (source && !(error instanceof graphql.GraphQLError)) { error.source = source; } throw error; } }, taskName); }; } async function normalize() { /* Load Require extensions */ const requireExtensions = pluginHelpers.normalizeInstanceOrArray(config.require); const loader = makeDefaultLoader(context.cwd); for (const mod of requireExtensions) { await loader(mod); } /* Root plugin config */ rootConfig = config.config || {}; /* Normalize root "schema" field */ rootSchemas = pluginHelpers.normalizeInstanceOrArray(config.schema); /* Normalize root "documents" field */ rootDocuments = pluginHelpers.normalizeInstanceOrArray(config.documents); /* Normalize "generators" field */ const generateKeys = Object.keys(config.generates || {}); if (generateKeys.length === 0) { throw new pluginHelpers.DetailedError('Invalid Codegen Configuration!', ` Please make sure that your codegen config file contains the "generates" field, with a specification for the plugins you need. It should looks like that: schema: - my-schema.graphql generates: my-file.ts: - plugin1 - plugin2 - plugin3 `); } for (const filename of generateKeys) { const output = (generates[filename] = pluginHelpers.normalizeOutputParam(config.generates[filename])); if (!output.preset && (!output.plugins || output.plugins.length === 0)) { throw new pluginHelpers.DetailedError('Invalid Codegen Configuration!', ` Please make sure that your codegen config file has defined plugins list for output "${filename}". It should looks like that: schema: - my-schema.graphql generates: my-file.ts: - plugin1 - plugin2 - plugin3 `); } } if (rootSchemas.length === 0 && Object.keys(generates).some(filename => !generates[filename].schema || generates[filename].schema.length === 0)) { throw new pluginHelpers.DetailedError('Invalid Codegen Configuration!', ` Please make sure that your codegen config file contains either the "schema" field or every generated file has its own "schema" field. It should looks like that: schema: - my-schema.graphql or: generates: path/to/output: schema: my-schema.graphql `); } } listr.add({ title: 'Parse configuration', task: () => normalize(), }); listr.add({ title: 'Generate outputs', task: () => { return new Listr(Object.keys(generates).map(filename => { const outputConfig = generates[filename]; const hasPreset = !!outputConfig.preset; return { title: hasPreset ? `Generate to ${filename} (using EXPERIMENTAL preset "${outputConfig.preset}")` : `Generate ${filename}`, task: () => { let outputSchemaAst; let outputSchema; const outputFileTemplateConfig = outputConfig.config || {}; const outputDocuments = []; const outputSpecificSchemas = pluginHelpers.normalizeInstanceOrArray(outputConfig.schema); const outputSpecificDocuments = pluginHelpers.normalizeInstanceOrArray(outputConfig.documents); return new Listr([ { title: 'Load GraphQL schemas', task: wrapTask(async () => { debugLog(`[CLI] Loading Schemas`); const schemaPointerMap = {}; const allSchemaUnnormalizedPointers = [...rootSchemas, ...outputSpecificSchemas]; for (const unnormalizedPtr of allSchemaUnnormalizedPointers) { if (typeof unnormalizedPtr === 'string') { schemaPointerMap[unnormalizedPtr] = {}; } else if (typeof unnormalizedPtr === 'object') { Object.assign(schemaPointerMap, unnormalizedPtr); } } const hash = JSON.stringify(schemaPointerMap); const result = await cache('schema', hash, async () => { const outputSchemaAst = await context.loadSchema(schemaPointerMap); const outputSchema = pluginHelpers.getCachedDocumentNodeFromSchema(outputSchemaAst); return { outputSchemaAst: outputSchemaAst, outputSchema: outputSchema, }; }); outputSchemaAst = await result.outputSchemaAst; outputSchema = result.outputSchema; }, filename, `Load GraphQL schemas: ${filename}`), }, { title: 'Load GraphQL documents', task: wrapTask(async () => { debugLog(`[CLI] Loading Documents`); // get different cache for shared docs and output specific docs const results = await Promise.all([rootDocuments, outputSpecificDocuments].map(docs => { const hash = JSON.stringify(docs); return cache('documents', hash, async () => { const documents = await context.loadDocuments(docs); return { documents: documents, }; }); })); const documents = []; results.forEach(source => documents.push(...source.documents)); if (documents.length > 0) { outputDocuments.push(...documents); } }, filename, `Load GraphQL documents: ${filename}`), }, { title: 'Generate', task: wrapTask(async () => { debugLog(`[CLI] Generating output`); const normalizedPluginsArray = pluginHelpers.normalizeConfig(outputConfig.plugins); const pluginLoader = config.pluginLoader || makeDefaultLoader(context.cwd); const pluginPackages = await Promise.all(normalizedPluginsArray.map(plugin => getPluginByName(Object.keys(plugin)[0], pluginLoader))); const pluginMap = {}; const preset = hasPreset ? typeof outputConfig.preset === 'string' ? await getPresetByName(outputConfig.preset, makeDefaultLoader(context.cwd)) : outputConfig.preset : null; pluginPackages.forEach((pluginPackage, i) => { const plugin = normalizedPluginsArray[i]; const name = Object.keys(plugin)[0]; pluginMap[name] = pluginPackage; }); const mergedConfig = { ...rootConfig, ...(typeof outputFileTemplateConfig === 'string' ? { value: outputFileTemplateConfig } : outputFileTemplateConfig), }; let outputs = []; if (hasPreset) { outputs = await context.profiler.run(async () => preset.buildGeneratesSection({ baseOutputDir: filename, presetConfig: outputConfig.presetConfig || {}, plugins: normalizedPluginsArray, schema: outputSchema, schemaAst: outputSchemaAst, documents: outputDocuments, config: mergedConfig, pluginMap, pluginContext, profiler: context.profiler, }), `Build Generates Section: ${filename}`); } else { outputs = [ { filename, plugins: normalizedPluginsArray, schema: outputSchema, schemaAst: outputSchemaAst, documents: outputDocuments, config: mergedConfig, pluginMap, pluginContext, profiler: context.profiler, }, ]; } const process = async (outputArgs) => { const output = await core.codegen({ ...outputArgs, cache, }); result.push({ filename: outputArgs.filename, content: output, hooks: outputConfig.hooks || {}, }); }; await context.profiler.run(() => Promise.all(outputs.map(process)), `Codegen: ${filename}`); }, filename, `Generate: ${filename}`), }, ], { // it stops when one of tasks failed exitOnError: true, }); }, }; }), { // it doesn't stop when one of tasks failed, to finish at least some of outputs exitOnError: false, concurrent: os.cpus().length, }); }, }); try { await listr.run(); } catch (err) { if (isListrError(err)) { const allErrs = err.errors.map(subErr => pluginHelpers.isDetailedError(subErr) ? `${subErr.message} for "${subErr.source}"${subErr.details}` : subErr.message || subErr.toString()); const newErr = new utils.AggregateError(err.errors, `${err.message} ${allErrs.join('\n\n')}`); // Best-effort to all stack traces for debugging newErr.stack = `${newErr.stack}\n\n${err.errors.map(subErr => subErr.stack).join('\n\n')}`; throw newErr; } throw err; } return result; } function log(msg) { // double spaces to inline the message with Listr getLogger().info(` ${msg}`); } function emitWatching() { log(`${logSymbols.info} Watching for changes...`); } const createWatcher = (initalContext, onNext) => { debugLog(`[Watcher] Starting watcher...`); let config = initalContext.getConfig(); const files = [initalContext.filepath].filter(a => a); const documents = pluginHelpers.normalizeInstanceOrArray(config.documents); const schemas = pluginHelpers.normalizeInstanceOrArray(config.schema); // Add schemas and documents from "generates" Object.keys(config.generates) .map(filename => pluginHelpers.normalizeOutputParam(config.generates[filename])) .forEach(conf => { schemas.push(...pluginHelpers.normalizeInstanceOrArray(conf.schema)); documents.push(...pluginHelpers.normalizeInstanceOrArray(conf.documents)); }); if (documents) { documents.forEach(doc => { if (typeof doc === 'string') { files.push(doc); } else { files.push(...Object.keys(doc)); } }); } schemas.forEach((schema) => { if (isGlob(schema) || utils.isValidPath(schema)) { files.push(schema); } }); if (typeof config.watch !== 'boolean') { files.push(...pluginHelpers.normalizeInstanceOrArray(config.watch)); } let watcher; const runWatcher = async () => { var _a, _b; const chokidar = await new Promise(function (resolve) { resolve(_interopNamespace(require('chokidar'))); }); let isShutdown = false; const debouncedExec = debounce(() => { if (!isShutdown) { executeCodegen(initalContext) .then(onNext, () => Promise.resolve()) .then(() => emitWatching()); } }, 100); emitWatching(); const ignored = []; Object.keys(config.generates) .map(filename => ({ filename, config: pluginHelpers.normalizeOutputParam(config.generates[filename]) })) .forEach(entry => { if (entry.config.preset) { const extension = entry.config.presetConfig && entry.config.presetConfig.extension; if (extension) { ignored.push(path.join(entry.filename, '**', '*' + extension)); } } else { ignored.push(entry.filename); } }); watcher = chokidar.watch(files, { persistent: true, ignoreInitial: true, followSymlinks: true, cwd: process.cwd(), disableGlobbing: false, usePolling: (_a = config.watchConfig) === null || _a === void 0 ? void 0 : _a.usePolling, interval: (_b = config.watchConfig) === null || _b === void 0 ? void 0 : _b.interval, depth: 99, awaitWriteFinish: true, ignorePermissionErrors: false, atomic: true, ignored, }); debugLog(`[Watcher] Started`); const shutdown = () => { isShutdown = true; debugLog(`[Watcher] Shutting down`); log(`Shutting down watch...`); watcher.close(); lifecycleHooks(config.hooks).beforeDone(); }; // it doesn't matter what has changed, need to run whole process anyway watcher.on('all', async (eventName, path$1) => { lifecycleHooks(config.hooks).onWatchTriggered(eventName, path$1); debugLog(`[Watcher] triggered due to a file ${eventName} event: ${path$1}`); const fullPath = path.join(process.cwd(), path$1); // In ESM require is not defined try { delete require.cache[fullPath]; } catch (err) { } if (eventName === 'change' && config.configFilePath && fullPath === config.configFilePath) { log(`${logSymbols.info} Config file has changed, reloading...`); const context = await loadContext(config.configFilePath); const newParsedConfig = context.getConfig(); newParsedConfig.watch = config.watch; newParsedConfig.silent = config.silent; newParsedConfig.overwrite = config.overwrite; newParsedConfig.configFilePath = config.configFilePath; config = newParsedConfig; initalContext.updateConfig(config); } debouncedExec(); }); process.once('SIGINT', shutdown); process.once('SIGTERM', shutdown); }; // the promise never resolves to keep process running return new Promise((resolve, reject) => { executeCodegen(initalContext) .then(onNext, () => Promise.resolve()) .then(runWatcher) .catch(err => { watcher.close(); reject(err); }); }); }; const { writeFile: fsWriteFile, readFile: fsReadFile, stat: fsStat } = fs.promises; function writeFile(filepath, content) { return fsWriteFile(filepath, content); } function readFile(filepath) { return fsReadFile(filepath, 'utf-8'); } async function fileExists(filePath) { try { return (await fsStat(filePath)).isFile(); } catch (err) { return false; } } function unlinkFile(filePath, cb) { fs.unlink(filePath, cb); } const hash = (content) => crypto.createHash('sha1').update(content).digest('base64'); async function generat