From 79ec96ce2d02658680560f2275d0511a9244f851 Mon Sep 17 00:00:00 2001 From: Robin Drexler Date: Thu, 7 May 2026 09:41:42 -0400 Subject: [PATCH] fix app url for ui extension builds --- .../inject-app-url-ui-extension-builds.md | 5 + .../src/cli/services/build/extension.test.ts | 95 ++++++++++++++++++- .../app/src/cli/services/build/extension.ts | 15 ++- 3 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 .changeset/inject-app-url-ui-extension-builds.md diff --git a/.changeset/inject-app-url-ui-extension-builds.md b/.changeset/inject-app-url-ui-extension-builds.md new file mode 100644 index 00000000000..010f4146e76 --- /dev/null +++ b/.changeset/inject-app-url-ui-extension-builds.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Default `process.env.APP_URL` to the app configuration `application_url` when bundling UI extensions for production builds, while preserving `.env` and shell environment variable overrides. diff --git a/packages/app/src/cli/services/build/extension.test.ts b/packages/app/src/cli/services/build/extension.test.ts index 7f4be985566..78002215342 100644 --- a/packages/app/src/cli/services/build/extension.test.ts +++ b/packages/app/src/cli/services/build/extension.test.ts @@ -1,6 +1,7 @@ -import {buildFunctionExtension} from './extension.js' -import {testFunctionExtension} from '../../models/app/app.test-data.js' +import {buildFunctionExtension, buildUIExtension} from './extension.js' +import {testApp, testFunctionExtension, testUIExtension} from '../../models/app/app.test-data.js' import {buildGraphqlTypes, buildJSFunction, runWasmOpt, runTrampoline} from '../function/build.js' +import {bundleExtension} from '../extensions/bundle.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {FunctionConfigType} from '../../models/extensions/specifications/function.js' import {beforeEach, describe, expect, test, vi} from 'vitest' @@ -12,9 +13,99 @@ import {joinPath} from '@shopify/cli-kit/node/path' vi.mock('@shopify/cli-kit/node/system') vi.mock('../function/build.js') +vi.mock('../extensions/bundle.js') vi.mock('proper-lockfile') vi.mock('@shopify/cli-kit/node/fs') +describe('buildUIExtension', () => { + let stdout: any + let stderr: any + + beforeEach(() => { + stdout = {write: vi.fn()} + stderr = {write: vi.fn()} + vi.mocked(bundleExtension).mockResolvedValue(undefined) + }) + + test('defaults APP_URL to the configured application_url for production builds', async () => { + // Given + const extension = await testUIExtension() + const app = testApp() + + // When + await buildUIExtension(extension, {stdout, stderr, app, environment: 'production'}) + + // Then + expect(bundleExtension).toHaveBeenCalledWith(expect.objectContaining({env: {APP_URL: 'https://myapp.com'}})) + }) + + test('allows app dotenv variables to override the configured application_url', async () => { + // Given + const extension = await testUIExtension() + const app = testApp({ + dotenv: { + path: '/tmp/project/.env', + variables: { + APP_URL: 'https://env.example.com', + FOO: 'bar', + }, + }, + }) + + // When + await buildUIExtension(extension, {stdout, stderr, app, environment: 'production'}) + + // Then + expect(bundleExtension).toHaveBeenCalledWith( + expect.objectContaining({env: {APP_URL: 'https://env.example.com', FOO: 'bar'}}), + ) + }) + + test('uses the development app URL when provided', async () => { + // Given + const extension = await testUIExtension() + const app = testApp({ + dotenv: { + path: '/tmp/project/.env', + variables: {APP_URL: 'https://env.example.com'}, + }, + }) + + // When + await buildUIExtension(extension, { + stdout, + stderr, + app, + environment: 'development', + appURL: 'https://dev-tunnel.example.com', + }) + + // Then + expect(bundleExtension).toHaveBeenCalledWith( + expect.objectContaining({env: {APP_URL: 'https://dev-tunnel.example.com'}}), + ) + }) + + test('does not mutate dotenv variables when adding APP_URL', async () => { + // Given + const extension = await testUIExtension() + const variables = {FOO: 'bar'} + const app = testApp({dotenv: {path: '/tmp/project/.env', variables}}) + + // When + await buildUIExtension(extension, { + stdout, + stderr, + app, + environment: 'development', + appURL: 'https://dev-tunnel.example.com', + }) + + // Then + expect(variables).toEqual({FOO: 'bar'}) + }) +}) + describe('buildFunctionExtension', () => { let extension: ExtensionInstance let stdout: any diff --git a/packages/app/src/cli/services/build/extension.ts b/packages/app/src/cli/services/build/extension.ts index 6006a9f4b57..c344f36d44b 100644 --- a/packages/app/src/cli/services/build/extension.ts +++ b/packages/app/src/cli/services/build/extension.ts @@ -62,10 +62,7 @@ export interface ExtensionBuildOptions { */ export async function buildUIExtension(extension: ExtensionInstance, options: ExtensionBuildOptions): Promise { options.stdout.write(`Bundling UI extension ${extension.localIdentifier}...`) - const env = options.app.dotenv?.variables ?? {} - if (options.appURL) { - env.APP_URL = options.appURL - } + const env = getUIExtensionBuildEnv(options) const buildDirectory = options.buildDirectory ?? '' @@ -131,6 +128,16 @@ export async function buildUIExtension(extension: ExtensionInstance, options: Ex return localOutputPath } +function getUIExtensionBuildEnv(options: ExtensionBuildOptions): {[variable: string]: string} { + return { + ...(options.environment === 'production' && options.app.configuration.application_url + ? {APP_URL: options.app.configuration.application_url} + : {}), + ...(options.app.dotenv?.variables ?? {}), + ...(options.appURL ? {APP_URL: options.appURL} : {}), + } +} + type BuildFunctionExtensionOptions = ExtensionBuildOptions /**