Repository: mercurius-js/mercurius (js)

Issue:
Allow internal graphiql to add custom plugin
The internal version of `graphiql` provided with `mercurius` loads the code from unpkg.com.

Allow to add `graphiql` plugin through the configuration of mercurius.

Eg.

```
{
      graphiql: {
        enabled: true,
        plugins: [
          {
            name: 'yourPlugin',
            props: {foo: 'bar'},
            umdUrl: 'https://unpkg.com/your-plugin@1.1.1/umd/yourplugin.production.min.js'
            fetcherWrapper: 'interceptResultFromFetch'
          }
        ]
      },
      schema,
      resolvers
    }
```

Pull request:
Feat/graphiql plugins #892
Allow injection of `graphiql` plugins in the configuration of a `mercurius` server:

```
app.register(mercurius, {
  schema,
  resolvers,
  graphiql: {
    enabled: true,
    plugins: [
      {
        name: 'samplePlugin',
        props: {},
        umdUrl: 'http://localhost:3000/graphiql/sample.js',
        fetcherWrapper: 'parseFetchResponse'
      }
    ]
  }
})
```

Closes #892 
 

Patch:
diff --git a/docs/api/options.md b/docs/api/options.md
index 0e73f19..3f89d56 100644
--- a/docs/api/options.md
+++ b/docs/api/options.md
@@ -34,9 +34,19 @@
 - `loaders`: Object. See [defineLoaders](#appgraphqlextendschemaschema-appgraphqldefineresolversresolvers-and-appgraphqldefineloadersloaders) for more
   details.
 - `schemaTransforms`: Array of schema-transformation functions. Accept a schema as an argument and return a schema.
-- `graphiql`: boolean | string. Serve
+- `graphiql`: boolean | string | Object. Serve
   [GraphiQL](https://www.npmjs.com/package/graphiql) on `/graphiql` if `true` or `'graphiql'`. Leave empty or `false` to disable.
   _only applies if `onlyPersisted` option is not `true`_
+  
+  An object can be passed in the config to allow the injection of external graphiql plugins exported in `umd` format.
+  - enabled: boolean, default `true`. Enable disable the graphiql extension
+  - plugins: Array
+    - `name`: string. The name of the plugin, it should be the same exported in the `umd`
+    - `props`: Object | undefined. The props to be passed to the plugin
+    - `umdUrl`: string. The urls of the plugin, it's downloaded at runtime. (eg. https://unpkg.com/myplugin/....)
+    - `fetcherWrapper`: string. A function name exported by the plugin to read/enrich the fetch response
+
+  Check the [`example folder`](https://github.com/mercurius-js/mercurius/tree/master/examples) for detailed usage or this [document](/examples/graphiql-plugin/README.md) for a detailed explanation on how to build a plugin.
 
   **Note**: If `routes` is false, this option does not have effects.
 
diff --git a/examples/graphiql-plugin/README.md b/examples/graphiql-plugin/README.md
new file mode 100644
index 0000000..b207a0e
--- /dev/null
+++ b/examples/graphiql-plugin/README.md
@@ -0,0 +1,172 @@
+# GraphiQL custom plugin
+
+## Quick start
+
+Execute the local `index.js` app to try the GraphiQL plugin integration.
+
+```javascript
+// examples/graphiql-plugin
+node ./index.js
+```
+
+## Create the plugin
+
+You can easily create a GraphiQL plugin and integrate it in Mercurius GraphiQL instance.
+
+* [GraphiQL.](https://github.com/graphql/graphiql)
+* [GraphiQL Explorer Plugin example.](https://github.com/graphql/graphiql/tree/main/packages/graphiql-plugin-explorer)
+
+### Plugin component
+
+A GraphiQL plugin is an object that contains these properties:
+
+* `title`: string. The title of the plugin
+* `icon`: React component. The icon shown in the toolbar
+* `content`: React component with the plugin implementation
+ 
+It can be created using the sample:
+
+```javascript
+import React from 'react'
+
+function Content() {
+  return (
+    <div style={{ maxWidth: '300px' }}>
+      <div style={{ height: '100%' }}>This is a sample plugin</div>
+    </div>
+  )
+}
+
+function Icon() {
+  return <p>P</p>
+}
+
+/* 
+ * Enrich, extract or modify the data returned by the GraphiQL fetcher.
+ * 
+ * Q: Why do I need to intercept the data?
+ * A: GraphiQL do not provide a direct access to the fetched data. 
+ * The data are fetched and injected directly in the viewer in a stringified format.
+ * 
+ * To provide a way to access the fetched data a similar function can implemented and passed 
+ * to the plugin in the attribute `fetcherWrapper` of the configuration.
+ * 
+ * {
+ *   name: '...',
+ *   props: ...,
+ *   umdUrl: '...',
+ *   fetcherWrapper: 'parseFetchResponse'
+ * }
+ */   
+export function parseFetchResponse(data) {
+  if (data) {
+    // Eg. storeDataSomewhere(data)
+    // Eg. addInfoToData(data)
+    // Eg. removeAttributeFromData(data)
+  }
+  return data
+}
+
+export function graphiqlSamplePlugin(props) {
+  return {
+    title: props.title || 'GraphiQL Sample',
+    icon: () => <Icon />,
+    content: () => {
+      return <Content {...props}/>
+    }
+  }
+}
+
+// This is required for the Mercurius integration
+export function umdPlugin(props) {
+    return graphiqlSamplePlugin(props)
+}
+```
+
+### Export as Umd
+
+```javascript
+import resolve from '@rollup/plugin-node-resolve'
+import commonjs from '@rollup/plugin-commonjs'
+import jsx from 'rollup-plugin-jsx'
+
+const packageJson = require('./package.json')
+
+const rollup = [
+  {
+    input: 'src/export.js', // path to the plugin entry point
+    output: [
+      {
+        file: packageJson.main,
+        format: 'cjs',
+        sourcemap: true
+      },
+      {
+        file: packageJson.module,
+        format: 'esm',
+        sourcemap: true
+      },
+      {
+        file: packageJson.umd,
+        format: 'umd',
+        sourcemap: true,
+        name: 'mercuriusPluginSample'
+      }
+    ],
+    external: ['react', '@graphiql/toolkit'],
+    plugins: [
+      resolve({
+        extensions: ['.js', '.jsx']
+      }),
+      svgr(),
+      commonjs(),
+      jsx({ factory: 'React.createElement' })
+    ]
+  }
+]
+
+export default rollup
+```
+
+Check the [plugin-sources](./plugin-sources) folder for a complete example.  
+
+### Serve the plugin
+
+To work with `Mercurius` the plugin should be available using a GET request.
+
+The preferred approach is to deploy the package on a public CDN like [unpkg.com](https://unpkg.com/).
+
+Alternatively it can be served by the local fastify.
+
+```javascript
+app.get('/graphiql/samplePlugin.js', (req, reply) => {
+    reply.sendFile('samplePlugin.js')
+})
+```
+
+## Add the plugin to Mercurius
+
+In the configuration file add the plugin in the `graphiql` parameter
+
+```javascript
+app.register(mercurius, {
+  schema,
+  resolvers,
+  graphiql: {
+    enabled: true,
+    plugins: [
+      {
+        name: 'mercuriusPluginSample',
+        props: {title: 'Sample plugin'},
+        umdUrl: 'http://localhost:3000/graphiql/samplePlugin.js',
+        fetcherWrapper: 'parseFetchResponse'
+      }
+    ]
+  }
+})
+```
+
+* `name`, string. The same used in the `rollup` export `output(format === umd).name`
+* `props`, object. The props to be passed to the plugin 
+* `umdUrl`, string. The url of the static `umd` file 
+* `fetcherWrapper`, function. The name of an exported function that intercept the result from the fetch. 
diff --git a/examples/graphiql-plugin/index.js b/examples/graphiql-plugin/index.js
new file mode 100644
index 0000000..b9a04a1
--- /dev/null
+++ b/examples/graphiql-plugin/index.js
@@ -0,0 +1,55 @@
+'use strict'
+
+const Fastify = require('fastify')
+const mercurius = require('../..')
+const Static = require('@fastify/static')
+const { join } = require('path')
+
+const app = Fastify()
+
+const schema = `
+  type Query {
+    add(x: Int, y: Int): Int
+  }
+`
+
+const resolvers = {
+  Query: {
+    add: async (_, obj) => {
+      const { x, y } = obj
+      return x + y
+    }
+  }
+}
+
+app.register(mercurius, {
+  schema,
+  resolvers,
+  graphiql: {
+    plugins: [
+      {
+        name: 'samplePlugin',
+        props: {},
+        umdUrl: 'http://localhost:3000/graphiql/samplePlugin.js',
+        fetcherWrapper: 'parseFetchResponse'
+      }
+    ]
+  }
+})
+
+app.register(Static, {
+  root: join(__dirname, './plugin'),
+  wildcard: false,
+  serve: false
+})
+
+app.get('/', async function (req, reply) {
+  const query = '{ add(x: 2, y: 2) }'
+  return reply.graphql(query)
+})
+
+app.get('/graphiql/samplePlugin.js', (req, reply) => {
+  reply.sendFile('samplePlugin.js')
+})
+
+app.listen({ port: 3000 })
diff --git a/examples/graphiql-plugin/plugin-sources/.gitignore b/examples/graphiql-plugin/plugin-sources/.gitignore
new file mode 100644
index 0000000..b732e99
--- /dev/null
+++ b/examples/graphiql-plugin/plugin-sources/.gitignore
@@ -0,0 +1,5 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/dist
diff --git a/examples/graphiql-plugin/plugin-sources/package.json b/examples/graphiql-plugin/plugin-sources/package.json
new file mode 100644
index 0000000..540535f
--- /dev/null
+++ b/examples/graphiql-plugin/plugin-sources/package.json
@@ -0,0 +1,21 @@
+{
+  "name": "graphiql-sample-plugin",
+  "version": "1.0.0",
+  "description": "This is a sample plugin for graphiql.",
+  "dependencies": {},
+  "scripts": {
+    "build": "rollup -c"
+  },
+  "devDependencies": {
+    "@rollup/plugin-commonjs": "^23.0.0",
+    "@rollup/plugin-node-resolve": "^15.0.0",
+    "rollup": "^2.79.1",
+    "rollup-plugin-jsx": "^1.0.3"
+  },
+  "main": "dist/cjs/index.js",
+  "module": "dist/esm/index.js",
+  "umd": "dist/umd/index.js",
+  "files": [
+    "dist"
+  ]
+}
diff --git a/examples/graphiql-plugin/plugin-sources/rollup.config.js b/examples/graphiql-plugin/plugin-sources/rollup.config.js
new file mode 100644
index 0000000..b37dca1
--- /dev/null
+++ b/examples/graphiql-plugin/plugin-sources/rollup.config.js
@@ -0,0 +1,45 @@
+import resolve from '@rollup/plugin-node-resolve'
+import commonjs from '@rollup/plugin-commonjs'
+import jsx from 'rollup-plugin-jsx'
+
+const packageJson = require('./package.json')
+
+const rollup = [
+  {
+    input: 'src/index.js',
+    output: [
+      {
+        file: packageJson.main,
+        format: 'cjs',
+        sourcemap: true
+      },
+      {
+        file: packageJson.module,
+        format: 'esm',
+        sourcemap: true
+      },
+      {
+        file: packageJson.umd,
+        format: 'umd',
+        sourcemap: true,
+        name: 'samplePlugin'
+      }
+    ],
+    external: ['react', '@graphiql/toolkit'],
+    plugins: [
+      resolve({
+        extensions: ['.js', '.jsx']
+      }),
+      commonjs(),
+      jsx({ factory: 'React.createElement' })
+    ]
+  }
+  // This is for typescript in the future
+  // {
+  //   input: 'dist/esm/types/index.d.ts',
+  //   output: [{ file: 'dist/index.d.ts', format: 'esm' }],
+  //   plugins: [dts()]
+  // }
+]
+
+export default rollup
diff --git a/examples/graphiql-plugin/plugin-sources/src/index.js b/examples/graphiql-plugin/plugin-sources/src/index.js
new file mode 100644
index 0000000..114be1b
--- /dev/null
+++ b/examples/graphiql-plugin/plugin-sources/src/index.js
@@ -0,0 +1,2 @@
+export { graphiqlSamplePlugin, umdPlugin } from './plugin'
+export { parseFetchResponse } from './utils'
diff --git a/examples/graphiql-plugin/plugin-sources/src/plugin.jsx b/examples/graphiql-plugin/plugin-sources/src/plugin.jsx
new file mode 100644
index 0000000..45c2f7f
--- /dev/null
+++ b/examples/graphiql-plugin/plugin-sources/src/plugin.jsx
@@ -0,0 +1,31 @@
+import React from 'react'
+import useSampleData from './useSampleData'
+
+export function Content () {
+  const { sampleData } = useSampleData()
+
+  return (
+    <div style={{ maxWidth: '300px' }}>
+      <div style={{ height: '100%' }}>This is a sample plugin</div>
+      {sampleData && <pre>{JSON.stringify(sampleData, null, 2)}</pre>}
+    </div>
+  )
+}
+
+export function Icon () {
+  return <p>GE</p>
+}
+
+export function graphiqlSamplePlugin (props) {
+  return {
+    title: props.title || 'GraphiQL Sample',
+    icon: () => <Icon />,
+    content: () => {
+      return <Content />
+    }
+  }
+}
+
+export function umdPlugin (props) {
+  return graphiqlSamplePlugin(props)
+}
diff --git a/examples/graphiql-plugin/plugin-sources/src/sampleDataManager.js b/examples/graphiql-plugin/plugin-sources/src/sampleDataManager.js
new file mode 100644
index 0000000..e9b5fcc
--- /dev/null
+++ b/examples/graphiql-plugin/plugin-sources/src/sampleDataManager.js
@@ -0,0 +1,22 @@
+/**
+ * A data manager to collect the data.
+ */
+class SampleDataManager extends EventTarget {
+  constructor () {
+    super()
+    this.sampleData = []
+  }
+
+  getSampleData () {
+    return this.sampleData
+  }
+
+  setSampleData (sampleData) {
+    this.sampleData = sampleData || []
+    this.dispatchEvent(new Event('updateSampleData'))
+  }
+}
+
+const sampleDataManager = new SampleDataManager()
+
+export default sampleDataManager
diff --git a/examples/graphiql-plugin/plugin-sources/src/useSampleData.js b/examples/graphiql-plugin/plugin-sources/src/useSampleData.js
new file mode 100644
index 0000000..d9b1fff
--- /dev/null
+++ b/examples/graphiql-plugin/plugin-sources/src/useSampleData.js
@@ -0,0 +1,26 @@
+import { useEffect, useState } from 'react'
+import sampleDataManager from './sampleDataManager'
+
+const useSampleData = () => {
+  const [sampleData, setSampleData] = useState(
+    sampleDataManager.getSampleData()
+  )
+
+  useEffect(() => {
+    const eventListener = sampleDataManager.addEventListener(
+      'updateSampleData',
+      (e, value) => {
+        setSampleData(_ => e.target.sampleData || [])
+      }
+    )
+    return () => {
+      sampleDataManager.removeEventListener('updateSampleData', eventListener)
+    }
+  }, [])
+
+  return {
+    sampleData
+  }
+}
+
+export default useSampleData
diff --git a/examples/graphiql-plugin/plugin-sources/src/utils.js b/examples/graphiql-plugin/plugin-sources/src/utils.js
new file mode 100644
index 0000000..c5540b5
--- /dev/null
+++ b/examples/graphiql-plugin/plugin-sources/src/utils.js
@@ -0,0 +1,11 @@
+import sampleDataManager from './sampleDataManager'
+
+/**
+ * Intercept and store the data fetched by GQL in the DataManager.
+ */
+export function parseFetchResponse (data) {
+  if (data.data) {
+    sampleDataManager.setSampleData(data.data)
+  }
+  return data
+}
diff --git a/examples/graphiql-plugin/plugin/samplePlugin.js b/examples/graphiql-plugin/plugin/samplePlugin.js
new file mode 100644
index 0000000..b312b00
--- /dev/null
+++ b/examples/graphiql-plugin/plugin/samplePlugin.js
@@ -0,0 +1,102 @@
+/* eslint-disable */
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined'
+    ? factory(exports, require('react'))
+    : typeof define === 'function' && define.amd
+      ? define(['exports', 'react'], factory)
+      : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.samplePlugin = {}, global.React))
+})(this, function (exports, React) {
+  'use strict'
+
+  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e } }
+
+  const React__default = /* #__PURE__ */_interopDefaultLegacy(React)
+
+  /**
+   * A data manager to collect the data.
+   */
+  class SampleDataManager extends EventTarget {
+    constructor () {
+      super()
+      this.sampleData = []
+    }
+
+    getSampleData () {
+      return this.sampleData
+    }
+
+    setSampleData (sampleData) {
+      this.sampleData = sampleData || []
+      this.dispatchEvent(new Event('updateSampleData'))
+    }
+  }
+
+  const sampleDataManager = new SampleDataManager()
+
+  const useSampleData = () => {
+    const [sampleData, setSampleData] = React.useState(
+      sampleDataManager.getSampleData()
+    )
+
+    React.useEffect(() => {
+      const eventListener = sampleDataManager.addEventListener(
+        'updateSampleData',
+        (e, value) => {
+          setSampleData(_ => e.target.sampleData || [])
+        }
+      )
+      return () => {
+        sampleDataManager.removeEventListener('updateSampleData', eventListener)
+      }
+    }, [])
+
+    return {
+      sampleData
+    }
+  }
+
+  function Content () {
+    const { sampleData } = useSampleData()
+
+    return (
+      React__default.default.createElement('div', { style: { maxWidth: '300px' } }, [
+        React__default.default.createElement('div', { style: { height: '100%' } }, ['This is a sample plugin']),
+        sampleData && React__default.default.createElement('pre', null, [JSON.stringify(sampleData, null, 2)])
+      ])
+    )
+  }
+
+  function Icon () {
+    return React__default.default.createElement('p', null, ['GE'])
+  }
+
+  function graphiqlSamplePlugin (props) {
+    return {
+      title: props.title || 'GraphiQL Sample',
+      icon: () => Icon(),
+      content: () => {
+        return Content()
+      }
+    }
+  }
+
+  function umdPlugin (props) {
+    return graphiqlSamplePlugin(props)
+  }
+
+  /**
+   * Intercept and store the data fetched by GQL in the DataManager.
+   */
+  function parseFetchResponse (data) {
+    if (data.data) {
+      sampleDataManager.setSampleData(data.data)
+    }
+    return data
+  }
+
+  exports.graphiqlSamplePlugin = graphiqlSamplePlugin
+  exports.parseFetchResponse = parseFetchResponse
+  exports.umdPlugin = umdPlugin
+
+  Object.defineProperty(exports, '__esModule', { value: true })
+})
diff --git a/lib/routes.js b/lib/routes.js
index b8afdd9..4ad4722 100644
--- a/lib/routes.js
+++ b/lib/routes.js
@@ -381,14 +381,14 @@ module.exports = async function (app, opts) {
     }
   })
 
