shopify-cli 2.7.0 → 2.7.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +2 -2
  3. data/.github/workflows/shopify.yml +1 -1
  4. data/.gitignore +2 -0
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +46 -0
  7. data/Codespace.dockerfile +2 -2
  8. data/Gemfile.lock +4 -4
  9. data/Rakefile +27 -0
  10. data/Tests.dockerfile +2 -2
  11. data/dev.yml +3 -3
  12. data/ext/javy/hashes/javy-arm-macos-v0.1.0.gz.sha256 +1 -0
  13. data/ext/javy/hashes/javy-x86_64-linux-v0.1.0.gz.sha256 +1 -0
  14. data/ext/javy/hashes/javy-x86_64-macos-v0.1.0.gz.sha256 +1 -0
  15. data/ext/javy/hashes/javy-x86_64-windows-v0.1.0.gz.sha256 +1 -0
  16. data/ext/javy/javy.rb +204 -0
  17. data/ext/javy/version +1 -0
  18. data/lib/graphql/get_extension_registrations.graphql +27 -0
  19. data/lib/project_types/extension/cli.rb +27 -2
  20. data/lib/project_types/extension/commands/build.rb +10 -14
  21. data/lib/project_types/extension/commands/create.rb +3 -6
  22. data/lib/project_types/extension/commands/push.rb +36 -8
  23. data/lib/project_types/extension/extension_project.rb +1 -1
  24. data/lib/project_types/extension/features/argo_serve.rb +6 -5
  25. data/lib/project_types/extension/forms/questions/ask_registration.rb +6 -2
  26. data/lib/project_types/extension/loaders/project.rb +29 -0
  27. data/lib/project_types/extension/loaders/specification_handler.rb +22 -0
  28. data/lib/project_types/extension/messages/messages.rb +4 -2
  29. data/lib/project_types/extension/models/app.rb +1 -1
  30. data/lib/project_types/extension/models/development_server.rb +2 -2
  31. data/lib/project_types/extension/models/specification_handlers/default.rb +4 -0
  32. data/lib/project_types/extension/tasks/convert_server_config.rb +3 -1
  33. data/lib/project_types/extension/tasks/execute_commands/base.rb +13 -0
  34. data/lib/project_types/extension/tasks/execute_commands/build.rb +29 -0
  35. data/lib/project_types/extension/tasks/execute_commands/create.rb +33 -0
  36. data/lib/project_types/extension/tasks/execute_commands/serve.rb +35 -0
  37. data/lib/project_types/extension/tasks/merge_server_config.rb +33 -22
  38. data/lib/project_types/rails/commands/create.rb +2 -4
  39. data/lib/project_types/script/cli.rb +9 -1
  40. data/lib/project_types/script/commands/connect.rb +19 -0
  41. data/lib/project_types/script/commands/create.rb +1 -3
  42. data/lib/project_types/script/commands/javy.rb +29 -0
  43. data/lib/project_types/script/commands/push.rb +2 -1
  44. data/lib/project_types/script/config/extension_points.yml +12 -30
  45. data/lib/project_types/script/forms/ask_app.rb +32 -0
  46. data/lib/project_types/script/forms/ask_org.rb +30 -0
  47. data/lib/project_types/script/forms/ask_script_uuid.rb +22 -0
  48. data/lib/project_types/script/forms/run_against_shopify_org.rb +14 -0
  49. data/lib/project_types/script/graphql/app_script_set.graphql +2 -2
  50. data/lib/project_types/script/layers/application/build_script.rb +0 -1
  51. data/lib/project_types/script/layers/application/connect_app.rb +79 -0
  52. data/lib/project_types/script/layers/application/create_script.rb +17 -17
  53. data/lib/project_types/script/layers/application/push_script.rb +1 -1
  54. data/lib/project_types/script/layers/domain/errors.rb +1 -4
  55. data/lib/project_types/script/layers/domain/push_package.rb +3 -3
  56. data/lib/project_types/script/layers/domain/{script_json.rb → script_config.rb} +2 -2
  57. data/lib/project_types/script/layers/domain/script_project.rb +5 -1
  58. data/lib/project_types/script/layers/infrastructure/errors.rb +36 -7
  59. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_task_runner.rb +0 -4
  60. data/lib/project_types/script/layers/infrastructure/languages/typescript_task_runner.rb +0 -4
  61. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +2 -2
  62. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +125 -27
  63. data/lib/project_types/script/layers/infrastructure/script_service.rb +11 -11
  64. data/lib/project_types/script/messages/messages.rb +32 -4
  65. data/lib/project_types/script/ui/error_handler.rb +31 -21
  66. data/lib/project_types/theme/commands/pull.rb +3 -0
  67. data/lib/project_types/theme/commands/push.rb +7 -1
  68. data/lib/project_types/theme/commands/serve.rb +1 -1
  69. data/lib/project_types/theme/messages/messages.rb +35 -1
  70. data/lib/project_types/theme/ui/sync_progress_bar.rb +2 -2
  71. data/lib/shopify_cli/command/project_command.rb +20 -7
  72. data/lib/shopify_cli/command.rb +6 -0
  73. data/lib/shopify_cli/commands/app/create/node.rb +1 -3
  74. data/lib/shopify_cli/commands/app/create/rails.rb +1 -3
  75. data/lib/shopify_cli/constants.rb +7 -0
  76. data/lib/shopify_cli/context.rb +11 -1
  77. data/lib/shopify_cli/environment.rb +4 -0
  78. data/lib/shopify_cli/form.rb +2 -0
  79. data/lib/shopify_cli/git.rb +2 -0
  80. data/lib/shopify_cli/identity_auth.rb +18 -0
  81. data/lib/shopify_cli/messages/messages.rb +9 -2
  82. data/lib/shopify_cli/partners_api/app_extensions/job.rb +36 -0
  83. data/lib/shopify_cli/partners_api/app_extensions.rb +46 -0
  84. data/lib/shopify_cli/partners_api/organizations.rb +2 -5
  85. data/lib/shopify_cli/partners_api.rb +2 -8
  86. data/lib/shopify_cli/project.rb +8 -7
  87. data/lib/shopify_cli/resources/env_file.rb +13 -5
  88. data/lib/shopify_cli/services/app/create/node_service.rb +2 -0
  89. data/lib/shopify_cli/services/app/create/php_service.rb +1 -1
  90. data/lib/shopify_cli/services/app/create/rails_service.rb +3 -1
  91. data/lib/shopify_cli/services/app/serve/node_service.rb +1 -1
  92. data/lib/shopify_cli/services/app/serve/rails_service.rb +1 -1
  93. data/lib/shopify_cli/tasks/ensure_authenticated.rb +9 -3
  94. data/lib/shopify_cli/theme/dev_server/cdn_fonts.rb +73 -0
  95. data/lib/shopify_cli/theme/dev_server/hot-reload.js +38 -9
  96. data/lib/shopify_cli/theme/dev_server/proxy/template_param_builder.rb +84 -0
  97. data/lib/shopify_cli/theme/dev_server/proxy.rb +9 -15
  98. data/lib/shopify_cli/theme/dev_server.rb +32 -19
  99. data/lib/shopify_cli/theme/syncer/error_reporter.rb +45 -0
  100. data/lib/shopify_cli/theme/syncer/operation.rb +56 -0
  101. data/lib/shopify_cli/theme/syncer/standard_reporter.rb +32 -0
  102. data/lib/shopify_cli/theme/syncer.rb +40 -39
  103. data/lib/shopify_cli/theme/theme.rb +31 -19
  104. data/lib/shopify_cli/thread_pool/job.rb +27 -0
  105. data/lib/shopify_cli/thread_pool.rb +37 -0
  106. data/lib/shopify_cli/tunnel.rb +26 -22
  107. data/lib/shopify_cli/version.rb +1 -1
  108. data/shopify-cli.gemspec +1 -1
  109. data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +3 -1
  110. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +1 -1
  111. metadata +34 -8
  112. data/lib/graphql/all_orgs_with_extensions.graphql +0 -37
  113. data/lib/project_types/extension/tasks/run_extension_command.rb +0 -82
  114. data/lib/project_types/script/tasks/ensure_env.rb +0 -106
