Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-graphiql-cdn-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphiql/cdn': major
---

Initial release. `@graphiql/cdn` is a pre-bundled CDN distribution of GraphiQL: a single ESM file (`dist/graphiql.js`) that loads in the browser from any static CDN with no build step, no importmap entries for transitive dependencies, and no third-party bundler in the request path. The package inlines `graphiql`, `@graphiql/react`, `@graphiql/plugin-explorer`, `@graphiql/toolkit`, and `graphql`; `react` and `react-dom` stay external. Monaco workers are emitted to `dist/workers/*` and loaded from the same origin as the bundle.
76 changes: 76 additions & 0 deletions packages/graphiql-cdn/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# `@graphiql/cdn`

A pre-bundled CDN distribution of [GraphiQL](https://github.com/graphql/graphiql). Load GraphiQL in a browser from a static CDN with no build step, no importmap, and no third-party bundler in the request path.

The package ships a single ESM file (`dist/graphiql.js`) with the GraphiQL UI, the default plugins, and all dependencies inlined. `react` and `react-dom` stay external and must be supplied by the page.

## Usage

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>GraphiQL</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@graphiql/cdn@<version>/dist/style.css"
/>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@19",
"react-dom": "https://esm.sh/react-dom@19"
}
}
</script>
<style>
body {
margin: 0;
}
#graphiql {
height: 100dvh;
}
</style>
</head>
<body>
<div id="graphiql"></div>
<script type="module">
import { createRoot } from 'react-dom/client';
import {
GraphiQL,
HISTORY_PLUGIN,
createGraphiQLFetcher,
explorerPlugin,
} from 'https://cdn.jsdelivr.net/npm/@graphiql/cdn@<version>/dist/graphiql.js';

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

