@graphql-mesh/serve-cli
Version:
244 lines (243 loc) • 10.8 kB
JavaScript
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;
}
;