@@ -7,6 +7,7 @@ module ShopifyCLI
7
7
  #
8
8
  class PartnersAPI < API
9
9
  autoload :Organizations, "shopify_cli/partners_api/organizations"
10
+ autoload :AppExtensions, "shopify_cli/partners_api/app_extensions"
10
11
 
11
12
  class << self
12
13
  ##
@@ -61,18 +62,11 @@ module ShopifyCLI
61
62
  def api_client(ctx)
62
63
  new(
63
64
  ctx: ctx,
64
- token: access_token(ctx),
65
+ token: IdentityAuth.fetch_or_auth_partners_token(ctx: ctx),
65
66
  url: "https://#{Environment.partners_domain}/api/cli/graphql",
66
67
  )
67
68
  end
68
69
 
69
- def access_token(ctx)
70
- ShopifyCLI::DB.get(:partners_exchange_token) do
71
- IdentityAuth.new(ctx: ctx).authenticate
72
- ShopifyCLI::DB.get(:partners_exchange_token)
73
- end
74
- end
75
-
76
70
  def auth_failure_info(ctx, error)
77
71
  if error.response
78
72
  headers = %w(www-authenticate x-request-id)
@@ -107,13 +107,6 @@ module ShopifyCLI
107
107
  @dir = nil
108
108
  end