createRoot(document.getElementById('graphiql')).render(
<GraphiQL
fetcher={fetcher}
plugins={[HISTORY_PLUGIN, explorerPlugin()]}
/>,
);
</script>
</body>
</html>
```

Pin a specific `<version>` for production use.

## What is exported

- `GraphiQL` — the main component (also the default export)
- `HISTORY_PLUGIN` — the history plugin instance
- `explorerPlugin` — the schema explorer plugin factory
- `createGraphiQLFetcher` — convenience fetcher for `GraphiQL`'s `fetcher` prop
- `createLocalStorage` — convenience storage factory for multi-instance pages
- `GraphiQLReact` — the full `@graphiql/react` namespace, for advanced customization
- `GraphQL` — the full `graphql-js` namespace, so plugins can reuse the same instance

## When not to use this package

If you have a build step (Vite, webpack, Next.js, etc.), install `graphiql` and the plugins you want from npm directly. This package exists for the no-build-step CDN use case.
54 changes: 54 additions & 0 deletions packages/graphiql-cdn/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@graphiql/cdn",
"version": "0.0.0",
"description": "Pre-bundled CDN distribution of GraphiQL: load GraphiQL in a browser from a static CDN with no build step.",
"repository": {
"type": "git",
"url": "https://github.com/graphql/graphiql",
"directory": "packages/graphiql-cdn"
},
"homepage": "https://github.com/graphql/graphiql/tree/main/packages/graphiql-cdn#readme",
"bugs": {
"url": "https://github.com/graphql/graphiql/issues?q=issue+label:@graphiql/cdn"
},
"license": "MIT",
"type": "module",
"main": "dist/graphiql.js",
"module": "dist/graphiql.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"exports": {
"./package.json": "./package.json",
"./style.css": "./dist/style.css",
".": {
"types": "./dist/index.d.ts",
"import": "./dist/graphiql.js"
}
},
"scripts": {
"types:check": "tsgo --noEmit",
"build": "vite build"
},
"peerDependencies": {
"graphql": "^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2 || ^17.0.0",
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
},
"dependencies": {
"@graphiql/plugin-explorer": "^5.1.1",
"@graphiql/react": "^0.37.0",
"@graphiql/toolkit": "^0.12.0",
"graphiql": "^5.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4",
"lightningcss": "^1.30.1",
"react": "^19",
"react-dom": "^19",
"terser": "^5",
"vite": "^6",
"vite-plugin-dts": "^4.5.4"
}
}
7 changes: 7 additions & 0 deletions packages/graphiql-cdn/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.css';

declare namespace globalThis {
import type * as monaco from 'monaco-editor';
var MonacoEnvironment: monaco.Environment | undefined;
}
22 changes: 22 additions & 0 deletions packages/graphiql-cdn/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as GraphiQLReact from '@graphiql/react';
import { explorerPlugin } from '@graphiql/plugin-explorer';
import { createGraphiQLFetcher, createLocalStorage } from '@graphiql/toolkit';
import * as GraphQL from 'graphql';
import { GraphiQL, HISTORY_PLUGIN } from 'graphiql';
import 'graphiql/style.css';
import '@graphiql/plugin-explorer/style.css';
// Configure MonacoEnvironment with workers inlined as blob URLs, so the
// bundle can be loaded from a CDN onto pages of any origin.
import './setup-workers.js';

export {
GraphiQL,
HISTORY_PLUGIN,
GraphiQLReact,
GraphQL,
createGraphiQLFetcher,
createLocalStorage,
explorerPlugin,
};

export default GraphiQL;
24 changes: 24 additions & 0 deletions packages/graphiql-cdn/src/setup-workers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* CDN-targeted Monaco worker setup. Workers are inlined as blob URLs
* (Vite's `?worker&inline`) so the spawning page can be any origin: there
* is no cross-origin `new Worker(url)` to fail.
*
* Equivalent to `@graphiql/react/setup-workers/vite`, but with `&inline`
* because the consumer-side bundler isn't in the picture.
*/
/* eslint-disable import-x/default -- false positive */
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker.js?worker&inline';
import GraphQLWorker from 'monaco-graphql/esm/graphql.worker.js?worker&inline';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker&inline';

globalThis.MonacoEnvironment = {
getWorker(_workerId: string, label: string) {
switch (label) {
case 'json':
return new JsonWorker();
case 'graphql':
return new GraphQLWorker();
}
return new EditorWorker();
},
};
3 changes: 3 additions & 0 deletions packages/graphiql-cdn/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../graphiql-react/tsconfig.json"
}
75 changes: 75 additions & 0 deletions packages/graphiql-cdn/vite.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import react from '@vitejs/plugin-react';

export default defineConfig({
// Emit relative asset/worker URLs so the bundle can be served from any
// CDN path (jsdelivr/unpkg/esm.sh `?raw`) rather than only the origin root.
// Without this, Vite hard-codes `new Worker("/workers/json.worker.js", ...)`
// which resolves to `https://<cdn-origin>/workers/...` (404) instead of
// `https://<cdn-origin>/.../dist/workers/...`.
base: './',
define: {
// graphql v17
'globalThis.process.env.NODE_ENV': 'true',
// https://github.com/graphql/graphql-js/blob/16.x.x/website/pages/docs/going-to-production.mdx
'globalThis.process': 'true',
'process.env.NODE_ENV': '"production"',
},
plugins: [
react(),
dts({
include: ['src/**'],
outDir: ['dist'],
}),
],
css: {
transformer: 'lightningcss',
},
build: {
minify: 'terser',
sourcemap: true,
lib: {
entry: 'src/index.ts',
formats: ['es'],
fileName: 'graphiql',
cssFileName: 'style',
},
rollupOptions: {
external: id =>
id === 'react' ||
id.startsWith('react/') ||
id === 'react-dom' ||
id.startsWith('react-dom/') ||
id === 'graphql' ||
id.startsWith('graphql/'),
output: {
// Inline every lazy `import(...)` into the main bundle. Critical for
// the CDN use case: monaco-editor's language contributions lazy-load
// their tokenizers via dynamic imports; if those land in separate
// chunks they ship as separate URLs that fragment monaco-editor into
// multiple instances at runtime. A single self-contained file
// guarantees one instance.
inlineDynamicImports: true,
},
},
},
worker: {
format: 'es',
rollupOptions: {
output: {
entryFileNames: 'workers/[name].js',
// Just to group worker assets, add shared/internal chunks too
chunkFileNames: 'workers/[name].js',
// Workers are inlined as blob URLs (Vite's `?worker&inline`). The
// default sourcemap setting writes a `.js.map` next to the worker
// and appends `//# sourceMappingURL=foo.worker.js.map` to the
// source — but blob URLs have no real origin, so the comment
// resolves to `blob://nullnull/foo.worker.js.map` and the browser
// logs a CORS/local-resource error trying to fetch it. Suppress
// worker sourcemaps to keep the consumer's console clean.
sourcemap: false,
},
},
},
});
Loading
Loading