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.
Files changed (179) 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 +35 -29
  10. data/Gemfile +4 -0
  11. data/Gemfile.lock +32 -0
  12. data/LICENSE +4 -1
  13. data/README.md +92 -26
  14. data/RELEASING.md +29 -7
  15. data/Rakefile +2 -2
  16. data/SECURITY.md +1 -1
  17. data/bin/load_shopify.rb +1 -1
  18. data/bin/shopify +3 -3
  19. data/dev.yml +1 -1
  20. data/docs/app/node/index.md +1 -1
  21. data/docs/app/rails/index.md +1 -1
  22. data/docs/core/index.md +1 -1
  23. data/docs/getting-started/index.md +1 -1
  24. data/docs/getting-started/install/index.md +1 -1
  25. data/docs/getting-started/migrate/index.md +1 -1
  26. data/docs/getting-started/uninstall/index.md +1 -1
  27. data/docs/getting-started/upgrade/index.md +1 -1
  28. data/docs/help/start-app/index.md +1 -1
  29. data/docs/index.md +1 -1
  30. data/ext/shopify-cli/extconf.rb +17 -5
  31. data/install.sh +1 -1
  32. data/lib/docgen/index_template.md.erb +2 -2
  33. data/lib/graphql/all_orgs_with_extensions.graphql +37 -0
  34. data/lib/graphql/find_organization.graphql +2 -1
  35. data/lib/project_types/extension/cli.rb +18 -15
  36. data/lib/project_types/extension/commands/build.rb +4 -5
  37. data/lib/project_types/extension/commands/connect.rb +35 -0
  38. data/lib/project_types/extension/commands/create.rb +12 -16
  39. data/lib/project_types/extension/commands/extension_command.rb +2 -2
  40. data/lib/project_types/extension/commands/info.rb +86 -0
  41. data/lib/project_types/extension/commands/push.rb +8 -7
  42. data/lib/project_types/extension/commands/register.rb +4 -5
  43. data/lib/project_types/extension/commands/serve.rb +5 -8
  44. data/lib/project_types/extension/commands/tunnel.rb +3 -1
  45. data/lib/project_types/extension/errors.rb +9 -0
  46. data/lib/project_types/extension/extension_project.rb +5 -0
  47. data/lib/project_types/extension/features/argo.rb +6 -6
  48. data/lib/project_types/extension/features/argo_runtime.rb +22 -66
  49. data/lib/project_types/extension/features/argo_serve.rb +25 -18
  50. data/lib/project_types/extension/forms/connect.rb +42 -0
  51. data/lib/project_types/extension/forms/questions/ask_name.rb +14 -6
  52. data/lib/project_types/extension/forms/questions/ask_registration.rb +51 -0
  53. data/lib/project_types/extension/messages/messages.rb +75 -11
  54. data/lib/project_types/extension/models/specification.rb +1 -0
  55. data/lib/project_types/extension/models/specification_handlers/{checkout_argo_extension.rb → checkout_ui_extension.rb} +3 -1
  56. data/lib/project_types/extension/models/specification_handlers/default.rb +13 -3
  57. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +86 -0
  58. data/lib/project_types/extension/models/specifications.rb +1 -0
  59. data/lib/project_types/extension/tasks/configure_features.rb +6 -7
  60. data/lib/project_types/extension/tasks/configure_options.rb +20 -0
  61. data/lib/project_types/extension/tasks/get_extensions.rb +32 -0
  62. data/lib/project_types/node/cli.rb +9 -21
  63. data/lib/project_types/node/commands/connect.rb +8 -2
  64. data/lib/project_types/node/commands/create.rb +9 -5
  65. data/lib/project_types/node/commands/deploy.rb +15 -5
  66. data/lib/project_types/node/commands/deploy/heroku.rb +29 -29
  67. data/lib/project_types/node/commands/generate.rb +4 -2
  68. data/lib/project_types/node/commands/open.rb +4 -2
  69. data/lib/project_types/node/commands/serve.rb +3 -2
  70. data/lib/project_types/node/commands/tunnel.rb +4 -2
  71. data/lib/project_types/node/messages/messages.rb +46 -89
  72. data/lib/project_types/rails/cli.rb +9 -21
  73. data/lib/project_types/rails/commands/connect.rb +8 -2
  74. data/lib/project_types/rails/commands/create.rb +10 -6
  75. data/lib/project_types/rails/commands/deploy.rb +15 -5
  76. data/lib/project_types/rails/commands/deploy/heroku.rb +84 -82
  77. data/lib/project_types/rails/commands/generate.rb +15 -5
  78. data/lib/project_types/rails/commands/generate/webhook.rb +28 -26
  79. data/lib/project_types/rails/commands/open.rb +4 -2
  80. data/lib/project_types/rails/commands/serve.rb +3 -2
  81. data/lib/project_types/rails/commands/tunnel.rb +4 -2
  82. data/lib/project_types/rails/messages/messages.rb +54 -101
  83. data/lib/project_types/script/cli.rb +5 -7
  84. data/lib/project_types/script/commands/create.rb +3 -1
  85. data/lib/project_types/script/commands/push.rb +4 -2
  86. data/lib/project_types/script/messages/messages.rb +52 -45
  87. data/lib/project_types/script/ui/error_handler.rb +2 -2
  88. data/lib/project_types/theme/cli.rb +15 -27
  89. data/lib/project_types/theme/commands/check.rb +33 -0
  90. data/lib/project_types/theme/commands/delete.rb +64 -0
  91. data/lib/project_types/theme/commands/language_server.rb +16 -0
  92. data/lib/project_types/theme/commands/package.rb +55 -0
  93. data/lib/project_types/theme/commands/publish.rb +43 -0
  94. data/lib/project_types/theme/commands/pull.rb +51 -0
  95. data/lib/project_types/theme/commands/push.rb +58 -32
  96. data/lib/project_types/theme/commands/serve.rb +7 -17
  97. data/lib/project_types/theme/forms/confirm_store.rb +15 -0
  98. data/lib/project_types/theme/forms/select.rb +59 -0
  99. data/lib/project_types/theme/messages/messages.rb +110 -106
  100. data/lib/project_types/theme/ui/sync_progress_bar.rb +20 -0
  101. data/lib/shopify-cli/admin_api.rb +53 -38
  102. data/lib/shopify-cli/admin_api/populate_resource_command.rb +6 -14
  103. data/lib/shopify-cli/admin_api/schema.rb +1 -10
  104. data/lib/shopify-cli/api.rb +29 -14
  105. data/lib/shopify-cli/command.rb +15 -3
  106. data/lib/shopify-cli/commands.rb +7 -2
  107. data/lib/shopify-cli/commands/help.rb +2 -29
  108. data/lib/shopify-cli/commands/login.rb +95 -0
  109. data/lib/shopify-cli/commands/logout.rb +24 -8
  110. data/lib/shopify-cli/commands/populate.rb +23 -0
  111. data/lib/{project_types/node → shopify-cli}/commands/populate/customer.rb +2 -8
  112. data/lib/{project_types/node → shopify-cli}/commands/populate/draft_order.rb +2 -2
  113. data/lib/{project_types/node → shopify-cli}/commands/populate/product.rb +2 -8
  114. data/lib/shopify-cli/commands/store.rb +15 -0
  115. data/lib/shopify-cli/commands/switch.rb +39 -0
  116. data/lib/shopify-cli/commands/system.rb +12 -0
  117. data/lib/shopify-cli/commands/whoami.rb +28 -0
  118. data/lib/shopify-cli/connect.rb +32 -0
  119. data/lib/shopify-cli/context.rb +52 -4
  120. data/lib/shopify-cli/core/entry_point.rb +3 -22
  121. data/lib/shopify-cli/db.rb +4 -4
  122. data/lib/shopify-cli/http_request.rb +10 -0
  123. data/lib/shopify-cli/identity_auth.rb +282 -0
  124. data/lib/shopify-cli/{oauth → identity_auth}/servlet.rb +11 -12
  125. data/lib/shopify-cli/messages/messages.rb +132 -39
  126. data/lib/shopify-cli/partners_api.rb +21 -44
  127. data/lib/shopify-cli/partners_api/organizations.rb +8 -0
  128. data/lib/shopify-cli/project_commands.rb +16 -0
  129. data/lib/shopify-cli/project_type.rb +0 -31
  130. data/lib/shopify-cli/shopifolk.rb +8 -11
  131. data/lib/shopify-cli/sub_command.rb +1 -0
  132. data/lib/shopify-cli/tasks.rb +3 -0
  133. data/lib/shopify-cli/tasks/confirm_store.rb +18 -0
  134. data/lib/shopify-cli/tasks/create_api_client.rb +2 -2
  135. data/lib/shopify-cli/tasks/ensure_authenticated.rb +13 -0
  136. data/lib/shopify-cli/tasks/ensure_loopback_url.rb +1 -1
  137. data/lib/shopify-cli/tasks/ensure_project_type.rb +12 -0
  138. data/lib/shopify-cli/tasks/select_org_and_shop.rb +0 -3
  139. data/lib/shopify-cli/theme/dev_server.rb +98 -0
  140. data/lib/shopify-cli/theme/dev_server/certificate_manager.rb +79 -0
  141. data/lib/shopify-cli/theme/dev_server/header_hash.rb +94 -0
  142. data/lib/shopify-cli/theme/dev_server/hot-reload.js +93 -0
  143. data/lib/shopify-cli/theme/dev_server/hot_reload.rb +76 -0
  144. data/lib/shopify-cli/theme/dev_server/local_assets.rb +87 -0
  145. data/lib/shopify-cli/theme/dev_server/proxy.rb +205 -0
  146. data/lib/shopify-cli/theme/dev_server/sse.rb +75 -0
  147. data/lib/shopify-cli/theme/dev_server/watcher.rb +59 -0
  148. data/lib/shopify-cli/theme/dev_server/web_server.rb +140 -0
  149. data/lib/shopify-cli/theme/development_theme.rb +69 -0
  150. data/lib/shopify-cli/theme/file.rb +112 -0
  151. data/lib/shopify-cli/theme/ignore_filter.rb +109 -0
  152. data/lib/shopify-cli/theme/mime_type.rb +34 -0
  153. data/lib/shopify-cli/theme/syncer.rb +328 -0
  154. data/lib/shopify-cli/theme/theme.rb +204 -0
  155. data/lib/shopify-cli/version.rb +1 -1
  156. data/lib/shopify_cli.rb +18 -11
  157. data/shopify-cli.gemspec +12 -5
  158. data/shopify.fish +1 -1
  159. data/shopify.sh +1 -1
  160. metadata +88 -34
  161. data/.github/workflows/release.yml +0 -59
  162. data/lib/project_types/extension/features/argo_serve_options.rb +0 -42
  163. data/lib/project_types/node/commands/populate.rb +0 -23
  164. data/lib/project_types/rails/commands/populate.rb +0 -23
  165. data/lib/project_types/rails/commands/populate/customer.rb +0 -31
  166. data/lib/project_types/rails/commands/populate/draft_order.rb +0 -28
  167. data/lib/project_types/rails/commands/populate/product.rb +0 -30
  168. data/lib/project_types/theme/commands/connect.rb +0 -54
  169. data/lib/project_types/theme/commands/create.rb +0 -48
  170. data/lib/project_types/theme/commands/deploy.rb +0 -38
  171. data/lib/project_types/theme/commands/generate.rb +0 -20
  172. data/lib/project_types/theme/commands/generate/env.rb +0 -79
  173. data/lib/project_types/theme/forms/connect.rb +0 -34
  174. data/lib/project_types/theme/forms/create.rb +0 -22
  175. data/lib/project_types/theme/tasks/ensure_themekit_installed.rb +0 -78
  176. data/lib/project_types/theme/themekit.rb +0 -113
  177. data/lib/shopify-cli/commands/connect.rb +0 -64
  178. data/lib/shopify-cli/commands/create.rb +0 -50
  179. data/lib/shopify-cli/oauth.rb +0 -198
