shopify-cli 1.1.0 → 1.3.1

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CONTRIBUTING.md +1 -1
  3. data/CHANGELOG.md +20 -0
  4. data/docs/core/index.md +16 -0
  5. data/docs/getting-started/index.md +3 -2
  6. data/docs/getting-started/install/index.md +55 -9
  7. data/docs/getting-started/uninstall/index.md +1 -1
  8. data/docs/getting-started/upgrade/index.md +8 -4
  9. data/lib/project_types/extension/cli.rb +6 -1
  10. data/lib/project_types/extension/commands/register.rb +1 -1
  11. data/lib/project_types/extension/features/argo/admin.rb +20 -0
  12. data/lib/project_types/extension/features/argo/base.rb +129 -0
  13. data/lib/project_types/extension/features/argo/checkout.rb +20 -0
  14. data/lib/project_types/extension/features/argo_config.rb +60 -0
  15. data/lib/project_types/extension/messages/messages.rb +11 -2
  16. data/lib/project_types/extension/models/type.rb +4 -0
  17. data/lib/project_types/extension/models/types/checkout_post_purchase.rb +6 -3
  18. data/lib/project_types/extension/models/types/product_subscription.rb +24 -0
  19. data/lib/project_types/node/commands/generate/billing.rb +1 -0
  20. data/lib/project_types/node/commands/generate/page.rb +1 -0
  21. data/lib/project_types/node/commands/generate/webhook.rb +1 -0
  22. data/lib/project_types/node/commands/serve.rb +5 -5
  23. data/lib/project_types/node/messages/messages.rb +4 -1
  24. data/lib/project_types/rails/commands/create.rb +4 -1
  25. data/lib/project_types/rails/commands/serve.rb +5 -5
  26. data/lib/project_types/rails/messages/messages.rb +5 -1
  27. data/lib/project_types/script/config/extension_points.yml +4 -4
  28. data/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb +36 -1
  29. data/lib/project_types/script/layers/infrastructure/errors.rb +7 -0
  30. data/lib/project_types/script/layers/infrastructure/script_service.rb +6 -2
  31. data/lib/project_types/script/messages/messages.rb +12 -37
  32. data/lib/project_types/script/ui/error_handler.rb +13 -5
  33. data/lib/shopify-cli/commands/config.rb +33 -1
  34. data/lib/shopify-cli/context.rb +40 -0
  35. data/lib/shopify-cli/core/entry_point.rb +3 -0
  36. data/lib/shopify-cli/git.rb +1 -1
  37. data/lib/shopify-cli/heroku.rb +1 -1
  38. data/lib/shopify-cli/js_system.rb +22 -5
  39. data/lib/shopify-cli/messages/messages.rb +39 -11
  40. data/lib/shopify-cli/project.rb +3 -3
  41. data/lib/shopify-cli/tunnel.rb +11 -2
  42. data/lib/shopify-cli/version.rb +1 -1
  43. metadata +7 -4
  44. data/lib/project_types/extension/features/argo.rb +0 -48
  45. data/lib/project_types/extension/models/types/subscription_management.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f47a49090da5b25b266e4186c08b377e91e6fe240620076f24bb0174accc854
4
- data.tar.gz: 68be2a02c00aa4e0c67295024d0997c453eb40b9d92f4cec9ae06dc5c0b3d0fa
3
+ metadata.gz: b46dd31f52ee5649fcf42ca444e124fb2bf91e65c8cf2450a3e0a296da669e13
4
+ data.tar.gz: 1712d16f7849ec0fa33c898fb425e5543dc792549bf1925311b5cb52b6dbc7d2
5
5
  SHA512:
