diff --git a/lib/shopify_cli/theme/dev_server.rb b/lib/shopify_cli/theme/dev_server.rb index ee5c6bccd6..48269f71a2 100644 --- a/lib/shopify_cli/theme/dev_server.rb +++ b/lib/shopify_cli/theme/dev_server.rb @@ -30,7 +30,7 @@ def start(ctx, root, bind: "127.0.0.1", port: 9292, poll: false) # Setup the middleware stack. Mimics Rack::Builder / config.ru, but in reverse order @app = Proxy.new(ctx, theme: theme, syncer: @syncer) @app = LocalAssets.new(ctx, @app, theme: theme) - @app = HotReload.new(ctx, @app, theme: theme, watcher: watcher, ignore_filter: ignore_filter) + @app = HotReload.new(ctx, @app, theme: theme, watcher: watcher, syncer: @syncer, ignore_filter: ignore_filter) stopped = false theme.ensure_exists! diff --git a/lib/shopify_cli/theme/dev_server/hot-reload.js b/lib/shopify_cli/theme/dev_server/hot-reload.js index bd90edafbe..ac42b30cfb 100644 --- a/lib/shopify_cli/theme/dev_server/hot-reload.js +++ b/lib/shopify_cli/theme/dev_server/hot-reload.js @@ -17,22 +17,44 @@ connect(); + let nonDynamicFileChanged = false; function handleUpdate(message) { var data = JSON.parse(message.data); - // Assume only one file is modified at a time - var modified = data.modified[0]; + if(data.modified) { + let containsJSFiles = false; - if (isCssFile(modified)) { - reloadCssFile(modified) - } else if (isSectionFile(modified)) { - reloadSection(modified); - } else { - console.log(`[HotReload] Refreshing entire page`); + data.modified.forEach(file => { + if (isCssFile(file)) { + reloadCssFile(file) + } else if (isSectionFile(file)) { + reloadSection(file); + } else if (isJSFile(file)) { + containsJSFiles = true; + } else { + nonDynamicFileChanged = true; + } + }); + + if(containsJSFiles && !nonDynamicFileChanged) { + console.log(`[HotReload] Refreshing entire page`); + return window.location.reload(); + } + } + + if(nonDynamicFileChanged) { + console.log(`[HotReload] Refreshing entire page, waiting for files to upload`); + } + + if(nonDynamicFileChanged && data.uploadComplete) { window.location.reload(); } } + function isJSFile(filename) { + return filename.endsWith('.js'); + } + function isCssFile(filename) { return filename.endsWith('.css'); } @@ -80,7 +102,7 @@ console.log(`[HotReload] Reloaded ${this.name} section`); } else { - window.location.reload() + nonDynamicFileChanged = true; console.log(`[HotReload] Hot-reloading not supported, fully reloading ${this.name} section`); } diff --git a/lib/shopify_cli/theme/dev_server/hot_reload.rb b/lib/shopify_cli/theme/dev_server/hot_reload.rb index 5fb13e9922..b11882b021 100644 --- a/lib/shopify_cli/theme/dev_server/hot_reload.rb +++ b/lib/shopify_cli/theme/dev_server/hot_reload.rb @@ -4,13 +4,15 @@ module ShopifyCLI module Theme module DevServer class HotReload - def initialize(ctx, app, theme:, watcher:, ignore_filter: nil) + def initialize(ctx, app, theme:, watcher:, syncer:, ignore_filter: nil) @ctx = ctx @app = app @theme = theme @streams = SSE::Streams.new @watcher = watcher @watcher.add_observer(self, :notify_streams_of_file_change) + @syncer = syncer + @syncer.add_observer(self, :notify_streams_of_upload_complete) @ignore_filter = ignore_filter end @@ -40,6 +42,14 @@ def notify_streams_of_file_change(modified, added, _removed) end end + def notify_streams_of_upload_complete(complete) + @streams.broadcast(JSON.generate( + uploadComplete: complete + )) + + @ctx.debug("[HotReload] upload complete") + end + private def request_is_html?(headers) diff --git a/lib/shopify_cli/theme/syncer.rb b/lib/shopify_cli/theme/syncer.rb index ab0547f0b8..f6eb3e1773 100644 --- a/lib/shopify_cli/theme/syncer.rb +++ b/lib/shopify_cli/theme/syncer.rb @@ -2,10 +2,13 @@ require "thread" require "json" require "base64" +require "observer" module ShopifyCLI module Theme class Syncer + include Observable + class Operation < Struct.new(:method, :file) def to_s "#{method} #{file&.relative_path}" @@ -218,6 +221,11 @@ def perform(operation) ) ensure @pending.delete(operation) + + if @pending.size == 0 + changed + notify_observers(true) + end end def update(file) diff --git a/test/shopify-cli/theme/dev_server/hot_reload_test.rb b/test/shopify-cli/theme/dev_server/hot_reload_test.rb index 804bccfa81..dab64f3166 100644 --- a/test/shopify-cli/theme/dev_server/hot_reload_test.rb +++ b/test/shopify-cli/theme/dev_server/hot_reload_test.rb @@ -12,7 +12,8 @@ def setup root = ShopifyCLI::ROOT + "/test/fixtures/theme" @ctx = TestHelpers::FakeContext.new(root: root) @theme = Theme.new(@ctx, root: root) - @syncer = stub("Syncer", enqueue_uploads: true) + @syncer = Syncer.new(@ctx, theme: @theme) + @syncer.start_threads @watcher = Watcher.new(@ctx, theme: @theme, syncer: @syncer) end @@ -67,7 +68,7 @@ def test_broadcasts_watcher_events .with(JSON.generate(modified: modified)) app = -> { [200, {}, []] } - HotReload.new(@ctx, app, theme: @theme, watcher: @watcher) + HotReload.new(@ctx, app, theme: @theme, watcher: @watcher, syncer: @syncer) @watcher.changed @watcher.notify_observers(modified, [], []) @@ -87,6 +88,7 @@ def test_doesnt_broadcast_watcher_events_when_the_list_is_empty @ctx, app, theme: @theme, watcher: @watcher, + syncer: @syncer, ignore_filter: ignore_filter ) @@ -94,13 +96,25 @@ def test_doesnt_broadcast_watcher_events_when_the_list_is_empty @watcher.notify_observers(modified, [], []) end + def test_broadcast_upload_complete_event + SSE::Streams.any_instance + .expects(:broadcast) + .with(JSON.generate(uploadComplete: true)) + + app = -> { [200, {}, []] } + HotReload.new(@ctx, app, theme: @theme, watcher: @watcher, syncer: @syncer) + + @syncer.enqueue_updates([@theme["snippets/snippet.liquid"]]) + @syncer.wait! + end + private def serve(response_body = "", path: "/", headers: {}) app = lambda do |_env| [200, headers, [response_body]] end - stack = HotReload.new(@ctx, app, theme: @theme, watcher: @watcher) + stack = HotReload.new(@ctx, app, theme: @theme, watcher: @watcher, syncer: @syncer) request = Rack::MockRequest.new(stack) request.get(path).body end diff --git a/test/shopify-cli/theme/dev_server/integration_test.rb b/test/shopify-cli/theme/dev_server/integration_test.rb index dbca39dd29..5f248a6fbe 100644 --- a/test/shopify-cli/theme/dev_server/integration_test.rb +++ b/test/shopify-cli/theme/dev_server/integration_test.rb @@ -164,7 +164,7 @@ def test_streams_hot_reload_events file = Pathname.new("#{ShopifyCLI::ROOT}/test/fixtures/theme/assets/theme.css") file.write("modified") begin - assert_equal("2a\r\ndata: {\"modified\":[\"assets/theme.css\"]}\n\n\n\r\n", socket.readpartial(1024)) + assert_includes(socket.readpartial(1024), "2a\r\ndata: {\"modified\":[\"assets/theme.css\"]}\n\n\n\r\n") ensure file.write("") end