From 71274f00b832e3a79d8ce84ae9d3f432f4c802e7 Mon Sep 17 00:00:00 2001 From: River Date: Mon, 16 Mar 2026 17:04:23 +0000 Subject: [PATCH] Fix: normalize cached store URL before comparison in storeContext The cached dev_store_url from app.toml or hidden config was compared against the already-normalized selectedStore.shopDomain without first normalizing it. This meant format differences (e.g. 'https://store.myshopify.com' vs 'store.myshopify.com', or 'store' vs 'store.myshopify.com') would cause a false mismatch and trigger unnecessary writes to .shopify/project.json on every `dev` run. Now we normalize cachedStoreURL before comparing, so the hidden config is only updated when the store actually changes. --- .../src/cli/services/store-context.test.ts | 85 +++++++++++++++++++ .../app/src/cli/services/store-context.ts | 8 +- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/packages/app/src/cli/services/store-context.test.ts b/packages/app/src/cli/services/store-context.test.ts index e622f2dad67..4b9b5ec36e3 100644 --- a/packages/app/src/cli/services/store-context.test.ts +++ b/packages/app/src/cli/services/store-context.test.ts @@ -203,6 +203,91 @@ describe('storeContext', () => { }) }) + test('does not update hidden config when cached URL differs only by format', async () => { + await inTemporaryDirectory(async (dir) => { + // The TOML has an un-normalized URL (with https:// prefix) + const appWithUnnormalizedUrl = testAppLinked({ + configuration: { + client_id: 'client_id', + name: 'app-config-name', + build: { + dev_store_url: 'https://test-store.myshopify.com', + }, + } as any, + hiddenConfig: { + dev_store_url: 'test-store.myshopify.com', + }, + }) + const updatedAppContextResult = {...appContextResult, app: appWithUnnormalizedUrl} + const storeMatchingNormalized = testOrganizationStore({ + shopId: 'store1', + shopDomain: 'test-store.myshopify.com', + }) + + await prepareAppFolder(appWithUnnormalizedUrl, dir) + vi.mocked(fetchStore).mockResolvedValue(storeMatchingNormalized) + + await storeContext({appContextResult: updatedAppContextResult, forceReselectStore: false}) + + // The hidden config should NOT be updated since the normalized URLs match + // and the hidden config already has a value + const hiddenConfig = await readFile(appHiddenConfigPath(dir)) + // The file was initialized empty, so if updateHiddenConfig was NOT called, + // it should still be empty + expect(hiddenConfig).toEqual('') + }) + }) + + test('does not update hidden config when cached URL is a short store name', async () => { + await inTemporaryDirectory(async (dir) => { + // The TOML has just the store name without .myshopify.com + const appWithShortUrl = testAppLinked({ + configuration: { + client_id: 'client_id', + name: 'app-config-name', + build: { + dev_store_url: 'test-store', + }, + } as any, + hiddenConfig: { + dev_store_url: 'test-store.myshopify.com', + }, + }) + const updatedAppContextResult = {...appContextResult, app: appWithShortUrl} + const storeMatchingNormalized = testOrganizationStore({ + shopId: 'store1', + shopDomain: 'test-store.myshopify.com', + }) + + await prepareAppFolder(appWithShortUrl, dir) + vi.mocked(fetchStore).mockResolvedValue(storeMatchingNormalized) + + await storeContext({appContextResult: updatedAppContextResult, forceReselectStore: false}) + + // The hidden config should NOT be updated since normalized URLs match + const hiddenConfig = await readFile(appHiddenConfigPath(dir)) + expect(hiddenConfig).toEqual('') + }) + }) + + test('updates hidden config when store actually changes', async () => { + await inTemporaryDirectory(async (dir) => { + await prepareAppFolder(mockApp, dir) + const differentStore = testOrganizationStore({ + shopId: 'store2', + shopDomain: 'different-store.myshopify.com', + }) + vi.mocked(fetchStore).mockResolvedValue(differentStore) + + await storeContext({appContextResult, forceReselectStore: false}) + + const hiddenConfig = await readFile(appHiddenConfigPath(dir)) + expect(hiddenConfig).toEqual( + '{\n "client_id": {\n "dev_store_url": "different-store.myshopify.com"\n }\n}', + ) + }) + }) + test('ensures user access to store', async () => { await inTemporaryDirectory(async (dir) => { await prepareAppFolder(mockApp, dir) diff --git a/packages/app/src/cli/services/store-context.ts b/packages/app/src/cli/services/store-context.ts index a0df8b837eb..4df0dc8bf1b 100644 --- a/packages/app/src/cli/services/store-context.ts +++ b/packages/app/src/cli/services/store-context.ts @@ -67,8 +67,12 @@ export async function storeContext({ await logMetadata(selectedStore, forceReselectStore) selectedStore.shopDomain = normalizeStoreFqdn(selectedStore.shopDomain) - // Save the selected store in the hidden config file - if (selectedStore.shopDomain !== cachedStoreURL || !devStoreUrlFromHiddenConfig) { + // Save the selected store in the hidden config file. + // Normalize the cached URL before comparing so that format differences + // (e.g. "https://store.myshopify.com" vs "store.myshopify.com") don't + // cause unnecessary writes. + const normalizedCachedURL = cachedStoreURL ? normalizeStoreFqdn(cachedStoreURL) : undefined + if (selectedStore.shopDomain !== normalizedCachedURL || !devStoreUrlFromHiddenConfig) { await app.updateHiddenConfig({dev_store_url: selectedStore.shopDomain}) }