trmnl_preview 0.3.1 → 0.4.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +55 -21
  4. data/bin/trmnlp +14 -0
  5. data/lib/trmnlp/api_client.rb +62 -0
  6. data/lib/trmnlp/app.rb +98 -0
  7. data/lib/trmnlp/cli.rb +51 -0
  8. data/lib/trmnlp/commands/base.rb +23 -0
  9. data/lib/trmnlp/commands/build.rb +22 -0
  10. data/lib/trmnlp/commands/login.rb +25 -0
  11. data/lib/trmnlp/commands/pull.rb +45 -0
  12. data/lib/trmnlp/commands/push.rb +43 -0
  13. data/lib/trmnlp/commands/serve.rb +25 -0
  14. data/lib/trmnlp/commands.rb +1 -0
  15. data/lib/trmnlp/config/app.rb +39 -0
  16. data/lib/trmnlp/config/plugin.rb +74 -0
  17. data/lib/trmnlp/config/project.rb +47 -0
  18. data/lib/trmnlp/config.rb +15 -0
  19. data/lib/trmnlp/context.rb +211 -0
  20. data/lib/trmnlp/custom_filters.rb +14 -0
  21. data/lib/trmnlp/paths.rb +50 -0
  22. data/lib/trmnlp/screen_generator.rb +137 -0
  23. data/lib/trmnlp/version.rb +5 -0
  24. data/lib/trmnlp.rb +13 -0
  25. data/trmnl_preview.gemspec +32 -14
  26. data/web/public/index.css +98 -0
  27. data/web/public/index.js +75 -0
  28. data/web/views/index.erb +15 -80
  29. data/web/views/{render_view.erb → render_html.erb} +1 -6
  30. metadata +148 -26
  31. data/.ruby-version +0 -1
  32. data/config.example.toml +0 -14
  33. data/docs/preview.png +0 -0
  34. data/exe/trmnlp +0 -12
  35. data/lib/trmnl_preview/app.rb +0 -78
  36. data/lib/trmnl_preview/cmd/build.rb +0 -25
  37. data/lib/trmnl_preview/cmd/serve.rb +0 -31
  38. data/lib/trmnl_preview/cmd/usage.rb +0 -10
  39. data/lib/trmnl_preview/context.rb +0 -129
  40. data/lib/trmnl_preview/liquid_filters.rb +0 -8
  41. data/lib/trmnl_preview/version.rb +0 -5
  42. data/lib/trmnl_preview.rb +0 -10
  43. data/web/public/live-render.js +0 -23
