UNPKG

apollo-subgraph-cli

Version:
610 lines (576 loc) 18.9 kB
// 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