Skip to content
Merged
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
34 changes: 29 additions & 5 deletions bin/bundling/esbuild-plugin-graphiql-imports.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,47 @@
import { readFile } from 'fs/promises'

const createRequireStatement = /const require = createRequire\(import\.meta\.url\);?\r?\n/

const resolveGraphiQLAssetHelper = `function resolveGraphiQLAsset(asset) {
const {existsSync} = require('node:fs')
const {dirname, join, parse} = require('node:path')
const {fileURLToPath} = require('node:url')

for (
let directory = dirname(fileURLToPath(import.meta.url));
directory !== parse(directory).root;
directory = dirname(directory)
) {
const candidate = join(directory, 'assets', 'graphiql', asset)
if (existsSync(candidate)) return candidate
}

return require.resolve(\`@shopify/cli-kit/assets/graphiql/\${asset}\`)
}
`

const GraphiQLImportsPlugin = {
name: 'GraphiQLImportsPlugin',
setup(build) {
// GraphiQL uses require.resolve with paths that won't work with esbuild
// We need to replace them with valid paths
// graphiql/server.ts uses require.resolve('@shopify/app/assets/...'). The bundled CLI does not ship
// @shopify/app as a dependency; assets are copied to dist/assets. Rewrite to paths relative to bundled
// command files under dist/cli/commands/** (e.g. app/dev.js -> ../../../assets/graphiql/...).
// graphiql/server.ts uses require.resolve('@shopify/cli-kit/assets/...'). The bundled CLI does not ship
// @shopify/cli-kit as a dependency; assets are copied to dist/assets. Rewrite to a resolver that works whether
// esbuild emits the GraphiQL server into a top-level shared chunk or a nested command file.
build.onLoad({filter: /[/\\]graphiql[/\\]server\.[cm]?[jt]s$/}, async (args) => {
const contents = await readFile(args.path, 'utf8')
if (!createRequireStatement.test(contents)) {
throw new Error(`Could not find the GraphiQL server createRequire statement in ${args.path}`)
}
// When `contents` is returned, esbuild defaults the loader to `js` unless set — TypeScript would then
// fail to parse (e.g. "Expected ')' but found ':'" on parameter type annotations).
const loader = args.path.endsWith('.tsx') ? 'tsx' : 'ts'
return {
loader,
contents: contents
.replace('@shopify/app/assets/graphiql/favicon.ico', '../../../assets/graphiql/favicon.ico')
.replace('@shopify/app/assets/graphiql/style.css', '../../../assets/graphiql/style.css'),
.replace(createRequireStatement, (match) => `${match}\n${resolveGraphiQLAssetHelper}`)
.replace("require.resolve('@shopify/cli-kit/assets/graphiql/favicon.ico')", "resolveGraphiQLAsset('favicon.ico')")
.replace("require.resolve('@shopify/cli-kit/assets/graphiql/style.css')", "resolveGraphiQLAsset('style.css')"),
Comment thread
gonzaloriestra marked this conversation as resolved.
}
})
},
Expand Down
34 changes: 0 additions & 34 deletions docs-shopify.dev/commands/config-autoupgrade-off.doc.ts

This file was deleted.

34 changes: 0 additions & 34 deletions docs-shopify.dev/commands/config-autoupgrade-on.doc.ts

This file was deleted.

34 changes: 0 additions & 34 deletions docs-shopify.dev/commands/config-autoupgrade-status.doc.ts

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

4 changes: 0 additions & 4 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@
"@shopify/cli-kit": "4.1.0",
"@shopify/plugin-cloudflare": "4.1.0",
"@shopify/organizations": "4.1.0",
"@shopify/polaris": "12.27.0",
"@shopify/polaris-icons": "8.11.1",
"@shopify/theme": "4.1.0",
"@shopify/theme-check-node": "3.26.1",
"@shopify/toml-patch": "0.3.0",
Expand All @@ -80,15 +78,13 @@
"prettier": "3.8.4",
"proper-lockfile": "4.1.2",
"react": "19.2.4",
"react-dom": "19.2.4",
"which": "4.0.0",
"ws": "8.21.0"
},
"devDependencies": {
"@types/diff": "^5.0.3",
"@types/proper-lockfile": "4.1.4",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/which": "3.0.4",
"@types/ws": "^8.5.13",
"@vitest/coverage-istanbul": "^3.1.4"
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/cli/services/dev/processes/graphiql.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {BaseProcess, DevProcessFunction} from './types.js'
import {setupGraphiQLServer} from '../graphiql/server.js'
import {setupGraphiQLServer} from '@shopify/cli-kit/node/graphiql/server'

interface GraphiQLServerProcessOptions {
appName: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {pushUpdatesForDraftableExtensions} from './draftable-extension.js'
import {pushUpdatesForDevSession} from './dev-session/dev-session-process.js'
import {runThemeAppExtensionsServer} from './theme-app-extension.js'
import {launchAppWatcher} from './app-watcher-process.js'
import {resolveGraphiQLKey} from '../graphiql/server.js'
import {
testAppAccessConfigExtension,
testAppConfigExtensions,
Expand All @@ -31,6 +30,7 @@ import {ensureDeploymentIdsPresence} from '../../context/identifiers.js'
import {DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js'
import {AppEventWatcher} from '../app-events/app-event-watcher.js'
import * as loader from '../../../models/app/loader.js'
import {resolveGraphiQLKey} from '@shopify/cli-kit/node/graphiql/server'
import {describe, test, expect, beforeEach, vi} from 'vitest'
import {ensureAuthenticatedAdmin, ensureAuthenticatedStorefront} from '@shopify/cli-kit/node/session'
import {Config} from '@oclif/core'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {DevSessionProcess, setupDevSessionProcess} from './dev-session/dev-sessi
import {AppLogsSubscribeProcess, setupAppLogsPollingProcess} from './app-logs-polling.js'
import {AppWatcherProcess, setupAppWatcherProcess} from './app-watcher-process.js'
import {DevSessionStatusManager} from './dev-session/dev-session-status-manager.js'
import {resolveGraphiQLKey} from '../graphiql/server.js'
import {environmentVariableNames} from '../../../constants.js'
import {AppLinkedInterface, getAppScopes, WebType} from '../../../models/app/app.js'

Expand All @@ -21,6 +20,7 @@ import {ApplicationURLs} from '../urls.js'
import {DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js'
import {AppEventWatcher} from '../app-events/app-event-watcher.js'
import {reloadApp} from '../../../models/app/loader.js'
import {resolveGraphiQLKey} from '@shopify/cli-kit/node/graphiql/server'
import {getAvailableTCPPort} from '@shopify/cli-kit/node/tcp'
import {isTruthy} from '@shopify/cli-kit/node/context/utilities'
import {firstPartyDev} from '@shopify/cli-kit/node/context/local'
Expand Down
5 changes: 5 additions & 0 deletions packages/cli-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@
"@graphql-typed-document-node/core": "3.2.0",
"@iarna/toml": "2.2.5",
"@oclif/core": "4.11.4",
"@shopify/polaris": "12.27.0",
"@shopify/polaris-icons": "8.11.1",
"@shopify/toml-patch": "0.3.0",
"@opentelemetry/api": "1.9.1",
"@opentelemetry/core": "1.30.1",
Expand Down Expand Up @@ -134,6 +136,7 @@
"gradient-string": "2.0.2",
"graphql": "16.14.2",
"graphql-request": "6.1.0",
"h3": "1.15.11",
"ignore": "6.0.2",
"ink": "6.8.0",
"is-executable": "2.0.2",
Expand All @@ -152,6 +155,7 @@
"pathe": "1.1.2",
"react": "19.2.4",
"semver": "7.8.4",
"react-dom": "19.2.4",
"stacktracey": "2.2.0",
"strip-ansi": "7.2.0",
"supports-hyperlinks": "3.2.0",
Expand All @@ -165,6 +169,7 @@
"@types/gradient-string": "^1.1.2",
"@types/lodash": "4.17.24",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/semver": "^7.5.2",
"@types/which": "3.0.4",
"@vitest/coverage-istanbul": "^3.1.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import {defaultQuery, graphiqlTemplate} from './templates/graphiql.js'
import {unauthorizedTemplate} from './templates/unauthorized.js'
import {filterCustomHeaders} from './utilities.js'
import {performActionWithRetryAfterRecovery} from '../../common/retry.js'
import {CLI_KIT_VERSION} from '../../common/version.js'
import {AbortError} from '../error.js'
import {adminUrl, supportedApiVersions} from '../api/admin.js'
import {fetch} from '../http.js'
import {renderLiquidTemplate} from '../liquid.js'
import {outputDebug} from '../output.js'
import {
createApp,
createRouter,
Expand All @@ -13,13 +20,6 @@ import {
setResponseStatus,
toNodeListener,
} from 'h3'
import {performActionWithRetryAfterRecovery} from '@shopify/cli-kit/common/retry'
import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version'
import {AbortError} from '@shopify/cli-kit/node/error'
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 {createHmac} from 'crypto'
import {createServer, Server} from 'http'
import {readFileSync} from 'fs'
Expand All @@ -30,6 +30,10 @@ import {createRequire} from 'module'
* Derives a deterministic GraphiQL authentication key from the app's API secret and store FQDN.
* The key is stable across dev server restarts (so browser tabs survive restarts)
* but is not guessable without the app secret.
*
* @param apiSecret - The Partners app's client secret used as the HMAC key.
* @param storeFqdn - The myshopify.com domain the GraphiQL session targets.
* @returns A 64-character hex string suitable for use as the `?key=` query param.
*/
export function deriveGraphiQLKey(apiSecret: string, storeFqdn: string): string {
return createHmac('sha256', apiSecret).update(`graphiql:${storeFqdn}`).digest('hex')
Expand All @@ -38,6 +42,11 @@ export function deriveGraphiQLKey(apiSecret: string, storeFqdn: string): string
/**
* Resolves the GraphiQL authentication key. Uses the explicitly provided key
* if non-empty, otherwise derives one deterministically from the app secret.
*
* @param providedKey - An explicit key supplied by the caller; takes precedence when non-empty.
* @param apiSecret - The Partners app's client secret, used to derive a stable key as a fallback.
* @param storeFqdn - The myshopify.com domain the GraphiQL session targets.
* @returns The resolved key.
*/
export function resolveGraphiQLKey(providedKey: string | undefined, apiSecret: string, storeFqdn: string): string {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional: empty string after trim should fall through to deriveGraphiQLKey
Expand All @@ -63,16 +72,18 @@ interface SetupGraphiQLServerOptions {
storeFqdn: string
}

export function setupGraphiQLServer({
stdout,
port,
appName,
appUrl,
apiKey,
apiSecret,
key: providedKey,
storeFqdn,
}: SetupGraphiQLServerOptions): Server {
/**
* Starts a local HTTP server that hosts the GraphiQL UI and proxies requests to the
* Admin API for the configured store. The server uses the OAuth `client_credentials`
* grant with the supplied `apiKey` / `apiSecret` to mint and refresh access tokens
* on the fly.
*
* @param options - Configuration for the server, including the target store, the
* Partners app credentials, and the local port to bind to.
* @returns The underlying Node `http.Server` instance, already listening on `options.port`.
*/
export function setupGraphiQLServer(options: SetupGraphiQLServerOptions): Server {
const {stdout, port, appName, appUrl, apiKey, apiSecret, key: providedKey, storeFqdn} = options
// Always require an authentication key. If not explicitly provided, derive one
// deterministically from apiSecret + storeFqdn so the key is stable across restarts
// (browser tabs survive dev server restarts) but not guessable without the app secret.
Expand Down Expand Up @@ -120,9 +131,9 @@ export function setupGraphiQLServer({
)
}

const faviconPath = require.resolve('@shopify/app/assets/graphiql/favicon.ico')
const faviconPath = require.resolve('@shopify/cli-kit/assets/graphiql/favicon.ico')
const faviconContent = readFileSync(faviconPath)
const stylePath = require.resolve('@shopify/app/assets/graphiql/style.css')
const stylePath = require.resolve('@shopify/cli-kit/assets/graphiql/style.css')
const styleContent = readFileSync(stylePath, 'utf8')

app.use(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {platformAndArch} from '@shopify/cli-kit/node/os'
import {platformAndArch} from '../../os.js'
import React from 'react'
import {renderToStaticMarkup} from 'react-dom/server'
import {AppProvider, Badge, Banner, BlockStack, Box, Grid, InlineStack, Link, Select, Text} from '@shopify/polaris'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* Headers that should NOT be forwarded from the GraphiQL client to the Admin API.
* These include:
* - Hop-by-hop headers (RFC 7230) that are connection-specific
* - Browser-specific headers that are not relevant to API requests
* - Headers the proxy sets itself (auth, content-type, etc.)
* - Hop-by-hop headers (RFC 7230) that are connection-specific.
* - Browser-specific headers that are not relevant to API requests.
* - Headers the proxy sets itself (auth, content-type, etc.).
*/
const BLOCKED_HEADERS = new Set([
// Hop-by-hop headers (RFC 7230 Section 6.1)
Expand All @@ -30,6 +30,9 @@ const BLOCKED_HEADERS = new Set([
/**
* Filters request headers to extract only custom headers that are safe to forward.
* Blocked headers and non-string values are excluded.
*
* @param headers - The raw incoming request headers.
* @returns The subset of headers that are safe to forward to the Admin API.
*/
export function filterCustomHeaders(headers: {[key: string]: string | string[] | undefined}): {[key: string]: string} {
const customHeaders: {[key: string]: string} = {}
Expand Down
Loading
Loading