@@ -26,6 +26,14 @@ module ShopifyCli
26
26
  org
27
27
  end
28
28
  end
29
+
30
+ def fetch_with_extensions(ctx, type)
31
+ resp = PartnersAPI.query(ctx, "all_orgs_with_extensions", type: type)
32
+ (resp&.dig("data", "organizations", "nodes") || []).map do |org|
33
+ org["apps"] = (org.dig("apps", "nodes") || [])
34
+ org
35
+ end
36
+ end
29
37
  end
30
38
  end
31
39
  end
@@ -0,0 +1,16 @@
1
+ module ShopifyCli
2
+ class ProjectCommands < Command
3
+ def call(*)
4
+ @ctx.puts(self.class.help)
5
+ end
6
+
7
+ def self.help
8
+ project_type = name.split("::")[0].downcase
9
+ ShopifyCli::Context.message(
10
+ "#{project_type}.help",
11
+ ShopifyCli::TOOL_NAME,
12
+ subcommand_registry.command_names.join(" | ")
13
+ )
14
+ end
15
+ end
16
+ end
@@ -5,7 +5,6 @@ module ShopifyCli
5
5
  class << self
6
6
  attr_accessor :project_type,
7
7
  :project_name,
8
- :project_creator_command_class,
9
8
  :project_load_shallow