-  if (opts.ide || opts.graphiql) {
+  if (opts.ide) {
     app.register(Static, {
       root: join(__dirname, '../static'),
       wildcard: false,
       serve: false
     })
 
-    if (opts.ide === true || opts.ide === 'graphiql' || opts.graphiql === true) {
+    if (opts.ide === true || opts.ide === 'graphiql' || (typeof opts.ide === 'object' && opts.ide !== null && opts.ide.enabled !== false)) {
       app.get('/graphiql', (req, reply) => {
         reply.sendFile('graphiql.html')
       })
@@ -402,9 +402,26 @@ module.exports = async function (app, opts) {
       })
 
       app.get('/graphiql/config.js', (req, reply) => {
+        const configRows = [
+          `window.GRAPHQL_ENDPOINT = '${app.prefix}${graphqlPath}'`
+        ]
+
+        const plugins = []
+        const idePlugins = opts.ide.plugins || []
+        idePlugins.forEach(plugin => {
+          if (plugin.name) {
+            configRows.push(`window.GRAPIHQL_PLUGIN_${plugin.name.toUpperCase()} = ${JSON.stringify(plugin)}`)
+            plugins.push(plugin.name)
+          } else {
+            app.log.warn('Graphiql plugin without a name defined')
+          }
+        })
+
+        configRows.push(`window.GRAPHIQL_PLUGIN_LIST = ${JSON.stringify(plugins)}`)
+
         reply
           .header('Content-Type', 'application/javascript')
-          .send(`window.GRAPHQL_ENDPOINT = '${app.prefix}${graphqlPath}'`)
+          .send(configRows.join(';\n'))
       })
     }
   }
diff --git a/static/main.js b/static/main.js
index 080ee29..4d257f1 100644
--- a/static/main.js
+++ b/static/main.js
@@ -17,6 +17,84 @@ const importer = {
   }
 }
 
