Skip to content
This repository has been archived by the owner on Jun 1, 2023. It is now read-only.

Commit

Permalink
Add a new --live-reload parameter to the theme serve command (#1871)
Browse files Browse the repository at this point in the history
  • Loading branch information
karreiro authored Jan 17, 2022
1 parent 44b1968 commit b10ce18
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 42 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ From version 2.6.0, the sections in this file adhere to the [keep a changelog](h
* [#1909](https://github.com/Shopify/shopify-cli/pull/1909): Fix `theme serve` on Safari

### Added
* [#1900](https://github.com/Shopify/shopify-cli/pull/1900): Add `-d'/`--development` flag to Shopify theme pull command
* [#1900](https://github.com/Shopify/shopify-cli/pull/1900): Add `-d`/`--development` flag to Shopify theme pull command
* [#1896](https://github.com/Shopify/shopify-cli/pull/1896): Release Typescript options for payment_methods and shipping_methods scripts
* [#1891](https://github.com/Shopify/shopify-cli/pull/1891): Allow for additional arguments in `shopify push script` on CI.
* [#1877](https://github.com/Shopify/shopify-cli/pull/1877): Add theme (`-t`/`--theme=NAME_OR_ID`) parameter to `theme push`/`theme pull` commands
* [#1871](https://github.com/Shopify/shopify-cli/pull/1871): Add a new `--live-reload` parameter to the `theme serve` command

## Version 2.8.0
### Fixed
Expand Down
5 changes: 5 additions & 0 deletions lib/project_types/theme/commands/serve.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Serve < ShopifyCLI::Command::SubCommand
parser.on("--host=HOST") { |host| flags[:host] = host.to_s }
parser.on("--port=PORT") { |port| flags[:port] = port.to_i }
parser.on("--poll") { flags[:poll] = true }
parser.on("--live-reload=MODE") { |mode| flags[:mode] = as_reload_mode(mode) }
end

def call(*)
Expand All @@ -23,6 +24,10 @@ def call(*)
ShopifyCLI::Context.message("theme.serve.error.address_binding_error", ShopifyCLI::TOOL_NAME)
end

def self.as_reload_mode(mode)
ShopifyCLI::Theme::DevServer::ReloadMode.get!(mode)
end

def self.help
ShopifyCLI::Context.message("theme.serve.help", ShopifyCLI::TOOL_NAME)
end
Expand Down
14 changes: 9 additions & 5 deletions lib/project_types/theme/messages/messages.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,16 @@ module Messages
Usage: {{command:%s theme serve}}
Options:
{{command:--port=PORT}} Local port to serve theme preview from
{{command:--poll}} Force polling to detect file changes
{{command:--host=HOST}} Set which network interface the web server listens on. The default value is 127.0.0.1.
{{command:--port=PORT}} Local port to serve theme preview from.
{{command:--poll}} Force polling to detect file changes.
{{command:--host=HOST}} Set which network interface the web server listens on. The default value is 127.0.0.1.
{{command:--live-reload=MODE}} The live reload mode switches the server behavior when a file is modified:
- {{command:hot-reload}} Hot reloads local changes to CSS and sections (default)
- {{command:full-page}} Always refreshes the entire page
- {{command:off}} Deactivate live reload
HELP
reload_mode_is_not_valid: "The live reload mode `%s` is not valid.",
try_a_valid_reload_mode: "Try a valid live reload mode: %s.",
viewing_theme: "Viewing theme…",
syncing_theme: "Syncing theme #%s on %s",
open_fail: "Couldn't open the theme",
Expand Down Expand Up @@ -134,9 +140,7 @@ module Messages
You are not authorized to edit themes on %s.
Make sure you are a user of that store, and allowed to edit themes.
ENSURE_USER
already_in_use_error: "Error",
address_already_in_use: "The address \"%s\" is already in use.",
try_this: "Try this",
try_port_option: "Use the --port=PORT option to serve the theme in a different port.",
},
check: {
Expand Down
65 changes: 57 additions & 8 deletions lib/shopify_cli/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,56 @@ def message(key, *params)
str = Context.messages.dig(*key_parts)
str ? str % params : key
end

# a wrapper around Kernel.puts to allow for easy formatting
#
# #### Parameters
# * `text` - a string message to output
def puts(*args)
Kernel.puts(CLI::UI.fmt(*args))
end

# aborts the current running command and outputs an error message:
# - when the `help_message` is not provided, the error message appears in
# a red frame, prefixed by an ✗ icon
# - when the `help_message` is provided, the error message appears in a
# red frame, and the help message appears in a green frame
#
# #### Parameters
# * `error_message` - an error message to output
# * `help_message` - an optional help message
#
# #### Example
#
# ShopifyCLI::Context.abort("Execution error")
# # Output:
# # ┏━━ Error ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# # ┃ ✗ Execution error
# # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#
# ShopifyCLI::Context.abort("Execution error", "export EXECUTION=1")
# # Output:
# # ┏━━ Error ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# # ┃ Execution error
# # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# # ┏━━ Try this ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# # ┃ export EXECUTION=1
# # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#
def abort(error_message, help_message = nil)
raise ShopifyCLI::Abort, "{{x}} #{error_message}" if help_message.nil?

frame(message("core.error"), color: :red) { self.puts(error_message) }
frame(message("core.try_this"), color: :green) { self.puts(help_message) }

raise ShopifyCLI::AbortSilent
end

private

def frame(title, color:, &block)
CLI::UI::Frame.open(title, color: CLI::UI.resolve_color(color), timing: false, &block)
end
end

# is the directory root that the current command is running in. If you want to
Expand Down Expand Up @@ -354,13 +404,13 @@ def print_task(text)
puts "{{yellow:*}} #{text}"
end

# a wrapper around Kernel.puts to allow for easy formatting
# proxy call to Context.puts.
#
# #### Parameters
# * `text` - a string message to output
#
def puts(*args)
Kernel.puts(CLI::UI.fmt(*args))
Context.puts(*args)
end

# a wrapper around $stderr.puts to allow for easy formatting
Expand Down Expand Up @@ -390,14 +440,13 @@ def done(text)
puts("{{v}} #{text}")
end

# aborts the current running command and outputs an error message, prefixed
# by a red x
# proxy call to Context.abort.
#
# #### Parameters
# * `text` - a string message to output
#
def abort(text)
raise ShopifyCLI::Abort, "{{x}} #{text}"
# * `error_message` - an error message to output
# * `help_message` - an optional help message
def abort(error_message, help_message = nil)
Context.abort(error_message, help_message)
end

# outputs a message, prefixed by a red `DEBUG` tag. This will only output to
Expand Down
2 changes: 2 additions & 0 deletions lib/shopify_cli/messages/messages.rb
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,8 @@ module Messages
logged_in_partner_only: "Logged into partner organization {{green:%s}}",
logged_in_partner_and_shop: "Logged into store {{green:%s}} in partner organization {{green:%s}}",
},
error: "Error",
try_this: "Try this",
},
}.freeze
end
Expand Down
27 changes: 6 additions & 21 deletions lib/shopify_cli/theme/dev_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require_relative "dev_server/cdn_fonts"
require_relative "dev_server/hot_reload"
require_relative "dev_server/header_hash"
require_relative "dev_server/reload_mode"
require_relative "dev_server/local_assets"
require_relative "dev_server/proxy"
require_relative "dev_server/sse"
Expand All @@ -25,7 +26,7 @@ module DevServer
class << self
attr_accessor :ctx

def start(ctx, root, host: "127.0.0.1", port: 9292, poll: false)
def start(ctx, root, host: "127.0.0.1", port: 9292, poll: false, mode: ReloadMode.default)
@ctx = ctx
theme = DevelopmentTheme.new(ctx, root: root)
ignore_filter = IgnoreFilter.from_path(root)
Expand All @@ -36,7 +37,7 @@ def start(ctx, root, host: "127.0.0.1", port: 9292, poll: false)
@app = Proxy.new(ctx, theme: theme, syncer: @syncer)
@app = CdnFonts.new(@app, theme: theme)
@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, mode: mode, ignore_filter: ignore_filter)
stopped = false
address = "http://#{host}:#{port}"

Expand Down Expand Up @@ -83,7 +84,9 @@ def start(ctx, root, host: "127.0.0.1", port: 9292, poll: false)
ShopifyCLI::API::APIRequestUnauthorizedError
raise ShopifyCLI::Abort, @ctx.message("theme.serve.ensure_user", theme.shop)
rescue Errno::EADDRINUSE
abort_address_already_in_use(address)
error_message = @ctx.message("theme.serve.address_already_in_use", address)
help_message = @ctx.message("theme.serve.try_port_option")
@ctx.abort(error_message, help_message)
rescue Errno::EADDRNOTAVAIL
raise AddressBindingError, "Error binding to the address #{host}."
end
Expand All @@ -94,24 +97,6 @@ def stop
@syncer.shutdown
WebServer.shutdown
end

private

def abort_address_already_in_use(address)
open_frame(@ctx.message("theme.serve.already_in_use_error"), color: :red) do
@ctx.puts(@ctx.message("theme.serve.address_already_in_use", address))
end

open_frame(@ctx.message("theme.serve.try_this"), color: :green) do
@ctx.puts(@ctx.message("theme.serve.try_port_option"))
end

raise ShopifyCLI::AbortSilent
end

def open_frame(title, color:, &block)
CLI::UI::Frame.open(title, color: CLI::UI.resolve_color(color), timing: false, &block)
end
end
end
end
Expand Down
20 changes: 19 additions & 1 deletion lib/shopify_cli/theme/dev_server/hot-reload.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,23 @@
eventSource.onerror = () => eventSource.close();
}

connect();
function reloadMode() {
var namespace = window.__SHOPIFY_CLI_ENV__;
return namespace.mode;
}

function isFullPageReloadMode(){
return reloadMode() === "full-page";
}

function isReloadModeActive(){
return reloadMode() !== "off";
}

function isRefreshRequired(files) {
if (isFullPageReloadMode()) {
return true;
}
return files.some((file) => !isCssFile(file) && !isSectionFile(file));
}

Expand Down Expand Up @@ -119,4 +133,8 @@
}
}
}

if (isReloadModeActive()) {
connect();
}
})();
20 changes: 18 additions & 2 deletions lib/shopify_cli/theme/dev_server/hot_reload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ module ShopifyCLI
module Theme
module DevServer
class HotReload
def initialize(ctx, app, theme:, watcher:, ignore_filter: nil)
def initialize(ctx, app, theme:, watcher:, mode:, ignore_filter: nil)
@ctx = ctx
@app = app
@theme = theme
@mode = mode
@streams = SSE::Streams.new
@watcher = watcher
@watcher.add_observer(self, :notify_streams_of_file_change)
Expand Down Expand Up @@ -48,12 +49,27 @@ def request_is_html?(headers)