10
9
 
11
10
  def repository
@@ -49,36 +48,6 @@ module ShopifyCli
49
48
  @project_name = name
50
49
  end
51
50
 
52
- def creator(command_const)
53
- @project_creator_command_class = command_const
54
- ShopifyCli::Commands::Create.subcommand(command_const, @project_type)
55
- end
56
-
57
- def create_command
58
- const_get(@project_creator_command_class)
59
- end
60
-
61
- def connector(command_const)
62
- @project_connector_command_class = command_const
63
- ShopifyCli::Commands::Connect.subcommand(command_const, @project_type)
64
- end
65
-
66
- def connect_command
67
- if @project_connector_command_class.nil?
68
- nil
69
- else
70
- const_get(@project_connector_command_class)
71
- end
72
- end
73
-
74
- def register_command(const, cmd)
75
- return if project_load_shallow
76
- Context.new.abort(
77
- Context.message("core.project_type.error.cannot_override_core", cmd, const)
78
- ) if Commands.core_command?(cmd)
79
- Commands.register(const, cmd)
80
- end
81
-
82
51
  def register_task(task, name)
83
52
  return if project_load_shallow
84
53
  ShopifyCli::Tasks.register(task, name)
@@ -11,8 +11,6 @@ module ShopifyCli
11
11
  FEATURE_NAME = "shopifolk"
