Skip to content

Add @graphiql/cdn package#4312

Open
trevor-scheer wants to merge 1 commit into
graphql:mainfrom
trevor-scheer:trevor/graphiql-cdn-package
Open

Add @graphiql/cdn package#4312
trevor-scheer wants to merge 1 commit into
graphql:mainfrom
trevor-scheer:trevor/graphiql-cdn-package

Conversation

@trevor-scheer
Copy link
Copy Markdown
Contributor

@trevor-scheer trevor-scheer commented May 20, 2026

Summary

@graphiql/cdn is a new package that ships GraphiQL as a self-contained, pre-bundled file for browser CDN consumption. One ESM file, one CSS file, react / react-dom / graphql as the only externals (peer deps). Drop it into a <script type="module"> on any page and you have a working GraphiQL.

Motivation

The current CDN story (examples/graphiql-cdn/index.html) relies on esm.sh's ?standalone mode to assemble the bundle on demand from our npm-published packages. In late April, an esm.sh change to how it handles small re-export modules started fragmenting monaco-editor into two runtime instances. The result is Cannot set tokens provider for unknown language json and dead syntax highlighting + autocomplete for every CDN consumer (#4303).

esm.sh caches each version's build immutably, so the regression is sticky for @graphiql/react@0.37.4+, and the fix has to come from either esm.sh or from us moving off ?standalone. Iterating on package.json shape to coax esm.sh's builder into a working output didn't pan out, and even if it had, the next esm.sh internal change could break us the same way.

Owning the bundle means:

  • The bytes a user gets are the bytes we built and tested.
  • We never end up with a fragmented monaco instance, because there is only ever one instance in the file.
  • A CDN-shape regression in CI maps to a code change in this repo, not a third-party rebuild we have no visibility into.
  • Every PR can be validated against the real-world CDN path before merge: pkg.pr.new builds the package on each commit, and esm.sh/pr/.../?raw serves the bundle untransformed. No publish-to-test cycle.

