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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/.vscode/settings.json +1 -2
  3. data/CHANGELOG.md +68 -20
  4. data/Gemfile.lock +1 -1
  5. data/Rakefile +21 -0
  6. data/ext/javy/hashes/javy-arm-macos-v0.3.0.gz.sha256 +1 -0
  7. data/ext/javy/hashes/javy-x86_64-linux-v0.3.0.gz.sha256 +1 -0
  8. data/ext/javy/hashes/javy-x86_64-macos-v0.3.0.gz.sha256 +1 -0
  9. data/ext/javy/hashes/javy-x86_64-windows-v0.3.0.gz.sha256 +1 -0
  10. data/ext/javy/version +1 -1
  11. data/ext/shopify-extensions/version +1 -1
  12. data/lib/project_types/extension/cli.rb +4 -0
  13. data/lib/project_types/extension/commands/check.rb +6 -1
  14. data/lib/project_types/extension/forms/questions/ask_template.rb +1 -2
  15. data/lib/project_types/extension/messages/messages.rb +1 -3
  16. data/lib/project_types/extension/models/development_server_requirements.rb +1 -0
  17. data/lib/project_types/extension/models/specification_handlers/beacon_extension.rb +57 -0
  18. data/lib/project_types/extension/models/specification_handlers/beacon_extension_utils/script_config.rb +33 -0
  19. data/lib/project_types/extension/models/specification_handlers/beacon_extension_utils/script_config_repository.rb +75 -0
  20. data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +16 -1
  21. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +4 -1
  22. data/lib/project_types/extension/tasks/configure_options.rb +2 -1
  23. data/lib/project_types/extension/tasks/convert_server_config.rb +13 -2
  24. data/lib/project_types/extension/tasks/merge_server_config.rb +5 -2
  25. data/lib/project_types/script/cli.rb +1 -0
  26. data/lib/project_types/script/layers/application/create_script.rb +14 -6
  27. data/lib/project_types/script/layers/infrastructure/errors.rb +17 -0
  28. data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +6 -21
  29. data/lib/project_types/script/layers/infrastructure/script_service.rb +2 -0
  30. data/lib/project_types/script/layers/infrastructure/sparse_checkout_details.rb +35 -0
  31. data/lib/project_types/script/messages/messages.rb +3 -0
  32. data/lib/project_types/script/ui/error_handler.rb +11 -0
  33. data/lib/project_types/theme/cli.rb +1 -0
  34. data/lib/project_types/theme/commands/check.rb +4 -1
  35. data/lib/project_types/theme/commands/open.rb +2 -2
  36. data/lib/project_types/theme/commands/push.rb +1 -3
  37. data/lib/project_types/theme/commands/serve.rb +1 -0
  38. data/lib/project_types/theme/commands/share.rb +56 -0
  39. data/lib/project_types/theme/messages/messages.rb +64 -11
  40. data/lib/shopify_cli/changelog.rb +97 -25
  41. data/lib/shopify_cli/command_options/command_serve_options.rb +10 -0
  42. data/lib/shopify_cli/commands/app/serve.rb +7 -7
  43. data/lib/shopify_cli/commands/login.rb +5 -2
  44. data/lib/shopify_cli/context.rb +13 -0
  45. data/lib/shopify_cli/git.rb +36 -0
  46. data/lib/shopify_cli/identity_auth.rb +24 -4
  47. data/lib/shopify_cli/messages/messages.rb +22 -11
  48. data/lib/shopify_cli/release.rb +120 -20
  49. data/lib/shopify_cli/services/app/create/rails_service.rb +9 -1
  50. data/lib/shopify_cli/services/app/serve/node_service.rb +2 -25
  51. data/lib/shopify_cli/services/app/serve/php_service.rb +2 -25
  52. data/lib/shopify_cli/services/app/serve/rails_service.rb +8 -28
  53. data/lib/shopify_cli/services/app/serve/serve_service.rb +57 -0
  54. data/lib/shopify_cli/services.rb +1 -0
  55. data/lib/shopify_cli/tasks/update_dashboard_urls.rb +7 -9
  56. data/lib/shopify_cli/theme/dev_server/hot-reload.js +40 -13
  57. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
  58. data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +51 -0
  59. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +6 -1
  60. data/lib/shopify_cli/theme/dev_server/local_assets.rb +1 -1
  61. data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +35 -0
  62. data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +44 -0
  63. data/lib/shopify_cli/theme/dev_server/watcher.rb +2 -8
  64. data/lib/shopify_cli/theme/dev_server.rb +18 -5
  65. data/lib/shopify_cli/theme/file.rb +15 -4
  66. data/lib/shopify_cli/theme/syncer/checksums.rb +60 -0
  67. data/lib/shopify_cli/theme/syncer/forms/apply_to_all.rb +39 -0
  68. data/lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb +35 -0
  69. data/lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb +62 -0
  70. data/lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb +27 -0
  71. data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +28 -0
  72. data/lib/shopify_cli/theme/syncer/ignore_helper.rb +33 -0
  73. data/lib/shopify_cli/theme/syncer/json_delete_handler.rb +51 -0
  74. data/lib/shopify_cli/theme/syncer/json_update_handler.rb +82 -0
  75. data/lib/shopify_cli/theme/syncer/merger.rb +53 -0
  76. data/lib/shopify_cli/theme/syncer/operation.rb +1 -1
  77. data/lib/shopify_cli/theme/syncer.rb +79 -63
  78. data/lib/shopify_cli/theme/theme.rb +21 -7
  79. data/lib/shopify_cli/theme/theme_admin_api.rb +23 -8
  80. data/lib/shopify_cli/thread_pool/job.rb +10 -2
  81. data/lib/shopify_cli/thread_pool.rb +15 -3
  82. data/lib/shopify_cli/tunnel.rb +3 -13
  83. data/lib/shopify_cli/version.rb +1 -1
  84. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +8 -0
  85. metadata +25 -2
