From 9dfd24d56b1dd187c8562c0cd998e7d94467ab1c Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Thu, 23 Apr 2026 12:51:21 +0200 Subject: [PATCH 1/8] chore: extract GraphiQL server to @shopify/cli-kit Move the GraphiQL HTTP proxy and its templates/assets out of @shopify/app and into @shopify/cli-kit so other packages (notably @shopify/store) can reuse it. This commit is purely mechanical: - Files moved to packages/cli-kit/src/public/node/graphiql/ and packages/cli-kit/assets/graphiql/. - Imports inside the moved server now use relative paths to other cli-kit modules instead of cross-package @shopify/cli-kit/* imports. - Asset resolution switched to @shopify/cli-kit/assets/graphiql/*. - @shopify/app consumers updated to import from @shopify/cli-kit/node/graphiql/server. - cli-kit package.json gains h3, @shopify/polaris, @shopify/polaris-icons, and react-dom (with @types/react-dom in devDependencies). Behavior is unchanged. Existing graphiql server.test.ts and utilities.test.ts move alongside the source. --- .../esbuild-plugin-graphiql-imports.js | 34 ++++++++++-- packages/app/package.json | 4 -- .../cli/services/dev/processes/graphiql.ts | 2 +- .../dev/processes/setup-dev-processes.test.ts | 2 +- .../dev/processes/setup-dev-processes.ts | 2 +- .../assets/graphiql/favicon.ico | Bin .../assets/graphiql/style.css | 0 packages/cli-kit/package.json | 5 ++ .../src/public/node}/graphiql/server.test.ts | 0 .../src/public/node}/graphiql/server.ts | 49 +++++++++++------- .../node}/graphiql/templates/graphiql.tsx | 2 +- .../node}/graphiql/templates/unauthorized.tsx | 0 .../public/node}/graphiql/utilities.test.ts | 0 .../src/public/node}/graphiql/utilities.ts | 9 ++-- pnpm-lock.yaml | 27 +++++----- 15 files changed, 89 insertions(+), 47 deletions(-) rename packages/{app => cli-kit}/assets/graphiql/favicon.ico (100%) rename packages/{app => cli-kit}/assets/graphiql/style.css (100%) rename packages/{app/src/cli/services/dev => cli-kit/src/public/node}/graphiql/server.test.ts (100%) rename packages/{app/src/cli/services/dev => cli-kit/src/public/node}/graphiql/server.ts (82%) rename packages/{app/src/cli/services/dev => cli-kit/src/public/node}/graphiql/templates/graphiql.tsx (99%) rename packages/{app/src/cli/services/dev => cli-kit/src/public/node}/graphiql/templates/unauthorized.tsx (100%) rename packages/{app/src/cli/services/dev => cli-kit/src/public/node}/graphiql/utilities.test.ts (100%) rename packages/{app/src/cli/services/dev => cli-kit/src/public/node}/graphiql/utilities.ts (80%) diff --git a/bin/bundling/esbuild-plugin-graphiql-imports.js b/bin/bundling/esbuild-plugin-graphiql-imports.js index e8d91797670..bd201644236 100644 --- a/bin/bundling/esbuild-plugin-graphiql-imports.js +++ b/bin/bundling/esbuild-plugin-graphiql-imports.js @@ -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')"), } }) }, diff --git a/packages/app/package.json b/packages/app/package.json index c2d7fec0cc9..486e0ee5db3 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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", @@ -80,7 +78,6 @@ "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" }, @@ -88,7 +85,6 @@ "@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" diff --git a/packages/app/src/cli/services/dev/processes/graphiql.ts b/packages/app/src/cli/services/dev/processes/graphiql.ts index 6b00d808c55..06776b27e78 100644 --- a/packages/app/src/cli/services/dev/processes/graphiql.ts +++ b/packages/app/src/cli/services/dev/processes/graphiql.ts @@ -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 diff --git a/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts b/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts index aeb6702a4ea..06ad0ad0f00 100644 --- a/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts +++ b/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts @@ -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, @@ -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' diff --git a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts index 0c6ca0b2f6d..9feb2b29f07 100644 --- a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts +++ b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts @@ -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' @@ -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' diff --git a/packages/app/assets/graphiql/favicon.ico b/packages/cli-kit/assets/graphiql/favicon.ico similarity index 100% rename from packages/app/assets/graphiql/favicon.ico rename to packages/cli-kit/assets/graphiql/favicon.ico diff --git a/packages/app/assets/graphiql/style.css b/packages/cli-kit/assets/graphiql/style.css similarity index 100% rename from packages/app/assets/graphiql/style.css rename to packages/cli-kit/assets/graphiql/style.css diff --git a/packages/cli-kit/package.json b/packages/cli-kit/package.json index 2f6720eab3d..c5fac1272f1 100644 --- a/packages/cli-kit/package.json +++ b/packages/cli-kit/package.json @@ -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", @@ -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", @@ -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", @@ -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", diff --git a/packages/app/src/cli/services/dev/graphiql/server.test.ts b/packages/cli-kit/src/public/node/graphiql/server.test.ts similarity index 100% rename from packages/app/src/cli/services/dev/graphiql/server.test.ts rename to packages/cli-kit/src/public/node/graphiql/server.test.ts diff --git a/packages/app/src/cli/services/dev/graphiql/server.ts b/packages/cli-kit/src/public/node/graphiql/server.ts similarity index 82% rename from packages/app/src/cli/services/dev/graphiql/server.ts rename to packages/cli-kit/src/public/node/graphiql/server.ts index 4ee8646d38a..4b83473910d 100644 --- a/packages/app/src/cli/services/dev/graphiql/server.ts +++ b/packages/cli-kit/src/public/node/graphiql/server.ts @@ -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, @@ -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' @@ -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') @@ -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 @@ -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. @@ -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( diff --git a/packages/app/src/cli/services/dev/graphiql/templates/graphiql.tsx b/packages/cli-kit/src/public/node/graphiql/templates/graphiql.tsx similarity index 99% rename from packages/app/src/cli/services/dev/graphiql/templates/graphiql.tsx rename to packages/cli-kit/src/public/node/graphiql/templates/graphiql.tsx index a14e1e0c9d6..edc3638df34 100644 --- a/packages/app/src/cli/services/dev/graphiql/templates/graphiql.tsx +++ b/packages/cli-kit/src/public/node/graphiql/templates/graphiql.tsx @@ -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' diff --git a/packages/app/src/cli/services/dev/graphiql/templates/unauthorized.tsx b/packages/cli-kit/src/public/node/graphiql/templates/unauthorized.tsx similarity index 100% rename from packages/app/src/cli/services/dev/graphiql/templates/unauthorized.tsx rename to packages/cli-kit/src/public/node/graphiql/templates/unauthorized.tsx diff --git a/packages/app/src/cli/services/dev/graphiql/utilities.test.ts b/packages/cli-kit/src/public/node/graphiql/utilities.test.ts similarity index 100% rename from packages/app/src/cli/services/dev/graphiql/utilities.test.ts rename to packages/cli-kit/src/public/node/graphiql/utilities.test.ts diff --git a/packages/app/src/cli/services/dev/graphiql/utilities.ts b/packages/cli-kit/src/public/node/graphiql/utilities.ts similarity index 80% rename from packages/app/src/cli/services/dev/graphiql/utilities.ts rename to packages/cli-kit/src/public/node/graphiql/utilities.ts index d900e443093..85573897f08 100644 --- a/packages/app/src/cli/services/dev/graphiql/utilities.ts +++ b/packages/cli-kit/src/public/node/graphiql/utilities.ts @@ -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) @@ -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} = {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b11d7cfa4b7..d45dc68ce1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,12 +163,6 @@ importers: '@shopify/plugin-cloudflare': specifier: 4.1.0 version: link:../plugin-cloudflare - '@shopify/polaris': - specifier: 12.27.0 - version: 12.27.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@shopify/polaris-icons': - specifier: 8.11.1 - version: 8.11.1(react@19.2.4) '@shopify/theme': specifier: 4.1.0 version: link:../theme @@ -211,9 +205,6 @@ importers: react: specifier: 19.2.4 version: 19.2.4 - react-dom: - specifier: 19.2.4 - version: 19.2.4(react@19.2.4) which: specifier: 4.0.0 version: 4.0.0 @@ -230,9 +221,6 @@ importers: '@types/react': specifier: 18.3.12 version: 18.3.12 - '@types/react-dom': - specifier: ^19.0.0 - version: 19.2.3(@types/react@18.3.12) '@types/which': specifier: 3.0.4 version: 3.0.4 @@ -327,6 +315,12 @@ importers: '@opentelemetry/sdk-metrics': specifier: 1.30.1 version: 1.30.1(@opentelemetry/api@1.9.1) + '@shopify/polaris': + specifier: 12.27.0 + version: 12.27.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@shopify/polaris-icons': + specifier: 8.11.1 + version: 8.11.1(react@19.2.4) '@shopify/toml-patch': specifier: 0.3.0 version: 0.3.0 @@ -393,6 +387,9 @@ importers: graphql-request: specifier: 6.1.0 version: 6.1.0(graphql@16.14.2) + h3: + specifier: 1.15.11 + version: 1.15.11 ignore: specifier: 6.0.2 version: 6.0.2 @@ -444,6 +441,9 @@ importers: react: specifier: 19.2.4 version: 19.2.4 + react-dom: + specifier: 19.2.4 + version: 19.2.4(react@19.2.4) semver: specifier: 7.8.4 version: 7.8.4 @@ -481,6 +481,9 @@ importers: '@types/react': specifier: 18.3.12 version: 18.3.12 + '@types/react-dom': + specifier: ^19.0.0 + version: 19.2.3(@types/react@18.3.12) '@types/semver': specifier: ^7.5.2 version: 7.7.1 From f9d07f6dae7f8bdbc5f8cac4dba5d6b70d30786a Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Mon, 8 Jun 2026 12:52:18 +0200 Subject: [PATCH 2/8] Fix Vitest 4 compatibility --- packages/cli-kit/src/public/node/base-command.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/cli-kit/src/public/node/base-command.ts b/packages/cli-kit/src/public/node/base-command.ts index 2cb2cf4b5a2..01dec899221 100644 --- a/packages/cli-kit/src/public/node/base-command.ts +++ b/packages/cli-kit/src/public/node/base-command.ts @@ -185,7 +185,7 @@ This flag is required in non-interactive terminal environments, such as a CI env ]) // Report successful application of the environment. - reportEnvironmentApplication( + await reportEnvironmentApplication( noDefaultsResult.flags, result.flags, isDefaultEnvironment ? 'default' : (environments[0] as string), @@ -244,7 +244,7 @@ function reportEnvironmentApplication< flagsWithEnvironments: ParserOutput['flags'], environmentName: string, environment: JsonMap, -): void { +): Promise { const changes: JsonMap = {} for (const [name, value] of Object.entries(flagsWithEnvironments)) { const userSpecifiedThisFlag = Object.prototype.hasOwnProperty.call(noDefaultsFlags, name) @@ -254,11 +254,10 @@ function reportEnvironmentApplication< changes[name] = valueToReport } } - if (Object.keys(changes).length === 0) return + if (Object.keys(changes).length === 0) return Promise.resolve() const items = Object.entries(changes).map(([name, value]) => `${name}: ${value}`) - // eslint-disable-next-line no-void - void import('./ui.js').then(({renderInfo}) => { + return import('./ui.js').then(({renderInfo}) => { renderInfo({ headline: ['Using applicable flags from', {userInput: environmentName}, 'environment:'], body: [{list: {items}}], From a137bc19578d16520e4ec1bab26c4a05b822babb Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Mon, 8 Jun 2026 13:42:57 +0200 Subject: [PATCH 3/8] Narrow Vitest 4 compatibility fixes --- packages/cli-kit/src/private/node/ui.tsx | 5 +---- packages/cli-kit/src/public/node/base-command.ts | 9 +++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/cli-kit/src/private/node/ui.tsx b/packages/cli-kit/src/private/node/ui.tsx index 7e53ea3e04d..d7cc567d0aa 100644 --- a/packages/cli-kit/src/private/node/ui.tsx +++ b/packages/cli-kit/src/private/node/ui.tsx @@ -65,10 +65,7 @@ export function renderOnce(element: JSX.Element, {logLevel = 'info', renderOptio } export async function render(element: JSX.Element, options?: RenderOptions) { - const {waitUntilExit} = inkRender({element}, { - patchConsole: !isUnitTest(), - ...options, - }) + const {waitUntilExit} = inkRender({element}, options) await waitUntilExit() } diff --git a/packages/cli-kit/src/public/node/base-command.ts b/packages/cli-kit/src/public/node/base-command.ts index 01dec899221..2cb2cf4b5a2 100644 --- a/packages/cli-kit/src/public/node/base-command.ts +++ b/packages/cli-kit/src/public/node/base-command.ts @@ -185,7 +185,7 @@ This flag is required in non-interactive terminal environments, such as a CI env ]) // Report successful application of the environment. - await reportEnvironmentApplication( + reportEnvironmentApplication( noDefaultsResult.flags, result.flags, isDefaultEnvironment ? 'default' : (environments[0] as string), @@ -244,7 +244,7 @@ function reportEnvironmentApplication< flagsWithEnvironments: ParserOutput['flags'], environmentName: string, environment: JsonMap, -): Promise { +): void { const changes: JsonMap = {} for (const [name, value] of Object.entries(flagsWithEnvironments)) { const userSpecifiedThisFlag = Object.prototype.hasOwnProperty.call(noDefaultsFlags, name) @@ -254,10 +254,11 @@ function reportEnvironmentApplication< changes[name] = valueToReport } } - if (Object.keys(changes).length === 0) return Promise.resolve() + if (Object.keys(changes).length === 0) return const items = Object.entries(changes).map(([name, value]) => `${name}: ${value}`) - return import('./ui.js').then(({renderInfo}) => { + // eslint-disable-next-line no-void + void import('./ui.js').then(({renderInfo}) => { renderInfo({ headline: ['Using applicable flags from', {userInput: environmentName}, 'environment:'], body: [{list: {items}}], From 5faa33674a28728089b5d4ca85109b7a7c08e5f7 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Mon, 8 Jun 2026 14:01:24 +0200 Subject: [PATCH 4/8] Fix Vitest 4 unit test compatibility --- packages/app/src/cli/services/dev/ui.test.tsx | 7 +++++++ packages/app/src/cli/utilities/mkcert.test.ts | 3 +++ 2 files changed, 10 insertions(+) diff --git a/packages/app/src/cli/services/dev/ui.test.tsx b/packages/app/src/cli/services/dev/ui.test.tsx index fceb2eebf76..fdb470bf0ef 100644 --- a/packages/app/src/cli/services/dev/ui.test.tsx +++ b/packages/app/src/cli/services/dev/ui.test.tsx @@ -9,6 +9,13 @@ import {AbortController} from '@shopify/cli-kit/node/abort' import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system' vi.mock('@shopify/cli-kit/node/system') +vi.mock('@shopify/cli-kit/node/ui', () => ({ + render: vi.fn(async (element: {type?: unknown; props?: unknown}) => { + if (typeof element.type === 'function') { + element.type(element.props, undefined) + } + }), +})) vi.mock('./ui/components/Dev.js') vi.mock('../context.js') vi.mock('./ui/components/DevSessionUI.js') diff --git a/packages/app/src/cli/utilities/mkcert.test.ts b/packages/app/src/cli/utilities/mkcert.test.ts index 02b396cb396..d5c6e00fddb 100644 --- a/packages/app/src/cli/utilities/mkcert.test.ts +++ b/packages/app/src/cli/utilities/mkcert.test.ts @@ -22,6 +22,9 @@ vi.mock('@shopify/cli-kit/node/ui', async () => { const actual = await vi.importActual('@shopify/cli-kit/node/ui') return { ...actual, + renderTasks: vi.fn(async (tasks: {task: () => Promise}[]) => { + await Promise.all(tasks.map(async (task) => task.task())) + }), renderWarning: vi.fn(), keypress: vi.fn(), } From 93e72323e89da88069b7a1fe7fb198d51e485c4e Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Mon, 8 Jun 2026 14:08:55 +0200 Subject: [PATCH 5/8] Disable Ink console patching in unit tests --- packages/app/src/cli/services/dev/ui.test.tsx | 7 ------- packages/app/src/cli/utilities/mkcert.test.ts | 3 --- packages/cli-kit/src/private/node/ui.tsx | 5 ++++- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/app/src/cli/services/dev/ui.test.tsx b/packages/app/src/cli/services/dev/ui.test.tsx index fdb470bf0ef..fceb2eebf76 100644 --- a/packages/app/src/cli/services/dev/ui.test.tsx +++ b/packages/app/src/cli/services/dev/ui.test.tsx @@ -9,13 +9,6 @@ import {AbortController} from '@shopify/cli-kit/node/abort' import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system' vi.mock('@shopify/cli-kit/node/system') -vi.mock('@shopify/cli-kit/node/ui', () => ({ - render: vi.fn(async (element: {type?: unknown; props?: unknown}) => { - if (typeof element.type === 'function') { - element.type(element.props, undefined) - } - }), -})) vi.mock('./ui/components/Dev.js') vi.mock('../context.js') vi.mock('./ui/components/DevSessionUI.js') diff --git a/packages/app/src/cli/utilities/mkcert.test.ts b/packages/app/src/cli/utilities/mkcert.test.ts index d5c6e00fddb..02b396cb396 100644 --- a/packages/app/src/cli/utilities/mkcert.test.ts +++ b/packages/app/src/cli/utilities/mkcert.test.ts @@ -22,9 +22,6 @@ vi.mock('@shopify/cli-kit/node/ui', async () => { const actual = await vi.importActual('@shopify/cli-kit/node/ui') return { ...actual, - renderTasks: vi.fn(async (tasks: {task: () => Promise}[]) => { - await Promise.all(tasks.map(async (task) => task.task())) - }), renderWarning: vi.fn(), keypress: vi.fn(), } diff --git a/packages/cli-kit/src/private/node/ui.tsx b/packages/cli-kit/src/private/node/ui.tsx index d7cc567d0aa..7e53ea3e04d 100644 --- a/packages/cli-kit/src/private/node/ui.tsx +++ b/packages/cli-kit/src/private/node/ui.tsx @@ -65,7 +65,10 @@ export function renderOnce(element: JSX.Element, {logLevel = 'info', renderOptio } export async function render(element: JSX.Element, options?: RenderOptions) { - const {waitUntilExit} = inkRender({element}, options) + const {waitUntilExit} = inkRender({element}, { + patchConsole: !isUnitTest(), + ...options, + }) await waitUntilExit() } From 856f81489e76c743a428ba8b61bb765fc79f5879 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Mon, 8 Jun 2026 18:24:48 +0300 Subject: [PATCH 6/8] Extract the shared --store flag out of the store auth and execute commands The `store auth` and `store execute` commands each defined their own `--store` flag inline (char `s`, `SHOPIFY_FLAG_STORE`, normalized via `normalizeStoreFqdn`, required). Lift the shared definition into `packages/store/src/cli/flags.ts` as `storeFlags.store` and have both commands consume it. Behavior-preserving except that the flag's help text is now the generic "The myshopify.com domain of the store." (previously each command appended its own "to authenticate against." / "to execute against." suffix); manifest, README, and reference docs are regenerated to match. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs-shopify.dev/generated/generated_docs_data_v2.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index 3c9205db032..57a6f11057b 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -6195,4 +6195,4 @@ "value": "export interface version {\n\n}" } } -} \ No newline at end of file +} From c9cc3b9b28155cf6a8fddfdf7d647e57059578b3 Mon Sep 17 00:00:00 2001 From: Josh Faigan Date: Thu, 4 Jun 2026 16:31:19 -0400 Subject: [PATCH 7/8] fix(theme): validate host header for app extension dev server --- .../theme-environment/host-validation.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/theme/src/cli/utilities/theme-environment/host-validation.ts b/packages/theme/src/cli/utilities/theme-environment/host-validation.ts index e9b4ce04b32..d4908753480 100644 --- a/packages/theme/src/cli/utilities/theme-environment/host-validation.ts +++ b/packages/theme/src/cli/utilities/theme-environment/host-validation.ts @@ -8,6 +8,7 @@ function createAllowedHostsSet(host: string, port: number): Set { allowedHosts.add(`${normalizedHost}${portSuffix}`) + // When binding to localhost variants or 0.0.0.0, allow all localhost forms const localhostVariants = ['localhost', '127.0.0.1', '::1', '0.0.0.0'] if (localhostVariants.includes(normalizedHost)) { allowedHosts.add(`localhost${portSuffix}`) @@ -33,9 +34,19 @@ function createAllowedHostsSet(host: string, port: number): Set { function normalizeHostHeader(hostHeader: string | undefined): string | undefined { if (!hostHeader) return undefined + // Lowercase, then strip trailing dot before port (or at end for plain hostname). + // IPv6 brackets: trailing dot would be after `]`, e.g. [::1].:9292 return hostHeader.toLowerCase().replace(/\.(?=:\d|$)/, '') } +/** + * Creates an h3 event handler that validates the request's Host header + * against an allowlist of configured host/port and localhost variants. + * + * Used to mitigate DNS rebinding attacks on local dev servers. + * + * Returns a 400 Bad Request when the Host header is missing or not in the allowlist. + */ export function createHostValidationHandler(host: string, port: number) { const allowedHosts = createAllowedHostsSet(host, port) @@ -46,7 +57,11 @@ export function createHostValidationHandler(host: string, port: number) { if (!normalizedHost || !allowedHosts.has(normalizedHost)) { return sendError( event, - createError({statusCode: 400, statusMessage: 'Bad Request', message: 'Invalid Host header'}), + createError({ + statusCode: 400, + statusMessage: 'Bad Request', + message: 'Invalid Host header', + }), ) } }) From 2e149514fffda522275653872a6dcb8d11519e79 Mon Sep 17 00:00:00 2001 From: Josh Faigan Date: Fri, 5 Jun 2026 12:07:39 -0400 Subject: [PATCH 8/8] fix(theme): allow LAN access when theme dev binds to a wildcard host --- .../commands/config-autoupgrade-off.doc.ts | 34 ------------------- .../commands/config-autoupgrade-on.doc.ts | 34 ------------------- .../commands/config-autoupgrade-status.doc.ts | 34 ------------------- .../config-autoupgrade-off.example.sh | 1 - .../examples/config-autoupgrade-on.example.sh | 1 - .../config-autoupgrade-status.example.sh | 1 - .../generated/generated_docs_data_v2.json | 2 +- .../theme-environment/host-validation.ts | 17 +--------- 8 files changed, 2 insertions(+), 122 deletions(-) delete mode 100644 docs-shopify.dev/commands/config-autoupgrade-off.doc.ts delete mode 100644 docs-shopify.dev/commands/config-autoupgrade-on.doc.ts delete mode 100644 docs-shopify.dev/commands/config-autoupgrade-status.doc.ts delete mode 100644 docs-shopify.dev/commands/examples/config-autoupgrade-off.example.sh delete mode 100644 docs-shopify.dev/commands/examples/config-autoupgrade-on.example.sh delete mode 100644 docs-shopify.dev/commands/examples/config-autoupgrade-status.example.sh diff --git a/docs-shopify.dev/commands/config-autoupgrade-off.doc.ts b/docs-shopify.dev/commands/config-autoupgrade-off.doc.ts deleted file mode 100644 index 75b62ff3224..00000000000 --- a/docs-shopify.dev/commands/config-autoupgrade-off.doc.ts +++ /dev/null @@ -1,34 +0,0 @@ -// This is an autogenerated file. Don't edit this file manually. -import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs' - -const data: ReferenceEntityTemplateSchema = { - name: 'config autoupgrade off', - description: `Disable automatic upgrades for Shopify CLI. - - When auto-upgrade is disabled, Shopify CLI won't automatically update. Run \`shopify upgrade\` to update manually. - - To enable auto-upgrade, run \`shopify config autoupgrade on\`. -`, - overviewPreviewDescription: `Disable automatic upgrades for Shopify CLI.`, - type: 'command', - isVisualComponent: false, - defaultExample: { - codeblock: { - tabs: [ - { - title: 'config autoupgrade off', - code: './examples/config-autoupgrade-off.example.sh', - language: 'bash', - }, - ], - title: 'config autoupgrade off', - }, - }, - definitions: [ - ], - category: 'general commands', - related: [ - ], -} - -export default data \ No newline at end of file diff --git a/docs-shopify.dev/commands/config-autoupgrade-on.doc.ts b/docs-shopify.dev/commands/config-autoupgrade-on.doc.ts deleted file mode 100644 index acc5bb39026..00000000000 --- a/docs-shopify.dev/commands/config-autoupgrade-on.doc.ts +++ /dev/null @@ -1,34 +0,0 @@ -// This is an autogenerated file. Don't edit this file manually. -import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs' - -const data: ReferenceEntityTemplateSchema = { - name: 'config autoupgrade on', - description: `Enable automatic upgrades for Shopify CLI. - - When auto-upgrade is enabled, Shopify CLI automatically updates to the latest version once per day. Major version upgrades are skipped and must be done manually. - - To disable auto-upgrade, run \`shopify config autoupgrade off\`. -`, - overviewPreviewDescription: `Enable automatic upgrades for Shopify CLI.`, - type: 'command', - isVisualComponent: false, - defaultExample: { - codeblock: { - tabs: [ - { - title: 'config autoupgrade on', - code: './examples/config-autoupgrade-on.example.sh', - language: 'bash', - }, - ], - title: 'config autoupgrade on', - }, - }, - definitions: [ - ], - category: 'general commands', - related: [ - ], -} - -export default data \ No newline at end of file diff --git a/docs-shopify.dev/commands/config-autoupgrade-status.doc.ts b/docs-shopify.dev/commands/config-autoupgrade-status.doc.ts deleted file mode 100644 index fce1d30aac7..00000000000 --- a/docs-shopify.dev/commands/config-autoupgrade-status.doc.ts +++ /dev/null @@ -1,34 +0,0 @@ -// This is an autogenerated file. Don't edit this file manually. -import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs' - -const data: ReferenceEntityTemplateSchema = { - name: 'config autoupgrade status', - description: `Check whether auto-upgrade is enabled, disabled, or not yet configured. - - When auto-upgrade is enabled, Shopify CLI automatically updates to the latest version after each command. - - Run \`shopify config autoupgrade on\` or \`shopify config autoupgrade off\` to configure it. -`, - overviewPreviewDescription: `Check whether auto-upgrade is enabled, disabled, or not yet configured.`, - type: 'command', - isVisualComponent: false, - defaultExample: { - codeblock: { - tabs: [ - { - title: 'config autoupgrade status', - code: './examples/config-autoupgrade-status.example.sh', - language: 'bash', - }, - ], - title: 'config autoupgrade status', - }, - }, - definitions: [ - ], - category: 'general commands', - related: [ - ], -} - -export default data \ No newline at end of file diff --git a/docs-shopify.dev/commands/examples/config-autoupgrade-off.example.sh b/docs-shopify.dev/commands/examples/config-autoupgrade-off.example.sh deleted file mode 100644 index e6b28d09bd1..00000000000 --- a/docs-shopify.dev/commands/examples/config-autoupgrade-off.example.sh +++ /dev/null @@ -1 +0,0 @@ -shopify config autoupgrade off \ No newline at end of file diff --git a/docs-shopify.dev/commands/examples/config-autoupgrade-on.example.sh b/docs-shopify.dev/commands/examples/config-autoupgrade-on.example.sh deleted file mode 100644 index 71f0109cbb5..00000000000 --- a/docs-shopify.dev/commands/examples/config-autoupgrade-on.example.sh +++ /dev/null @@ -1 +0,0 @@ -shopify config autoupgrade on \ No newline at end of file diff --git a/docs-shopify.dev/commands/examples/config-autoupgrade-status.example.sh b/docs-shopify.dev/commands/examples/config-autoupgrade-status.example.sh deleted file mode 100644 index 5c94f013999..00000000000 --- a/docs-shopify.dev/commands/examples/config-autoupgrade-status.example.sh +++ /dev/null @@ -1 +0,0 @@ -shopify config autoupgrade status \ No newline at end of file diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index 57a6f11057b..3c9205db032 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -6195,4 +6195,4 @@ "value": "export interface version {\n\n}" } } -} +} \ No newline at end of file diff --git a/packages/theme/src/cli/utilities/theme-environment/host-validation.ts b/packages/theme/src/cli/utilities/theme-environment/host-validation.ts index d4908753480..e9b4ce04b32 100644 --- a/packages/theme/src/cli/utilities/theme-environment/host-validation.ts +++ b/packages/theme/src/cli/utilities/theme-environment/host-validation.ts @@ -8,7 +8,6 @@ function createAllowedHostsSet(host: string, port: number): Set { allowedHosts.add(`${normalizedHost}${portSuffix}`) - // When binding to localhost variants or 0.0.0.0, allow all localhost forms const localhostVariants = ['localhost', '127.0.0.1', '::1', '0.0.0.0'] if (localhostVariants.includes(normalizedHost)) { allowedHosts.add(`localhost${portSuffix}`) @@ -34,19 +33,9 @@ function createAllowedHostsSet(host: string, port: number): Set { function normalizeHostHeader(hostHeader: string | undefined): string | undefined { if (!hostHeader) return undefined - // Lowercase, then strip trailing dot before port (or at end for plain hostname). - // IPv6 brackets: trailing dot would be after `]`, e.g. [::1].:9292 return hostHeader.toLowerCase().replace(/\.(?=:\d|$)/, '') } -/** - * Creates an h3 event handler that validates the request's Host header - * against an allowlist of configured host/port and localhost variants. - * - * Used to mitigate DNS rebinding attacks on local dev servers. - * - * Returns a 400 Bad Request when the Host header is missing or not in the allowlist. - */ export function createHostValidationHandler(host: string, port: number) { const allowedHosts = createAllowedHostsSet(host, port) @@ -57,11 +46,7 @@ export function createHostValidationHandler(host: string, port: number) { if (!normalizedHost || !allowedHosts.has(normalizedHost)) { return sendError( event, - createError({ - statusCode: 400, - statusMessage: 'Bad Request', - message: 'Invalid Host header', - }), + createError({statusCode: 400, statusMessage: 'Bad Request', message: 'Invalid Host header'}), ) } })