Skip to content

Commit 8d9eef5

Browse files
authored
Merge pull request #5245 from Shopify/01-21-patch_the_app_manifest_with_the_development_urls_add_tests
Patch the app manifest with the development URLs, add tests
2 parents 3feb8cf + 1e1b0eb commit 8d9eef5

File tree

5 files changed

+249
-5
lines changed

5 files changed

+249
-5
lines changed

packages/app/src/cli/models/app/app.test-data.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ import {
7171
} from '../../api/graphql/partners/generated/update-draft.js'
7272
import {SchemaDefinitionByTargetQueryVariables} from '../../api/graphql/functions/generated/schema-definition-by-target.js'
7373
import {SchemaDefinitionByApiTypeQueryVariables} from '../../api/graphql/functions/generated/schema-definition-by-api-type.js'
74+
import {AppHomeSpecIdentifier} from '../extensions/specifications/app_config_app_home.js'
75+
import {AppProxySpecIdentifier} from '../extensions/specifications/app_config_app_proxy.js'
7476
import {vi} from 'vitest'
7577
import {joinPath} from '@shopify/cli-kit/node/path'
7678

@@ -120,6 +122,7 @@ export function testApp(app: Partial<AppInterface> = {}, schemaType: 'current' |
120122
configSchema: (app.configSchema ?? AppConfigurationSchema) as any,
121123
remoteFlags: app.remoteFlags ?? [],
122124
hiddenConfig: app.hiddenConfig ?? {},
125+
devApplicationURLs: app.devApplicationURLs,
123126
})
124127