@@ -23,19 +23,23 @@ module Script
23
23
  )
24
24
 
25
25
  # remove the need to pass the whole extension-point object to the infra layer
26
- sparse_checkout_repo = extension_point.libraries.for(language).repo
27
26
  type = extension_point.dasherize_type
28
27
  domain = extension_point.domain
29
28
 
29
+ sparse_checkout_details = Infrastructure::SparseCheckoutDetails.new(
30
+ repo: extension_point.libraries.for(language).repo,
31
+ branch: sparse_checkout_branch,
32
+ path: "#{domain}/#{language}/#{type}/default",
33
+ input_queries_enabled: input_queries_enabled?,
34
+ )
35
+
30
36
  project_creator = Infrastructure::Languages::ProjectCreator.for(
31
37
  ctx: ctx,
32
38
  language: language,
33
39
  type: type,
34
40
  project_name: title,
35
41
  path_to_project: project.id,
36
- sparse_checkout_repo: sparse_checkout_repo,
37
- sparse_checkout_branch: sparse_checkout_branch,
38
- sparse_checkout_set_path: "#{domain}/#{language}/#{type}/default"
42
+ sparse_checkout_details: sparse_checkout_details,
39
43
  )
40
44
 
41
45
  install_dependencies(ctx, language, title, project_creator)
@@ -49,12 +53,12 @@ module Script
49
53
  task_runner = Infrastructure::Languages::TaskRunner.for(ctx, language)
50
54
  CLI::UI::Frame.open(ctx.message(
51
55
  "core.git.pulling_from_to",
52
- project_creator.sparse_checkout_repo,
56
+ project_creator.sparse_checkout_details.repo,
53
57
  title,
54
58
  )) do
55
59
  UI::StrictSpinner.spin(ctx.message(
56
60
  "core.git.pulling",
57
- project_creator.sparse_checkout_repo,
61
+ project_creator.sparse_checkout_details.repo,
58
62
  title,
59
63
  )) do |spinner|
60
64
  project_creator.setup_dependencies
@@ -75,6 +79,10 @@ module Script
75
79
  ensure
