diff --git a/packages/app/package.json b/packages/app/package.json index 2bed8e7c94f..f435c98caf3 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -54,7 +54,6 @@ "@shopify/function-runner": "4.1.1", "@shopify/plugin-cloudflare": "3.87.0", "@shopify/polaris": "12.27.0", - "@shopify/polaris-icons": "8.11.1", "@shopify/theme": "3.87.0", "@shopify/theme-check-node": "3.23.0", "@shopify/toml-patch": "0.3.0", diff --git a/packages/app/project.json b/packages/app/project.json index a142717c91d..fbf7e04fced 100644 --- a/packages/app/project.json +++ b/packages/app/project.json @@ -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", diff --git a/packages/app/src/cli/services/dev/graphiql/server.ts b/packages/app/src/cli/services/dev/graphiql/server.ts index 30bda113e6e..4d52955d7c2 100644 --- a/packages/app/src/cli/services/dev/graphiql/server.ts +++ b/packages/app/src/cli/services/dev/graphiql/server.ts @@ -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' @@ -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' @@ -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 { return performActionWithRetryAfterRecovery( @@ -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'})) }) @@ -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 { @@ -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 + // 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, '\\u003e').replace(/&/g, '\\u0026') + const configScript = `` + indexHtml = indexHtml.replace('', `${configScript}\n `) + + res.setHeader('Content-Type', 'text/html') + res.send(indexHtml) }) app.use(bodyParser.json()) diff --git a/packages/app/src/cli/services/dev/graphiql/templates/graphiql.tsx b/packages/app/src/cli/services/dev/graphiql/templates/graphiql.tsx deleted file mode 100644 index a767c707433..00000000000 --- a/packages/app/src/cli/services/dev/graphiql/templates/graphiql.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import {platformAndArch} from '@shopify/cli-kit/node/os' -import React from 'react' -import {renderToStaticMarkup} from 'react-dom/server' -import {AppProvider, Badge, Banner, BlockStack, Box, Grid, InlineStack, Link, Select, Text} from '@shopify/polaris' -import {AlertCircleIcon, DisabledIcon, LinkIcon} from '@shopify/polaris-icons' - -const controlKey = platformAndArch().platform === 'darwin' ? 'MAC_COMMAND_KEY' : 'Ctrl' - -const graphiqlIntroMessage = ` -# Welcome to GraphiQL for the Shopify Admin API! If you've used -# GraphiQL before, you can jump to the next tab. -# -# GraphiQL is an in-browser tool for writing, validating, and -# testing GraphQL queries. -# -# Type queries into this side of the screen, and you will see intelligent -# typeaheads aware of the current GraphQL type schema and live syntax and -# validation errors highlighted within the text. -# -# GraphQL queries typically start with a "{" character. Lines that start -# with a # are ignored. -# -# Keyboard shortcuts: -# -# Prettify query: Shift-${controlKey}-P (or press the prettify button) -# -# Merge fragments: Shift-${controlKey}-M (or press the merge button) -# -# Run Query: ${controlKey}-Enter (or press the play button) -# -# Auto Complete: ${controlKey}-Space (or just start typing) -# -` - -export const defaultQuery = `query shopInfo { - shop { - name - url - myshopifyDomain - plan { - displayName - partnerDevelopment - shopifyPlus - } - } -} -`.replace(/\n/g, '\\n') - -interface GraphiQLTemplateOptions { - apiVersion: string - apiVersions: string[] - appName: string - appUrl: string - key?: string - storeFqdn: string -} - -export function graphiqlTemplate({ - apiVersion, - apiVersions, - appName, - appUrl, - key, - storeFqdn, -}: GraphiQLTemplateOptions): string { - return ` - - - GraphiQL - - - - - - - - - -
- ${renderToStaticMarkup( - -
- - - - - -
-
- Status: - - Running - -
-
- Status: - - App uninstalled - -
-
- Status: - - Disconnected - -
-
-
- API version: -