def inject_hot_reload_javascript(body)
hot_reload_js = ::File.read("#{__dir__}/hot-reload.js")
hot_reload_script = "<script>\n#{hot_reload_js}</script>"
hot_reload_script = [
"<script>",
params_js,
hot_reload_js,
"</script>",
].join("\n")

body = body.join.gsub("</body>", "#{hot_reload_script}\n</body>")

[body]
end

def params_js
env = { mode: @mode }
<<~JS
(() => {
window.__SHOPIFY_CLI_ENV__ = #{env.to_json};
})();
JS
end

def create_stream
stream = @streams.new

Expand Down
34 changes: 34 additions & 0 deletions lib/shopify_cli/theme/dev_server/reload_mode.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module ShopifyCLI
module Theme
module DevServer
class ReloadMode
MODES = [:"hot-reload", :"full-page", :off]

class << self
def default
:"hot-reload"
end

def get!(mode)
MODES.find { |m| m == mode.to_sym } || raise_error(mode)
end

private

def raise_error(invalid_mode)
error_message = ShopifyCLI::Context.message("theme.serve.reload_mode_is_not_valid", invalid_mode)
help_message = ShopifyCLI::Context.message("theme.serve.try_a_valid_reload_mode", valid_modes)

ShopifyCLI::Context.abort(error_message, help_message)
end

