UNPKG

@graphql-mesh/serve-cli

Version:
244 lines (243 loc) 10.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.run = run; const tslib_1 = require("tslib"); require("json-bigint-patch"); // JSON.parse/stringify with bigints support require("dotenv/config"); // inject dotenv options to process.env const node_cluster_1 = tslib_1.__importDefault(require("node:cluster")); const node_os_1 = require("node:os"); const node_path_1 = require("node:path"); const jiti_1 = tslib_1.__importDefault(require("jiti")); const extra_typings_1 = require("@commander-js/extra-typings"); const serve_runtime_1 = require("@graphql-mesh/serve-runtime"); const utils_1 = require("@graphql-mesh/utils"); const nodeHttp_js_1 = require("./nodeHttp.js"); const uWebSockets_js_1 = require("./uWebSockets.js"); const defaultFork = process.env.NODE_ENV === 'production' ? (0, node_os_1.availableParallelism)() : 1; /** Default config paths sorted by priority. */ const defaultConfigPaths = [ 'mesh.config.ts', 'mesh.config.mts', 'mesh.config.cts', 'mesh.config.js', 'mesh.config.mjs', 'mesh.config.cjs', ]; let program = new extra_typings_1.Command() .addOption(new extra_typings_1.Option('--fork [count]', 'count of workers to spawn. defaults to `os.availableParallelism()` when NODE_ENV is "production", otherwise only one (the main) worker') .env('FORK') .argParser(v => { const count = parseInt(v); if (isNaN(count)) { throw new extra_typings_1.InvalidArgumentError('not a number.'); } return count; }) .default(defaultFork)) .addOption(new extra_typings_1.Option('-c, --config-path <path>', `path to the configuration file. defaults to the following files respectively in the current working directory: ${defaultConfigPaths.join(', ')}`).env('CONFIG_PATH')) .option('-h, --host <hostname>', 'host to use for serving', (0, node_os_1.release)().toLowerCase().includes('microsoft') ? '127.0.0.1' : '0.0.0.0') .addOption(new extra_typings_1.Option('-p, --port <number>', 'port to use for serving') .env('PORT') .default(4000) .argParser(v => { const port = parseInt(v); if (isNaN(port)) { throw new extra_typings_1.InvalidArgumentError('not a number.'); } return port; })) .option('--supergraph <path>', 'path to the supergraph schema') .addOption(new extra_typings_1.Option('--polling <intervalInMs>', 'schema polling interval in milliseconds') .env('POLLING') .argParser(v => { const interval = parseInt(v); if (isNaN(interval)) { throw new extra_typings_1.InvalidArgumentError('not a number.'); } return interval; })) .option('--masked-errors', 'mask unexpected errors in responses') .option('--subgraph <path>', 'path to the subgraph schema'); async function run({ log: rootLog = new utils_1.DefaultLogger(), productName = 'Mesh', productDescription = 'serve GraphQL federated architecture for any API service(s)', binName = 'mesh-serve', version = globalThis.__VERSION__, }) { program = program.name(binName).description(productDescription); if (version) program.version(version); const opts = program.parse().opts(); const log = rootLog.child(node_cluster_1.default.worker?.id ? `🕸️ ${productName} Worker#${node_cluster_1.default.worker.id}` : `🕸️ ${productName}`); let importedConfig; if (!opts.configPath) { log.info(`Searching for default config files`); for (const configPath of defaultConfigPaths) { importedConfig = await importConfig(log, (0, node_path_1.resolve)(process.cwd(), configPath)); if (importedConfig) { log.info(`Found default config file ${configPath}`); break; } } } else { // using user-provided config const configPath = (0, node_path_1.isAbsolute)(opts.configPath) ? opts.configPath : (0, node_path_1.resolve)(process.cwd(), opts.configPath); log.info(`Loading config file at path ${configPath}`); importedConfig = await importConfig(log, configPath); if (!importedConfig) { throw new Error(`Cannot find config file at ${configPath}`); } } if (importedConfig) { log.info('Loaded config file'); } else { log.info('No config file loaded, using defaults'); } const config = { ...importedConfig, ...opts, }; let unifiedGraphPath = null; if ('supergraph' in config) { // path unifiedGraphPath = config.supergraph; log.info(`Loading Supergraph from ${unifiedGraphPath}`); } else if ('hive' in config || process.env.HIVE_CDN_ENDPOINT) { // hive log.info('Loading Supergraph from Hive CDN'); } else if (!('proxy' in config)) { // default unifiedGraphPath = 'supergraph.graphql'; log.info(`Loading Supergraph from ${unifiedGraphPath}`); } if (node_cluster_1.default.isPrimary) { const fork = opts.fork === true ? defaultFork : opts.fork; if (unifiedGraphPath) { // eslint-disable-next-line @typescript-eslint/consistent-type-imports let watcher; try { watcher = await Promise.resolve().then(() => tslib_1.__importStar(require('@parcel/watcher'))); } catch (err) { log.warn(`If you want to enable hot reloading when ${unifiedGraphPath} changes, install "@parcel/watcher"`); } if (watcher) { try { const absoluteUnifiedGraphPath = (0, node_path_1.isAbsolute)(String(unifiedGraphPath)) ? String(unifiedGraphPath) : (0, node_path_1.resolve)(process.cwd(), String(unifiedGraphPath)); const absolutUnifiedGraphDir = (0, node_path_1.dirname)(absoluteUnifiedGraphPath); const subscription = await watcher.subscribe(absolutUnifiedGraphDir, (err, events) => { if (err) { log.error(err); return; } if (events.some(event => event.path === absoluteUnifiedGraphPath && event.type === 'update')) { log.info(`Supergraph: ${unifiedGraphPath} updated on the filesystem. Invalidating...`); if (fork > 1) { for (const workerId in node_cluster_1.default.workers) { node_cluster_1.default.workers[workerId].send('invalidateUnifiedGraph'); } } else { handler.invalidateUnifiedGraph(); } } }); (0, utils_1.registerTerminateHandler)(eventName => { log.info(`Closing watcher for ${absoluteUnifiedGraphPath} for ${eventName}`); return subscription.unsubscribe(); }); } catch (err) { log.error(`Failed to watch ${unifiedGraphPath}!`); throw err; } } } if (fork > 1) { log.info(`Forking ${fork} ${productName} Workers`); for (let i = 0; i < fork; i++) { log.info(`Forking ${productName} Worker #${i}`); const worker = node_cluster_1.default.fork(); (0, utils_1.registerTerminateHandler)(eventName => { log.info(`Closing ${productName} Worker #${i} for ${eventName}`); worker.kill(eventName); log.info(`Closed ${productName} Worker #${i} for ${eventName}`); }); log.info(`Forked ${productName} Worker #${i}`); } log.info(`Forked ${fork} ${productName} Workers`); return; } } const port = config.port; const host = config.host; const protocol = config.sslCredentials ? 'https' : 'http'; const handler = (0, serve_runtime_1.createServeRuntime)({ logging: log, ...config, }); const terminateStack = (0, utils_1.getTerminateStack)(); terminateStack.use(handler); process.on('message', message => { if (message === 'invalidateUnifiedGraph') { log.info(`Invalidating Supergraph`); handler.invalidateUnifiedGraph(); } }); let uWebSocketsAvailable = false; try { await Promise.resolve().then(() => tslib_1.__importStar(require('uWebSockets.js'))); uWebSocketsAvailable = true; } catch (err) { log.warn('uWebSockets.js is not available currently so the server will fallback to node:http.'); } const startServer = uWebSocketsAvailable ? uWebSockets_js_1.startuWebSocketsServer : nodeHttp_js_1.startNodeHttpServer; const server = await startServer({ handler, log, protocol, host, port, sslCredentials: config.sslCredentials, maxHeaderSize: config.maxHeaderSize || 16_384, }); terminateStack.use(server); } const jiti = (0, jiti_1.default)( // import.meta.url is not available in CJS (and cant even be in the syntax) and __filename is not available in ESM // instead, we dont care about the file path because we'll require config imports to have absolute paths ''); async function importConfig(log, path) { if (!(0, node_path_1.isAbsolute)(path)) { throw new Error('Configs can be imported using absolute paths only'); // see createJITI for explanation } try { const importedConfigModule = await jiti.import(path, {}); if (!importedConfigModule || typeof importedConfigModule !== 'object') { throw new Error('Invalid imported config module!'); } if ('default' in importedConfigModule) { // eslint-disable-next-line dot-notation return importedConfigModule.default['serveConfig']; } else if ('serveConfig' in importedConfigModule) { return importedConfigModule.serveConfig; } } catch (err) { // NOTE: we dont use the err.code because maybe the config itself is importing a module that does not exist. // if we were to use the MODULE_NOT_FOUND code, then those configs will fail silently if (String(err).includes(`Cannot find module '${path}'`)) { // config at path not found } else { log.error(`Importing config at ${path} failed!`); throw err; } } return null; }