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,329 lines (1,298 loc) 69.5 kB
import { isDetailedError, DetailedError, createNoopProfiler, createProfiler, getCachedDocumentNodeFromSchema, normalizeInstanceOrArray, normalizeConfig, normalizeOutputParam } from '@graphql-codegen/plugin-helpers'; import { codegen } from '@graphql-codegen/core'; import { AggregateError, isValidPath } from '@graphql-tools/utils'; import chalk from 'chalk'; import logSymbols from 'log-symbols'; import ansiEscapes from 'ansi-escapes'; import wrapAnsi from 'wrap-ansi'; import { stripIndent } from 'common-tags'; import { dummyLogger } from 'ts-log'; import UpdateRenderer from 'listr-update-renderer'; import { print, GraphQLError } from 'graphql'; import path, { resolve, extname, join, delimiter, sep, dirname, isAbsolute, relative } from 'path'; import { cosmiconfig, defaultLoaders } from 'cosmiconfig'; import { env } from 'string-env-interpolation'; import yargs from 'yargs'; import { loadConfig } from 'graphql-config'; import { ApolloEngineLoader } from '@graphql-tools/apollo-engine-loader'; import { CodeFileLoader } from '@graphql-tools/code-file-loader'; import { GitLoader } from '@graphql-tools/git-loader'; import { GithubLoader } from '@graphql-tools/github-loader'; import { PrismaLoader } from '@graphql-tools/prisma-loader'; import { loadSchema as loadSchema$1, loadDocuments as loadDocuments$1 } from '@graphql-tools/load'; import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; import { JsonFileLoader } from '@graphql-tools/json-file-loader'; import { UrlLoader } from '@graphql-tools/url-loader'; import yaml from 'yaml'; import { createRequire } from 'module'; import fs, { promises, unlink, writeFileSync, readFileSync } from 'fs'; import { createHash } from 'crypto'; import { cpus } from 'os'; import Listr from 'listr'; import { exec } from 'child_process'; import isGlob from 'is-glob'; import debounce from 'debounce'; import mkdirp from 'mkdirp'; import inquirer from 'inquirer'; import detectIndent from 'detect-indent'; import getLatestVersion from 'latest-version'; /** 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)); } let logger; function getLogger() { return logger || 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 = []; } 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: isDetailedError(error) ? error.details : null, rawError: error }; }) .map(({ msg, rawError }, i) => { const source = err.errors[i].source; msg = msg ? chalk.gray(indentString(stripIndent(`${msg}`), 4)) : null; const stack = rawError.stack ? chalk.gray(indentString(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(resolve(process.cwd(), name)); for (const moduleName of possibleModules) { try { return await pluginLoader(moduleName); } catch (err) { if (err.code !== 'MODULE_NOT_FOUND') { throw new 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 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, 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 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 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({ pluckConfig: { skipIndent: true, }, })); api.loaders.schema.register(new GitLoader()); api.loaders.schema.register(new GithubLoader()); api.loaders.schema.register(new ApolloEngineLoader()); api.loaders.schema.register(new PrismaLoader()); // Documents api.loaders.documents.register(new CodeFileLoader({ pluckConfig: { skipIndent: true, }, })); api.loaders.documents.register(new GitLoader()); api.loaders.documents.register(new GithubLoader()); return { name: 'codegen', }; }; async function findAndLoadGraphQLConfig(filepath) { const config = await 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(), new GitLoader(), new GithubLoader(), new GraphQLFileLoader(), new JsonFileLoader(), new UrlLoader(), new ApolloEngineLoader(), new PrismaLoader(), ]; const schema = await loadSchema$1(schemaPointers, { ...defaultSchemaLoadOptions, loaders, ...config, ...config.config, }); return schema; } catch (e) { throw new 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({ pluckConfig: { skipIndent: true, }, }), new GitLoader(), new GithubLoader(), new GraphQLFileLoader(), ]; const ignore = []; for (const generatePath of Object.keys(config.generates)) { if (extname(generatePath) === '') { // we omit paths that don't resolve to a specific file continue; } ignore.push(join(process.cwd(), generatePath)); } const loadedFromToolkit = await loadDocuments$1(documentPointers, { ...defaultDocumentsLoadOptions, ignore, loaders, ...config, ...config.config, }); return loadedFromToolkit; } const { lstat } = 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 = env(content); } if (ext === 'json') { return 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 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(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 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 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 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 ? 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 = createRequire(process.cwd()); await Promise.all(cliFlags.require.map(mod => import(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 = 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 = 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 createHash('sha256').update(content).digest('hex'); } function hashSchema(schema) { return hashContent(print(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(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.statSync(from).isDirectory()) { from = path.join(from, '__fake.js'); } const relativeRequire = createRequire(from); return (mod) => { return import(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 GraphQLError)) { error.source = source; } throw error; } }, taskName); }; } async function normalize() { /* Load Require extensions */ const requireExtensions = 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 = normalizeInstanceOrArray(config.schema); /* Normalize root "documents" field */ rootDocuments = normalizeInstanceOrArray(config.documents); /* Normalize "generators" field */ const generateKeys = Object.keys(config.generates || {}); if (generateKeys.length === 0) { throw new 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] = normalizeOutputParam(config.generates[filename])); if (!output.preset && (!output.plugins || output.plugins.length === 0)) { throw new 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 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 = normalizeInstanceOrArray(outputConfig.schema); const outputSpecificDocuments = 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 = 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 = 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 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: cpus().length, }); }, }); try { await listr.run(); } catch (err) { if (isListrError(err)) { const allErrs = err.errors.map(subErr => isDetailedError(subErr) ? `${subErr.message} for "${subErr.source}"${subErr.details}` : subErr.message || subErr.toString()); const newErr = new 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; } 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) => { exec(cmd, { env: { ...process.env, PATH: `${process.env.PATH}${delimiter}${process.cwd()}${sep}node_modules${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), }; }; 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 = normalizeInstanceOrArray(config.documents); const schemas = normalizeInstanceOrArray(config.schema); // Add schemas and documents from "generates" Object.keys(config.generates) .map(filename => normalizeOutputParam(config.generates[filename])) .forEach(conf => { schemas.push(...normalizeInstanceOrArray(conf.schema)); documents.push(...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) || isValidPath(schema)) { files.push(schema); } }); if (typeof config.watch !== 'boolean') { files.push(...normalizeInstanceOrArray(config.watch)); } let watcher; const runWatcher = async () => { var _a, _b; const chokidar = await import('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: normalizeOutputParam(config.generates[filename]) })) .forEach(entry => { if (entry.config.preset) { const extension = entry.config.presetConfig && entry.config.presetConfig.extension; if (extension) { ignored.push(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) => { lifecycleHooks(config.hooks).onWatchTriggered(eventName, path); debugLog(`[Watcher] triggered due to a file ${eventName} event: ${path}`); const fullPath = join(process.cwd(), path); // 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 } = 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) { unlink(filePath, cb); } const hash = (content) => createHash('sha1').update(content).digest('base64'); async function generate(input, saveToFile = true) { const context = ensureContext(input); const config = context.getConfig(); await context.profiler.run(() => lifecycleHooks(config.hooks).afterStart(), 'Lifecycle: afterStart'); let previouslyGeneratedFilenames = []; function removeStaleFiles(config, generationResult) { const filenames = generationResult.map(o => o.filename); // find stale files from previous build which are not present in current build const staleFilenames = previouslyGeneratedFilenames.filter(f => !filenames.includes(f)); staleFilenames.forEach(filename => { if (shouldOverwrite(config, filename)) { return unlinkFile(filename, err => { const prettyFilename = filename.replace(`${input.cwd || process.cwd()}/`, ''); if (err) { debugLog(`Cannot remove stale file: ${prettyFilename}\n${err}`); } else { debugLog(`Removed stale file: ${prettyFilename}`); } }); } }); previouslyGeneratedFilenames = filenames; } const recentOutputHash = new Map(); async function writeOutput(generationResult) { if (!saveToFile) { return generationResult; } if (config.watch) { removeStaleFiles(config, generationResult); } await context.profiler.run(async () => { await lifecycleHooks(config.hooks).beforeAllFileWrite(generationResult.map(r => r.filename)); }, 'Lifecycle: beforeAllFileWrite'); await context.profiler.run(() => Promise.all(generationResult.map(async (result) => { const exists = await fileExists(result.filename); if (!shouldOverwrite(config, result.filename) && exists) { return; }