76
80
  script_project_repo.change_to_initial_directory
77
81
  end
82
+
83
+ def input_queries_enabled?
84
+ ShopifyCLI::Feature.enabled?(:scripts_beta_input_queries)
85
+ end
78
86
  end
79
87
  end
80
88
  end
@@ -180,6 +180,23 @@ module Script
180
180
  @max_size = max_size
181
181
  end
182
182
  end
183
+
184
+ class InvalidInputQueryErrors < ScriptProjectError
185
+ attr_reader :messages
186
+
187
+ def initialize(messages)
188
+ @messages = messages
189
+ super()
190
+ end
191
+
192
+ def input_query_path
193
+ ScriptProjectRepository::INPUT_QUERY_PATH
194
+ end
195
+
196
+ def message
197
+ messages.join("\n")
198
+ end
199
+ end
183
200
  end
184
201
  end
185
202
  end
@@ -10,9 +10,7 @@ module Script
10
10
  property! :type, accepts: String
11
11
  property! :project_name, accepts: String
12
12
  property! :path_to_project, accepts: String
13
- property! :sparse_checkout_repo, accepts: String
14
- property! :sparse_checkout_branch, accepts: String
15
- property! :sparse_checkout_set_path, accepts: String
13
+ property! :sparse_checkout_details, accepts: SparseCheckoutDetails
16
14
 
17
15
  def self.for(
18
16
  ctx:,
@@ -20,9 +18,7 @@ module Script
20
18
  type:,
21
19
  project_name:,
22
20
  path_to_project:,
23
- sparse_checkout_repo:,
24
- sparse_checkout_branch:,
25
- sparse_checkout_set_path:
21
+ sparse_checkout_details:
26
22
  )
27
23
 
28
24
  project_creators = {
@@ -36,33 +32,22 @@ module Script
36
32
  type: type,
37
33
  project_name: project_name,
38
34
  path_to_project: path_to_project,
39
- sparse_checkout_repo: sparse_checkout_repo,
40
- sparse_checkout_branch: sparse_checkout_branch,
41
- sparse_checkout_set_path: sparse_checkout_set_path
35
+ sparse_checkout_details: sparse_checkout_details,
42
36
  )
43
37
  end
44
38
 
45
39
  # the sparse checkout process is common to all script types
46
40
  def setup_dependencies
47
- setup_sparse_checkout
41
+ sparse_checkout_details.setup(ctx)
48
42
  clean
49
43
  end
50
44
 
51
45
  private
52
46
 
53
- def setup_sparse_checkout
54
- ShopifyCLI::Git.sparse_checkout(
55
- sparse_checkout_repo,
56
- sparse_checkout_set_path,
57
- sparse_checkout_branch,
58
- ctx
59
- )
60
- end
61
-
62
47
  def clean
63
- source = File.join(path_to_project, sparse_checkout_set_path, ".")
48
+ source = File.join(path_to_project, sparse_checkout_details.path, ".")
64
49
  FileUtils.cp_r(source, path_to_project)
65
- ctx.rm_rf(sparse_checkout_set_path.split("/")[0])
50
+ ctx.rm_rf(sparse_checkout_details.path.split("/")[0])
66
51
  ctx.rm_rf(".git")
67
52
  end
68
53
 
@@ -83,6 +83,8 @@ module Script
83
83
  valid_types: e["message"],
84
84
  filename: script_config.filename,
85
85
  )
86
+ elsif (errors = user_errors.filter { |err| err["tag"] == "input_query_validation_error" }).any?
87
+ raise Errors::InvalidInputQueryErrors, errors.map { |err| err["message"] }
86
88
  elsif user_errors.find { |err| %w(not_use_msgpack_error schema_version_argument_error).include?(err["tag"]) }
87
89
  raise Domain::Errors::MetadataValidationError
