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
11 changes: 11 additions & 0 deletions packages/app/src/cli/models/extensions/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ const NewExtensionPointSchema = zod.object({
should_render: ShouldRenderSchema.optional(),
tools: zod.string().optional(),
instructions: zod.string().optional(),
intents: zod
.array(
zod.object({
type: zod.string(),
action: zod.string(),
schema: zod.string(),
name: zod.string().optional(),
description: zod.string().optional(),
}),
)
.optional(),
metafields: zod.array(MetafieldSchema).optional(),
default_placement: zod.string().optional(),
urls: zod
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/cli/models/extensions/specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export enum AssetIdentifier {
Main = 'main',
Tools = 'tools',
Instructions = 'instructions',
Intents = 'intents',
}

export interface Asset {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,63 +77,6 @@ describe('ui_extension', async () => {
})
}

async function setupToolsExtension(
tmpDir: string,
options: {tools?: string; instructions?: string; createFiles?: boolean} = {},
) {
await mkdir(joinPath(tmpDir, 'src'))
await touchFile(joinPath(tmpDir, 'src', 'ExtensionPointA.js'))

if (options.createFiles) {
if (options.tools) {
await writeFile(joinPath(tmpDir, options.tools), '{"tools": []}')
}
if (options.instructions) {
await writeFile(joinPath(tmpDir, options.instructions), '# Instructions')
}
}

const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!

const targetConfig: any = {
target: 'EXTENSION::POINT::A',
module: './src/ExtensionPointA.js',
}

if (options.tools) targetConfig.tools = options.tools
if (options.instructions) targetConfig.instructions = options.instructions

const configuration = {
targeting: [targetConfig],
api_version: '2023-01' as const,
name: 'UI Extension',
description: 'This is an ordinary test extension',
type: 'ui_extension',
handle: 'test-ui-extension',
capabilities: {
block_progress: false,
network_access: false,
api_access: false,
collect_buyer_consent: {
customer_privacy: true,
sms_marketing: false,
},
iframe: {
sources: [],
},
},
settings: {},
}

const parsed = specification.parseConfigurationObject(configuration)
if (parsed.state !== 'ok') {
throw new Error("Couldn't parse configuration")
}

const result = await specification.validate?.(parsed.data, joinPath(tmpDir, 'shopify.extension.toml'), tmpDir)
return {result, tmpDir}
}

describe('validate()', () => {
test('returns ok({}) if there are no errors', async () => {
Expand Down Expand Up @@ -208,6 +151,7 @@ describe('ui_extension', async () => {
target: 'EXTENSION::POINT::A',
tools: undefined,
instructions: undefined,
intents: undefined,
module: './src/ExtensionPointA.js',
metafields: [{namespace: 'test', key: 'test'}],
default_placement_reference: undefined,
Expand Down Expand Up @@ -275,6 +219,7 @@ describe('ui_extension', async () => {
target: 'EXTENSION::POINT::A',
tools: undefined,
instructions: undefined,
intents: undefined,
module: './src/ExtensionPointA.js',
metafields: [],
default_placement_reference: 'PLACEMENT_REFERENCE1',
Expand Down Expand Up @@ -338,6 +283,7 @@ describe('ui_extension', async () => {
target: 'EXTENSION::POINT::A',
tools: undefined,
instructions: undefined,
intents: undefined,
module: './src/ExtensionPointA.js',
metafields: [],
urls: {},
Expand Down Expand Up @@ -401,6 +347,7 @@ describe('ui_extension', async () => {
target: 'EXTENSION::POINT::A',
tools: undefined,
instructions: undefined,
intents: undefined,
module: './src/ExtensionPointA.js',
metafields: [],
default_placement_reference: undefined,
Expand Down Expand Up @@ -467,6 +414,7 @@ describe('ui_extension', async () => {
target: 'EXTENSION::POINT::A',
tools: undefined,
instructions: undefined,
intents: undefined,
module: './src/ExtensionPointA.js',
metafields: [],
default_placement_reference: undefined,
Expand Down Expand Up @@ -535,6 +483,7 @@ describe('ui_extension', async () => {
target: 'EXTENSION::POINT::A',
tools: undefined,
instructions: undefined,
intents: undefined,
module: './src/ExtensionPointA.js',
metafields: [],
default_placement_reference: undefined,
Expand Down Expand Up @@ -603,6 +552,7 @@ describe('ui_extension', async () => {
module: './src/ExtensionPointA.js',
tools: './tools.json',
instructions: undefined,
intents: undefined,
metafields: [],
default_placement_reference: undefined,
capabilities: undefined,
Expand All @@ -613,11 +563,6 @@ describe('ui_extension', async () => {
filepath: 'test-ui-extension.js',
module: './src/ExtensionPointA.js',
},
tools: {
filepath: 'test-ui-extension-tools-tools.json',
module: './tools.json',
static: true,
},
},
},
urls: {},
Expand Down Expand Up @@ -671,6 +616,7 @@ describe('ui_extension', async () => {
module: './src/ExtensionPointA.js',
tools: undefined,
instructions: './instructions.md',
intents: undefined,
metafields: [],
default_placement_reference: undefined,
capabilities: undefined,
Expand All @@ -681,11 +627,6 @@ describe('ui_extension', async () => {
filepath: 'test-ui-extension.js',
module: './src/ExtensionPointA.js',
},
instructions: {
filepath: 'test-ui-extension-instructions-instructions.md',
module: './instructions.md',
static: true,
},
},
},
urls: {},
Expand Down Expand Up @@ -791,67 +732,6 @@ Please check the configuration in ${uiExtension.configurationPath}`),
})
})

test('shows an error when the tools file is missing', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const {result} = await setupToolsExtension(tmpDir, {tools: './tools.json'})

const notFoundPath = joinPath(tmpDir, './tools.json')
expect(result).toEqual(
err(`Couldn't find ${notFoundPath}
Please check the tools path for EXTENSION::POINT::A

Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`),
)
})
})

test('shows an error when the instructions file is missing', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const {result} = await setupToolsExtension(tmpDir, {instructions: './instructions.md'})

const notFoundPath = joinPath(tmpDir, './instructions.md')
expect(result).toEqual(
err(`Couldn't find ${notFoundPath}
Please check the instructions path for EXTENSION::POINT::A

Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`),
)
})
})

test('shows multiple errors when both tools and instructions files are missing', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const {result} = await setupToolsExtension(tmpDir, {
tools: './tools.json',
instructions: './instructions.md',
})

const toolsNotFoundPath = joinPath(tmpDir, './tools.json')
const instructionsNotFoundPath = joinPath(tmpDir, './instructions.md')
expect(result).toEqual(
err(`Couldn't find ${toolsNotFoundPath}
Please check the tools path for EXTENSION::POINT::A

Couldn't find ${instructionsNotFoundPath}
Please check the instructions path for EXTENSION::POINT::A

Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`),
)
})
})

test('succeeds when both tools and instructions files exist', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const {result} = await setupToolsExtension(tmpDir, {
tools: './tools.json',
instructions: './instructions.md',
createFiles: true,
})

expect(result).toStrictEqual(ok({}))
})
})

test('build_manifest includes both tools and instructions when both are present', async () => {
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
Expand Down Expand Up @@ -899,6 +779,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`
module: './src/ExtensionPointA.js',
tools: './tools.json',
instructions: './instructions.md',
intents: undefined,
metafields: [],
default_placement_reference: undefined,
capabilities: undefined,
Expand All @@ -909,16 +790,6 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`
filepath: 'test-ui-extension.js',
module: './src/ExtensionPointA.js',
},
tools: {
filepath: 'test-ui-extension-tools-tools.json',
module: './tools.json',
static: true,
},
instructions: {
filepath: 'test-ui-extension-instructions-instructions.md',
module: './instructions.md',
static: true,
},
},
},
urls: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {ExtensionInstance} from '../extension-instance.js'
import {formatContent} from '../../../utilities/file-formatter.js'
import {err, ok, Result} from '@shopify/cli-kit/node/result'
import {copyFile, fileExists, readFile} from '@shopify/cli-kit/node/fs'
import {joinPath, basename, dirname} from '@shopify/cli-kit/node/path'
import {joinPath, dirname} from '@shopify/cli-kit/node/path'
import {outputContent, outputToken, outputWarn} from '@shopify/cli-kit/node/output'
import {zod} from '@shopify/cli-kit/node/schema'

Expand Down Expand Up @@ -61,24 +61,6 @@ export const UIExtensionSchema = BaseSchema.extend({
},
}
: null),
...(targeting.tools
? {
[AssetIdentifier.Tools]: {
filepath: `${config.handle}-${AssetIdentifier.Tools}-${basename(targeting.tools)}`,
module: targeting.tools,
static: true,
},
}
: null),
...(targeting.instructions
? {
[AssetIdentifier.Instructions]: {
filepath: `${config.handle}-${AssetIdentifier.Instructions}-${basename(targeting.instructions)}`,
module: targeting.instructions,
static: true,
},
}
: null),
},
}

Expand All @@ -93,6 +75,7 @@ export const UIExtensionSchema = BaseSchema.extend({
build_manifest: buildManifest,
tools: targeting.tools,
instructions: targeting.instructions,
intents: targeting.intents,
}
})
return {...config, extension_points: extensionPoints}
Expand All @@ -109,7 +92,35 @@ const uiExtensionSpec = createExtensionSpecification({
lifecycle: 'deploy',
steps: [
{id: 'bundle-ui', name: 'Bundle UI Extension', type: 'bundle_ui', config: {}},
{id: 'copy-static-assets', name: 'Copy Static Assets', type: 'copy_static_assets', config: {}},
{
id: 'include-ui-extension-assets',
name: 'Include UI Extension Assets',
type: 'include_assets',
config: {
generatesAssetsManifest: true,
builtAssets: {anchor: 'extension_points[]', groupBy: 'target', key: 'build_manifest'},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be also part of the inclusions, not on the top level. actually, i am wondering if it wouldn't work as a normal type configKey?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't work because the toml points to the raw module which we don't want to copy over. We need to the compiled asset copied over and there's no toml reference for that. The bundle_ui step already handles the building and copying. What we're missing is putting an entry for it inside the manifest.json for the generatesAssetsManifest step. If we can update bundle_ui to take ingeneratesAssetsManifestthat would work but right now the manifest.json does a replace only instead of each step adding to it.

inclusions: [
{
type: 'configKey',
anchor: 'extension_points[]',
groupBy: 'target',
key: 'extension_points[].tools',
},
{
type: 'configKey',
anchor: 'extension_points[]',
groupBy: 'target',
key: 'extension_points[].instructions',
},
{
type: 'configKey',
anchor: 'extension_points[]',
groupBy: 'target',
key: 'extension_points[].intents[].schema',
},
],
},
},
],
},
],
Expand Down Expand Up @@ -388,33 +399,13 @@ async function validateUIExtensionPointConfig(
}

for await (const extensionPoint of extensionPoints) {
const {module, target, build_manifest: buildManifest} = extensionPoint
const {module, target} = extensionPoint

const missingModuleError = await checkForMissingPath(directory, module, target, 'module')
if (missingModuleError) {
errors.push(missingModuleError)
}

const missingToolsError = await checkForMissingPath(
directory,
buildManifest?.assets[AssetIdentifier.Tools]?.module,
target,
AssetIdentifier.Tools,
)
if (missingToolsError) {
errors.push(missingToolsError)
}

const missingInstructionsError = await checkForMissingPath(
directory,
buildManifest?.assets[AssetIdentifier.Instructions]?.module,
target,
AssetIdentifier.Instructions,
)
if (missingInstructionsError) {
errors.push(missingInstructionsError)
}

if (uniqueTargets.includes(target)) {
duplicateTargets.push(target)
} else {
Expand Down
Loading
Loading