webpacker_uploader 0.3.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,23 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class WebpackerUploader::Instance
4
- cattr_accessor(:logger) { ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT)) }
5
-
6
4
  attr_writer :config
7
5
 
6
+ # @private
8
7
  def manifest
9
8
  @manifest ||= WebpackerUploader::Manifest.new
10
9
  end
11
10
 
11
+ # Returns the global WebpackerUploader::Configuration object.
12
+ # Use this to set and retrieve specific configuration options.
13
+ #
14
+ # @return [WebpackerUploader::Configuration]
15
+ #
16
+ # @example Disable log output
17
+ # WebpackerUploader.config.log_output = false
18
+ #
19
+ # @example Get the list of excluded file extension
20
+ # puts WebpackerUploader.config.ignored_extensions
21
+ #
22
+ # @see WebpackerUploader::Configuration
12
23
  def config
13
24
  @config ||= WebpackerUploader::Configuration.new
14
25
  end
15
26
 
27
+ # Yields the global configuration to a block. Use this to
28
+ # configure the base gem features.
29
+ #
30
+ # @example
31
+ # WebpackerUploader.configure do |config|
32
+ # config.ignored_extensions = [".png", ".jpg", ".webp"]
33
+ # config.log_output = false
34
+ # config.public_manifest_path = "path/to/manifest.json"
35
+ # config.public_path = "path/to/public/dir"
36
+ # end
37
+ #
38
+ # @see WebpackerUploader::Configuration
16
39
  def configure
17
40
  yield config
18
41
  end
19
42
 
20
- def upload!(provider, prefix: nil)
43
+ # Uploads assets using the supplied provider. Currently only AWS S3 is implemented.
44
+ #
45
+ # @return [void]
46
+ # @param provider [WebpackerUploader::Providers::Aws] A provider to use for file uploading.
47
+ # @param prefix [String] Used to prefix the remote file paths.
48
+ # @param cache_control [String] Used to add cache-control header to files.
49
+ def upload!(provider, prefix: nil, cache_control: nil)
21
50
  manifest.assets.each do |name, js_path|
22
51
  path = js_path[1..-1]
23
52
 
@@ -31,13 +60,13 @@ class WebpackerUploader::Instance
31
60
  file_path = config.public_path.join(path)
32
61
 
33
62
  if name.end_with?(*config.ignored_extensions)
34
- logger.info("Skipping #{file_path}") if config.log_output?
63
+ config.logger.info("Skipping #{file_path}") if config.log_output?
35
64
  else
36
65
  content_type = WebpackerUploader::Mime.mime_type(path)
37
66
 
38
- logger.info("Processing #{file_path} as #{content_type}") if config.log_output?
67
+ config.logger.info("Processing #{file_path} as #{content_type}") if config.log_output?
39
68
 
40
- provider.upload!(remote_path, file_path, content_type)
69
+ provider.upload!(remote_path, file_path, content_type, cache_control)
41
70
  end
42
71
  end
43
72
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class WebpackerUploader::Manifest
3
+ class WebpackerUploader::Manifest # @private
4
4
  attr_reader :assets
5
5
 
6
6
  def initialize
@@ -3,6 +3,12 @@
3
3
  require "mime-types"
4
4
 
5
5
  module WebpackerUploader::Mime
6
+ # Returns the mime type for the given file in the filesystem.
7
+ # If it's unable to detect the mime type, it returns +application/octet-stream+
8
+ # as a fallback.
9
+ #
10
+ # @param file_path [String] A file path in the local filesystem.
11
+ # @return [String] The file mime type.
6
12
  def mime_type(file_path)
7
13
  fallback = MIME::Types.type_for(file_path).first&.content_type || "application/octet-stream"
8
14
  Rack::Mime.mime_type(File.extname(file_path), fallback)
@@ -3,34 +3,105 @@
3
3
  require "aws-sdk-s3"
4
4
 
5
5
  module WebpackerUploader
6
+ # Namespace for the upload providers.
6
7
  module Providers
