ariadne_view_components 0.0.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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +68 -0
  4. data/app/assets/javascripts/ariadne_view_components.js +2 -0
  5. data/app/assets/javascripts/ariadne_view_components.js.map +1 -0
  6. data/app/assets/stylesheets/application.tailwind.css +3 -0
  7. data/app/components/ariadne/ariadne.ts +14 -0
  8. data/app/components/ariadne/base_button.rb +60 -0
  9. data/app/components/ariadne/base_component.rb +155 -0
  10. data/app/components/ariadne/button_component.html.erb +4 -0
  11. data/app/components/ariadne/button_component.rb +158 -0
  12. data/app/components/ariadne/clipboard_copy_component.html.erb +8 -0
  13. data/app/components/ariadne/clipboard_copy_component.rb +50 -0
  14. data/app/components/ariadne/clipboard_copy_component.ts +19 -0
  15. data/app/components/ariadne/component.rb +123 -0
  16. data/app/components/ariadne/content.rb +12 -0
  17. data/app/components/ariadne/counter_component.rb +100 -0
  18. data/app/components/ariadne/flash_component.html.erb +31 -0
  19. data/app/components/ariadne/flash_component.rb +125 -0
  20. data/app/components/ariadne/heading_component.rb +49 -0
  21. data/app/components/ariadne/heroicon_component.html.erb +7 -0
  22. data/app/components/ariadne/heroicon_component.rb +116 -0
  23. data/app/components/ariadne/image_component.rb +51 -0
  24. data/app/components/ariadne/text.rb +25 -0
  25. data/app/components/ariadne/tooltip_component.rb +105 -0
  26. data/app/lib/ariadne/audited/dsl.rb +32 -0
  27. data/app/lib/ariadne/class_name_helper.rb +22 -0
  28. data/app/lib/ariadne/fetch_or_fallback_helper.rb +100 -0
  29. data/app/lib/ariadne/icon_helper.rb +47 -0
  30. data/app/lib/ariadne/join_style_arguments_helper.rb +14 -0
  31. data/app/lib/ariadne/logger_helper.rb +23 -0
  32. data/app/lib/ariadne/status/dsl.rb +41 -0
  33. data/app/lib/ariadne/tab_nav_helper.rb +35 -0
  34. data/app/lib/ariadne/tabbed_component_helper.rb +39 -0
  35. data/app/lib/ariadne/test_selector_helper.rb +20 -0
  36. data/app/lib/ariadne/underline_nav_helper.rb +44 -0
  37. data/app/lib/ariadne/view_helper.rb +22 -0
  38. data/lib/ariadne/classify/utilities.rb +199 -0
  39. data/lib/ariadne/classify/utilities.yml +1817 -0
  40. data/lib/ariadne/classify/validation.rb +18 -0
  41. data/lib/ariadne/classify.rb +210 -0
  42. data/lib/ariadne/view_components/constants.rb +53 -0
  43. data/lib/ariadne/view_components/engine.rb +30 -0
  44. data/lib/ariadne/view_components/linters.rb +3 -0
  45. data/lib/ariadne/view_components/statuses.rb +14 -0
  46. data/lib/ariadne/view_components/version.rb +7 -0
  47. data/lib/ariadne/view_components.rb +59 -0
  48. data/lib/rubocop/config/default.yml +14 -0
  49. data/lib/rubocop/cop/ariadne/ariadne_heroicon.rb +252 -0
  50. data/lib/rubocop/cop/ariadne/base_cop.rb +26 -0
  51. data/lib/rubocop/cop/ariadne/component_name_migration.rb +35 -0
  52. data/lib/rubocop/cop/ariadne/no_tag_memoize.rb +43 -0
  53. data/lib/rubocop/cop/ariadne/system_argument_instead_of_class.rb +57 -0
  54. data/lib/rubocop/cop/ariadne.rb +3 -0
  55. data/lib/tasks/ariadne_view_components.rake +47 -0
  56. data/lib/tasks/coverage.rake +19 -0
  57. data/lib/tasks/custom_utilities.yml +310 -0
  58. data/lib/tasks/docs.rake +525 -0
  59. data/lib/tasks/helpers/ast_processor.rb +44 -0
  60. data/lib/tasks/helpers/ast_traverser.rb +77 -0
  61. data/lib/tasks/static.rake +15 -0
  62. data/lib/tasks/tailwind.rake +31 -0
  63. data/lib/tasks/utilities.rake +121 -0
  64. data/lib/yard/docs_helper.rb +83 -0
  65. data/lib/yard/renders_many_handler.rb +19 -0
  66. data/lib/yard/renders_one_handler.rb +19 -0
  67. data/static/arguments.yml +251 -0
  68. data/static/assets/view-components.svg +18 -0
  69. data/static/audited_at.json +14 -0
  70. data/static/classes.yml +89 -0
  71. data/static/constants.json +243 -0
  72. data/static/statuses.json +14 -0
  73. data/static/tailwindcss.yml +727 -0
  74. metadata +193 -0