88
90
  else
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Script
4
+ module Layers
5
+ module Infrastructure
6
+ class SparseCheckoutDetails
7
+ include SmartProperties
8
+ property! :repo, accepts: String
9
+ property! :branch, accepts: String
10
+ property! :path, accepts: String
11
+ property! :input_queries_enabled, accepts: [true, false]
12
+
13
+ def ==(other)
14
+ self.class == other.class &&
15
+ self.class.properties.all? { |name, _| self[name] == other[name] }
16
+ end
17
+
18
+ def setup(ctx)
19
+ ShopifyCLI::Git.sparse_checkout(repo, patterns_to_checkout, branch, ctx)
20
+ end
21
+
22
+ private
23
+
24
+ def patterns_to_checkout
25
+ paths = [path]
26
+ unless input_queries_enabled
27
+ paths << "!#{path}/#{ScriptProjectRepository::INPUT_QUERY_PATH}"
28
+ paths << "!#{path}/schema.graphql"
29
+ end
30
+ paths.join(" ")
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -86,6 +86,9 @@ module Script
86
86
  "type(s): %{valid_types}.",
87
87
  configuration_schema_field_invalid_value_error_help: "Change the value of the type.",
88
88
 
89
+ input_query_error_cause: "Input query is invalid:\n%{messages}\n\n",
90
+ input_query_error_help: "Fix the query in the `%{input_query_path}` file.",
91
+
89
92
  script_not_found_cause: "Can't find script %s for Script API %s",
90
93
 
91
94
  system_call_failure_cause: "Something went wrong while running: {{command:%{cmd}}}.",
@@ -216,6 +216,17 @@ module Script
216
216
  "script.error.configuration_schema_field_invalid_value_error_help"
217
217
  ),
218
218
  }
219
+ when Layers::Infrastructure::Errors::InvalidInputQueryErrors
220
+ {
221
+ cause_of_error: ShopifyCLI::Context.message(
222
+ "script.error.input_query_error_cause",
223
+ messages: e.messages.map { |message| " {{x}} #{message}." }.join("\n"),
224
+ ),
225
+ help_suggestion: ShopifyCLI::Context.message(
226
+ "script.error.input_query_error_help",
227
+ input_query_path: e.input_query_path,
228
+ ),
229
+ }
219
230
  when Layers::Infrastructure::Errors::DependencyInstallError
