Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e5d4940
feat(graphiql): add modern React 18 frontend package for issue #21548
amcaplan Nov 2, 2025
6596095
feat(graphiql): integrate static file serving and config injection fo…
amcaplan Nov 2, 2025
17db403
feat(graphiql): configure NX, TypeScript, and Vite build for issue #2…
amcaplan Nov 2, 2025
b8bd5d9
fix: Remove built assets from git, configure NX dependencies, fix sta…
amcaplan Nov 2, 2025
c69512f
fix: Remove Monaco configuration, let GraphiQL handle it internally
amcaplan Nov 2, 2025
564201d
fix: Restore vite-plugin-monaco-editor with correct configuration
amcaplan Nov 2, 2025
61e15d6
fix: Remove ES worker format and fix config field name
amcaplan Nov 2, 2025
6f7ef73
fix: Add missing Monaco language workers (json, typescript)
amcaplan Nov 2, 2025
447f5d3
fix: Enable interactive GraphiQL editor with complete Monaco styling
amcaplan Nov 2, 2025
0afa912
Restore GraphiQL features, add tests, fix security
amcaplan Nov 2, 2025
291c1b9
Fix favicon and query escaping bugs
amcaplan Nov 2, 2025
4d6ed67
Fix query escaping in defaultQuery template
amcaplan Nov 2, 2025
e2ea0d5
Restore correct favicon from git history
amcaplan Nov 2, 2025
ecb6cc2
Remove old GraphiQL template implementation
amcaplan Nov 2, 2025
f742805
Restore GraphiQL header layout and styling to match original
amcaplan Nov 4, 2025
3060441
Fix GraphiQL tab order and remove DEFAULT_SHOP_QUERY duplication
amcaplan Nov 4, 2025
ea1f5d4
Fix GraphiQL header responsive behavior
amcaplan Nov 4, 2025
37b0b18
Fix linter issue
amcaplan Nov 4, 2025
ae2bbbf
Remove placeholder file
amcaplan Nov 4, 2025
144e483
Add graphiql-console to vitest workspace
amcaplan Nov 4, 2025
e764cd1
Fix nx cache invalidation for graphiql-console builds
amcaplan Nov 4, 2025
7719dd5
Fix XSS vulnerability in GraphiQL config injection
amcaplan Nov 4, 2025
3f360c8
Fix lint issues
amcaplan Nov 6, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ vite.config.ts.timestamp*