109
109
 
110
- private
111
-
112
- def directory(dir)
113
- @dir ||= Hash.new { |h, k| h[k] = __directory(k) }
114
- @dir[dir]
115
- end
116
-
117
110
  def at(dir)
118
111
  proj_dir = directory(dir)
119
112
  unless proj_dir
@@ -123,6 +116,13 @@ module ShopifyCLI
123
116
  @at[proj_dir]
124
117
  end
125
118
 
119
+ private
120
+
121
+ def directory(dir)
122
+ @dir ||= Hash.new { |h, k| h[k] = __directory(k) }
123
+ @dir[dir]
124
+ end
125
+
126
126
  def __directory(curr)
127
127
  loop do
128
128
  return nil if curr == "/" || /^[A-Z]:\/$/.match?(curr)
@@ -134,6 +134,7 @@ module ShopifyCLI
134
134
  end
135
135
 
136
136
  property :directory # :nodoc:
137
+ property :env # :nodoc:
137
138
 
138
139
  ##
139
140
  # will read, parse and return the envfile for the project
@@ -14,11 +14,15 @@ module ShopifyCLI
14
14
  }
15
15
 
16
16
  class << self
17
- def read(_directory = Dir.pwd)
18
- input = parse_external_env
17
+ def read(_directory = Dir.pwd, overrides: {})
18
+ input = parse_external_env(overrides: overrides)
19
19
  new(input)
20
20
  end
21
21
 
22
+ def from_hash(hash)
23
+ new(env_input(hash))
24
+ end
25
+
22
26
  def parse(directory)
23
27
  File.read(File.join(directory, FILENAME))
24
28
  .gsub("\r\n", "\n").split("\n").each_with_object({}) do |line, output|
@@ -37,10 +41,14 @@ module ShopifyCLI
37
41
  end
38
42
  end
39
43
 
40
- def parse_external_env(directory = Dir.pwd)
44
+ def parse_external_env(directory = Dir.pwd, overrides: {})
45
+ env_input(parse(directory), overrides: overrides)
46
+ end
47
+
48
+ def env_input(parsed_source, overrides: {})
41
49
  env_details = {}
42
50
  extra = {}
43
- parse(directory).each do |key, value|
51
+ parsed_source.merge(overrides).each do |key, value|
44
52
  if KEY_MAP[key]
