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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/lib/project_types/extension/commands/serve.rb +57 -3
- data/lib/project_types/extension/extension_project.rb +8 -1
- data/lib/project_types/extension/loaders/project.rb +3 -2
- data/lib/project_types/extension/messages/messages.rb +21 -6
- data/lib/project_types/extension/models/server_config/development_renderer.rb +1 -1
- data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +18 -6
- data/lib/project_types/theme/commands/serve.rb +15 -3
- data/lib/project_types/theme/messages/messages.rb +4 -2
- data/lib/shopify_cli/commands/logout.rb +13 -2
- data/lib/shopify_cli/environment.rb +1 -1
- data/lib/shopify_cli/file_system_listener.rb +30 -0
- data/lib/shopify_cli/git.rb +116 -33
- data/lib/shopify_cli/identity_auth.rb +1 -0
- data/lib/shopify_cli/project.rb +1 -1
- data/lib/shopify_cli/tasks/ensure_project_type.rb +3 -1
- data/lib/shopify_cli/theme/dev_server/cdn_fonts.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/certificate_manager.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/errors.rb +9 -0
- data/lib/shopify_cli/theme/dev_server/header_hash.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/hooks/file_change_hook.rb +77 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_deleter.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/{hot-reload-no-script.html → hot_reload/resources/hot-reload-no-script.html} +0 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/resources/hot_reload.js +48 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/resources/sse_client.js +43 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/resources/theme.js +114 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/resources/theme_extension.js +121 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/script_injector.rb +57 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/hot_reload.rb +8 -76
- data/lib/shopify_cli/theme/dev_server/local_assets.rb +28 -28
- data/lib/shopify_cli/theme/dev_server/proxy.rb +33 -25
- data/lib/shopify_cli/theme/dev_server/proxy_param_builder.rb +82 -0
- data/lib/shopify_cli/theme/dev_server/reload_mode.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/sse.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/watcher.rb +8 -9
- data/lib/shopify_cli/theme/dev_server/web_server.rb +1 -1
- data/lib/shopify_cli/theme/dev_server.rb +287 -99
- data/lib/shopify_cli/theme/extension/app_extension.rb +40 -0
- data/lib/shopify_cli/theme/extension/dev_server/hooks/file_change_hook.rb +68 -0
- data/lib/shopify_cli/theme/extension/dev_server/hot_reload/script_injector.rb +30 -0
- data/lib/shopify_cli/theme/extension/dev_server/hot_reload.rb +13 -0
- data/lib/shopify_cli/theme/extension/dev_server/local_assets.rb +30 -0
- data/lib/shopify_cli/theme/{dev_server/proxy/template_param_builder.rb → extension/dev_server/proxy_param_builder.rb} +26 -16
- data/lib/shopify_cli/theme/extension/dev_server/watcher.rb +47 -0
- data/lib/shopify_cli/theme/extension/dev_server.rb +150 -0
- data/lib/shopify_cli/theme/extension/host_theme.rb +104 -0
- data/lib/shopify_cli/theme/extension/syncer/extension_serve_job.rb +133 -0
- data/lib/shopify_cli/theme/extension/syncer/operation.rb +21 -0
- data/lib/shopify_cli/theme/extension/syncer.rb +81 -0
- data/lib/shopify_cli/theme/extension/ui/host_theme_progress_bar.rb +35 -0
- data/lib/shopify_cli/theme/ignore_helper.rb +31 -0
- data/lib/shopify_cli/theme/root.rb +62 -0
- data/lib/shopify_cli/theme/syncer.rb +12 -6
- data/lib/shopify_cli/theme/theme.rb +10 -52
- data/lib/shopify_cli/version.rb +1 -1
- metadata +27 -7
- data/.github/workflows/triage.yml +0 -22
- data/lib/shopify_cli/theme/dev_server/hot-reload.js +0 -194
- data/lib/shopify_cli/theme/syncer/ignore_helper.rb +0 -33
@@ -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
|
File without changes
|
@@ -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
|
@@ -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
|
-
|
5
|
+
class DevServer
|
10
6
|
class HotReload
|
11
|
-
def initialize(ctx, app,
|
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
|
-
|
43
|
-
.
|
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
|
-
|
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
|
-
|
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
|