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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import webPixelSpec from './specifications/web_pixel_extension.js'
import editorExtensionCollectionSpecification from './specifications/editor_extension_collection.js'
import channelSpecificationSpec from './specifications/channel.js'
import adminSpecificationSpec from './specifications/admin.js'
import adminLinkSpec from './specifications/admin_link.js'

const SORTED_CONFIGURATION_SPEC_IDENTIFIERS = [
BrandingSpecIdentifier,
Expand Down Expand Up @@ -80,6 +81,7 @@ function loadSpecifications() {
webPixelSpec,
editorExtensionCollectionSpecification,
channelSpecificationSpec,
adminLinkSpec,
]

return [...configModuleSpecs, ...moduleSpecs] as ExtensionSpecification[]
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/cli/models/extensions/specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ export function createContractBasedModuleSpecification<TConfiguration extends Ba
| 'clientSteps'
| 'experience'
| 'transformRemoteToLocal'
| 'getOutputRelativePath'
>,
) {
return createExtensionSpecification({
Expand All @@ -305,6 +306,7 @@ export function createContractBasedModuleSpecification<TConfiguration extends Ba
buildConfig: spec.buildConfig ?? {mode: 'none'},
clientSteps: spec.clientSteps,
uidStrategy: spec.uidStrategy,
getOutputRelativePath: spec.getOutputRelativePath,
transformRemoteToLocal: spec.transformRemoteToLocal,
deployConfig: async (config, directory) => {
let parsedConfig = configWithoutFirstClassFields(config)
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 generateManifest 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', key: 'targeting[].tools'},
{type: 'configKey', key: 'targeting[].instructions'},
{type: 'configKey', 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,49 @@
import {createContractBasedModuleSpecification} from '../specification.js'

const adminLinkSpec = createContractBasedModuleSpecification({
identifier: 'admin_link',
buildConfig: {
mode: 'copy_files',
filePatterns: [],
},
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',
},
],
},
},
],
},
],
// This needs to be a file path so the extension directory can be resolved to the correct path
getOutputRelativePath: () => 'shopify.extension.toml',
appModuleFeatures: () => ['localization', 'ui_preview'],
})

export default adminLinkSpec
40 changes: 39 additions & 1 deletion packages/app/src/cli/services/dev/extension/payload.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {UIExtensionPayload} from './payload/models.js'
import {getUIExtensionPayload} from './payload.js'
import {ExtensionsPayloadStoreOptions} from './payload/store.js'
import {testUIExtension} from '../../../models/app/app.test-data.js'
Expand Down Expand Up @@ -244,6 +243,45 @@ describe('getUIExtensionPayload', () => {
})
})

test('reads from targeting when extension_points is not set', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const uiExtension = await testUIExtension({
directory: tmpDir,
configuration: {
name: 'test-admin-link',
type: 'admin_link',
targeting: [{target: 'admin.app.link', url: '/editor', tools: './tools.json'}],
} as any,
devUUID: 'devUUID',
})

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

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

expect(got.extensionPoints).toMatchObject([
{
target: 'admin.app.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
3 changes: 2 additions & 1 deletion packages/app/src/cli/services/dev/extension/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,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