@@ -332,6 +332,147 @@ describe('app-event-watcher', () => {
332332 )
333333 } )
334334
335+ describe ( 'generateExtensionTypes' , ( ) => {
336+ test ( 'is called after extensions are rebuilt on file changes' , async ( ) => {
337+ await inTemporaryDirectory ( async ( tmpDir ) => {
338+ const fileWatchEvent : WatcherEvent = {
339+ type : 'file_updated' ,
340+ path : '/extensions/ui_extension_1/src/file.js' ,
341+ extensionPath : '/extensions/ui_extension_1' ,
342+ startTime : [ 0 , 0 ] ,
343+ }
344+
345+ // Given
346+ const buildOutputPath = joinPath ( tmpDir , '.shopify' , 'bundle' )
347+ const app = testAppLinked ( {
348+ allExtensions : [ extension1 ] ,
349+ configuration : { scopes : '' , extension_directories : [ ] , path : 'shopify.app.custom.toml' } ,
350+ } )
351+ const generateTypesSpy = vi . spyOn ( app , 'generateExtensionTypes' )
352+
353+ const mockManager = new MockESBuildContextManager ( )
354+ const mockFileWatcher = new MockFileWatcher ( app , outputOptions , [ fileWatchEvent ] )
355+ const watcher = new AppEventWatcher ( app , 'url' , buildOutputPath , mockManager , mockFileWatcher )
356+
357+ // When
358+ await watcher . start ( { stdout, stderr, signal : abortController . signal } )
359+ await flushPromises ( )
360+
361+ // Wait for event processing
362+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) )
363+
364+ // Then
365+ expect ( generateTypesSpy ) . toHaveBeenCalled ( )
366+ } )
367+ } )
368+
369+ test ( 'is not called again when extensions are created (already called during app reload)' , async ( ) => {
370+ await inTemporaryDirectory ( async ( tmpDir ) => {
371+ const fileWatchEvent : WatcherEvent = {
372+ type : 'extension_folder_created' ,
373+ path : '/extensions/ui_extension_2' ,
374+ extensionPath : '/extensions/ui_extension_2' ,
375+ startTime : [ 0 , 0 ] ,
376+ }
377+
378+ // Given
379+ const mockedApp = testAppLinked ( { allExtensions : [ extension1 , extension2 ] } )
380+ const generateTypesSpy = vi . spyOn ( mockedApp , 'generateExtensionTypes' )
381+ vi . mocked ( reloadApp ) . mockResolvedValue ( mockedApp )
382+
383+ const buildOutputPath = joinPath ( tmpDir , '.shopify' , 'bundle' )
384+ const app = testAppLinked ( {
385+ allExtensions : [ extension1 ] ,
386+ configuration : { scopes : '' , extension_directories : [ ] , path : 'shopify.app.custom.toml' } ,
387+ } )
388+
389+ const mockManager = new MockESBuildContextManager ( )
390+ const mockFileWatcher = new MockFileWatcher ( app , outputOptions , [ fileWatchEvent ] )
391+ const watcher = new AppEventWatcher ( app , 'url' , buildOutputPath , mockManager , mockFileWatcher )
392+
393+ // When
394+ await watcher . start ( { stdout, stderr, signal : abortController . signal } )
395+ await flushPromises ( )
396+
397+ // Wait for event processing
398+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) )
399+
400+ // Then - not called in watcher because it was already called during reloadApp
401+ expect ( generateTypesSpy ) . not . toHaveBeenCalled ( )
402+ } )
403+ } )
404+
405+ test ( 'is not called again when app config is updated (already called during app reload)' , async ( ) => {
406+ await inTemporaryDirectory ( async ( tmpDir ) => {
407+ const fileWatchEvent : WatcherEvent = {
408+ type : 'extensions_config_updated' ,
409+ path : 'shopify.app.custom.toml' ,
410+ extensionPath : '/' ,
411+ startTime : [ 0 , 0 ] ,
412+ }
413+
414+ // Given
415+ const mockedApp = testAppLinked ( { allExtensions : [ extension1 , posExtensionUpdated ] } )
416+ const generateTypesSpy = vi . spyOn ( mockedApp , 'generateExtensionTypes' )
417+ vi . mocked ( reloadApp ) . mockResolvedValue ( mockedApp )
418+
419+ const buildOutputPath = joinPath ( tmpDir , '.shopify' , 'bundle' )
420+ const app = testAppLinked ( {
421+ allExtensions : [ extension1 , posExtension ] ,
422+ configuration : { scopes : '' , extension_directories : [ ] , path : 'shopify.app.custom.toml' } ,
423+ } )
424+
425+ const mockManager = new MockESBuildContextManager ( )
426+ const mockFileWatcher = new MockFileWatcher ( app , outputOptions , [ fileWatchEvent ] )
427+ const watcher = new AppEventWatcher ( app , 'url' , buildOutputPath , mockManager , mockFileWatcher )
428+
429+ // When
430+ await watcher . start ( { stdout, stderr, signal : abortController . signal } )
431+ await flushPromises ( )
432+
433+ // Wait for event processing
434+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) )
435+
436+ // Then - not called in watcher because it was already called during reloadApp
437+ expect ( generateTypesSpy ) . not . toHaveBeenCalled ( )
438+ } )
439+ } )
440+
441+ test ( 'is called when extensions are deleted to clean up types' , async ( ) => {
442+ await inTemporaryDirectory ( async ( tmpDir ) => {
443+ const fileWatchEvent : WatcherEvent = {
444+ type : 'extension_folder_deleted' ,
445+ path : '/extensions/ui_extension_1' ,
446+ extensionPath : '/extensions/ui_extension_1' ,
447+ startTime : [ 0 , 0 ] ,
448+ }
449+
450+ // Given
451+ const buildOutputPath = joinPath ( tmpDir , '.shopify' , 'bundle' )
452+ const app = testAppLinked ( {
453+ allExtensions : [ extension1 , extension2 ] ,
454+ configuration : { scopes : '' , extension_directories : [ ] , path : 'shopify.app.custom.toml' } ,
455+ } )
456+ const generateTypesSpy = vi . spyOn ( app , 'generateExtensionTypes' )
457+
458+ const mockManager = new MockESBuildContextManager ( )
459+ const mockFileWatcher = new MockFileWatcher ( app , outputOptions , [ fileWatchEvent ] )
460+ const watcher = new AppEventWatcher ( app , 'url' , buildOutputPath , mockManager , mockFileWatcher )
461+
462+ // When
463+ await watcher . start ( { stdout, stderr, signal : abortController . signal } )
464+ await flushPromises ( )
465+
466+ // Wait for event processing
467+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) )
468+
469+ // Then - generateExtensionTypes should still be called when extensions are deleted
470+ // to clean up type definitions for the removed extension
471+ expect ( generateTypesSpy ) . toHaveBeenCalled ( )
472+ } )
473+ } )
474+ } )
475+
335476 describe ( 'app-event-watcher build extension errors' , ( ) => {
336477 test ( 'esbuild errors are logged with a custom format' , async ( ) => {
337478 await inTemporaryDirectory ( async ( tmpDir ) => {
0 commit comments