8
+ # AWS provider uploads files to AWS S3. It uses the +aws-sdk-s3+ gem.
7
9
  class Aws
8
- attr_reader :client
9
- attr_reader :resource
10
- attr_reader :bucket
10
+ # AWS provider error class. Raised when the provided AWS client credentials options are wrong.
11
+ class CredentialsError < StandardError; end
11
12
 
13
+ # @param [Hash] options
14
+ # * :region (String) The S3 region name.
15
+ # * :bucket (String) The S3 bucket name.
16
+ # * :credentials (Hash) credential options for the AWS provider:
17
+ # * :profile_name (String) use a named profile configured in ~/.aws/credentials
18
+ # * :instance_profile (Boolean) use an instance profile from an EC2
19
+ # * :access_key_id (String) the AWS credentials access id.
20
+ # * :secret_access_key (String) the AWS credentials secret access key.
21
+ #
22
+ # @note Any unknown options will be passed directly to the +Aws::S3::Client+ class
23
+ # during initialization.
24
+ #
25
+ # @raise [CredentialsError] if the credential options Hash is not correct.
26
+ #
27
+ # @example Initialize using a named profile:
28
+ #
29
+ # provider_options = {
30
+ # credentials: { profile_name: "staging" },
31
+ # region: "eu-central-1",
32
+ # bucket: "application-assets-20200929124523451600000001"
33
+ # }
34
+ # provider = WebpackerUploader::Providers::Aws.new(provider_options)
35
+ #
36
+ # @example Initialize using IAM keys
37
+ #
38
+ # provider_options = {
39
+ # credentials: { access_key_id: "KEY_ID", secret_access_key: "ACCESS_KEY" },
40
+ # region: "eu-central-1",
41
+ # bucket: "application-assets-20200929124523451600000001"
42
+ # }
43
+ # provider = WebpackerUploader::Providers::Aws.new(provider_options)
44
+ #
45
+ # @example Initialize using an EC2 instance profile
46
+ #
47
+ # provider_options = {
48
+ # credentials: { instance_profile: true },
49
+ # region: "eu-central-1",
50
+ # bucket: "application-assets-20200929124523451600000001"
51
+ # }
52
+ # provider = WebpackerUploader::Providers::Aws.new(provider_options)
12
53
  def initialize(options)
13
- @client = ::Aws::S3::Client.new(credentials: credentials(options[:credentials]), region: options[:region])
14
- @resource = ::Aws::S3::Resource.new(client: @client)
15
- @bucket = @resource.bucket(options[:bucket])
54
+ @region = options.delete(:region)
55
+ @bucket_name = options.delete(:bucket)
56
+ @credentials = credentials(options.delete(:credentials))
57
+ @aws_options = options
58
+ @resource = ::Aws::S3::Resource.new(client: client)
16
59
  end
17
60
 
18
- def upload!(object_key, file, content_type = "")
19
- object = @bucket.object(object_key)
20
- object.upload_file(file, content_type: content_type)
61
+ # Uploads a file to AWS S3.
62
+ #
63
+ # @param object_key [String] Is the remote path name for the S3 object.
64
+ # @param file [Pathname] Path of the local file.
65
+ # @param content_type [String] The content type that will be set to the S3 object.
66
+ # @return [void]
67
+ def upload!(object_key, file, content_type = "", cache_control = "")
68
+ object = @resource.bucket(@bucket_name).object(object_key)
69
+ object.upload_file(file, content_type: content_type, cache_control: cache_control)
21
70
  end
22
71
 
23
72
  private
24
-
25
73
  def credentials(options)
26
- if options[:profile_name].present?
27
- ::Aws::SharedCredentials.new(profile_name: options[:profile_name])
74
+ if options.key?(:profile_name)
75
+ { profile: options[:profile_name] }
28
76
  elsif options.key?(:instance_profile) && options[:instance_profile]
29
77
  ::Aws::InstanceProfileCredentials.new
30
- else
78
+ elsif options.key?(:access_key_id) && options.key?(:secret_access_key)
31
79
  ::Aws::Credentials.new(options[:access_key_id], options[:secret_access_key])
