shopify-cli 1.14.0 → 2.0.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/.github/CODEOWNERS +1 -1
- data/.github/CONTRIBUTING.md +7 -7
- data/.github/DESIGN.md +3 -3
- data/.github/workflows/build.yml +1 -1
- data/.gitignore +3 -0
- data/.rubocop.yml +3 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +35 -29
- data/Gemfile +4 -0
- data/Gemfile.lock +32 -0
- data/LICENSE +4 -1
- data/README.md +92 -26
- data/RELEASING.md +29 -7
- data/Rakefile +2 -2
- data/SECURITY.md +1 -1
- data/bin/load_shopify.rb +1 -1
- data/bin/shopify +3 -3
- data/dev.yml +1 -1
- data/docs/app/node/index.md +1 -1
- data/docs/app/rails/index.md +1 -1
- data/docs/core/index.md +1 -1
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/install/index.md +1 -1
- data/docs/getting-started/migrate/index.md +1 -1
- data/docs/getting-started/uninstall/index.md +1 -1
- data/docs/getting-started/upgrade/index.md +1 -1
- data/docs/help/start-app/index.md +1 -1
- data/docs/index.md +1 -1
- data/ext/shopify-cli/extconf.rb +17 -5
- data/install.sh +1 -1
- data/lib/docgen/index_template.md.erb +2 -2
- data/lib/graphql/all_orgs_with_extensions.graphql +37 -0
- data/lib/graphql/find_organization.graphql +2 -1
- data/lib/project_types/extension/cli.rb +18 -15
- data/lib/project_types/extension/commands/build.rb +4 -5
- data/lib/project_types/extension/commands/connect.rb +35 -0
- data/lib/project_types/extension/commands/create.rb +12 -16
- data/lib/project_types/extension/commands/extension_command.rb +2 -2
- data/lib/project_types/extension/commands/info.rb +86 -0
- data/lib/project_types/extension/commands/push.rb +8 -7
- data/lib/project_types/extension/commands/register.rb +4 -5
- data/lib/project_types/extension/commands/serve.rb +5 -8
- data/lib/project_types/extension/commands/tunnel.rb +3 -1
- data/lib/project_types/extension/errors.rb +9 -0
- data/lib/project_types/extension/extension_project.rb +5 -0
- data/lib/project_types/extension/features/argo.rb +6 -6
- data/lib/project_types/extension/features/argo_runtime.rb +22 -66
- data/lib/project_types/extension/features/argo_serve.rb +25 -18
- data/lib/project_types/extension/forms/connect.rb +42 -0
- data/lib/project_types/extension/forms/questions/ask_name.rb +14 -6
- data/lib/project_types/extension/forms/questions/ask_registration.rb +51 -0
- data/lib/project_types/extension/messages/messages.rb +75 -11
- data/lib/project_types/extension/models/specification.rb +1 -0
- data/lib/project_types/extension/models/specification_handlers/{checkout_argo_extension.rb → checkout_ui_extension.rb} +3 -1
- data/lib/project_types/extension/models/specification_handlers/default.rb +13 -3
- data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +86 -0
- data/lib/project_types/extension/models/specifications.rb +1 -0
- data/lib/project_types/extension/tasks/configure_features.rb +6 -7
- data/lib/project_types/extension/tasks/configure_options.rb +20 -0
- data/lib/project_types/extension/tasks/get_extensions.rb +32 -0
- data/lib/project_types/node/cli.rb +9 -21
- data/lib/project_types/node/commands/connect.rb +8 -2
- data/lib/project_types/node/commands/create.rb +9 -5
- data/lib/project_types/node/commands/deploy.rb +15 -5
- data/lib/project_types/node/commands/deploy/heroku.rb +29 -29
- data/lib/project_types/node/commands/generate.rb +4 -2
- data/lib/project_types/node/commands/open.rb +4 -2
- data/lib/project_types/node/commands/serve.rb +3 -2
- data/lib/project_types/node/commands/tunnel.rb +4 -2
- data/lib/project_types/node/messages/messages.rb +46 -89
- data/lib/project_types/rails/cli.rb +9 -21
- data/lib/project_types/rails/commands/connect.rb +8 -2
- data/lib/project_types/rails/commands/create.rb +10 -6
- data/lib/project_types/rails/commands/deploy.rb +15 -5
- data/lib/project_types/rails/commands/deploy/heroku.rb +84 -82
- data/lib/project_types/rails/commands/generate.rb +15 -5
- data/lib/project_types/rails/commands/generate/webhook.rb +28 -26
- data/lib/project_types/rails/commands/open.rb +4 -2
- data/lib/project_types/rails/commands/serve.rb +3 -2
- data/lib/project_types/rails/commands/tunnel.rb +4 -2
- data/lib/project_types/rails/messages/messages.rb +54 -101
- data/lib/project_types/script/cli.rb +5 -7
- data/lib/project_types/script/commands/create.rb +3 -1
- data/lib/project_types/script/commands/push.rb +4 -2
- data/lib/project_types/script/messages/messages.rb +52 -45
- data/lib/project_types/script/ui/error_handler.rb +2 -2
- data/lib/project_types/theme/cli.rb +15 -27
- data/lib/project_types/theme/commands/check.rb +33 -0
- data/lib/project_types/theme/commands/delete.rb +64 -0
- data/lib/project_types/theme/commands/language_server.rb +16 -0
- data/lib/project_types/theme/commands/package.rb +55 -0
- data/lib/project_types/theme/commands/publish.rb +43 -0
- data/lib/project_types/theme/commands/pull.rb +51 -0
- data/lib/project_types/theme/commands/push.rb +58 -32
- data/lib/project_types/theme/commands/serve.rb +7 -17
- data/lib/project_types/theme/forms/confirm_store.rb +15 -0
- data/lib/project_types/theme/forms/select.rb +59 -0
- data/lib/project_types/theme/messages/messages.rb +110 -106
- data/lib/project_types/theme/ui/sync_progress_bar.rb +20 -0
- data/lib/shopify-cli/admin_api.rb +53 -38
- data/lib/shopify-cli/admin_api/populate_resource_command.rb +6 -14
- data/lib/shopify-cli/admin_api/schema.rb +1 -10
- data/lib/shopify-cli/api.rb +29 -14
- data/lib/shopify-cli/command.rb +15 -3
- data/lib/shopify-cli/commands.rb +7 -2
- data/lib/shopify-cli/commands/help.rb +2 -29
- data/lib/shopify-cli/commands/login.rb +95 -0
- data/lib/shopify-cli/commands/logout.rb +24 -8
- data/lib/shopify-cli/commands/populate.rb +23 -0
- data/lib/{project_types/node → shopify-cli}/commands/populate/customer.rb +2 -8
- data/lib/{project_types/node → shopify-cli}/commands/populate/draft_order.rb +2 -2
- data/lib/{project_types/node → shopify-cli}/commands/populate/product.rb +2 -8
- data/lib/shopify-cli/commands/store.rb +15 -0
- data/lib/shopify-cli/commands/switch.rb +39 -0
- data/lib/shopify-cli/commands/system.rb +12 -0
- data/lib/shopify-cli/commands/whoami.rb +28 -0
- data/lib/shopify-cli/connect.rb +32 -0
- data/lib/shopify-cli/context.rb +52 -4
- data/lib/shopify-cli/core/entry_point.rb +3 -22
- data/lib/shopify-cli/db.rb +4 -4
- data/lib/shopify-cli/http_request.rb +10 -0
- data/lib/shopify-cli/identity_auth.rb +282 -0
- data/lib/shopify-cli/{oauth → identity_auth}/servlet.rb +11 -12
- data/lib/shopify-cli/messages/messages.rb +132 -39
- data/lib/shopify-cli/partners_api.rb +21 -44
- data/lib/shopify-cli/partners_api/organizations.rb +8 -0
- data/lib/shopify-cli/project_commands.rb +16 -0
- data/lib/shopify-cli/project_type.rb +0 -31
- data/lib/shopify-cli/shopifolk.rb +8 -11
- data/lib/shopify-cli/sub_command.rb +1 -0
- data/lib/shopify-cli/tasks.rb +3 -0
- data/lib/shopify-cli/tasks/confirm_store.rb +18 -0
- data/lib/shopify-cli/tasks/create_api_client.rb +2 -2
- data/lib/shopify-cli/tasks/ensure_authenticated.rb +13 -0
- data/lib/shopify-cli/tasks/ensure_loopback_url.rb +1 -1
- data/lib/shopify-cli/tasks/ensure_project_type.rb +12 -0
- data/lib/shopify-cli/tasks/select_org_and_shop.rb +0 -3
- data/lib/shopify-cli/theme/dev_server.rb +98 -0
- data/lib/shopify-cli/theme/dev_server/certificate_manager.rb +79 -0
- data/lib/shopify-cli/theme/dev_server/header_hash.rb +94 -0
- data/lib/shopify-cli/theme/dev_server/hot-reload.js +93 -0
- data/lib/shopify-cli/theme/dev_server/hot_reload.rb +76 -0
- data/lib/shopify-cli/theme/dev_server/local_assets.rb +87 -0
- data/lib/shopify-cli/theme/dev_server/proxy.rb +205 -0
- data/lib/shopify-cli/theme/dev_server/sse.rb +75 -0
- data/lib/shopify-cli/theme/dev_server/watcher.rb +59 -0
- data/lib/shopify-cli/theme/dev_server/web_server.rb +140 -0
- data/lib/shopify-cli/theme/development_theme.rb +69 -0
- data/lib/shopify-cli/theme/file.rb +112 -0
- data/lib/shopify-cli/theme/ignore_filter.rb +109 -0
- data/lib/shopify-cli/theme/mime_type.rb +34 -0
- data/lib/shopify-cli/theme/syncer.rb +328 -0
- data/lib/shopify-cli/theme/theme.rb +204 -0
- data/lib/shopify-cli/version.rb +1 -1
- data/lib/shopify_cli.rb +18 -11
- data/shopify-cli.gemspec +12 -5
- data/shopify.fish +1 -1
- data/shopify.sh +1 -1
- metadata +88 -34
- data/.github/workflows/release.yml +0 -59
- data/lib/project_types/extension/features/argo_serve_options.rb +0 -42
- data/lib/project_types/node/commands/populate.rb +0 -23
- data/lib/project_types/rails/commands/populate.rb +0 -23
- data/lib/project_types/rails/commands/populate/customer.rb +0 -31
- data/lib/project_types/rails/commands/populate/draft_order.rb +0 -28
- data/lib/project_types/rails/commands/populate/product.rb +0 -30
- data/lib/project_types/theme/commands/connect.rb +0 -54
- data/lib/project_types/theme/commands/create.rb +0 -48
- data/lib/project_types/theme/commands/deploy.rb +0 -38
- data/lib/project_types/theme/commands/generate.rb +0 -20
- data/lib/project_types/theme/commands/generate/env.rb +0 -79
- data/lib/project_types/theme/forms/connect.rb +0 -34
- data/lib/project_types/theme/forms/create.rb +0 -22
- data/lib/project_types/theme/tasks/ensure_themekit_installed.rb +0 -78
- data/lib/project_types/theme/themekit.rb +0 -113
- data/lib/shopify-cli/commands/connect.rb +0 -64
- data/lib/shopify-cli/commands/create.rb +0 -50
- data/lib/shopify-cli/oauth.rb +0 -198
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyCli
|
|
4
|
+
module Theme
|
|
5
|
+
class IgnoreFilter
|
|
6
|
+
FILE = ".shopifyignore"
|
|
7
|
+
|
|
8
|
+
DEFAULT_REGEXES = [
|
|
9
|
+
/\.git/,
|
|
10
|
+
/\.hg/,
|
|
11
|
+
/\.bzr/,
|
|
12
|
+
/\.svn/,
|
|
13
|
+
/_darcs/,
|
|
14
|
+
/CVS/,
|
|
15
|
+
/\.sublime-(project|workspace)/,
|
|
16
|
+
/\.DS_Store/,
|
|
17
|
+
/\.sass-cache/,
|
|
18
|
+
/Thumbs\.db/,
|
|
19
|
+
/desktop\.ini/,
|
|
20
|
+
/config.yml/,
|
|
21
|
+
/node_modules/,
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
DEFAULT_GLOBS = [].freeze
|
|
25
|
+
|
|
26
|
+
attr_reader :root, :globs, :regexes
|
|
27
|
+
|
|
28
|
+
def self.from_path(root)
|
|
29
|
+
root = Pathname.new(root)
|
|
30
|
+
ignore_file = root.join(FILE)
|
|
31
|
+
patterns = if ignore_file.file?
|
|
32
|
+
parse_ignore_file(ignore_file)
|
|
33
|
+
else
|
|
34
|
+
[]
|
|
35
|
+
end
|
|
36
|
+
new(root, patterns: patterns)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.parse_ignore_file(file)
|
|
40
|
+
patterns = []
|
|
41
|
+
|
|
42
|
+
file.each_line do |line|
|
|
43
|
+
line.strip!
|
|
44
|
+
|
|
45
|
+
next if line.empty? || line.start_with?("#")
|
|
46
|
+
|
|
47
|
+
patterns << line
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
patterns
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def initialize(root, patterns: [])
|
|
54
|
+
@root = root
|
|
55
|
+
|
|
56
|
+
regexes, globs = patterns_to_regexes_and_globs(patterns)
|
|
57
|
+
|
|
58
|
+
@regexes = regexes
|
|
59
|
+
@globs = globs
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def match?(path)
|
|
63
|
+
path = path.to_s
|
|
64
|
+
|
|
65
|
+
return true if path.empty?
|
|
66
|
+
|
|
67
|
+
regexes.each do |regex|
|
|
68
|
+
return true if regex.match(path)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
globs.each do |glob|
|
|
72
|
+
return true if ::File.fnmatch?(glob, path)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
alias_method :ignore?, :match?
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Take in string patterns and convert them to either
|
|
82
|
+
# regex patterns or glob patterns so that they are handled in an expected manner.
|
|
83
|
+
def patterns_to_regexes_and_globs(patterns)
|
|
84
|
+
new_regexes = DEFAULT_REGEXES.dup
|
|
85
|
+
new_globs = DEFAULT_GLOBS.dup
|
|
86
|
+
|
|
87
|
+
patterns.each do |pattern|
|
|
88
|
+
pattern = pattern.strip
|
|
89
|
+
|
|
90
|
+
if pattern.start_with?("/") && pattern.end_with?("/")
|
|
91
|
+
new_regexes << Regexp.new(pattern.gsub(%r{^\/|\/$}, ""))
|
|
92
|
+
next
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# if specifying a directory, match everything below it
|
|
96
|
+
pattern += "*" if pattern.end_with?("/")
|
|
97
|
+
|
|
98
|
+
# The pattern will be scoped to root directory, so it should match anything
|
|
99
|
+
# within that space
|
|
100
|
+
pattern.prepend("*") unless pattern.start_with?("*")
|
|
101
|
+
|
|
102
|
+
new_globs << pattern
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
[new_regexes, new_globs]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "webrick"
|
|
3
|
+
|
|
4
|
+
module ShopifyCli
|
|
5
|
+
module Theme
|
|
6
|
+
class MimeType < Struct.new(:name)
|
|
7
|
+
MIME_TYPES = WEBrick::HTTPUtils::DefaultMimeTypes.merge(
|
|
8
|
+
"liquid" => "text/x-liquid",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def by_filename(filename)
|
|
13
|
+
new(WEBrick::HTTPUtils.mime_type(filename.to_s, MIME_TYPES))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def text?
|
|
18
|
+
/text/.match?(name) || json? || javascript?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def json?
|
|
22
|
+
name == "application/json"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def javascript?
|
|
26
|
+
name == "application/javascript"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_s
|
|
30
|
+
name
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "thread"
|
|
3
|
+
require "json"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module ShopifyCli
|
|
7
|
+
module Theme
|
|
8
|
+
class Syncer
|
|
9
|
+
class Operation < Struct.new(:method, :file)
|
|
10
|
+
def to_s
|
|
11
|
+
"#{method} #{file&.relative_path}"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
API_VERSION = "unstable"
|
|
15
|
+
|
|
16
|
+
attr_reader :checksums
|
|
17
|
+
attr_accessor :ignore_filter
|
|
18
|
+
|
|
19
|
+
def initialize(ctx, theme:, ignore_filter: nil)
|
|
20
|
+
@ctx = ctx
|
|
21
|
+
@theme = theme
|
|
22
|
+
@ignore_filter = ignore_filter
|
|
23
|
+
|
|
24
|
+
# Queue of `Operation`s waiting to be picked up from a thread for processing.
|
|
25
|
+
@queue = Queue.new
|
|
26
|
+
# `Operation`s will be removed from this Array completed.
|
|
27
|
+
@pending = []
|
|
28
|
+
# Thread making the API requests.
|
|
29
|
+
@threads = []
|
|
30
|
+
# Mutex used to pause all threads when backing-off when hitting API rate limits
|
|
31
|
+
@backoff_mutex = Mutex.new
|
|
32
|
+
|
|
33
|
+
# Allows delaying log of errors, mainly to not break the progress bar.
|
|
34
|
+
@delay_errors = false
|
|
35
|
+
@delayed_errors = []
|
|
36
|
+
|
|
37
|
+
# Latest theme assets checksums. Updated on each upload.
|
|
38
|
+
@checksums = {}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def enqueue_updates(files)
|
|
42
|
+
files.each { |file| enqueue(:update, file) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def enqueue_get(files)
|
|
46
|
+
files.each { |file| enqueue(:get, file) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def enqueue_deletes(files)
|
|
50
|
+
files.each { |file| enqueue(:delete, file) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def size
|
|
54
|
+
@pending.size
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def empty?
|
|
58
|
+
@pending.empty?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pending_updates
|
|
62
|
+
@pending.select { |op| op.method == :update }.map(&:file)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def remote_file?(file)
|
|
66
|
+
checksums.key?(@theme[file].relative_path.to_s)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def wait!
|
|
70
|
+
raise ThreadError, "No syncer threads" if @threads.empty?
|
|
71
|
+
total = size
|
|
72
|
+
last_size = size
|
|
73
|
+
until empty? || @queue.closed?
|
|
74
|
+
if block_given? && last_size != size
|
|
75
|
+
yield size, total
|
|
76
|
+
last_size = size
|
|
77
|
+
end
|
|
78
|
+
Thread.pass
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def fetch_checksums!
|
|
83
|
+
_status, response = ShopifyCli::AdminAPI.rest_request(
|
|
84
|
+
@ctx,
|
|
85
|
+
shop: @theme.shop,
|
|
86
|
+
path: "themes/#{@theme.id}/assets.json",
|
|
87
|
+
api_version: API_VERSION,
|
|
88
|
+
)
|
|
89
|
+
update_checksums(response)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def shutdown
|
|
93
|
+
@queue.close unless @queue.closed?
|
|
94
|
+
ensure
|
|
95
|
+
@threads.each { |thread| thread.join if thread.alive? }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def start_threads(count = 2)
|
|
99
|
+
count.times do
|
|
100
|
+
@threads << Thread.new do
|
|
101
|
+
loop do
|
|
102
|
+
operation = @queue.pop
|
|
103
|
+
break if operation.nil? # shutdown was called
|
|
104
|
+
perform(operation)
|
|
105
|
+
rescue Exception => e
|
|
106
|
+
report_error(
|
|
107
|
+
"{{red:ERROR}} {{blue:#{operation}}}: #{e}" +
|
|
108
|
+
(@ctx.debug? ? "\n\t#{e.backtrace.join("\n\t")}" : "")
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def delay_errors!
|
|
116
|
+
@delay_errors = true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def report_errors!
|
|
120
|
+
@delay_errors = false
|
|
121
|
+
@delayed_errors.each { |error| report_error(error) }
|
|
122
|
+
@delayed_errors.clear
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def upload_theme!(delay_low_priority_files: false, delete: true, &block)
|
|
126
|
+
fetch_checksums!
|
|
127
|
+
|
|
128
|
+
if delete
|
|
129
|
+
# Delete remote files not present locally
|
|
130
|
+
removed_files = checksums.keys - @theme.theme_files.map { |file| file.relative_path.to_s }
|
|
131
|
+
enqueue_deletes(removed_files)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Some files must be uploaded after the other ones
|
|
135
|
+
delayed_config_files = [
|
|
136
|
+
@theme["config/settings_schema.json"],
|
|
137
|
+
@theme["config/settings_data.json"],
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
enqueue_updates(@theme.liquid_files)
|
|
141
|
+
enqueue_updates(@theme.json_files - delayed_config_files)
|
|
142
|
+
enqueue_updates(delayed_config_files)
|
|
143
|
+
|
|
144
|
+
if delay_low_priority_files
|
|
145
|
+
# Wait for liquid & JSON files to upload, because those are rendered remotely
|
|
146
|
+
wait!(&block)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Process lower-priority files in the background
|
|
150
|
+
|
|
151
|
+
# Assets are served locally, so can be uploaded in the background
|
|
152
|
+
enqueue_updates(@theme.asset_files)
|
|
153
|
+
|
|
154
|
+
unless delay_low_priority_files
|
|
155
|
+
wait!(&block)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def download_theme!(delete: true, &block)
|
|
160
|
+
fetch_checksums!
|
|
161
|
+
|
|
162
|
+
if delete
|
|
163
|
+
# Delete local files not present remotely
|
|
164
|
+
missing_files = @theme.theme_files
|
|
165
|
+
.reject { |file| checksums.key?(file.relative_path.to_s) }.uniq
|
|
166
|
+
.reject { |file| @ignore_filter&.ignore?(file) }
|
|
167
|
+
missing_files.each do |file|
|
|
168
|
+
@ctx.debug("rm #{file.relative_path}")
|
|
169
|
+
file.delete
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
enqueue_get(checksums.keys)
|
|
174
|
+
|
|
175
|
+
wait!(&block)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def enqueue(method, file)
|
|
181
|
+
raise ArgumentError, "file required" unless file
|
|
182
|
+
|
|
183
|
+
operation = Operation.new(method, @theme[file])
|
|
184
|
+
|
|
185
|
+
# Already enqueued
|
|
186
|
+
return if @pending.include?(operation)
|
|
187
|
+
|
|
188
|
+
if @ignore_filter&.ignore?(operation.file.relative_path)
|
|
189
|
+
@ctx.debug("ignore #{operation.file.relative_path}")
|
|
190
|
+
return
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
if [:update, :get].include?(method) && operation.file.exist? && !file_has_changed?(operation.file)
|
|
194
|
+
@ctx.debug("skip #{operation}")
|
|
195
|
+
return
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
@pending << operation
|
|
199
|
+
@queue << operation unless @queue.closed?
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def perform(operation)
|
|
203
|
+
return if @queue.closed?
|
|
204
|
+
wait_for_backoff!
|
|
205
|
+
@ctx.debug(operation.to_s)
|
|
206
|
+
|
|
207
|
+
response = send(operation.method, operation.file)
|
|
208
|
+
|
|
209
|
+
# Check if the API told us we're near the rate limit
|
|
210
|
+
if !backingoff? && (limit = response["x-shopify-shop-api-call-limit"])
|
|
211
|
+
used, total = limit.split("/").map(&:to_i)
|
|
212
|
+
backoff_if_near_limit!(used, total)
|
|
213
|
+
end
|
|
214
|
+
rescue ShopifyCli::API::APIRequestError => e
|
|
215
|
+
report_error(
|
|
216
|
+
"{{red:ERROR}} {{blue:#{operation}}}:\n " +
|
|
217
|
+
parse_api_errors(e).join("\n ")
|
|
218
|
+
)
|
|
219
|
+
ensure
|
|
220
|
+
@pending.delete(operation)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def update(file)
|
|
224
|
+
asset = { key: file.relative_path.to_s }
|
|
225
|
+
if file.text?
|
|
226
|
+
asset[:value] = file.read
|
|
227
|
+
else
|
|
228
|
+
asset[:attachment] = Base64.encode64(file.read)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
_status, body, response = ShopifyCli::AdminAPI.rest_request(
|
|
232
|
+
@ctx,
|
|
233
|
+
shop: @theme.shop,
|
|
234
|
+
path: "themes/#{@theme.id}/assets.json",
|
|
235
|
+
method: "PUT",
|
|
236
|
+
api_version: API_VERSION,
|
|
237
|
+
body: JSON.generate(asset: asset)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
update_checksums(body)
|
|
241
|
+
|
|
242
|
+
response
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def get(file)
|
|
246
|
+
_status, body, response = ShopifyCli::AdminAPI.rest_request(
|
|
247
|
+
@ctx,
|
|
248
|
+
shop: @theme.shop,
|
|
249
|
+
path: "themes/#{@theme.id}/assets.json",
|
|
250
|
+
method: "GET",
|
|
251
|
+
api_version: API_VERSION,
|
|
252
|
+
query: URI.encode_www_form("asset[key]" => file.relative_path.to_s),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
update_checksums(body)
|
|
256
|
+
|
|
257
|
+
value = body.dig("asset", "value") || Base64.decode64(body.dig("asset", "attachment"))
|
|
258
|
+
file.write(value)
|
|
259
|
+
|
|
260
|
+
response
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def delete(file)
|
|
264
|
+
_status, _body, response = ShopifyCli::AdminAPI.rest_request(
|
|
265
|
+
@ctx,
|
|
266
|
+
shop: @theme.shop,
|
|
267
|
+
path: "themes/#{@theme.id}/assets.json",
|
|
268
|
+
method: "DELETE",
|
|
269
|
+
api_version: API_VERSION,
|
|
270
|
+
body: JSON.generate(asset: {
|
|
271
|
+
key: file.relative_path.to_s
|
|
272
|
+
})
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
response
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def update_checksums(api_response)
|
|
279
|
+
api_response.values.flatten.each do |asset|
|
|
280
|
+
if asset["key"] && asset["checksum"]
|
|
281
|
+
@checksums[asset["key"]] = asset["checksum"]
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
# Generate .liquid asset files are reported twice in checksum:
|
|
285
|
+
# once of generated, once for .liquid. We only keep the .liquid, that's the one we have
|
|
286
|
+
# on disk.
|
|
287
|
+
@checksums.reject! { |key, _| @checksums.key?("#{key}.liquid") }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def file_has_changed?(file)
|
|
291
|
+
file.checksum != @checksums[file.relative_path.to_s]
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def report_error(error)
|
|
295
|
+
if @delay_errors
|
|
296
|
+
@delayed_errors << error
|
|
297
|
+
else
|
|
298
|
+
@ctx.puts(error)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def parse_api_errors(exception)
|
|
303
|
+
parsed_body = JSON.parse(exception&.response&.body)
|
|
304
|
+
message = parsed_body.dig("errors", "asset") || parsed_body["message"] || exception.message
|
|
305
|
+
# Truncate to first lines
|
|
306
|
+
[message].flatten.map { |message| message.split("\n", 2).first }
|
|
307
|
+
rescue JSON::ParserError
|
|
308
|
+
[exception.message]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def backoff_if_near_limit!(used, limit)
|
|
312
|
+
if used > limit - @threads.size
|
|
313
|
+
@ctx.debug("Near API call limit, waiting 2 sec ...")
|
|
314
|
+
@backoff_mutex.synchronize { sleep 2 }
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def backingoff?
|
|
319
|
+
@backoff_mutex.locked?
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def wait_for_backoff!
|
|
323
|
+
# Sleeping in the mutex in another thread. Wait for unlock
|
|
324
|
+
@backoff_mutex.synchronize {} if backingoff?
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|