diff --git a/.github/scripts/update-cdn-versions.mjs b/.github/scripts/update-cdn-versions.mjs new file mode 100644 index 0000000000..38aeaf3560 --- /dev/null +++ b/.github/scripts/update-cdn-versions.mjs @@ -0,0 +1,113 @@ +#!/usr/bin/env node + +/** + * Generates examples/graphiql-cdn/index.html + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import crypto from 'node:crypto'; + +const PACKAGES = [ + 'react', + 'react-dom', + 'graphiql', + '@graphiql/plugin-explorer', + '@graphiql/react', + '@graphiql/toolkit', + 'graphql', +]; + +/** + * Given the name of an npm package, return a tuple of: [name, latest version] + * + * @example + * + * fetchLatestVersion('left-pad') + * => ['left-pad', '1.0.1'] + */ +async function fetchLatestVersion(packageName) { + const url = `https://registry.npmjs.org/${packageName}/latest`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch ${packageName}: ${response.statusText}`); + } + const { version } = await response.json(); + return [packageName, version] +} + +/** + * Given the url of a file, return a tuple of: [url, sha384] + * + * @example + * + * fetchLatestVersion('https://esm.sh/left-pad/lib/index.js') + * => ['https://esm.sh/left-pad/lib/index.js', 'sha384-deadbeef123'] + */ +async function fetchIntegrityHash(url) { + const response = await fetch(url, { redirect: 'follow' }); + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.statusText}`); + } + const content = await response.text(); + const hash = crypto.createHash('sha384').update(content).digest('base64'); + return [url, `sha384-${hash}`]; +} + +async function main () { + const versions = Object.fromEntries(await Promise.all(PACKAGES.map(fetchLatestVersion))); + const cdnUrl = packageName => `https://esm.sh/${packageName}@${versions[packageName]}`; + + // JS + const imports = { + 'react': cdnUrl('react'), + 'react/': `${cdnUrl('react-dom')}/`, + 'react-dom': cdnUrl('react-dom'), + 'react-dom/': `${cdnUrl('react-dom')}/`, + 'graphiql': `${cdnUrl('graphiql')}?standalone&external=react,react-dom,@graphiql/react,graphql`, + 'graphiql/': `${cdnUrl('graphiql')}/`, + '@graphiql/plugin-explorer': `${cdnUrl('@graphiql/plugin-explorer')}?standalone&external=react,@graphiql/react,graphql`, + '@graphiql/react': `${cdnUrl('@graphiql/react')}?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid`, + '@graphiql/toolkit': `${cdnUrl('@graphiql/toolkit')}?standalone&external=graphql`, + 'graphql': cdnUrl('graphql'), + '@emotion/is-prop-valid': "data:text/javascript," + }; + + const integrity = Object.fromEntries(await Promise.all([ + cdnUrl('react'), + cdnUrl('react-dom'), + cdnUrl('graphiql'), + `${cdnUrl('graphiql')}?standalone&external=react,react-dom,@graphiql/react,graphql`, + cdnUrl('@graphiql/plugin-explorer'), + `${cdnUrl('@graphiql/react')}?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid`, + `${cdnUrl('@graphiql/toolkit')}?standalone&external=graphql`, + cdnUrl('graphql'), + ].map(fetchIntegrityHash))); + + let importMap = JSON.stringify({ imports, integrity }, null, 2); + + // CSS + const graphiqlCss = `${cdnUrl('graphiql')}/dist/style.css`; + const graphiqlCssHash = (await fetchIntegrityHash(graphiqlCss))[1]; + const graphiqlPluginExplorer = `${cdnUrl('@graphiql/plugin-explorer')}/dist/style.css`; + const graphiqlPluginExplorerHash = (await fetchIntegrityHash(graphiqlPluginExplorer))[1]; + + // Generate index.html + const templatePath = path.join(import.meta.dirname, '../../resources/index.html.template'); + const template = fs.readFileSync(templatePath, 'utf8'); + + // Indent import map to be correctly formatted in index.html + const indent = lines => lines.split('\n').map(line => ` ${line}`).join('\n'); + importMap = indent(importMap); + + const output = template + .replace('{{IMPORTMAP}}', importMap) + .replace('{{GRAPHIQL_CSS_URL}}', graphiqlCss) + .replace('{{GRAPHIQL_CSS_INTEGRITY}}', graphiqlCssHash) + .replace('{{PLUGIN_EXPLORER_CSS_URL}}', graphiqlPluginExplorer) + .replace('{{PLUGIN_EXPLORER_CSS_INTEGRITY}}', graphiqlPluginExplorerHash); + + console.log(output); +} + +main(); diff --git a/.github/workflows/update-cdn-example.yml b/.github/workflows/update-cdn-example.yml new file mode 100644 index 0000000000..fff13b5079 --- /dev/null +++ b/.github/workflows/update-cdn-example.yml @@ -0,0 +1,55 @@ +name: Update CDN Example Dependencies + +on: + schedule: + # Run every Monday at 10:00 UTC + - cron: '0 10 * * 1' + workflow_dispatch: # Allow manual triggering + release: + types: [released] + +permissions: + contents: write + pull-requests: write + +jobs: + update-dependencies: + name: Check and Update CDN Dependencies + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Generate index.html + id: generate + run: | + node .github/scripts/update-cdn-versions.mjs > examples/graphiql-cdn/index.html + + - name: Check for Changes + id: check-changes + run: | + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.check-changes.outputs.has_changes == 'true' + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore(cdn-example): update dependencies to latest versions' + title: 'chore(cdn-example): update dependencies to latest versions' + body: | + This PR automatically updates the CDN example dependencies to their latest versions. + + 🤖 This PR was automatically generated automatically, beep boop + branch: automated/update-cdn-example-dependencies + delete-branch: true + labels: | + dependencies + automated diff --git a/examples/graphiql-cdn/index.html b/examples/graphiql-cdn/index.html index 1fb5b7cef3..25405fbf51 100644 --- a/examples/graphiql-cdn/index.html +++ b/examples/graphiql-cdn/index.html @@ -28,10 +28,17 @@ font-size: 4rem; } - + + + + + + + GraphiQL 5 with React 19 and GraphiQL Explorer + + + + + + + + +
+
Loading…
+
+ +