shopify-cli 2.24.0 → 2.25.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/lib/project_types/extension/commands/serve.rb +57 -3
  6. data/lib/project_types/extension/extension_project.rb +8 -1
  7. data/lib/project_types/extension/loaders/project.rb +3 -2
  8. data/lib/project_types/extension/messages/messages.rb +21 -6
  9. data/lib/project_types/extension/models/server_config/development_renderer.rb +1 -1
  10. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +18 -6
  11. data/lib/project_types/theme/commands/serve.rb +15 -3
  12. data/lib/project_types/theme/messages/messages.rb +4 -2
  13. data/lib/shopify_cli/commands/logout.rb +13 -2
  14. data/lib/shopify_cli/environment.rb +1 -1
  15. data/lib/shopify_cli/file_system_listener.rb +30 -0
  16. data/lib/shopify_cli/git.rb +116 -33
  17. data/lib/shopify_cli/identity_auth.rb +1 -0
  18. data/lib/shopify_cli/project.rb +1 -1
  19. data/lib/shopify_cli/tasks/ensure_project_type.rb +3 -1
  20. data/lib/shopify_cli/theme/dev_server/cdn_fonts.rb +1 -1
  21. data/lib/shopify_cli/theme/dev_server/certificate_manager.rb +1 -1
  22. data/lib/shopify_cli/theme/dev_server/errors.rb +9 -0
  23. data/lib/shopify_cli/theme/dev_server/header_hash.rb +1 -1
  24. data/lib/shopify_cli/theme/dev_server/hooks/file_change_hook.rb +77 -0
  25. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_deleter.rb +1 -1
  26. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
  27. data/lib/shopify_cli/theme/dev_server/{hot-reload-no-script.html → hot_reload/resources/hot-reload-no-script.html} +0 -0
  28. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/hot_reload.js +48 -0
  29. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/sse_client.js +43 -0
  30. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/theme.js +114 -0
  31. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/theme_extension.js +121 -0
  32. data/lib/shopify_cli/theme/dev_server/hot_reload/script_injector.rb +57 -0
  33. data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +1 -1
  34. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +8 -76
  35. data/lib/shopify_cli/theme/dev_server/local_assets.rb +28 -28
  36. data/lib/shopify_cli/theme/dev_server/proxy.rb +33 -25
  37. data/lib/shopify_cli/theme/dev_server/proxy_param_builder.rb +82 -0
  38. data/lib/shopify_cli/theme/dev_server/reload_mode.rb +1 -1
  39. data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +1 -1
  40. data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +1 -1
  41. data/lib/shopify_cli/theme/dev_server/sse.rb +1 -1
  42. data/lib/shopify_cli/theme/dev_server/watcher.rb +8 -9
  43. data/lib/shopify_cli/theme/dev_server/web_server.rb +1 -1
  44. data/lib/shopify_cli/theme/dev_server.rb +287 -99
  45. data/lib/shopify_cli/theme/extension/app_extension.rb +40 -0
  46. data/lib/shopify_cli/theme/extension/dev_server/hooks/file_change_hook.rb +68 -0
  47. data/lib/shopify_cli/theme/extension/dev_server/hot_reload/script_injector.rb +30 -0
  48. data/lib/shopify_cli/theme/extension/dev_server/hot_reload.rb +13 -0
  49. data/lib/shopify_cli/theme/extension/dev_server/local_assets.rb +30 -0
  50. data/lib/shopify_cli/theme/{dev_server/proxy/template_param_builder.rb → extension/dev_server/proxy_param_builder.rb} +26 -16
  51. data/lib/shopify_cli/theme/extension/dev_server/watcher.rb +47 -0
  52. data/lib/shopify_cli/theme/extension/dev_server.rb +150 -0
  53. data/lib/shopify_cli/theme/extension/host_theme.rb +104 -0
  54. data/lib/shopify_cli/theme/extension/syncer/extension_serve_job.rb +133 -0
  55. data/lib/shopify_cli/theme/extension/syncer/operation.rb +21 -0
  56. data/lib/shopify_cli/theme/extension/syncer.rb +81 -0
  57. data/lib/shopify_cli/theme/extension/ui/host_theme_progress_bar.rb +35 -0
  58. data/lib/shopify_cli/theme/ignore_helper.rb +31 -0
  59. data/lib/shopify_cli/theme/root.rb +62 -0
  60. data/lib/shopify_cli/theme/syncer.rb +12 -6
  61. data/lib/shopify_cli/theme/theme.rb +10 -52
  62. data/lib/shopify_cli/version.rb +1 -1
  63. metadata +27 -7
  64. data/.github/workflows/triage.yml +0 -22
  65. data/lib/shopify_cli/theme/dev_server/hot-reload.js +0 -194
  66. data/lib/shopify_cli/theme/syncer/ignore_helper.rb +0 -33
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyCLI
3
+ module Theme
4
+ class DevServer
5
+ # Errors
6
+ class Error < StandardError; end
7
+ end
8
+ end
9
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ShopifyCLI
4
4
  module Theme