@@ -0,0 +1,74 @@
1
+ require 'yaml'
2
+
3
+ module TRMNLP
4
+ class Config
5
+ class Plugin
6
+ def initialize(paths, trmnlp_config)
7
+ @paths = paths
8
+ @trmnlp_config = trmnlp_config
9
+ reload!
10
+ end
11
+
12
+ def reload!
13
+ if paths.plugin_config.exist?
14
+ @config = YAML.load_file(paths.plugin_config)
15
+ else
16
+ @config = {}
17
+ end
18
+ end
19
+
20
+ def strategy = @config['strategy']
21
+ def polling? = strategy == 'polling'
22
+ def webhook? = strategy == 'webhook'
23
+ def static? = strategy == 'static'
24
+
25
+ def polling_urls
26
+ return [] if @config['polling_url'].nil? || @config['polling_url'].empty?
27
+
28
+ urls = @config['polling_url'].split("\n").map(&:strip)
29
+
30
+ urls.map { |url| with_custom_fields(url) }
31
+ end
32
+
33
+ def polling_url_text = polling_urls.join("\r\n") # for {{ trmnl }}
34
+
35
+ def polling_verb = @config['polling_verb'] || 'GET'
36
+
37
+ def polling_headers
38
+ string_to_hash(@config['polling_headers'] || '').transform_values { |v| with_custom_fields(v) }
39
+ end
40
+
41
+ def polling_headers_encoded = polling_headers.map { |k, v| "#{k}=#{v}" }.join('&') # for {{ trmnl }}
42
+
43
+ def polling_body = with_custom_fields(@config['polling_body'] || '')
44
+
45
+ def dark_mode = @config['dark_mode'] || 'no'
46
+
47
+ def no_screen_padding = @config['no_screen_padding'] || 'no'
48
+
49
+ def id = @config['id']
50
+
51
+ def static_data
52
+ JSON.parse(@config['static_data'] || '{}')
53
+ rescue JSON::ParserError
54
+ raise Error, 'invalid JSON in static_data'
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :paths, :trmnlp_config
60
+
61
+ def with_custom_fields(value) = trmnlp_config.with_custom_fields(value)
62
+
63
+ # copied from TRMNL core
64
+ def string_to_hash(str, delimiter: '=')
65
+ str.split('&').map do |k_v|
66
+ key, value = k_v.split(delimiter)
67
+ next if value.nil?
68
+
69
+ { key => CGI.unescape_uri_component(value) }
70
+ end.compact.reduce({}, :merge)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,47 @@
1
+ require 'yaml'
2
+
3
+ module TRMNLP
4
+ class Config
5
+ class Project
6
+ attr_reader :paths
7
+
8
+ def initialize(paths)
9
+ @paths = paths
10
+ reload!
11
+ end
12
+
13
+ def reload!
14
+ if paths.trmnlp_config.exist?
15
+ @config = YAML.load_file(paths.trmnlp_config)
16
+ else
17
+ @config = {}
18
+ end
19
+ end
20
+
21
+ def user_filters = @config['custom_filters'] || []
22
+
23
+ def live_render? = !watch_paths.empty?
24
+
25
+ def watch_paths
26
+ (@config['watch'] || []).map { |watch_path| paths.expand(watch_path) }.uniq
27
+ end
28
+
29
+ def custom_fields = @config['custom_fields'] || {}
30
+
31
+ def user_data_overrides = @config['variables'] || {}
32
+
33
+ # for interpolating custom_fields into polling_* options
34
+ def with_custom_fields(value)
35
+ custom_fields_with_env = custom_fields.transform_values { |v| with_env(v) }
36
+ Liquid::Template.parse(value).render(custom_fields_with_env)
37
+ end
38
+
39
+ private
40
+
41
+ # for interpolating ENV vars into custom_fields
42
+ def with_env(value)
43
+ Liquid::Template.parse(value).render({ 'env' => ENV.to_h })
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'config/app'
2
+ require_relative 'config/plugin'
3
+ require_relative 'config/project'
4
+
5
+ module TRMNLP
6
+ class Config
7
+ attr_reader :app, :project, :plugin
8
+
9
+ def initialize(path)
10
+ @app = App.new(path)
11
+ @project = Project.new(path)
12
+ @plugin = Plugin.new(path, @project)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,211 @@
1
+ require 'erb'
2
+ require 'faraday'
3
+ require 'filewatcher'
4
+ require 'json'
5
+ require 'liquid'
6
+
7
+ require_relative 'config'
8
+ require_relative 'custom_filters'
9
+ require_relative 'paths'
10
+
11
+ module TRMNLP
12
+ class Context
13
+ attr_reader :config, :paths
14
+
15
+ def initialize(root_dir)
16
+ @paths = Paths.new(root_dir)
17
+ @config = Config.new(paths)
18
+ end
19
+
20
+ def validate!
21
+ raise Error, "not a plugin directory (did not find #{paths.trmnlp_config})" unless paths.valid?
22
+ end
23
+
24
+ def start_filewatcher
25
+ @filewatcher_thread ||= Thread.new do
26
+ loop do
27
+ begin
28
+ Filewatcher.new(config.project.watch_paths).watch do |changes|
29
+ config.project.reload!
30
+ config.plugin.reload!
31
+ new_user_data = user_data
32
+
33
+ views = changes.map { |path, _change| File.basename(path, '.liquid') }
34
+ views.each do |view|
35
+ @view_change_callback.call(view, new_user_data) if @view_change_callback
36
+ end
37
+ end
38
+ rescue => e
39
+ puts "Error during live render: #{e}"
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def on_view_change(&block)
46
+ @view_change_callback = block
47
+ end
48
+
49
+ def user_data
50
+ merged_data = base_trmnl_data
51
+
52
+ if config.plugin.static?
53
+ merged_data.merge!(config.plugin.static_data)
54
+ elsif paths.user_data.exist?
55
+ merged_data.merge!(JSON.parse(paths.user_data.read))
56
+ end
57
+
58
+ # Praise be to ActiveSupport
59
+ merged_data.deep_merge!(config.project.user_data_overrides)
60
+ end
61
+
62
+ def poll_data
63
+ return unless config.plugin.polling?
64
+
65
+ data = {}
66
+
67
+ if config.plugin.polling_urls.empty?
68
+ raise Error, "config must specify polling_url or polling_urls"
69
+ end
70
+
71
+ config.plugin.polling_urls.each.with_index do |url, i|
72
+ verb = config.plugin.polling_verb.upcase
73
+
74
+ print "#{verb} #{url}... "
75
+
76
+ conn = Faraday.new(url:, headers: config.plugin.polling_headers)
77
+
78
+ case verb
79
+ when 'GET'
80
+ response = conn.get
81
+ when 'POST'
82
+ response = conn.post do |req|
83
+ req.body = config.plugin.polling_body
84
+ end
85
+ end
86
+
87
+ puts "received #{response.body.length} bytes (#{response.status} status)"
88
+ if response.status == 200
89
+ json = wrap_array(JSON.parse(response.body))
90
+ else
91
+ json = {}
92
+ puts response.body
93
+ end
94
+
95
+ if config.plugin.polling_urls.count == 1
96
+ # For a single polling URL, we just return the JSON directly
97
+ data = json
98
+ break
99
+ else
100
+ # Multiple URLs are namespaced by index
101
+ data["IDX_#{i}"] = json
102
+ end
103
+ end
104
+
105
+ write_user_data(data)
106
+
107
+ data
108
+ rescue StandardError => e
109
+ puts "error: #{e.message}"
110
+ {}
111
+ end
112
+
113
+ def put_webhook(payload)
114
+ data = wrap_array(JSON.parse(payload))
115
+ write_user_data(data)
116
+ rescue
117
+ puts "webhook error: #{e.message}"
118
+ end
119
+
120
+ def render_template(view)
121
+ template_path = paths.template(view)
122
+ return "Missing template: #{template_path}" unless template_path.exist?
123
+
124
+ user_template = Liquid::Template.parse(template_path.read, environment: liquid_environment)
125
+ user_template.render(user_data)
126
+ rescue StandardError => e
127
+ e.message
128
+ end
129
+
130
+ def render_full_page(view)
131
+ template = paths.render_template.read
132
+
133
+ ERB.new(template).result(TemplateBinding.new(self, view).get_binding do
134
+ render_template(view)
135
+ end)
136
+ end
137
+
138
+ def screen_classes
139
+ classes = 'screen'
140
+ classes << ' screen--no-bleed' if config.plugin.no_screen_padding == 'yes'
141
+ classes << ' dark-mode' if config.plugin.dark_mode == 'yes'
142
+ classes
143
+ end
144
+
145
+ private
146
+
147
+ # bindings must match the `GET /render/{view}.html` route in app.rb
148
+ class TemplateBinding
149
+ def initialize(context, view)
150
+ @screen_classes = context.screen_classes
151
+ @view = view
152
+ end
153
+
154
+ def get_binding = binding
155
+ end
156
+
157
+ def wrap_array(json)
158
+ json.is_a?(Array) ? { data: json } : json
159
+ end
160
+
161
+ def base_trmnl_data
162
+ {
163
+ 'trmnl' => {
164
+ 'user' => {
165
+ 'name' => 'name',
166
+ 'first_name' => 'first_name',
167
+ 'last_name' => 'last_name',
168
+ 'locale' => 'en',
169
+ 'time_zone' => 'Eastern Time (US & Canada)',
170
+ 'time_zone_iana' => 'America/New_York',
171
+ 'utc_offset' => -14400
172
+ },
173
+ 'device' => {
174
+ 'friendly_id' => 'ABC123',
175
+ 'percent_charged' => 85.0,
176
+ 'wifi_strength' => 90,
177
+ 'height' => 480,
178
+ 'width' => 800
179
+ },
180
+ 'system' => {
181
+ 'timestamp_utc' => Time.now.utc.to_i,
182
+ },
183
+ 'plugin_settings' => {
184
+ 'instance_name' => 'instance_name',
185
+ 'strategy' => config.plugin.strategy,
186
+ 'dark_mode' => config.plugin.dark_mode,
187
+ 'polling_headers' => config.plugin.polling_headers_encoded,
188
+ 'polling_url' => config.plugin.polling_url_text,
189
+ 'custom_fields_values' => config.project.custom_fields
190
+ }
191
+ }
192
+ }
193
+ end
194
+
195
+ def liquid_environment
196
+ @liquid_environment ||= Liquid::Environment.build do |env|
197
+ env.register_filter(CustomFilters)
198
+
199
+ config.project.user_filters.each do |module_name, relative_path|
200
+ require paths.root_dir.join(relative_path)
201
+ env.register_filter(Object.const_get(module_name))
202
+ end
203
+ end
204
+ end
205
+
206
+ def write_user_data(data)
207
+ paths.create_cache_dir
208
+ paths.user_data.write(JSON.generate(data))
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,14 @@
1
+ require 'active_support'
2
+
3
+ module TRMNLP
4
+ module CustomFilters
5
+ # TODO: sync up with core
6
+ def number_with_delimiter(*args)
7
+ ActiveSupport::NumberHelper.number_to_delimited(*args)
8
+ end
9
+
10
+ def number_to_currency(*args)
11
+ ActiveSupport::NumberHelper.number_to_currency(*args)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,50 @@
1
+ require 'xdg'
2
+
3
+ module TRMNLP
4
+ class Paths
5
+ attr_reader :root_dir
6
+
7
+ def initialize(root_dir)
8
+ @root_dir = Pathname.new(root_dir)
9
+ @xdg = XDG.new
10
+ end
11
+
12
+ # --- directories ---
13
+
14
+ def src_dir = root_dir.join('src')
15
+
16
+ def build_dir = root_dir.join('_build')
17
+ def create_build_dir = build_dir.mkpath
18
+
19
+ def app_config_dir = xdg.config_home.join('trmnlp')
20
+
21
+ def cache_dir = xdg.cache_home.join('trmnl')
22
+ def create_cache_dir = cache_dir.mkpath
23
+
24
+ def valid? = trmnlp_config.exist?
25
+
26
+ # --- files ---
27
+
28
+ def trmnlp_config = root_dir.join('.trmnlp.yml')
29
+
30
+ def plugin_config = src_dir.join('settings.yml')
31
+
32
+ def template(view) = src_dir.join("#{view}.liquid")
33
+
34
+ def app_config = app_config_dir.join('config.yml')
35
+
36
+ def user_data = cache_dir.join('data.json')
37
+
38
+ def render_template = Pathname.new(__dir__).join('..', '..', 'web', 'views', 'render_html.erb')
39
+
40
+ def src_files = src_dir.glob('*').select(&:file?)
41
+
42
+ # --- utilities ---
43
+
44
+ def expand(path) = Pathname.new(path).expand_path(root_dir)
45
+
46
+ private
47
+
48
+ attr_reader :xdg
49
+ end
50
+ end
@@ -0,0 +1,137 @@
1
+ require 'ferrum'
2
+ require 'mini_magick'
3
+ require 'puppeteer-ruby'
4
+ require 'base64'
5
+
6
+ module TRMNLP
7
+ class ScreenGenerator
8
+
9
+ def initialize(html, opts = {})
10
+ self.input = html
11
+ self.image = !!opts[:image]
12
+ end
13
+
14
+ attr_accessor :input, :output, :image, :processor, :img_path
15
+
16
+ def process
17
+ convert_to_image
18
+ image ? mono_image(output) : mono(output)
19
+ output.path
20
+ # IO.copy_stream(output, img_path)
21
+ end
22
+
23
+ private
24
+
25
+ # def img_path
26
+ # "#{Dir.pwd}/public/images/generated/#{SecureRandom.hex(3)}.bmp"
27
+ # end
28
+
29
+ # Constructs the command and passes the input to the vendor/puppeteer.js
30
+ # script for processing. Returns a base64 encoded string
31
+ def convert_to_image
32
+ retry_count = 0
33
+ begin
34
+ # context = browser_instance.create_incognito_browser_context
35
+ page = firefox_browser.new_page
36
+ page.viewport = Puppeteer::Viewport.new(width: width, height: height)
37
+ # NOTE: Use below for chromium
38
+ # page.set_content(input, wait_until: ['networkidle0', 'domcontentloaded'])
39
+ # Note: Use below for firefox
40
+ page.set_content(input, timeout: 10000)
41
+ page.evaluate(<<~JAVASCRIPT)
42
+ () => {
43
+ document.getElementsByTagName('html')[0].style.overflow = "hidden";
44
+ document.getElementsByTagName('body')[0].style.overflow = "hidden";
45
+ }
46
+ JAVASCRIPT
47
+ self.output = Tempfile.new
48
+ page.screenshot(path: output.path, type: 'png')
49
+ firefox_browser.close
50
+ end
51
+ rescue Puppeteer::TimeoutError, Puppeteer::FrameManager::NavigationError => e
52
+ retry_count += 1
53
+ firefox_browser.close
54
+ if retry_count <= 1
55
+ @browser = nil
56
+ retry
57
+ else
58
+ puts "ERROR -> Converter::Html#convert_to_image_by_firefox -> #{e.message}"
59
+ end
60
+ end
61
+
62
+ # Refer this PR where the author reused the browser instance https://github.com/YusukeIwaki/puppeteer-ruby/pull/100/files
63
+ # This will increase the throughput of our image rendering process by 60-70%, saving about ~1.5 second per image generation.
64
+ # On local it takes < 1 second now to generate the subsequent image.
65
+ def firefox_browser
66
+ @browser ||= Puppeteer.launch(
67
+ product: 'firefox',
68
+ headless: true,
69
+ args: [
70
+ "--window-size=#{width},#{height}",
71
+ "--disable-web-security"
72
+ # "--hide-scrollbars" #works only on chrome, using page.evaluate for firefox
73
+ ]
74
+ )
75
+ end
76
+
77
+ def Ferrum.cached_browser
78
+ return nil unless $cached_browser
79
+
80
+ $cached_browser
81
+ end
82
+
83
+ def Ferrum.cached_browser=(value)
84
+ $cached_browser = value
85
+ end
86
+
87
+ # Overall at max wait for 2.5 seconds
88
+ def wait_for_stop_loading(page)
89
+ count = 0
90
+ while page.frames.first.state != :stopped_loading && count < 20
91
+ count += 1
92
+ sleep 0.1
93
+ end
94
+ sleep 0.5 # wait_until: DomContentLoaded event is not available in ferrum
95
+ end
96
+
97
+ def mono(img)
98
+ MiniMagick::Tool::Convert.new do |m|
99
+ m << img.path
100
+ m.monochrome # Use built-in smart monochrome dithering (but it's not working as expected)
101
+ m.depth(color_depth) # Should be set to 1 for 1-bit output
102
+ m.strip # Remove any additional metadata
103
+ m << ('bmp3:' << img.path)
104
+ end
105
+ end
106
+
107
+ def mono_image(img)
108
+ # Changelog:
109
+ # ImageMagick 6.XX used to convert the png to bitmap with dithering while maintaining the channel to 1
110
+ # The same seems to be broken with imagemagick 7.XX
111
+ # So in order to reduce the channel from 8 to 1, I just rerun the command, and it's working
112
+ # TODO for future, find a better way to generate image screens.
113
+ MiniMagick::Tool::Convert.new do |m|
114
+ m << img.path
115
+ m.dither << 'FloydSteinberg'
116
+ m.remap << 'pattern:gray50'
117
+ m.depth(color_depth) # Should be set to 1 for 1-bit output
118
+ m.strip # Remove any additional metadata
119
+ m << ('bmp3:' << img.path) # Converts to Bitmap.
120
+ end
121
+ MiniMagick::Tool::Convert.new do |m|
122
+ m << img.path
123
+ m.dither << 'FloydSteinberg'
124
+ m.remap << 'pattern:gray50'
125
+ m.depth(color_depth) # Should be set to 1 for 1-bit output
126
+ m.strip # Remove any additional metadata
127
+ m << ('bmp3:' << img.path) # Converts to Bitmap.
128
+ end
129
+ end
130
+
131
+ def width = 800
132
+
133
+ def height = 480
134
+
135
+ def color_depth = 1
136
+ end
137
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRMNLP
4
+ VERSION = "0.4.0".freeze
5
+ end
data/lib/trmnlp.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRMNLP; end
4
+
5
+ require_relative "trmnlp/config"
6
+ require_relative "trmnlp/context"
7
+ require_relative "trmnlp/version"
8
+
9
+ module TRMNLP
10
+ VIEWS = %w{full half_horizontal half_vertical quadrant}
11
+
12
+ class Error < StandardError; end
13
+ end
@@ -1,44 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/trmnl_preview/version"
3
+ require_relative "lib/trmnlp/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "trmnl_preview"
7
- spec.version = TRMNLPreview::VERSION
7
+ spec.version = TRMNLP::VERSION
8
8
  spec.authors = ["Rockwell Schrock"]