220
231
  {
221
232
  cause_of_error: ShopifyCLI::Context.message("script.error.dependency_install_cause"),
@@ -16,6 +16,7 @@ module Theme
16
16
  subcommand :Package, "package", Project.project_filepath("commands/package")
17
17
  subcommand :Open, "open", Project.project_filepath("commands/open")
18
18
  subcommand :List, "list", Project.project_filepath("commands/list")
19
+ subcommand :Share, "share", Project.project_filepath("commands/share")
19
20
  subcommand :LanguageServer, "language-server", Project.project_filepath("commands/language_server")
20
21
  end
21
22
  ShopifyCLI::Commands.register("Theme::Command", "theme")
@@ -24,7 +24,10 @@ module Theme
24
24
  end
25
25
 
26
26
  def call(*)
27
- @theme_check.run
27
+ @theme_check.run!
28
+ rescue ThemeCheck::Cli::Abort, ThemeCheck::ThemeCheckError => e
29
+ raise ShopifyCLI::Abort,
30
+ ShopifyCLI::Context.message("theme.check.error", e.full_message)
28
31
  end
29
32
 
30
33
  def self.help
@@ -17,8 +17,8 @@ module Theme
17
17
  def call(_args, _name)
18
18
  theme = find_theme(**options.flags)
19
19
 
20
- @ctx.puts(@ctx.message("theme.open.details", theme.name, theme.editor_url))
21
- @ctx.open_url!(theme.preview_url)
20
+ @ctx.puts(@ctx.message("theme.open.details", theme.name, theme.preview_url, theme.editor_url))
21
+ @ctx.open_browser_url!(theme.preview_url)
22
22
  end
23
23
 
24
24
  def self.help
@@ -104,9 +104,7 @@ module Theme
104
104
 
105
105
  if unpublished
106
106
  name = theme || ask_theme_name
107
- new_theme = ShopifyCLI::Theme::Theme.new(@ctx, root: root, name: name, role: "unpublished")
108
- new_theme.create
109
- return new_theme
107
+ return ShopifyCLI::Theme::Theme.create_unpublished(@ctx, root: root, name: name)
110
108
  end
111
109
 
112
110
  if theme
@@ -16,6 +16,7 @@ module Theme
16
16
  parser.on("--port=PORT") { |port| flags[:port] = port.to_i }
17
17
  parser.on("--poll") { flags[:poll] = true }
18
18
  parser.on("--live-reload=MODE") { |mode| flags[:mode] = as_reload_mode(mode) }
19
+ parser.on("--theme-editor-sync") { flags[:editor_sync] = true }
19
20
  end
20
21
 
21
22
  def call(_args, name)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shopify_cli/theme/theme"
4
+ require "shopify_cli/theme/syncer"
5
+ require "project_types/theme/commands/common/root_helper"
6
+
7
+ module Theme
8
+ class Command
9
+ class Share < ShopifyCLI::Command::SubCommand
10
+ include Common::RootHelper
11
+
12
+ recommend_default_ruby_range
13
+
14
+ def call(_args, name)
15
+ root = root_value(options, name)
16
+ theme = create_theme(root)
17
+
18
+ upload(theme)
19
+
20
+ @ctx.done(done_message(theme))
21
+ end
22
+
23
+ def self.help
24
+ tool = ShopifyCLI::TOOL_NAME
25
+ @ctx.message("theme.share.help", tool, tool)
26
+ end
27
+
28
+ private
29
+
30
+ def create_theme(root)
31
+ ShopifyCLI::Theme::Theme.create_unpublished(@ctx, root: root)
32
+ end
33
+
34
+ def upload(theme)
35
+ syncer = ShopifyCLI::Theme::Syncer.new(@ctx, theme: theme)
36
+ syncer.start_threads
37
+
38
+ CLI::UI::Frame.open(upload_message(theme)) do
39
+ UI::SyncProgressBar.new(syncer).progress(:upload_theme!)
40
+ end
41
+
42
+ raise ShopifyCLI::AbortSilent if syncer.has_any_error?
43
+ ensure
44
+ syncer.shutdown
45
+ end
46
+
47
+ def upload_message(theme)
48
+ @ctx.message("theme.share.upload", theme.name, theme.id, theme.shop)
49
+ end
50
+
51
+ def done_message(theme)
52
+ @ctx.message("theme.share.done", theme.name, theme.preview_url, theme.editor_url)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -8,8 +8,9 @@ module Theme
8
8
  Usage: {{command:%1$s theme [ %2$s ]}}
9
9
  HELP
10
10
  ensure_user_error: "You are not authorized to edit themes on %s.",
11
- ensure_user_try_this: "Make sure you are a user of that store, and allowed to edit themes.",
12
-
11
+ ensure_user_try_this: <<~ENSURE_USER,
12
+ Check if your user is activated, has permission to edit themes at the store, and try to re-login.
13
+ ENSURE_USER
13
14
  init: {
14
15
  help: <<~HELP,
15
16
  {{command:%s theme init}}: Clones a Git repository to use as a starting point for building a new theme.
@@ -101,13 +102,14 @@ module Theme
101
102
  Usage: {{command:%s theme serve [ ROOT ]}}
102
103
 
103
104
  Options:
104
- {{command:--port=PORT}} Local port to serve theme preview from.
105
- {{command:--poll}} Force polling to detect file changes.
106
- {{command:--host=HOST}} Set which network interface the web server listens on. The default value is 127.0.0.1.
107
- {{command:--live-reload=MODE}} The live reload mode switches the server behavior when a file is modified:
108
- - {{command:hot-reload}} Hot reloads local changes to CSS and sections (default)
109
- - {{command:full-page}} Always refreshes the entire page
110
- - {{command:off}} Deactivate live reload
105
+ {{command:--port=PORT}} Local port to serve theme preview from.
106
+ {{command:--poll}} Force polling to detect file changes.
107
+ {{command:--host=HOST}} Set which network interface the web server listens on. The default value is 127.0.0.1.
108
+ {{command:--theme-editor-sync}} Synchronize Theme Editor updates in the local theme files.
109
+ {{command:--live-reload=MODE}} The live reload mode switches the server behavior when a file is modified:
110
+ - {{command:hot-reload}} Hot reloads local changes to CSS and sections (default)
111
+ - {{command:full-page}} Always refreshes the entire page
112
+ - {{command:off}} Deactivate live reload
111
113
  HELP
112
114
  reload_mode_is_not_valid: "The live reload mode `%s` is not valid.",
113
115
  try_a_valid_reload_mode: "Try a valid live reload mode: %s.",
@@ -121,6 +123,36 @@ module Theme
121
123
  fixed: "Fixed",
122
124
  },
123
125
  },