@@ -0,0 +1,525 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/inflector"
4
+ require "fileutils"
5
+
6
+ namespace :docs do
7
+ desc "Rebuilds docs on change; run via the Procfile"
8
+ task :livereload do
9
+ require "listen"
10
+
11
+ Rake::Task["docs:build"].execute
12
+
13
+ puts "Listening for changes to documentation..."
14
+
15
+ listener = Listen.to("app") do |modified, added, removed|
16
+ puts "modified absolute path: #{modified}"
17
+ puts "added absolute path: #{added}"
18
+ puts "removed absolute path: #{removed}"
19
+
20
+ unless modified.length.zero?
21
+ changed = modified.dup.uniq
22
+ while (path = changed.shift)
23
+ puts "Reloading #{path}"
24
+ # reload constants (in case they changed)
25
+ load(path)
26
+ end
27
+ end
28
+
29
+ Rake::Task["docs:build"].execute
30
+ end
31
+ listener.start # not blocking
32
+ sleep
33
+ end
34
+
35
+ desc "Generate the documentation."
36
+ task :build do
37
+ registry = generate_yard_registry
38
+
39
+ puts "Converting YARD documentation to Markdown files."
40
+
41
+ # Rails controller for rendering arbitrary ERB
42
+ view_context = ApplicationController.new.tap { |c| c.request = ActionDispatch::TestRequest.create }.view_context
43
+ components = [
44
+ # FIXME: these need to be filled in
45
+ Ariadne::BaseButton,
46
+ Ariadne::ButtonComponent,
47
+ # Ariadne::Alpha::Layout,
48
+ # Ariadne::HellipButton,
49
+ # Ariadne::BorderBox::Header,
50
+ # Ariadne::IconButton,
51
+ # Ariadne::Beta::AutoComplete,
52
+ # Ariadne::Beta::AutoComplete::Item,
53
+ # Ariadne::Beta::Avatar,
54
+ # Ariadne::Beta::AvatarStack,
55
+ # Ariadne::Beta::Blankslate,
56
+ # Ariadne::BorderBoxComponent,
57
+ # Ariadne::BoxComponent,
58
+ # Ariadne::Beta::Breadcrumbs,
59
+ # Ariadne::ButtonGroup,
60
+ # Ariadne::Alpha::ButtonMarketing,
61
+ Ariadne::ClipboardCopyComponent,
62
+ # Ariadne::CloseButton,
63
+ Ariadne::CounterComponent,
64
+ # Ariadne::DetailsComponent,
65
+ # Ariadne::Dropdown,
66
+ # Ariadne::DropdownMenuComponent,
67
+ Ariadne::FlashComponent,
68
+ # Ariadne::FlexComponent,
69
+ # Ariadne::FlexItemComponent,
70
+ Ariadne::HeadingComponent,
71
+ # Ariadne::HiddenTextExpander,
72
+ # Ariadne::LabelComponent,
73
+ # Ariadne::LayoutComponent,
74
+ # Ariadne::LinkComponent,
75
+ # Ariadne::Markdown,
76
+ # Ariadne::MenuComponent,
77
+ # Ariadne::Navigation::TabComponent,
78
+ Ariadne::HeroiconComponent,
79
+ # Ariadne::LocalTime,
80
+ Ariadne::ImageComponent,
81
+ # Ariadne::ImageCrop,
82
+ # Ariadne::PopoverComponent,
83
+ # Ariadne::ProgressBarComponent,
84
+ # Ariadne::StateComponent,
85
+ # Ariadne::SpinnerComponent,
86
+ # Ariadne::SubheadComponent,
87
+ # Ariadne::TabContainerComponent,
88
+ Ariadne::Text,
89
+ # Ariadne::TimeAgoComponent,
90
+ # Ariadne::TimelineItemComponent,
91
+ # Ariadne::TooltipComponent,
92
+ # Ariadne::Truncate,
93
+ # Ariadne::Beta::Truncate,
94
+ # Ariadne::Alpha::UnderlineNav,
95
+ # Ariadne::Alpha::UnderlinePanels,
96
+ # Ariadne::Alpha::TabNav,
97
+ # Ariadne::Alpha::TabPanels,
98
+ Ariadne::TooltipComponent,
99
+ ]
100
+
101
+ js_components = [
102
+ # Ariadne::Dropdown,
103
+ # Ariadne::LocalTime,
104
+ # Ariadne::ImageCrop,
105
+ # Ariadne::Beta::AutoComplete,
106
+ Ariadne::ClipboardCopyComponent,
107
+ # Ariadne::TabContainerComponent,
108
+ # Ariadne::TimeAgoComponent,
109
+ # Ariadne::Alpha::UnderlinePanels,
110
+ # Ariadne::Alpha::TabPanels,
111
+ # Ariadne::TooltipComponent,
112
+ # Ariadne::ButtonComponent,
113
+ # Ariadne::LinkComponent,
114
+ ]
115
+
116
+ all_components = Ariadne::Component.descendants - [Ariadne::BaseComponent, Ariadne::Content] # TODO: why is `Ariadne::Content` not picked up?
117
+ components_needing_docs = all_components - components
118
+
119
+ args_for_components = []
120
+ classes_found_in_examples = []
121
+
122
+ errors = []
123
+
124
+ # Deletes docs before regenerating them, guaranteeing that we don't keep stale docs.
125
+ components_content_glob = File.join("docs", "content", "components", "**", "*.md")
126
+ FileUtils.rm_rf(components_content_glob)
127
+
128
+ components.sort_by(&:name).each do |component|
129
+ documentation = registry.get(component.name)
130
+
131
+ data = docs_metadata(component)
132
+
133
+ path = Pathname.new(data[:path])
134
+ path.dirname.mkpath unless path.dirname.exist?
135
+ File.open(path, "w") do |f|
136
+ f.puts("---")
137
+ f.puts("title: #{data[:title]}")
138
+ f.puts("componentId: #{data[:component_id]}")
139
+ f.puts("status: #{data[:status]}")
140
+ f.puts("source: #{data[:source]}")
141
+ f.puts("lookbook: #{data[:lookbook]}")
142
+ f.puts("---")
143
+ f.puts
144
+ f.puts("import Example from '#{data[:example_path]}'")
145
+
146
+ initialize_method = documentation.meths.find(&:constructor?)
147
+
148
+ if js_components.include?(component)
149
+ f.puts("import RequiresJSFlash from '#{data[:require_js_path]}'")
150
+ f.puts
151
+ f.puts("<RequiresJSFlash />")
152
+ end
153
+
154
+ f.puts
155
+ f.puts("<!-- Warning: AUTO-GENERATED file, do not edit. Add code comments to your Ruby instead <3 -->")
156
+ f.puts
157
+ f.puts(view_context.render(inline: documentation.base_docstring))
158
+
159
+ if documentation.tags(:deprecated).any?
160
+ f.puts
161
+ f.puts("## Deprecation")
162
+ documentation.tags(:deprecated).each do |tag|
163
+ f.puts
164
+ f.puts view_context.render(inline: tag.text)
165
+ end
166
+ end
167
+
168
+ if documentation.tags(:accessibility).any?
169
+ f.puts
170
+ f.puts("## Accessibility")
171
+ documentation.tags(:accessibility).each do |tag|
172
+ f.puts
173
+ f.puts view_context.render(inline: tag.text)
174
+ end
175
+ end
176
+
177
+ params = initialize_method.tags(:param)
178
+
179
+ errors << { component.name => { arguments: "No argument documentation found" } } unless params.any?
180
+
181
+ f.puts
182
+ f.puts("## Arguments")
183
+ f.puts
184
+ f.puts("| Name | Type | Default | Description |")
185
+ f.puts("| :- | :- | :- | :- |")
186
+
187
+ documented_params = params.map(&:name)
188
+ component_params = component.instance_method(:initialize).parameters.map { |p| p.last.to_s }
189
+
190
+ if (documented_params & component_params).size != component_params.size
191
+ err = { arguments: {} }
192
+ (component_params - documented_params).each do |arg|
193
+ err[:arguments][arg] = "Not documented"
194
+ end
195
+
196
+ errors << { component.name => err }
197
+ end
198
+
199
+ args = []
200
+ params.each do |tag|
201
+ default_value = pretty_default_value(tag, component)
202
+
203
+ args << {
204
+ "name" => tag.name,
205
+ "type" => tag.types.join(", "),
206
+ "default" => default_value,
207
+ "description" => view_context.render(inline: tag.text.squish),
208
+ }
209
+
210
+ f.puts("| `#{tag.name}` | `#{tag.types.join(", ")}` | #{default_value} | #{view_context.render(inline: tag.text.squish)} |")
211
+ end
212
+
213
+ component_args = {
214
+ "component" => data[:title],
215
+ "source" => data[:source],
216
+ "parameters" => args,
217
+ }
218
+
219
+ args_for_components << component_args
220
+
221
+ # Slots V2 docs
222
+ slot_v2_methods = documentation.meths.select { |x| x[:renders_one] || x[:renders_many] }
223
+
224
+ if slot_v2_methods.any?
225
+ f.puts
226
+ f.puts("## Slots")
227
+
228
+ slot_v2_methods.each do |slot_documentation|
229
+ f.puts
230
+ f.puts("### `#{slot_documentation.name.to_s.capitalize}`")
231
+
232
+ if slot_documentation.base_docstring.to_s.present?
233
+ f.puts
234
+ f.puts(view_context.render(inline: slot_documentation.base_docstring))
235
+ end
236
+
237
+ param_tags = slot_documentation.tags(:param)
238
+ if param_tags.any?
239
+ f.puts
240
+ f.puts("| Name | Type | Default | Description |")
241
+ f.puts("| :- | :- | :- | :- |")
242
+ end
243
+
244
+ param_tags.each do |tag|
245
+ f.puts("| `#{tag.name}` | `#{tag.types.join(", ")}` | #{pretty_default_value(tag, component)} | #{view_context.render(inline: tag.text)} |")
246
+ end
247
+ end
248
+ end
249
+
250
+ errors << { component.name => { example: "No examples found" } } unless initialize_method.tags(:example).any?
251
+
252
+ f.puts
253
+ f.puts("## Examples")
254
+
255
+ initialize_method.tags(:example).each do |tag|
256
+ name, description, code = parse_example_tag(tag)
257
+ f.puts
258
+ f.puts("### #{name}")
259
+ if description
260
+ f.puts
261
+ f.puts(view_context.render(inline: description.squish))
262
+ end
263
+ f.puts
264
+ html = view_context.render(inline: code)
265
+ html.scan(/class="([^"]*)"/) do |classnames|
266
+ classes_found_in_examples.concat(classnames[0].split.reject { |c| c.starts_with?("heroicon", "js", "my-") }.map { ".#{_1}" })
267
+ end
268
+ f.puts("<Example src=\"#{html.tr('"', "\'").delete("\n")}\" />")
269
+ f.puts
270
+ f.puts("```erb")
271
+ f.puts(code.to_s)
272
+ f.puts("```")
273
+ end
274
+ end
275
+ end
276
+
277
+ unless errors.empty?
278
+ puts "==============================================="
279
+ puts "===================== ERRORS =================="
280
+ puts "===============================================\n\n"
281
+ puts JSON.pretty_generate(errors)
282
+ puts "\n\n==============================================="
283
+ puts "==============================================="
284
+ puts "==============================================="
285
+
286
+ raise
287
+ end
288
+
289
+ File.open("static/classes.yml", "w") do |f|
290
+ f.puts YAML.dump(classes_found_in_examples.sort.uniq)
291
+ end
292
+
293
+ File.open("static/arguments.yml", "w") do |f|
294
+ f.puts YAML.dump(args_for_components)
295
+ end
296
+
297
+ # Build system arguments docs from BaseComponent
298
+ documentation = registry.get(Ariadne::BaseComponent.name)
299
+ File.open("docs/content/system-arguments.md", "w") do |f|
300
+ f.puts("---")
301
+ f.puts("title: System arguments")
302
+ f.puts("---")
303
+ f.puts
304
+ f.puts("<!-- Warning: AUTO-GENERATED file, do not edit. Add code comments to your Ruby instead <3 -->")
305
+ f.puts
306
+ f.puts(documentation.base_docstring)
307
+ f.puts
308
+
309
+ initialize_method = documentation.meths.find(&:constructor?)
310
+
311
+ f.puts(view_context.render(inline: initialize_method.base_docstring))
312
+ end
313
+
314
+ # Copy over ADR docs and insert them into the nav
315
+ # puts "Copying ADRs..."
316
+ # Rake::Task["docs:build_adrs"].invoke
317
+
318
+ puts "Markdown compiled."
319
+
320
+ if components_needing_docs.any?
321
+ puts
322
+ puts "The following components needs docs. Care to contribute them? #{components_needing_docs.map(&:name).join(", ")}"
323
+ end
324
+ end
325
+
326
+ # task :build_adrs do
327
+ # adr_content_dir = File.join("docs", "content", "adr")
328
+
329
+ # FileUtils.rm_rf(File.join(adr_content_dir))
330
+ # FileUtils.mkdir(adr_content_dir)
331
+
332
+ # nav_entries = Dir[File.join("adr", "*.md")].sort.map do |orig_path|
333
+ # orig_file_name = File.basename(orig_path)
334
+ # url_name = orig_file_name.chomp(".md")
335
+
336
+ # file_contents = File.read(orig_path)
337
+ # file_contents = <<~CONTENTS.sub(/\n+\z/, "\n")
338
+ # <!-- Warning: AUTO-GENERATED file, do not edit. Make changes to the files in the adr/ directory instead. -->
339
+ # #{file_contents}
340
+ # CONTENTS
341
+
342
+ # title_match = /^# (.+)/.match(file_contents)
343
+ # title = title_match[1]
344
+
345
+ # # Don't include initial ADR for recording ADRs
346
+ # next nil if title == "Record architecture decisions"
347
+
348
+ # File.write(File.join(adr_content_dir, orig_file_name), file_contents)
349
+ # puts "Copied #{orig_path}"
350
+
351
+ # { "title" => title, "url" => "/adr/#{url_name}" }
352
+ # end
353
+
354
+ # nav_yaml_file = File.join("docs", "src", "@primer", "gatsby-theme-doctocat", "nav.yml")
355
+ # nav_yaml = YAML.load_file(nav_yaml_file)
356
+ # adr_entry = {
357
+ # "title" => "Architecture decisions",
358
+ # "children" => nav_entries.compact,
359
+ # }
360
+
361
+ # existing_index = nav_yaml.index { |entry| entry["title"] == "Architecture decisions" }
362
+ # if existing_index
363
+ # nav_yaml[existing_index] = adr_entry
364
+ # else
365
+ # nav_yaml << adr_entry
366
+ # end
367
+
368
+ # File.write(nav_yaml_file, YAML.dump(nav_yaml))
369
+ # end
370
+
371
+ desc "Generate previews from documentation examples"
372
+ task :preview do
373
+ registry = generate_yard_registry
374
+
375
+ FileUtils.rm_rf("lookbook/test/components/previews/ariadne/docs/")
376
+
377
+ components = Ariadne::Component.descendants
378
+
379
+ components.each do |component|
380
+ documentation = registry.get(component.name)
381
+ short_name = component.name.gsub(/Ariadne|::/, "")
382
+ initialize_method = documentation.meths.find(&:constructor?)
383
+
384
+ next unless initialize_method&.tags(:example)&.any?
385
+
386
+ yard_example_tags = initialize_method.tags(:example)
387
+
388
+ path = Pathname.new("lookbook/test/components/previews/ariadne/docs/#{short_name.underscore}_preview.rb")
389
+ FileUtils.mkdir_p("lookbook/test/components/previews/ariadne/docs") unless path.dirname.exist?
390
+
391
+ File.open(path, "w") do |f|
392
+ f.puts("module Ariadne")
393
+ f.puts(" module Docs")
394
+ f.puts(" class #{short_name}Preview < ViewComponent::Preview")
395
+
396
+ yard_example_tags.each_with_index do |tag, index|
397
+ name, _, code = parse_example_tag(tag)
398
+ method_name = name.split("|").first.downcase.parameterize.underscore
399
+ f.puts(" def #{method_name}; end")
400
+ f.puts unless index == yard_example_tags.size - 1
401
+ path = Pathname.new("lookbook/test/components/previews/ariadne/docs/#{short_name.underscore}_preview/#{method_name}.html.erb")
402
+ FileUtils.mkdir_p("lookbook/test/components/previews/ariadne/docs/#{short_name.underscore}_preview") unless path.dirname.exist?
403
+ File.open(path, "w") do |view_file|
404
+ view_file.puts(code.to_s)
405
+ end
406
+ end
407
+
408
+ f.puts(" end")
409
+ f.puts(" end")
410
+ f.puts("end")
411
+ end
412
+ end
413
+ end
414
+ end
415
+
416
+ def generate_yard_registry
417
+ require "action_dispatch"
418
+ require_relative "../../app/lib/ariadne/view_helper"
419
+ require File.expand_path("./../../lookbook/config/environment.rb", __dir__)
420
+
421
+ YARD::Registry.yardoc_file = ".yardoc"
422
+
423
+ require "./app/components/ariadne/component.rb"
424
+ require "ariadne/view_components"
425
+ require "yard/docs_helper"
426
+ require "view_component/base"
427
+ require "view_component/test_helpers"
428
+ include(ViewComponent::TestHelpers)
429
+ include(Ariadne::ViewHelper)
430
+ include(YARD::DocsHelper)
431
+
432
+ Dir["./app/components/ariadne/**/*.rb"].sort.each do |file|
433
+ puts file
434
+ require file
435
+ end
436
+
437
+ YARD::Rake::YardocTask.new
438
+
439
+ # Custom tags for yard
440
+ YARD::Tags::Library.define_tag("Accessibility", :accessibility)
441
+ YARD::Tags::Library.define_tag("Deprecation", :deprecation)
442
+ YARD::Tags::Library.define_tag("Parameter", :param, :with_types_name_and_default)
443
+
444
+ puts "Building YARD documentation."
445
+ Rake::Task["yard"].execute
446
+
447
+ registry = YARD::RegistryStore.new
448
+ registry.load!(".yardoc")
449
+ registry
450
+ end
451
+
452
+ def parse_example_tag(tag)
453
+ name = tag.name
454
+ description = nil
455
+ code = nil
456
+
457
+ if tag.text.include?("@description")
458
+ splitted = tag.text.split(/@description|@code/)
459
+ description = splitted.second.gsub(/^[ \t]{2}/, "").strip
460
+ code = splitted.last.gsub(/^[ \t]{2}/, "").strip
461
+ else
462
+ code = tag.text
463
+ end
464
+
465
+ [name, description, code]
466
+ end
467
+
468
+ def pretty_default_value(tag, component)
469
+ params = tag.object.parameters.find { |param| [tag.name.to_s, "#{tag.name}:"].include?(param[0]) }
470
+ default = tag.defaults&.first || params&.second
471
+
472
+ return "N/A" unless default
473
+
474
+ constant_name = "#{component.name}::#{default}"
475
+ constant_value = default.safe_constantize || constant_name.safe_constantize
476
+
477
+ return pretty_value(default) if constant_value.nil?
478
+
479
+ pretty_value(constant_value)
480
+ end
481
+
482
+ def docs_metadata(component)
483
+ (status_module, short_name) = status_module_and_short_name(component)
484
+ status_path = status_module.nil? ? "" : "/"
485
+ status = component.status.to_s
486
+
487
+ {
488
+ title: short_name,
489
+ component_id: short_name.underscore,
490
+ status: status.capitalize,
491
+ source: source_url(component),
492
+ lookbook: lookbook_url(component),
493
+ path: "docs/content/components/#{status_path}#{short_name.downcase}.md",
494
+ example_path: example_path(component),
495
+ require_js_path: require_js_path(component),
496
+ }
497
+ end
498
+
499
+ def source_url(component)
500
+ path = component.name.split("::").map(&:underscore).join("/")
501
+
502
+ "https://github.com/yettoapp/ariadne/ruby/view_components/tree/main/app/components/#{path}.rb"
503
+ end
504
+
505
+ def lookbook_url(component)
506
+ path = component.name.split("::").map { |n| n.underscore.dasherize }.join("-")
507
+
508
+ "https://ariadne.style/view-components/lookbook/?path=/component/#{path}"
509
+ end
510
+
511
+ def example_path(component)
512
+ example_path = "../../src/@primer/gatsby-theme-doctocat/components/example"
513
+ example_path = "../#{example_path}" if status_module?(component)
514
+ example_path
515
+ end
516
+
517
+ def require_js_path(component)
518
+ require_js_path = "../../src/@primer/gatsby-theme-doctocat/components/requires-js-flash"
519
+ require_js_path = "../#{require_js_path}" if status_module?(component)
520
+ require_js_path
521
+ end
522
+
523
+ def status_module?(component)
524
+ (["Alpha", "Beta"] & component.name.split("::")).any?
525
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ast_traverser"
4
+
5
+ # :nodoc:
6
+ class AstProcessor
7
+ class << self
8
+ def increment(stats, component, arg_name, value)
9
+ stats[component][:arguments][arg_name][value] = 0 unless stats[component][:arguments][arg_name][value]
10
+ stats[component][:arguments][arg_name][value] += 1
11
+ end
12
+
13
+ def process_ast(ast, stats)
14
+ traverser = AstTraverser.new
15
+ traverser.walk(ast)
16
+
17
+ return if traverser.stats.empty?
18
+
19
+ traverser.stats.each do |component, component_info|
20
+ stats[component] ||= {
21
+ paths: [],
22
+ }
23
+
24
+ stats[component][:paths] << component_info[:path]
25
+ stats[component][:paths].uniq!
26
+ stats[component][:arguments] ||= {}
27
+
28
+ component_info[:arguments]&.each do |arg, value|
29
+ arg_name = arg.to_s
30
+ stats[component][:arguments][arg_name] ||= {}
31
+
32
+ # we want to count each class separately
33
+ if arg_name == "classes"
34
+ value.split.each do |val|
35
+ increment(stats, component, arg_name, val)
36
+ end
37
+ else
38
+ increment(stats, component, arg_name, value)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ariadne/view_components/statuses"
4
+ require_relative "../../../app/lib/ariadne/view_helper"
5
+
6
+ # :nodoc:
7
+ class AstTraverser
8
+ include RuboCop::AST::Traversal
9
+
10
+ attr_reader :stats
11
+
12
+ def initialize
13
+ @stats = {}
14
+ end
15
+
16
+ def on_send(node)
17
+ return super(node) unless component_node?(node)
18
+
19
+ name = component_name(node)
20
+ args = extract_arguments(node, name)
21
+
22
+ @stats[name] = { path: node.loc.expression.source_buffer.name }
23
+ @stats[name][:arguments] = args unless args.empty?
24
+
25
+ super(node) # recursively iterate over children
26
+ end
27
+
28
+ def view_helpers
29
+ @view_helpers ||= ::Ariadne::ViewHelper::HELPERS.keys.map { |key| "ariadne_#{key}".to_sym }
30
+ end
31
+
32
+ def component_node?(node)
33
+ view_helpers.include?(node.method_name) || (node.method_name == :new && !node.receiver.nil? && ::Ariadne::ViewComponents::STATUSES.key?(node.receiver.const_name))
34
+ end
35
+
36
+ def component_name(node)
37
+ return node.receiver.const_name if node.method_name == :new
38
+
39
+ helper_key = node.method_name.to_s.gsub("ariadne_", "").to_sym
40
+ Ariadne::ViewHelper::HELPERS[helper_key]
41
+ end
42
+
43
+ def extract_arguments(node, name)
44
+ args = node.arguments
45
+ res = {}
46
+
47
+ return res if args.empty?
48
+
49
+ kwargs = args.last
50
+ if kwargs.respond_to?(:pairs)
51
+ res = kwargs.pairs.each_with_object({}) do |pair, h|
52
+ h.merge!(extract_values(pair))
53
+ end
54
+ end
55
+
56
+ # Heroicon is the only component that accepts positional arguments.
57
+ res[:icon] = args.first.source if name == "Ariadne::HeroiconComponent" && args.size > 1
58
+
59
+ res
60
+ end
61
+
62
+ def extract_values(pair)
63
+ return { pair.key.value => pair.value.source } unless pair.value.type == :hash
64
+
65
+ flatten_pairs(pair, prefix: "#{pair.key.value}-")
66
+ end
67
+
68
+ def flatten_pairs(pair, prefix: "")
69
+ pair.value.pairs.each_with_object({}) do |value_pair, h|
70
+ if value_pair.value.type == :hash
71
+ h.merge!(flatten_pairs(value_pair, prefix: "#{prefix}#{value_pair.key.value}-"))
72
+ else
73
+ h["#{prefix}#{value_pair.key.value}"] = value_pair.value.source
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :static do
4
+ desc "Generate static JSON mappings of components for easier loading"
5
+ task :dump do
6
+ require File.expand_path("./../../lookbook/config/environment.rb", __dir__)
7
+ require "ariadne/view_components"
8
+ # Loads all components for `.descendants` to work properly
9
+ Dir["./app/components/ariadne/**/*.rb"].sort.each { |file| require file }
10
+
11
+ Ariadne::ViewComponents.dump(:statuses)
12
+ Ariadne::ViewComponents.dump(:constants)
13
+ Ariadne::ViewComponents.dump(:audited_at)
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # "Hacks, but they work" ™
4
+ if !defined?(Rails) || ENV.fetch("DOCS_BUILD", false).blank?
5
+
6
+ # Spoof the Rails environment
7
+ module Rails
8
+ def self.root
9
+ Pathname.new(File.join(File.dirname(__FILE__), "..", ".."))
10
+ end
11
+ end
12
+
13
+ rule "" do |t|
14
+ task_name = t.name
15
+ if task_name == "assets:precompile"
16
+ puts "Stubbing non-existent #{task_name}"
17
+ else
18
+ raise "No task named #{task_name}"
19
+ end
20
+ end
21
+ end
22
+
23
+ spec = Gem::Specification.find_by_name("tailwindcss-rails")
24
+ load "#{spec.gem_dir}/lib/tasks/build.rake"
25
+
26
+ Rake::Task["tailwindcss:build"].enhance do
27
+ if File.exist?("app/assets/builds/tailwind.css")
28
+ puts "Renaming tailwind.css to ariadne_view_components.css..."
29
+ File.rename("app/assets/builds/tailwind.css", "app/assets/builds/ariadne_view_components.css")
30
+ end
31
+ end