Skip to content

Commit 9c3118d

Browse files
committed
create auto-updater for examples/graphiql-cdn/index.html
1 parent 624d292 commit 9c3118d

File tree

4 files changed

+244
-7
lines changed

4 files changed

+244
-7
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Generates examples/graphiql-cdn/index.html
5+
*/
6+
7+
import fs from 'node:fs';
8+
import path from 'node:path';
9+
import crypto from 'node:crypto';
10+
11+
const PACKAGES = [
12+
'react',
13+
'react-dom',
14+
'graphiql',
15+
'@graphiql/plugin-explorer',
16+
'@graphiql/react',
17+
'@graphiql/toolkit',
18+
'graphql',
19+
];
20+
21+
async function fetchIntegrityHash(url) {
22+
const response = await fetch(url, { redirect: 'follow' });
23+
if (!response.ok) {
24+
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
25+
}
26+
const content = await response.text();
27+
const hash = crypto.createHash('sha384').update(content).digest('base64');
28+
return [url, `sha384-${hash}`];
29+
}
30+
31+
async function fetchLatestVersion(packageName) {
32+
const url = `https://registry.npmjs.org/${packageName}/latest`;
33+
const response = await fetch(url);
34+
if (!response.ok) {
35+
throw new Error(`Failed to fetch ${packageName}: ${response.statusText}`);
36+
}
37+
const { version } = await response.json();
38+
return [packageName, version]
39+
}
40+
41+
async function main () {
42+
const versions = Object.fromEntries(await Promise.all(PACKAGES.map(fetchLatestVersion)));
43+
const cdnUrl = packageName => `https://esm.sh/${packageName}@${versions[packageName]}`;
44+
45+
// JS
46+
const imports = {
47+
'react': cdnUrl('react'),
48+
'react/': `${cdnUrl('react-dom')}/`,
49+
'react-dom': cdnUrl('react-dom'),
50+
'react-dom/': `${cdnUrl('react-dom')}/`,
51+
'graphiql': `${cdnUrl('graphiql')}?standalone&external=react,react-dom,@graphiql/react,graphql`,
52+
'graphiql/': `${cdnUrl('graphiql')}/`,
53+
'@graphiql/plugin-explorer': `${cdnUrl('@graphiql/plugin-explorer')}?standalone&external=react,@graphiql/react,graphql`,
54+
'@graphiql/react': `${cdnUrl('@graphiql/react')}?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid`,
55+
'@graphiql/toolkit': `${cdnUrl('@graphiql/toolkit')}?standalone&external=graphql`,
56+
'graphql': cdnUrl('graphql'),
57+
'@emotion/is-prop-valid': "data:text/javascript,"
58+
};
59+
60+
const integrity = Object.fromEntries(await Promise.all([
61+
cdnUrl('react'),
62+
cdnUrl('react-dom'),
63+
cdnUrl('graphiql'),
64+
`${cdnUrl('graphiql')}?standalone&external=react,react-dom,@graphiql/react,graphql`,
65+
cdnUrl('@graphiql/plugin-explorer'),
66+
`${cdnUrl('@graphiql/react')}?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid`,
67+
`${cdnUrl('@graphiql/toolkit')}?standalone&external=graphql`,
68+
cdnUrl('graphql'),
69+
].map(fetchIntegrityHash)));
70+
71+
const importMap = { imports, integrity };
72+
73+
// CSS
74+
const graphiqlCss = `${cdnUrl('graphiql')}/dist/style.css`;
75+
const graphiqlPluginExplorer = `${cdnUrl('@graphiql/plugin-explorer')}/dist/style.css`;
76+
77+
// Generate index.html
78+
const templatePath = path.join(import.meta.dirname, '../../resources/index.html.template');
79+
const template = fs.readFileSync(templatePath, 'utf8');
80+
81+
const indent = lines => lines.split('\n').map(line => ` ${line}`).join('\n');
82+
83+
const output = template
84+
.replace('{{IMPORTMAP}}', indent(JSON.stringify(importMap, null, 2)))
85+
.replace('{{GRAPHIQL_CSS_URL}}', graphiqlCss)
86+
.replace('{{GRAPHIQL_CSS_INTEGRITY}}', (await fetchIntegrityHash(graphiqlCss))[1])
87+
.replace('{{PLUGIN_EXPLORER_CSS_URL}}', graphiqlPluginExplorer)
88+
.replace('{{PLUGIN_EXPLORER_CSS_INTEGRITY}}', (await fetchIntegrityHash(graphiqlPluginExplorer))[1]);
89+
90+
console.log(output);
91+
}
92+
93+
main();
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Update CDN Example Dependencies
2+
3+
on:
4+
schedule:
5+
# Run every Monday at 10:00 UTC
6+
- cron: '0 10 * * 1'
7+
workflow_dispatch: # Allow manual triggering
8+
repository_dispatch:
9+
types: [update-cdn-dependencies]
10+
11+
permissions:
12+
contents: write
13+
pull-requests: write
14+
15+
jobs:
16+
update-dependencies:
17+
name: Check and Update CDN Dependencies
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- name: Checkout Code
22+
uses: actions/checkout@v4
23+
24+
- name: Setup Node.js
25+
uses: actions/setup-node@v4
26+
with:
27+
node-version: '22'
28+
29+
- name: Install Dependencies
30+
run: npm install -g npm-check-updates semver
31+
32+
- name: Check for Updates and Update index.html
33+
id: update
34+
run: |
35+
node .github/scripts/update-cdn-versions.mjs > examples/graphiql-cdn/index.html
36+
37+
- name: Check for Changes
38+
id: check-changes
39+
run: |
40+
if git diff --quiet; then
41+
echo "has_changes=false" >> $GITHUB_OUTPUT
42+
else
43+
echo "has_changes=true" >> $GITHUB_OUTPUT
44+
fi
45+
46+
- name: Create Pull Request
47+
if: steps.check-changes.outputs.has_changes == 'true'
48+
uses: peter-evans/create-pull-request@v7
49+
with:
50+
token: ${{ secrets.GITHUB_TOKEN }}
51+
commit-message: 'chore(cdn-example): update dependencies to latest versions'
52+
title: 'chore(cdn-example): update dependencies to latest versions'
53+
body: |
54+
This PR automatically updates the CDN example dependencies to their latest versions.
55+
56+
🤖 This PR was automatically generated automatically, beep boop
57+
branch: automated/update-cdn-dependencies
58+
delete-branch: true
59+
labels: |
60+
dependencies
61+
automated

