wgc
Version:
The official CLI tool to manage the GraphQL Federation Platform Cosmo
361 lines (358 loc) • 11.2 kB
JavaScript
/* eslint-disable import/named */
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { access } from 'node:fs/promises';
import { federateSubgraphs, ROUTER_COMPATIBILITY_VERSION_ONE, } from '@wundergraph/composition';
import boxen from 'boxen';
import { buildClientSchema, printSchema } from 'graphql';
import yaml from 'js-yaml';
import pc from 'picocolors';
import { program } from 'commander';
import { isValidSubscriptionProtocol, isValidWebsocketSubprotocol, } from '@wundergraph/cosmo-shared';
import { config, configDir, configFile } from './core/config.js';
const introspectionQuery = `query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type {
...TypeRef
}
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}`;
const sdlQuery = `
{
_service{
sdl
}
}
`;
export const introspectSubgraph = async ({ subgraphURL, additionalHeaders, rawIntrospection, }) => {
const headers = new Headers();
headers.append('Content-Type', 'application/json');
for (const header of additionalHeaders) {
headers.append(header.key, header.value);
}
const graphql = JSON.stringify({
query: rawIntrospection ? introspectionQuery : sdlQuery,
variables: {},
});
const response = await fetch(subgraphURL, {
method: 'POST',
headers,
body: graphql,
});
if (response.status !== 200) {
return {
success: false,
errorMessage: 'Could not introspect the subgraph.',
sdl: '',
};
}
const body = await response.json();
const data = body.data;
const sdl = rawIntrospection ? printSchema(buildClientSchema(data)) : data._service.sdl;
return {
success: true,
sdl,
};
};
/**
* Composes a list of subgraphs into a single schema.
*/
export function composeSubgraphs(subgraphs, options) {
// @TODO get router compatibility version programmatically
return federateSubgraphs({
options,
subgraphs,
version: ROUTER_COMPATIBILITY_VERSION_ONE,
});
}
/**
* Asynchronously checks whether a file (or directory) exists at the given path.
*/
export const fileExists = async (filePath) => {
try {
await access(filePath);
return true;
}
catch {
return false;
}
};
export const readConfigFile = () => {
if (!existsSync(configFile)) {
return {};
}
const data = yaml.load(readFileSync(configFile, 'utf8'));
return data !== null && data !== void 0 ? data : {};
};
export const updateConfigFile = (newData) => {
const existingData = readConfigFile();
const updatedData = yaml.dump({
...existingData,
...newData,
});
writeFileSync(configFile, updatedData);
};
export const checkForUpdates = async () => {
if (config.disableUpdateCheck === 'true') {
return;
}
try {
mkdirSync(configDir, { recursive: true });
const currentTime = Date.now();
const configFileData = readConfigFile();
if (configFileData.lastUpdateCheck && currentTime - configFileData.lastUpdateCheck < 24 * 60 * 60 * 1000) {
return;
}
const response = await fetch(`https://registry.npmjs.org/wgc/latest`);
const latestVersion = (await response.json()).version;
if (config.version === latestVersion) {
return;
}
const message = `Update available! ${pc.red(config.version)} → ${pc.green(latestVersion)}
Changelog: https://github.com/wundergraph/cosmo/releases/tag/wgc@${latestVersion}
Run npm i -g wgc@latest`;
console.warn(boxen(message, {
padding: 1,
margin: 1,
align: 'center',
borderColor: 'yellow',
borderStyle: 'round',
}));
updateConfigFile({
lastUpdateCheck: currentTime,
});
}
catch (e) {
throw new Error(`Failed to check for updates. You can disable update check by setting env DISABLE_UPDATE_CHECK=true. ${e.message}`);
}
};
export const validateSubscriptionProtocols = ({ subscriptionProtocol, websocketSubprotocol, }) => {
if (subscriptionProtocol && !isValidSubscriptionProtocol(subscriptionProtocol)) {
program.error(pc.red(pc.bold(`The subscription protocol '${pc.bold(subscriptionProtocol)}' is not valid. Please use one of the following: sse, sse_post, ws.`)));
}
if (websocketSubprotocol) {
if (subscriptionProtocol && subscriptionProtocol !== 'ws') {
program.error(pc.red(pc.bold(`The websocket subprotocol '${pc.bold(websocketSubprotocol)}' can only be used if the subscription protocol is 'ws'.`)));
}
if (!isValidWebsocketSubprotocol(websocketSubprotocol)) {
program.error(pc.red(pc.bold(`The websocket subprotocol '${pc.bold(websocketSubprotocol)}' is not valid. Please use one of the following: auto, graphql-ws, graphql-transport-ws.`)));
}
}
};
/**
* Waits for a single keypress matching one of the keys in the provided map.
* Keys are case-sensitive strings. Use 'Enter' for the enter key.
* Each entry is either a callback function or a descriptor `{ callback, persistent }`.
* When `persistent` is true the callback fires but the prompt keeps listening,
* useful for side-effect actions (e.g. opening a URL) alongside a terminating key.
*/
export function waitForKeyPress(keyMap, message) {
const { promise, resolve } = Promise.withResolvers();
if (message) {
process.stdout.write(pc.dim(message));
}
process.stdin.setRawMode(true);
process.stdin.resume();
const onData = async (data) => {
const key = data.toString();
// Ctrl+C
if (key === '\u0003') {
process.stdin.setRawMode(false);
process.stdin.pause();
process.stdout.write('\n');
process.exit(0);
}
// Normalize Enter (\r or \n)
const normalized = key === '\r' || key === '\n' ? 'Enter' : key;
if (!(normalized in keyMap)) {
return;
}
const entry = keyMap[normalized];
if (!entry) {
return;
}
const isDescriptor = typeof entry !== 'function';
const callback = isDescriptor ? entry.callback : entry;
const persistent = isDescriptor ? entry.persistent : false;
if (persistent) {
await callback();
return;
}
process.stdin.removeListener('data', onData);
process.stdin.setRawMode(false);
process.stdin.pause();
process.stdout.write('\n');
await callback();
resolve();
};
process.stdin.on('data', onData);
return promise;
}
export function printTruncationWarning({ displayedErrorCounts, totalErrorCounts }) {
if (!totalErrorCounts) {
return;
}
const truncatedItems = [];
if (totalErrorCounts.compositionErrors > displayedErrorCounts.compositionErrors) {
truncatedItems.push(`composition errors (${displayedErrorCounts.compositionErrors} of ${totalErrorCounts.compositionErrors} shown)`);
}
if (totalErrorCounts.compositionWarnings > displayedErrorCounts.compositionWarnings) {
truncatedItems.push(`composition warnings (${displayedErrorCounts.compositionWarnings} of ${totalErrorCounts.compositionWarnings} shown)`);
}
if (totalErrorCounts.deploymentErrors > displayedErrorCounts.deploymentErrors) {
truncatedItems.push(`deployment errors (${displayedErrorCounts.deploymentErrors} of ${totalErrorCounts.deploymentErrors} shown)`);
}
if (truncatedItems.length > 0) {
console.log(pc.yellow(`\nNote: Some results were truncated: ${truncatedItems.join(', ')}.`));
}
}
/**
* Prints text with rainbow-like effect. Respects NO_COLOR
*/
export function rainbow(text) {
if (!pc.isColorSupported) {
return text;
}
const chars = [...text];
return (chars
.map((char, i) => {
const t = chars.length > 1 ? i / (chars.length - 1) : 0;
const [r, g, b] = interpolateColor(t);
return `\u001B[38;2;${r};${g};${b}m${char}`;
})
.join('') + '\u001B[0m');
}
/** Strips ANSI SGR escape sequences (colors, bold, dim, etc.) from a string. */
export function stripAnsi(s) {
const ESC = String.fromCodePoint(0x1b);
return s.replaceAll(new RegExp(`${ESC}\\[[\\d;]*m`, 'g'), '');
}
/** Returns the visible character count of a string, ignoring ANSI escape sequences. */
export function visibleLength(s) {
return stripAnsi(s).length;
}
// Gradient color stops: pink → orange → yellow → green → cyan → blue → purple
const gradientStops = [
[255, 100, 150], // pink
[255, 160, 50], // orange
[255, 220, 50], // yellow
[80, 220, 100], // green
[50, 200, 220], // cyan
[80, 120, 255], // blue
[180, 100, 255], // purple
];
/**
* JSON.stringify replacer that strips protobuf-es v2 internal fields
* (`$typeName`, `$unknown`) from message objects, so CLI `--json` output stays a
* clean, user-facing shape. It runs for every nested object too, so nested
* messages are handled as well.
*/
export function stripProtobufInternals(_key, value) {
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
const record = value;
if ('$typeName' in record || '$unknown' in record) {
const { $typeName, $unknown, ...rest } = record;
return rest;
}
}
return value;
}
function interpolateColor(t) {
const segment = t * (gradientStops.length - 1);
const i = Math.min(Math.floor(segment), gradientStops.length - 2);
const f = segment - i;
return [
Math.round(gradientStops[i][0] + (gradientStops[i + 1][0] - gradientStops[i][0]) * f),
Math.round(gradientStops[i][1] + (gradientStops[i + 1][1] - gradientStops[i][1]) * f),
Math.round(gradientStops[i][2] + (gradientStops[i + 1][2] - gradientStops[i][2]) * f),
];
}
//# sourceMappingURL=utils.js.map