Skip to content

Commit

Permalink
feat: implied or external fragments, resolves #612
Browse files Browse the repository at this point in the history
allows external, implied fragments in autocompletion, validation, and operation execution
  • Loading branch information
acao committed Jan 4, 2021
1 parent 4038cf2 commit ab65f53
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 18 deletions.
42 changes: 42 additions & 0 deletions packages/codemirror-graphql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
1 change: 1 addition & 0 deletions packages/codemirror-graphql/src/hint.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ CodeMirror.registerHelper('hint', 'graphql', (editor, options) => {
editor.getValue(),
position,
token,
options.externalFragmentDefinitions || undefined,
);

const results = {
Expand Down
3 changes: 2 additions & 1 deletion packages/graphiql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
3 changes: 3 additions & 0 deletions packages/graphiql/src/components/GraphiQL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
IntrospectionQuery,
GraphQLType,
ValidationRule,
FragmentDefinitionNode,
} from 'graphql';
import copyToClipboard from 'copy-to-clipboard';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -558,6 +560,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
onRunQuery={this.handleEditorRunQuery}
editorTheme={this.props.editorTheme}
readOnly={this.props.readOnly}
externalFragments={this.props.externalFragments}
/>
<section
className="variable-editor secondary-editor"
Expand Down
47 changes: 40 additions & 7 deletions packages/graphiql/src/components/QueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@

import React from 'react';
import type * as CM from 'codemirror';
import { GraphQLSchema, GraphQLType, ValidationRule } from 'graphql';
import {
FragmentDefinitionNode,
GraphQLSchema,
GraphQLType,
ValidationRule,
visit,
parse,
} from 'graphql';
import MD from 'markdown-it';
import { normalizeWhitespace } from '../utility/normalizeWhitespace';
import onHasCompletion from '../utility/onHasCompletion';
Expand All @@ -30,8 +37,18 @@ type QueryEditorProps = {
onMergeQuery?: () => 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
*
Expand Down Expand Up @@ -84,6 +101,27 @@ export class QueryEditor extends React.Component<QueryEditorProps, {}>
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,
Expand All @@ -102,12 +140,7 @@ export class QueryEditor extends React.Component<QueryEditorProps, {}>
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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export class GraphQLLanguageService {
);

const fragmentInfo = Array.from(fragmentDefinitions).map(
([, info]) => info,
([, info]) => info.definition,
);

if (schema) {
Expand All @@ -240,7 +240,7 @@ export class GraphQLLanguageService {
query,
position,
undefined,
fragmentInfo.map(({ definition }) => definition),
fragmentInfo,
);
}
return [];
Expand Down
55 changes: 53 additions & 2 deletions packages/graphql-language-service/src/LanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +33,7 @@ export type GraphQLLanguageConfig = {
schemaString?: string;
parseOptions?: ParseOptions;
schemaConfig: SchemaConfig;
exteralFragmentDefinitions?: FragmentDefinitionNode[] | string;
};

export class LanguageService {
Expand All @@ -39,13 +47,18 @@ 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,
schemaBuilder,
schemaConfig,
schemaString,
parseOptions,
exteralFragmentDefinitions,
}: GraphQLLanguageConfig) {
this._schemaConfig = schemaConfig;
if (parser) {
Expand All @@ -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() {
Expand All @@ -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}
Expand Down Expand Up @@ -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 (
Expand Down
18 changes: 14 additions & 4 deletions packages/monaco-graphql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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

Expand Down Expand Up @@ -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`
Expand Down
22 changes: 20 additions & 2 deletions packages/monaco-graphql/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +36,10 @@ export class LanguageServiceAPI {
private _workerPromise: Promise<WorkerAccessor>;
private _resolveWorkerPromise: (value: WorkerAccessor) => void = () => {};
private _schemaString: string | null = null;
private _externalFragmentDefinitions:
| string
| FragmentDefinitionNode[]
| null = null;

constructor({
languageId,
Expand All @@ -48,6 +57,7 @@ export class LanguageServiceAPI {
}
this.setModeConfiguration(modeConfiguration);
this.setFormattingOptions(formattingOptions);
this.setFormattingOptions(formattingOptions);
}
public get onDidChange(): IEvent<LanguageServiceAPI> {
return this._onDidChange.event;
Expand All @@ -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);
}
Expand Down Expand Up @@ -113,10 +126,15 @@ export class LanguageServiceAPI {

public updateSchemaConfig(options: Partial<SchemaConfig>): 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 });
}
Expand Down
2 changes: 2 additions & 0 deletions packages/monaco-graphql/src/workerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export class WorkerManager {
languageConfig: {
schemaString: this._defaults.schemaString,
schemaConfig: this._defaults.schemaConfig,
exteralFragmentDefinitions: this._defaults
.externalFragmentDefinitions,
},
} as ICreateData,
});
Expand Down
Loading

0 comments on commit ab65f53

Please sign in to comment.