From 574a949a7eafba3747cb4de2b4daf9431cb958eb Mon Sep 17 00:00:00 2001 From: River Date: Fri, 2 Oct 2026 08:49:36 +0000 Subject: [PATCH 1/3] Allow --config with --client-id on app deploy Deploy the configuration selected by --config to the app identified by --client-id, even if another TOML is linked to that client ID. This matches what already happens with `app config use ` followed by `app deploy --client-id `. Requested by Isaac Roldan Co-authored-by: Isaac Roldan --- .changeset/deploy-config-with-client-id.md | 5 ++ .../app/src/cli/commands/app/deploy.test.ts | 47 +++++++++++++++++++ packages/app/src/cli/commands/app/deploy.ts | 8 ++++ packages/cli/README.md | 7 +-- packages/cli/oclif.manifest.json | 5 +- 5 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 .changeset/deploy-config-with-client-id.md create mode 100644 packages/app/src/cli/commands/app/deploy.test.ts diff --git a/.changeset/deploy-config-with-client-id.md b/.changeset/deploy-config-with-client-id.md new file mode 100644 index 00000000000..54e85f6adf7 --- /dev/null +++ b/.changeset/deploy-config-with-client-id.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': minor +--- + +Allow `--config` and `--client-id` together on `app deploy` to deploy the selected configuration to a different app diff --git a/packages/app/src/cli/commands/app/deploy.test.ts b/packages/app/src/cli/commands/app/deploy.test.ts new file mode 100644 index 00000000000..75918029e0e --- /dev/null +++ b/packages/app/src/cli/commands/app/deploy.test.ts @@ -0,0 +1,47 @@ +import Deploy from './deploy.js' +import {deploy} from '../../services/deploy.js' +import {linkedAppContext} from '../../services/app-context.js' +import {testAppLinked, testOrganizationApp} from '../../models/app/app.test-data.js' +import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs' +import {beforeEach, describe, expect, test, vi} from 'vitest' + +vi.mock('../../services/deploy.js') +vi.mock('../../services/app-context.js') + +describe('app deploy command', () => { + beforeEach(() => { + vi.mocked(linkedAppContext).mockReset() + const app = testAppLinked() + vi.mocked(linkedAppContext).mockResolvedValue({ + app, + remoteApp: testOrganizationApp(), + } as Awaited>) + vi.mocked(deploy).mockResolvedValue({app} as Awaited>) + }) + + test('accepts --config together with --client-id to deploy a configuration to a different app', async () => { + await inTemporaryDirectory(async (tmp) => { + await Deploy.run( + ['--path', tmp, '--config', 'prod', '--client-id', 'a-different-app', '--allow-updates'], + import.meta.url, + ) + + expect(linkedAppContext).toHaveBeenCalledWith({ + directory: tmp, + clientId: 'a-different-app', + forceRelink: false, + userProvidedConfigName: 'prod', + }) + }) + }) + + test('still rejects --reset together with --config', async () => { + await inTemporaryDirectory(async (tmp) => { + await expect( + Deploy.run(['--path', tmp, '--config', 'prod', '--reset', '--allow-updates'], import.meta.url), + ).rejects.toThrow() + + expect(linkedAppContext).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/app/src/cli/commands/app/deploy.ts b/packages/app/src/cli/commands/app/deploy.ts index 4eee47e7100..d22bdd58f49 100644 --- a/packages/app/src/cli/commands/app/deploy.ts +++ b/packages/app/src/cli/commands/app/deploy.ts @@ -25,6 +25,14 @@ export default class Deploy extends AppLinkedCommand { static flags = { ...globalFlags, ...appFlags, + // Unlike the shared app flag, deploy accepts --client-id together with --config: + // the configuration selected by --config is deployed to the app identified by --client-id. + 'client-id': Flags.string({ + hidden: false, + description: + 'The Client ID of your app. Use with --config to deploy that configuration to a different app than the one it is linked to.', + env: 'SHOPIFY_FLAG_CLIENT_ID', + }), 'allow-updates': Flags.boolean({ hidden: false, description: diff --git a/packages/cli/README.md b/packages/cli/README.md index 01447abaa18..591739e965a 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -605,8 +605,8 @@ Deploy your Shopify app. ``` USAGE - $ shopify app deploy [--auth-alias ] [--client-id | -c ] [--json-schema] [--message - ] [--no-build] [--no-color] [--no-release | --allow-updates | --allow-deletes] [--path ] [--reset | ] + $ shopify app deploy [--auth-alias ] [--client-id ] [--json-schema] [--message ] + [--no-build] [--no-color] [--no-release | --allow-updates | --allow-deletes] [--path ] [--reset | -c ] [--source-control-url ] [--verbose] [--version ] FLAGS @@ -630,7 +630,8 @@ FLAGS [env: SHOPIFY_FLAG_AUTH_ALIAS] --client-id= - The Client ID of your app. + The Client ID of your app. Use with --config to deploy that configuration to a different app than the one it is + linked to. [env: SHOPIFY_FLAG_CLIENT_ID] --json-schema diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index b5981d99db4..c57672f6833 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -934,11 +934,8 @@ "type": "option" }, "client-id": { - "description": "The Client ID of your app.", + "description": "The Client ID of your app. Use with --config to deploy that configuration to a different app than the one it is linked to.", "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], "hasDynamicHelp": false, "hidden": false, "multiple": false, From e2439898176b8c705337cd52b5e1fe0387822504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Mon, 5 Oct 2026 12:18:14 +0200 Subject: [PATCH 2/3] Keep the specification configuration parser internal --- packages/app/src/cli/models/app/loader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/cli/models/app/loader.ts b/packages/app/src/cli/models/app/loader.ts index ea04a8a7971..76da89d8927 100644 --- a/packages/app/src/cli/models/app/loader.ts +++ b/packages/app/src/cli/models/app/loader.ts @@ -140,7 +140,7 @@ export function parseConfigurationObject( /** * Parses a configuration object using a specification's schema, and returns a result. */ -export function parseConfigurationObjectAgainstSpecification( +function parseConfigurationObjectAgainstSpecification( spec: ExtensionSpecification, filepath: string, configurationObject: object, From aa2cd5c13a62327cd3c3d64a18bbbfe282dafb18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Mon, 5 Oct 2026 12:27:55 +0200 Subject: [PATCH 3/3] Support config and client-ID overrides throughout app dev --- .changeset/deploy-config-with-client-id.md | 2 +- .../generated/generated_docs_data_v2.json | 8 +-- packages/app/src/cli/commands/app/dev.test.ts | 40 +++++++++++++++ packages/app/src/cli/commands/app/dev.ts | 7 +++ .../app/src/cli/commands/app/dev/clean.ts | 6 +++ packages/app/src/cli/models/app/app.ts | 5 ++ packages/app/src/cli/models/app/loader.ts | 9 ++-- .../app/src/cli/services/app-context.test.ts | 51 ++++++++++++++++++- packages/app/src/cli/services/app-context.ts | 2 +- packages/app/src/cli/services/deploy.test.ts | 2 +- packages/app/src/cli/services/deploy.ts | 3 +- packages/cli/README.md | 18 ++++--- packages/cli/oclif.manifest.json | 10 +--- 13 files changed, 131 insertions(+), 32 deletions(-) diff --git a/.changeset/deploy-config-with-client-id.md b/.changeset/deploy-config-with-client-id.md index 54e85f6adf7..5c481dd7897 100644 --- a/.changeset/deploy-config-with-client-id.md +++ b/.changeset/deploy-config-with-client-id.md @@ -2,4 +2,4 @@ '@shopify/app': minor --- -Allow `--config` and `--client-id` together on `app deploy` to deploy the selected configuration to a different app +Allow `--config` and `--client-id` together on `app deploy`, `app dev`, and `app dev clean` to use the selected configuration with a different app diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index 8b263ecf5d3..e32fa234c82 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -1045,7 +1045,7 @@ "syntaxKind": "PropertySignature", "name": "--client-id ", "value": "string", - "description": "The Client ID of your app.", + "description": "The Client ID of your app. Use with --config to clean up the dev preview for a different app than the one it is linked to.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_CLIENT_ID" }, @@ -1122,7 +1122,7 @@ "environmentValue": "SHOPIFY_FLAG_STORE" } ], - "value": "export interface appdevclean {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias '?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Store URL. Must be an existing development store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store '?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" + "value": "export interface appdevclean {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias '?: string\n\n /**\n * The Client ID of your app. Use with --config to clean up the dev preview for a different app than the one it is linked to.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Store URL. Must be an existing development store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store '?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" } }, "appdev": { @@ -1155,7 +1155,7 @@ "syntaxKind": "PropertySignature", "name": "--client-id ", "value": "string", - "description": "The Client ID of your app.", + "description": "The Client ID of your app. Use with --config to run dev with that configuration for a different app than the one it is linked to.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_CLIENT_ID" }, @@ -1340,7 +1340,7 @@ "environmentValue": "SHOPIFY_FLAG_THEME" } ], - "value": "export interface appdev {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias '?: string\n\n /**\n * Resource URL for checkout UI extension. Format: \"/cart/{productVariantID}:{productQuantity}\"\n * @environment SHOPIFY_FLAG_CHECKOUT_CART_URL\n */\n '--checkout-cart-url '?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Install and use mkcert to generate localhost certificates when --use-localhost is enabled without prompting.\n * @environment SHOPIFY_FLAG_INSTALL_MKCERT\n */\n '--install-mkcert'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Port to use for localhost. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_LOCALHOST_PORT\n */\n '--localhost-port '?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\n\n /**\n * Uses the app URL from the toml file instead an autogenerated URL for dev.\n * @environment SHOPIFY_FLAG_NO_UPDATE\n */\n '--no-update'?: ''\n\n /**\n * The file path or URL. The file path is to a file that you want updated on idle. The URL path is where you want a webhook posted to report on file changes.\n * @environment SHOPIFY_FLAG_NOTIFY\n */\n '--notify '?: string\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Skips the installation of dependencies. Deprecated, use workspaces instead.\n * @environment SHOPIFY_FLAG_SKIP_DEPENDENCIES_INSTALLATION\n */\n '--skip-dependencies-installation'?: ''\n\n /**\n * Store URL. Must be an existing development or Shopify Plus sandbox store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store '?: string\n\n /**\n * The password for storefronts with password protection.\n * @environment SHOPIFY_FLAG_STORE_PASSWORD\n */\n '--store-password '?: string\n\n /**\n * Resource URL for subscription UI extension. Format: \"/products/{productId}\"\n * @environment SHOPIFY_FLAG_SUBSCRIPTION_PRODUCT_URL\n */\n '--subscription-product-url '?: string\n\n /**\n * Theme ID or name of the theme app extension host theme.\n * @environment SHOPIFY_FLAG_THEME\n */\n '-t, --theme '?: string\n\n /**\n * Local port of the theme app extension development server. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_THEME_APP_EXTENSION_PORT\n */\n '--theme-app-extension-port '?: string\n\n /**\n * Use a custom tunnel, it must be running before executing dev. Format: \"https://my-tunnel-url:port\".\n * @environment SHOPIFY_FLAG_TUNNEL_URL\n */\n '--tunnel-url '?: string\n\n /**\n * Allow app dev to continue with warnings for eligible app version validation failures.\n * @environment SHOPIFY_FLAG_UNSAFE_VALIDATION\n */\n '--unsafe-validation'?: ''\n\n /**\n * Service entry point will listen to localhost. A tunnel won't be used. Will work for testing many app features, but not those that directly invoke your app (E.g: Webhooks)\n * @environment SHOPIFY_FLAG_USE_LOCALHOST\n */\n '--use-localhost'?: ''\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" + "value": "export interface appdev {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias '?: string\n\n /**\n * Resource URL for checkout UI extension. Format: \"/cart/{productVariantID}:{productQuantity}\"\n * @environment SHOPIFY_FLAG_CHECKOUT_CART_URL\n */\n '--checkout-cart-url '?: string\n\n /**\n * The Client ID of your app. Use with --config to run dev with that configuration for a different app than the one it is linked to.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Install and use mkcert to generate localhost certificates when --use-localhost is enabled without prompting.\n * @environment SHOPIFY_FLAG_INSTALL_MKCERT\n */\n '--install-mkcert'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Port to use for localhost. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_LOCALHOST_PORT\n */\n '--localhost-port '?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\n\n /**\n * Uses the app URL from the toml file instead an autogenerated URL for dev.\n * @environment SHOPIFY_FLAG_NO_UPDATE\n */\n '--no-update'?: ''\n\n /**\n * The file path or URL. The file path is to a file that you want updated on idle. The URL path is where you want a webhook posted to report on file changes.\n * @environment SHOPIFY_FLAG_NOTIFY\n */\n '--notify '?: string\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Skips the installation of dependencies. Deprecated, use workspaces instead.\n * @environment SHOPIFY_FLAG_SKIP_DEPENDENCIES_INSTALLATION\n */\n '--skip-dependencies-installation'?: ''\n\n /**\n * Store URL. Must be an existing development or Shopify Plus sandbox store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store '?: string\n\n /**\n * The password for storefronts with password protection.\n * @environment SHOPIFY_FLAG_STORE_PASSWORD\n */\n '--store-password '?: string\n\n /**\n * Resource URL for subscription UI extension. Format: \"/products/{productId}\"\n * @environment SHOPIFY_FLAG_SUBSCRIPTION_PRODUCT_URL\n */\n '--subscription-product-url '?: string\n\n /**\n * Theme ID or name of the theme app extension host theme.\n * @environment SHOPIFY_FLAG_THEME\n */\n '-t, --theme '?: string\n\n /**\n * Local port of the theme app extension development server. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_THEME_APP_EXTENSION_PORT\n */\n '--theme-app-extension-port '?: string\n\n /**\n * Use a custom tunnel, it must be running before executing dev. Format: \"https://my-tunnel-url:port\".\n * @environment SHOPIFY_FLAG_TUNNEL_URL\n */\n '--tunnel-url '?: string\n\n /**\n * Allow app dev to continue with warnings for eligible app version validation failures.\n * @environment SHOPIFY_FLAG_UNSAFE_VALIDATION\n */\n '--unsafe-validation'?: ''\n\n /**\n * Service entry point will listen to localhost. A tunnel won't be used. Will work for testing many app features, but not those that directly invoke your app (E.g: Webhooks)\n * @environment SHOPIFY_FLAG_USE_LOCALHOST\n */\n '--use-localhost'?: ''\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" } }, "appenvpull": { diff --git a/packages/app/src/cli/commands/app/dev.test.ts b/packages/app/src/cli/commands/app/dev.test.ts index 5bd68660525..f14870d69fa 100644 --- a/packages/app/src/cli/commands/app/dev.test.ts +++ b/packages/app/src/cli/commands/app/dev.test.ts @@ -1,4 +1,6 @@ import Dev from './dev.js' +import DevClean from './dev/clean.js' +import {devClean} from '../../services/dev-clean.js' import {dev} from '../../services/dev.js' import {linkedAppContext} from '../../services/app-context.js' import {storeContext} from '../../services/store-context.js' @@ -17,6 +19,7 @@ import {addPublicMetadata} from '@shopify/cli-kit/node/metadata' import {beforeEach, describe, expect, test, vi} from 'vitest' vi.mock('../../services/dev.js') +vi.mock('../../services/dev-clean.js') vi.mock('../../services/app-context.js') vi.mock('../../services/store-context.js') vi.mock('../../services/dev/tunnel-mode.js') @@ -51,6 +54,42 @@ describe('app dev command', () => { return {store} } + test.each([ + ['app dev', Dev, dev], + ['app dev clean', DevClean, devClean], + ])('%s accepts --config together with --client-id', async (_name, command, service) => { + await inTemporaryDirectory(async (tmp) => { + const {store} = mockAppAndStore(tmp) + vi.mocked(getTunnelMode).mockResolvedValue({mode: 'auto'}) + + await command.run( + ['--path', tmp, '--store', store.shopDomain, '--config', 'prod', '--client-id', 'a-different-app'], + import.meta.url, + ) + + expect(linkedAppContext).toHaveBeenCalledWith({ + directory: tmp, + clientId: 'a-different-app', + forceRelink: false, + userProvidedConfigName: 'prod', + }) + expect(service).toHaveBeenCalled() + }) + }) + + test.each([ + ['app dev', Dev], + ['app dev clean', DevClean], + ])('%s still rejects --reset together with --config', async (_name, command) => { + await inTemporaryDirectory(async (tmp) => { + await expect(command.run(['--path', tmp, '--config', 'prod', '--reset'], import.meta.url)).rejects.toThrow() + + expect(linkedAppContext).not.toHaveBeenCalled() + expect(dev).not.toHaveBeenCalled() + expect(devClean).not.toHaveBeenCalled() + }) + }) + test('does not require --use-localhost when --install-mkcert is not passed', async () => { await inTemporaryDirectory(async (tmp) => { const {store} = mockAppAndStore(tmp) @@ -113,6 +152,7 @@ describe('app dev command', () => { await expect(Dev.run(['--path', tmp, '--install-mkcert'], import.meta.url)).rejects.toThrow() expect(dev).not.toHaveBeenCalled() + expect(devClean).not.toHaveBeenCalled() }) }) }) diff --git a/packages/app/src/cli/commands/app/dev.ts b/packages/app/src/cli/commands/app/dev.ts index 1b0de21e116..6fe12af0385 100644 --- a/packages/app/src/cli/commands/app/dev.ts +++ b/packages/app/src/cli/commands/app/dev.ts @@ -24,6 +24,13 @@ export default class Dev extends AppLinkedCommand { static flags = { ...globalFlags, ...appFlags, + // The selected configuration is used with the app identified by --client-id. + 'client-id': Flags.string({ + hidden: false, + description: + 'The Client ID of your app. Use with --config to run dev with that configuration for a different app than the one it is linked to.', + env: 'SHOPIFY_FLAG_CLIENT_ID', + }), store: Flags.string({ char: 's', description: 'Store URL. Must be an existing development or Shopify Plus sandbox store.', diff --git a/packages/app/src/cli/commands/app/dev/clean.ts b/packages/app/src/cli/commands/app/dev/clean.ts index 6b87c55a849..c4c78c56772 100644 --- a/packages/app/src/cli/commands/app/dev/clean.ts +++ b/packages/app/src/cli/commands/app/dev/clean.ts @@ -20,6 +20,12 @@ export default class DevClean extends AppLinkedCommand { static flags = { ...globalFlags, ...appFlags, + 'client-id': Flags.string({ + hidden: false, + description: + 'The Client ID of your app. Use with --config to clean up the dev preview for a different app than the one it is linked to.', + env: 'SHOPIFY_FLAG_CLIENT_ID', + }), store: Flags.string({ hidden: false, char: 's', diff --git a/packages/app/src/cli/models/app/app.ts b/packages/app/src/cli/models/app/app.ts index 73eec01f3fc..b101e37125b 100644 --- a/packages/app/src/cli/models/app/app.ts +++ b/packages/app/src/cli/models/app/app.ts @@ -194,6 +194,8 @@ export interface AppConfigurationInterface< directory: string configPath: string configuration: TConfig + /** The client ID provided by the command, preserved when the app reloads. */ + readonly clientIdOverride?: string configSchema: SchemaForConfig specifications: TModuleSpec[] remoteFlags: Flag[] @@ -277,6 +279,7 @@ export class App< directory: string configPath: string configuration: TConfig + readonly clientIdOverride?: string webs: Web[] dotenv?: DotEnvFile errors: AppErrors @@ -292,6 +295,7 @@ export class App< directory, configPath, configuration, + clientIdOverride, webs, modules, dotenv, @@ -306,6 +310,7 @@ export class App< this.directory = directory this.configPath = configPath this.configuration = configuration + this.clientIdOverride = clientIdOverride this.webs = webs this.dotenv = dotenv this.realExtensions = modules diff --git a/packages/app/src/cli/models/app/loader.ts b/packages/app/src/cli/models/app/loader.ts index 76da89d8927..8c240700171 100644 --- a/packages/app/src/cli/models/app/loader.ts +++ b/packages/app/src/cli/models/app/loader.ts @@ -340,6 +340,7 @@ export async function loadAppFromContext { +export async function reloadApp(app: AppLinkedInterface): Promise { const {project, activeConfig} = await getAppConfigurationContext(app.directory, basename(app.configPath)) const reloadState: ReloadState = { extensionDevUUIDs: new Map(app.allExtensions.map((ext) => [ext.handle, ext.devUUID])), @@ -459,7 +457,7 @@ export async function reloadApp( specifications: app.specifications, remoteFlags: app.remoteFlags ?? [], reloadState, - clientIdOverride: options?.clientIdOverride, + clientIdOverride: app.clientIdOverride, }) if (!newApp.errors.isEmpty()) { const errors = newApp.errors.getErrors() @@ -533,6 +531,7 @@ class AppLoader { + await inTemporaryDirectory(async (tmp) => { + const content = 'name = "test-app"\nclient_id = "source-client-id"\n' + await writeAppConfig(tmp, content) + vi.mocked(appFromIdentifiers).mockResolvedValue({...mockRemoteApp, apiKey: 'source-client-id'}) + const {app} = await linkedAppContext({ + directory: tmp, + clientId, + forceRelink: false, + userProvidedConfigName: undefined, + }) + await writeAppConfig(tmp, content.replace('source-client-id', 'updated-client-id')) + + const reloadedApp = await loader.reloadApp(app) + + expect(reloadedApp.configuration.client_id).toBe(expectedClientId) + expect(reloadedApp.clientIdOverride).toBe(clientId) + }) + }) + test('resets app when there is a valid toml but reset option is true', async () => { await inTemporaryDirectory(async (tmp) => { // Given diff --git a/packages/app/src/cli/services/app-context.ts b/packages/app/src/cli/services/app-context.ts index 00d37b86fce..d2d84124a14 100644 --- a/packages/app/src/cli/services/app-context.ts +++ b/packages/app/src/cli/services/app-context.ts @@ -136,7 +136,7 @@ export async function linkedAppContext({ activeConfig, specifications, remoteFlags: remoteApp.flags, - clientIdOverride: clientId && clientId !== configClientId ? clientId : undefined, + clientIdOverride: clientId, }) if (!unsafeTolerateErrors && !localApp.errors.isEmpty()) { diff --git a/packages/app/src/cli/services/deploy.test.ts b/packages/app/src/cli/services/deploy.test.ts index 3fc7622542f..49e2063611c 100644 --- a/packages/app/src/cli/services/deploy.test.ts +++ b/packages/app/src/cli/services/deploy.test.ts @@ -808,7 +808,7 @@ describe('ImportExtensionsIfNeeded', () => { developerPlatformClient, extensions: mockExtensions, }) - expect(reloadApp).toHaveBeenCalledWith(app, {clientIdOverride: remoteApp.apiKey}) + expect(reloadApp).toHaveBeenCalledWith(app) expect(result).toBe(reloadedApp) }) diff --git a/packages/app/src/cli/services/deploy.ts b/packages/app/src/cli/services/deploy.ts index a9326f9ce55..2b002608893 100644 --- a/packages/app/src/cli/services/deploy.ts +++ b/packages/app/src/cli/services/deploy.ts @@ -106,8 +106,7 @@ async function handleDashboardExtensions( developerPlatformClient, extensions, }) - // Keep the deployment target when the selected TOML is linked to another app. - return reloadApp(app, {clientIdOverride: remoteApp.apiKey}) + return reloadApp(app) } throw new AbortSilentError() diff --git a/packages/cli/README.md b/packages/cli/README.md index 03616c81622..e0dfaf08d02 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -733,11 +733,11 @@ Run the app. ``` USAGE - $ shopify app dev [--auth-alias ] [--checkout-cart-url ] [--client-id | -c ] + $ shopify app dev [--auth-alias ] [--checkout-cart-url ] [--client-id ] [--install-mkcert --use-localhost] [--json-schema] [--localhost-port ] [--no-color] [--no-input] - [--no-update] [--notify ] [--path ] [--reset | ] [--skip-dependencies-installation] [-s ] - [--store-password ] [--subscription-product-url ] [-t ] [--theme-app-extension-port ] - [--tunnel-url | ] [--unsafe-validation] [--verbose] + [--no-update] [--notify ] [--path ] [--reset | -c ] [--skip-dependencies-installation] [-s + ] [--store-password ] [--subscription-product-url ] [-t ] [--theme-app-extension-port + ] [--tunnel-url | ] [--unsafe-validation] [--verbose] FLAGS -c, --config= @@ -761,7 +761,8 @@ FLAGS [env: SHOPIFY_FLAG_CHECKOUT_CART_URL] --client-id= - The Client ID of your app. + The Client ID of your app. Use with --config to run dev with that configuration for a different app than the one it + is linked to. [env: SHOPIFY_FLAG_CLIENT_ID] --install-mkcert @@ -847,8 +848,8 @@ Cleans up the dev preview from the selected store. ``` USAGE - $ shopify app dev clean [--auth-alias ] [--client-id | -c ] [--json-schema] [--no-color] - [--no-input] [--path ] [--reset | ] [-s ] [--verbose] + $ shopify app dev clean [--auth-alias ] [--client-id ] [--json-schema] [--no-color] [--no-input] + [--path ] [--reset | -c ] [-s ] [--verbose] FLAGS -c, --config= @@ -864,7 +865,8 @@ FLAGS [env: SHOPIFY_FLAG_AUTH_ALIAS] --client-id= - The Client ID of your app. + The Client ID of your app. Use with --config to clean up the dev preview for a different app than the one it is + linked to. [env: SHOPIFY_FLAG_CLIENT_ID] --json-schema diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 248559d36a5..65c22767600 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -1141,11 +1141,8 @@ "type": "option" }, "client-id": { - "description": "The Client ID of your app.", + "description": "The Client ID of your app. Use with --config to run dev with that configuration for a different app than the one it is linked to.", "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], "hasDynamicHelp": false, "hidden": false, "multiple": false, @@ -1369,11 +1366,8 @@ "type": "option" }, "client-id": { - "description": "The Client ID of your app.", + "description": "The Client ID of your app. Use with --config to clean up the dev preview for a different app than the one it is linked to.", "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], "hasDynamicHelp": false, "hidden": false, "multiple": false,