Skip to content

Commit 40f72cb

Browse files
committed
Call generateExtensionTypes after building extensions when filters are updated
1 parent 9dd0147 commit 40f72cb

File tree

3 files changed

+152
-0
lines changed

3 files changed

+152
-0
lines changed

.changeset/twelve-memes-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/app': patch
3+
---
4+
5+
Enable types to be re-generated when extensions are rebuilt during dev

packages/app/src/cli/services/dev/app-events/app-event-watcher.test.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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) => {

packages/app/src/cli/services/dev/app-events/app-event-watcher.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ export class AppEventWatcher extends EventEmitter {
163163
// Build the created/updated extensions and update the extension events with the build result
164164
await this.buildExtensions(buildableEvents)
165165

166+
// Generate the extension types after building the extensions so new imports are included
167+
// Skip if the app was reloaded, as generateExtensionTypes was already called during reload
168+
if (!appEvent.appWasReloaded) {
169+
await this.app.generateExtensionTypes()
170+
}
171+
166172
// Find deleted extensions and delete their previous build output
167173
await this.deleteExtensionsBuildOutput(appEvent)
168174
this.emit('all', appEvent)

0 commit comments

Comments
 (0)