80
+ else
81
+ raise CredentialsError, "Wrong AWS provider credentials options."
32
82
  end
33
83
  end
84
+
85
+ def profile?
86
+ @credentials.is_a?(Hash) && @credentials.key?(:profile)
87
+ end
88
+
89
+ def credentials_object?
90
+ !profile?
91
+ end
92
+
93
+ def client
94
+ ::Aws::S3::Client.new(client_options)
95
+ end
96
+
97
+ def client_options
98
+ opts = {}
99
+ opts.merge!(@aws_options)
100
+ opts[:region] = @region
101
+ opts.merge!(@credentials) if profile?
102
+ opts[:credentials] = @credentials if credentials_object?
103
+ opts
104
+ end
34
105
  end
35
106
  end
36
107
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module WebpackerUploader
2
- VERSION = "0.3.0"
4
+ VERSION = "0.7.0"
3
5
  end
@@ -1,21 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/module"
3
4
  require "active_support/core_ext/object/blank"
4
- require "active_support/logger"
5
- require "active_support/tagged_logging"
6
5
 
7
6
  module WebpackerUploader
8
7
  extend self
9
8
 
9
+ # @private
10
10
  def instance=(instance)
11
11
  @instance = instance
12
12
  end
13
13
 
14
+ # @private
14
15
  def instance
15
16
  @instance ||= WebpackerUploader::Instance.new
16
17
  end
17
18
 
18
- delegate :logger, :logger=, :configure, :config, :upload!, to: :instance
19
+ # @!attribute [rw] config
20
+ # @see Instance#config
21
+ # @!scope class
22
+ # @!method configure
23
+ # @see Instance#configure
24
+ # @!scope class
25
+ # @!method upload!
26
+ # @return [void]
27
+ # @see Instance#upload!
28
+ # @!scope class
29
+ delegate :configure, :config, :upload!, to: :instance
19
30
  end
20
31
 
