From 59a43e69cdffad64b9d5c0a2f9798addac4edcd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Mon, 6 Jul 2026 11:57:25 +0200 Subject: [PATCH 1/2] Fix config link from extension directories Assisted-By: devx/60f87861-3b15-4c10-b879-f284b3a36465 --- .changeset/config-link-extension-directory.md | 5 +++ .../config/__snapshots__/link.test.ts.snap | 23 +++++++++++ .../src/cli/services/app/config/link.test.ts | 39 ++++++++++++++++++- .../app/src/cli/services/app/config/link.ts | 7 ++-- 4 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 .changeset/config-link-extension-directory.md diff --git a/.changeset/config-link-extension-directory.md b/.changeset/config-link-extension-directory.md new file mode 100644 index 00000000000..65c93b15e0d --- /dev/null +++ b/.changeset/config-link-extension-directory.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli': patch +--- + +Prevent `app config link` from overwriting the root app config when run from an extension directory diff --git a/packages/app/src/cli/services/app/config/__snapshots__/link.test.ts.snap b/packages/app/src/cli/services/app/config/__snapshots__/link.test.ts.snap index d1ec13a5d2b..ce03398d6be 100644 --- a/packages/app/src/cli/services/app/config/__snapshots__/link.test.ts.snap +++ b/packages/app/src/cli/services/app/config/__snapshots__/link.test.ts.snap @@ -282,6 +282,29 @@ use_legacy_install_flow = true " `; +exports[`link > prompts for a new config name when linking a new app from an extension directory 1`] = ` +"# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration + +client_id = "new-api-key" +name = "app1" +application_url = "https://example.com" +embedded = true + +[auth] +redirect_urls = [ "https://example.com/callback1" ] + +[webhooks] +api_version = "2023-07" + +[pos] +embedded = false + +[access_scopes] +# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes +use_legacy_install_flow = true +" +`; + exports[`link > replace arrays content with the remote one 1`] = ` "# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration diff --git a/packages/app/src/cli/services/app/config/link.test.ts b/packages/app/src/cli/services/app/config/link.test.ts index d06ff3ce9e8..ab523f12ebd 100644 --- a/packages/app/src/cli/services/app/config/link.test.ts +++ b/packages/app/src/cli/services/app/config/link.test.ts @@ -15,7 +15,7 @@ import {fetchAppRemoteConfiguration} from '../select-app.js' import {DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js' import {MinimalAppIdentifiers, OrganizationApp} from '../../../models/organization.js' import {beforeEach, describe, expect, test, vi} from 'vitest' -import {fileExistsSync, inTemporaryDirectory, readFile, writeFileSync} from '@shopify/cli-kit/node/fs' +import {fileExistsSync, inTemporaryDirectory, mkdir, readFile, writeFileSync} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' import {renderSuccess} from '@shopify/cli-kit/node/ui' import {outputContent} from '@shopify/cli-kit/node/output' @@ -514,6 +514,43 @@ describe('link', () => { }) }) + test('prompts for a new config name when linking a new app from an extension directory', async () => { + await inTemporaryDirectory(async (tmp) => { + const developerPlatformClient = buildDeveloperPlatformClient() + const extensionDirectory = joinPath(tmp, 'extensions', 'discount_function') + await mkdir(extensionDirectory) + const initialContent = 'client_id = "existing-api-key"\nname = "existing app"\n' + writeFileSync(joinPath(tmp, 'shopify.app.toml'), initialContent) + const options: LinkOptions = { + directory: extensionDirectory, + developerPlatformClient, + } + const localApp = { + configPath: joinPath(tmp, 'shopify.app.toml'), + configuration: { + name: 'existing app', + client_id: 'existing-api-key', + webhooks: {api_version: '2023-04'}, + application_url: 'https://myapp.com', + } as CurrentAppConfiguration, + } + await mockLoadOpaqueAppWithApp(tmp, localApp, [], 'current') + vi.mocked(fetchOrCreateOrganizationApp).mockResolvedValue( + testOrganizationApp({ + apiKey: 'new-api-key', + developerPlatformClient, + }), + ) + vi.mocked(selectConfigName).mockResolvedValue('shopify.app.staging.toml') + + await link(options) + + expect(selectConfigName).toHaveBeenCalledWith(tmp, 'app1') + expect(await readFile(joinPath(tmp, 'shopify.app.toml'))).toBe(initialContent) + expect(await readFile(joinPath(tmp, 'shopify.app.staging.toml'))).toMatchSnapshot() + }) + }) + test('updates the shopify.app.toml when it already exists and is unlinked', async () => { await inTemporaryDirectory(async (tmp) => { // Given diff --git a/packages/app/src/cli/services/app/config/link.ts b/packages/app/src/cli/services/app/config/link.ts index 373664c32e1..df7722cb194 100644 --- a/packages/app/src/cli/services/app/config/link.ts +++ b/packages/app/src/cli/services/app/config/link.ts @@ -68,7 +68,7 @@ export default async function link(options: LinkOptions, shouldRenderSuccess = t const flags = remoteApp.flags const localAppOptions = await loadLocalAppOptions(options, specifications, flags, remoteApp.apiKey) const configFileName = await loadConfigurationFileName(remoteApp, options, { - appDirectory: localAppOptions.appDirectory, + appDirectory: localAppOptions.appDirectory ?? appDirectory, }) await logMetadataForLoadedContext(remoteApp, developerPlatformClient.organizationSource) @@ -304,14 +304,15 @@ async function loadConfigurationFileName( const cache = getCachedCommandInfo() if (cache?.selectedToml) return cache.selectedToml as AppConfigurationFileName - const existingTomls = await getTomls(options.directory) + const configDirectory = localAppInfo.appDirectory ?? options.directory + const existingTomls = await getTomls(configDirectory) const currentToml = existingTomls[remoteApp.apiKey] if (currentToml) return currentToml // If no TOML files exist at all, use the default filename without prompting if (isEmpty(existingTomls)) return 'shopify.app.toml' - return selectConfigName(localAppInfo.appDirectory ?? options.directory, remoteApp.title) + return selectConfigName(configDirectory, remoteApp.title) } /** From 2af44e05db09f849601383927ad5b9e4ce6b00aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Mon, 6 Jul 2026 15:24:51 +0200 Subject: [PATCH 2/2] fix lint --- packages/app/src/cli/services/app/config/link.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/cli/services/app/config/link.test.ts b/packages/app/src/cli/services/app/config/link.test.ts index ab523f12ebd..ff69cf0f38a 100644 --- a/packages/app/src/cli/services/app/config/link.test.ts +++ b/packages/app/src/cli/services/app/config/link.test.ts @@ -546,8 +546,8 @@ describe('link', () => { await link(options) expect(selectConfigName).toHaveBeenCalledWith(tmp, 'app1') - expect(await readFile(joinPath(tmp, 'shopify.app.toml'))).toBe(initialContent) - expect(await readFile(joinPath(tmp, 'shopify.app.staging.toml'))).toMatchSnapshot() + await expect(readFile(joinPath(tmp, 'shopify.app.toml'))).resolves.toBe(initialContent) + await expect(readFile(joinPath(tmp, 'shopify.app.staging.toml'))).resolves.toMatchSnapshot() }) })