diff --git a/packages/app/src/cli/models/extensions/load-specifications.ts b/packages/app/src/cli/models/extensions/load-specifications.ts index 363362edda..427bc76003 100644 --- a/packages/app/src/cli/models/extensions/load-specifications.ts +++ b/packages/app/src/cli/models/extensions/load-specifications.ts @@ -21,6 +21,7 @@ import taxCalculationSpec from './specifications/tax_calculation.js' import themeSpec from './specifications/theme.js' import uiExtensionSpec from './specifications/ui_extension.js' import webPixelSpec from './specifications/web_pixel_extension.js' +import editorExtensionCollectionSpecification from './specifications/editor_extension_collection.js' const SORTED_CONFIGURATION_SPEC_IDENTIFIERS = [ BrandingSpecIdentifier, @@ -66,6 +67,7 @@ function loadSpecifications() { themeSpec, uiExtensionSpec, webPixelSpec, + editorExtensionCollectionSpecification, ] as ExtensionSpecification[] return [...configModuleSpecs, ...moduleSpecs] diff --git a/packages/app/src/cli/models/extensions/specifications/editor_extension_collection.test.ts b/packages/app/src/cli/models/extensions/specifications/editor_extension_collection.test.ts new file mode 100644 index 0000000000..84b1b3d97c --- /dev/null +++ b/packages/app/src/cli/models/extensions/specifications/editor_extension_collection.test.ts @@ -0,0 +1,124 @@ +import {ExtensionInstance} from '../extension-instance.js' +import {loadLocalExtensionsSpecifications} from '../load-specifications.js' +import {DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js' +import {testDeveloperPlatformClient} 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} from 'vitest' + +const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient() + +describe('editor_extension_collection', async () => { + interface EditorExtensionCollectionProps { + directory: string + configuration: {name: string; handle: string; includes?: string[]; include?: {handle: string}[]} + } + + async function getTestEditorExtensionCollection({ + directory, + configuration: passedConfig, + }: EditorExtensionCollectionProps) { + const configurationPath = joinPath(directory, 'shopify.extension.toml') + const allSpecs = await loadLocalExtensionsSpecifications() + const specification = allSpecs.find((spec) => spec.identifier === 'editor_extension_collection')! + const configuration = { + ...passedConfig, + type: 'editor_extension_collection', + metafields: [], + } + + return new ExtensionInstance({ + configuration, + directory, + specification, + configurationPath, + entryPath: '', + }) + } + + describe('deployConfig()', () => { + test('returns the deploy config when includes and include is passed in', async () => { + await inTemporaryDirectory(async (tmpDir) => { + const configuration = { + name: 'Order summary', + handle: 'order-summary-collection', + includes: ['handle1'], + include: [ + { + handle: 'handle2', + }, + ], + } + const extensionCollection = await getTestEditorExtensionCollection({ + directory: tmpDir, + configuration, + }) + + const deployConfig = await extensionCollection.deployConfig({ + apiKey: 'apiKey', + developerPlatformClient, + }) + + expect(deployConfig).toStrictEqual({ + name: extensionCollection.configuration.name, + handle: extensionCollection.configuration.handle, + in_collection: [{handle: 'handle1'}, {handle: 'handle2'}], + }) + }) + }) + + test('returns the deploy config when only include is passed in', async () => { + await inTemporaryDirectory(async (tmpDir) => { + const configuration = { + name: 'Order summary', + handle: 'order-summary-collection', + include: [ + { + handle: 'handle2', + }, + ], + } + const extensionCollection = await getTestEditorExtensionCollection({ + directory: tmpDir, + configuration, + }) + + const deployConfig = await extensionCollection.deployConfig({ + apiKey: 'apiKey', + developerPlatformClient, + }) + + expect(deployConfig).toStrictEqual({ + name: extensionCollection.configuration.name, + handle: extensionCollection.configuration.handle, + in_collection: [{handle: 'handle2'}], + }) + }) + }) + + test('returns the deploy config when only includes is passed in', async () => { + await inTemporaryDirectory(async (tmpDir) => { + const configuration = { + name: 'Order summary', + handle: 'order-summary-collection', + includes: ['handle1'], + } + const extensionCollection = await getTestEditorExtensionCollection({ + directory: tmpDir, + configuration, + }) + + const deployConfig = await extensionCollection.deployConfig({ + apiKey: 'apiKey', + developerPlatformClient, + }) + + expect(deployConfig).toStrictEqual({ + name: extensionCollection.configuration.name, + handle: extensionCollection.configuration.handle, + in_collection: [{handle: 'handle1'}], + }) + }) + }) + }) +}) diff --git a/packages/app/src/cli/models/extensions/specifications/editor_extension_collection.ts b/packages/app/src/cli/models/extensions/specifications/editor_extension_collection.ts new file mode 100644 index 0000000000..02a89c097a --- /dev/null +++ b/packages/app/src/cli/models/extensions/specifications/editor_extension_collection.ts @@ -0,0 +1,42 @@ +import {BaseSchema} from '../schemas.js' +import {createExtensionSpecification} from '../specification.js' +import {zod} from '@shopify/cli-kit/node/schema' + +interface IncludeSchema { + handle: string +} + +const IncludeSchema = zod.object({ + handle: zod.string(), +}) + +export const EditorExtensionCollectionSchema = BaseSchema.extend({ + include: zod.array(IncludeSchema).optional(), + includes: zod.array(zod.string()).optional(), + type: zod.literal('editor_extension_collection'), +}) + +const editorExtensionCollectionSpecification = createExtensionSpecification({ + identifier: 'editor_extension_collection', + schema: EditorExtensionCollectionSchema, + appModuleFeatures: (_) => [], + deployConfig: async (config, _) => { + const includes = + config.includes?.map((handle) => { + return {handle} + }) ?? [] + const include = config.include ?? [] + const inCollection = [...includes, ...include] + + // eslint-disable-next-line no-warning-comments + // TODO: Validation to check either one of include or includes was defined + + return { + name: config.name, + handle: config.handle, + in_collection: inCollection, + } + }, +}) + +export default editorExtensionCollectionSpecification