45
53
  env_details[KEY_MAP[key]] = value
46
54
  else
@@ -53,7 +61,7 @@ module ShopifyCLI
53
61
  end
54
62
 
55
63
  property :api_key, required: true
56
- property :secret, required: true
64
+ property :secret
57
65
  property :shop
58
66
  property :scopes
59
67
  property :host
@@ -26,6 +26,8 @@ module ShopifyCLI
26
26
  verbose: verbose,
27
27
  })
28
28
 
29
+ raise ShopifyCLI::AbortSilent if form.nil?
30
+
29
31
  check_node
30
32
  check_npm
31
33
  build(form.name)
@@ -25,7 +25,7 @@ module ShopifyCLI
25
25
  type: type,
26
26
  verbose: verbose,
27
27
  })
28
- return context.puts(self.class.help) if form.nil?
28
+ raise ShopifyCLI::AbortSilent if form.nil?
29
29
 
30
30
  check_php
31
31
  check_composer
@@ -39,6 +39,8 @@ module ShopifyCLI
39
39
  form_options[:rails_opts] = rails_opts unless rails_opts.nil?
40
40
  form = form_data(form_options)
41
41
 
42
+ raise ShopifyCLI::AbortSilent if form.nil?
43
+
42
44
  ruby_version = Rails::Ruby.version(context)
43
45
  context.abort(context.message("core.app.create.rails.error.invalid_ruby_version")) unless
44
46
  ruby_version.satisfies?("~>2.5") || ruby_version.satisfies?("~>3.0.0")
@@ -157,10 +159,10 @@ module ShopifyCLI
157
159
 
158
160
  CLI::UI::Frame.open(context.message("core.app.create.rails.generating_app", name)) do
159
161
  new_command = %w(rails new)
162
+ new_command << name
160
163
  new_command += DEFAULT_RAILS_FLAGS
161
164
  new_command << "--database=#{db}"
162
165
  new_command += rails_opts.split unless rails_opts.nil?
163
- new_command << name
164
166
 
165
167
  syscall(new_command)
166
168
  end
@@ -14,7 +14,7 @@ module ShopifyCLI
14
14
 
15
15
  def call
16
16
  project = ShopifyCLI::Project.current
17
- url = host || ShopifyCLI::Tunnel.start(context)
17
+ url = host || ShopifyCLI::Tunnel.start(context, port: port)
18
18
  raise ShopifyCLI::Abort,
19
19
  context.message("core.app.serve.error.host_must_be_https") if url.match(/^https/i).nil?
20
20
  project.env.update(context, :host, url)
@@ -14,7 +14,7 @@ module ShopifyCLI
14
14
 
15
15
  def call
16
16
  project = ShopifyCLI::Project.current
17
- url = host || ShopifyCLI::Tunnel.start(context)
17
+ url = host || ShopifyCLI::Tunnel.start(context, port: port)
18
18
  raise ShopifyCLI::Abort,
19
19
  context.message("core.app.serve.error.host_must_be_https") if url.match(/^https/i).nil?
20
20
  project.env.update(context, :host, url)
@@ -4,9 +4,15 @@ module ShopifyCLI
4
4
  module Tasks
5
5
  class EnsureAuthenticated < ShopifyCLI::Task
6
6
  def call(ctx)
7
- ctx.abort(
8
- ctx.message("core.identity_auth.login_prompt", ShopifyCLI::TOOL_NAME)
9
- ) unless ShopifyCLI::IdentityAuth::IDENTITY_ACCESS_TOKENS.all? { |key| ShopifyCLI::DB.exists?(key) }
7
+ return if ShopifyCLI::Environment.acceptance_test?
8
+ unless ShopifyCLI::IdentityAuth.authenticated?
9
+ raise ShopifyCLI::Abort,
10
+ ctx.message("core.identity_auth.login_prompt", ShopifyCLI::TOOL_NAME)
11
+ end
12
+ if ShopifyCLI::IdentityAuth.environment_auth_token?
13
+ ctx.puts(ctx.message("core.identity_auth.token_authentication",
14
+ ShopifyCLI::Constants::EnvironmentVariables::AUTH_TOKEN))
15
+ end
10
16
  end