5
- module DevServer
5
+ class DevServer
6
6
  # Based on Rack::HeaderHash
7
7
  class HeaderHash < Hash
8
8
  def self.[](headers)
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../hot_reload/remote_file_reloader"
3
+ require_relative "../hot_reload/remote_file_deleter"
4
+ require "shopify_cli/theme/ignore_helper"
5
+
6
+ module ShopifyCLI
7
+ module Theme
8
+ class DevServer
9
+ module Hooks
10
+ class FileChangeHook
11
+ include ShopifyCLI::Theme::IgnoreHelper
12
+
13
+ attr_reader :include_filter, :ignore_filter
14
+
15
+ def initialize(ctx, theme:, include_filter: nil, ignore_filter: nil)
16
+ @ctx = ctx
17
+ @theme = theme
18
+ @include_filter = include_filter
19
+ @ignore_filter = ignore_filter
20
+ end
21
+
22
+ def call(modified, added, removed, streams: nil)
23
+ @streams = streams
24
+ files = (modified + added)
25
+ .map { |f| @theme[f] }
26
+ .reject { |f| ignore_file?(f) }
27
+ files -= liquid_css_files = files.select(&:liquid_css?)
28
+ deleted_files = removed
29
+ .map { |f| @theme[f] }
30
+ .reject { |f| ignore_file?(f) }
31
+
32
+ remote_delete(deleted_files) unless deleted_files.empty?
33
+ reload_page(removed) unless deleted_files.empty?
34
+
35
+ hot_reload(files) unless files.empty?
36
+ remote_reload(liquid_css_files)
37
+ end
38
+
39
+ private
40
+
41
+ def hot_reload(files)
42
+ paths = files.map(&:relative_path)
43
+ @streams.broadcast(JSON.generate(modified: paths))
44
+ @ctx.debug("[HotReload] Modified #{paths.join(", ")}")
45
+ end
46
+
47
+ def reload_page(removed)
48
+ @streams.broadcast(JSON.generate(reload_page: true))
49
+ @ctx.debug("[ReloadPage] Deleted #{removed.join(", ")}")
50
+ end
51
+
52
+ def remote_reload(files)
53
+ files.each do |file|
54
+ @ctx.debug("reload file each -> file.relative_path #{file.relative_path}")
55
+ remote_file_reloader.reload(file)
56
+ end
57
+ end
58
+
59
+ def remote_delete(files)
60
+ files.each do |file|
61
+ @ctx.debug("delete file each -> file.relative_path #{file.relative_path}")
62
+ remote_file_deleter.delete(file)
63
+ end
64
+ end
65
+
66
+ def remote_file_deleter
67
+ @remote_file_deleter ||= HotReload::RemoteFileDeleter.new(@ctx, theme: @theme, streams: @streams)
68
+ end
69
+
70
+ def remote_file_reloader
71
+ @remote_file_reloader ||= HotReload::RemoteFileReloader.new(@ctx, theme: @theme, streams: @streams)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ShopifyCLI
4
4
  module Theme
5
- module DevServer
5
+ class DevServer
6
6
  class HotReload
7
7
  class RemoteFileDeleter
8
8
  def initialize(ctx, theme:, streams:)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ShopifyCLI
4
4
  module Theme
5
- module DevServer
5
+ class DevServer
6
6
  class HotReload
7
7
  class RemoteFileReloader
8
8
  def initialize(ctx, theme:, streams:)
