UNPKG

@graphql-mesh/cli

Version:
396 lines (394 loc) 17.5 kB
import { config as dotEnvRegister } from 'dotenv'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { fs, path as pathModule, process } from '@graphql-mesh/cross-helpers'; import { include, registerTsconfigPaths } from '@graphql-mesh/include'; import { getMesh } from '@graphql-mesh/runtime'; import { FsStoreStorageAdapter, MeshStore } from '@graphql-mesh/store'; import { DefaultLogger, pathExists, registerTerminateHandler, rmdirs, writeFile, } from '@graphql-mesh/utils'; import { fakePromise, printSchemaWithDirectives } from '@graphql-tools/utils'; import { serveMesh } from './commands/serve/serve.js'; import { generateTsArtifacts } from './commands/ts-artifacts.js'; import { findAndParseConfig } from './config.js'; import { handleFatalError } from './handleFatalError.js'; export { findConfig } from './config.js'; export { generateTsArtifacts, serveMesh, findAndParseConfig, handleFatalError }; export const DEFAULT_CLI_PARAMS = { commandName: 'mesh', initialLoggerPrefix: '🕸️ Mesh', configName: 'mesh', artifactsDir: process.env.MESH_ARTIFACTS_DIRNAME || '.mesh', serveMessage: 'Serving GraphQL Mesh', playgroundTitle: 'GraphiQL Mesh', builtMeshFactoryName: 'getBuiltMesh', builtMeshSDKFactoryName: 'getMeshSDK', devServerCommand: 'dev', prodServerCommand: 'start', buildArtifactsCommand: 'build', sourceServerCommand: 'serve-source', validateCommand: 'validate', additionalPackagePrefixes: [], }; export async function graphqlMesh(cliParams = DEFAULT_CLI_PARAMS, args = hideBin(process.argv), cwdPath = process.cwd()) { let baseDir = cwdPath; let logger = new DefaultLogger(cliParams.initialLoggerPrefix); const unregisterTsconfigPaths = registerTsconfigPaths({ cwd: baseDir }); 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(moduleName => { const localModulePath = pathModule.resolve(baseDir, moduleName); const islocalModule = fs.existsSync(localModulePath); return include(islocalModule ? localModulePath : moduleName); })), }) .option('dir', { describe: 'Modified the base directory to use for looking for ' + cliParams.configName + ' config file', type: 'string', default: baseDir, coerce: dir => { if (pathModule.isAbsolute(dir)) { baseDir = dir; } else { baseDir = pathModule.resolve(cwdPath, dir); } unregisterTsconfigPaths(); registerTsconfigPaths({ cwd: baseDir }); if (fs.existsSync(pathModule.join(baseDir, '.env'))) { dotEnvRegister({ path: pathModule.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) => { try { const outputDir = pathModule.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, importFn: include, }); logger = meshConfig.logger; // eslint-disable-next-line no-inner-declarations function buildMeshInstance() { return getMesh(meshConfig).then(meshInstance => { // We already handle Mesh instance errors inside `serveMesh` // eslint-disable-next-line @typescript-eslint/no-floating-promises writeFile(pathModule.join(outputDir, 'schema.graphql'), printSchemaWithDirectives(meshInstance.schema)).catch(e => logger.error(`An error occured while writing the schema file: `, e)); // eslint-disable-next-line @typescript-eslint/no-floating-promises generateTsArtifacts({ unifiedSchema: meshInstance.schema, rawSources: meshInstance.rawSources, mergerType: meshConfig.merger.name, documents: meshConfig.documents, flattenTypes: false, importedModulesSet: new Set(), baseDir, pollingInterval: meshConfig.config.pollingInterval, 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<TServerContext = {}>(): MeshHTTPHandler<TServerContext> { return createMeshHTTPHandler<TServerContext>({ 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}`); }); return meshInstance; }); } let meshInstance$; meshInstance$ = buildMeshInstance(); if (meshConfig.config.pollingInterval) { logger.info(`Polling enabled with interval of ${meshConfig.config.pollingInterval}ms`); const interval = setInterval(() => { logger.info(`Polling for changes...`); buildMeshInstance() .then(newMeshInstance => meshInstance$.then(oldMeshInstance => { oldMeshInstance.destroy(); meshInstance$ = fakePromise(newMeshInstance); })) .catch(e => { logger.error(`Mesh polling failed so the previous version will be served: `, e); }); }, meshConfig.config.pollingInterval); registerTerminateHandler(() => { logger.info(`Terminating polling...`); clearInterval(interval); }); } const serveMeshOptions = { baseDir, argsPort: args.port, getBuiltMesh: () => meshInstance$, logger: meshConfig.logger.child('Server'), rawServeConfig: meshConfig.config.serve, registerTerminateHandler, }; 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 = pathModule.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 = pathModule.join(builtMeshArtifactsPath, 'index'); const builtMeshArtifacts = await include(mainModule); const rawServeConfig = builtMeshArtifacts.rawServeConfig; const meshOptions = await builtMeshArtifacts.getMeshOptions(); logger = meshOptions.logger; const serveMeshOptions = { baseDir, argsPort: args.port, getBuiltMesh: builtMeshArtifacts[cliParams.builtMeshFactoryName], logger, rawServeConfig, registerTerminateHandler, }; await serveMesh(serveMeshOptions, cliParams); } catch (e) { handleFatalError(e, logger); } }) .command(cliParams.validateCommand, 'Validates artifacts', builder => { }, async (args) => { let destroy; try { if (!(await pathExists(pathModule.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: include, fileType: 'ts', }), { readonly: false, validate: true, }); logger.info(`Reading the configuration`); const meshConfig = await findAndParseConfig({ dir: baseDir, store, importFn: include, 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 are valid!`); destroy = 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 = pathModule.join(baseDir, cliParams.artifactsDir); logger.info('Cleaning existing artifacts'); await rmdirs(outputDir); const importedModulesSet = new Set(); const importPromises = []; const importFn = (moduleId, noCache) => { const importPromise = include(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(pathModule.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<TServerContext = {}>(): MeshHTTPHandler<TServerContext> { return createMeshHTTPHandler<TServerContext>({ 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, pollingInterval: meshConfig.config.pollingInterval, }, 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) => { 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 getMeshOpts = { ...meshConfig, additionalTypeDefs: undefined, additionalResolvers: [], transforms: [], sources: [meshConfig.sources[sourceIndex]], }; let meshInstance$; if (meshConfig.config.pollingInterval) { const interval = setInterval(() => { getMesh(getMeshOpts) .then(newMeshInstance => meshInstance$.then(oldMeshInstance => { oldMeshInstance.destroy(); meshInstance$ = fakePromise(newMeshInstance); })) .catch(e => { logger.error(`Mesh polling failed so the previous version will be served: `, e); }); }, meshConfig.config.pollingInterval); registerTerminateHandler(() => { clearInterval(interval); }); } else { meshInstance$ = getMesh(getMeshOpts); } const serveMeshOptions = { baseDir, argsPort: 4000 + sourceIndex + 1, getBuiltMesh: () => meshInstance$, logger: meshConfig.logger.child('Server'), rawServeConfig: meshConfig.config.serve, playgroundTitle: `${args.source} GraphiQL`, registerTerminateHandler, }; await serveMesh(serveMeshOptions, cliParams); }).argv; }