shopify-cli 2.7.0 → 2.7.4

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