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.
- 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
|