diff --git a/packages/codemirror-graphql/README.md b/packages/codemirror-graphql/README.md index 0959bf1402d..1ed8ba49e2e 100644 --- a/packages/codemirror-graphql/README.md +++ b/packages/codemirror-graphql/README.md @@ -37,5 +37,47 @@ CodeMirror.fromTextArea(myTextarea, { }); ``` +## External Fragments Example + +If you want to have autcompletion for external fragment definitions, there's a new configuration setting available + +```ts +import { parse, visit } from 'graphql'; +import CodeMirror from 'codemirror'; +import 'codemirror/addon/hint/show-hint'; +import 'codemirror/addon/lint/lint'; +import 'codemirror-graphql/hint'; +import 'codemirror-graphql/lint'; +import 'codemirror-graphql/mode'; + +const externalFragmentsExample = ` + fragment MyFragment on Example { + id: ID! + name: String! + } + fragment AnotherFragment on Example { + id: ID! + title: String! + } +`; + +const fragmentDefinitions = visit(parse(externalFragmentsExample), { + FragmentDefinition(node) { + return node; + }, +}); + +CodeMirror.fromTextArea(myTextarea, { + mode: 'graphql', + lint: { + schema: myGraphQLSchema, + }, + hintOptions: { + schema: myGraphQLSchema, + externalFragmentDefinitions: fragmentDefinitions, + }, +}); +``` + Build for the web with [webpack](http://webpack.github.io/) or [browserify](http://browserify.org/). diff --git a/packages/codemirror-graphql/src/hint.js b/packages/codemirror-graphql/src/hint.js index abeebdad5bc..1b8854cca01 100644 --- a/packages/codemirror-graphql/src/hint.js +++ b/packages/codemirror-graphql/src/hint.js @@ -52,6 +52,7 @@ CodeMirror.registerHelper('hint', 'graphql', (editor, options) => { editor.getValue(), position, token, + options.externalFragmentDefinitions || undefined, ); const results = { diff --git a/packages/graphiql/README.md b/packages/graphiql/README.md index 893ddfda7fc..89750e77526 100644 --- a/packages/graphiql/README.md +++ b/packages/graphiql/README.md @@ -178,7 +178,8 @@ For more details on props, see the [API Docs](https://graphiql-test.netlify.app/ | `query` | `string` (GraphQL) | initial displayed query, if `undefined` is provided, the stored query or `defaultQuery` will be used. You can also set this value at runtime to override the current operation editor state. | | `validationRules` | `ValidationRule[]` | A array of validation rules that will be used for validating the GraphQL operations. If `undefined` is provided, the default rules (exported as `specifiedRules` from `graphql`) will be used. | | `variables` | `string` (JSON) | initial displayed query variables, if `undefined` is provided, the stored variables will be used. | -| `headers` | `string` (JSON) | initial displayed request headers. if not defined, it will default to the stored headers if `shouldPersistHeaders` is enabled. | +| `headers` | `string` | initial displayed request headers. if not defined, it will default to the stored headers if `shouldPersistHeaders` is enabled. | +| `externalFragments` | `string | FragmentDefinitionNode[]` | provide fragments external to the operation for completion, validation, and for use when executing operations. | | `operationName` | `string` | an optional name of which GraphQL operation should be executed. | | `response` | `string` (JSON) | an optional JSON string to use as the initial displayed response. If not provided, no response will be initially shown. You might provide this if illustrating the result of the initial query. | | `storage` | [`Storage`](https://graphiql-test.netlify.app/typedoc/interfaces/graphiql.storage.html) | **Default:** `window.localStorage`. an interface that matches `window.localStorage` signature that GraphiQL will use to persist state. | diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index fb9080f0007..07c6a5920b6 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -21,6 +21,7 @@ import { IntrospectionQuery, GraphQLType, ValidationRule, + FragmentDefinitionNode, } from 'graphql'; import copyToClipboard from 'copy-to-clipboard'; @@ -122,6 +123,7 @@ export type GraphiQLProps = { defaultSecondaryEditorOpen?: boolean; headerEditorEnabled?: boolean; shouldPersistHeaders?: boolean; + externalFragments?: string | FragmentDefinitionNode[]; onCopyQuery?: (query?: string) => void; onEditQuery?: (query?: string) => void; onEditVariables?: (value: string) => void; @@ -558,6 +560,7 @@ export class GraphiQL extends React.Component { onRunQuery={this.handleEditorRunQuery} editorTheme={this.props.editorTheme} readOnly={this.props.readOnly} + externalFragments={this.props.externalFragments} />
void; onRunQuery?: () => void; editorTheme?: string; + externalFragments?: string | FragmentDefinitionNode[]; }; +function gatherFragmentDefinitions(graphqlString: string) { + const definitions: FragmentDefinitionNode[] = []; + visit(parse(graphqlString), { + FragmentDefinition(node) { + definitions.push(node); + }, + }); + return definitions; +} /** * QueryEditor * @@ -84,6 +101,27 @@ export class QueryEditor extends React.Component require('codemirror-graphql/jump'); require('codemirror-graphql/mode'); + const hintOptions: { + schema?: GraphQLSchema; + externalFragmentDefinitions?: FragmentDefinitionNode[]; + closeOnUnfocus: boolean; + completeSingle: boolean; + container: any; + } = { + schema: this.props.schema, + closeOnUnfocus: false, + completeSingle: false, + container: this._node, + }; + + if (this.props.externalFragments) { + hintOptions.externalFragmentDefinitions = Array.isArray( + this.props?.externalFragments, + ) + ? this.props.externalFragments + : gatherFragmentDefinitions(this.props.externalFragments); + } + const editor: CM.Editor = (this.editor = CodeMirror(this._node, { value: this.props.value || '', lineNumbers: true, @@ -102,12 +140,7 @@ export class QueryEditor extends React.Component schema: this.props.schema, validationRules: this.props.validationRules ?? null, }, - hintOptions: { - schema: this.props.schema, - closeOnUnfocus: false, - completeSingle: false, - container: this._node, - }, + hintOptions, info: { schema: this.props.schema, renderDescription: (text: string) => md.render(text), diff --git a/packages/graphql-language-service-interface/src/GraphQLLanguageService.ts b/packages/graphql-language-service-interface/src/GraphQLLanguageService.ts index 9b15322edf6..e9d3c59c6a1 100644 --- a/packages/graphql-language-service-interface/src/GraphQLLanguageService.ts +++ b/packages/graphql-language-service-interface/src/GraphQLLanguageService.ts @@ -231,7 +231,7 @@ export class GraphQLLanguageService { ); const fragmentInfo = Array.from(fragmentDefinitions).map( - ([, info]) => info, + ([, info]) => info.definition, ); if (schema) { @@ -240,7 +240,7 @@ export class GraphQLLanguageService { query, position, undefined, - fragmentInfo.map(({ definition }) => definition), + fragmentInfo, ); } return []; diff --git a/packages/graphql-language-service/src/LanguageService.ts b/packages/graphql-language-service/src/LanguageService.ts index f7f53d3a922..5f7951cc264 100644 --- a/packages/graphql-language-service/src/LanguageService.ts +++ b/packages/graphql-language-service/src/LanguageService.ts @@ -4,7 +4,14 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ -import { parse, GraphQLSchema, ParseOptions, ValidationRule } from 'graphql'; +import { + parse, + GraphQLSchema, + ParseOptions, + ValidationRule, + FragmentDefinitionNode, + visit, +} from 'graphql'; import type { Position } from 'graphql-language-service-types'; import { getAutocompleteSuggestions, @@ -26,6 +33,7 @@ export type GraphQLLanguageConfig = { schemaString?: string; parseOptions?: ParseOptions; schemaConfig: SchemaConfig; + exteralFragmentDefinitions?: FragmentDefinitionNode[] | string; }; export class LanguageService { @@ -39,6 +47,10 @@ export class LanguageService { private _schemaBuilder = defaultSchemaBuilder; private _schemaString: string | null = null; private _parseOptions: ParseOptions | undefined = undefined; + private _exteralFragmentDefinitionNodes: + | FragmentDefinitionNode[] + | null = null; + private _exteralFragmentDefinitionsString: string | null = null; constructor({ parser, schemaLoader, @@ -46,6 +58,7 @@ export class LanguageService { schemaConfig, schemaString, parseOptions, + exteralFragmentDefinitions, }: GraphQLLanguageConfig) { this._schemaConfig = schemaConfig; if (parser) { @@ -63,6 +76,13 @@ export class LanguageService { if (parseOptions) { this._parseOptions = parseOptions; } + if (exteralFragmentDefinitions) { + if (Array.isArray(exteralFragmentDefinitions)) { + this._exteralFragmentDefinitionNodes = exteralFragmentDefinitions; + } else { + this._exteralFragmentDefinitionsString = exteralFragmentDefinitions; + } + } } public get schema() { @@ -76,6 +96,31 @@ export class LanguageService { return this.loadSchema(); } + public async getExternalFragmentDefinitions(): Promise< + FragmentDefinitionNode[] + > { + if ( + !this._exteralFragmentDefinitionNodes && + this._exteralFragmentDefinitionsString + ) { + const definitionNodes: FragmentDefinitionNode[] = []; + try { + visit(await this._parser(this._exteralFragmentDefinitionsString), { + FragmentDefinition(node) { + definitionNodes.push(node); + }, + }); + } catch (err) { + throw Error( + `Failed parsing exteralFragmentDefinitions string:\n${this._exteralFragmentDefinitionsString}`, + ); + } + + this._exteralFragmentDefinitionNodes = definitionNodes; + } + return this._exteralFragmentDefinitionNodes as FragmentDefinitionNode[]; + } + /** * setSchema statically, ignoring URI * @param schema {schemaString} @@ -133,7 +178,13 @@ export class LanguageService { if (!documentText || documentText.length < 1 || !schema) { return []; } - return getAutocompleteSuggestions(schema, documentText, position); + return getAutocompleteSuggestions( + schema, + documentText, + position, + undefined, + await this.getExternalFragmentDefinitions(), + ); }; public getDiagnostics = async ( diff --git a/packages/monaco-graphql/README.md b/packages/monaco-graphql/README.md index 27e35c44e9d..867b1af3981 100644 --- a/packages/monaco-graphql/README.md +++ b/packages/monaco-graphql/README.md @@ -2,10 +2,13 @@ GraphQL language plugin for the Monaco Editor. You can use it to build vscode/codespaces-like web or desktop IDEs using whatever frontend javascript libraries or frameworks you want, or none! -- [webpack example](../../examples/monaco-graphql-webpack/) using plain javascript -- [live demo](https://monaco-graphql.netlify.com)of the monaco webpack example +- [webpack example](https://github.com/graphql/graphiql/tree/main/examples/monaco-graphql-webpack/) using plain javascript +- [graphiql 2.x RFC example](https://github.com/graphql/graphiql/tree/main/packages/graphiql-2-rfc-context/) using react 16 +- [live demo](https://monaco-graphql.netlify.com) of the monaco webpack example (prompts for github access token!) -> **NOTE:** This is in pre-release state. Helping out with this project will help advance GraphiQL and many other GraphQL IDE projects. `codemirror-graphql` has more features, such as JSON variables validation, and is more stable. +> **NOTE:** This is in pre-release state as we build towards GraphiQL 2.0.x. [`codemirror-graphql`](https://github.com/graphql/graphiql/tree/main/packages/codemirror-graphql) has more features (such as JSON variables validation) and is more stable. + +## Features It provides the following features while editing GraphQL files: @@ -14,8 +17,9 @@ It provides the following features while editing GraphQL files: - Validation (schema driven) - Formatting - using prettier - Syntax Highlighting -- Configurable schema loading (or custom) +- Configurable schema loading (or custom) - only handles a single schema currently - Configurable formatting options +- Providing external fragments ## Usage @@ -138,6 +142,12 @@ GraphQLAPI.setFormattingOptions({ }); ``` +#### `GraphQLAPI.setExternalFragmentDefintions()` + +Append external fragments to be used by autocomplete and other language features. + +This accepts either a string that contains fragment definitions, or `TypeDefinitionNode[]` + #### `GraphQLAPI.getSchema()` Returns either an AST `DocumentNode` or `IntrospectionQuery` result json using default or provided `schemaLoader` diff --git a/packages/monaco-graphql/src/api.ts b/packages/monaco-graphql/src/api.ts index b275299dc9f..905f343cdb6 100644 --- a/packages/monaco-graphql/src/api.ts +++ b/packages/monaco-graphql/src/api.ts @@ -12,7 +12,12 @@ import type { WorkerAccessor } from './languageFeatures'; import type { IEvent } from 'monaco-editor'; import { Emitter } from 'monaco-editor'; -import { DocumentNode, GraphQLSchema, printSchema } from 'graphql'; +import { + DocumentNode, + FragmentDefinitionNode, + GraphQLSchema, + printSchema, +} from 'graphql'; export type LanguageServiceAPIOptions = { languageId: string; @@ -31,6 +36,10 @@ export class LanguageServiceAPI { private _workerPromise: Promise; private _resolveWorkerPromise: (value: WorkerAccessor) => void = () => {}; private _schemaString: string | null = null; + private _externalFragmentDefinitions: + | string + | FragmentDefinitionNode[] + | null = null; constructor({ languageId, @@ -48,6 +57,7 @@ export class LanguageServiceAPI { } this.setModeConfiguration(modeConfiguration); this.setFormattingOptions(formattingOptions); + this.setFormattingOptions(formattingOptions); } public get onDidChange(): IEvent { return this._onDidChange.event; @@ -65,6 +75,9 @@ export class LanguageServiceAPI { public get formattingOptions(): FormattingOptions { return this._formattingOptions; } + public get externalFragmentDefinitions() { + return this._externalFragmentDefinitions; + } public get hasSchema() { return Boolean(this._schemaString); } @@ -113,10 +126,15 @@ export class LanguageServiceAPI { public updateSchemaConfig(options: Partial): void { this._schemaConfig = { ...this._schemaConfig, ...options }; - this._onDidChange.fire(this); } + public setExternalFragmentDefinitions( + externalFragmentDefinitions: string | FragmentDefinitionNode[], + ) { + this._externalFragmentDefinitions = externalFragmentDefinitions; + } + public setSchemaUri(schemaUri: string): void { this.setSchemaConfig({ ...this._schemaConfig, uri: schemaUri }); } diff --git a/packages/monaco-graphql/src/workerManager.ts b/packages/monaco-graphql/src/workerManager.ts index b9c8934f4b8..25c06c2a8fa 100644 --- a/packages/monaco-graphql/src/workerManager.ts +++ b/packages/monaco-graphql/src/workerManager.ts @@ -78,6 +78,8 @@ export class WorkerManager { languageConfig: { schemaString: this._defaults.schemaString, schemaConfig: this._defaults.schemaConfig, + exteralFragmentDefinitions: this._defaults + .externalFragmentDefinitions, }, } as ICreateData, }); diff --git a/resources/tsconfig.build.esm.json b/resources/tsconfig.build.esm.json index f9406e160c4..e39c8ea8876 100644 --- a/resources/tsconfig.build.esm.json +++ b/resources/tsconfig.build.esm.json @@ -28,6 +28,9 @@ }, { "path": "../packages/graphql-language-service-server/tsconfig.esm.json" + }, + { + "path": "../packages/graphql-language-service-cli/tsconfig.esm.json" } ] }