Repository: graphql-rust/juniper (language: rust)

Issue:
A better way to keep graphiql updated
Current we hardcode it, which means it gets out of date. See https://github.com/graphql-rust/juniper/pull/1069 for discussion.



Discussion / hints:

Fix patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 1cf1a19bb..b90aa62a6 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -9,3 +9,8 @@ updates:
     directory: /
     schedule:
       interval: daily
+
+  - package-ecosystem: npm
+    directory: /juniper/
+    schedule:
+      interval: daily
diff --git a/Makefile b/Makefile
index 8c1db098a..69d57839c 100644
--- a/Makefile
+++ b/Makefile
@@ -148,6 +148,23 @@ book.serve:
 
 
 
+######################
+# Forwarded commands #
+######################
+
+# Download and prepare actual version of GraphiQL static files, used for
+# integrating it.
+#
+# Usage:
+#	make graphiql
+
+graphiql:
+	@cd juniper/ && \
+	make graphiql
+
+
+
+
 ##################
 # .PHONY section #
 ##################
@@ -155,4 +172,5 @@ book.serve:
 .PHONY: book fmt lint release test \
         book.build book.serve \
         cargo.fmt cargo.lint cargo.release cargo.test \
+        graphiql \
         test.book test.cargo
diff --git a/examples/warp_subscriptions/Cargo.toml b/examples/warp_subscriptions/Cargo.toml
index b03381233..f708c99e3 100644
--- a/examples/warp_subscriptions/Cargo.toml
+++ b/examples/warp_subscriptions/Cargo.toml
@@ -11,6 +11,7 @@ env_logger = "0.10"
 futures = "0.3"
 juniper = { path = "../../juniper" }
 juniper_graphql_ws = { path = "../../juniper_graphql_ws" }
+juniper_graphql_transport_ws = { path = "../../juniper_graphql_transport_ws" }
 juniper_warp = { path = "../../juniper_warp", features = ["subscriptions"] }
 log = "0.4.8"
 serde = { version = "1.0", features = ["derive"] }
diff --git a/examples/warp_subscriptions/src/main.rs b/examples/warp_subscriptions/src/main.rs
index ded890623..1f6800a6d 100644
--- a/examples/warp_subscriptions/src/main.rs
+++ b/examples/warp_subscriptions/src/main.rs
@@ -7,8 +7,12 @@ use juniper::{
     graphql_object, graphql_subscription, graphql_value, EmptyMutation, FieldError, GraphQLEnum,
     RootNode,
 };
-use juniper_graphql_ws::ConnectionConfig;
-use juniper_warp::{playground_filter, subscriptions::serve_graphql_ws};
+use juniper_graphql_transport_ws::ConnectionConfig;
+use juniper_graphql_ws::ConnectionConfig as LegacyConnectionConfig;
+use juniper_warp::{
+    graphiql_filter, playground_filter,
+    subscriptions::{serve_graphql_transport_ws, serve_graphql_ws},
+};
 use warp::{http::Response, Filter};
 
 #[derive(Clone)]
