@graphql-mesh/cli
Version:
1,096 lines (1,076 loc) β’ 46.8 kB
JavaScript
import { processConfig } from '@graphql-mesh/config';
import { jsonSchema } from '@graphql-mesh/types';
import { defaultImportFn, DefaultLogger, loadYaml, writeFile, pathExists, writeJSON, loadFromModuleExportExpression, rmdirs } from '@graphql-mesh/utils';
import Ajv from 'ajv';
import { cosmiconfig, defaultLoaders } from 'cosmiconfig';
import { path, process, fs } from '@graphql-mesh/cross-helpers';
import { AggregateError, getRootTypeMap, buildOperationNodeForField, parseGraphQLSDL, printSchemaWithDirectives } from '@graphql-tools/utils';
import { getMesh } from '@graphql-mesh/runtime';
import * as tsBasePlugin from '@graphql-codegen/typescript';
import { TsVisitor } from '@graphql-codegen/typescript';
import * as tsResolversPlugin from '@graphql-codegen/typescript-resolvers';
import { print, Kind } from 'graphql';
import { codegen } from '@graphql-codegen/core';
import { pascalCase } from 'pascal-case';
import * as tsOperationsPlugin from '@graphql-codegen/typescript-operations';
import * as typescriptGenericSdk from '@graphql-codegen/typescript-generic-sdk';
import * as typedDocumentNodePlugin from '@graphql-codegen/typed-document-node';
import ts from 'typescript';
import JSON5 from 'json5';
import cluster from 'cluster';
import { platform, release, cpus } from 'os';
import 'json-bigint-patch';
import { createServer as createServer$1 } from 'http';
import ws from 'ws';
import { createServer } from 'https';
import open from 'open';
import { useServer } from 'graphql-ws/lib/use/ws';
import dnscache from 'dnscache';
import { createMeshHTTPHandler } from '@graphql-mesh/http';
import { MeshStore, FsStoreStorageAdapter } from '@graphql-mesh/store';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { register } from 'ts-node';
import { register as register$1 } from 'tsconfig-paths';
import { config } from 'dotenv';
function validateConfig(config, filepath, initialLoggerPrefix, throwOnInvalidConfig = false) {
const ajv = new Ajv({
strict: false,
});
jsonSchema.$schema = undefined;
const isValid = ajv.validate(jsonSchema, config);
if (!isValid) {
if (throwOnInvalidConfig) {
const aggregateError = new AggregateError(ajv.errors.map(e => {
const error = new Error(e.message);
error.stack += `\n at ${filepath}:0:0`;
return error;
}), 'Configuration file is not valid');
throw aggregateError;
}
const logger = new DefaultLogger(initialLoggerPrefix).child('config');
logger.warn('Configuration file is not valid!');
logger.warn("This is just a warning! It doesn't have any effects on runtime.");
ajv.errors.forEach(error => {
logger.warn(error.message);
});
}
}
async function findAndParseConfig(options) {
const { configName = 'mesh', dir: configDir = '', initialLoggerPrefix = 'πΈοΈ Mesh', importFn, ...restOptions } = options || {};
const dir = path.isAbsolute(configDir) ? configDir : path.join(process.cwd(), configDir);
const explorer = cosmiconfig(configName, {
searchPlaces: [
'package.json',
`.${configName}rc`,
`.${configName}rc.json`,
`.${configName}rc.yaml`,
`.${configName}rc.yml`,
`.${configName}rc.js`,
`.${configName}rc.ts`,
`.${configName}rc.cjs`,
`${configName}.config.js`,
`${configName}.config.cjs`,
],
loaders: {
'.json': customLoader('json', importFn, initialLoggerPrefix),
'.yaml': customLoader('yaml', importFn, initialLoggerPrefix),
'.yml': customLoader('yaml', importFn, initialLoggerPrefix),
'.js': customLoader('js', importFn, initialLoggerPrefix),
'.ts': customLoader('js', importFn, initialLoggerPrefix),
noExt: customLoader('yaml', importFn, initialLoggerPrefix),
},
});
const results = await explorer.search(dir);
if (!results) {
throw new Error(`No ${configName} config file found in "${dir}"!`);
}
const config = results.config;
validateConfig(config, results.filepath, initialLoggerPrefix);
return processConfig(config, { dir, initialLoggerPrefix, importFn, ...restOptions });
}
function customLoader(ext, importFn = defaultImportFn, initialLoggerPrefix = 'πΈοΈ Mesh') {
const logger = new DefaultLogger(initialLoggerPrefix).child('config');
function loader(filepath, content) {
if (process.env) {
content = content.replace(/\$\{(.*?)\}/g, (_, variable) => {
let varName = variable;
let defaultValue = '';
if (variable.includes(':')) {
const spl = variable.split(':');
varName = spl.shift();
defaultValue = spl.join(':');
}
return process.env[varName] || defaultValue;
});
}
if (ext === 'json') {
return defaultLoaders['.json'](filepath, content);
}
if (ext === 'yaml') {
return loadYaml(filepath, content, logger);
}
if (ext === 'js') {
return importFn(filepath);
}
}
return loader;
}
function generateOperations(schema, options) {
var _a;
const sources = [];
const rootTypeMap = getRootTypeMap(schema);
for (const [operationType, rootType] of rootTypeMap) {
const fieldMap = rootType.getFields();
for (const fieldName in fieldMap) {
const operationNode = buildOperationNodeForField({
schema,
kind: operationType,
field: fieldName,
depthLimit: options.selectionSetDepth,
});
const defaultName = `operation_${sources.length}`;
const virtualFileName = ((_a = operationNode.name) === null || _a === void 0 ? void 0 : _a.value) || defaultName;
const rawSDL = print(operationNode);
const source = parseGraphQLSDL(`${virtualFileName}.graphql`, rawSDL);
sources.push(source);
}
}
return sources;
}
const unifiedContextIdentifier = 'MeshContext';
class CodegenHelpers extends TsVisitor {
getTypeToUse(namedType) {
if (this.scalars[namedType.name.value]) {
return this._getScalar(namedType.name.value);
}
return this._getTypeForNode(namedType);
}
}
function buildSignatureBasedOnRootFields(codegenHelpers, type) {
if (!type) {
return {};
}
const fields = type.getFields();
const operationMap = {};
for (const fieldName in fields) {
const field = fields[fieldName];
const argsExists = field.args && field.args.length > 0;
const argsName = argsExists ? `${type.name}${field.name}Args` : '{}';
const parentTypeNode = {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: type.name,
},
};
operationMap[fieldName] = ` /** ${field.description} **/\n ${field.name}: InContextSdkMethod<${codegenHelpers.getTypeToUse(parentTypeNode)}['${fieldName}'], ${argsName}, ${unifiedContextIdentifier}>`;
}
return operationMap;
}
async function generateTypesForApi(options) {
const config = {
skipTypename: true,
namingConvention: 'keep',
enumsAsTypes: true,
ignoreEnumValuesFromSchema: true,
};
const baseTypes = await codegen({
filename: options.name + '_types.ts',
documents: [],
config,
schemaAst: options.schema,
schema: undefined,
skipDocumentsValidation: true,
plugins: [
{
typescript: {},
},
],
pluginMap: {
typescript: tsBasePlugin,
},
});
const codegenHelpers = new CodegenHelpers(options.schema, config, {});
const namespace = pascalCase(`${options.name}Types`);
const queryOperationMap = buildSignatureBasedOnRootFields(codegenHelpers, options.schema.getQueryType());
const mutationOperationMap = buildSignatureBasedOnRootFields(codegenHelpers, options.schema.getMutationType());
const subscriptionsOperationMap = buildSignatureBasedOnRootFields(codegenHelpers, options.schema.getSubscriptionType());
const codeAst = `
import { InContextSdkMethod } from '@graphql-mesh/types';
import { MeshContext } from '@graphql-mesh/runtime';
export namespace ${namespace} {
${baseTypes}
export type QuerySdk = {
${Object.values(queryOperationMap).join(',\n')}
};
export type MutationSdk = {
${Object.values(mutationOperationMap).join(',\n')}
};
export type SubscriptionSdk = {
${Object.values(subscriptionsOperationMap).join(',\n')}
};
export type Context = {
[${JSON.stringify(options.name)}]: { Query: QuerySdk, Mutation: MutationSdk, Subscription: SubscriptionSdk },
${Object.keys(options.contextVariables)
.map(key => `[${JSON.stringify(key)}]: ${options.contextVariables[key]}`)
.join(',\n')}
};
}
`;
return {
identifier: namespace,
codeAst,
};
}
const BASEDIR_ASSIGNMENT_COMMENT = `/* BASEDIR_ASSIGNMENT */`;
async function generateTsArtifacts({ unifiedSchema, rawSources, mergerType = 'stitching', documents, flattenTypes, importedModulesSet, baseDir, meshConfigImportCodes, meshConfigCodes, logger, sdkConfig, fileType, codegenConfig = {}, }, cliParams) {
var _a, _b, _c;
const artifactsDir = path.join(baseDir, cliParams.artifactsDir);
logger.info('Generating index file in TypeScript');
for (const rawSource of rawSources) {
const transformedSchema = unifiedSchema.extensions.sourceMap.get(rawSource);
const sdl = printSchemaWithDirectives(transformedSchema);
await writeFile(path.join(artifactsDir, `sources/${rawSource.name}/schema.graphql`), sdl);
}
const documentsInput = (sdkConfig === null || sdkConfig === void 0 ? void 0 : sdkConfig.generateOperations)
? generateOperations(unifiedSchema, sdkConfig.generateOperations)
: documents;
const pluginsInput = [
{
typescript: {},
},
{
resolvers: {},
},
{
contextSdk: {},
},
];
if (documentsInput.length) {
pluginsInput.push({
typescriptOperations: {},
}, {
typedDocumentNode: {},
}, {
typescriptGenericSdk: {
documentMode: 'external',
importDocumentNodeExternallyFrom: 'NOWHERE',
},
});
}
const codegenOutput = '// @ts-nocheck\n' +
(await codegen({
filename: 'types.ts',
documents: documentsInput,
config: {
skipTypename: true,
flattenGeneratedTypes: flattenTypes,
onlyOperationTypes: flattenTypes,
preResolveTypes: flattenTypes,
namingConvention: 'keep',
documentMode: 'graphQLTag',
gqlImport: '@graphql-mesh/utils#gql',
enumsAsTypes: true,
ignoreEnumValuesFromSchema: true,
useIndexSignature: true,
noSchemaStitching: false,
contextType: unifiedContextIdentifier,
federation: mergerType === 'federation',
...codegenConfig,
},
schemaAst: unifiedSchema,
schema: undefined,
// skipDocumentsValidation: true,
pluginMap: {
typescript: tsBasePlugin,
typescriptOperations: tsOperationsPlugin,
typedDocumentNode: typedDocumentNodePlugin,
typescriptGenericSdk,
resolvers: tsResolversPlugin,
contextSdk: {
plugin: async () => {
const importCodes = new Set([
...meshConfigImportCodes,
`import { getMesh, ExecuteMeshFn, SubscribeMeshFn, MeshContext as BaseMeshContext, MeshInstance } from '@graphql-mesh/runtime';`,
`import { MeshStore, FsStoreStorageAdapter } from '@graphql-mesh/store';`,
`import { path as pathModule } from '@graphql-mesh/cross-helpers';`,
`import { ImportFn } from '@graphql-mesh/types';`,
]);
const results = await Promise.all(rawSources.map(async (source) => {
const sourceMap = unifiedSchema.extensions.sourceMap;
const sourceSchema = sourceMap.get(source);
const { identifier, codeAst } = await generateTypesForApi({
schema: sourceSchema,
name: source.name,
contextVariables: source.contextVariables,
});
if (codeAst) {
const content = '// @ts-nocheck\n' + codeAst;
await writeFile(path.join(artifactsDir, `sources/${source.name}/types.ts`), content);
}
if (identifier) {
importCodes.add(`import type { ${identifier} } from './sources/${source.name}/types';`);
}
return {
identifier,
codeAst,
};
}));
const contextType = `export type ${unifiedContextIdentifier} = ${results
.map(r => `${r === null || r === void 0 ? void 0 : r.identifier}.Context`)
.filter(Boolean)
.join(' & ')} & BaseMeshContext;`;
let meshMethods = `
${BASEDIR_ASSIGNMENT_COMMENT}
const importFn: ImportFn = <T>(moduleId: string) => {
const relativeModuleId = (pathModule.isAbsolute(moduleId) ? pathModule.relative(baseDir, moduleId) : moduleId).split('\\\\').join('/').replace(baseDir + '/', '');
switch(relativeModuleId) {${[...importedModulesSet]
.map(importedModuleName => {
let moduleMapProp = importedModuleName;
let importPath = importedModuleName;
if (importPath.startsWith('.')) {
importPath = path.join(baseDir, importPath);
}
if (path.isAbsolute(importPath)) {
moduleMapProp = path.relative(baseDir, importedModuleName).split('\\').join('/');
importPath = `./${path.relative(artifactsDir, importedModuleName).split('\\').join('/')}`;
}
return `
case ${JSON.stringify(moduleMapProp)}:
return import(${JSON.stringify(importPath)}) as T;
`;
})
.join('')}
default:
return Promise.reject(new Error(\`Cannot find module '\${relativeModuleId}'.\`));
}
};
const rootStore = new MeshStore('${cliParams.artifactsDir}', new FsStoreStorageAdapter({
cwd: baseDir,
importFn,
fileType: ${JSON.stringify(fileType)},
}), {
readonly: true,
validate: false
});
${[...meshConfigCodes].join('\n')}
let meshInstance$: Promise<MeshInstance> | undefined;
export function ${cliParams.builtMeshFactoryName}(): Promise<MeshInstance> {
if (meshInstance$ == null) {
meshInstance$ = getMeshOptions().then(meshOptions => getMesh(meshOptions)).then(mesh => {
const id = mesh.pubsub.subscribe('destroy', () => {
meshInstance$ = undefined;
mesh.pubsub.unsubscribe(id);
});
return mesh;
});
}
return meshInstance$;
}
export const execute: ExecuteMeshFn = (...args) => ${cliParams.builtMeshFactoryName}().then(({ execute }) => execute(...args));
export const subscribe: SubscribeMeshFn = (...args) => ${cliParams.builtMeshFactoryName}().then(({ subscribe }) => subscribe(...args));`;
if (documentsInput.length) {
meshMethods += `
export function ${cliParams.builtMeshSDKFactoryName}<TGlobalContext = any, TOperationContext = any>(globalContext?: TGlobalContext) {
const sdkRequester$ = ${cliParams.builtMeshFactoryName}().then(({ sdkRequesterFactory }) => sdkRequesterFactory(globalContext));
return getSdk<TOperationContext, TGlobalContext>((...args) => sdkRequester$.then(sdkRequester => sdkRequester(...args)));
}`;
}
return {
prepend: [[...importCodes].join('\n'), '\n\n'],
content: [contextType, meshMethods].join('\n\n'),
};
},
},
},
plugins: pluginsInput,
}))
.replace(`import * as Operations from 'NOWHERE';\n`, '')
.replace(`import { DocumentNode } from 'graphql';`, '');
const baseUrlAssignmentESM = `import { fileURLToPath } from '@graphql-mesh/utils';
const baseDir = pathModule.join(pathModule.dirname(fileURLToPath(import.meta.url)), '${path.relative(artifactsDir, baseDir)}');`;
const baseUrlAssignmentCJS = `const baseDir = pathModule.join(typeof __dirname === 'string' ? __dirname : '/', '${path.relative(artifactsDir, baseDir)}');`;
const tsFilePath = path.join(artifactsDir, 'index.ts');
const jobs = [];
const jsFilePath = path.join(artifactsDir, 'index.js');
const dtsFilePath = path.join(artifactsDir, 'index.d.ts');
const esmJob = (ext) => async () => {
logger.info('Writing index.ts for ESM to the disk.');
await writeFile(tsFilePath, codegenOutput.replace(BASEDIR_ASSIGNMENT_COMMENT, baseUrlAssignmentESM));
const esmJsFilePath = path.join(artifactsDir, `index.${ext}`);
if (await pathExists(esmJsFilePath)) {
await fs.promises.unlink(esmJsFilePath);
}
if (fileType !== 'ts') {
logger.info(`Compiling TS file as ES Module to "index.${ext}"`);
compileTS(tsFilePath, ts.ModuleKind.ESNext, [jsFilePath, dtsFilePath]);
if (ext === 'mjs') {
const mjsFilePath = path.join(artifactsDir, 'index.mjs');
await fs.promises.rename(jsFilePath, mjsFilePath);
}
logger.info('Deleting index.ts');
await fs.promises.unlink(tsFilePath);
}
};
const cjsJob = async () => {
logger.info('Writing index.ts for CJS to the disk.');
await writeFile(tsFilePath, codegenOutput.replace(BASEDIR_ASSIGNMENT_COMMENT, baseUrlAssignmentCJS));
if (await pathExists(jsFilePath)) {
await fs.promises.unlink(jsFilePath);
}
if (fileType !== 'ts') {
logger.info('Compiling TS file as CommonJS Module to `index.js`');
compileTS(tsFilePath, ts.ModuleKind.CommonJS, [jsFilePath, dtsFilePath]);
logger.info('Deleting index.ts');
await fs.promises.unlink(tsFilePath);
}
};
const packageJsonJob = (module) => () => writeJSON(path.join(artifactsDir, 'package.json'), {
name: 'mesh-artifacts',
private: true,
type: module,
main: 'index.js',
module: 'index.mjs',
sideEffects: false,
typings: 'index.d.ts',
typescript: {
definition: 'index.d.ts',
},
exports: {
'.': {
require: './index.js',
import: './index.mjs',
},
'./*': {
require: './*.js',
import: './*.mjs',
},
},
});
const tsConfigPath = path.join(baseDir, 'tsconfig.json');
if (await pathExists(tsConfigPath)) {
const tsConfigStr = await fs.promises.readFile(tsConfigPath, 'utf-8');
const tsConfig = JSON5.parse(tsConfigStr);
if ((_c = (_b = (_a = tsConfig === null || tsConfig === void 0 ? void 0 : tsConfig.compilerOptions) === null || _a === void 0 ? void 0 : _a.module) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === null || _c === void 0 ? void 0 : _c.startsWith('es')) {
jobs.push(esmJob('js'));
if (fileType !== 'ts') {
jobs.push(packageJsonJob('module'));
}
}
else {
jobs.push(cjsJob);
if (fileType !== 'ts') {
jobs.push(packageJsonJob('commonjs'));
}
}
}
else {
jobs.push(esmJob('mjs'));
if (fileType === 'js') {
jobs.push(packageJsonJob('module'));
}
else {
jobs.push(cjsJob);
jobs.push(packageJsonJob('commonjs'));
}
}
for (const job of jobs) {
await job();
}
}
function compileTS(tsFilePath, module, outputFilePaths) {
const options = {
target: ts.ScriptTarget.ESNext,
module,
sourceMap: false,
inlineSourceMap: false,
importHelpers: true,
allowSyntheticDefaultImports: true,
esModuleInterop: true,
declaration: true,
};
const host = ts.createCompilerHost(options);
const hostWriteFile = host.writeFile.bind(host);
host.writeFile = (fileName, ...rest) => {
if (outputFilePaths.some(f => path.normalize(f) === path.normalize(fileName))) {
return hostWriteFile(fileName, ...rest);
}
};
// Prepare and emit the d.ts files
const program = ts.createProgram([tsFilePath], options, host);
program.emit();
}
function handleFatalError(e, logger) {
logger.error(e);
if (process.env.JEST == null) {
process.exit(1);
}
}
/* eslint-disable import/no-nodejs-modules */
const terminateEvents = ['SIGINT', 'SIGTERM'];
function registerTerminateHandler(callback) {
for (const eventName of terminateEvents) {
process.on(eventName, () => callback(eventName));
}
}
function portSelectorFn(sources, logger) {
const port = sources.find(source => Boolean(source)) || 4000;
if (sources.filter(source => Boolean(source)).length > 1) {
const activeSources = [];
if (sources[0]) {
activeSources.push('CLI');
}
if (sources[1]) {
activeSources.push('serve configuration');
}
if (sources[2]) {
activeSources.push('environment variable');
}
logger.warn(`Multiple ports specified (${activeSources.join(', ')}), using ${port}`);
}
return port;
}
async function serveMesh({ baseDir, argsPort, getBuiltMesh, logger, rawServeConfig = {}, playgroundTitle }, cliParams) {
const { fork, port: configPort, hostname = platform() === 'win32' ||
// is WSL?
release().toLowerCase().includes('microsoft')
? '127.0.0.1'
: '0.0.0.0', sslCredentials, endpoint: graphqlPath = '/graphql', browser,
// TODO
// trustProxy = 'loopback',
} = rawServeConfig;
const port = portSelectorFn([argsPort, parseInt(configPort === null || configPort === void 0 ? void 0 : configPort.toString()), parseInt(process.env.PORT)], logger);
const protocol = sslCredentials ? 'https' : 'http';
const serverUrl = `${protocol}://${hostname}:${port}`;
if (!playgroundTitle) {
playgroundTitle = (rawServeConfig === null || rawServeConfig === void 0 ? void 0 : rawServeConfig.playgroundTitle) || cliParams.playgroundTitle;
}
if (!cluster.isWorker && Boolean(fork)) {
const forkNum = fork > 0 && typeof fork === 'number' ? fork : cpus().length;
for (let i = 0; i < forkNum; i++) {
const worker = cluster.fork();
registerTerminateHandler(eventName => worker.kill(eventName));
}
logger.info(`${cliParams.serveMessage}: ${serverUrl} in ${forkNum} forks`);
}
else {
logger.info(`Generating the unified schema...`);
const mesh$ = getBuiltMesh()
.then(mesh => {
dnscache({
enable: true,
cache: function CacheCtor({ ttl }) {
return {
get: (key, callback) => mesh.cache
.get(key)
.then(value => callback(null, value))
.catch(e => callback(e)),
set: (key, value, callback) => mesh.cache
.set(key, value, { ttl })
.then(() => callback())
.catch(e => callback(e)),
};
},
});
logger.info(`${cliParams.serveMessage}: ${serverUrl}`);
registerTerminateHandler(eventName => {
const eventLogger = logger.child(`${eventName} π`);
eventLogger.info(`Destroying the server`);
mesh.destroy();
});
return mesh;
})
.catch(e => handleFatalError(e, logger));
let httpServer;
const requestHandler = createMeshHTTPHandler({
baseDir,
getBuiltMesh,
rawServeConfig,
playgroundTitle,
});
if (sslCredentials) {
const [key, cert] = await Promise.all([
fs.promises.readFile(sslCredentials.key, 'utf-8'),
fs.promises.readFile(sslCredentials.cert, 'utf-8'),
]);
httpServer = createServer({ key, cert }, requestHandler);
}
else {
httpServer = createServer$1(requestHandler);
}
registerTerminateHandler(eventName => {
const eventLogger = logger.child(`${eventName}π`);
eventLogger.debug(`Stopping HTTP Server`);
httpServer.close(error => {
if (error) {
eventLogger.debug(`HTTP Server couldn't be stopped: `, error);
}
else {
eventLogger.debug(`HTTP Server has been stopped`);
}
});
});
const wsServer = new ws.Server({
path: graphqlPath,
server: httpServer,
});
registerTerminateHandler(eventName => {
const eventLogger = logger.child(`${eventName}π`);
eventLogger.debug(`Stopping WebSocket Server`);
wsServer.close(error => {
if (error) {
eventLogger.debug(`WebSocket Server couldn't be stopped: `, error);
}
else {
eventLogger.debug(`WebSocket Server has been stopped`);
}
});
});
const { dispose: stopGraphQLWSServer } = useServer({
onSubscribe: async ({ connectionParams, extra: { request } }, msg) => {
var _a;
// spread connectionParams.headers to upgrade request headers.
// we completely ignore the root connectionParams because
// [@graphql-tools/url-loader adds the headers inside the "headers" field](https://github.com/ardatan/graphql-tools/blob/9a13357c4be98038c645f6efb26f0584828177cf/packages/loaders/url/src/index.ts#L597)
for (const [key, value] of Object.entries((_a = connectionParams === null || connectionParams === void 0 ? void 0 : connectionParams.headers) !== null && _a !== void 0 ? _a : {})) {
// dont overwrite existing upgrade headers due to security reasons
if (!(key.toLowerCase() in request.headers)) {
request.headers[key.toLowerCase()] = value;
}
}
const { getEnveloped } = await mesh$;
const { schema, execute, subscribe, contextFactory, parse, validate } = getEnveloped({
// req object holds the Node request used for extracting the headers (see packages/runtime/src/get-mesh.ts)
req: request,
});
const args = {
schema,
operationName: msg.payload.operationName,
document: parse(msg.payload.query),
variableValues: msg.payload.variables,
contextValue: await contextFactory(),
execute,
subscribe,
};
const errors = validate(args.schema, args.document);
if (errors.length)
return errors;
return args;
},
execute: (args) => args.execute(args),
subscribe: (args) => args.subscribe(args),
}, wsServer);
registerTerminateHandler(eventName => {
const eventLogger = logger.child(`${eventName}π`);
eventLogger.debug(`Stopping GraphQL WS`);
Promise.resolve()
.then(() => stopGraphQLWSServer())
.then(() => {
eventLogger.debug(`GraphQL WS has been stopped`);
})
.catch(error => {
eventLogger.debug(`GraphQL WS couldn't be stopped: `, error);
});
});
httpServer
.listen(port, hostname, () => {
var _a;
const shouldntOpenBrowser = ((_a = process.env.NODE_ENV) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'production' || browser === false;
if (!shouldntOpenBrowser) {
open(serverUrl, typeof browser === 'string' ? { app: browser } : undefined).catch(() => { });
}
})
.on('error', handleFatalError);
return mesh$.then(mesh => ({
mesh,
httpServer,
logger,
}));
}
return null;
}
const DEFAULT_CLI_PARAMS = {
commandName: 'mesh',
initialLoggerPrefix: 'πΈοΈ Mesh',
configName: 'mesh',
artifactsDir: '.mesh',
serveMessage: 'Serving GraphQL Mesh',
playgroundTitle: 'GraphiQL Mesh',
builtMeshFactoryName: 'getBuiltMesh',
builtMeshSDKFactoryName: 'getMeshSDK',
devServerCommand: 'dev',
prodServerCommand: 'start',
buildArtifactsCommand: 'build',
sourceServerCommand: 'serve-source',
validateCommand: 'validate',
additionalPackagePrefixes: [],
};
async function graphqlMesh(cliParams = DEFAULT_CLI_PARAMS, args = hideBin(process.argv), cwdPath = process.cwd()) {
let baseDir = cwdPath;
let logger = new DefaultLogger(cliParams.initialLoggerPrefix);
return yargs(args)
.help()
.option('r', {
alias: 'require',
describe: 'Loads specific require.extensions before running the codegen and reading the configuration',
type: 'array',
default: [],
coerce: (externalModules) => Promise.all(externalModules.map(module => {
const localModulePath = path.resolve(baseDir, module);
const islocalModule = fs.existsSync(localModulePath);
return defaultImportFn(islocalModule ? localModulePath : module);
})),
})
.option('dir', {
describe: 'Modified the base directory to use for looking for ' + cliParams.configName + ' config file',
type: 'string',
default: baseDir,
coerce: dir => {
var _a;
if (path.isAbsolute(dir)) {
baseDir = dir;
}
else {
baseDir = path.resolve(cwdPath, dir);
}
const tsConfigPath = path.join(baseDir, 'tsconfig.json');
const tsConfigExists = fs.existsSync(tsConfigPath);
register({
transpileOnly: true,
typeCheck: false,
dir: baseDir,
require: ['graphql-import-node/register'],
compilerOptions: {
module: 'commonjs',
},
});
if (tsConfigExists) {
try {
const tsConfigStr = fs.readFileSync(tsConfigPath, 'utf-8');
const tsConfig = JSON5.parse(tsConfigStr);
if ((_a = tsConfig.compilerOptions) === null || _a === void 0 ? void 0 : _a.paths) {
register$1({
baseUrl: baseDir,
paths: tsConfig.compilerOptions.paths,
});
}
}
catch (e) {
logger.warn(`Unable to read TSConfig file ${tsConfigPath};\n`, e);
}
}
if (fs.existsSync(path.join(baseDir, '.env'))) {
config({
path: path.join(baseDir, '.env'),
});
}
},
})
.command(cliParams.devServerCommand, 'Serves a GraphQL server with GraphQL interface by building artifacts on the fly', builder => {
builder.option('port', {
type: 'number',
});
}, async (args) => {
var _a;
try {
const outputDir = path.join(baseDir, cliParams.artifactsDir);
process.env.NODE_ENV = 'development';
const meshConfig = await findAndParseConfig({
dir: baseDir,
artifactsDir: cliParams.artifactsDir,
configName: cliParams.configName,
additionalPackagePrefixes: cliParams.additionalPackagePrefixes,
initialLoggerPrefix: cliParams.initialLoggerPrefix,
});
logger = meshConfig.logger;
const meshInstance$ = getMesh(meshConfig);
// We already handle Mesh instance errors inside `serveMesh`
// eslint-disable-next-line @typescript-eslint/no-floating-promises
meshInstance$.then(({ schema }) => writeFile(path.join(outputDir, 'schema.graphql'), printSchemaWithDirectives(schema)).catch(e => logger.error(`An error occured while writing the schema file: `, e)));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
meshInstance$.then(({ schema, rawSources }) => generateTsArtifacts({
unifiedSchema: schema,
rawSources,
mergerType: meshConfig.merger.name,
documents: meshConfig.documents,
flattenTypes: false,
importedModulesSet: new Set(),
baseDir,
meshConfigImportCodes: new Set([
`import { findAndParseConfig } from '@graphql-mesh/cli';`,
`import { createMeshHTTPHandler, MeshHTTPHandler } from '@graphql-mesh/http';`,
]),
meshConfigCodes: new Set([
`
export function getMeshOptions() {
console.warn('WARNING: These artifacts are built for development mode. Please run "${cliParams.commandName} build" to build production artifacts');
return findAndParseConfig({
dir: baseDir,
artifactsDir: ${JSON.stringify(cliParams.artifactsDir)},
configName: ${JSON.stringify(cliParams.configName)},
additionalPackagePrefixes: ${JSON.stringify(cliParams.additionalPackagePrefixes)},
initialLoggerPrefix: ${JSON.stringify(cliParams.initialLoggerPrefix)},
});
}
export function createBuiltMeshHTTPHandler(): MeshHTTPHandler<MeshContext> {
return createMeshHTTPHandler<MeshContext>({
baseDir,
getBuiltMesh: ${cliParams.builtMeshFactoryName},
rawServeConfig: ${JSON.stringify(meshConfig.config.serve)},
})
}
`.trim(),
]),
logger,
sdkConfig: meshConfig.config.sdk,
fileType: 'ts',
codegenConfig: meshConfig.config.codegen,
}, cliParams).catch(e => {
logger.error(`An error occurred while building the artifacts: ${e.stack || e.message}`);
}));
const serveMeshOptions = {
baseDir,
argsPort: args.port,
getBuiltMesh: () => meshInstance$,
logger: meshConfig.logger.child('Server'),
rawServeConfig: meshConfig.config.serve,
};
if ((_a = meshConfig.config.serve) === null || _a === void 0 ? void 0 : _a.customServerHandler) {
const customServerHandler = await loadFromModuleExportExpression(meshConfig.config.serve.customServerHandler, {
defaultExportName: 'default',
cwd: baseDir,
importFn: defaultImportFn,
});
await customServerHandler(serveMeshOptions);
}
else {
await serveMesh(serveMeshOptions, cliParams);
}
}
catch (e) {
handleFatalError(e, logger);
}
})
.command(cliParams.prodServerCommand, 'Serves a GraphQL server with GraphQL interface based on your generated artifacts', builder => {
builder.option('port', {
type: 'number',
});
}, async (args) => {
try {
const builtMeshArtifactsPath = path.join(baseDir, cliParams.artifactsDir);
if (!(await pathExists(builtMeshArtifactsPath))) {
throw new Error(`Seems like you haven't build the artifacts yet to start production server! You need to build artifacts first with "${cliParams.commandName} build" command!`);
}
process.env.NODE_ENV = 'production';
const mainModule = path.join(builtMeshArtifactsPath, 'index');
const builtMeshArtifacts = await defaultImportFn(mainModule);
const getMeshOptions = await builtMeshArtifacts.getMeshOptions();
logger = getMeshOptions.logger;
const rawServeConfig = builtMeshArtifacts.rawServeConfig;
const serveMeshOptions = {
baseDir,
argsPort: args.port,
getBuiltMesh: () => getMesh(getMeshOptions),
logger: getMeshOptions.logger.child('Server'),
rawServeConfig,
};
if (rawServeConfig === null || rawServeConfig === void 0 ? void 0 : rawServeConfig.customServerHandler) {
const customServerHandler = await loadFromModuleExportExpression(rawServeConfig.customServerHandler, {
defaultExportName: 'default',
cwd: baseDir,
importFn: defaultImportFn,
});
await customServerHandler(serveMeshOptions);
}
else {
await serveMesh(serveMeshOptions, cliParams);
}
}
catch (e) {
handleFatalError(e, logger);
}
})
.command(cliParams.validateCommand, 'Validates artifacts', builder => { }, async (args) => {
let destroy;
try {
if (!(await pathExists(path.join(baseDir, cliParams.artifactsDir)))) {
throw new Error(`You cannot validate artifacts now because you don't have built artifacts yet! You need to build artifacts first with "${cliParams.commandName} build" command!`);
}
const store = new MeshStore(cliParams.artifactsDir, new FsStoreStorageAdapter({
cwd: baseDir,
importFn: defaultImportFn,
fileType: 'ts',
}), {
readonly: false,
validate: true,
});
logger.info(`Reading the configuration`);
const meshConfig = await findAndParseConfig({
dir: baseDir,
store,
importFn: defaultImportFn,
ignoreAdditionalResolvers: true,
artifactsDir: cliParams.artifactsDir,
configName: cliParams.configName,
additionalPackagePrefixes: cliParams.additionalPackagePrefixes,
initialLoggerPrefix: cliParams.initialLoggerPrefix,
});
logger = meshConfig.logger;
logger.info(`Generating the unified schema`);
const mesh = await getMesh(meshConfig);
logger.info(`Artifacts have been validated successfully`);
destroy = mesh === null || mesh === void 0 ? void 0 : mesh.destroy;
}
catch (e) {
handleFatalError(e, logger);
}
if (destroy) {
destroy();
}
})
.command(cliParams.buildArtifactsCommand, 'Builds artifacts', builder => {
builder.option('fileType', {
type: 'string',
choices: ['json', 'ts', 'js'],
default: 'ts',
});
builder.option('throwOnInvalidConfig', {
type: 'boolean',
default: false,
});
}, async (args) => {
try {
const outputDir = path.join(baseDir, cliParams.artifactsDir);
logger.info('Cleaning existing artifacts');
await rmdirs(outputDir);
const importedModulesSet = new Set();
const importPromises = [];
const importFn = (moduleId, noCache) => {
const importPromise = defaultImportFn(moduleId)
.catch(e => {
if (e.message.includes('getter')) {
return e;
}
else {
throw e;
}
})
.then(m => {
if (!noCache) {
importedModulesSet.add(moduleId);
}
return m;
});
importPromises.push(importPromise.catch(() => { }));
return importPromise;
};
await Promise.all(importPromises);
const store = new MeshStore(cliParams.artifactsDir, new FsStoreStorageAdapter({
cwd: baseDir,
importFn,
fileType: args.fileType,
}), {
readonly: false,
validate: false,
});
logger.info(`Reading the configuration`);
const meshConfig = await findAndParseConfig({
dir: baseDir,
store,
importFn,
ignoreAdditionalResolvers: true,
artifactsDir: cliParams.artifactsDir,
configName: cliParams.configName,
additionalPackagePrefixes: cliParams.additionalPackagePrefixes,
generateCode: true,
initialLoggerPrefix: cliParams.initialLoggerPrefix,
throwOnInvalidConfig: args.throwOnInvalidConfig,
});
logger = meshConfig.logger;
logger.info(`Generating the unified schema`);
const { schema, destroy, rawSources } = await getMesh(meshConfig);
await writeFile(path.join(outputDir, 'schema.graphql'), printSchemaWithDirectives(schema));
logger.info(`Generating artifacts`);
meshConfig.importCodes.add(`import { createMeshHTTPHandler, MeshHTTPHandler } from '@graphql-mesh/http';`);
meshConfig.codes.add(`
export function createBuiltMeshHTTPHandler(): MeshHTTPHandler<MeshContext> {
return createMeshHTTPHandler<MeshContext>({
baseDir,
getBuiltMesh: ${cliParams.builtMeshFactoryName},
rawServeConfig: ${JSON.stringify(meshConfig.config.serve)},
})
}
`);
await generateTsArtifacts({
unifiedSchema: schema,
rawSources,
mergerType: meshConfig.merger.name,
documents: meshConfig.documents,
flattenTypes: false,
importedModulesSet,
baseDir,
meshConfigImportCodes: meshConfig.importCodes,
meshConfigCodes: meshConfig.codes,
logger,
sdkConfig: meshConfig.config.sdk,
fileType: args.fileType,
codegenConfig: meshConfig.config.codegen,
}, cliParams);
logger.info(`Cleanup`);
destroy();
logger.info('Done! => ' + outputDir);
}
catch (e) {
handleFatalError(e, logger);
}
})
.command(cliParams.sourceServerCommand + ' <source>', 'Serves specific source in development mode', builder => {
builder.positional('source', {
type: 'string',
requiresArg: true,
});
}, async (args) => {
var _a;
process.env.NODE_ENV = 'development';
const meshConfig = await findAndParseConfig({
dir: baseDir,
artifactsDir: cliParams.artifactsDir,
configName: cliParams.configName,
additionalPackagePrefixes: cliParams.additionalPackagePrefixes,
initialLoggerPrefix: cliParams.initialLoggerPrefix,
});
logger = meshConfig.logger;
const sourceIndex = meshConfig.sources.findIndex(rawSource => rawSource.name === args.source);
if (sourceIndex === -1) {
throw new Error(`Source ${args.source} not found`);
}
const meshInstance$ = getMesh({
...meshConfig,
additionalTypeDefs: undefined,
additionalResolvers: [],
transforms: [],
sources: [meshConfig.sources[sourceIndex]],
});
const serveMeshOptions = {
baseDir,
argsPort: 4000 + sourceIndex + 1,
getBuiltMesh: () => meshInstance$,
logger: meshConfig.logger.child('Server'),
rawServeConfig: meshConfig.config.serve,
playgroundTitle: `${args.source} GraphiQL`,
};
if ((_a = meshConfig.config.serve) === null || _a === void 0 ? void 0 : _a.customServerHandler) {
const customServerHandler = await loadFromModuleExportExpression(meshConfig.config.serve.customServerHandler, {
defaultExportName: 'default',
cwd: baseDir,
importFn: defaultImportFn,
});
await customServerHandler(serveMeshOptions);
}
else {
await serveMesh(serveMeshOptions, cliParams);
}
}).argv;
}
export { DEFAULT_CLI_PARAMS, findAndParseConfig, generateTsArtifacts, graphqlMesh, serveMesh };