shopify-cli 2.24.0 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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