+// The functions above are required to wrap the fetcher and access/enrich the data returned by the GQL query
+// Except `fetcherWrapper`, they are copy/pasted directly from the `graphiql` codebase.
+
+function observableToPromise (observable) {
+  return new Promise((resolve, reject) => {
+    const subscription = observable.subscribe({
+      next: v => {
+        resolve(v)
+        subscription.unsubscribe()
+      },
+      error: reject,
+      complete: () => {
+        reject(new Error('no value resolved'))
+      }
+    })
+  })
+}
+
+function isObservable (value) {
+  return (
+    typeof value === 'object' &&
+    value !== null &&
+    'subscribe' in value &&
+    typeof value.subscribe === 'function'
+  )
+}
+
+function isAsyncIterable (input) {
+  return (
+    typeof input === 'object' &&
+    input !== null &&
+    ((input)[Symbol.toStringTag] === 'AsyncGenerator' ||
+      Symbol.asyncIterator in input)
+  )
+}
+
+function asyncIterableToPromise (
+  input
+) {
+  return new Promise((resolve, reject) => {
+    const iteratorReturn = (
+      'return' in input ? input : input[Symbol.asyncIterator]()
+    ).return?.bind(input)
+    const iteratorNext = (
+      'next' in input ? input : input[Symbol.asyncIterator]()
+    ).next.bind(input)
+
+    iteratorNext()
+      .then(result => {
+        resolve(result.value)
+        // ensure cleanup
+        iteratorReturn?.()
+      })
+      .catch(err => {
+        reject(err)
+      })
+  })
+}
+
+function fetcherReturnToPromise (fetcherResult) {
+  return Promise.resolve(fetcherResult).then(result => {
+    if (isAsyncIterable(result)) {
+      return asyncIterableToPromise(result)
+    } else if (isObservable(result)) {
+      return observableToPromise(result)
+    }
+    return result
+  })
+}
+
+function fetcherWrapper (fetcher, cbs = []) {
+  return async (gqlp, fetchOpt) => {
+    const fetchResponse = await fetcher(gqlp, fetchOpt)
+    const result = await fetcherReturnToPromise(fetchResponse)
+    return cbs.reduce((acc, cb) => cb(acc), result)
+  }
+}
+
 function render () {
   const host = window.location.host
 
@@ -25,16 +103,27 @@ function render () {
   const url = `${window.location.protocol}//${host}${window.GRAPHQL_ENDPOINT}`
   const subscriptionUrl = `${websocketProtocol}//${host}${window.GRAPHQL_ENDPOINT}`
 
-  const fetcher = GraphiQL.createFetcher({
+  const availablePlugins = window.GRAPHIQL_PLUGIN_LIST
+    .map(plugin => window[`GRAPIHQL_PLUGIN_${plugin.toUpperCase()}`])
+    .filter(pluginData => pluginData && pluginData.umdUrl)
+
+  const fetcherWrapperPlugins = availablePlugins
+    .filter(plugin => plugin.fetcherWrapper)
+    .map(pluginData => window[pluginData.name][window[`GRAPIHQL_PLUGIN_${pluginData.name.toUpperCase()}`].fetcherWrapper])
+
+  const fetcher = fetcherWrapper(GraphiQL.createFetcher({
     url,
     subscriptionUrl
-  })
+  }), fetcherWrapperPlugins)
+
+  const plugins = availablePlugins.map(pluginData => window[pluginData.name].umdPlugin(window[`GRAPIHQL_PLUGIN_${pluginData.name.toUpperCase()}`].props))
 
   ReactDOM.render(
     React.createElement(GraphiQL, {
       fetcher,
       headerEditorEnabled: true,
-      shouldPersistHeaders: true
+      shouldPersistHeaders: true,
+      plugins
     }),
     document.getElementById('main')
   )
@@ -53,7 +142,15 @@ function importDependencies () {
     'https://unpkg.com/react@18.2.0/umd/react.production.min.js',
     'https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js',
     'https://unpkg.com/graphiql@2.0.9/graphiql.min.js'
-  ])
+  ]).then(function () {
+    const pluginUrls = window.GRAPHIQL_PLUGIN_LIST
+      .map(plugin => window[`GRAPIHQL_PLUGIN_${plugin.toUpperCase()}`].umdUrl)
+      .filter(url => !!url)
+
+    if (pluginUrls.length) {
+      return importer.urls(pluginUrls)
+    }
+  })
 }
 
 if ('serviceWorker' in navigator) {


Test patch:
diff --git a/tap-snapshots/test/routes.js.test.cjs b/tap-snapshots/test/routes.js.test.cjs
index 0bc98fd..91a38f5 100644
--- a/tap-snapshots/test/routes.js.test.cjs
+++ b/tap-snapshots/test/routes.js.test.cjs
@@ -67,7 +67,8 @@ exports['test/routes.js TAP POST return 400 on error > must match snapshot 1'] =
 `
 
 exports['test/routes.js TAP if ide is graphiql, serve config.js with the correct endpoint > must match snapshot 1'] = `
-window.GRAPHQL_ENDPOINT = '/app/graphql'
+window.GRAPHQL_ENDPOINT = '/app/graphql';
+window.GRAPHIQL_PLUGIN_LIST = []
 `
 
 exports['test/routes.js TAP mutation with GET errors > must match snapshot 1'] = `
diff --git a/test/routes.js b/test/routes.js
index 693db57..1a0f7b1 100644
--- a/test/routes.js
+++ b/test/routes.js
@@ -339,7 +339,12 @@ test('GET route with extensions', async (t) => {
 
   const res = await app.inject({
     method: 'GET',
-    url: `/graphql?extensions=${JSON.stringify({ persistedQuery: { version: 1, sha256Hash } })}&variables=${JSON.stringify({ x: 2, y: 2 })}`
+    url: `/graphql?extensions=${JSON.stringify({
+      persistedQuery: {
+        version: 1,
+        sha256Hash
+      }
+    })}&variables=${JSON.stringify({ x: 2, y: 2 })}`
   })
 
   t.same(JSON.parse(res.body), {
@@ -1995,7 +2000,7 @@ test('if ide is graphiql with a prefix, serve config.js with the correct endpoin
   })
   t.equal(res.statusCode, 200)
   t.equal(res.headers['content-type'], 'application/javascript')
-  t.equal(res.body.toString(), 'window.GRAPHQL_ENDPOINT = \'/something/app/graphql\'')
+  t.equal(res.body.toString(), 'window.GRAPHQL_ENDPOINT = \'/something/app/graphql\';\nwindow.GRAPHIQL_PLUGIN_LIST = []')
 })
 
 test('if ide is graphiql with a prefix from a wrapping plugin, serve config.js with the correct endpoint', async (t) => {
@@ -2020,7 +2025,7 @@ test('if ide is graphiql with a prefix from a wrapping plugin, serve config.js w
   })
   t.equal(res.statusCode, 200)
   t.equal(res.headers['content-type'], 'application/javascript')
-  t.equal(res.body.toString(), 'window.GRAPHQL_ENDPOINT = \'/something/app/graphql\'')
+  t.equal(res.body.toString(), 'window.GRAPHQL_ENDPOINT = \'/something/app/graphql\';\nwindow.GRAPHIQL_PLUGIN_LIST = []')
 })
 
 test('if operationName is null, it should work fine', async (t) => {
@@ -2062,3 +2067,110 @@ test('if operationName is null, it should work fine', async (t) => {
     }
   })
 })
