apollo-subgraph-cli
Version:
A CLI for subgraph schema management
610 lines (576 loc) • 18.9 kB
JavaScript
// src/cli/index.ts
import fs5 from "fs-extra";
import { Command } from "commander";
// src/cli/constants/cliBanner.ts
import boxen from "boxen";
import chalk from "chalk";
var cliBanner = {
header: (command) => {
console.log(
boxen(chalk.blueBright(`subgraph: ${chalk.green(command)}`), {
borderStyle: "round",
textAlignment: "center",
width: 40
})
);
},
footer: () => {
console.log(chalk.green("\nTasks completed successfully. Happy coding!"));
}
};
// src/cli/constants/common.ts
var commands = {
init: "init",
print: "print",
check: "check"
};
var scripts = {
print: `subgraph schema ${commands.print} --config subgraph.config.ts`,
check: `subgraph schema ${commands.check} --config subgraph.config.ts`
};
var commonIgnoreDirs = /* @__PURE__ */ new Set([
"node_modules",
".git",
"dist",
"build",
"out",
"bin",
"obj",
".cache",
"tmp",
"temp",
".idea",
".vscode",
"__pycache__",
".venv",
"coverage",
"logs",
"test-output",
".gradle",
"target",
"vendor",
"migrations"
]);
// src/cli/tasks/index.ts
import { Listr } from "listr2";
// src/cli/tasks/generateSubgraphConfig.ts
import { join } from "node:path";
import { writeFileSync } from "node:fs";
// src/cli/templates/index.ts
var subgraphConfigTemplate = (schemaPaths, outputPath) => {
return `import type { SubgraphConfig } from 'apollo-subgraph-cli'
/**
* @property schema - The location of your subgraph's typeDefs
* @property output - The path to the printed schema file
*/
const config: SubgraphConfig = {
schema: [${schemaPaths.map((path) => `'${path}'`).join(", ")}],
output: '${outputPath}'
}
export default config
`;
};
// src/cli/tasks/generateSubgraphConfig.ts
var subTaskOptions = {
concurrent: false,
rendererOptions: { collapse: true }
};
var generateSubgraphConfig = {
title: "Generate subgraph config file",
task: (_, task) => task.newListr(
[
{
title: "Writing subgraph config to project root...",
task: async ({ schema: { paths, output } }) => {
try {
const configFilePath = join(process.cwd(), "subgraph.config.ts");
const configContent = subgraphConfigTemplate(paths, output);
writeFileSync(configFilePath, configContent);
} catch (e) {
throw new Error(`Error generating subgraph config file: ${e.message}`);
}
}
}
],
subTaskOptions
)
};
// src/cli/tasks/generateSubgraphSchema.ts
import { join as join6, parse as parse3 } from "node:path";
import { writeFileSync as writeFileSync3 } from "node:fs";
import fs3 from "fs-extra";
import { printSubgraphSchema as printSubgraphSchema2 } from "@apollo/subgraph";
// src/cli/utils/buildFileWatcher.ts
import { parse as parse2 } from "path";
import chalk3 from "chalk";
// src/cli/utils/logPrefix.ts
import chalk2 from "chalk";
var colorMap = {
error: chalk2.red,
info: chalk2.blue,
success: chalk2.green
};
var logPrefix = (type, message) => `[${colorMap[type].bold(message)}]`;
// src/cli/utils/parseGlobPatterns.ts
import { resolve } from "node:path";
import fg from "fast-glob";
async function parseGlobPatterns(patterns, type = "file") {
const entries = await fg(patterns, {
dot: true,
onlyFiles: type === "file",
onlyDirectories: type === "directory"
});
return entries.map((entry) => resolve(process.cwd(), entry));
}
// src/cli/utils/generateSubgraphSchema.ts
import { join as join2, parse } from "node:path";
import { mkdirSync, writeFileSync as writeFileSync2 } from "node:fs";
import fs from "fs-extra";
import { printSubgraphSchema } from "@apollo/subgraph";
// src/cli/utils/loadSchema.ts
import { CodeFileLoader } from "@graphql-tools/code-file-loader";
import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader";
import {
loadSchema as loadSchemaToolkit
} from "@graphql-tools/load";
var defaultSchemaLoadOptions = {
assumeValidSDL: true,
sort: true,
convertExtensions: true,
includeSources: true
};
async function loadSchema(schemaPointers, config = {}) {
try {
const loaders = [new CodeFileLoader(), new GraphQLFileLoader()];
const schema = await loadSchemaToolkit(schemaPointers, {
...defaultSchemaLoadOptions,
loaders,
...config,
...config.config
});
return schema;
} catch (e) {
throw new Error(
`
Failed to load schema from ${Object.keys(schemaPointers).join(",")}:
${e.message || e}
${e.stack || ""}
Apollo Subgraph CLI supports all extensions for:
- GraphQL files
- TS/JS files
Try to use one of above options and run codegen again.
`
);
}
}
// src/cli/utils/generateSubgraphSchema.ts
var { pathExists } = fs;
var generateSubgraphSchema = async ({ paths, output }) => {
let loadedSchema;
let schemaFilePath;
try {
const schemaPaths = paths.map((path) => join2(process.cwd(), path));
loadedSchema = await loadSchema(schemaPaths);
schemaFilePath = join2(process.cwd(), output);
const { dir } = parse(schemaFilePath);
const exists = await pathExists(dir);
if (!exists) {
mkdirSync(dir, { recursive: true });
}
} catch (e) {
throw new Error(`${logPrefix("error", "ERROR DETECTED")}: ${e.message}`);
}
let schema;
try {
schema = printSubgraphSchema(loadedSchema);
} catch (e) {
throw new Error(`${logPrefix("error", "SCHEMA ERROR DETECTED")}: ${e.message}`);
}
try {
writeFileSync2(schemaFilePath, schema);
} catch (e) {
throw new Error(`${logPrefix("error", "ERROR DETECTED")}: ${e.message}`);
}
};
// src/cli/utils/resolveModule.ts
import { dirname, join as join3 } from "node:path";
import { access } from "node:fs/promises";
var resolveModule = async (moduleName, startDir) => {
const modulePath = join3(startDir, "node_modules", moduleName);
try {
await access(modulePath);
return modulePath;
} catch {
const parentDir = dirname(startDir);
if (parentDir === startDir) {
throw new Error(`Cannot find module '${moduleName}'`);
}
return resolveModule(moduleName, parentDir);
}
};
// src/cli/utils/loadChokidar.ts
var loadChokidar = async () => {
try {
await resolveModule("chokidar", process.cwd());
return await import("chokidar");
} catch (e) {
throw new Error(
'The "chokidar" package is required and needs to be installed if watching for file changes.'
);
}
};
// src/cli/utils/buildFileWatcher.ts
var buildFileWatcher = async (gqlSchemaPath, outputPath) => {
const schemaPaths = await parseGlobPatterns(gqlSchemaPath, "file");
const chokidar = await loadChokidar();
const watcher = chokidar.watch(schemaPaths, { persistent: true });
const printedPaths = schemaPaths.map((path) => {
const { name, ext } = parse2(path);
return ` - ${chalk3.green(name + ext)}`;
}).join("\n");
console.log(`${logPrefix("info", "INFO")}: Watching for schema changes in`);
console.log(printedPaths);
watcher.on("change", async (filePath) => {
const { name, ext } = parse2(filePath);
try {
await generateSubgraphSchema({ paths: gqlSchemaPath, output: outputPath });
console.info(
`${logPrefix("success", "CHANGE DETECTED")}:${logPrefix("success", name + ext)} Writing changes to ${chalk3.green(outputPath)}`
);
} catch (e) {
if (e instanceof Error) {
console.error(e.message);
}
}
});
watcher.on("error", async (e) => {
await watcher?.close();
console.error(e);
});
};
// src/cli/utils/getSubgraphArgsFromCosmicConfigFile.ts
import { cosmiconfig } from "cosmiconfig";
import { TypeScriptLoader } from "cosmiconfig-typescript-loader";
var getSubgraphArgsFromCosmicConfigFile = async (search) => {
const moduleName = "subgraph";
const searchPlaces = search ? [search] : [
"package.json",
`.${moduleName}rc`,
`.${moduleName}rc.json`,
`.${moduleName}rc.yaml`,
`.${moduleName}rc.yml`,
`.${moduleName}rc.js`,
`.${moduleName}rc.ts`,
`${moduleName}.config.js`,
`${moduleName}.config.ts`
];
const result = await cosmiconfig(moduleName, {
searchPlaces,
loaders: { ".ts": TypeScriptLoader() }
}).search();
return result?.config;
};
// src/cli/utils/getPackageJson.ts
import { join as join4 } from "node:path";
import fs2 from "fs-extra";
var { readJsonSync } = fs2;
var getPackageJson = () => {
const packageJsonPath = join4(process.cwd(), "package.json");
const packageJson = readJsonSync(packageJsonPath);
return { output: packageJson, path: packageJsonPath };
};
// src/cli/utils/findFilesByFileName.ts
import { readdirSync } from "node:fs";
import { join as join5, relative } from "node:path";
var findFilesByFileName = (dir, fileNames, ig) => {
const results = [];
function searchDir(currentDir) {
const entries = readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const relativePath = relative(process.cwd(), join5(currentDir, entry.name));
if (ig.ignores(relativePath)) continue;
const entryPath = join5(currentDir, entry.name);
if (entry.isDirectory()) {
searchDir(entryPath);
} else if (fileNames.includes(entry.name)) {
results.push(entryPath);
}
}
}
searchDir(dir);
return results;
};
// src/cli/utils/isGitRepo.ts
import { execSync } from "child_process";
var isGitRepo = () => {
try {
execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
return true;
} catch (e) {
return false;
}
};
// src/cli/tasks/generateSubgraphSchema.ts
var { pathExists: pathExists2, mkdir } = fs3;
var subTaskOptions2 = {
concurrent: false,
rendererOptions: { collapse: true }
};
var generateSubgraphSchema2 = {
title: "Generate subgraph schema file",
task: (_, task) => task.newListr(
[
{
title: "Printing schema file output...",
task: async ({ schema: { paths, output } }) => {
try {
const schemaPaths = paths.map((path) => join6(process.cwd(), path));
const loadedSchema = await loadSchema(schemaPaths);
const schemaFilePath = join6(process.cwd(), output);
const { dir } = parse3(schemaFilePath);
const exists = await pathExists2(dir);
if (!exists) {
await mkdir(dir, { recursive: true });
}
writeFileSync3(schemaFilePath, printSubgraphSchema2(loadedSchema));
} catch (e) {
throw new Error(`Error generating subgraph schema file: ${e.message}`);
}
}
}
],
subTaskOptions2
)
};
// src/cli/tasks/checkSubgraphSchema.ts
import { join as join7 } from "node:path";
import { printSubgraphSchema as printSubgraphSchema3 } from "@apollo/subgraph";
var subTaskOptions3 = {
concurrent: false,
rendererOptions: { collapse: true }
};
var checkSubgraphSchema = {
title: "Check subgraph schema",
task: (_, task) => task.newListr(
[
{
title: "Checking schema file output...",
task: async ({ schema: { paths } }, task2) => {
try {
const schemaPaths = paths.map((path) => join7(process.cwd(), path));
const loadedSchema = await loadSchema(schemaPaths);
printSubgraphSchema3(loadedSchema);
task2.output = "No issues found with federated schema";
} catch (e) {
throw new Error(`Error found with federated schema: ${e.message}`);
}
}
}
],
subTaskOptions3
)
};
// src/cli/tasks/updatePackageJSON.ts
import fs4 from "fs-extra";
var { writeJSONSync } = fs4;
var subTaskOptions4 = {
concurrent: false,
rendererOptions: { collapse: true }
};
var updatePackageJSON = {
title: "Update package.json",
task: (_, task) => task.newListr(
[
{
title: "Adding and updating npm scripts",
task: async ({ printScriptName, checkScriptName }) => {
try {
const { output, path } = getPackageJson();
output.scripts = {
...output.scripts ?? {},
[printScriptName ?? "schema:print"]: scripts.print,
[checkScriptName ?? "schema:check"]: scripts.check
};
writeJSONSync(path, output, { spaces: 2 });
} catch (e) {
throw new Error(`Error writing to package.json: ${e.message}`);
}
}
}
],
subTaskOptions4
)
};
// src/cli/tasks/addFilesToGitIndex.ts
import { join as join8, parse as parse4 } from "node:path";
import { execSync as execSync2 } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import ignore from "ignore";
var subTaskOptions5 = {
concurrent: false,
rendererOptions: { collapse: true }
};
var addFilesToGitIndex = {
title: "Add files to git",
skip: ({ git = false }) => !git ? "Skipping add files to git" : false,
task: ({ schema: { output } }, task) => task.newListr(
[
{
title: "Performing git add...",
task: (_, task2) => {
const gitRepo = isGitRepo();
if (!gitRepo) {
task2.output = "No git repo found.";
return;
}
const { name, ext } = parse4(output);
const generatedSchemaFileName = `${name}.${ext}`;
const fileNames = ["subgraph.config.ts", "package.json", generatedSchemaFileName];
const gitignorePath = join8(process.cwd(), ".gitignore");
const ig = ignore();
if (existsSync(gitignorePath)) {
const gitignoreContent = readFileSync(gitignorePath, "utf-8");
ig.add(gitignoreContent);
} else {
ig.add([...commonIgnoreDirs].map((dir) => `/${dir}/`));
}
try {
const filesPaths = findFilesByFileName(process.cwd(), fileNames, ig);
if (!filesPaths.length) {
task2.output = "No files found to add to git staging.";
return;
} else {
execSync2(`git add ${filesPaths.map((file) => `${file}`).join(" ")}`);
}
} catch (e) {
throw new Error(`Error adding files to git: ${e.message}`);
}
}
}
],
subTaskOptions5
)
};
// src/cli/tasks/index.ts
var listrOptions = {
concurrent: false,
rendererOptions: { collapseSkips: false }
};
var tasks = {
init: new Listr(
[generateSubgraphConfig, generateSubgraphSchema2, updatePackageJSON, addFilesToGitIndex],
listrOptions
),
print: new Listr([generateSubgraphSchema2], listrOptions),
check: new Listr([checkSubgraphSchema], listrOptions)
};
// src/cli/prompts/index.ts
import { confirm, input } from "@inquirer/prompts";
var getSchemaPaths = async (paths = []) => {
const path = await input({
message: "Enter schema path:",
default: "./src/gql/typeDefs/*.ts"
});
paths.push(path);
const additionalInput = await confirm({ message: "Do you have another path to provide?" });
if (additionalInput) {
await getSchemaPaths(paths);
}
return paths;
};
var getOutputPath = async () => input({
message: "Where would you like to write the output?",
default: "./schema.graphql",
validate: (val) => {
return val?.includes(".graphql") || "The output has to use the .graphql file extension";
}
});
var getPrintScriptName = async () => input({
message: "What would you like to name the npm script to print your schema?",
default: "schema:print",
validate: (val) => !!val?.length
});
var getCheckScriptName = async () => input({
message: "What would you like to name the npm script to check your schema?",
default: "schema:check",
validate: (val) => !!val?.length
});
var confirmAddFilesToGitStaging = async () => confirm({ message: "Do you want to add the new files to the git staging area?" });
// src/cli/index.ts
(async () => {
const packageJSON = await fs5.readJson("./package.json");
const subgraph = new Command("subgraph").description("A CLI for federated subgraph management").version(packageJSON.version);
const schema = new Command("schema").description(
"Helpful commands for managing your subgraph schema"
);
schema.command(commands.check).option("-c, --config <path-to-subgraph-config>", "The path to your subgraph config file").option("-s, --schema <schema-location...>", "Location of your subgraph's typeDefs").action(async (options) => {
cliBanner.header("schema check");
const config = await getSubgraphArgsFromCosmicConfigFile(options.config);
const gqlSchemaPath = options.schema ?? config?.schema ?? await getSchemaPaths();
try {
await tasks.check.run({
schema: { paths: gqlSchemaPath }
});
cliBanner.footer();
} catch (e) {
console.error(e);
process.exit(1);
}
});
schema.command(commands.print).option("-c, --config <path-to-subgraph-config>", "The path to your subgraph config file").option("-s, --schema <schema-location...>", "Location of your subgraph's typeDefs").option("-o, --output <output-path>", "The path to the printed schema file").option("-w, --watch", "Watch for file changes and print the resulting schema file").action(async (options) => {
const config = await getSubgraphArgsFromCosmicConfigFile(options.config);
const gqlSchemaPath = options.schema ?? config?.schema ?? await getSchemaPaths();
const outputPath = options.output ?? config?.output ?? await getOutputPath();
if (options.watch) {
try {
await generateSubgraphSchema({ paths: gqlSchemaPath, output: outputPath });
} catch (e) {
if (e instanceof Error) {
console.error(e.message);
}
}
await buildFileWatcher(gqlSchemaPath, outputPath);
} else {
cliBanner.header("schema print");
try {
await tasks.print.run({
schema: {
paths: gqlSchemaPath,
output: outputPath
}
});
cliBanner.footer();
} catch (e) {
console.error(e);
process.exit(1);
}
}
});
schema.command(commands.init).action(async () => {
cliBanner.header("schema init");
console.log("Let's get started by answering a few questions about your service.\n");
const schemaPaths = await getSchemaPaths();
const outputPath = await getOutputPath();
const printScriptName = await getPrintScriptName();
const checkScriptName = await getCheckScriptName();
const git = await confirmAddFilesToGitStaging();
console.log("\n");
try {
await tasks.init.run({
git,
printScriptName,
checkScriptName,
schema: {
paths: schemaPaths,
output: outputPath
}
});
cliBanner.footer();
} catch (e) {
process.exit(1);
}
});
subgraph.addCommand(schema);
subgraph.parse();
})();
//# sourceMappingURL=index.js.map