shopify-cli 2.14.0 → 2.15.2
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/.github/CONTRIBUTING.md +1 -1
- data/.github/workflows/stale.yml +46 -0
- data/CHANGELOG.md +24 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +39 -7
- data/Rakefile +40 -0
- data/ext/shopify-extensions/version +1 -1
- data/lib/project_types/extension/forms/questions/ask_template.rb +5 -8
- data/lib/project_types/extension/messages/messages.rb +11 -1
- data/lib/project_types/extension/models/development_server_requirements.rb +13 -7
- data/lib/project_types/extension/models/server_config/root.rb +2 -0
- data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +13 -0
- data/lib/project_types/script/config/extension_points.yml +18 -0
- data/lib/project_types/script/layers/infrastructure/errors.rb +17 -0
- data/lib/project_types/script/layers/infrastructure/script_service.rb +2 -0
- data/lib/project_types/script/messages/messages.rb +3 -0
- data/lib/project_types/script/ui/error_handler.rb +11 -0
- data/lib/project_types/theme/commands/pull.rb +2 -2
- data/lib/project_types/theme/commands/push.rb +2 -2
- data/lib/project_types/theme/commands/serve.rb +1 -0
- data/lib/project_types/theme/conversions/base_glob.rb +20 -5
- data/lib/project_types/theme/messages/messages.rb +47 -8
- data/lib/shopify_cli/changelog.rb +76 -0
- data/lib/shopify_cli/command.rb +8 -7
- data/lib/shopify_cli/environment.rb +19 -11
- data/lib/shopify_cli/git.rb +36 -0
- data/lib/shopify_cli/messages/messages.rb +16 -9
- data/lib/shopify_cli/release.rb +194 -0
- data/lib/shopify_cli/sed.rb +19 -0
- data/lib/shopify_cli/services/app/create/node_service.rb +2 -14
- data/lib/shopify_cli/services/app/create/php_service.rb +1 -6
- data/lib/shopify_cli/services/app/create/rails_service.rb +4 -12
- data/lib/shopify_cli/theme/dev_server/hot-reload.js +40 -13
- data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +51 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload.rb +6 -1
- data/lib/shopify_cli/theme/dev_server/local_assets.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +34 -0
- data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +44 -0
- data/lib/shopify_cli/theme/dev_server/watcher.rb +1 -1
- data/lib/shopify_cli/theme/dev_server.rb +15 -3
- data/lib/shopify_cli/theme/file.rb +15 -4
- data/lib/shopify_cli/theme/syncer/checksums.rb +60 -0
- data/lib/shopify_cli/theme/syncer/forms/apply_to_all.rb +39 -0
- data/lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb +35 -0
- data/lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb +62 -0
- data/lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb +27 -0
- data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +28 -0
- data/lib/shopify_cli/theme/syncer/ignore_helper.rb +33 -0
- data/lib/shopify_cli/theme/syncer/json_delete_handler.rb +51 -0
- data/lib/shopify_cli/theme/syncer/json_update_handler.rb +82 -0
- data/lib/shopify_cli/theme/syncer/merger.rb +53 -0
- data/lib/shopify_cli/theme/syncer/operation.rb +1 -1
- data/lib/shopify_cli/theme/syncer.rb +79 -63
- data/lib/shopify_cli/theme/theme.rb +12 -4
- data/lib/shopify_cli/theme/theme_admin_api.rb +24 -23
- data/lib/shopify_cli/thread_pool/job.rb +10 -2
- data/lib/shopify_cli/thread_pool.rb +15 -3
- data/lib/shopify_cli/tunnel.rb +9 -0
- data/lib/shopify_cli/version.rb +1 -1
- data/shopify-cli.gemspec +3 -1
- metadata +21 -4
- data/lib/project_types/rails/ruby.rb +0 -17
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "forms/apply_to_all"
|
4
|
+
require_relative "forms/select_update_strategy"
|
5
|
+
|
6
|
+
module ShopifyCLI
|
7
|
+
module Theme
|
8
|
+
class Syncer
|
9
|
+
module JsonUpdateHandler
|
10
|
+
def enqueue_json_updates(files)
|
11
|
+
# Some files must be uploaded after the other ones
|
12
|
+
delayed_files = [
|
13
|
+
theme["config/settings_schema.json"],
|
14
|
+
theme["config/settings_data.json"],
|
15
|
+
]
|
16
|
+
|
17
|
+
# Update remote JSON files and delays `delayed_files` update
|
18
|
+
files = files
|
19
|
+
.select { |file| !ignore_file?(file) && file.exist? && checksums.file_has_changed?(file) }
|
20
|
+
.sort_by { |file| delayed_files.include?(file) ? 1 : 0 }
|
21
|
+
|
22
|
+
if overwrite_json?
|
23
|
+
enqueue_updates(files)
|
24
|
+
else
|
25
|
+
# Handle conflicts when JSON files cannot be overwritten
|
26
|
+
handle_update_conflicts(files)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def handle_update_conflicts(files)
|
33
|
+
to_get = []
|
34
|
+
to_delete = []
|
35
|
+
to_update = []
|
36
|
+
to_union_merge = []
|
37
|
+
|
38
|
+
apply_to_all = Forms::ApplyToAll.new(@ctx, files.size)
|
39
|
+
|
40
|
+
files.each do |file|
|
41
|
+
update_strategy = apply_to_all.value || ask_update_strategy(file)
|
42
|
+
apply_to_all.apply?(update_strategy)
|
43
|
+
|
44
|
+
case update_strategy
|
45
|
+
when :keep_remote
|
46
|
+
if file_exist_remotely?(file)
|
47
|
+
to_get << file
|
48
|
+
else
|
49
|
+
delete_locally(file)
|
50
|
+
end
|
51
|
+
when :keep_local
|
52
|
+
to_update << file
|
53
|
+
when :union_merge
|
54
|
+
if file_exist_remotely?(file)
|
55
|
+
to_union_merge << file
|
56
|
+
else
|
57
|
+
to_update << file
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
enqueue_get(to_get)
|
63
|
+
enqueue_deletes(to_delete)
|
64
|
+
enqueue_updates(to_update)
|
65
|
+
enqueue_union_merges(to_union_merge)
|
66
|
+
end
|
67
|
+
|
68
|
+
def file_exist_remotely?(file)
|
69
|
+
!checksums[file.relative_path].nil?
|
70
|
+
end
|
71
|
+
|
72
|
+
def delete_locally(file)
|
73
|
+
::File.delete(file.absolute_path)
|
74
|
+
end
|
75
|
+
|
76
|
+
def ask_update_strategy(file)
|
77
|
+
Forms::SelectUpdateStrategy.ask(@ctx, [], file: file).strategy
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tempfile"
|
4
|
+
|
5
|
+
module ShopifyCLI
|
6
|
+
module Theme
|
7
|
+
class Syncer
|
8
|
+
class Merger
|
9
|
+
class << self
|
10
|
+
##
|
11
|
+
# Merge `theme_file` with the `new_content` by relying on the union merge
|
12
|
+
#
|
13
|
+
def union_merge(theme_file, new_content)
|
14
|
+
git_merge(theme_file, new_content, ["--union", "-p"])
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
##
|
20
|
+
# Merge theme file (`ShopifyCLI::Theme::File`) with a new content (String),
|
21
|
+
# by creating a temporary file based on the `new_content`.
|
22
|
+
#
|
23
|
+
def git_merge(theme_file, new_content, opts)
|
24
|
+
remote_file = create_tmp_file(tmp_file_name(theme_file), new_content)
|
25
|
+
empty_file = create_tmp_file("empty")
|
26
|
+
|
27
|
+
ShopifyCLI::Git.merge_file(
|
28
|
+
theme_file.absolute_path,
|
29
|
+
empty_file.path,
|
30
|
+
remote_file.path,
|
31
|
+
opts
|
32
|
+
)
|
33
|
+
ensure
|
34
|
+
# Remove temporary files on Windows as well
|
35
|
+
remote_file.close!
|
36
|
+
empty_file.close!
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_tmp_file(basename, content = "")
|
40
|
+
tmp_file = Tempfile.new(basename)
|
41
|
+
tmp_file.write(content)
|
42
|
+
tmp_file.close # Make it ready to merge
|
43
|
+
tmp_file
|
44
|
+
end
|
45
|
+
|
46
|
+
def tmp_file_name(ref_file)
|
47
|
+
"shopify-cli-merge-#{ref_file.name(".*")}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -1,12 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "thread"
|
3
4
|
require "json"
|
4
5
|
require "base64"
|
5
6
|
require "forwardable"
|
6
7
|
|
8
|
+
require_relative "syncer/checksums"
|
7
9
|
require_relative "syncer/error_reporter"
|
8
|
-
require_relative "syncer/
|
10
|
+
require_relative "syncer/ignore_helper"
|
11
|
+
require_relative "syncer/json_delete_handler"
|
12
|
+
require_relative "syncer/json_update_handler"
|
13
|
+
require_relative "syncer/merger"
|
9
14
|
require_relative "syncer/operation"
|
15
|
+
require_relative "syncer/standard_reporter"
|
10
16
|
require_relative "theme_admin_api"
|
11
17
|
|
12
18
|
module ShopifyCLI
|
@@ -14,36 +20,46 @@ module ShopifyCLI
|
|
14
20
|
class Syncer
|
15
21
|
extend Forwardable
|
16
22
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
23
|
+
include IgnoreHelper
|
24
|
+
include JsonDeleteHandler
|
25
|
+
include JsonUpdateHandler
|
26
|
+
|
27
|
+
QUEUEABLE_METHODS = [
|
28
|
+
:get, # - Updates the local file with the remote file content
|
29
|
+
:update, # - Updates the remote file with the local file content
|
30
|
+
:delete, # - Deletes the remote file
|
31
|
+
:union_merge, # - Union merges the local file content with the remote file content
|
32
|
+
]
|
33
|
+
|
34
|
+
attr_reader :theme, :checksums, :error_checksums
|
35
|
+
attr_accessor :include_filter, :ignore_filter
|
21
36
|
|
22
37
|
def_delegators :@error_reporter, :has_any_error?
|
23
38
|
|
24
|
-
def initialize(ctx, theme:, include_filter: nil, ignore_filter: nil)
|
39
|
+
def initialize(ctx, theme:, include_filter: nil, ignore_filter: nil, overwrite_json: true)
|
25
40
|
@ctx = ctx
|
26
41
|
@theme = theme
|
27
42
|
@include_filter = include_filter
|
28
43
|
@ignore_filter = ignore_filter
|
44
|
+
@overwrite_json = overwrite_json
|
29
45
|
@error_reporter = ErrorReporter.new(ctx)
|
30
46
|
@standard_reporter = StandardReporter.new(ctx)
|
31
47
|
@reporters = [@error_reporter, @standard_reporter]
|
32
48
|
|
33
49
|
# Queue of `Operation`s waiting to be picked up from a thread for processing.
|
34
50
|
@queue = Queue.new
|
51
|
+
|
35
52
|
# `Operation`s will be removed from this Array completed.
|
36
53
|
@pending = []
|
54
|
+
|
37
55
|
# Thread making the API requests.
|
38
56
|
@threads = []
|
57
|
+
|
39
58
|
# Mutex used to pause all threads when backing-off when hitting API rate limits
|
40
59
|
@backoff_mutex = Mutex.new
|
41
60
|
|
42
|
-
# Mutex used to coordinate changes in the checksums (shared accross all threads)
|
43
|
-
@checksums_mutex = Mutex.new
|
44
|
-
|
45
61
|
# Latest theme assets checksums. Updated on each upload.
|
46
|
-
@checksums =
|
62
|
+
@checksums = Checksums.new(theme)
|
47
63
|
|
48
64
|
# Checksums of assets with errors.
|
49
65
|
@error_checksums = []
|
@@ -73,6 +89,10 @@ module ShopifyCLI
|
|
73
89
|
files.each { |file| enqueue(:delete, file) }
|
74
90
|
end
|
75
91
|
|
92
|
+
def enqueue_union_merges(files)
|
93
|
+
files.each { |file| enqueue(:union_merge, file) }
|
94
|
+
end
|
95
|
+
|
76
96
|
def size
|
77
97
|
@pending.size
|
78
98
|
end
|
@@ -86,7 +106,11 @@ module ShopifyCLI
|
|
86
106
|
end
|
87
107
|
|
88
108
|
def remote_file?(file)
|
89
|
-
checksums.
|
109
|
+
checksums.has?(file)
|
110
|
+
end
|
111
|
+
|
112
|
+
def broken_file?(file)
|
113
|
+
error_checksums.include?(checksums[file.relative_path])
|
90
114
|
end
|
91
115
|
|
92
116
|
def wait!
|
@@ -135,20 +159,18 @@ module ShopifyCLI
|
|
135
159
|
fetch_checksums!
|
136
160
|
|
137
161
|
if delete
|
138
|
-
|
139
|
-
|
162
|
+
removed_json_files, removed_files = checksums
|
163
|
+
.keys
|
164
|
+
.-(@theme.theme_files.map(&:relative_path))
|
165
|
+
.map { |file| @theme[file] }
|
166
|
+
.partition(&:json?)
|
167
|
+
|
140
168
|
enqueue_deletes(removed_files)
|
169
|
+
enqueue_json_deletes(removed_json_files)
|
141
170
|
end
|
142
171
|
|
143
|
-
# Some files must be uploaded after the other ones
|
144
|
-
delayed_config_files = [
|
145
|
-
@theme["config/settings_schema.json"],
|
146
|
-
@theme["config/settings_data.json"],
|
147
|
-
]
|
148
|
-
|
149
172
|
enqueue_updates(@theme.liquid_files)
|
150
|
-
|
151
|
-
enqueue_updates(delayed_config_files)
|
173
|
+
enqueue_json_updates(@theme.json_files)
|
152
174
|
|
153
175
|
if delay_low_priority_files
|
154
176
|
# Wait for liquid & JSON files to upload, because those are rendered remotely
|
@@ -171,7 +193,7 @@ module ShopifyCLI
|
|
171
193
|
if delete
|
172
194
|
# Delete local files not present remotely
|
173
195
|
missing_files = @theme.theme_files
|
174
|
-
.reject { |file| checksums.
|
196
|
+
.reject { |file| checksums.has?(file) }.uniq
|
175
197
|
.reject { |file| ignore_file?(file) }
|
176
198
|
missing_files.each do |file|
|
177
199
|
@ctx.debug("rm #{file.relative_path}")
|
@@ -187,12 +209,13 @@ module ShopifyCLI
|
|
187
209
|
private
|
188
210
|
|
189
211
|
def report_error(operation, error_suffix = "")
|
190
|
-
@error_checksums <<
|
212
|
+
@error_checksums << checksums[operation.file_path]
|
191
213
|
@error_reporter.report("#{operation.as_error_message}#{error_suffix}")
|
192
214
|
end
|
193
215
|
|
194
216
|
def enqueue(method, file)
|
195
217
|
raise ArgumentError, "file required" unless file
|
218
|
+
raise ArgumentError, "method '#{method}' cannot be queued" unless QUEUEABLE_METHODS.include?(method)
|
196
219
|
|
197
220
|
operation = Operation.new(@ctx, method, @theme[file])
|
198
221
|
|
@@ -204,10 +227,10 @@ module ShopifyCLI
|
|
204
227
|
return
|
205
228
|
end
|
206
229
|
|
207
|
-
if [:update, :get].include?(method) && operation.file.exist?
|
230
|
+
if [:update, :get].include?(method) && operation.file.exist?
|
208
231
|
is_fixed = !!@error_checksums.delete(operation.file.checksum)
|
209
232
|
@standard_reporter.report(operation.as_fix_message) if is_fixed
|
210
|
-
return
|
233
|
+
return unless checksums.file_has_changed?(operation.file)
|
211
234
|
end
|
212
235
|
|
213
236
|
@pending << operation
|
@@ -236,7 +259,7 @@ module ShopifyCLI
|
|
236
259
|
end
|
237
260
|
|
238
261
|
def update(file)
|
239
|
-
asset = { key: file.relative_path
|
262
|
+
asset = { key: file.relative_path }
|
240
263
|
if file.text?
|
241
264
|
asset[:value] = file.read
|
242
265
|
else
|
@@ -253,32 +276,10 @@ module ShopifyCLI
|
|
253
276
|
response
|
254
277
|
end
|
255
278
|
|
256
|
-
def ignore_operation?(operation)
|
257
|
-
path = operation.file_path
|
258
|
-
ignore_path?(path)
|
259
|
-
end
|
260
|
-
|
261
|
-
def ignore_file?(file)
|
262
|
-
path = file.path
|
263
|
-
ignore_path?(path)
|
264
|
-
end
|
265
|
-
|
266
|
-
def ignore_path?(path)
|
267
|
-
ignored_by_ignore_filter?(path) || ignored_by_include_filter?(path)
|
268
|
-
end
|
269
|
-
|
270
|
-
def ignored_by_ignore_filter?(path)
|
271
|
-
ignore_filter&.ignore?(path)
|
272
|
-
end
|
273
|
-
|
274
|
-
def ignored_by_include_filter?(path)
|
275
|
-
!!include_filter && !include_filter.match?(path)
|
276
|
-
end
|
277
|
-
|
278
279
|
def get(file)
|
279
280
|
_status, body, response = api_client.get(
|
280
281
|
path: "themes/#{@theme.id}/assets.json",
|
281
|
-
query: URI.encode_www_form("asset[key]" => file.relative_path
|
282
|
+
query: URI.encode_www_form("asset[key]" => file.relative_path),
|
282
283
|
)
|
283
284
|
|
284
285
|
update_checksums(body)
|
@@ -297,37 +298,48 @@ module ShopifyCLI
|
|
297
298
|
_status, _body, response = api_client.delete(
|
298
299
|
path: "themes/#{@theme.id}/assets.json",
|
299
300
|
body: JSON.generate(asset: {
|
300
|
-
key: file.relative_path
|
301
|
+
key: file.relative_path,
|
301
302
|
})
|
302
303
|
)
|
303
304
|
|
304
305
|
response
|
305
306
|
end
|
306
307
|
|
308
|
+
def union_merge(file)
|
309
|
+
_status, body, response = api_client.get(
|
310
|
+
path: "themes/#{@theme.id}/assets.json",
|
311
|
+
query: URI.encode_www_form("asset[key]" => file.relative_path),
|
312
|
+
)
|
313
|
+
|
314
|
+
return response unless file.text?
|
315
|
+
|
316
|
+
remote_content = body.dig("asset", "value")
|
317
|
+
|
318
|
+
return response if remote_content.nil?
|
319
|
+
|
320
|
+
content = Merger.union_merge(file, remote_content)
|
321
|
+
|
322
|
+
file.write(content)
|
323
|
+
|
324
|
+
enqueue(:update, file)
|
325
|
+
|
326
|
+
response
|
327
|
+
end
|
328
|
+
|
307
329
|
def update_checksums(api_response)
|
308
330
|
api_response.values.flatten.each do |asset|
|
309
331
|
next unless asset["key"]
|
310
|
-
|
311
|
-
@checksums[asset["key"]] = asset["checksum"]
|
312
|
-
end
|
313
|
-
end
|
314
|
-
# Generate .liquid asset files are reported twice in checksum:
|
315
|
-
# once of generated, once for .liquid. We only keep the .liquid, that's the one we have
|
316
|
-
# on disk.
|
317
|
-
checksums_mutex.synchronize do
|
318
|
-
@checksums.reject! { |key, _| @checksums.key?("#{key}.liquid") }
|
332
|
+
checksums[asset["key"]] = asset["checksum"]
|
319
333
|
end
|
320
|
-
end
|
321
334
|
|
322
|
-
|
323
|
-
file.checksum != @checksums[file.relative_path.to_s]
|
335
|
+
checksums.reject_duplicated_checksums!
|
324
336
|
end
|
325
337
|
|
326
338
|
def parse_api_errors(exception)
|
327
339
|
parsed_body = JSON.parse(exception&.response&.body)
|
328
340
|
message = parsed_body.dig("errors", "asset") || parsed_body["message"] || exception.message
|
329
341
|
# Truncate to first lines
|
330
|
-
[message].flatten.map { |
|
342
|
+
[message].flatten.map { |m| m.split("\n", 2).first }
|
331
343
|
rescue JSON::ParserError
|
332
344
|
[exception.message]
|
333
345
|
end
|
@@ -339,6 +351,10 @@ module ShopifyCLI
|
|
339
351
|
end
|
340
352
|
end
|
341
353
|
|
354
|
+
def overwrite_json?
|
355
|
+
@overwrite_json
|
356
|
+
end
|
357
|
+
|
342
358
|
def backingoff?
|
343
359
|
@backoff_mutex.locked?
|
344
360
|
end
|
@@ -21,11 +21,11 @@ module ShopifyCLI
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def theme_files
|
24
|
-
glob(["**/*.liquid", "**/*.json"
|
24
|
+
(glob(["**/*.liquid", "**/*.json"]) + static_asset_files).uniq
|
25
25
|
end
|
26
26
|
|
27
27
|
def static_asset_files
|
28
|
-
glob("assets/*").reject(&:liquid?)
|
28
|
+
glob("assets/*", raise_on_dir: true).reject(&:liquid?)
|
29
29
|
end
|
30
30
|
|
31
31
|
def liquid_files
|
@@ -36,8 +36,11 @@ module ShopifyCLI
|
|
36
36
|
glob("**/*.json")
|
37
37
|
end
|
38
38
|
|
39
|
-
def glob(pattern)
|
40
|
-
root.glob(pattern).map
|
39
|
+
def glob(pattern, raise_on_dir: false)
|
40
|
+
root.glob(pattern).map do |path|
|
41
|
+
abort_if_directory!(path) if raise_on_dir
|
42
|
+
File.new(path, root)
|
43
|
+
end
|
41
44
|
end
|
42
45
|
|
43
46
|
def theme_file?(file)
|
@@ -218,6 +221,11 @@ module ShopifyCLI
|
|
218
221
|
|
219
222
|
self
|
220
223
|
end
|
224
|
+
|
225
|
+
def abort_if_directory!(path)
|
226
|
+
return unless ::File.directory?(path)
|
227
|
+
@ctx.abort(@ctx.message("theme.serve.error.invalid_subdirectory", path.to_s))
|
228
|
+
end
|
221
229
|
end
|
222
230
|
end
|
223
231
|
end
|
@@ -11,35 +11,19 @@ module ShopifyCLI
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def get(path:, **args)
|
14
|
-
rest_request(
|
15
|
-
method: "GET",
|
16
|
-
path: path,
|
17
|
-
**args
|
18
|
-
)
|
14
|
+
rest_request(method: "GET", path: path, **args)
|
19
15
|
end
|
20
16
|
|
21
17
|
def put(path:, **args)
|
22
|
-
rest_request(
|
23
|
-
method: "PUT",
|
24
|
-
path: path,
|
25
|
-
**args
|
26
|
-
)
|
18
|
+
rest_request(method: "PUT", path: path, **args)
|
27
19
|
end
|
28
20
|
|
29
21
|
def post(path:, **args)
|
30
|
-
rest_request(
|
31
|
-
method: "POST",
|
32
|
-
path: path,
|
33
|
-
**args
|
34
|
-
)
|
22
|
+
rest_request(method: "POST", path: path, **args)
|
35
23
|
end
|
36
24
|
|
37
25
|
def delete(path:, **args)
|
38
|
-
rest_request(
|
39
|
-
method: "DELETE",
|
40
|
-
path: path,
|
41
|
-
**args
|
42
|
-
)
|
26
|
+
rest_request(method: "DELETE", path: path, **args)
|
43
27
|
end
|
44
28
|
|
45
29
|
def get_shop_or_abort # rubocop:disable Naming/AccessorMethodName
|
@@ -56,16 +40,33 @@ module ShopifyCLI
|
|
56
40
|
**args.compact
|
57
41
|
)
|
58
42
|
rescue ShopifyCLI::API::APIRequestForbiddenError,
|
59
|
-
ShopifyCLI::API::APIRequestUnauthorizedError
|
60
|
-
|
43
|
+
ShopifyCLI::API::APIRequestUnauthorizedError => error
|
44
|
+
# The Admin API returns 403 Forbidden responses on different
|
45
|
+
# scenarios:
|
46
|
+
#
|
47
|
+
# * when a user doesn't have permissions for a request:
|
48
|
+
# <APIRequestForbiddenError: 403 {}>
|
49
|
+
#
|
50
|
+
# * when an asset operation cannot be performed:
|
51
|
+
# <APIRequestForbiddenError: 403 {"message":"templates/gift_card.liquid could not be deleted"}>
|
52
|
+
if empty_response_error?(error)
|
53
|
+
return handle_permissions_error
|
54
|
+
end
|
55
|
+
|
56
|
+
raise error
|
61
57
|
end
|
62
58
|
|
63
59
|
def handle_permissions_error
|
64
|
-
ensure_user_error = @ctx.message("theme.ensure_user_error",
|
60
|
+
ensure_user_error = @ctx.message("theme.ensure_user_error", shop)
|
65
61
|
ensure_user_try_this = @ctx.message("theme.ensure_user_try_this")
|
66
62
|
|
67
63
|
@ctx.abort(ensure_user_error, ensure_user_try_this)
|
68
64
|
end
|
65
|
+
|
66
|
+
def empty_response_error?(error)
|
67
|
+
error_message = error&.response&.body.to_s
|
68
|
+
error_message.empty?
|
69
|
+
end
|
69
70
|
end
|
70
71
|
end
|
71
72
|
end
|
@@ -3,10 +3,14 @@
|
|
3
3
|
module ShopifyCLI
|
4
4
|
class ThreadPool
|
5
5
|
class Job
|
6
|
-
attr_reader :error
|
6
|
+
attr_reader :error, :interval
|
7
|
+
|
8
|
+
def initialize(interval = 0)
|
9
|
+
@interval = interval
|
10
|
+
end
|
7
11
|
|
8
12
|
def perform!
|
9
|
-
raise "`#{self.class.name}#perform
|
13
|
+
raise "`#{self.class.name}#perform!' must be defined"
|
10
14
|
end
|
11
15
|
|
12
16
|
def call
|
@@ -22,6 +26,10 @@ module ShopifyCLI
|
|
22
26
|
def error?
|
23
27
|
!!@error
|
24
28
|
end
|
29
|
+
|
30
|
+
def recurring?
|
31
|
+
!interval.zero?
|
32
|
+
end
|
25
33
|
end
|
26
34
|
end
|
27
35
|
end
|
@@ -27,11 +27,23 @@ module ShopifyCLI
|
|
27
27
|
def spawn_thread
|
28
28
|
Thread.new do
|
29
29
|
catch(:stop_thread) do
|
30
|
-
loop
|
31
|
-
@jobs.pop.call
|
32
|
-
end
|
30
|
+
loop { perform(@jobs.pop) }
|
33
31
|
end
|
34
32
|
end
|
35
33
|
end
|
34
|
+
|
35
|
+
def perform(job)
|
36
|
+
job.call
|
37
|
+
reschedule(job) if job.recurring?
|
38
|
+
end
|
39
|
+
|
40
|
+
def reschedule(job)
|
41
|
+
wait(job.interval)
|
42
|
+
schedule(job)
|
43
|
+
end
|
44
|
+
|
45
|
+
def wait(seconds)
|
46
|
+
sleep(seconds)
|
47
|
+
end
|
36
48
|
end
|
37
49
|
end
|
data/lib/shopify_cli/tunnel.rb
CHANGED
@@ -178,6 +178,15 @@ module ShopifyCLI
|
|
178
178
|
|
179
179
|
def fetch_url(ctx, log_path)
|
180
180
|
LogParser.new(log_path)
|
181
|
+
rescue NgrokError => e
|
182
|
+
# Full error messages/descriptions: https://ngrok.com/docs/errors
|
183
|
+
case e.message
|
184
|
+
when /ERR_NGROK_107/
|
185
|
+
ctx.abort(ctx.message("tunnel.invalid_token", e.message))
|
186
|
+
when /ERR_NGROK_108/
|
187
|
+
ctx.abort(ctx.message("tunnel.duplicate_session", e.message))
|
188
|
+
end
|
189
|
+
raise e.class, e.message
|
181
190
|
rescue RuntimeError => e
|
182
191
|
stop(ctx)
|
183
192
|
raise e.class, e.message
|
data/lib/shopify_cli/version.rb
CHANGED
data/shopify-cli.gemspec
CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
29
29
|
%x(git ls-files -z).split("\x0").reject do |f|
|
30
30
|
f.match(%r{^(test|spec|features|packaging)/}) ||
|
31
|
-
|
31
|
+
f.match(%r{^bin/(update-deps|shopify.bat)$})
|
32
32
|
end
|
33
33
|
end
|
34
34
|
spec.bindir = "bin"
|
@@ -49,4 +49,6 @@ Gem::Specification.new do |spec|
|
|
49
49
|
# Both shopify-cli and theme-check gems are owned and developed by Shopify.
|
50
50
|
# These gems are currently being actively developed and it's easiest to update them together.
|
51
51
|
spec.add_dependency("theme-check", "~> 1.10.1")
|
52
|
+
|
53
|
+
spec.extensions = ["ext/shopify-extensions/extconf.rb"]
|
52
54
|
end
|