Skip to content

Commit 5d1a2ca

Browse files
committed
Patch the app manifest with the development URLs, add tests
1 parent 04b045f commit 5d1a2ca

File tree

5 files changed

+266
-6
lines changed

5 files changed

+266
-6
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: 160 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,163 @@ describe('allExtensions', () => {
470473
})
471474
})
472475

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

0 commit comments

Comments
 (0)