Skip to content

Commit 576467d

Browse files
authored
Merge pull request #7132 from Shopify/03-31-support_assets_for_admin_links_app_intents
Support assets for admin links app intents
2 parents f0d496f + cfea426 commit 576467d

5 files changed

Lines changed: 234 additions & 2 deletions

File tree

packages/app/src/cli/models/extensions/load-specifications.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import editorExtensionCollectionSpecification from './specifications/editor_exte
2929
import channelSpecificationSpec from './specifications/channel.js'
3030
import orderAttributionConfigSpec from './specifications/order_attribution_config.js'
3131
import adminSpecificationSpec from './specifications/admin.js'
32+
import adminLinkSpec from './specifications/admin_link.js'
3233

3334
const SORTED_CONFIGURATION_SPEC_IDENTIFIERS = [
3435
BrandingSpecIdentifier,
@@ -82,6 +83,7 @@ function loadSpecifications() {
8283
editorExtensionCollectionSpecification,
8384
channelSpecificationSpec,
8485
orderAttributionConfigSpec,
86+
adminLinkSpec,
8587
]
8688

8789
return [...configModuleSpecs, ...moduleSpecs] as ExtensionSpecification[]
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import * as loadLocales from '../../../utilities/extensions/locales-configuration.js'
2+
import {ExtensionInstance} from '../extension-instance.js'
3+
import {loadLocalExtensionsSpecifications} from '../load-specifications.js'
4+
import {placeholderAppConfiguration} from '../../app/app.test-data.js'
5+
import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs'
6+
import {joinPath} from '@shopify/cli-kit/node/path'
7+
import {describe, expect, test, vi} from 'vitest'
8+
9+
describe('admin_link', async () => {
10+
async function getTestAdminLink(directory: string, configuration: Record<string, unknown> = {}) {
11+
const configurationPath = joinPath(directory, 'shopify.extension.toml')
12+
const allSpecs = await loadLocalExtensionsSpecifications()
13+
const specification = allSpecs.find((spec) => spec.identifier === 'admin_link')!
14+
const parsed = specification.parseConfigurationObject(configuration)
15+
if (parsed.state !== 'ok') {
16+
throw new Error("Couldn't parse configuration")
17+
}
18+
19+
return new ExtensionInstance({
20+
configuration: parsed.data,
21+
directory,
22+
specification,
23+
configurationPath,
24+
entryPath: '',
25+
})
26+
}
27+
28+
test('has the correct identifier', async () => {
29+
await inTemporaryDirectory(async (tmpDir) => {
30+
const extension = await getTestAdminLink(tmpDir)
31+
expect(extension.specification.identifier).toBe('admin_link')
32+
})
33+
})
34+
35+
test('has localization and ui_preview in appModuleFeatures', async () => {
36+
await inTemporaryDirectory(async (tmpDir) => {
37+
const extension = await getTestAdminLink(tmpDir)
38+
expect(extension.specification.appModuleFeatures()).toContain('localization')
39+
expect(extension.specification.appModuleFeatures()).toContain('ui_preview')
40+
})
41+
})
42+
43+
test('is previewable', async () => {
44+
await inTemporaryDirectory(async (tmpDir) => {
45+
const extension = await getTestAdminLink(tmpDir)
46+
expect(extension.isPreviewable).toBe(true)
47+
})
48+
})
49+
50+
test('has include_assets client step with generatesAssetsManifest enabled', async () => {
51+
await inTemporaryDirectory(async (tmpDir) => {
52+
const extension = await getTestAdminLink(tmpDir)
53+
const clientSteps = extension.specification.clientSteps!
54+
expect(clientSteps).toHaveLength(1)
55+
expect(clientSteps[0]!.lifecycle).toBe('deploy')
56+
57+
const steps = clientSteps[0]!.steps
58+
expect(steps).toHaveLength(1)
59+
expect(steps[0]).toMatchObject({
60+
id: 'include-admin-link-assets',
61+
name: 'Include Admin Link Assets',
62+
type: 'include_assets',
63+
config: {
64+
generatesAssetsManifest: true,
65+
inclusions: [
66+
{type: 'configKey', anchor: 'targeting[]', groupBy: 'target', key: 'targeting[].tools'},
67+
{type: 'configKey', anchor: 'targeting[]', groupBy: 'target', key: 'targeting[].instructions'},
68+
{type: 'configKey', anchor: 'targeting[]', groupBy: 'target', key: 'targeting[].intents[].schema'},
69+
],
70+
},
71+
})
72+
})
73+
})
74+
75+
describe('deployConfig()', () => {
76+
test('includes localization in deploy config', async () => {
77+
await inTemporaryDirectory(async (tmpDir) => {
78+
const localization = {
79+
default_locale: 'en',
80+
translations: {title: 'Hello!'},
81+
}
82+
vi.spyOn(loadLocales, 'loadLocalesConfig').mockResolvedValue(localization)
83+
84+
const extension = await getTestAdminLink(tmpDir, {
85+
name: 'My Admin Link',
86+
targeting: [{url: 'https://example.com'}],
87+
})
88+
89+
const deployConfig = await extension.deployConfig({
90+
apiKey: 'apiKey',
91+
appConfiguration: placeholderAppConfiguration,
92+
})
93+
94+
expect(deployConfig).toMatchObject({localization})
95+
expect(loadLocales.loadLocalesConfig).toHaveBeenCalledWith(tmpDir, 'admin_link')
96+
})
97+
})
98+
99+
test('strips first-class fields from deploy config', async () => {
100+
await inTemporaryDirectory(async (tmpDir) => {
101+
vi.spyOn(loadLocales, 'loadLocalesConfig').mockResolvedValue({})
102+
103+
const extension = await getTestAdminLink(tmpDir, {
104+
type: 'admin_link',
105+
handle: 'my-link',
106+
name: 'My Admin Link',
107+
targeting: [{url: 'https://example.com'}],
108+
})
109+
110+
const deployConfig = await extension.deployConfig({
111+
apiKey: 'apiKey',
112+
appConfiguration: placeholderAppConfiguration,
113+
})
114+
115+
expect(deployConfig).not.toHaveProperty('type')
116+
expect(deployConfig).not.toHaveProperty('handle')
117+
expect(deployConfig).toHaveProperty('name', 'My Admin Link')
118+
expect(deployConfig).toHaveProperty('targeting')
119+
})
120+
})
121+
})
122+
})
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {createContractBasedModuleSpecification} from '../specification.js'
2+
3+
const adminLinkSpec = createContractBasedModuleSpecification({
4+
identifier: 'admin_link',
5+
clientSteps: [
6+
{
7+
lifecycle: 'deploy',
8+
steps: [
9+
{
10+
id: 'include-admin-link-assets',
11+
name: 'Include Admin Link Assets',
12+
type: 'include_assets',
13+
config: {
14+
generatesAssetsManifest: true,
15+
inclusions: [
16+
{
17+
type: 'configKey',
18+
anchor: 'targeting[]',
19+
groupBy: 'target',
20+
key: 'targeting[].tools',
21+
},
22+
{
23+
type: 'configKey',
24+
anchor: 'targeting[]',
25+
groupBy: 'target',
26+
key: 'targeting[].instructions',
27+
},
28+
{
29+
type: 'configKey',
30+
anchor: 'targeting[]',
31+
groupBy: 'target',
32+
key: 'targeting[].intents[].schema',
33+
},
34+
],
35+
},
36+
},
37+
],
38+
},
39+
],
40+
appModuleFeatures: () => ['localization', 'ui_preview'],
41+
})
42+
43+
export default adminLinkSpec

packages/app/src/cli/services/dev/extension/payload.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {getUIExtensionPayload} from './payload.js'
22
import {ExtensionsPayloadStoreOptions} from './payload/store.js'
33
import {testUIExtension} from '../../../models/app/app.test-data.js'
4+
import {ExtensionInstance} from '../../../models/extensions/extension-instance.js'
5+
import {loadLocalExtensionsSpecifications} from '../../../models/extensions/load-specifications.js'
46
import * as appModel from '../../../models/app/app.js'
57
import {describe, expect, test, vi, beforeEach} from 'vitest'
68
import {inTemporaryDirectory, mkdir, touchFile, writeFile} from '@shopify/cli-kit/node/fs'
@@ -57,6 +59,28 @@ describe('getUIExtensionPayload', () => {
5759
}
5860
}
5961

62+
async function testAdminLink(
63+
directory: string,
64+
configuration: Record<string, unknown>,
65+
overrides: {devUUID?: string} = {},
66+
) {
67+
const allSpecs = await loadLocalExtensionsSpecifications()
68+
const specification = allSpecs.find((spec) => spec.identifier === 'admin_link')!
69+
const parsed = specification.parseConfigurationObject(configuration)
70+
if (parsed.state !== 'ok') {
71+
throw new Error("Couldn't parse admin_link configuration")
72+
}
73+
const extension = new ExtensionInstance({
74+
configuration: parsed.data,
75+
directory,
76+
specification,
77+
configurationPath: joinPath(directory, 'shopify.extension.toml'),
78+
entryPath: '',
79+
})
80+
if (overrides.devUUID) extension.devUUID = overrides.devUUID
81+
return extension
82+
}
83+
6084
test('returns the right payload', async () => {
6185
await inTemporaryDirectory(async (tmpDir) => {
6286
const outputPath = joinPath(tmpDir, 'test-ui-extension.js')
@@ -367,6 +391,44 @@ describe('getUIExtensionPayload', () => {
367391
})
368392
})
369393

394+
test('reads from targeting when extension_points is not set (admin_link)', async () => {
395+
await inTemporaryDirectory(async (tmpDir) => {
396+
const adminLinkExtension = await testAdminLink(
397+
tmpDir,
398+
{
399+
name: 'test-admin-link',
400+
targeting: [{target: 'admin.app.intent.link', url: '/editor', tools: './tools.json'}],
401+
},
402+
{devUUID: 'devUUID'},
403+
)
404+
405+
await setupBuildOutput(
406+
adminLinkExtension,
407+
tmpDir,
408+
{'admin.app.intent.link': {tools: 'tools.json'}},
409+
{'tools.json': '{"tools": []}'},
410+
)
411+
412+
const got = await getUIExtensionPayload(adminLinkExtension, tmpDir, {
413+
...createMockOptions(tmpDir, [adminLinkExtension]),
414+
currentDevelopmentPayload: {hidden: true, status: 'success'},
415+
})
416+
417+
expect(got.extensionPoints).toMatchObject([
418+
{
419+
target: 'admin.app.intent.link',
420+
assets: {
421+
tools: {
422+
name: 'tools',
423+
url: 'http://tunnel-url.com/extensions/devUUID/assets/tools.json',
424+
lastUpdated: expect.any(Number),
425+
},
426+
},
427+
},
428+
])
429+
})
430+
})
431+
370432
test('returns the right payload for post-purchase extensions', async () => {
371433
await inTemporaryDirectory(async (tmpDir) => {
372434
const outputPath = joinPath(tmpDir, 'test-post-purchase-extension.js')

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ export async function getUIExtensionPayload(
3131
const url = `${options.url}/extensions/${extension.devUUID}`
3232
const {localization, status: localizationStatus} = await getLocalization(extension, options)
3333
const renderer = await getUIExtensionRendererVersion(extension)
34-
const buildDirectory = dirname(extensionOutputPath)
34+
// If the extension has a custom output relative path, use that as the build directory
35+
// ex. ext/dist/handle.js -> ext/dist
36+
const buildDirectory = extension.outputRelativePath ? dirname(extensionOutputPath) : extensionOutputPath
3537
const extensionPoints = await getExtensionPoints(extension, url, buildDirectory)
3638

3739
let metafields: {namespace: string; key: string}[] | null = null
@@ -103,7 +105,8 @@ export async function getUIExtensionPayload(
103105
}
104106

105107
async function getExtensionPoints(extension: ExtensionInstance, url: string, buildDirectory: string) {
106-
let extensionPoints = extension.configuration.extension_points as DevNewExtensionPointSchema[]
108+
const config = extension.configuration as Record<string, unknown>
109+
let extensionPoints = (config.extension_points ?? config.targeting) as DevNewExtensionPointSchema[]
107110

108111
if (extension.type === 'checkout_post_purchase') {
109112
// Mock target for post-purchase in order to get the right extension point redirect url

0 commit comments

Comments
 (0)