126
+ syncer: {
127
+ forms: {
128
+ apply_to_all: {
129
+ title: "Would like apply this to all the other %s files?",
130
+ yes: "Yes",
131
+ no: "No",
132
+ },
133
+ update_strategy: {
134
+ title_context: <<~TITLE,
135
+
136
+ The local file {{command:%s}} is different from the remote version in the development theme."
137
+ TITLE
138
+ title_question: "What would you like to do?",
139
+ keep_remote: "Keep the remote version",
140
+ keep_local: "Keep the local version",
141
+ union_merge: "Merge files (it may break the local file)",
142
+ exit: "Exit",
143
+ },
144
+ delete_strategy: {
145
+ title_context: <<~TITLE,
146
+
147
+ The local file {{command:%s}} has been recently removed, but it's present on your remote development theme.",
148
+ TITLE
149
+ title_question: "What would you like to do?",
150
+ delete: "Delete permanently",
151
+ restore: "Restore with the remote version",
152
+ exit: "Exit",
153
+ },
154
+ },
155
+ },
124
156
  error: {
125
157
  address_binding_error: "Couldn't bind to localhost."\
126
158
  " To serve your theme, set a different address with {{command:%s theme serve --host=<address>}}",
@@ -137,9 +169,10 @@ module Theme
137
169
  Serving %s
138
170
 
139
171
  SERVING
172
+ download_changes: ", and use 'theme pull' to get the changes",
140
173
  customize_or_preview: <<~CUSTOMIZE_OR_PREVIEW,
141
174
 
142
- Customize this theme in the Theme Editor:
175
+ Customize this theme in the Theme Editor%s:
143
176
  {{green:%s}}
144
177
 
145
178
  Share this theme preview:
@@ -149,7 +182,7 @@ module Theme
149
182
  CUSTOMIZE_OR_PREVIEW
150
183
  ensure_user: <<~ENSURE_USER,
151
184
  You are not authorized to edit themes on %s.
152
- Make sure you are a user of that store, and allowed to edit themes.
185
+ Check if your user is activated, has permission to edit themes at the store, and try to re-login.
153
186
  ENSURE_USER
154
187
  address_already_in_use: "The address \"%s\" is already in use.",
155
188
  try_port_option: "Use the --port=PORT option to serve the theme in a different port.",
@@ -159,6 +192,7 @@ module Theme
159
192
  Check your theme for errors, suggestions, and best practices.
160
193
  Usage: {{command:%s check}}
161
194
  HELP
195
+ error: "Theme check failed with error:\n%s",
162
196
  },
163
197
  delete: {
164
198
  help: <<~HELP,
@@ -230,6 +264,9 @@ module Theme
230
264
  details: <<~DETAILS,
231
265
  {{*}} {{bold:%s}}
232
266
 
267
+ Preview your theme:
268
+ {{green:%s}}
269
+
233
270
  Customize your theme in the Theme Editor:
234
271
  {{green:%s}}
235
272
 
@@ -253,6 +290,22 @@ module Theme
253
290
  Usage: {{command:%s theme list}}
254
291
  HELP
255
292
  },