# from nested gitignores
packages/app/assets/dev-console
packages/app/assets/graphiql
packages/ui-extensions-server-kit/*.d.ts
packages/ui-extensions-server-kit/!typings.d.ts
packages/ui-extensions-server-kit/index.*
Expand Down
58 changes: 0 additions & 58 deletions packages/app/assets/graphiql/style.css

This file was deleted.

2 changes: 1 addition & 1 deletion packages/app/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"sourceRoot": "packages/app/src",
"projectType": "library",
"tags": ["scope:feature"],
"implicitDependencies": ["ui-extensions-dev-console"],
"implicitDependencies": ["ui-extensions-dev-console", "graphiql-console"],
"targets": {
"clean": {
"executor": "nx:run-commands",
Expand Down
91 changes: 57 additions & 34 deletions packages/app/src/cli/services/dev/graphiql/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {defaultQuery, graphiqlTemplate} from './templates/graphiql.js'
import {unauthorizedTemplate} from './templates/unauthorized.js'
import express from 'express'
import bodyParser from 'body-parser'
Expand All @@ -9,6 +8,8 @@ import {adminUrl, supportedApiVersions} from '@shopify/cli-kit/node/api/admin'
import {fetch} from '@shopify/cli-kit/node/http'
import {renderLiquidTemplate} from '@shopify/cli-kit/node/liquid'
import {outputDebug} from '@shopify/cli-kit/node/output'
import {readFile, findPathUp} from '@shopify/cli-kit/node/fs'
import {joinPath, moduleDirectory} from '@shopify/cli-kit/node/path'
import {Server} from 'http'
import {Writable} from 'stream'
import {createRequire} from 'module'
Expand Down Expand Up @@ -98,15 +99,14 @@ export function setupGraphiQLServer({
res.send('pong')
})

const faviconPath = require.resolve('@shopify/app/assets/graphiql/favicon.ico')
app.get('/graphiql/favicon.ico', (_req, res) => {
res.sendFile(faviconPath)
})

const stylePath = require.resolve('@shopify/app/assets/graphiql/style.css')
app.get('/graphiql/simple.css', (_req, res) => {
res.sendFile(stylePath)
})
// Serve static assets for the React app (JS, CSS, workers)
const graphiqlIndexPath = require.resolve('@shopify/app/assets/graphiql/index.html')
const graphiqlAssetsDir = graphiqlIndexPath.replace('/index.html', '')
app.use(
'/extensions/graphiql/assets',
express.static(joinPath(graphiqlAssetsDir, 'extensions', 'graphiql', 'assets')),
)
app.use('/monacoeditorwork', express.static(joinPath(graphiqlAssetsDir, 'monacoeditorwork')))

async function fetchApiVersionsWithTokenRefresh(): Promise<string[]> {
return performActionWithRetryAfterRecovery(
Expand All @@ -117,7 +117,14 @@ export function setupGraphiQLServer({

app.get('/graphiql/status', (_req, res) => {
fetchApiVersionsWithTokenRefresh()
.then(() => res.send({status: 'OK', storeFqdn, appName, appUrl}))
.then(() => {
res.send({
status: 'OK',
storeFqdn,
appName,
appUrl,
})
})
.catch(() => res.send({status: 'UNAUTHENTICATED'}))
})

Expand All @@ -127,7 +134,7 @@ export function setupGraphiQLServer({
if (failIfUnmatchedKey(req.query.key as string, res)) return

const usesHttps = req.protocol === 'https' || req.headers['x-forwarded-proto'] === 'https'
const url = `http${usesHttps ? 's' : ''}://${req.get('host')}`
const baseUrl = `http${usesHttps ? 's' : ''}://${req.get('host')}`

let apiVersions: string[]
try {
Expand All @@ -137,41 +144,57 @@ export function setupGraphiQLServer({
return res.send(
await renderLiquidTemplate(unauthorizedTemplate, {
previewUrl: appUrl,
url,
url: baseUrl,
}),
)
}
throw err
}

const sortedVersions = apiVersions.sort().reverse()
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const apiVersion = apiVersions.sort().reverse()[0]!
const apiVersion = sortedVersions[0]!

function decodeQueryString(input: string | undefined) {
return input ? decodeURIComponent(input).replace(/\n/g, '\\n') : undefined
return input ? decodeURIComponent(input) : undefined
}

const query = decodeQueryString(req.query.query as string)
const variables = decodeQueryString(req.query.variables as string)

res.send(
await renderLiquidTemplate(
graphiqlTemplate({
apiVersion,
apiVersions: [...apiVersions, 'unstable'],
appName,
appUrl,
key,
storeFqdn,
}),
{
url,
defaultQueries: [{query: defaultQuery}],
query,
variables,
},
),
)
// Read the built React index.html
const graphiqlAssetsDir = await findPathUp(joinPath('assets', 'graphiql'), {
type: 'directory',
cwd: moduleDirectory(import.meta.url),
})

if (!graphiqlAssetsDir) {
return res.status(404).send('GraphiQL assets not found')
}

const indexHtmlPath = joinPath(graphiqlAssetsDir, 'index.html')
let indexHtml = await readFile(indexHtmlPath)

// Build config object to inject (never include apiSecret or tokens)
const config = {
apiVersion,
apiVersions: [...apiVersions, 'unstable'],
appName,
appUrl,
storeFqdn,
baseUrl,
key: key ?? undefined,
query: query ?? undefined,
}

// Inject config script before </head>
// Escape < > & in JSON to prevent XSS when embedding in HTML script tags
// Use Unicode escapes so JavaScript correctly decodes them (HTML entities would break the query)
const safeJson = JSON.stringify(config).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026')
const configScript = `<script>window.__GRAPHIQL_CONFIG__ = ${safeJson};</script>`
indexHtml = indexHtml.replace('</head>', `${configScript}\n </head>`)

res.setHeader('Content-Type', 'text/html')
res.send(indexHtml)
})

app.use(bodyParser.json())
Expand Down
Loading
Loading