@@ -0,0 +1,48 @@
1
+ class HotReload {
2
+ static reloadMode = () => {
3
+ const namespace = window.__SHOPIFY_CLI_ENV__;
4
+ return namespace.mode;
5
+ };
6
+ static isFullPageReloadMode = () => {
7
+ return HotReload.reloadMode() === "full-page";
8
+ };
9
+ static isReloadModeActive = () => {
10
+ return HotReload.reloadMode() !== "off";
11
+ };
12
+ static setHotReloadCookie = (files) => {
13
+ const date = new Date();
14
+
15
+ // Hot reload cookie expires in 3 seconds
16
+ date.setSeconds(date.getSeconds() + 3);
17
+
18
+ const sections = files.join(",");
19
+ const expires = date.toUTCString();
20
+
21
+ document.cookie = `hot_reload_files=${sections}; expires=${expires}; path=/`;
22
+ };
23
+ static refreshPage = (files) => {
24
+ HotReload.setHotReloadCookie(files);
25
+ console.log("[HotReload] Refreshing entire page");
26
+ window.location.reload();
27
+ };
28
+ static isCSSFile = (filename) => {
29
+ return filename.endsWith(".css");
30
+ };
31
+ static reloadCssFile = (filename) => {
32
+ // Find a stylesheet link starting with /assets (locally-served only) containing the filename
33
+ let links = document.querySelectorAll(
34
+ `link[href^="/assets"][href*="${filename}"][rel="stylesheet"]`
35
+ );
36
+
37
+ Array.from(links).forEach((link) => {
38
+ if (!link) {
39
+ console.log(
40
+ `[HotReload] Could not find link for stylesheet ${filename}`
41
+ );
42
+ } else {
43
+ link.href = new URL(link.href).pathname + `?v=${Date.now()}`;
44
+ console.log(`[HotReload] Reloaded stylesheet ${filename}`);
45
+ }
46
+ });
47
+ };
48
+ }
@@ -0,0 +1,43 @@
1
+ class SSEClient {
2
+ constructor(eventsUrl, eventHandler) {
3
+ SSEClient.verifySSE();
4
+ this.eventsUrl = eventsUrl;
5
+ this.eventHandler = eventHandler;
6
+ }
7
+ static verifySSE() {
8
+ if (typeof EventSource === "undefined") {
9
+ console.error(
10
+ "[HotReload] Error: SSE features are not supported. Try a different browser."
11
+ );
12
+ }
13
+
14
+ console.log("[HotReload] Initializing...");
15
+ }
16
+ connect() {
17
+ const eventSource = new EventSource(this.eventsUrl);
18
+ eventSource.onmessage = (msg) => {
19
+ this.handleMessage(msg);
20
+ };
21
+
22
+ eventSource.onopen = () => console.log("[HotReload] SSE connected.");
23
+
24
+ eventSource.onclose = () => {
25
+ console.log("[HotReload] SSE closed. Attempting to reconnect...");
26
+
27
+ setTimeout(this.connect, 5000);
28
+ };
29
+
30
+ eventSource.onerror = () => {
31
+ console.log("[HotReload] SSE closed.");
32
+ eventSource.close();
33
+ };
34
+ }
35
+ handleMessage(message) {
36
+ const data = JSON.parse(message.data);
37
+ if (data.reload_page) {
38
+ HotReload.refreshPage([]);
39
+ return;
40
+ }
41
+ this.eventHandler(data);
42
+ }
43
+ }
@@ -0,0 +1,114 @@
1
+ (() => {
2
+ function sectionNamesByType(type) {
3
+ const namespace = window.__SHOPIFY_CLI_ENV__;
4
+ return namespace.section_names_by_type[type] || [];
5
+ }
6
+
7
+ function querySelectDOMSections(idSuffix) {
8
+ const elements = document.querySelectorAll(
9
+ `[id^='shopify-section'][id$='${idSuffix}']`
10
+ );
11
+ return Array.from(elements);
12
+ }
13
+
14
+ function fetchDOMSections(name) {
15
+ const domSections = sectionNamesByType(name).flatMap((n) =>
16
+ querySelectDOMSections(n)
17
+ );
18
+
19
+ if (domSections.length > 0) {
20
+ return domSections;
21
+ }
22
+
23
+ return querySelectDOMSections(name);
24
+ }
25
+
26
+ function isRefreshRequired(files) {
27
+ if (HotReload.isFullPageReloadMode()) {
28
+ return true;
29
+ }
30
+ return files.some(
31
+ (file) => !HotReload.isCSSFile(file) && !isSectionFile(file)
32
+ );
33
+ }
34
+
35
+ function refreshFile(file) {
36
+ if (HotReload.isCSSFile(file)) {
37
+ HotReload.reloadCssFile(file);
38
+ return;
39
+ }
40
+
41
+ if (isSectionFile(file)) {
42
+ reloadSection(file);
43
+ }
44
+ }
45
+
46
+ function handleUpdate(data) {
47
+ const modifiedFiles = data.modified;
48
+
49
+ if (modifiedFiles === undefined) {
50
+ return;
51
+ }
52
+
53
+ if (isRefreshRequired(modifiedFiles)) {
54
+ HotReload.refreshPage(modifiedFiles);
55
+ } else {
56
+ modifiedFiles.forEach(refreshFile);
57
+ }
58
+ }
59
+
60
+ function isSectionFile(filename) {
61
+ return new Section(filename).valid();
62
+ }
63
+
64
+ function reloadSection(filename) {
65
+ new Section(filename).refresh();
66
+ }
67
+
68
+ class Section {
69
+ constructor(filename) {
70
+ this.filename = filename;
71
+ this.name = filename.split("/").pop().replace(".liquid", "");
72
+ this.elements = fetchDOMSections(this.name);
73
+ }
74
+
75
+ valid() {
76
+ return this.filename.startsWith("sections/") && this.elements.length > 0;
77
+ }
78
+
79
+ async refreshElement(element) {
80
+ const sectionId = element.id.replace(/^shopify-section-/, "");
81
+ const url = new URL(window.location.href);
82
+
83
+ url.searchParams.append("section_id", sectionId);
84
+
85
+ const response = await fetch(url);
86
+
87
+ try {
88
+ if (response.headers.get("x-templates-from-params") === "1") {
89
+ element.outerHTML = await response.text();
90
+ } else {
91
+ window.location.reload();
92
+
93
+ console.log(
94
+ `[HotReload] Hot-reloading not supported, fully reloading ${this.name} section`
95
+ );
96
+ }
97
+ } catch (e) {
98
+ console.log(
99
+ `[HotReload] Failed to reload ${this.name} section: ${e.message}`
100
+ );
101
+ }
102
+ }
103
+
104
+ async refresh() {
105
+ console.log(`[HotReload] Reloaded ${this.name} sections`);
106
+ this.elements.forEach(this.refreshElement);
107
+ }
108
+ }
109
+
110
+ if (HotReload.isReloadModeActive()) {
111
+ let client = new SSEClient("/hot-reload", handleUpdate);
112
+ client.connect();
113
+ }
114
+ })();
@@ -0,0 +1,121 @@
1
+ (() => {
2
+ const APP_BLOCK = "app-block",
3
+ APP_EMBED_BLOCK = "app-embed-block";
4
+
5
+ function querySelectHotReloadElements(handle) {
6
+ // Gets all blocks (app and embed) with specified handle TODO: add app ID check here
7
+ const blocks = Array.from(
8
+ document.querySelectorAll(`[data-block-handle$='${handle}']`)
9
+ );
10
+ if (blocks.length) {
11
+ const queryString = "shopify-section-template";
12
+ const is_section = blocks[0].closest(`[id^=${queryString}]`) !== null;
13
+ if (is_section)
14
+ return [
15
+ blocks.map((block) => block.closest(`[id^=${queryString}]`)),
16
+ APP_BLOCK,
17
+ ];
18
+
19
+ return [blocks, APP_EMBED_BLOCK];
20
+ }
21
+ return [[], null];
22
+ }
23
+
24
+ function isRefreshRequired(files) {
25
+ if (HotReload.isFullPageReloadMode()) {
26
+ return true;
27
+ }
28
+ return files.some(
29
+ (file) => !HotReload.isCSSFile(file) && !isBlockFile(file)
30
+ );
31
+ }
32
+
33
+ function refreshFile(file) {
34
+ if (HotReload.isCSSFile(file)) {
35
+ HotReload.reloadCssFile(file);
36
+ return;
37
+ }
38
+
39
+ let block = new Block(file); // minimize DOM queries
40
+ if (block.valid()) return block.refresh();
41
+ }
42
+
43
+ function refreshPage(files) {
44
+ HotReload.setHotReloadCookie(files);
45
+ console.log("[HotReload] Refreshing entire page");
46
+ window.location.reload();
47
+ }
48
+
49
+ function handleUpdate(data) {
50
+ const modifiedFiles = data.modified;
51
+
52
+ if (modifiedFiles === undefined) {
53
+ return;
54
+ }
55
+
56
+ if (isRefreshRequired(modifiedFiles)) {
57
+ refreshPage(modifiedFiles);
58
+ } else {
59
+ modifiedFiles.forEach(refreshFile);
60
+ }
61
+ }
62
+
63
+ function isBlockFile(filename) {
64
+ return new Block(filename).valid();
65
+ }
66
+
67
+ class Block {
68
+ constructor(filename) {
69
+ this.filename = filename;
70
+ this.name = filename.split("/").pop().replace(".liquid", "");
71
+ const [elements, type] = querySelectHotReloadElements(this.name);
72
+ this.elements = elements;
73
+ this.type = type;
74
+
75
+ this.refreshElement = this.refreshElement.bind(this);
76
+ this.refresh = this.refresh.bind(this);
77
+ this.valid = this.valid.bind(this);
78
+ }
79
+
80
+ valid() {
81
+ return this.filename.startsWith("blocks/") && this.elements.length > 0;
82
+ }
83
+
84
+ async refreshElement(element) {
85
+ const url = new URL(window.location.href);
86
+ let regex, key;
87
+ if (this.type === APP_BLOCK) {
88
+ regex = /^shopify-section-/;
89
+ key = "section_id";
90
+ } else {
91
+ regex = /^shopify-block-/;
92
+ key = "app_block_id";
93
+ }
94
+ const elementId = element.id.replace(regex, "");
95
+
96
+ url.searchParams.append(key, elementId);
97
+
98
+ HotReload.setHotReloadCookie([this.filename]);
99
+
100
+ const response = await fetch(url);
101
+
102
+ try {
103
+ element.outerHTML = await response.text();
104
+ } catch (e) {
105
+ console.log(
106
+ `[HotReload] Failed to reload ${this.name} ${this.type}: ${e.message}`
107
+ );
108
+ }
109
+ }
110
+
111
+ async refresh() {
112
+ console.log(`[HotReload] Reloaded ${this.name} ${this.type}s`);
113
+ this.elements.forEach(this.refreshElement);
114
+ }
115
+ }
116
+
117
+ if (HotReload.isReloadModeActive()) {
118
+ let client = new SSEClient("/hot-reload", handleUpdate);
119
+ client.connect();
120
+ }
121
+ })();
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../hot_reload/sections_index"
3
+
4
+ module ShopifyCLI
5
+ module Theme
6
+ class DevServer
7
+ class HotReload
8
+ class ScriptInjector
9
+ def initialize(ctx, theme: nil)
10
+ @ctx = ctx
11
+ @theme = theme
12
+ @sections_index = HotReload::SectionsIndex.new(theme) unless theme.nil?
13
+ end
14
+
15
+ def inject(body:, dir:, mode:)
16
+ @mode = mode
17
+ @dir = dir
18
+ hot_reload_script = [
19
+ read("hot-reload-no-script.html"),
20
+ "<script>",
21
+ "(() => {",
22
+ javascript_inline,
23
+ *javascript_files.map { |file| read(file) },
24
+ "})();",
25
+ "</script>",
26
+ ].join("\n")
27
+
28
+ body = body.join.gsub("</body>", "#{hot_reload_script}\n</body>")
29
+
30
+ [body]
31
+ end
32
+
33
+ private
34
+
35
+ def javascript_files
36
+ %w(hot_reload.js sse_client.js theme.js)
37
+ end
38
+
39
+ def javascript_inline
40
+ env = { mode: @mode }
41
+ env[:section_names_by_type] = @sections_index.section_names_by_type
42
+
43
+ <<~JS
44
+ (() => {
45
+ window.__SHOPIFY_CLI_ENV__ = #{env.to_json};
46
+ })();
47
+ JS
48
+ end
49
+
50
+ def read(filename)
51
+ ::File.read("#{@dir}/hot_reload/resources/#{filename}")
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ShopifyCLI
4
4
  module Theme