125128
if (app.updateDependencies) {
@@ -340,6 +343,55 @@ export async function testAppAccessConfigExtension(
340343
return extension
341344
}
342345

346+
export async function testAppHomeConfigExtension(): Promise<ExtensionInstance> {
347+
const configuration = {
348+
name: 'App Home',
349+
type: 'app_home',
350+
handle: 'app-home',
351+
application_url: 'https://example.com',
352+
embedded: true,
353+
metafields: [],
354+
}
355+
356+
const allSpecs = await loadLocalExtensionsSpecifications()
357+
const specification = allSpecs.find((spec) => spec.identifier === AppHomeSpecIdentifier)!
358+
359+
const extension = new ExtensionInstance({
360+
configuration,
361+
configurationPath: '',
362+
directory: './',
363+
specification,
364+
})
365+
366+
return extension
367+
}
368+
369+
export async function testAppProxyConfigExtension(): Promise<ExtensionInstance> {
370+
const configuration = {
371+
name: 'App Proxy',
372+
type: 'app_proxy',
373+
handle: 'app-proxy',
374+
metafields: [],
375+
app_proxy: {
376+
url: 'https://example.com',
377+
subpath: 'apps',
378+
prefix: 'apps',
379+
},
380+
}
381+
382+
const allSpecs = await loadLocalExtensionsSpecifications()
383+
const specification = allSpecs.find((spec) => spec.identifier === AppProxySpecIdentifier)!
384+
385+
const extension = new ExtensionInstance({
386+
configuration,
387+
configurationPath: '',
388+
directory: './',
389+
specification,
390+
})
391+
392+
return extension
393+
}
394+
343395
export async function testPaymentExtensions(directory = './my-extension'): Promise<ExtensionInstance> {
344396
const configuration = {
345397
name: 'Payment Extension Name',

packages/app/src/cli/models/app/app.test.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ import {
1818
testWebhookExtensions,
1919
testEditorExtensionCollection,
2020
testAppAccessConfigExtension,
21+
testAppHomeConfigExtension,
22+
testAppProxyConfigExtension,
2123
} from './app.test-data.js'
2224
import {ExtensionInstance} from '../extensions/extension-instance.js'
2325
import {FunctionConfigType} from '../extensions/specifications/function.js'
2426
import {WebhooksConfig} from '../extensions/specifications/types/app_config_webhook.js'
2527
import {EditorExtensionCollectionType} from '../extensions/specifications/editor_extension_collection.js'
28+
import {ApplicationURLs} from '../../services/dev/urls.js'
2629
import {describe, expect, test} from 'vitest'
2730
import {inTemporaryDirectory, mkdir, writeFile} from '@shopify/cli-kit/node/fs'
2831
import {joinPath} from '@shopify/cli-kit/node/path'
@@ -470,6 +473,150 @@ describe('allExtensions', () => {
470473
})
471474
})
472475

476+
describe('manifest', () => {
477+
test('generates a manifest with basic app information and modules', async () => {
478+
// Given
479+
const appAccessModule = await testAppAccessConfigExtension()
480+
481+
const app = await testApp({
482+
name: 'my-app',
483+
allExtensions: [appAccessModule],
484+
configuration: {
485+
...DEFAULT_CONFIG,
486+
client_id: 'test-client-id',
487+
},
488+
})
489+
490+
// When
491+
const manifest = await app.manifest()
492+
493+
// Then
494+
expect(manifest).toEqual({
495+
name: 'my-app',
496+
handle: '',
497+
modules: [
498+
{
499+
type: 'app_access_external',
500+
handle: 'app-access',
501+
uid: appAccessModule.uid,
502+
assets: appAccessModule.uid,
503+
target: appAccessModule.contextValue,
504+
config: expect.objectContaining({
505+
redirect_url_allowlist: ['https://example.com/auth/callback'],
506+
}),
507+
},
508+
],
509+
})
510+
})
511+
512+
test('patches the manifest with development URLs when available', async () => {
513+
// Given
514+
const appHome = await testAppHomeConfigExtension()
515+
const appAccess = await testAppAccessConfigExtension()
516+
const appProxy = await testAppProxyConfigExtension()
517+
518+
const devApplicationURLs: ApplicationURLs = {
519+
applicationUrl: 'https://new-url.io',
520+
redirectUrlWhitelist: ['https://new-url.io/auth/callback'],
521+
appProxy: {
522+
proxyUrl: 'https://new-proxy-url.io',
523+
proxySubPath: '/updated-path',
524+
proxySubPathPrefix: 'updated-prefix',
525+
},
526+
}
527+
528+
const app = await testApp({
529+
name: 'my-app',
530+
allExtensions: [appHome, appProxy, appAccess],
531+
configuration: {
532+
...DEFAULT_CONFIG,
533+
client_id: 'test-client-id',
534+
},
535+
devApplicationURLs,
536+
})
537+
538+
// When
539+
const manifest = await app.manifest()
540+
541+
// Then
542+
expect(manifest).toEqual({
543+
name: 'my-app',
544+
handle: '',
545+
modules: [
546+
{
547+
type: 'app_home_external',
548+
handle: 'app-home',
549+
uid: appHome.uid,
550+
assets: appHome.uid,
551+
target: appHome.contextValue,
552+
config: expect.objectContaining({
553+
app_url: 'https://new-url.io',
554+
}),
555+
},
556+
{
557+
type: 'app_proxy_external',
558+
handle: 'app-proxy',
559+
uid: appProxy.uid,
560+
assets: appProxy.uid,
561+
target: appProxy.contextValue,
562+
config: expect.objectContaining({
563+
url: 'https://new-proxy-url.io',
564+
subpath: '/updated-path',
565+
prefix: 'updated-prefix',
566+
}),
567+
},
568+
{
569+
type: 'app_access_external',
570+
handle: 'app-access',
571+
uid: appAccess.uid,
572+
assets: appAccess.uid,
573+
target: appAccess.contextValue,
574+
config: expect.objectContaining({
575+
redirect_url_allowlist: ['https://new-url.io/auth/callback'],
576+
}),
577+
},
578+
],
579+
})
580+
})
581+
582+
test('does not patch URLs when devApplicationURLs is not available', async () => {
583+
// Given
584+
const appHome = await testAppHomeConfigExtension()
585+
586+
const app = await testApp({
587+
name: 'my-app',
588+
allExtensions: [appHome],
589+
configuration: {
590+
...DEFAULT_CONFIG,
591+
client_id: 'test-client-id',
592+
},
593+
devApplicationURLs: undefined,
594+
})
595+
596+
// When
597+
const manifest = await app.manifest()
598+
599+
// Then
600+
expect(manifest).toEqual({
601+
name: 'my-app',
602+
handle: '',
603+
modules: [
604+
{
605+
type: 'app_home_external',
606+
handle: 'app-home',
607+
uid: appHome.uid,
608+
assets: appHome.uid,
609+
target: appHome.contextValue,
610+
config: {
611+
app_url: 'https://example.com',
612+
embedded: true,
613+
},
614+
},
615+
],
616+
})
617+
})
618+
})
619+
473620
function createPackageJson(tmpDir: string, type: string, version: string) {
474621
const packagePath = joinPath(tmpDir, 'node_modules', '@shopify', type, 'package.json')
475622
const packageJson = {name: 'name', version}

packages/app/src/cli/models/app/app.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ import {AppConfigurationUsedByCli} from '../extensions/specifications/types/app_
99
import {EditorExtensionCollectionType} from '../extensions/specifications/editor_extension_collection.js'
1010
import {UIExtensionSchema} from '../extensions/specifications/ui_extension.js'
1111
import {CreateAppOptions, Flag} from '../../utilities/developer-platform-client.js'
12-
import {AppAccessSpecIdentifier} from '../extensions/specifications/app_config_app_access.js'
12+
import appAccessSpec, {AppAccessSpecIdentifier} from '../extensions/specifications/app_config_app_access.js'
1313
import {WebhookSubscriptionSchema} from '../extensions/specifications/app_config_webhook_schemas/webhook_subscription_schema.js'
1414
import {configurationFileNames} from '../../constants.js'
15+
import {ApplicationURLs} from '../../services/dev/urls.js'
16+
import appHomeSpec from '../extensions/specifications/app_config_app_home.js'
17+
import appProxySpec from '../extensions/specifications/app_config_app_proxy.js'
1518
import {ZodObjectOf, zod} from '@shopify/cli-kit/node/schema'
1619
import {DotEnvFile} from '@shopify/cli-kit/node/dot-env'
1720
import {getDependencies, PackageManager, readAndParsePackageJson} from '@shopify/cli-kit/node/node-package-manager'
@@ -272,6 +275,7 @@ export interface AppInterface<
272275
errors?: AppErrors
273276
hiddenConfig: AppHiddenConfig
274277
includeConfigOnDeploy: boolean | undefined
278+
devApplicationURLs?: ApplicationURLs
275279
updateDependencies: () => Promise<void>
276280
extensionsForType: (spec: {identifier: string; externalIdentifier: string}) => ExtensionInstance[]
277281
updateExtensionUUIDS: (uuids: {[key: string]: string}) => void
@@ -307,6 +311,7 @@ type AppConstructor<
307311
specifications: ExtensionSpecification[]
308312
remoteFlags?: Flag[]
309313
hiddenConfig: AppHiddenConfig
314+
devApplicationURLs?: ApplicationURLs
310315
}
311316

312317
export class App<
@@ -329,6 +334,7 @@ export class App<
329334
remoteFlags: Flag[]
330335
realExtensions: ExtensionInstance[]
331336
hiddenConfig: AppHiddenConfig
337+
devApplicationURLs?: ApplicationURLs
332338

333339
constructor({
334340
name,
@@ -345,6 +351,7 @@ export class App<
345351
configSchema,
346352
remoteFlags,
347353
hiddenConfig,
354+
devApplicationURLs,
348355
}: AppConstructor<TConfig, TModuleSpec>) {
349356
this.name = name
350357
this.directory = directory
@@ -360,6 +367,7 @@ export class App<
360367
this.configSchema = configSchema ?? AppSchema
361368
this.remoteFlags = remoteFlags ?? []
362369
this.hiddenConfig = hiddenConfig
370+
this.devApplicationURLs = devApplicationURLs
363371
}
364372

365373
get allExtensions() {
@@ -396,10 +404,11 @@ export class App<
396404
}),
397405
)
398406
const realModules = getArrayRejectingUndefined(modules)
407+
const patchedModules = this.patchManifestWithDevURLs(realModules)
399408
return {
400409
name: this.name,
401410
handle: '',
402-
modules: realModules,
411+
modules: patchedModules,
403412
}
404413
}
405414

