diff --git a/.changeset/shy-parents-bow.md b/.changeset/shy-parents-bow.md new file mode 100644 index 00000000000..90126c595f9 --- /dev/null +++ b/.changeset/shy-parents-bow.md @@ -0,0 +1,5 @@ +--- +'graphiql': minor +--- + +Add a toolbar button for manually triggering introspection diff --git a/.changeset/wild-rings-leave.md b/.changeset/wild-rings-leave.md new file mode 100644 index 00000000000..b751d4e1dd9 --- /dev/null +++ b/.changeset/wild-rings-leave.md @@ -0,0 +1,5 @@ +--- +'@graphiql/react': minor +--- + +Add a method `introspect` to the schema context and provide a short key (`Shift-Ctrl-R`) for triggering introspection diff --git a/packages/graphiql-react/src/schema.tsx b/packages/graphiql-react/src/schema.tsx index af5a0309e20..1ab32b7cefc 100644 --- a/packages/graphiql-react/src/schema.tsx +++ b/packages/graphiql-react/src/schema.tsx @@ -19,6 +19,7 @@ import { Dispatch, ReactNode, SetStateAction, + useCallback, useEffect, useMemo, useRef, @@ -32,14 +33,15 @@ import { createContextHook, createNullableContext } from './utility/context'; * There's a semantic difference between `null` and `undefined`: * - When `null` is passed explicitly as prop, GraphiQL will run schema-less * (i.e. it will never attempt to fetch the schema, even when calling the - * `useFetchSchema` hook). + * `introspect` function). * - When `schema` is `undefined` GraphiQL will attempt to fetch the schema - * when calling `useFetchSchema`. + * when calling `introspect`. */ type MaybeGraphQLSchema = GraphQLSchema | null | undefined; export type SchemaContextType = { fetchError: string | null; + introspect(): void; isFetching: boolean; schema: MaybeGraphQLSchema; setFetchError: Dispatch>; @@ -68,6 +70,12 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { const [isFetching, setIsFetching] = useState(false); const [fetchError, setFetchError] = useState(null); + /** + * A counter that is incremented each time introspection is triggered or the + * schema state is updated. + */ + const counterRef = useRef(0); + /** * Synchronize prop changes with state */ @@ -79,6 +87,12 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { ? props.schema : undefined, ); + + /** + * Increment the counter so that in-flight introspection requests don't + * override this change. + */ + counterRef.current++; }, [props.schema]); /** @@ -108,7 +122,7 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { * Fetch the schema */ const { fetcher, onSchemaChange } = props; - useEffect(() => { + const introspect = useCallback(() => { /** * Only introspect if there is no schema provided via props. If the * prop is passed an introspection result, we do continue but skip the @@ -118,11 +132,12 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { return; } - let isActive = true; + const counter = ++counterRef.current; + setSchema(undefined); const maybeIntrospectionData = props.schema; - async function introspect() { + async function fetchIntrospectionData() { if (maybeIntrospectionData) { // No need to introspect if we already have the data return maybeIntrospectionData; @@ -193,43 +208,37 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { setFetchError(responseString); } - introspect() + fetchIntrospectionData() .then(introspectionData => { - // Don't continue if the effect has already been cleaned up - if (!isActive || !introspectionData) { + console.log(counter, counterRef.current); + /** + * Don't continue if another introspection request has been started in + * the meantime or if there is no introspection data. + */ + if (counter !== counterRef.current || !introspectionData) { return; } try { const newSchema = buildClientSchema(introspectionData); - // Only override the schema in state if it's still `undefined` (the - // prop and thus the state could have changed while introspecting, - // so this avoids a race condition by prioritizing the state that - // was set after the introspection request was initialized) - setSchema(current => { - if (current === undefined) { - onSchemaChange?.(newSchema); - return newSchema; - } - return current; - }); + setSchema(newSchema); + onSchemaChange?.(newSchema); } catch (error) { setFetchError(formatError(error as Error)); } }) .catch(error => { - // Don't continue if the effect has already been cleaned up - if (!isActive) { + /** + * Don't continue if another introspection request has been started in + * the meantime. + */ + if (counter !== counterRef.current) { return; } setFetchError(formatError(error)); setIsFetching(false); }); - - return () => { - isActive = false; - }; }, [ fetcher, introspectionQueryName, @@ -239,6 +248,26 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { props.schema, ]); + /** + * Trigger introspection automatically + */ + useEffect(() => { + introspect(); + }, [introspect]); + + /** + * Trigger introspection manually via short key + */ + useEffect(() => { + function triggerIntrospection(event: KeyboardEvent) { + if (event.keyCode === 82 && event.shiftKey && event.ctrlKey) { + introspect(); + } + } + window.addEventListener('keydown', triggerIntrospection); + return () => window.removeEventListener('keydown', triggerIntrospection); + }); + /** * Derive validation errors from the schema */ @@ -256,13 +285,14 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { const value = useMemo( () => ({ fetchError, + introspect, isFetching, schema, setFetchError, setSchema, validationErrors, }), - [fetchError, isFetching, schema, validationErrors], + [fetchError, introspect, isFetching, schema, validationErrors], ); return ( diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index a18d945241f..21d1f23e872 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -672,6 +672,11 @@ class GraphiQLWithContext extends React.Component< } label="History" /> + this.props.schemaContext.introspect()} + title="Fetch GraphQL schema using introspection (Shift-Ctrl-R)" + label="Introspect" + /> {this.props.toolbar?.additionalContent ? this.props.toolbar.additionalContent : null} diff --git a/packages/graphiql/src/components/__tests__/DocExplorer.spec.tsx b/packages/graphiql/src/components/__tests__/DocExplorer.spec.tsx index 9ac04de5e1d..1c3ecc0f93b 100644 --- a/packages/graphiql/src/components/__tests__/DocExplorer.spec.tsx +++ b/packages/graphiql/src/components/__tests__/DocExplorer.spec.tsx @@ -17,6 +17,7 @@ import { ExampleSchema } from './ExampleSchema'; const defaultSchemaContext: SchemaContextType = { fetchError: null, + introspect() {}, isFetching: false, schema: ExampleSchema, setFetchError() {}, diff --git a/packages/vscode-graphql-syntax/CHANGELOG.md b/packages/vscode-graphql-syntax/CHANGELOG.md index 9fc6ac02d44..74ad01f05c8 100644 --- a/packages/vscode-graphql-syntax/CHANGELOG.md +++ b/packages/vscode-graphql-syntax/CHANGELOG.md @@ -1,20 +1,19 @@ # vscode-graphql-syntax ## 1.0.4 -### Patch Changes - +### Patch Changes - [#2573](https://github.com/graphql/graphiql/pull/2573) [`a358ac1d`](https://github.com/graphql/graphiql/commit/a358ac1d00082643e124085bca09992adeef212a) Thanks [@acao](https://github.com/acao)! - ## Enhancement - + Here we move vscode grammars and basic language support to a new [`GraphQL.vscode-graphql-syntax`](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql-syntax) extension. `GraphQL.vscode-graphql` now depends on this new syntax extension. This constitutes no breaking change for `vscode-graphql` users, as this extension will be installed automatically as an `extensionDependency` for `vscode-graphql`. Both extensions will now have independent release lifecycles, but vscode will keep them both up to date for you :) - + Firstly, this allows users to only install the syntax highlighting extension if they don't need LSP server features. - + Secondly, this subtle but important change allows alternative LSP servers and non-LSP graphql extensions to use (and contribute!) to our shared, graphql community syntax highlighting. In some ways, it acts as a shared tooling & annotation spec, though it is intended just for vscode, it perhaps can be used as a point of reference for others implementing (embedded) graphql syntax highlighting elsewhere! - + If your language and/or library and/or framework would like vscode highlighting, come [join the party](https://github.com/graphql/graphiql/tree/main/packages/vscode-graphql-syntax#contributing)! - + If you use relay, we would highly reccomend using the `relay-compiler lsp` extension for vscode [Relay Graphql](https://marketplace.visualstudio.com/items?itemName=meta.relay) (`meta.relay`). They will be [using the new standalone syntax extension](https://github.com/facebook/relay/pull/4032) very soon! - + Even non-relay users may want to try this extension as an alternative to our reference implementation, as relay's configuration has relative similarity with `graphql-config`'s format, and doesn't necessitate the use of relay client afaik. We are working hard to optimize and improve `graphql-language-service-server` as a typescript reference implementation, and have some exciting features coming soon, however it's hard to offer more than a brand new & highly performant graphql LSP server written in Rust based on the latest graphql spec with a (mostly) paid team and dedicated open source ecosystem community of co-maintainers! And their implementation appears to allow you to opt out of any relay-specific conventions if you need more flexibility.