293
+ share: {
294
+ help: <<~HELP,
295
+ {{command:%s theme share}}: Creates a shareable, unpublished, and new theme on your theme library with a randomized name.
296
+ Works like an alias to {{command:theme push -u -t=RANDOMIZED_NAME}}.
297
+
298
+ Usage: {{command:%s theme share [ ROOT ]}}
299
+ HELP
300
+ done: <<~DONE,
301
+ {{green:The {{bold:%s}} theme was pushed successfully}}
302
+
303
+ {{info:Share your theme preview:}}
304
+ {{underline:%s}}
305
+
306
+ DONE
307
+ upload: "Pushing theme files to %s (#%s) on %s",
308
+ },
256
309
  },
257
310
  }.freeze
258
311
  end
@@ -1,23 +1,32 @@
1
+ require "date"
1
2
  require "shopify_cli/sed"
3
+ require "octokit"
2
4
 
3
5
  module ShopifyCLI
4
6
  class Changelog
5
7
  CHANGELOG_FILE = File.join(ShopifyCLI::ROOT, "CHANGELOG.md")
8
+ CHANGE_CATEGORIES = %w(Added Changed Deprecated Removed Fixed Security)
6
9
 
7
10
  def initialize
8
11
  load(File.read(CHANGELOG_FILE))
9
12
  end
10
13
 
11
14
  def update_version!(new_version)
12
- Sed.new.replace_inline(
13
- CHANGELOG_FILE,
14
- "## \\[Unreleased\\]",
15
- "## [Unreleased]\\n\\n## Version #{new_version}"
16
- )
15
+ changes[new_version] = changes["Unreleased"]
16
+ changes[new_version][:date] = Date.today.iso8601
17
+ changes["Unreleased"] = { changes: [], date: nil }
18
+ save!
19
+ end
20
+
21
+ def update!
22
+ pr = pr_for_current_branch
23
+ category = CLI::UI::Prompt.ask("What type of change?", options: CHANGE_CATEGORIES)
24
+ add_change(category, { pr_id: pr.number, desc: pr.title })
25
+ save!
17
26
  end
18
27
 
19
28
  def release_notes(version)
20
- changes[version].map do |change_category, changes|
29
+ changes[version][:changes].map do |change_category, changes|
21
30
  <<~CHANGES
22
31
  ### #{change_category}
23
32
  #{changes.map { |change| entry(**change) }.join("\n")}
@@ -25,17 +34,61 @@ module ShopifyCLI
25
34
  end.join("\n")
26
35
  end
27
36
 
37
+ def add_change(category, change)
38
+ changes["Unreleased"][:changes][category] << change
39
+ end
40
+
28
41
  def entry(pr_id:, desc:)
29
42
  "* [##{pr_id}](https://github.com/Shopify/shopify-cli/pull/#{pr_id}): #{desc}"
30
43
  end
31
44
 
45
+ def full_contents
46
+ sorted_changes = changes.each_key.sort_by do |change|
47
+ if change == "Unreleased"
48
+ [Float::INFINITY] * 3 # end of the list
49
+ else
50
+ major, minor, patch = change.split(".").map(&:to_i)
51
+ [major, minor, patch]
52
+ end
53
+ end.reverse
54
+ [
55
+ heading,
56
+ *sorted_changes.each.map { |version| release_notes_with_header(version) }.join,
57
+ remainder,
58
+ ].map { |section| section.chomp << "\n" }.join
59
+ end
60
+
61
+ def save!
62
+ File.write(CHANGELOG_FILE, full_contents)
63
+ end
64
+
32
65
  private
33
66
 
67
+ attr_reader :heading, :remainder
68
+
69
+ def release_notes_with_header(version)
70
+ header_line =
71
+ if version == "Unreleased"
72
+ "[Unreleased]"
73
+ else
74
+ date = changes[version][:date]
75
+ "Version #{version}#{" - #{date}" if date}"
76
+ end
77
+
78
+ [
79
+ "## #{header_line}",
80
+ release_notes(version),
81
+ ].reject(&:empty?).map { |section| section.chomp << "\n\n" }.join
82
+ end
83
+
34
84
  def changes
35
85
  @changes ||= Hash.new do |h, k|