5
- module DevServer
5
+ class DevServer
6
6
  class HotReload
7
7
  class SectionsIndex
8
8
  def initialize(theme)
@@ -1,25 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "hot_reload/remote_file_reloader"
4
- require_relative "hot_reload/remote_file_deleter"
5
- require_relative "hot_reload/sections_index"
6
-
7
3
  module ShopifyCLI
8
4
  module Theme
9
- module DevServer
5
+ class DevServer
10
6
  class HotReload
11
- def initialize(ctx, app, theme:, watcher:, mode:, ignore_filter: nil)
7
+ def initialize(ctx, app, broadcast_hooks: [], script_injector: nil, watcher:, mode:)
12
8
  @ctx = ctx
13
9
  @app = app
14
- @theme = theme
15
10
  @mode = mode
11
+ @broadcast_hooks = broadcast_hooks
12
+ @script_injector = script_injector
16
13
  @streams = SSE::Streams.new
17
- @remote_file_reloader = RemoteFileReloader.new(ctx, theme: @theme, streams: @streams)
18
- @remote_file_deleter = RemoteFileDeleter.new(ctx, theme: @theme, streams: @streams)
19
- @sections_index = SectionsIndex.new(@theme)
20
14
  @watcher = watcher
21
15
  @watcher.add_observer(self, :notify_streams_of_file_change)