@@ -482,6 +491,37 @@ export class App<
482491
if (this.appManagementApiEnabled) return true
483492
return this.configuration.build?.include_config_on_deploy
484493
}
494+
495+
/**
496+
* Patches the manifest with the development URLs.
497+
* @param modules - All App modules
498+
* @returns All app modules with patches applied.
499+
*/
500+
private patchManifestWithDevURLs(modules: {type: string; config: JsonMapType}[]) {
501+
if (!this.devApplicationURLs) return modules
502+
503+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
504+
const appHome: any = modules.find((module) => module.type === appHomeSpec.externalIdentifier)
505+
if (appHome) {
506+
appHome.config.app_url = this.devApplicationURLs.applicationUrl
507+
}
508+
509+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
510+
const appProxy: any = modules.find((module) => module.type === appProxySpec.externalIdentifier)
511+
if (appProxy && this.devApplicationURLs?.appProxy) {
512+
appProxy.config.url = this.devApplicationURLs.appProxy.proxyUrl
513+
appProxy.config.subpath = this.devApplicationURLs.appProxy.proxySubPath
514+
appProxy.config.prefix = this.devApplicationURLs.appProxy.proxySubPathPrefix
515+
}
516+
517+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
518+
const appAccess: any = modules.find((module) => module.type === appAccessSpec.externalIdentifier)
519+
if (appAccess) {
520+
appAccess.config.redirect_url_allowlist = this.devApplicationURLs.redirectUrlWhitelist
521+
}
522+
523+
return modules
524+
}
485525
}
486526

487527
export function validateFunctionExtensionsWithUiHandle(

packages/app/src/cli/models/app/loader.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
373373
configSchema,
374374
remoteFlags: this.remoteFlags,
375375
hiddenConfig,
376+
devApplicationURLs: this.previousApp?.devApplicationURLs,
376377
})
377378

378379
// Show CLI notifications that are targetted for when your app has specific extension types

packages/app/src/cli/services/dev.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {
2-
FrontendURLOptions,
32
ApplicationURLs,
4-
generateFrontendURL,
3+
FrontendURLOptions,
54
generateApplicationURLs,
5+
generateFrontendURL,
66
getURLs,
77
shouldOrPromptUpdateURLs,
88
startTunnelPlugin,
@@ -278,7 +278,11 @@ async function handleUpdatingOfPartnerUrls(
278278
})
279279
// When running dev app urls are pushed directly to API Client config instead of creating a new app version
280280
// so current app version and API Client config will have diferent url values.
281-
if (shouldUpdateURLs) await updateURLs(newURLs, apiKey, developerPlatformClient, localApp)
281+
if (shouldUpdateURLs) {
282+
await updateURLs(newURLs, apiKey, developerPlatformClient, localApp)
283+
// eslint-disable-next-line require-atomic-updates
284+
localApp.devApplicationURLs = newURLs
285+
}
282286
}
283287
}
284288
return shouldUpdateURLs

0 commit comments

Comments
 (0)