shopify-cli 1.11.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.
Files changed (207) 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/PULL_REQUEST_TEMPLATE.md +1 -1
  6. data/.github/workflows/build.yml +1 -1
  7. data/.gitignore +3 -0
  8. data/.rubocop.yml +3 -1
  9. data/.ruby-version +1 -1
  10. data/CHANGELOG.md +48 -20
  11. data/Gemfile +4 -0
  12. data/Gemfile.lock +32 -0
  13. data/LICENSE +4 -1
  14. data/README.md +92 -26
  15. data/RELEASING.md +29 -7
  16. data/Rakefile +2 -2
  17. data/SECURITY.md +1 -1
  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/find_organization.graphql +2 -1
  36. data/lib/project_types/extension/cli.rb +18 -15
  37. data/lib/project_types/extension/commands/build.rb +4 -5
  38. data/lib/project_types/extension/commands/connect.rb +35 -0
  39. data/lib/project_types/extension/commands/create.rb +12 -16
  40. data/lib/project_types/extension/commands/extension_command.rb +2 -2
  41. data/lib/project_types/extension/commands/info.rb +86 -0
  42. data/lib/project_types/extension/commands/push.rb +8 -7
  43. data/lib/project_types/extension/commands/register.rb +4 -5
  44. data/lib/project_types/extension/commands/serve.rb +5 -8
  45. data/lib/project_types/extension/commands/tunnel.rb +3 -1
  46. data/lib/project_types/extension/errors.rb +9 -0
  47. data/lib/project_types/extension/extension_project.rb +5 -0
  48. data/lib/project_types/extension/features/argo.rb +6 -6
  49. data/lib/project_types/extension/features/argo_runtime.rb +22 -38
  50. data/lib/project_types/extension/features/argo_serve.rb +25 -20
  51. data/lib/project_types/extension/forms/connect.rb +42 -0
  52. data/lib/project_types/extension/forms/questions/ask_name.rb +14 -6
  53. data/lib/project_types/extension/forms/questions/ask_registration.rb +51 -0
  54. data/lib/project_types/extension/messages/messages.rb +75 -11
  55. data/lib/project_types/extension/models/specification.rb +1 -0
  56. data/lib/project_types/extension/models/specification_handlers/{checkout_argo_extension.rb → checkout_ui_extension.rb} +3 -1
  57. data/lib/project_types/extension/models/specification_handlers/default.rb +21 -6
  58. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +86 -0
  59. data/lib/project_types/extension/models/specifications.rb +1 -0
  60. data/lib/project_types/extension/tasks/configure_features.rb +6 -7
  61. data/lib/project_types/extension/tasks/configure_options.rb +20 -0
  62. data/lib/project_types/extension/tasks/get_extensions.rb +32 -0
  63. data/lib/project_types/node/cli.rb +9 -21
  64. data/lib/project_types/node/commands/connect.rb +8 -2
  65. data/lib/project_types/node/commands/create.rb +9 -5
  66. data/lib/project_types/node/commands/deploy.rb +15 -5
  67. data/lib/project_types/node/commands/deploy/heroku.rb +29 -29
  68. data/lib/project_types/node/commands/generate.rb +4 -2
  69. data/lib/project_types/node/commands/open.rb +4 -2
  70. data/lib/project_types/node/commands/serve.rb +3 -2
  71. data/lib/project_types/node/commands/tunnel.rb +4 -2
  72. data/lib/project_types/node/messages/messages.rb +46 -89
  73. data/lib/project_types/rails/cli.rb +9 -21
  74. data/lib/project_types/rails/commands/connect.rb +8 -2
  75. data/lib/project_types/rails/commands/create.rb +10 -6
  76. data/lib/project_types/rails/commands/deploy.rb +15 -5
  77. data/lib/project_types/rails/commands/deploy/heroku.rb +84 -82
  78. data/lib/project_types/rails/commands/generate.rb +15 -5
  79. data/lib/project_types/rails/commands/generate/webhook.rb +28 -26
  80. data/lib/project_types/rails/commands/open.rb +4 -2
  81. data/lib/project_types/rails/commands/serve.rb +3 -2
  82. data/lib/project_types/rails/commands/tunnel.rb +4 -2
  83. data/lib/project_types/rails/messages/messages.rb +54 -101
  84. data/lib/project_types/script/cli.rb +18 -20
  85. data/lib/project_types/script/commands/create.rb +3 -1
  86. data/lib/project_types/script/commands/push.rb +12 -5
  87. data/lib/project_types/script/config/extension_points.yml +0 -3
  88. data/lib/project_types/script/graphql/app_script_update_or_create.graphql +9 -3
  89. data/lib/project_types/script/layers/application/create_script.rb +6 -5
  90. data/lib/project_types/script/layers/application/push_script.rb +2 -1
  91. data/lib/project_types/script/layers/domain/errors.rb +6 -11
  92. data/lib/project_types/script/layers/domain/push_package.rb +4 -8
  93. data/lib/project_types/script/layers/domain/script_json.rb +32 -0
  94. data/lib/project_types/script/layers/domain/script_project.rb +1 -1
  95. data/lib/project_types/script/layers/infrastructure/errors.rb +14 -18
  96. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_project_creator.rb +105 -0
  97. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_task_runner.rb +103 -0
  98. data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +26 -0
  99. data/lib/project_types/script/layers/infrastructure/languages/rust_project_creator.rb +73 -0
  100. data/lib/project_types/script/layers/infrastructure/languages/rust_task_runner.rb +60 -0
  101. data/lib/project_types/script/layers/infrastructure/languages/task_runner.rb +21 -0
  102. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +2 -4
  103. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +45 -34
  104. data/lib/project_types/script/layers/infrastructure/script_service.rb +20 -14
  105. data/lib/project_types/script/messages/messages.rb +66 -55
  106. data/lib/project_types/script/tasks/ensure_env.rb +22 -1
  107. data/lib/project_types/script/ui/error_handler.rb +32 -32
  108. data/lib/project_types/theme/cli.rb +15 -27
  109. data/lib/project_types/theme/commands/check.rb +33 -0
  110. data/lib/project_types/theme/commands/delete.rb +64 -0
  111. data/lib/project_types/theme/commands/language_server.rb +16 -0
  112. data/lib/project_types/theme/commands/package.rb +55 -0
  113. data/lib/project_types/theme/commands/publish.rb +43 -0
  114. data/lib/project_types/theme/commands/pull.rb +51 -0
  115. data/lib/project_types/theme/commands/push.rb +58 -32
  116. data/lib/project_types/theme/commands/serve.rb +7 -17
  117. data/lib/project_types/theme/forms/confirm_store.rb +15 -0
  118. data/lib/project_types/theme/forms/select.rb +59 -0
  119. data/lib/project_types/theme/messages/messages.rb +110 -106
  120. data/lib/project_types/theme/ui/sync_progress_bar.rb +20 -0
  121. data/lib/shopify-cli/admin_api.rb +53 -35
  122. data/lib/shopify-cli/admin_api/populate_resource_command.rb +6 -14
  123. data/lib/shopify-cli/admin_api/schema.rb +1 -10
  124. data/lib/shopify-cli/api.rb +29 -14
  125. data/lib/shopify-cli/command.rb +15 -3
  126. data/lib/shopify-cli/commands.rb +7 -2
  127. data/lib/shopify-cli/commands/help.rb +2 -29
  128. data/lib/shopify-cli/commands/login.rb +95 -0
  129. data/lib/shopify-cli/commands/logout.rb +24 -8
  130. data/lib/shopify-cli/commands/populate.rb +23 -0
  131. data/lib/{project_types/node → shopify-cli}/commands/populate/customer.rb +2 -8
  132. data/lib/{project_types/node → shopify-cli}/commands/populate/draft_order.rb +2 -2
  133. data/lib/{project_types/node → shopify-cli}/commands/populate/product.rb +2 -8
  134. data/lib/shopify-cli/commands/store.rb +15 -0
  135. data/lib/shopify-cli/commands/switch.rb +39 -0
  136. data/lib/shopify-cli/commands/system.rb +12 -0
  137. data/lib/shopify-cli/commands/whoami.rb +28 -0
  138. data/lib/shopify-cli/connect.rb +32 -0
  139. data/lib/shopify-cli/context.rb +65 -4
  140. data/lib/shopify-cli/core/entry_point.rb +3 -22
  141. data/lib/shopify-cli/db.rb +4 -4
  142. data/lib/shopify-cli/http_request.rb +10 -0
  143. data/lib/shopify-cli/identity_auth.rb +282 -0
  144. data/lib/shopify-cli/{oauth → identity_auth}/servlet.rb +11 -12
  145. data/lib/shopify-cli/messages/messages.rb +133 -39
  146. data/lib/shopify-cli/partners_api.rb +21 -41
  147. data/lib/shopify-cli/partners_api/organizations.rb +8 -0
  148. data/lib/shopify-cli/project_commands.rb +16 -0
  149. data/lib/shopify-cli/project_type.rb +0 -31
  150. data/lib/shopify-cli/resources/env_file.rb +1 -1
  151. data/lib/shopify-cli/shopifolk.rb +8 -11
  152. data/lib/shopify-cli/sub_command.rb +1 -0
  153. data/lib/shopify-cli/tasks.rb +3 -0
  154. data/lib/shopify-cli/tasks/confirm_store.rb +18 -0
  155. data/lib/shopify-cli/tasks/create_api_client.rb +2 -2
  156. data/lib/shopify-cli/tasks/ensure_authenticated.rb +13 -0
  157. data/lib/shopify-cli/tasks/ensure_loopback_url.rb +1 -1
  158. data/lib/shopify-cli/tasks/ensure_project_type.rb +12 -0
  159. data/lib/shopify-cli/tasks/select_org_and_shop.rb +0 -3
  160. data/lib/shopify-cli/theme/dev_server.rb +98 -0
  161. data/lib/shopify-cli/theme/dev_server/certificate_manager.rb +79 -0
  162. data/lib/shopify-cli/theme/dev_server/header_hash.rb +94 -0
  163. data/lib/shopify-cli/theme/dev_server/hot-reload.js +93 -0
  164. data/lib/shopify-cli/theme/dev_server/hot_reload.rb +76 -0
  165. data/lib/shopify-cli/theme/dev_server/local_assets.rb +87 -0
  166. data/lib/shopify-cli/theme/dev_server/proxy.rb +205 -0
  167. data/lib/shopify-cli/theme/dev_server/sse.rb +75 -0
  168. data/lib/shopify-cli/theme/dev_server/watcher.rb +59 -0
  169. data/lib/shopify-cli/theme/dev_server/web_server.rb +140 -0
  170. data/lib/shopify-cli/theme/development_theme.rb +69 -0
  171. data/lib/shopify-cli/theme/file.rb +112 -0
  172. data/lib/shopify-cli/theme/ignore_filter.rb +109 -0
  173. data/lib/shopify-cli/theme/mime_type.rb +34 -0
  174. data/lib/shopify-cli/theme/syncer.rb +328 -0
  175. data/lib/shopify-cli/theme/theme.rb +204 -0
  176. data/lib/shopify-cli/version.rb +1 -1
  177. data/lib/shopify_cli.rb +18 -11
  178. data/shopify-cli.gemspec +12 -5
  179. data/shopify.fish +1 -1
  180. data/shopify.sh +1 -1
  181. metadata +95 -41
  182. data/.github/workflows/release.yml +0 -61
  183. data/lib/project_types/extension/features/argo_serve_options.rb +0 -40
  184. data/lib/project_types/node/commands/populate.rb +0 -23
  185. data/lib/project_types/rails/commands/populate.rb +0 -23
  186. data/lib/project_types/rails/commands/populate/customer.rb +0 -31
  187. data/lib/project_types/rails/commands/populate/draft_order.rb +0 -28
  188. data/lib/project_types/rails/commands/populate/product.rb +0 -30
  189. data/lib/project_types/script/layers/domain/config_ui.rb +0 -16
  190. data/lib/project_types/script/layers/infrastructure/assemblyscript_project_creator.rb +0 -95
  191. data/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb +0 -101
  192. data/lib/project_types/script/layers/infrastructure/project_creator.rb +0 -24
  193. data/lib/project_types/script/layers/infrastructure/rust_project_creator.rb +0 -71
  194. data/lib/project_types/script/layers/infrastructure/rust_task_runner.rb +0 -58
  195. data/lib/project_types/script/layers/infrastructure/task_runner.rb +0 -19
  196. data/lib/project_types/theme/commands/connect.rb +0 -54
  197. data/lib/project_types/theme/commands/create.rb +0 -48
  198. data/lib/project_types/theme/commands/deploy.rb +0 -38
  199. data/lib/project_types/theme/commands/generate.rb +0 -20
  200. data/lib/project_types/theme/commands/generate/env.rb +0 -79
  201. data/lib/project_types/theme/forms/connect.rb +0 -34
  202. data/lib/project_types/theme/forms/create.rb +0 -22
  203. data/lib/project_types/theme/tasks/ensure_themekit_installed.rb +0 -78
  204. data/lib/project_types/theme/themekit.rb +0 -113
  205. data/lib/shopify-cli/commands/connect.rb +0 -64
  206. data/lib/shopify-cli/commands/create.rb +0 -50
  207. 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(content)
24
+ path.parent.mkpath unless path.parent.directory?
25
+ path.write(content)
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