1704 lines
62 KiB
JavaScript
Executable File
1704 lines
62 KiB
JavaScript
Executable File
#!/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 chalk = _interopDefault(require('chalk'));
|
|
const indentString = _interopDefault(require('indent-string'));
|
|
const logSymbols = _interopDefault(require('log-symbols'));
|
|
const ansiEscapes = _interopDefault(require('ansi-escapes'));
|
|
const wrapAnsi = _interopDefault(require('wrap-ansi'));
|
|
const commonTags = require('common-tags');
|
|
const graphql = require('graphql');
|
|
const utils = require('@graphql-tools/utils');
|
|
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 fs = require('fs');
|
|
const fs__default = _interopDefault(fs);
|
|
const module$1 = require('module');
|
|
const isGlob = _interopDefault(require('is-glob'));
|
|
const debounce = _interopDefault(require('debounce'));
|
|
const mkdirp = require('mkdirp');
|
|
const crypto = require('crypto');
|
|
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),
|
|
};
|
|
};
|
|
|
|
const UpdateRenderer = require('listr-update-renderer');
|
|
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' || !err.message.includes(moduleName)) {
|
|
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];
|
|
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' || !err.message.includes(moduleName)) {
|
|
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());
|
|
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());
|
|
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(), new gitLoader.GitLoader(), new githubLoader.GithubLoader(), new graphqlFileLoader.GraphQLFileLoader()];
|
|
const loadedFromToolkit = await load.loadDocuments(documentPointers, {
|
|
...defaultDocumentsLoadOptions,
|
|
ignore: Object.keys(config.generates).map(p => path.join(process.cwd(), p)),
|
|
loaders,
|
|
...config,
|
|
...config.config,
|
|
});
|
|
return loadedFromToolkit;
|
|
}
|
|
|
|
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 loadContext(configFilePath) {
|
|
const moduleName = 'codegen';
|
|
const cosmi = cosmiconfig.cosmiconfig(moduleName, {
|
|
searchPlaces: generateSearchPlaces(moduleName),
|
|
packageProp: moduleName,
|
|
loaders: {
|
|
'.json': customLoader('json'),
|
|
'.yaml': customLoader('yaml'),
|
|
'.yml': customLoader('yaml'),
|
|
'.js': customLoader('js'),
|
|
noExt: customLoader('yaml'),
|
|
},
|
|
});
|
|
const graphqlConfig = await findAndLoadGraphQLConfig(configFilePath);
|
|
if (graphqlConfig) {
|
|
return new CodegenContext({
|
|
graphqlConfig,
|
|
});
|
|
}
|
|
const result = await (configFilePath ? cosmi.load(configFilePath) : cosmi.search(process.cwd()));
|
|
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 true;
|
|
},
|
|
},
|
|
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',
|
|
},
|
|
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) {
|
|
await Promise.all(cliFlags.require.map(mod => new Promise(function (resolve) { resolve(_interopNamespace(require(require.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);
|
|
}
|
|
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();
|
|
}
|
|
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,
|
|
};
|
|
}
|
|
getPluginContext() {
|
|
return this._pluginContext;
|
|
}
|
|
async loadSchema(pointer) {
|
|
const config = this.getConfig(defaultSchemaLoadOptions);
|
|
if (this._graphqlConfig) {
|
|
// TODO: SchemaWithLoader won't work here
|
|
return this._graphqlConfig.getProject(this._project).loadSchema(pointer, 'GraphQLSchema', config);
|
|
}
|
|
return loadSchema(pointer, config);
|
|
}
|
|
async loadDocuments(pointer) {
|
|
const config = this.getConfig(defaultDocumentsLoadOptions);
|
|
if (this._graphqlConfig) {
|
|
// TODO: pointer won't work here
|
|
const documents = await this._graphqlConfig.getProject(this._project).loadDocuments(pointer, config);
|
|
return documents;
|
|
}
|
|
return loadDocuments(pointer, config);
|
|
}
|
|
}
|
|
function ensureContext(input) {
|
|
return input instanceof CodegenContext ? input : new CodegenContext({ config: input });
|
|
}
|
|
|
|
const makeDefaultLoader = (from) => {
|
|
if (fs__default.statSync(from).isDirectory()) {
|
|
from = path__default.join(from, '__fake.js');
|
|
}
|
|
const relativeRequire = (module$1.createRequire || module$1.createRequireFromPath)(from);
|
|
return (mod) => {
|
|
return new Promise(function (resolve) { resolve(_interopNamespace(require(relativeRequire.resolve(mod)))); });
|
|
};
|
|
};
|
|
async function executeCodegen(input) {
|
|
function wrapTask(task, source) {
|
|
return async () => {
|
|
try {
|
|
await Promise.resolve().then(() => task());
|
|
}
|
|
catch (error) {
|
|
if (source && !(error instanceof graphql.GraphQLError)) {
|
|
error.source = source;
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
}
|
|
const context = ensureContext(input);
|
|
const config = context.getConfig();
|
|
const pluginContext = context.getPluginContext();
|
|
const result = [];
|
|
const commonListrOptions = {
|
|
exitOnError: true,
|
|
};
|
|
const Listr = await new Promise(function (resolve) { resolve(_interopNamespace(require('listr'))); }).then(m => ('default' in m ? m.default : m));
|
|
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 = {};
|
|
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) {
|
|
generates[filename] = pluginHelpers.normalizeOutputParam(config.generates[filename]);
|
|
if (generates[filename].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);
|
|
}
|
|
}
|
|
outputSchemaAst = await context.loadSchema(schemaPointerMap);
|
|
outputSchema = graphql.parse(utils.printSchemaWithDirectives(outputSchemaAst));
|
|
}, filename),
|
|
},
|
|
{
|
|
title: 'Load GraphQL documents',
|
|
task: wrapTask(async () => {
|
|
debugLog(`[CLI] Loading Documents`);
|
|
const allDocuments = [...rootDocuments, ...outputSpecificDocuments];
|
|
const documents = await context.loadDocuments(allDocuments);
|
|
if (documents.length > 0) {
|
|
outputDocuments.push(...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 preset.buildGeneratesSection({
|
|
baseOutputDir: filename,
|
|
presetConfig: outputConfig.presetConfig || {},
|
|
plugins: normalizedPluginsArray,
|
|
schema: outputSchema,
|
|
schemaAst: outputSchemaAst,
|
|
documents: outputDocuments,
|
|
config: mergedConfig,
|
|
pluginMap,
|
|
pluginContext,
|
|
});
|
|
}
|
|
else {
|
|
outputs = [
|
|
{
|
|
filename,
|
|
plugins: normalizedPluginsArray,
|
|
schema: outputSchema,
|
|
schemaAst: outputSchemaAst,
|
|
documents: outputDocuments,
|
|
config: mergedConfig,
|
|
pluginMap,
|
|
pluginContext,
|
|
},
|
|
];
|
|
}
|
|
const process = async (outputArgs) => {
|
|
const output = await core.codegen(outputArgs);
|
|
result.push({
|
|
filename: outputArgs.filename,
|
|
content: output,
|
|
hooks: outputConfig.hooks || {},
|
|
});
|
|
};
|
|
await Promise.all(outputs.map(process));
|
|
}, 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,
|
|
// run 4 at once
|
|
concurrent: 4,
|
|
});
|
|
},
|
|
});
|
|
await listr.run();
|
|
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);
|
|
delete require.cache[fullPath];
|
|
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);
|
|
});
|
|
});
|
|
};
|
|
|
|
function writeSync(filepath, content) {
|
|
return fs.writeFileSync(filepath, content);
|
|
}
|
|
function readSync(filepath) {
|
|
return fs.readFileSync(filepath, 'utf-8');
|
|
}
|
|
function fileExists(filePath) {
|
|
try {
|
|
return fs.statSync(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 generate(input, saveToFile = true) {
|
|
const context = ensureContext(input);
|
|
const config = context.getConfig();
|
|
await lifecycleHooks(config.hooks).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)) {
|
|
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 lifecycleHooks(config.hooks).beforeAllFileWrite(generationResult.map(r => r.filename));
|
|
await Promise.all(generationResult.map(async (result) => {
|
|
const exists = fileExists(result.filename);
|
|
if (!shouldOverwrite(config, result.filename) && exists) {
|
|
return;
|
|
}
|
|
const content = result.content || '';
|
|
const currentHash = hash(content);
|
|
let previousHash = recentOutputHash.get(result.filename);
|
|
if (!previousHash && exists) {
|
|
previousHash = hash(readSync(result.filename));
|
|
}
|
|
if (previousHash && currentHash === previousHash) {
|
|
debugLog(`Skipping file (${result.filename}) writing due to indentical hash...`);
|
|
return;
|
|
}
|
|
if (content.length === 0) {
|
|
return;
|
|
}
|
|
recentOutputHash.set(result.filename, currentHash);
|
|
const basedir = path.dirname(result.filename);
|
|
await lifecycleHooks(result.hooks).beforeOneFileWrite(result.filename);
|
|
await lifecycleHooks(config.hooks).beforeOneFileWrite(result.filename);
|
|
mkdirp.sync(basedir);
|
|
const absolutePath = path.isAbsolute(result.filename)
|
|
? result.filename
|
|
: path.join(input.cwd || process.cwd(), result.filename);
|
|
writeSync(absolutePath, result.content);
|
|
await lifecycleHooks(result.hooks).afterOneFileWrite(result.filename);
|
|
await lifecycleHooks(config.hooks).afterOneFileWrite(result.filename);
|
|
}));
|
|
await lifecycleHooks(config.hooks).afterAllFileWrite(generationResult.map(r => r.filename));
|
|
return generationResult;
|
|
}
|
|
// watch mode
|
|
if (config.watch) {
|
|
return createWatcher(context, writeOutput);
|
|
}
|
|
const outputFiles = await executeCodegen(context);
|
|
await writeOutput(outputFiles);
|
|
lifecycleHooks(config.hooks).beforeDone();
|
|
return outputFiles;
|
|
}
|
|
function shouldOverwrite(config, outputPath) {
|
|
const globalValue = config.overwrite === undefined ? true : !!config.overwrite;
|
|
const outputConfig = config.generates[outputPath];
|
|
if (!outputConfig) {
|
|
debugLog(`Couldn't find a config of ${outputPath}`);
|
|
return globalValue;
|
|
}
|
|
if (isConfiguredOutput(outputConfig) && typeof outputConfig.overwrite === 'boolean') {
|
|
return outputConfig.overwrite;
|
|
}
|
|
return globalValue;
|
|
}
|
|
function isConfiguredOutput(output) {
|
|
return typeof output.plugins !== 'undefined';
|
|
}
|
|
|
|
// Parses config and writes it to a file
|
|
async function writeConfig(answers, config) {
|
|
const YAML = await new Promise(function (resolve) { resolve(_interopNamespace(require('json-to-pretty-yaml'))); }).then(m => ('default' in m ? m.default : m));
|
|
const ext = answers.config.toLocaleLowerCase().endsWith('.json') ? 'json' : 'yml';
|
|
const content = ext === 'json' ? JSON.stringify(config) : YAML.stringify(config);
|
|
const fullPath = path.resolve(process.cwd(), answers.config);
|
|
const relativePath = path.relative(process.cwd(), answers.config);
|
|
fs.writeFileSync(fullPath, content, {
|
|
encoding: 'utf-8',
|
|
});
|
|
return {
|
|
relativePath,
|
|
fullPath,
|
|
};
|
|
}
|
|
// Updates package.json (script and plugins as dependencies)
|
|
async function writePackage(answers, configLocation) {
|
|
// script
|
|
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
|
const pkgContent = fs.readFileSync(pkgPath, {
|
|
encoding: 'utf-8',
|
|
});
|
|
const pkg = JSON.parse(pkgContent);
|
|
const { indent } = detectIndent(pkgContent);
|
|
if (!pkg.scripts) {
|
|
pkg.scripts = {};
|
|
}
|
|
pkg.scripts[answers.script] = `graphql-codegen --config ${configLocation}`;
|
|
// plugin
|
|
if (!pkg.devDependencies) {
|
|
pkg.devDependencies = {};
|
|
}
|
|
await Promise.all(answers.plugins.map(async (plugin) => {
|
|
pkg.devDependencies[plugin.package] = await getLatestVersion(plugin.package);
|
|
}));
|
|
if (answers.introspection) {
|
|
pkg.devDependencies['@graphql-codegen/introspection'] = await getLatestVersion('@graphql-codegen/introspection');
|
|
}
|
|
pkg.devDependencies['@graphql-codegen/cli'] = await getLatestVersion('@graphql-codegen/cli');
|
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, indent));
|
|
}
|
|
function bold(str) {
|
|
return chalk.bold(str);
|
|
}
|
|
function grey(str) {
|
|
return chalk.grey(str);
|
|
}
|
|
function italic(str) {
|
|
return chalk.italic(str);
|
|
}
|
|
|
|
var Tags;
|
|
(function (Tags) {
|
|
Tags["browser"] = "Browser";
|
|
Tags["node"] = "Node";
|
|
Tags["typescript"] = "TypeScript";
|
|
Tags["flow"] = "Flow";
|
|
Tags["angular"] = "Angular";
|
|
Tags["stencil"] = "Stencil";
|
|
Tags["react"] = "React";
|
|
Tags["vue"] = "Vue";
|
|
})(Tags || (Tags = {}));
|
|
|
|
const plugins = [
|
|
{
|
|
name: `TypeScript ${italic('(required by other typescript plugins)')}`,
|
|
package: '@graphql-codegen/typescript',
|
|
value: 'typescript',
|
|
pathInRepo: 'typescript/typescript',
|
|
available: hasTag(Tags.typescript),
|
|
shouldBeSelected: tags => oneOf(tags, Tags.angular, Tags.stencil) || allOf(tags, Tags.typescript, Tags.react) || noneOf(tags, Tags.flow),
|
|
defaultExtension: '.ts',
|
|
},
|
|
{
|
|
name: `TypeScript Operations ${italic('(operations and fragments)')}`,
|
|
package: '@graphql-codegen/typescript-operations',
|
|
value: 'typescript-operations',
|
|
pathInRepo: 'typescript/operations',
|
|
available: tags => allOf(tags, Tags.browser, Tags.typescript),
|
|
shouldBeSelected: tags => oneOf(tags, Tags.angular, Tags.stencil) || allOf(tags, Tags.typescript, Tags.react),
|
|
defaultExtension: '.ts',
|
|
},
|
|
{
|
|
name: `TypeScript Resolvers ${italic('(strongly typed resolve functions)')}`,
|
|
package: '@graphql-codegen/typescript-resolvers',
|
|
value: 'typescript-resolvers',
|
|
pathInRepo: 'typescript/resolvers',
|
|
available: tags => allOf(tags, Tags.node, Tags.typescript),
|
|
shouldBeSelected: tags => noneOf(tags, Tags.flow),
|
|
defaultExtension: '.ts',
|
|
},
|
|
{
|
|
name: `Flow ${italic('(required by other flow plugins)')}`,
|
|
package: '@graphql-codegen/flow',
|
|
value: 'flow',
|
|
pathInRepo: 'flow/flow',
|
|
available: hasTag(Tags.flow),
|
|
shouldBeSelected: tags => noneOf(tags, Tags.typescript),
|
|
defaultExtension: '.js',
|
|
},
|
|
{
|
|
name: `Flow Operations ${italic('(operations and fragments)')}`,
|
|
package: '@graphql-codegen/flow-operations',
|
|
value: 'flow-operations',
|
|
pathInRepo: 'flow/operations',
|
|
available: tags => allOf(tags, Tags.browser, Tags.flow),
|
|
shouldBeSelected: tags => noneOf(tags, Tags.typescript),
|
|
defaultExtension: '.js',
|
|
},
|
|
{
|
|
name: `Flow Resolvers ${italic('(strongly typed resolve functions)')}`,
|
|
package: '@graphql-codegen/flow-resolvers',
|
|
value: 'flow-resolvers',
|
|
pathInRepo: 'flow/resolvers',
|
|
available: tags => allOf(tags, Tags.node, Tags.flow),
|
|
shouldBeSelected: tags => noneOf(tags, Tags.typescript),
|
|
defaultExtension: '.js',
|
|
},
|
|
{
|
|
name: `TypeScript Apollo Angular ${italic('(typed GQL services)')}`,
|
|
package: '@graphql-codegen/typescript-apollo-angular',
|
|
value: 'typescript-apollo-angular',
|
|
pathInRepo: 'typescript/apollo-angular',
|
|
available: hasTag(Tags.angular),
|
|
shouldBeSelected: () => true,
|
|
defaultExtension: '.js',
|
|
},
|
|
{
|
|
name: `TypeScript Vue Apollo ${italic('(typed composition functions)')}`,
|
|
package: '@graphql-codegen/typescript-vue-apollo',
|
|
value: 'typescript-vue-apollo',
|
|
pathInRepo: 'typescript/vue-apollo',
|
|
available: tags => allOf(tags, Tags.vue, Tags.typescript),
|
|
shouldBeSelected: () => true,
|
|
defaultExtension: '.ts',
|
|
},
|
|
{
|
|
name: `TypeScript React Apollo ${italic('(typed components and HOCs)')}`,
|
|
package: '@graphql-codegen/typescript-react-apollo',
|
|
value: 'typescript-react-apollo',
|
|
pathInRepo: 'typescript/react-apollo',
|
|
available: tags => allOf(tags, Tags.react, Tags.typescript),
|
|
shouldBeSelected: () => true,
|
|
defaultExtension: '.tsx',
|
|
},
|
|
{
|
|
name: `TypeScript Stencil Apollo ${italic('(typed components)')}`,
|
|
package: '@graphql-codegen/typescript-stencil-apollo',
|
|
value: 'typescript-stencil-apollo',
|
|
pathInRepo: 'typescript/stencil-apollo',
|
|
available: hasTag(Tags.stencil),
|
|
shouldBeSelected: () => true,
|
|
defaultExtension: '.tsx',
|
|
},
|
|
{
|
|
name: `TypeScript MongoDB ${italic('(typed MongoDB objects)')}`,
|
|
package: '@graphql-codegen/typescript-mongodb',
|
|
value: 'typescript-mongodb',
|
|
pathInRepo: 'typescript/mongodb',
|
|
available: tags => allOf(tags, Tags.node, Tags.typescript),
|
|
shouldBeSelected: () => false,
|
|
defaultExtension: '.ts',
|
|
},
|
|
{
|
|
name: `TypeScript GraphQL files modules ${italic('(declarations for .graphql files)')}`,
|
|
package: '@graphql-codegen/typescript-graphql-files-modules',
|
|
value: 'typescript-graphql-files-modules',
|
|
pathInRepo: 'typescript/graphql-files-modules',
|
|
available: tags => allOf(tags, Tags.browser, Tags.typescript),
|
|
shouldBeSelected: () => false,
|
|
defaultExtension: '.ts',
|
|
},
|
|
{
|
|
name: `TypeScript GraphQL document nodes ${italic('(embedded GraphQL document)')}`,
|
|
package: '@graphql-codegen/typescript-document-nodes',
|
|
value: 'typescript-document-nodes',
|
|
pathInRepo: 'typescript/document-nodes',
|
|
available: tags => allOf(tags, Tags.typescript),
|
|
shouldBeSelected: () => false,
|
|
defaultExtension: '.ts',
|
|
},
|
|
{
|
|
name: `Introspection Fragment Matcher ${italic('(for Apollo Client)')}`,
|
|
package: '@graphql-codegen/fragment-matcher',
|
|
value: 'fragment-matcher',
|
|
pathInRepo: 'other/fragment-matcher',
|
|
available: hasTag(Tags.browser),
|
|
shouldBeSelected: () => false,
|
|
defaultExtension: '.ts',
|
|
},
|
|
{
|
|
name: `Urql Introspection ${italic('(for Urql Client)')}`,
|
|
package: '@graphql-codegen/urql-introspection',
|
|
value: 'urql-introspection',
|
|
pathInRepo: 'other/urql-introspection',
|
|
available: hasTag(Tags.browser),
|
|
shouldBeSelected: () => false,
|
|
defaultExtension: '.ts',
|
|
},
|
|
];
|
|
function hasTag(tag) {
|
|
return (tags) => tags.includes(tag);
|
|
}
|
|
function oneOf(list, ...items) {
|
|
return list.some(i => items.includes(i));
|
|
}
|
|
function noneOf(list, ...items) {
|
|
return !list.some(i => items.includes(i));
|
|
}
|
|
function allOf(list, ...items) {
|
|
return items.every(i => list.includes(i));
|
|
}
|
|
|
|
function getQuestions(possibleTargets) {
|
|
return [
|
|
{
|
|
type: 'checkbox',
|
|
name: 'targets',
|
|
message: `What type of application are you building?`,
|
|
choices: getApplicationTypeChoices(possibleTargets),
|
|
validate: ((targets) => targets.length > 0),
|
|
},
|
|
{
|
|
type: 'input',
|
|
name: 'schema',
|
|
message: 'Where is your schema?:',
|
|
suffix: grey(' (path or url)'),
|
|
default: 'http://localhost:4000',
|
|
validate: (str) => str.length > 0,
|
|
},
|
|
{
|
|
type: 'input',
|
|
name: 'documents',
|
|
message: 'Where are your operations and fragments?:',
|
|
when: answers => {
|
|
// flatten targets
|
|
// I can't find an API in Inquirer that would do that
|
|
answers.targets = normalizeTargets(answers.targets);
|
|
return answers.targets.includes(Tags.browser);
|
|
},
|
|
default: 'src/**/*.graphql',
|
|
validate: (str) => str.length > 0,
|
|
},
|
|
{
|
|
type: 'checkbox',
|
|
name: 'plugins',
|
|
message: 'Pick plugins:',
|
|
choices: getPluginChoices,
|
|
validate: ((plugins) => plugins.length > 0),
|
|
},
|
|
{
|
|
type: 'input',
|
|
name: 'output',
|
|
message: 'Where to write the output:',
|
|
default: getOutputDefaultValue,
|
|
validate: (str) => str.length > 0,
|
|
},
|
|
{
|
|
type: 'confirm',
|
|
name: 'introspection',
|
|
message: 'Do you want to generate an introspection file?',
|
|
},
|
|
{
|
|
type: 'input',
|
|
name: 'config',
|
|
message: 'How to name the config file?',
|
|
default: 'codegen.yml',
|
|
validate: (str) => {
|
|
const isNotEmpty = str.length > 0;
|
|
const hasCorrectExtension = ['json', 'yml', 'yaml'].some(ext => str.toLocaleLowerCase().endsWith(`.${ext}`));
|
|
return isNotEmpty && hasCorrectExtension;
|
|
},
|
|
},
|
|
{
|
|
type: 'input',
|
|
name: 'script',
|
|
message: 'What script in package.json should run the codegen?',
|
|
validate: (str) => str.length > 0,
|
|
},
|
|
];
|
|
}
|
|
function getApplicationTypeChoices(possibleTargets) {
|
|
function withFlowOrTypescript(tags) {
|
|
if (possibleTargets.TypeScript) {
|
|
tags.push(Tags.typescript);
|
|
}
|
|
else if (possibleTargets.Flow) {
|
|
tags.push(Tags.flow);
|
|
}
|
|
else {
|
|
tags.push(Tags.flow, Tags.typescript);
|
|
}
|
|
return tags;
|
|
}
|
|
return [
|
|
{
|
|
name: 'Backend - API or server',
|
|
key: 'backend',
|
|
value: withFlowOrTypescript([Tags.node]),
|
|
checked: possibleTargets.Node,
|
|
},
|
|
{
|
|
name: 'Application built with Angular',
|
|
key: 'angular',
|
|
value: [Tags.angular, Tags.browser, Tags.typescript],
|
|
checked: possibleTargets.Angular,
|
|
},
|
|
{
|
|
name: 'Application built with React',
|
|
key: 'react',
|
|
value: withFlowOrTypescript([Tags.react, Tags.browser]),
|
|
checked: possibleTargets.React,
|
|
},
|
|
{
|
|
name: 'Application built with Stencil',
|
|
key: 'stencil',
|
|
value: [Tags.stencil, Tags.browser, Tags.typescript],
|
|
checked: possibleTargets.Stencil,
|
|
},
|
|
{
|
|
name: 'Application built with other framework or vanilla JS',
|
|
key: 'client',
|
|
value: [Tags.browser, Tags.typescript, Tags.flow],
|
|
checked: possibleTargets.Browser && !possibleTargets.Angular && !possibleTargets.React && !possibleTargets.Stencil,
|
|
},
|
|
];
|
|
}
|
|
function getPluginChoices(answers) {
|
|
return plugins
|
|
.filter(p => p.available(answers.targets))
|
|
.map(p => {
|
|
return {
|
|
name: p.name,
|
|
value: p,
|
|
checked: p.shouldBeSelected(answers.targets),
|
|
};
|
|
});
|
|
}
|
|
function normalizeTargets(targets) {
|
|
return [].concat(...targets);
|
|
}
|
|
function getOutputDefaultValue(answers) {
|
|
if (answers.plugins.some(plugin => plugin.defaultExtension === '.tsx')) {
|
|
return 'src/generated/graphql.tsx';
|
|
}
|
|
else if (answers.plugins.some(plugin => plugin.defaultExtension === '.ts')) {
|
|
return 'src/generated/graphql.ts';
|
|
}
|
|
else {
|
|
return 'src/generated/graphql.js';
|
|
}
|
|
}
|
|
|
|
async function guessTargets() {
|
|
const pkg = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), 'package.json'), {
|
|
encoding: 'utf-8',
|
|
}));
|
|
const dependencies = Object.keys({
|
|
...pkg.dependencies,
|
|
...pkg.devDependencies,
|
|
});
|
|
return {
|
|
[Tags.angular]: isAngular(dependencies),
|
|
[Tags.react]: isReact(dependencies),
|
|
[Tags.stencil]: isStencil(dependencies),
|
|
[Tags.vue]: isVue(dependencies),
|
|
[Tags.browser]: false,
|
|
[Tags.node]: false,
|
|
[Tags.typescript]: isTypescript(dependencies),
|
|
[Tags.flow]: isFlow(dependencies),
|
|
};
|
|
}
|
|
function isAngular(dependencies) {
|
|
return dependencies.includes('@angular/core');
|
|
}
|
|
function isReact(dependencies) {
|
|
return dependencies.includes('react');
|
|
}
|
|
function isStencil(dependencies) {
|
|
return dependencies.includes('@stencil/core');
|
|
}
|
|
function isVue(dependencies) {
|
|
return dependencies.includes('vue') || dependencies.includes('nuxt');
|
|
}
|
|
function isTypescript(dependencies) {
|
|
return dependencies.includes('typescript');
|
|
}
|
|
function isFlow(dependencies) {
|
|
return dependencies.includes('flow');
|
|
}
|
|
|
|
function log$1(...msgs) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(...msgs);
|
|
}
|
|
async function init() {
|
|
log$1(`
|
|
Welcome to ${bold('GraphQL Code Generator')}!
|
|
Answer few questions and we will setup everything for you.
|
|
`);
|
|
const possibleTargets = await guessTargets();
|
|
const answers = await inquirer.prompt(getQuestions(possibleTargets));
|
|
// define config
|
|
const config = {
|
|
overwrite: true,
|
|
schema: answers.schema,
|
|
documents: answers.targets.includes(Tags.browser) ? answers.documents : null,
|
|
generates: {
|
|
[answers.output]: {
|
|
plugins: answers.plugins.map(p => p.value),
|
|
},
|
|
},
|
|
};
|
|
// introspection
|
|
if (answers.introspection) {
|
|
addIntrospection(config);
|
|
}
|
|
// config file
|
|
const { relativePath } = await writeConfig(answers, config);
|
|
log$1(`Fetching latest versions of selected plugins...`);
|
|
// write package.json
|
|
await writePackage(answers, relativePath);
|
|
// Emit status to the terminal
|
|
log$1(`
|
|
Config file generated at ${bold(relativePath)}
|
|
|
|
${bold('$ npm install')}
|
|
|
|
To install the plugins.
|
|
|
|
${bold(`$ npm run ${answers.script}`)}
|
|
|
|
To run GraphQL Code Generator.
|
|
`);
|
|
}
|
|
// adds an introspection to `generates`
|
|
function addIntrospection(config) {
|
|
config.generates['./graphql.schema.json'] = {
|
|
plugins: ['introspection'],
|
|
};
|
|
}
|
|
|
|
function runCli(cmd) {
|
|
ensureGraphQlPackage();
|
|
switch (cmd) {
|
|
case 'init':
|
|
return init();
|
|
default: {
|
|
return createContext().then(context => {
|
|
return generate(context).catch(async (error) => {
|
|
await lifecycleHooks(context.getConfig().hooks).onError(error.toString());
|
|
throw error;
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
function ensureGraphQlPackage() {
|
|
try {
|
|
require('graphql');
|
|
}
|
|
catch (e) {
|
|
throw new pluginHelpers.DetailedError(`Unable to load "graphql" package. Please make sure to install "graphql" as a dependency!`, `
|
|
To install "graphql", run:
|
|
yarn add graphql
|
|
Or, with NPM:
|
|
npm install --save graphql
|
|
`);
|
|
}
|
|
}
|
|
|
|
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
|
|
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 [, , cmd] = process.argv;
|
|
runCli(cmd)
|
|
.then(() => {
|
|
process.exit(0);
|
|
})
|
|
.catch(error => {
|
|
cliError(error);
|
|
});
|