12
12
 
13
13
  class << self
14
- attr_writer :acting_as_shopify_organization
15
-
16
14
  ##
17
15
  # will return if the user appears to be a Shopify employee, based on several heuristics
18
16
  #
@@ -29,15 +27,16 @@ module ShopifyCli
29
27
  end
30
28
 
31
29
  def act_as_shopify_organization
32
- @acting_as_shopify_organization = true
30
+ DB.set(acting_as_shopify_organization: true)
33
31
  end
34
32
 
35
33
  def acting_as_shopify_organization?
36
- !!(@acting_as_shopify_organization || (Project.has_current? && Project.current.config["shopify_organization"]))
34
+ !!(DB.get(:acting_as_shopify_organization) ||
35
+ (Project.has_current? && Project.current.config["shopify_organization"]))
37
36
  end
38
37
 
39
38
  def reset
40
- @acting_as_shopify_organization = nil
39
+ DB.del(:acting_as_shopify_organization)
41
40
  end
42
41
  end
43
42
 
@@ -72,12 +71,10 @@ module ShopifyCli
72
71
  end
73
72
 
74
73
  def ini
75
- @ini ||= begin
76
- if File.exist?(GCLOUD_CONFIG_FILE)
77
- CLI::Kit::Ini
78
- .new(GCLOUD_CONFIG_FILE, default_section: "[#{SECTION}]", convert_types: false)
79
- .tap(&:parse).ini
80
- end
74
+ @ini ||= if File.exist?(GCLOUD_CONFIG_FILE)
75
+ CLI::Kit::Ini
76
+ .new(GCLOUD_CONFIG_FILE, default_section: "[#{SECTION}]", convert_types: false)
77
+ .tap(&:parse).ini
81
78
  end
82
79
  end
83
80
  end
@@ -9,6 +9,7 @@ module ShopifyCli
9
9
  args = cmd.options.parse(@_options, args[1..-1] || [])
10
10
  return call_help(parent_command, command_name) if cmd.options.help
11
11
  run_prerequisites
12
+
12
13
  cmd.call(args, command_name)
13
14
  end
14
15
  end
@@ -30,9 +30,12 @@ module ShopifyCli
30
30
  Registry.add(-> () { const_get(task) }, name)
31
31
  end
32
32
 
33
+ register :ConfirmStore, :confirm_store, "shopify-cli/tasks/confirm_store"
33
34
  register :CreateApiClient, :create_api_client, "shopify-cli/tasks/create_api_client"
35
+ register :EnsureAuthenticated, :ensure_authenticated, "shopify-cli/tasks/ensure_authenticated"
34
36
  register :EnsureEnv, :ensure_env, "shopify-cli/tasks/ensure_env"
35
37
  register :EnsureLoopbackURL, :ensure_loopback_url, "shopify-cli/tasks/ensure_loopback_url"