What's in the package

  • dist/graphiql.js — single self-contained ESM bundle, ~11 MB raw, ~2 MB gzipped. Inlines graphiql, @graphiql/react, the explorer and history plugins, @graphiql/toolkit, and the monaco workers (as blob URLs, so there are no separate worker fetches and no cross-origin worker constraints).
  • dist/style.css — bundled stylesheet (graphiql + explorer styles).
  • Peer dependencies (externalized from the bundle, supplied by the consumer's importmap): react, react-dom, graphql.

Usage

<link
  rel="stylesheet"
  href="https://esm.sh/@graphiql/cdn@<v>/dist/style.css?raw"
  integrity="sha384-<computed-per-version>"
  crossorigin="anonymous"
/>
<script type="importmap">
  {
    "imports": {
      "react": "https://esm.sh/react@19",
      "react/": "https://esm.sh/react@19/",
      "react-dom": "https://esm.sh/react-dom@19",
      "react-dom/": "https://esm.sh/react-dom@19/",
      "graphql": "https://esm.sh/graphql@16",
      "@graphiql/cdn": "https://esm.sh/@graphiql/cdn@<v>/dist/graphiql.js?raw"
    },
    "integrity": {
      "https://esm.sh/@graphiql/cdn@<v>/dist/graphiql.js?raw": "sha384-<computed-per-version>"
    }
  }
</script>
<script type="module">
  import { createRoot } from 'react-dom/client';
  import {
    GraphiQL,
    HISTORY_PLUGIN,
    createGraphiQLFetcher,
    explorerPlugin,
  } from '@graphiql/cdn';

  const fetcher = createGraphiQLFetcher({ url: 'https://your-endpoint/graphql' });
  createRoot(document.getElementById('graphiql')).render(
    <GraphiQL fetcher={fetcher} plugins={[HISTORY_PLUGIN, explorerPlugin()]} />,
  );
</script>

Per-asset SRI hashes are exact-content hashes — compute them once after pinning a version: curl -sL <url> | openssl dgst -sha384 -binary | openssl base64 -A.

Test plan

How to test this out

Save the snippet below as cdn-pr-4312.html and open it in a browser (no server needed — every URL is absolute). It pins to commit 58fc180 of this PR with real SRI hashes. Type in the operation editor to verify syntax highlighting and autocompletion against the countries.trevorblades.com endpoint.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@graphiql/cdn — PR #4312 preview</title>
    <link
      rel="stylesheet"
      href="https://esm.sh/pr/graphql/graphiql/@graphiql/cdn@58fc180/dist/style.css?raw"
      integrity="sha384-pt9abHwbmOWombYTMScDYL3pUU3n8R1M4S2nB3MzQOfZVtTAgN2n12aevJDSpHXE"
      crossorigin="anonymous"
    />
    <style>
      body { margin: 0; }
      #graphiql { height: 100dvh; }
      .loading { height: 100%; display: flex; align-items: center; justify-content: center; font-size: 4rem; }
    </style>
    <script type="importmap">
      {
        "imports": {
          "react": "https://esm.sh/react@19",
          "react/": "https://esm.sh/react@19/",
          "react-dom": "https://esm.sh/react-dom@19",
          "react-dom/": "https://esm.sh/react-dom@19/",
          "graphql": "https://esm.sh/graphql@16",
          "@graphiql/cdn": "https://esm.sh/pr/graphql/graphiql/@graphiql/cdn@58fc180/dist/graphiql.js?raw"
        },
        "integrity": {
          "https://esm.sh/pr/graphql/graphiql/@graphiql/cdn@58fc180/dist/graphiql.js?raw": "sha384-XDKvG6WTOzYhfpeARb1YkqEKax3FiFwf6KSjYlvKRvfNVEI8NqLQMbbnTR/cLbaO"
        }
      }
    </script>
    <script type="module">
      import React from 'react';
      import { createRoot } from 'react-dom/client';
      import {
        GraphiQL,
        HISTORY_PLUGIN,
        createGraphiQLFetcher,
        explorerPlugin,
      } from '@graphiql/cdn';

      const fetcher = createGraphiQLFetcher({
        url: 'https://countries.trevorblades.com',
      });

      createRoot(document.getElementById('graphiql')).render(
        React.createElement(GraphiQL, {
          fetcher,
          plugins: [HISTORY_PLUGIN, explorerPlugin()],
          defaultEditorToolsVisibility: true,
        }),
      );
    </script>
  </head>
  <body>
    <div id="graphiql"><div class="loading">Loading…</div></div>
  </body>
</html>
  • Open the pkg.pr.new preview snippet above. GraphiQL renders, GraphQL syntax is colored, autocomplete fires when typing a field name, the explorer opens, Execute returns data.
  • In a bundler-based consumer (Vite/webpack/Next), confirm import { GraphiQL } from '@graphiql/cdn' still resolves and the component mounts. The package ships normal ESM; bundlers treat it like any other dep.

Follow-ups (not in this PR)

  • Rewrite examples/graphiql-cdn/index.html to consume @graphiql/cdn.
  • Migrate the cypress fixture chain off the old dist/index.umd.js and delete packages/graphiql/src/cdn.ts + its UMD vite config.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 20, 2026

🦋 Changeset detected

Latest commit: 58fc180

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@graphiql/cdn Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@trevor-scheer trevor-scheer force-pushed the trevor/graphiql-cdn-package branch 2 times, most recently from 1627bc2 to d955761 Compare May 20, 2026 05:57
A self-contained, pre-bundled ESM distribution of GraphiQL for browser
CDN consumption. One `.js` file, one `.css` file, with `react`,
`react-dom`, and `graphql` as the only externals (peer deps). Monaco
editor and workers are inlined into the bundle, so loading the file
from any static CDN onto any origin gives a working GraphiQL with no
build step on the consumer side.

Replaces the previous esm.sh `?standalone` approach for CDN consumers.
See graphql#4303 for the underlying esm.sh regression that motivated owning
the bundle.
@trevor-scheer trevor-scheer force-pushed the trevor/graphiql-cdn-package branch from d955761 to 58fc180 Compare May 20, 2026 06:05
@trevor-scheer trevor-scheer marked this pull request as ready for review May 20, 2026 06:10
@trevor-scheer
Copy link
Copy Markdown
Contributor Author

@dimaMachina FYI this was a pretty Claude-heavy investigation + PR but I'm pretty happy with the direction and I've reviewed it and iterated quite a bit already. Would love to get your take on the approach.

This is solving a real problem with our example usage of esm.sh as it is today.
See: #4303

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant