shopify-cli 2.15.1 → 2.15.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.
- checksums.yaml +4 -4
- data/.vscode/settings.json +1 -2
- data/CHANGELOG.md +68 -20
- data/Gemfile.lock +1 -1
- data/Rakefile +21 -0
- data/ext/javy/hashes/javy-arm-macos-v0.3.0.gz.sha256 +1 -0
- data/ext/javy/hashes/javy-x86_64-linux-v0.3.0.gz.sha256 +1 -0
- data/ext/javy/hashes/javy-x86_64-macos-v0.3.0.gz.sha256 +1 -0
- data/ext/javy/hashes/javy-x86_64-windows-v0.3.0.gz.sha256 +1 -0
- data/ext/javy/version +1 -1
- data/ext/shopify-extensions/version +1 -1
- data/lib/project_types/extension/cli.rb +4 -0
- data/lib/project_types/extension/commands/check.rb +6 -1
- data/lib/project_types/extension/forms/questions/ask_template.rb +1 -2
- data/lib/project_types/extension/messages/messages.rb +1 -3
- data/lib/project_types/extension/models/development_server_requirements.rb +1 -0
- data/lib/project_types/extension/models/specification_handlers/beacon_extension.rb +57 -0
- data/lib/project_types/extension/models/specification_handlers/beacon_extension_utils/script_config.rb +33 -0
- data/lib/project_types/extension/models/specification_handlers/beacon_extension_utils/script_config_repository.rb +75 -0
- data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +16 -1
- data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +4 -1
- data/lib/project_types/extension/tasks/configure_options.rb +2 -1
- data/lib/project_types/extension/tasks/convert_server_config.rb +13 -2
- data/lib/project_types/extension/tasks/merge_server_config.rb +5 -2
- data/lib/project_types/script/cli.rb +1 -0
- data/lib/project_types/script/layers/application/create_script.rb +14 -6
- data/lib/project_types/script/layers/infrastructure/errors.rb +17 -0
- data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +6 -21
- data/lib/project_types/script/layers/infrastructure/script_service.rb +2 -0
- data/lib/project_types/script/layers/infrastructure/sparse_checkout_details.rb +35 -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/cli.rb +1 -0
- data/lib/project_types/theme/commands/check.rb +4 -1
- data/lib/project_types/theme/commands/open.rb +2 -2
- data/lib/project_types/theme/commands/push.rb +1 -3
- data/lib/project_types/theme/commands/serve.rb +1 -0
- data/lib/project_types/theme/commands/share.rb +56 -0
- data/lib/project_types/theme/messages/messages.rb +64 -11
- data/lib/shopify_cli/changelog.rb +97 -25
- data/lib/shopify_cli/command_options/command_serve_options.rb +10 -0
- data/lib/shopify_cli/commands/app/serve.rb +7 -7
- data/lib/shopify_cli/commands/login.rb +5 -2
- data/lib/shopify_cli/context.rb +13 -0
- data/lib/shopify_cli/git.rb +36 -0
- data/lib/shopify_cli/identity_auth.rb +24 -4
- data/lib/shopify_cli/messages/messages.rb +22 -11
- data/lib/shopify_cli/release.rb +120 -20
- data/lib/shopify_cli/services/app/create/rails_service.rb +9 -1
- data/lib/shopify_cli/services/app/serve/node_service.rb +2 -25
- data/lib/shopify_cli/services/app/serve/php_service.rb +2 -25
- data/lib/shopify_cli/services/app/serve/rails_service.rb +8 -28
- data/lib/shopify_cli/services/app/serve/serve_service.rb +57 -0
- data/lib/shopify_cli/services.rb +1 -0
- data/lib/shopify_cli/tasks/update_dashboard_urls.rb +7 -9
- 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 +35 -0
- data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +44 -0
- data/lib/shopify_cli/theme/dev_server/watcher.rb +2 -8
- data/lib/shopify_cli/theme/dev_server.rb +18 -5
- 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 +21 -7
- data/lib/shopify_cli/theme/theme_admin_api.rb +23 -8
- 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 +3 -13
- data/lib/shopify_cli/version.rb +1 -1
- data/vendor/deps/cli-ui/lib/cli/ui/os.rb +8 -0
- metadata +25 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyCLI
|
|
4
|
+
module Theme
|
|
5
|
+
class Syncer
|
|
6
|
+
module Forms
|
|
7
|
+
class BaseStrategyForm < ShopifyCLI::Form
|
|
8
|
+
attr_accessor :strategy
|
|
9
|
+
|
|
10
|
+
def ask
|
|
11
|
+
ctx.puts(title_context(file))
|
|
12
|
+
|
|
13
|
+
self.strategy = CLI::UI::Prompt.ask(title_question, allow_empty: false) do |handler|
|
|
14
|
+
strategies.each do |strategy|
|
|
15
|
+
handler.option(as_text(strategy)) { strategy }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
exit_cli if self.strategy == :exit
|
|
20
|
+
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
protected
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# List of strategies that populate the form options
|
|
28
|
+
#
|
|
29
|
+
def strategies
|
|
30
|
+
raise "`#{self.class.name}#strategies' must be defined"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# Message prefix for the form title and options (strategies).
|
|
35
|
+
# See the methods `title` and `as_text`
|
|
36
|
+
#
|
|
37
|
+
def prefix
|
|
38
|
+
raise "`#{self.class.name}#prefix' must be defined"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def exit_cli
|
|
44
|
+
exit(0)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def title_context(file)
|
|
48
|
+
ctx.message("#{prefix}.title_context", file.relative_path)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def title_question
|
|
52
|
+
ctx.message("#{prefix}.title_question")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def as_text(strategy)
|
|
56
|
+
ctx.message("#{prefix}.#{strategy}")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_strategy_form"
|
|
4
|
+
|
|
5
|
+
module ShopifyCLI
|
|
6
|
+
module Theme
|
|
7
|
+
class Syncer
|
|
8
|
+
module Forms
|
|
9
|
+
class SelectDeleteStrategy < BaseStrategyForm
|
|
10
|
+
flag_arguments :file
|
|
11
|
+
|
|
12
|
+
def strategies
|
|
13
|
+
%i[
|
|
14
|
+
delete
|
|
15
|
+
restore
|
|
16
|
+
exit
|
|
17
|
+
]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def prefix
|
|
21
|
+
"theme.serve.syncer.forms.delete_strategy"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_strategy_form"
|
|
4
|
+
|
|
5
|
+
module ShopifyCLI
|
|
6
|
+
module Theme
|
|
7
|
+
class Syncer
|
|
8
|
+
module Forms
|
|
9
|
+
class SelectUpdateStrategy < BaseStrategyForm
|
|
10
|
+
flag_arguments :file
|
|
11
|
+
|
|
12
|
+
def strategies
|
|
13
|
+
%i[
|
|
14
|
+
keep_remote
|
|
15
|
+
keep_local
|
|
16
|
+
union_merge
|
|
17
|
+
exit
|
|
18
|
+
]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def prefix
|
|
22
|
+
"theme.serve.syncer.forms.update_strategy"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyCLI
|
|
4
|
+
module Theme
|
|
5
|
+
class Syncer
|
|
6
|
+
module IgnoreHelper
|
|
7
|
+
def ignore_operation?(operation)
|
|
8
|
+
path = operation.file_path
|
|
9
|
+
ignore_path?(path)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def ignore_file?(file)
|
|
13
|
+
path = file.path
|
|
14
|
+
ignore_path?(path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ignore_path?(path)
|
|
18
|
+
ignored_by_ignore_filter?(path) || ignored_by_include_filter?(path)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def ignored_by_ignore_filter?(path)
|
|
24
|
+
ignore_filter&.ignore?(path)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def ignored_by_include_filter?(path)
|
|
28
|
+
!!include_filter && !include_filter.match?(path)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "forms/apply_to_all"
|
|
4
|
+
require_relative "forms/select_delete_strategy"
|
|
5
|
+
|
|
6
|
+
module ShopifyCLI
|
|
7
|
+
module Theme
|
|
8
|
+
class Syncer
|
|
9
|
+
module JsonDeleteHandler
|
|
10
|
+
def enqueue_json_deletes(files)
|
|
11
|
+
files = files.select { |file| !ignore_file?(file) }
|
|
12
|
+
|
|
13
|
+
if overwrite_json?
|
|
14
|
+
enqueue_deletes(files)
|
|
15
|
+
else
|
|
16
|
+
# Handle conflicts when JSON files cannot be overwritten
|
|
17
|
+
handle_delete_conflicts(files)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def handle_delete_conflicts(files)
|
|
24
|
+
to_delete = []
|
|
25
|
+
to_get = []
|
|
26
|
+
|
|
27
|
+
apply_to_all = Forms::ApplyToAll.new(@ctx, files.size)
|
|
28
|
+
|
|
29
|
+
files.each do |file|
|
|
30
|
+
delete_strategy = apply_to_all.value || ask_delete_strategy(file)
|
|
31
|
+
apply_to_all.apply?(delete_strategy)
|
|
32
|
+
|
|
33
|
+
case delete_strategy
|
|
34
|
+
when :delete
|
|
35
|
+
to_delete << file
|
|
36
|
+
when :restore
|
|
37
|
+
to_get << file
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
enqueue_deletes(to_delete)
|
|
42
|
+
enqueue_get(to_get)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ask_delete_strategy(file)
|
|
46
|
+
Forms::SelectDeleteStrategy.ask(@ctx, [], file: file).strategy
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -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
|
|
@@ -37,10 +37,10 @@ module ShopifyCLI
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def glob(pattern, raise_on_dir: false)
|
|
40
|
-
root
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
root
|
|
41
|
+
.glob(pattern)
|
|
42
|
+
.select { |path| file?(path, raise_on_dir) }
|
|
43
|
+
.map { |path| File.new(path, root) }
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def theme_file?(file)
|
|
@@ -155,6 +155,13 @@ module ShopifyCLI
|
|
|
155
155
|
end
|
|
156
156
|
|
|
157
157
|
class << self
|
|
158
|
+
def create_unpublished(ctx, root: nil, name: nil)
|
|
159
|
+
name ||= random_name
|
|
160
|
+
theme = new(ctx, root: root, name: name, role: "unpublished")
|
|
161
|
+
theme.create
|
|
162
|
+
theme
|
|
163
|
+
end
|
|
164
|
+
|
|
158
165
|
def all(ctx, root: nil)
|
|
159
166
|
_status, body = fetch_themes(ctx)
|
|
160
167
|
|
|
@@ -182,6 +189,10 @@ module ShopifyCLI
|
|
|
182
189
|
|
|
183
190
|
private
|
|
184
191
|
|
|
192
|
+
def random_name
|
|
193
|
+
ShopifyCLI::Helpers::Haikunator.haikunate(9999)
|
|
194
|
+
end
|
|
195
|
+
|
|
185
196
|
def find(ctx, root, &block)
|
|
186
197
|
_status, body = fetch_themes(ctx)
|
|
187
198
|
|
|
@@ -222,9 +233,12 @@ module ShopifyCLI
|
|
|
222
233
|
self
|
|
223
234
|
end
|
|
224
235
|
|
|
225
|
-
def
|
|
226
|
-
|
|
227
|
-
|
|
236
|
+
def file?(path, raise_on_dir = false)
|
|
237
|
+
if raise_on_dir && ::File.directory?(path)
|
|
238
|
+
@ctx.abort(@ctx.message("theme.serve.error.invalid_subdirectory", path.to_s))
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
::File.file?(path)
|
|
228
242
|
end
|
|
229
243
|
end
|
|
230
244
|
end
|