diff --git a/packages/app/src/cli/services/dev/app-events/file-watcher.test.ts b/packages/app/src/cli/services/dev/app-events/file-watcher.test.ts index 5704a97ed6..131e827a18 100644 --- a/packages/app/src/cli/services/dev/app-events/file-watcher.test.ts +++ b/packages/app/src/cli/services/dev/app-events/file-watcher.test.ts @@ -663,6 +663,64 @@ describe('file-watcher events', () => { mockedExtractImportPaths.mockReset() }) + test('detects app config changes when chokidar reports backslash paths (Windows)', async () => { + // Simulates the Windows scenario where chokidar returns paths with backslashes + // but the app configuration stores paths with forward slashes + let eventHandler: any + + mockExtensionWatchedFiles(extension1, ['/extensions/ui_extension_1/index.js']) + mockExtensionWatchedFiles(extension1B, ['/extensions/ui_extension_1/index.js']) + mockExtensionWatchedFiles(extension2, ['/extensions/ui_extension_2/index.js']) + mockExtensionWatchedFiles(functionExtension, ['/extensions/my-function/src/index.js']) + mockExtensionWatchedFiles(posExtension, []) + mockExtensionWatchedFiles(appAccessExtension, []) + + const testApp = { + ...defaultApp, + allExtensions: defaultApp.allExtensions, + nonConfigExtensions: defaultApp.allExtensions.filter((ext) => !ext.isAppConfigExtension), + realExtensions: defaultApp.allExtensions, + } + + const mockWatcher = { + on: vi.fn((event: string, listener: any) => { + if (event === 'all') { + eventHandler = listener + } + return mockWatcher + }), + close: vi.fn(() => Promise.resolve()), + } + vi.spyOn(chokidar, 'watch').mockReturnValue(mockWatcher as any) + + const fileWatcher = new FileWatcher(testApp, outputOptions, 50) + const onChange = vi.fn() + fileWatcher.onChange(onChange) + + await fileWatcher.start() + await flushPromises() + + // Fire event with backslash path (as chokidar would on Windows) + // while app.configuration.path uses forward slashes + await eventHandler('change', '\\shopify.app.toml') + + await vi.waitFor( + () => { + expect(onChange).toHaveBeenCalled() + const calls = onChange.mock.calls + const actualEvents = calls.find((call) => call[0].length > 0)?.[0] + + if (!actualEvents) { + throw new Error('Expected onChange to be called with events, but all calls had empty arrays') + } + + expect(actualEvents).toHaveLength(1) + expect(actualEvents[0].type).toBe('extensions_config_updated') + }, + {timeout: 1000, interval: 50}, + ) + }) + test('handles rapid file changes without hanging', async () => { let eventHandler: any const events: WatcherEvent[] = [] diff --git a/packages/app/src/cli/services/dev/app-events/file-watcher.ts b/packages/app/src/cli/services/dev/app-events/file-watcher.ts index 56fca818a5..7893c5d1b8 100644 --- a/packages/app/src/cli/services/dev/app-events/file-watcher.ts +++ b/packages/app/src/cli/services/dev/app-events/file-watcher.ts @@ -230,13 +230,13 @@ export class FileWatcher { private readonly handleFileEvent = (event: string, path: string) => { const startTime = startHRTime() const normalizedPath = normalizePath(path) - const isConfigAppPath = path === this.app.configuration.path + const isConfigAppPath = normalizedPath === normalizePath(this.app.configuration.path) const isExtensionToml = path.endsWith('.extension.toml') outputDebug(`🌀: ${event} ${path.replace(this.app.directory, '')}\n`) if (isConfigAppPath) { - this.handleEventForExtension(event, path, this.app.directory, startTime, false) + this.handleEventForExtension(event, path, this.app.directory, startTime, false, isExtensionToml, isConfigAppPath) } else { const affectedExtensions = this.extensionWatchedFiles.get(normalizedPath) const isUnknownExtension = affectedExtensions === undefined || affectedExtensions.size === 0 @@ -249,10 +249,10 @@ export class FileWatcher { } for (const extensionPath of affectedExtensions ?? []) { - this.handleEventForExtension(event, path, extensionPath, startTime, false) + this.handleEventForExtension(event, path, extensionPath, startTime, false, isExtensionToml, isConfigAppPath) } if (isUnknownExtension) { - this.handleEventForExtension(event, path, this.app.directory, startTime, true) + this.handleEventForExtension(event, path, this.app.directory, startTime, true, isExtensionToml, isConfigAppPath) } } this.debouncedEmit() @@ -264,9 +264,9 @@ export class FileWatcher { extensionPath: string, startTime: StartTime, isUnknownExtension: boolean, + isExtensionToml: boolean, + isConfigAppPath: boolean, ) { - const isExtensionToml = path.endsWith('.extension.toml') - const isConfigAppPath = path === this.app.configuration.path switch (event) { case 'change':