@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
JavaScript
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;
}