Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/inject-app-url-ui-extension-builds.md
Original file line number Diff line number Diff line change
@@ -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.
95 changes: 93 additions & 2 deletions packages/app/src/cli/services/build/extension.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<FunctionConfigType>
let stdout: any
Expand Down
15 changes: 11 additions & 4 deletions packages/app/src/cli/services/build/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,7 @@ export interface ExtensionBuildOptions {
*/
export async function buildUIExtension(extension: ExtensionInstance, options: ExtensionBuildOptions): Promise<string> {
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 ?? ''

Expand Down Expand Up @@ -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

/**
Expand Down
Loading