From ab0a791cc0d75efe05ca8b6f9da8e21271fbf309 Mon Sep 17 00:00:00 2001 From: "netop://ウィビ" Date: Sun, 26 Apr 2026 21:30:43 -0700 Subject: adds awesome new GraphiQL renderer and an example --- README.md | 47 ++++--- deno.json | 4 +- deno.lock | 290 +++++++++++++++++++++++++++++++++++++++++++ example.ts | 29 +++++ source/graphiql/markup.ts | 309 ---------------------------------------------- source/graphiql/render.ts | 239 +++++++++++------------------------ 6 files changed, 418 insertions(+), 500 deletions(-) create mode 100644 example.ts delete mode 100755 source/graphiql/markup.ts 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` handler. Pluggable into `Deno.serve`, Options: -| Option | Type | Description | -| ------------------- | ------------------------------------- | ---------------------------------------------------- | -| `schema` | `GraphQLSchema` | Required. Executable schema. | -| `context` | `(req) => Ctx \| Promise` | 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` | Passthrough options for the Playground renderer. | +| Option | Type | Description | +| ------------------- | ------------------------------------- | ---------------------------------------------- | +| `schema` | `GraphQLSchema` | Required. Executable schema. | +| `context` | `(req) => Ctx \| Promise` | 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` | 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 ``. 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Loading - GraphQL Playground -
- - `, - 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 ` - - ${typeof faviconUrl === "string" ? `` : ""} - ${faviconUrl === undefined ? `` : ""} - - `; -} - -const renderConfig = (config: unknown) => { - return filterXSS(`
${JSON.stringify(config)}
`, { - 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 ``. `null` skips, `undefined` falls back to default. */ faviconUrl?: string | null; + /** Document ``. */ 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"} - ${extendedOptions.env === "react" || extendedOptions.env === "electron" ? "" : getCdnMarkup(extendedOptions)} - + + + ${safeTitle} + ${faviconLink} + - + - ${loading.container} - ${renderConfig(extendedOptions)} -
+ +
+ + -- cgit v1.2.3