36
- h[k] = Hash.new do |h2, k2|
37
- h2[k2] = []
38
- end
86
+ h[k] = {
87
+ date: nil,
88
+ changes: Hash.new do |h2, k2|
89
+ h2[k2] = []
90
+ end,
91
+ }
39
92
  end
40
93
  end
41
94
 
@@ -43,34 +96,53 @@ module ShopifyCLI
43
96
  state = :initial
44
97
  change_category = nil
45
98
  current_version = nil
99
+ @heading = ""
46
100
  @remainder = ""
47
101
  log.each_line do |line|
48
102
  case state
49
103
  when :initial
50
- next unless line.chomp == "\#\# [Unreleased]"
51
- state = :unreleased
52
- current_version = "Unreleased"
53
- when :unreleased, :last_version
104
+ if line.chomp == "\#\# [Unreleased]"
105
+ state = :unreleased
106
+ current_version = "Unreleased"
107
+ # Ensure Unreleased changeset exists even if no changes have happened yet
108
+ changes["Unreleased"]
109
+ else
110
+ @heading << line
111
+ end
112
+ when :unreleased, :prior_versions
54
113
  if /\A\#\#\# (?<category>\w+)/ =~ line
55
114
  change_category = category
56
- elsif %r{\A\* \[\#(?<pr_id>\d+)\]\(https://github.com/Shopify/shopify-cli/pull/\d+\): (?<desc>.+)\n} =~ line
57
- changes[current_version][change_category] << { pr_id: pr_id, desc: desc }
58
- elsif /\A\#\# Version (?<version>\d+\.\d+\.\d+)/ =~ line
115
+ elsif %r{\A\* \[\#(?<id>\d+)\]\(https://github.com/Shopify/shopify-cli/pull/\k<id>\): (?<desc>.+)\n} =~ line
116
+ changes[current_version][:changes][change_category] << { pr_id: id, desc: desc }
117
+ elsif /\A\#\# Version (?<version>\d+\.\d+\.\d+)( - (?<date>\d{4}-\d{2}-\d{2}))?/ =~ line
59
118
  current_version = version
60
- state =
61
- case state
62
- when :unreleased
63
- :last_version
64
- else
65
- :finished
66
- end
119
+ state = :prior_versions
120
+ major, minor, _patch = current_version.split(".")
121
+ if major.to_i <= 2 && minor.to_i < 7
122
+ # Changelog starts to become irregular in 2.6.x
123
+ state = :finished
124
+ end
125
+ changes[current_version][:date] = date unless state == :finished
67
126
  elsif !line.match?(/\s*\n/)
68
127
  raise "Unrecognized line: #{line.inspect}"
69
128
  end
70
- when :finished
71
- @remainder << line
72
129
  end
130
+ @remainder << line if state == :finished
73
131
  end
74
132
  end
133
+
134
+ def pr_for_current_branch
135
+ current_branch = %x(git branch --show-current).chomp
136
+ search_term = "repo:Shopify/shopify-cli is:pr is:open head:#{current_branch}"
137
+ results = Octokit::Client.new.search_issues(search_term)
138
+ case results.total_count
139
+ when 0
140
+ raise "PR not opened yet!"
141
+ when (2..)
142
+ raise "Multiple open PRs, not sure which one to use for changelog!"
143
+ end
144
+
145
+ results.items.first
146
+ end
75
147
  end
76
148
  end
@@ -20,6 +20,10 @@ module ShopifyCLI
20
20
  end
21
21
  host
22
22
  end
23
+
24
+ def no_update
25
+ options.flags[:no_update] || false
26
+ end
23
27
  end
24
28
  end
25
29
 
@@ -37,6 +41,12 @@ module ShopifyCLI
37
41
  parser.on("--port=PORT") { |port| flags[:port] = port }
38
42
  end
39
43
  end
44
+
45
+ def parse_no_update_option
46
+ options do |parser, flags|
47
+ parser.on("--no-update") { flags[:no_update] = true }
48
+ end
49
+ end
40
50
  end
41
51
  end
42
52
  end