22
- @ignore_filter = ignore_filter
23
16
  end
24
17
 
25
18
  def call(env)
@@ -39,80 +32,19 @@ module ShopifyCLI
39
32
  end
40
33
 
41
34
  def notify_streams_of_file_change(modified, added, removed)
42
- files = (modified + added)
43
- .map { |file| @theme[file] }
44
- .reject { |file| @ignore_filter&.ignore?(file.relative_path) }
45
-
46
- files -= liquid_css_files = files.select(&:liquid_css?)
47
-
48
- deleted_files = removed
49
- .map { |file| @theme[file] }
50
- .reject { |file| @ignore_filter&.ignore?(file.relative_path) }
51
-
52
- remote_delete(deleted_files) unless deleted_files.empty?
53
- reload_page(removed) unless deleted_files.empty?
54
-
55
- hot_reload(files) unless files.empty?
56
- remote_reload(liquid_css_files)
57
- end
58
-
59
- private
60
-
61
- def hot_reload(files)
62
- paths = files.map(&:relative_path)
63
- @streams.broadcast(JSON.generate(modified: paths))
64
- @ctx.debug("[HotReload] Modified #{paths.join(", ")}")
65
- end
66
-
67
- def reload_page(removed)
68
- @streams.broadcast(JSON.generate(reload_page: true))
69
- @ctx.debug("[ReloadPage] Deleted #{removed.join(", ")}")
70
- end
71
-
72
- def remote_delete(files)
73
- files.each do |file|
74
- @ctx.debug("delete file each -> file.relative_path #{file.relative_path}")
75
- @remote_file_deleter.delete(file)
35
+ @broadcast_hooks.each do |hook|
36
+ hook.call(modified, added, removed, streams: @streams)
76
37
  end
