shopify-cli 1.13.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (200) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/.github/CONTRIBUTING.md +7 -7
  4. data/.github/DESIGN.md +3 -3
  5. data/.github/workflows/build.yml +1 -1
  6. data/.gitignore +3 -0
  7. data/.rubocop.yml +3 -1
  8. data/.ruby-version +1 -1
  9. data/CHANGELOG.md +60 -26
  10. data/Gemfile +4 -0
  11. data/Gemfile.lock +32 -0
  12. data/LICENSE +4 -1
  13. data/README.md +94 -26
  14. data/RELEASING.md +31 -7
  15. data/Rakefile +2 -2
  16. data/SECURITY.md +1 -1
  17. data/THEMEKIT_MIGRATION.md +18 -0
  18. data/bin/load_shopify.rb +1 -1
  19. data/bin/shopify +3 -3
  20. data/dev.yml +1 -1
  21. data/docs/app/node/index.md +1 -1
  22. data/docs/app/rails/index.md +1 -1
  23. data/docs/core/index.md +1 -1
  24. data/docs/getting-started/index.md +1 -1
  25. data/docs/getting-started/install/index.md +1 -1
  26. data/docs/getting-started/migrate/index.md +1 -1
  27. data/docs/getting-started/uninstall/index.md +1 -1
  28. data/docs/getting-started/upgrade/index.md +1 -1
  29. data/docs/help/start-app/index.md +1 -1
  30. data/docs/index.md +1 -1
  31. data/ext/shopify-cli/extconf.rb +17 -5
  32. data/install.sh +1 -1
  33. data/lib/docgen/index_template.md.erb +2 -2
  34. data/lib/graphql/all_orgs_with_extensions.graphql +37 -0
  35. data/lib/graphql/api_versions.graphql +1 -1
  36. data/lib/graphql/find_organization.graphql +2 -1
  37. data/lib/project_types/extension/cli.rb +19 -15
  38. data/lib/project_types/extension/commands/build.rb +4 -5
  39. data/lib/project_types/extension/commands/check.rb +44 -0
  40. data/lib/project_types/extension/commands/connect.rb +35 -0
  41. data/lib/project_types/extension/commands/create.rb +12 -16
  42. data/lib/project_types/extension/commands/extension_command.rb +2 -2
  43. data/lib/project_types/extension/commands/info.rb +86 -0
  44. data/lib/project_types/extension/commands/push.rb +8 -7
  45. data/lib/project_types/extension/commands/register.rb +4 -5
  46. data/lib/project_types/extension/commands/serve.rb +5 -8
  47. data/lib/project_types/extension/commands/tunnel.rb +3 -1
  48. data/lib/project_types/extension/errors.rb +9 -0
  49. data/lib/project_types/extension/extension_project.rb +24 -1
  50. data/lib/project_types/extension/extension_project_keys.rb +1 -0
  51. data/lib/project_types/extension/features/argo.rb +6 -6
  52. data/lib/project_types/extension/features/argo_runtime.rb +22 -66
  53. data/lib/project_types/extension/features/argo_serve.rb +25 -18
  54. data/lib/project_types/extension/forms/connect.rb +42 -0
  55. data/lib/project_types/extension/forms/questions/ask_name.rb +14 -6
  56. data/lib/project_types/extension/forms/questions/ask_registration.rb +51 -0
  57. data/lib/project_types/extension/messages/messages.rb +84 -16
  58. data/lib/project_types/extension/models/specification.rb +1 -0
  59. data/lib/project_types/extension/models/specification_handlers/{checkout_argo_extension.rb → checkout_ui_extension.rb} +3 -1
  60. data/lib/project_types/extension/models/specification_handlers/default.rb +13 -3
  61. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +89 -0
  62. data/lib/project_types/extension/models/specifications.rb +1 -0
  63. data/lib/project_types/extension/tasks/configure_features.rb +6 -7
  64. data/lib/project_types/extension/tasks/configure_options.rb +20 -0
  65. data/lib/project_types/extension/tasks/get_extensions.rb +32 -0
  66. data/lib/project_types/node/cli.rb +9 -21
  67. data/lib/project_types/node/commands/connect.rb +8 -2
  68. data/lib/project_types/node/commands/create.rb +9 -5
  69. data/lib/project_types/node/commands/deploy.rb +15 -5
  70. data/lib/project_types/node/commands/deploy/heroku.rb +29 -29
  71. data/lib/project_types/node/commands/generate.rb +4 -2
  72. data/lib/project_types/node/commands/open.rb +4 -2
  73. data/lib/project_types/node/commands/serve.rb +3 -2
  74. data/lib/project_types/node/commands/tunnel.rb +4 -2
  75. data/lib/project_types/node/messages/messages.rb +47 -90
  76. data/lib/project_types/rails/cli.rb +9 -21
  77. data/lib/project_types/rails/commands/connect.rb +8 -2
  78. data/lib/project_types/rails/commands/create.rb +10 -6
  79. data/lib/project_types/rails/commands/deploy.rb +15 -5
  80. data/lib/project_types/rails/commands/deploy/heroku.rb +84 -82
  81. data/lib/project_types/rails/commands/generate.rb +15 -5
  82. data/lib/project_types/rails/commands/generate/webhook.rb +28 -26
  83. data/lib/project_types/rails/commands/open.rb +4 -2
  84. data/lib/project_types/rails/commands/serve.rb +3 -2
  85. data/lib/project_types/rails/commands/tunnel.rb +4 -2
  86. data/lib/project_types/rails/messages/messages.rb +72 -119
  87. data/lib/project_types/script/cli.rb +6 -8
  88. data/lib/project_types/script/commands/create.rb +3 -1
  89. data/lib/project_types/script/commands/push.rb +7 -4
  90. data/lib/project_types/script/graphql/app_script_update_or_create.graphql +9 -3
  91. data/lib/project_types/script/layers/application/create_script.rb +4 -3
  92. data/lib/project_types/script/layers/domain/errors.rb +6 -11
  93. data/lib/project_types/script/layers/domain/push_package.rb +4 -8
  94. data/lib/project_types/script/layers/domain/script_json.rb +32 -0
  95. data/lib/project_types/script/layers/domain/script_project.rb +1 -1
  96. data/lib/project_types/script/layers/infrastructure/errors.rb +13 -17
  97. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_project_creator.rb +29 -21
  98. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +2 -4
  99. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +45 -34
  100. data/lib/project_types/script/layers/infrastructure/script_service.rb +37 -16
  101. data/lib/project_types/script/messages/messages.rb +64 -54
  102. data/lib/project_types/script/tasks/ensure_env.rb +3 -1
  103. data/lib/project_types/script/ui/error_handler.rb +32 -32
  104. data/lib/project_types/theme/cli.rb +16 -27
  105. data/lib/project_types/theme/commands/check.rb +33 -0
  106. data/lib/project_types/theme/commands/delete.rb +64 -0
  107. data/lib/project_types/theme/commands/init.rb +42 -0
  108. data/lib/project_types/theme/commands/language_server.rb +16 -0
  109. data/lib/project_types/theme/commands/package.rb +55 -0
  110. data/lib/project_types/theme/commands/publish.rb +43 -0
  111. data/lib/project_types/theme/commands/pull.rb +51 -0
  112. data/lib/project_types/theme/commands/push.rb +58 -32
  113. data/lib/project_types/theme/commands/serve.rb +8 -16
  114. data/lib/project_types/theme/forms/confirm_store.rb +15 -0
  115. data/lib/project_types/theme/forms/select.rb +59 -0
  116. data/lib/project_types/theme/messages/messages.rb +118 -103
  117. data/lib/project_types/theme/ui/sync_progress_bar.rb +20 -0
  118. data/lib/shopify-cli/admin_api.rb +57 -38
  119. data/lib/shopify-cli/admin_api/populate_resource_command.rb +6 -14
  120. data/lib/shopify-cli/admin_api/schema.rb +1 -10
  121. data/lib/shopify-cli/api.rb +29 -14
  122. data/lib/shopify-cli/command.rb +15 -3
  123. data/lib/shopify-cli/commands.rb +7 -2
  124. data/lib/shopify-cli/commands/help.rb +2 -29
  125. data/lib/shopify-cli/commands/login.rb +95 -0
  126. data/lib/shopify-cli/commands/logout.rb +24 -8
  127. data/lib/shopify-cli/commands/populate.rb +23 -0
  128. data/lib/{project_types/node → shopify-cli}/commands/populate/customer.rb +2 -8
  129. data/lib/{project_types/node → shopify-cli}/commands/populate/draft_order.rb +2 -2
  130. data/lib/{project_types/node → shopify-cli}/commands/populate/product.rb +2 -8
  131. data/lib/shopify-cli/commands/store.rb +15 -0
  132. data/lib/shopify-cli/commands/switch.rb +39 -0
  133. data/lib/shopify-cli/commands/system.rb +12 -0
  134. data/lib/shopify-cli/commands/whoami.rb +28 -0
  135. data/lib/shopify-cli/connect.rb +32 -0
  136. data/lib/shopify-cli/context.rb +65 -4
  137. data/lib/shopify-cli/core/entry_point.rb +3 -22
  138. data/lib/shopify-cli/core/monorail.rb +6 -2
  139. data/lib/shopify-cli/db.rb +4 -4
  140. data/lib/shopify-cli/http_request.rb +16 -0
  141. data/lib/shopify-cli/identity_auth.rb +282 -0
  142. data/lib/shopify-cli/{oauth → identity_auth}/servlet.rb +11 -12
  143. data/lib/shopify-cli/messages/messages.rb +140 -46
  144. data/lib/shopify-cli/packager.rb +5 -5
  145. data/lib/shopify-cli/partners_api.rb +21 -44
  146. data/lib/shopify-cli/partners_api/organizations.rb +8 -0
  147. data/lib/shopify-cli/project_commands.rb +16 -0
  148. data/lib/shopify-cli/project_type.rb +0 -31
  149. data/lib/shopify-cli/shopifolk.rb +8 -11
  150. data/lib/shopify-cli/sub_command.rb +1 -0
  151. data/lib/shopify-cli/tasks.rb +3 -0
  152. data/lib/shopify-cli/tasks/confirm_store.rb +18 -0
  153. data/lib/shopify-cli/tasks/create_api_client.rb +2 -2
  154. data/lib/shopify-cli/tasks/ensure_authenticated.rb +13 -0
  155. data/lib/shopify-cli/tasks/ensure_loopback_url.rb +1 -1
  156. data/lib/shopify-cli/tasks/ensure_project_type.rb +12 -0
  157. data/lib/shopify-cli/tasks/select_org_and_shop.rb +0 -3
  158. data/lib/shopify-cli/theme/dev_server.rb +98 -0
  159. data/lib/shopify-cli/theme/dev_server/certificate_manager.rb +79 -0
  160. data/lib/shopify-cli/theme/dev_server/header_hash.rb +94 -0
  161. data/lib/shopify-cli/theme/dev_server/hot-reload.js +93 -0
  162. data/lib/shopify-cli/theme/dev_server/hot_reload.rb +76 -0
  163. data/lib/shopify-cli/theme/dev_server/local_assets.rb +87 -0
  164. data/lib/shopify-cli/theme/dev_server/proxy.rb +205 -0
  165. data/lib/shopify-cli/theme/dev_server/sse.rb +75 -0
  166. data/lib/shopify-cli/theme/dev_server/watcher.rb +59 -0
  167. data/lib/shopify-cli/theme/dev_server/web_server.rb +140 -0
  168. data/lib/shopify-cli/theme/development_theme.rb +69 -0
  169. data/lib/shopify-cli/theme/file.rb +112 -0
  170. data/lib/shopify-cli/theme/ignore_filter.rb +109 -0
  171. data/lib/shopify-cli/theme/mime_type.rb +34 -0
  172. data/lib/shopify-cli/theme/syncer.rb +332 -0
  173. data/lib/shopify-cli/theme/theme.rb +204 -0
  174. data/lib/shopify-cli/tunnel.rb +1 -1
  175. data/lib/shopify-cli/version.rb +1 -1
  176. data/lib/shopify_cli.rb +18 -11
  177. data/shopify-cli.gemspec +12 -5
  178. data/shopify.fish +1 -1
  179. data/shopify.sh +1 -1
  180. metadata +92 -35
  181. data/.github/workflows/release.yml +0 -59
  182. data/lib/project_types/extension/features/argo_serve_options.rb +0 -42
  183. data/lib/project_types/node/commands/populate.rb +0 -23
  184. data/lib/project_types/rails/commands/populate.rb +0 -23
  185. data/lib/project_types/rails/commands/populate/customer.rb +0 -31
  186. data/lib/project_types/rails/commands/populate/draft_order.rb +0 -28
  187. data/lib/project_types/rails/commands/populate/product.rb +0 -30
  188. data/lib/project_types/script/layers/domain/config_ui.rb +0 -16
  189. data/lib/project_types/theme/commands/connect.rb +0 -54
  190. data/lib/project_types/theme/commands/create.rb +0 -48
  191. data/lib/project_types/theme/commands/deploy.rb +0 -38
  192. data/lib/project_types/theme/commands/generate.rb +0 -20
  193. data/lib/project_types/theme/commands/generate/env.rb +0 -79
  194. data/lib/project_types/theme/forms/connect.rb +0 -34
  195. data/lib/project_types/theme/forms/create.rb +0 -22
  196. data/lib/project_types/theme/tasks/ensure_themekit_installed.rb +0 -78
  197. data/lib/project_types/theme/themekit.rb +0 -113
  198. data/lib/shopify-cli/commands/connect.rb +0 -64
  199. data/lib/shopify-cli/commands/create.rb +0 -50
  200. data/lib/shopify-cli/oauth.rb +0 -198
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+ require "thread"
3
+
4
+ module ShopifyCli
5
+ module Theme
6
+ module DevServer
7
+ # Server-Sent events implementation for Rack.
8
+ # Based on https://gist.github.com/raggi/ff7971991297e5c8a1ce
9
+ class SSE
10
+ class Event < Struct.new(:data)
11
+ def to_s
12
+ "data: #{data}\n" \
13
+ "\n\n"
14
+ end
15
+ end
16
+
17
+ class Stream
18
+ def initialize(streams)
19
+ @streams = streams
20
+ @queue = Queue.new
21
+ end
22
+
23
+ def <<(event)
24
+ raise TypeError, "expected SSE::Event" unless event.is_a?(Event)
25
+ @queue << event
26
+ end
27
+
28
+ def each
29
+ while (event = @queue.pop)
30
+ yield event.to_s
31
+ end
32
+ end
33
+
34
+ def close
35
+ @streams.remove(self)
36
+ @queue << nil
37
+ end
38
+ end
39
+
40
+ class Streams
41
+ def initialize
42
+ @list = []
43
+ end
44
+
45
+ def size
46
+ @list.size
47
+ end
48
+
49
+ def new
50
+ stream = Stream.new(self)
51
+ @list << stream
52
+ stream
53
+ end
54
+
55
+ def remove(stream)
56
+ raise TypeError, "expected SSE::Stream" unless stream.is_a?(Stream)
57
+ @list.delete(stream)
58
+ end
59
+
60
+ def broadcast(data)
61
+ raise TypeError, "expected String" unless data.is_a?(String)
62
+ @list.each do |stream|
63
+ stream << Event.new(data)
64
+ end
65
+ end
66
+
67
+ def close
68
+ @list.each(&:close)
69
+ @list.clear
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+ require "listen"
3
+ require "observer"
4
+
5
+ module ShopifyCli
6
+ module Theme
7
+ module DevServer
8
+ # Watches for file changes and publish events to the theme
9
+ class Watcher
10
+ include Observable
11
+
12
+ def initialize(ctx, theme:, syncer:, ignore_filter: nil)
13
+ @ctx = ctx
14
+ @theme = theme
15
+ @syncer = syncer
16
+ @ignore_filter = ignore_filter
17
+ @listener = Listen.to(@theme.root) do |modified, added, removed|
18
+ changed
19
+ notify_observers(modified, added, removed)
20
+ end
21
+
22
+ add_observer(self, :upload_files_when_changed)
23
+ end
24
+
25
+ def start
26
+ @listener.start
27
+ end
28
+
29
+ def stop
30
+ @listener.stop
31
+ end
32
+
33
+ def upload_files_when_changed(modified, added, removed)
34
+ modified_theme_files = filter_theme_files(modified + added)
35
+ if modified_theme_files.any?
36
+ @syncer.enqueue_updates(modified_theme_files)
37
+ end
38
+
39
+ removed_theme_files = filter_remote_files(removed)
40
+ if removed_theme_files.any?
41
+ @syncer.enqueue_deletes(removed_theme_files)
42
+ end
43
+ end
44
+
45
+ def filter_theme_files(files)
46
+ files
47
+ .select { |file| @theme.theme_file?(file) }
48
+ .reject { |file| @ignore_filter&.ignore?(file) }
49
+ end
50
+
51
+ def filter_remote_files(files)
52
+ files
53
+ .select { |file| @syncer.remote_file?(file) }
54
+ .reject { |file| @ignore_filter&.ignore?(file) }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webrick"
4
+ require "stringio"
5
+
6
+ module ShopifyCli
7
+ module Theme
8
+ module DevServer
9
+ # WEBrick will sometimes cause a fatal deadlock error on shutdown.
10
+ # The error happens because `Thread#join` is called without a timeout argument.
11
+ # We monkey-patch WEBrick to call `Thread#join(timeout)` before the existing
12
+ # `Thread#join`.
13
+ module WEBrickGenericServerThreadJoinWithTimeout
14
+ # Hook into a method called right before the threads are shutdown.
15
+ def cleanup_listener
16
+ # Force a Thread#join with a timeout to prevent any deadlock error on stop
17
+ Thread.list.each do |thread|
18
+ next unless thread[:WEBrickThread]
19
+ thread.join(2)
20
+ # Prevent the `join` call without a timeout inside WEBrick.
21
+ thread[:WEBrickThread] = false
22
+ end
23
+ super
24
+ end
25
+ end
26
+
27
+ # Base on Rack::Handler::WEBrick
28
+ class WebServer < ::WEBrick::HTTPServlet::AbstractServlet
29
+ def self.run(app, **options)
30
+ environment = ENV["RACK_ENV"] || "development"
31
+ default_host = environment == "development" ? "localhost" : nil
32
+
33
+ if !options[:BindAddress] || options[:Host]
34
+ options[:BindAddress] = options.delete(:Host) || default_host
35
+ end
36
+ options[:Port] ||= 8080
37
+ if options[:SSLEnable]
38
+ require "webrick/https"
39
+ end
40
+
41
+ @server = ::WEBrick::HTTPServer.new(options)
42
+ @server.extend(WEBrickGenericServerThreadJoinWithTimeout)
43
+ @server.mount("/", WebServer, app)
44
+ yield @server if block_given?
45
+ @server.start
46
+ end
47
+
48
+ def self.valid_options
49
+ environment = ENV["RACK_ENV"] || "development"
50
+ default_host = environment == "development" ? "localhost" : "0.0.0.0"
51
+
52
+ {
53
+ "Host=HOST" => "Hostname to listen on (default: #{default_host})",
54
+ "Port=PORT" => "Port to listen on (default: 8080)",
55
+ }
56
+ end
57
+
58
+ def self.shutdown
59
+ if @server
60
+ @server.shutdown
61
+ @server = nil
62
+ end
63
+ end
64
+
65
+ def initialize(server, app)
66
+ super(server)
67
+ @app = app
68
+ end
69
+
70
+ def service(req, res)
71
+ # res.rack = true
72
+ env = req.meta_vars
73
+ env.delete_if { |_k, v| v.nil? }
74
+
75
+ rack_input = StringIO.new(req.body.to_s)
76
+ rack_input.set_encoding(Encoding::BINARY)
77
+
78
+ env.update(
79
+ "rack.version" => [1, 3],
80
+ "rack.input" => rack_input,
81
+ "rack.errors" => $stderr,
82
+ "rack.multithread" => true,
83
+ "rack.multiprocess" => false,
84
+ "rack.run_once" => false,
85
+ "rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http",
86
+ "rack.hijack?" => true,
87
+ "rack.hijack" => lambda { raise NotImplementedError, "only partial hijack is supported." },
88
+ "rack.hijack_io" => nil
89
+ )
90
+
91
+ env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
92
+ env["QUERY_STRING"] ||= ""
93
+ unless env["PATH_INFO"] == ""
94
+ path = req.request_uri.path
95
+ n = env["SCRIPT_NAME"].length
96
+ env["PATH_INFO"] = path[n, path.length - n]
97
+ end
98
+ env["REQUEST_PATH"] ||= [env["SCRIPT_NAME"], env["PATH_INFO"]].join
99
+
100
+ status, headers, body = @app.call(env)
101
+
102
+ res.status = status.to_i
103
+ io_lambda = nil
104
+ headers.each do |k, vs|
105
+ if k == "rack.hijack"
106
+ io_lambda = vs
107
+ elsif k == "webrick.chunked"
108
+ res.chunked = true
109
+ elsif k.downcase == "set-cookie"
110
+ res.cookies.concat(vs.split("\n"))
111
+ else
112
+ # Since WEBrick won't accept repeated headers,
113
+ # merge the values per RFC 1945 section 4.2.
114
+ res[k] = vs.split("\n").join(", ")
115
+ end
116
+ end
117
+
118
+ if io_lambda
119
+ rd, wr = IO.pipe
120
+ res.body = rd
121
+ res.chunked = true
122
+ io_lambda.call(wr)
123
+ body.close if body.respond_to?(:close)
124
+ elsif body.respond_to?(:to_path)
125
+ res.body = ::File.open(body.to_path, "rb")
126
+ body.close if body.respond_to?(:close)
127
+ else
128
+ res.body = lambda do |out|
129
+ out.set_encoding(Encoding::BINARY) if out.respond_to?(:set_encoding)
130
+ body.each do |part|
131
+ out.write(part)
132
+ end
133
+ body.close if body.respond_to?(:close)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ require_relative "theme"
3
+
4
+ require "socket"
5
+ require "securerandom"
6
+
7
+ module ShopifyCli
8
+ module Theme
9
+ class DevelopmentTheme < Theme
10
+ def id
11
+ ShopifyCli::DB.get(:development_theme_id)
12
+ end
13
+
14
+ def name
15
+ ShopifyCli::DB.get(:development_theme_name) || generate_theme_name
16
+ end
17
+
18
+ def role
19
+ "development"
20
+ end
21
+
22
+ def ensure_exists!
23
+ if exists?
24
+ @ctx.debug("Using temporary development theme: ##{id} #{name}")
25
+ else
26
+ create
27
+ @ctx.debug("Created temporary development theme: #{@id}")
28
+ ShopifyCli::DB.set(development_theme_id: @id)
29
+ end
30
+ end
31
+
32
+ def exists?
33
+ return false unless id
34
+
35
+ ShopifyCli::AdminAPI.rest_request(
36
+ @ctx,
37
+ shop: shop,
38
+ path: "themes/#{id}.json",
39
+ api_version: "unstable",
40
+ )
41
+ rescue ShopifyCli::API::APIRequestNotFoundError
42
+ false
43
+ end
44
+
45
+ def delete
46
+ super if exists?
47
+ ShopifyCli::DB.del(:development_theme_id) if ShopifyCli::DB.exists?(:development_theme_id)
48
+ ShopifyCli::DB.del(:development_theme_name) if ShopifyCli::DB.exists?(:development_theme_name)
49
+ end
50
+
51
+ def self.delete(ctx)
52
+ new(ctx).delete
53
+ end
54
+
55
+ private
56
+
57
+ def generate_theme_name
58
+ hostname = Socket.gethostname.split(".").shift
59
+ hash = SecureRandom.hex(3)
60
+
61
+ theme_name = "Development (#{hash}-#{hostname})"
62
+
63
+ ShopifyCli::DB.set(development_theme_name: theme_name)
64
+
65
+ theme_name
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+ require_relative "mime_type"
3
+
4
+ module ShopifyCli
5
+ module Theme
6
+ class File < Struct.new(:path)
7
+ attr_reader :relative_path
8
+ attr_accessor :remote_checksum
9
+
10
+ def initialize(path, root)
11
+ super(Pathname.new(path))
12
+
13
+ # Path may be relative or absolute depending on the source.
14
+ # By converting both the path and the root to absolute paths, we
15
+ # can safely fetch a relative path.
16
+ @relative_path = self.path.expand_path.relative_path_from(root.expand_path)
17
+ end
18
+
19
+ def read
20
+ path.read
21
+ end
22
+
23
+ def write(*args)
24
+ path.parent.mkpath unless path.parent.directory?
25
+ path.write(*args)
26
+ end
27
+
28
+ def delete
29
+ path.delete
30
+ end
31
+
32
+ def exist?
33
+ path.exist?
34
+ end
35
+
36
+ def mime_type
37
+ @mime_type ||= MimeType.by_filename(relative_path)
38
+ end
39
+
40
+ def text?
41
+ mime_type.text?
42
+ end
43
+
44
+ def liquid?
45
+ path.extname == ".liquid"
46
+ end
47
+
48
+ def json?
49
+ path.extname == ".json"
50
+ end
51
+
52
+ def template?
53
+ relative_path.to_s.start_with?("templates/")
54
+ end
55
+
56
+ def checksum
57
+ content = read
58
+ if mime_type.json?
59
+ # Normalize JSON to match backend
60
+ begin
61
+ content = normalize_json(content)
62
+ rescue JSON::JSONError
63
+ # Fallback to using the raw content
64
+ end
65
+ end
66
+ Digest::MD5.hexdigest(content)
67
+ end
68
+
69
+ # Make it possible to check whether a given File is within a list of Files with `include?`,
70
+ # some of which may be relative paths while others are absolute paths.
71
+ def ==(other)
72
+ relative_path == other.relative_path
73
+ end
74
+
75
+ private
76
+
77
+ def normalize_json(content)
78
+ parsed = JSON.parse(content)
79
+
80
+ if template?
81
+ JsonTemplateNormalizer.new.visit_document(parsed)
82
+ end
83
+
84
+ normalized = JSON.generate(parsed)
85
+ # Backend escapes forward slashes
86
+ normalized.gsub!(/\//, "\\/")
87
+ normalized
88
+ end
89
+
90
+ class JsonTemplateNormalizer
91
+ def visit_document(value)
92
+ visit_hash(value["sections"])
93
+ end
94
+
95
+ def visit_hash(hash)
96
+ return unless hash.is_a?(Hash)
97
+ hash.each do |_, value|
98
+ visit_value(value)
99
+ end
100
+ end
101
+
102
+ def visit_value(value)
103
+ # Reinsert settings to force the same ordering as in the backend
104
+ settings = value.delete("settings") || {}
105
+ value["settings"] = settings
106
+
107
+ visit_hash(value["blocks"])
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end