gemstar 1.0.4 → 1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -1
- data/README.md +28 -3
- data/bin/gemstar +5 -1
- data/lib/gemstar/cache_warmer.rb +93 -35
- data/lib/gemstar/change_log.rb +123 -33
- data/lib/gemstar/cli.rb +5 -1
- data/lib/gemstar/commands/diff.rb +197 -31
- data/lib/gemstar/commands/server.rb +93 -10
- data/lib/gemstar/data/importmap_package_metadata.json +22 -0
- data/lib/gemstar/data/ruby_gems_metadata.json +9 -0
- data/lib/gemstar/git_repo.rb +41 -3
- data/lib/gemstar/importmap_file.rb +193 -0
- data/lib/gemstar/npm_metadata.rb +159 -0
- data/lib/gemstar/outputs/html.rb +53 -4
- data/lib/gemstar/outputs/markdown.rb +29 -3
- data/lib/gemstar/package_lock_file.rb +101 -0
- data/lib/gemstar/project.rb +319 -35
- data/lib/gemstar/ruby_gems_metadata.rb +77 -2
- data/lib/gemstar/version.rb +1 -1
- data/lib/gemstar/web/app.rb +377 -67
- data/lib/gemstar/web/templates/app.css +35 -0
- data/lib/gemstar/web/templates/app.js.erb +51 -16
- data/lib/gemstar/web/templates/page.html.erb +2 -1
- data/lib/gemstar.rb +3 -0
- metadata +6 -1
data/lib/gemstar/web/app.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Gemstar
|
|
|
13
13
|
module Web
|
|
14
14
|
class App < Roda
|
|
15
15
|
MISSING_METADATA = Object.new
|
|
16
|
+
CACHE_VERSION = "v6"
|
|
16
17
|
|
|
17
18
|
class << self
|
|
18
19
|
def build(projects:, config_home:, cache_warmer: nil)
|
|
@@ -44,6 +45,16 @@ module Gemstar
|
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
r.get "favicon.svg" do
|
|
49
|
+
response["Content-Type"] = "image/svg+xml; charset=utf-8"
|
|
50
|
+
favicon_svg
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
r.get "favicon.ico" do
|
|
54
|
+
response["Content-Type"] = "image/svg+xml; charset=utf-8"
|
|
55
|
+
favicon_svg
|
|
56
|
+
end
|
|
57
|
+
|
|
47
58
|
r.get "detail" do
|
|
48
59
|
request_cache_key = detail_request_cache_key(r.params)
|
|
49
60
|
request_cache = self.class.opts[:detail_request_cache]
|
|
@@ -62,7 +73,7 @@ module Gemstar
|
|
|
62
73
|
project_index = selected_project_index(r.params["project"])
|
|
63
74
|
project = @projects[project_index]
|
|
64
75
|
response.status = 404
|
|
65
|
-
next "Gemfile not found" unless project
|
|
76
|
+
next "Gemfile not found" unless project&.gemfile?
|
|
66
77
|
|
|
67
78
|
response["Content-Type"] = "text/plain; charset=utf-8"
|
|
68
79
|
File.read(project.gemfile_path)
|
|
@@ -86,23 +97,39 @@ module Gemstar
|
|
|
86
97
|
project = @projects[project_index]
|
|
87
98
|
return nil unless project
|
|
88
99
|
|
|
89
|
-
lockfile_stamp =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
0
|
|
94
|
-
end
|
|
100
|
+
lockfile_stamp = File.file?(project.lockfile_path) ? File.mtime(project.lockfile_path).to_i : 0
|
|
101
|
+
importmap_stamp = File.file?(project.importmap_path) ? File.mtime(project.importmap_path).to_i : 0
|
|
102
|
+
importmap_vendor_stamp = importmap_vendor_mtime(project)
|
|
103
|
+
package_lock_stamp = File.file?(project.package_lock_path) ? File.mtime(project.package_lock_path).to_i : 0
|
|
95
104
|
|
|
96
105
|
[
|
|
106
|
+
CACHE_VERSION,
|
|
97
107
|
project_index,
|
|
98
108
|
params["from"],
|
|
99
109
|
params["to"],
|
|
100
110
|
params["filter"],
|
|
101
|
-
params["
|
|
102
|
-
|
|
111
|
+
params["scope"],
|
|
112
|
+
package_param(params),
|
|
113
|
+
lockfile_stamp,
|
|
114
|
+
importmap_stamp,
|
|
115
|
+
importmap_vendor_stamp,
|
|
116
|
+
package_lock_stamp
|
|
103
117
|
]
|
|
104
118
|
end
|
|
105
119
|
|
|
120
|
+
def importmap_vendor_mtime(project)
|
|
121
|
+
return 0 unless project&.importmap?
|
|
122
|
+
|
|
123
|
+
project.current_importmap.specs.values.filter_map do |target|
|
|
124
|
+
next unless target.to_s.end_with?(".js", ".mjs")
|
|
125
|
+
|
|
126
|
+
path = File.join(project.directory, "vendor", "javascript", target.to_s)
|
|
127
|
+
File.mtime(path).to_i if File.file?(path)
|
|
128
|
+
end.max || 0
|
|
129
|
+
rescue StandardError
|
|
130
|
+
0
|
|
131
|
+
end
|
|
132
|
+
|
|
106
133
|
def page_title
|
|
107
134
|
return "Gemstar" unless @selected_project
|
|
108
135
|
|
|
@@ -117,9 +144,15 @@ module Gemstar
|
|
|
117
144
|
@selected_from_revision_id = selected_from_revision_id(params["from"])
|
|
118
145
|
@selected_to_revision_id = selected_to_revision_id(@selected_to_revision_id)
|
|
119
146
|
@gem_states = @selected_project ? @selected_project.gem_states(from_revision_id: @selected_from_revision_id, to_revision_id: @selected_to_revision_id) : []
|
|
120
|
-
|
|
121
|
-
@
|
|
122
|
-
@
|
|
147
|
+
requested_package_name = package_param(params)
|
|
148
|
+
@requested_gem_name = requested_package_name
|
|
149
|
+
@selected_package_scope = selected_package_scope(params["scope"], requested_package_name)
|
|
150
|
+
@selected_filter = selected_filter(params["filter"], requested_package_name)
|
|
151
|
+
@selected_gem = selected_gem_state(requested_package_name)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def package_param(params)
|
|
155
|
+
params["package"] || params["gem"]
|
|
123
156
|
end
|
|
124
157
|
|
|
125
158
|
def prioritize_selected_gem
|
|
@@ -167,10 +200,24 @@ module Gemstar
|
|
|
167
200
|
@gem_states.any? { |gem| gem[:status] != :unchanged } ? "updated" : "all"
|
|
168
201
|
end
|
|
169
202
|
|
|
203
|
+
def selected_package_scope(raw_scope, raw_gem_name)
|
|
204
|
+
return "all" if @gem_states.empty?
|
|
205
|
+
|
|
206
|
+
available_scopes = available_package_scopes
|
|
207
|
+
default_scope = available_scopes == ["gems"] ? "gems" : "all"
|
|
208
|
+
return raw_scope if raw_scope == "all" || available_scopes.include?(raw_scope)
|
|
209
|
+
|
|
210
|
+
selected_gem = @gem_states.find { |gem| gem[:name] == raw_gem_name }
|
|
211
|
+
return selected_gem[:package_scope] if selected_gem
|
|
212
|
+
|
|
213
|
+
default_scope
|
|
214
|
+
end
|
|
215
|
+
|
|
170
216
|
def selected_gem_state(raw_gem_name)
|
|
171
217
|
return nil if @gem_states.empty?
|
|
172
218
|
|
|
173
|
-
exact_match = @gem_states.find { |gem| gem[:name] == raw_gem_name }
|
|
219
|
+
exact_match = @gem_states.find { |gem| gem[:name] == raw_gem_name && gem_visible_in_selected_scope?(gem) }
|
|
220
|
+
exact_match ||= @gem_states.find { |gem| gem[:name] == raw_gem_name }
|
|
174
221
|
return exact_match if exact_match
|
|
175
222
|
|
|
176
223
|
@gem_states.find { |gem| gem_visible_in_selected_filter?(gem) && gem[:status] != :unchanged } ||
|
|
@@ -180,11 +227,20 @@ module Gemstar
|
|
|
180
227
|
end
|
|
181
228
|
|
|
182
229
|
def gem_visible_in_selected_filter?(gem_state)
|
|
230
|
+
return false unless gem_visible_in_selected_scope?(gem_state)
|
|
183
231
|
return true if @selected_filter != "updated"
|
|
184
232
|
|
|
185
233
|
gem_state[:status] != :unchanged
|
|
186
234
|
end
|
|
187
235
|
|
|
236
|
+
def gem_visible_in_selected_scope?(gem_state)
|
|
237
|
+
@selected_package_scope == "all" || gem_state[:package_scope] == @selected_package_scope
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def available_package_scopes
|
|
241
|
+
@selected_project ? @selected_project.package_scope_options.map { |option| option[:id] } : []
|
|
242
|
+
end
|
|
243
|
+
|
|
188
244
|
def render_shell
|
|
189
245
|
return render_empty_workspace if @projects.empty?
|
|
190
246
|
|
|
@@ -322,7 +378,7 @@ module Gemstar
|
|
|
322
378
|
#{render_toolbar}
|
|
323
379
|
<div class="workspace-body">
|
|
324
380
|
#{render_sidebar}
|
|
325
|
-
#{
|
|
381
|
+
#{render_initial_detail}
|
|
326
382
|
</div>
|
|
327
383
|
</main>
|
|
328
384
|
HTML
|
|
@@ -332,7 +388,7 @@ module Gemstar
|
|
|
332
388
|
<<~HTML
|
|
333
389
|
<section class="toolbar">
|
|
334
390
|
<div class="toolbar-meta">
|
|
335
|
-
<strong>#{@gem_states.count}</strong>
|
|
391
|
+
<strong>#{@gem_states.count}</strong> #{h(@selected_project&.package_collection_label&.downcase || "packages")}
|
|
336
392
|
<span>·</span>
|
|
337
393
|
<strong>#{@gem_states.count { |gem| gem[:status] != :unchanged }}</strong> changes from #{h(selected_from_revision_label)} to #{h(selected_to_revision_label)}
|
|
338
394
|
</div>
|
|
@@ -357,17 +413,18 @@ module Gemstar
|
|
|
357
413
|
<aside class="sidebar" data-sidebar-panel tabindex="0">
|
|
358
414
|
<div class="sidebar-header">
|
|
359
415
|
<div class="sidebar-header-row">
|
|
360
|
-
<h2
|
|
416
|
+
<h2>#{h(@selected_project&.package_collection_label || "Packages")}</h2>
|
|
361
417
|
<div class="list-filters" data-list-filters>
|
|
362
418
|
<button type="button" class="list-filter-button#{' is-active' if @selected_filter == "updated"}" data-filter-button="updated">Updated</button>
|
|
363
419
|
<button type="button" class="list-filter-button#{' is-active' if @selected_filter == "all"}" data-filter-button="all">All</button>
|
|
364
420
|
</div>
|
|
365
421
|
</div>
|
|
422
|
+
#{render_package_scope_filters}
|
|
366
423
|
<input
|
|
367
424
|
type="search"
|
|
368
425
|
class="gem-search"
|
|
369
426
|
data-gem-search
|
|
370
|
-
placeholder="
|
|
427
|
+
placeholder="Search"
|
|
371
428
|
autocomplete="off"
|
|
372
429
|
spellcheck="false"
|
|
373
430
|
>
|
|
@@ -380,7 +437,7 @@ module Gemstar
|
|
|
380
437
|
def render_gem_list
|
|
381
438
|
return <<~HTML if @gem_states.empty?
|
|
382
439
|
<section class="empty-panel">
|
|
383
|
-
<p>No
|
|
440
|
+
<p>No #{h(@selected_project&.package_collection_label&.downcase || "packages")} found in the current lockfile.</p>
|
|
384
441
|
</section>
|
|
385
442
|
HTML
|
|
386
443
|
|
|
@@ -388,19 +445,23 @@ module Gemstar
|
|
|
388
445
|
selected = gem[:name] == @selected_gem[:name] ? " is-selected" : ""
|
|
389
446
|
status_class = " status-#{gem[:status]}"
|
|
390
447
|
updated = gem[:status] != :unchanged
|
|
391
|
-
hidden = @selected_filter == "updated" && !updated && gem[:name] != @requested_gem_name ? ' hidden="hidden"' : ""
|
|
448
|
+
hidden = (!gem_visible_in_selected_scope?(gem) || (@selected_filter == "updated" && !updated && gem[:name] != @requested_gem_name)) ? ' hidden="hidden"' : ""
|
|
392
449
|
<<~HTML
|
|
393
450
|
<a
|
|
394
451
|
class="gem-row#{selected}#{status_class}"
|
|
395
|
-
href="#{project_query(project: @selected_project_index, from: @selected_from_revision_id, to: @selected_to_revision_id, filter: @selected_filter,
|
|
452
|
+
href="#{project_query(project: @selected_project_index, from: @selected_from_revision_id, to: @selected_to_revision_id, filter: @selected_filter, scope: @selected_package_scope, package: gem[:name])}"
|
|
396
453
|
data-gem-link="true"
|
|
397
454
|
data-gem-name="#{h(gem[:name])}"
|
|
398
455
|
data-gem-updated="#{updated}"
|
|
399
|
-
data-
|
|
456
|
+
data-package-scope="#{h(gem[:package_scope])}"
|
|
457
|
+
data-detail-url="#{h(detail_query(project: @selected_project_index, from: @selected_from_revision_id, to: @selected_to_revision_id, filter: @selected_filter, scope: @selected_package_scope, package: gem[:name]))}"
|
|
400
458
|
#{hidden}
|
|
401
459
|
>
|
|
402
460
|
<span class="gem-name-row">
|
|
403
|
-
<span class="gem-name"
|
|
461
|
+
<span class="gem-name-lockup">
|
|
462
|
+
<span class="gem-name">#{h(gem[:name])}</span>
|
|
463
|
+
<span class="package-type-tag">#{h(gem[:package_type_label])}</span>
|
|
464
|
+
</span>
|
|
404
465
|
#{updated ? '<span class="gem-updated-dot" aria-label="Updated"></span>' : ""}
|
|
405
466
|
</span>
|
|
406
467
|
<span class="gem-version">#{h(gem[:version_label])}</span>
|
|
@@ -413,7 +474,7 @@ module Gemstar
|
|
|
413
474
|
#{items}
|
|
414
475
|
</nav>
|
|
415
476
|
<section class="empty-panel gem-list-empty" data-gem-list-empty hidden="hidden">
|
|
416
|
-
<p>No updated
|
|
477
|
+
<p>No updated #{h(@selected_project&.package_collection_label&.downcase || "packages")} in this revision range.</p>
|
|
417
478
|
</section>
|
|
418
479
|
HTML
|
|
419
480
|
end
|
|
@@ -421,25 +482,32 @@ module Gemstar
|
|
|
421
482
|
def render_detail
|
|
422
483
|
return empty_detail_html unless @selected_gem
|
|
423
484
|
|
|
485
|
+
metadata = metadata_for(@selected_gem, refresh_if_missing: true)
|
|
424
486
|
cache_key = [
|
|
487
|
+
CACHE_VERSION,
|
|
425
488
|
@selected_project_index,
|
|
426
489
|
@selected_from_revision_id,
|
|
427
490
|
@selected_to_revision_id,
|
|
428
491
|
@selected_filter,
|
|
492
|
+
@selected_package_scope,
|
|
429
493
|
@selected_gem[:name],
|
|
494
|
+
@selected_gem[:package_scope],
|
|
495
|
+
@selected_gem[:package_source_file],
|
|
430
496
|
@selected_gem[:old_version],
|
|
431
497
|
@selected_gem[:new_version],
|
|
498
|
+
@selected_gem[:raw_old_version],
|
|
499
|
+
@selected_gem[:raw_new_version],
|
|
500
|
+
effective_package_version(@selected_gem, metadata || {}),
|
|
432
501
|
@selected_gem[:status]
|
|
433
502
|
]
|
|
434
503
|
detail_cache = self.class.opts[:detail_html_cache]
|
|
435
504
|
return detail_cache[cache_key] if detail_cache.key?(cache_key)
|
|
436
505
|
|
|
437
|
-
metadata = metadata_for(@selected_gem[:name], refresh_if_missing: true)
|
|
438
506
|
groups = grouped_change_sections(@selected_gem)
|
|
439
507
|
detail_pending = detail_pending?(@selected_gem[:name], metadata, groups)
|
|
440
508
|
|
|
441
509
|
detail_html = <<~HTML
|
|
442
|
-
<section class="detail" data-detail-panel tabindex="0" data-detail-pending="#{detail_pending}" data-detail-url="#{h(detail_query(project: @selected_project_index, from: @selected_from_revision_id, to: @selected_to_revision_id, filter: @selected_filter,
|
|
510
|
+
<section class="detail" data-detail-panel tabindex="0" data-detail-pending="#{detail_pending}" data-detail-url="#{h(detail_query(project: @selected_project_index, from: @selected_from_revision_id, to: @selected_to_revision_id, filter: @selected_filter, scope: @selected_package_scope, package: @selected_gem[:name]))}">
|
|
443
511
|
#{render_detail_hero(metadata)}
|
|
444
512
|
#{render_detail_loading_notice if detail_pending}
|
|
445
513
|
#{render_detail_revision_panel(groups)}
|
|
@@ -450,12 +518,33 @@ module Gemstar
|
|
|
450
518
|
detail_html
|
|
451
519
|
end
|
|
452
520
|
|
|
453
|
-
def
|
|
521
|
+
def render_initial_detail
|
|
522
|
+
return empty_detail_html(loading: true) unless @selected_gem
|
|
523
|
+
|
|
524
|
+
detail_url = detail_query(
|
|
525
|
+
project: @selected_project_index,
|
|
526
|
+
from: @selected_from_revision_id,
|
|
527
|
+
to: @selected_to_revision_id,
|
|
528
|
+
filter: @selected_filter,
|
|
529
|
+
scope: @selected_package_scope,
|
|
530
|
+
package: @selected_gem[:name]
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
<<~HTML
|
|
534
|
+
<section class="detail" data-detail-panel tabindex="0" data-detail-pending="false" data-detail-deferred="true" data-detail-url="#{h(detail_url)}">
|
|
535
|
+
<div class="detail-loading-shell" aria-hidden="true">
|
|
536
|
+
<div class="detail-loading-spinner"></div>
|
|
537
|
+
</div>
|
|
538
|
+
</section>
|
|
539
|
+
HTML
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def empty_detail_html(loading: false)
|
|
454
543
|
<<~HTML
|
|
455
544
|
<section class="detail" data-detail-panel tabindex="0">
|
|
456
545
|
<div class="empty-panel">
|
|
457
|
-
<h2
|
|
458
|
-
<p
|
|
546
|
+
<h2>#{loading ? "Loading details" : "No gem selected"}</h2>
|
|
547
|
+
<p>#{loading ? "Preparing the selected gem details." : "Choose a gem from the list to inspect its current version and changelog revisions."}</p>
|
|
459
548
|
</div>
|
|
460
549
|
</section>
|
|
461
550
|
HTML
|
|
@@ -465,10 +554,10 @@ module Gemstar
|
|
|
465
554
|
description = metadata&.dig("info")
|
|
466
555
|
bundle_origins = Array(@selected_gem[:bundle_origins])
|
|
467
556
|
requirement_names = selected_gem_requirements
|
|
468
|
-
bundled_version =
|
|
557
|
+
bundled_version = detail_bundled_version(metadata)
|
|
469
558
|
added_on = selected_gem_added_on
|
|
470
559
|
title_url = metadata&.dig("homepage_uri")
|
|
471
|
-
title_url =
|
|
560
|
+
title_url = repo_url_for(@selected_gem, metadata: metadata) if title_url.to_s.empty?
|
|
472
561
|
title_markup = if title_url.to_s.empty?
|
|
473
562
|
h(@selected_gem[:name])
|
|
474
563
|
else
|
|
@@ -484,13 +573,24 @@ module Gemstar
|
|
|
484
573
|
</div>
|
|
485
574
|
#{render_detail_links(metadata)}
|
|
486
575
|
</div>
|
|
487
|
-
<
|
|
576
|
+
<div class="detail-subtitle">#{render_detail_subtitle(description)}</div>
|
|
488
577
|
#{render_dependency_details(bundle_origins, requirement_names, added_on)}
|
|
489
578
|
</div>
|
|
490
579
|
</section>
|
|
491
580
|
HTML
|
|
492
581
|
end
|
|
493
582
|
|
|
583
|
+
def render_detail_subtitle(description)
|
|
584
|
+
text = description.to_s.strip
|
|
585
|
+
return "<p>Metadata will appear here when package information is available.</p>" if text.empty?
|
|
586
|
+
|
|
587
|
+
options = { hard_wrap: false }
|
|
588
|
+
options[:input] = "GFM" if defined?(Kramdown::Parser::GFM)
|
|
589
|
+
with_external_links(Kramdown::Document.new(text, options).to_html)
|
|
590
|
+
rescue Kramdown::Error
|
|
591
|
+
"<p>#{h(text)}</p>"
|
|
592
|
+
end
|
|
593
|
+
|
|
494
594
|
def render_added_on(added_on)
|
|
495
595
|
return "" unless added_on
|
|
496
596
|
|
|
@@ -522,12 +622,16 @@ module Gemstar
|
|
|
522
622
|
end
|
|
523
623
|
|
|
524
624
|
def render_detail_links(metadata)
|
|
525
|
-
repo_url =
|
|
625
|
+
repo_url = repo_url_for(@selected_gem, metadata: metadata)
|
|
526
626
|
homepage_url = metadata&.dig("homepage_uri")
|
|
527
|
-
rubygems_url = "https://rubygems.org/gems/#{URI.encode_www_form_component(@selected_gem[:name])}"
|
|
528
627
|
|
|
529
628
|
buttons = []
|
|
530
|
-
|
|
629
|
+
if @selected_gem[:package_scope] == "gems"
|
|
630
|
+
rubygems_url = "https://rubygems.org/gems/#{URI.encode_www_form_component(@selected_gem[:name])}"
|
|
631
|
+
buttons << icon_button("RubyGems", rubygems_url, icon_type: :rubygems)
|
|
632
|
+
elsif homepage_url && !homepage_url.empty? && (!repo_url || homepage_url != repo_url)
|
|
633
|
+
buttons << icon_button("Source", homepage_url, icon_type: :home)
|
|
634
|
+
end
|
|
531
635
|
buttons << icon_button("GitHub", repo_url, icon_type: :github) if repo_url && !repo_url.empty?
|
|
532
636
|
buttons << icon_button("Homepage", homepage_url, icon_type: :home) if homepage_url && !homepage_url.empty?
|
|
533
637
|
|
|
@@ -575,6 +679,8 @@ module Gemstar
|
|
|
575
679
|
end
|
|
576
680
|
|
|
577
681
|
def selected_gem_requirements
|
|
682
|
+
return [] unless @selected_gem[:package_scope] == "gems"
|
|
683
|
+
|
|
578
684
|
lockfile = if @selected_gem[:new_version]
|
|
579
685
|
@selected_project&.lockfile_for_revision(@selected_to_revision_id)
|
|
580
686
|
else
|
|
@@ -586,7 +692,12 @@ module Gemstar
|
|
|
586
692
|
|
|
587
693
|
def selected_gem_added_on
|
|
588
694
|
revision_id = @selected_gem[:new_version] ? @selected_to_revision_id : @selected_from_revision_id
|
|
589
|
-
@selected_project&.
|
|
695
|
+
@selected_project&.package_added_on(
|
|
696
|
+
@selected_gem[:name],
|
|
697
|
+
package_scope: @selected_gem[:package_scope],
|
|
698
|
+
source_file: @selected_gem[:package_source_file],
|
|
699
|
+
revision_id: revision_id
|
|
700
|
+
)
|
|
590
701
|
end
|
|
591
702
|
|
|
592
703
|
def selected_gem_platform_items
|
|
@@ -617,6 +728,31 @@ module Gemstar
|
|
|
617
728
|
when :rubygems
|
|
618
729
|
remote = source[:remote]
|
|
619
730
|
[remote.to_s.empty? ? "RubyGems" : "RubyGems (#{h(remote)})"]
|
|
731
|
+
when :importmap
|
|
732
|
+
remote = source[:remote]
|
|
733
|
+
package_name = source[:package_name]
|
|
734
|
+
package_version = source[:package_version]
|
|
735
|
+
label =
|
|
736
|
+
if package_name && package_version
|
|
737
|
+
"Importmap (#{h(package_name)} @ #{h(package_version)})"
|
|
738
|
+
elsif package_name
|
|
739
|
+
"Importmap (#{h(package_name)})"
|
|
740
|
+
elsif remote.to_s.empty?
|
|
741
|
+
"Importmap"
|
|
742
|
+
else
|
|
743
|
+
"Importmap (#{h(remote)})"
|
|
744
|
+
end
|
|
745
|
+
[label]
|
|
746
|
+
when :npm
|
|
747
|
+
remote = source[:remote]
|
|
748
|
+
registry_url = source[:registry_url]
|
|
749
|
+
if remote.to_s.empty? && registry_url.to_s.empty?
|
|
750
|
+
["npm"]
|
|
751
|
+
elsif remote.to_s.empty?
|
|
752
|
+
["npm (#{h(registry_url)})"]
|
|
753
|
+
else
|
|
754
|
+
["npm (#{h(remote)})"]
|
|
755
|
+
end
|
|
620
756
|
else
|
|
621
757
|
[]
|
|
622
758
|
end
|
|
@@ -645,7 +781,8 @@ module Gemstar
|
|
|
645
781
|
from: @selected_from_revision_id,
|
|
646
782
|
to: @selected_to_revision_id,
|
|
647
783
|
filter: @selected_filter,
|
|
648
|
-
|
|
784
|
+
scope: @selected_package_scope,
|
|
785
|
+
package: name
|
|
649
786
|
)
|
|
650
787
|
|
|
651
788
|
%(<a href="#{h(href)}" data-gem-link-inline="true">#{h(name)}</a>)
|
|
@@ -664,7 +801,7 @@ module Gemstar
|
|
|
664
801
|
def render_detail_loading_notice
|
|
665
802
|
<<~HTML
|
|
666
803
|
<section class="empty-panel">
|
|
667
|
-
<p>Loading
|
|
804
|
+
<p>Loading package metadata and changelog in the background...</p>
|
|
668
805
|
</section>
|
|
669
806
|
HTML
|
|
670
807
|
end
|
|
@@ -732,17 +869,31 @@ module Gemstar
|
|
|
732
869
|
end
|
|
733
870
|
|
|
734
871
|
def change_sections(gem_state)
|
|
735
|
-
|
|
872
|
+
metadata_hash = metadata_for(gem_state) || {}
|
|
873
|
+
current_version = effective_package_version(gem_state, metadata_hash)
|
|
874
|
+
cache_key = [
|
|
875
|
+
CACHE_VERSION,
|
|
876
|
+
gem_state[:name],
|
|
877
|
+
gem_state[:package_scope],
|
|
878
|
+
gem_state[:package_source_file],
|
|
879
|
+
gem_state[:old_version],
|
|
880
|
+
gem_state[:new_version],
|
|
881
|
+
gem_state[:raw_old_version],
|
|
882
|
+
gem_state[:raw_new_version],
|
|
883
|
+
current_version,
|
|
884
|
+
gem_state[:status]
|
|
885
|
+
]
|
|
736
886
|
change_sections_cache = self.class.opts[:change_sections_cache]
|
|
737
887
|
return change_sections_cache[cache_key] if change_sections_cache.key?(cache_key)
|
|
738
888
|
|
|
739
|
-
return [] if gem_state[:new_version].nil? &&
|
|
740
|
-
|
|
741
|
-
|
|
889
|
+
return [] if gem_state[:new_version].nil? &&
|
|
890
|
+
gem_state[:old_version].nil? &&
|
|
891
|
+
(current_version.nil? || current_version.to_s.empty?)
|
|
892
|
+
metadata = metadata_adapter_for(gem_state)
|
|
893
|
+
return change_sections_cache[cache_key] = [] unless metadata
|
|
742
894
|
sections = resolved_sections(metadata, gem_state)
|
|
743
895
|
return change_sections_cache[cache_key] = [] if sections.nil? || sections.empty?
|
|
744
896
|
|
|
745
|
-
current_version = gem_state[:new_version] || gem_state[:old_version]
|
|
746
897
|
previous_version = gem_state[:old_version]
|
|
747
898
|
|
|
748
899
|
rendered_sections = sections.keys.filter_map do |version|
|
|
@@ -769,18 +920,35 @@ module Gemstar
|
|
|
769
920
|
cached_sections = changelog.sections(cache_only: true) || {}
|
|
770
921
|
return cached_sections unless selected_gem_requires_refresh?(gem_state, cached_sections)
|
|
771
922
|
|
|
772
|
-
@metadata_cache.delete(gem_state[:name])
|
|
923
|
+
@metadata_cache.delete([gem_state[:package_scope], gem_state[:name]])
|
|
773
924
|
metadata.meta(cache_only: false, force_refresh: true)
|
|
774
925
|
metadata.repo_uri(cache_only: false, force_refresh: true)
|
|
775
|
-
|
|
926
|
+
refreshed_sections = metadata.changelog_sections(
|
|
927
|
+
versions: relevant_package_versions(gem_state, metadata),
|
|
928
|
+
cache_only: false,
|
|
929
|
+
force_refresh: true
|
|
930
|
+
)
|
|
931
|
+
cached_sections.merge(refreshed_sections || {})
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
def relevant_package_versions(gem_state, metadata)
|
|
935
|
+
metadata_hash = metadata_for(gem_state) || {}
|
|
936
|
+
[
|
|
937
|
+
gem_state[:old_version],
|
|
938
|
+
gem_state[:new_version],
|
|
939
|
+
gem_state[:raw_old_version],
|
|
940
|
+
gem_state[:raw_new_version],
|
|
941
|
+
gem_state.dig(:source, :package_version),
|
|
942
|
+
metadata_hash["version"]
|
|
943
|
+
].compact
|
|
776
944
|
end
|
|
777
945
|
|
|
778
946
|
def selected_gem_requires_refresh?(gem_state, cached_sections)
|
|
779
947
|
return false unless @selected_gem && gem_state[:name] == @selected_gem[:name]
|
|
780
948
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
949
|
+
metadata = metadata_for(gem_state) || {}
|
|
950
|
+
bundled_version = effective_package_version(gem_state, metadata)
|
|
951
|
+
return false if bundled_version.nil? || bundled_version.to_s.empty?
|
|
784
952
|
has_upstream_release_source =
|
|
785
953
|
!metadata["changelog_uri"].to_s.empty? ||
|
|
786
954
|
!metadata["source_code_uri"].to_s.empty? ||
|
|
@@ -801,6 +969,7 @@ module Gemstar
|
|
|
801
969
|
def section_kind(version, previous_version, current_version, status)
|
|
802
970
|
return :future if compare_versions(version, current_version) == 1
|
|
803
971
|
return :current if status == :added && compare_versions(version, current_version) <= 0
|
|
972
|
+
return :current if previous_version.nil? && current_version && compare_versions(version, current_version) == 0
|
|
804
973
|
|
|
805
974
|
lower_bound = previous_version || current_version
|
|
806
975
|
if compare_versions(version, lower_bound) == 1 && compare_versions(version, current_version) <= 0
|
|
@@ -836,7 +1005,7 @@ module Gemstar
|
|
|
836
1005
|
return { title: heading_version.to_s, html: "<p>No changelog text available.</p>" } if text.strip.empty?
|
|
837
1006
|
|
|
838
1007
|
if heading_version
|
|
839
|
-
text = text
|
|
1008
|
+
text = strip_leading_version_heading(text, heading_version)
|
|
840
1009
|
end
|
|
841
1010
|
|
|
842
1011
|
options = { hard_wrap: false }
|
|
@@ -863,29 +1032,135 @@ module Gemstar
|
|
|
863
1032
|
{ title: title, html: fragment.to_html }
|
|
864
1033
|
end
|
|
865
1034
|
|
|
1035
|
+
def strip_leading_version_heading(text, heading_version)
|
|
1036
|
+
stripped = text.sub(/\A\s*#+\s*v?#{Regexp.escape(heading_version)}\s*\n+/i, "")
|
|
1037
|
+
return strip_leading_hash_separator(stripped) unless stripped == text
|
|
1038
|
+
|
|
1039
|
+
lines = text.lines
|
|
1040
|
+
return text if lines.empty?
|
|
1041
|
+
|
|
1042
|
+
first_line = lines.first.to_s
|
|
1043
|
+
heading_like =
|
|
1044
|
+
first_line.match?(/\A\s*v?#{Regexp.escape(heading_version)}\b/i) ||
|
|
1045
|
+
first_line.match?(/\A\s*[\[(]?v?#{Regexp.escape(heading_version)}\b/i)
|
|
1046
|
+
|
|
1047
|
+
return text unless heading_like
|
|
1048
|
+
|
|
1049
|
+
remaining = lines.drop(1)
|
|
1050
|
+
remaining.shift while remaining.first&.strip&.empty?
|
|
1051
|
+
strip_leading_hash_separator(remaining.join)
|
|
1052
|
+
end
|
|
1053
|
+
|
|
1054
|
+
def strip_leading_hash_separator(text)
|
|
1055
|
+
text.sub(/\A\s*#{Regexp.escape("#")}{4,}\s*\n+/, "")
|
|
1056
|
+
end
|
|
1057
|
+
|
|
866
1058
|
def compare_versions(left, right)
|
|
867
1059
|
Gem::Version.new(left.to_s.gsub(/-[\w\-]+$/, "")) <=> Gem::Version.new(right.to_s.gsub(/-[\w\-]+$/, ""))
|
|
868
1060
|
rescue ArgumentError
|
|
869
1061
|
left.to_s <=> right.to_s
|
|
870
1062
|
end
|
|
871
1063
|
|
|
872
|
-
def metadata_for(
|
|
873
|
-
|
|
1064
|
+
def metadata_for(package_state_or_name, refresh_if_missing: false)
|
|
1065
|
+
package_state = package_state_or_name.is_a?(Hash) ? package_state_or_name : { name: package_state_or_name, package_scope: "gems" }
|
|
1066
|
+
gem_name = package_state[:name]
|
|
1067
|
+
cache_key = [package_state[:package_scope], gem_name]
|
|
1068
|
+
cached = @metadata_cache[cache_key]
|
|
874
1069
|
return nil if cached.equal?(MISSING_METADATA)
|
|
875
1070
|
return cached if cached
|
|
876
1071
|
|
|
1072
|
+
if package_state[:package_scope] != "gems"
|
|
1073
|
+
metadata = local_package_metadata(package_state)
|
|
1074
|
+
adapter = metadata_adapter_for(package_state)
|
|
1075
|
+
remote_metadata = adapter&.meta(cache_only: true)
|
|
1076
|
+
remote_metadata = adapter&.meta(cache_only: false, force_refresh: true) if remote_metadata.nil? && refresh_if_missing
|
|
1077
|
+
metadata = metadata.compact.merge(remote_metadata || {})
|
|
1078
|
+
@metadata_cache[cache_key] = metadata || MISSING_METADATA
|
|
1079
|
+
return metadata
|
|
1080
|
+
end
|
|
1081
|
+
|
|
877
1082
|
metadata = Gemstar::RubyGemsMetadata.new(gem_name).meta(cache_only: true)
|
|
878
1083
|
if metadata.nil? && refresh_if_missing
|
|
879
1084
|
metadata = Gemstar::RubyGemsMetadata.new(gem_name).meta(cache_only: false, force_refresh: true)
|
|
880
1085
|
end
|
|
881
1086
|
|
|
882
|
-
@metadata_cache[
|
|
1087
|
+
@metadata_cache[cache_key] = metadata || MISSING_METADATA
|
|
883
1088
|
metadata
|
|
884
1089
|
rescue StandardError
|
|
885
|
-
@metadata_cache[
|
|
1090
|
+
@metadata_cache[cache_key] = MISSING_METADATA
|
|
886
1091
|
nil
|
|
887
1092
|
end
|
|
888
1093
|
|
|
1094
|
+
def local_package_metadata(package_state)
|
|
1095
|
+
source = package_state[:source] || {}
|
|
1096
|
+
remote = source[:remote].to_s
|
|
1097
|
+
repo_url = source[:repo_url].to_s
|
|
1098
|
+
package_name = source[:package_name].to_s
|
|
1099
|
+
package_version = source[:package_version].to_s
|
|
1100
|
+
registry_url = source[:registry_url].to_s
|
|
1101
|
+
provider_gem = source[:provider_gem].to_s
|
|
1102
|
+
package_name = package_state[:name].to_s if package_name.empty? && source[:type] == :npm
|
|
1103
|
+
package_version = (package_state[:new_version] || package_state[:old_version]).to_s if package_version.empty? && source[:type] == :npm
|
|
1104
|
+
registry_url = "https://www.npmjs.com/package/#{package_name}" if registry_url.empty? && !package_name.empty?
|
|
1105
|
+
info = if package_name.empty?
|
|
1106
|
+
"JavaScript package pinned in config/importmap.rb"
|
|
1107
|
+
elsif !provider_gem.empty? && !package_version.empty?
|
|
1108
|
+
"JavaScript package `#{package_name}` provided by the `#{provider_gem}` gem at version `#{package_version}`"
|
|
1109
|
+
elsif !provider_gem.empty?
|
|
1110
|
+
"JavaScript package `#{package_name}` provided by the `#{provider_gem}` gem"
|
|
1111
|
+
elsif source[:type] == :npm && !package_version.empty?
|
|
1112
|
+
"JavaScript package `#{package_name}` locked to `#{package_version}` in package-lock.json"
|
|
1113
|
+
elsif package_version.empty?
|
|
1114
|
+
"JavaScript package `#{package_name}` pinned in config/importmap.rb"
|
|
1115
|
+
else
|
|
1116
|
+
"JavaScript package `#{package_name}` pinned to `#{package_version}` in config/importmap.rb"
|
|
1117
|
+
end
|
|
1118
|
+
|
|
1119
|
+
{
|
|
1120
|
+
"info" => info,
|
|
1121
|
+
"project_uri" => registry_url.empty? ? nil : registry_url,
|
|
1122
|
+
"homepage_uri" => absolute_url?(remote) ? remote : nil,
|
|
1123
|
+
"source_code_uri" => repo_url.empty? ? nil : repo_url
|
|
1124
|
+
}
|
|
1125
|
+
end
|
|
1126
|
+
|
|
1127
|
+
def repo_url_for(package_state, metadata: nil)
|
|
1128
|
+
return nil unless package_state
|
|
1129
|
+
if package_state[:package_scope] != "gems"
|
|
1130
|
+
url = package_state.dig(:source, :repo_url) || metadata&.dig("source_code_uri")
|
|
1131
|
+
return normalize_repo_url(url)
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
normalize_repo_url(Gemstar::RubyGemsMetadata.new(package_state[:name]).repo_uri(cache_only: true))
|
|
1135
|
+
end
|
|
1136
|
+
|
|
1137
|
+
def normalize_repo_url(url)
|
|
1138
|
+
value = url.to_s
|
|
1139
|
+
return nil if value.empty?
|
|
1140
|
+
|
|
1141
|
+
value = value.sub(/\Agit\+/, "")
|
|
1142
|
+
value = value.sub(/\Agit:\/\//, "https://")
|
|
1143
|
+
value = value.gsub(/\.git\z/, "")
|
|
1144
|
+
value
|
|
1145
|
+
end
|
|
1146
|
+
|
|
1147
|
+
def metadata_adapter_for(package_state)
|
|
1148
|
+
return Gemstar::RubyGemsMetadata.new(package_state[:name]) if package_state[:package_scope] == "gems"
|
|
1149
|
+
return nil unless package_state[:package_scope] == "js"
|
|
1150
|
+
|
|
1151
|
+
provider_gem = package_state.dig(:source, :provider_gem)
|
|
1152
|
+
return Gemstar::RubyGemsMetadata.new(provider_gem) unless provider_gem.to_s.empty?
|
|
1153
|
+
|
|
1154
|
+
package_name = package_state.dig(:source, :package_name) || package_state[:name]
|
|
1155
|
+
return nil if package_name.to_s.empty?
|
|
1156
|
+
|
|
1157
|
+
Gemstar::NpmMetadata.new(package_name)
|
|
1158
|
+
end
|
|
1159
|
+
|
|
1160
|
+
def absolute_url?(value)
|
|
1161
|
+
value.to_s.match?(%r{\Ahttps?://}i)
|
|
1162
|
+
end
|
|
1163
|
+
|
|
889
1164
|
def detail_pending?(gem_name, metadata, groups)
|
|
890
1165
|
false
|
|
891
1166
|
end
|
|
@@ -940,7 +1215,7 @@ module Gemstar
|
|
|
940
1215
|
end
|
|
941
1216
|
|
|
942
1217
|
def revision_card_links(section)
|
|
943
|
-
repo_url =
|
|
1218
|
+
repo_url = repo_url_for(@selected_gem)
|
|
944
1219
|
return [] if repo_url.to_s.empty?
|
|
945
1220
|
|
|
946
1221
|
links = []
|
|
@@ -953,25 +1228,33 @@ module Gemstar
|
|
|
953
1228
|
end
|
|
954
1229
|
|
|
955
1230
|
def fallback_current_section(gem_state, previous_sections, latest_sections)
|
|
956
|
-
|
|
1231
|
+
metadata = metadata_for(gem_state) || {}
|
|
1232
|
+
version = effective_package_version(gem_state, metadata)
|
|
957
1233
|
return nil if version.nil?
|
|
958
1234
|
return nil if previous_sections.any? { |section| section[:version] == version }
|
|
959
1235
|
return nil if latest_sections.any? { |section| section[:version] == version }
|
|
960
1236
|
|
|
961
|
-
|
|
962
|
-
repo_url = Gemstar::RubyGemsMetadata.new(gem_state[:name]).repo_uri(cache_only: true)
|
|
1237
|
+
repo_url = repo_url_for(gem_state, metadata: metadata)
|
|
963
1238
|
fallback_url =
|
|
964
1239
|
if !repo_url.to_s.empty?
|
|
965
1240
|
repo_url
|
|
966
1241
|
elsif metadata["project_uri"]
|
|
967
1242
|
metadata["project_uri"]
|
|
1243
|
+
elsif metadata["source_code_uri"]
|
|
1244
|
+
metadata["source_code_uri"]
|
|
1245
|
+
elsif metadata["homepage_uri"]
|
|
1246
|
+
metadata["homepage_uri"]
|
|
968
1247
|
else
|
|
969
1248
|
metadata["documentation_uri"]
|
|
970
1249
|
end
|
|
971
1250
|
fallback_label = if repo_url.to_s.empty?
|
|
972
|
-
|
|
1251
|
+
if gem_state[:package_scope] == "gems"
|
|
1252
|
+
metadata["project_uri"] ? "the RubyGems page" : "the gem documentation"
|
|
1253
|
+
else
|
|
1254
|
+
fallback_url == metadata["documentation_uri"] ? "the package documentation" : "the package source"
|
|
1255
|
+
end
|
|
973
1256
|
else
|
|
974
|
-
"the gem repository"
|
|
1257
|
+
gem_state[:package_scope] == "gems" ? "the gem repository" : "the package repository"
|
|
975
1258
|
end
|
|
976
1259
|
fallback_link = if fallback_url.to_s.empty?
|
|
977
1260
|
fallback_label
|
|
@@ -995,6 +1278,16 @@ module Gemstar
|
|
|
995
1278
|
nil
|
|
996
1279
|
end
|
|
997
1280
|
|
|
1281
|
+
def detail_bundled_version(metadata)
|
|
1282
|
+
effective_package_version(@selected_gem, metadata || {})
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
def effective_package_version(package_state, metadata)
|
|
1286
|
+
package_state[:new_version] ||
|
|
1287
|
+
package_state[:old_version] ||
|
|
1288
|
+
(package_state[:package_scope] == "js" ? metadata["version"] : nil)
|
|
1289
|
+
end
|
|
1290
|
+
|
|
998
1291
|
def github_compare_url(repo_url, previous_version, current_version)
|
|
999
1292
|
return nil unless repo_url.include?("github.com")
|
|
1000
1293
|
return nil if previous_version.nil? || current_version.nil?
|
|
@@ -1024,17 +1317,18 @@ module Gemstar
|
|
|
1024
1317
|
html
|
|
1025
1318
|
end
|
|
1026
1319
|
|
|
1027
|
-
def detail_query(project:, from:, to:, filter:,
|
|
1028
|
-
"/detail?#{URI.encode_www_form(project: project, from: from, to: to, filter: filter,
|
|
1320
|
+
def detail_query(project:, from:, to:, filter:, scope:, package:)
|
|
1321
|
+
"/detail?#{URI.encode_www_form(project: project, from: from, to: to, filter: filter, scope: scope, package: package)}"
|
|
1029
1322
|
end
|
|
1030
1323
|
|
|
1031
|
-
def project_query(project:, from:, to:, filter:,
|
|
1324
|
+
def project_query(project:, from:, to:, filter:, scope:, package:)
|
|
1032
1325
|
params = {
|
|
1033
1326
|
project: project,
|
|
1034
1327
|
from: from,
|
|
1035
1328
|
to: to,
|
|
1036
1329
|
filter: filter,
|
|
1037
|
-
|
|
1330
|
+
scope: scope,
|
|
1331
|
+
package: package
|
|
1038
1332
|
}.compact
|
|
1039
1333
|
|
|
1040
1334
|
"/?#{URI.encode_www_form(params)}"
|
|
@@ -1044,21 +1338,18 @@ module Gemstar
|
|
|
1044
1338
|
render_template(
|
|
1045
1339
|
"page.html.erb",
|
|
1046
1340
|
title: h(title),
|
|
1047
|
-
favicon_data_uri: favicon_data_uri,
|
|
1048
1341
|
styles_css: template_source("app.css"),
|
|
1049
1342
|
body_html: yield
|
|
1050
1343
|
)
|
|
1051
1344
|
end
|
|
1052
1345
|
|
|
1053
|
-
def
|
|
1054
|
-
|
|
1346
|
+
def favicon_svg
|
|
1347
|
+
<<~SVG
|
|
1055
1348
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
1056
1349
|
<rect width="64" height="64" rx="14" fill="#b44d25"/>
|
|
1057
1350
|
<text x="32" y="44" text-anchor="middle" font-family="Avenir Next, Helvetica Neue, Segoe UI, sans-serif" font-size="34" font-weight="700" fill="#ffffff">G</text>
|
|
1058
1351
|
</svg>
|
|
1059
1352
|
SVG
|
|
1060
|
-
|
|
1061
|
-
"data:image/svg+xml,#{URI.encode_www_form_component(svg)}"
|
|
1062
1353
|
end
|
|
1063
1354
|
|
|
1064
1355
|
def render_behavior_script
|
|
@@ -1066,6 +1357,7 @@ module Gemstar
|
|
|
1066
1357
|
"app.js.erb",
|
|
1067
1358
|
empty_detail_html_json: empty_detail_html.dump,
|
|
1068
1359
|
selected_filter_json: @selected_filter.dump,
|
|
1360
|
+
selected_package_scope_json: @selected_package_scope.dump,
|
|
1069
1361
|
selected_project_index: @selected_project_index || 0
|
|
1070
1362
|
)
|
|
1071
1363
|
|
|
@@ -1091,6 +1383,24 @@ module Gemstar
|
|
|
1091
1383
|
def h(value)
|
|
1092
1384
|
CGI.escapeHTML(value.to_s)
|
|
1093
1385
|
end
|
|
1386
|
+
|
|
1387
|
+
def render_package_scope_filters
|
|
1388
|
+
options = @selected_project&.package_scope_options || []
|
|
1389
|
+
return "" if options.size <= 1
|
|
1390
|
+
|
|
1391
|
+
buttons = []
|
|
1392
|
+
buttons << %(<button type="button" class="list-filter-button#{' is-active' if @selected_package_scope == "all"}" data-ecosystem-button="all">All</button>)
|
|
1393
|
+
|
|
1394
|
+
buttons.concat(options.map do |option|
|
|
1395
|
+
%(<button type="button" class="list-filter-button#{' is-active' if @selected_package_scope == option[:id]}" data-ecosystem-button="#{h(option[:id])}">#{h(option[:label])}</button>)
|
|
1396
|
+
end)
|
|
1397
|
+
|
|
1398
|
+
<<~HTML
|
|
1399
|
+
<div class="list-filters list-filters-secondary" data-ecosystem-filters>
|
|
1400
|
+
#{buttons.join}
|
|
1401
|
+
</div>
|
|
1402
|
+
HTML
|
|
1403
|
+
end
|
|
1094
1404
|
end
|
|
1095
1405
|
end
|
|
1096
1406
|
end
|