def valid_modes
MODES.map { |v| "`#{v}`" }.join(", ")
end
end
end
end
end
end
47 changes: 47 additions & 0 deletions test/shopify-cli/context_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,58 @@ def test_puts
expected_stdout = /info message/
expected_stderr = /^$/

assert_output(expected_stdout, expected_stderr) do
Context.puts("info message")
end
end

def test_proxy_puts
expected_stdout = /info message/
expected_stderr = /^$/

assert_output(expected_stdout, expected_stderr) do
@ctx.puts("info message")
end
end

def test_abort
io = capture_io_and_assert_raises(ShopifyCLI::Abort) do
Context.abort("error message")
end

io = io.join
assert_match(/error message/, io)
end

def test_abort_proxy
io = capture_io_and_assert_raises(ShopifyCLI::Abort) do
@ctx.abort("error message")
end

io = io.join
assert_match(/error message/, io)
end

def test_abort_with_help_message
io = capture_io_and_assert_raises(ShopifyCLI::AbortSilent) do
Context.abort("error message", "help message")
end

io = io.join
assert_match(/error message/, io)
assert_match(/help message/, io)
end

def test_abort_proxy_with_help_message
io = capture_io_and_assert_raises(ShopifyCLI::AbortSilent) do
@ctx.abort("error message", "help message")
end

io = io.join
assert_match(/error message/, io)
assert_match(/help message/, io)
end

def test_error
expected_stdout = /^$/
expected_stderr = /error message/
Expand Down
Loading

0 comments on commit b10ce18

Please sign in to comment.