6
- metadata.gz: f0cd5e1c1e79ec89fcba9834eb1c1ea98d53a98ad46f2fcc45e5fefebfabcd9b1655cb73c1431c503b9c1a26f2d684d6a1c330813423733601670839c7e82efc
7
- data.tar.gz: a3f83e966247aee450f3a73615dce3d324f82ab944304be131dec81973d1984c405d060844601047ceb259163faaa79524d859bbe9a874479ea4a26c21530152
6
+ metadata.gz: 20fa92e7de4e785804a620c612ca40317c341d69ef636e0d611d9dda081decb57bca95ca478cef2ed4f151fb7613fe749000f1f4d85c17d211c4367cfbd8f676
7
+ data.tar.gz: af418cf9c0d05c82e4f29776111286760865d54e7b99774a8d63d70537de69073152a8a643c33965870d85e3209af1f21e27daea6f77826ff096bfbc2aa50ceb
@@ -4,7 +4,7 @@ Shopify App CLI is an open source project. We want to make it as easy and transp
4
4
 
5
5
  ## Code of conduct
6
6
 
7
- We expect all participants to read our [code of conduct](https://github.com/Shopify/shopify-app-cli/.github/CODE_OF_CONDUCT.md) to understand which actions are and aren’t tolerated.
7
+ We expect all participants to read our [code of conduct](https://github.com/Shopify/shopify-app-cli/blob/master/.github/CODE_OF_CONDUCT.md) to understand which actions are and aren’t tolerated.
8
8
 
9
9
  ## Open development
10
10
 
@@ -1,3 +1,23 @@
1
+ Version 1.3.1
2
+ ------
3
+ * Allow any characters in ngrok account names
4
+
5
+ Version 1.3.0
6
+ ------
7
+ * Support for new `shopify config analytics` command to enable/disable anonymous usage reporting
8
+
9
+ Version 1.2.0
10
+ ------
11
+ * Improvements and new functionality to various internal components
12
+
13
+ Version 1.1.2
14
+ ------
15
+ * Fix various minor bugs (check dir before creating Rails project, catch stderr from failed git command)
16
+
17
+ Version 1.1.1
18
+ ------
19
+ * Fix a bug where usernames with spaces caused issues on Windows
20
+
1
21
  Version 1.1.0
2
22
  ------
3
23
  * Add native Windows 10 support, including variety of stability fixes.
@@ -68,3 +68,19 @@ Log out of the currently authenticated partner organization and store. The `logo
68
68
  $ shopify logout
69
69
  ```
70
70
 
71
+ ## `config`
72
+
73
+ Configure Shopify App CLI options. Currently there are two available options.
74
+
75
+ ### `analytics`
76
+
77
+ Configure anonymous usage reporting by enabling or disabling analytics
78
+ ```console
79
+ $ shopify config analytics [ --status | --enable | --disable ]
80
+ ```
81
+
82
+ ### `feature`
83
+ Configure active [feature sets](https://github.com/Shopify/shopify-app-cli/wiki/Feature-Sets) in the CLI. This command is used for development and debugging work on the CLI tool itself. Only alter it if you know what you're doing. Check the [Shopify App CLI development guide](https://github.com/Shopify/shopify-app-cli/wiki) for more information.
84
+ ```console
85
+ $ shopify config feature [ feature_name ] [ --status | --enable | --disable ]
86
+ ```
@@ -8,14 +8,15 @@ Developers should have some prior knowledge of the [Shopify app ecosystem](https
8
8
 
9
9
  ## Requirements
10
10
 
11
- - [Ruby](https://www.ruby-lang.org) 2.5.1+
12
11
  - A [Shopify partner account](https://partners.shopify.com/signup)
13
12
  - A [Shopify development store](https://help.shopify.com/en/partners/dashboard/development-stores#create-a-development-store) to install and test apps
13
+ - [Ruby](https://www.ruby-lang.org) 2.5.1+
14
14
 
15
15
  ### Windows requirements
16
16
 
17
- You’ll need to install the following tools to use Shopify App CLI on Windows:
17
+ If you wish to use Shopify App CLI natively on **Windows 10**, we recommend installing Ruby using [RubyInstaller for Windows](https://rubyinstaller.org/downloads/).
18
18
 
19
+ Alternatively, you can also use Shopify App CLI using **Windows Subsystem for Linux**, in which case you need:
19
20
  - [Linux Subsystem for Windows](https://docs.microsoft.com/en-us/windows/wsl/install-win10)
20
21
  - [Ubuntu VM](https://www.microsoft.com/en-ca/p/ubuntu/9nblggh4msv6)
21
22
 
@@ -5,9 +5,15 @@ toc: false
5
5
  redirect_from: "/install/"
6
6
  ---
7
7
 
8
- Shopify App CLI can be installed using a variety of package managers.
8
+ Shopify App CLI can be installed on a variety of systems, using a variety of package managers.
9
+ > Note that for systems that have multiple installation options, you only need to use one of these methods to install.
9
10
 
10
- ### Homebrew (macOS)
11
+ ---
12
+ ### macOS
13
+
14
+ Shopify App CLI is available through Homebrew _or_ RubyGems.
15
+
16
+ **Homebrew**
11
17
 
12
18
  You’ll need to run `brew tap` first to add Shopify’s third-party repositories to Homebrew.
13
19
 
@@ -16,28 +22,68 @@ $ brew tap shopify/shopify
16
22
  $ brew install shopify-cli
17
23
  ```
18
24
 
19
- ### apt (Debian, Ubuntu)
25
+ **RubyGems**
26
+
27
+ See the [RubyGems]({{ site.baseurl }}/getting-started/install/#rubygems-all-platforms) section for further details.
28
+
29
+ ---
30
+
31
+ ### Debian/Ubuntu Linux
20
32
 
21
- You’ll need to install a downloaded .deb file with an explicit version number. Check the [releases page](https://github.com/Shopify/shopify-app-cli/releases) to make sure you install the latest package.
33
+ On Debian-based Linux systems, Shopify App CLI is available through the `apt` command _or_ RubyGems.
22
34
 
35
+ **apt**
36
+
37
+ You’ll need to install a downloaded `.deb` file with an explicit version number. Check the [releases page](https://github.com/Shopify/shopify-app-cli/releases) to make sure you install the latest package.
38
+
39
+ 1. Download the `.deb` file from the [releases page](https://github.com/Shopify/shopify-app-cli/releases)
40
+ 1. Install the downloaded file
23
41
  ```console
24
- $ sudo apt install shopify-cli-x.y.z.deb
42
+ $ sudo apt install /path/to/downloaded/shopify-cli-x.y.z.deb
25
43
  ```
26
44
 
27
- ### yum (CentOS 8+, Fedora, Red Hat, SUSE)
45
+ **RubyGems**
46
+
47
+ See the [RubyGems]({{ site.baseurl }}/getting-started/install/#rubygems-all-platforms) section for further details.
48
+
49
+ ---
50
+
51
+ ### CentOS 8+/Fedora/Red Hat/SUSE Linux
28
52
 
29
- You’ll need to install a downloaded .rpm file with an explicit version number. Check the [releases page](https://github.com/Shopify/shopify-app-cli/releases) to make sure you install the latest package.
53
+ On RPM-based Linux systems, Shopify App CLI is available through the `yum` command _or_ RubyGems.
30
54
 
55
+ **yum**
56
+
57
+ You’ll need to install a downloaded `.rpm` file with an explicit version number. Check the [releases page](https://github.com/Shopify/shopify-app-cli/releases) to make sure you install the latest package.
58
+
59
+ 1. Download the `.rpm` file from the [releases page](https://github.com/Shopify/shopify-app-cli/releases)
60
+ 1. Install the downloaded file
31
61
  ```console
32
- $ sudo yum install shopify-cli-x.y.x.rpm
62
+ $ sudo yum install /path/to/downloaded/shopify-cli-x.y.x.rpm
33
63
  ```
34
64
 
35
- ### Ruby gem
65
+ **RubyGems**
66
+
67
+ See the [RubyGems]({{ site.baseurl }}/getting-started/install/#rubygems-all-platforms) section for further details.
68
+
69
+ ---
70
+
71
+ ### Windows 10
72
+
73
+ On Windows 10 systems, Shopify App CLI is available through [RubyGems]({{ site.baseurl }}/getting-started/install/#rubygem-all-platforms).
74
+
75
+ ---
76
+
77
+ ### RubyGems (all platforms)
78
+
79
+ Shopify App CLI is available on all platforms as a RubyGem through [RubyGems.org](https://rubygems.org/).
36
80
 
37
81
  ```console
38
82
  $ gem install shopify-cli
39
83
  ```
40
84
 
85
+ ---
86
+
41
87
  ### To check that Shopify App CLI is installed correctly:
42
88
 
43
89
  ```console
@@ -25,7 +25,7 @@ $ sudo apt remove shopify-cli
25
25
  $ sudo yum remove shopify-cli
26
26
  ```
27
27
 
28
- ### Ruby gem
28
+ ### RubyGems (macOS, Linux, Windows 10)
29
29
 
30
30
  ```console
31
31
  $ gem uninstall shopify-cli
@@ -5,7 +5,7 @@ toc: false
5
5
  redirect_from: "/upgrade/"
6
6
  ---
7
7
 
8
- You can manage upgrades to Shopify App CLI with the package manager for your platform.
8
+ You can manage upgrades to Shopify App CLI with the package manager for your platform. **Note** that it's important to use the same package manager to upgrade that you originally used to install Shopify App CLI.
9
9
 
10
10
  ### Homebrew (macOS)
11
11
 
@@ -18,19 +18,23 @@ $ brew upgrade shopify-cli
18
18
 
19
19
  On Debian-based Linux distributions, download the latest `.deb` file for Shopify App CLI from the [releases page](https://github.com/Shopify/shopify-app-cli/releases) and install it to update.
20
20
 
21
+ 1. Download the `.deb` file from the [releases page](https://github.com/Shopify/shopify-app-cli/releases)
22
+ 1. Install the downloaded file
21
23
  ```console
22
- $ sudo apt install shopify-cli-x.y.z.deb
24
+ $ sudo apt install /path/to/downloaded/shopify-cli-x.y.z.deb
23
25
  ```
24
26
 
25
27
  ### yum (CentOS 8+, Fedora, Red Hat, SUSE)
26
28
 
27
29
  On Red Hat–based Linux distributions, download the latest `.rpm` file for Shopify App CLI from the [releases page](https://github.com/Shopify/shopify-app-cli/releases) and install it to update.
28
30
 
31
+ 1. Download the `.rpm` file from the [releases page](https://github.com/Shopify/shopify-app-cli/releases)
32
+ 1. Install the downloaded file
29
33
  ```console
30
- $ sudo yum install shopify-cli-x.y.z.rpm
34
+ $ sudo yum install /path/to/downloaded/shopify-cli-x.y.x.rpm
31
35
  ```
32
36
 
33
- ### Ruby gem
37
+ ### RubyGems (macOS, Linux, Windows 10)
34
38
 
35
39
  ```console
36
40
  $ gem update shopify-cli
@@ -48,11 +48,16 @@ module Extension
48
48
  end
49
49
 
50
50
  module Features
51
- autoload :Argo, Project.project_filepath('features/argo')
52
51
  autoload :ArgoSetup, Project.project_filepath('features/argo_setup')
53
52
  autoload :ArgoSetupStep, Project.project_filepath('features/argo_setup_step')
54
53
  autoload :ArgoSetupSteps, Project.project_filepath('features/argo_setup_steps')
55
54
  autoload :ArgoDependencies, Project.project_filepath('features/argo_dependencies')
55
+ autoload :ArgoConfig, Project.project_filepath('features/argo_config')
56
+ module Argo
57
+ autoload :Base, Project.project_filepath('features/argo/base')
58
+ autoload :Admin, Project.project_filepath('features/argo/admin')
59
+ autoload :Checkout, Project.project_filepath('features/argo/checkout')
60
+ end
56
61
  end
57
62
 
58
63
  module Models
@@ -52,7 +52,7 @@ module Extension
52
52
  Tasks::CreateExtension.call(
53
53
  context: @ctx,
54
54
  api_key: app.api_key,
55
- type: extension_type.identifier,
55
+ type: extension_type.graphql_identifier,
56
56
  title: project.title,
57
57
  config: {},
58
58
  extension_context: extension_type.extension_context(@ctx)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ module Extension
3
+ module Features
4
+ module Argo
5
+ class Admin < Base
6
+ GIT_TEMPLATE = 'https://github.com/Shopify/argo-admin-template.git'
7
+ RENDERER_PACKAGE = '@shopify/argo-admin'
8
+ private_constant :GIT_TEMPLATE, :RENDERER_PACKAGE
9
+
10
+ def git_template
11
+ GIT_TEMPLATE
12
+ end
13
+
14
+ def renderer_package_name
15
+ RENDERER_PACKAGE
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+ require 'base64'
3
+ require 'shopify_cli'
4
+ require 'semantic/semantic'
5
+
6
+ module Extension
7
+ module Features
8
+ module Argo
9
+ class Base
10
+ include SmartProperties
11
+
12
+ SCRIPT_PATH = %w(build main.js).freeze
13
+
14
+ NPM_LIST_COMMAND = %w(list).freeze
15
+ YARN_LIST_COMMAND = %w(list --pattern).freeze
16
+ NPM_LIST_PARAMETERS = %w(--prod).freeze
17
+ YARN_LIST_PARAMETERS = %w(--production).freeze
18
+ private_constant :NPM_LIST_COMMAND, :YARN_LIST_COMMAND, :NPM_LIST_PARAMETERS, :YARN_LIST_PARAMETERS
19
+
20
+ YARN_INSTALL_COMMAND = %w(install).freeze
21
+ YARN_INSTALL_PARAMETERS = %w(--silent).freeze
22
+ YARN_RUN_COMMAND = %w(run).freeze
23
+ YARN_RUN_SCRIPT_NAME = %w(build).freeze
24
+ private_constant :YARN_INSTALL_COMMAND, :YARN_INSTALL_PARAMETERS, :YARN_RUN_COMMAND, :YARN_RUN_SCRIPT_NAME
25
+
26
+ def create(directory_name, identifier, context)
27
+ Features::ArgoSetup.new(git_template: git_template).call(directory_name, identifier, context)
28
+ end
29
+
30
+ def config(context)
31
+ js_system = ShopifyCli::JsSystem.new(ctx: context)
32
+ if js_system.package_manager == 'yarn'
33
+ run_yarn_install(context, js_system)
34
+ run_yarn_run_script(context, js_system)
35
+ end
36
+ filepath = File.join(context.root, SCRIPT_PATH)
37
+ context.abort(context.message('features.argo.missing_file_error')) unless File.exist?(filepath)
38
+ begin
39
+ {
40
+ renderer_version: extract_argo_renderer_version(context),
41
+ serialized_script: Base64.strict_encode64(File.read(filepath).chomp),
42
+ }
43
+ rescue StandardError
44
+ context.abort(context.message('features.argo.script_prepare_error'))
45
+ end
46
+ end
47
+
48
+ def git_template
49
+ raise NotImplementedError, "'#{__method__}' must be implemented for #{self.class}"
50
+ end
51
+
52
+ def renderer_package_name
53
+ # The renderer_package_name is used as a regex pattern to
54
+ # find a match in the output of yarn or npm list command.
55
+ # Use the full package name as it appears in the template without targeting a version.
56
+ # Examples: "@shopify/some-renderer-package", "argo-renderer-package"
57
+
58
+ raise NotImplementedError, "'#{__method__}' must be implemented for #{self.class}"
59
+ end
60
+
61
+ private
62
+
63
+ def extract_argo_renderer_version(context)
64
+ result = run_list_command(context)
65
+ found_version = find_version_number(context, result)
66
+ context.abort(
67
+ context.message('features.argo.dependencies.argo_renderer_package_invalid_version_error')
68
+ ) if found_version.nil?
69
+ ::Semantic::Version.new(found_version).to_s
70
+ rescue ArgumentError
71
+ context.abort(
72
+ context.message('features.argo.dependencies.argo_renderer_package_invalid_version_error')
73
+ )
74
+ end
75
+
76
+ def find_version_number(context, result)
77
+ packages = result.to_json.split('\n')
78
+ found_package = packages.find do |package|
79
+ package.match(/#{renderer_package_name}@/)
80
+ end
81
+ if found_package.nil?
82
+ error = "'#{renderer_package_name}' not found."
83
+ context.abort(
84
+ context.message('features.argo.dependencies.argo_missing_renderer_package_error', error)
85
+ )
86
+ end
87
+ found_package.split('@')[2]&.strip
88
+ end
89
+
90
+ def run_list_command(context)
91
+ js_system = ShopifyCli::JsSystem.new(ctx: context)
92
+ result, error, status = js_system.call(
93
+ yarn: YARN_LIST_COMMAND + [renderer_package_name] + YARN_LIST_PARAMETERS,
94
+ npm: NPM_LIST_COMMAND + [renderer_package_name] + NPM_LIST_PARAMETERS,
95
+ capture_response: true
96
+ )
97
+ context.abort(
98
+ context.message('features.argo.dependencies.argo_missing_renderer_package_error', error)
99
+ ) unless status.success?
100
+ result
101
+ end
102
+
103
+ def run_yarn_install(context, js_system)
104
+ _result, error, status = js_system.call(
105
+ yarn: YARN_INSTALL_COMMAND + YARN_INSTALL_PARAMETERS,
106
+ npm: [],
107
+ capture_response: true
108
+ )
109
+
110
+ context.abort(
111
+ context.message('features.argo.dependencies.yarn_install_error', error)
112
+ ) unless status.success?
113
+ end
114
+
115
+ def run_yarn_run_script(context, js_system)
116
+ _result, error, status = js_system.call(
117
+ yarn: YARN_RUN_COMMAND + YARN_RUN_SCRIPT_NAME,
118
+ npm: [],
119
+ capture_response: true
120
+ )
121
+
122
+ context.abort(
123
+ context.message('features.argo.dependencies.yarn_run_script_error', error)
124
+ ) unless status.success?
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ module Extension
3
+ module Features
4
+ module Argo
5
+ class Checkout < Base
6
+ GIT_TEMPLATE = 'https://github.com/Shopify/argo-checkout-template.git'
7
+ RENDERER_PACKAGE = '@shopify/argo-checkout'
8
+ private_constant :GIT_TEMPLATE, :RENDERER_PACKAGE
9
+
10
+ def git_template
11
+ GIT_TEMPLATE
12
+ end
13
+
14
+ def renderer_package_name
15
+ RENDERER_PACKAGE
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Extension
4
+ module Features
5
+ class ArgoConfig
6
+ CONFIG_FILE_NAME = 'extension.config.yml'
7
+
8
+ class << self
9
+ def parse_yaml(context, permitted_keys = [])
10
+ file_name = File.join(context.root, CONFIG_FILE_NAME)
11
+
12
+ return {} unless File.size?(file_name)
13
+
14
+ require 'yaml' # takes 20ms, so deferred as late as possible.
15
+ begin
16
+ config = YAML.load_file(file_name)
17
+
18
+ # `YAML.load_file` returns nil if the file is not empty
19
+ # but does not contain any parsable yml data, e.g. only comments
20
+ # We consider this valid
21
+ return {} if config.nil?
22
+
23
+ unless config.is_a?(Hash)
24
+ raise ShopifyCli::Abort, ShopifyCli::Context.message('core.yaml.error.not_hash', CONFIG_FILE_NAME)
25
+ end
26
+
27
+ config.transform_keys!(&:to_sym)
28
+ assert_valid_config(config, permitted_keys) unless permitted_keys.empty?
29
+
30
+ config
31
+ rescue Psych::SyntaxError => e
32
+ raise(
33
+ ShopifyCli::Abort,
34
+ ShopifyCli::Context.message('core.yaml.error.invalid', CONFIG_FILE_NAME, e.message)
35
+ )
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def assert_valid_config(config, permitted_keys)
42
+ unpermitted_keys = config.keys.select do |k|
43
+ !permitted_keys.include?(k)
44
+ end
45
+
46
+ unless unpermitted_keys.empty?
47
+ raise(
48
+ ShopifyCli::Abort,
49
+ ShopifyCli::Context.message(
50
+ 'features.argo.config.unpermitted_keys',
51
+ CONFIG_FILE_NAME,
52
+ unpermitted_keys.map { |k| "\n- #{k}" }.join
53
+ )
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end