diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e81040eed..e9f5f7148 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,12 @@ # Contributing guide +We are using Yarn workspaces, so make sure you have the latest version of Yarn installed. + ## Building project +To build the entire monorepo, start by installing the dependencies by running `yarn` in the root directory, and then: + ```sh -yarn install yarn build ``` @@ -13,3 +16,4 @@ yarn build cd packages/cli npm link . ``` + diff --git a/README.md b/README.md index 5ab86f4e1..3219274e7 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The equivalent npm global install will also work. ## Migration from 3.x.x to 4.x.x -**Important: many aspects of GraphQL CLI syntax and structure have changed in 4.x.x.** Please check out the [Migration Guide](MIGRATION.md) to learn more. +**Important: many aspects of GraphQL CLI syntax and structure have changed in 4.x.x.** Please check out the [Migration Guide](./docs/MIGRATION.md) to learn more. ## Usage / Initialization @@ -126,6 +126,10 @@ extensions: More plugins are definitely welcome! Please check the existing ones to see how to use GraphQL Config and GraphQL CLI API. +## Writing you own plugin + +GraphQL CLI supports custom plugins, [you can find a tutorial and example here](./docs/CUSTOM_EXTENSION.md) + ## Help & Community [![Discord Chat](https://img.shields.io/discord/625400653321076807)](https://discord.gg/xud7bH9) Join our [Discord chat](https://discord.gg/xud7bH9) if you run into issues or have questions. We're excited to welcome you to the community! diff --git a/docs/CUSTOM_EXTENSION.md b/docs/CUSTOM_EXTENSION.md new file mode 100644 index 000000000..7bb49e46c --- /dev/null +++ b/docs/CUSTOM_EXTENSION.md @@ -0,0 +1,130 @@ +## Writing your own GraphQL-CLI Extension + +`graphql-cli` allow you to write your own plugin/extenion, and intergrate external tools and configuration, and run it from a single CLI. + +The current implementation of `graphql-cli` is using [Commander](https://github.com/tj/commander.js#common-option-types-boolean-and-value) to manage it's CLI commands, and it exposes a ready-to-use `Commander` instance that you can extend and add logic to it. + +Plugins and extension are treated as NodeJS module by the `graphql-cli`, so it means you can use JavaScript/TypeScript/Any other super-set of JavaScript to write your extension. It means that you plugin will be loaded by it's name under `node_modules` - for example `graphql-cli my-custom-plugin ...`. + +`graphql-cli` also support `graphql-config`, so it can help you easily load your GraphQL schema, operations and configuration from a unified config file. + +> If you are wrapping an existing tool that has it's own CLI already, consider to expose a programtic API so it will be easier to consume. + +### TL;DR + +We have a ready-to-use boilerplate for that purpose, [you can find it here](https://github.com/dotansimha/graphql-cli-plugin-example). + +Also, inside this repo, under `packages/commands` you can find a set of plugins implementation you can use as reference. + +### Getting Started + +Start by creating a simple JavaScript/TypeScript project, according to your preference, and have your `index` file exporting a variable called `plugin`, structed as object, with `init` method. + +The `init` method will get trigged by the CLI host, and will pass the following to your method: + +- `cwd` - The current directory. +- `program` - A `commander` instance you can use to register your CLI commands. +- `loadConfig` - A method you can use to load a GraphQL schema or documents, based on `graphql-config`. +- `reportError` - Helper method that allow you to report errors back to the GraphQL CLI, and effect the exit code of the CLI host. It's useful if you are dealing with async code in your extension. + +It should be similar to this if you are using plain JavaScript: + +```js +module.exports = { + plugin: { + init: ({ cwd, program, loadConfig, reportError }) => { + // Your plugin code here, you can use "program" to register sub-commands. + } + } +}; +``` + +Or, with TypeScript: + +```ts +import { plugin } from '@test-graphql-cli/common'; + +export const plugin: CliPlugin = { + init({ cwd, program, loadConfig, reportError }) { + // Your plugin code here + } +}; +``` + +## Registering CLI sub-commands + +To register your CLI commands, use the `program` instance: + +```ts +program.command('my-plugin').action(async (cmd: string) => { + // do something +}); +``` + +Now, your plugin will be avaiable to use with the following command: `graphql my-plugin`. + +You can also add custom validations, flags, default values and much more with Commander. [You can read the documentation here](https://github.com/tj/commander.js#common-option-types-boolean-and-value). + +## Testing your plugin locally + +To test your plugin locally, install `graphql-cli` in your project as a `devDependency`, and run the following command: + +``` +graphql ./src/index.js +``` + +If you registerd sub-commands, you should be able to run those this way: + +``` +graphql ./src/index.js do-something +``` + +> The path should point to the entry point of your script, and if you are using TypeScript - point to the compile file. + +## Loading GraphQL Schema + +To easily load GraphQL schema, you can use `loadConfig` to get it from a `graphql-config` file: + +```ts +import { plugin } from '@test-graphql-cli/common'; + +export const plugin: CliPlugin = { + async init({ cwd, program, loadConfig, reportError }) { + const config = await loadConfig(); + const schema = await config.getSchema(); + } +}; +``` + +> You can also extend the `loadConfig` behavior by specifying custom loaders and extensions. + +If you are using `graphql-config` to define your configuration, and you wish to load your extenion config from it, do: + +```ts +type MyConfig = { ... }; + +const extensionConfig = await config.extension('my-plugin'); +``` + +## Error Handling + +If you wish to fail the execution of your plugin and report it back to GraphQL CLI host, you should use `reportError`: + +```ts +import { plugin } from '@test-graphql-cli/common'; + +export const plugin: CliPlugin = { + async init({ cwd, program, loadConfig, reportError }) { + try { + // do something risky + // or, throw: + + if (somethingIsMissing) { + return reportError(new Error(`Ooops, something is missing`)); + } + } catch (e) { + reportError(e); + } + } +}; +``` diff --git a/MIGRATION.md b/docs/MIGRATION.md similarity index 100% rename from MIGRATION.md rename to docs/MIGRATION.md diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index cbef1bc99..7e63db813 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -4,9 +4,7 @@ import { LoadConfigOptions } from '@test-graphql-cli/common'; import { loadConfig } from 'graphql-config'; export async function cli(argv = process.argv): Promise { - try { - const rootCommand = argv[2]; if (!rootCommand || rootCommand === '') { @@ -33,30 +31,34 @@ export async function cli(argv = process.argv): Promise { cwd: process.cwd(), program, reportError, - loadConfig: (loadConfigOptions: LoadConfigOptions = {}) => loadConfig({ - rootDir: process.cwd(), - throwOnEmpty: false, - throwOnMissing: false, - ...loadConfigOptions, - }).then(c => { - const projectNames = Object.keys(c.projects); - if (projectName && !projectNames.includes(projectName)) { - throw new Error(` + loadConfig: (loadConfigOptions: LoadConfigOptions = {}) => + loadConfig({ + rootDir: process.cwd(), + throwOnEmpty: false, + throwOnMissing: false, + ...loadConfigOptions + }).then(c => { + const projectNames = Object.keys(c.projects); + if (projectName && !projectNames.includes(projectName)) { + throw new Error(` You don't have project ${projectName}. Available projects are ${projectNames.join(',')}. `); - } - if (!projectNames.includes('default') && projectNames.length > 0) { - throw new Error(` + } + if (!projectNames.includes('default') && projectNames.length > 0) { + throw new Error(` You don't have 'default' project so you need to specify a project name. Available projects are ${projectNames.join(',')}. - `) - } - projectName = 'default'; - return c.getProject(projectName); - }), + `); + } + projectName = 'default'; + return c.getProject(projectName); + }) }); + // Remove the root object before running, to allow develoeprs to write + // their own sub-commands. + argv.splice(2, 1); program.parse(argv); if (program.project) { @@ -66,7 +68,6 @@ export async function cli(argv = process.argv): Promise { if (program.require) { await import(program.require); } - } catch (e) { console.error(e); process.exit(1);