9
9
  spec.email = ["rockwell@schrock.me"]
10
10
 
11
11
  spec.summary = "Local web server to preview TRMNL plugins"
12
12
  spec.description = "Automatically rebuild and preview TRNML plugins in multiple views"
13
- spec.homepage = "https://github.com/schrockwell/trmnl_preview"
13
+ spec.homepage = "https://github.com/usetrmnl/trmnlp"
14
14
  spec.license = "MIT"
15
15
  spec.required_ruby_version = ">= 3.0.0"
16
16
 
17
17
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
18
 
19
19
  spec.metadata["homepage_uri"] = spec.homepage
20
- spec.metadata["source_code_uri"] = "https://github.com/schrockwell/trmnl_preview"
20
+ spec.metadata["source_code_uri"] = "https://github.com/usetrmnl/trmnlp"
21
21
 
22
- # Specify which files should be added to the gem when it is released.
23
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
22
  spec.files = Dir.chdir(__dir__) do
25
- `git ls-files -z`.split("\x0").reject do |f|
26
- (File.expand_path(f) == __FILE__) ||
27
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile Dockerfile])
28
- end
23
+ [
24
+ 'bin/**/*',
25
+ 'lib/**/*',
26
+ 'web/**/*',
27
+ 'CHANGELOG.md',
28
+ 'LICENSE.txt',
29
+ 'README.md',
30
+ 'trmnl_preview.gemspec'
31
+ ].flat_map { |glob| Dir[glob] }
29
32
  end