+
+test('GET graphiql endpoint with object and enabled', async (t) => {
+  const app = Fastify()
+  const schema = `
+    type Query {
+      add(x: Int, y: Int): Int
+    }
+  `
+  app.register(GQL, {
+    ide: {},
+    schema
+  })
+
+  const res = await app.inject({
+    method: 'GET',
+    url: '/graphiql'
+  })
+  t.equal(res.statusCode, 200)
+})
+
+test('disable GET graphiql endpoint with object if note enabled', async (t) => {
+  const app = Fastify()
+  const schema = `
+    type Query {
+      add(x: Int, y: Int): Int
+    }
+  `
+  app.register(GQL, {
+    ide: { enabled: false },
+    schema
+  })
+
+  const res = await app.inject({
+    method: 'GET',
+    url: '/graphiql'
+  })
+  t.equal(res.statusCode, 404)
+})
+
+test('if ide has plugin set, serve config.js with the correct endpoint', async (t) => {
+  const app = Fastify()
+  const schema = `
+    type Query {
+      add(x: Int, y: Int): Int
+    }
+  `
+  app.register(GQL, {
+    ide: {
+      enabled: true,
+      plugins: [
+        {
+          name: 'samplePlugin',
+          props: { foo: 'bar' },
+          umdUrl: '/graphiql/explain.js',
+          fetcherWrapper: 'parsePlugin'
+        }
+      ]
+    },
+    path: '/app/graphql',
+    prefix: '/something',
+    schema
+  })
+
+  const res = await app.inject({
+    method: 'GET',
+    url: '/something/graphiql/config.js'
+  })
+  t.equal(res.statusCode, 200)
+  t.equal(res.headers['content-type'], 'application/javascript')
+  t.equal(res.body.toString(), 'window.GRAPHQL_ENDPOINT = \'/something/app/graphql\';\n' +
+    'window.GRAPIHQL_PLUGIN_SAMPLEPLUGIN = ' +
+    '{"name":"samplePlugin","props":{"foo":"bar"},"umdUrl":"/graphiql/explain.js","fetcherWrapper":"parsePlugin"};\n' +
+    'window.GRAPHIQL_PLUGIN_LIST = ["samplePlugin"]')
+})
+
+test('if ide has plugin set, serve config.js with the correct endpoint', async (t) => {
+  const app = Fastify()
+  const schema = `
+    type Query {
+      add(x: Int, y: Int): Int
+    }
+  `
+  app.register(GQL, {
+    ide: {
+      enabled: true,
+      plugins: [
+        {
+          props: { foo: 'bar' },
+          umdUrl: '/graphiql/explain.js',
+          fetcherWrapper: 'parsePlugin'
+        }
+      ]
+    },
+    path: '/app/graphql',
+    prefix: '/something',
+    schema
+  })
+
+  const res = await app.inject({
+    method: 'GET',
+    url: '/something/graphiql/config.js'
+  })
+  t.equal(res.statusCode, 200)
+  t.equal(res.headers['content-type'], 'application/javascript')
+  t.equal(res.body.toString(), 'window.GRAPHQL_ENDPOINT = \'/something/app/graphql\';\n' +
+    'window.GRAPHIQL_PLUGIN_LIST = []')
+})