11
17
  end
12
18
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ module DevServer
6
+ class CdnFonts
7
+ FONTS_PATH = "/fonts"
8
+ FONTS_CDN = "https://fonts.shopifycdn.com"
9
+ FONTS_REGEX = %r{#{FONTS_CDN}}
10
+
11
+ def initialize(app, theme:)
12
+ @app = app
13
+ @theme = theme
14
+ end
15
+
16
+ def call(env)
17
+ path = env["PATH_INFO"]
18
+
19
+ # Serve from fonts CDN
20
+ return serve_font(env) if path.start_with?(FONTS_PATH)
21
+
22
+ # Proxy the request, and replace the URLs in the response
23
+ status, headers, body = @app.call(env)
24
+ body = replace_font_urls(body)
25
+ [status, headers, body]
26
+ end
27
+
28
+ private
29
+
30
+ def serve_font(env)
31
+ parameters = %w(PATH_INFO QUERY_STRING REQUEST_METHOD rack.input)
32
+ path, query, method, body_stream = *env.slice(*parameters).values
33
+
34
+ uri = fonts_cdn_uri(path, query)
35
+
36
+ response = Net::HTTP.start(uri.host, 443, use_ssl: true) do |http|
37
+ req_class = Net::HTTP.const_get(method.capitalize)
38
+ req = req_class.new(uri)
39
+ req.initialize_http_header(fonts_cdn_headers)
40
+ req.body_stream = body_stream
41
+ http.request(req)
42
+ end
43
+
44
+ [
45
+ response.code.to_s,
46
+ {
47
+ "Content-Type" => response.content_type,
48
+ "Content-Length" => response.content_length.to_s,
49
+ },
50
+ [response.body],
51
+ ]
52
+ end
53
+
54
+ def fonts_cdn_headers
55
+ {
56
+ "Referer" => "https://#{@theme.shop}",
57
+ "Transfer-Encoding" => "chunked",
58
+ }
59
+ end
60
+
61
+ def fonts_cdn_uri(path, query)
62
+ uri = URI.join("#{FONTS_CDN}/", path.gsub(%r{^#{FONTS_PATH}\/}, ""))
63
+ uri.query = query.split("&").last
64
+ uri
65
+ end
66
+
67
+ def replace_font_urls(body)
68
+ [body.join.gsub(FONTS_REGEX, FONTS_PATH)]
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -17,19 +17,48 @@
17
17
 
18
18
  connect();
19
19
 
20
+ function isRefreshRequired(files) {
21
+ return files.some((file) => !isCssFile(file) && !isSectionFile(file));
22
+ }
23
+
24
+ function refreshFile(file) {
25
+ if (isCssFile(file)) {
26
+ reloadCssFile(file);
27
+ return;
28
+ }
29
+
30
+ if (isSectionFile(file)) {
31
+ reloadSection(file);
32
+ return;
33
+ }
34
+ }
35
+
36
+ function setHotReloadCookie(files) {
37
+ var date = new Date();
38
+
39
+ // Hot reload cookie expires in 3 seconds
40
+ date.setSeconds(date.getSeconds() + 3);
41
+
42
+ var sections = files.join(',');
43
+ var expires = date.toUTCString();
44
+
45
+ document.cookie = `hot_reload_sections=${sections}; expires=${expires}; path=/`;
46
+ }
47
+
48
+ function refreshPage(files) {
49
+ setHotReloadCookie(files);
50
+ console.log('[HotReload] Refreshing entire page');
51
+ window.location.reload();
52
+ }
53
+
20
54
  function handleUpdate(message) {
21
55
  var data = JSON.parse(message.data);
56
+ var modifiedFiles = data.modified;
22
57
 
23
- // Assume only one file is modified at a time
24
- var modified = data.modified[0];
25
-
26
- if (isCssFile(modified)) {
27
- reloadCssFile(modified)
28
- } else if (isSectionFile(modified)) {
29
- reloadSection(modified);
58
+ if (isRefreshRequired(modifiedFiles)) {
59
+ refreshPage(modifiedFiles);
30
60
  } else {
31
- console.log(`[HotReload] Refreshing entire page`);
32
- window.location.reload();
61
+ modifiedFiles.forEach(refreshFile);
33
62
  }
34
63
  }
35
64
 
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ module DevServer
8
+ class Proxy
9
+ class TemplateParamBuilder
10
+ def build
11
+ # Core doesn't support replace_templates
12
+ return {} if core?(current_path)
13
+
14
+ (syncer_templates + request_templates)
15
+ .select { |file| file.liquid? || file.json? }
16
+ .uniq(&:relative_path)
17
+ .map { |file| as_param(file) }
18
+ .to_h
19
+ end
20
+
21
+ def with_core_endpoints(core_endpoints)
22
+ @core_endpoints = core_endpoints
23
+ self
24
+ end
25
+
26
+ def with_syncer(syncer)
27
+ @syncer = syncer
28
+ self
29
+ end
30
+
31
+ def with_rack_env(rack_env)
32
+ @rack_env = rack_env
33
+ self
34
+ end
35
+
36
+ def with_theme(theme)
37
+ @theme = theme
38
+ self
39
+ end
40
+
41
+ private
42
+
43
+ def as_param(file)
44
+ ["replace_templates[#{file.relative_path}]", file.read]
45
+ end
46
+
47
+ def syncer_templates
48
+ @syncer&.pending_updates || []
49
+ end
50
+
51
+ def request_templates
52
+ cookie_sections
53
+ .map { |section| @theme[section] unless @theme.nil? }
54
+ .compact
55
+ end
56
+
57
+ def cookie_sections
58
+ CGI::Cookie.parse(cookie)["hot_reload_sections"].join.split(",") || []
59
+ end
60
+
61
+ def core?(path)
62
+ core_endpoints.include?(path)
63
+ end
64
+
65
+ def current_path
66
+ rack_env["PATH_INFO"]
67
+ end
68
+
69
+ def cookie
70
+ rack_env["HTTP_COOKIE"]
71
+ end
72
+
73
+ def core_endpoints
74
+ @core_endpoints || []
75
+ end
76
+
77
+ def rack_env
78
+ @rack_env || {}
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -2,6 +2,9 @@
2
2
  require "net/http"
3
3
  require "stringio"
4
4
  require "time"
5
+ require "cgi"
6
+
7
+ require_relative "proxy/template_param_builder"
5
8
 
6
9
  module ShopifyCLI
7
10
  module Theme
@@ -112,21 +115,12 @@ module ShopifyCLI
112
115
  end
113
116
 
114
117
  def build_replace_templates_param(env)
115
- params = {}
116
-
117
- # Core doesn't support replace_templates
118
- return params if @core_endpoints.include?(env["PATH_INFO"])
119
-
120
- pending_templates = @syncer.pending_updates.select do |file|
121
- # Only replace Liquid or JSON files
122
- file.liquid? || file.json?
123
- end
124
-
125
- pending_templates.each do |path|
126
- params["replace_templates[#{path.relative_path}]"] = path.read
127
- end
128
-
129
- params
118
+ TemplateParamBuilder.new
119
+ .with_core_endpoints(@core_endpoints)
120
+ .with_syncer(@syncer)
121
+ .with_theme(@theme)
122
+ .with_rack_env(env)
123
+ .build
130
124
  end
131
125
 
132
126
  def add_session_cookie(cookie_header)
@@ -3,6 +3,7 @@ require_relative "development_theme"
3
3
  require_relative "ignore_filter"
4
4
  require_relative "syncer"
5
5
 
6
+ require_relative "dev_server/cdn_fonts"
6
7
  require_relative "dev_server/hot_reload"
7
8
  require_relative "dev_server/header_hash"
8
9
  require_relative "dev_server/local_assets"
@@ -24,7 +25,7 @@ module ShopifyCLI
24
25
  class << self
25
26
  attr_accessor :ctx
26
27
 
27
- def start(ctx, root, http_bind: "127.0.0.1", port: 9292, poll: false)
28
+ def start(ctx, root, host: "127.0.0.1", port: 9292, poll: false)
28
29
  @ctx = ctx
29
30
  theme = DevelopmentTheme.new(ctx, root: root)
30
31
  ignore_filter = IgnoreFilter.from_path(root)
@@ -33,9 +34,11 @@ module ShopifyCLI
33
34
 
34
35
  # Setup the middleware stack. Mimics Rack::Builder / config.ru, but in reverse order
35
36
  @app = Proxy.new(ctx, theme: theme, syncer: @syncer)
37
+ @app = CdnFonts.new(@app, theme: theme)
36
38
  @app = LocalAssets.new(ctx, @app, theme: theme)
37
39
  @app = HotReload.new(ctx, @app, theme: theme, watcher: watcher, ignore_filter: ignore_filter)
38
40
  stopped = false
41
+ address = "http://#{host}:#{port}"
39
42
 
40
43
  theme.ensure_exists!
41
44
 
@@ -44,8 +47,8 @@ module ShopifyCLI
44
47
  stop
45
48
  end
46
49
 
47
- CLI::UI::Frame.open(@ctx.message("theme.serve.serve")) do
48
- ctx.print_task("Syncing theme ##{theme.id} on #{theme.shop}")
50
+ CLI::UI::Frame.open(@ctx.message("theme.serve.viewing_theme")) do
51
+ ctx.print_task(ctx.message("theme.serve.syncing_theme", theme.id, theme.shop))
49
52
  @syncer.start_threads
50
53
  if block_given?
51
54
  yield @syncer
@@ -55,18 +58,9 @@ module ShopifyCLI
55
58
 
56
59
  return if stopped
57
60
 
58
- ctx.puts("")
59
- ctx.puts("Serving #{theme.root}")
60
- ctx.puts("")
61
- ctx.open_url!("http://127.0.0.1:#{port}")
62
- ctx.puts("")
63
- ctx.puts("Customize this theme in the Online Store Editor:")
64
- ctx.puts("{{green:#{theme.editor_url}}}")
65
- ctx.puts("")
66
- ctx.puts("Share this theme preview:")
67
- ctx.puts("{{green:#{theme.preview_url}}}")
68
- ctx.puts("")
69
- ctx.puts("(Use Ctrl-C to stop)")
61
+ ctx.puts(ctx.message("theme.serve.serving", theme.root))
62
+ ctx.open_url!(address)
63
+ ctx.puts(ctx.message("theme.serve.customize_or_preview", theme.editor_url, theme.preview_url))
70
64
  end
71
65
 
72
66
  logger = if ctx.debug?
@@ -78,7 +72,7 @@ module ShopifyCLI
78
72
  watcher.start
79
73
  WebServer.run(
80
74
  @app,
81
- BindAddress: http_bind,
75
+ BindAddress: host,
82
76
  Port: port,
83
77
  Logger: logger,
84
78
  AccessLog: [],
@@ -87,10 +81,11 @@ module ShopifyCLI
87
81
 
88
82
  rescue ShopifyCLI::API::APIRequestForbiddenError,
89
83
  ShopifyCLI::API::APIRequestUnauthorizedError
90
- @ctx.abort("You are not authorized to edit themes on #{theme.shop}.\n" \
91
- "Make sure you are a user of that store, and allowed to edit themes.")
84
+ raise ShopifyCLI::Abort, @ctx.message("theme.serve.ensure_user", theme.shop)
85
+ rescue Errno::EADDRINUSE
86
+ abort_address_already_in_use(address)
92
87
  rescue Errno::EADDRNOTAVAIL
93
- raise AddressBindingError, "Error binding to the address #{http_bind}."
88
+ raise AddressBindingError, "Error binding to the address #{host}."
94
89
  end
95
90
 
96
91
  def stop
@@ -99,6 +94,24 @@ module ShopifyCLI
99
94
  @syncer.shutdown
100
95
  WebServer.shutdown
101
96
  end
97
+
98
+ private
99
+
100
+ def abort_address_already_in_use(address)
101
+ open_frame(@ctx.message("theme.serve.already_in_use_error"), color: :red) do
102
+ @ctx.puts(@ctx.message("theme.serve.address_already_in_use", address))
103
+ end
104
+
105
+ open_frame(@ctx.message("theme.serve.try_this"), color: :green) do
106
+ @ctx.puts(@ctx.message("theme.serve.try_port_option"))
107
+ end
108
+
109
+ raise ShopifyCLI::AbortSilent
110
+ end
111
+
112
+ def open_frame(title, color:, &block)
113
+ CLI::UI::Frame.open(title, color: CLI::UI.resolve_color(color), timing: false, &block)
114
+ end
102
115
  end
103
116
  end
104
117
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ ##
7
+ # ShopifyCLI::Theme::Syncer::ErrorReporter allows delaying log of errors,
8
+ # mainly to not break the progress bar.
9
+ #
10
+ class ErrorReporter
11
+ attr_reader :ctx, :delayed_errors
12
+
13
+ def initialize(ctx)
14
+ @ctx = ctx
15
+ @has_any_error = false
16
+ @delay_errors = false
17
+ @delayed_errors = []
18
+ end
19
+
20
+ def disable!
21
+ @delay_errors = true
22
+ end
23
+
24
+ def enable!
25
+ @delay_errors = false
26
+ @delayed_errors.each { |error| report(error) }
27
+ @delayed_errors.clear
28
+ end
29
+
30
+ def report(error_message)
31
+ if @delay_errors
32
+ @delayed_errors << error_message
33
+ else
34
+ @has_any_error = true
35
+ @ctx.error(error_message)
36
+ end
37
+ end
38
+
39
+ def has_any_error?
40
+ @has_any_error
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ class Operation
7
+ attr_accessor :method, :file
8
+
9
+ COLOR_BY_STATUS = {
10
+ error: :red,
11
+ synced: :green,
12
+ fixed: :cyan,
13
+ }
14
+
15
+ def initialize(ctx, method, file)
16
+ @ctx = ctx
17
+ @method = method
18
+ @file = file
19
+ end
20
+
21
+ def to_s
22
+ "#{method} #{file_path}"
23
+ end
24
+
25
+ def as_error_message
26
+ as_message_with(status: :error)
27
+ end
28
+
29
+ def as_synced_message
30
+ as_message_with(status: :synced)
31
+ end
32
+
33
+ def as_fix_message
34
+ as_message_with(status: :fixed)
35
+ end
36
+
37
+ def file_path
38
+ file&.relative_path.to_s
39
+ end
40
+
41
+ private
42
+
43
+ def as_message_with(status:)
44
+ status_color = COLOR_BY_STATUS[status]
45
+ status_text = @ctx.message("theme.serve.operation.status.#{status}").ljust(6)
46
+
47
+ "#{timestamp} {{#{status_color}:#{status_text}}} {{>}} {{blue:#{self}}}"
48
+ end
49
+
50
+ def timestamp
51
+ Time.now.strftime("%T")
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ ##
7
+ # ShopifyCLI::Theme::Syncer::StdReporter allows disabling/enabling
8
+ # messages reported in the standard output (ShopifyCLI::Context#puts).
9
+ #
10
+ class StandardReporter
11
+ attr_reader :ctx
12
+
13
+ def initialize(ctx)
14
+ @enabled = true
15
+ @ctx = ctx
16
+ end
17
+
18
+ def disable!
19
+ @enabled = false
20
+ end
21
+
22
+ def enable!
23
+ @enabled = true
24
+ end
25
+
26
+ def report(message)
27
+ ctx.puts(message) if @enabled
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end