30
- spec.bindir = "exe"
33
+ spec.bindir = "bin"
31
34
  spec.executables = ["trmnlp"]
32
35
  spec.require_paths = ["lib"]
33
36
 
34
- # Uncomment to register a new dependency of your gem
37
+
38
+ # Web server
35
39
  spec.add_dependency "sinatra", "~> 4.1"
36
40
  spec.add_dependency "rackup", "~> 2.2"
37
41
  spec.add_dependency "puma", "~> 6.5"
42
+ spec.add_dependency "faye-websocket", "~> 0.11.3"
43
+
44
+ # HTML rendering
38
45
  spec.add_dependency "liquid", "~> 5.6"
39
- spec.add_dependency "toml-rb", "~> 3.0"
46
+ spec.add_dependency "activesupport", "~> 8.0"
47
+
48
+ # BMP rendering
49
+ spec.add_dependency "ferrum", "~> 0.16"
50
+ spec.add_dependency 'puppeteer-ruby', '~> 0.45.6'
51
+ spec.add_dependency 'mini_magick', '~> 4.12.0'
52
+
53
+ # Utilities
40
54
  spec.add_dependency "filewatcher", "~> 2.1"
41
- spec.add_dependency "faye-websocket", "~> 0.11.3"
55
+ spec.add_dependency "faraday", "~> 2.1"
56
+ spec.add_dependency "faraday-multipart", "~> 1.1"
57
+ spec.add_dependency "xdg", "~> 9.1"
58
+ spec.add_dependency "rubyzip", "~> 2.3.0"
59
+ spec.add_dependency "thor", "~> 1.3"
42
60
 
43
61
  # For more information and examples about making a new gem, check out our
44
62
  # guide at: https://bundler.io/guides/creating_gem.html