Skip to content

Commit cdfe500

Browse files
committed
Add support for uploading instructions.md for ui extensions
1 parent 29d44c3 commit cdfe500

4 files changed

Lines changed: 376 additions & 11 deletions

File tree

packages/app/src/cli/models/extensions/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const NewExtensionPointSchema = zod.object({
4646
module: zod.string(),
4747
should_render: ShouldRenderSchema.optional(),
4848
tools: zod.string().optional(),
49+
instructions: zod.string().optional(),
4950
metafields: zod.array(MetafieldSchema).optional(),
5051
default_placement: zod.string().optional(),
5152
urls: zod

packages/app/src/cli/models/extensions/specification.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export enum AssetIdentifier {
3939
ShouldRender = 'should_render',
4040
Main = 'main',
4141
Tools = 'tools',
42+
Instructions = 'instructions',
4243
}
4344

4445
export interface Asset {

packages/app/src/cli/models/extensions/specifications/ui_extension.test.ts

Lines changed: 324 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,64 @@ describe('ui_extension', async () => {
7777
})
7878
}
7979

80+
async function setupToolsExtension(
81+
tmpDir: string,
82+
options: {tools?: string; instructions?: string; createFiles?: boolean} = {},
83+
) {
84+
await mkdir(joinPath(tmpDir, 'src'))
85+
await touchFile(joinPath(tmpDir, 'src', 'ExtensionPointA.js'))
86+
87+
if (options.createFiles) {
88+
if (options.tools) {
89+
await writeFile(joinPath(tmpDir, options.tools), '{"tools": []}')
90+
}
91+
if (options.instructions) {
92+
await writeFile(joinPath(tmpDir, options.instructions), '# Instructions')
93+
}
94+
}
95+
96+
const allSpecs = await loadLocalExtensionsSpecifications()
97+
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
98+
99+
const targetConfig: any = {
100+
target: 'EXTENSION::POINT::A',
101+
module: './src/ExtensionPointA.js',
102+
}
103+
104+
if (options.tools) targetConfig.tools = options.tools
105+
if (options.instructions) targetConfig.instructions = options.instructions
106+
107+
const configuration = {
108+
targeting: [targetConfig],
109+
api_version: '2023-01' as const,
110+
name: 'UI Extension',
111+
description: 'This is an ordinary test extension',
112+
type: 'ui_extension',
113+
handle: 'test-ui-extension',
114+
capabilities: {
115+
block_progress: false,
116+
network_access: false,
117+
api_access: false,
118+
collect_buyer_consent: {
119+
customer_privacy: true,
120+
sms_marketing: false,
121+
},
122+
iframe: {
123+
sources: [],
124+
},
125+
},
126+
settings: {},
127+
}
128+
129+
const parsed = specification.parseConfigurationObject(configuration)
130+
if (parsed.state !== 'ok') {
131+
throw new Error("Couldn't parse configuration")
132+
}
133+
134+
const result = await specification.validate?.(parsed.data, joinPath(tmpDir, 'shopify.extension.toml'), tmpDir)
135+
return {result, tmpDir}
136+
}
137+
80138
describe('validate()', () => {
81139
test('returns ok({}) if there are no errors', async () => {
82140
await inTemporaryDirectory(async (tmpDir) => {
@@ -487,6 +545,138 @@ describe('ui_extension', async () => {
487545
])
488546
})
489547

548+
test('build_manifest includes tools asset when tools is present', async () => {
549+
const allSpecs = await loadLocalExtensionsSpecifications()
550+
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
551+
const configuration = {
552+
targeting: [
553+
{
554+
target: 'EXTENSION::POINT::A',
555+
module: './src/ExtensionPointA.js',
556+
tools: './tools.json',
557+
},
558+
],
559+
api_version: '2023-01' as const,
560+
name: 'UI Extension',
561+
description: 'This is an ordinary test extension',
562+
type: 'ui_extension',
563+
handle: 'test-ui-extension',
564+
capabilities: {
565+
block_progress: false,
566+
network_access: false,
567+
api_access: false,
568+
collect_buyer_consent: {
569+
customer_privacy: true,
570+
sms_marketing: false,
571+
},
572+
iframe: {
573+
sources: [],
574+
},
575+
},
576+
settings: {},
577+
}
578+
579+
// When
580+
const parsed = specification.parseConfigurationObject(configuration)
581+
if (parsed.state !== 'ok') {
582+
throw new Error("Couldn't parse configuration")
583+
}
584+
585+
const got = parsed.data
586+
587+
// Then
588+
expect(got.extension_points).toStrictEqual([
589+
{
590+
target: 'EXTENSION::POINT::A',
591+
module: './src/ExtensionPointA.js',
592+
metafields: [],
593+
default_placement_reference: undefined,
594+
capabilities: undefined,
595+
preloads: {},
596+
build_manifest: {
597+
assets: {
598+
main: {
599+
filepath: 'test-ui-extension.js',
600+
module: './src/ExtensionPointA.js',
601+
},
602+
tools: {
603+
filepath: 'test-ui-extension-tools-tools.json',
604+
module: './tools.json',
605+
static: true,
606+
},
607+
},
608+
},
609+
urls: {},
610+
},
611+
])
612+
})
613+
614+
test('build_manifest includes instructions asset when instructions is present', async () => {
615+
const allSpecs = await loadLocalExtensionsSpecifications()
616+
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
617+
const configuration = {
618+
targeting: [
619+
{
620+
target: 'EXTENSION::POINT::A',
621+
module: './src/ExtensionPointA.js',
622+
instructions: './instructions.md',
623+
},
624+
],
625+
api_version: '2023-01' as const,
626+
name: 'UI Extension',
627+
description: 'This is an ordinary test extension',
628+
type: 'ui_extension',
629+
handle: 'test-ui-extension',
630+
capabilities: {
631+
block_progress: false,
632+
network_access: false,
633+
api_access: false,
634+
collect_buyer_consent: {
635+
customer_privacy: true,
636+
sms_marketing: false,
637+
},
638+
iframe: {
639+
sources: [],
640+
},
641+
},
642+
settings: {},
643+
}
644+
645+
// When
646+
const parsed = specification.parseConfigurationObject(configuration)
647+
if (parsed.state !== 'ok') {
648+
throw new Error("Couldn't parse configuration")
649+
}
650+
651+
const got = parsed.data
652+
653+
// Then
654+
expect(got.extension_points).toStrictEqual([
655+
{
656+
target: 'EXTENSION::POINT::A',
657+
module: './src/ExtensionPointA.js',
658+
metafields: [],
659+
default_placement_reference: undefined,
660+
capabilities: undefined,
661+
preloads: {},
662+
build_manifest: {
663+
assets: {
664+
main: {
665+
filepath: 'test-ui-extension.js',
666+
module: './src/ExtensionPointA.js',
667+
},
668+
instructions: {
669+
filepath: 'test-ui-extension-instructions-instructions.md',
670+
module: './instructions.md',
671+
static: true,
672+
},
673+
},
674+
},
675+
urls: {},
676+
},
677+
])
678+
})
679+
490680
test('returns error if there is no targeting or extension_points', async () => {
491681
// Given
492682
const allSpecs = await loadLocalExtensionsSpecifications()
@@ -545,7 +735,7 @@ describe('ui_extension', async () => {
545735

546736
expect(result).toEqual(
547737
err(`Couldn't find ${notFoundPath}
548-
Please check the module path for EXTENSION::POINT::A
738+
Please check the module path for EXTENSION::POINT::A
549739
550740
Please check the configuration in ${uiExtension.configurationPath}`),
551741
)
@@ -584,6 +774,139 @@ Please check the configuration in ${uiExtension.configurationPath}`),
584774
)
585775
})
586776
})
777+
778+
test('shows an error when the tools file is missing', async () => {
779+
await inTemporaryDirectory(async (tmpDir) => {
780+
const {result} = await setupToolsExtension(tmpDir, {tools: './tools.json'})
781+
782+
const notFoundPath = joinPath(tmpDir, './tools.json')
783+
expect(result).toEqual(
784+
err(`Couldn't find ${notFoundPath}
785+
Please check the tools path for EXTENSION::POINT::A
786+
787+
Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`),
788+
)
789+
})
790+
})
791+
792+
test('shows an error when the instructions file is missing', async () => {
793+
await inTemporaryDirectory(async (tmpDir) => {
794+
const {result} = await setupToolsExtension(tmpDir, {instructions: './instructions.md'})
795+
796+
const notFoundPath = joinPath(tmpDir, './instructions.md')
797+
expect(result).toEqual(
798+
err(`Couldn't find ${notFoundPath}
799+
Please check the instructions path for EXTENSION::POINT::A
800+
801+
Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`),
802+
)
803+
})
804+
})
805+
806+
test('shows multiple errors when both tools and instructions files are missing', async () => {
807+
await inTemporaryDirectory(async (tmpDir) => {
808+
const {result} = await setupToolsExtension(tmpDir, {
809+
tools: './tools.json',
810+
instructions: './instructions.md',
811+
})
812+
813+
const toolsNotFoundPath = joinPath(tmpDir, './tools.json')
814+
const instructionsNotFoundPath = joinPath(tmpDir, './instructions.md')
815+
expect(result).toEqual(
816+
err(`Couldn't find ${toolsNotFoundPath}
817+
Please check the tools path for EXTENSION::POINT::A
818+
819+
Couldn't find ${instructionsNotFoundPath}
820+
Please check the instructions path for EXTENSION::POINT::A
821+
822+
Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`),
823+
)
824+
})
825+
})
826+
827+
test('succeeds when both tools and instructions files exist', async () => {
828+
await inTemporaryDirectory(async (tmpDir) => {
829+
const {result} = await setupToolsExtension(tmpDir, {
830+
tools: './tools.json',
831+
instructions: './instructions.md',
832+
createFiles: true,
833+
})
834+
835+
expect(result).toStrictEqual(ok({}))
836+
})
837+
})
838+
839+
test('build_manifest includes both tools and instructions when both are present', async () => {
840+
const allSpecs = await loadLocalExtensionsSpecifications()
841+
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
842+
const configuration = {
843+
targeting: [
844+
{
845+
target: 'EXTENSION::POINT::A',
846+
module: './src/ExtensionPointA.js',
847+
tools: './tools.json',
848+
instructions: './instructions.md',
849+
},
850+
],
851+
api_version: '2023-01' as const,
852+
name: 'UI Extension',
853+
description: 'This is an ordinary test extension',
854+
type: 'ui_extension',
855+
handle: 'test-ui-extension',
856+
capabilities: {
857+
block_progress: false,
858+
network_access: false,
859+
api_access: false,
860+
collect_buyer_consent: {
861+
customer_privacy: true,
862+
sms_marketing: false,
863+
},
864+
iframe: {
865+
sources: [],
866+
},
867+
},
868+
settings: {},
869+
}
870+
871+
// When
872+
const parsed = specification.parseConfigurationObject(configuration)
873+
if (parsed.state !== 'ok') {
874+
throw new Error("Couldn't parse configuration")
875+
}
876+
877+
const got = parsed.data
878+
879+
// Then
880+
expect(got.extension_points).toStrictEqual([
881+
{
882+
target: 'EXTENSION::POINT::A',
883+
module: './src/ExtensionPointA.js',
884+
metafields: [],
885+
default_placement_reference: undefined,
886+
capabilities: undefined,
887+
preloads: {},
888+
build_manifest: {
889+
assets: {
890+
main: {
891+
filepath: 'test-ui-extension.js',
892+
module: './src/ExtensionPointA.js',
893+
},
894+
tools: {
895+
filepath: 'test-ui-extension-tools-tools.json',
896+
module: './tools.json',
897+
static: true,
898+
},
899+
instructions: {
900+
filepath: 'test-ui-extension-instructions-instructions.md',
901+
module: './instructions.md',
902+
static: true,
903+
},
904+
},
905+
},
906+
urls: {},
907+
},
908+
])
909+
})
587910
})
588911

589912
describe('deployConfig()', () => {

0 commit comments

Comments
 (0)