38
+ register :EnsureProjectType, :ensure_project_type, "shopify-cli/tasks/ensure_project_type"
36
39
  register :EnsureDevStore, :ensure_dev_store, "shopify-cli/tasks/ensure_dev_store"
37
40
  register :SelectOrgAndShop, :select_org_and_shop, "shopify-cli/tasks/select_org_and_shop"
38
41
  register :UpdateDashboardURLS, :update_dashboard_urls, "shopify-cli/tasks/update_dashboard_urls"
@@ -0,0 +1,18 @@
1
+ require "shopify_cli"
2
+
3
+ module ShopifyCli
4
+ module Tasks
5
+ class ConfirmStore < ShopifyCli::Task
6
+ def call(ctx)
7
+ @ctx = ctx
8
+ store = ShopifyCli::AdminAPI.get_shop_or_abort(ctx)
9
+ if CLI::UI::Prompt.confirm(ctx.message("core.tasks.confirm_store.prompt", store), default: false)
10
+ ctx.puts(ctx.message("core.tasks.confirm_store.confirmation", store))
11
+ else
12
+ ctx.puts(ctx.message("core.tasks.confirm_store.cancelling"))
13
+ raise AbortSilent
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -4,7 +4,7 @@ module ShopifyCli
4
4
  module Tasks
5
5
  class CreateApiClient < ShopifyCli::Task
6
6
  VALID_APP_TYPES = %w(public custom)
7
- DEFAULT_APP_URL = "https://shopify.github.io/shopify-app-cli/help/start-app/"
7
+ DEFAULT_APP_URL = "https://shopify.github.io/shopify-cli/help/start-app/"
8
8
 
9
9
  def call(ctx, org_id:, title:, type:)
10
10
  resp = ShopifyCli::PartnersAPI.query(
@@ -14,7 +14,7 @@ module ShopifyCli
14
14
  title: title,
15
15
  type: type,
16
16
  app_url: DEFAULT_APP_URL,
17
- redir: [OAuth::REDIRECT_HOST]
17
+ redir: [IdentityAuth::REDIRECT_HOST]
18
18
  )
19
19
 
20
20
  unless resp
@@ -0,0 +1,13 @@
1
+ require "shopify_cli"
2
+
3
+ module ShopifyCli
4
+ module Tasks
5
+ class EnsureAuthenticated < ShopifyCli::Task
6
+ def call(ctx)
7
+ ctx.abort(
8
+ ctx.message("core.identity_auth.login_prompt", ShopifyCli::TOOL_NAME)
9
+ ) unless ShopifyCli::IdentityAuth::IDENTITY_ACCESS_TOKENS.all? { |key| ShopifyCli::DB.exists?(key) }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -5,7 +5,7 @@ module ShopifyCli
5
5
  @ctx = ctx
6
6
  api_key = Project.current.env.api_key
7
7
  result = ShopifyCli::PartnersAPI.query(ctx, "get_app_urls", apiKey: api_key)
8
- loopback = OAuth::REDIRECT_HOST
8
+ loopback = IdentityAuth::REDIRECT_HOST
9
9
  app = result["data"]["app"]
10
10
  urls = app["redirectUrlWhitelist"]
