From 975f29ed6e21c7354c42ed778dfd1b52287f70c6 Mon Sep 17 00:00:00 2001 From: Cristina Shaver Date: Sat, 22 Aug 2020 10:03:26 -0700 Subject: [PATCH] fix: improve setSchema & schema loading, allow primitive schema (#1648) - allow null schema - `api.setSchema()` works on it's own, and static string is evaluated as a schema - `api.setSchema()` now allows `string | GraphQLSchema` Co-authored-by: Rikki --- examples/monaco-graphql-webpack/src/index.ts | 120 +++++++++++++++++- .../src/LanguageService.ts | 71 ++++++----- .../src/schemaLoader.ts | 11 +- packages/monaco-graphql/src/GraphQLWorker.ts | 8 +- packages/monaco-graphql/src/api.ts | 62 ++++++--- packages/monaco-graphql/src/graphqlMode.ts | 4 + packages/monaco-graphql/src/workerManager.ts | 1 + yarn.lock | 4 +- 8 files changed, 216 insertions(+), 65 deletions(-) diff --git a/examples/monaco-graphql-webpack/src/index.ts b/examples/monaco-graphql-webpack/src/index.ts index bc2214817c5..6f053034130 100644 --- a/examples/monaco-graphql-webpack/src/index.ts +++ b/examples/monaco-graphql-webpack/src/index.ts @@ -28,11 +28,74 @@ window.MonacoEnvironment = { }, }; +const schemas = { + remote: { + op: ` +query Example($limit: Int) { + launchesPast(limit: $limit) { + mission_name + # format me using the right click context menu + launch_date_local + launch_site { + site_name_long + } + links { + article_link + video_link + } + } +} + `, + variables: `{ "limit": 10 }`, + load: ({ op, variables }: { op: string; variables: string }) => { + GraphQLAPI.setSchemaConfig({ uri: SCHEMA_URL }); + variablesEditor.setValue(variables); + operationEditor.setValue(op); + }, + }, + local: { + op: `query Example { + allTodos { + id + name + } +}`, + load: ({ op }: { op: string }) => { + setRawSchema(); + variablesEditor.setValue('{}'); + operationEditor.setValue(op); + }, + }, +}; + +const THEME = 'vs-dark'; + const schemaInput = document.createElement('input'); schemaInput.type = 'text'; schemaInput.value = SCHEMA_URL; +const selectEl = document.createElement('select'); + +selectEl.onchange = e => { + e.preventDefault(); + const type = selectEl.value as 'local' | 'remote'; + if (schemas[type]) { + // @ts-ignore + schemas[type].load(schemas[type]); + } +}; + +const createOption = (label: string, value: string) => { + const el = document.createElement('option'); + el.label = label; + el.value = value; + return el; +}; + +selectEl.appendChild(createOption('Remote', 'remote')); +selectEl.appendChild(createOption('Local', 'local')); + schemaInput.onkeyup = e => { e.preventDefault(); // @ts-ignore @@ -44,6 +107,41 @@ schemaInput.onkeyup = e => { const toolbar = document.getElementById('toolbar'); toolbar?.appendChild(schemaInput); +toolbar?.appendChild(selectEl); + +async function setRawSchema() { + await GraphQLAPI.setSchema(`# Enumeration type for a level of priority + enum Priority { + LOW + MEDIUM + HIGH + } + + # Our main todo type + type Todo { + id: ID! + name: String! + description: String + priority: Priority! + } + + type Query { + # Get one todo item + todo(id: ID!): Todo + # Get all todo items + allTodos: [Todo!]! + } + + type Mutation { + addTodo(name: String!, priority: Priority = LOW): Todo! + removeTodo(id: ID!): Todo! + } + + schema { + query: Query + mutation: Mutation + }`); +} const variablesModel = monaco.editor.createModel( `{}`, @@ -56,6 +154,7 @@ const resultsEditor = monaco.editor.create( { model: variablesModel, automaticLayout: true, + theme: THEME, }, ); const variablesEditor = monaco.editor.create( @@ -64,11 +163,12 @@ const variablesEditor = monaco.editor.create( value: `{ "limit": 10 }`, language: 'json', automaticLayout: true, + theme: THEME, }, ); const model = monaco.editor.createModel( ` -query Example($limit: Int) { +query Example($limit: Int) { launchesPast(limit: $limit) { mission_name # format me using the right click context menu @@ -95,6 +195,7 @@ const operationEditor = monaco.editor.create( formatOnPaste: true, formatOnType: true, folding: true, + theme: THEME, }, ); @@ -104,8 +205,6 @@ GraphQLAPI.setFormattingOptions({ }, }); -GraphQLAPI.setSchemaConfig({ uri: SCHEMA_URL }); - /** * Basic Operation Exec Example */ @@ -148,6 +247,17 @@ operationEditor.addAction(opAction); variablesEditor.addAction(opAction); resultsEditor.addAction(opAction); +/** + * load local schema by default + */ + +let initialSchema = false; + +if (!initialSchema) { + schemas.remote.load(schemas.remote); + initialSchema = true; +} + // add your own diagnostics? why not! // monaco.editor.setModelMarkers( // model, @@ -161,7 +271,3 @@ resultsEditor.addAction(opAction); // endColumn: 0, // }], // ); - -// operationEditor.onDidChangeModelContent(() => { -// // this is where -// }) diff --git a/packages/graphql-language-service/src/LanguageService.ts b/packages/graphql-language-service/src/LanguageService.ts index 762bede130f..ee1d90da796 100644 --- a/packages/graphql-language-service/src/LanguageService.ts +++ b/packages/graphql-language-service/src/LanguageService.ts @@ -12,8 +12,6 @@ import { getHoverInformation, } from 'graphql-language-service-interface'; -import { RawSchema } from './types'; - import { defaultSchemaLoader, SchemaConfig, @@ -25,7 +23,7 @@ export type GraphQLLanguageConfig = { parser?: typeof parse; schemaLoader?: typeof defaultSchemaLoader; schemaBuilder?: typeof defaultSchemaBuilder; - rawSchema?: RawSchema; + schemaString?: string; parseOptions?: ParseOptions; schemaConfig: SchemaConfig; }; @@ -37,16 +35,16 @@ export class LanguageService { private _schemaResponse: SchemaResponse | null = null; private _schemaLoader: ( schemaConfig: SchemaConfig, - ) => Promise = defaultSchemaLoader; + ) => Promise = defaultSchemaLoader; private _schemaBuilder = defaultSchemaBuilder; - private _rawSchema: RawSchema | null = null; + private _schemaString: string | null = null; private _parseOptions: ParseOptions | undefined = undefined; constructor({ parser, schemaLoader, schemaBuilder, schemaConfig, - rawSchema, + schemaString, parseOptions, }: GraphQLLanguageConfig) { this._schemaConfig = schemaConfig; @@ -59,8 +57,8 @@ export class LanguageService { if (schemaBuilder) { this._schemaBuilder = schemaBuilder; } - if (rawSchema) { - this._rawSchema = rawSchema; + if (schemaString) { + this._schemaString = schemaString; } if (parseOptions) { this._parseOptions = parseOptions; @@ -80,28 +78,28 @@ export class LanguageService { /** * setSchema statically, ignoring URI - * @param schema {RawSchema} + * @param schema {schemaString} */ - public async setSchema(schema: RawSchema): Promise { - this._rawSchema = schema; - return this.loadSchema(); + public async setSchema(schema: string): Promise { + this._schemaString = schema; + await this.loadSchema(); } - public async getSchemaResponse(): Promise { + public async getSchemaResponse(): Promise { if (this._schemaResponse) { return this._schemaResponse; } return this.loadSchemaResponse(); } - public async loadSchemaResponse(): Promise { - if (this._rawSchema) { - return typeof this._rawSchema === 'string' - ? this.parse(this._rawSchema) - : this._rawSchema; + public async loadSchemaResponse(): Promise { + if (this._schemaString) { + return typeof this._schemaString === 'string' + ? this.parse(this._schemaString) + : this._schemaString; } if (!this._schemaConfig?.uri) { - throw new Error('uri missing'); + return null; } this._schemaResponse = (await this._schemaLoader( this._schemaConfig, @@ -111,11 +109,15 @@ export class LanguageService { public async loadSchema() { const schemaResponse = await this.loadSchemaResponse(); - this._schema = this._schemaBuilder( - schemaResponse, - this._schemaConfig.buildSchemaOptions, - ) as GraphQLSchema; - return this._schema; + if (schemaResponse) { + this._schema = this._schemaBuilder( + schemaResponse, + this._schemaConfig.buildSchemaOptions, + ) as GraphQLSchema; + return this._schema; + } else { + return null; + } } public async parse(text: string, options?: ParseOptions) { @@ -126,23 +128,34 @@ export class LanguageService { _uri: string, documentText: string, position: Position, - ) => - getAutocompleteSuggestions(await this.getSchema(), documentText, position); + ) => { + const schema = await this.getSchema(); + if (!schema) { + return []; + } + return getAutocompleteSuggestions(schema, documentText, position); + }; public getDiagnostics = async ( _uri: string, documentText: string, customRules?: ValidationRule[], ) => { - if (!documentText || documentText.length < 1) { + const schema = await this.getSchema(); + if (!documentText || documentText.length < 1 || !schema) { return []; } - return getDiagnostics(documentText, await this.getSchema(), customRules); + return getDiagnostics(documentText, schema, customRules); }; public getHover = async ( _uri: string, documentText: string, position: Position, - ) => getHoverInformation(await this.getSchema(), documentText, position); + ) => + getHoverInformation( + (await this.getSchema()) as GraphQLSchema, + documentText, + position, + ); } diff --git a/packages/graphql-language-service/src/schemaLoader.ts b/packages/graphql-language-service/src/schemaLoader.ts index 52d45cb3b6d..d806224f534 100644 --- a/packages/graphql-language-service/src/schemaLoader.ts +++ b/packages/graphql-language-service/src/schemaLoader.ts @@ -9,7 +9,7 @@ import { } from 'graphql'; export type SchemaConfig = { - uri: string; + uri?: string; requestOpts?: RequestInit; introspectionOptions?: IntrospectionOptions; buildSchemaOptions?: BuildSchemaOptions; @@ -17,12 +17,17 @@ export type SchemaConfig = { export type SchemaResponse = IntrospectionQuery | DocumentNode; -export type SchemaLoader = (config: SchemaConfig) => Promise; +export type SchemaLoader = ( + config: SchemaConfig, +) => Promise; export const defaultSchemaLoader: SchemaLoader = async ( schemaConfig: SchemaConfig, -): Promise => { +): Promise => { const { requestOpts, uri, introspectionOptions } = schemaConfig; + if (!uri) { + return null; + } const fetchResult = await fetch(uri, { method: requestOpts?.method ?? 'post', body: JSON.stringify({ diff --git a/packages/monaco-graphql/src/GraphQLWorker.ts b/packages/monaco-graphql/src/GraphQLWorker.ts index 86daacbf3f7..5e6ee6d2519 100644 --- a/packages/monaco-graphql/src/GraphQLWorker.ts +++ b/packages/monaco-graphql/src/GraphQLWorker.ts @@ -12,7 +12,6 @@ import type { worker, editor, Position, IRange } from 'monaco-editor'; import { getRange, LanguageService } from 'graphql-language-service'; import type { - RawSchema, SchemaResponse, CompletionItem as GraphQLCompletionItem, } from 'graphql-language-service'; @@ -37,20 +36,19 @@ export class GraphQLWorker { private _formattingOptions: FormattingOptions | undefined; constructor(ctx: worker.IWorkerContext, createData: ICreateData) { this._ctx = ctx; - // if you must, we have a nice default schema loader at home this._languageService = new LanguageService(createData.languageConfig); this._formattingOptions = createData.formattingOptions; } - async getSchemaResponse(_uri?: string): Promise { + async getSchemaResponse(_uri?: string): Promise { return this._languageService.getSchemaResponse(); } - async setSchema(schema: RawSchema): Promise { + async setSchema(schema: string): Promise { await this._languageService.setSchema(schema); } - async loadSchema(_uri?: string): Promise { + async loadSchema(_uri?: string): Promise { return this._languageService.getSchema(); } diff --git a/packages/monaco-graphql/src/api.ts b/packages/monaco-graphql/src/api.ts index 12ba180ce2e..b275299dc9f 100644 --- a/packages/monaco-graphql/src/api.ts +++ b/packages/monaco-graphql/src/api.ts @@ -5,17 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -import type { - SchemaConfig, - RawSchema, - SchemaResponse, -} from 'graphql-language-service'; +import { SchemaConfig, SchemaResponse } from 'graphql-language-service'; + import type { FormattingOptions, ModeConfiguration } from './typings'; import type { WorkerAccessor } from './languageFeatures'; import type { IEvent } from 'monaco-editor'; import { Emitter } from 'monaco-editor'; -import { DocumentNode } from 'graphql'; +import { DocumentNode, GraphQLSchema, printSchema } from 'graphql'; export type LanguageServiceAPIOptions = { languageId: string; @@ -26,11 +23,14 @@ export type LanguageServiceAPIOptions = { export class LanguageServiceAPI { private _onDidChange = new Emitter(); - private _schemaConfig!: SchemaConfig; + private _schemaConfig: SchemaConfig = {}; private _formattingOptions!: FormattingOptions; private _modeConfiguration!: ModeConfiguration; private _languageId: string; private _worker: WorkerAccessor | null; + private _workerPromise: Promise; + private _resolveWorkerPromise: (value: WorkerAccessor) => void = () => {}; + private _schemaString: string | null = null; constructor({ languageId, @@ -39,8 +39,13 @@ export class LanguageServiceAPI { formattingOptions, }: LanguageServiceAPIOptions) { this._worker = null; + this._workerPromise = new Promise(resolve => { + this._resolveWorkerPromise = resolve; + }); this._languageId = languageId; - this.setSchemaConfig(schemaConfig); + if (schemaConfig && schemaConfig.uri) { + this.setSchemaConfig(schemaConfig); + } this.setModeConfiguration(modeConfiguration); this.setFormattingOptions(formattingOptions); } @@ -60,34 +65,55 @@ export class LanguageServiceAPI { public get formattingOptions(): FormattingOptions { return this._formattingOptions; } - public get worker(): WorkerAccessor { - return this._worker as WorkerAccessor; + public get hasSchema() { + return Boolean(this._schemaString); + } + public get schemaString() { + return this._schemaString; + } + public get worker(): Promise { + if (this._worker) { + return Promise.resolve(this._worker); + } + return this._workerPromise; } setWorker(worker: WorkerAccessor) { this._worker = worker; + this._resolveWorkerPromise(worker); } - public async getSchema(): Promise { - const langWorker = await this.worker(); + public async getSchema(): Promise { + if (this._schemaString) { + return this._schemaString; + } + const langWorker = await (await this.worker)(); return langWorker.getSchemaResponse(); } - public async setSchema(schema: RawSchema): Promise { - const langWorker = await this.worker(); - await langWorker.setSchema(schema); + public async setSchema(schema: string | GraphQLSchema): Promise { + let rawSchema = schema as string; + + if (typeof schema !== 'string') { + rawSchema = printSchema(schema, { commentDescriptions: true }); + } + this._schemaString = rawSchema; + const langWorker = await (await this.worker)(); + await langWorker.setSchema(rawSchema); this._onDidChange.fire(this); } public async parse(graphqlString: string): Promise { - const langWorker = await this.worker(); + const langWorker = await (await this.worker)(); return langWorker.doParse(graphqlString); } public setSchemaConfig(options: SchemaConfig): void { this._schemaConfig = options || Object.create(null); + this._onDidChange.fire(this); } public updateSchemaConfig(options: Partial): void { this._schemaConfig = { ...this._schemaConfig, ...options }; + this._onDidChange.fire(this); } @@ -119,9 +145,7 @@ export const modeConfigurationDefault: Required = { selectionRanges: false, }; -export const schemaDefault: SchemaConfig = { - uri: 'http://localhost:8000', -}; +export const schemaDefault: SchemaConfig = {}; export const formattingDefaults: FormattingOptions = { prettierConfig: { diff --git a/packages/monaco-graphql/src/graphqlMode.ts b/packages/monaco-graphql/src/graphqlMode.ts index 448a899ee82..8416948e474 100644 --- a/packages/monaco-graphql/src/graphqlMode.ts +++ b/packages/monaco-graphql/src/graphqlMode.ts @@ -34,6 +34,7 @@ export function setupMode(defaults: LanguageServiceAPI): IDisposable { throw Error('Error fetching graphql language service worker'); } }; + defaults.setWorker(worker); monaco.languages.setLanguageConfiguration(languageId, richLanguageConfig); @@ -83,6 +84,9 @@ export function setupMode(defaults: LanguageServiceAPI): IDisposable { let { modeConfiguration, schemaConfig, formattingOptions } = defaults; defaults.onDidChange(newDefaults => { + if (defaults.schemaString !== newDefaults.schemaString) { + registerProviders(); + } if (newDefaults.modeConfiguration !== modeConfiguration) { modeConfiguration = newDefaults.modeConfiguration; registerProviders(); diff --git a/packages/monaco-graphql/src/workerManager.ts b/packages/monaco-graphql/src/workerManager.ts index 33adefc4e42..15718f85958 100644 --- a/packages/monaco-graphql/src/workerManager.ts +++ b/packages/monaco-graphql/src/workerManager.ts @@ -77,6 +77,7 @@ export class WorkerManager { languageId: this._defaults.languageId, formattingOptions: this._defaults.formattingOptions, languageConfig: { + schemaString: this._defaults.schemaString, schemaConfig: this._defaults.schemaConfig, }, } as ICreateData, diff --git a/yarn.lock b/yarn.lock index 483a1db4734..4b4f708cdb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10264,7 +10264,7 @@ grapheme-breaker@^0.3.2: unicode-trie "^0.3.1" "graphiql@file:packages/graphiql": - version "2.0.0-alpha.2" + version "2.0.0-alpha.3" dependencies: "@emotion/core" "^10.0.28" "@mdx-js/react" "^1.5.2" @@ -10277,7 +10277,7 @@ grapheme-breaker@^0.3.2: i18next-browser-languagedetector "^4.1.1" markdown-it "^10.0.0" monaco-editor "^0.20.0" - monaco-graphql "^0.3.1-alpha.0" + monaco-graphql "^0.3.1-alpha.1" react-i18next "^11.4.0" theme-ui "^0.3.1"