21
32
  require "webpacker_uploader/configuration"
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class ConfigurationTest < Minitest::Test
6
+ def setup
7
+ @config = WebpackerUploader::Configuration.new
8
+ end
9
+
10
+ def teardown
11
+ WebpackerUploader.reset!
12
+ end
13
+
14
+ def test_default_config_options
15
+ assert_empty @config.ignored_extensions
16
+
17
+ assert_instance_of ActiveSupport::Logger, @config.logger
18
+
19
+ assert @config.log_output
20
+ assert @config.log_output?
21
+
22
+ public_manifest_path = Pathname.new(File.expand_path("test_app/public/packs/manifest.json", __dir__))
23
+ assert_equal public_manifest_path, @config.public_manifest_path
24
+
25
+ public_path = Pathname.new(File.expand_path("test_app/public/", __dir__))
26
+ assert_equal public_path, @config.public_path
27
+ end
28
+
29
+ def test_changing_config_options
30
+ @config.ignored_extensions = [".css", ".js"]
31
+ assert_equal [".css", ".js"], @config.ignored_extensions
32
+
33
+ @config.logger = Logger.new(STDOUT)
34
+ assert_instance_of Logger, @config.logger
35
+
36
+ @config.log_output = false
37
+ refute @config.log_output
38
+ refute @config.log_output?
39
+
40
+ @config.public_manifest_path = "test_app/manifest.json"
41
+ assert_equal "test_app/manifest.json", @config.public_manifest_path.to_s
42
+
43
+ @config.public_path = "test_app"
44
+ assert_equal "test_app", @config.public_path.to_s
45
+ end
46
+
47
+ def test_configure_block
48
+ WebpackerUploader.configure do |c|
49
+ c.ignored_extensions = [".js"]
50
+ c.logger = Logger.new(STDOUT)
51
+ c.log_output = false
52
+ c.public_manifest_path = "path/to/manifest.json"
53
+ c.public_path = "path/to/public/dir"
54
+ end
55
+
56
+ assert_equal [".js"], WebpackerUploader.config.ignored_extensions
57
+ assert_instance_of Logger, WebpackerUploader.config.logger
58
+ refute WebpackerUploader.config.log_output
59
+ refute WebpackerUploader.config.log_output?
60
+ assert_equal "path/to/manifest.json", WebpackerUploader.config.public_manifest_path.to_s
61
+ assert_equal "path/to/public/dir", WebpackerUploader.config.public_path.to_s
62
+
63
+ assert_raises(NoMethodError) do
64
+ WebpackerUploader.configure do |c|
65
+ c.unknown = true
66
+ end
67
+ end
68
+ end
69
+
70
+ def test_pathname_casting
71
+ WebpackerUploader.config do |c|
72
+ c.public_manifest_path = "path/to/manifest.json"
73
+ c.public_path = "path/to/public/dir"
74
+ end
75
+
76
+ assert_instance_of Pathname, WebpackerUploader.config.public_manifest_path
77
+ assert_instance_of Pathname, WebpackerUploader.config.public_path
78
+
79
+ WebpackerUploader.configure do |c|
80
+ c.public_manifest_path = Pathname.new("path/to/manifest.json")
81
+ c.public_path = Pathname.new("path/to/public/dir")
82
+ end
83
+
84
+ assert_instance_of Pathname, WebpackerUploader.config.public_manifest_path
85
+ assert_instance_of Pathname, WebpackerUploader.config.public_path
86
+ end
87
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class ManifestTest < Minitest::Test
6
+ def setup
7
+ @manifest = ::WebpackerUploader::Manifest.new
8
+ end
9
+
10
+ def teardown
11
+ WebpackerUploader.reset!
12
+ end
13
+
14
+ def test_assets
15
+ assert_includes @manifest.assets, "application.css"
16
+ assert_includes @manifest.assets, "application.js"
17
+ assert_includes @manifest.assets, "application.png"
18
+ refute_includes @manifest.assets, "entrypoints"
19
+ end
20
+
21
+ def test_missing_manifest
22
+ WebpackerUploader.config.public_manifest_path = "missing.json"
23
+ @empty_manifest = WebpackerUploader::Manifest.new
24
+
25
+ assert_empty @empty_manifest.assets
26
+ end
27
+ end
data/test/mime_test.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class MimeTest < Minitest::Test
6
+ def test_for_webp
7
+ # Rack < 3.x does not support this so we have a fallback mechanism in place
8
+ assert_equal "image/webp", WebpackerUploader::Mime.mime_type("image-dd6b1cd38bfa093df600.webp")
9
+ end
10
+
11
+ def test_for_sourcemap
12
+ assert_equal "application/octet-stream", WebpackerUploader::Mime.mime_type("application-dd6b1cd38bfa093df600.css.map")
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "webpacker_uploader/providers/aws"
5
+
6
+ class AwsTest < Minitest::Test
7
+ def test_credentials_initialization
8
+ assert_raises(WebpackerUploader::Providers::Aws::CredentialsError) do
9
+ WebpackerUploader::Providers::Aws.new(credentials: {})
10
+ end
11
+
12
+ assert_raises(WebpackerUploader::Providers::Aws::CredentialsError) do
13
+ WebpackerUploader::Providers::Aws.new(credentials: { access_key_id: "test" })
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,103 @@
1
+ # Note: You must restart bin/webpack-dev-server for changes to take effect
2
+
3
+ default: &default
4
+ source_path: app/javascript
5
+ source_entry_path: packs
6
+ public_root_path: public
7
+ public_output_path: packs
8
+ cache_path: tmp/cache/webpacker
9
+ webpack_compile_output: false
10
+
11
+ # Additional paths webpack should lookup modules
12
+ # ['app/assets', 'engine/foo/app/assets']
13
+ additional_paths:
14
+ - app/assets
15
+ - /etc/yarn
16
+
17
+ # This configuration option is deprecated and is only here for testing, to
18
+ # ensure backwards-compatibility. Please use `additional_paths`.
19
+ resolved_paths:
20
+ - app/elm
21
+
22
+ # Reload manifest.json on all requests so we reload latest compiled packs
23
+ cache_manifest: false
24
+
25
+ # Extract and emit a css file
26
+ extract_css: false
27
+
28
+ static_assets_extensions:
29
+ - .jpg
30
+ - .jpeg
31
+ - .png
32
+ - .gif
33
+ - .tiff
34
+ - .ico
35
+ - .svg
36
+
37
+ extensions:
38
+ - .mjs
39
+ - .js
40
+ - .sass
41
+ - .scss
42
+ - .css
43
+ - .module.sass
44
+ - .module.scss
45
+ - .module.css
46
+ - .png
47
+ - .svg
48
+ - .gif
49
+ - .jpeg
50
+ - .jpg
51
+ - .elm
52
+
53
+ development:
54
+ <<: *default
55
+ compile: true
56
+
57
+ # Reference: https://webpack.js.org/configuration/dev-server/
58
+ dev_server:
59
+ https: false
60
+ host: localhost
61
+ port: 3035
62
+ public: localhost:3035
63
+ hmr: false
64
+ # Inline should be set to true if using HMR
65
+ inline: true
66
+ overlay: true
67
+ disable_host_check: true
68
+ use_local_ip: false
69
+ pretty: false
70
+
71
+ test:
72
+ <<: *default
73
+ compile: true
74
+
75
+ # Compile test packs to a separate directory
76
+ public_output_path: packs-test
77
+
78
+ production:
79
+ <<: *default
80
+
81
+ # Production depends on precompilation of packs prior to booting for performance.
82
+ compile: false
83
+
84
+ # Extract and emit a css file
85
+ extract_css: true
86
+
87
+ # Cache manifest.json for performance
88
+ cache_manifest: true
89
+
90
+ staging:
91
+ <<: *default
92
+
93
+ # Production depends on precompilation of packs prior to booting for performance.
94
+ compile: false
95
+
96
+ # Extract and emit a css file
97
+ extract_css: true
98
+
99
+ # Cache manifest.json for performance
100
+ cache_manifest: true
101
+
102
+ # Compile staging packs to a separate directory
103
+ public_output_path: packs-staging
@@ -0,0 +1,33 @@
1
+ {
2
+ "bootstrap.css": "/packs/bootstrap-c38deda30895059837cf.css",
3
+ "application.css": "/packs/application-dd6b1cd38bfa093df600.css",
4
+ "application.css.map": "/packs/application-dd6b1cd38bfa093df600.css.map",
5
+ "bootstrap.js": "/packs/bootstrap-300631c4f0e0f9c865bc.js",
6
+ "application.js": "/packs/application-k344a6d59eef8632c9d1.js",
7
+ "application.png": "/packs/application-k344a6d59eef8632c9d1.png",
8
+ "fonts/fa-regular-400.woff2": "/packs/fonts/fa-regular-400-944fb546bd7018b07190a32244f67dc9.woff2",
9
+ "media/images/image.jpg": "/packs/media/images/image-c38deda30895059837cf.jpg",
10
+ "media/images/image-2x.jpg": "/packs/media/images/image-2x-7cca48e6cae66ec07b8e.jpg",
11
+ "media/images/nested/image.jpg": "/packs/media/images/nested/image-c38deda30895059837cf.jpg",
12
+ "media/images/mb-icon.png": "/packs/media/images/mb-icon-c38deda30895059837cf.png",
13
+ "media/images/nested/mb-icon.png": "/packs/media/images/nested/mb-icon-c38deda30895059837cf.png",
14
+ "entrypoints": {
15
+ "application": {
16
+ "js": [
17
+ "/packs/vendors~application~bootstrap-c20632e7baf2c81200d3.chunk.js",
18
+ "/packs/vendors~application-e55f2aae30c07fb6d82a.chunk.js",
19
+ "/packs/application-k344a6d59eef8632c9d1.js"
20
+ ],
21
+ "css": [
22
+ "/packs/1-c20632e7baf2c81200d3.chunk.css",
23
+ "/packs/application-k344a6d59eef8632c9d1.chunk.css"
24
+ ]
25
+ },
26
+ "hello_stimulus": {
27
+ "css": [
28
+ "/packs/1-c20632e7baf2c81200d3.chunk.css",
29
+ "/packs/hello_stimulus-k344a6d59eef8632c9d1.chunk.css"
30
+ ]
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "rails"
5
+ require "rails/test_help"
6
+ require "webpacker"
7
+ require "webpacker_uploader"
8
+
9
+ module TestApp
10
+ class Application < ::Rails::Application
11
+ config.root = File.join(File.dirname(__FILE__), "test_app")
12
+ config.eager_load = true
13
+ end
14
+ end
15
+
16
+ TestApp::Application.initialize!
17
+
18
+ module WebpackerUploader::Providers
19
+ class TestProvider
20
+ def initialize(asset_objects)
21
+ @asset_objects = asset_objects
22
+ end
23
+
24
+ def upload!(object_key, file, content_type = "", cache_control = "")
25
+ @asset_objects << { object_key: object_key, file: file, content_type: content_type, cache_control: cache_control }
26
+ end
27
+ end
28
+ end
29
+
30
+ module WebpackerUploader
31
+ def reset!
32
+ WebpackerUploader.instance.config = nil
33
+ end
34
+ module_function :reset!
35
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class WebpackerUploaderTest < Minitest::Test
6
+ def setup
7
+ @asset_objects = []
8
+ @provider = WebpackerUploader::Providers::TestProvider.new(@asset_objects)
9
+
10
+ WebpackerUploader.config.log_output = false
11
+ end
12
+
13
+ def teardown
14
+ WebpackerUploader.reset!
15
+ end
16
+
17
+ def test_upload
18
+ WebpackerUploader.upload!(@provider)
19
+
20
+ assert_equal "packs/bootstrap-c38deda30895059837cf.css", @asset_objects.first[:object_key]
21
+ assert_instance_of Pathname, @asset_objects.first[:file]
22
+ assert_equal "text/css", @asset_objects.first[:content_type]
23
+ end
24
+
25
+ def test_upload_with_prefix
26
+ WebpackerUploader.upload!(@provider, prefix: "prefix")
27
+
28
+ assert_equal "prefix/packs/bootstrap-c38deda30895059837cf.css", @asset_objects.first[:object_key]
29
+ assert_instance_of Pathname, @asset_objects.first[:file]
30
+ assert_equal "text/css", @asset_objects.first[:content_type]
31
+ end
32
+
33
+ def test_upload_with_cache_control
34
+ WebpackerUploader.upload!(@provider, cache_control: "cache_control")
35
+ assert_equal "cache_control", @asset_objects.first[:cache_control]
36
+ end
37
+ end
@@ -20,20 +20,16 @@ Gem::Specification.new do |s|
20
20
  "source_code_uri" => "https://github.com/tlatsas/webpacker_uploader/tree/v#{s.version}"
21
21
  }
22
22
 
23
- # Specify which files should be added to the gem when it is released.
24
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
- s.files = Dir.chdir(File.expand_path("..", __FILE__)) do
26
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
- end
28
-
29
- s.bindir = "exe"
30
- s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ s.files = `git ls-files`.split("\n").reject { |f| f.match(%r{^(bin|test|integration|.github)/}) }
24
+ s.test_files = `git ls-files -- test/*`.split("\n")
31
25
  s.require_paths = ["lib"]
32
26
 
33
27
  s.add_dependency "webpacker", ">= 5.1"
34
28
  s.add_dependency "mime-types"
29
+ s.add_dependency "rack", "~> 2.0"
35
30
 
36
31
  s.add_development_dependency "bundler", ">= 1.3.0"
37
- s.add_development_dependency "rubocop", "< 0.69"
32
+ s.add_development_dependency "rubocop", "1.11.0"
38
33
  s.add_development_dependency "rubocop-performance"
34
+ s.add_development_dependency "rubocop-minitest"
39
35
  end