aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornetop://ウィビ <paul@webb.page>2026-04-26 21:30:43 -0700
committernetop://ウィビ <paul@webb.page>2026-04-26 21:30:43 -0700
commitab0a791cc0d75efe05ca8b6f9da8e21271fbf309 (patch)
tree55ac984ba79be706ee2f83219fc2670bce89f75c
parent84e0d5ad8ebb8e933823d9dabc8060294a4780dc (diff)
downloadgq-ab0a791cc0d75efe05ca8b6f9da8e21271fbf309.tar.gz
gq-ab0a791cc0d75efe05ca8b6f9da8e21271fbf309.zip
adds awesome new GraphiQL renderer and an example
Diffstat (limited to '')
-rw-r--r--README.md47
-rw-r--r--deno.json4
-rw-r--r--deno.lock290
-rw-r--r--example.ts29
-rwxr-xr-xsource/graphiql/markup.ts309
-rwxr-xr-xsource/graphiql/render.ts239
6 files changed, 418 insertions, 500 deletions
diff --git a/README.md b/README.md
index 76412a1..e2e8f18 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# @eol/gq
-A batteries-included GraphQL toolkit for Deno, wrapping `graphql-js` and `@graphql-tools/schema` with the bits most APIs end up reaching for anyway: a Fetch-compatible HTTP handler, a cached `gql` tag, a `.graphql` file loader, and a cleaned-up GraphQL Playground.
+A batteries-included GraphQL toolkit for Deno, wrapping `graphql-js` and `@graphql-tools/schema` with the bits most APIs end up reaching for anyway: a Fetch-compatible HTTP handler, a cached `gql` tag, a `.graphql` file loader, and an embedded GraphiQL UI powered by [`@eeeooolll/graphiql`](https://www.npmjs.com/package/@eeeooolll/graphiql).
## Install
@@ -40,7 +40,7 @@ Deno.serve(
);
```
-Visit `http://localhost:8000` in a browser for the Playground, or `POST` a query to the same URL.
+Visit `http://localhost:8000` in a browser for GraphiQL, or `POST` a query to the same URL.
## Loading schemas from `.graphql` files
@@ -89,13 +89,13 @@ Returns a `(request) => Promise<Response>` handler. Pluggable into `Deno.serve`,
Options:
-| Option | Type | Description |
-| ------------------- | ------------------------------------- | ---------------------------------------------------- |
-| `schema` | `GraphQLSchema` | Required. Executable schema. |
-| `context` | `(req) => Ctx \| Promise<Ctx>` | Builds the resolver context per request. |
-| `graphiql` | `boolean` | Serve the Playground on `GET` + `Accept: text/html`. |
-| `headers` | `HeadersInit` | Extra headers merged into every response. |
-| `playgroundOptions` | `Omit<RenderPageOptions, "endpoint">` | Passthrough options for the Playground renderer. |
+| Option | Type | Description |
+| ------------------- | ------------------------------------- | ---------------------------------------------- |
+| `schema` | `GraphQLSchema` | Required. Executable schema. |
+| `context` | `(req) => Ctx \| Promise<Ctx>` | Builds the resolver context per request. |
+| `graphiql` | `boolean` | Serve GraphiQL on `GET` + `Accept: text/html`. |
+| `headers` | `HeadersInit` | Extra headers merged into every response. |
+| `playgroundOptions` | `Omit<RenderPageOptions, "endpoint">` | Passthrough options for the GraphiQL renderer. |
### `executeSchema(config)`
@@ -114,28 +114,35 @@ Companion knobs:
### `importQL(path)`
-Reads a `.graphql` file and resolves its imports. See [Loading schemas from `.graphql` files](#loading-schemas-from-graphql-files).
+Reads a `.graphql` file and resolves its imports. See [Loading schemas from `.graphql` files](#loading-schemas-from-graphql-files).
### `runHttpQuery(params, options, request)`
-Low-level executor that `GraphQLHTTP` delegates to. Use it if you’re rolling your own transport but still want the context wiring.
+Low-level executor that `GraphQLHTTP` delegates to. Use it if you’re rolling your own transport but still want the context wiring.
### Types
-`GQLOptions`, `GQLRequest`, `GraphQLParams`, `GraphQLHandler`, plus the Playground types (`RenderPageOptions`, `MiddlewareOptions`, `ISettings`, `EditorColours`, `Tab`, `Theme`, `CursorShape`, `IntrospectionResult`).
+`GQLOptions`, `GQLRequest`, `GraphQLParams`, `GraphQLHandler`, plus `RenderPageOptions` for the GraphiQL shell.
## Features
-- Import `*.graphql` files — explicit and dynamic — via `importQL`.
-- GraphiQL/Playground code cleaned up; SVGs redrawn so they actually make sense.
-- Ships typed; passes `deno check entry.ts` with no fuss.
-- Zero build step — it’s Deno, you just import it.
+- Import `*.graphql` files — explicit and dynamic — via `importQL`.
+- Embedded GraphiQL via [`@eeeooolll/graphiql`](https://www.npmjs.com/package/@eeeooolll/graphiql) — Svelte 5, CodeMirror 6, served as a prebuilt IIFE bundle from jsDelivr.
+- Ships typed; passes `deno check entry.ts` with no fuss.
+- Zero build step — it’s Deno, you just import it.
-## TODO
+## GraphiQL renderer options
-- Add a runnable example.
-- Replace React Playground with Svelte/SvelteKit.
-- Take over the world (real world, metaverse, or [yggdrasil](https://yggdrasil-network.github.io), whichever comes first).
+`playgroundOptions` is forwarded to the HTML shell builder. All fields are optional:
+
+| Field | Type | Description |
+| ------------ | ---------------- | ------------------------------------------------------------------------ |
+| `cdnUrl` | `string` | CDN base. Defaults to `//cdn.jsdelivr.net/npm`. |
+| `faviconUrl` | `string \| null` | `null` skips the favicon; `undefined` uses the bundle's default. |
+| `title` | `string` | Document `<title>`. Defaults to `"GraphiQL"`. |
+| `version` | `string` | Pin `@eeeooolll/graphiql` to a specific version on the CDN. Recommended. |
+
+Pinning `version` is recommended — without it the shell hits jsDelivr's `@latest` cache, which can lag behind new releases by ~12h.
## License
diff --git a/deno.json b/deno.json
index baa37cd..8b1c4de 100644
--- a/deno.json
+++ b/deno.json
@@ -1,12 +1,14 @@
{
"exports": "./entry.ts",
"imports": {
+ "@eeeooolll/graphiql": "npm:@eeeooolll/graphiql@0.4.0",
"@graphql-tools/schema": "npm:@graphql-tools/schema@^10.0.33",
+ "@netopwibby/dedent": "jsr:@netopwibby/dedent@^0.1.0",
"@std/path": "jsr:@std/path@^1.1.4",
"graphql": "npm:graphql@^16.13.2",
"xss": "npm:xss@^1.0.15"
},
"license": "MIT",
"name": "@eol/gq",
- "version": "0.3.0"
+ "version": "0.4.0"
}
diff --git a/deno.lock b/deno.lock
index 462c783..4e680a4 100644
--- a/deno.lock
+++ b/deno.lock
@@ -1,13 +1,18 @@
{
"version": "5",
"specifiers": {
+ "jsr:@netopwibby/dedent@0.1": "0.1.0",
"jsr:@std/internal@^1.0.12": "1.0.13",
"jsr:@std/path@^1.1.4": "1.1.4",
+ "npm:@eeeooolll/graphiql@0.4.0": "0.4.0_svelte@5.55.5_@codemirror+lint@6.9.5",
"npm:@graphql-tools/schema@^10.0.33": "10.0.33_graphql@16.13.2",
"npm:graphql@^16.13.2": "16.13.2",
"npm:xss@^1.0.15": "1.0.15"
},
"jsr": {
+ "@netopwibby/dedent@0.1.0": {
+ "integrity": "fcd8ffe607efdbe11ddcefa9169935514744b6b064ed634390818227fa0b401e"
+ },
"@std/internal@1.0.13": {
"integrity": "2f9546691d4ac2d32859c82dff284aaeac980ddeca38430d07941e7e288725c0"
},
@@ -19,6 +24,92 @@
}
},
"npm": {
+ "@codemirror/autocomplete@6.20.1": {
+ "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==",
+ "dependencies": [
+ "@codemirror/language",
+ "@codemirror/state",
+ "@codemirror/view",
+ "@lezer/common"
+ ]
+ },
+ "@codemirror/commands@6.10.3": {
+ "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==",
+ "dependencies": [
+ "@codemirror/language",
+ "@codemirror/state",
+ "@codemirror/view",
+ "@lezer/common"
+ ]
+ },
+ "@codemirror/lang-json@6.0.2": {
+ "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
+ "dependencies": [
+ "@codemirror/language",
+ "@lezer/json"
+ ]
+ },
+ "@codemirror/language@6.12.3": {
+ "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==",
+ "dependencies": [
+ "@codemirror/state",
+ "@codemirror/view",
+ "@lezer/common",
+ "@lezer/highlight",
+ "@lezer/lr",
+ "style-mod"
+ ]
+ },
+ "@codemirror/lint@6.9.5": {
+ "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==",
+ "dependencies": [
+ "@codemirror/state",
+ "@codemirror/view",
+ "crelt"
+ ]
+ },
+ "@codemirror/search@6.7.0": {
+ "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==",
+ "dependencies": [
+ "@codemirror/state",
+ "@codemirror/view",
+ "crelt"
+ ]
+ },
+ "@codemirror/state@6.6.0": {
+ "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
+ "dependencies": [
+ "@marijn/find-cluster-break"
+ ]
+ },
+ "@codemirror/view@6.41.1": {
+ "integrity": "sha512-ToDnWKbBnke+ZLrP6vgTTDScGi5H37YYuZGniQaBzxMVdtCxMrslsmtnOvbPZk4RX9bvkQqnWR/WS/35tJA0qg==",
+ "dependencies": [
+ "@codemirror/state",
+ "crelt",
+ "style-mod",
+ "w3c-keyname"
+ ]
+ },
+ "@eeeooolll/graphiql@0.4.0_svelte@5.55.5_@codemirror+lint@6.9.5": {
+ "integrity": "sha512-mUjq0sIFEy4OvHADmlrvbZET9LvgPfmKlnB13ao2fYQElHHilEtMDoN/09ATz2a5BAb5Zii07ZeTRNXsvWgAig==",
+ "dependencies": [
+ "@codemirror/autocomplete",
+ "@codemirror/commands",
+ "@codemirror/lang-json",
+ "@codemirror/language",
+ "@codemirror/state",
+ "@codemirror/view",
+ "@inc/uchu",
+ "@lezer/highlight",
+ "cm6-graphql",
+ "codemirror",
+ "graphql",
+ "graphql-sse",
+ "graphql-ws",
+ "svelte"
+ ]
+ },
"@graphql-tools/merge@9.1.9_graphql@16.13.2": {
"integrity": "sha512-iHUWNjRHeQRYdgIMIuChThOwoKzA9vrzYeslgfBo5eUYEyHGZCoDPjAavssoYXLwstYt1dZj2J22jSzc2DrN0Q==",
"dependencies": [
@@ -52,15 +143,124 @@
"graphql"
]
},
+ "@inc/uchu@2.2.0": {
+ "integrity": "sha512-AUx8lGbXYG7MemmYnpfpbqsTlnPt2n8prtVqDrPEqGPJ6K1DoIrqdt2HcHqjLv+vYq46rvwMEpY8/53V9xPSPw=="
+ },
+ "@jridgewell/gen-mapping@0.3.13": {
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dependencies": [
+ "@jridgewell/sourcemap-codec",
+ "@jridgewell/trace-mapping"
+ ]
+ },
+ "@jridgewell/remapping@2.3.5": {
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dependencies": [
+ "@jridgewell/gen-mapping",
+ "@jridgewell/trace-mapping"
+ ]
+ },
+ "@jridgewell/resolve-uri@3.1.2": {
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="
+ },
+ "@jridgewell/sourcemap-codec@1.5.5": {
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
+ },
+ "@jridgewell/trace-mapping@0.3.31": {
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dependencies": [
+ "@jridgewell/resolve-uri",
+ "@jridgewell/sourcemap-codec"
+ ]
+ },
+ "@lezer/common@1.5.2": {
+ "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ=="
+ },
+ "@lezer/highlight@1.2.3": {
+ "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
+ "dependencies": [
+ "@lezer/common"
+ ]
+ },
+ "@lezer/json@1.0.3": {
+ "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
+ "dependencies": [
+ "@lezer/common",
+ "@lezer/highlight",
+ "@lezer/lr"
+ ]
+ },
+ "@lezer/lr@1.4.10": {
+ "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
+ "dependencies": [
+ "@lezer/common"
+ ]
+ },
+ "@marijn/find-cluster-break@1.0.2": {
+ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="
+ },
+ "@sveltejs/acorn-typescript@1.0.9_acorn@8.16.0": {
+ "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==",
+ "dependencies": [
+ "acorn"
+ ]
+ },
+ "@types/estree@1.0.8": {
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="
+ },
+ "@types/trusted-types@2.0.7": {
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
+ },
"@whatwg-node/promise-helpers@1.3.2": {
"integrity": "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==",
"dependencies": [
"tslib"
]
},
+ "acorn@8.16.0": {
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "bin": true
+ },
+ "aria-query@5.3.1": {
+ "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="
+ },
+ "axobject-query@4.1.0": {
+ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="
+ },
+ "clsx@2.1.1": {
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="
+ },
+ "cm6-graphql@0.2.1_@codemirror+autocomplete@6.20.1_@codemirror+language@6.12.3_@codemirror+lint@6.9.5_@codemirror+state@6.6.0_@codemirror+view@6.41.1_@lezer+highlight@1.2.3_graphql@16.13.2": {
+ "integrity": "sha512-FIAFHn6qyiXChTz3Pml0NgTM8LyyXs8QfP2iPG7MLA8Xi83WuVlkGG5PDs+DDeEVabHkLIZmcyNngQlxLXKk6A==",
+ "dependencies": [
+ "@codemirror/autocomplete",
+ "@codemirror/language",
+ "@codemirror/lint",
+ "@codemirror/state",
+ "@codemirror/view",
+ "@lezer/highlight",
+ "graphql",
+ "graphql-language-service"
+ ]
+ },
+ "codemirror@6.0.2": {
+ "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
+ "dependencies": [
+ "@codemirror/autocomplete",
+ "@codemirror/commands",
+ "@codemirror/language",
+ "@codemirror/lint",
+ "@codemirror/search",
+ "@codemirror/state",
+ "@codemirror/view"
+ ]
+ },
"commander@2.20.3": {
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
+ "crelt@1.0.6": {
+ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
+ },
"cross-inspect@1.0.1": {
"integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==",
"dependencies": [
@@ -70,12 +270,97 @@
"cssfilter@0.0.10": {
"integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="
},
+ "debounce-promise@3.1.2": {
+ "integrity": "sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg=="
+ },
+ "devalue@5.7.1": {
+ "integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA=="
+ },
+ "esm-env@1.2.2": {
+ "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
+ },
+ "esrap@2.2.5": {
+ "integrity": "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==",
+ "dependencies": [
+ "@jridgewell/sourcemap-codec"
+ ]
+ },
+ "graphql-language-service@5.5.0_graphql@16.13.2": {
+ "integrity": "sha512-9EvWrLLkF6Y5e29/2cmFoAO6hBPPAZlCyjznmpR11iFtRydfkss+9m6x+htA8h7YznGam+TtJwS6JuwoWWgb2Q==",
+ "dependencies": [
+ "debounce-promise",
+ "graphql",
+ "nullthrows",
+ "vscode-languageserver-types"
+ ],
+ "bin": true
+ },
+ "graphql-sse@2.6.0_graphql@16.13.2": {
+ "integrity": "sha512-BXT5Rjv9UFunjQsmN9WWEIq+TFNhgYibgwo1xkXLxzguQVyOd6paJ4v5DlL9K5QplS0w74bhF+aUiqaGXZBaug==",
+ "dependencies": [
+ "graphql"
+ ]
+ },
+ "graphql-ws@6.0.8_graphql@16.13.2": {
+ "integrity": "sha512-m3EOaNsUBXwAnkBWbzPfe0Nq8pXUfxsWnolC54sru3FzHvhTZL0Ouf/BoQsaGAXqM+YPerXOJ47BUnmgmoupCw==",
+ "dependencies": [
+ "graphql"
+ ]
+ },
"graphql@16.13.2": {
"integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig=="
},
+ "is-reference@3.0.3": {
+ "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
+ "dependencies": [
+ "@types/estree"
+ ]
+ },
+ "locate-character@3.0.0": {
+ "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="
+ },
+ "magic-string@0.30.21": {
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dependencies": [
+ "@jridgewell/sourcemap-codec"
+ ]
+ },
+ "nullthrows@1.1.1": {
+ "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="
+ },
+ "style-mod@4.1.3": {
+ "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="
+ },
+ "svelte@5.55.5": {
+ "integrity": "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==",
+ "dependencies": [
+ "@jridgewell/remapping",
+ "@jridgewell/sourcemap-codec",
+ "@sveltejs/acorn-typescript",
+ "@types/estree",
+ "@types/trusted-types",
+ "acorn",
+ "aria-query",
+ "axobject-query",
+ "clsx",
+ "devalue",
+ "esm-env",
+ "esrap",
+ "is-reference",
+ "locate-character",
+ "magic-string",
+ "zimmerframe"
+ ]
+ },
"tslib@2.8.1": {
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
+ "vscode-languageserver-types@3.17.5": {
+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
+ },
+ "w3c-keyname@2.2.8": {
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
+ },
"xss@1.0.15": {
"integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==",
"dependencies": [
@@ -83,11 +368,16 @@
"cssfilter"
],
"bin": true
+ },
+ "zimmerframe@1.1.4": {
+ "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="
}
},
"workspace": {
"dependencies": [
+ "jsr:@netopwibby/dedent@0.1",
"jsr:@std/path@^1.1.4",
+ "npm:@eeeooolll/graphiql@0.4.0",
"npm:@graphql-tools/schema@^10.0.33",
"npm:graphql@^16.13.2",
"npm:xss@^1.0.15"
diff --git a/example.ts b/example.ts
new file mode 100644
index 0000000..9364fe0
--- /dev/null
+++ b/example.ts
@@ -0,0 +1,29 @@
+
+
+
+/*** UTILITY ------------------------------------------ ***/
+
+import { executeSchema, GraphQLHTTP, gql } from "./entry.ts";
+
+const schema = executeSchema({
+ resolvers: {
+ Query: {
+ hello: (_: unknown, { name }: { name?: string }) => `hello, ${name ?? "world"}`
+ }
+ },
+ typeDefs: gql`type Query { hello(name: String): String }`
+});
+
+const handler = GraphQLHTTP({
+ graphiql: true,
+ playgroundOptions: { version: "0.4.0" },
+ schema
+});
+
+/*** PROGRAM ------------------------------------------ ***/
+
+Deno.serve({ port: 4000 }, handler);
+
+
+
+/*** deno run -A example.ts ***/
diff --git a/source/graphiql/markup.ts b/source/graphiql/markup.ts
deleted file mode 100755
index 6bacbf3..0000000
--- a/source/graphiql/markup.ts
+++ /dev/null
@@ -1,309 +0,0 @@
-
-
-
-/*** EXPORT ------------------------------------------- ***/
-
-export const getLoadingMarkup = () => ({
- container: `
- <style>
- @keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(-10px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-
- @keyframes fadeOut {
- from {
- opacity: 1;
- transform: translateY(0);
- }
-
- to {
- opacity: 0;
- transform: translateY(-10px);
- }
- }
-
- .fadeOut {
- animation: fadeOut 0.5s ease-out forwards;
- }
-
- #loading-wrapper {
- width: 100vw; height: 100vh;
-
- align-items: center;
- display: flex;
- flex-direction: column;
- justify-content: center;
- position: absolute;
- }
-
- .logo {
- width: 75px; height: 75px;
-
- animation: fadeIn 0.5s ease-out forwards;
- margin-bottom: 20px;
- opacity: 0;
- }
-
- .text {
- animation: fadeIn 0.5s ease-out forwards;
- color: rgba(255, 255, 255, 0.6);
- font-size: 32px;
- font-weight: 200;
- opacity: 0;
- text-align: center;
- }
-
- .text-inner {
- font-weight: 400;
- }
- </style>
-
- <div id="loading-wrapper">
- <svg class="logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
- <title>GraphQL Playground Logo</title>
-
- <style>
- @keyframes appearIn {
- from {
- opacity: 0;
- transform: translateY(0px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-
- @keyframes scaleIn {
- from {
- transform: scale(0);
- }
-
- to {
- transform: scale(1);
- }
- }
-
- @keyframes innerDrawIn {
- 0% {
- stroke-dashoffset: 70;
- }
-
- 50% {
- stroke-dashoffset: 140;
- }
-
- 100% {
- stroke-dashoffset: 210;
- }
- }
-
- @keyframes outerDrawIn {
- 0% {
- stroke-dashoffset: 76;
- }
-
- 100% {
- stroke-dashoffset: 152;
- }
- }
-
- .circle-top,
- .circle-top-right,
- .circle-bottom-right,
- .circle-bottom,
- .circle-bottom-left,
- .circle-top-left {
- fill: white;
- }
-
- .circle-top {
- animation: scaleIn 0.25s linear forwards 0.2222222222222222s;
- transform: scale(0);
- transform-origin: 64px 28px;
- }
-
- .circle-top-right {
- animation: scaleIn 0.25s linear forwards 0.4222222222222222s;
- transform: scale(0);
- transform-origin: 95.98500061035156px 46.510000228881836px;
- }
-
- .circle-bottom-right {
- animation: scaleIn 0.25s linear forwards 0.6222222222222222s;
- transform: scale(0);
- transform-origin: 95.97162628173828px 83.4900016784668px;
- }
-
- .circle-bottom {
- animation: scaleIn 0.25s linear forwards 0.8222222222222223s;
- transform: scale(0);
- transform-origin: 64px 101.97999572753906px;
- }
-
- .circle-bottom-left {
- animation: scaleIn 0.25s linear forwards 1.0222222222222221s;
- transform: scale(0);
- transform-origin: 32.03982162475586px 83.4900016784668px;
- }
-
- .circle-top-left {
- animation: scaleIn 0.25s linear forwards 1.2222222222222223s;
- transform: scale(0);
- transform-origin: 32.033552169799805px 46.510000228881836px;
- }
-
- .octoline-top-right,
- .octoline-right,
- .octoline-bottom-right,
- .octoline-bottom-left,
- .octoline-left,
- .octoline-top-left {
- stroke: white;
- stroke-linecap: round;
- stroke-linejoin: round;
- stroke-width: 4;
- }
-
- .octoline-top-right {
- animation: outerDrawIn 0.5s ease-out forwards 0.3333333333333333s, appearIn 0.1s ease-out forwards 0.3333333333333333s;
- animation-iteration-count: 1, 1;
- opacity: 0;
- stroke-dasharray: 76;
- }
-
- .octoline-right {
- animation: outerDrawIn 0.5s ease-out forwards 0.5333333333333333s, appearIn 0.1s ease-out forwards 0.5333333333333333s;
- animation-iteration-count: 1, 1;
- opacity: 0;
- stroke-dasharray: 76;
- }
-
- .octoline-bottom-right {
- animation: outerDrawIn 0.5s ease-out forwards 0.7333333333333334s, appearIn 0.1s ease-out forwards 0.7333333333333334s;
- animation-iteration-count: 1, 1;
- opacity: 0;
- stroke-dasharray: 76;
- }
-
- .octoline-bottom-left {
- animation: outerDrawIn 0.5s ease-out forwards 0.9333333333333333s, appearIn 0.1s ease-out forwards 0.9333333333333333s;
- animation-iteration-count: 1, 1;
- opacity: 0;
- stroke-dasharray: 76;
- }
-
- .octoline-left {
- animation: outerDrawIn 0.5s ease-out forwards 1.1333333333333333s, appearIn 0.1s ease-out forwards 1.1333333333333333s;
- animation-iteration-count: 1, 1;
- opacity: 0;
- stroke-dasharray: 76;
- }
-
- .octoline-top-left {
- animation: outerDrawIn 0.5s ease-out forwards 1.3333333333333333s, appearIn 0.1s ease-out forwards 1.3333333333333333s;
- animation-iteration-count: 1, 1;
- opacity: 0;
- stroke-dasharray: 76;
- }
-
- .triangle-bottom,
- .triangle-left,
- .triangle-right {
- stroke: white;
- stroke-linecap: round;
- stroke-width: 4;
- }
-
- .triangle-bottom {
- animation: innerDrawIn 1s ease-in-out forwards 1.3666666666666667s, appearIn 0.1s linear forwards 1.3666666666666667s;
- animation-iteration-count: infinite, 1;
- opacity: 0;
- stroke-dasharray: 70;
- }
-
- .triangle-left {
- animation: innerDrawIn 1s ease-in-out forwards 1.5333333333333332s, appearIn 0.1s linear forwards 1.5333333333333332s;
- animation-iteration-count: infinite, 1;
- opacity: 0;
- stroke-dasharray: 70;
- }
-
- .triangle-right {
- animation: innerDrawIn 1s ease-in-out forwards 1.7000000000000002s, appearIn 0.1s linear forwards 1.7000000000000002s;
- animation-iteration-count: infinite, 1;
- opacity: 0;
- stroke-dasharray: 70;
- }
- </style>
-
- <defs>
- <linearGradient id="linearGradient" x1="4.86%" x2="96.21%" y1="0%" y2="99.66%">
- <stop stop-color="#e00082" stop-opacity="0.8" offset="0%"></stop>
- <stop stop-color="#e00082" offset="100%"></stop>
- </linearGradient>
- </defs>
-
- <g>
- <rect id="gradient" width="127.96" height="127.96" y="1" fill="url(#linearGradient)" rx="4"></rect>
- <path id="border" fill="#e00082" fill-rule="nonzero" d="M4.7 2.84c-1.58 0-2.86 1.28-2.86 2.85v116.57c0 1.57 1.28 2.84 2.85 2.84h116.57c1.57 0 2.84-1.26 2.84-2.83V5.67c0-1.55-1.26-2.83-2.83-2.83H4.67zM4.7 0h116.58c3.14 0 5.68 2.55 5.68 5.7v116.58c0 3.14-2.54 5.68-5.68 5.68H4.68c-3.13 0-5.68-2.54-5.68-5.68V5.68C-1 2.56 1.55 0 4.7 0z"></path>
-
- <path
- class="circle-top" x="64" y="28"
- d="M64 36c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8"
- style="transform: translate(100px, 100px);"></path>
- <path
- class="circle-top-right" x="95.98500061035156" y="46.510000228881836"
- d="M89.04 50.52c-2.2-3.84-.9-8.73 2.94-10.96 3.83-2.2 8.72-.9 10.95 2.94 2.2 3.84.9 8.73-2.94 10.96-3.85 2.2-8.76.9-10.97-2.94"
- style="transform: translate(100px, 100px);"></path>
- <path
- class="circle-bottom-right" x="95.97162628173828" y="83.4900016784668"
- d="M102.9 87.5c-2.2 3.84-7.1 5.15-10.94 2.94-3.84-2.2-5.14-7.12-2.94-10.96 2.2-3.84 7.12-5.15 10.95-2.94 3.86 2.23 5.16 7.12 2.94 10.96"
- style="transform: translate(100px, 100px);"></path>
- <path
- class="circle-bottom" x="64" y="101.97999572753906"
- d="M64 110c-4.43 0-8-3.6-8-8.02 0-4.44 3.57-8.02 8-8.02s8 3.58 8 8.02c0 4.4-3.57 8.02-8 8.02"
- style="transform: translate(100px, 100px);"></path>
- <path
- class="circle-bottom-left" x="32.03982162475586" y="83.4900016784668"
- d="M25.1 87.5c-2.2-3.84-.9-8.73 2.93-10.96 3.83-2.2 8.72-.9 10.95 2.94 2.2 3.84.9 8.73-2.94 10.96-3.85 2.2-8.74.9-10.95-2.94"
- style="transform: translate(100px, 100px);"></path>
- <path
- class="circle-top-left" x="32.033552169799805" y="46.510000228881836"
- d="M38.96 50.52c-2.2 3.84-7.12 5.15-10.95 2.94-3.82-2.2-5.12-7.12-2.92-10.96 2.2-3.84 7.12-5.15 10.95-2.94 3.83 2.23 5.14 7.12 2.94 10.96"
- style="transform: translate(100px, 100px);"></path>
-
- <path class="octoline-top-right" d="M63.55 27.5l32.9 19-32.9-19z"></path>
- <path class="octoline-right" d="M96 46v38-38z"></path>
- <path class="octoline-bottom-right" d="M96.45 84.5l-32.9 19 32.9-19z"></path>
- <path class="octoline-bottom-left" d="M64.45 103.5l-32.9-19 32.9 19z"></path>
- <path class="octoline-left" d="M32 84V46v38z"></path>
- <path class="octoline-top-left" d="M31.55 46.5l32.9-19-32.9 19z"></path>
-
- <path class="triangle-bottom" d="M30 84h70"></path>
- <path class="triangle-left" d="M65 26L30 87"></path>
- <path class="triangle-right" d="M98 87L63 26"></path>
- </g>
- </svg>
-
- <div class="text">Loading
- <span class="text-inner">GraphQL Playground</span>
- </div>
- </div>
- `,
- script: `
- const loadingWrapper = document.getElementById("loading-wrapper");
-
- if (loadingWrapper)
- loadingWrapper.classList.add("fadeOut");
- `
-});
diff --git a/source/graphiql/render.ts b/source/graphiql/render.ts
index a0e4dc2..7adec57 100755
--- a/source/graphiql/render.ts
+++ b/source/graphiql/render.ts
@@ -3,213 +3,112 @@
/*** IMPORT ------------------------------------------- ***/
+import { default as dedent } from "@netopwibby/dedent";
import { filterXSS } from "xss";
/*** UTILITY ------------------------------------------ ***/
-import { getLoadingMarkup } from "./markup.ts";
+const filter = (val: string) => filterXSS(val, {
+ stripIgnoreTag: true,
+ stripIgnoreTagBody: ["script"],
+ whiteList: {}
+});
-const CONFIG_ID = "playground-config";
-const loading = getLoadingMarkup();
-
-const filter = (val: string) => {
- return filterXSS(val, {
- stripIgnoreTag: true,
- stripIgnoreTagBody: ["script"],
- whiteList: {}
- });
-}
-
-const getCdnMarkup = ({ cdnUrl = "//cdn.jsdelivr.net/npm", faviconUrl, version }: {
- cdnUrl?: string
- faviconUrl?: string | null
+const buildAssetUrl = ({ cdnUrl, suffix, version }: {
+ cdnUrl: string
+ suffix: string
version?: string
-}) => {
- const buildCDNUrl = (packageName: string, suffix: string) =>
- filter(`${cdnUrl}/${packageName}${version ? `@${version}` : ""}/${suffix}` || "");
-
- return `
- <link rel="stylesheet" href="${buildCDNUrl("graphql-playground-react", "build/static/css/index.css")}"/>
- ${typeof faviconUrl === "string" ? `<link rel="shortcut icon" href="${filter(faviconUrl || "")}" />` : ""}
- ${faviconUrl === undefined ? `<link rel="shortcut icon" href="${buildCDNUrl("graphql-playground-react", "build/favicon.png")}" />` : ""}
- <script src="${buildCDNUrl("graphql-playground-react", "build/static/js/middleware.js")}"></script>
- `;
-}
-
-const renderConfig = (config: unknown) => {
- return filterXSS(`<div id="${CONFIG_ID}">${JSON.stringify(config)}</div>`, {
- whiteList: { div: ["id"] }
- });
-};
+}) => filter(`${cdnUrl}/@eeeooolll/graphiql${version ? `@${version}` : ""}/dist/${suffix}`);
/*** EXPORT ------------------------------------------- ***/
-export interface MiddlewareOptions {
- codeTheme?: EditorColours;
- config?: { [key: string]: unknown; };
- endpoint?: string;
- env?: "electron" | "react";
- schema?: IntrospectionResult;
- settings?: ISettings;
- subscriptionEndpoint?: string;
- tabs?: Tab[];
- workspaceName?: string;
-}
-
-export type CursorShape = "line" | "block" | "underline";
-export type Theme = "dark" | "light";
-
-export interface ISettings {
- "editor.cursorShape": CursorShape;
- "editor.fontFamily": string;
- "editor.fontSize": number;
- "editor.reuseHeaders": boolean;
- "editor.theme": Theme;
- "general.betaUpdates": boolean;
- "request.credentials": string;
- "request.globalHeaders": { [key: string]: string };
- "schema.polling.enable": boolean;
- "schema.polling.endpointFilter": string;
- "schema.polling.interval": number;
- "tracing.hideTracingResponse": boolean;
- "tracing.tracingSupported": boolean;
-}
-
-export interface EditorColours {
- atom: string;
- attribute: string;
- builtin: string;
- comment: string;
- cursorColor: string;
- def: string;
- editorBackground: string;
- keyword: string;
- leftDrawerBackground: string;
- meta: string;
- number: string;
- property: string;
- punctuation: string;
- qualifier: string;
- resultBackground: string;
- rightDrawerBackground: string;
- selection: string;
- string: string;
- string2: string;
- variable: string;
- ws: string;
-}
-
-export interface IntrospectionResult {
- __schema: { [key: string]: unknown; };
-}
-
-export interface RenderPageOptions extends MiddlewareOptions {
+export interface RenderPageOptions {
+ /** Base CDN. Defaults to jsDelivr. */
cdnUrl?: string;
- env?: "electron" | "react";
+ /** GraphQL endpoint the embedded fetcher posts to. */
+ endpoint?: string;
+ /** Optional `<link rel="shortcut icon">`. `null` skips, `undefined` falls back to default. */
faviconUrl?: string | null;
+ /** Document `<title>`. */
title?: string;
+ /** Pin `@eeeooolll/graphiql` to a specific version on the CDN. */
version?: string;
}
-export interface Tab {
- endpoint: string;
- headers?: { [key: string]: string };
- name?: string;
- query: string;
- responses?: string[];
- variables?: string;
-}
-
/**
- * Renders the GraphQL Playground HTML shell.
+ * Renders the GraphiQL HTML shell.
+ *
+ * Loads the prebuilt IIFE bundle from `@eeeooolll/graphiql` (registered as
+ * `window.EolGraphiQL`) and mounts it against `endpoint`.
*
* Usually called indirectly via `GraphQLHTTP({ graphiql: true })`; invoke it
- * directly if you need to embed the Playground in a custom route.
+ * directly if you need to embed GraphiQL in a custom route.
*/
-export function renderPlaygroundPage(options: RenderPageOptions) {
- const extendedOptions:
- & Partial<{
- canSaveConfig: boolean
- configString: string
- }>
- & RenderPageOptions = {
- ...options,
- canSaveConfig: false
- };
-
- if (options.config)
- extendedOptions.configString = JSON.stringify(options.config, null, 2);
-
- if (!extendedOptions.endpoint && !extendedOptions.configString)
- console.warn("WARNING: You did not provide an endpoint and do not have a .graphqlconfig. Make sure you have at least one of them.");
- else if (extendedOptions.endpoint)
- extendedOptions.endpoint = filter(extendedOptions.endpoint || "");
-
- return `
+export function renderPlaygroundPage(options: RenderPageOptions): string {
+ const {
+ cdnUrl = "//cdn.jsdelivr.net/npm",
+ endpoint = "/graphql",
+ faviconUrl,
+ title = "GraphiQL",
+ version
+ } = options;
+
+ const safeEndpoint = filter(endpoint);
+ const safeTitle = filter(title);
+ const scriptUrl = buildAssetUrl({ cdnUrl, suffix: "standalone.js", version });
+ const styleUrl = buildAssetUrl({ cdnUrl, suffix: "standalone.css", version });
+
+ const faviconLink =
+ faviconUrl === null ?
+ "" :
+ typeof faviconUrl === "string" ?
+ `<link rel="shortcut icon" href="${filter(faviconUrl)}"/>` :
+ `<link rel="shortcut icon" href="${buildAssetUrl({ cdnUrl, suffix: "favicon.svg", version })}"/>`;
+
+ return dedent`
<!DOCTYPE html>
<html lang="en">
<head>
- <meta charset=utf-8/>
- <meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui"/>
- <link href="https://brick.a.ssl.fastly.net/Open+Sans:300,400,600,700/Source+Code+Pro:400,700" rel="stylesheet"/>
- <title>${extendedOptions.title || "GraphQL Playground"}</title>
- ${extendedOptions.env === "react" || extendedOptions.env === "electron" ? "" : getCdnMarkup(extendedOptions)}
- </head>
+ <meta charset="utf-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
+ <title>${safeTitle}</title>
+ ${faviconLink}
+ <link rel="stylesheet" href="${styleUrl}"/>
- <body>
<style>
- html {
- font-family: "Open Sans", sans-serif;
- overflow: hidden;
- }
-
- body {
- background-color: #172a3a;
- margin: 0;
+ *,
+ *::before,
+ *::after {
+ margin: 0; padding: 0;
+ box-sizing: inherit;
}
- #${CONFIG_ID} {
- display: none;
+ html {
+ box-sizing: border-box;
}
- .playgroundIn {
- animation: playgroundIn 0.5s ease-out forwards;
+ html,
+ body {
+ height: 100%;
}
- @keyframes playgroundIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
+ #root {
+ height: 100vh;
}
</style>
+ </head>
- ${loading.container}
- ${renderConfig(extendedOptions)}
- <div id="root"/>
+ <body>
+ <div id="root"></div>
+
+ <script src="${scriptUrl}"></script>
<script>
window.addEventListener("load", () => {
- ${loading.script}
-
const root = document.getElementById("root");
- root.classList.add("playgroundIn");
- const configText = document.getElementById("${CONFIG_ID}").innerText;
-
- if (configText && configText.length) {
- try {
- GraphQLPlayground.init(root, JSON.parse(configText));
- } catch(_) {
- console.error("could not find config");
- }
- } else {
- GraphQLPlayground.init(root);
- }
+ const fetcher = window.EolGraphiQL.createHttpFetcher({ url: "${safeEndpoint}" });
+
+ window.EolGraphiQL.mount(root, { fetcher });
});
</script>
</body>