77
38
  end
78
39
 
79
- def remote_reload(files)
80
- files.each do |file|
81
- @ctx.debug("reload file each -> file.relative_path #{file.relative_path}")
82
- @remote_file_reloader.reload(file)
83
- end
84
- end
40
+ private
85
41
 
86
42
  def request_is_html?(headers)
87
43
  headers["content-type"]&.start_with?("text/html")
88
44
  end
89
45
 
90
46
  def inject_hot_reload_javascript(body)
91
- hot_reload_js = ::File.read("#{__dir__}/hot-reload.js")
92
- hot_reload_no_script = ::File.read("#{__dir__}/hot-reload-no-script.html")
93
- hot_reload_script = [
94
- hot_reload_no_script,
95
- "<script>",
96
- params_js,
97
- hot_reload_js,
98
- "</script>",
99
- ].join("\n")
100
-
101
- body = body.join.gsub("</body>", "#{hot_reload_script}\n</body>")
102
-
103
- [body]
104
- end
105
-
106
- def params_js
107
- env = {
108
- mode: @mode,
109
- section_names_by_type: @sections_index.section_names_by_type,
110
- }
111
- <<~JS
112
- (() => {
113
- window.__SHOPIFY_CLI_ENV__ = #{env.to_json};
114
- })();
115
- JS
47
+ @script_injector&.inject(body: body, dir: __dir__, mode: @mode)
116
48
  end
117
49
 
118
50
  def create_stream