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