@@ -163,34 +163,85 @@ interface CreateTypeDefinitionOptions {
163163 targets : string [ ]
164164 apiVersion : string
165165 toolsTypeDefinition ?: string
166+ intentsTypeDefinition ?: string
166167}
167168
168- /**
169- * Builds the shopify API type based on targets and optional tools type.
170- * Returns null if no targets are provided.
171- */
172- function buildShopifyType ( targets : string [ ] , toolsTypeDefinition ?: string ) : string | null {
173- const toolsSuffix = toolsTypeDefinition ? ' & { tools: ShopifyTools }' : ''
169+ interface ShopifyTypeOptions {
170+ includesTools : boolean
171+ includesIntents : boolean
172+ }
174173
174+ function buildBaseShopifyType ( targets : string [ ] ) : string | null {
175175 if ( targets . length === 1 ) {
176176 const target = targets [ 0 ] ?? ''
177- return `import('@shopify/ui-extensions/${ target } ').Api${ toolsSuffix } `
177+ return `import('@shopify/ui-extensions/${ target } ').Api`
178178 }
179179
180180 if ( targets . length > 1 ) {
181181 const unionType = targets . map ( ( target ) => `import('@shopify/ui-extensions/${ target } ').Api` ) . join ( ' | ' )
182- return `(${ unionType } )${ toolsSuffix } `
182+ return `(${ unionType } )`
183183 }
184184
185185 return null
186186}
187187
188+ /**
189+ * Builds the shopify API type based on targets and optional generated tool / intent types.
190+ * Returns null if no targets are provided.
191+ */
192+ function buildShopifyType ( targets : string [ ] , { includesTools, includesIntents} : ShopifyTypeOptions ) : string | null {
193+ const baseShopifyType = buildBaseShopifyType ( targets )
194+ if ( ! baseShopifyType ) return null
195+
196+ if ( ! includesTools && ! includesIntents ) {
197+ return baseShopifyType
198+ }
199+
200+ const wrappers = [
201+ ...( includesIntents ? [ 'WithGeneratedIntents' ] : [ ] ) ,
202+ ...( includesTools ? [ 'WithGeneratedTools' ] : [ ] ) ,
203+ ]
204+
205+ return wrappers . reduce ( ( shopifyType , wrapper ) => `${ wrapper } <${ shopifyType } >` , baseShopifyType )
206+ }
207+
208+ function buildShopifyUtilityTypes ( { includesTools, includesIntents} : ShopifyTypeOptions ) : string {
209+ const utilityTypes : string [ ] = [ ]
210+
211+ if ( includesTools ) {
212+ utilityTypes . push ( `type WithGeneratedTools<T> = T extends {tools?: infer Tools}
213+ ? Omit<T, 'tools'> & {tools: Omit<NonNullable<Tools>, 'register'> & ShopifyTools}
214+ : T & {tools: ShopifyTools}` )
215+ }
216+
217+ if ( includesIntents ) {
218+ utilityTypes . push ( `type MergeGeneratedIntentResponse<Intents> = ShopifyGeneratedIntentsApi extends infer Generated
219+ ? Generated extends {response?: infer GeneratedResponse}
220+ ? Omit<Generated, 'response'> & {
221+ response?: Intents extends {response?: infer BaseResponse}
222+ ? Omit<NonNullable<BaseResponse>, 'ok'> & NonNullable<GeneratedResponse>
223+ : NonNullable<GeneratedResponse>
224+ }
225+ : Generated
226+ : never` )
227+
228+ utilityTypes . push ( `type WithGeneratedIntents<T> = T extends {intents?: infer Intents}
229+ ? Omit<T, 'intents'> & {
230+ intents: Omit<NonNullable<Intents>, 'request' | 'response'> & MergeGeneratedIntentResponse<NonNullable<Intents>>
231+ }
232+ : T & {intents: ShopifyGeneratedIntentsApi}` )
233+ }
234+
235+ return utilityTypes . join ( '\n\n' )
236+ }
237+
188238export function createTypeDefinition ( {
189239 fullPath,
190240 typeFilePath,
191241 targets,
192242 apiVersion,
193243 toolsTypeDefinition,
244+ intentsTypeDefinition,
194245} : CreateTypeDefinitionOptions ) : string | null {
195246 try {
196247 // Validate that all targets can be resolved
@@ -208,14 +259,20 @@ export function createTypeDefinition({
208259 }
209260
210261 const relativePath = relativizePath ( fullPath , dirname ( typeFilePath ) )
262+ const includesTools = Boolean ( toolsTypeDefinition )
263+ const includesIntents = Boolean ( intentsTypeDefinition )
211264
212- const shopifyType = buildShopifyType ( targets , toolsTypeDefinition )
265+ const shopifyType = buildShopifyType ( targets , { includesTools , includesIntents } )
213266 if ( ! shopifyType ) return null
214267
268+ const shopifyUtilityTypes = buildShopifyUtilityTypes ( { includesTools, includesIntents} )
269+
215270 const lines = [
216271 '//@ts-ignore' ,
217272 `declare module './${ relativePath } ' {` ,
218- ...( toolsTypeDefinition ? [ ` ${ toolsTypeDefinition } ` ] : [ ] ) ,
273+ ...( toolsTypeDefinition ? [ toolsTypeDefinition ] : [ ] ) ,
274+ ...( intentsTypeDefinition ? [ intentsTypeDefinition ] : [ ] ) ,
275+ ...( shopifyUtilityTypes ? [ shopifyUtilityTypes ] : [ ] ) ,
219276 ` const shopify: ${ shopifyType } ;` ,
220277 ' const globalThis: { shopify: typeof shopify };' ,
221278 '}' ,
@@ -269,6 +326,92 @@ const ToolDefinitionSchema: zod.ZodType<ToolDefinition> = zod.object({
269326
270327export const ToolsFileSchema = zod . array ( ToolDefinitionSchema )
271328
329+ interface IntentTypeDefinition {
330+ action : string
331+ type : string
332+ inputSchema : object
333+ valueSchema ?: object
334+ outputSchema ?: object
335+ }
336+
337+ interface IntentSchemaFile {
338+ value ?: object
339+ inputSchema : object
340+ outputSchema ?: object
341+ }
342+
343+ export const IntentSchemaFileSchema : zod . ZodType < IntentSchemaFile > = zod . object ( {
344+ value : zod . object ( { } ) . passthrough ( ) . optional ( ) ,
345+ inputSchema : zod . object ( { } ) . passthrough ( ) ,
346+ outputSchema : zod . object ( { } ) . passthrough ( ) . optional ( ) ,
347+ } )
348+
349+ function intentTypeBaseName ( intent : Pick < IntentTypeDefinition , 'action' | 'type' > ) : string {
350+ return pascalize ( `${ intent . action } ${ intent . type } ` . replace ( / [ ^ a - z A - Z 0 - 9 ] + / g, ' ' ) )
351+ }
352+
353+ /**
354+ * Generates TypeScript types for shopify.intents.request and shopify.intents.response.ok
355+ * based on intent schema definitions.
356+ */
357+ export async function createIntentsTypeDefinition ( intents : IntentTypeDefinition [ ] ) : Promise < string > {
358+ if ( intents . length === 0 ) return ''
359+
360+ const intentKeys = new Set < string > ( )
361+ const typePromises = intents . map ( async ( intent ) => {
362+ const intentKey = `${ intent . action } :${ intent . type } `
363+ if ( intentKeys . has ( intentKey ) ) {
364+ throw new AbortError ( `Intent "${ intentKey } " is defined multiple times. Intents must be unique within a target.` )
365+ }
366+ intentKeys . add ( intentKey )
367+
368+ const typeBaseName = intentTypeBaseName ( intent )
369+ const inputTypeName = `${ typeBaseName } IntentInput`
370+ const valueTypeName = `${ typeBaseName } IntentValue`
371+ const outputTypeName = `${ typeBaseName } IntentOutput`
372+ const requestTypeName = `${ typeBaseName } IntentRequest`
373+
374+ const inputType = await formatJsonSchemaType ( inputTypeName , intent . inputSchema )
375+ const valueType = await formatJsonSchemaType ( valueTypeName , intent . valueSchema )
376+ const outputType = await formatJsonSchemaType ( outputTypeName , intent . outputSchema )
377+
378+ const requestType = `interface ${ requestTypeName } {
379+ action: ${ JSON . stringify ( intent . action ) } ;
380+ type: ${ JSON . stringify ( intent . type ) } ;
381+ data: ${ inputTypeName } ;
382+ value?: ${ valueTypeName } ;
383+ }`
384+
385+ return {
386+ inputType,
387+ valueType,
388+ outputType,
389+ requestType,
390+ requestTypeName,
391+ outputTypeName,
392+ }
393+ } )
394+
395+ const types = await Promise . all ( typePromises )
396+
397+ const generatedIntents = types
398+ . map ( ( { requestTypeName, outputTypeName} ) => {
399+ return ` | {
400+ request: ${ requestTypeName } ;
401+ response?: ShopifyGeneratedIntentResponse<${ outputTypeName } >;
402+ }`
403+ } )
404+ . join ( '\n' )
405+
406+ return `${ types
407+ . map (
408+ ( { inputType, valueType, outputType, requestType} ) => `${ inputType } \n${ valueType } \n${ outputType } \n${ requestType } ` ,
409+ )
410+ . join ( '\n\n' ) } \n\ntype ShopifyGeneratedIntentResponse<Data = unknown> = {
411+ ok(data?: Data): Promise<void>;
412+ }\n\ntype ShopifyGeneratedIntentsApi =\n${ generatedIntents } \n`
413+ }
414+
272415/**
273416 * Generates TypeScript types for shopify.tools.register based on tool definitions
274417 * @param tools - Array of tool definitions from tools.json
@@ -323,8 +466,15 @@ export async function createToolsTypeDefinition(tools: ToolDefinition[]): Promis
323466 . join ( '\n' ) } \ninterface ShopifyTools {\n${ toolRegistrations } \n}\n`
324467}
325468
469+ function renameGeneratedType ( typeDefinition : string , name : string ) : string {
470+ return typeDefinition . replace ( / ^ ( i n t e r f a c e | t y p e | e n u m ) \s + [ A - Z a - z 0 - 9 _ ] + / , `$1 ${ name } ` )
471+ }
472+
326473async function formatJsonSchemaType ( name : string , schema ?: object ) : Promise < string > {
327- const outputType = schema ? await compile ( schema , name , { bannerComment : '' } ) : `type ${ name } = unknown`
328- // The json-schema-to-typescript library adds an export keyword to the type definition, we need to remove it
329- return outputType . startsWith ( 'export ' ) ? outputType . slice ( 7 ) : outputType
474+ if ( ! schema ) return `type ${ name } = unknown`
475+
476+ const outputType = await compile ( schema , name , { bannerComment : '' } )
477+ const normalizedOutputType = outputType . startsWith ( 'export ' ) ? outputType . slice ( 7 ) : outputType
478+
479+ return renameGeneratedType ( normalizedOutputType , name )
330480}
0 commit comments