examples/graphiql-cdn/index.html

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@
4848
<script type="importmap">
4949
{
5050
"imports": {
51-
"react": "https://esm.sh/react@19.1.0",
52-
"react/": "https://esm.sh/react@19.1.0/",
53-
"react-dom": "https://esm.sh/react-dom@19.1.0",
54-
"react-dom/": "https://esm.sh/react-dom@19.1.0/",
51+
"react": "https://esm.sh/react@19.2.0",
52+
"react/": "https://esm.sh/react-dom@19.2.0/",
53+
"react-dom": "https://esm.sh/react-dom@19.2.0",
54+
"react-dom/": "https://esm.sh/react-dom@19.2.0/",
5555
"graphiql": "https://esm.sh/[email protected]?standalone&external=react,react-dom,@graphiql/react,graphql",
5656
"graphiql/": "https://esm.sh/[email protected]/",
5757
"@graphiql/plugin-explorer": "https://esm.sh/@graphiql/[email protected]?standalone&external=react,@graphiql/react,graphql",
@@ -61,11 +61,11 @@
6161
"@emotion/is-prop-valid": "data:text/javascript,"
6262
},
6363
"integrity": {
64-
"https://esm.sh/react@19.1.0": "sha384-C3ApUaeHIj1v0KX4cY/+K3hQZ/8HcAbbmkw1gBK8H5XN4LCEguY7+A3jga11SaHF",
65-
"https://esm.sh/react-dom@19.1.0": "sha384-CKiqgCWLo5oVMbiCv36UR0pLRrzeRKhw1jFUpx0j/XdZOpZ43zOHhjf8yjLNuLEy",
64+
"https://esm.sh/react@19.2.0": "sha384-nOZt6fKb998R5rObOHPrlA6K4MfsPJA1SM1RF6XLKrCs7R7DRWe1QMpy9pFMCd7u",
65+
"https://esm.sh/react-dom@19.2.0": "sha384-g9QErJ4ghsQsmmARwwncWld1ANkSBgPJme9hzmI2Vmq4+iZDrt3tq9hu1iwQI2xG",
6666
"https://esm.sh/[email protected]": "sha384-iJccq+zsT06wL6UQ27mjQ6OoghntU/ZdWkOmza8f/iD4hVJXQOgZeH/230Pm2y3V",
6767
"https://esm.sh/[email protected]?standalone&external=react,react-dom,@graphiql/react,graphql": "sha384-32Vv0P2Qy9UWdE0/n9/nFmGh8tM5/vMgpAarsa+UdD6So+aS6DVBQZDIjS2lU52e",
68-
"https://esm.sh/@graphiql/plugin-explorer": "sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb",
68+
"https://esm.sh/@graphiql/plugin-explorer@5.1.1": "sha384-KrkvikOOZWjeIeO92D9EDOJQQ7QI7LQPDQrBqloLwTVOz4jNXTPfYA1PYOpI8/UI",
6969
"https://esm.sh/@graphiql/[email protected]?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid": "sha384-vbrVVt6MhT20iaS0B9nwXO210Cix1sSiP+RyMEdFGEj5ZHbXJz96XxtckeWTrnkd",
7070
"https://esm.sh/@graphiql/[email protected]?standalone&external=graphql": "sha384-ZsnupyYmzpNjF1Z/81zwi4nV352n4P7vm0JOFKiYnAwVGOf9twnEMnnxmxabMBXe",
7171
"https://esm.sh/[email protected]": "sha384-uhRXaGfgCFqosYlwSLNd7XpDF9kcSUycv5yVbjjhH5OrE675kd0+MNIAAaSc+1Pi"
@@ -104,3 +104,4 @@
104104
</div>
105105
</body>
106106
</html>
107+

resources/index.html.template

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<!--
2+
* Copyright (c) 2025 GraphQL Contributors
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
-->
8+
<!doctype html>
9+
<html lang="en">
10+
<head>
11+
<meta charset="UTF-8" />
12+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
13+
<title>GraphiQL 5 with React 19 and GraphiQL Explorer</title>
14+
<style>
15+
body {
16+
margin: 0;
17+
}
18+
19+
#graphiql {
20+
height: 100dvh;
21+
}
22+
23+
.loading {
24+
height: 100%;
25+
display: flex;
26+
align-items: center;
27+
justify-content: center;
28+
font-size: 4rem;
29+
}
30+
</style>
31+
<link
32+
rel="stylesheet"
33+
href="{{GRAPHIQL_CSS_URL}}"
34+
integrity="{{GRAPHIQL_CSS_INTEGRITY}}"
35+
crossorigin="anonymous"
36+
/>
37+
<link
38+
rel="stylesheet"
39+
href="{{PLUGIN_EXPLORER_CSS_URL}}"
40+
integrity="{{PLUGIN_EXPLORER_CSS_INTEGRITY}}"
41+
crossorigin="anonymous"
42+
/>
43+
<!--
44+
* Note:
45+
* The ?standalone flag bundles the module along with all of its `dependencies`, excluding `peerDependencies`, into a single JavaScript file.
46+
* `@emotion/is-prop-valid` is a shim to remove the console error ` module "@emotion /is-prop-valid" not found`. Upstream issue: https://github.com/motiondivision/motion/issues/3126
47+
-->
48+
<script type="importmap">
49+
{{IMPORTMAP}}
50+
</script>
51+
<script type="module">
52+
import React from 'react';
53+
import ReactDOM from 'react-dom/client';
54+
import { GraphiQL, HISTORY_PLUGIN } from 'graphiql';
55+
import { createGraphiQLFetcher } from '@graphiql/toolkit';
56+
import { explorerPlugin } from '@graphiql/plugin-explorer';
57+
import 'graphiql/setup-workers/esm.sh';
58+
59+
const fetcher = createGraphiQLFetcher({
60+
url: 'https://countries.trevorblades.com',
61+
});
62+
const plugins = [HISTORY_PLUGIN, explorerPlugin()];
63+
64+
function App() {
65+
return React.createElement(GraphiQL, {
66+
fetcher,
67+
plugins,
68+
defaultEditorToolsVisibility: true,
69+
});
70+
}
71+
72+
const container = document.getElementById('graphiql');
73+
const root = ReactDOM.createRoot(container);
74+
root.render(React.createElement(App));
75+
</script>
76+
</head>
77+
<body>
78+
<div id="graphiql">
79+
<div class="loading">Loading…</div>
80+
</div>
81+
</body>
82+
</html>

0 commit comments

Comments
 (0)