@@ -108,13 +112,13 @@ struct Subscription;
 #[graphql_subscription(context = Context)]
 impl Subscription {
     async fn users() -> UsersStream {
-        let mut counter = 0;
         let mut interval = tokio::time::interval(Duration::from_secs(5));
         let stream = async_stream::stream! {
-            counter += 1;
+            let mut counter = 0;
             loop {
+                counter += 1;
                 interval.tick().await;
-                if counter == 2 {
+                if counter == 5 {
                     yield Err(FieldError::new(
                         "some field error from handler",
                         graphql_value!("some additional string"),
@@ -156,36 +160,58 @@ async fn main() {
     let qm_state = warp::any().map(|| Context);
     let qm_graphql_filter = juniper_warp::make_graphql_filter(qm_schema, qm_state.boxed());
 
-    let root_node = Arc::new(schema());
+    let ws_schema = Arc::new(schema());
+    let transport_ws_schema = ws_schema.clone();
 
     log::info!("Listening on 127.0.0.1:8080");
 
-    let routes = (warp::path("subscriptions")
+    let routes = warp::path("subscriptions")
         .and(warp::ws())
         .map(move |ws: warp::ws::Ws| {
-            let root_node = root_node.clone();
+            let transport_ws_schema = transport_ws_schema.clone();
             ws.on_upgrade(move |websocket| async move {
-                serve_graphql_ws(websocket, root_node, ConnectionConfig::new(Context))
-                    .map(|r| {
-                        if let Err(e) = r {
-                            println!("Websocket error: {e}");
-                        }
-                    })
-                    .await
+                serve_graphql_transport_ws(
+                    websocket,
+                    transport_ws_schema,
+                    ConnectionConfig::new(Context),
+                )
+                .map(|r| {
+                    if let Err(e) = r {
+                        println!("Websocket error: {e}");
+                    }
+                })
+                .await
+            })
+        })
+        .or(warp::path("legacy-subscriptions")
+            .and(warp::ws())
+            .map(move |ws: warp::ws::Ws| {
+                let ws_schema = ws_schema.clone();
+                ws.on_upgrade(move |websocket| async move {
+                    serve_graphql_ws(websocket, ws_schema, LegacyConnectionConfig::new(Context))
+                        .map(|r| {
+                            if let Err(e) = r {
+                                println!("Websocket error: {e}");
+                            }
+                        })
+                        .await
+                })
             })
-        }))
-    .map(|reply| {
-        // TODO#584: remove this workaround
-        warp::reply::with_header(reply, "Sec-WebSocket-Protocol", "graphql-ws")
-    })
-    .or(warp::post()
-        .and(warp::path("graphql"))
-        .and(qm_graphql_filter))
-    .or(warp::get()
-        .and(warp::path("playground"))
-        .and(playground_filter("/graphql", Some("/subscriptions"))))
-    .or(homepage)
-    .with(log);
+            .map(|reply| {
+                // TODO#584: remove this workaround
+                warp::reply::with_header(reply, "Sec-WebSocket-Protocol", "graphql-ws")
+            }))
+        .or(warp::post()
+            .and(warp::path("graphql"))
+            .and(qm_graphql_filter))
+        .or(warp::get()
+            .and(warp::path("playground"))
+            .and(playground_filter("/graphql", Some("/legacy-subscriptions"))))
+        .or(warp::get()
+            .and(warp::path("graphiql"))
+            .and(graphiql_filter("/graphql", Some("/subscriptions"))))
+        .or(homepage)
+        .with(log);
 
     warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
 }
diff --git a/juniper/.gitignore b/juniper/.gitignore
new file mode 100644
index 000000000..b16fdf455
--- /dev/null
+++ b/juniper/.gitignore
@@ -0,0 +1,3 @@
+/node_modules/
+/package-lock.json
+/yarn.lock
diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md
index 1d2228db4..e0e6975c0 100644
--- a/juniper/CHANGELOG.md
+++ b/juniper/CHANGELOG.md
@@ -51,6 +51,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
     - Disabled `chrono` [Cargo feature] by default.
     - Removed `scalar-naivetime` [Cargo feature].
 - Removed lifetime parameter from `ParseError`, `GraphlQLError`, `GraphQLBatchRequest` and `GraphQLRequest`. ([#1081], [#528])
+- Upgraded [GraphiQL] to 3.0.5 version (requires new [`graphql-ws` GraphQL over WebSocket Protocol] integration on server, see `examples/warp_subscriptions`). ([#1188])
 
 ### Added
 
@@ -121,6 +122,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
 [#1145]: /../../pull/1145
 [#1147]: /../../pull/1147
 [#1176]: /../../pull/1176
+[#1188]: /../../pull/1188
 [ba1ed85b]: /../../commit/ba1ed85b3c3dd77fbae7baf6bc4e693321a94083
 [CVE-2022-31173]: /../../security/advisories/GHSA-4rx6-g5vg-5f3j
 
@@ -140,6 +142,8 @@ See [old CHANGELOG](/../../blob/juniper-v0.15.9/juniper/CHANGELOG.md).
 [`chrono-tz` crate]: https://docs.rs/chrono-tz
 [`time` crate]: https://docs.rs/time
 [Cargo feature]: https://doc.rust-lang.org/cargo/reference/features.html
+[`graphql-ws` GraphQL over WebSocket Protocol]: https://github.com/graphql/graphiql
+[GraphiQL]: https://github.com/enisdenjo/graphql-ws/master/PROTOCOL.md
 [graphql-scalars.dev]: https://graphql-scalars.dev
 [October 2021]: https://spec.graphql.org/October2021
 [object safety]: https://doc.rust-lang.org/reference/items/traits.html#object-safety
diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml
index 4c1a403c2..bc9b3d8bd 100644
--- a/juniper/Cargo.toml
+++ b/juniper/Cargo.toml
@@ -18,7 +18,7 @@ repository = "https://github.com/graphql-rust/juniper"
 readme = "README.md"
 categories = ["asynchronous", "web-programming", "web-programming::http-server"]
 keywords = ["apollo", "graphql", "server", "web"]
-exclude = ["/release.toml"]
+include = ["/src/", "/CHANGELOG.md", "/LICENSE", "/README.md"]
 
 [package.metadata.docs.rs]
 all-features = true
diff --git a/juniper/Makefile b/juniper/Makefile
new file mode 100644
index 000000000..fb43bc27d
--- /dev/null
+++ b/juniper/Makefile
@@ -0,0 +1,56 @@
+###############################
+# Common defaults/definitions #
+###############################
+
+# Checks two given strings for equality.
+eq = $(if $(or $(1),$(2)),$(and $(findstring $(1),$(2)),\
+                                $(findstring $(2),$(1))),1)
+
+# Multiplatform prefix of `sed -i` commands.
+sed-i = sed -i$(if $(call eq,$(shell uname -s),Darwin), '',)
+
+
+
+
+######################
+# Project parameters #
+######################
+
+GRAPHIQL_VER ?= $(strip \
+	$(shell grep -m1 '"graphiql": "' package.json | cut -d '"' -f4))
+
+
+
+
+############
+# Commands #
+############
+
+# Download and prepare actual version of GraphiQL static files, used for
+# integrating it.
+#
+# Usage:
+#	make graphiql
+
+graphiql:
+	curl -fL -o src/http/graphiql.html \
+		https://raw.githubusercontent.com/graphql/graphiql/graphiql%40$(GRAPHIQL_VER)/examples/graphiql-cdn/index.html
+	$(sed-i) 's|https://unpkg.com/graphiql/|https://unpkg.com/graphiql@$(GRAPHIQL_VER)/|g' \
+		src/http/graphiql.html
+	$(sed-i) "s|'https://swapi-graphql.netlify.app/.netlify/functions/index'|GRAPHQL_URL|g" \
+		src/http/graphiql.html
+	$(sed-i) "s|url: GRAPHQL_URL,|url: GRAPHQL_URL,\n        subscriptionUrl: normalizeSubscriptionEndpoint(GRAPHQL_URL, GRAPHQL_SUBSCRIPTIONS_URL)|" \
+		src/http/graphiql.html
+	$(sed-i) 's|<script>|<script>\n<!-- inject -->|' \
+		src/http/graphiql.html
+	$(sed-i) '/X-Example-Header/d' \
+		src/http/graphiql.html
+
+
+
+
+##################
+# .PHONY section #
+##################
+
+.PHONY: graphiql
diff --git a/juniper/package.json b/juniper/package.json
new file mode 100644
index 000000000..68a4cdb1a
--- /dev/null
+++ b/juniper/package.json
@@ -0,0 +1,9 @@
+{
+  "private": true,
+  "scripts": {
+    "postinstall": "make graphiql"
+  },
+  "dependencies": {
+    "graphiql": "3.0.5"
+  }
+}
diff --git a/juniper/src/http/graphiql.html b/juniper/src/http/graphiql.html
new file mode 100644
index 000000000..7b209dcb0
--- /dev/null
+++ b/juniper/src/http/graphiql.html
@@ -0,0 +1,75 @@
+<!--
+ *  Copyright (c) 2021 GraphQL Contributors
+ *  All rights reserved.
+ *
+ *  This source code is licensed under the license found in the
+ *  LICENSE file in the root directory of this source tree.
+-->
+<!doctype html>
+<html lang="en">
+  <head>
+    <title>GraphiQL</title>
+    <style>
+      body {
+        height: 100%;
+        margin: 0;
+        width: 100%;
+        overflow: hidden;
+      }
+
+      #graphiql {
+        height: 100vh;
+      }
+    </style>
+
+    <!--
+      This GraphiQL example depends on Promise and fetch, which are available in
+      modern browsers, but can be "polyfilled" for older browsers.
+      GraphiQL itself depends on React DOM.
+      If you do not want to rely on a CDN, you can host these files locally or
+      include them directly in your favored resource bundler.
+    -->
+    <script
+      crossorigin
+      src="https://unpkg.com/react@18/umd/react.development.js"
+    ></script>
+    <script
+      crossorigin
+      src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
+    ></script>
+    <script
+      src="https://unpkg.com/graphiql@3.0.5/graphiql.min.js"
+      type="application/javascript"
+    ></script>
+    <script
+      src="https://unpkg.com/@graphiql/plugin-explorer/dist/index.umd.js"
+      crossorigin
+    ></script>
+    <!--
+      These two files can be found in the npm module, however you may wish to
+      copy them directly into your environment, or perhaps include them in your
+      favored resource bundler.
+     -->
+    <link rel="stylesheet" href="https://unpkg.com/graphiql@3.0.5/graphiql.min.css" />
+  </head>
+
+  <body>
+    <div id="graphiql">Loading...</div>
+    <script>
+<!-- inject -->
+      const root = ReactDOM.createRoot(document.getElementById('graphiql'));
+      const fetcher = GraphiQL.createFetcher({
+        url: GRAPHQL_URL,
+        subscriptionUrl: normalizeSubscriptionEndpoint(GRAPHQL_URL, GRAPHQL_SUBSCRIPTIONS_URL)
+      });
+      const explorerPlugin = GraphiQLPluginExplorer.explorerPlugin();
+      root.render(
+        React.createElement(GraphiQL, {
+          fetcher,
+          defaultEditorToolsVisibility: true,
+          plugins: [explorerPlugin],
+        }),
+      );
+    </script>
+  </body>
+</html>
diff --git a/juniper/src/http/graphiql.js b/juniper/src/http/graphiql.js
new file mode 100644
index 000000000..b914cc097
--- /dev/null
+++ b/juniper/src/http/graphiql.js
@@ -0,0 +1,14 @@
+function normalizeSubscriptionEndpoint(endpoint, subscriptionEndpoint) {
+    if (subscriptionEndpoint) {
+        if (subscriptionEndpoint.startsWith('/')) {
+            const secure =
+                endpoint.includes('https') || location.href.includes('https')
+                    ? 's'
+                    : ''
+            return `ws${secure}://${location.host}${subscriptionEndpoint}`
+        } else {
+            return subscriptionEndpoint.replace(/^http/, 'ws')
+        }
+    }
+    return null
+}
diff --git a/juniper/src/http/graphiql.rs b/juniper/src/http/graphiql.rs
index a3ac57218..588bc90e3 100644
--- a/juniper/src/http/graphiql.rs
+++ b/juniper/src/http/graphiql.rs
@@ -6,7 +6,7 @@
 ///
 /// ```
 /// # use juniper::http::graphiql::graphiql_source;
-/// let graphiql = graphiql_source("/graphql", Some("ws://localhost:8080/subscriptions"));
+/// let graphiql = graphiql_source("/graphql", Some("/subscriptions"));
 /// ```
 pub fn graphiql_source(
     graphql_endpoint_url: &str,
@@ -18,110 +18,20 @@ pub fn graphiql_source(
         ""
     };
 
-    let stylesheet_source = r#"
-    <style>
-      body {
-        height: 100%;
-        margin: 0;
-        width: 100%;
-        overflow: hidden;
-      }
-
-      #graphiql {
-        height: 100vh;
-      }
-    </style>
-    "#;
-    let fetcher_source = r#"
-    <script>
-        if (usingSubscriptions) {
-            var subscriptionEndpoint = normalizeSubscriptionEndpoint(GRAPHQL_URL, GRAPHQL_SUBSCRIPTIONS_URL);
-            var subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(subscriptionEndpoint, { reconnect: true });
-        }
-
-        function normalizeSubscriptionEndpoint(endpoint, subscriptionEndpoint) {
-            if (subscriptionEndpoint) {
-                if (subscriptionEndpoint.startsWith('/')) {
-                    const secure =
-                        endpoint.includes('https') || location.href.includes('https')
-                        ? 's'
-                        : ''
-                    return `ws${secure}://${location.host}${subscriptionEndpoint}`
-                } else {
-                    return subscriptionEndpoint.replace(/^http/, 'ws')
-                }
-            }
-            return null
-        }
-
-        function graphQLFetcher(graphQLParams, opts) {
-            const { headers = {} } = opts;
-
-            return fetch(
-                GRAPHQL_URL,
-                {
-                    method: 'post',
-                    headers: {
-                        Accept: 'application/json',
-                        'Content-Type': 'application/json',
-                        ...headers,
-                    },
-                    body: JSON.stringify(graphQLParams),
-                    credentials: 'omit',
-                },
-            ).then(function (response) {
-                return response.json().catch(function () {
-                    return response.text();
-                });
-            });
-        }
-
-        var fetcher = usingSubscriptions ? window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher) : graphQLFetcher;
-
-        ReactDOM.render(
-            React.createElement(GraphiQL, {
-              fetcher,
-              defaultVariableEditorOpen: true,
-            }),
-            document.getElementById('graphiql'),
-        );
-    </script>
-    "#;
-
-    format!(
-        r#"
-<!DOCTYPE html>
-<html>
-<head>
-    <title>GraphQL</title>
-    {stylesheet_source}
-    <script
-      crossorigin
-      src="https://unpkg.com/react@17/umd/react.development.js"
-    ></script>
-    <script
-      crossorigin
-      src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
-    ></script>
-    <link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
-</head>
-<body>
-    <div id="graphiql">Loading...</div>
-    <script
-        src="https://unpkg.com/graphiql/graphiql.min.js"
-        type="application/javascript"
-    ></script>
-    <script>var GRAPHQL_URL = '{graphql_url}';</script>
-    <script>var usingSubscriptions = {using_subscriptions};</script>
-    <script>var GRAPHQL_SUBSCRIPTIONS_URL = '{graphql_subscriptions_url}';</script>
-    {fetcher_source}
-</body>
-</html>
-"#,
-        graphql_url = graphql_endpoint_url,
-        stylesheet_source = stylesheet_source,
-        fetcher_source = fetcher_source,
-        graphql_subscriptions_url = subscriptions_endpoint,
-        using_subscriptions = subscriptions_endpoint_url.is_some(),
+    include_str!("graphiql.html").replace(
+        "<!-- inject -->",
+        // Language: JavaScript
+        &format!(
+            "
+      var GRAPHQL_URL = '{graphql_url}';
+      var GRAPHQL_SUBSCRIPTIONS_URL = '{graphql_subscriptions_url}';
+
+{grahiql_js}
+
+            ",
+            graphql_url = graphql_endpoint_url,
+            graphql_subscriptions_url = subscriptions_endpoint,
+            grahiql_js = include_str!("graphiql.js"),
+        ),
     )
 }
diff --git a/juniper_actix/src/lib.rs b/juniper_actix/src/lib.rs
index 7a2e5b4be..c18eeee1b 100644
--- a/juniper_actix/src/lib.rs
+++ b/juniper_actix/src/lib.rs
@@ -515,10 +515,8 @@ mod tests {
             "text/html; charset=utf-8"
         );
         let body = take_response_body_string(resp).await;
-        assert!(body.contains("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>"));
-        assert!(body.contains(
-            "<script>var GRAPHQL_SUBSCRIPTIONS_URL = '/dogs-api/subscriptions';</script>"
-        ))
+        assert!(body.contains("var GRAPHQL_URL = '/dogs-api/graphql';"));
+        assert!(body.contains("var GRAPHQL_SUBSCRIPTIONS_URL = '/dogs-api/subscriptions';"))
     }
 
     #[actix_web::rt::test]
diff --git a/juniper_warp/src/lib.rs b/juniper_warp/src/lib.rs
index 88ee8cb0f..d6ccc2c67 100644
--- a/juniper_warp/src/lib.rs
+++ b/juniper_warp/src/lib.rs
@@ -556,7 +556,7 @@ mod tests {
         );
         let body = String::from_utf8(response.body().to_vec()).unwrap();
 
-        assert!(body.contains("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>"));
+        assert!(body.contains("var GRAPHQL_URL = '/dogs-api/graphql';"));
     }
 
     #[tokio::test]


Test patch: