Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import editorExtensionCollectionSpecification from './specifications/editor_exte
import channelSpecificationSpec from './specifications/channel.js'
import orderAttributionConfigSpec from './specifications/order_attribution_config.js'
import adminSpecificationSpec from './specifications/admin.js'
import adminLinkSpec from './specifications/admin_link.js'

const SORTED_CONFIGURATION_SPEC_IDENTIFIERS = [
BrandingSpecIdentifier,
Expand Down Expand Up @@ -82,6 +83,7 @@ function loadSpecifications() {
editorExtensionCollectionSpecification,
channelSpecificationSpec,
orderAttributionConfigSpec,
adminLinkSpec,
]

return [...configModuleSpecs, ...moduleSpecs] as ExtensionSpecification[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as loadLocales from '../../../utilities/extensions/locales-configuration.js'
import {ExtensionInstance} from '../extension-instance.js'
import {loadLocalExtensionsSpecifications} from '../load-specifications.js'
import {placeholderAppConfiguration} from '../../app/app.test-data.js'
import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs'
import {joinPath} from '@shopify/cli-kit/node/path'
import {describe, expect, test, vi} from 'vitest'

describe('admin_link', async () => {
async function getTestAdminLink(directory: string, configuration: Record<string, unknown> = {}) {
const configurationPath = joinPath(directory, 'shopify.extension.toml')
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'admin_link')!
const parsed = specification.parseConfigurationObject(configuration)
if (parsed.state !== 'ok') {
throw new Error("Couldn't parse configuration")
}

return new ExtensionInstance({
configuration: parsed.data,
directory,
specification,
configurationPath,
entryPath: '',
})
}

test('has the correct identifier', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const extension = await getTestAdminLink(tmpDir)
expect(extension.specification.identifier).toBe('admin_link')
})
})

test('has localization and ui_preview in appModuleFeatures', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const extension = await getTestAdminLink(tmpDir)
expect(extension.specification.appModuleFeatures()).toContain('localization')
expect(extension.specification.appModuleFeatures()).toContain('ui_preview')
})
})

test('is previewable', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const extension = await getTestAdminLink(tmpDir)
expect(extension.isPreviewable).toBe(true)
})
})

test('has include_assets client step with generatesAssetsManifest enabled', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const extension = await getTestAdminLink(tmpDir)
const clientSteps = extension.specification.clientSteps!
expect(clientSteps).toHaveLength(1)
expect(clientSteps[0]!.lifecycle).toBe('deploy')

const steps = clientSteps[0]!.steps
expect(steps).toHaveLength(1)
expect(steps[0]).toMatchObject({
id: 'include-admin-link-assets',
name: 'Include Admin Link Assets',
type: 'include_assets',
config: {
generatesAssetsManifest: true,
inclusions: [
{type: 'configKey', anchor: 'targeting[]', groupBy: 'target', key: 'targeting[].tools'},
{type: 'configKey', anchor: 'targeting[]', groupBy: 'target', key: 'targeting[].instructions'},
{type: 'configKey', anchor: 'targeting[]', groupBy: 'target', key: 'targeting[].intents[].schema'},
],
},
})
})
})

describe('deployConfig()', () => {
test('includes localization in deploy config', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const localization = {
default_locale: 'en',
translations: {title: 'Hello!'},
}
vi.spyOn(loadLocales, 'loadLocalesConfig').mockResolvedValue(localization)

const extension = await getTestAdminLink(tmpDir, {
name: 'My Admin Link',
targeting: [{url: 'https://example.com'}],
})

const deployConfig = await extension.deployConfig({
apiKey: 'apiKey',
appConfiguration: placeholderAppConfiguration,
})

expect(deployConfig).toMatchObject({localization})
expect(loadLocales.loadLocalesConfig).toHaveBeenCalledWith(tmpDir, 'admin_link')
})
})

test('strips first-class fields from deploy config', async () => {
await inTemporaryDirectory(async (tmpDir) => {
vi.spyOn(loadLocales, 'loadLocalesConfig').mockResolvedValue({})

const extension = await getTestAdminLink(tmpDir, {
type: 'admin_link',
handle: 'my-link',
name: 'My Admin Link',
targeting: [{url: 'https://example.com'}],
})

const deployConfig = await extension.deployConfig({
apiKey: 'apiKey',
appConfiguration: placeholderAppConfiguration,
})

expect(deployConfig).not.toHaveProperty('type')
expect(deployConfig).not.toHaveProperty('handle')
expect(deployConfig).toHaveProperty('name', 'My Admin Link')
expect(deployConfig).toHaveProperty('targeting')
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {createContractBasedModuleSpecification} from '../specification.js'

const adminLinkSpec = createContractBasedModuleSpecification({
identifier: 'admin_link',
clientSteps: [
{
lifecycle: 'deploy',
steps: [
{
id: 'include-admin-link-assets',
name: 'Include Admin Link Assets',
type: 'include_assets',
config: {
generatesAssetsManifest: true,
inclusions: [
{
type: 'configKey',
anchor: 'targeting[]',
groupBy: 'target',
key: 'targeting[].tools',
},
{
type: 'configKey',
anchor: 'targeting[]',
groupBy: 'target',
key: 'targeting[].instructions',
},
{
type: 'configKey',
anchor: 'targeting[]',
groupBy: 'target',
key: 'targeting[].intents[].schema',
},
],
},
},
],
},
],
appModuleFeatures: () => ['localization', 'ui_preview'],
})

export default adminLinkSpec
62 changes: 62 additions & 0 deletions packages/app/src/cli/services/dev/extension/payload.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {getUIExtensionPayload} from './payload.js'
import {ExtensionsPayloadStoreOptions} from './payload/store.js'
import {testUIExtension} from '../../../models/app/app.test-data.js'
import {ExtensionInstance} from '../../../models/extensions/extension-instance.js'
import {loadLocalExtensionsSpecifications} from '../../../models/extensions/load-specifications.js'
import * as appModel from '../../../models/app/app.js'
import {describe, expect, test, vi, beforeEach} from 'vitest'
import {inTemporaryDirectory, mkdir, touchFile, writeFile} from '@shopify/cli-kit/node/fs'
Expand Down Expand Up @@ -57,6 +59,28 @@ describe('getUIExtensionPayload', () => {
}
}

async function testAdminLink(
directory: string,
configuration: Record<string, unknown>,
overrides: {devUUID?: string} = {},
) {
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'admin_link')!
const parsed = specification.parseConfigurationObject(configuration)
if (parsed.state !== 'ok') {
throw new Error("Couldn't parse admin_link configuration")
}
const extension = new ExtensionInstance({
configuration: parsed.data,
directory,
specification,
configurationPath: joinPath(directory, 'shopify.extension.toml'),
entryPath: '',
})
if (overrides.devUUID) extension.devUUID = overrides.devUUID
return extension
}

test('returns the right payload', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const outputPath = joinPath(tmpDir, 'test-ui-extension.js')
Expand Down Expand Up @@ -367,6 +391,44 @@ describe('getUIExtensionPayload', () => {
})
})

test('reads from targeting when extension_points is not set (admin_link)', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const adminLinkExtension = await testAdminLink(
tmpDir,
{
name: 'test-admin-link',
targeting: [{target: 'admin.app.intent.link', url: '/editor', tools: './tools.json'}],
},
{devUUID: 'devUUID'},
)

await setupBuildOutput(
adminLinkExtension,
tmpDir,
{'admin.app.intent.link': {tools: 'tools.json'}},
{'tools.json': '{"tools": []}'},
)

const got = await getUIExtensionPayload(adminLinkExtension, tmpDir, {
...createMockOptions(tmpDir, [adminLinkExtension]),
currentDevelopmentPayload: {hidden: true, status: 'success'},
})

expect(got.extensionPoints).toMatchObject([
{
target: 'admin.app.intent.link',
assets: {
tools: {
name: 'tools',
url: 'http://tunnel-url.com/extensions/devUUID/assets/tools.json',
lastUpdated: expect.any(Number),
},
},
},
])
})
})

test('returns the right payload for post-purchase extensions', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const outputPath = joinPath(tmpDir, 'test-post-purchase-extension.js')
Expand Down
7 changes: 5 additions & 2 deletions packages/app/src/cli/services/dev/extension/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export async function getUIExtensionPayload(
const url = `${options.url}/extensions/${extension.devUUID}`
const {localization, status: localizationStatus} = await getLocalization(extension, options)
const renderer = await getUIExtensionRendererVersion(extension)
const buildDirectory = dirname(extensionOutputPath)
// If the extension has a custom output relative path, use that as the build directory
// ex. ext/dist/handle.js -> ext/dist
const buildDirectory = extension.outputRelativePath ? dirname(extensionOutputPath) : extensionOutputPath
const extensionPoints = await getExtensionPoints(extension, url, buildDirectory)

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

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

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