11
11
  if urls.grep(/#{loopback}/).empty?
@@ -0,0 +1,12 @@
1
+ require "shopify_cli"
2
+
3
+ module ShopifyCli
4
+ module Tasks
5
+ class EnsureProjectType < ShopifyCli::Task
6
+ def call(ctx, project_type)
7
+ return true if project_type.to_sym == ShopifyCli::Project.current_project_type
8
+ ctx.abort(ctx.message("core.tasks.ensure_project_type.wrong_project_type", project_type))
9
+ end
10
+ end
11
+ end
12
+ end
@@ -8,9 +8,6 @@ module ShopifyCli
8
8
  def call(ctx, organization_id: nil, shop_domain: nil)
9
9
  @ctx = ctx
10
10
  return response(organization_id.to_i, shop_domain) unless organization_id.nil? || shop_domain.nil?
11
- if Shopifolk.check && wants_to_run_against_shopify_org?
12
- Shopifolk.act_as_shopify_organization
13
- end
14
11
  org = get_organization(organization_id)
15
12
  unless Shopifolk.acting_as_shopify_organization?
16
13
  shop_domain ||= get_shop_domain(org)
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+ require_relative "development_theme"
3
+ require_relative "ignore_filter"
4
+ require_relative "syncer"
5
+
6
+ require_relative "dev_server/hot_reload"
7
+ require_relative "dev_server/header_hash"
8
+ require_relative "dev_server/local_assets"
9
+ require_relative "dev_server/proxy"
10
+ require_relative "dev_server/sse"
11
+ require_relative "dev_server/watcher"
12
+ require_relative "dev_server/web_server"
13
+ require_relative "dev_server/certificate_manager"
14
+
15
+ require "pathname"
16
+
17
+ module ShopifyCli
18
+ module Theme
19
+ module DevServer
20
+ class << self
21
+ attr_accessor :ctx
22
+
23
+ def start(ctx, root, port: 9292, silent: false)
24
+ @ctx = ctx
25
+ theme = DevelopmentTheme.new(ctx, root: root)
26
+ ignore_filter = IgnoreFilter.from_path(root)
27
+ @syncer = Syncer.new(ctx, theme: theme, ignore_filter: ignore_filter)
28
+ watcher = Watcher.new(ctx, theme: theme, syncer: @syncer, ignore_filter: ignore_filter)
29
+
30
+ # Setup the middleware stack. Mimics Rack::Builder / config.ru, but in reverse order
31
+ @app = Proxy.new(ctx, theme: theme, syncer: @syncer)
32
+ @app = LocalAssets.new(ctx, @app, theme: theme)
33
+ @app = HotReload.new(ctx, @app, theme: theme, watcher: watcher, ignore_filter: ignore_filter)
34
+ stopped = false
35
+
36
+ theme.ensure_exists!
37
+
38
+ trap("INT") do
39
+ stopped = true
40
+ stop
41
+ end
42
+
43
+ CLI::UI::Frame.open(@ctx.message("theme.serve.serve")) do
44
+ ctx.print_task("Syncing theme ##{theme.id} on #{theme.shop}")
45
+ @syncer.start_threads
46
+ if silent
47
+ @syncer.upload_theme!(delay_low_priority_files: true)
48
+ else
49
+ @syncer.upload_theme_with_progress_bar!(delay_low_priority_files: true)
50
+ end
51
+
52
+ return if stopped
53
+
54
+ ctx.puts("")
55
+ ctx.puts("Serving #{theme.root}")
56
+ ctx.puts("")
57
+ ctx.open_url!("http://127.0.0.1:#{port}")
58
+ ctx.puts("")
59
+ ctx.puts("Customize this theme in the Online Store Editor:")
60
+ ctx.puts("{{green:#{theme.editor_url}}}")
61
+ ctx.puts("")
62
+ ctx.puts("Share this theme preview:")
63
+ ctx.puts("{{green:#{theme.preview_url}}}")
64
+ ctx.puts("")
65
+ ctx.puts("(Use Ctrl-C to stop)")
66
+ end
67
+
68
+ logger = if ctx.debug?
69
+ WEBrick::Log.new(nil, WEBrick::BasicLog::INFO)
70
+ else
71
+ WEBrick::Log.new(nil, WEBrick::BasicLog::FATAL)
72
+ end
73
+
74
+ watcher.start
75
+ WebServer.run(
76
+ @app,
77
+ Port: port,
78
+ Logger: logger,
79
+ AccessLog: [],
80
+ )
81
+ watcher.stop
82
+
83
+ rescue ShopifyCli::API::APIRequestForbiddenError,
84
+ ShopifyCli::API::APIRequestUnauthorizedError
85
+ @ctx.abort("You are not authorized to edit themes on #{theme.shop}.\n" \
86
+ "Make sure you are a user of that store, and allowed to edit themes.")
87
+ end
88
+
89
+ def stop
90
+ @ctx.puts("Stopping ...")
91
+ @app.close
92
+ @syncer.shutdown
93
+ WebServer.shutdown
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module ShopifyCli
6
+ module Theme
7
+ module DevServer
8
+ class CertificateManager
9
+ attr_reader :ctx, :domain_name, :certificate, :private_key
10
+
11
+ ISSUER_EXTENSIONS = [
12
+ ["subjectKeyIdentifier", "hash", false],
13
+ ["authorityKeyIdentifier", "keyid:always", false],
14
+ ]
15
+
16
+ def initialize(ctx, domain_name)
17
+ @ctx = ctx
18
+ @domain_name = domain_name
19
+ end
20
+
21
+ def find_or_create_certificates!
22
+ @private_key = if (private_key_pem = ShopifyCli::DB.get(:ssl_private_key))
23
+ OpenSSL::PKey::RSA.new(private_key_pem)
24
+ else
25
+ OpenSSL::PKey::RSA.new(2048)
26
+ end
27
+
28
+ @certificate = if (certificate_pem = ShopifyCli::DB.get(:ssl_certificate))
29
+ OpenSSL::X509::Certificate.new(certificate_pem)
30
+ else
31
+ x509_certificate = build_x509_certificate
32
+
33
+ sign_certificate!(x509_certificate)
34
+
35
+ x509_certificate
36
+ end
37
+
38
+ ShopifyCli::DB.set(ssl_certificate: certificate.to_pem)
39
+ ShopifyCli::DB.set(ssl_private_key: private_key.to_pem)
40
+ end
41
+
42
+ private
43
+
44
+ def build_x509_certificate
45
+ certificate = OpenSSL::X509::Certificate.new
46
+
47
+ certificate.public_key = private_key.public_key
48
+ certificate.subject = subject
49
+ certificate.version = 2
50
+ certificate.serial = 0x0
51
+
52
+ certificate.not_before = Time.now.utc
53
+ certificate.not_after = Time.now.utc + 365 * 24 * 60 * 60
54
+
55
+ certificate
56
+ end
57
+
58
+ def sign_certificate!(certificate)
59
+ ef = OpenSSL::X509::ExtensionFactory.new
60
+
61
+ ef.subject_certificate = certificate
62
+ ef.issuer_certificate = certificate
63
+
64
+ ISSUER_EXTENSIONS.each do |args|
65
+ certificate.add_extension(ef.create_extension(*args))
66
+ end
67
+
68
+ certificate.add_extension(ef.create_extension("subjectAltName", "DNS:#{@domain_name}", false))
69
+
70
+ certificate.sign(private_key, OpenSSL::Digest.new("SHA256"))
71
+ end
72
+
73
+ def subject
74
+ OpenSSL::X509::Name.parse("/CN=#{@domain_name}/")
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCli
4
+ module Theme
5
+ module DevServer
6
+ # Based on Rack::HeaderHash
7
+ class HeaderHash < Hash
8
+ def self.[](headers)
9
+ if headers.is_a?(HeaderHash) && !headers.frozen?
10
+ headers
11
+ else
12
+ new(headers)
13
+ end
14
+ end
15
+
16
+ def initialize(hash = {})
17
+ super()
18
+ @names = {}
19
+ hash.each { |k, v| self[k] = v }
20
+ end
21
+
22
+ # on dup/clone, we need to duplicate @names hash
23
+ def initialize_copy(other)
24
+ super
25
+ @names = other.names.dup
26
+ end
27
+
28
+ # on clear, we need to clear @names hash
29
+ def clear
30
+ super
31
+ @names.clear
32
+ end
33
+
34
+ def each
35
+ super do |k, v|
36
+ yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
37
+ end
38
+ end
39
+
40
+ def to_hash
41
+ hash = {}
42
+ each { |k, v| hash[k] = v }
43
+ hash
44
+ end
45
+
46
+ def [](k)
47
+ super(k) || super(@names[k.downcase])
48
+ end
49
+
50
+ def []=(k, v)
51
+ canonical = k.downcase.freeze
52
+ # .delete is expensive, don't invoke it unless necessary
53
+ delete(k) if @names[canonical] && @names[canonical] != k
54
+ @names[canonical] = k
55
+ super(k, v)
56
+ end
57
+
58
+ def delete(k)
59
+ canonical = k.downcase
60
+ result = super(@names.delete(canonical))
61
+ result
62
+ end
63
+
64
+ def include?(k)
65
+ super || @names.include?(k.downcase)
66
+ end
67
+
68
+ alias_method :has_key?, :include?
69
+ alias_method :member?, :include?
70
+ alias_method :key?, :include?
71
+
72
+ def merge!(other)
73
+ other.each { |k, v| self[k] = v }
74
+ self
75
+ end
76
+
77
+ def merge(other)
78
+ hash = dup
79
+ hash.merge!(other)
80
+ end
81
+
82
+ def replace(other)
83
+ clear
84
+ other.each { |k, v| self[k] = v }
85
+ self
86
+ end
87
+
88
+ protected
89
+
90
+ attr_reader :names
91
+ end
92
+ end
93
+ end
94
+ end